diff --git a/lib/config/color_themes.dart b/lib/config/color_themes.dart new file mode 100644 index 0000000..6e67cf5 --- /dev/null +++ b/lib/config/color_themes.dart @@ -0,0 +1,64 @@ +import 'package:flutter/material.dart'; + +class ColorTheme { + final Color background; + final Color speedText; + final Color speedTextSub; + final Color headingText; + final Color unitText; + final String name; + + const ColorTheme({ + required this.background, + required this.speedText, + required this.speedTextSub, + required this.headingText, + required this.unitText, + required this.name, + }); +} + +class ColorThemes { + static const List themes = [ + ColorTheme( + name: 'Yuzuki', + background: Color.fromARGB(255, 114, 85, 109), + speedText: Color.fromARGB(255, 246, 224, 255), + speedTextSub: Color.fromARGB(255, 245, 217, 245), + headingText: Color.fromARGB(255, 244, 201, 255), + unitText: Color.fromARGB(255, 244, 201, 255), + ), + ColorTheme( + name: 'Yuuparo', + background: Color.fromARGB(255, 85, 99, 112), + speedText: Color.fromARGB(255, 255, 171, 16), + speedTextSub: Color.fromARGB(255, 245, 204, 116), + headingText: Color.fromARGB(255, 228, 235, 238), + unitText: Color.fromARGB(255, 141, 196, 184), + ), + ColorTheme( + name: 'Yuzuki Light', + background: Color.fromARGB(255, 246, 224, 255), + speedText: Color.fromARGB(255, 114, 85, 109), + speedTextSub: Color.fromARGB(255, 161, 138, 170), + headingText: Color.fromARGB(255, 140, 94, 151), + unitText: Color.fromARGB(255, 114, 85, 109), + ), + ColorTheme( + name: 'Yuuparo Light', + background: Color.fromARGB(255, 228, 235, 238), + speedText: Color.fromARGB(255, 85, 99, 112), + speedTextSub: Color.fromARGB(255, 100, 100, 131), + headingText: Color.fromARGB(255, 221, 118, 0), + unitText: Color.fromARGB(255, 98, 124, 118), + ), + ]; + + static ColorTheme getTheme(int index) { + return themes[index % themes.length]; + } + + static int getNextThemeIndex(int currentIndex) { + return (currentIndex + 1) % themes.length; + } +} diff --git a/lib/config/speed_unit.dart b/lib/config/speed_unit.dart new file mode 100644 index 0000000..1163cd3 --- /dev/null +++ b/lib/config/speed_unit.dart @@ -0,0 +1,20 @@ +enum SpeedUnit { + kmh('km/h', 1.0), + mph('mph', 0.621371), + knots('kts', 0.539957); + + const SpeedUnit(this.label, this.multiplier); + + final String label; + final double multiplier; + + double convert(double speedMps) { + return speedMps * 3.6 * multiplier; + } + + SpeedUnit get next { + final values = SpeedUnit.values; + final currentIndex = values.indexOf(this); + return values[(currentIndex + 1) % values.length]; + } +} diff --git a/lib/models/overlay_message.dart b/lib/models/overlay_message.dart new file mode 100644 index 0000000..000fa46 --- /dev/null +++ b/lib/models/overlay_message.dart @@ -0,0 +1,113 @@ +/// Typed message class for overlay IPC communication +/// Replaces unvalidated Map data with type-safe serialization +class OverlayMessage { + final String action; + final String? speedText; + final String? unitText; + final String? headingText; + final double? heading; + final int? unitIndex; + final int? themeIndex; + final double? overlayWidth; + final double? overlayHeight; + + const OverlayMessage({ + required this.action, + this.speedText, + this.unitText, + this.headingText, + this.heading, + this.unitIndex, + this.themeIndex, + this.overlayWidth, + this.overlayHeight, + }); + + /// Factory constructor for display update messages + factory OverlayMessage.updateDisplay({ + required String speedText, + required String unitText, + required String headingText, + required double heading, + required int unitIndex, + required int themeIndex, + double? overlayWidth, + double? overlayHeight, + }) { + return OverlayMessage( + action: 'updateDisplay', + speedText: speedText, + unitText: unitText, + headingText: headingText, + heading: heading, + unitIndex: unitIndex, + themeIndex: themeIndex, + overlayWidth: overlayWidth, + overlayHeight: overlayHeight, + ); + } + + /// Factory constructor for long press close signal + factory OverlayMessage.longPressClose() { + return const OverlayMessage(action: 'longPressClose'); + } + + /// Factory constructor for overlay closed notification + factory OverlayMessage.overlayClosed() { + return const OverlayMessage(action: 'overlayClosed'); + } + + /// Serialize to Map for overlay IPC + Map toMap() { + final map = {'action': action}; + if (speedText != null) map['speedText'] = speedText; + if (unitText != null) map['unitText'] = unitText; + if (headingText != null) map['headingText'] = headingText; + if (heading != null) map['heading'] = heading; + if (unitIndex != null) map['unitIndex'] = unitIndex; + if (themeIndex != null) map['themeIndex'] = themeIndex; + if (overlayWidth != null) map['overlayWidth'] = overlayWidth; + if (overlayHeight != null) map['overlayHeight'] = overlayHeight; + return map; + } + + /// Deserialize from Map received via overlay IPC + factory OverlayMessage.fromMap(Map map) { + return OverlayMessage( + action: map['action'] as String? ?? 'unknown', + speedText: map['speedText'] as String?, + unitText: map['unitText'] as String?, + headingText: map['headingText'] as String?, + heading: (map['heading'] as num?)?.toDouble(), + unitIndex: map['unitIndex'] as int?, + themeIndex: map['themeIndex'] as int?, + overlayWidth: (map['overlayWidth'] as num?)?.toDouble(), + overlayHeight: (map['overlayHeight'] as num?)?.toDouble(), + ); + } + + /// Create a copy with updated fields + OverlayMessage copyWith({ + String? action, + String? speedText, + String? unitText, + String? headingText, + double? heading, + int? unitIndex, + int? themeIndex, + double? overlayWidth, + double? overlayHeight, + }) { + return OverlayMessage( + action: action ?? this.action, + speedText: speedText ?? this.speedText, + unitText: unitText ?? this.unitText, + headingText: headingText ?? this.headingText, + heading: heading ?? this.heading, + unitIndex: unitIndex ?? this.unitIndex, + themeIndex: themeIndex ?? this.themeIndex, + overlayWidth: overlayWidth ?? this.overlayWidth, + overlayHeight: overlayHeight ?? this.overlayHeight, + ); + } +} diff --git a/lib/models/processed_gps_data.dart b/lib/models/processed_gps_data.dart new file mode 100644 index 0000000..642815a --- /dev/null +++ b/lib/models/processed_gps_data.dart @@ -0,0 +1,35 @@ +class ProcessedGpsData { + final double speed; // Raw speed in m/s + final double heading; // Raw heading in degrees + final String displaySpeed; // Formatted speed text (with -- logic) + final String displayHeading; // Formatted heading text + final bool isSpeedValid; // True if from recent GPS + final bool isHeadingValid; // True if from recent GPS + + const ProcessedGpsData({ + required this.speed, + required this.heading, + required this.displaySpeed, + required this.displayHeading, + required this.isSpeedValid, + required this.isHeadingValid, + }); + + ProcessedGpsData copyWith({ + double? speed, + double? heading, + String? displaySpeed, + String? displayHeading, + bool? isSpeedValid, + bool? isHeadingValid, + }) { + return ProcessedGpsData( + speed: speed ?? this.speed, + heading: heading ?? this.heading, + displaySpeed: displaySpeed ?? this.displaySpeed, + displayHeading: displayHeading ?? this.displayHeading, + isSpeedValid: isSpeedValid ?? this.isSpeedValid, + isHeadingValid: isHeadingValid ?? this.isHeadingValid, + ); + } +} diff --git a/lib/models/theme_model.dart b/lib/models/theme_model.dart new file mode 100644 index 0000000..bf4375b --- /dev/null +++ b/lib/models/theme_model.dart @@ -0,0 +1,32 @@ +import '../config/color_themes.dart'; + +/// Theme model that wraps ColorThemes with rotation and state management. +/// Holds all possible color palettes via delegation, exposing only the current set. +class ThemeModel { + final int _currentIndex; + + const ThemeModel({int currentIndex = 0}) : _currentIndex = currentIndex; + + /// Get the current active color theme + ColorTheme get current => ColorThemes.getTheme(_currentIndex); + + /// Get the name of the current theme + String get currentSetName => current.name; + + /// Get the current theme index + int get currentIndex => _currentIndex; + + /// Rotate to the next theme (circular) + ThemeModel rotate() { + return ThemeModel( + currentIndex: ColorThemes.getNextThemeIndex(_currentIndex), + ); + } + + /// Create a copy with an updated theme index + ThemeModel copyWith({int? currentIndex}) { + return ThemeModel( + currentIndex: currentIndex ?? _currentIndex, + ); + } +}