Browse Source

feat(neon): redesign settings and implement cupertino style

Signed-off-by: Nikolas Rimikis <leptopoda@users.noreply.github.com>
pull/1012/head
Nikolas Rimikis 1 year ago
parent
commit
08c6d21399
No known key found for this signature in database
GPG Key ID: 85ED1DE9786A4FF2
  1. 8
      packages/app/pubspec.lock
  2. 105
      packages/neon/neon/lib/src/pages/account_settings.dart
  3. 505
      packages/neon/neon/lib/src/pages/settings.dart
  4. 212
      packages/neon/neon/lib/src/settings/widgets/option_settings_tile.dart
  5. 53
      packages/neon/neon/lib/src/settings/widgets/select_settings_tile.dart
  6. 63
      packages/neon/neon/lib/src/settings/widgets/settings_category.dart
  7. 21
      packages/neon/neon/lib/src/settings/widgets/settings_list.dart
  8. 23
      packages/neon/neon/lib/src/settings/widgets/toggle_settings_tile.dart
  9. 5
      packages/neon/neon/lib/src/theme/theme.dart
  10. 1
      packages/neon/neon/pubspec.yaml

8
packages/app/pubspec.lock

@ -185,6 +185,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.17.3" version: "0.17.3"
cupertino_icons:
dependency: transitive
description:
name: cupertino_icons
sha256: d57953e10f9f8327ce64a508a355f0b1ec902193f66288e8cb5070e7c47eeb2d
url: "https://pub.dev"
source: hosted
version: "1.0.6"
dbus: dbus:
dependency: transitive dependency: transitive
description: description:

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

