cb138a3248
Implements the visual theme system specified in mock up software.pdf.
Flutter project — display/:
- pubspec.yaml: ar_autopilot_display v0.4.0+4 (provider + shared_preferences)
- lib/theme/autopilot_theme.dart: AutopilotTheme class with 30+ design tokens
(backgrounds, panels, text, accent, set-point, semantic states, DISENGAGE,
action buttons, glow helpers, backgroundDecoration getter)
- lib/theme/theme_registry.dart: ThemeRegistry with 4 factory themes,
byId() fallback, architecture stub for Sprint 9 custom YAML themes
- lib/theme/theme_provider.dart: AutopilotThemeProvider (ChangeNotifier),
SharedPreferences persistence under 'autopilot.theme.id', NOT sent to ESP32
- lib/theme/themes/: 4 factory themes with exact hex values from spec:
light (cream/navy, accentGlowRadius=0 — daytime no-glow rule)
cyan (deep navy/neon-cyan, default, glowRadius=16)
wine (vinotinto, DISENGAGE=amber not red, glowRadius=18)
ochre (warm brown/gold, okColor=lime for contrast, glowRadius=18)
- lib/screens/settings/appearance_settings.dart: Appearance screen with
4-card theme previews (200x120px), 400ms AnimatedContainer transitions,
triple-tap shortcut note, Sprint-5 placeholders for auto day/night and
ambient light sensor toggles
- lib/widgets/themed/: 4 themed widgets consuming AutopilotThemeProvider:
compass_rose.dart (heading arc, N mark, set-point tick, glow ring)
disengage_button.dart (60x60 min touch target, gradient, glow)
mode_selector.dart (STANDBY/HDG HOLD/TRACK with accent highlight)
rudder_indicator.dart (horizontal bar -35° to +35°, accent knob)
- lib/main.dart: app entry point with ChangeNotifierProvider
Tests — display/test/theme/:
- theme_registry_test.dart: 4 themes load, correct IDs, display order,
no null tokens, glow rules, backgroundGradient rules
- theme_provider_test.dart: default load=cyan, persistence across restarts,
setTheme notifies listeners, unknown ID falls back to default
- theme_contrast_test.dart: WCAG checks — DISENGAGE ≥7:1 AAA,
action buttons ≥4.5:1 AA, textMain ≥4.5:1 AA, setLight/okColor ≥3:1
Also:
- .gitignore: added !display/lib/ exception (lib/ was excluded for Python venv)
Design rules enforced:
- No glow in light mode (accentGlowRadius=0)
- DISENGAGE = amber in wine theme (red would blend into palette)
- North mark = warm colour on all themes (nautical convention)
- Touch targets: 48px nominal, 60px critical
- Theme not sent to ESP32, not synced between displays
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
60 lines
2.1 KiB
Dart
60 lines
2.1 KiB
Dart
import 'package:flutter/foundation.dart';
|
|
import 'package:shared_preferences/shared_preferences.dart';
|
|
|
|
import 'autopilot_theme.dart';
|
|
import 'theme_registry.dart';
|
|
|
|
/// Manages the active [AutopilotTheme] and persists the user's selection.
|
|
///
|
|
/// ## Persistence
|
|
/// - Stored locally in [SharedPreferences] under [_kThemeKey].
|
|
/// - **NOT sent to the ESP32** — firmware is colour-agnostic.
|
|
/// - **NOT synchronised between displays** — each display (bridge / engine room)
|
|
/// maintains its own independent preference.
|
|
///
|
|
/// ## Usage
|
|
/// ```dart
|
|
/// // In main.dart — wrap the widget tree:
|
|
/// ChangeNotifierProvider<AutopilotThemeProvider>(
|
|
/// create: (_) => await AutopilotThemeProvider.load(),
|
|
/// child: const AutopilotApp(),
|
|
/// )
|
|
///
|
|
/// // Read the current theme anywhere in the tree:
|
|
/// final theme = context.watch<AutopilotThemeProvider>().current;
|
|
///
|
|
/// // Switch theme (e.g. from the Appearance screen):
|
|
/// await context.read<AutopilotThemeProvider>().setTheme('wine');
|
|
/// ```
|
|
class AutopilotThemeProvider extends ChangeNotifier {
|
|
static const String _kThemeKey = 'autopilot.theme.id';
|
|
|
|
AutopilotTheme _current;
|
|
|
|
AutopilotThemeProvider(this._current);
|
|
|
|
/// The currently active [AutopilotTheme].
|
|
AutopilotTheme get current => _current;
|
|
|
|
/// Loads the persisted theme preference from [SharedPreferences].
|
|
///
|
|
/// Falls back to [ThemeRegistry.defaultId] if no preference is stored
|
|
/// (e.g. first launch) or the stored id is no longer registered.
|
|
static Future<AutopilotThemeProvider> load() async {
|
|
final prefs = await SharedPreferences.getInstance();
|
|
final id = prefs.getString(_kThemeKey) ?? ThemeRegistry.defaultId;
|
|
return AutopilotThemeProvider(ThemeRegistry.byId(id));
|
|
}
|
|
|
|
/// Switches to the theme identified by [id] and persists the selection.
|
|
///
|
|
/// Unknown ids silently fall back to [ThemeRegistry.defaultId].
|
|
/// Notifies all listeners after the switch so the UI rebuilds.
|
|
Future<void> setTheme(String id) async {
|
|
_current = ThemeRegistry.byId(id);
|
|
final prefs = await SharedPreferences.getInstance();
|
|
await prefs.setString(_kThemeKey, _current.id);
|
|
notifyListeners();
|
|
}
|
|
}
|