feat(ground-suite): add theme selector
This commit is contained in:
1 parent
eb6a343983
commit
c67d553076
6 files changed
+66
-3
No files matched your search
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
|
||||
Reference in new issue
Block a user