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
|
Ground Suite provides light and dark themes, selecting the operating system's
|
||||||
current preference by default. The dark theme uses near-black, muted-silver,
|
current preference by default. The dark theme uses near-black, muted-silver,
|
||||||
and restrained green accents with locally blurred translucent panels.
|
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
|
Ground Suite uses a receive-only source/decoder/classifier boundary. The
|
||||||
replay implementation feeds typed observations today. A future live adapter
|
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
|
green accents; translucent, locally blurred panels keep operational content
|
||||||
legible without turning diagnostics into decoration.
|
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
|
## Deliberately deferred work
|
||||||
|
|
||||||
- TODO(gcs-live-mavlink): generated MAVLink Common plus custom-dialect
|
- TODO(gcs-live-mavlink): generated MAVLink Common plus custom-dialect
|
||||||
|
|||||||
@@ -1,19 +1,21 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
import 'app_shell.dart';
|
import 'app_shell.dart';
|
||||||
import 'theme.dart';
|
import 'theme.dart';
|
||||||
|
import 'theme_mode_controller.dart';
|
||||||
|
|
||||||
class GroundSuiteApp extends StatelessWidget {
|
class GroundSuiteApp extends ConsumerWidget {
|
||||||
const GroundSuiteApp({super.key});
|
const GroundSuiteApp({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
return MaterialApp(
|
return MaterialApp(
|
||||||
title: 'Ground Suite',
|
title: 'Ground Suite',
|
||||||
debugShowCheckedModeBanner: false,
|
debugShowCheckedModeBanner: false,
|
||||||
theme: groundSuiteLightTheme(),
|
theme: groundSuiteLightTheme(),
|
||||||
darkTheme: groundSuiteDarkTheme(),
|
darkTheme: groundSuiteDarkTheme(),
|
||||||
themeMode: ThemeMode.system,
|
themeMode: ref.watch(themeModeProvider),
|
||||||
home: const AppShell(),
|
home: const AppShell(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
|
import 'theme_mode_controller.dart';
|
||||||
import '../features/diagnostics/presentation/diagnostics_page.dart';
|
import '../features/diagnostics/presentation/diagnostics_page.dart';
|
||||||
import '../features/observability/presentation/observability_controller.dart';
|
import '../features/observability/presentation/observability_controller.dart';
|
||||||
import '../features/overview/presentation/overview_page.dart';
|
import '../features/overview/presentation/overview_page.dart';
|
||||||
@@ -220,6 +221,7 @@ class _ConnectionBar extends StatelessWidget {
|
|||||||
_StatusChip(label: state.status.label),
|
_StatusChip(label: state.status.label),
|
||||||
Text(peer),
|
Text(peer),
|
||||||
Text(frameTime),
|
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 {
|
class _CriticalAnnouncement extends StatelessWidget {
|
||||||
const _CriticalAnnouncement({required this.message});
|
const _CriticalAnnouncement({required this.message});
|
||||||
|
|
||||||
@@ -257,3 +280,9 @@ class _StatusChip extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
String _formatTimestamp(DateTime value) => value.toIso8601String();
|
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('Replay fixture'), findsAtLeastNWidgets(1));
|
||||||
expect(find.text('Vehicle identity'), findsOneWidget);
|
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.tap(find.byIcon(Icons.receipt_long_outlined));
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user