feat(ui): theme-mode toggle (system / light / dark) with persistence

Three-way switch in the sidebar footer next to the settings
gear. Cycles system → light → dark → system. Choice persists
via shared_preferences across app launches; restored before the
first frame so there's no theme flicker on startup.

The icon changes per state (auto / sun / moon) and the tooltip
spells it out so the cycling behaviour is discoverable.

- StudioApp is now stateful; exposes StudioApp.of(context) to
  let descendants flip the theme without prop-drilling.
- ThemeModeValue enum lives in data/hub.dart so the persistence
  layer stays free of flutter/material imports.

Bumps fai_studio 0.6.0 -> 0.7.0.

Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
This commit is contained in:
flemming-it 2026-05-05 22:19:13 +02:00
parent b3fdd69139
commit 671194c105
3 changed files with 119 additions and 29 deletions

View file

@ -52,6 +52,22 @@ class HubService {
await prefs.setBool(_kSecureKey, endpoint.secure);
}
static const _kThemeKey = 'theme.mode';
/// Read the persisted theme mode (system / light / dark) for
/// initial app startup. Defaults to system.
Future<ThemeModeValue> loadThemeMode() async {
final prefs = await SharedPreferences.getInstance();
final raw = prefs.getString(_kThemeKey);
return ThemeModeValue.fromWire(raw) ?? ThemeModeValue.system;
}
/// Persist the theme mode the user chose.
Future<void> saveThemeMode(ThemeModeValue mode) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setString(_kThemeKey, mode.wire);
}
Future<bool> healthy() => _client.healthy();
Future<List<ModuleSummary>> listModules() async {
@ -216,6 +232,24 @@ class ServiceEntry {
});
}
/// Three-way switch persisted across launches.
enum ThemeModeValue {
system('system'),
light('light'),
dark('dark');
final String wire;
const ThemeModeValue(this.wire);
static ThemeModeValue? fromWire(String? s) {
if (s == null) return null;
for (final v in values) {
if (v.wire == s) return v;
}
return null;
}
}
/// UI-side type, decoupled from the proto wire type so pages
/// don't import protobuf packages.
class ModuleSummary {