Skip to content

Commit 935652c

Browse files
authored
Merge pull request #350 from Scriptbash/date-filters
Add date filters and OpenAlex API key
2 parents 75e7b14 + a88d17d commit 935652c

15 files changed

Lines changed: 1650 additions & 691 deletions

lib/l10n/app_en.arb

Lines changed: 718 additions & 436 deletions
Large diffs are not rendered by default.

lib/models/feed_filter_entity.dart

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ class FeedFilter {
44
final String include;
55
final String exclude;
66
final Set<String> journals;
7+
final String? dateMode;
8+
final String? dateAfter;
9+
final String? dateBefore;
710
final String dateCreated;
811

912
FeedFilter({
@@ -12,6 +15,9 @@ class FeedFilter {
1215
required this.include,
1316
required this.exclude,
1417
required this.journals,
18+
this.dateMode,
19+
this.dateAfter,
20+
this.dateBefore,
1521
required this.dateCreated,
1622
});
1723
}

lib/models/openAlex_works_models.dart

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -67,17 +67,22 @@ class OpenAlexWorks {
6767
}
6868

6969
String? reconstructAbstract(Map<String, dynamic>? invertedIndex) {
70-
if (invertedIndex == null) return null;
70+
if (invertedIndex == null || invertedIndex.isEmpty) return null;
7171

72-
int maxIndex = invertedIndex.values
73-
.expand((positions) => positions)
74-
.reduce((a, b) => a > b ? a : b);
72+
final allPositions =
73+
invertedIndex.values.expand((positions) => positions as List);
74+
75+
if (allPositions.isEmpty) return null;
76+
77+
int maxIndex = allPositions.reduce((a, b) => a > b ? a : b);
7578

7679
List<String> words = List<String>.filled(maxIndex + 1, '', growable: false);
7780

7881
invertedIndex.forEach((word, positions) {
79-
for (int pos in positions) {
80-
words[pos] = word;
82+
for (int pos in (positions as List)) {
83+
if (pos <= maxIndex) {
84+
words[pos] = word;
85+
}
8186
}
8287
});
8388

Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
import 'package:flutter/material.dart';
2+
import 'package:shared_preferences/shared_preferences.dart';
3+
import 'package:url_launcher/url_launcher.dart';
4+
import 'package:wispar/services/openAlex_api.dart';
5+
import 'package:wispar/generated_l10n/app_localizations.dart';
6+
7+
class ApiSettingsScreen extends StatefulWidget {
8+
const ApiSettingsScreen({super.key});
9+
10+
@override
11+
State<ApiSettingsScreen> createState() => ApiSettingsScreenState();
12+
}
13+
14+
class ApiSettingsScreenState extends State<ApiSettingsScreen> {
15+
final TextEditingController _openAlexKeyController = TextEditingController();
16+
bool passwordVisible = false;
17+
18+
bool _scrapeAbstracts = true; // Default to scraping missing abstracts
19+
int _fetchInterval = 6; // Default API fetch to 6 hours
20+
int _concurrentFetches = 3; // Default to 3 concurrent fetches
21+
22+
bool _overrideUserAgent = false;
23+
final TextEditingController _userAgentController = TextEditingController();
24+
25+
@override
26+
void initState() {
27+
super.initState();
28+
_loadKeys();
29+
}
30+
31+
@override
32+
void dispose() {
33+
_userAgentController.dispose();
34+
super.dispose();
35+
}
36+
37+
Future<void> _loadKeys() async {
38+
final prefs = await SharedPreferences.getInstance();
39+
40+
final openAlexKey = prefs.getString('openalex_api_key') ?? '';
41+
final fetchInterval = prefs.getInt('fetchInterval') ?? 6;
42+
final scrapeAbstracts = prefs.getBool('scrapeAbstracts') ?? true;
43+
final concurrentFetches = prefs.getInt('concurrentFetches') ?? 3;
44+
final overrideUserAgent = prefs.getBool('overrideUserAgent') ?? false;
45+
final customUserAgent = prefs.getString('customUserAgent') ?? '';
46+
47+
setState(() {
48+
_openAlexKeyController.text = openAlexKey;
49+
_fetchInterval = fetchInterval;
50+
_scrapeAbstracts = scrapeAbstracts;
51+
_concurrentFetches = concurrentFetches;
52+
_overrideUserAgent = overrideUserAgent;
53+
_userAgentController.text = customUserAgent;
54+
});
55+
}
56+
57+
Future<void> _saveKeys() async {
58+
final prefs = await SharedPreferences.getInstance();
59+
60+
await prefs.setString(
61+
'openalex_api_key', _openAlexKeyController.text.trim());
62+
63+
await prefs.setInt('fetchInterval', _fetchInterval);
64+
await prefs.setBool('scrapeAbstracts', _scrapeAbstracts);
65+
await prefs.setInt('concurrentFetches', _concurrentFetches);
66+
await prefs.setBool('overrideUserAgent', _overrideUserAgent);
67+
if (_overrideUserAgent) {
68+
await prefs.setString('customUserAgent', _userAgentController.text);
69+
}
70+
71+
OpenAlexApi.apiKey = _openAlexKeyController.text.trim();
72+
73+
if (mounted) {
74+
ScaffoldMessenger.of(context).showSnackBar(
75+
SnackBar(content: Text(AppLocalizations.of(context)!.settingsSaved)),
76+
);
77+
}
78+
}
79+
80+
@override
81+
Widget build(BuildContext context) {
82+
return Scaffold(
83+
appBar: AppBar(
84+
title: Text(AppLocalizations.of(context)!.apiSettings),
85+
),
86+
body: SafeArea(
87+
child: SingleChildScrollView(
88+
padding: const EdgeInsets.all(16),
89+
child: Column(
90+
children: [
91+
Text(
92+
AppLocalizations.of(context)!.openAlexApiKeyDesc,
93+
style: TextStyle(fontSize: 12, color: Colors.grey),
94+
),
95+
const SizedBox(height: 8),
96+
FilledButton(
97+
onPressed: () {
98+
launchUrl(
99+
Uri.parse('https://openalex.org/settings/api'),
100+
);
101+
},
102+
child: Text(AppLocalizations.of(context)!.zoteroCreateKey)),
103+
const SizedBox(height: 16),
104+
TextField(
105+
controller: _openAlexKeyController,
106+
obscureText: !passwordVisible,
107+
decoration: InputDecoration(
108+
labelText: AppLocalizations.of(context)!.openAlexApiKey,
109+
border: OutlineInputBorder(),
110+
suffixIcon: IconButton(
111+
icon: Icon(
112+
passwordVisible
113+
? Icons.visibility_outlined
114+
: Icons.visibility_off_outlined,
115+
),
116+
onPressed: () {
117+
setState(() {
118+
passwordVisible = !passwordVisible;
119+
});
120+
},
121+
),
122+
),
123+
onChanged: (value) {},
124+
),
125+
const SizedBox(height: 16),
126+
DropdownButtonFormField<int>(
127+
isExpanded: true,
128+
initialValue: _fetchInterval,
129+
onChanged: (int? newValue) {
130+
setState(() {
131+
_fetchInterval = newValue!;
132+
});
133+
},
134+
decoration: InputDecoration(
135+
labelText: AppLocalizations.of(context)!.apiFetchInterval,
136+
hintText: AppLocalizations.of(context)!.apiFetchIntervalHint,
137+
),
138+
items: [
139+
DropdownMenuItem(
140+
value: 3,
141+
child: Text('3 ${AppLocalizations.of(context)!.hours}'),
142+
),
143+
DropdownMenuItem(
144+
value: 6,
145+
child: Text('6 ${AppLocalizations.of(context)!.hours}'),
146+
),
147+
DropdownMenuItem(
148+
value: 12,
149+
child: Text('12 ${AppLocalizations.of(context)!.hours}'),
150+
),
151+
DropdownMenuItem(
152+
value: 24,
153+
child: Text('24 ${AppLocalizations.of(context)!.hours}'),
154+
),
155+
DropdownMenuItem(
156+
value: 48,
157+
child: Text('48 ${AppLocalizations.of(context)!.hours}'),
158+
),
159+
DropdownMenuItem(
160+
value: 72,
161+
child: Text('72 ${AppLocalizations.of(context)!.hours}'),
162+
),
163+
],
164+
),
165+
SizedBox(height: 16),
166+
Text(
167+
AppLocalizations.of(context)!
168+
.concurrentFetches(_concurrentFetches),
169+
),
170+
Slider(
171+
value: _concurrentFetches.toDouble(),
172+
min: 1,
173+
max: 5,
174+
divisions: 4,
175+
label: _concurrentFetches.toString(),
176+
onChanged: (double value) {
177+
setState(() {
178+
_concurrentFetches = value.toInt();
179+
});
180+
},
181+
),
182+
SizedBox(height: 16),
183+
Row(
184+
mainAxisAlignment: MainAxisAlignment.spaceBetween,
185+
children: [
186+
Text(
187+
AppLocalizations.of(context)!.scrapeAbstracts,
188+
),
189+
Switch(
190+
value: _scrapeAbstracts,
191+
onChanged: (bool value) async {
192+
setState(() {
193+
_scrapeAbstracts = value;
194+
});
195+
},
196+
),
197+
],
198+
),
199+
const SizedBox(height: 16),
200+
Row(
201+
mainAxisAlignment: MainAxisAlignment.spaceBetween,
202+
children: [
203+
Text(AppLocalizations.of(context)!.overrideUserAgent),
204+
Switch(
205+
value: _overrideUserAgent,
206+
onChanged: (bool value) {
207+
setState(() {
208+
_overrideUserAgent = value;
209+
});
210+
},
211+
),
212+
],
213+
),
214+
if (_overrideUserAgent)
215+
TextFormField(
216+
controller: _userAgentController,
217+
decoration: InputDecoration(
218+
labelText: AppLocalizations.of(context)!.customUserAgent,
219+
hintText:
220+
"Mozilla/5.0 (Android 16; Mobile; LG-M255; rv:140.0) Gecko/140.0 Firefox/140.0",
221+
),
222+
),
223+
const SizedBox(height: 20),
224+
FilledButton(
225+
onPressed: _saveKeys,
226+
child: Text(AppLocalizations.of(context)!.saveSettings),
227+
),
228+
],
229+
),
230+
),
231+
),
232+
);
233+
}
234+
}

lib/screens/article_search_results_screen.dart

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ class ArticleSearchResultsScreenState
3535
final ScrollController _scrollController = ScrollController();
3636
bool _isLoadingMore = false;
3737
bool _hasMoreResults = true;
38-
int _currentOpenAlexPage = 1;
38+
int _currentOpenAlexPage = 2;
3939

4040
SwipeAction _swipeLeftAction = SwipeAction.hide;
4141
SwipeAction _swipeRightAction = SwipeAction.favorite;
@@ -135,14 +135,16 @@ class ArticleSearchResultsScreenState
135135
widget.queryParams['scope'] ?? 1,
136136
widget.queryParams['sortField'],
137137
widget.queryParams['sortOrder'],
138+
widget.queryParams['dateFilter'],
138139
page: _currentOpenAlexPage,
139140
);
140141

141142
if (newResults.isNotEmpty) {
142143
_currentOpenAlexPage++;
144+
145+
hasMore = newResults.length >= 25;
143146
} else {
144147
hasMore = false;
145-
_isLoadingMore = false;
146148
}
147149
}
148150

0 commit comments

Comments
 (0)