working recording of settings

Only tested on Linux, but should work elsewhere hopefully.
This commit is contained in:
Henry Hiles 2026-07-14 21:20:37 -04:00
commit 89057b48c2
Signed by: Henry-Hiles
SSH key fingerprint: SHA256:VKQUdS31Q90KvX7EkKMHMBpUspcmItAh86a+v7PGiIs
7 changed files with 213 additions and 143 deletions

View file

@ -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<Settings> {
@override
Future<Settings> build() async {
final file = await ref.watch(SettingsFileController.provider.future);
try {
return Settings.fromJson(json.decode(await file.readAsString()));
} catch (_) {
return Settings();
}
}
Future<void> 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, Settings>(
SettingsController.new,
);
}

View file

@ -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<File> {
@override
Future<File> 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, File>(
SettingsFileController.new,
);
}

View file

@ -1,22 +1,12 @@
import "package:flutter/material.dart";
class Setting<T> {
final String id;
class Setting {
final String title;
final T initialValue;
final String description;
final Widget Function(
String title,
String description,
ValueChanged<T> 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,
});

16
lib/models/settings.dart Normal file
View file

@ -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<String, Object?> json) =>
_$SettingsFromJson(json);
}

View file

@ -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<String, IList<SettingsCategory>>
settingsCategoryGroups = .new({
IMap<String, IList<SettingsCategory>> buildCategories(
Settings settings,
) => .new({
"General": .new([
.new(
title: "Appearance",
icon: Icons.brush,
settings: .new([
Setting<ThemeMode>(
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<ThemeMode>(
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<ThemeMode>(
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<bool>(
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<bool>(
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

View file

@ -1646,7 +1646,7 @@ packages:
source: hosted
version: "0.5.1"
xdg_directories:
dependency: transitive
dependency: "direct main"
description:
name: xdg_directories
sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15"

View file

@ -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