From 60e5a285d3ee3668c89a73d7cb6e764cfde97bda Mon Sep 17 00:00:00 2001 From: jld3103 Date: Sat, 27 Aug 2022 02:31:02 +0200 Subject: [PATCH] neon: Improve multi-account UI --- packages/neon/lib/l10n/en.arb | 12 +- packages/neon/lib/l10n/localizations.dart | 12 +- packages/neon/lib/l10n/localizations_en.dart | 9 +- .../neon/lib/src/apps/files/blocs/files.dart | 3 +- packages/neon/lib/src/blocs/accounts.dart | 2 +- packages/neon/lib/src/models/account.dart | 8 +- packages/neon/lib/src/pages/home/home.dart | 467 +++++++++--------- .../settings/account_specific_settings.dart | 14 +- .../neon/lib/src/pages/settings/settings.dart | 435 ++++++++-------- .../widgets/account_settings_tile.dart | 16 +- .../neon/lib/src/utils/global_options.dart | 23 +- .../neon/lib/src/widgets/account_tile.dart | 158 +++--- packages/settings/lib/src/options/option.dart | 10 +- 13 files changed, 574 insertions(+), 595 deletions(-) diff --git a/packages/neon/lib/l10n/en.arb b/packages/neon/lib/l10n/en.arb index 9209521a..bf21482a 100644 --- a/packages/neon/lib/l10n/en.arb +++ b/packages/neon/lib/l10n/en.arb @@ -94,18 +94,16 @@ "globalOptionsSystemTrayEnabled": "Enable system tray", "globalOptionsSystemTrayHideToTrayWhenMinimized": "Hide to system tray when minimized", "globalOptionsAccountsRememberLastUsedAccount": "Remember last used account", - "globalOptionsAccountsRemoveConfirm": "Are you sure you want to remove the account {name} from {url}?", - "@globalOptionsAccountsRemoveConfirm": { + "globaloptionsaccountsInitialAccount": "Initial account", + "globalOptionsAccountsAdd": "Add account", + "accountOptionsRemoveConfirm": "Are you sure you want to remove the account {id}?", + "@accountOptionsRemoveConfirm": { "placeholders": { - "name": { - "type": "String" - }, - "url": { + "id": { "type": "String" } } }, - "globalOptionsAccountsAdd": "Add account", "accountOptionsInitialApp": "App to show initially", "accountOptionsAutomatic": "Automatic", "licenses": "Licenses", diff --git a/packages/neon/lib/l10n/localizations.dart b/packages/neon/lib/l10n/localizations.dart index 6cfb5f49..15ee243d 100644 --- a/packages/neon/lib/l10n/localizations.dart +++ b/packages/neon/lib/l10n/localizations.dart @@ -485,11 +485,11 @@ abstract class AppLocalizations { /// **'Remember last used account'** String get globalOptionsAccountsRememberLastUsedAccount; - /// No description provided for @globalOptionsAccountsRemoveConfirm. + /// No description provided for @globaloptionsaccountsInitialAccount. /// /// In en, this message translates to: - /// **'Are you sure you want to remove the account {name} from {url}?'** - String globalOptionsAccountsRemoveConfirm(String name, String url); + /// **'Initial account'** + String get globaloptionsaccountsInitialAccount; /// No description provided for @globalOptionsAccountsAdd. /// @@ -497,6 +497,12 @@ abstract class AppLocalizations { /// **'Add account'** String get globalOptionsAccountsAdd; + /// No description provided for @accountOptionsRemoveConfirm. + /// + /// In en, this message translates to: + /// **'Are you sure you want to remove the account {id}?'** + String accountOptionsRemoveConfirm(String id); + /// No description provided for @accountOptionsInitialApp. /// /// 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 bd3eb208..20a9f1f5 100644 --- a/packages/neon/lib/l10n/localizations_en.dart +++ b/packages/neon/lib/l10n/localizations_en.dart @@ -216,13 +216,16 @@ class AppLocalizationsEn extends AppLocalizations { String get globalOptionsAccountsRememberLastUsedAccount => 'Remember last used account'; @override - String globalOptionsAccountsRemoveConfirm(String name, String url) { - return 'Are you sure you want to remove the account $name from $url?'; - } + String get globaloptionsaccountsInitialAccount => 'Initial account'; @override String get globalOptionsAccountsAdd => 'Add account'; + @override + String accountOptionsRemoveConfirm(String id) { + return 'Are you sure you want to remove the account $id?'; + } + @override String get accountOptionsInitialApp => 'App to show initially'; diff --git a/packages/neon/lib/src/apps/files/blocs/files.dart b/packages/neon/lib/src/apps/files/blocs/files.dart index a2ea437c..d8eb0cf2 100644 --- a/packages/neon/lib/src/apps/files/blocs/files.dart +++ b/packages/neon/lib/src/apps/files/blocs/files.dart @@ -4,6 +4,7 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:neon/src/apps/files/app.dart'; import 'package:neon/src/apps/files/blocs/browser.dart'; +import 'package:neon/src/models/account.dart'; import 'package:neon/src/neon.dart'; import 'package:nextcloud/nextcloud.dart'; import 'package:open_file/open_file.dart'; @@ -79,7 +80,7 @@ class FilesBloc extends $FilesBloc { final file = File( p.join( await _platform.getUserAccessibleAppDataPath(), - '${client.username!}@${Uri.parse(client.baseURL).host}', + client.humanReadableID, 'files', path.join(Platform.pathSeparator), ), diff --git a/packages/neon/lib/src/blocs/accounts.dart b/packages/neon/lib/src/blocs/accounts.dart index 1ba28b1b..6c0234c3 100644 --- a/packages/neon/lib/src/blocs/accounts.dart +++ b/packages/neon/lib/src/blocs/accounts.dart @@ -106,7 +106,7 @@ class AccountsBloc extends $AccountsBloc { final lastUsedAccountID = _storage.getString(_keyLastUsedAccount); _activeAccountSubject.add(accounts.singleWhere((final account) => account.id == lastUsedAccountID)); } else { - _globalOptions.lastAccount.stream.first.then((final lastAccount) { + _globalOptions.initialAccount.stream.first.then((final lastAccount) { final matches = accounts.where((final account) => account.id == lastAccount).toList(); if (matches.isNotEmpty) { _activeAccountSubject.add(matches[0]); diff --git a/packages/neon/lib/src/models/account.dart b/packages/neon/lib/src/models/account.dart index 54ca1090..5ed9194c 100644 --- a/packages/neon/lib/src/models/account.dart +++ b/packages/neon/lib/src/models/account.dart @@ -70,7 +70,7 @@ class Account { Map _idCache = {}; -extension NextcloudClientID on NextcloudClient { +extension NextcloudClientHelpers on NextcloudClient { String get id { final key = '$username@$baseURL'; if (_idCache[key] != null) { @@ -78,6 +78,12 @@ extension NextcloudClientID on NextcloudClient { } return _idCache[key] = sha1.convert(utf8.encode(key)).toString(); } + + String get humanReadableID { + final uri = Uri.parse(baseURL); + // Maybe also show path if it is not '/' ? + return '${username!}@${uri.port != 443 ? '${uri.host}:${uri.port}' : uri.host}'; + } } class AccountSpecificOptions { diff --git a/packages/neon/lib/src/pages/home/home.dart b/packages/neon/lib/src/pages/home/home.dart index 8fd9c260..aada62fe 100644 --- a/packages/neon/lib/src/pages/home/home.dart +++ b/packages/neon/lib/src/pages/home/home.dart @@ -327,53 +327,63 @@ class _HomePageState extends State with tray.TrayListener, WindowListe _scaffoldKey.currentState!.openDrawer(); return false; }, - child: Scaffold( - key: _scaffoldKey, - resizeToAvoidBottomInset: false, - appBar: AppBar( - title: Row( - children: [ - Expanded( - child: Row( + 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: [ - 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, - ), + 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, ), ], - const SizedBox( - width: 8, - ), ], ), - ), - Row( - children: [ + actions: [ if (appsData != null && activeAppIDSnapshot.hasData) ...[ IconButton( icon: const Icon(Icons.settings), @@ -388,209 +398,210 @@ class _HomePageState extends State with tray.TrayListener, WindowListe ); }, ), - Builder( - builder: (final context) { - if (accountsSnapshot.hasData) { - final matches = accountsSnapshot.data! - .where( - (final account) => account.id == widget.account.id, - ) - .toList(); - if (matches.length == 1) { - return AccountAvatar( - account: matches[0], - requestManager: _requestManager, - ); - } - } - return Container(); + 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, - ), - ), - 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, + 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, ), - ], - 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, + 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, ), + ), + if (capabilitiesData.capabilities!.theming!.logo != null) ...[ + Flexible( + child: CachedURLImage( + url: capabilitiesData.capabilities!.theming!.logo!, + requestManager: _requestManager, + client: widget.account.client, ), - ) - .toList(), - onChanged: (final id) { - for (final account in accountsSnapshot.data!) { - if (account.id == id) { - accountsBloc.setActiveAccount(account); - break; - } - } - }, - ), + ), + ], + ] 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, + ), + ), + ) + .toList(), + onChanged: (final id) { + for (final account in accountsSnapshot.data!) { + 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) ...[ - if (appsData.map((final a) => a.id).contains(appImplementation.id)) ...[ - ListTile( - key: Key('app-${appImplementation.id}'), - title: 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, - ), - ), - ], - ], - ), - ), - leading: appImplementation.buildIcon(context), - minLeadingWidth: 0, - onTap: () { - _appsBloc.setActiveApp(appImplementation.id); - Navigator.of(context).pop(); + ); + } + return Container(); }, ), + ExceptionWidget( + appsError, + onRetry: () { + _appsBloc.refresh(); + }, + ), + CustomLinearProgressIndicator( + visible: appsLoading, + ), + if (appsData != null) ...[ + for (final appImplementation in appsData) ...[ + if (appsData.map((final a) => a.id).contains(appImplementation.id)) ...[ + ListTile( + key: Key('app-${appImplementation.id}'), + title: 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, + ), + ), + ], + ], + ), + ), + 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, + 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(), + ), + ); + }, ), - ), + ], ), - ] else ...[ - if (activeAppIDSnapshot.hasData) ...[ - Expanded( - child: appsData - .singleWhere((final a) => a.id == activeAppIDSnapshot.data!) - .buildPage(context, _appsBloc), + ), + 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, + ), + ), + ), + ] else ...[ + if (activeAppIDSnapshot.hasData) ...[ + Expanded( + child: appsData + .singleWhere((final a) => a.id == activeAppIDSnapshot.data!) + .buildPage(context, _appsBloc), + ), + ], + ], + ], ], - ], - ], - ], - ), + ), + ); + } + return Container(); + }, ), ), ), diff --git a/packages/neon/lib/src/pages/settings/account_specific_settings.dart b/packages/neon/lib/src/pages/settings/account_specific_settings.dart index a91ff1a6..634c8e84 100644 --- a/packages/neon/lib/src/pages/settings/account_specific_settings.dart +++ b/packages/neon/lib/src/pages/settings/account_specific_settings.dart @@ -11,7 +11,7 @@ class AccountSpecificSettingsPage extends StatelessWidget { final Account account; late final _options = bloc.getOptions(account)!; - late final _name = '${account.username}@${Uri.parse(account.serverURL).host}'; + late final _name = account.client.humanReadableID; @override Widget build(final BuildContext context) => Scaffold( @@ -19,6 +19,18 @@ class AccountSpecificSettingsPage extends StatelessWidget { appBar: AppBar( title: Text(_name), actions: [ + IconButton( + onPressed: () async { + if (await showConfirmationDialog( + context, + AppLocalizations.of(context).accountOptionsRemoveConfirm(account.client.humanReadableID), + )) { + bloc.removeAccount(account); + Navigator.of(context).pop(); + } + }, + icon: const Icon(MdiIcons.delete), + ), IconButton( onPressed: () async { if (await showConfirmationDialog( diff --git a/packages/neon/lib/src/pages/settings/settings.dart b/packages/neon/lib/src/pages/settings/settings.dart index 9b59c1b7..d355d29d 100644 --- a/packages/neon/lib/src/pages/settings/settings.dart +++ b/packages/neon/lib/src/pages/settings/settings.dart @@ -72,271 +72,226 @@ class _SettingsPageState extends State { final context, final pushNotificationsEnabledEnabledSnapshot, ) => - OptionBuilder( - option: globalOptions.rememberLastUsedAccount, - builder: (final context, final rememberLastUsedAccount) => OptionBuilder( - option: globalOptions.lastAccount, - builder: (final context, final lastAccount) => SettingsList( - categories: [ - SettingsCategory( - title: Text(AppLocalizations.of(context).settingsApps), - tiles: [ - for (final appImplementation in appImplementations) ...[ - if (appImplementation.options.options.isNotEmpty) ...[ - CustomSettingsTile( - leading: appImplementation.buildIcon(context), - title: Text(appImplementation.name(context)), - onTap: () async { - await Navigator.of(context).push( - MaterialPageRoute( - builder: (final context) => NextcloudAppSpecificSettingsPage( - appImplementation: appImplementation, - ), - ), - ); - }, - ), - ], - ], + SettingsList( + categories: [ + SettingsCategory( + title: Text(AppLocalizations.of(context).settingsApps), + tiles: [ + for (final appImplementation in appImplementations) ...[ + if (appImplementation.options.options.isNotEmpty) ...[ + CustomSettingsTile( + leading: appImplementation.buildIcon(context), + title: Text(appImplementation.name(context)), + onTap: () async { + await Navigator.of(context).push( + MaterialPageRoute( + builder: (final context) => NextcloudAppSpecificSettingsPage( + appImplementation: appImplementation, + ), + ), + ); + }, + ), ], + ], + ], + ), + SettingsCategory( + title: Text(AppLocalizations.of(context).optionsCategoryTheme), + tiles: [ + DropdownButtonSettingsTile( + option: globalOptions.themeMode, ), - SettingsCategory( - title: Text(AppLocalizations.of(context).optionsCategoryTheme), - tiles: [ - DropdownButtonSettingsTile( - option: globalOptions.themeMode, + CheckBoxSettingsTile( + option: globalOptions.themeOLEDAsDark, + ), + ], + ), + if (platform.canUsePushNotifications) ...[ + SettingsCategory( + title: Text(AppLocalizations.of(context).optionsCategoryPushNotifications), + tiles: [ + TextSettingsTile( + text: AppLocalizations.of(context).globalOptionsPushNotificationsNotice, + style: const TextStyle( + fontWeight: FontWeight.w300, + fontStyle: FontStyle.italic, ), - CheckBoxSettingsTile( - option: globalOptions.themeOLEDAsDark, + ), + if (pushNotificationsEnabledEnabledSnapshot.data != null && + !pushNotificationsEnabledEnabledSnapshot.data!) ...[ + TextSettingsTile( + text: AppLocalizations.of(context).globalOptionsPushNotificationsEnabledDisabledNotice, + style: const TextStyle( + fontWeight: FontWeight.w600, + fontStyle: FontStyle.italic, + color: Colors.red, + ), ), ], - ), - if (platform.canUsePushNotifications) ...[ - SettingsCategory( - title: Text(AppLocalizations.of(context).optionsCategoryPushNotifications), - tiles: [ - TextSettingsTile( - text: AppLocalizations.of(context).globalOptionsPushNotificationsNotice, - style: const TextStyle( - fontWeight: FontWeight.w300, - fontStyle: FontStyle.italic, - ), - ), - if (pushNotificationsEnabledEnabledSnapshot.data != null && - !pushNotificationsEnabledEnabledSnapshot.data!) ...[ - TextSettingsTile( - text: AppLocalizations.of(context).globalOptionsPushNotificationsEnabledDisabledNotice, - style: const TextStyle( - fontWeight: FontWeight.w600, - fontStyle: FontStyle.italic, - color: Colors.red, - ), - ), - ], - CheckBoxSettingsTile( - option: globalOptions.pushNotificationsEnabled, - ), - DropdownButtonSettingsTile( - option: globalOptions.pushNotificationsDistributor, - ), - ], + CheckBoxSettingsTile( + option: globalOptions.pushNotificationsEnabled, ), - ], - if (platform.canUseWindowManager) ...[ - SettingsCategory( - title: Text(AppLocalizations.of(context).optionsCategoryStartup), - tiles: [ - CheckBoxSettingsTile( - option: globalOptions.startupMinimized, - ), - CheckBoxSettingsTile( - option: globalOptions.startupMinimizeInsteadOfExit, - ), - ], + DropdownButtonSettingsTile( + option: globalOptions.pushNotificationsDistributor, ), ], - if (platform.canUseWindowManager && platform.canUseSystemTray) ...[ - SettingsCategory( - title: Text(AppLocalizations.of(context).optionsCategorySystemTray), - tiles: [ - CheckBoxSettingsTile( - option: globalOptions.systemTrayEnabled, - ), - CheckBoxSettingsTile( - option: globalOptions.systemTrayHideToTrayWhenMinimized, - ), - ], + ), + ], + if (platform.canUseWindowManager) ...[ + SettingsCategory( + title: Text(AppLocalizations.of(context).optionsCategoryStartup), + tiles: [ + CheckBoxSettingsTile( + option: globalOptions.startupMinimized, + ), + CheckBoxSettingsTile( + option: globalOptions.startupMinimizeInsteadOfExit, ), ], - if (accountsSnapshot.hasData) ...[ - SettingsCategory( - title: Text(AppLocalizations.of(context).optionsCategoryAccounts), - tiles: [ - if (accountsSnapshot.data!.length > 1) ...[ - CheckBoxSettingsTile( - option: globalOptions.rememberLastUsedAccount, - ), - ], - for (final account in accountsSnapshot.data!) ...[ - AccountSettingsTile( - account: account, - color: activeAccountSnapshot.data == account && accountsSnapshot.data!.length > 1 - ? Theme.of(context).colorScheme.primary - : Theme.of(context).colorScheme.onBackground, - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - PopupMenuButton( - itemBuilder: (final context) => [ - PopupMenuItem( - value: SettingsAccountAction.settings, - child: Text(AppLocalizations.of(context).settings), - ), - PopupMenuItem( - value: SettingsAccountAction.delete, - child: Text(AppLocalizations.of(context).delete), - ), - ], - onSelected: (final action) async { - switch (action) { - case SettingsAccountAction.settings: - await Navigator.of(context).push( - MaterialPageRoute( - builder: (final context) => AccountSpecificSettingsPage( - bloc: accountsBloc, - account: account, - ), - ), - ); - break; - case SettingsAccountAction.delete: - if (await showConfirmationDialog( - context, - AppLocalizations.of(context).globalOptionsAccountsRemoveConfirm( - account.username, - account.serverURL, - ), - )) { - accountsBloc.removeAccount(account); - } - break; - } - }, - ), - if (accountsSnapshot.data!.length > 1 && rememberLastUsedAccount != null) ...[ - Radio( - groupValue: lastAccount, - value: account.id, - onChanged: !rememberLastUsedAccount ? globalOptions.lastAccount.set : null, - ), - ], - ], - ), - ), - ], - CustomSettingsTile( - title: ElevatedButton.icon( - onPressed: () async { - await Navigator.of(context).push( - MaterialPageRoute( - builder: (final context) => const LoginPage(), - ), - ); - }, - icon: const Icon(MdiIcons.accountPlus), - label: Text(AppLocalizations.of(context).globalOptionsAccountsAdd), - ), - ) - ], + ), + ], + if (platform.canUseWindowManager && platform.canUseSystemTray) ...[ + SettingsCategory( + title: Text(AppLocalizations.of(context).optionsCategorySystemTray), + tiles: [ + CheckBoxSettingsTile( + option: globalOptions.systemTrayEnabled, + ), + CheckBoxSettingsTile( + option: globalOptions.systemTrayHideToTrayWhenMinimized, ), ], - SettingsCategory( - title: Text(AppLocalizations.of(context).optionsCategoryOther), - tiles: [ - CustomSettingsTile( - leading: Icon( - MdiIcons.scriptText, - color: Theme.of(context).colorScheme.primary, - ), - title: Text(AppLocalizations.of(context).licenses), - onTap: () async { - showLicensePage( - context: context, - applicationName: AppLocalizations.of(context).appName, - applicationIcon: const NeonLogo( - withoutText: true, + ), + ], + if (accountsSnapshot.hasData) ...[ + SettingsCategory( + title: Text(AppLocalizations.of(context).optionsCategoryAccounts), + tiles: [ + if (accountsSnapshot.data!.length > 1) ...[ + CheckBoxSettingsTile( + option: globalOptions.rememberLastUsedAccount, + ), + DropdownButtonSettingsTile( + option: globalOptions.initialAccount, + ), + ], + for (final account in accountsSnapshot.data!) ...[ + AccountSettingsTile( + account: account, + onTap: () { + Navigator.of(context).push( + MaterialPageRoute( + builder: (final context) => AccountSpecificSettingsPage( + bloc: accountsBloc, + account: account, + ), ), - applicationLegalese: await rootBundle.loadString('assets/LEGALESE.txt'), - applicationVersion: Provider.of(context, listen: false).version, ); }, ), - CustomSettingsTile( - leading: Icon( - MdiIcons.export, - color: Theme.of(context).colorScheme.primary, - ), - title: Text(AppLocalizations.of(context).settingsExport), - onTap: () async { - try { - final fileName = - 'nextcloud-neon-settings-${DateTime.now().millisecondsSinceEpoch ~/ 1000}.json.base64'; - final data = base64.encode( - utf8.encode( - json.encode( - settingsExportHelper.toJsonExport(), - ), - ), - ); - await saveFileWithPickDialog(fileName, Uint8List.fromList(utf8.encode(data))); - } catch (e, s) { - debugPrint(e.toString()); - debugPrintStack(stackTrace: s); - ExceptionWidget.showSnackbar(context, e); - } + ], + CustomSettingsTile( + title: ElevatedButton.icon( + onPressed: () async { + await Navigator.of(context).push( + MaterialPageRoute( + builder: (final context) => const LoginPage(), + ), + ); }, + icon: const Icon(MdiIcons.accountPlus), + label: Text(AppLocalizations.of(context).globalOptionsAccountsAdd), ), - CustomSettingsTile( - leading: Icon( - MdiIcons.import, - color: Theme.of(context).colorScheme.primary, + ) + ], + ), + ], + SettingsCategory( + title: Text(AppLocalizations.of(context).optionsCategoryOther), + tiles: [ + CustomSettingsTile( + leading: Icon( + MdiIcons.scriptText, + color: Theme.of(context).colorScheme.primary, + ), + title: Text(AppLocalizations.of(context).licenses), + onTap: () async { + showLicensePage( + context: context, + applicationName: AppLocalizations.of(context).appName, + applicationIcon: const NeonLogo( + withoutText: true, ), - title: Text(AppLocalizations.of(context).settingsImport), - onTap: () async { - try { - final result = await FilePicker.platform.pickFiles( - withData: true, - ); - - if (result == null) { - return; - } + applicationLegalese: await rootBundle.loadString('assets/LEGALESE.txt'), + applicationVersion: Provider.of(context, listen: false).version, + ); + }, + ), + CustomSettingsTile( + leading: Icon( + MdiIcons.export, + color: Theme.of(context).colorScheme.primary, + ), + title: Text(AppLocalizations.of(context).settingsExport), + onTap: () async { + try { + final fileName = + 'nextcloud-neon-settings-${DateTime.now().millisecondsSinceEpoch ~/ 1000}.json.base64'; + final data = base64.encode( + utf8.encode( + json.encode( + settingsExportHelper.toJsonExport(), + ), + ), + ); + await saveFileWithPickDialog(fileName, Uint8List.fromList(utf8.encode(data))); + } catch (e, s) { + debugPrint(e.toString()); + debugPrintStack(stackTrace: s); + ExceptionWidget.showSnackbar(context, e); + } + }, + ), + CustomSettingsTile( + leading: Icon( + MdiIcons.import, + color: Theme.of(context).colorScheme.primary, + ), + title: Text(AppLocalizations.of(context).settingsImport), + onTap: () async { + try { + final result = await FilePicker.platform.pickFiles( + withData: true, + ); - if (!result.files.single.path!.endsWith('.json.base64')) { - if (mounted) { - ExceptionWidget.showSnackbar( - context, - AppLocalizations.of(context).settingsImportWrongFileExtension, - ); - } - return; - } + if (result == null) { + return; + } - final data = - json.decode(utf8.decode(base64.decode(utf8.decode(result.files.single.bytes!)))); - await settingsExportHelper.applyFromJson(data as Map); - } catch (e, s) { - debugPrint(e.toString()); - debugPrintStack(stackTrace: s); - ExceptionWidget.showSnackbar(context, e); + if (!result.files.single.path!.endsWith('.json.base64')) { + if (mounted) { + ExceptionWidget.showSnackbar( + context, + AppLocalizations.of(context).settingsImportWrongFileExtension, + ); } - }, - ), - ], + return; + } + + final data = + json.decode(utf8.decode(base64.decode(utf8.decode(result.files.single.bytes!)))); + await settingsExportHelper.applyFromJson(data as Map); + } catch (e, s) { + debugPrint(e.toString()); + debugPrintStack(stackTrace: s); + ExceptionWidget.showSnackbar(context, e); + } + }, ), ], ), - ), + ], ), ), ); diff --git a/packages/neon/lib/src/pages/settings/widgets/account_settings_tile.dart b/packages/neon/lib/src/pages/settings/widgets/account_settings_tile.dart index f92a2227..541d4474 100644 --- a/packages/neon/lib/src/pages/settings/widgets/account_settings_tile.dart +++ b/packages/neon/lib/src/pages/settings/widgets/account_settings_tile.dart @@ -5,22 +5,20 @@ class AccountSettingsTile extends SettingsTile { required this.account, this.color, this.trailing, + this.onTap, super.key, }); final Account account; final Color? color; final Widget? trailing; + final VoidCallback? onTap; @override - Widget build(final BuildContext context) => Container( - padding: const EdgeInsets.symmetric( - horizontal: 16, - ), - child: AccountTile( - account: account, - color: color, - trailing: trailing, - ), + Widget build(final BuildContext context) => AccountTile( + account: account, + color: color, + trailing: trailing, + onTap: onTap, ); } diff --git a/packages/neon/lib/src/utils/global_options.dart b/packages/neon/lib/src/utils/global_options.dart index 9bf6724b..c7f2b2c5 100644 --- a/packages/neon/lib/src/utils/global_options.dart +++ b/packages/neon/lib/src/utils/global_options.dart @@ -22,20 +22,26 @@ class GlobalOptions { }); rememberLastUsedAccount.stream.listen((final remember) async { + _initialAccountEnabledSubject.add(!remember); if (remember) { - await lastAccount.set(null); + await initialAccount.set(null); } else { - await lastAccount.set((await lastAccount.values.first).keys.toList()[0]); + // Only override the initial account if there already has been a value, + // which means it's not the initial emit from rememberLastUsedAccount + if (initialAccount.hasValue) { + await initialAccount.set((await initialAccount.values.first).keys.toList()[0]); + } } }); } final Storage _storage; final PackageInfo _packageInfo; - final _accountsIDsSubject = BehaviorSubject>(); final _themeOLEDAsDarkEnabledSubject = BehaviorSubject(); final _pushNotificationsEnabledEnabledSubject = BehaviorSubject(); final _pushNotificationsDistributorsSubject = BehaviorSubject>(); + final _accountsIDsSubject = BehaviorSubject>(); + final _initialAccountEnabledSubject = BehaviorSubject(); late final _distributorsMap = { _packageInfo.packageName: (final context) => @@ -61,7 +67,7 @@ class GlobalOptions { systemTrayEnabled, systemTrayHideToTrayWhenMinimized, rememberLastUsedAccount, - lastAccount, + initialAccount, ]; Future reset() async { @@ -84,7 +90,7 @@ class GlobalOptions { } _accountsIDsSubject.add({ for (final account in accounts) ...{ - account.id: (final _) => '', + account.id: (final context) => account.client.humanReadableID, }, }); } @@ -183,11 +189,12 @@ class GlobalOptions { defaultValue: BehaviorSubject.seeded(true), ); - late final lastAccount = SelectOption( + late final initialAccount = SelectOption( storage: _storage, - key: 'last-account', - label: (final _) => '', + key: 'initial-account', + label: (final context) => AppLocalizations.of(context).globaloptionsaccountsInitialAccount, defaultValue: BehaviorSubject.seeded(null), values: _accountsIDsSubject, + enabled: _initialAccountEnabledSubject, ); } diff --git a/packages/neon/lib/src/widgets/account_tile.dart b/packages/neon/lib/src/widgets/account_tile.dart index 2d386ed8..88545565 100644 --- a/packages/neon/lib/src/widgets/account_tile.dart +++ b/packages/neon/lib/src/widgets/account_tile.dart @@ -3,14 +3,18 @@ part of '../neon.dart'; class AccountTile extends StatefulWidget { const AccountTile({ required this.account, - this.trailing, this.color, + this.trailing, + this.onTap, + this.dense = false, super.key, }); final Account account; - final Widget? trailing; final Color? color; + final Widget? trailing; + final VoidCallback? onTap; + final bool dense; @override State createState() => _AccountTileState(); @@ -27,101 +31,71 @@ class _AccountTileState extends State { } @override - Widget build(final BuildContext context) => Row( - children: [ - AccountAvatar( - account: widget.account, - requestManager: Provider.of(context), - ), - const SizedBox( - width: 10, - ), - Expanded( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - StandardRxResultBuilder( - bloc: _userDetailsBloc, - state: (final bloc) => bloc.userDetails, - builder: ( - final context, - final userDetailsData, - final userDetailsError, - final userDetailsLoading, - final _, - ) => - Row( - children: [ - if (userDetailsData != null) ...[ - Text( - userDetailsData.getDisplayName()!, - style: Theme.of(context).textTheme.bodyLarge!.copyWith( - color: widget.color, - ), - ), - const SizedBox( - width: 5, - ), - Text( - '(', - style: Theme.of(context).textTheme.bodySmall!.copyWith( - color: widget.color, - ), - ), - ], - Text( - widget.account.username, - style: Theme.of(context).textTheme.bodySmall!.copyWith( - color: widget.color, - ), - ), - if (userDetailsData != null) ...[ - Text( - ')', - style: Theme.of(context).textTheme.bodySmall!.copyWith( - color: widget.color, - ), - ), - ], - if (userDetailsLoading) ...[ - const SizedBox( - width: 5, - ), - SizedBox( - height: 10, - width: 10, - child: CircularProgressIndicator( - strokeWidth: 1, - color: widget.color, - ), - ), - ], - if (userDetailsError != null) ...[ - const SizedBox( - width: 5, - ), - Icon( - Icons.error_outline, - size: 20, - color: widget.color, - ), - ], - ], - ), - ), + Widget build(final BuildContext context) => ListTile( + onTap: widget.onTap, + dense: widget.dense, + contentPadding: widget.dense ? EdgeInsets.zero : null, + visualDensity: widget.dense + ? const VisualDensity( + horizontal: -4, + vertical: -4, + ) + : null, + leading: AccountAvatar( + account: widget.account, + requestManager: Provider.of(context), + ), + title: StandardRxResultBuilder( + bloc: _userDetailsBloc, + state: (final bloc) => bloc.userDetails, + builder: ( + final context, + final userDetailsData, + final userDetailsError, + final userDetailsLoading, + final _, + ) => + Row( + children: [ + if (userDetailsData != null) ...[ Text( - widget.account.serverURL, - style: Theme.of(context).textTheme.bodySmall!.copyWith( + userDetailsData.getDisplayName()!, + style: Theme.of(context).textTheme.bodyLarge!.copyWith( color: widget.color, ), ), ], - ), + if (userDetailsLoading) ...[ + const SizedBox( + width: 5, + ), + SizedBox( + height: 10, + width: 10, + child: CircularProgressIndicator( + strokeWidth: 1, + color: widget.color, + ), + ), + ], + if (userDetailsError != null) ...[ + const SizedBox( + width: 5, + ), + Icon( + Icons.error_outline, + size: 20, + color: widget.color, + ), + ], + ], ), - if (widget.trailing != null) ...[ - widget.trailing!, - ], - ], + ), + subtitle: Text( + widget.account.client.humanReadableID, + style: Theme.of(context).textTheme.bodySmall!.copyWith( + color: widget.color, + ), + ), ); } diff --git a/packages/settings/lib/src/options/option.dart b/packages/settings/lib/src/options/option.dart index 2127dd00..648db375 100644 --- a/packages/settings/lib/src/options/option.dart +++ b/packages/settings/lib/src/options/option.dart @@ -27,11 +27,19 @@ abstract class Option { late BehaviorSubject stream; T get value { + if (hasValue) { + return stream.value; + } + + return defaultValue.value; + } + + bool get hasValue { if (!enabled.value) { throw OptionDisableException(); } - return stream.value ?? defaultValue.value; + return stream.hasValue; } Future reset() async {