From dcfe9780167ea4fe96697cf7b1453a39b29a8810 Mon Sep 17 00:00:00 2001 From: Abhishek Shroff Date: Sun, 18 May 2025 08:10:46 +0530 Subject: [PATCH] [client] WIP: primary instance takeover --- client/lib/libphylum/phylum_account.dart | 10 +- client/lib/main.dart | 65 +------------ client/lib/presence_stub.dart | 1 - client/lib/presence_web.dart | 4 - client/lib/ui/account/account_selector.dart | 62 +++++++++++++ .../account_selector_app.dart} | 42 ++++----- .../lib/ui/{login => account}/login_page.dart | 0 .../ui/{logout => account}/logout_page.dart | 2 +- client/lib/ui/account/presence_stub.dart | 4 + client/lib/ui/account/presence_web.dart | 93 +++++++++++++++++++ client/lib/ui/app/app.dart | 31 ++++--- client/lib/ui/app/nav_list.dart | 2 +- client/lib/ui/logout/logout_app.dart | 81 ---------------- client/pubspec.lock | 8 +- client/pubspec.yaml | 6 +- client/web/index.html | 16 +++- 16 files changed, 225 insertions(+), 202 deletions(-) delete mode 100644 client/lib/presence_stub.dart delete mode 100644 client/lib/presence_web.dart create mode 100644 client/lib/ui/account/account_selector.dart rename client/lib/ui/{login/login_app.dart => account/account_selector_app.dart} (57%) rename client/lib/ui/{login => account}/login_page.dart (100%) rename client/lib/ui/{logout => account}/logout_page.dart (93%) create mode 100644 client/lib/ui/account/presence_stub.dart create mode 100644 client/lib/ui/account/presence_web.dart delete mode 100644 client/lib/ui/logout/logout_app.dart diff --git a/client/lib/libphylum/phylum_account.dart b/client/lib/libphylum/phylum_account.dart index 14356d26..9e03a6b3 100644 --- a/client/lib/libphylum/phylum_account.dart +++ b/client/lib/libphylum/phylum_account.dart @@ -97,7 +97,7 @@ class PhylumAccount extends Account { if (errorResponse is PhylumApiErrorResponse) { if (errorResponse.code == "credentials_invalid") { logger.i('Invalid Credentials - Logging out'); - logOut(); + close(clearData: true); l.call(); } } @@ -108,9 +108,11 @@ class PhylumAccount extends Account { Future transaction(Future Function() fn) => db.transaction(fn); @override - Future cleanup() async { - await super.cleanup(); - await db.dropDatabase(); + Future cleanup(bool clearData) async { + await super.cleanup(clearData); + if (clearData) { + await db.dropDatabase(); + } } static Future createFromLoginResponse( diff --git a/client/lib/main.dart b/client/lib/main.dart index c253575e..887591a1 100644 --- a/client/lib/main.dart +++ b/client/lib/main.dart @@ -3,31 +3,22 @@ import 'dart:io'; import 'package:drift/drift.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_state_notifier/flutter_state_notifier.dart'; import 'package:flutter_web_plugins/url_strategy.dart'; import 'package:hive_ce/hive.dart'; import 'package:logger/logger.dart'; import 'package:offtheline/offtheline.dart'; import 'package:path/path.dart' as path; import 'package:path_provider/path_provider.dart'; -import 'package:phylum/ui/app/app.dart'; import 'package:phylum/integrations/directories.dart'; import 'package:phylum/libphylum/actions/deserializers.dart'; import 'package:phylum/libphylum/db/db.dart'; -import 'package:phylum/libphylum/phylum_account.dart'; -import 'package:phylum/ui/login/login_app.dart'; import 'package:phylum/util/logging.dart'; -import 'package:provider/provider.dart'; -import 'presence_stub.dart' if (dart.library.js_interop) 'presence_web.dart'; +import 'ui/account/presence_stub.dart' if (dart.library.js_interop) 'ui/account/presence_web.dart'; const storageDir = String.fromEnvironment("STORAGE_DIR"); void main() async { - if (isAnother()) { - runApp(const AnotherRunningMessage()); - return; - } usePathUrlStrategy(); WidgetsFlutterBinding.ensureInitialized(); @@ -62,57 +53,5 @@ void main() async { } Hive.registerAdapter(OfflineActionAdapter(actionDeserializers)); - final accountManager = await AccountManager.restore((id) async { - final account = PhylumAccount.restore(id: id); - final err = await account.initialized; - if (err != null) { - logger.e("Unable to restore account: $err"); - return null; - } - return account; - }); - - runApp(AccountSelector(accountManager: accountManager)); -} - -class AnotherRunningMessage extends StatelessWidget { - const AnotherRunningMessage({super.key}); - - @override - Widget build(BuildContext context) { - return const Directionality( - textDirection: TextDirection.ltr, - child: Center( - child: Card( - child: Padding( - padding: EdgeInsets.all(16.0), - child: Text('Phylum is running in another tab or window.'), - ), - ), - ), - ); - } -} - -class AccountSelector extends StatelessWidget { - final AccountManager accountManager; - - const AccountSelector({ - super.key, - required this.accountManager, - }); - - @override - Widget build(BuildContext context) => - StateNotifierProvider, AccountManagerState>.value( - value: accountManager, - builder: (context, child) { - final account = - context.select, PhylumAccount?>((state) => state.selectedAccount); - - if (account == null) { - return const LoginApp(key: ValueKey('login')); - } - return PhylumApp.create(account); - }); + runApp(buildApp()); } diff --git a/client/lib/presence_stub.dart b/client/lib/presence_stub.dart deleted file mode 100644 index 5441a88b..00000000 --- a/client/lib/presence_stub.dart +++ /dev/null @@ -1 +0,0 @@ -bool isAnother() => false; diff --git a/client/lib/presence_web.dart b/client/lib/presence_web.dart deleted file mode 100644 index 658a0dc5..00000000 --- a/client/lib/presence_web.dart +++ /dev/null @@ -1,4 +0,0 @@ -import 'dart:js_interop'; - -@JS() -external bool isAnother(); diff --git a/client/lib/ui/account/account_selector.dart b/client/lib/ui/account/account_selector.dart new file mode 100644 index 00000000..9052a73c --- /dev/null +++ b/client/lib/ui/account/account_selector.dart @@ -0,0 +1,62 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_state_notifier/flutter_state_notifier.dart'; +import 'package:offtheline/offtheline.dart'; +import 'package:phylum/libphylum/phylum_account.dart'; +import 'package:phylum/ui/account/login_page.dart'; +import 'package:phylum/ui/app/app.dart'; +import 'package:phylum/ui/account/account_selector_app.dart'; +import 'package:provider/provider.dart'; + +class AccountSelector extends StatefulWidget { + const AccountSelector({super.key}); + + @override + State createState() => AccountSelectorState(); +} + +class AccountSelectorState extends State { + AccountManager? _accountManager; + bool _errorsDismissed = false; + + @override + void initState() { + AccountManager.restore((id) async { + final account = PhylumAccount.restore(id: id); + final err = await account.initialized; + if (err != null) { + throw err; + } + return account; + }).then((accountManager) { + setState(() { + _accountManager = accountManager; + }); + }); + + super.initState(); + } + + @override + Widget build(BuildContext context) { + final accountManager = _accountManager; + if (accountManager == null) { + return Placeholder(); + } + return StateNotifierProvider, AccountManagerState>.value( + value: accountManager, + builder: (context, child) { + if (!_errorsDismissed) {} + final account = + context.select, PhylumAccount?>((state) => state.selectedAccount); + + if (account == null) { + return const AccountSelectorApp(key: ValueKey('login'), child: LoginPage()); + } + + return PhylumApp.create(account); + }); + } + + Future closeAccountManager() => _accountManager?.close() ?? SynchronousFuture(null); +} diff --git a/client/lib/ui/login/login_app.dart b/client/lib/ui/account/account_selector_app.dart similarity index 57% rename from client/lib/ui/login/login_app.dart rename to client/lib/ui/account/account_selector_app.dart index 422d5971..b8905d18 100644 --- a/client/lib/ui/login/login_app.dart +++ b/client/lib/ui/account/account_selector_app.dart @@ -2,17 +2,16 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:uri/uri.dart'; -import 'login_page.dart'; - -class LoginApp extends StatefulWidget { - const LoginApp({super.key}); +class AccountSelectorApp extends StatefulWidget { + final Widget child; + const AccountSelectorApp({super.key, required this.child}); @override - State createState() => _LoginAppState(); + State createState() => _AccountSelectorAppState(); } -class _LoginAppState extends State { - final delegate = LoginRouterDelegate(); +class _AccountSelectorAppState extends State { + late final delegate = AccountSelectorRouterDelegate(child: widget.child); @override Widget build(BuildContext context) { @@ -27,7 +26,7 @@ class _LoginAppState extends State { brightness: Brightness.dark, ); return MaterialApp.router( - title: 'Login | Phylum', + title: 'Phylum', debugShowCheckedModeBanner: false, theme: theme, darkTheme: darkTheme, @@ -37,28 +36,28 @@ class _LoginAppState extends State { } } -class LoginRouteConfiguration { +class AccountSelectorRouteConfiguration { final String? next; - LoginRouteConfiguration(this.next); + AccountSelectorRouteConfiguration(this.next); } -class LoginRouteInformationParser extends RouteInformationParser { +class LoginRouteInformationParser extends RouteInformationParser { const LoginRouteInformationParser(); @override - Future parseRouteInformation(RouteInformation routeInformation) { + Future parseRouteInformation(RouteInformation routeInformation) { final uri = routeInformation.uri; final segments = uri.pathSegments; final next = (segments.isEmpty || (segments.length == 1 && (segments[0] == 'home' || segments[0] == 'login'))) ? null : uri.path; - return SynchronousFuture(LoginRouteConfiguration(next)); + return SynchronousFuture(AccountSelectorRouteConfiguration(next)); } @override - RouteInformation? restoreRouteInformation(LoginRouteConfiguration configuration) { + RouteInformation? restoreRouteInformation(AccountSelectorRouteConfiguration configuration) { final b = UriBuilder()..path = '/login'; if (configuration.next != null) { b.queryParameters['next'] = configuration.next!; @@ -68,27 +67,28 @@ class LoginRouteInformationParser extends RouteInformationParser - with ChangeNotifier, PopNavigatorRouterDelegateMixin { +class AccountSelectorRouterDelegate extends RouterDelegate + with ChangeNotifier, PopNavigatorRouterDelegateMixin { + final Widget child; @override final navigatorKey = GlobalKey(); - LoginRouterDelegate(); + AccountSelectorRouterDelegate({required this.child}); - LoginRouteConfiguration? _currentConfiguration; + AccountSelectorRouteConfiguration? _currentConfiguration; @override - LoginRouteConfiguration? get currentConfiguration => _currentConfiguration; + AccountSelectorRouteConfiguration? get currentConfiguration => _currentConfiguration; @override Widget build(BuildContext context) { return Navigator( key: navigatorKey, - onGenerateRoute: (settings) => MaterialPageRoute(builder: (context) => const LoginPage()), + onGenerateRoute: (settings) => MaterialPageRoute(builder: (context) => child), ); } @override - Future setNewRoutePath(LoginRouteConfiguration configuration) { + Future setNewRoutePath(AccountSelectorRouteConfiguration configuration) { _currentConfiguration = configuration; notifyListeners(); return SynchronousFuture(null); diff --git a/client/lib/ui/login/login_page.dart b/client/lib/ui/account/login_page.dart similarity index 100% rename from client/lib/ui/login/login_page.dart rename to client/lib/ui/account/login_page.dart diff --git a/client/lib/ui/logout/logout_page.dart b/client/lib/ui/account/logout_page.dart similarity index 93% rename from client/lib/ui/logout/logout_page.dart rename to client/lib/ui/account/logout_page.dart index 4ae3d73e..fad69939 100644 --- a/client/lib/ui/logout/logout_page.dart +++ b/client/lib/ui/account/logout_page.dart @@ -17,7 +17,7 @@ class LogoutPage extends StatelessWidget { child: Card( child: Padding( padding: const EdgeInsets.all(16.0), - child: state == AccountState.loggedOut ? buildLoggedOutView(context) : buildLoggingOutView(), + child: state == AccountState.closed ? buildLoggedOutView(context) : buildLoggingOutView(), ), ), ), diff --git a/client/lib/ui/account/presence_stub.dart b/client/lib/ui/account/presence_stub.dart new file mode 100644 index 00000000..5a36924d --- /dev/null +++ b/client/lib/ui/account/presence_stub.dart @@ -0,0 +1,4 @@ +import 'package:flutter/material.dart'; +import 'package:phylum/ui/account/account_selector.dart'; + +Widget buildApp() => AccountSelector(); diff --git a/client/lib/ui/account/presence_web.dart b/client/lib/ui/account/presence_web.dart new file mode 100644 index 00000000..3dfca65c --- /dev/null +++ b/client/lib/ui/account/presence_web.dart @@ -0,0 +1,93 @@ +import 'dart:js_interop'; + +import 'package:flutter/material.dart'; +import 'package:phylum/ui/account/account_selector.dart'; +import 'package:web/web.dart' as web; + +@JS() +external bool isPrimary(); + +@JS() +external void setPrimary(JSBoolean value); + +Widget buildApp() => const PrimaryNegotiator(); + +class PrimaryNegotiator extends StatefulWidget { + const PrimaryNegotiator({super.key}); + + @override + State createState() => _PrimaryNegotiatorState(); +} + +class _PrimaryNegotiatorState extends State { + final key = GlobalKey(); + final channel = web.BroadcastChannel('takover'); + bool primary = isPrimary(); + + @override + void initState() { + super.initState(); + channel.onmessage = processMessage.toJS; + } + + @override + Widget build(BuildContext context) { + if (primary) { + return AccountSelector(key: key); + } + + return Directionality( + textDirection: TextDirection.ltr, + child: Center( + child: Card( + child: Padding( + padding: EdgeInsets.all(16.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Text('Phylum is running in another tab or window.'), + Align( + alignment: Alignment.bottomRight, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + ElevatedButton( + child: Text('Use here'), + onPressed: () { + channel.postMessage('close'.toJS); + }, + ) + ], + ), + ) + ], + ), + ), + ), + ), + ); + } + + void processMessage(web.MessageEvent event) { + final msg = event.data.toString(); + if (msg == 'close') { + final state = key.currentState; + if (state != null) { + channel.postMessage('closing'.toJS); + state.closeAccountManager().then((v) { + channel.postMessage('closed'.toJS); + setPrimary(false.toJS); + setState(() { + primary = false; + }); + }); + } + } + if (msg == 'closed') { + setPrimary(true.toJS); + setState(() { + primary = true; + }); + } + } +} diff --git a/client/lib/ui/app/app.dart b/client/lib/ui/app/app.dart index c433ca63..fe2cc2a9 100644 --- a/client/lib/ui/app/app.dart +++ b/client/lib/ui/app/app.dart @@ -3,12 +3,13 @@ import 'package:flutter/material.dart'; import 'package:flutter_state_notifier/flutter_state_notifier.dart'; import 'package:offtheline/offtheline.dart'; import 'package:phylum/integrations/download_manager.dart'; +import 'package:phylum/ui/account/account_selector_app.dart'; +import 'package:phylum/ui/account/logout_page.dart'; import 'package:phylum/ui/app/app_shortcuts.dart'; import 'package:phylum/ui/app/router.dart'; import 'package:phylum/libphylum/phylum_account.dart'; import 'package:phylum/libphylum/phylum_api_types.dart'; import 'package:phylum/ui/app/action_queue_status_notifier.dart'; -import 'package:phylum/ui/logout/logout_app.dart'; import 'package:provider/provider.dart'; class PhylumApp extends StatefulWidget { @@ -55,7 +56,6 @@ class _PhylumAppState extends State { @override Widget build(BuildContext context) { - final ready = context.select((state) => state == AccountState.ready); final theme = ThemeData( colorScheme: ColorScheme.fromSeed( seedColor: Color(0xff769e57), @@ -66,18 +66,21 @@ class _PhylumAppState extends State { colorScheme: theme.colorScheme.copyWith(brightness: Brightness.dark), brightness: Brightness.dark, ); - if (ready) { - return MaterialApp.router( - key: ValueKey('ready'), - title: 'Phylum', - debugShowCheckedModeBanner: false, - theme: theme, - darkTheme: darkTheme, - routeInformationParser: _routeInformationParser, - routerDelegate: _routerDelegate, - shortcuts: appShortcuts, - ); + + final ready = context.select((state) => state == AccountState.ready); + + if (!ready) { + return const AccountSelectorApp(key: ValueKey('logout'), child: LogoutPage()); } - return const LogoutApp(key: ValueKey('logout')); + return MaterialApp.router( + key: ValueKey('ready'), + title: 'Phylum', + debugShowCheckedModeBanner: false, + theme: theme, + darkTheme: darkTheme, + routeInformationParser: _routeInformationParser, + routerDelegate: _routerDelegate, + shortcuts: appShortcuts, + ); } } diff --git a/client/lib/ui/app/nav_list.dart b/client/lib/ui/app/nav_list.dart index 5bc543a8..b0391c4c 100644 --- a/client/lib/ui/app/nav_list.dart +++ b/client/lib/ui/app/nav_list.dart @@ -174,7 +174,7 @@ class NavList extends StatelessWidget { leading: const Icon(Icons.logout), title: const Text('Log Out'), onTap: () { - context.read().logOut(); + context.read().close(clearData: true); }, ), ], diff --git a/client/lib/ui/logout/logout_app.dart b/client/lib/ui/logout/logout_app.dart deleted file mode 100644 index 805b99c8..00000000 --- a/client/lib/ui/logout/logout_app.dart +++ /dev/null @@ -1,81 +0,0 @@ -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:phylum/ui/logout/logout_page.dart'; - -class LogoutApp extends StatefulWidget { - const LogoutApp({super.key}); - - @override - State createState() => _LoginAppState(); -} - -class _LoginAppState extends State { - final delegate = LogoutRouterDelegate(); - - @override - Widget build(BuildContext context) { - final theme = ThemeData( - colorScheme: ColorScheme.fromSeed( - seedColor: Color(0xff769e57), - dynamicSchemeVariant: DynamicSchemeVariant.neutral, - ), - ); - final darkTheme = ThemeData( - colorScheme: theme.colorScheme.copyWith(brightness: Brightness.dark), - brightness: Brightness.dark, - ); - return MaterialApp.router( - title: 'Login | Phylum', - debugShowCheckedModeBanner: false, - theme: theme, - darkTheme: darkTheme, - routeInformationParser: const LogoutRouteInformationParser(), - routerDelegate: delegate, - ); - } -} - -class LogoutRouteConfiguration { - const LogoutRouteConfiguration(); -} - -class LogoutRouteInformationParser extends RouteInformationParser { - const LogoutRouteInformationParser(); - - @override - Future parseRouteInformation(RouteInformation routeInformation) { - return SynchronousFuture(const LogoutRouteConfiguration()); - } - - @override - RouteInformation? restoreRouteInformation(LogoutRouteConfiguration configuration) { - return RouteInformation(uri: Uri(path: '/logout')); - } -} - -class LogoutRouterDelegate extends RouterDelegate - with ChangeNotifier, PopNavigatorRouterDelegateMixin { - @override - final navigatorKey = GlobalKey(); - - LogoutRouterDelegate(); - - LogoutRouteConfiguration? _currentConfiguration; - @override - LogoutRouteConfiguration? get currentConfiguration => _currentConfiguration; - - @override - Widget build(BuildContext context) { - return Navigator( - key: navigatorKey, - onGenerateRoute: (settings) => MaterialPageRoute(builder: (context) => const LogoutPage()), - ); - } - - @override - Future setNewRoutePath(LogoutRouteConfiguration configuration) { - _currentConfiguration = configuration; - notifyListeners(); - return SynchronousFuture(null); - } -} diff --git a/client/pubspec.lock b/client/pubspec.lock index f85ab552..184cc25d 100644 --- a/client/pubspec.lock +++ b/client/pubspec.lock @@ -478,10 +478,10 @@ packages: dependency: "direct main" description: name: hive_ce - sha256: fdc19336f03ecd01dbc1d1afe69d87ed9336bdf996c5374a25f9c21ef5f2989e + sha256: "192b7334299e3672efa1f85d544fef0091c5c592be5caada61417e37723addc6" url: "https://pub.dev" source: hosted - version: "2.11.1" + version: "2.11.2" http: dependency: "direct main" description: @@ -662,8 +662,8 @@ packages: dependency: "direct main" description: path: "." - ref: "2a01d7ce8e8c0964a133c74b2b019d66fd611a69" - resolved-ref: "2a01d7ce8e8c0964a133c74b2b019d66fd611a69" + ref: "2365704ff865d530169124600b4753f51656e815" + resolved-ref: "2365704ff865d530169124600b4753f51656e815" url: "https://codeberg.org/shroff/offtheline.git" source: git version: "0.16.0" diff --git a/client/pubspec.yaml b/client/pubspec.yaml index 85770040..f213576a 100644 --- a/client/pubspec.yaml +++ b/client/pubspec.yaml @@ -4,7 +4,7 @@ publish_to: 'none' version: 0.3.0+30 environment: - sdk: ^3.5.0 + sdk: ^3.6.0 dependencies: flutter: @@ -27,7 +27,7 @@ dependencies: offtheline: git: url: https://codeberg.org/shroff/offtheline.git - ref: 2a01d7ce8e8c0964a133c74b2b019d66fd611a69 + ref: 2365704ff865d530169124600b4753f51656e815 open_file: package_info_plus: path: @@ -68,4 +68,4 @@ dependency_overrides: git: url: https://github.com/moshe5745/flutter_keyboard_visibility.git ref: b8393c50c483fa6fd16148d43dff454e6e845201 - path: flutter_keyboard_visibility_web + path: flutter_keyboard_visibility_web \ No newline at end of file diff --git a/client/web/index.html b/client/web/index.html index 396a20c9..bd31e7b9 100644 --- a/client/web/index.html +++ b/client/web/index.html @@ -36,18 +36,24 @@