Skip to content
Merged
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,154 changes: 718 additions & 436 deletions lib/l10n/app_en.arb

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions lib/models/feed_filter_entity.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ class FeedFilter {
final String include;
final String exclude;
final Set<String> journals;
final String? dateMode;
final String? dateAfter;
final String? dateBefore;
final String dateCreated;

FeedFilter({
Expand All @@ -12,6 +15,9 @@ class FeedFilter {
required this.include,
required this.exclude,
required this.journals,
this.dateMode,
this.dateAfter,
this.dateBefore,
required this.dateCreated,
});
}
17 changes: 11 additions & 6 deletions lib/models/openAlex_works_models.dart
Original file line number Diff line number Diff line change
Expand Up @@ -67,17 +67,22 @@ class OpenAlexWorks {
}

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

int maxIndex = invertedIndex.values
.expand((positions) => positions)
.reduce((a, b) => a > b ? a : b);
final allPositions =
invertedIndex.values.expand((positions) => positions as List);

if (allPositions.isEmpty) return null;

int maxIndex = allPositions.reduce((a, b) => a > b ? a : b);

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

invertedIndex.forEach((word, positions) {
for (int pos in positions) {
words[pos] = word;
for (int pos in (positions as List)) {
if (pos <= maxIndex) {
words[pos] = word;
}
}
});

Expand Down
234 changes: 234 additions & 0 deletions lib/screens/api_settings_screen.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,234 @@
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:wispar/services/openAlex_api.dart';
import 'package:wispar/generated_l10n/app_localizations.dart';

class ApiSettingsScreen extends StatefulWidget {
const ApiSettingsScreen({super.key});

@override
State<ApiSettingsScreen> createState() => ApiSettingsScreenState();
}

class ApiSettingsScreenState extends State<ApiSettingsScreen> {
final TextEditingController _openAlexKeyController = TextEditingController();
bool passwordVisible = false;

bool _scrapeAbstracts = true; // Default to scraping missing abstracts
int _fetchInterval = 6; // Default API fetch to 6 hours
int _concurrentFetches = 3; // Default to 3 concurrent fetches

bool _overrideUserAgent = false;
final TextEditingController _userAgentController = TextEditingController();

@override
void initState() {
super.initState();
_loadKeys();
}

@override
void dispose() {
_userAgentController.dispose();
super.dispose();
}

Future<void> _loadKeys() async {
final prefs = await SharedPreferences.getInstance();

final openAlexKey = prefs.getString('openalex_api_key') ?? '';
final fetchInterval = prefs.getInt('fetchInterval') ?? 6;
final scrapeAbstracts = prefs.getBool('scrapeAbstracts') ?? true;
final concurrentFetches = prefs.getInt('concurrentFetches') ?? 3;
final overrideUserAgent = prefs.getBool('overrideUserAgent') ?? false;
final customUserAgent = prefs.getString('customUserAgent') ?? '';

setState(() {
_openAlexKeyController.text = openAlexKey;
_fetchInterval = fetchInterval;
_scrapeAbstracts = scrapeAbstracts;
_concurrentFetches = concurrentFetches;
_overrideUserAgent = overrideUserAgent;
_userAgentController.text = customUserAgent;
});
}

Future<void> _saveKeys() async {
final prefs = await SharedPreferences.getInstance();

await prefs.setString(
'openalex_api_key', _openAlexKeyController.text.trim());

await prefs.setInt('fetchInterval', _fetchInterval);
await prefs.setBool('scrapeAbstracts', _scrapeAbstracts);
await prefs.setInt('concurrentFetches', _concurrentFetches);
await prefs.setBool('overrideUserAgent', _overrideUserAgent);
if (_overrideUserAgent) {
await prefs.setString('customUserAgent', _userAgentController.text);
}

OpenAlexApi.apiKey = _openAlexKeyController.text.trim();

if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(AppLocalizations.of(context)!.settingsSaved)),
);
}
}

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(AppLocalizations.of(context)!.apiSettings),
),
body: SafeArea(
child: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
children: [
Text(
AppLocalizations.of(context)!.openAlexApiKeyDesc,
style: TextStyle(fontSize: 12, color: Colors.grey),
),
const SizedBox(height: 8),
FilledButton(
onPressed: () {
launchUrl(
Uri.parse('https://openalex.org/settings/api'),
);
},
child: Text(AppLocalizations.of(context)!.zoteroCreateKey)),
const SizedBox(height: 16),
TextField(
controller: _openAlexKeyController,
obscureText: !passwordVisible,
decoration: InputDecoration(
labelText: AppLocalizations.of(context)!.openAlexApiKey,
border: OutlineInputBorder(),
suffixIcon: IconButton(
icon: Icon(
passwordVisible
? Icons.visibility_outlined
: Icons.visibility_off_outlined,
),
onPressed: () {
setState(() {
passwordVisible = !passwordVisible;
});
},
),
),
onChanged: (value) {},
),
const SizedBox(height: 16),
DropdownButtonFormField<int>(
isExpanded: true,
initialValue: _fetchInterval,
onChanged: (int? newValue) {
setState(() {
_fetchInterval = newValue!;
});
},
decoration: InputDecoration(
labelText: AppLocalizations.of(context)!.apiFetchInterval,
hintText: AppLocalizations.of(context)!.apiFetchIntervalHint,
),
items: [
DropdownMenuItem(
value: 3,
child: Text('3 ${AppLocalizations.of(context)!.hours}'),
),
DropdownMenuItem(
value: 6,
child: Text('6 ${AppLocalizations.of(context)!.hours}'),
),
DropdownMenuItem(
value: 12,
child: Text('12 ${AppLocalizations.of(context)!.hours}'),
),
DropdownMenuItem(
value: 24,
child: Text('24 ${AppLocalizations.of(context)!.hours}'),
),
DropdownMenuItem(
value: 48,
child: Text('48 ${AppLocalizations.of(context)!.hours}'),
),
DropdownMenuItem(
value: 72,
child: Text('72 ${AppLocalizations.of(context)!.hours}'),
),
],
),
SizedBox(height: 16),
Text(
AppLocalizations.of(context)!
.concurrentFetches(_concurrentFetches),
),
Slider(
value: _concurrentFetches.toDouble(),
min: 1,
max: 5,
divisions: 4,
label: _concurrentFetches.toString(),
onChanged: (double value) {
setState(() {
_concurrentFetches = value.toInt();
});
},
),
SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
AppLocalizations.of(context)!.scrapeAbstracts,
),
Switch(
value: _scrapeAbstracts,
onChanged: (bool value) async {
setState(() {
_scrapeAbstracts = value;
});
},
),
],
),
const SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(AppLocalizations.of(context)!.overrideUserAgent),
Switch(
value: _overrideUserAgent,
onChanged: (bool value) {
setState(() {
_overrideUserAgent = value;
});
},
),
],
),
if (_overrideUserAgent)
TextFormField(
controller: _userAgentController,
decoration: InputDecoration(
labelText: AppLocalizations.of(context)!.customUserAgent,
hintText:
"Mozilla/5.0 (Android 16; Mobile; LG-M255; rv:140.0) Gecko/140.0 Firefox/140.0",
),
),
const SizedBox(height: 20),
FilledButton(
onPressed: _saveKeys,
child: Text(AppLocalizations.of(context)!.saveSettings),
),
],
),
),
),
);
}
}
6 changes: 4 additions & 2 deletions lib/screens/article_search_results_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ class ArticleSearchResultsScreenState
final ScrollController _scrollController = ScrollController();
bool _isLoadingMore = false;
bool _hasMoreResults = true;
int _currentOpenAlexPage = 1;
int _currentOpenAlexPage = 2;

SwipeAction _swipeLeftAction = SwipeAction.hide;
SwipeAction _swipeRightAction = SwipeAction.favorite;
Expand Down Expand Up @@ -135,14 +135,16 @@ class ArticleSearchResultsScreenState
widget.queryParams['scope'] ?? 1,
widget.queryParams['sortField'],
widget.queryParams['sortOrder'],
widget.queryParams['dateFilter'],
page: _currentOpenAlexPage,
);

if (newResults.isNotEmpty) {
_currentOpenAlexPage++;

hasMore = newResults.length >= 25;
} else {
hasMore = false;
_isLoadingMore = false;
}
}

Expand Down
Loading