@ -8,13 +8,13 @@ import 'package:neon/src/blocs/accounts.dart';
import 'package:neon/src/models/account.dart'; import 'package:neon/src/models/account.dart';
import 'package:neon/src/router.dart'; import 'package:neon/src/router.dart';
import 'package:neon/src/settings/widgets/custom_settings_tile.dart'; import 'package:neon/src/settings/widgets/custom_settings_tile.dart';
import 'package:neon/src/settings/widgets/select_settings_tile.dart'; import 'package:neon/src/settings/widgets/option_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/theme/dialog.dart';
import 'package:neon/src/utils/adaptive.dart';
import 'package:neon/src/utils/confirmation_dialog.dart'; import 'package:neon/src/utils/confirmation_dialog.dart';
import 'package:neon/src/widgets/error.dart'; import 'package:neon/src/widgets/error.dart';
import 'package:neon/src/widgets/linear_progress_indicator.dart';
import 'package:nextcloud/provisioning_api.dart' as provisioning_api; import 'package:nextcloud/provisioning_api.dart' as provisioning_api;
/// Account settings page. /// Account settings page.
@ -84,61 +84,62 @@ class AccountSettingsPage extends StatelessWidget {
], ],
); );
final body = ResultBuilder<provisioning_api.UserDetails>.behaviorSubject( final body = SettingsList(
subject: userDetailsBloc.userDetails, categories: [
builder: (final context, final userDetails) { SettingsCategory(
final quotaRelative = userDetails.data?.quota.relative?.$int ?? userDetails.data?.quota.relative?.$double ?? 0; title: Text(NeonLocalizations.of(context).accountOptionsCategoryStorageInfo),
final quotaTotal = userDetails.data?.quota.total?.$int ?? userDetails.data?.quota.total?.$double ?? 0; tiles: [
final quotaUsed = userDetails.data?.quota.used?.$int ?? userDetails.data?.quota.used?.$double ?? 0; ResultBuilder<provisioning_api.UserDetails>.behaviorSubject(
subject: userDetailsBloc.userDetails,
builder: (final context, final userDetails) {
if (userDetails.hasError) {
return NeonError(
userDetails.error ?? 'Something went wrong',
type: NeonErrorType.listTile,
onRetry: userDetailsBloc.refresh,
);
}
return SettingsList( double? value;
categories: [ Widget? subtitle;
SettingsCategory( if (userDetails.hasData) {
title: Text(NeonLocalizations.of(context).accountOptionsCategoryStorageInfo), final quotaRelative =
tiles: [ userDetails.data?.quota.relative?.$int ?? userDetails.data?.quota.relative?.$double ?? 0;
CustomSettingsTile( final quotaTotal = userDetails.data?.quota.total?.$int ?? userDetails.data?.quota.total?.$double ?? 0;
title: Column( final quotaUsed = userDetails.data?.quota.used?.$int ?? userDetails.data?.quota.used?.$double ?? 0;
crossAxisAlignment: CrossAxisAlignment.start,
children: [ value = quotaRelative / 100;
if (userDetails.hasData) ...[ subtitle = Text(
LinearProgressIndicator( NeonLocalizations.of(context).accountOptionsQuotaUsedOf(
value: quotaRelative / 100, filesize(quotaUsed, 1),
backgroundColor: Theme.of(context).colorScheme.primary.withOpacity(0.3), filesize(quotaTotal, 1),
), quotaRelative.toString(),
const SizedBox( ),
height: 10, );
), }
Text(
NeonLocalizations.of(context).accountOptionsQuotaUsedOf( return CustomSettingsTile(
filesize(quotaUsed, 1), title: LinearProgressIndicator(
filesize(quotaTotal, 1), value: value,
quotaRelative.toString(), minHeight: isCupertino(context) ? 15 : null,
), borderRadius: BorderRadius.circular(isCupertino(context) ? 5 : 3),
), backgroundColor: Theme.of(context).colorScheme.primary.withOpacity(0.3),
],
NeonError(
userDetails.error,
onRetry: userDetailsBloc.refresh,
),
NeonLinearProgressIndicator(
visible: userDetails.isLoading,
),
],
), ),
), subtitle: subtitle,
], );
},
), ),
SettingsCategory( ],
title: Text(NeonLocalizations.of(context).optionsCategoryGeneral), ),
tiles: [ SettingsCategory(
SelectSettingsTile( title: Text(NeonLocalizations.of(context).optionsCategoryGeneral),
option: options.initialApp, tiles: [
), SelectSettingsTile(
], option: options.initialApp,
), ),
], ],
); ),
}, ],
); );
return Scaffold( return Scaffold(

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

@ -1,4 +1,5 @@
import 'package:file_picker/file_picker.dart'; import 'package:file_picker/file_picker.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_material_design_icons/flutter_material_design_icons.dart'; import 'package:flutter_material_design_icons/flutter_material_design_icons.dart';
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
@ -10,14 +11,14 @@ import 'package:neon/src/router.dart';
import 'package:neon/src/settings/utils/settings_export_helper.dart'; import 'package:neon/src/settings/utils/settings_export_helper.dart';
import 'package:neon/src/settings/widgets/account_settings_tile.dart'; import 'package:neon/src/settings/widgets/account_settings_tile.dart';
import 'package:neon/src/settings/widgets/custom_settings_tile.dart'; import 'package:neon/src/settings/widgets/custom_settings_tile.dart';
import 'package:neon/src/settings/widgets/select_settings_tile.dart'; import 'package:neon/src/settings/widgets/option_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/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/settings/widgets/toggle_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/theme/dialog.dart';
import 'package:neon/src/utils/adaptive.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/provider.dart'; import 'package:neon/src/utils/provider.dart';
@ -112,259 +113,194 @@ class _SettingsPageState extends State<SettingsPage> {
), ),
], ],
); );
final body = StreamBuilder( final body = SettingsList(
stream: accountsBloc.accounts, initialCategory: widget.initialCategory?.name,
initialData: accountsBloc.accounts.valueOrNull, categories: [
builder: ( SettingsCategory(
final context, hasLeading: true,
final accountsSnapshot, title: Text(NeonLocalizations.of(context).settingsApps),
) => key: ValueKey(SettingsCategories.apps.name),
ValueListenableBuilder( tiles: <SettingsTile>[
valueListenable: globalOptions.pushNotificationsEnabled, for (final appImplementation in appImplementations) ...[
builder: ( if (appImplementation.options.options.isNotEmpty) ...[
final context, CustomSettingsTile(
final _, leading: appImplementation.buildIcon(),
final __, title: Text(appImplementation.name(context)),
) => onTap: () {
SettingsList( NextcloudAppSettingsRoute(appid: appImplementation.id).go(context);
initialCategory: widget.initialCategory?.name, },
categories: [
SettingsCategory(
title: Text(NeonLocalizations.of(context).settingsApps),
key: ValueKey(SettingsCategories.apps.name),
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(NeonLocalizations.of(context).optionsCategoryTheme),
key: ValueKey(SettingsCategories.theme.name),
tiles: [
SelectSettingsTile(
option: globalOptions.themeMode,
),
ToggleSettingsTile(
option: globalOptions.themeOLEDAsDark,
),
ToggleSettingsTile(
option: globalOptions.themeUseNextcloudTheme,
), ),
], ],
],
],
),
SettingsCategory(
title: Text(NeonLocalizations.of(context).optionsCategoryTheme),
key: ValueKey(SettingsCategories.theme.name),
tiles: [
SelectSettingsTile(
option: globalOptions.themeMode,
), ),
SettingsCategory( ToggleSettingsTile(
title: Text(NeonLocalizations.of(context).optionsCategoryNavigation), option: globalOptions.themeOLEDAsDark,
key: ValueKey(SettingsCategories.navigation.name), ),
tiles: [ ToggleSettingsTile(
SelectSettingsTile( option: globalOptions.themeUseNextcloudTheme,
option: globalOptions.navigationMode, ),
), ],
], ),
SettingsCategory(
title: Text(NeonLocalizations.of(context).optionsCategoryNavigation),
key: ValueKey(SettingsCategories.navigation.name),
tiles: [
SelectSettingsTile(
option: globalOptions.navigationMode,
), ),
if (NeonPlatform.instance.canUsePushNotifications) ...[ ],
SettingsCategory( ),
title: Text(NeonLocalizations.of(context).optionsCategoryPushNotifications), if (NeonPlatform.instance.canUsePushNotifications) buildNotificationsCategory(),
key: ValueKey(SettingsCategories.pushNotifications.name), if (NeonPlatform.instance.canUseWindowManager) ...[
tiles: [ SettingsCategory(
if (!globalOptions.pushNotificationsEnabled.enabled) ...[ title: Text(NeonLocalizations.of(context).optionsCategoryStartup),
TextSettingsTile( key: ValueKey(SettingsCategories.startup.name),
text: NeonLocalizations.of(context).globalOptionsPushNotificationsEnabledDisabledNotice, tiles: [
style: TextStyle( ToggleSettingsTile(
fontWeight: FontWeight.w600, option: globalOptions.startupMinimized,
fontStyle: FontStyle.italic,
color: Theme.of(context).colorScheme.error,
),
),
],
ToggleSettingsTile(
option: globalOptions.pushNotificationsEnabled,
),
SelectSettingsTile(
option: globalOptions.pushNotificationsDistributor,
),
],
), ),
], ToggleSettingsTile(
if (NeonPlatform.instance.canUseWindowManager) ...[ option: globalOptions.startupMinimizeInsteadOfExit,
SettingsCategory(
title: Text(NeonLocalizations.of(context).optionsCategoryStartup),
key: ValueKey(SettingsCategories.startup.name),
tiles: [
ToggleSettingsTile(
option: globalOptions.startupMinimized,
),
ToggleSettingsTile(
option: globalOptions.startupMinimizeInsteadOfExit,
),
],
), ),
], ],
if (NeonPlatform.instance.canUseWindowManager && NeonPlatform.instance.canUseSystemTray) ...[ ),
SettingsCategory( ],
title: Text(NeonLocalizations.of(context).optionsCategorySystemTray), if (NeonPlatform.instance.canUseWindowManager && NeonPlatform.instance.canUseSystemTray) ...[
key: ValueKey(SettingsCategories.systemTray.name), SettingsCategory(
tiles: [ title: Text(NeonLocalizations.of(context).optionsCategorySystemTray),
ToggleSettingsTile( key: ValueKey(SettingsCategories.systemTray.name),
option: globalOptions.systemTrayEnabled, tiles: [
), ToggleSettingsTile(
ToggleSettingsTile( option: globalOptions.systemTrayEnabled,
option: globalOptions.systemTrayHideToTrayWhenMinimized, ),
), ToggleSettingsTile(
], option: globalOptions.systemTrayHideToTrayWhenMinimized,
), ),
], ],
SettingsCategory( ),
title: Text(NeonLocalizations.of(context).optionsCategoryAccounts), ],
key: ValueKey(SettingsCategories.accounts.name), ...buildAccountCategory(),
tiles: [ SettingsCategory(
if (accountsSnapshot.requireData.length > 1) ...[ hasLeading: true,
ToggleSettingsTile( title: Text(NeonLocalizations.of(context).optionsCategoryOther),
option: globalOptions.rememberLastUsedAccount, key: ValueKey(SettingsCategories.other.name),
), tiles: [
SelectSettingsTile( if (branding.sourceCodeURL != null)
option: globalOptions.initialAccount, CustomSettingsTile(
), leading: Icon(
], Icons.code,
for (final account in accountsSnapshot.requireData) ...[ color: Theme.of(context).colorScheme.primary,
AccountSettingsTile(
account: account,
onTap: () {
AccountSettingsRoute(accountID: account.id).go(context);
},
),
],
CustomSettingsTile(
title: ElevatedButton.icon(
onPressed: () async => const LoginRoute().push(context),
icon: const Icon(MdiIcons.accountPlus),
label: Text(NeonLocalizations.of(context).globalOptionsAccountsAdd),
),
), ),
], title: Text(NeonLocalizations.of(context).sourceCode),
), onTap: () async {
SettingsCategory( await launchUrlString(
title: Text(NeonLocalizations.of(context).optionsCategoryOther), branding.sourceCodeURL!,
key: ValueKey(SettingsCategories.other.name), mode: LaunchMode.externalApplication,
tiles: <SettingsTile>[ );
if (branding.sourceCodeURL != null) },
CustomSettingsTile( ),
leading: Icon( if (branding.issueTrackerURL != null)
Icons.code, CustomSettingsTile(
color: Theme.of(context).colorScheme.primary, leading: Icon(
), MdiIcons.textBoxEditOutline,
title: Text(NeonLocalizations.of(context).sourceCode), color: Theme.of(context).colorScheme.primary,
onTap: () async {
await launchUrlString(
branding.sourceCodeURL!,
mode: LaunchMode.externalApplication,
);
},
),
if (branding.issueTrackerURL != null)
CustomSettingsTile(
leading: Icon(
MdiIcons.textBoxEditOutline,
color: Theme.of(context).colorScheme.primary,
),
title: Text(NeonLocalizations.of(context).issueTracker),
onTap: () async {
await launchUrlString(
branding.issueTrackerURL!,
mode: LaunchMode.externalApplication,
);
},
),
CustomSettingsTile(
leading: Icon(
MdiIcons.scriptText,
color: Theme.of(context).colorScheme.primary,
),
title: Text(NeonLocalizations.of(context).licenses),
onTap: () async {
showLicensePage(
context: context,
applicationName: branding.name,
applicationIcon: branding.logo,
applicationLegalese: branding.legalese,
applicationVersion: NeonProvider.of<PackageInfo>(context).version,
);
},
), ),
CustomSettingsTile( title: Text(NeonLocalizations.of(context).issueTracker),
leading: Icon( onTap: () async {
MdiIcons.export, await launchUrlString(
color: Theme.of(context).colorScheme.primary, branding.issueTrackerURL!,
), mode: LaunchMode.externalApplication,
title: Text(NeonLocalizations.of(context).settingsExport), );
onTap: () async { },
final settingsExportHelper = _buildSettingsExportHelper(context); ),
CustomSettingsTile(
try { leading: Icon(
final fileName = 'nextcloud-neon-settings-${DateTime.now().millisecondsSinceEpoch ~/ 1000}.json'; MdiIcons.scriptText,
color: Theme.of(context).colorScheme.primary,
),
title: Text(NeonLocalizations.of(context).licenses),
onTap: () async {
showLicensePage(
context: context,
applicationName: branding.name,
applicationIcon: branding.logo,
applicationLegalese: branding.legalese,
applicationVersion: NeonProvider.of<PackageInfo>(context).version,
);
},
),
CustomSettingsTile(
leading: Icon(
MdiIcons.export,
color: Theme.of(context).colorScheme.primary,
),
title: Text(NeonLocalizations.of(context).settingsExport),
onTap: () async {
final settingsExportHelper = _buildSettingsExportHelper(context);
final data = settingsExportHelper.exportToFile(); try {
await saveFileWithPickDialog(fileName, data); final fileName = 'nextcloud-neon-settings-${DateTime.now().millisecondsSinceEpoch ~/ 1000}.json';
} catch (e, s) {
debugPrint(e.toString());
debugPrint(s.toString());
if (mounted) {
NeonError.showSnackbar(context, e);
}
}
},
),
CustomSettingsTile(
leading: Icon(
MdiIcons.import,
color: Theme.of(context).colorScheme.primary,
),
title: Text(NeonLocalizations.of(context).settingsImport),
onTap: () async {
final settingsExportHelper = _buildSettingsExportHelper(context);
try { final data = settingsExportHelper.exportToFile();
final result = await FilePicker.platform.pickFiles( await saveFileWithPickDialog(fileName, data);
withReadStream: true, } catch (e, s) {
); debugPrint(e.toString());
debugPrint(s.toString());
if (mounted) {
NeonError.showSnackbar(context, e);
}
}
},
),
CustomSettingsTile(
leading: Icon(
MdiIcons.import,
color: Theme.of(context).colorScheme.primary,
),
title: Text(NeonLocalizations.of(context).settingsImport),
onTap: () async {
final settingsExportHelper = _buildSettingsExportHelper(context);
if (result == null) { try {
return; final result = await FilePicker.platform.pickFiles(
} withReadStream: true,
);
if (!result.files.single.path!.endsWith('.json')) { if (result == null) {
if (mounted) { return;
NeonError.showSnackbar( }
context,
NeonLocalizations.of(context).settingsImportWrongFileExtension,
);
}
return;
}
await settingsExportHelper.applyFromFile(result.files.single.readStream); if (!result.files.single.path!.endsWith('.json')) {
} catch (e, s) { if (mounted) {
debugPrint(e.toString()); NeonError.showSnackbar(
debugPrint(s.toString()); context,
if (mounted) { NeonLocalizations.of(context).settingsImportWrongFileExtension,
NeonError.showSnackbar(context, e); );
}
} }
}, return;
), }
],
await settingsExportHelper.applyFromFile(result.files.single.readStream);
} catch (e, s) {
debugPrint(e.toString());
debugPrint(s.toString());
if (mounted) {
NeonError.showSnackbar(context, e);
}
}
},
), ),
], ],
), ),
), ],
); );
return Scaffold( return Scaffold(
@ -381,6 +317,105 @@ class _SettingsPageState extends State<SettingsPage> {
); );
} }
Widget buildNotificationsCategory() {
final globalOptions = NeonProvider.of<GlobalOptions>(context);
return ValueListenableBuilder(
valueListenable: globalOptions.pushNotificationsEnabled,
builder: (final context, final _, final __) => SettingsCategory(
title: Text(NeonLocalizations.of(context).optionsCategoryPushNotifications),
key: ValueKey(SettingsCategories.pushNotifications.name),
tiles: [
if (!globalOptions.pushNotificationsEnabled.enabled)
TextSettingsTile(
text: NeonLocalizations.of(context).globalOptionsPushNotificationsEnabledDisabledNotice,
style: TextStyle(
fontWeight: FontWeight.w600,
fontStyle: FontStyle.italic,
color: Theme.of(context).colorScheme.error,
),
),
ToggleSettingsTile(
option: globalOptions.pushNotificationsEnabled,
),
SelectSettingsTile(
option: globalOptions.pushNotificationsDistributor,
),
],
),
);
}
Iterable<Widget> buildAccountCategory() sync* {
final globalOptions = NeonProvider.of<GlobalOptions>(context);
final accountsBloc = NeonProvider.of<AccountsBloc>(context);
final accounts = accountsBloc.accounts.value;
final hasMultipleAccounts = accounts.length > 1;
final title = Text(NeonLocalizations.of(context).optionsCategoryAccounts);
final key = ValueKey(SettingsCategories.accounts.name);
final accountTiles = accounts.map(
(final account) => AccountSettingsTile(
account: account,
onTap: () => AccountSettingsRoute(accountID: account.id).go(context),
),
);
final rememberLastUsedAccountTile = ToggleSettingsTile(
option: globalOptions.rememberLastUsedAccount,
);
final initialAccountTile = SelectSettingsTile(
option: globalOptions.initialAccount,
);
if (isCupertino(context)) {
if (hasMultipleAccounts) {
yield SettingsCategory(
title: title,
key: key,
tiles: [
rememberLastUsedAccountTile,
initialAccountTile,
],
);
}
final addAccountTile = CustomSettingsTile(
leading: const Icon(
CupertinoIcons.add,
),
title: Text(NeonLocalizations.of(context).globalOptionsAccountsAdd),
onTap: () async => const LoginRoute().push(context),
);
yield CupertinoListSection.insetGrouped(
header: !hasMultipleAccounts ? title : null,
key: !hasMultipleAccounts ? key : null,
children: [
...accountTiles,
addAccountTile,
],
);
} else {
final addAccountTile = CustomSettingsTile(
title: ElevatedButton.icon(
onPressed: () async => const LoginRoute().push(context),
icon: const Icon(MdiIcons.accountPlus),
label: Text(NeonLocalizations.of(context).globalOptionsAccountsAdd),
),
);
yield SettingsCategory(
title: title,
key: key,
tiles: [
if (hasMultipleAccounts) rememberLastUsedAccountTile,
if (hasMultipleAccounts) initialAccountTile,
...accountTiles,
addAccountTile,
],
);
}
}
SettingsExportHelper _buildSettingsExportHelper(final BuildContext context) { SettingsExportHelper _buildSettingsExportHelper(final BuildContext context) {
final globalOptions = NeonProvider.of<GlobalOptions>(context); final globalOptions = NeonProvider.of<GlobalOptions>(context);
final accountsBloc = NeonProvider.of<AccountsBloc>(context); final accountsBloc = NeonProvider.of<AccountsBloc>(context);

212
packages/neon/neon/lib/src/settings/widgets/option_settings_tile.dart

@ -1,9 +1,13 @@
import 'package:flutter/widgets.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
import 'package:neon/settings.dart'; import 'package:neon/settings.dart';
import 'package:neon/src/settings/widgets/select_settings_tile.dart'; import 'package:neon/src/settings/models/option.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/toggle_settings_tile.dart'; import 'package:neon/src/theme/dialog.dart';
import 'package:neon/src/utils/adaptive.dart';
import 'package:neon/src/widgets/adaptive_widgets/list_tile.dart';
import 'package:neon/utils.dart';
@internal @internal
class OptionSettingsTile extends InputSettingsTile { class OptionSettingsTile extends InputSettingsTile {
@ -18,3 +22,205 @@ class OptionSettingsTile extends InputSettingsTile {
SelectOption() => SelectSettingsTile(option: option as SelectOption), SelectOption() => SelectSettingsTile(option: option as SelectOption),
}; };
} }
@internal
class ToggleSettingsTile extends InputSettingsTile<ToggleOption> {
const ToggleSettingsTile({
required super.option,
super.key,
});
@override
Widget build(final BuildContext context) => ValueListenableBuilder(
valueListenable: option,
builder: (final context, final value, final child) => SwitchListTile.adaptive(
title: child,
value: value,
onChanged: option.enabled ? (final value) => option.value = value : null,
),
child: Text(option.label(context)),
);
}
@internal
class SelectSettingsTile<T> extends InputSettingsTile<SelectOption<T>> {
const SelectSettingsTile({
required super.option,
this.immediateSelection = true,
super.key,
});
final bool immediateSelection;
@override
Widget build(final BuildContext context) => ValueListenableBuilder(
valueListenable: option,
builder: (final context, final value, final child) {
final valueText = option.values[value]?.call(context);
return AdaptiveListTile.additionalInfo(
enabled: option.enabled,
title: child!,
additionalInfo: valueText != null ? Text(valueText) : null,
onTap: () async {
assert(option.enabled, 'Option must be enabled to handle taps');
final showCupertino = isCupertino(context);
if (showCupertino) {
await Navigator.push(
context,
CupertinoPageRoute<void>(
builder: (final _) => SelectSettingsTileScreen(option: option),
),
);
} else {
final result = await showAdaptiveDialog<T>(
context: context,
builder: (final context) => SelectSettingsTileDialog(
option: option,
immediateSelection: immediateSelection,
),
);
if (result != null) {
option.value = result;
}
}
},
);
},
child: Text(
option.label(context),
),
);
}
@internal
class SelectSettingsTileDialog<T> extends StatefulWidget {
const SelectSettingsTileDialog({
required this.option,
this.immediateSelection = true,
super.key,
});
final SelectOption<T> option;
final bool immediateSelection;
@override
State<SelectSettingsTileDialog<T>> createState() => _SelectSettingsTileDialogState<T>();
}
class _SelectSettingsTileDialogState<T> extends State<SelectSettingsTileDialog<T>> {
late T value;
late SelectOption<T> option = widget.option;
@override
void initState() {
value = option.value;
super.initState();
}
void submit() => Navigator.pop(context, value);
void cancel() => Navigator.pop(context);
@override
Widget build(final BuildContext context) {
final content = SingleChildScrollView(
child: Column(
children: [
...option.values.keys.map(
(final k) => RadioListTile(
title: Text(
option.values[k]!(context),
overflow: TextOverflow.ellipsis,
),
value: k,
groupValue: value,
onChanged: (final value) {
setState(() {
this.value = value as T;
});
if (widget.immediateSelection) {
submit();
}
},
),
),
],
),
);
final actions = [
TextButton(
onPressed: cancel,
child: Text(NeonLocalizations.of(context).actionClose),
),
TextButton(
onPressed: submit,
child: Text(NeonLocalizations.of(context).actionContinue),
),
];
return AlertDialog(
title: Text(
option.label(context),
),
content: content,
actions: widget.immediateSelection ? null : actions,
);
}
}
@internal
class SelectSettingsTileScreen<T> extends StatelessWidget {
const SelectSettingsTileScreen({
required this.option,
super.key,
});
final SelectOption<T> option;
@override
Widget build(final BuildContext context) {
final dialogTheme = NeonDialogTheme.of(context);
final selector = ValueListenableBuilder(
valueListenable: option,
builder: (final context, final value, final child) => CupertinoListSection.insetGrouped(
hasLeading: false,
header: child,
children: [
...option.values.keys.map(
(final k) => RadioListTile.adaptive(
controlAffinity: ListTileControlAffinity.trailing,
title: Text(
option.values[k]!(context),
overflow: TextOverflow.ellipsis,
),
value: k,
groupValue: value,
onChanged: (final value) {
option.value = value as T;
},
),
),
],
),
child: Text(
option.label(context),
),
);
return Scaffold(
appBar: AppBar(),
body: Center(
child: ConstrainedBox(
constraints: dialogTheme.constraints,
child: selector,
),
),
);
}
}

53
packages/neon/neon/lib/src/settings/widgets/select_settings_tile.dart

@ -1,53 +0,0 @@
import 'package:flutter/material.dart';
import 'package:meta/meta.dart';
import 'package:neon/src/settings/models/option.dart';
import 'package:neon/src/settings/widgets/settings_tile.dart';
@internal
class SelectSettingsTile<T> extends InputSettingsTile<SelectOption<T>> {
const SelectSettingsTile({
required super.option,
super.key,
});
@override
Widget build(final BuildContext context) => ValueListenableBuilder(
valueListenable: option,
builder: (final context, final value, final child) => LayoutBuilder(
builder: (final context, final constraints) => ListTile(
enabled: option.enabled,
title: child,
trailing: ConstrainedBox(
constraints: BoxConstraints(
maxWidth: constraints.maxWidth * 0.5,
),
child: IntrinsicWidth(
child: DropdownButton<T>(
isExpanded: true,
value: value,
items: option.values.keys
.map(
(final k) => DropdownMenuItem(
value: k,
child: Text(
option.values[k]!(context),
overflow: TextOverflow.ellipsis,
),
),
)
.toList(),
onChanged: option.enabled
? (final value) {
option.value = value as T;
}
: null,
),
),
),
),
),
child: Text(
option.label(context),
),
);
}

63
packages/neon/neon/lib/src/settings/widgets/settings_category.dart

@ -1,41 +1,72 @@
import 'package:flutter/cupertino.dart';
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_tile.dart'; import 'package:neon/src/utils/adaptive.dart';
@internal @internal
class SettingsCategory extends StatelessWidget { class SettingsCategory extends StatelessWidget {
const SettingsCategory({ const SettingsCategory({
required this.tiles, required this.tiles,
this.title, this.title,
this.footer,
this.hasLeading = false,
super.key, super.key,
}); });
final Widget? title; final Widget? title;
final List<SettingsTile> tiles; final List<Widget> tiles;
final Widget? footer;
final bool hasLeading;
@override @override
Widget build(final BuildContext context) { Widget build(final BuildContext context) {
if (isCupertino(context)) {
return CupertinoListSection.insetGrouped(
hasLeading: hasLeading,
header: title,
footer: footer,
children: tiles,
);
} else {
return MaterialSettingsCategory(
header: title,
children: tiles,
);
}
}
}
class MaterialSettingsCategory extends StatelessWidget {
const MaterialSettingsCategory({
required this.children,
this.header,
super.key,
});
final Widget? header;
final List<Widget> children;
@override
Widget build(final BuildContext context) {
final theme = Theme.of(context);
final textTheme = Theme.of(context).textTheme; final textTheme = Theme.of(context).textTheme;
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
if (title != null) if (header != null)
DefaultTextStyle( Padding(
style: textTheme.titleMedium!.copyWith( padding: const EdgeInsets.only(top: 25, bottom: 5),
fontWeight: FontWeight.bold, child: DefaultTextStyle(
style: textTheme.titleMedium!.copyWith(
color: theme.colorScheme.secondary,
fontWeight: FontWeight.bold,
),
child: header!,
), ),
child: title!,
), ),
...tiles, ...children,
] ],
.intersperse(
const SizedBox(
height: 10,
),
)
.toList(),
); );
} }
} }

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

