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:
parent
b3fdd69139
commit
671194c105
3 changed files with 119 additions and 29 deletions
|
|
@ -18,15 +18,53 @@ import 'widgets/widgets.dart';
|
|||
|
||||
Future<void> main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
// Restore the persisted endpoint before the first frame so the
|
||||
// sidebar pill doesn't briefly say "127.0.0.1:50051" before
|
||||
// switching to the operator's actual hub.
|
||||
// Restore the persisted endpoint and theme mode before the
|
||||
// first frame so neither flicker on startup.
|
||||
await HubService.instance.loadPersistedEndpoint();
|
||||
runApp(const StudioApp());
|
||||
final themeMode = await HubService.instance.loadThemeMode();
|
||||
runApp(StudioApp(initialThemeMode: themeMode));
|
||||
}
|
||||
|
||||
class StudioApp extends StatelessWidget {
|
||||
const StudioApp({super.key});
|
||||
class StudioApp extends StatefulWidget {
|
||||
final ThemeModeValue initialThemeMode;
|
||||
|
||||
const StudioApp({super.key, required this.initialThemeMode});
|
||||
|
||||
/// Lookup helper so descendants can flip the theme without
|
||||
/// passing callbacks down through every level.
|
||||
static StudioAppState? of(BuildContext context) =>
|
||||
context.findAncestorStateOfType<StudioAppState>();
|
||||
|
||||
@override
|
||||
State<StudioApp> createState() => StudioAppState();
|
||||
}
|
||||
|
||||
class StudioAppState extends State<StudioApp> {
|
||||
late ThemeModeValue _mode;
|
||||
|
||||
ThemeModeValue get mode => _mode;
|
||||
|
||||
Future<void> setMode(ThemeModeValue m) async {
|
||||
setState(() => _mode = m);
|
||||
await HubService.instance.saveThemeMode(m);
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_mode = widget.initialThemeMode;
|
||||
}
|
||||
|
||||
ThemeMode _flutterMode() {
|
||||
switch (_mode) {
|
||||
case ThemeModeValue.system:
|
||||
return ThemeMode.system;
|
||||
case ThemeModeValue.light:
|
||||
return ThemeMode.light;
|
||||
case ThemeModeValue.dark:
|
||||
return ThemeMode.dark;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
|
@ -35,7 +73,7 @@ class StudioApp extends StatelessWidget {
|
|||
debugShowCheckedModeBanner: false,
|
||||
theme: FaiTheme.light(),
|
||||
darkTheme: FaiTheme.dark(),
|
||||
themeMode: ThemeMode.system,
|
||||
themeMode: _flutterMode(),
|
||||
home: const StudioShell(),
|
||||
);
|
||||
}
|
||||
|
|
@ -204,20 +242,13 @@ class _Sidebar extends StatelessWidget {
|
|||
],
|
||||
),
|
||||
),
|
||||
// Footer: settings + version.
|
||||
// Footer: theme toggle + settings.
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: FaiSpace.md),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
'platform target',
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
fontSize: 10,
|
||||
),
|
||||
),
|
||||
),
|
||||
_ThemeToggle(),
|
||||
const Spacer(),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.settings_outlined, size: 16),
|
||||
tooltip: 'Hub endpoint…',
|
||||
|
|
@ -387,3 +418,34 @@ class _NavPage {
|
|||
required this.page,
|
||||
});
|
||||
}
|
||||
|
||||
class _ThemeToggle extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final app = StudioApp.of(context);
|
||||
final mode = app?.mode ?? ThemeModeValue.system;
|
||||
final icon = switch (mode) {
|
||||
ThemeModeValue.system => Icons.brightness_auto_outlined,
|
||||
ThemeModeValue.light => Icons.light_mode_outlined,
|
||||
ThemeModeValue.dark => Icons.dark_mode_outlined,
|
||||
};
|
||||
final tooltip = switch (mode) {
|
||||
ThemeModeValue.system => 'theme: follow system',
|
||||
ThemeModeValue.light => 'theme: light',
|
||||
ThemeModeValue.dark => 'theme: dark',
|
||||
};
|
||||
return IconButton(
|
||||
icon: Icon(icon, size: 16),
|
||||
tooltip: tooltip,
|
||||
visualDensity: VisualDensity.compact,
|
||||
onPressed: () {
|
||||
final next = switch (mode) {
|
||||
ThemeModeValue.system => ThemeModeValue.light,
|
||||
ThemeModeValue.light => ThemeModeValue.dark,
|
||||
ThemeModeValue.dark => ThemeModeValue.system,
|
||||
};
|
||||
app?.setMode(next);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue