feat(ground-suite): add theme selector

This commit is contained in:
Onur Özkan committed 2026-08-10 16:04:07 +03:00
1 parent eb6a343983
commit c67d553076
6 files changed
+66 -3

No files matched your search

+2
View File
@@ -16,6 +16,8 @@ or write capability.
Ground Suite provides light and dark themes, selecting the operating system's
current preference by default. The dark theme uses near-black, muted-silver,
and restrained green accents with locally blurred translucent panels.
The observation bar also offers a current-session System, Light, or Dark
appearance selector.
Ground Suite uses a receive-only source/decoder/classifier boundary. The
replay implementation feeds typed observations today. A future live adapter
+3
View File
@@ -52,6 +52,9 @@ by default. Its dark palette uses near-black, muted-silver, and restrained
green accents; translucent, locally blurred panels keep operational content
legible without turning diagnostics into decoration.
Use the appearance button in the observation bar to select **System default**,
**Light**, or **Dark**. The choice applies for the current session only.
## Deliberately deferred work
- TODO(gcs-live-mavlink): generated MAVLink Common plus custom-dialect
+5 -3
View File
@@ -1,19 +1,21 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'app_shell.dart';
import 'theme.dart';
import 'theme_mode_controller.dart';
class GroundSuiteApp extends StatelessWidget {
class GroundSuiteApp extends ConsumerWidget {
const GroundSuiteApp({super.key});
@override
Widget build(BuildContext context) {
Widget build(BuildContext context, WidgetRef ref) {
return MaterialApp(
title: 'Ground Suite',
debugShowCheckedModeBanner: false,
theme: groundSuiteLightTheme(),
darkTheme: groundSuiteDarkTheme(),
themeMode: ThemeMode.system,
themeMode: ref.watch(themeModeProvider),
home: const AppShell(),
);
}
+29
View File
@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'theme_mode_controller.dart';
import '../features/diagnostics/presentation/diagnostics_page.dart';
import '../features/observability/presentation/observability_controller.dart';
import '../features/overview/presentation/overview_page.dart';
@@ -220,6 +221,7 @@ class _ConnectionBar extends StatelessWidget {
_StatusChip(label: state.status.label),
Text(peer),
Text(frameTime),
const _ThemeMenu(),
],
),
),
@@ -229,6 +231,27 @@ class _ConnectionBar extends StatelessWidget {
}
}
class _ThemeMenu extends ConsumerWidget {
const _ThemeMenu();
@override
Widget build(BuildContext context, WidgetRef ref) {
final mode = ref.watch(themeModeProvider);
return PopupMenuButton<ThemeMode>(
tooltip: 'Choose theme',
initialValue: mode,
onSelected: ref.read(themeModeProvider.notifier).select,
icon: Icon(_iconFor(mode)),
itemBuilder: (context) => const [
PopupMenuItem(value: ThemeMode.system, child: Text('System default')),
PopupMenuItem(value: ThemeMode.light, child: Text('Light')),
PopupMenuItem(value: ThemeMode.dark, child: Text('Dark')),
],
);
}
}
class _CriticalAnnouncement extends StatelessWidget {
const _CriticalAnnouncement({required this.message});
@@ -257,3 +280,9 @@ class _StatusChip extends StatelessWidget {
}
String _formatTimestamp(DateTime value) => value.toIso8601String();
IconData _iconFor(ThemeMode mode) => switch (mode) {
ThemeMode.system => Icons.brightness_auto_outlined,
ThemeMode.light => Icons.light_mode_outlined,
ThemeMode.dark => Icons.dark_mode_outlined,
};
@@ -0,0 +1,17 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
final themeModeProvider = NotifierProvider<ThemeModeController, ThemeMode>(
ThemeModeController.new,
);
/// Holds the local display preference for the current application session.
///
/// It intentionally starts at the system preference. Persisting an operator's
/// visual preference can be added later without coupling it to vehicle state.
class ThemeModeController extends Notifier<ThemeMode> {
@override
ThemeMode build() => ThemeMode.system;
void select(ThemeMode mode) => state = mode;
}
+10
View File
@@ -21,6 +21,16 @@ void main() {
expect(find.text('Replay fixture'), findsAtLeastNWidgets(1));
expect(find.text('Vehicle identity'), findsOneWidget);
await tester.tap(find.byTooltip('Choose theme'));
await tester.pumpAndSettle();
await tester.tap(find.text('Dark'));
await tester.pumpAndSettle();
expect(
tester.widget<MaterialApp>(find.byType(MaterialApp)).themeMode,
ThemeMode.dark,
);
await tester.tap(find.byIcon(Icons.receipt_long_outlined));
await tester.pumpAndSettle();