From 89057b48c22f9ac0faa825b0c0dfffd880454db5 Mon Sep 17 00:00:00 2001 From: Henry-Hiles Date: Tue, 14 Jul 2026 21:20:37 -0400 Subject: [PATCH] working recording of settings Only tested on Linux, but should work elsewhere hopefully. --- lib/controllers/settings_controller.dart | 27 ++ lib/controllers/settings_file_controller.dart | 23 ++ lib/models/setting.dart | 14 +- lib/models/settings.dart | 16 + lib/pages/settings_page.dart | 273 +++++++++--------- pubspec.lock | 2 +- pubspec.yaml | 1 + 7 files changed, 213 insertions(+), 143 deletions(-) create mode 100644 lib/controllers/settings_controller.dart create mode 100644 lib/controllers/settings_file_controller.dart create mode 100644 lib/models/settings.dart diff --git a/lib/controllers/settings_controller.dart b/lib/controllers/settings_controller.dart new file mode 100644 index 0000000..02e5649 --- /dev/null +++ b/lib/controllers/settings_controller.dart @@ -0,0 +1,27 @@ +import "dart:convert"; +import "package:flutter_riverpod/flutter_riverpod.dart"; +import "package:nexus/controllers/settings_file_controller.dart"; +import "package:nexus/models/settings.dart"; + +class SettingsController extends AsyncNotifier { + @override + Future build() async { + final file = await ref.watch(SettingsFileController.provider.future); + + try { + return Settings.fromJson(json.decode(await file.readAsString())); + } catch (_) { + return Settings(); + } + } + + Future set(Settings settings) async { + state = AsyncData(settings); + final file = await ref.watch(SettingsFileController.provider.future); + await file.writeAsString(json.encode(settings.toJson())); + } + + static final provider = AsyncNotifierProvider( + SettingsController.new, + ); +} diff --git a/lib/controllers/settings_file_controller.dart b/lib/controllers/settings_file_controller.dart new file mode 100644 index 0000000..2c050bc --- /dev/null +++ b/lib/controllers/settings_file_controller.dart @@ -0,0 +1,23 @@ +import "dart:io"; +import "package:flutter_riverpod/flutter_riverpod.dart"; +import "package:path/path.dart"; +import "package:path_provider/path_provider.dart"; +import "package:xdg_directories/xdg_directories.dart"; + +class SettingsFileController extends AsyncNotifier { + @override + Future build() async { + final directory = await switch (Platform.isLinux) { + true => Directory( + join(configHome.absolute.path, "nexus"), + ).create(recursive: true), + false => getApplicationSupportDirectory(), + }; + + return File(join(directory.absolute.path, "config.json")); + } + + static final provider = AsyncNotifierProvider( + SettingsFileController.new, + ); +} diff --git a/lib/models/setting.dart b/lib/models/setting.dart index 7c643a5..9d355b1 100644 --- a/lib/models/setting.dart +++ b/lib/models/setting.dart @@ -1,22 +1,12 @@ import "package:flutter/material.dart"; -class Setting { - final String id; +class Setting { final String title; - final T initialValue; final String description; - final Widget Function( - String title, - String description, - ValueChanged onChanged, - T currentValue, - ) - builder; + final Widget Function(String title, String description) builder; Setting({ - required this.id, required this.title, - required this.initialValue, required this.description, required this.builder, }); diff --git a/lib/models/settings.dart b/lib/models/settings.dart new file mode 100644 index 0000000..a9c7605 --- /dev/null +++ b/lib/models/settings.dart @@ -0,0 +1,16 @@ +import "package:flutter/material.dart"; +import "package:freezed_annotation/freezed_annotation.dart"; +part "settings.freezed.dart"; +part "settings.g.dart"; + +@freezed +abstract class Settings with _$Settings { + const factory Settings({ + @Default(ThemeMode.light) ThemeMode theme, + @Default(true) bool useClientSideGenerations, + @Default(false) bool useSystemFont, + }) = _Settings; + + factory Settings.fromJson(Map json) => + _$SettingsFromJson(json); +} diff --git a/lib/pages/settings_page.dart b/lib/pages/settings_page.dart index 716dd04..1ffe66c 100644 --- a/lib/pages/settings_page.dart +++ b/lib/pages/settings_page.dart @@ -6,11 +6,15 @@ 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/helpers/extensions/better_when.dart"; import "package:nexus/models/setting.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/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}); @@ -19,60 +23,60 @@ class SettingsPage extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) => LayoutBuilder( builder: (_, constraints) => HookBuilder( builder: (context) { - final IMap> - settingsCategoryGroups = .new({ + IMap> buildCategories( + Settings settings, + ) => .new({ "General": .new([ .new( title: "Appearance", icon: Icons.brush, settings: .new([ - Setting( - id: "dark_mode", - title: "Dark Mode", - initialValue: ThemeMode.system, + Setting( + title: "Theme", description: "Toggle between Light Mode, Dark Mode, and System themes.", - builder: (title, description, onChanged, currentValue) => - DialogListTile( - icon: Icon(Icons.palette), - title: title, - subtitle: Text(description), - initialValue: currentValue, - options: ThemeMode.values, - getName: (option) => - toBeginningOfSentenceCase(option.name), - onChanged: onChanged, - ), + builder: (title, description) => DialogListTile( + icon: Icon(Icons.palette), + 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( - id: "use_csd", - initialValue: true, + Setting( title: "Use Client Side Decorations", description: "On desktop, toggle between client-side or server-side decorations", - builder: (title, description, onChanged, currentValue) => - SwitchListTile( - title: Text(title), - secondary: Icon(Icons.border_top), - subtitle: Text(description), - value: currentValue, - onChanged: onChanged, - ), + builder: (title, description) => SwitchListTile( + title: Text(title), + secondary: Icon(Icons.border_top), + subtitle: Text(description), + value: settings.useClientSideGenerations, + onChanged: (value) => ref + .watch(SettingsController.provider.notifier) + .set(settings.copyWith(useClientSideGenerations: value)) + .onError(showError), + ), ), - Setting( - id: "use_system_font", + Setting( title: "Use System Font", - initialValue: true, description: "Use the system's sans and emoji fonts, instead of Flutter's bundled fonts. Turn this off if you are having issues rendering emoji.", - builder: (title, description, onChanged, currentValue) => - SwitchListTile( - title: Text(title), - subtitle: Text(description), - secondary: Icon(Icons.abc), - value: currentValue, - onChanged: onChanged, - ), + builder: (title, description) => SwitchListTile( + title: Text(title), + subtitle: Text(description), + secondary: Icon(Icons.abc), + value: settings.useSystemFont, + onChanged: (value) => ref + .watch(SettingsController.provider.notifier) + .set(settings.copyWith(useSystemFont: value)) + .onError(showError), + ), ), ]), ), @@ -96,103 +100,112 @@ class SettingsPage extends ConsumerWidget { title: Text("Settings"), actionsPadding: .symmetric(horizontal: 12), ), - body: categoriesArePages - ? CustomScrollView( - slivers: [ - SliverToBoxAdapter( - child: Padding( - padding: EdgeInsets.all(12).copyWith(bottom: 8), - child: searchBar, - ), - ), - ...settingsCategoryGroups - .mapTo( - (categoryGroup, categories) => [ - SliverToBoxAdapter( - child: Padding( - padding: EdgeInsets.symmetric( - horizontal: 16, - ).copyWith(bottom: 4), - child: DividerText(categoryGroup), - ), + body: ref + .watch(SettingsController.provider) + .betterWhen( + data: (settings) => categoriesArePages + ? CustomScrollView( + slivers: [ + SliverToBoxAdapter( + child: Padding( + padding: EdgeInsets.all(12).copyWith(bottom: 8), + child: searchBar, ), - SliverM3ECardList( - padding: .symmetric(horizontal: 4, vertical: 8), - margin: .symmetric(horizontal: 12), - color: Theme.of( - context, - ).colorScheme.primaryContainer, - itemCount: categories.length, - onTap: (index) => Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => Scaffold( - appBar: AppBar( + ), + ...buildCategories(settings) + .mapTo( + (categoryGroup, categories) => [ + SliverToBoxAdapter( + child: Padding( + padding: EdgeInsets.symmetric( + horizontal: 16, + ).copyWith(bottom: 4), + child: DividerText(categoryGroup), + ), + ), + SliverM3ECardList( + padding: .symmetric( + horizontal: 4, + vertical: 8, + ), + margin: .symmetric(horizontal: 12), + color: Theme.of( + context, + ).colorScheme.primaryContainer, + itemCount: categories.length, + onTap: (index) => + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => Scaffold( + appBar: AppBar( + title: Text( + categories[index].title, + ), + ), + body: ListView(), + ), + ), + ), + itemBuilder: (context, index) => ListTile( + leading: Icon(categories[index].icon), title: Text(categories[index].title), ), - body: ListView(), ), - ), - ), - itemBuilder: (context, index) => ListTile( - leading: Icon(categories[index].icon), - title: Text(categories[index].title), - ), + ], + ) + .flattened, + ], + ) + : Row( + children: [ + NavigationRailM3E( + type: .alwaysExpand, + trailing: searchBar, + scrollable: true, + sections: buildCategories(settings) + .mapTo( + (categoryGroup, categories) => + NavigationRailM3ESection( + header: DividerText(categoryGroup), + destinations: categories + .map( + (category) => + NavigationRailM3EDestination( + icon: Icon(category.icon), + label: category.title, + ), + ) + .toList(), + ), + ) + .toList(), + selectedIndex: selected.value, + onDestinationSelected: (value) => + selected.value = value, + ), + VerticalDivider(), + Expanded( + child: SuperListView( + padding: .symmetric(vertical: 12), + children: buildCategories(settings) + .values + .flattenedToList[selected.value] + .settings + .map( + (setting) => Padding( + padding: .only(bottom: 4), + child: setting.builder( + setting.title, + setting.description, + ), + ), + ) + .toList(), ), - ], - ) - .flattened, - ], - ) - : Row( - children: [ - NavigationRailM3E( - type: .alwaysExpand, - trailing: searchBar, - scrollable: true, - sections: settingsCategoryGroups - .mapTo( - (categoryGroup, categories) => - NavigationRailM3ESection( - header: DividerText(categoryGroup), - destinations: categories - .map( - (category) => - NavigationRailM3EDestination( - icon: Icon(category.icon), - label: category.title, - ), - ) - .toList(), - ), - ) - .toList(), - selectedIndex: selected.value, - onDestinationSelected: (value) => selected.value = value, - ), - VerticalDivider(), - Expanded( - child: SuperListView( - padding: .symmetric(vertical: 12), - children: settingsCategoryGroups - .values - .flattenedToList[selected.value] - .settings - .map( - (setting) => Padding( - padding: .only(bottom: 4), - child: setting.builder( - setting.title, - setting.description, - (value) {}, - setting.initialValue, - ), - ), - ) - .toList(), + ), + ], ), - ), - ], - ), + ), ); return constraints.maxWidth < 650 diff --git a/pubspec.lock b/pubspec.lock index 4e30e17..153ab3e 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1646,7 +1646,7 @@ packages: source: hosted version: "0.5.1" xdg_directories: - dependency: transitive + dependency: "direct main" description: name: xdg_directories sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" diff --git a/pubspec.yaml b/pubspec.yaml index da8b1ec..aaae1e5 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -67,6 +67,7 @@ dependencies: url: https://github.com/Henry-Hiles/material_3_expressive path: packages/navigation_rail_m3e m3e_card_list: ^0.1.0 + xdg_directories: ^1.1.0 dev_dependencies: build_runner: 2.15.0