working search on mobile
This commit is contained in:
parent
c89bced551
commit
0d891bedbd
3 changed files with 203 additions and 138 deletions
99
lib/controllers/settings_sections_controller.dart
Normal file
99
lib/controllers/settings_sections_controller.dart
Normal 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);
|
||||
}
|
||||
77
lib/pages/settings_category_page.dart
Normal file
77
lib/pages/settings_category_page.dart
Normal 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(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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 (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
|
||||
|
|
|
|||
Loading…
Reference in a new issue