Browse Source

neon: add ability to scroll to a specific setting

pull/456/head
Nikolas Rimikis 2 years 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" 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:

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: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,6 +56,7 @@ 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( return Scaffold(
resizeToAvoidBottomInset: false, resizeToAvoidBottomInset: false,
appBar: AppBar( appBar: AppBar(
@ -68,245 +83,254 @@ class _SettingsPageState extends State<SettingsPage> {
), ),
body: StreamBuilder<List<Account>>( body: StreamBuilder<List<Account>>(
stream: accountsBloc.accounts, stream: accountsBloc.accounts,
initialData: accountsBloc.accounts.valueOrNull,
builder: ( builder: (
final context, final context,
final accountsSnapshot, 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); final platform = Provider.of<NeonPlatform>(context, listen: false);
return StreamBuilder<Account?>( return StreamBuilder<bool>(
stream: accountsBloc.activeAccount, stream: globalOptions.pushNotificationsEnabled.enabled,
initialData: globalOptions.pushNotificationsEnabled.enabled.valueOrNull,
builder: ( builder: (
final context, final context,
final activeAccountSnapshot, final pushNotificationsEnabledEnabledSnapshot,
) => ) =>
StreamBuilder<bool>( SettingsList(
stream: globalOptions.pushNotificationsEnabled.enabled, initialCategory: widget.initialCategory?.name,
builder: ( categories: [
final context, SettingsCategory(
final pushNotificationsEnabledEnabledSnapshot, title: Text(AppLocalizations.of(context).settingsApps),
) => key: ValueKey(SettingsCageories.apps.name),
SettingsList( tiles: <SettingsTile>[
categories: [ 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( SettingsCategory(
title: Text(AppLocalizations.of(context).settingsApps), title: Text(AppLocalizations.of(context).optionsCategoryPushNotifications),
tiles: <SettingsTile>[ key: ValueKey(SettingsCageories.pushNotifications.name),
for (final appImplementation in appImplementations) ...[ tiles: [
if (appImplementation.options.options.isNotEmpty) ...[ if (pushNotificationsEnabledEnabledSnapshot.hasData &&
CustomSettingsTile( !pushNotificationsEnabledEnabledSnapshot.requireData) ...[
leading: appImplementation.buildIcon(), TextSettingsTile(
title: Text(appImplementation.name(context)), text: AppLocalizations.of(context).globalOptionsPushNotificationsEnabledDisabledNotice,
onTap: () { style: TextStyle(
NextcloudAppSettingsRoute(appid: appImplementation.id).go(context); fontWeight: FontWeight.w600,
}, fontStyle: FontStyle.italic,
color: Theme.of(context).colorScheme.error,
), ),
], ),
], ],
CheckBoxSettingsTile(
option: globalOptions.pushNotificationsEnabled,
),
DropdownButtonSettingsTile(
option: globalOptions.pushNotificationsDistributor,
),
], ],
), ),
],
if (platform.canUseWindowManager) ...[
SettingsCategory( SettingsCategory(
title: Text(AppLocalizations.of(context).optionsCategoryTheme), title: Text(AppLocalizations.of(context).optionsCategoryStartup),
key: ValueKey(SettingsCageories.startup.name),
tiles: [ tiles: [
DropdownButtonSettingsTile(
option: globalOptions.themeMode,
),
CheckBoxSettingsTile( CheckBoxSettingsTile(
option: globalOptions.themeOLEDAsDark, option: globalOptions.startupMinimized,
), ),
CheckBoxSettingsTile( CheckBoxSettingsTile(
option: globalOptions.themeKeepOriginalAccentColor, option: globalOptions.startupMinimizeInsteadOfExit,
), ),
], ],
), ),
],
if (platform.canUseWindowManager && platform.canUseSystemTray) ...[
SettingsCategory( SettingsCategory(
title: Text(AppLocalizations.of(context).optionsCategoryNavigation), title: Text(AppLocalizations.of(context).optionsCategorySystemTray),
key: ValueKey(SettingsCageories.systemTray.name),
tiles: [ tiles: [
DropdownButtonSettingsTile( CheckBoxSettingsTile(
option: globalOptions.navigationMode, option: globalOptions.systemTrayEnabled,
),
CheckBoxSettingsTile(
option: globalOptions.systemTrayHideToTrayWhenMinimized,
), ),
], ],
), ),
if (platform.canUsePushNotifications) ...[ ],
SettingsCategory( SettingsCategory(
title: Text(AppLocalizations.of(context).optionsCategoryPushNotifications), title: Text(AppLocalizations.of(context).optionsCategoryAccounts),
tiles: [ key: ValueKey(SettingsCageories.accounts.name),
if (pushNotificationsEnabledEnabledSnapshot.hasData && tiles: [
!pushNotificationsEnabledEnabledSnapshot.requireData) ...[ if (accountsSnapshot.requireData.length > 1) ...[
TextSettingsTile( CheckBoxSettingsTile(
text: AppLocalizations.of(context).globalOptionsPushNotificationsEnabledDisabledNotice, option: globalOptions.rememberLastUsedAccount,
style: TextStyle( ),
fontWeight: FontWeight.w600, DropdownButtonSettingsTile(
fontStyle: FontStyle.italic, option: globalOptions.initialAccount,
color: Theme.of(context).colorScheme.error, ),
), ],
), for (final account in accountsSnapshot.requireData) ...[
], AccountSettingsTile(
CheckBoxSettingsTile( account: account,
option: globalOptions.pushNotificationsEnabled, onTap: () {
), AccountSettingsRoute(accountid: account.id).go(context);
DropdownButtonSettingsTile( },
option: globalOptions.pushNotificationsDistributor, ),
), ],
], CustomSettingsTile(
), title: ElevatedButton.icon(
], onPressed: () async => const LoginRoute().push(context),
if (platform.canUseWindowManager) ...[ icon: Icon(MdiIcons.accountPlus),
SettingsCategory( label: Text(AppLocalizations.of(context).globalOptionsAccountsAdd),
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,
),
],
),
], ],
if (accountsSnapshot.hasData) ...[ ),
SettingsCategory( SettingsCategory(
title: Text(AppLocalizations.of(context).optionsCategoryAccounts), title: Text(AppLocalizations.of(context).optionsCategoryOther),
tiles: [ key: ValueKey(SettingsCageories.other.name),
if (accountsSnapshot.requireData.length > 1) ...[ tiles: <SettingsTile>[
CheckBoxSettingsTile( CustomSettingsTile(
option: globalOptions.rememberLastUsedAccount, leading: Icon(
), MdiIcons.scriptText,
DropdownButtonSettingsTile( color: Theme.of(context).colorScheme.primary,
option: globalOptions.initialAccount, ),
), title: Text(AppLocalizations.of(context).licenses),
], onTap: () async {
for (final account in accountsSnapshot.requireData) ...[ final branding = Branding.of(context);
AccountSettingsTile( showLicensePage(
account: account, context: context,
onTap: () { applicationName: branding.name,
AccountSettingsRoute(accountid: account.id).go(context); applicationIcon: branding.logo,
}, applicationLegalese: branding.legalese,
), applicationVersion: Provider.of<PackageInfo>(context, listen: false).version,
], );
CustomSettingsTile( },
title: ElevatedButton.icon(
onPressed: () async => const LoginRoute().push(context),
icon: Icon(MdiIcons.accountPlus),
label: Text(AppLocalizations.of(context).globalOptionsAccountsAdd),
),
)
],
), ),
], CustomSettingsTile(
SettingsCategory( leading: Icon(
title: Text(AppLocalizations.of(context).optionsCategoryOther), MdiIcons.export,
tiles: <SettingsTile>[ color: Theme.of(context).colorScheme.primary,
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( title: Text(AppLocalizations.of(context).settingsExport),
leading: Icon( onTap: () async {
MdiIcons.export, final settingsExportHelper = _buildSettingsExportHelper(context);
color: Theme.of(context).colorScheme.primary,
), try {
title: Text(AppLocalizations.of(context).settingsExport), final fileName =
onTap: () async { 'nextcloud-neon-settings-${DateTime.now().millisecondsSinceEpoch ~/ 1000}.json.base64';
try { final data = base64.encode(
final fileName = utf8.encode(
'nextcloud-neon-settings-${DateTime.now().millisecondsSinceEpoch ~/ 1000}.json.base64'; json.encode(
final data = base64.encode( settingsExportHelper.toJsonExport(),
utf8.encode(
json.encode(
settingsExportHelper.toJsonExport(),
),
), ),
); ),
await saveFileWithPickDialog(fileName, Uint8List.fromList(utf8.encode(data))); );
} catch (e, s) { await saveFileWithPickDialog(fileName, Uint8List.fromList(utf8.encode(data)));
debugPrint(e.toString()); } catch (e, s) {
debugPrint(s.toString()); debugPrint(e.toString());
NeonException.showSnackbar(context, e); debugPrint(s.toString());
} NeonException.showSnackbar(context, e);
}, }
},
),
CustomSettingsTile(
leading: Icon(
MdiIcons.import,
color: Theme.of(context).colorScheme.primary,
), ),
CustomSettingsTile( title: Text(AppLocalizations.of(context).settingsImport),
leading: Icon( onTap: () async {
MdiIcons.import, final settingsExportHelper = _buildSettingsExportHelper(context);
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 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);
}
},
),
],
),
],
), ),
); );
}, },
), ),
); );
} }
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(),
); );
} }

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