Browse Source

Merge pull request #456 from Leptopoda/feature/account_settings

Feature/account switcher
pull/473/head
Nikolas Rimikis 1 year ago committed by GitHub
parent
commit
243397dbfa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 8
      packages/app/pubspec.lock
  2. 1
      packages/neon/neon/lib/l10n/en.arb
  3. 6
      packages/neon/neon/lib/l10n/localizations.dart
  4. 3
      packages/neon/neon/lib/l10n/localizations_en.dart
  5. 190
      packages/neon/neon/lib/src/pages/account_settings.dart
  6. 98
      packages/neon/neon/lib/src/pages/nextcloud_app_settings.dart
  7. 511
      packages/neon/neon/lib/src/pages/settings.dart
  8. 7
      packages/neon/neon/lib/src/router.dart
  9. 50
      packages/neon/neon/lib/src/router.g.dart
  10. 27
      packages/neon/neon/lib/src/settings/widgets/settings_list.dart
  11. 3
      packages/neon/neon/lib/src/utils/global_popups.dart
  12. 92
      packages/neon/neon/lib/src/widgets/account_switcher.dart
  13. 1
      packages/neon/neon/lib/src/widgets/account_tile.dart
  14. 16
      packages/neon/neon/lib/src/widgets/app_bar.dart
  15. 122
      packages/neon/neon/lib/src/widgets/drawer.dart
  16. 1
      packages/neon/neon/pubspec.yaml

8
packages/app/pubspec.lock

@ -1037,6 +1037,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.1.6" version: "0.1.6"
scrollable_positioned_list:
dependency: transitive
description:
name: scrollable_positioned_list
sha256: "1b54d5f1329a1e263269abc9e2543d90806131aa14fe7c6062a8054d57249287"
url: "https://pub.dev"
source: hosted
version: "0.3.8"
share_plus: share_plus:
dependency: transitive dependency: transitive
description: description:

1
packages/neon/neon/lib/l10n/en.arb

@ -82,6 +82,7 @@
"settings": "Settings", "settings": "Settings",
"settingsApps": "Apps", "settingsApps": "Apps",
"settingsAccount": "Account", "settingsAccount": "Account",
"settingsAccountManage": "Manage accounts",
"settingsExport": "Export settings", "settingsExport": "Export settings",
"settingsImport": "Import settings", "settingsImport": "Import settings",
"settingsImportWrongFileExtension": "Settings import has wrong file extension (has to be .json.base64)", "settingsImportWrongFileExtension": "Settings import has wrong file extension (has to be .json.base64)",

6
packages/neon/neon/lib/l10n/localizations.dart

