diff --git a/packages/neon/lib/l10n/en.arb b/packages/neon/lib/l10n/en.arb index 251c5bba..9a4a0924 100644 --- a/packages/neon/lib/l10n/en.arb +++ b/packages/neon/lib/l10n/en.arb @@ -72,6 +72,7 @@ "optionsCategoryAccounts": "Accounts", "optionsCategoryStartup": "Startup", "optionsCategorySystemTray": "System tray", + "optionsCategoryNavigation": "Navigation", "optionsSortOrderAscending": "Ascending", "optionsSortOrderDescending": "Descending", "globalOptionsThemeMode": "Theme mode", @@ -94,8 +95,12 @@ "globalOptionsSystemTrayEnabled": "Enable system tray", "globalOptionsSystemTrayHideToTrayWhenMinimized": "Hide to system tray when minimized", "globalOptionsAccountsRememberLastUsedAccount": "Remember last used account", - "globaloptionsaccountsInitialAccount": "Initial account", + "globalOptionsAccountsInitialAccount": "Initial account", "globalOptionsAccountsAdd": "Add account", + "globalOptionsNavigationMode": "Navigation mode", + "globalOptionsNavigationModeDrawer": "Drawer", + "globalOptionsNavigationModeDrawerAlwaysVisible": "Drawer always visible", + "globalOptionsNavigationModeQuickBar": "Quick bar", "accountOptionsRemoveConfirm": "Are you sure you want to remove the account {id}?", "@accountOptionsRemoveConfirm": { "placeholders": { diff --git a/packages/neon/lib/l10n/localizations.dart b/packages/neon/lib/l10n/localizations.dart index 24c1e57d..591b1aed 100644 --- a/packages/neon/lib/l10n/localizations.dart +++ b/packages/neon/lib/l10n/localizations.dart @@ -353,6 +353,12 @@ abstract class AppLocalizations { /// **'System tray'** String get optionsCategorySystemTray; + /// No description provided for @optionsCategoryNavigation. + /// + /// In en, this message translates to: + /// **'Navigation'** + String get optionsCategoryNavigation; + /// No description provided for @optionsSortOrderAscending. /// /// In en, this message translates to: @@ -485,11 +491,11 @@ abstract class AppLocalizations { /// **'Remember last used account'** String get globalOptionsAccountsRememberLastUsedAccount; - /// No description provided for @globaloptionsaccountsInitialAccount. + /// No description provided for @globalOptionsAccountsInitialAccount. /// /// In en, this message translates to: /// **'Initial account'** - String get globaloptionsaccountsInitialAccount; + String get globalOptionsAccountsInitialAccount; /// No description provided for @globalOptionsAccountsAdd. /// @@ -497,6 +503,30 @@ abstract class AppLocalizations { /// **'Add account'** String get globalOptionsAccountsAdd; + /// No description provided for @globalOptionsNavigationMode. + /// + /// In en, this message translates to: + /// **'Navigation mode'** + String get globalOptionsNavigationMode; + + /// No description provided for @globalOptionsNavigationModeDrawer. + /// + /// In en, this message translates to: + /// **'Drawer'** + String get globalOptionsNavigationModeDrawer; + + /// No description provided for @globalOptionsNavigationModeDrawerAlwaysVisible. + /// + /// In en, this message translates to: + /// **'Drawer always visible'** + String get globalOptionsNavigationModeDrawerAlwaysVisible; + + /// No description provided for @globalOptionsNavigationModeQuickBar. + /// + /// In en, this message translates to: + /// **'Quick bar'** + String get globalOptionsNavigationModeQuickBar; + /// No description provided for @accountOptionsRemoveConfirm. /// /// In en, this message translates to: diff --git a/packages/neon/lib/l10n/localizations_en.dart b/packages/neon/lib/l10n/localizations_en.dart index d4497969..f7883e86 100644 --- a/packages/neon/lib/l10n/localizations_en.dart +++ b/packages/neon/lib/l10n/localizations_en.dart @@ -147,6 +147,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get optionsCategorySystemTray => 'System tray'; + @override + String get optionsCategoryNavigation => 'Navigation'; + @override String get optionsSortOrderAscending => 'Ascending'; @@ -216,11 +219,23 @@ class AppLocalizationsEn extends AppLocalizations { String get globalOptionsAccountsRememberLastUsedAccount => 'Remember last used account'; @override - String get globaloptionsaccountsInitialAccount => 'Initial account'; + String get globalOptionsAccountsInitialAccount => 'Initial account'; @override String get globalOptionsAccountsAdd => 'Add account'; + @override + String get globalOptionsNavigationMode => 'Navigation mode'; + + @override + String get globalOptionsNavigationModeDrawer => 'Drawer'; + + @override + String get globalOptionsNavigationModeDrawerAlwaysVisible => 'Drawer always visible'; + + @override + String get globalOptionsNavigationModeQuickBar => 'Quick bar'; + @override String accountOptionsRemoveConfirm(String id) { return 'Are you sure you want to remove the account $id?'; diff --git a/packages/neon/lib/src/app.dart b/packages/neon/lib/src/app.dart index 2365d244..ffcfea68 100644 --- a/packages/neon/lib/src/app.dart +++ b/packages/neon/lib/src/app.dart @@ -53,20 +53,27 @@ class _NeonAppState extends State with WidgetsBindingObserver { (final _) => false, ); } else { - await _navigatorKey.currentState!.pushAndRemoveUntil( - MaterialPageRoute( - settings: const RouteSettings( - name: 'home', - ), - builder: (final context) => HomePage( + const settings = RouteSettings( + name: 'home', + ); + Widget builder(final context) => HomePage( account: activeAccount, onThemeChanged: (final theme) { setState(() { _userTheme = theme; }); }, - ), - ), + ); + await _navigatorKey.currentState!.pushAndRemoveUntil( + widget.globalOptions.navigationMode.value == NavigationMode.drawer + ? MaterialPageRoute( + settings: settings, + builder: builder, + ) + : NoAnimationPageRoute( + settings: settings, + builder: builder, + ), (final _) => false, ); } diff --git a/packages/neon/lib/src/apps/files/widgets/file_preview.dart b/packages/neon/lib/src/apps/files/widgets/file_preview.dart index be74267a..2c3e432f 100644 --- a/packages/neon/lib/src/apps/files/widgets/file_preview.dart +++ b/packages/neon/lib/src/apps/files/widgets/file_preview.dart @@ -96,11 +96,8 @@ class FilePreview extends StatelessWidget { ), ], if (previewLoading) ...[ - Center( - child: CircularProgressIndicator( - strokeWidth: 2, - color: color, - ), + const Center( + child: CustomLinearProgressIndicator(), ), ], ], diff --git a/packages/neon/lib/src/apps/news/pages/article.dart b/packages/neon/lib/src/apps/news/pages/article.dart index dee54833..e16b22b7 100644 --- a/packages/neon/lib/src/apps/news/pages/article.dart +++ b/packages/neon/lib/src/apps/news/pages/article.dart @@ -140,6 +140,7 @@ class _NewsArticlePageState extends State { ), body: widget.useWebView ? Stack( + alignment: Alignment.center, children: [ WebView( javascriptMode: JavascriptMode.unrestricted, @@ -160,11 +161,16 @@ class _NewsArticlePageState extends State { }, ), if (_webviewLoading) ...[ - ColoredBox( - color: Theme.of(context).colorScheme.background, - child: const Center( - child: CircularProgressIndicator( - strokeWidth: 3, + Expanded( + child: ColoredBox( + color: Theme.of(context).colorScheme.background, + child: Center( + child: LayoutBuilder( + builder: (final context, final constraints) => SizedBox( + width: constraints.maxWidth / 2, + child: const CustomLinearProgressIndicator(), + ), + ), ), ), ), diff --git a/packages/neon/lib/src/blocs/apps.dart b/packages/neon/lib/src/blocs/apps.dart index 55a8a50f..817ab936 100644 --- a/packages/neon/lib/src/blocs/apps.dart +++ b/packages/neon/lib/src/blocs/apps.dart @@ -36,8 +36,10 @@ class AppsBloc extends $AppsBloc { _$refreshEvent.listen((final _) => _loadApps); _$setActiveAppEvent.listen((final appId) async { final data = (await _appImplementationsSubject.firstWhere((final result) => result.data != null)).data!; - if (data.where((final app) => app.id == appId).isNotEmpty && _activeAppSubject.valueOrNull != appId) { - _activeAppSubject.add(appId); + if (data.where((final app) => app.id == appId).isNotEmpty) { + if (_activeAppSubject.valueOrNull != appId) { + _activeAppSubject.add(appId); + } } else { debugPrint('App $appId not found'); } diff --git a/packages/neon/lib/src/neon.dart b/packages/neon/lib/src/neon.dart index 9e2dfe23..a4ba337f 100644 --- a/packages/neon/lib/src/neon.dart +++ b/packages/neon/lib/src/neon.dart @@ -96,5 +96,6 @@ part 'widgets/exception.dart'; part 'widgets/image_wrapper.dart'; part 'widgets/neon_logo.dart'; part 'widgets/nextcloud_logo.dart'; +part 'widgets/no_animation_page_route.dart'; part 'widgets/result_stream_builder.dart'; part 'widgets/standard_rx_result_builder.dart'; diff --git a/packages/neon/lib/src/pages/home/home.dart b/packages/neon/lib/src/pages/home/home.dart index eb90072f..76360dd3 100644 --- a/packages/neon/lib/src/pages/home/home.dart +++ b/packages/neon/lib/src/pages/home/home.dart @@ -1,5 +1,7 @@ part of '../../neon.dart'; +const kQuickBarWidth = kAvatarSize + 20; + class HomePage extends StatefulWidget { const HomePage({ required this.account, @@ -291,6 +293,14 @@ class _HomePageState extends State with tray.TrayListener, WindowListe ); } + Future _openSettings() async { + await Navigator.of(context).push( + MaterialPageRoute( + builder: (final context) => const SettingsPage(), + ), + ); + } + @override void dispose() { _capabilitiesBloc.dispose(); @@ -345,290 +355,420 @@ class _HomePageState extends State with tray.TrayListener, WindowListe final accountsSnapshot, final _, ) => - WillPopScope( - onWillPop: () async { - if (_scaffoldKey.currentState!.isDrawerOpen) { - Navigator.pop(context); - return true; - } + OptionBuilder( + option: _globalOptions.navigationMode, + builder: (final context, final navigationMode) => WillPopScope( + onWillPop: () async { + if (_scaffoldKey.currentState!.isDrawerOpen) { + Navigator.pop(context); + return true; + } - _scaffoldKey.currentState!.openDrawer(); - return false; - }, - child: Builder( - builder: (final context) { - if (accountsSnapshot.hasData) { - final accounts = accountsSnapshot.data!; - final account = accounts.singleWhere((final account) => account.id == widget.account.id); - return Scaffold( - key: _scaffoldKey, - resizeToAvoidBottomInset: false, - appBar: AppBar( - title: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - if (appsData != null && activeAppIDSnapshot.hasData) ...[ - Flexible( - child: Text( - appsData - .singleWhere((final a) => a.id == activeAppIDSnapshot.data!) - .name(context), - ), - ), - ], - if (appsError != null) ...[ - const SizedBox( - width: 8, - ), - Icon( - Icons.error_outline, - size: 30, - color: Theme.of(context).colorScheme.onPrimary, - ), - ], - if (appsLoading) ...[ - const SizedBox( - width: 8, - ), - SizedBox( - height: 30, - width: 30, - child: CircularProgressIndicator( - color: Theme.of(context).colorScheme.onPrimary, - strokeWidth: 2, - ), - ), - ], - ], - ), - if (accounts.length > 1) ...[ - Text( - account.client.humanReadableID, - style: Theme.of(context).textTheme.bodySmall!, - ), - ], - ], - ), - actions: [ - if (appsData != null && activeAppIDSnapshot.hasData) ...[ - IconButton( - icon: const Icon(Icons.settings), - onPressed: () async { - await Navigator.of(context).push( - MaterialPageRoute( - builder: (final context) => NextcloudAppSpecificSettingsPage( - appImplementation: - appsData.singleWhere((final a) => a.id == activeAppIDSnapshot.data!), - ), - ), - ); - }, - ), - IconButton( - icon: AccountAvatar( - account: account, - requestManager: _requestManager, - ), - onPressed: () async { - await Navigator.of(context).push( - MaterialPageRoute( - builder: (final context) => AccountSpecificSettingsPage( - bloc: accountsBloc, - account: account, - ), - ), - ); - }, - ), - ], - ], - ), - drawer: Drawer( - child: Column( - children: [ - Expanded( - child: Scrollbar( - child: ListView( - // Needed for the drawer header to also render in the statusbar - padding: EdgeInsets.zero, - children: [ - Builder( - builder: (final context) { - if (accountsSnapshot.hasData) { - return DrawerHeader( - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.primary, - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - if (capabilitiesData != null) ...[ - Text( - capabilitiesData.capabilities!.theming!.name!, - style: DefaultTextStyle.of(context).style.copyWith( - color: Theme.of(context).colorScheme.onPrimary, + _scaffoldKey.currentState!.openDrawer(); + return false; + }, + child: Builder( + builder: (final context) { + if (accountsSnapshot.hasData) { + final accounts = accountsSnapshot.data!; + final account = accounts.singleWhere((final account) => account.id == widget.account.id); + + final isQuickBar = navigationMode == NavigationMode.quickBar; + final drawer = Drawer( + width: isQuickBar ? kQuickBarWidth : null, + child: Container( + padding: isQuickBar ? const EdgeInsets.all(5) : null, + color: isQuickBar ? Theme.of(context).appBarTheme.backgroundColor : null, + child: Column( + children: [ + Expanded( + child: Scrollbar( + child: ListView( + // 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: Tooltip( + message: account.client.humanReadableID, + child: IconButton( + onPressed: () { + accountsBloc.setActiveAccount(account); + }, + icon: IntrinsicHeight( + child: AccountAvatar( + account: account, + ), + ), + ), ), - ), - if (capabilitiesData.capabilities!.theming!.logo != null) ...[ - Flexible( - child: CachedURLImage( - url: capabilitiesData.capabilities!.theming!.logo!, - requestManager: _requestManager, - client: widget.account.client, + ), + ], + Container( + margin: const EdgeInsets.only( + top: 10, + ), + child: Divider( + height: 5, + color: Theme.of(context).appBarTheme.foregroundColor, ), ), ], - ] else ...[ - ExceptionWidget( - capabilitiesError, - onRetry: () { - _capabilitiesBloc.refresh(); - }, - ), - CustomLinearProgressIndicator( - visible: capabilitiesLoading, - ), ], - if (accountsSnapshot.data!.length != 1) ...[ - DropdownButtonHideUnderline( - child: DropdownButton( - isExpanded: true, - dropdownColor: Theme.of(context).colorScheme.primary, - iconEnabledColor: Theme.of(context).colorScheme.onPrimary, - value: widget.account.id, - items: accountsSnapshot.data! - .map>( - (final account) => DropdownMenuItem( - value: account.id, - child: AccountTile( - account: account, - dense: true, - textColor: Theme.of(context).colorScheme.onPrimary, + ); + } + return DrawerHeader( + decoration: BoxDecoration( + color: Theme.of(context).appBarTheme.backgroundColor, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + if (capabilitiesData != null) ...[ + Text( + capabilitiesData.capabilities!.theming!.name!, + style: DefaultTextStyle.of(context).style.copyWith( + color: Theme.of(context).appBarTheme.foregroundColor, + ), + ), + if (capabilitiesData.capabilities!.theming!.logo != null) ...[ + Flexible( + child: CachedURLImage( + url: capabilitiesData.capabilities!.theming!.logo!, + requestManager: _requestManager, + client: widget.account.client, + ), + ), + ], + ] else ...[ + ExceptionWidget( + capabilitiesError, + onRetry: () { + _capabilitiesBloc.refresh(); + }, + ), + CustomLinearProgressIndicator( + visible: capabilitiesLoading, + ), + ], + if (accounts.length != 1) ...[ + DropdownButtonHideUnderline( + child: DropdownButton( + isExpanded: true, + dropdownColor: Theme.of(context).colorScheme.primary, + iconEnabledColor: Theme.of(context).appBarTheme.foregroundColor, + value: widget.account.id, + items: accounts + .map>( + (final account) => DropdownMenuItem( + value: account.id, + child: AccountTile( + account: account, + dense: true, + textColor: + Theme.of(context).appBarTheme.foregroundColor, + ), ), - ), - ) - .toList(), - onChanged: (final id) { - for (final account in accountsSnapshot.data!) { - if (account.id == id) { - accountsBloc.setActiveAccount(account); - break; + ) + .toList(), + onChanged: (final id) { + for (final account in accounts) { + if (account.id == id) { + accountsBloc.setActiveAccount(account); + break; + } } - } - }, + }, + ), ), - ), + ], ], - ], - ), - ); - } - return Container(); - }, - ), - ExceptionWidget( - appsError, - onRetry: () { - _appsBloc.refresh(); - }, - ), - CustomLinearProgressIndicator( - visible: appsLoading, - ), - if (appsData != null) ...[ - for (final appImplementation in appsData) ...[ - ListTile( - key: Key('app-${appImplementation.id}'), - title: StreamBuilder( + ), + ); + } + return Container(); + }, + ), + ExceptionWidget( + appsError, + onlyIcon: isQuickBar, + onRetry: () { + _appsBloc.refresh(); + }, + ), + CustomLinearProgressIndicator( + visible: appsLoading, + ), + if (appsData != null) ...[ + for (final appImplementation in appsData) ...[ + StreamBuilder( stream: appImplementation.getUnreadCounter(_appsBloc) ?? BehaviorSubject.seeded(0), - builder: (final context, final unreadCounterSnapshot) => Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text(appImplementation.name(context)), - if (unreadCounterSnapshot.hasData && - unreadCounterSnapshot.data! > 0) ...[ - Text( - unreadCounterSnapshot.data!.toString(), - style: TextStyle( - color: Theme.of(context).colorScheme.primary, - fontWeight: FontWeight.bold, - fontSize: 14, + builder: (final context, final unreadCounterSnapshot) { + final unreadCount = unreadCounterSnapshot.data ?? 0; + if (isQuickBar) { + return Tooltip( + message: appImplementation.name(context), + child: IconButton( + onPressed: () { + _appsBloc.setActiveApp(appImplementation.id); + }, + icon: Stack( + alignment: Alignment.bottomRight, + children: [ + Container( + margin: const EdgeInsets.all(5), + child: appImplementation.buildIcon( + context, + height: kAvatarSize, + width: kAvatarSize, + color: Theme.of(context).appBarTheme.foregroundColor, + ), + ), + if (unreadCount > 0) ...[ + Text( + unreadCount.toString(), + style: TextStyle( + color: Theme.of(context).appBarTheme.foregroundColor, + ), + ), + ], + ], ), ), - ], - ], - ), + ); + } + 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: () { + _appsBloc.setActiveApp(appImplementation.id); + if (navigationMode == NavigationMode.drawer) { + // Don't pop when the drawer is always shown + Navigator.of(context).pop(); + } + }, + ); + }, ), - leading: appImplementation.buildIcon(context), - minLeadingWidth: 0, - onTap: () { - _appsBloc.setActiveApp(appImplementation.id); - Navigator.of(context).pop(); - }, - ), + ], ], ], - ], + ), ), ), - ), - ListTile( - key: const Key('settings'), - title: Text(AppLocalizations.of(context).settings), - leading: const Icon(Icons.settings), - minLeadingWidth: 0, - onTap: () async { - await Navigator.of(context).push( - MaterialPageRoute( - builder: (final context) => const SettingsPage(), - ), - ); - }, - ), - ], - ), - ), - body: Column( - children: [ - ServerStatus( - account: widget.account, - ), - ExceptionWidget( - appsError, - onRetry: () { - _appsBloc.refresh(); - }, - ), - if (appsData != null) ...[ - if (appsData.isEmpty) ...[ - Expanded( - child: Center( - child: Text( - AppLocalizations.of(context).errorNoCompatibleNextcloudAppsFound, - textAlign: TextAlign.center, + if (isQuickBar) ...[ + IconButton( + icon: Icon( + Icons.settings, + color: Theme.of(context).appBarTheme.foregroundColor, ), + onPressed: _openSettings, ), - ), - ] else ...[ - if (activeAppIDSnapshot.hasData) ...[ - Expanded( - child: appsData - .singleWhere((final a) => a.id == activeAppIDSnapshot.data!) - .buildPage(context, _appsBloc), + ] 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 Scaffold( + body: 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: capabilitiesData?.capabilities?.theming?.logo != null + ? CachedURLImage( + url: capabilitiesData!.capabilities!.theming!.logo!, + requestManager: _requestManager, + client: widget.account.client, + ) + : null, + ) + : null, + title: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + if (appsData != null && activeAppIDSnapshot.hasData) ...[ + Flexible( + child: Text( + appsData + .singleWhere((final a) => a.id == activeAppIDSnapshot.data!) + .name(context), + ), + ), + ], + if (appsError != null) ...[ + const SizedBox( + width: 8, + ), + Icon( + Icons.error_outline, + size: 30, + color: Theme.of(context).colorScheme.onPrimary, + ), + ], + if (appsLoading) ...[ + const SizedBox( + width: 8, + ), + Expanded( + child: CustomLinearProgressIndicator( + color: Theme.of(context).appBarTheme.foregroundColor, + ), + ), + ], + ], + ), + if (accounts.length > 1) ...[ + Text( + account.client.humanReadableID, + style: Theme.of(context).textTheme.bodySmall!, + ), + ], + ], + ), + actions: [ + if (appsData != null && activeAppIDSnapshot.hasData) ...[ + IconButton( + icon: const Icon(Icons.settings), + onPressed: () async { + await Navigator.of(context).push( + MaterialPageRoute( + builder: (final context) => NextcloudAppSpecificSettingsPage( + appImplementation: appsData + .singleWhere((final a) => a.id == activeAppIDSnapshot.data!), + ), + ), + ); + }, + ), + IconButton( + icon: IntrinsicWidth( + child: AccountAvatar( + account: account, + ), + ), + onPressed: () async { + await Navigator.of(context).push( + MaterialPageRoute( + builder: (final context) => AccountSpecificSettingsPage( + bloc: accountsBloc, + account: account, + ), + ), + ); + }, + ), + ], + ], + ), + body: Row( + children: [ + if (navigationMode == NavigationMode.quickBar) ...[ + drawer, + ], + Expanded( + child: Column( + children: [ + ServerStatus( + account: widget.account, + ), + ExceptionWidget( + appsError, + onRetry: () { + _appsBloc.refresh(); + }, + ), + if (appsData != null) ...[ + if (appsData.isEmpty) ...[ + Expanded( + child: Center( + child: Text( + AppLocalizations.of(context).errorNoCompatibleNextcloudAppsFound, + textAlign: TextAlign.center, + ), + ), + ), + ] else ...[ + if (activeAppIDSnapshot.hasData) ...[ + Expanded( + child: appsData + .singleWhere((final a) => a.id == activeAppIDSnapshot.data!) + .buildPage(context, _appsBloc), + ), + ], + ], + ], + ], + ), + ), + ], + ), + ), + ), ], - ], - ), - ); - } - return Container(); - }, + ), + ); + } + return Container(); + }, + ), ), ), ), diff --git a/packages/neon/lib/src/pages/settings/settings.dart b/packages/neon/lib/src/pages/settings/settings.dart index 5c11f897..651ab389 100644 --- a/packages/neon/lib/src/pages/settings/settings.dart +++ b/packages/neon/lib/src/pages/settings/settings.dart @@ -107,6 +107,14 @@ class _SettingsPageState extends State { ), ], ), + SettingsCategory( + title: Text(AppLocalizations.of(context).optionsCategoryNavigation), + tiles: [ + DropdownButtonSettingsTile( + option: globalOptions.navigationMode, + ), + ], + ), if (platform.canUsePushNotifications) ...[ SettingsCategory( title: Text(AppLocalizations.of(context).optionsCategoryPushNotifications), diff --git a/packages/neon/lib/src/utils/app_implementation.dart b/packages/neon/lib/src/utils/app_implementation.dart index 57a5867b..9bbb63b5 100644 --- a/packages/neon/lib/src/utils/app_implementation.dart +++ b/packages/neon/lib/src/utils/app_implementation.dart @@ -42,13 +42,14 @@ abstract class AppImplementation SizedBox( height: height, width: width, child: SvgPicture.asset( 'assets/apps/$id.svg', - color: Theme.of(context).colorScheme.primary, + color: color ?? Theme.of(context).colorScheme.primary, ), ); diff --git a/packages/neon/lib/src/utils/global_options.dart b/packages/neon/lib/src/utils/global_options.dart index 60c2641f..7e8dd2f8 100644 --- a/packages/neon/lib/src/utils/global_options.dart +++ b/packages/neon/lib/src/utils/global_options.dart @@ -68,6 +68,7 @@ class GlobalOptions { systemTrayHideToTrayWhenMinimized, rememberLastUsedAccount, initialAccount, + navigationMode, ]; Future reset() async { @@ -192,9 +193,32 @@ class GlobalOptions { late final initialAccount = SelectOption( storage: _storage, key: 'initial-account', - label: (final context) => AppLocalizations.of(context).globaloptionsaccountsInitialAccount, + label: (final context) => AppLocalizations.of(context).globalOptionsAccountsInitialAccount, defaultValue: BehaviorSubject.seeded(null), values: _accountsIDsSubject, enabled: _initialAccountEnabledSubject, ); + + late final navigationMode = SelectOption( + storage: _storage, + key: 'navigation-mode', + label: (final context) => AppLocalizations.of(context).globalOptionsNavigationMode, + defaultValue: BehaviorSubject.seeded( + Platform.isAndroid || Platform.isIOS ? NavigationMode.drawer : NavigationMode.drawerAlwaysVisible, + ), + values: BehaviorSubject.seeded({ + NavigationMode.drawer: (final context) => AppLocalizations.of(context).globalOptionsNavigationModeDrawer, + if (!Platform.isAndroid && !Platform.isIOS) ...{ + NavigationMode.drawerAlwaysVisible: (final context) => + AppLocalizations.of(context).globalOptionsNavigationModeDrawerAlwaysVisible, + }, + NavigationMode.quickBar: (final context) => AppLocalizations.of(context).globalOptionsNavigationModeQuickBar, + }), + ); +} + +enum NavigationMode { + drawer, + drawerAlwaysVisible, + quickBar, } diff --git a/packages/neon/lib/src/widgets/account_avatar.dart b/packages/neon/lib/src/widgets/account_avatar.dart index ab0d3b85..38a2c4c4 100644 --- a/packages/neon/lib/src/widgets/account_avatar.dart +++ b/packages/neon/lib/src/widgets/account_avatar.dart @@ -5,19 +5,18 @@ const kAvatarSize = 40.0; class AccountAvatar extends StatelessWidget { const AccountAvatar({ required this.account, - required this.requestManager, super.key, }); final Account account; - final RequestManager requestManager; @override Widget build(final BuildContext context) => Stack( + alignment: Alignment.center, children: [ ResultStreamBuilder( // TODO: See TODO in cached_url_image.dart - stream: requestManager.wrapBytes( + stream: Provider.of(context, listen: false).wrapBytes( account.client.id, 'accounts-avatar-${account.id}', () async => account.client.core.getAvatar( @@ -48,9 +47,7 @@ class AccountAvatar extends StatelessWidget { ), ], if (avatarLoading) ...[ - const CircularProgressIndicator( - strokeWidth: 2, - ), + const CustomLinearProgressIndicator(), ], ], ), diff --git a/packages/neon/lib/src/widgets/account_tile.dart b/packages/neon/lib/src/widgets/account_tile.dart index 9ffd1af4..7a9027b3 100644 --- a/packages/neon/lib/src/widgets/account_tile.dart +++ b/packages/neon/lib/src/widgets/account_tile.dart @@ -33,9 +33,10 @@ class AccountTile extends StatelessWidget { vertical: -4, ) : null, - leading: AccountAvatar( - account: account, - requestManager: Provider.of(context), + leading: IntrinsicWidth( + child: AccountAvatar( + account: account, + ), ), title: StandardRxResultBuilder( bloc: userDetailsBloc, @@ -64,12 +65,9 @@ class AccountTile extends StatelessWidget { const SizedBox( width: 5, ), - SizedBox( - height: 10, - width: 10, - child: CircularProgressIndicator( - strokeWidth: 1, - color: color, + Expanded( + child: CustomLinearProgressIndicator( + color: textColor, ), ), ], @@ -77,10 +75,12 @@ class AccountTile extends StatelessWidget { const SizedBox( width: 5, ), - Icon( - Icons.error_outline, - size: 20, - color: color, + ExceptionWidget( + userDetailsError, + onlyIcon: true, + onRetry: () { + userDetailsBloc.refresh(); + }, ), ], ], diff --git a/packages/neon/lib/src/widgets/cached_url_image.dart b/packages/neon/lib/src/widgets/cached_url_image.dart index ff4af1e8..551ba5dd 100644 --- a/packages/neon/lib/src/widgets/cached_url_image.dart +++ b/packages/neon/lib/src/widgets/cached_url_image.dart @@ -81,11 +81,9 @@ class CachedURLImage extends StatelessWidget { ), ], if (loading) ...[ - Container( - margin: const EdgeInsets.all(3), - child: const CircularProgressIndicator( - strokeWidth: 2, - ), + SizedBox( + width: width, + child: const CustomLinearProgressIndicator(), ), ], ], diff --git a/packages/neon/lib/src/widgets/custom_linear_progress_indicator.dart b/packages/neon/lib/src/widgets/custom_linear_progress_indicator.dart index 0637103e..cf79b596 100644 --- a/packages/neon/lib/src/widgets/custom_linear_progress_indicator.dart +++ b/packages/neon/lib/src/widgets/custom_linear_progress_indicator.dart @@ -2,20 +2,29 @@ part of '../neon.dart'; class CustomLinearProgressIndicator extends StatelessWidget { const CustomLinearProgressIndicator({ - required this.visible, + this.visible = true, this.margin = const EdgeInsets.symmetric(horizontal: 10), + this.color, + this.backgroundColor = Colors.transparent, super.key, }); final bool visible; final EdgeInsets? margin; + final Color? color; + final Color? backgroundColor; @override Widget build(final BuildContext context) => Container( margin: margin, child: SizedBox( height: 3, - child: visible ? const LinearProgressIndicator() : null, + child: visible + ? LinearProgressIndicator( + color: color, + backgroundColor: backgroundColor, + ) + : null, ), ); } diff --git a/packages/neon/lib/src/widgets/exception.dart b/packages/neon/lib/src/widgets/exception.dart index e3e9fc6a..fc40117b 100644 --- a/packages/neon/lib/src/widgets/exception.dart +++ b/packages/neon/lib/src/widgets/exception.dart @@ -4,11 +4,13 @@ class ExceptionWidget extends StatelessWidget { const ExceptionWidget( this.exception, { required this.onRetry, + this.onlyIcon = false, super.key, }); final dynamic exception; final Function() onRetry; + final bool onlyIcon; static void showSnackbar(final BuildContext context, final dynamic exception) { final details = _getExceptionDetails(context, exception); @@ -35,16 +37,31 @@ class ExceptionWidget extends StatelessWidget { builder: (final context) { final details = _getExceptionDetails(context, exception); + const errorIcon = Icon( + Icons.error_outline, + size: 30, + color: Colors.red, + ); + + if (onlyIcon) { + return IconButton( + onPressed: () async { + if (details.isUnauthorized) { + await _openLoginPage(context); + } else { + onRetry(); + } + }, + icon: errorIcon, + ); + } + return Column( children: [ Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - const Icon( - Icons.error_outline, - size: 30, - color: Colors.red, - ), + errorIcon, const SizedBox( width: 10, ), @@ -65,7 +82,7 @@ class ExceptionWidget extends StatelessWidget { ), ] else ...[ ElevatedButton( - onPressed: () async => onRetry(), + onPressed: onRetry, child: Text(AppLocalizations.of(context).retry), ), ], diff --git a/packages/neon/lib/src/widgets/no_animation_page_route.dart b/packages/neon/lib/src/widgets/no_animation_page_route.dart new file mode 100644 index 00000000..f1f4f8c3 --- /dev/null +++ b/packages/neon/lib/src/widgets/no_animation_page_route.dart @@ -0,0 +1,11 @@ +part of '../neon.dart'; + +class NoAnimationPageRoute extends MaterialPageRoute { + NoAnimationPageRoute({ + required super.builder, + super.settings, + }); + + @override + Duration get transitionDuration => Duration.zero; +}