diff --git a/packages/neon/neon/lib/neon.dart b/packages/neon/neon/lib/neon.dart index 0f19bedd..2c42de04 100644 --- a/packages/neon/neon/lib/neon.dart +++ b/packages/neon/neon/lib/neon.dart @@ -66,6 +66,7 @@ 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'; diff --git a/packages/neon/neon/lib/src/app.dart b/packages/neon/neon/lib/src/app.dart index 0f812909..83563284 100644 --- a/packages/neon/neon/lib/src/app.dart +++ b/packages/neon/neon/lib/src/app.dart @@ -25,7 +25,15 @@ class _NeonAppState extends State with WidgetsBindingObserver, tray.Tra late NeonPlatform _platform; late GlobalOptions _globalOptions; late AccountsBloc _accountsBloc; - final _globalPopups = const GlobalPopups(); + late final _routerDelegate = AppRouter( + navigatorKey: _navigatorKey, + accountsBloc: _accountsBloc, + onThemeChanged: (final nextcloudTheme) { + setState(() { + _nextcloudTheme = nextcloudTheme; + }); + }, + ); NextcloudCoreServerCapabilities_Ocs_Data_Capabilities_Theming? _nextcloudTheme; final _platformBrightness = BehaviorSubject.seeded(WidgetsBinding.instance.window.platformBrightness); @@ -55,50 +63,6 @@ class _NeonAppState extends State with WidgetsBindingObserver, tray.Tra } WidgetsBinding.instance.addPostFrameCallback((final _) async { - widget.accountsBloc.activeAccount.listen((final activeAccount) async { - FlutterNativeSplash.remove(); - - if (activeAccount == null) { - await _navigatorKey.currentState!.pushAndRemoveUntil( - MaterialPageRoute( - builder: (final context) => const LoginPage(), - ), - (final _) => false, - ); - } else { - const settings = RouteSettings( - name: 'home', - ); - Widget builder(final context) => Scaffold( - resizeToAvoidBottomInset: false, - body: Stack( - children: [ - _globalPopups, - HomePage( - account: activeAccount, - onThemeChanged: (final nextcloudTheme) { - setState(() { - _nextcloudTheme = nextcloudTheme; - }); - }, - ), - ], - ), - ); - await _navigatorKey.currentState!.pushAndRemoveUntil( - widget.globalOptions.navigationMode.value == NavigationMode.drawer - ? MaterialPageRoute( - settings: settings, - builder: builder, - ) - : _NoAnimationPageRoute( - settings: settings, - builder: builder, - ), - (final _) => false, - ); - } - }); final localizations = await appLocalizationsFromSystem(); if (!mounted) { @@ -310,36 +274,30 @@ class _NeonAppState extends State with WidgetsBindingObserver, tray.Tra option: widget.globalOptions.themeOLEDAsDark, builder: (final context, final themeOLEDAsDark) => OptionBuilder( option: widget.globalOptions.themeKeepOriginalAccentColor, - builder: (final context, final themeKeepOriginalAccentColor) { - if (themeMode == null || !platformBrightnessSnapshot.hasData || themeOLEDAsDark == null) { - return Container(); - } - return MaterialApp( - localizationsDelegates: AppLocalizations.localizationsDelegates, - supportedLocales: AppLocalizations.supportedLocales, - navigatorKey: _navigatorKey, - theme: getThemeFromNextcloudTheme( - _nextcloudTheme, - themeMode, - platformBrightnessSnapshot.data!, - oledAsDark: themeOLEDAsDark, - keepOriginalAccentColor: _nextcloudTheme == null || (themeKeepOriginalAccentColor ?? false), - ), - home: Container(), - ); - }, + builder: (final context, final themeKeepOriginalAccentColor) => StreamBuilder( + stream: widget.accountsBloc.activeAccount, + builder: (final context, final activeAccountSnapshot) { + if (themeMode == null || !platformBrightnessSnapshot.hasData || themeOLEDAsDark == null) { + return Container(); + } + + FlutterNativeSplash.remove(); + return MaterialApp.router( + localizationsDelegates: AppLocalizations.localizationsDelegates, + supportedLocales: AppLocalizations.supportedLocales, + theme: getThemeFromNextcloudTheme( + _nextcloudTheme, + themeMode, + platformBrightnessSnapshot.data!, + oledAsDark: themeOLEDAsDark, + keepOriginalAccentColor: _nextcloudTheme == null || (themeKeepOriginalAccentColor ?? false), + ), + routerDelegate: _routerDelegate, + ); + }, + ), ), ), ), ); } - -class _NoAnimationPageRoute extends MaterialPageRoute { - _NoAnimationPageRoute({ - required super.builder, - super.settings, - }); - - @override - Duration get transitionDuration => Duration.zero; -} diff --git a/packages/neon/neon/lib/src/pages/home.dart b/packages/neon/neon/lib/src/pages/home.dart index 6915eac4..1153fd6c 100644 --- a/packages/neon/neon/lib/src/pages/home.dart +++ b/packages/neon/neon/lib/src/pages/home.dart @@ -202,15 +202,15 @@ class _HomePageState extends State { ) => OptionBuilder( option: _globalOptions.navigationMode, - builder: (final context, final navigationMode) => WillPopScope( - onWillPop: () async { + builder: (final context, final navigationMode) => BackButtonListener( + onBackButtonPressed: () async { if (_scaffoldKey.currentState!.isDrawerOpen) { Navigator.pop(context); - return true; + return false; } _scaffoldKey.currentState!.openDrawer(); - return false; + return true; }, child: Builder( builder: (final context) { diff --git a/packages/neon/neon/lib/src/pages/login.dart b/packages/neon/neon/lib/src/pages/login.dart index 88f71b22..42de3d24 100644 --- a/packages/neon/neon/lib/src/pages/login.dart +++ b/packages/neon/neon/lib/src/pages/login.dart @@ -90,18 +90,18 @@ class _LoginPageState extends State { @override Widget build(final BuildContext context) => StreamBuilder>( stream: _accountsBloc.accounts, - builder: (final context, final accountsSnapshot) => WillPopScope( - onWillPop: () async { + builder: (final context, final accountsSnapshot) => BackButtonListener( + onBackButtonPressed: () async { if (accountsSnapshot.data?.isNotEmpty ?? false) { - return true; + return false; } if ((await _loginBloc.serverURL.first) == null) { - return true; + return false; } _loginBloc.setServerURL(null); - return false; + return true; }, child: StreamBuilder( stream: _loginBloc.serverURL, diff --git a/packages/neon/neon/lib/src/router.dart b/packages/neon/neon/lib/src/router.dart new file mode 100644 index 00000000..51d8622d --- /dev/null +++ b/packages/neon/neon/lib/src/router.dart @@ -0,0 +1,51 @@ +part of '../neon.dart'; + +// ignore: prefer_mixin +class AppRouter extends RouterDelegate with ChangeNotifier, PopNavigatorRouterDelegateMixin { + AppRouter({ + required this.navigatorKey, + required this.accountsBloc, + required this.onThemeChanged, + }); + + final AccountsBloc accountsBloc; + final Function(NextcloudTheme? theme) onThemeChanged; + final _globalPopups = const GlobalPopups(); + + @override + final GlobalKey navigatorKey; + + @override + Future setNewRoutePath(final Account? configuration) async {} + + @override + Account? get currentConfiguration => accountsBloc.activeAccount.valueOrNull; + + @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( + child: Scaffold( + resizeToAvoidBottomInset: false, + body: Stack( + children: [ + _globalPopups, + HomePage( + account: currentConfiguration!, + onThemeChanged: onThemeChanged, + ), + ], + ), + ), + ), + ], + ], + ); +} diff --git a/packages/neon/neon_files/lib/widgets/browser_view.dart b/packages/neon/neon_files/lib/widgets/browser_view.dart index bcef6875..70421090 100644 --- a/packages/neon/neon_files/lib/widgets/browser_view.dart +++ b/packages/neon/neon_files/lib/widgets/browser_view.dart @@ -46,142 +46,154 @@ class _FilesBrowserViewState extends State { !uploadTasksSnapshot.hasData || !downloadTasksSnapshot.hasData ? Container() - : Scaffold( - resizeToAvoidBottomInset: false, - floatingActionButton: widget.enableCreateActions - ? FloatingActionButton( - onPressed: () async { - await showDialog( - context: context, - builder: (final context) => FilesChooseCreateDialog( - bloc: widget.filesBloc, - basePath: widget.bloc.path.value, - ), - ); - }, - child: const Icon(Icons.add), - ) - : null, - body: NeonListView( - scrollKey: 'files-${pathSnapshot.data!.join('/')}', - withFloatingActionButton: true, - items: [ - for (final uploadTask in files.data == null - ? [] - : uploadTasksSnapshot.data!.where( - (final task) => - files.data!.where((final file) => _pathMatchesFile(task.path, file.name)).isEmpty, - )) ...[ - StreamBuilder( - stream: uploadTask.progress, - builder: (final context, final uploadTaskProgressSnapshot) => - !uploadTaskProgressSnapshot.hasData - ? Container() - : _buildFile( - context: context, - details: FileDetails( - path: uploadTask.path, - isDirectory: false, - size: uploadTask.size, - etag: null, - mimeType: null, - lastModified: uploadTask.lastModified, - hasPreview: null, - isFavorite: null, + : BackButtonListener( + onBackButtonPressed: () async { + final path = pathSnapshot.data!; + if (path.isNotEmpty) { + widget.bloc.setPath(path.sublist(0, path.length - 1)); + return true; + } + return false; + }, + child: Scaffold( + resizeToAvoidBottomInset: false, + floatingActionButton: widget.enableCreateActions + ? FloatingActionButton( + onPressed: () async { + await showDialog( + context: context, + builder: (final context) => FilesChooseCreateDialog( + bloc: widget.filesBloc, + basePath: widget.bloc.path.value, + ), + ); + }, + child: const Icon(Icons.add), + ) + : null, + body: NeonListView( + scrollKey: 'files-${pathSnapshot.data!.join('/')}', + withFloatingActionButton: true, + items: [ + for (final uploadTask in files.data == null + ? [] + : uploadTasksSnapshot.data!.where( + (final task) => files.data! + .where((final file) => _pathMatchesFile(task.path, file.name)) + .isEmpty, + )) ...[ + StreamBuilder( + stream: uploadTask.progress, + builder: (final context, final uploadTaskProgressSnapshot) => + !uploadTaskProgressSnapshot.hasData + ? Container() + : _buildFile( + context: context, + details: FileDetails( + path: uploadTask.path, + isDirectory: false, + size: uploadTask.size, + etag: null, + mimeType: null, + lastModified: uploadTask.lastModified, + hasPreview: null, + isFavorite: null, + ), + uploadProgress: uploadTaskProgressSnapshot.data, + downloadProgress: null, ), - uploadProgress: uploadTaskProgressSnapshot.data, - downloadProgress: null, - ), - ), - ], - if (files.data != null) ...[ - for (final file in files.data!) ...[ - if (!widget.onlyShowDirectories || file.isDirectory) ...[ - Builder( - builder: (final context) { - final matchingUploadTasks = uploadTasksSnapshot.data! - .where((final task) => _pathMatchesFile(task.path, file.name)); - final matchingDownloadTasks = downloadTasksSnapshot.data! - .where((final task) => _pathMatchesFile(task.path, file.name)); + ), + ], + if (files.data != null) ...[ + for (final file in files.data!) ...[ + if (!widget.onlyShowDirectories || file.isDirectory) ...[ + Builder( + builder: (final context) { + final matchingUploadTasks = uploadTasksSnapshot.data! + .where((final task) => _pathMatchesFile(task.path, file.name)); + final matchingDownloadTasks = downloadTasksSnapshot.data! + .where((final task) => _pathMatchesFile(task.path, file.name)); - return StreamBuilder( - stream: matchingUploadTasks.isNotEmpty - ? matchingUploadTasks.first.progress - : Stream.value(null), - builder: (final context, final uploadTaskProgressSnapshot) => StreamBuilder( - stream: matchingDownloadTasks.isNotEmpty - ? matchingDownloadTasks.first.progress + return StreamBuilder( + stream: matchingUploadTasks.isNotEmpty + ? matchingUploadTasks.first.progress : Stream.value(null), - builder: (final context, final downloadTaskProgressSnapshot) => _buildFile( - context: context, - details: FileDetails( - path: [...widget.bloc.path.value, file.name], - isDirectory: matchingUploadTasks.isEmpty && file.isDirectory, - size: matchingUploadTasks.isNotEmpty - ? matchingUploadTasks.first.size - : file.size!, - etag: matchingUploadTasks.isNotEmpty ? null : file.etag, - mimeType: matchingUploadTasks.isNotEmpty ? null : file.mimeType, - lastModified: matchingUploadTasks.isNotEmpty - ? matchingUploadTasks.first.lastModified - : file.lastModified!, - hasPreview: matchingUploadTasks.isNotEmpty ? null : file.hasPreview, - isFavorite: matchingUploadTasks.isNotEmpty ? null : file.favorite, + builder: (final context, final uploadTaskProgressSnapshot) => + StreamBuilder( + stream: matchingDownloadTasks.isNotEmpty + ? matchingDownloadTasks.first.progress + : Stream.value(null), + builder: (final context, final downloadTaskProgressSnapshot) => _buildFile( + context: context, + details: FileDetails( + path: [...widget.bloc.path.value, file.name], + isDirectory: matchingUploadTasks.isEmpty && file.isDirectory, + size: matchingUploadTasks.isNotEmpty + ? matchingUploadTasks.first.size + : file.size!, + etag: matchingUploadTasks.isNotEmpty ? null : file.etag, + mimeType: matchingUploadTasks.isNotEmpty ? null : file.mimeType, + lastModified: matchingUploadTasks.isNotEmpty + ? matchingUploadTasks.first.lastModified + : file.lastModified!, + hasPreview: matchingUploadTasks.isNotEmpty ? null : file.hasPreview, + isFavorite: matchingUploadTasks.isNotEmpty ? null : file.favorite, + ), + uploadProgress: uploadTaskProgressSnapshot.data, + downloadProgress: downloadTaskProgressSnapshot.data, ), - uploadProgress: uploadTaskProgressSnapshot.data, - downloadProgress: downloadTaskProgressSnapshot.data, ), - ), - ); - }, - ), + ); + }, + ), + ], ], ], ], - ], - isLoading: files.loading, - error: files.error, - onRefresh: widget.bloc.refresh, - builder: (final context, final widget) => widget, - topScrollingChildren: [ - Align( - alignment: Alignment.topLeft, - child: Container( - margin: const EdgeInsets.symmetric( - horizontal: 10, - ), - child: Wrap( - crossAxisAlignment: WrapCrossAlignment.center, - children: [ - SizedBox( - height: 40, - child: InkWell( - onTap: () { - widget.bloc.setPath([]); - }, - child: const Icon(Icons.house), - ), - ), - for (var i = 0; i < pathSnapshot.data!.length; i++) ...[ - InkWell( - onTap: () { - widget.bloc.setPath(pathSnapshot.data!.sublist(0, i + 1)); - }, - child: Text(pathSnapshot.data![i]), + isLoading: files.loading, + error: files.error, + onRefresh: widget.bloc.refresh, + builder: (final context, final widget) => widget, + topScrollingChildren: [ + Align( + alignment: Alignment.topLeft, + child: Container( + margin: const EdgeInsets.symmetric( + horizontal: 10, + ), + child: Wrap( + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + SizedBox( + height: 40, + child: InkWell( + onTap: () { + widget.bloc.setPath([]); + }, + child: const Icon(Icons.house), + ), ), - ], - ] - .intersperse( - const Icon( - Icons.keyboard_arrow_right, - size: 40, + for (var i = 0; i < pathSnapshot.data!.length; i++) ...[ + InkWell( + onTap: () { + widget.bloc.setPath(pathSnapshot.data!.sublist(0, i + 1)); + }, + child: Text(pathSnapshot.data![i]), ), - ) - .toList(), + ], + ] + .intersperse( + const Icon( + Icons.keyboard_arrow_right, + size: 40, + ), + ) + .toList(), + ), ), ), - ), - ], + ], + ), ), ), ), diff --git a/packages/neon/neon_news/lib/pages/article.dart b/packages/neon/neon_news/lib/pages/article.dart index f3e55c72..10515dd3 100644 --- a/packages/neon/neon_news/lib/pages/article.dart +++ b/packages/neon/neon_news/lib/pages/article.dart @@ -81,17 +81,17 @@ class _NewsArticlePageState extends State { } @override - Widget build(final BuildContext context) => WillPopScope( - onWillPop: () async { + Widget build(final BuildContext context) => BackButtonListener( + onBackButtonPressed: () async { if (_webviewController != null && await _webviewController!.canGoBack()) { await _webviewController!.goBack(); - return false; + return true; } if (mounted && Provider.of(context, listen: false).canUseWakelock) { await Wakelock.disable(); } - return true; + return false; }, child: Scaffold( resizeToAvoidBottomInset: false, diff --git a/packages/neon/neon_notes/lib/pages/note.dart b/packages/neon/neon_notes/lib/pages/note.dart index e9320d8f..837ca10e 100644 --- a/packages/neon/neon_notes/lib/pages/note.dart +++ b/packages/neon/neon_notes/lib/pages/note.dart @@ -63,12 +63,12 @@ class _NotesNotePageState extends State { } @override - Widget build(final BuildContext context) => WillPopScope( - onWillPop: () async { + Widget build(final BuildContext context) => BackButtonListener( + onBackButtonPressed: () async { if (Provider.of(context, listen: false).canUseWakelock) { await Wakelock.disable(); } - return true; + return false; }, child: Scaffold( resizeToAvoidBottomInset: true,