Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ If you run into any issue while using Wispar, have a question or want to share y
<li><a href="https://unpaywall.org/" target='_blank'>Unpaywall</a></li>
<li><a href="https://www.crossref.org/" target='_blank'>Crossref</a></li>
<li><a href="https://openalex.org/" target='_blank'>OpenAlex</a></li>
<li><a href="https://github.com/hitfyd/ShowJCR" target='_blank'>Journal topics database</a> by <a href="https://github.com/hitfyd"target='_blank'> hitfyd</a></li>
</ul>

## Screenshots
Expand Down
8 changes: 8 additions & 0 deletions lib/l10n/app_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down Expand Up @@ -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.",
Expand All @@ -215,6 +221,8 @@
"@category": {},
"publisher": "Publisher",
"@publisher": {},
"publisherName": "Publisher: {publisherName}",
"@publisherName":{},
"publishedin": "Published in",
"@publishedin": {},
"subjects": "Subjects",
Expand Down
13 changes: 13 additions & 0 deletions lib/models/journal_topics_models.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
class JournalTopicsCsv {
final String journal;
final String issn;
final String eissn;
final List<String> categories;

JournalTopicsCsv({
required this.journal,
required this.issn,
required this.eissn,
required this.categories,
});
}
28 changes: 15 additions & 13 deletions lib/screens/journals_search_results_screen.dart
Original file line number Diff line number Diff line change
@@ -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<Journals.Item> 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<SearchResultsScreen> {
class SearchResultsScreenState extends State<SearchResultsScreen> {
final logger = LogsService().logger;
List<Journals.Item> items = [];
bool isLoading = false;
Expand Down Expand Up @@ -101,10 +101,12 @@ class _SearchResultsScreenState extends State<SearchResultsScreen> {
}
});
} 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);
}
Expand Down
46 changes: 46 additions & 0 deletions lib/services/journal_topics_helper.dart
Original file line number Diff line number Diff line change
@@ -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<List<JournalTopicsCsv>> 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<JournalTopicsCsv> 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;
}
103 changes: 55 additions & 48 deletions lib/widgets/article_crossref_search_form.dart
Original file line number Diff line number Diff line change
@@ -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<CrossRefSearchForm> {
class CrossRefSearchFormState extends State<CrossRefSearchForm> {
final logger = LogsService().logger;
int selectedSearchIndex = 0; // 0 for Query, 1 for DOI
final TextEditingController doiController = TextEditingController();
Expand Down Expand Up @@ -63,41 +65,47 @@ class _CrossRefSearchFormState extends State<CrossRefSearchForm> {
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)),
);
}
}
}

Expand All @@ -118,6 +126,16 @@ class _CrossRefSearchFormState extends State<CrossRefSearchForm> {
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,
Expand All @@ -131,16 +149,6 @@ class _CrossRefSearchFormState extends State<CrossRefSearchForm> {
child: Text(AppLocalizations.of(context)!.searchByDOI),
),
],
isSelected: [
selectedSearchIndex == 0,
selectedSearchIndex == 1,
],
onPressed: (int index) {
setState(() {
selectedSearchIndex = index;
});
},
borderRadius: BorderRadius.circular(15.0),
);
},
),
Expand All @@ -155,7 +163,6 @@ class _CrossRefSearchFormState extends State<CrossRefSearchForm> {
floatingActionButton: FloatingActionButton(
onPressed: _handleSearch,
child: Icon(Icons.search),
shape: CircleBorder(),
),
);
}
Expand Down
1 change: 0 additions & 1 deletion lib/widgets/article_openAlex_search_form.dart
Original file line number Diff line number Diff line change
Expand Up @@ -438,7 +438,6 @@ class _OpenAlexSearchFormState extends State<OpenAlexSearchForm> {
floatingActionButton: FloatingActionButton(
onPressed: _executeSearch,
child: Icon(Icons.search),
shape: CircleBorder(),
),
);
}
Expand Down
10 changes: 6 additions & 4 deletions lib/widgets/article_search_form.dart
Original file line number Diff line number Diff line change
@@ -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<ArticleSearchScreen> {
class ArticleSearchScreenState extends State<ArticleSearchScreen> {
int selectedProviderIndex = 0; // 0 = OpenAlex, 1 = Crossref

@override
Expand Down
11 changes: 6 additions & 5 deletions lib/widgets/journal_header.dart
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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),
],
Expand Down
Loading