@ -1,5 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:neon/src/settings/widgets/settings_category.dart'; import 'package:neon/src/utils/adaptive.dart';
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
@visibleForTesting @visibleForTesting
@ -10,7 +10,7 @@ class SettingsList extends StatelessWidget {
super.key, super.key,
}); });
final List<SettingsCategory> categories; final List<Widget> categories;
final String? initialCategory; final String? initialCategory;
int? _getIndex(final String? initialCategory) { int? _getIndex(final String? initialCategory) {
@ -25,11 +25,14 @@ class SettingsList extends StatelessWidget {
} }
@override @override
Widget build(final BuildContext context) => ScrollablePositionedList.separated( Widget build(final BuildContext context) {
padding: const EdgeInsets.all(20), final hasPadding = !isCupertino(context);
itemCount: categories.length,
initialScrollIndex: _getIndex(initialCategory) ?? 0, return ScrollablePositionedList.builder(
itemBuilder: (final context, final index) => categories[index], padding: hasPadding ? const EdgeInsets.symmetric(horizontal: 20) : null,
separatorBuilder: (final context, final index) => const Divider(), itemCount: categories.length,
); initialScrollIndex: _getIndex(initialCategory) ?? 0,
itemBuilder: (final context, final index) => categories[index],
);
}
} }

