From c00b2b8cac93c2bacf4b8d80fd3c8a0d7c3bce08 Mon Sep 17 00:00:00 2001 From: Nikolas Rimikis Date: Thu, 6 Jul 2023 08:37:20 +0200 Subject: [PATCH] neon: use go_router for the main app page --- packages/neon/neon/lib/src/app.dart | 10 +- .../neon/neon/lib/src/blocs/accounts.dart | 24 ++++- packages/neon/neon/lib/src/blocs/apps.dart | 8 ++ .../neon/lib/src/pages/account_settings.dart | 14 --- packages/neon/neon/lib/src/pages/home.dart | 41 ++++---- packages/neon/neon/lib/src/pages/login.dart | 5 - .../neon/neon/lib/src/pages/settings.dart | 12 +-- packages/neon/neon/lib/src/router.dart | 73 ++++++++------ packages/neon/neon/lib/src/router.g.dart | 98 +++++++++---------- .../neon/lib/src/utils/global_popups.dart | 4 +- .../neon/neon/lib/src/widgets/app_bar.dart | 4 +- .../neon/neon/lib/src/widgets/drawer.dart | 4 +- 12 files changed, 152 insertions(+), 145 deletions(-) diff --git a/packages/neon/neon/lib/src/app.dart b/packages/neon/neon/lib/src/app.dart index 1bf5e11e..4ae60241 100644 --- a/packages/neon/neon/lib/src/app.dart +++ b/packages/neon/neon/lib/src/app.dart @@ -37,15 +37,12 @@ class NeonApp extends StatefulWidget { // ignore: prefer_mixin class _NeonAppState extends State with WidgetsBindingObserver, tray.TrayListener, WindowListener { final _appRegex = RegExp(r'^app_([a-z]+)$', multiLine: true); - final _navigatorKey = GlobalKey(); + late final GlobalKey _navigatorKey; late final Iterable _appImplementations; late final NeonPlatform _platform; late final GlobalOptions _globalOptions; late final AccountsBloc _accountsBloc; - late final _routerDelegate = AppRouter( - navigatorKey: _navigatorKey, - accountsBloc: _accountsBloc, - ); + late final AppRouter _routerDelegate; Rect? _lastBounds; @@ -58,6 +55,9 @@ class _NeonAppState extends State with WidgetsBindingObserver, tray.Tra _globalOptions = Provider.of(context, listen: false); _accountsBloc = Provider.of(context, listen: false); + _routerDelegate = _accountsBloc.router; + _navigatorKey = _routerDelegate.navigatorKey; + WidgetsBinding.instance.addObserver(this); if (_platform.canUseSystemTray) { tray.trayManager.addListener(this); diff --git a/packages/neon/neon/lib/src/blocs/accounts.dart b/packages/neon/neon/lib/src/blocs/accounts.dart index c3a5ef28..07e72745 100644 --- a/packages/neon/neon/lib/src/blocs/accounts.dart +++ b/packages/neon/neon/lib/src/blocs/accounts.dart @@ -1,7 +1,7 @@ import 'dart:async'; import 'dart:convert'; -import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; import 'package:neon/src/bloc/bloc.dart'; import 'package:neon/src/blocs/apps.dart'; import 'package:neon/src/blocs/capabilities.dart'; @@ -10,6 +10,7 @@ import 'package:neon/src/blocs/user_statuses.dart'; import 'package:neon/src/models/account.dart'; import 'package:neon/src/models/app_implementation.dart'; import 'package:neon/src/platform/platform.dart'; +import 'package:neon/src/router.dart'; import 'package:neon/src/settings/models/storage.dart'; import 'package:neon/src/utils/account_options.dart'; import 'package:neon/src/utils/global_options.dart'; @@ -63,6 +64,11 @@ class AccountsBloc extends Bloc implements AccountsBlocEvents, AccountsBlocState this._globalOptions, this._allAppImplementations, ) { + router = AppRouter( + navigatorKey: GlobalKey(), + accountsBloc: this, + appImplementations: _allAppImplementations, + ); accounts ..add(loadAccounts(_storage)) ..listen((final as) async { @@ -109,6 +115,7 @@ class AccountsBloc extends Bloc implements AccountsBlocEvents, AccountsBlocState final SharedPreferences _sharedPreferences; final GlobalOptions _globalOptions; final Iterable _allAppImplementations; + late final AppRouter router; final _keyLastUsedAccount = 'last-used-account'; final _accountsOptions = {}; @@ -134,8 +141,7 @@ class AccountsBloc extends Bloc implements AccountsBlocEvents, AccountsBlocState BehaviorSubject> accounts = BehaviorSubject>.seeded([]); @override - BehaviorSubject activeAccount = BehaviorSubject.seeded(null) - ..distinct((final current, final next) => current?.id != next?.id); + BehaviorSubject activeAccount = BehaviorSubject.seeded(null); @override void addAccount(final Account account) { @@ -154,6 +160,9 @@ class AccountsBloc extends Bloc implements AccountsBlocEvents, AccountsBlocState if (aa?.id == account.id) { if (as.firstOrNull != null) { setActiveAccount(as.first); + + final activeApp = getAppsBlocFor(as.first).activeApp.value.initialRouteName; + router.goNamed(activeApp); } else { activeAccount.add(null); } @@ -171,7 +180,13 @@ class AccountsBloc extends Bloc implements AccountsBlocEvents, AccountsBlocState @override void setActiveAccount(final Account account) { - activeAccount.add(account); + if (activeAccount.valueOrNull?.id != account.id) { + if (activeAccount.valueOrNull == null) { + router.go(const HomeRoute().location); + } + + activeAccount.add(account); + } } @override @@ -240,6 +255,7 @@ class AccountsBloc extends Bloc implements AccountsBlocEvents, AccountsBlocState this, account, _allAppImplementations, + router, ); } diff --git a/packages/neon/neon/lib/src/blocs/apps.dart b/packages/neon/neon/lib/src/blocs/apps.dart index c70d7a79..12381c1f 100644 --- a/packages/neon/neon/lib/src/blocs/apps.dart +++ b/packages/neon/neon/lib/src/blocs/apps.dart @@ -9,6 +9,7 @@ import 'package:neon/src/blocs/capabilities.dart'; import 'package:neon/src/models/account.dart'; import 'package:neon/src/models/app_implementation.dart'; import 'package:neon/src/models/notifications_interface.dart'; +import 'package:neon/src/router.dart'; import 'package:neon/src/settings/models/nextcloud_app_options.dart'; import 'package:neon/src/utils/request_manager.dart'; import 'package:nextcloud/nextcloud.dart'; @@ -43,6 +44,7 @@ class AppsBloc extends InteractiveBloc implements AppsBlocEvents, AppsBlocStates this._accountsBloc, this._account, this._allAppImplementations, + this._router, ) { apps.listen((final result) { appImplementations @@ -156,6 +158,7 @@ class AppsBloc extends InteractiveBloc implements AppsBlocEvents, AppsBlocStates final AccountsBloc _accountsBloc; final Account _account; final Iterable _allAppImplementations; + final AppRouter _router; @override void dispose() { @@ -216,6 +219,11 @@ class AppsBloc extends InteractiveBloc implements AppsBlocEvents, AppsBlocStates if (app != null) { if (activeApp.valueOrNull?.id != appID) { activeApp.add(app); + + // avoid inactive accounts calling the navigator with a potentially unsupported app. + if (_accountsBloc.activeAccount.value?.id == _account.id) { + _router.goNamed(app.initialRouteName); + } } } else { throw Exception('App $appID not found'); diff --git a/packages/neon/neon/lib/src/pages/account_settings.dart b/packages/neon/neon/lib/src/pages/account_settings.dart index daebb0fc..f4dd2c01 100644 --- a/packages/neon/neon/lib/src/pages/account_settings.dart +++ b/packages/neon/neon/lib/src/pages/account_settings.dart @@ -5,7 +5,6 @@ import 'package:neon/l10n/localizations.dart'; import 'package:neon/src/bloc/result_builder.dart'; import 'package:neon/src/blocs/accounts.dart'; import 'package:neon/src/models/account.dart'; -import 'package:neon/src/router.dart'; import 'package:neon/src/settings/widgets/custom_settings_tile.dart'; import 'package:neon/src/settings/widgets/dropdown_button_settings_tile.dart'; import 'package:neon/src/settings/widgets/settings_category.dart'; @@ -41,20 +40,7 @@ class AccountSettingsPage extends StatelessWidget { context, AppLocalizations.of(context).accountOptionsRemoveConfirm(account.client.humanReadableID), )) { - final isActive = bloc.activeAccount.value == account; - bloc.removeAccount(account); - - // ignore: use_build_context_synchronously - if (!context.mounted) { - return; - } - - if (isActive) { - const HomeRoute().go(context); - } else { - Navigator.of(context).pop(); - } } }, tooltip: AppLocalizations.of(context).accountOptionsRemove, diff --git a/packages/neon/neon/lib/src/pages/home.dart b/packages/neon/neon/lib/src/pages/home.dart index db3c6419..7bf2f383 100644 --- a/packages/neon/neon/lib/src/pages/home.dart +++ b/packages/neon/neon/lib/src/pages/home.dart @@ -21,9 +21,12 @@ const kQuickBarWidth = kAvatarSize + 20; class HomePage extends StatefulWidget { const HomePage({ + this.appView, super.key, }); + final Widget? appView; + @override State createState() => _HomePageState(); } @@ -121,34 +124,26 @@ class _HomePageState extends State { const drawer = NeonDrawer(); const appBar = NeonAppBar(); - final appView = ResultBuilder>.behaviorSubject( - stream: _appsBloc.appImplementations, - builder: (final context, final appImplementations) { - if (!appImplementations.hasData) { - return const SizedBox(); - } - - if (appImplementations.requireData.isEmpty) { - return Center( - child: Text( - AppLocalizations.of(context).errorNoCompatibleNextcloudAppsFound, - textAlign: TextAlign.center, - ), - ); - } - - return StreamBuilder( - stream: _appsBloc.activeApp, - builder: (final context, final activeAppIDSnapshot) { - if (!activeAppIDSnapshot.hasData) { + final appView = widget.appView ?? + ResultBuilder>.behaviorSubject( + stream: _appsBloc.appImplementations, + builder: (final context, final appImplementations) { + if (!appImplementations.hasData) { return const SizedBox(); } - return activeAppIDSnapshot.requireData.page; + if (appImplementations.requireData.isEmpty) { + return Center( + child: Text( + AppLocalizations.of(context).errorNoCompatibleNextcloudAppsFound, + textAlign: TextAlign.center, + ), + ); + } + + return const SizedBox(); }, ); - }, - ); final body = OptionBuilder( option: _globalOptions.navigationMode, diff --git a/packages/neon/neon/lib/src/pages/login.dart b/packages/neon/neon/lib/src/pages/login.dart index 637b622b..53c38d48 100644 --- a/packages/neon/neon/lib/src/pages/login.dart +++ b/packages/neon/neon/lib/src/pages/login.dart @@ -5,7 +5,6 @@ import 'package:neon/src/blocs/login.dart'; import 'package:neon/src/models/account.dart'; import 'package:neon/src/models/branding.dart'; import 'package:neon/src/platform/platform.dart'; -import 'package:neon/src/router.dart'; import 'package:neon/src/utils/validators.dart'; import 'package:neon/src/widgets/exception.dart'; import 'package:neon/src/widgets/linear_progress_indicator.dart'; @@ -99,10 +98,6 @@ class _LoginPageState extends State { ..addAccount(account) ..setActiveAccount(account); } - - if (mounted) { - const HomeRoute().go(context); - } } catch (e, s) { debugPrint(e.toString()); debugPrint(s.toString()); diff --git a/packages/neon/neon/lib/src/pages/settings.dart b/packages/neon/neon/lib/src/pages/settings.dart index fa82ec24..9a5f088a 100644 --- a/packages/neon/neon/lib/src/pages/settings.dart +++ b/packages/neon/neon/lib/src/pages/settings.dart @@ -106,9 +106,7 @@ class _SettingsPageState extends State { CustomSettingsTile( leading: appImplementation.buildIcon(), title: Text(appImplementation.name(context)), - onTap: () { - NextcloudAppSettingsRoute(appid: appImplementation.id).go(context); - }, + onTap: () async => NextcloudAppSettingsRoute(appid: appImplementation.id).push(context), ), ], ], @@ -201,16 +199,12 @@ class _SettingsPageState extends State { for (final account in accountsSnapshot.requireData) ...[ AccountSettingsTile( account: account, - onTap: () { - AccountSettingsRoute(accountid: account.id).go(context); - }, + onTap: () async => AccountSettingsRoute(accountid: account.id).push(context), ), ], CustomSettingsTile( title: ElevatedButton.icon( - onPressed: () { - const AddAccountRoute().go(context); - }, + onPressed: () async => const AddAccountRoute().push(context), icon: Icon(MdiIcons.accountPlus), label: Text(AppLocalizations.of(context).globalOptionsAccountsAdd), ), diff --git a/packages/neon/neon/lib/src/router.dart b/packages/neon/neon/lib/src/router.dart index 663a3f1d..6b4fb59a 100644 --- a/packages/neon/neon/lib/src/router.dart +++ b/packages/neon/neon/lib/src/router.dart @@ -18,8 +18,9 @@ part 'router.g.dart'; @internal class AppRouter extends GoRouter { AppRouter({ - required final GlobalKey navigatorKey, + required this.navigatorKey, required final AccountsBloc accountsBloc, + required final Iterable appImplementations, }) : super( debugLogDiagnostics: kDebugMode, refreshListenable: StreamListenable(accountsBloc.activeAccount), @@ -35,8 +36,30 @@ class AppRouter extends GoRouter { return null; }, - routes: $appRoutes, + routes: [ + StatefulShellRoute.indexedStack( + branches: appImplementations.map((final a) => a.mainBranch).toList(), + builder: _mainViewBuilder, + ), + ...$appRoutes, + ], ); + + final GlobalKey navigatorKey; +} + +Widget _mainViewBuilder( + final BuildContext context, + final GoRouterState state, [ + final StatefulNavigationShell? navigationShell, +]) { + final accountsBloc = Provider.of(context, listen: false); + final account = accountsBloc.activeAccount.valueOrNull!; + + return HomePage( + appView: navigationShell, + key: Key(account.id), + ); } @immutable @@ -62,38 +85,14 @@ class AccountSettingsRoute extends GoRouteData { @TypedGoRoute( path: '/', name: 'home', - routes: [ - TypedGoRoute( - path: 'settings', - name: 'Settings', - routes: [ - TypedGoRoute( - path: 'apps/:appid', - name: 'NextcloudAppSettings', - ), - TypedGoRoute( - path: 'account/add', - name: 'addAccount', - ), - TypedGoRoute( - path: 'account/:accountid', - name: 'AccountSettings', - ), - ], - ) - ], ) @immutable +@internal class HomeRoute extends GoRouteData { const HomeRoute(); @override - Widget build(final BuildContext context, final GoRouterState state) { - final accountsBloc = Provider.of(context, listen: false); - final account = accountsBloc.activeAccount.valueOrNull!; - - return HomePage(key: Key(account.id)); - } + Widget build(final BuildContext context, final GoRouterState state) => _mainViewBuilder(context, state); } @TypedGoRoute( @@ -135,6 +134,24 @@ class NextcloudAppSettingsRoute extends GoRouteData { } } +@TypedGoRoute( + path: '/settings', + name: 'Settings', + routes: [ + TypedGoRoute( + path: 'apps/:appid', + name: 'NextcloudAppSettings', + ), + TypedGoRoute( + path: 'account/add', + name: 'addAccount', + ), + TypedGoRoute( + path: 'account/:accountid', + name: 'AccountSettings', + ), + ], +) @immutable class SettingsRoute extends GoRouteData { const SettingsRoute(); diff --git a/packages/neon/neon/lib/src/router.g.dart b/packages/neon/neon/lib/src/router.g.dart index 8a9d98dc..fa27ab78 100644 --- a/packages/neon/neon/lib/src/router.g.dart +++ b/packages/neon/neon/lib/src/router.g.dart @@ -9,36 +9,13 @@ part of 'router.dart'; List get $appRoutes => [ $homeRoute, $loginRoute, + $settingsRoute, ]; RouteBase get $homeRoute => GoRouteData.$route( path: '/', name: 'home', factory: $HomeRouteExtension._fromState, - routes: [ - GoRouteData.$route( - path: 'settings', - name: 'Settings', - factory: $SettingsRouteExtension._fromState, - routes: [ - GoRouteData.$route( - path: 'apps/:appid', - name: 'NextcloudAppSettings', - factory: $NextcloudAppSettingsRouteExtension._fromState, - ), - GoRouteData.$route( - path: 'account/add', - name: 'addAccount', - factory: $AddAccountRouteExtension._fromState, - ), - GoRouteData.$route( - path: 'account/:accountid', - name: 'AccountSettings', - factory: $AccountSettingsRouteExtension._fromState, - ), - ], - ), - ], ); extension $HomeRouteExtension on HomeRoute { @@ -55,6 +32,54 @@ extension $HomeRouteExtension on HomeRoute { void pushReplacement(BuildContext context) => context.pushReplacement(location); } +RouteBase get $loginRoute => GoRouteData.$route( + path: '/login', + name: 'login', + factory: $LoginRouteExtension._fromState, + ); + +extension $LoginRouteExtension on LoginRoute { + static LoginRoute _fromState(GoRouterState state) => LoginRoute( + server: state.queryParameters['server'], + ); + + String get location => GoRouteData.$location( + '/login', + queryParams: { + if (server != null) 'server': server, + }, + ); + + void go(BuildContext context) => context.go(location); + + Future push(BuildContext context) => context.push(location); + + void pushReplacement(BuildContext context) => context.pushReplacement(location); +} + +RouteBase get $settingsRoute => GoRouteData.$route( + path: '/settings', + name: 'Settings', + factory: $SettingsRouteExtension._fromState, + routes: [ + GoRouteData.$route( + path: 'apps/:appid', + name: 'NextcloudAppSettings', + factory: $NextcloudAppSettingsRouteExtension._fromState, + ), + GoRouteData.$route( + path: 'account/add', + name: 'addAccount', + factory: $AddAccountRouteExtension._fromState, + ), + GoRouteData.$route( + path: 'account/:accountid', + name: 'AccountSettings', + factory: $AccountSettingsRouteExtension._fromState, + ), + ], + ); + extension $SettingsRouteExtension on SettingsRoute { static SettingsRoute _fromState(GoRouterState state) => const SettingsRoute(); @@ -114,28 +139,3 @@ extension $AccountSettingsRouteExtension on AccountSettingsRoute { void pushReplacement(BuildContext context) => context.pushReplacement(location); } - -RouteBase get $loginRoute => GoRouteData.$route( - path: '/login', - name: 'login', - factory: $LoginRouteExtension._fromState, - ); - -extension $LoginRouteExtension on LoginRoute { - static LoginRoute _fromState(GoRouterState state) => LoginRoute( - server: state.queryParameters['server'], - ); - - String get location => GoRouteData.$location( - '/login', - queryParams: { - if (server != null) 'server': server, - }, - ); - - void go(BuildContext context) => context.go(location); - - Future push(BuildContext context) => context.push(location); - - void pushReplacement(BuildContext context) => context.pushReplacement(location); -} diff --git a/packages/neon/neon/lib/src/utils/global_popups.dart b/packages/neon/neon/lib/src/utils/global_popups.dart index 05a732a7..a503c5a4 100644 --- a/packages/neon/neon/lib/src/utils/global_popups.dart +++ b/packages/neon/neon/lib/src/utils/global_popups.dart @@ -42,9 +42,7 @@ class GlobalPopups { content: Text(AppLocalizations.of(context).firstLaunchGoToSettingsToEnablePushNotifications), action: SnackBarAction( label: AppLocalizations.of(context).settings, - onPressed: () { - const SettingsRoute().go(context); - }, + onPressed: () => const SettingsRoute().push(context), ), ), ); diff --git a/packages/neon/neon/lib/src/widgets/app_bar.dart b/packages/neon/neon/lib/src/widgets/app_bar.dart index 383c85dc..1a80b2c6 100644 --- a/packages/neon/neon/lib/src/widgets/app_bar.dart +++ b/packages/neon/neon/lib/src/widgets/app_bar.dart @@ -80,9 +80,7 @@ class NeonAppBar extends StatelessWidget implements PreferredSizeWidget { actions: [ const NotificationIconButton(), IconButton( - onPressed: () { - AccountSettingsRoute(accountid: account.id).go(context); - }, + onPressed: () async => AccountSettingsRoute(accountid: account.id).push(context), tooltip: AppLocalizations.of(context).settingsAccount, icon: NeonUserAvatar( account: account, diff --git a/packages/neon/neon/lib/src/widgets/drawer.dart b/packages/neon/neon/lib/src/widgets/drawer.dart index 488cf416..a0923fb7 100644 --- a/packages/neon/neon/lib/src/widgets/drawer.dart +++ b/packages/neon/neon/lib/src/widgets/drawer.dart @@ -60,7 +60,7 @@ class __NeonDrawerState extends State<_NeonDrawer> with SingleTickerProviderStat late AppsBloc _appsBloc; late List _apps; - int _activeApp = 0; + late int _activeApp; @override void initState() { @@ -89,7 +89,7 @@ class __NeonDrawerState extends State<_NeonDrawer> with SingleTickerProviderStat // selected item is not a registered app like the SettingsPage if (index >= _apps.length) { - const SettingsRoute().go(context); + unawaited(const SettingsRoute().push(context)); return; }