Dark/bright: Auto (Android's dark theme), Dark or Bright, kept across restarts
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:
+4
-1
@@ -16,7 +16,7 @@ pirate-station-aide's (main views, then the rarely used ones grouped as Tools).
|
||||
| `pages/` | One file per view: `dash`, `signals` (values and raw traffic), `checklist` (Captures), `camera`, `link_test`, `settings` |
|
||||
| `actions/` | Things done from anywhere, as dialogs and sheets: `marks` (free-form mark), `orin_power` (power off / reboot), `server_url` (gateway address) |
|
||||
| `link/` | Talking to the Orin: `boundary` (the protocol client), `net` (binding to the USB link), `reading` (one signal's state as a display needs it) |
|
||||
| `theme/` | `palette.dart`: the palette roles and `hudFont` |
|
||||
| `theme/` | `palette.dart`: the palette roles, `hudFont`, and `Themes` (which variant shows: auto or held, kept across restarts) |
|
||||
| `widgets/` | Reusable display widgets, one per file, each fed values, never the boundary (`link_status` takes it whole: it shows the link) |
|
||||
|
||||
## Widgets
|
||||
@@ -40,6 +40,9 @@ pirate-station-aide's (main views, then the rarely used ones grouped as Tools).
|
||||
and the state gradient `good` → `caution` → `alarm`; plus the derived `surface` (raised bars) and
|
||||
`onFill(colour)` (text on a fill). No widget names a colour. Themes are JSON in `assets/themes/`, `rei` by
|
||||
default; smart-serow's four-key files still load (their `highlight` is `alarm`).
|
||||
- **Dark or bright.** `Themes.mode` is `auto` (Android's dark theme, schedule included), `dark` or
|
||||
`bright`, chosen in Settings and kept across restarts. The Dash's quick button holds the other variant;
|
||||
a long press there is auto again.
|
||||
- **Cosmetic fonts are opt-in.** A widget that wants the HUD face names `fontFamily: hudFont` in its own
|
||||
`TextStyle`; everything else keeps the basic font, which covers CJK where DIN 1451 does not.
|
||||
- **Missing is a state** (app guideline 2): a widget given no value draws "—" or subdued, never an error.
|
||||
|
||||
+13
-7
@@ -164,7 +164,7 @@ class DashPage extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
/// The dash's own thin top line: the menu, link state, and the dark/bright switch.
|
||||
/// The dash's own thin top line: the menu, link state, and the dark/bright quick switch.
|
||||
class _DashBar extends StatelessWidget {
|
||||
const _DashBar({required this.boundary, required this.themes});
|
||||
final Boundary boundary;
|
||||
@@ -185,12 +185,18 @@ class _DashBar extends StatelessWidget {
|
||||
MenuButton(color: p.subdued),
|
||||
Text(text, style: TextStyle(color: color, letterSpacing: 2)),
|
||||
const Spacer(),
|
||||
IconButton(
|
||||
tooltip: themes.isDark ? 'Bright' : 'Dark',
|
||||
onPressed: themes.toggle,
|
||||
icon: Icon(
|
||||
themes.isDark ? Icons.light_mode : Icons.dark_mode,
|
||||
color: p.subdued,
|
||||
// Tap: hold the other variant. Long press: back to auto (Settings has both).
|
||||
GestureDetector(
|
||||
onLongPress: () => themes.mode = VariantMode.auto,
|
||||
child: IconButton(
|
||||
tooltip: themes.isDark ? 'Bright' : 'Dark',
|
||||
onPressed: themes.toggle,
|
||||
icon: Icon(
|
||||
themes.mode == VariantMode.auto
|
||||
? Icons.brightness_auto
|
||||
: (themes.isDark ? Icons.light_mode : Icons.dark_mode),
|
||||
color: p.subdued,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
+31
-7
@@ -52,14 +52,38 @@ class SettingsPage extends StatelessWidget {
|
||||
onTap: () => editUrl(context, boundary, usb),
|
||||
),
|
||||
section('Display'),
|
||||
SwitchListTile(
|
||||
secondary: Icon(
|
||||
themes.isDark ? Icons.dark_mode : Icons.light_mode,
|
||||
ListTile(
|
||||
leading: Icon(themes.isDark ? Icons.dark_mode : Icons.light_mode),
|
||||
title: const Text('Dark or bright'),
|
||||
subtitle: const Text(
|
||||
"Auto follows Android's dark theme and its schedule. "
|
||||
"The Dash's button holds one; a long press there is auto again.",
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(72, 0, 16, 8),
|
||||
child: SegmentedButton<VariantMode>(
|
||||
segments: const [
|
||||
ButtonSegment(
|
||||
value: VariantMode.auto,
|
||||
icon: Icon(Icons.brightness_auto),
|
||||
label: Text('Auto'),
|
||||
),
|
||||
ButtonSegment(
|
||||
value: VariantMode.dark,
|
||||
icon: Icon(Icons.dark_mode),
|
||||
label: Text('Dark'),
|
||||
),
|
||||
ButtonSegment(
|
||||
value: VariantMode.bright,
|
||||
icon: Icon(Icons.light_mode),
|
||||
label: Text('Bright'),
|
||||
),
|
||||
],
|
||||
selected: {themes.mode},
|
||||
showSelectedIcon: false,
|
||||
onSelectionChanged: (s) => themes.mode = s.single,
|
||||
),
|
||||
title: const Text('Dark variant'),
|
||||
subtitle: const Text('Bright for daylight, dark for night'),
|
||||
value: themes.isDark,
|
||||
onChanged: (v) => themes.isDark = v,
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.palette),
|
||||
|
||||
+60
-12
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user