diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 52fadcd..9a6aa83 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -193,6 +193,10 @@ "@publishedBetween":{}, "noFilter": "No filter", "@noFilter":{}, + "issnFilter":"ISSN filter", + "@issnFilter":{}, + "openAccessOnly":"Open access only", + "@openAccessOnly":{}, "selectStartDate": "Select start date", "@selectStartDate":{}, "selectEndDate": "Select end date", diff --git a/lib/screens/article_search_results_screen.dart b/lib/screens/article_search_results_screen.dart index a6062a5..efd4ce4 100644 --- a/lib/screens/article_search_results_screen.dart +++ b/lib/screens/article_search_results_screen.dart @@ -89,6 +89,8 @@ class _ArticleSearchResultsScreenState widget.queryParams['sortField'], widget.queryParams['sortOrder'], widget.queryParams['dateFilter'], + widget.queryParams['issnFilter'], + widget.queryParams['isOpenAccess'] ?? false, page: pageKey, ); diff --git a/lib/services/openAlex_api.dart b/lib/services/openAlex_api.dart index 8e33838..71fd6d0 100644 --- a/lib/services/openAlex_api.dart +++ b/lib/services/openAlex_api.dart @@ -11,8 +11,14 @@ class OpenAlexApi { static const String worksEndpoint = '/works?'; static String? apiKey; - static Future> getOpenAlexWorksByQuery(String query, - int scope, String? sortField, String? sortOrder, String? dateFilter, + static Future> getOpenAlexWorksByQuery( + String query, + int scope, + String? sortField, + String? sortOrder, + String? dateFilter, + String? issnFilter, + bool isOpenAccess, {int page = 1}) async { final prefs = await SharedPreferences.getInstance(); apiKey = prefs.getString('openalex_api_key'); @@ -41,6 +47,21 @@ class OpenAlexApi { filterPart += ',$dateFilter'; } } + if (issnFilter != null && issnFilter.isNotEmpty) { + if (filterPart.isEmpty) { + filterPart = 'filter=$issnFilter'; + } else { + filterPart += ',$issnFilter'; + } + } + + if (isOpenAccess) { + if (filterPart.isEmpty) { + filterPart = 'filter=is_oa:true'; + } else { + filterPart += ',is_oa:true'; + } + } String sortPart = ''; if (sortField != null && sortOrder != null) { diff --git a/lib/widgets/article_openAlex_search_form.dart b/lib/widgets/article_openAlex_search_form.dart index e292541..8635cb7 100644 --- a/lib/widgets/article_openAlex_search_form.dart +++ b/lib/widgets/article_openAlex_search_form.dart @@ -19,6 +19,8 @@ class OpenAlexSearchFormState extends State { String selectedSortOrder = '-'; DateTime? _publishedAfter; DateTime? _publishedBefore; + final TextEditingController issnController = TextEditingController(); + bool _isOpenAccess = false; String _dateMode = 'none'; // bool _filtersExpanded = false; @@ -33,6 +35,13 @@ class OpenAlexSearchFormState extends State { _checkApiKey(); } + @override + void dispose() { + issnController.dispose(); + queryNameController.dispose(); + super.dispose(); + } + void _addQueryPart(String type) { setState(() { if (queryParts.isNotEmpty && queryParts.last['type'] != 'operator') { @@ -125,6 +134,7 @@ class OpenAlexSearchFormState extends State { String? sortField = selectedSortField == '-' ? null : selectedSortField; String? sortOrder = selectedSortOrder == '-' ? null : selectedSortOrder; String? dateFilter; + String? issnFilter; String formatDate(DateTime d) => d.toIso8601String().split('T')[0]; @@ -139,6 +149,16 @@ class OpenAlexSearchFormState extends State { "to_publication_date:${formatDate(_publishedBefore!)}"; } + if (issnController.text.trim().isNotEmpty) { + final issns = issnController.text + .split(',') + .map((e) => e.trim()) + .where((e) => e.isNotEmpty) + .join('|'); + + issnFilter = "locations.source.issn:$issns"; + } + try { showDialog( context: context, @@ -182,8 +202,33 @@ class OpenAlexSearchFormState extends State { } } + String issnPart = ''; + + if (issnFilter != null && issnFilter.isNotEmpty) { + if (searchField.startsWith('filter=')) { + issnPart = ',$issnFilter'; + } else if (datePart.isNotEmpty) { + issnPart = ',$issnFilter'; + } else { + issnPart = '&filter=$issnFilter'; + } + } + + String oaPart = ''; + if (_isOpenAccess) { + if (searchField.startsWith('filter=')) { + oaPart = ',is_oa:true'; + } else if (datePart.isNotEmpty || issnPart.isNotEmpty) { + oaPart = ',is_oa:true'; + } else { + oaPart = '&filter=is_oa:true'; + } + } + queryString = '$searchField$query' '$datePart' + '$issnPart' + '$oaPart' '$selectedSortBy' '$selectedSortOrder'; await dbHelper.saveSearchQuery(queryName, queryString, 'OpenAlex'); @@ -208,6 +253,8 @@ class OpenAlexSearchFormState extends State { if (sortField != null) 'sortField': sortField, if (sortOrder != null) 'sortOrder': sortOrder, if (dateFilter != null) 'dateFilter': dateFilter, + if (issnFilter != null) 'issnFilter': issnFilter, + 'isOpenAccess': _isOpenAccess, }, source: 'OpenAlex', ), @@ -281,6 +328,16 @@ class OpenAlexSearchFormState extends State { border: OutlineInputBorder(), ), ), + SizedBox(height: 16), + TextFormField( + controller: issnController, + decoration: InputDecoration( + labelText: AppLocalizations.of(context)!.issnFilter, + hintText: "0022-1694, 1234-5678", + border: OutlineInputBorder(), + ), + ), + SizedBox(height: 16), DropdownButtonFormField( @@ -416,8 +473,24 @@ class OpenAlexSearchFormState extends State { ), ], ), + SizedBox(height: 16), + SwitchListTile( + title: Text( + AppLocalizations.of(context)!.openAccessOnly, + style: TextStyle(fontWeight: FontWeight.w500), + ), + value: _isOpenAccess, + onChanged: (bool value) { + setState(() { + _isOpenAccess = value; + }); + }, + ), + SizedBox(height: 10), + Divider( + height: 5, + ), SizedBox(height: 10), - // Dynamic query builder Column( children: () { @@ -548,11 +621,11 @@ class OpenAlexSearchFormState extends State { ], ),*/ SizedBox(height: 20), - Text( - AppLocalizations.of(context)!.saveQuery, - style: TextStyle(fontWeight: FontWeight.bold), - ), - Switch( + SwitchListTile( + title: Text( + AppLocalizations.of(context)!.saveQuery, + style: TextStyle(fontWeight: FontWeight.w600), + ), value: saveQuery, onChanged: (bool value) { setState(() { @@ -560,15 +633,17 @@ class OpenAlexSearchFormState extends State { }); }, ), - SizedBox(height: 8), - if (saveQuery) + if (saveQuery) ...[ + const SizedBox(height: 8), TextFormField( controller: queryNameController, decoration: InputDecoration( labelText: AppLocalizations.of(context)!.queryName, - border: OutlineInputBorder(), + border: const OutlineInputBorder(), ), ), + ], + SizedBox(height: 70), ], ), diff --git a/lib/widgets/search_query_card.dart b/lib/widgets/search_query_card.dart index d59174b..575d68d 100644 --- a/lib/widgets/search_query_card.dart +++ b/lib/widgets/search_query_card.dart @@ -68,7 +68,9 @@ class SearchQueryCardState extends State { String? sortField; String? sortOrder; String? dateFilter; + String? issnFilter; String? filterValue; + bool isOpenAccess = false; if (widget.queryProvider == 'Crossref') { // Convert the params string to the needed mapstring @@ -77,7 +79,7 @@ class SearchQueryCardState extends State { queryMap = Uri.splitQueryString(widget.queryParams); String? sortParam = queryMap['sort']; - String? filterValue = queryMap['filter']; + filterValue = queryMap['filter']; String? searchValue = queryMap['search']; sortField = null; @@ -109,6 +111,10 @@ class SearchQueryCardState extends State { } else if (f.startsWith('from_publication_date:') || f.startsWith('to_publication_date:')) { remainingFilters.add(f); + } else if (f == 'is_oa:true') { + isOpenAccess = true; + } else if (f.startsWith('locations.source.issn:')) { + issnFilter = f; } } if (remainingFilters.isNotEmpty) { @@ -136,6 +142,8 @@ class SearchQueryCardState extends State { 'sortField': sortField, 'sortOrder': sortOrder, 'dateFilter': dateFilter, + 'issnFilter': issnFilter, + 'isOpenAccess': isOpenAccess, 'filter': filterValue, }, source: widget.queryProvider,