working search on mobile

This commit is contained in:
Henry Hiles 2026-07-17 20:29:22 -04:00
commit 0d891bedbd
Signed by: Henry-Hiles
SSH key fingerprint: SHA256:VKQUdS31Q90KvX7EkKMHMBpUspcmItAh86a+v7PGiIs
3 changed files with 203 additions and 138 deletions

View file

@ -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<IMap<String, IList<SettingsCategory>>> {
@override
Future<IMap<String, IList<SettingsCategory>>> 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<ThemeMode>(
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<String, IList<SettingsCategory>>
>(SettingsSectionsController.new);
}

View file

@ -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<int?>(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(),
),
),
);
}
}

View file

@ -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<String, IList<SettingsCategory>> 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<ThemeMode>(
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 (categoriesArePages) {
// Navigator.of(context).push(route);
} else {
if (context.mounted) Navigator.of(suggestionsContext).pop();
controller.text = "";
if (categoriesArePages) {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => SettingsCategoryPage(
match.$1.$1,
initialHighlight: match.$1.$2,
),
),
);
} else {
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,47 +144,11 @@ class SettingsPage extends ConsumerWidget {
context,
).colorScheme.primaryContainer,
itemCount: categories.length,
onTap: (index) => Navigator.of(context).push(
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(),
),
),
),
),
builder: (context) =>
SettingsCategoryPage(index),
),
),
itemBuilder: (context, index) => ListTile(
@ -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