Browse Source

neon: add ability to scroll to a specific setting

pull/456/head
Nikolas Rimikis 1 year ago
parent
commit
58e8392dca
No known key found for this signature in database
GPG Key ID: 85ED1DE9786A4FF2
  1. 8
      packages/app/pubspec.lock
  2. 420
      packages/neon/neon/lib/src/pages/settings.dart
  3. 7
      packages/neon/neon/lib/src/router.dart
  4. 50
      packages/neon/neon/lib/src/router.g.dart
  5. 27
      packages/neon/neon/lib/src/settings/widgets/settings_list.dart
  6. 1
      packages/neon/neon/pubspec.yaml

8
packages/app/pubspec.lock

@ -1037,6 +1037,14 @@ packages:
url: "https://pub.dev"
source: hosted
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:
dependency: transitive
description:

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

@ -27,11 +27,25 @@ import 'package:neon/src/widgets/exception.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:provider/provider.dart';
enum SettingsCageories {
apps,
theme,
navigation,
pushNotifications,
startup,
systemTray,
accounts,
other,
}
class SettingsPage extends StatefulWidget {
const SettingsPage({
this.initialCategory,
super.key,
});
final SettingsCageories? initialCategory;
@override
State<SettingsPage> createState() => _SettingsPageState();
}
@ -42,6 +56,7 @@ class _SettingsPageState extends State<SettingsPage> {
final globalOptions = Provider.of<GlobalOptions>(context);
final accountsBloc = Provider.of<AccountsBloc>(context, listen: false);
final appImplementations = Provider.of<Iterable<AppImplementation>>(context);
return Scaffold(
resizeToAvoidBottomInset: false,
appBar: AppBar(
@ -68,245 +83,254 @@ class _SettingsPageState extends State<SettingsPage> {
),
body: StreamBuilder<List<Account>>(
stream: accountsBloc.accounts,
initialData: accountsBloc.accounts.valueOrNull,
builder: (
final context,
final accountsSnapshot,
) {
final settingsExportHelper = SettingsExportHelper(
globalOptions: globalOptions,
appImplementations: appImplementations,
accountSpecificOptions: {
if (accountsSnapshot.hasData) ...{
for (final account in accountsSnapshot.requireData) ...{
account: accountsBloc.getOptionsFor(account).options,
},
},
},
);
final platform = Provider.of<NeonPlatform>(context, listen: false);
return StreamBuilder<Account?>(
stream: accountsBloc.activeAccount,
return StreamBuilder<bool>(
stream: globalOptions.pushNotificationsEnabled.enabled,
initialData: globalOptions.pushNotificationsEnabled.enabled.valueOrNull,
builder: (
final context,
final activeAccountSnapshot,
final pushNotificationsEnabledEnabledSnapshot,
) =>
StreamBuilder<bool>(
stream: globalOptions.pushNotificationsEnabled.enabled,
builder: (
final context,
final pushNotificationsEnabledEnabledSnapshot,
) =>
SettingsList(
categories: [
SettingsList(
initialCategory: widget.initialCategory?.name,
categories: [
SettingsCategory(
title: Text(AppLocalizations.of(context).settingsApps),
key: ValueKey(SettingsCageories.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(AppLocalizations.of(context).optionsCategoryTheme),
key: ValueKey(SettingsCageories.theme.name),
tiles: [
DropdownButtonSettingsTile(
option: globalOptions.themeMode,
),
CheckBoxSettingsTile(
option: globalOptions.themeOLEDAsDark,
),
CheckBoxSettingsTile(
option: globalOptions.themeKeepOriginalAccentColor,
),
],
),
SettingsCategory(
title: Text(AppLocalizations.of(context).optionsCategoryNavigation),
key: ValueKey(SettingsCageories.navigation.name),
tiles: [
DropdownButtonSettingsTile(
option: globalOptions.navigationMode,
),
],
),
if (platform.canUsePushNotifications) ...[
SettingsCategory(
title: Text(AppLocalizations.of(context).settingsApps),
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);
},
title: Text(AppLocalizations.of(context).optionsCategoryPushNotifications),
key: ValueKey(SettingsCageories.pushNotifications.name),
tiles: [
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).optionsCategoryTheme),
title: Text(AppLocalizations.of(context).optionsCategoryStartup),
key: ValueKey(SettingsCageories.startup.name),
tiles: [
DropdownButtonSettingsTile(
option: globalOptions.themeMode,
),
CheckBoxSettingsTile(
option: globalOptions.themeOLEDAsDark,
option: globalOptions.startupMinimized,
),
CheckBoxSettingsTile(
option: globalOptions.themeKeepOriginalAccentColor,
option: globalOptions.startupMinimizeInsteadOfExit,
),
],
),
],
if (platform.canUseWindowManager && platform.canUseSystemTray) ...[
SettingsCategory(
title: Text(AppLocalizations.of(context).optionsCategoryNavigation),
title: Text(AppLocalizations.of(context).optionsCategorySystemTray),
key: ValueKey(SettingsCageories.systemTray.name),
tiles: [
DropdownButtonSettingsTile(
option: globalOptions.navigationMode,
CheckBoxSettingsTile(
option: globalOptions.systemTrayEnabled,
),
CheckBoxSettingsTile(
option: globalOptions.systemTrayHideToTrayWhenMinimized,
),
],
),
if (platform.canUsePushNotifications) ...[
SettingsCategory(
title: Text(AppLocalizations.of(context).optionsCategoryPushNotifications),
tiles: [
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),
tiles: [
CheckBoxSettingsTile(
option: globalOptions.startupMinimized,
),
CheckBoxSettingsTile(
option: globalOptions.startupMinimizeInsteadOfExit,
),
],
),
],
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).optionsCategoryAccounts),
key: ValueKey(SettingsCageories.accounts.name),
tiles: [
if (accountsSnapshot.requireData.length > 1) ...[
CheckBoxSettingsTile(
option: globalOptions.rememberLastUsedAccount,
),
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),
),
)
],
if (accountsSnapshot.hasData) ...[
SettingsCategory(
title: Text(AppLocalizations.of(context).optionsCategoryAccounts),
tiles: [
if (accountsSnapshot.requireData.length > 1) ...[
CheckBoxSettingsTile(
option: globalOptions.rememberLastUsedAccount,
),
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(
title: Text(AppLocalizations.of(context).optionsCategoryOther),
key: ValueKey(SettingsCageories.other.name),
tiles: <SettingsTile>[
CustomSettingsTile(
leading: Icon(
MdiIcons.scriptText,
color: Theme.of(context).colorScheme.primary,
),
title: Text(AppLocalizations.of(context).licenses),
onTap: () async {
final branding = Branding.of(context);
showLicensePage(
context: context,
applicationName: branding.name,
applicationIcon: branding.logo,
applicationLegalese: branding.legalese,
applicationVersion: Provider.of<PackageInfo>(context, listen: false).version,
);
},
),
],
SettingsCategory(
title: Text(AppLocalizations.of(context).optionsCategoryOther),
tiles: <SettingsTile>[
CustomSettingsTile(
leading: Icon(
MdiIcons.scriptText,
color: Theme.of(context).colorScheme.primary,
),
title: Text(AppLocalizations.of(context).licenses),
onTap: () async {
final branding = Branding.of(context);
showLicensePage(
context: context,
applicationName: branding.name,
applicationIcon: branding.logo,
applicationLegalese: branding.legalese,
applicationVersion: Provider.of<PackageInfo>(context, listen: false).version,
);
},
CustomSettingsTile(
leading: Icon(
MdiIcons.export,
color: Theme.of(context).colorScheme.primary,
),
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(),
),
title: Text(AppLocalizations.of(context).settingsExport),
onTap: () async {
final settingsExportHelper = _buildSettingsExportHelper(context);
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());
debugPrint(s.toString());
NeonException.showSnackbar(context, e);
}
},
),
);
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,
),
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,
);
title: Text(AppLocalizations.of(context).settingsImport),
onTap: () async {
final settingsExportHelper = _buildSettingsExportHelper(context);
if (result == null) {
return;
}
try {
final result = await FilePicker.platform.pickFiles(
withData: true,
);
if (!result.files.single.path!.endsWith('.json.base64')) {
if (mounted) {
NeonException.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<String, dynamic>);
} catch (e, s) {
debugPrint(e.toString());
debugPrint(s.toString());
NeonException.showSnackbar(context, e);
if (!result.files.single.path!.endsWith('.json.base64')) {
if (mounted) {
NeonException.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<String, dynamic>);
} catch (e, s) {
debugPrint(e.toString());
debugPrint(s.toString());
NeonException.showSnackbar(context, e);
}
},
),
],
),
],
),
);
},
),
);
}
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 {

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

@ -367,8 +367,11 @@ class NextcloudAppSettingsRoute extends GoRouteData {
@immutable
class SettingsRoute extends GoRouteData {
const SettingsRoute();
const SettingsRoute({this.initialCategory});
/// The initial category to show.
final SettingsCageories? initialCategory;
@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);
void pushReplacement(BuildContext context) => context.pushReplacement(location);
void replace(BuildContext context) => context.replace(location);
}
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(
'/settings',
queryParams: {
if (initialCategory != null) 'initial-category': _$SettingsCageoriesEnumMap[initialCategory!],
},
);
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);
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 {
static NextcloudAppSettingsRoute _fromState(GoRouterState state) => NextcloudAppSettingsRoute(
appid: state.pathParameters['appid']!,
@ -101,6 +122,8 @@ extension $NextcloudAppSettingsRouteExtension on NextcloudAppSettingsRoute {
Future<T?> push<T>(BuildContext context) => context.push<T>(location);
void pushReplacement(BuildContext context) => context.pushReplacement(location);
void replace(BuildContext context) => context.replace(location);
}
extension $_AddAccountRouteExtension on _AddAccountRoute {
@ -115,6 +138,8 @@ extension $_AddAccountRouteExtension on _AddAccountRoute {
Future<T?> push<T>(BuildContext context) => context.push<T>(location);
void pushReplacement(BuildContext context) => context.pushReplacement(location);
void replace(BuildContext context) => context.replace(location);
}
extension $_AddAccountFlowRouteExtension on _AddAccountFlowRoute {
@ -206,6 +231,21 @@ extension $AccountSettingsRouteExtension on AccountSettingsRoute {
Future<T?> push<T>(BuildContext context) => context.push<T>(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(
@ -244,6 +284,8 @@ extension $LoginRouteExtension on LoginRoute {
Future<T?> push<T>(BuildContext context) => context.push<T>(location);
void pushReplacement(BuildContext context) => context.pushReplacement(location);
void replace(BuildContext context) => context.replace(location);
}
extension $LoginFlowRouteExtension on LoginFlowRoute {
@ -277,6 +319,8 @@ extension $LoginQrcodeRouteExtension on LoginQrcodeRoute {
Future<T?> push<T>(BuildContext context) => context.push<T>(location);
void pushReplacement(BuildContext context) => context.pushReplacement(location);
void replace(BuildContext context) => context.replace(location);
}
extension $LoginCheckServerStatusRouteExtension on LoginCheckServerStatusRoute {
@ -296,6 +340,8 @@ extension $LoginCheckServerStatusRouteExtension on LoginCheckServerStatusRoute {
Future<T?> push<T>(BuildContext context) => context.push<T>(location);
void pushReplacement(BuildContext context) => context.pushReplacement(location);
void replace(BuildContext context) => context.replace(location);
}
extension $LoginCheckAccountRouteExtension on LoginCheckAccountRoute {
@ -319,4 +365,6 @@ extension $LoginCheckAccountRouteExtension on LoginCheckAccountRoute {
Future<T?> push<T>(BuildContext context) => context.push<T>(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:intersperse/intersperse.dart';
import 'package:meta/meta.dart';
import 'package:neon/src/settings/widgets/settings_category.dart';
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
@internal
class SettingsList extends StatelessWidget {
const SettingsList({
required this.categories,
this.initialCategory,
super.key,
});
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
Widget build(final BuildContext context) => Scrollbar(
child: ListView(
primary: true,
padding: const EdgeInsets.all(20),
children: categories.cast<Widget>().intersperse(const Divider()).toList(),
),
Widget build(final BuildContext context) => ScrollablePositionedList.separated(
padding: const EdgeInsets.all(20),
itemCount: categories.length,
initialScrollIndex: _getIndex(initialCategory) ?? 0,
itemBuilder: (final context, final index) => categories[index],
separatorBuilder: (final context, final index) => const Divider(),
);
}

1
packages/neon/neon/pubspec.yaml

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

Loading…
Cancel
Save