@ -341,6 +341,12 @@ abstract class AppLocalizations {
/// **'Account'** /// **'Account'**
String get settingsAccount; String get settingsAccount;
/// No description provided for @settingsAccountManage.
///
/// In en, this message translates to:
/// **'Manage accounts'**
String get settingsAccountManage;
/// No description provided for @settingsExport. /// No description provided for @settingsExport.
/// ///
/// In en, this message translates to: /// In en, this message translates to:

3
packages/neon/neon/lib/l10n/localizations_en.dart

@ -160,6 +160,9 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get settingsAccount => 'Account'; String get settingsAccount => 'Account';
@override
String get settingsAccountManage => 'Manage accounts';
@override @override
String get settingsExport => 'Export settings'; String get settingsExport => 'Export settings';

190
packages/neon/neon/lib/src/pages/account_settings.dart

@ -10,13 +10,14 @@ import 'package:neon/src/settings/widgets/custom_settings_tile.dart';
import 'package:neon/src/settings/widgets/dropdown_button_settings_tile.dart'; import 'package:neon/src/settings/widgets/dropdown_button_settings_tile.dart';
import 'package:neon/src/settings/widgets/settings_category.dart'; import 'package:neon/src/settings/widgets/settings_category.dart';
import 'package:neon/src/settings/widgets/settings_list.dart'; import 'package:neon/src/settings/widgets/settings_list.dart';
import 'package:neon/src/theme/dialog.dart';
import 'package:neon/src/utils/confirmation_dialog.dart'; import 'package:neon/src/utils/confirmation_dialog.dart';
import 'package:neon/src/widgets/exception.dart'; import 'package:neon/src/widgets/exception.dart';
import 'package:neon/src/widgets/linear_progress_indicator.dart'; import 'package:neon/src/widgets/linear_progress_indicator.dart';
import 'package:nextcloud/nextcloud.dart'; import 'package:nextcloud/nextcloud.dart';
class AccountSettingsPage extends StatelessWidget { class AccountSettingsPage extends StatelessWidget {
AccountSettingsPage({ const AccountSettingsPage({
required this.bloc, required this.bloc,
required this.account, required this.account,
super.key, super.key,
@ -25,104 +26,115 @@ class AccountSettingsPage extends StatelessWidget {
final AccountsBloc bloc; final AccountsBloc bloc;
final Account account; final Account account;
late final _options = bloc.getOptionsFor(account);
late final _userDetailsBloc = bloc.getUserDetailsBlocFor(account);
late final _name = account.client.humanReadableID;
@override @override
Widget build(final BuildContext context) => Scaffold( Widget build(final BuildContext context) {
resizeToAvoidBottomInset: false, final options = bloc.getOptionsFor(account);
appBar: AppBar( final userDetailsBloc = bloc.getUserDetailsBlocFor(account);
title: Text(_name), final name = account.client.humanReadableID;
actions: [
IconButton( final appBar = AppBar(
onPressed: () async { title: Text(name),
if (await showConfirmationDialog( actions: [
context, IconButton(
AppLocalizations.of(context).accountOptionsRemoveConfirm(account.client.humanReadableID), onPressed: () async {
)) { if (await showConfirmationDialog(
final isActive = bloc.activeAccount.value == account; context,
AppLocalizations.of(context).accountOptionsRemoveConfirm(account.client.humanReadableID),
)) {
final isActive = bloc.activeAccount.value == account;
bloc.removeAccount(account); bloc.removeAccount(account);
// ignore: use_build_context_synchronously // ignore: use_build_context_synchronously
if (!context.mounted) { if (!context.mounted) {
return; return;
} }
if (isActive) { if (isActive) {
const HomeRoute().go(context); const HomeRoute().go(context);
} else { } else {
Navigator.of(context).pop(); Navigator.of(context).pop();
} }
} }
}, },
tooltip: AppLocalizations.of(context).accountOptionsRemove, tooltip: AppLocalizations.of(context).accountOptionsRemove,
icon: Icon(MdiIcons.delete), icon: Icon(MdiIcons.delete),
),
IconButton(
onPressed: () async {
if (await showConfirmationDialog(
context,
AppLocalizations.of(context).settingsResetForConfirmation(_name),
)) {
await _options.reset();
}
},
tooltip: AppLocalizations.of(context).settingsResetFor(_name),
icon: Icon(MdiIcons.cogRefresh),
),
],
), ),
body: ResultBuilder<ProvisioningApiUserDetails>.behaviorSubject( IconButton(
stream: _userDetailsBloc.userDetails, onPressed: () async {
builder: (final context, final userDetails) => SettingsList( if (await showConfirmationDialog(
categories: [ context,
SettingsCategory( AppLocalizations.of(context).settingsResetForConfirmation(name),
title: Text(AppLocalizations.of(context).accountOptionsCategoryStorageInfo), )) {
tiles: [ await options.reset();
CustomSettingsTile( }
title: Column( },
crossAxisAlignment: CrossAxisAlignment.start, tooltip: AppLocalizations.of(context).settingsResetFor(name),
children: [ icon: Icon(MdiIcons.cogRefresh),
if (userDetails.hasData) ...[ ),
LinearProgressIndicator( ],
value: userDetails.requireData.quota.relative / 100, );
backgroundColor: Theme.of(context).colorScheme.primary.withOpacity(0.3),
), final body = ResultBuilder<ProvisioningApiUserDetails>.behaviorSubject(
const SizedBox( stream: userDetailsBloc.userDetails,
height: 10, builder: (final context, final userDetails) => SettingsList(
), categories: [
Text( SettingsCategory(
AppLocalizations.of(context).accountOptionsQuotaUsedOf( title: Text(AppLocalizations.of(context).accountOptionsCategoryStorageInfo),
filesize(userDetails.requireData.quota.used, 1), tiles: [
filesize(userDetails.requireData.quota.total, 1), CustomSettingsTile(
userDetails.requireData.quota.relative.toString(), title: Column(
), crossAxisAlignment: CrossAxisAlignment.start,
), children: [
], if (userDetails.hasData) ...[
NeonException( LinearProgressIndicator(
userDetails.error, value: userDetails.requireData.quota.relative / 100,
onRetry: _userDetailsBloc.refresh, backgroundColor: Theme.of(context).colorScheme.primary.withOpacity(0.3),
), ),
NeonLinearProgressIndicator( const SizedBox(
visible: userDetails.isLoading, height: 10,
),
Text(
AppLocalizations.of(context).accountOptionsQuotaUsedOf(
filesize(userDetails.requireData.quota.used, 1),
filesize(userDetails.requireData.quota.total, 1),
userDetails.requireData.quota.relative.toString(),
), ),
], ),
],
NeonException(
userDetails.error,
onRetry: userDetailsBloc.refresh,
), ),
), NeonLinearProgressIndicator(
], visible: userDetails.isLoading,
),
],
),
), ),
SettingsCategory( ],
title: Text(AppLocalizations.of(context).optionsCategoryGeneral), ),
tiles: [ SettingsCategory(
DropdownButtonSettingsTile( title: Text(AppLocalizations.of(context).optionsCategoryGeneral),
option: _options.initialApp, tiles: [
), DropdownButtonSettingsTile(
], option: options.initialApp,
), ),
], ],
), ),
],
),
);
return Scaffold(
resizeToAvoidBottomInset: false,
appBar: appBar,
body: Center(
child: ConstrainedBox(
constraints: NeonDialogTheme.of(context).constraints,
child: body,
), ),
); ),
);
}
} }

98
packages/neon/neon/lib/src/pages/nextcloud_app_settings.dart

@ -8,6 +8,7 @@ import 'package:neon/src/settings/widgets/checkbox_settings_tile.dart';
import 'package:neon/src/settings/widgets/dropdown_button_settings_tile.dart'; import 'package:neon/src/settings/widgets/dropdown_button_settings_tile.dart';
import 'package:neon/src/settings/widgets/settings_category.dart'; import 'package:neon/src/settings/widgets/settings_category.dart';
import 'package:neon/src/settings/widgets/settings_list.dart'; import 'package:neon/src/settings/widgets/settings_list.dart';
import 'package:neon/src/theme/dialog.dart';
import 'package:neon/src/utils/confirmation_dialog.dart'; import 'package:neon/src/utils/confirmation_dialog.dart';
class NextcloudAppSettingsPage extends StatelessWidget { class NextcloudAppSettingsPage extends StatelessWidget {
@ -19,53 +20,62 @@ class NextcloudAppSettingsPage extends StatelessWidget {
final AppImplementation appImplementation; final AppImplementation appImplementation;
@override @override
Widget build(final BuildContext context) => Scaffold( Widget build(final BuildContext context) {
resizeToAvoidBottomInset: false, final appBar = AppBar(
appBar: AppBar( title: Text(appImplementation.name(context)),
title: Text(appImplementation.name(context)), actions: [
actions: [ IconButton(
IconButton( onPressed: () async {
onPressed: () async { if (await showConfirmationDialog(
if (await showConfirmationDialog( context,
context, AppLocalizations.of(context).settingsResetForConfirmation(appImplementation.name(context)),
AppLocalizations.of(context).settingsResetForConfirmation(appImplementation.name(context)), )) {
)) { await appImplementation.options.reset();
await appImplementation.options.reset(); }
} },
}, tooltip: AppLocalizations.of(context).settingsResetFor(appImplementation.name(context)),
tooltip: AppLocalizations.of(context).settingsResetFor(appImplementation.name(context)), icon: Icon(MdiIcons.cogRefresh),
icon: Icon(MdiIcons.cogRefresh),
),
],
), ),
body: SettingsList( ],
categories: [ );
for (final category in [...appImplementation.options.categories, null]) ...[
if (appImplementation.options.options final body = SettingsList(
.where((final option) => option.category == category) categories: [
.isNotEmpty) ...[ for (final category in [...appImplementation.options.categories, null]) ...[
SettingsCategory( if (appImplementation.options.options.where((final option) => option.category == category).isNotEmpty) ...[
title: Text( SettingsCategory(
category != null ? category.name(context) : AppLocalizations.of(context).optionsCategoryOther, title: Text(
), category != null ? category.name(context) : AppLocalizations.of(context).optionsCategoryOther,
tiles: [ ),
for (final option tiles: [
in appImplementation.options.options.where((final option) => option.category == category)) ...[ for (final option
if (option is ToggleOption) ...[ in appImplementation.options.options.where((final option) => option.category == category)) ...[
CheckBoxSettingsTile( if (option is ToggleOption) ...[
option: option, CheckBoxSettingsTile(
), option: option,
] else if (option is SelectOption) ...[ ),
DropdownButtonSettingsTile( ] else if (option is SelectOption) ...[
option: option, DropdownButtonSettingsTile(
), option: option,
], ),
],
], ],
), ],
], ],
], ),
], ],
],
],
);
return Scaffold(
resizeToAvoidBottomInset: false,
appBar: appBar,
body: Center(
child: ConstrainedBox(
constraints: NeonDialogTheme.of(context).constraints,
child: body,
), ),
); ),
);
}
} }

511
packages/neon/neon/lib/src/pages/settings.dart

@ -19,6 +19,7 @@ import 'package:neon/src/settings/widgets/settings_list.dart';
import 'package:neon/src/settings/widgets/settings_tile.dart'; import 'package:neon/src/settings/widgets/settings_tile.dart';
import 'package:neon/src/settings/widgets/text_settings_tile.dart'; import 'package:neon/src/settings/widgets/text_settings_tile.dart';
import 'package:neon/src/theme/branding.dart'; import 'package:neon/src/theme/branding.dart';
import 'package:neon/src/theme/dialog.dart';
import 'package:neon/src/utils/confirmation_dialog.dart'; import 'package:neon/src/utils/confirmation_dialog.dart';
import 'package:neon/src/utils/global_options.dart'; import 'package:neon/src/utils/global_options.dart';
import 'package:neon/src/utils/save_file.dart'; import 'package:neon/src/utils/save_file.dart';
@ -27,11 +28,25 @@ import 'package:neon/src/widgets/exception.dart';
import 'package:package_info_plus/package_info_plus.dart'; import 'package:package_info_plus/package_info_plus.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
enum SettingsCageories {
apps,
theme,
navigation,
pushNotifications,
startup,
systemTray,
accounts,
other,
}
class SettingsPage extends StatefulWidget { class SettingsPage extends StatefulWidget {
const SettingsPage({ const SettingsPage({
this.initialCategory,
super.key, super.key,
}); });
final SettingsCageories? initialCategory;
@override @override
State<SettingsPage> createState() => _SettingsPageState(); State<SettingsPage> createState() => _SettingsPageState();
} }
@ -42,271 +57,289 @@ class _SettingsPageState extends State<SettingsPage> {
final globalOptions = Provider.of<GlobalOptions>(context); final globalOptions = Provider.of<GlobalOptions>(context);
final accountsBloc = Provider.of<AccountsBloc>(context, listen: false); final accountsBloc = Provider.of<AccountsBloc>(context, listen: false);
final appImplementations = Provider.of<Iterable<AppImplementation>>(context); final appImplementations = Provider.of<Iterable<AppImplementation>>(context);
return Scaffold(
resizeToAvoidBottomInset: false,
appBar: AppBar(
title: Text(AppLocalizations.of(context).settings),
actions: [
IconButton(
onPressed: () async {
if (await showConfirmationDialog(context, AppLocalizations.of(context).settingsResetAllConfirmation)) {
await globalOptions.reset();
for (final appImplementation in appImplementations) { final appBar = AppBar(
await appImplementation.options.reset(); title: Text(AppLocalizations.of(context).settings),
} actions: [
IconButton(
onPressed: () async {
if (await showConfirmationDialog(context, AppLocalizations.of(context).settingsResetAllConfirmation)) {
await globalOptions.reset();
for (final account in accountsBloc.accounts.value) { for (final appImplementation in appImplementations) {
await accountsBloc.getOptionsFor(account).reset(); await appImplementation.options.reset();
}
} }
},
tooltip: AppLocalizations.of(context).settingsResetAll, for (final account in accountsBloc.accounts.value) {
icon: Icon(MdiIcons.cogRefresh), await accountsBloc.getOptionsFor(account).reset();
), }
], }
), },
body: StreamBuilder<List<Account>>( tooltip: AppLocalizations.of(context).settingsResetAll,
stream: accountsBloc.accounts, icon: Icon(MdiIcons.cogRefresh),
builder: ( ),
final context, ],
final accountsSnapshot, );
) { final body = StreamBuilder<List<Account>>(
final settingsExportHelper = SettingsExportHelper( stream: accountsBloc.accounts,
globalOptions: globalOptions, initialData: accountsBloc.accounts.valueOrNull,
appImplementations: appImplementations, builder: (
accountSpecificOptions: { final context,
if (accountsSnapshot.hasData) ...{ final accountsSnapshot,
for (final account in accountsSnapshot.requireData) ...{ ) {
account: accountsBloc.getOptionsFor(account).options, final platform = Provider.of<NeonPlatform>(context, listen: false);
}, return StreamBuilder<bool>(
}, stream: globalOptions.pushNotificationsEnabled.enabled,
}, initialData: globalOptions.pushNotificationsEnabled.enabled.valueOrNull,
); builder: (
final platform = Provider.of<NeonPlatform>(context, listen: false); final context,
return StreamBuilder<Account?>( final pushNotificationsEnabledEnabledSnapshot,
stream: accountsBloc.activeAccount, ) =>
builder: ( SettingsList(
final context, initialCategory: widget.initialCategory?.name,
final activeAccountSnapshot, categories: [
) => SettingsCategory(
StreamBuilder<bool>( title: Text(AppLocalizations.of(context).settingsApps),
stream: globalOptions.pushNotificationsEnabled.enabled, key: ValueKey(SettingsCageories.apps.name),
builder: ( tiles: <SettingsTile>[
final context, for (final appImplementation in appImplementations) ...[
final pushNotificationsEnabledEnabledSnapshot, if (appImplementation.options.options.isNotEmpty) ...[
) => CustomSettingsTile(
SettingsList( leading: appImplementation.buildIcon(),
categories: [ title: Text(appImplementation.name(context)),
SettingsCategory( onTap: () {
title: Text(AppLocalizations.of(context).settingsApps), NextcloudAppSettingsRoute(appid: appImplementation.id).go(context);
tiles: <SettingsTile>[ },
for (final appImplementation in appImplementations) ...[
if (appImplementation.options.options.isNotEmpty) ...[
CustomSettingsTile(
leading: appImplementation.buildIcon(),
title: Text(appImplementation.name(context)),
onTap: () {
NextcloudAppSettingsRoute(appid: appImplementation.id).go(context);
},
),
],
],
],
),
SettingsCategory(
title: Text(AppLocalizations.of(context).optionsCategoryTheme),
tiles: [
DropdownButtonSettingsTile(
option: globalOptions.themeMode,
),
CheckBoxSettingsTile(
option: globalOptions.themeOLEDAsDark,
),
CheckBoxSettingsTile(
option: globalOptions.themeKeepOriginalAccentColor,
), ),
], ],
],
],
),
SettingsCategory(
title: Text(AppLocalizations.of(context).optionsCategoryTheme),
key: ValueKey(SettingsCageories.theme.name),
tiles: [
DropdownButtonSettingsTile(
option: globalOptions.themeMode,
), ),
SettingsCategory( CheckBoxSettingsTile(
title: Text(AppLocalizations.of(context).optionsCategoryNavigation), option: globalOptions.themeOLEDAsDark,
tiles: [
DropdownButtonSettingsTile(
option: globalOptions.navigationMode,
),
],
), ),
if (platform.canUsePushNotifications) ...[ CheckBoxSettingsTile(
SettingsCategory( option: globalOptions.themeKeepOriginalAccentColor,
title: Text(AppLocalizations.of(context).optionsCategoryPushNotifications), ),
tiles: [ ],
if (pushNotificationsEnabledEnabledSnapshot.hasData && ),
!pushNotificationsEnabledEnabledSnapshot.requireData) ...[ SettingsCategory(
TextSettingsTile( title: Text(AppLocalizations.of(context).optionsCategoryNavigation),
text: AppLocalizations.of(context).globalOptionsPushNotificationsEnabledDisabledNotice, key: ValueKey(SettingsCageories.navigation.name),
style: TextStyle( tiles: [
fontWeight: FontWeight.w600, DropdownButtonSettingsTile(
fontStyle: FontStyle.italic, option: globalOptions.navigationMode,
color: Theme.of(context).colorScheme.error, ),
), ],
), ),
], if (platform.canUsePushNotifications) ...[
CheckBoxSettingsTile( SettingsCategory(
option: globalOptions.pushNotificationsEnabled, title: Text(AppLocalizations.of(context).optionsCategoryPushNotifications),
), key: ValueKey(SettingsCageories.pushNotifications.name),
DropdownButtonSettingsTile( tiles: [
option: globalOptions.pushNotificationsDistributor, if (pushNotificationsEnabledEnabledSnapshot.hasData &&
!pushNotificationsEnabledEnabledSnapshot.requireData) ...[
TextSettingsTile(
text: AppLocalizations.of(context).globalOptionsPushNotificationsEnabledDisabledNotice,
style: TextStyle(
fontWeight: FontWeight.w600,
fontStyle: FontStyle.italic,
color: Theme.of(context).colorScheme.error,
), ),
], ),
],
CheckBoxSettingsTile(
option: globalOptions.pushNotificationsEnabled,
),
DropdownButtonSettingsTile(
option: globalOptions.pushNotificationsDistributor,
), ),
], ],
if (platform.canUseWindowManager) ...[ ),
SettingsCategory( ],
title: Text(AppLocalizations.of(context).optionsCategoryStartup), if (platform.canUseWindowManager) ...[
tiles: [ SettingsCategory(
CheckBoxSettingsTile( title: Text(AppLocalizations.of(context).optionsCategoryStartup),
option: globalOptions.startupMinimized, key: ValueKey(SettingsCageories.startup.name),
), tiles: [
CheckBoxSettingsTile( CheckBoxSettingsTile(
option: globalOptions.startupMinimizeInsteadOfExit, option: globalOptions.startupMinimized,
), ),
], CheckBoxSettingsTile(
option: globalOptions.startupMinimizeInsteadOfExit,
), ),
], ],
if (platform.canUseWindowManager && platform.canUseSystemTray) ...[ ),
SettingsCategory( ],
title: Text(AppLocalizations.of(context).optionsCategorySystemTray), if (platform.canUseWindowManager && platform.canUseSystemTray) ...[
tiles: [ SettingsCategory(
CheckBoxSettingsTile( title: Text(AppLocalizations.of(context).optionsCategorySystemTray),
option: globalOptions.systemTrayEnabled, key: ValueKey(SettingsCageories.systemTray.name),
), tiles: [
CheckBoxSettingsTile( CheckBoxSettingsTile(
option: globalOptions.systemTrayHideToTrayWhenMinimized, option: globalOptions.systemTrayEnabled,
), ),
], CheckBoxSettingsTile(
option: globalOptions.systemTrayHideToTrayWhenMinimized,
), ),
], ],
if (accountsSnapshot.hasData) ...[ ),
SettingsCategory( ],
title: Text(AppLocalizations.of(context).optionsCategoryAccounts), SettingsCategory(
tiles: [ title: Text(AppLocalizations.of(context).optionsCategoryAccounts),
if (accountsSnapshot.requireData.length > 1) ...[ key: ValueKey(SettingsCageories.accounts.name),
CheckBoxSettingsTile( tiles: [
option: globalOptions.rememberLastUsedAccount, if (accountsSnapshot.requireData.length > 1) ...[
), CheckBoxSettingsTile(
DropdownButtonSettingsTile( option: globalOptions.rememberLastUsedAccount,
option: globalOptions.initialAccount, ),
), DropdownButtonSettingsTile(
], option: globalOptions.initialAccount,
for (final account in accountsSnapshot.requireData) ...[
AccountSettingsTile(
account: account,
onTap: () {
AccountSettingsRoute(accountid: account.id).go(context);
},
),
],
CustomSettingsTile(
title: ElevatedButton.icon(
onPressed: () async => const LoginRoute().push(context),
icon: Icon(MdiIcons.accountPlus),
label: Text(AppLocalizations.of(context).globalOptionsAccountsAdd),
),
)
],
), ),
], ],
SettingsCategory( for (final account in accountsSnapshot.requireData) ...[
title: Text(AppLocalizations.of(context).optionsCategoryOther), AccountSettingsTile(
tiles: <SettingsTile>[ account: account,
CustomSettingsTile( onTap: () {
leading: Icon( AccountSettingsRoute(accountid: account.id).go(context);
MdiIcons.scriptText, },
color: Theme.of(context).colorScheme.primary, ),
), ],
title: Text(AppLocalizations.of(context).licenses), CustomSettingsTile(
onTap: () async { title: ElevatedButton.icon(
final branding = Branding.of(context); onPressed: () async => const LoginRoute().push(context),
showLicensePage( icon: Icon(MdiIcons.accountPlus),
context: context, label: Text(AppLocalizations.of(context).globalOptionsAccountsAdd),
applicationName: branding.name, ),
applicationIcon: branding.logo, )
applicationLegalese: branding.legalese, ],
applicationVersion: Provider.of<PackageInfo>(context, listen: false).version, ),
); SettingsCategory(
}, title: Text(AppLocalizations.of(context).optionsCategoryOther),
), key: ValueKey(SettingsCageories.other.name),
CustomSettingsTile( tiles: <SettingsTile>[
leading: Icon( CustomSettingsTile(
MdiIcons.export, leading: Icon(
color: Theme.of(context).colorScheme.primary, MdiIcons.scriptText,
), color: Theme.of(context).colorScheme.primary,
title: Text(AppLocalizations.of(context).settingsExport), ),
onTap: () async { title: Text(AppLocalizations.of(context).licenses),
try { onTap: () async {
final fileName = final branding = Branding.of(context);
'nextcloud-neon-settings-${DateTime.now().millisecondsSinceEpoch ~/ 1000}.json.base64'; showLicensePage(
final data = base64.encode( context: context,
utf8.encode( applicationName: branding.name,
json.encode( applicationIcon: branding.logo,
settingsExportHelper.toJsonExport(), applicationLegalese: branding.legalese,
), applicationVersion: Provider.of<PackageInfo>(context, listen: false).version,
), );
); },
await saveFileWithPickDialog(fileName, Uint8List.fromList(utf8.encode(data))); ),
} catch (e, s) { CustomSettingsTile(
debugPrint(e.toString()); leading: Icon(
debugPrint(s.toString()); MdiIcons.export,
NeonException.showSnackbar(context, e); color: Theme.of(context).colorScheme.primary,
} ),
}, title: Text(AppLocalizations.of(context).settingsExport),
), onTap: () async {
CustomSettingsTile( final settingsExportHelper = _buildSettingsExportHelper(context);
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 == null) { try {
return; 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());
debugPrint(s.toString());
NeonException.showSnackbar(context, e);
}
},
),
CustomSettingsTile(
leading: Icon(
MdiIcons.import,
color: Theme.of(context).colorScheme.primary,
),
title: Text(AppLocalizations.of(context).settingsImport),
onTap: () async {
final settingsExportHelper = _buildSettingsExportHelper(context);
try {
final result = await FilePicker.platform.pickFiles(
withData: true,
);
if (!result.files.single.path!.endsWith('.json.base64')) { if (result == null) {
if (mounted) { return;
NeonException.showSnackbar( }
context,
AppLocalizations.of(context).settingsImportWrongFileExtension,
);
}
return;
}
final data = if (!result.files.single.path!.endsWith('.json.base64')) {
json.decode(utf8.decode(base64.decode(utf8.decode(result.files.single.bytes!)))); if (mounted) {
await settingsExportHelper.applyFromJson(data as Map<String, dynamic>); NeonException.showSnackbar(
} catch (e, s) { context,
debugPrint(e.toString()); AppLocalizations.of(context).settingsImportWrongFileExtension,
debugPrint(s.toString()); );
NeonException.showSnackbar(context, e);
} }
}, return;
), }
],
final data = json.decode(utf8.decode(base64.decode(utf8.decode(result.files.single.bytes!))));
await settingsExportHelper.applyFromJson(data as Map<String, dynamic>);
} catch (e, s) {
debugPrint(e.toString());
debugPrint(s.toString());
NeonException.showSnackbar(context, e);
}
},
), ),
], ],
), ),
), ],
); ),
}, );
},
);
return Scaffold(
resizeToAvoidBottomInset: false,
appBar: appBar,
body: Center(
child: ConstrainedBox(
constraints: NeonDialogTheme.of(context).constraints,
child: body,
),
), ),
); );
} }
SettingsExportHelper _buildSettingsExportHelper(final BuildContext context) {
final globalOptions = Provider.of<GlobalOptions>(context);
final accountsBloc = Provider.of<AccountsBloc>(context, listen: false);
final appImplementations = Provider.of<Iterable<AppImplementation>>(context);
return SettingsExportHelper(
globalOptions: globalOptions,
appImplementations: appImplementations,
accountSpecificOptions: accountsBloc.accounts.value.asMap().map(
(final _, final account) => MapEntry(account, accountsBloc.getOptionsFor(account).options),
),
);
}
} }
enum SettingsAccountAction { enum SettingsAccountAction {

7
packages/neon/neon/lib/src/router.dart

@ -367,8 +367,11 @@ class NextcloudAppSettingsRoute extends GoRouteData {
@immutable @immutable
class SettingsRoute extends GoRouteData { class SettingsRoute extends GoRouteData {
const SettingsRoute(); const SettingsRoute({this.initialCategory});
/// The initial category to show.
final SettingsCageories? initialCategory;
@override @override
Widget build(final BuildContext context, final GoRouterState state) => const SettingsPage(); Widget build(final BuildContext context, final GoRouterState state) => SettingsPage(initialCategory: initialCategory);
} }

50
packages/neon/neon/lib/src/router.g.dart

@ -71,13 +71,21 @@ extension $HomeRouteExtension on HomeRoute {
Future<T?> push<T>(BuildContext context) => context.push<T>(location); Future<T?> push<T>(BuildContext context) => context.push<T>(location);
void pushReplacement(BuildContext context) => context.pushReplacement(location); void pushReplacement(BuildContext context) => context.pushReplacement(location);
void replace(BuildContext context) => context.replace(location);
} }
extension $SettingsRouteExtension on SettingsRoute { extension $SettingsRouteExtension on SettingsRoute {
static SettingsRoute _fromState(GoRouterState state) => const SettingsRoute(); static SettingsRoute _fromState(GoRouterState state) => SettingsRoute(
initialCategory:
_$convertMapValue('initial-category', state.queryParameters, _$SettingsCageoriesEnumMap._$fromName),
);
String get location => GoRouteData.$location( String get location => GoRouteData.$location(
'/settings', '/settings',
queryParams: {
if (initialCategory != null) 'initial-category': _$SettingsCageoriesEnumMap[initialCategory!],
},
); );
void go(BuildContext context) => context.go(location); void go(BuildContext context) => context.go(location);
@ -85,8 +93,21 @@ extension $SettingsRouteExtension on SettingsRoute {
Future<T?> push<T>(BuildContext context) => context.push<T>(location); Future<T?> push<T>(BuildContext context) => context.push<T>(location);
void pushReplacement(BuildContext context) => context.pushReplacement(location); void pushReplacement(BuildContext context) => context.pushReplacement(location);
void replace(BuildContext context) => context.replace(location);
} }
const _$SettingsCageoriesEnumMap = {
SettingsCageories.apps: 'apps',
SettingsCageories.theme: 'theme',
SettingsCageories.navigation: 'navigation',
SettingsCageories.pushNotifications: 'push-notifications',
SettingsCageories.startup: 'startup',
SettingsCageories.systemTray: 'system-tray',
SettingsCageories.accounts: 'accounts',
SettingsCageories.other: 'other',
};
extension $NextcloudAppSettingsRouteExtension on NextcloudAppSettingsRoute { extension $NextcloudAppSettingsRouteExtension on NextcloudAppSettingsRoute {
static NextcloudAppSettingsRoute _fromState(GoRouterState state) => NextcloudAppSettingsRoute( static NextcloudAppSettingsRoute _fromState(GoRouterState state) => NextcloudAppSettingsRoute(
appid: state.pathParameters['appid']!, appid: state.pathParameters['appid']!,
@ -101,6 +122,8 @@ extension $NextcloudAppSettingsRouteExtension on NextcloudAppSettingsRoute {
Future<T?> push<T>(BuildContext context) => context.push<T>(location); Future<T?> push<T>(BuildContext context) => context.push<T>(location);
void pushReplacement(BuildContext context) => context.pushReplacement(location); void pushReplacement(BuildContext context) => context.pushReplacement(location);
void replace(BuildContext context) => context.replace(location);
} }
extension $_AddAccountRouteExtension on _AddAccountRoute { extension $_AddAccountRouteExtension on _AddAccountRoute {
@ -115,6 +138,8 @@ extension $_AddAccountRouteExtension on _AddAccountRoute {
Future<T?> push<T>(BuildContext context) => context.push<T>(location); Future<T?> push<T>(BuildContext context) => context.push<T>(location);
void pushReplacement(BuildContext context) => context.pushReplacement(location); void pushReplacement(BuildContext context) => context.pushReplacement(location);
void replace(BuildContext context) => context.replace(location);
} }
extension $_AddAccountFlowRouteExtension on _AddAccountFlowRoute { extension $_AddAccountFlowRouteExtension on _AddAccountFlowRoute {
@ -206,6 +231,21 @@ extension $AccountSettingsRouteExtension on AccountSettingsRoute {
Future<T?> push<T>(BuildContext context) => context.push<T>(location); Future<T?> push<T>(BuildContext context) => context.push<T>(location);
void pushReplacement(BuildContext context) => context.pushReplacement(location); void pushReplacement(BuildContext context) => context.pushReplacement(location);
void replace(BuildContext context) => context.replace(location);
}
T? _$convertMapValue<T>(
String key,
Map<String, String> map,
T Function(String) converter,
) {
final value = map[key];
return value == null ? null : converter(value);
}
extension<T extends Enum> on Map<T, String> {
T _$fromName(String value) => entries.singleWhere((element) => element.value == value).key;
} }
RouteBase get $loginRoute => GoRouteData.$route( RouteBase get $loginRoute => GoRouteData.$route(
@ -244,6 +284,8 @@ extension $LoginRouteExtension on LoginRoute {
Future<T?> push<T>(BuildContext context) => context.push<T>(location); Future<T?> push<T>(BuildContext context) => context.push<T>(location);
void pushReplacement(BuildContext context) => context.pushReplacement(location); void pushReplacement(BuildContext context) => context.pushReplacement(location);
void replace(BuildContext context) => context.replace(location);
} }
extension $LoginFlowRouteExtension on LoginFlowRoute { extension $LoginFlowRouteExtension on LoginFlowRoute {
@ -277,6 +319,8 @@ extension $LoginQrcodeRouteExtension on LoginQrcodeRoute {
Future<T?> push<T>(BuildContext context) => context.push<T>(location); Future<T?> push<T>(BuildContext context) => context.push<T>(location);
void pushReplacement(BuildContext context) => context.pushReplacement(location); void pushReplacement(BuildContext context) => context.pushReplacement(location);
void replace(BuildContext context) => context.replace(location);
} }
extension $LoginCheckServerStatusRouteExtension on LoginCheckServerStatusRoute { extension $LoginCheckServerStatusRouteExtension on LoginCheckServerStatusRoute {
@ -296,6 +340,8 @@ extension $LoginCheckServerStatusRouteExtension on LoginCheckServerStatusRoute {
Future<T?> push<T>(BuildContext context) => context.push<T>(location); Future<T?> push<T>(BuildContext context) => context.push<T>(location);
void pushReplacement(BuildContext context) => context.pushReplacement(location); void pushReplacement(BuildContext context) => context.pushReplacement(location);
void replace(BuildContext context) => context.replace(location);
} }
extension $LoginCheckAccountRouteExtension on LoginCheckAccountRoute { extension $LoginCheckAccountRouteExtension on LoginCheckAccountRoute {
@ -319,4 +365,6 @@ extension $LoginCheckAccountRouteExtension on LoginCheckAccountRoute {
Future<T?> push<T>(BuildContext context) => context.push<T>(location); Future<T?> push<T>(BuildContext context) => context.push<T>(location);
void pushReplacement(BuildContext context) => context.pushReplacement(location); void pushReplacement(BuildContext context) => context.pushReplacement(location);
void replace(BuildContext context) => context.replace(location);
} }

27
packages/neon/neon/lib/src/settings/widgets/settings_list.dart

@ -1,23 +1,36 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:intersperse/intersperse.dart';
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
import 'package:neon/src/settings/widgets/settings_category.dart'; import 'package:neon/src/settings/widgets/settings_category.dart';
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
@internal @internal
class SettingsList extends StatelessWidget { class SettingsList extends StatelessWidget {
const SettingsList({ const SettingsList({
required this.categories, required this.categories,
this.initialCategory,
super.key, super.key,
}); });
final List<SettingsCategory> categories; final List<SettingsCategory> categories;
final String? initialCategory;
int? _getIndex(final String? initialCategory) {
if (initialCategory == null) {
return null;
}
final key = Key(initialCategory);
final index = categories.indexWhere((final category) => category.key == key);
return index != -1 ? index : null;
}
@override @override
Widget build(final BuildContext context) => Scrollbar( Widget build(final BuildContext context) => ScrollablePositionedList.separated(
child: ListView( padding: const EdgeInsets.all(20),
primary: true, itemCount: categories.length,
padding: const EdgeInsets.all(20), initialScrollIndex: _getIndex(initialCategory) ?? 0,
children: categories.cast<Widget>().intersperse(const Divider()).toList(), itemBuilder: (final context, final index) => categories[index],
), separatorBuilder: (final context, final index) => const Divider(),
); );
} }

3
packages/neon/neon/lib/src/utils/global_popups.dart

@ -3,6 +3,7 @@ import 'package:meta/meta.dart';
import 'package:neon/l10n/localizations.dart'; import 'package:neon/l10n/localizations.dart';
import 'package:neon/src/blocs/first_launch.dart'; import 'package:neon/src/blocs/first_launch.dart';
import 'package:neon/src/blocs/next_push.dart'; import 'package:neon/src/blocs/next_push.dart';
import 'package:neon/src/pages/settings.dart';
import 'package:neon/src/router.dart'; import 'package:neon/src/router.dart';
import 'package:neon/src/utils/global_options.dart'; import 'package:neon/src/utils/global_options.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@ -43,7 +44,7 @@ class GlobalPopups {
action: SnackBarAction( action: SnackBarAction(
label: AppLocalizations.of(context).settings, label: AppLocalizations.of(context).settings,
onPressed: () { onPressed: () {
const SettingsRoute().go(context); const SettingsRoute(initialCategory: SettingsCageories.pushNotifications).go(context);
}, },
), ),
), ),

92
packages/neon/neon/lib/src/widgets/account_switcher.dart

@ -0,0 +1,92 @@
import 'package:flutter/material.dart';
import 'package:meta/meta.dart';
import 'package:neon/l10n/localizations.dart';
import 'package:neon/src/blocs/accounts.dart';
import 'package:neon/src/pages/settings.dart';
import 'package:neon/src/router.dart';
import 'package:neon/src/theme/dialog.dart';
import 'package:neon/src/widgets/account_tile.dart';
import 'package:neon/src/widgets/user_avatar.dart';
import 'package:provider/provider.dart';
@internal
class AccountSwitcherButton extends StatelessWidget {
const AccountSwitcherButton({
super.key,
});
Future<void> _onPressed(final BuildContext context) async {
final accountsBloc = Provider.of<AccountsBloc>(context, listen: false);
final accounts = accountsBloc.accounts.value;
final aa = accountsBloc.activeAccount.value!;
await showDialog(
context: context,
builder: (final context) {
final body = Column(
children: [
NeonAccountTile(
account: aa,
trailing: const Icon(Icons.check_circle),
onTap: Navigator.of(context).pop,
),
const Divider(),
if (accounts.length > 1)
Builder(
builder: (final context) {
final inactiveAccounts = List.of(accounts)..removeWhere((final account) => (account.id == aa.id));
final tiles = inactiveAccounts.map(
(final account) => NeonAccountTile(
account: account,
onTap: () {
accountsBloc.setActiveAccount(account);
Navigator.of(context).pop();
},
),
);
return SingleChildScrollView(
child: ListBody(
children: tiles.toList(),
),
);
},
),
ListTile(
leading: const Icon(Icons.settings),
title: Text(AppLocalizations.of(context).settingsAccountManage),
onTap: () {
Navigator.of(context).pop();
const SettingsRoute(initialCategory: SettingsCageories.accounts).push(context);
},
)
],
);
return Dialog(
child: IntrinsicHeight(
child: Container(
padding: const EdgeInsets.all(24),
constraints: NeonDialogTheme.of(context).constraints,
child: body,
),
),
);
},
);
}
@override
Widget build(final BuildContext context) {
final accountsBloc = Provider.of<AccountsBloc>(context, listen: false);
final account = accountsBloc.activeAccount.value!;
return IconButton(
onPressed: () async => _onPressed(context),
tooltip: AppLocalizations.of(context).settingsAccount,
icon: NeonUserAvatar(
account: account,
),
);
}
}

1
packages/neon/neon/lib/src/widgets/account_tile.dart

@ -49,6 +49,7 @@ class NeonAccountTile extends StatelessWidget {
account: account, account: account,
showStatus: showStatus, showStatus: showStatus,
), ),
trailing: trailing,
title: ResultBuilder<ProvisioningApiUserDetails>.behaviorSubject( title: ResultBuilder<ProvisioningApiUserDetails>.behaviorSubject(
stream: userDetailsBloc.userDetails, stream: userDetailsBloc.userDetails,
builder: (final context, final userDetails) => Row( builder: (final context, final userDetails) => Row(

16
packages/neon/neon/lib/src/widgets/app_bar.dart

@ -9,7 +9,7 @@ import 'package:neon/src/blocs/apps.dart';
import 'package:neon/src/models/account.dart'; import 'package:neon/src/models/account.dart';
import 'package:neon/src/models/app_implementation.dart'; import 'package:neon/src/models/app_implementation.dart';
import 'package:neon/src/models/notifications_interface.dart'; import 'package:neon/src/models/notifications_interface.dart';
import 'package:neon/src/router.dart'; import 'package:neon/src/widgets/account_switcher.dart';
import 'package:neon/src/widgets/app_implementation_icon.dart'; import 'package:neon/src/widgets/app_implementation_icon.dart';
import 'package:neon/src/widgets/exception.dart'; import 'package:neon/src/widgets/exception.dart';
import 'package:neon/src/widgets/linear_progress_indicator.dart'; import 'package:neon/src/widgets/linear_progress_indicator.dart';
@ -77,17 +77,9 @@ class NeonAppBar extends StatelessWidget implements PreferredSizeWidget {
], ],
], ],
), ),
actions: [ actions: const [
const NotificationIconButton(), NotificationIconButton(),
IconButton( AccountSwitcherButton(),
onPressed: () {
AccountSettingsRoute(accountid: account.id).go(context);
},
tooltip: AppLocalizations.of(context).settingsAccount,
icon: NeonUserAvatar(
account: account,
),
),
], ],
), ),
), ),

122
packages/neon/neon/lib/src/widgets/drawer.dart

@ -7,10 +7,8 @@ import 'package:neon/src/bloc/result_builder.dart';
import 'package:neon/src/blocs/accounts.dart'; import 'package:neon/src/blocs/accounts.dart';
import 'package:neon/src/blocs/apps.dart'; import 'package:neon/src/blocs/apps.dart';
import 'package:neon/src/blocs/capabilities.dart'; import 'package:neon/src/blocs/capabilities.dart';
import 'package:neon/src/models/account.dart';
import 'package:neon/src/models/app_implementation.dart'; import 'package:neon/src/models/app_implementation.dart';
import 'package:neon/src/router.dart'; import 'package:neon/src/router.dart';
import 'package:neon/src/widgets/account_tile.dart';
import 'package:neon/src/widgets/cached_image.dart'; import 'package:neon/src/widgets/cached_image.dart';
import 'package:neon/src/widgets/drawer_destination.dart'; import 'package:neon/src/widgets/drawer_destination.dart';
import 'package:neon/src/widgets/exception.dart'; import 'package:neon/src/widgets/exception.dart';
@ -54,13 +52,12 @@ class _NeonDrawer extends StatefulWidget {
State<_NeonDrawer> createState() => __NeonDrawerState(); State<_NeonDrawer> createState() => __NeonDrawerState();
} }
class __NeonDrawerState extends State<_NeonDrawer> with SingleTickerProviderStateMixin { class __NeonDrawerState extends State<_NeonDrawer> {
late TabController _tabController;
late AccountsBloc _accountsBloc; late AccountsBloc _accountsBloc;
late AppsBloc _appsBloc; late AppsBloc _appsBloc;
late List<AppImplementation> _apps; late List<AppImplementation> _apps;
int _activeApp = 0; late int _activeApp;
@override @override
void initState() { void initState() {
@ -71,17 +68,6 @@ class __NeonDrawerState extends State<_NeonDrawer> with SingleTickerProviderStat
_apps = widget.apps.toList(); _apps = widget.apps.toList();
_activeApp = _apps.indexWhere((final app) => app.id == _appsBloc.activeApp.valueOrNull?.id); _activeApp = _apps.indexWhere((final app) => app.id == _appsBloc.activeApp.valueOrNull?.id);
_tabController = TabController(
vsync: this,
length: widget.apps.length,
);
}
@override
void dispose() {
_tabController.dispose();
super.dispose();
} }
void onAppChange(final int index) { void onAppChange(final int index) {
@ -135,85 +121,57 @@ class NeonDrawerHeader extends StatelessWidget {
final accountsBloc = Provider.of<AccountsBloc>(context, listen: false); final accountsBloc = Provider.of<AccountsBloc>(context, listen: false);
final capabilitiesBloc = accountsBloc.activeCapabilitiesBloc; final capabilitiesBloc = accountsBloc.activeCapabilitiesBloc;
final accountSelecor = StreamBuilder<List<Account>>( final branding = ResultBuilder<Capabilities>.behaviorSubject(
stream: accountsBloc.accounts, stream: capabilitiesBloc.capabilities,
builder: (final context, final accountsSnapshot) { builder: (final context, final capabilities) {
final accounts = accountsSnapshot.data; if (!capabilities.hasData) {
if (accounts == null || accounts.length <= 1) { return NeonLinearProgressIndicator(
return const SizedBox.shrink(); visible: capabilities.isLoading,
);
} }
final items = accounts.map((final account) { if (capabilities.hasError) {
final child = NeonAccountTile( return NeonException(
account: account, capabilities.error,
dense: true, onRetry: capabilitiesBloc.refresh,
textColor: Theme.of(context).appBarTheme.foregroundColor,
); );
}
return DropdownMenuItem( final theme = capabilities.requireData.capabilities.theming;
value: account,
child: child,
);
}).toList();
return DropdownButtonHideUnderline(
child: DropdownButton(
isExpanded: true,
dropdownColor: Theme.of(context).colorScheme.primary,
iconEnabledColor: Theme.of(context).colorScheme.onBackground,
value: accountsBloc.activeAccount.value,
items: items,
onChanged: (final account) {
if (account == null) {
return;
}
accountsBloc.setActiveAccount(account);
},
),
);
},
);
return ResultBuilder<Capabilities>.behaviorSubject( if (theme == null) {
stream: capabilitiesBloc.capabilities, return const SizedBox();
builder: (final context, final capabilities) => DrawerHeader( }
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primary, return Column(
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
if (capabilities.hasData) ...[ if (theme.name != null) ...[
if (capabilities.requireData.capabilities.theming?.name != null) ...[ Text(
Text( theme.name!,
capabilities.requireData.capabilities.theming!.name!, style: DefaultTextStyle.of(context).style.copyWith(
style: DefaultTextStyle.of(context).style.copyWith( color: Theme.of(context).appBarTheme.foregroundColor,
color: Theme.of(context).appBarTheme.foregroundColor, ),
),
),
],
if (capabilities.requireData.capabilities.theming?.logo != null) ...[
Flexible(
child: NeonCachedImage.url(
url: capabilities.requireData.capabilities.theming!.logo!,
),
),
],
] else ...[
NeonException(
capabilities.error,
onRetry: capabilitiesBloc.refresh,
), ),
NeonLinearProgressIndicator( ],
visible: capabilities.isLoading, if (theme.logo != null) ...[
Flexible(
child: NeonCachedImage.url(
url: theme.logo!,
),
), ),
], ],
accountSelecor,
], ],
), );
},
);
return DrawerHeader(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primary,
), ),
child: branding,
); );
} }
} }

1
packages/neon/neon/pubspec.yaml

@ -40,6 +40,7 @@ dependencies:
provider: ^6.0.5 provider: ^6.0.5
quick_actions: ^1.0.3 quick_actions: ^1.0.3
rxdart: ^0.27.7 rxdart: ^0.27.7
scrollable_positioned_list: ^0.3.8
shared_preferences: ^2.1.1 shared_preferences: ^2.1.1
sort_box: sort_box:
git: git:

Loading…
Cancel
Save