diff --git a/packages/app/pubspec.lock b/packages/app/pubspec.lock index dfe31e2c..1d9dbc9d 100644 --- a/packages/app/pubspec.lock +++ b/packages/app/pubspec.lock @@ -317,6 +317,14 @@ packages: description: flutter source: sdk version: "0.0.0" + go_router: + dependency: transitive + description: + name: go_router + sha256: "00d1b67d6e9fa443331da229084dd3eb04407f5a2dff22940bd7bba6af5722c3" + url: "https://pub.dev" + source: hosted + version: "7.1.1" html: dependency: transitive description: @@ -434,6 +442,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.2" + logging: + dependency: transitive + description: + name: logging + sha256: "04094f2eb032cbb06c6f6e8d3607edcfcb0455e2bb6cbc010cb01171dcb64e6d" + url: "https://pub.dev" + source: hosted + version: "1.1.1" markdown: dependency: transitive description: diff --git a/packages/neon/neon/lib/neon.dart b/packages/neon/neon/lib/neon.dart index da10472b..b30ad6bb 100644 --- a/packages/neon/neon/lib/neon.dart +++ b/packages/neon/neon/lib/neon.dart @@ -23,6 +23,7 @@ import 'package:material_design_icons_flutter/material_design_icons_flutter.dart import 'package:neon/l10n/localizations.dart'; import 'package:neon/src/models/account.dart'; import 'package:neon/src/models/push_notification.dart'; +import 'package:neon/src/router.dart'; import 'package:nextcloud/nextcloud.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:path/path.dart' as p; @@ -67,7 +68,6 @@ part 'src/platform/abstract.dart'; part 'src/platform/android.dart'; part 'src/platform/linux.dart'; part 'src/platform/platform.dart'; -part 'src/router.dart'; part 'src/utils/account_options.dart'; part 'src/utils/app_implementation.dart'; part 'src/utils/bloc.dart'; @@ -89,6 +89,7 @@ part 'src/utils/settings_export_helper.dart'; part 'src/utils/sort_box_builder.dart'; part 'src/utils/sort_box_order_option_values.dart'; part 'src/utils/storage.dart'; +part 'src/utils/stream_listenable.dart'; part 'src/utils/theme.dart'; part 'src/utils/validators.dart'; part 'src/widgets/account_avatar.dart'; diff --git a/packages/neon/neon/lib/src/app.dart b/packages/neon/neon/lib/src/app.dart index d7d01963..b4687d30 100644 --- a/packages/neon/neon/lib/src/app.dart +++ b/packages/neon/neon/lib/src/app.dart @@ -295,7 +295,7 @@ class _NeonAppState extends State with WidgetsBindingObserver, tray.Tra keepOriginalAccentColor: nextcloudTheme == null || (themeKeepOriginalAccentColor ?? false), oledAsDark: themeOLEDAsDark, ), - routerDelegate: _routerDelegate, + routerConfig: _routerDelegate, ); }, ); diff --git a/packages/neon/neon/lib/src/pages/account_settings.dart b/packages/neon/neon/lib/src/pages/account_settings.dart index 69e8bd52..11813dbb 100644 --- a/packages/neon/neon/lib/src/pages/account_settings.dart +++ b/packages/neon/neon/lib/src/pages/account_settings.dart @@ -29,8 +29,6 @@ class AccountSettingsPage extends StatelessWidget { AppLocalizations.of(context).accountOptionsRemoveConfirm(account.client.humanReadableID), )) { bloc.removeAccount(account); - // ignore: use_build_context_synchronously - 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 b672184a..38ce0375 100644 --- a/packages/neon/neon/lib/src/pages/home.dart +++ b/packages/neon/neon/lib/src/pages/home.dart @@ -4,12 +4,9 @@ const kQuickBarWidth = kAvatarSize + 20; class HomePage extends StatefulWidget { const HomePage({ - required this.account, super.key, }); - final Account account; - @override State createState() => _HomePageState(); } @@ -19,6 +16,7 @@ class _HomePageState extends State { final _scaffoldKey = GlobalKey(); final drawerScrollController = ScrollController(); + late Account _account; late GlobalOptions _globalOptions; late AccountsBloc _accountsBloc; late AppsBloc _appsBloc; @@ -27,11 +25,11 @@ class _HomePageState extends State { @override void initState() { super.initState(); - _globalOptions = Provider.of(context, listen: false); _accountsBloc = Provider.of(context, listen: false); - _appsBloc = _accountsBloc.getAppsBloc(widget.account); - _capabilitiesBloc = _accountsBloc.getCapabilitiesBloc(widget.account); + _account = _accountsBloc.activeAccount.value!; + _appsBloc = _accountsBloc.getAppsBloc(_account); + _capabilitiesBloc = _accountsBloc.getCapabilitiesBloc(_account); _appsBloc.openNotifications.listen((final _) async { final notificationsAppImplementation = _appsBloc.notificationsAppImplementation.valueOrNull; @@ -61,9 +59,9 @@ class _HomePageState extends State { ]) { try { final (supported, _) = switch (id) { - 'core' => await widget.account.client.core.isSupported(result.data), - 'news' => await widget.account.client.news.isSupported(), - 'notes' => await widget.account.client.notes.isSupported(result.data), + 'core' => await _account.client.core.isSupported(result.data), + 'news' => await _account.client.news.isSupported(), + 'notes' => await _account.client.notes.isSupported(result.data), _ => (true, null), }; if (supported || !mounted) { @@ -92,7 +90,7 @@ class _HomePageState extends State { Future _checkMaintenanceMode() async { try { - final status = await widget.account.client.core.getStatus(); + final status = await _account.client.core.getStatus(); if (status.maintenance) { if (!mounted) { return; @@ -129,14 +127,6 @@ class _HomePageState extends State { ); } - Future _openSettings() async { - await Navigator.of(context).push( - MaterialPageRoute( - builder: (final context) => const SettingsPage(), - ), - ); - } - Future _openNotifications( final NotificationsAppInterface app, final List accounts, @@ -207,381 +197,378 @@ class _HomePageState extends State { builder: (final context) { if (accountsSnapshot.hasData) { final accounts = accountsSnapshot.data!; - final account = accounts.find(widget.account.id)!; - - final isQuickBar = navigationMode == NavigationMode.quickBar; - final drawer = Drawer( - width: isQuickBar ? kQuickBarWidth : null, - child: Container( - padding: isQuickBar ? const EdgeInsets.all(5) : null, - child: Column( - children: [ - Expanded( - child: Scrollbar( - controller: drawerScrollController, - interactive: true, - child: ListView( - controller: drawerScrollController, - // Needed for the drawer header to also render in the statusbar - padding: EdgeInsets.zero, - children: [ - Builder( - builder: (final context) { - if (accountsSnapshot.hasData) { - if (isQuickBar) { - return Column( - children: [ - if (accounts.length != 1) ...[ - for (final account in accounts) ...[ - Container( - margin: const EdgeInsets.symmetric( - vertical: 5, - ), - child: IconButton( - onPressed: () { - _accountsBloc.setActiveAccount(account); - }, - tooltip: account.client.humanReadableID, - icon: IntrinsicHeight( - child: NeonAccountAvatar( - account: account, + final account = accounts.find(_account.id); + if (account != null) { + final isQuickBar = navigationMode == NavigationMode.quickBar; + final drawer = Builder( + builder: (final context) => Drawer( + width: isQuickBar ? kQuickBarWidth : null, + child: Container( + padding: isQuickBar ? const EdgeInsets.all(5) : null, + child: Column( + children: [ + Expanded( + child: Scrollbar( + controller: drawerScrollController, + interactive: true, + child: ListView( + controller: drawerScrollController, + // Needed for the drawer header to also render in the statusbar + padding: EdgeInsets.zero, + children: [ + Builder( + builder: (final context) { + if (accountsSnapshot.hasData) { + if (isQuickBar) { + return Column( + children: [ + if (accounts.length != 1) ...[ + for (final account in accounts) ...[ + Container( + margin: const EdgeInsets.symmetric( + vertical: 5, ), + child: IconButton( + onPressed: () { + _accountsBloc.setActiveAccount(account); + }, + tooltip: account.client.humanReadableID, + icon: IntrinsicHeight( + child: NeonAccountAvatar( + account: account, + ), + ), + ), + ), + ], + Container( + margin: const EdgeInsets.only( + top: 10, + ), + child: Divider( + height: 5, + color: Theme.of(context).appBarTheme.foregroundColor, ), ), - ), + ], ], - Container( - margin: const EdgeInsets.only( - top: 10, - ), - child: Divider( - height: 5, - color: Theme.of(context).appBarTheme.foregroundColor, - ), - ), - ], - ], - ); - } - return DrawerHeader( - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.primary, - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - if (capabilities.data != null) ...[ - if (capabilities.data!.capabilities.theming?.name != null) ...[ - Text( - capabilities.data!.capabilities.theming!.name!, - style: DefaultTextStyle.of(context).style.copyWith( - color: Theme.of(context).appBarTheme.foregroundColor, + ); + } + return DrawerHeader( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.primary, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + if (capabilities.data != null) ...[ + if (capabilities.data!.capabilities.theming?.name != + null) ...[ + Text( + capabilities.data!.capabilities.theming!.name!, + style: DefaultTextStyle.of(context).style.copyWith( + color: + Theme.of(context).appBarTheme.foregroundColor, + ), + ), + ], + if (capabilities.data!.capabilities.theming?.logo != + null) ...[ + Flexible( + child: NeonCachedUrlImage( + url: capabilities.data!.capabilities.theming!.logo!, ), - ), - ], - if (capabilities.data!.capabilities.theming?.logo != null) ...[ - Flexible( - child: NeonCachedUrlImage( - url: capabilities.data!.capabilities.theming!.logo!, + ), + ], + ] else ...[ + NeonException( + capabilities.error, + onRetry: _capabilitiesBloc.refresh, ), - ), + NeonLinearProgressIndicator( + visible: capabilities.loading, + ), + ], + if (accounts.length != 1) ...[ + DropdownButtonHideUnderline( + child: DropdownButton( + isExpanded: true, + dropdownColor: Theme.of(context).colorScheme.primary, + iconEnabledColor: + Theme.of(context).colorScheme.onBackground, + value: _account.id, + items: accounts + .map>( + (final account) => DropdownMenuItem( + value: account.id, + child: NeonAccountTile( + account: account, + dense: true, + textColor: Theme.of(context) + .appBarTheme + .foregroundColor, + ), + ), + ) + .toList(), + onChanged: (final id) { + if (id != null) { + _accountsBloc.setActiveAccount(accounts.find(id)); + } + }, + ), + ), + ], ], - ] else ...[ - NeonException( - capabilities.error, - onRetry: _capabilitiesBloc.refresh, - ), - NeonLinearProgressIndicator( - visible: capabilities.loading, - ), - ], - if (accounts.length != 1) ...[ - DropdownButtonHideUnderline( - child: DropdownButton( - isExpanded: true, - dropdownColor: Theme.of(context).colorScheme.primary, - iconEnabledColor: - Theme.of(context).colorScheme.onBackground, - value: widget.account.id, - items: accounts - .map>( - (final account) => DropdownMenuItem( - value: account.id, - child: NeonAccountTile( - account: account, - dense: true, - textColor: - Theme.of(context).appBarTheme.foregroundColor, - ), - ), - ) - .toList(), - onChanged: (final id) { - if (id != null) { - _accountsBloc.setActiveAccount(accounts.find(id)); - } - }, - ), - ), - ], - ], - ), - ); - } - return Container(); - }, - ), - NeonException( - appImplementations.error, - onlyIcon: isQuickBar, - onRetry: _appsBloc.refresh, - ), - NeonLinearProgressIndicator( - visible: appImplementations.loading, - ), - if (appImplementations.data != null) ...[ - for (final appImplementation in appImplementations.data!) ...[ - StreamBuilder( - stream: appImplementation.getUnreadCounter(_appsBloc) ?? - BehaviorSubject.seeded(0), - builder: (final context, final unreadCounterSnapshot) { - final unreadCount = unreadCounterSnapshot.data ?? 0; - if (isQuickBar) { - return IconButton( - onPressed: () async { - await _appsBloc.setActiveApp(appImplementation.id); - }, - tooltip: appImplementation.name(context), - icon: NeonAppImplementationIcon( - appImplementation: appImplementation, - unreadCount: unreadCount, - color: Theme.of(context).colorScheme.primary, ), ); } - return ListTile( - key: Key('app-${appImplementation.id}'), - title: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text(appImplementation.name(context)), - if (unreadCount > 0) ...[ - Text( - unreadCount.toString(), - style: TextStyle( - color: Theme.of(context).colorScheme.primary, - fontWeight: FontWeight.bold, - fontSize: 14, - ), + return Container(); + }, + ), + NeonException( + appImplementations.error, + onlyIcon: isQuickBar, + onRetry: _appsBloc.refresh, + ), + NeonLinearProgressIndicator( + visible: appImplementations.loading, + ), + if (appImplementations.data != null) ...[ + for (final appImplementation in appImplementations.data!) ...[ + StreamBuilder( + stream: appImplementation.getUnreadCounter(_appsBloc) ?? + BehaviorSubject.seeded(0), + builder: (final context, final unreadCounterSnapshot) { + final unreadCount = unreadCounterSnapshot.data ?? 0; + if (isQuickBar) { + return IconButton( + onPressed: () async { + await _appsBloc.setActiveApp(appImplementation.id); + }, + tooltip: appImplementation.name(context), + icon: NeonAppImplementationIcon( + appImplementation: appImplementation, + unreadCount: unreadCount, + color: Theme.of(context).colorScheme.primary, ), - ], - ], - ), - leading: appImplementation.buildIcon(context), - minLeadingWidth: 0, - onTap: () async { - await _appsBloc.setActiveApp(appImplementation.id); - if (navigationMode == NavigationMode.drawer) { - // Don't pop when the drawer is always shown - if (!mounted) { - return; - } - Navigator.of(context).pop(); + ); } + return ListTile( + key: Key('app-${appImplementation.id}'), + title: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(appImplementation.name(context)), + if (unreadCount > 0) ...[ + Text( + unreadCount.toString(), + style: TextStyle( + color: Theme.of(context).colorScheme.primary, + fontWeight: FontWeight.bold, + fontSize: 14, + ), + ), + ], + ], + ), + leading: appImplementation.buildIcon(context), + minLeadingWidth: 0, + onTap: () async { + await _appsBloc.setActiveApp(appImplementation.id); + + if (!mounted) { + return; + } + Scaffold.maybeOf(context)?.closeDrawer(); + }, + ); }, - ); - }, - ), + ), + ], + ], ], - ], - ], + ), + ), ), - ), + if (isQuickBar) ...[ + IconButton( + onPressed: () => const SettingsRoute().go(context), + tooltip: AppLocalizations.of(context).settings, + icon: Icon( + Icons.settings, + color: Theme.of(context).appBarTheme.foregroundColor, + ), + ), + ] else ...[ + ListTile( + key: const Key('settings'), + title: Text(AppLocalizations.of(context).settings), + leading: const Icon(Icons.settings), + minLeadingWidth: 0, + onTap: () async { + Scaffold.maybeOf(context)?.closeDrawer(); + const SettingsRoute().go(context); + }, + ), + ], + ], ), - if (isQuickBar) ...[ - IconButton( - onPressed: _openSettings, - tooltip: AppLocalizations.of(context).settings, - icon: Icon( - Icons.settings, - color: Theme.of(context).appBarTheme.foregroundColor, - ), - ), - ] else ...[ - ListTile( - key: const Key('settings'), - title: Text(AppLocalizations.of(context).settings), - leading: const Icon(Icons.settings), - minLeadingWidth: 0, - onTap: () async { - if (navigationMode == NavigationMode.drawer) { - Navigator.of(context).pop(); - } - await _openSettings(); - }, - ), - ], - ], + ), ), - ), - ); + ); - return Row( - children: [ - if (navigationMode == NavigationMode.drawerAlwaysVisible) ...[ - drawer, - ], - Expanded( - child: Scaffold( - key: _scaffoldKey, - resizeToAvoidBottomInset: false, - drawer: navigationMode == NavigationMode.drawer ? drawer : null, - appBar: AppBar( - scrolledUnderElevation: navigationMode != NavigationMode.drawer ? 0 : null, - automaticallyImplyLeading: navigationMode == NavigationMode.drawer, - leadingWidth: isQuickBar ? kQuickBarWidth : null, - leading: isQuickBar - ? Container( - padding: const EdgeInsets.all(5), - child: capabilities.data?.capabilities.theming?.logo != null - ? NeonCachedUrlImage( - url: capabilities.data!.capabilities.theming!.logo!, - svgColor: Theme.of(context).iconTheme.color, - ) - : null, - ) - : null, - title: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - if (appImplementations.data != null && activeAppIDSnapshot.hasData) ...[ - Flexible( - child: Text( - appImplementations.data! - .find(activeAppIDSnapshot.data!)! - .name(context), + return Row( + children: [ + if (navigationMode == NavigationMode.drawerAlwaysVisible) ...[ + drawer, + ], + Expanded( + child: Scaffold( + key: _scaffoldKey, + resizeToAvoidBottomInset: false, + drawer: navigationMode == NavigationMode.drawer ? drawer : null, + appBar: AppBar( + scrolledUnderElevation: navigationMode != NavigationMode.drawer ? 0 : null, + automaticallyImplyLeading: navigationMode == NavigationMode.drawer, + leadingWidth: isQuickBar ? kQuickBarWidth : null, + leading: isQuickBar + ? Container( + padding: const EdgeInsets.all(5), + child: capabilities.data?.capabilities.theming?.logo != null + ? NeonCachedUrlImage( + url: capabilities.data!.capabilities.theming!.logo!, + svgColor: Theme.of(context).iconTheme.color, + ) + : null, + ) + : null, + title: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + if (appImplementations.data != null && activeAppIDSnapshot.hasData) ...[ + Flexible( + child: Text( + appImplementations.data! + .find(activeAppIDSnapshot.data!)! + .name(context), + ), ), - ), - ], - if (appImplementations.error != null) ...[ - const SizedBox( - width: 8, - ), - NeonException( - appImplementations.error, - onRetry: _appsBloc.refresh, - onlyIcon: true, - ), - ], - if (appImplementations.loading) ...[ - const SizedBox( - width: 8, - ), - Expanded( - child: NeonLinearProgressIndicator( - color: Theme.of(context).appBarTheme.foregroundColor, + ], + if (appImplementations.error != null) ...[ + const SizedBox( + width: 8, ), - ), + NeonException( + appImplementations.error, + onRetry: _appsBloc.refresh, + onlyIcon: true, + ), + ], + if (appImplementations.loading) ...[ + const SizedBox( + width: 8, + ), + Expanded( + child: NeonLinearProgressIndicator( + color: Theme.of(context).appBarTheme.foregroundColor, + ), + ), + ], ], + ), + if (accounts.length > 1) ...[ + Text( + account.client.humanReadableID, + style: Theme.of(context).textTheme.bodySmall, + ), ], - ), - if (accounts.length > 1) ...[ - Text( - account.client.humanReadableID, - style: Theme.of(context).textTheme.bodySmall, + ], + ), + actions: [ + if (notificationsAppImplementation.data != null) ...[ + StreamBuilder( + stream: notificationsAppImplementation.data!.getUnreadCounter(_appsBloc), + builder: (final context, final unreadCounterSnapshot) { + final unreadCount = unreadCounterSnapshot.data ?? 0; + return IconButton( + key: Key('app-${notificationsAppImplementation.data!.id}'), + onPressed: () async { + await _openNotifications( + notificationsAppImplementation.data!, + accounts, + account, + ); + }, + tooltip: AppLocalizations.of(context) + .appImplementationName(notificationsAppImplementation.data!.id), + icon: NeonAppImplementationIcon( + appImplementation: notificationsAppImplementation.data!, + unreadCount: unreadCount, + color: unreadCount > 0 + ? Theme.of(context).colorScheme.primary + : Theme.of(context).colorScheme.onBackground, + size: const Size.square(kAvatarSize * 2 / 3), + ), + ); + }, ), ], - ], - ), - actions: [ - if (notificationsAppImplementation.data != null) ...[ - StreamBuilder( - stream: notificationsAppImplementation.data!.getUnreadCounter(_appsBloc), - builder: (final context, final unreadCounterSnapshot) { - final unreadCount = unreadCounterSnapshot.data ?? 0; - return IconButton( - key: Key('app-${notificationsAppImplementation.data!.id}'), - onPressed: () async { - await _openNotifications( - notificationsAppImplementation.data!, - accounts, - account, - ); - }, - tooltip: AppLocalizations.of(context) - .appImplementationName(notificationsAppImplementation.data!.id), - icon: NeonAppImplementationIcon( - appImplementation: notificationsAppImplementation.data!, - unreadCount: unreadCount, - color: unreadCount > 0 - ? Theme.of(context).colorScheme.primary - : Theme.of(context).colorScheme.onBackground, - size: const Size.square(kAvatarSize * 2 / 3), - ), - ); + IconButton( + onPressed: () { + AccountSettingsRoute(accountid: account.id).go(context); }, - ), - ], - IconButton( - onPressed: () async { - await Navigator.of(context).push( - MaterialPageRoute( - builder: (final context) => AccountSettingsPage( - bloc: _accountsBloc, - account: account, - ), + tooltip: AppLocalizations.of(context).settingsAccount, + icon: IntrinsicWidth( + child: NeonAccountAvatar( + account: account, ), - ); - }, - tooltip: AppLocalizations.of(context).settingsAccount, - icon: IntrinsicWidth( - child: NeonAccountAvatar( - account: account, ), ), - ), - ], - ), - body: Row( - children: [ - if (navigationMode == NavigationMode.quickBar) ...[ - drawer, ], - Expanded( - child: Column( - children: [ - if (appImplementations.data != null) ...[ - if (appImplementations.data!.isEmpty) ...[ - Expanded( - child: Center( - child: Text( - AppLocalizations.of(context).errorNoCompatibleNextcloudAppsFound, - textAlign: TextAlign.center, - ), - ), - ), - ] else ...[ - if (activeAppIDSnapshot.hasData) ...[ + ), + body: Row( + children: [ + if (navigationMode == NavigationMode.quickBar) ...[ + drawer, + ], + Expanded( + child: Column( + children: [ + if (appImplementations.data != null) ...[ + if (appImplementations.data!.isEmpty) ...[ Expanded( - child: appImplementations.data! - .find(activeAppIDSnapshot.data!)! - .buildPage(context, _appsBloc), + child: Center( + child: Text( + AppLocalizations.of(context) + .errorNoCompatibleNextcloudAppsFound, + textAlign: TextAlign.center, + ), + ), ), + ] else ...[ + if (activeAppIDSnapshot.hasData) ...[ + Expanded( + child: appImplementations.data! + .find(activeAppIDSnapshot.data!)! + .buildPage(context, _appsBloc), + ), + ], ], ], ], - ], + ), ), - ), - ], + ], + ), ), ), - ), - ], - ); + ], + ); + } } return Container(); }, diff --git a/packages/neon/neon/lib/src/pages/login.dart b/packages/neon/neon/lib/src/pages/login.dart index e998f2d7..b3ed5c2d 100644 --- a/packages/neon/neon/lib/src/pages/login.dart +++ b/packages/neon/neon/lib/src/pages/login.dart @@ -73,7 +73,6 @@ class _LoginPageState extends State { if (widget.serverURL != null) { _accountsBloc.updateAccount(account); - Navigator.of(context).pop(); } else { final existingAccount = _accountsBloc.accounts.value.find(account.id); if (existingAccount != null) { diff --git a/packages/neon/neon/lib/src/pages/settings.dart b/packages/neon/neon/lib/src/pages/settings.dart index 95c956f1..e51b1c33 100644 --- a/packages/neon/neon/lib/src/pages/settings.dart +++ b/packages/neon/neon/lib/src/pages/settings.dart @@ -80,14 +80,8 @@ class _SettingsPageState extends State { CustomSettingsTile( leading: appImplementation.buildIcon(context), title: Text(appImplementation.name(context)), - onTap: () async { - await Navigator.of(context).push( - MaterialPageRoute( - builder: (final context) => NextcloudAppSettingsPage( - appImplementation: appImplementation, - ), - ), - ); + onTap: () { + NextcloudAppSettingsRoute(appid: appImplementation.id).go(context); }, ), ], @@ -181,26 +175,15 @@ class _SettingsPageState extends State { for (final account in accountsSnapshot.data!) ...[ NeonAccountSettingsTile( account: account, - onTap: () async { - await Navigator.of(context).push( - MaterialPageRoute( - builder: (final context) => AccountSettingsPage( - bloc: accountsBloc, - account: account, - ), - ), - ); + onTap: () { + AccountSettingsRoute(accountid: account.id).go(context); }, ), ], CustomSettingsTile( title: ElevatedButton.icon( - onPressed: () async { - await Navigator.of(context).push( - MaterialPageRoute( - builder: (final context) => const LoginPage(), - ), - ); + onPressed: () { + const LoginRoute().go(context); }, icon: const 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 4fd6a164..d6c397c5 100644 --- a/packages/neon/neon/lib/src/router.dart +++ b/packages/neon/neon/lib/src/router.dart @@ -1,41 +1,124 @@ -part of '../neon.dart'; - // ignore: prefer_mixin -class AppRouter extends RouterDelegate with ChangeNotifier, PopNavigatorRouterDelegateMixin { +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:neon/neon.dart'; +import 'package:provider/provider.dart'; + +part 'router.g.dart'; + +class AppRouter extends GoRouter { AppRouter({ - required this.navigatorKey, - required this.accountsBloc, + required final GlobalKey navigatorKey, + required final AccountsBloc accountsBloc, + }) : super( + refreshListenable: StreamListenable.behaviorSubject(accountsBloc.activeAccount), + navigatorKey: navigatorKey, + initialLocation: const HomeRoute().location, + redirect: (final context, final state) { + final account = accountsBloc.activeAccount.valueOrNull; + + if (account == null) { + return const LoginRoute().location; + } + + if (state.location == const LoginRoute().location) { + return const HomeRoute().location; + } + + return null; + }, + routes: $appRoutes, + ); +} + +@immutable +class AccountSettingsRoute extends GoRouteData { + const AccountSettingsRoute({ + required this.accountid, }); - final AccountsBloc accountsBloc; + final String accountid; + + @override + Widget build(final BuildContext context, final GoRouterState state) { + final bloc = Provider.of(context, listen: false); + final account = bloc.accounts.value.find(accountid)!; + + return AccountSettingsPage( + bloc: bloc, + account: account, + ); + } +} + +@TypedGoRoute( + path: '/', + name: 'home', + routes: [ + TypedGoRoute( + path: 'settings', + name: 'Settings', + routes: [ + TypedGoRoute( + path: ':appid', + name: 'NextcloudAppSettings', + ), + TypedGoRoute( + path: 'account/:accountid', + name: 'AccountSettings', + ), + ], + ) + ], +) +@immutable +class HomeRoute extends GoRouteData { + const HomeRoute(); @override - final GlobalKey navigatorKey; + 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)); + } +} + +@TypedGoRoute( + path: '/login', + name: 'login', +) +@immutable +class LoginRoute extends GoRouteData { + const LoginRoute({this.server}); + + final String? server; @override - Future setNewRoutePath(final Account? configuration) async {} + Widget build(final BuildContext context, final GoRouterState state) => LoginPage(serverURL: server); +} + +@immutable +class NextcloudAppSettingsRoute extends GoRouteData { + const NextcloudAppSettingsRoute({ + required this.appid, + }); + + final String appid; @override - Account? get currentConfiguration => accountsBloc.activeAccount.valueOrNull; + Widget build(final BuildContext context, final GoRouterState state) { + final appImplementations = Provider.of>(context, listen: false); + final appImplementation = appImplementations.firstWhere((final app) => app.id == appid); + + return NextcloudAppSettingsPage(appImplementation: appImplementation); + } +} + +@immutable +class SettingsRoute extends GoRouteData { + const SettingsRoute(); @override - Widget build(final BuildContext context) => Navigator( - key: navigatorKey, - onPopPage: (final route, final result) => route.didPop(result), - pages: [ - if (currentConfiguration == null) ...[ - const MaterialPage( - child: LoginPage(), - ), - ] else ...[ - MaterialPage( - name: 'home', - child: HomePage( - key: Key(currentConfiguration!.id), - account: currentConfiguration!, - ), - ), - ], - ], - ); + Widget build(final BuildContext context, final GoRouterState state) => const SettingsPage(); } diff --git a/packages/neon/neon/lib/src/router.g.dart b/packages/neon/neon/lib/src/router.g.dart new file mode 100644 index 00000000..406213ea --- /dev/null +++ b/packages/neon/neon/lib/src/router.g.dart @@ -0,0 +1,122 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'router.dart'; + +// ************************************************************************** +// GoRouterGenerator +// ************************************************************************** + +List get $appRoutes => [ + $homeRoute, + $loginRoute, + ]; + +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: ':appid', + name: 'NextcloudAppSettings', + factory: $NextcloudAppSettingsRouteExtension._fromState, + ), + GoRouteData.$route( + path: 'account/:accountid', + name: 'AccountSettings', + factory: $AccountSettingsRouteExtension._fromState, + ), + ], + ), + ], + ); + +extension $HomeRouteExtension on HomeRoute { + static HomeRoute _fromState(GoRouterState state) => const HomeRoute(); + + String get location => GoRouteData.$location( + '/', + ); + + void go(BuildContext context) => context.go(location); + + Future push(BuildContext context) => context.push(location); + + void pushReplacement(BuildContext context) => context.pushReplacement(location); +} + +extension $SettingsRouteExtension on SettingsRoute { + static SettingsRoute _fromState(GoRouterState state) => const SettingsRoute(); + + String get location => GoRouteData.$location( + '/settings', + ); + + void go(BuildContext context) => context.go(location); + + Future push(BuildContext context) => context.push(location); + + void pushReplacement(BuildContext context) => context.pushReplacement(location); +} + +extension $NextcloudAppSettingsRouteExtension on NextcloudAppSettingsRoute { + static NextcloudAppSettingsRoute _fromState(GoRouterState state) => NextcloudAppSettingsRoute( + appid: state.pathParameters['appid']!, + ); + + String get location => GoRouteData.$location( + '/settings/${Uri.encodeComponent(appid)}', + ); + + void go(BuildContext context) => context.go(location); + + Future push(BuildContext context) => context.push(location); + + void pushReplacement(BuildContext context) => context.pushReplacement(location); +} + +extension $AccountSettingsRouteExtension on AccountSettingsRoute { + static AccountSettingsRoute _fromState(GoRouterState state) => AccountSettingsRoute( + accountid: state.pathParameters['accountid']!, + ); + + String get location => GoRouteData.$location( + '/settings/account/${Uri.encodeComponent(accountid)}', + ); + + void go(BuildContext context) => context.go(location); + + Future push(BuildContext context) => context.push(location); + + 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 06376b8e..43ac125c 100644 --- a/packages/neon/neon/lib/src/utils/global_popups.dart +++ b/packages/neon/neon/lib/src/utils/global_popups.dart @@ -27,12 +27,8 @@ class GlobalPopups { content: Text(AppLocalizations.of(context).firstLaunchGoToSettingsToEnablePushNotifications), action: SnackBarAction( label: AppLocalizations.of(context).settings, - onPressed: () async { - await Navigator.of(context).push( - MaterialPageRoute( - builder: (final context) => const SettingsPage(), - ), - ); + onPressed: () { + const SettingsRoute().go(context); }, ), ), diff --git a/packages/neon/neon/lib/src/utils/stream_listenable.dart b/packages/neon/neon/lib/src/utils/stream_listenable.dart new file mode 100644 index 00000000..9a93ea49 --- /dev/null +++ b/packages/neon/neon/lib/src/utils/stream_listenable.dart @@ -0,0 +1,38 @@ +part of '../../neon.dart'; + +/// Listenable Stream +/// +/// A class that implements [Listenable] for a stream. +/// Objects need to be manually disposed. +class StreamListenable extends ChangeNotifier { + /// Listenable Stream + /// + /// Implementation for all types of [Stream]s. + /// For an implementation tailored towards [BehaviorSubject] have a look at [StreamListenable.behaviorSubject]. + StreamListenable(final Stream stream) { + notifyListeners(); + + _subscription = stream.asBroadcastStream().listen((final value) { + notifyListeners(); + }); + } + + /// Listenable BehaviorSubject + /// + /// Implementation for a [BehaviorSubject]. It ensures to not unececcary notify listeners. + /// For an implementation tailored towards otnher kinds of [Stream] have a look at [StreamListenable]. + StreamListenable.behaviorSubject(final BehaviorSubject subject) { + _subscription = subject.listen((final value) { + notifyListeners(); + }); + } + + late final StreamSubscription _subscription; + + @override + void dispose() { + unawaited(_subscription.cancel()); + + super.dispose(); + } +} diff --git a/packages/neon/neon/lib/src/widgets/exception.dart b/packages/neon/neon/lib/src/widgets/exception.dart index a16ddee4..1f88bf48 100644 --- a/packages/neon/neon/lib/src/widgets/exception.dart +++ b/packages/neon/neon/lib/src/widgets/exception.dart @@ -62,7 +62,7 @@ class NeonException extends StatelessWidget { : AppLocalizations.of(context).actionRetry, onPressed: () async { if (details.isUnauthorized) { - await _openLoginPage(context); + _openLoginPage(context); } else { onRetry(); } @@ -177,14 +177,10 @@ class NeonException extends StatelessWidget { ); } - static Future _openLoginPage(final BuildContext context) async { - await Navigator.of(context).push( - MaterialPageRoute( - builder: (final context) => LoginPage( - serverURL: Provider.of(context, listen: false).activeAccount.value!.serverURL, - ), - ), - ); + static void _openLoginPage(final BuildContext context) { + LoginRoute( + server: Provider.of(context, listen: false).activeAccount.value!.serverURL, + ).go(context); } } diff --git a/packages/neon/neon/pubspec.yaml b/packages/neon/neon/pubspec.yaml index 071b5c59..5bca6d64 100644 --- a/packages/neon/neon/pubspec.yaml +++ b/packages/neon/neon/pubspec.yaml @@ -21,6 +21,7 @@ dependencies: sdk: flutter flutter_native_splash: ^2.2.19 flutter_svg: ^2.0.5 + go_router: ^7.1.1 http: ^0.13.6 intl: ^0.18.0 json_annotation: ^4.8.1 @@ -56,7 +57,8 @@ dependencies: xml: ^6.3.0 dev_dependencies: - build_runner: ^2.4.2 + build_runner: ^2.4.4 + go_router_builder: ^2.0.1 json_serializable: ^6.6.2 nit_picking: git: