diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 5255b4f6..1f91d5bc 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -306,6 +306,14 @@ "@session_neverSynced": { "description": "Default value for the last sync time." }, + "session_renewDialogMessage": "The server refuses the current session token. You will need to log in again to resume the synchronization.", + "@session_renewDialogMessage": { + "description": "The message of the confirmation dialog to renew the session." + }, + "session_renewDialogTitle": "Renew session", + "@session_renewDialogTitle": { + "description": "The title of the confirmation dialog to renew the session." + }, "session_title": "Session details", "@session_title": { "description": "The title of the session details screen." diff --git a/lib/pages/listing.dart b/lib/pages/listing.dart index a67a3ab8..87c0c35b 100644 --- a/lib/pages/listing.dart +++ b/lib/pages/listing.dart @@ -50,12 +50,6 @@ class _ListingPageState extends State { final settings = storage.settings; final queryProvider = context.watch(); - storage.onError = (error) { - ScaffoldMessenger.maybeOf(context)?.showSnackBar( - SnackBar(content: Text(error.toString())), - ); - }; - Future doRefresh() async { _log.info('triggered refresh'); await context.read().synchronize(withFinalRefresh: true); diff --git a/lib/pages/login.dart b/lib/pages/login.dart index 6f33ac1b..40d5a263 100644 --- a/lib/pages/login.dart +++ b/lib/pages/login.dart @@ -21,6 +21,8 @@ class LoginPage extends StatefulWidget { final Map? initial; + bool get hasInitialData => initial != null && initial!.isNotEmpty; + @override State createState() => _LoginPageState(); } @@ -35,7 +37,18 @@ class _LoginPageState extends State { @override void initState() { super.initState(); - _initialData = widget.initial; + + final wallabag = WallabagInstance.get(); + if (!widget.hasInitialData && wallabag.hasCredentials) { + _initialData = { + 'server': wallabag.credentials.server.toString(), + 'clientId': wallabag.credentials.clientId, + 'clientSecret': wallabag.credentials.clientSecret, + }; + } else { + _initialData = widget.initial; + } + if (WallabagInstance.isReady) { WidgetsBinding.instance.addPostFrameCallback((_) async { final settings = context.read(); @@ -61,7 +74,7 @@ class _LoginPageState extends State { @override Widget build(BuildContext context) { - if (_initialData != widget.initial) { + if (widget.hasInitialData && _initialData != widget.initial) { // when a deeplink is opened and the login page is already shown _initialData = widget.initial; if (_initialData?['server'] != null) { @@ -85,7 +98,7 @@ class _LoginPageState extends State { _configuredServer = check; } }), - initial: widget.initial?['server'], + initial: _initialData?['server'], ), Padding( padding: const EdgeInsets.all(8.0), @@ -108,7 +121,7 @@ class _LoginPageState extends State { icon: const Icon(Icons.key), labelText: context.L.login_fieldClientId, ), - initialValue: widget.initial?['clientId'], + initialValue: _initialData?['clientId'], ), FormBuilderTextField( name: 'clientSecret', @@ -118,7 +131,7 @@ class _LoginPageState extends State { icon: const Icon(Icons.key), labelText: context.L.login_fieldClientSecret, ), - initialValue: widget.initial?['clientSecret'], + initialValue: _initialData?['clientSecret'], ), FormBuilderTextField( name: 'username', @@ -129,7 +142,7 @@ class _LoginPageState extends State { icon: const Icon(Icons.person), labelText: context.L.login_fieldUsername, ), - initialValue: widget.initial?['username'], + initialValue: _initialData?['username'], ), FormBuilderTextField( name: 'password', @@ -141,7 +154,7 @@ class _LoginPageState extends State { icon: const Icon(Icons.password), labelText: context.L.login_fieldPassword, ), - initialValue: widget.initial?['password'], + initialValue: _initialData?['password'], ), const SizedBox(height: 8.0), ]), diff --git a/lib/pages/session_details.dart b/lib/pages/session_details.dart index ee11e73d..f090051c 100644 --- a/lib/pages/session_details.dart +++ b/lib/pages/session_details.dart @@ -95,7 +95,7 @@ class SessionDetailsPage extends StatelessWidget { ); if (result == OkCancelResult.cancel) return; - await WallabagInstance.get().resetTokenData(); + await WallabagInstance.get().resetSession(); settings.clear(); await DB.clear(); if (context.mounted) { diff --git a/lib/services/wallabag_storage.dart b/lib/services/wallabag_storage.dart index d6fdbeae..c0fa017b 100644 --- a/lib/services/wallabag_storage.dart +++ b/lib/services/wallabag_storage.dart @@ -16,7 +16,7 @@ import '../wallabag/wallabag.dart'; final _log = Logger('wallabag.storage'); class WallabagStorage with ChangeNotifier { - WallabagStorage(this.settings, {this.onError}) { + WallabagStorage(this.settings) { _watcher = db.articles.watchLazy().listen((_) => notifyListeners()); // ensure a relative freshness of the articles @@ -28,7 +28,6 @@ class WallabagStorage with ChangeNotifier { final WallabagClient wallabag = WallabagInstance.get(); StreamSubscription? _watcher; final SettingsProvider settings; - void Function(Exception)? onError; @override void dispose() { @@ -124,11 +123,7 @@ class WallabagStorage with ChangeNotifier { perPage: 100, detail: DetailValue.metadata, ); - await for (final (entries, err) in entriesStream) { - if (err != null) { - onError?.call(err); - break; - } + await for (final entries in entriesStream) { localIds = localIds.difference(entries.map((e) => e.id).toSet()); } @@ -174,42 +169,37 @@ class WallabagStorage with ChangeNotifier { if (since == null) clearArticles(); - try { - final stopwatch = Stopwatch()..start(); - var entriesStream = - wallabag.fetchAllEntries(since: since, onProgress: onProgress); - await for (final (entries, err) in entriesStream) { - if (err != null) throw err; - final articles = { - for (var e in entries) e.id: Article.fromWallabagEntry(e) - }; - final positions = - await db.articleScrollPositions.getAll(articles.keys.toList()); - final invalidPositions = positions - .whereType() - .where((e) => e.readingTime != articles[e.id]?.readingTime) - .map((e) => e.id!) - .toList(); - - final putCount = await db.writeTxn(() async { - final res = await db.articles.putAll(articles.values.toList()); - await db.articleScrollPositions.deleteAll(invalidPositions); - return res.length; - }); - _log.info('saved $putCount entries to the database'); - - count += entries.length; - } - _log.info( - 'completed refresh of $count entries in ${stopwatch.elapsed.inSeconds} s'); + final stopwatch = Stopwatch()..start(); + var entriesStream = + wallabag.fetchAllEntries(since: since, onProgress: onProgress); + await for (final entries in entriesStream) { + final articles = { + for (var e in entries) e.id: Article.fromWallabagEntry(e) + }; + final positions = + await db.articleScrollPositions.getAll(articles.keys.toList()); + final invalidPositions = positions + .whereType() + .where((e) => e.readingTime != articles[e.id]?.readingTime) + .map((e) => e.id!) + .toList(); + + final putCount = await db.writeTxn(() async { + final res = await db.articles.putAll(articles.values.toList()); + await db.articleScrollPositions.deleteAll(invalidPositions); + return res.length; + }); + _log.info('saved $putCount entries to the database'); + + count += entries.length; + } + _log.info( + 'completed refresh of $count entries in ${stopwatch.elapsed.inSeconds} s'); - final now = DateTime.now().millisecondsSinceEpoch / 1000; - settings[Sk.lastRefresh] = now.toInt(); + final now = DateTime.now().millisecondsSinceEpoch / 1000; + settings[Sk.lastRefresh] = now.toInt(); - _syncRemoteDeletes(); - } on Exception catch (e) { - onError?.call(e); - } + _syncRemoteDeletes(); updateAppBadge(); diff --git a/lib/wallabag/wallabag.dart b/lib/wallabag/wallabag.dart index 4d415e17..421ba152 100644 --- a/lib/wallabag/wallabag.dart +++ b/lib/wallabag/wallabag.dart @@ -52,6 +52,16 @@ class WallabagError implements Exception { } factory WallabagError.fromException(Exception e, {http.Response? response}) => WallabagError('unknown error', source: e, response: response); + + bool get isInvalidTokenError { + if (response?.body == null) return false; + try { + final json = jsonDecode(response!.body); + return json['error'] == 'invalid_grant'; + } catch (_) { + return false; + } + } } typedef Decoder = T Function(Map); @@ -93,6 +103,7 @@ class WallabagClient extends http.BaseClient { final CredentialsManager _credsManager; final String? userAgent; + bool get hasCredentials => _credsManager.credentials != null; Credentials get credentials => _credsManager.credentials!; bool get canRefreshToken => _credsManager.canRefreshToken; bool get tokenIsExpired => _credsManager.tokenIsExpired; @@ -151,6 +162,8 @@ class WallabagClient extends http.BaseClient { _credsManager.token = null; } + Future resetSession() => _credsManager.clear(); + Future fetchToken(String username, String password) { return authenticate({ 'grant_type': 'password', @@ -280,7 +293,7 @@ class WallabagClient extends http.BaseClient { return data.total; } - Stream<(List, WallabagError?)> fetchAllEntries({ + Stream> fetchAllEntries({ int? archive, int? starred, SortValue? sort, @@ -313,21 +326,21 @@ class WallabagClient extends http.BaseClient { domainName: domainName, ); } catch (source, st) { - final e = switch (source.runtimeType) { - WallabagError e => e, - Exception e => WallabagError.fromException(e), - _ => WallabagError.fromException(Exception(source.toString())), - }; _log.severe( 'error fetching entries (page $pageIndex)', source.toString(), st); - yield ([], e); - return; + if (source is WallabagError) { + rethrow; + } else if (source is Exception) { + throw WallabagError.fromException(source); + } else { + throw WallabagError.fromException(Exception(source.toString())); + } } lastPage = pageData.pages; _log.info('fetched entries (page $pageIndex of $lastPage)'); onProgress?.call(pageIndex / lastPage); pageIndex++; - yield (pageData.embedded.items, null); + yield pageData.embedded.items; } } } diff --git a/lib/widgets/remote_sync_progress_indicator.dart b/lib/widgets/remote_sync_progress_indicator.dart index dfa3d7fe..ebf154b1 100644 --- a/lib/widgets/remote_sync_progress_indicator.dart +++ b/lib/widgets/remote_sync_progress_indicator.dart @@ -1,8 +1,12 @@ +import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; import 'package:logging/logging.dart'; import 'package:provider/provider.dart'; +import '../buildcontext_extension.dart'; import '../services/remote_sync.dart'; +import '../wallabag/wallabag.dart'; final _log = Logger('widgets.remote_sync_progress_reporter'); @@ -18,9 +22,24 @@ class RemoteSyncProgressIndicator extends StatelessWidget { final error = provider?.lastError; if (error != null) { - WidgetsBinding.instance.addPostFrameCallback((_) { - final snackbar = SnackBar(content: Text(error.toString())); - ScaffoldMessenger.maybeOf(context)?.showSnackBar(snackbar); + WidgetsBinding.instance.addPostFrameCallback((_) async { + if (error is WallabagError && error.isInvalidTokenError) { + final result = await showOkCancelAlertDialog( + context: context, + title: context.L.session_renewDialogTitle, + message: context.L.session_renewDialogMessage, + okLabel: context.L.login_actionLogin, + ); + if (result == OkCancelResult.ok) { + await WallabagInstance.get().resetTokenData(); + if (context.mounted) { + context.go('/login'); + } + } + } else { + final snackbar = SnackBar(content: Text(error.toString())); + ScaffoldMessenger.maybeOf(context)?.showSnackBar(snackbar); + } }); }