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:collection/collection.dart";
|
||||||
import "package:fast_immutable_collections/fast_immutable_collections.dart";
|
import "package:fast_immutable_collections/fast_immutable_collections.dart";
|
||||||
import "package:flutter/material.dart";
|
import "package:flutter/material.dart";
|
||||||
import "package:flutter_hooks/flutter_hooks.dart";
|
import "package:flutter_hooks/flutter_hooks.dart";
|
||||||
import "package:hooks_riverpod/hooks_riverpod.dart";
|
import "package:hooks_riverpod/hooks_riverpod.dart";
|
||||||
import "package:intl/intl.dart";
|
|
||||||
import "package:m3e_card_list/m3e_card_list.dart";
|
import "package:m3e_card_list/m3e_card_list.dart";
|
||||||
import "package:navigation_rail_m3e/navigation_rail_m3e.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/better_when.dart";
|
||||||
import "package:nexus/helpers/extensions/show_about_dialog.dart";
|
import "package:nexus/helpers/extensions/show_about_dialog.dart";
|
||||||
import "package:nexus/models/setting.dart";
|
import "package:nexus/pages/settings_category_page.dart";
|
||||||
import "package:nexus/models/settings.dart";
|
|
||||||
import "package:nexus/models/settings_category.dart";
|
|
||||||
import "package:nexus/widgets/divider_text.dart";
|
import "package:nexus/widgets/divider_text.dart";
|
||||||
import "package:nexus/widgets/highlight_wrapper.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:super_sliver_list/super_sliver_list.dart";
|
||||||
import "package:nexus/main.dart";
|
|
||||||
|
|
||||||
class SettingsPage extends ConsumerWidget {
|
class SettingsPage extends ConsumerWidget {
|
||||||
const SettingsPage({super.key});
|
const SettingsPage({super.key});
|
||||||
|
|
@ -26,85 +20,6 @@ class SettingsPage extends ConsumerWidget {
|
||||||
Widget build(BuildContext context, WidgetRef ref) => LayoutBuilder(
|
Widget build(BuildContext context, WidgetRef ref) => LayoutBuilder(
|
||||||
builder: (_, constraints) => HookBuilder(
|
builder: (_, constraints) => HookBuilder(
|
||||||
builder: (context) {
|
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 categoriesArePages = constraints.maxWidth < 550;
|
||||||
|
|
||||||
final selected = useState(0);
|
final selected = useState(0);
|
||||||
|
|
@ -115,8 +30,10 @@ class SettingsPage extends ConsumerWidget {
|
||||||
|
|
||||||
final searchBar = SearchAnchor.bar(
|
final searchBar = SearchAnchor.bar(
|
||||||
barHintText: "Search...",
|
barHintText: "Search...",
|
||||||
suggestionsBuilder: (context, controller) {
|
suggestionsBuilder: (suggestionsContext, controller) async {
|
||||||
final categories = buildCategories(Settings());
|
final categories = await ref.watch(
|
||||||
|
SettingsSectionsController.provider.future,
|
||||||
|
);
|
||||||
final query = controller.text.toLowerCase();
|
final query = controller.text.toLowerCase();
|
||||||
|
|
||||||
final matches = categories.values
|
final matches = categories.values
|
||||||
|
|
@ -146,11 +63,19 @@ class SettingsPage extends ConsumerWidget {
|
||||||
return matches.map(
|
return matches.map(
|
||||||
(match) => ListTile(
|
(match) => ListTile(
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
if (context.mounted) Navigator.of(context).pop();
|
if (context.mounted) Navigator.of(suggestionsContext).pop();
|
||||||
if (categoriesArePages) {
|
|
||||||
// Navigator.of(context).push(route);
|
|
||||||
} else {
|
|
||||||
controller.text = "";
|
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;
|
selected.value = match.$1.$1;
|
||||||
listController.value.animateToItem(
|
listController.value.animateToItem(
|
||||||
index: match.$1.$2,
|
index: match.$1.$2,
|
||||||
|
|
@ -187,9 +112,9 @@ class SettingsPage extends ConsumerWidget {
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
body: ref
|
body: ref
|
||||||
.watch(SettingsController.provider)
|
.watch(SettingsSectionsController.provider)
|
||||||
.betterWhen(
|
.betterWhen(
|
||||||
data: (settings) => categoriesArePages
|
data: (sections) => categoriesArePages
|
||||||
? CustomScrollView(
|
? CustomScrollView(
|
||||||
slivers: [
|
slivers: [
|
||||||
SliverToBoxAdapter(
|
SliverToBoxAdapter(
|
||||||
|
|
@ -198,7 +123,7 @@ class SettingsPage extends ConsumerWidget {
|
||||||
child: searchBar,
|
child: searchBar,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
...buildCategories(settings)
|
...sections
|
||||||
.mapTo(
|
.mapTo(
|
||||||
(categoryGroup, categories) => [
|
(categoryGroup, categories) => [
|
||||||
SliverToBoxAdapter(
|
SliverToBoxAdapter(
|
||||||
|
|
@ -219,47 +144,11 @@ class SettingsPage extends ConsumerWidget {
|
||||||
context,
|
context,
|
||||||
).colorScheme.primaryContainer,
|
).colorScheme.primaryContainer,
|
||||||
itemCount: categories.length,
|
itemCount: categories.length,
|
||||||
onTap: (index) => Navigator.of(context).push(
|
onTap: (index) =>
|
||||||
|
Navigator.of(context).push(
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (context) => Scaffold(
|
builder: (context) =>
|
||||||
appBar: AppBar(
|
SettingsCategoryPage(index),
|
||||||
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(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
itemBuilder: (context, index) => ListTile(
|
itemBuilder: (context, index) => ListTile(
|
||||||
|
|
@ -278,7 +167,7 @@ class SettingsPage extends ConsumerWidget {
|
||||||
type: .alwaysExpand,
|
type: .alwaysExpand,
|
||||||
trailing: searchBar,
|
trailing: searchBar,
|
||||||
scrollable: true,
|
scrollable: true,
|
||||||
sections: buildCategories(settings)
|
sections: sections
|
||||||
.mapTo(
|
.mapTo(
|
||||||
(categoryGroup, categories) =>
|
(categoryGroup, categories) =>
|
||||||
NavigationRailM3ESection(
|
NavigationRailM3ESection(
|
||||||
|
|
@ -305,7 +194,7 @@ class SettingsPage extends ConsumerWidget {
|
||||||
listController: listController.value,
|
listController: listController.value,
|
||||||
controller: scrollController,
|
controller: scrollController,
|
||||||
padding: .symmetric(vertical: 12),
|
padding: .symmetric(vertical: 12),
|
||||||
children: buildCategories(settings)
|
children: sections
|
||||||
.values
|
.values
|
||||||
.flattenedToList[selected.value]
|
.flattenedToList[selected.value]
|
||||||
.settings
|
.settings
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue