From 0d891bedbdc98fd1120ec0229b4b0fcf337f0945 Mon Sep 17 00:00:00 2001 From: Henry-Hiles Date: Fri, 17 Jul 2026 20:29:22 -0400 Subject: [PATCH] working search on mobile --- .../settings_sections_controller.dart | 99 +++++++++++ lib/pages/settings_category_page.dart | 77 ++++++++ lib/pages/settings_page.dart | 165 +++--------------- 3 files changed, 203 insertions(+), 138 deletions(-) create mode 100644 lib/controllers/settings_sections_controller.dart create mode 100644 lib/pages/settings_category_page.dart diff --git a/lib/controllers/settings_sections_controller.dart b/lib/controllers/settings_sections_controller.dart new file mode 100644 index 0000000..41a5afd --- /dev/null +++ b/lib/controllers/settings_sections_controller.dart @@ -0,0 +1,99 @@ +import "dart:io"; +import "package:fast_immutable_collections/fast_immutable_collections.dart"; +import "package:flutter/material.dart"; +import "package:flutter_riverpod/flutter_riverpod.dart"; +import "package:intl/intl.dart"; +import "package:nexus/controllers/settings_controller.dart"; +import "package:nexus/models/setting.dart"; +import "package:nexus/models/settings_category.dart"; +import "package:nexus/main.dart"; +import "package:nexus/widgets/settings/dialog_list_tile.dart"; + +class SettingsSectionsController + extends AsyncNotifier>> { + @override + Future>> build() async { + final settings = await ref.watch(SettingsController.provider.future); + + return .new({ + "General": .new([ + .new( + title: "Appearance", + icon: Icons.brush, + settings: .new([ + Setting( + title: "Theme", + description: + "Toggle between Light Mode, Dark Mode, and System themes.", + icon: Icons.contrast, + builder: (title, description, icon) => DialogListTile( + icon: Icon(icon), + title: title, + subtitle: Text(description), + initialValue: settings.theme, + options: ThemeMode.values, + getName: (option) => toBeginningOfSentenceCase(option.name), + onChanged: (value) => ref + .watch(SettingsController.provider.notifier) + .set(settings.copyWith(theme: value)) + .onError(showError), + ), + ), + Setting( + title: "Use Dynamic Theme", + icon: Icons.palette, + description: + "Toggle on or off Dynamic Theme. Only available on Android, Linux, Windows, or MacOS.", + builder: (title, description, icon) => SwitchListTile( + title: Text(title), + subtitle: Text(description), + secondary: Icon(icon), + value: settings.useDynamicTheming, + onChanged: + (Platform.isAndroid || + Platform.isLinux || + Platform.isMacOS || + Platform.isWindows) + ? (value) => ref + .watch(SettingsController.provider.notifier) + .set(settings.copyWith(useDynamicTheming: value)) + .onError(showError) + : null, + ), + ), + ]), + ), + .new( + title: "Behavior", + icon: Icons.psychology, + settings: .new([ + Setting( + title: "Linux Mobile Mode", + description: + "Enables some fixes for Linux mobile, e.g. disabling dragging appbar for moving window.", + icon: Icons.construction, + builder: (title, description, icon) => SwitchListTile( + title: Text(title), + subtitle: Text(description), + secondary: Icon(icon), + value: settings.linuxMobileMode, + onChanged: Platform.isLinux + ? (value) => ref + .watch(SettingsController.provider.notifier) + .set(settings.copyWith(linuxMobileMode: value)) + .onError(showError) + : null, + ), + ), + ]), + ), + ]), + }); + } + + static final provider = + AsyncNotifierProvider< + SettingsSectionsController, + IMap> + >(SettingsSectionsController.new); +} diff --git a/lib/pages/settings_category_page.dart b/lib/pages/settings_category_page.dart new file mode 100644 index 0000000..04f1578 --- /dev/null +++ b/lib/pages/settings_category_page.dart @@ -0,0 +1,77 @@ +import "dart:async"; + +import "package:collection/collection.dart"; +import "package:flutter/material.dart"; +import "package:flutter_hooks/flutter_hooks.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:nexus/controllers/settings_sections_controller.dart"; +import "package:nexus/helpers/extensions/better_when.dart"; +import "package:nexus/widgets/highlight_wrapper.dart"; +import "package:super_sliver_list/super_sliver_list.dart"; + +class SettingsCategoryPage extends HookConsumerWidget { + final int index; + final int? initialHighlight; + const SettingsCategoryPage(this.index, {this.initialHighlight, super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final highlight = useState(initialHighlight); + final listController = useRef(ListController()); + final scrollController = useScrollController(); + + useEffect(() { + if (initialHighlight == null) return null; + Timer? timer; + + void listener() => WidgetsBinding.instance.addPostFrameCallback((_) { + if (!listController.value.isAttached) return; + + listController.value.animateToItem( + index: initialHighlight!, + scrollController: scrollController, + alignment: 0.5, + duration: (_) => .new(milliseconds: 700), + curve: (_) => Curves.easeInOut, + ); + timer = Timer(.new(seconds: 1), () { + highlight.value = null; + }); + listController.value.removeListener(listener); + }); + + listController.value.addListener(listener); + return timer?.cancel; + }, []); + + return ref + .watch(SettingsSectionsController.provider) + .betterWhen( + data: (sections) => Scaffold( + appBar: AppBar( + title: Text(sections.values.flattenedToList[index].title), + ), + body: SuperListView( + controller: scrollController, + listController: listController.value, + padding: .symmetric(vertical: 12, horizontal: 8), + children: sections.values.flattenedToList[index].settings + .mapIndexed( + (index, setting) => Padding( + padding: .only(bottom: 4), + child: HighlightWrapper( + setting.builder( + setting.title, + setting.description, + setting.icon, + ), + isHighlighted: highlight.value == index, + ), + ), + ) + .toList(), + ), + ), + ); + } +} diff --git a/lib/pages/settings_page.dart b/lib/pages/settings_page.dart index 8fa57ee..66add90 100644 --- a/lib/pages/settings_page.dart +++ b/lib/pages/settings_page.dart @@ -1,23 +1,17 @@ -import "dart:io"; import "package:collection/collection.dart"; import "package:fast_immutable_collections/fast_immutable_collections.dart"; import "package:flutter/material.dart"; import "package:flutter_hooks/flutter_hooks.dart"; import "package:hooks_riverpod/hooks_riverpod.dart"; -import "package:intl/intl.dart"; import "package:m3e_card_list/m3e_card_list.dart"; import "package:navigation_rail_m3e/navigation_rail_m3e.dart"; -import "package:nexus/controllers/settings_controller.dart"; +import "package:nexus/controllers/settings_sections_controller.dart"; import "package:nexus/helpers/extensions/better_when.dart"; import "package:nexus/helpers/extensions/show_about_dialog.dart"; -import "package:nexus/models/setting.dart"; -import "package:nexus/models/settings.dart"; -import "package:nexus/models/settings_category.dart"; +import "package:nexus/pages/settings_category_page.dart"; import "package:nexus/widgets/divider_text.dart"; import "package:nexus/widgets/highlight_wrapper.dart"; -import "package:nexus/widgets/settings/dialog_list_tile.dart"; import "package:super_sliver_list/super_sliver_list.dart"; -import "package:nexus/main.dart"; class SettingsPage extends ConsumerWidget { const SettingsPage({super.key}); @@ -26,85 +20,6 @@ class SettingsPage extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) => LayoutBuilder( builder: (_, constraints) => HookBuilder( builder: (context) { - IMap> buildCategories( - Settings settings, - ) => .new({ - "General": .new([ - .new( - title: "Appearance", - icon: Icons.brush, - settings: .new([ - Setting( - title: "Theme", - description: - "Toggle between Light Mode, Dark Mode, and System themes.", - icon: Icons.contrast, - builder: (title, description, icon) => - DialogListTile( - icon: Icon(icon), - title: title, - subtitle: Text(description), - initialValue: settings.theme, - options: ThemeMode.values, - getName: (option) => - toBeginningOfSentenceCase(option.name), - onChanged: (value) => ref - .watch(SettingsController.provider.notifier) - .set(settings.copyWith(theme: value)) - .onError(showError), - ), - ), - Setting( - title: "Use Dynamic Theme", - icon: Icons.palette, - description: - "Toggle on or off Dynamic Theme. Only available on Android, Linux, Windows, or MacOS.", - builder: (title, description, icon) => SwitchListTile( - title: Text(title), - subtitle: Text(description), - secondary: Icon(icon), - value: settings.useDynamicTheming, - onChanged: - (Platform.isAndroid || - Platform.isLinux || - Platform.isMacOS || - Platform.isWindows) - ? (value) => ref - .watch(SettingsController.provider.notifier) - .set(settings.copyWith(useDynamicTheming: value)) - .onError(showError) - : null, - ), - ), - ]), - ), - .new( - title: "Behavior", - icon: Icons.psychology, - settings: .new([ - Setting( - title: "Linux Mobile Mode", - description: - "Enables some fixes for Linux mobile, e.g. disabling dragging appbar for moving window.", - icon: Icons.construction, - builder: (title, description, icon) => SwitchListTile( - title: Text(title), - subtitle: Text(description), - secondary: Icon(icon), - value: settings.linuxMobileMode, - onChanged: Platform.isLinux - ? (value) => ref - .watch(SettingsController.provider.notifier) - .set(settings.copyWith(linuxMobileMode: value)) - .onError(showError) - : null, - ), - ), - ]), - ), - ]), - }); - final categoriesArePages = constraints.maxWidth < 550; final selected = useState(0); @@ -115,8 +30,10 @@ class SettingsPage extends ConsumerWidget { final searchBar = SearchAnchor.bar( barHintText: "Search...", - suggestionsBuilder: (context, controller) { - final categories = buildCategories(Settings()); + suggestionsBuilder: (suggestionsContext, controller) async { + final categories = await ref.watch( + SettingsSectionsController.provider.future, + ); final query = controller.text.toLowerCase(); final matches = categories.values @@ -146,11 +63,19 @@ class SettingsPage extends ConsumerWidget { return matches.map( (match) => ListTile( onTap: () async { - if (context.mounted) Navigator.of(context).pop(); + if (context.mounted) Navigator.of(suggestionsContext).pop(); + controller.text = ""; + if (categoriesArePages) { - // Navigator.of(context).push(route); + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => SettingsCategoryPage( + match.$1.$1, + initialHighlight: match.$1.$2, + ), + ), + ); } else { - controller.text = ""; selected.value = match.$1.$1; listController.value.animateToItem( index: match.$1.$2, @@ -187,9 +112,9 @@ class SettingsPage extends ConsumerWidget { ], ), body: ref - .watch(SettingsController.provider) + .watch(SettingsSectionsController.provider) .betterWhen( - data: (settings) => categoriesArePages + data: (sections) => categoriesArePages ? CustomScrollView( slivers: [ SliverToBoxAdapter( @@ -198,7 +123,7 @@ class SettingsPage extends ConsumerWidget { child: searchBar, ), ), - ...buildCategories(settings) + ...sections .mapTo( (categoryGroup, categories) => [ SliverToBoxAdapter( @@ -219,49 +144,13 @@ class SettingsPage extends ConsumerWidget { context, ).colorScheme.primaryContainer, itemCount: categories.length, - onTap: (index) => Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => Scaffold( - appBar: AppBar( - title: Text( - categories[index].title, - ), - ), - body: Consumer( - builder: (context, ref, _) => ref - .watch( - SettingsController.provider, - ) - .betterWhen( - data: (settings) => SuperListView( - padding: .symmetric( - vertical: 12, - horizontal: 8, - ), - children: buildCategories(settings) - .values - .flattenedToList[index] - .settings - .map( - (setting) => Padding( - padding: .only( - bottom: 4, - ), - child: setting.builder( - setting.title, - setting - .description, - setting.icon, - ), - ), - ) - .toList(), - ), - ), + onTap: (index) => + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => + SettingsCategoryPage(index), ), ), - ), - ), itemBuilder: (context, index) => ListTile( leading: Icon(categories[index].icon), title: Text(categories[index].title), @@ -278,7 +167,7 @@ class SettingsPage extends ConsumerWidget { type: .alwaysExpand, trailing: searchBar, scrollable: true, - sections: buildCategories(settings) + sections: sections .mapTo( (categoryGroup, categories) => NavigationRailM3ESection( @@ -305,7 +194,7 @@ class SettingsPage extends ConsumerWidget { listController: listController.value, controller: scrollController, padding: .symmetric(vertical: 12), - children: buildCategories(settings) + children: sections .values .flattenedToList[selected.value] .settings