23
packages/neon/neon/lib/src/settings/widgets/toggle_settings_tile.dart

@ -1,23 +0,0 @@
import 'package:flutter/material.dart';
import 'package:meta/meta.dart';
import 'package:neon/src/settings/models/option.dart';
import 'package:neon/src/settings/widgets/settings_tile.dart';
@internal
class ToggleSettingsTile extends InputSettingsTile<ToggleOption> {
const ToggleSettingsTile({
required super.option,
super.key,
});
@override
Widget build(final BuildContext context) => ValueListenableBuilder(
valueListenable: option,
builder: (final context, final value, final child) => SwitchListTile.adaptive(
title: child,
value: value,
onChanged: option.enabled ? (final value) => option.value = value : null,
),
child: Text(option.label(context)),
);
}

5
packages/neon/neon/lib/src/theme/theme.dart

@ -1,3 +1,4 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
import 'package:neon/src/theme/colors.dart'; import 'package:neon/src/theme/colors.dart';
@ -90,6 +91,10 @@ class AppTheme {
dividerTheme: _dividerTheme, dividerTheme: _dividerTheme,
scrollbarTheme: _scrollbarTheme, scrollbarTheme: _scrollbarTheme,
inputDecorationTheme: _inputDecorationTheme, inputDecorationTheme: _inputDecorationTheme,
cupertinoOverrideTheme: CupertinoThemeData(
brightness: brightness,
textTheme: const CupertinoTextThemeData(),
),
extensions: [ extensions: [
neonTheme, neonTheme,
...?appThemes, ...?appThemes,

1
packages/neon/neon/pubspec.yaml

@ -9,6 +9,7 @@ environment:
dependencies: dependencies:
collection: ^1.0.0 collection: ^1.0.0
crypto: ^3.0.0 crypto: ^3.0.0
cupertino_icons: ^1.0.0
dynamic_color: ^1.0.0 dynamic_color: ^1.0.0
file_picker: ^6.0.0 file_picker: ^6.0.0
filesize: ^2.0.0 filesize: ^2.0.0

Loading…
Cancel
Save