Dark/bright: Auto (Android's dark theme), Dark or Bright, kept across restarts
/ crosscheck (push) Successful in 1s
CI / Analyze & Test (push) Successful in 19s
CI / Debug APK (push) Successful in 1m5s

Settings has a segmented Auto / Dark / Bright. Auto follows platformBrightness, so
Android's schedule too. The Dash's quick button holds the other variant; a long press
is auto again. The mode is kept with shared_preferences.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-15 22:14:29 +09:00
parent 0d0940e6b7
commit 78a6564f0a
8 changed files with 285 additions and 28 deletions
+60 -12
View File
@@ -20,6 +20,7 @@ import 'dart:math' show max, min;
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:shared_preferences/shared_preferences.dart';
/// The HUD face (smart-serow's DIN 1451). Opt-in: a widget names it in its own
/// TextStyle; everything else keeps the basic font, which covers CJK where DIN does not.
@@ -116,29 +117,70 @@ class Palette {
);
}
/// A theme: both variants, and which one is showing. Listen to it to follow a switch.
class Themes extends ChangeNotifier {
Themes(this.name, this.dark, this.bright);
/// Which variant shows: Android's own dark theme (auto), or one held fixed.
enum VariantMode { auto, dark, bright }
/// A theme: both variants, and which one is showing. Listen to it to follow a switch,
/// and, in auto, Android's dark theme turning on or off (by hand or on its schedule).
/// The mode is kept across restarts.
class Themes extends ChangeNotifier with WidgetsBindingObserver {
Themes(this.name, this.dark, this.bright, [this._prefs]) {
_mode =
VariantMode.values.asNameMap()[_prefs?.getString(_key)] ??
VariantMode.auto;
WidgetsBinding.instance.addObserver(this);
}
static const _key = 'variant_mode';
final String name;
final Palette dark;
final Palette bright;
bool _isDark = true;
final SharedPreferences? _prefs;
late VariantMode _mode;
bool get isDark => _isDark;
Palette get current => _isDark ? dark : bright;
VariantMode get mode => _mode;
set isDark(bool v) {
if (v == _isDark) return;
_isDark = v;
set mode(VariantMode v) {
if (v == _mode) return;
_mode = v;
_prefs?.setString(_key, v.name);
notifyListeners();
}
void toggle() => isDark = !_isDark;
bool get isDark => switch (_mode) {
VariantMode.auto =>
WidgetsBinding.instance.platformDispatcher.platformBrightness ==
Brightness.dark,
VariantMode.dark => true,
VariantMode.bright => false,
};
Palette get current => isDark ? dark : bright;
/// The quick switch: hold the other variant fixed.
void toggle() => mode = isDark ? VariantMode.bright : VariantMode.dark;
@override
void didChangePlatformBrightness() {
if (_mode == VariantMode.auto) notifyListeners();
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
/// [themeName], falling back to `default`, then to the hard-coded palette
/// (smart-serow's chain).
static Future<Themes> load([String name = themeName]) async {
SharedPreferences? prefs;
try {
prefs = await SharedPreferences.getInstance();
} catch (_) {
// No storage: the mode starts at auto each time.
}
for (final n in {name, 'default'}) {
try {
final j = jsonDecode(
@@ -148,19 +190,25 @@ class Themes extends ChangeNotifier {
n,
Palette._fromJson(j['dark'] as Map<String, dynamic>),
Palette._fromJson(j['bright'] as Map<String, dynamic>),
prefs,
);
} catch (_) {
// Missing or malformed: try the next one.
}
}
return Themes('fallback', Palette.fallbackDark, Palette.fallbackBright);
return Themes(
'fallback',
Palette.fallbackDark,
Palette.fallbackBright,
prefs,
);
}
/// Material's theme for the rest of the app, from the same roles: buttons and
/// selection in accent, errors in alarm, raised surfaces in surface.
ThemeData material() {
final p = current;
final brightness = _isDark ? Brightness.dark : Brightness.light;
final brightness = isDark ? Brightness.dark : Brightness.light;
return ThemeData(
useMaterial3: true,
brightness: brightness,