diff --git a/README.md b/README.md
index 45111f2d..49a9f9b2 100644
--- a/README.md
+++ b/README.md
@@ -87,6 +87,7 @@ If you run into any issue while using Wispar, have a question or want to share y
Unpaywall
Crossref
OpenAlex
+ Journal topics database by hitfyd
## Screenshots
diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb
index 3803cb6d..3cbb4ef6 100644
--- a/lib/l10n/app_en.arb
+++ b/lib/l10n/app_en.arb
@@ -134,6 +134,10 @@
"journals": "Journals",
"@journals": {
"description": "The journals menu button and the app bar title when in the journals screen."
+ },
+ "countJournals": "{count, plural, =0{No journals} =1{1 journal} other{{count} journals}}",
+ "@countJournals": {
+ "placeholders": {"count": {}}
},
"queries": "Queries",
"@queries": {
@@ -189,6 +193,8 @@
"@searchByDOI": {},
"searchByTitle": "Search by title",
"@searchByTitle": {},
+ "searchByTopic": "Search by topic",
+ "@searchByTopic":{},
"searchByISSN": "Search by ISSN",
"@searchByISSN": {},
"queryHasNoNameError": "You must enter a query name in order to save it.",
@@ -215,6 +221,8 @@
"@category": {},
"publisher": "Publisher",
"@publisher": {},
+ "publisherName": "Publisher: {publisherName}",
+ "@publisherName":{},
"publishedin": "Published in",
"@publishedin": {},
"subjects": "Subjects",
diff --git a/lib/models/journal_topics_models.dart b/lib/models/journal_topics_models.dart
new file mode 100644
index 00000000..552afafc
--- /dev/null
+++ b/lib/models/journal_topics_models.dart
@@ -0,0 +1,13 @@
+class JournalTopicsCsv {
+ final String journal;
+ final String issn;
+ final String eissn;
+ final List categories;
+
+ JournalTopicsCsv({
+ required this.journal,
+ required this.issn,
+ required this.eissn,
+ required this.categories,
+ });
+}
diff --git a/lib/screens/journals_search_results_screen.dart b/lib/screens/journals_search_results_screen.dart
index 93856e93..5245594a 100644
--- a/lib/screens/journals_search_results_screen.dart
+++ b/lib/screens/journals_search_results_screen.dart
@@ -1,25 +1,25 @@
import 'package:flutter/material.dart';
-import '../generated_l10n/app_localizations.dart';
-import '../services/crossref_api.dart';
-import '../models/crossref_journals_models.dart' as Journals;
-import '../widgets/journal_search_results_card.dart';
-import '../services/logs_helper.dart';
+import 'package:wispar/generated_l10n/app_localizations.dart';
+import 'package:wispar/services/crossref_api.dart';
+import 'package:wispar/models/crossref_journals_models.dart' as Journals;
+import 'package:wispar/widgets/journal_search_results_card.dart';
+import 'package:wispar/services/logs_helper.dart';
class SearchResultsScreen extends StatefulWidget {
final ListAndMore searchResults;
final String searchQuery;
const SearchResultsScreen({
- Key? key,
+ super.key,
required this.searchResults,
required this.searchQuery,
- }) : super(key: key);
+ });
@override
- _SearchResultsScreenState createState() => _SearchResultsScreenState();
+ SearchResultsScreenState createState() => SearchResultsScreenState();
}
-class _SearchResultsScreenState extends State {
+class SearchResultsScreenState extends State {
final logger = LogsService().logger;
List items = [];
bool isLoading = false;
@@ -101,10 +101,12 @@ class _SearchResultsScreenState extends State {
}
});
} catch (e, stackTrace) {
- logger.severe('Failed to load more journals.', e, stackTrace);
- ScaffoldMessenger.of(context).showSnackBar(SnackBar(
- content:
- Text(AppLocalizations.of(context)!.failLoadMorePublication)));
+ if (mounted) {
+ logger.severe('Failed to load more journals.', e, stackTrace);
+ ScaffoldMessenger.of(context).showSnackBar(SnackBar(
+ content:
+ Text(AppLocalizations.of(context)!.failLoadMorePublication)));
+ }
} finally {
setState(() => isLoading = false);
}
diff --git a/lib/services/journal_topics_helper.dart b/lib/services/journal_topics_helper.dart
new file mode 100644
index 00000000..82c81903
--- /dev/null
+++ b/lib/services/journal_topics_helper.dart
@@ -0,0 +1,46 @@
+import 'package:http/http.dart' as http;
+import 'package:wispar/models/journal_topics_models.dart';
+import 'package:csv/csv.dart';
+
+Future> fetchCsvCategories() async {
+ final url =
+ "https://raw.githubusercontent.com/hitfyd/ShowJCR/refs/heads/master/%E4%B8%AD%E7%A7%91%E9%99%A2%E5%88%86%E5%8C%BA%E8%A1%A8%E5%8F%8AJCR%E5%8E%9F%E5%A7%8B%E6%95%B0%E6%8D%AE%E6%96%87%E4%BB%B6/JCR2024-UTF8.csv";
+
+ final response = await http.get(Uri.parse(url));
+ if (response.statusCode != 200) {
+ throw Exception("Failed to load CSV");
+ }
+
+ final csvRows = const CsvToListConverter(eol: '\n', shouldParseNumbers: false)
+ .convert(response.body);
+
+ final List entries = [];
+
+ for (int i = 1; i < csvRows.length; i++) {
+ final row = csvRows[i];
+ final journal = row[0].toString();
+ final issn =
+ (row[1].toString().toUpperCase() == 'N/A') ? '' : row[1].toString();
+ final eissn =
+ (row[2].toString().toUpperCase() == 'N/A') ? '' : row[2].toString();
+ final categories = row[3].toString().split(';').map((c) {
+ String clean = c.replaceAll(RegExp(r'\([A-Z]+\)$'), '').trim();
+ return clean
+ .toLowerCase()
+ .split(' ')
+ .map((word) => word.isEmpty
+ ? ''
+ : '${word[0].toUpperCase()}${word.substring(1)}')
+ .join(' ');
+ }).toList();
+
+ entries.add(JournalTopicsCsv(
+ journal: journal,
+ issn: issn,
+ eissn: eissn,
+ categories: categories,
+ ));
+ }
+
+ return entries;
+}
diff --git a/lib/widgets/article_crossref_search_form.dart b/lib/widgets/article_crossref_search_form.dart
index af4d93ee..1919b26a 100644
--- a/lib/widgets/article_crossref_search_form.dart
+++ b/lib/widgets/article_crossref_search_form.dart
@@ -1,17 +1,19 @@
import 'package:flutter/material.dart';
-import '../generated_l10n/app_localizations.dart';
-import 'article_doi_search_form.dart';
-import 'article_query_search_form.dart';
-import '../services/crossref_api.dart';
-import '../screens/article_screen.dart';
-import '../services/logs_helper.dart';
+import 'package:wispar/generated_l10n/app_localizations.dart';
+import 'package:wispar/widgets/article_doi_search_form.dart';
+import 'package:wispar/widgets/article_query_search_form.dart';
+import 'package:wispar/services/crossref_api.dart';
+import 'package:wispar/screens/article_screen.dart';
+import 'package:wispar/services/logs_helper.dart';
class CrossRefSearchForm extends StatefulWidget {
+ const CrossRefSearchForm({super.key});
+
@override
- _CrossRefSearchFormState createState() => _CrossRefSearchFormState();
+ CrossRefSearchFormState createState() => CrossRefSearchFormState();
}
-class _CrossRefSearchFormState extends State {
+class CrossRefSearchFormState extends State {
final logger = LogsService().logger;
int selectedSearchIndex = 0; // 0 for Query, 1 for DOI
final TextEditingController doiController = TextEditingController();
@@ -63,41 +65,47 @@ class _CrossRefSearchFormState extends State {
final article = await CrossRefApi.getWorkByDOI(extractedDoi);
// Dismiss the loading dialog
- Navigator.pop(context);
- Navigator.push(
- context,
- MaterialPageRoute(
- builder: (context) => ArticleScreen(
- doi: article.doi,
- title: article.title,
- issn: article.issn,
- abstract: article.abstract,
- journalTitle: article.journalTitle,
- publishedDate: article.publishedDate,
- authors: article.authors,
- url: article.url,
- license: article.license,
- licenseName: article.licenseName,
- publisher: article.publisher,
+ if (mounted) {
+ Navigator.pop(context);
+ Navigator.push(
+ context,
+ MaterialPageRoute(
+ builder: (context) => ArticleScreen(
+ doi: article.doi,
+ title: article.title,
+ issn: article.issn,
+ abstract: article.abstract,
+ journalTitle: article.journalTitle,
+ publishedDate: article.publishedDate,
+ authors: article.authors,
+ url: article.url,
+ license: article.license,
+ licenseName: article.licenseName,
+ publisher: article.publisher,
+ ),
),
- ),
- );
+ );
+ }
} catch (e, stackTrace) {
- logger.severe(
- 'Error searching by DOI for DOI ${doi}.', e, stackTrace);
- Navigator.pop(context); // Close loading dialog
- ScaffoldMessenger.of(context).showSnackBar(
- SnackBar(content: Text(AppLocalizations.of(context)!.errorOccured)),
- );
+ logger.severe('Error searching by DOI for DOI $doi.', e, stackTrace);
+ if (mounted) {
+ Navigator.pop(context); // Close loading dialog
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(
+ content: Text(AppLocalizations.of(context)!.errorOccured)),
+ );
+ }
}
}
} catch (e, stackTrace) {
- logger.severe('Error searching articles using ${selectedSearchIndex}.', e,
+ logger.severe('Error searching articles using $selectedSearchIndex.', e,
stackTrace);
- Navigator.pop(context); // Close loading dialog
- ScaffoldMessenger.of(context).showSnackBar(
- SnackBar(content: Text(AppLocalizations.of(context)!.errorOccured)),
- );
+ if (mounted) {
+ Navigator.pop(context); // Close loading dialog
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(content: Text(AppLocalizations.of(context)!.errorOccured)),
+ );
+ }
}
}
@@ -118,6 +126,16 @@ class _CrossRefSearchFormState extends State {
child: LayoutBuilder(
builder: (context, constraints) {
return ToggleButtons(
+ isSelected: [
+ selectedSearchIndex == 0,
+ selectedSearchIndex == 1,
+ ],
+ onPressed: (int index) {
+ setState(() {
+ selectedSearchIndex = index;
+ });
+ },
+ borderRadius: BorderRadius.circular(15.0),
children: [
Container(
width: constraints.maxWidth / 2 - 1.5,
@@ -131,16 +149,6 @@ class _CrossRefSearchFormState extends State {
child: Text(AppLocalizations.of(context)!.searchByDOI),
),
],
- isSelected: [
- selectedSearchIndex == 0,
- selectedSearchIndex == 1,
- ],
- onPressed: (int index) {
- setState(() {
- selectedSearchIndex = index;
- });
- },
- borderRadius: BorderRadius.circular(15.0),
);
},
),
@@ -155,7 +163,6 @@ class _CrossRefSearchFormState extends State {
floatingActionButton: FloatingActionButton(
onPressed: _handleSearch,
child: Icon(Icons.search),
- shape: CircleBorder(),
),
);
}
diff --git a/lib/widgets/article_openAlex_search_form.dart b/lib/widgets/article_openAlex_search_form.dart
index dc5065b9..e7aaf41f 100644
--- a/lib/widgets/article_openAlex_search_form.dart
+++ b/lib/widgets/article_openAlex_search_form.dart
@@ -438,7 +438,6 @@ class _OpenAlexSearchFormState extends State {
floatingActionButton: FloatingActionButton(
onPressed: _executeSearch,
child: Icon(Icons.search),
- shape: CircleBorder(),
),
);
}
diff --git a/lib/widgets/article_search_form.dart b/lib/widgets/article_search_form.dart
index 816da702..41669a16 100644
--- a/lib/widgets/article_search_form.dart
+++ b/lib/widgets/article_search_form.dart
@@ -1,13 +1,15 @@
import 'package:flutter/material.dart';
-import './article_crossref_search_form.dart';
-import './article_openAlex_search_form.dart';
+import 'package:wispar/widgets/article_crossref_search_form.dart';
+import 'package:wispar/widgets/article_openAlex_search_form.dart';
class ArticleSearchScreen extends StatefulWidget {
+ const ArticleSearchScreen({super.key});
+
@override
- _ArticleSearchScreenState createState() => _ArticleSearchScreenState();
+ ArticleSearchScreenState createState() => ArticleSearchScreenState();
}
-class _ArticleSearchScreenState extends State {
+class ArticleSearchScreenState extends State {
int selectedProviderIndex = 0; // 0 = OpenAlex, 1 = Crossref
@override
diff --git a/lib/widgets/journal_header.dart b/lib/widgets/journal_header.dart
index aeb0df73..55311c45 100644
--- a/lib/widgets/journal_header.dart
+++ b/lib/widgets/journal_header.dart
@@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
-import '../generated_l10n/app_localizations.dart';
-import '../models/crossref_journals_models.dart' as Journals;
-import './journal_follow_button.dart';
+import 'package:wispar/generated_l10n/app_localizations.dart';
+import 'package:wispar/models/crossref_journals_models.dart' as Journals;
+import 'package:wispar/widgets/journal_follow_button.dart';
class JournalInfoHeader extends SliverPersistentHeaderDelegate {
final String title;
@@ -57,8 +57,9 @@ class JournalInfoHeader extends SliverPersistentHeaderDelegate {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(height: 8.0),
- Text(
- '${AppLocalizations.of(context)!.publisher}: ${publisher}'),
+ if (publisher.isNotEmpty)
+ Text(AppLocalizations.of(context)!
+ .publisherName(publisher)),
Text('ISSN: ${issn}'),
SizedBox(height: 8.0),
],
diff --git a/lib/widgets/journal_search_form.dart b/lib/widgets/journal_search_form.dart
index 089533a8..bd07681e 100644
--- a/lib/widgets/journal_search_form.dart
+++ b/lib/widgets/journal_search_form.dart
@@ -1,48 +1,32 @@
import 'package:flutter/material.dart';
-import '../generated_l10n/app_localizations.dart';
-import '../screens/journals_search_results_screen.dart';
-import '../services/crossref_api.dart';
-import '../models/crossref_journals_models.dart' as Journals;
-import '../services/logs_helper.dart';
+import 'package:wispar/generated_l10n/app_localizations.dart';
+import 'package:wispar/screens/journals_search_results_screen.dart';
+import 'package:wispar/services/crossref_api.dart';
+import 'package:wispar/models/crossref_journals_models.dart' as Journals;
+import 'package:wispar/services/logs_helper.dart';
+import 'package:wispar/widgets/journal_topics_widget.dart';
+import 'package:wispar/models/journal_topics_models.dart';
+import 'package:wispar/services/journal_topics_helper.dart';
class JournalSearchForm extends StatefulWidget {
+ const JournalSearchForm({super.key});
+
@override
- _JournalSearchFormState createState() => _JournalSearchFormState();
+ JournalSearchFormState createState() => JournalSearchFormState();
}
-class _JournalSearchFormState extends State {
+class JournalSearchFormState extends State {
final logger = LogsService().logger;
bool saveQuery = false;
- int selectedSearchIndex = 0; // 0 for 'name', 1 for 'issn'
+ int selectedSearchIndex = 0; // 0 for 'name', 1 for topics, 2 for 'issn'
late Journals.Item selectedJournal;
- TextEditingController _searchController = TextEditingController();
+ final TextEditingController _searchController = TextEditingController();
+ Map>? _topicCategories;
+ bool showCategories = false;
@override
void initState() {
super.initState();
-
- _searchController.addListener(() {
- if (selectedSearchIndex == 1) {
- /*String text = _searchController.text;
-
- // Limit input to 9 characters
- if (text.length > 9) {
- _searchController.value = TextEditingValue(
- text: text.substring(0, 9),
- selection: TextSelection.collapsed(offset: 9),
- );
- return;
- }
-
- // Automatically add a dash after the first 4 digits
- if (text.length == 4 && !text.contains('-')) {
- _searchController.value = TextEditingValue(
- text: '${text}-',
- selection: TextSelection.collapsed(offset: text.length + 1),
- );
- }*/
- }
- });
}
@override
@@ -55,11 +39,10 @@ class _JournalSearchFormState extends State {
Widget build(BuildContext context) {
return Scaffold(
body: Padding(
- padding: const EdgeInsets.all(16.0),
+ padding: const EdgeInsets.all(6.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
- SizedBox(height: 16),
Center(
child: LayoutBuilder(
builder: (context, constraints) {
@@ -67,61 +50,81 @@ class _JournalSearchFormState extends State {
isSelected: [
selectedSearchIndex == 0,
selectedSearchIndex == 1,
+ selectedSearchIndex == 2,
],
- onPressed: (int index) {
+ onPressed: (index) {
setState(() {
selectedSearchIndex = index;
_searchController.clear();
+ if (index == 1) _loadTopics();
});
},
+ borderRadius: BorderRadius.circular(15),
children: [
Container(
- width: constraints.maxWidth / 2 - 1.5,
+ width: constraints.maxWidth / 3 - 1.5,
+ alignment: Alignment.center,
+ child: Text(
+ AppLocalizations.of(context)!.searchByTitle,
+ textAlign: TextAlign.center,
+ ),
+ ),
+ Container(
+ width: constraints.maxWidth / 3 - 1.5,
alignment: Alignment.center,
- child:
- Text(AppLocalizations.of(context)!.searchByTitle),
+ child: Text(
+ AppLocalizations.of(context)!.searchByTopic,
+ textAlign: TextAlign.center,
+ ),
),
Container(
- width: constraints.maxWidth / 2 - 1.5,
+ width: constraints.maxWidth / 3 - 1.5,
alignment: Alignment.center,
- child: Text(AppLocalizations.of(context)!.searchByISSN),
+ child: Text(
+ AppLocalizations.of(context)!.searchByISSN,
+ textAlign: TextAlign.center,
+ ),
),
],
- borderRadius: BorderRadius.circular(15.0),
);
},
),
),
- SizedBox(height: 32),
- TextField(
- controller: _searchController,
- decoration: InputDecoration(
- labelText: selectedSearchIndex == 0
- ? AppLocalizations.of(context)!.journaltitle
- : 'ISSN',
- border: OutlineInputBorder(),
- ),
- ),
SizedBox(height: 16),
+ if (selectedSearchIndex == 1)
+ _topicCategories == null
+ ? Expanded(child: Center(child: CircularProgressIndicator()))
+ : TopicsListWidget(categories: _topicCategories!),
+ if (selectedSearchIndex != 1)
+ TextField(
+ controller: _searchController,
+ decoration: InputDecoration(
+ labelText: selectedSearchIndex == 0
+ ? AppLocalizations.of(context)!.journaltitle
+ : "ISSN",
+ border: OutlineInputBorder(),
+ ),
+ ),
],
),
),
- floatingActionButton: FloatingActionButton(
- onPressed: () {
- String query = _searchController.text.trim();
- if (query.isNotEmpty) {
- _handleSearch(query);
- } else {
- ScaffoldMessenger.of(context).showSnackBar(
- SnackBar(
- content:
- Text(AppLocalizations.of(context)!.emptySearchQuery)),
- );
- }
- },
- child: Icon(Icons.search),
- shape: CircleBorder(),
- ),
+ floatingActionButton: selectedSearchIndex == 1
+ ? null
+ : FloatingActionButton(
+ onPressed: () {
+ String query = _searchController.text.trim();
+ if (query.isNotEmpty) {
+ _handleSearch(query);
+ } else {
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(
+ content: Text(
+ AppLocalizations.of(context)!.emptySearchQuery)),
+ );
+ }
+ },
+ child: Icon(Icons.search),
+ ),
);
}
@@ -142,30 +145,51 @@ class _JournalSearchFormState extends State {
ListAndMore searchResults;
if (selectedSearchIndex == 0) {
searchResults = await CrossRefApi.queryJournalsByName(query);
- } else if (selectedSearchIndex == 1) {
+ } else if (selectedSearchIndex == 2) {
searchResults = await CrossRefApi.queryJournalsByISSN(query);
} else {
throw Exception('Invalid search type selected');
}
- Navigator.pop(context);
+ if (mounted) {
+ Navigator.pop(context);
- Navigator.push(
- context,
- MaterialPageRoute(
- builder: (context) => SearchResultsScreen(
- searchResults: searchResults,
- searchQuery: query,
+ Navigator.push(
+ context,
+ MaterialPageRoute(
+ builder: (context) => SearchResultsScreen(
+ searchResults: searchResults,
+ searchQuery: query,
+ ),
),
- ),
- );
+ );
+ }
} catch (e, stackTrace) {
logger.severe("Unable to search for journals.", e, stackTrace);
- ScaffoldMessenger.of(context).showSnackBar(
- SnackBar(
- content: Text(AppLocalizations.of(context)!.journalSearchError)),
- );
- Navigator.pop(context);
+
+ if (mounted) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(
+ content: Text(AppLocalizations.of(context)!.journalSearchError)),
+ );
+ Navigator.pop(context);
+ }
+ }
+ }
+
+ void _loadTopics() async {
+ setState(() => _topicCategories = null);
+ try {
+ final entries = await fetchCsvCategories();
+ final Map> grouped = {};
+ for (final e in entries) {
+ for (final c in e.categories) {
+ grouped.putIfAbsent(c, () => []).add(e);
+ }
+ }
+ if (mounted) setState(() => _topicCategories = grouped);
+ } catch (e) {
+ logger.severe("Failed to load topics: $e");
}
}
}
diff --git a/lib/widgets/journal_search_results_card.dart b/lib/widgets/journal_search_results_card.dart
index a1901d6f..cd764d25 100644
--- a/lib/widgets/journal_search_results_card.dart
+++ b/lib/widgets/journal_search_results_card.dart
@@ -1,24 +1,23 @@
import 'package:flutter/material.dart';
-import '../generated_l10n/app_localizations.dart';
-import '../models/crossref_journals_models.dart' as Journals;
-import '../services/database_helper.dart';
-import '../screens/journals_details_screen.dart';
-import './journal_follow_button.dart';
+import 'package:wispar/generated_l10n/app_localizations.dart';
+import 'package:wispar/models/crossref_journals_models.dart' as Journals;
+import 'package:wispar/services/database_helper.dart';
+import 'package:wispar/screens/journals_details_screen.dart';
+import 'package:wispar/widgets/journal_follow_button.dart';
class JournalsSearchResultCard extends StatefulWidget {
final Journals.Item item;
final bool isFollowed;
const JournalsSearchResultCard(
- {Key? key, required this.item, required this.isFollowed})
- : super(key: key);
+ {super.key, required this.item, required this.isFollowed});
@override
- _JournalsSearchResultCardState createState() =>
- _JournalsSearchResultCardState();
+ JournalsSearchResultCardState createState() =>
+ JournalsSearchResultCardState();
}
-class _JournalsSearchResultCardState extends State {
+class JournalsSearchResultCardState extends State {
late bool _isFollowed = false; // Initialize with false by default
@override
@@ -56,8 +55,9 @@ class _JournalsSearchResultCardState extends State {
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
- Text(
- '${AppLocalizations.of(context)!.publisher}: ${widget.item.publisher}'),
+ if (widget.item.publisher.isNotEmpty)
+ Text(AppLocalizations.of(context)!
+ .publisherName(widget.item.publisher)),
if (widget.item.issn.isNotEmpty)
Text('ISSN: ${widget.item.issn.toSet().join(', ')}'),
],
diff --git a/lib/widgets/journal_topics_widget.dart b/lib/widgets/journal_topics_widget.dart
new file mode 100644
index 00000000..92e39591
--- /dev/null
+++ b/lib/widgets/journal_topics_widget.dart
@@ -0,0 +1,77 @@
+import 'package:flutter/material.dart';
+import 'package:wispar/models/journal_topics_models.dart';
+import 'package:wispar/screens/journals_search_results_screen.dart';
+import 'package:wispar/models/crossref_journals_models.dart' as Journals;
+import 'package:wispar/services/crossref_api.dart';
+import 'package:wispar/generated_l10n/app_localizations.dart';
+
+class TopicsListWidget extends StatelessWidget {
+ final Map> categories;
+
+ const TopicsListWidget({super.key, required this.categories});
+
+ @override
+ Widget build(BuildContext context) {
+ final keys = categories.keys.toList()..sort();
+
+ return Expanded(
+ child: ListView.builder(
+ itemCount: keys.length,
+ itemBuilder: (context, index) {
+ final category = keys[index];
+ final journals = categories[category]!;
+
+ // Converts the topics into a Journals.Item for the results screen
+ final List converted = journals.map((j) {
+ return Journals.Item(
+ title: j.journal,
+ issn: [j.issn, j.eissn].where((s) => s.isNotEmpty).toList(),
+ publisher: '',
+ lastStatusCheckTime: 0,
+ counts: Journals.Counts(
+ currentDois: 0,
+ backfileDois: 0,
+ totalDois: 0,
+ ),
+ breakdowns: Journals.Breakdowns(
+ doisByIssuedYear: [],
+ ),
+ coverage: {},
+ coverageType: Journals.CoverageType(
+ all: {},
+ backfile: {},
+ current: {},
+ ),
+ flags: {},
+ issnType: [],
+ );
+ }).toList();
+
+ return Card(
+ child: ListTile(
+ title: Text(category),
+ subtitle: Text(
+ AppLocalizations.of(context)!.countJournals(journals.length)),
+ trailing: Icon(Icons.chevron_right),
+ onTap: () {
+ Navigator.push(
+ context,
+ MaterialPageRoute(
+ builder: (_) => SearchResultsScreen(
+ searchQuery: category,
+ searchResults: ListAndMore(
+ list: converted,
+ hasMore: false,
+ totalResults: converted.length,
+ ),
+ ),
+ ),
+ );
+ },
+ ),
+ );
+ },
+ ),
+ );
+ }
+}
diff --git a/pubspec.lock b/pubspec.lock
index 63ad86f6..8315e3ab 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -105,6 +105,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.0.2"
+ csv:
+ dependency: "direct main"
+ description:
+ name: csv
+ sha256: c6aa2679b2a18cb57652920f674488d89712efaf4d3fdf2e537215b35fc19d6c
+ url: "https://pub.dev"
+ source: hosted
+ version: "6.0.0"
cupertino_icons:
dependency: "direct main"
description:
diff --git a/pubspec.yaml b/pubspec.yaml
index 40f7a609..1aa48ca0 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -66,6 +66,7 @@ dependencies:
flutter_markdown_plus: ^1.0.3
path_provider: ^2.1.5
archive: ^4.0.7
+ csv: ^6.0.0
dev_dependencies:
flutter_test: