refactor(01-01): rewire all imports, replace logging, apply constants

- Update gps_data_manager.dart: import ProcessedGpsData from models/, use GpsConfig constants, replace all print() with Logger calls
- Update gps_service.dart: use GpsConfig.VALID_HEADING_MIN/MAX for heading validation
- Update main.dart: import from config/, models/, replace all customDebugPrint() and print() with Logger, use TimingConfig/OverlayConfig constants, convert overlay IPC to OverlayMessage typed class
- Delete customDebugPrint() function definition
- Delete lib/speed_units.dart and lib/color_themes.dart (moved to config/)
- Zero analyzer errors, zero bare print() calls remaining
This commit is contained in:
Mikkeli Matlock
2026-02-09 21:33:28 +09:00
parent 5122be4bc1
commit a0d93d7f6d
5 changed files with 209 additions and 332 deletions
-64
View File
@@ -1,64 +0,0 @@
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<ColorTheme> 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;
}
}
+6 -5
View File
@@ -1,10 +1,11 @@
import 'dart:async';
import 'package:geolocator/geolocator.dart';
import 'package:permission_handler/permission_handler.dart';
import 'config/gps_constants.dart';
class GpsService {
static double? _lastValidHeading;
static Stream<Position> get positionStream {
return Geolocator.getPositionStream(
locationSettings: const LocationSettings(
@@ -13,18 +14,18 @@ class GpsService {
),
);
}
static Future<bool> requestPermissions() async {
final permission = await Permission.locationWhenInUse.request();
return permission.isGranted;
}
static Future<bool> isLocationServiceEnabled() async {
return await Geolocator.isLocationServiceEnabled();
}
static double getCachedHeading(double heading) {
if (heading >= 0 && heading <= 360) {
if (heading >= GpsConfig.VALID_HEADING_MIN && heading <= GpsConfig.VALID_HEADING_MAX) {
_lastValidHeading = heading;
return heading;
}
+156 -162
View File
@@ -5,20 +5,16 @@ import 'dart:async';
import 'package:wakelock_plus/wakelock_plus.dart';
import 'package:flutter_overlay_window/flutter_overlay_window.dart';
import 'package:bg_launcher/bg_launcher.dart'; // For bringing app to foreground
import 'speed_units.dart';
import 'color_themes.dart';
import 'config/speed_unit.dart';
import 'config/color_themes.dart';
import 'config/timing_constants.dart';
import 'config/overlay_constants.dart';
import 'models/overlay_message.dart';
import 'models/processed_gps_data.dart';
import 'services/gps_data_manager.dart';
import 'services/logger.dart';
import 'dart:math' as math; // Import for math.pi
// Global debug configuration (removed - now handled by GPS manager)
// Debug helper - only prints in debug builds
void customDebugPrint(String message) {
if (kDebugMode) {
print(message);
}
}
void main() {
runApp(const SpeedoApp());
}
@@ -26,7 +22,7 @@ void main() {
// Entry point for overlay window
@pragma("vm:entry-point")
void overlayMain() {
customDebugPrint('[Overlay] 🚀 overlayMain() called');
Logger.info('overlayMain() called', 'Overlay');
runApp(const MaterialApp(
debugShowCheckedModeBanner: false,
home: OverlaySpeedometer(),
@@ -85,11 +81,11 @@ class _SpeedometerScreenState extends State<SpeedometerScreen> with WidgetsBindi
Map<String, int> _getOverlaySize() {
final window = WidgetsBinding.instance.platformDispatcher.views.first;
final physicalSize = window.physicalSize;
// Use actual physical pixels for overlay sizing
final overlayWidth = (physicalSize.width * 0.45).round(); // 45% of actual screen width
final overlayHeight = (overlayWidth * 0.6).round();
final overlayWidth = (physicalSize.width * OverlayConfig.WIDTH_PERCENTAGE).round();
final overlayHeight = (overlayWidth * OverlayConfig.ASPECT_RATIO).round();
return {'width': overlayWidth, 'height': overlayHeight};
}
@@ -125,22 +121,22 @@ class _SpeedometerScreenState extends State<SpeedometerScreen> with WidgetsBindi
_tapCloseRequested = true; // Reusing the flag for long press
// Set a timer to reset this flag if overlay doesn't close soon
_tapCloseTimer?.cancel();
_tapCloseTimer = Timer(const Duration(milliseconds: 500), () {
_tapCloseTimer = Timer(TimingConfig.TAP_CLOSE_DELAY, () {
_tapCloseRequested = false;
});
break;
// 'bringToFront' action handler removed per NOTES.txt (tap to bring front functionality disabled)
default:
customDebugPrint('[Main] ⚠️ Unknown overlay action: "$action"');
Logger.warn('Unknown overlay action: "$action"', 'Main');
}
}
} catch (e, stackTrace) {
customDebugPrint('[Main] ❌ Error processing overlay message: $e');
customDebugPrint('[Main] 📚 Stack trace: $stackTrace');
Logger.error('Error processing overlay message: $e', 'Main');
Logger.error('Stack trace: $stackTrace', 'Main');
}
},
onError: (error) {
customDebugPrint('[Main] ❌ Overlay listener error: $error');
Logger.error('Overlay listener error: $error', 'Main');
},
);
}
@@ -154,7 +150,7 @@ class _SpeedometerScreenState extends State<SpeedometerScreen> with WidgetsBindi
// Cancel any existing timer first
_overlayStatusCheckTimer?.cancel();
_overlayStatusCheckTimer = Timer.periodic(const Duration(milliseconds: 1000), (timer) async {
_overlayStatusCheckTimer = Timer.periodic(TimingConfig.OVERLAY_STATUS_CHECK_INTERVAL, (timer) async {
if (_isOverlayActive) {
try {
final isActive = await FlutterOverlayWindow.isActive();
@@ -163,14 +159,14 @@ class _SpeedometerScreenState extends State<SpeedometerScreen> with WidgetsBindi
_handleOverlayClose();
}
} catch (e) {
customDebugPrint('[Main] ❌ Overlay status check failed: $e');
Logger.error('Overlay status check failed: $e', 'Main');
}
} else {
// If overlay is not supposed to be active, cancel the timer
timer.cancel();
}
});
customDebugPrint('[Main] 🔍 Started overlay status monitoring');
Logger.debug('Started overlay status monitoring', 'Main');
}
void _handleOverlayClose({bool bringToForeground = false}) {
@@ -196,7 +192,7 @@ class _SpeedometerScreenState extends State<SpeedometerScreen> with WidgetsBindi
void _startBackgroundHeartbeat() {
// Aggressive heartbeat to keep main app process active for overlay communication
_backgroundHeartbeatTimer = Timer.periodic(const Duration(seconds: 5), (timer) {
_backgroundHeartbeatTimer = Timer.periodic(TimingConfig.HEARTBEAT_INTERVAL, (timer) {
if (_isInBackground && _isOverlayActive) {
// Minimal activity to prevent hibernation
if (mounted) {
@@ -231,8 +227,8 @@ class _SpeedometerScreenState extends State<SpeedometerScreen> with WidgetsBindi
});
}
} catch (e) {
customDebugPrint('[Main] ❌ BgLauncher failed to bring app to front: $e');
Logger.error('BgLauncher failed to bring app to front: $e', 'Main');
// Enhanced fallback: try alternative approach
try {
if (mounted) {
@@ -241,7 +237,7 @@ class _SpeedometerScreenState extends State<SpeedometerScreen> with WidgetsBindi
});
}
} catch (fallbackError) {
customDebugPrint('[Main] ❌ Fallback method also failed: $fallbackError');
Logger.error('Fallback method also failed: $fallbackError', 'Main');
}
}
}
@@ -254,11 +250,11 @@ class _SpeedometerScreenState extends State<SpeedometerScreen> with WidgetsBindi
// Double-check wake lock status for debugging
if (!isEnabled) {
customDebugPrint('[Main] ⚠️ Wake lock not properly enabled, retrying...');
Logger.warn('Wake lock not properly enabled, retrying...', 'Main');
await WakelockPlus.enable();
}
} catch (e) {
customDebugPrint('[Main] ❌ Wake lock failed: $e');
Logger.error('Wake lock failed: $e', 'Main');
// Wake lock not supported on this platform, continue normally
}
}
@@ -272,7 +268,7 @@ class _SpeedometerScreenState extends State<SpeedometerScreen> with WidgetsBindi
_overlayStatusCheckTimer?.cancel();
_tapCloseTimer?.cancel();
WakelockPlus.disable();
customDebugPrint('[Main] 🛑 Main app disposed - all subscriptions and timers canceled');
Logger.info('Main app disposed - all subscriptions and timers canceled', 'Main');
super.dispose();
}
@@ -316,66 +312,66 @@ class _SpeedometerScreenState extends State<SpeedometerScreen> with WidgetsBindi
// Note: This requires platform-specific implementation which we'd need to add via method channel
// For now, we'll rely on other persistence mechanisms
} catch (e) {
customDebugPrint('[Main] ⚠️ Battery optimization exemption not available: $e');
Logger.warn('Battery optimization exemption not available: $e', 'Main');
}
}
Future<void> _ensureOverlayPermission() async {
try {
// Check if overlay permission is granted (required for bg_launcher to work properly)
final isGranted = await FlutterOverlayWindow.isPermissionGranted();
if (!isGranted) {
await FlutterOverlayWindow.requestPermission();
}
} catch (e) {
customDebugPrint('[Main] ❌ Error checking overlay permission: $e');
Logger.error('Error checking overlay permission: $e', 'Main');
}
}
Future<void> _initializeGpsManager() async {
print('[Main] 🚀 Starting GPS manager initialization...');
Logger.info('Starting GPS manager initialization...', 'Main');
try {
// Initialize the GPS manager
print('[Main] 📡 Calling GpsDataManager.instance.initialize()...');
Logger.debug('Calling GpsDataManager.instance.initialize()...', 'Main');
await GpsDataManager.instance.initialize();
print('[Main] ✅ GPS manager initialization completed');
Logger.info('GPS manager initialization completed', 'Main');
// Subscribe to processed GPS data
print('[Main] 🎧 Setting up data stream subscription...');
Logger.debug('Setting up data stream subscription...', 'Main');
_gpsDataSubscription = GpsDataManager.instance.dataStream.listen(
_onGpsDataUpdate,
onError: (error) {
print('[Main] ❌ GPS data stream error: $error');
Logger.error('GPS data stream error: $error', 'Main');
setState(() => _errorMessage = 'GPS manager error: $error');
},
);
print('[Main] ✅ GPS data stream subscription active');
customDebugPrint('[Main] 🛰️ GPS manager initialized and subscribed');
Logger.info('GPS data stream subscription active', 'Main');
Logger.info('GPS manager initialized and subscribed', 'Main');
} catch (e) {
print('[Main] ❌ GPS manager initialization exception: $e');
Logger.error('GPS manager initialization exception: $e', 'Main');
setState(() => _errorMessage = 'Failed to initialize GPS manager');
customDebugPrint('[Main] ❌ GPS manager initialization failed: $e');
Logger.error('GPS manager initialization failed: $e', 'Main');
}
}
void _onGpsDataUpdate(ProcessedGpsData gpsData) {
print('[Main] 📥 Received GPS data: ${gpsData.speed.toStringAsFixed(1)} m/s, ${gpsData.displayHeading}');
Logger.debug('Received GPS data: ${gpsData.speed.toStringAsFixed(1)} m/s, ${gpsData.displayHeading}', 'Main');
setState(() {
_currentGpsData = gpsData;
_errorMessage = '';
});
print('[Main] ✅ State updated with GPS data');
Logger.debug('State updated with GPS data', 'Main');
final convertedSpeed = _currentUnit.convert(gpsData.speed);
customDebugPrint('[Main] 📡 Converted for display: ${convertedSpeed.toStringAsFixed(1)} ${_currentUnit.label}, ${gpsData.displayHeading}');
Logger.debug('Converted for display: ${convertedSpeed.toStringAsFixed(1)} ${_currentUnit.label}, ${gpsData.displayHeading}', 'Main');
// Always push display data to overlay if active - critical for background communication
if (_isOverlayActive) {
_pushDataToOverlay();
customDebugPrint('[Main] 📤 Data pushed to overlay from GPS update');
Logger.debug('Data pushed to overlay from GPS update', 'Main');
}
}
@@ -383,32 +379,31 @@ class _SpeedometerScreenState extends State<SpeedometerScreen> with WidgetsBindi
void _pushDataToOverlay() {
// Only push data if overlay is actually active (listener check was causing race conditions)
if (!_isOverlayActive) {
customDebugPrint('[Main] 🚫 Skipping data push - no active overlay (active: $_isOverlayActive, subscription: ${_overlaySubscription != null})');
Logger.debug('Skipping data push - no active overlay (active: $_isOverlayActive, subscription: ${_overlaySubscription != null})', 'Main');
return;
}
customDebugPrint('[Main] ✅ Overlay guard passed - pushing data (active: $_isOverlayActive, subscription: ${_overlaySubscription != null})');
Logger.debug('Overlay guard passed - pushing data (active: $_isOverlayActive, subscription: ${_overlaySubscription != null})', 'Main');
// Get formatted speed for current unit from GPS manager
final speedText = GpsDataManager.instance.getFormattedSpeed(_currentUnit);
final unitText = _currentUnit.label.toString();
final headingText = _currentGpsData.displayHeading;
customDebugPrint('[Main] 📤 SENDING to overlay:');
customDebugPrint(' speedText: "$speedText"');
customDebugPrint(' headingText: "$headingText"');
customDebugPrint(' direction: ${_currentGpsData.heading.toStringAsFixed(1)}°');
customDebugPrint(' unit: $unitText, theme: $_currentThemeIndex');
FlutterOverlayWindow.shareData({
'action': 'updateDisplay',
'speedText': speedText,
'unitText': unitText,
'headingText': headingText,
'heading': _currentGpsData.heading,
'unitIndex': _currentUnit.index,
'themeIndex': _currentThemeIndex,
});
Logger.debug('SENDING to overlay:', 'Main');
Logger.debug(' speedText: "$speedText"', 'Main');
Logger.debug(' headingText: "$headingText"', 'Main');
Logger.debug(' direction: ${_currentGpsData.heading.toStringAsFixed(1)}°', 'Main');
Logger.debug(' unit: $unitText, theme: $_currentThemeIndex', 'Main');
FlutterOverlayWindow.shareData(OverlayMessage.updateDisplay(
speedText: speedText,
unitText: unitText,
headingText: headingText,
heading: _currentGpsData.heading,
unitIndex: _currentUnit.index,
themeIndex: _currentThemeIndex,
).toMap());
}
void _cycleUnit() {
@@ -437,17 +432,18 @@ class _SpeedometerScreenState extends State<SpeedometerScreen> with WidgetsBindi
// Send initial display data WITH size info (only needed on creation)
final speedText = GpsDataManager.instance.getFormattedSpeed(_currentUnit);
final headingText = _currentGpsData.displayHeading;
await FlutterOverlayWindow.shareData({
'action': 'updateDisplay',
'speedText': speedText,
'headingText': headingText,
'heading': _currentGpsData.heading,
'unitIndex': _currentUnit.index,
'themeIndex': _currentThemeIndex,
'overlayWidth': overlaySize['width']!.toDouble(),
'overlayHeight': overlaySize['height']!.toDouble(),
});
final unitText = _currentUnit.label.toString();
await FlutterOverlayWindow.shareData(OverlayMessage.updateDisplay(
speedText: speedText,
unitText: unitText,
headingText: headingText,
heading: _currentGpsData.heading,
unitIndex: _currentUnit.index,
themeIndex: _currentThemeIndex,
overlayWidth: overlaySize['width']!.toDouble(),
overlayHeight: overlaySize['height']!.toDouble(),
).toMap());
// Show the overlay with proportional sizing based on system resolution
await FlutterOverlayWindow.showOverlay(
@@ -462,11 +458,11 @@ class _SpeedometerScreenState extends State<SpeedometerScreen> with WidgetsBindi
height: overlaySize['height']!,
);
customDebugPrint('[Main] 🟢 OVERLAY LAUNCHED - Size: ${overlaySize['width']}x${overlaySize['height']}');
Logger.info('OVERLAY LAUNCHED - Size: ${overlaySize['width']}x${overlaySize['height']}', 'Main');
// Set overlay active immediately and synchronously
_isOverlayActive = true;
customDebugPrint('[Main] ✅ _isOverlayActive set to: $_isOverlayActive');
Logger.debug('_isOverlayActive set to: $_isOverlayActive', 'Main');
// Start listening to overlay messages after state is set
_startListeningToOverlayMessages();
@@ -483,16 +479,16 @@ class _SpeedometerScreenState extends State<SpeedometerScreen> with WidgetsBindi
Future<void> _closeFloatingWindow() async {
if (!_isOverlayActive) {
customDebugPrint('[Main] ⚠️ No overlay to close');
Logger.warn('No overlay to close', 'Main');
return;
}
try {
await FlutterOverlayWindow.closeOverlay();
customDebugPrint('[Main] 🔴 OVERLAY CLOSED - Requested from main app');
Logger.info('OVERLAY CLOSED - Requested from main app', 'Main');
_handleOverlayClose(); // This will set _isOverlayActive = false and stop listener
} catch (e) {
customDebugPrint('[Main] ❌ Error closing overlay: $e');
Logger.error('Error closing overlay: $e', 'Main');
}
}
@@ -647,7 +643,7 @@ class _SpeedometerScreenState extends State<SpeedometerScreen> with WidgetsBindi
child: GestureDetector(
onTap: () {
HapticFeedback.lightImpact();
customDebugPrint('[Main] 🖱️ Navigation area tapped - overlay active: $_isOverlayActive');
Logger.debug('Navigation area tapped - overlay active: $_isOverlayActive', 'Main');
if (_isOverlayActive) {
_closeFloatingWindow();
} else {
@@ -655,7 +651,7 @@ class _SpeedometerScreenState extends State<SpeedometerScreen> with WidgetsBindi
}
},
onLongPress: () {
customDebugPrint('[Main] 🖱️ Navigation area long-pressed - overlay active: $_isOverlayActive');
Logger.debug('Navigation area long-pressed - overlay active: $_isOverlayActive', 'Main');
_closeFloatingWindow();
},
child: Container(
@@ -774,7 +770,7 @@ class _SpeedometerScreenState extends State<SpeedometerScreen> with WidgetsBindi
child: GestureDetector(
onTap: () {
HapticFeedback.lightImpact();
customDebugPrint('[Main] 🖱️ Navigation area tapped (landscape) - overlay active: $_isOverlayActive');
Logger.debug('Navigation area tapped (landscape) - overlay active: $_isOverlayActive', 'Main');
if (_isOverlayActive) {
_closeFloatingWindow();
} else {
@@ -782,7 +778,7 @@ class _SpeedometerScreenState extends State<SpeedometerScreen> with WidgetsBindi
}
},
onLongPress: () {
customDebugPrint('[Main] 🖱️ Navigation area long-pressed (landscape) - overlay active: $_isOverlayActive');
Logger.debug('Navigation area long-pressed (landscape) - overlay active: $_isOverlayActive', 'Main');
_closeFloatingWindow();
},
child: Container(
@@ -856,14 +852,14 @@ class _OverlaySpeedometerState extends State<OverlaySpeedometer> {
@override
void initState() {
super.initState();
customDebugPrint('[Overlay] 🟢 CREATED - Floating window initialized (${DateTime.now().millisecondsSinceEpoch})');
Logger.info('CREATED - Floating window initialized (${DateTime.now().millisecondsSinceEpoch})', 'Overlay');
_getSystemScreenSize();
_listenToMainAppMessages();
}
@override
void dispose() {
customDebugPrint('[Overlay] 🔴 DISPOSING - Overlay widget disposed (${DateTime.now().millisecondsSinceEpoch})');
Logger.info('DISPOSING - Overlay widget disposed (${DateTime.now().millisecondsSinceEpoch})', 'Overlay');
super.dispose();
}
@@ -875,53 +871,53 @@ class _OverlaySpeedometerState extends State<OverlaySpeedometer> {
final devicePixelRatio = window.devicePixelRatio;
final systemSize = physicalSize / devicePixelRatio;
customDebugPrint('[Overlay] 🔍 Screen detection:');
customDebugPrint(' physicalSize: ${physicalSize.width.round()}x${physicalSize.height.round()}');
customDebugPrint(' devicePixelRatio: $devicePixelRatio');
customDebugPrint(' calculated systemSize: ${systemSize.width.round()}x${systemSize.height.round()}');
Logger.debug('Screen detection:', 'Overlay');
Logger.debug(' physicalSize: ${physicalSize.width.round()}x${physicalSize.height.round()}', 'Overlay');
Logger.debug(' devicePixelRatio: $devicePixelRatio', 'Overlay');
Logger.debug(' calculated systemSize: ${systemSize.width.round()}x${systemSize.height.round()}', 'Overlay');
setState(() {
_systemScreenWidth = systemSize.width;
});
customDebugPrint('[Overlay] ✅ _systemScreenWidth set to: ${_systemScreenWidth.round()}');
Logger.debug('_systemScreenWidth set to: ${_systemScreenWidth.round()}', 'Overlay');
} catch (e) {
customDebugPrint('[Overlay] ❌ Error getting system screen size: $e');
Logger.error('Error getting system screen size: $e', 'Overlay');
// Fallback to reasonable default
setState(() {
_systemScreenWidth = 400;
_systemScreenWidth = OverlayConfig.DEFAULT_SCREEN_WIDTH;
});
}
}
void _listenToMainAppMessages() {
FlutterOverlayWindow.overlayListener.listen((data) {
if (data is Map && data['action'] == 'updateDisplay') {
setState(() {
_speedText = data['speedText'] ?? '--';
_unitText = data['unitText'];
_headingText = data['headingText'] ?? 'N/A';
_heading = data['heading']?.toDouble() ?? -1.0;
_currentThemeIndex = data['themeIndex'] ?? 0;
// Update screen size and overlay dimensions if provided by main app
if (data.containsKey('systemScreenWidth')) {
_systemScreenWidth = data['systemScreenWidth']?.toDouble() ?? _systemScreenWidth;
}
if (data.containsKey('overlayWidth')) {
_overlayWidth = data['overlayWidth']?.toDouble() ?? _overlayWidth;
}
if (data.containsKey('overlayHeight')) {
_overlayHeight = data['overlayHeight']?.toDouble() ?? _overlayHeight;
}
});
customDebugPrint('[Overlay] 📡 UPDATE received:');
customDebugPrint(' speedText: "$_speedText"');
customDebugPrint(' headingText: "$_headingText"');
customDebugPrint(' direction: ${_heading.toStringAsFixed(1)}°');
customDebugPrint(' themeIndex: $_currentThemeIndex');
customDebugPrint(' systemScreenWidth: ${_systemScreenWidth.round()}');
customDebugPrint(' overlaySize from main: ${_overlayWidth.round()}x${_overlayHeight.round()}');
if (data is Map) {
final message = OverlayMessage.fromMap(data);
if (message.action == 'updateDisplay') {
setState(() {
_speedText = message.speedText ?? '--';
_unitText = message.unitText ?? '';
_headingText = message.headingText ?? 'N/A';
_heading = message.heading ?? -1.0;
_currentThemeIndex = message.themeIndex ?? 0;
// Update overlay dimensions if provided by main app
if (message.overlayWidth != null) {
_overlayWidth = message.overlayWidth!;
}
if (message.overlayHeight != null) {
_overlayHeight = message.overlayHeight!;
}
});
Logger.debug('UPDATE received:', 'Overlay');
Logger.debug(' speedText: "$_speedText"', 'Overlay');
Logger.debug(' headingText: "$_headingText"', 'Overlay');
Logger.debug(' direction: ${_heading.toStringAsFixed(1)}°', 'Overlay');
Logger.debug(' themeIndex: $_currentThemeIndex', 'Overlay');
Logger.debug(' systemScreenWidth: ${_systemScreenWidth.round()}', 'Overlay');
Logger.debug(' overlaySize from main: ${_overlayWidth.round()}x${_overlayHeight.round()}', 'Overlay');
}
}
});
}
@@ -933,10 +929,10 @@ class _OverlaySpeedometerState extends State<OverlaySpeedometer> {
final currentTheme = ColorThemes.getTheme(_currentThemeIndex);
// Use overlay dimensions provided by main app (not calculated locally)
final fontSize = (_overlayWidth * 0.2); // Font size proportional to actual overlay width
customDebugPrint('[Overlay] 🔨 Building overlay widget ($buildTimestamp)');
customDebugPrint('[Overlay] 📐 Window size: ${_overlayWidth.round()}x${_overlayHeight.round()}, fontSize: ${fontSize.round()}');
final fontSize = (_overlayWidth * OverlayConfig.FONT_SIZE_RATIO);
Logger.debug('Building overlay widget ($buildTimestamp)', 'Overlay');
Logger.debug('Window size: ${_overlayWidth.round()}x${_overlayHeight.round()}, fontSize: ${fontSize.round()}', 'Overlay');
return Material(
color: Colors.transparent,
@@ -946,10 +942,10 @@ class _OverlaySpeedometerState extends State<OverlaySpeedometer> {
height: double.infinity,
child: Container(
decoration: BoxDecoration(
color: currentTheme.background.withValues(alpha: 0.85),
color: currentTheme.background.withValues(alpha: OverlayConfig.BACKGROUND_OPACITY),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: currentTheme.speedText.withValues(alpha: 0.3),
color: currentTheme.speedText.withValues(alpha: OverlayConfig.BORDER_OPACITY),
width: 1
),
),
@@ -989,7 +985,7 @@ class _OverlaySpeedometerState extends State<OverlaySpeedometer> {
child: Text(
_unitText,
style: TextStyle(
fontSize: fontSize * 0.6,
fontSize: fontSize * OverlayConfig.UNIT_FONT_RATIO,
fontWeight: FontWeight.w300,
color: currentTheme.unitText,
fontFamily: 'DIN1451Alt',
@@ -1018,7 +1014,7 @@ class _OverlaySpeedometerState extends State<OverlaySpeedometer> {
fit: BoxFit.contain,
child: Icon(
Icons.navigation,
size: fontSize * 0.72,
size: fontSize * OverlayConfig.ICON_SIZE_RATIO,
color: currentTheme.headingText,
),
),
@@ -1031,7 +1027,7 @@ class _OverlaySpeedometerState extends State<OverlaySpeedometer> {
child: Text(
_headingText,
style: TextStyle(
fontSize: fontSize * 0.4,
fontSize: fontSize * OverlayConfig.HEADING_FONT_RATIO,
color: currentTheme.headingText,
fontFamily: 'DIN1451Alt',
),
@@ -1049,43 +1045,41 @@ class _OverlaySpeedometerState extends State<OverlaySpeedometer> {
child: GestureDetector(
behavior: HitTestBehavior.translucent, // Ensure gestures are captured
onPanStart: (details) {
customDebugPrint('[Overlay] 🖐️ Pan start detected - gestures are working!');
Logger.debug('Pan start detected - gestures are working!', 'Overlay');
},
onLongPress: () async {
final timestamp = DateTime.now().millisecondsSinceEpoch;
customDebugPrint('[Overlay] 🔴 Long press detected - closing overlay and bringing main app to front ($timestamp)');
Logger.info('Long press detected - closing overlay and bringing main app to front ($timestamp)', 'Overlay');
try {
// Skip haptic feedback in overlay - platform services not available
customDebugPrint('[Overlay] 📳 Skipping haptic feedback (overlay context limitation)');
Logger.debug('Skipping haptic feedback (overlay context limitation)', 'Overlay');
// Signal main app to bring to foreground when overlay closes (non-blocking)
customDebugPrint('[Overlay] 📤 Sending bring-to-front close signal...');
FlutterOverlayWindow.shareData({
'action': 'longPressClose',
}).then((_) {
customDebugPrint('[Overlay] ✅ Long press close signal sent successfully');
Logger.debug('Sending bring-to-front close signal...', 'Overlay');
FlutterOverlayWindow.shareData(OverlayMessage.longPressClose().toMap()).then((_) {
Logger.debug('Long press close signal sent successfully', 'Overlay');
}).catchError((error) {
customDebugPrint('[Overlay] ❌ Long press close signal failed (expected): $error');
Logger.warn('Long press close signal failed (expected): $error', 'Overlay');
});
// Small delay to give signal a chance
await Future.delayed(const Duration(milliseconds: 50));
await Future.delayed(TimingConfig.LONG_PRESS_SIGNAL_DELAY);
// Close the overlay
customDebugPrint('[Overlay] 🔴 Attempting to close overlay...');
Logger.info('Attempting to close overlay...', 'Overlay');
await FlutterOverlayWindow.closeOverlay();
customDebugPrint('[Overlay] ✅ Overlay closed successfully via long press');
Logger.info('Overlay closed successfully via long press', 'Overlay');
} catch (e, stackTrace) {
customDebugPrint('[Overlay] ❌ Long press close failed: $e');
customDebugPrint('[Overlay] 📚 Stack trace: $stackTrace');
Logger.error('Long press close failed: $e', 'Overlay');
Logger.error('Stack trace: $stackTrace', 'Overlay');
// Fallback: try to close anyway
try {
customDebugPrint('[Overlay] 🔄 Attempting fallback close...');
Logger.debug('Attempting fallback close...', 'Overlay');
await FlutterOverlayWindow.closeOverlay();
customDebugPrint('[Overlay] ✅ Fallback close succeeded');
Logger.info('Fallback close succeeded', 'Overlay');
} catch (fallbackError) {
customDebugPrint('[Overlay] ❌ Fallback close also failed: $fallbackError');
Logger.error('Fallback close also failed: $fallbackError', 'Overlay');
}
}
},
+47 -81
View File
@@ -1,43 +1,10 @@
import 'dart:async';
import 'package:geolocator/geolocator.dart';
import '../gps_service.dart';
import '../speed_units.dart';
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,
);
}
}
import '../config/speed_unit.dart';
import '../config/gps_constants.dart';
import '../models/processed_gps_data.dart';
import 'logger.dart';
class GpsDataManager {
static GpsDataManager? _instance;
@@ -58,7 +25,6 @@ class GpsDataManager {
isHeadingValid: false,
);
static const Duration _staleDataThreshold = Duration(seconds: 4);
Timer? _staleDataTimer;
// Public stream for UI components to subscribe to
@@ -71,17 +37,17 @@ class GpsDataManager {
Future<void> initialize() async {
if (_isInitialized) {
print('[GpsDataManager] ⚠️ Already initialized, skipping...');
Logger.warn('Already initialized, skipping...', 'GpsDataManager');
return;
}
print('[GpsDataManager] 🚀 Starting GPS manager initialization...');
Logger.info('Starting GPS manager initialization...', 'GpsDataManager');
// Check GPS permissions and services first
final serviceEnabled = await GpsService.isLocationServiceEnabled();
print('[GpsDataManager] 📍 Location service enabled: $serviceEnabled');
Logger.debug('Location service enabled: $serviceEnabled', 'GpsDataManager');
if (!serviceEnabled) {
print('[GpsDataManager] ❌ GPS service disabled');
Logger.error('GPS service disabled', 'GpsDataManager');
_updateData(_currentData.copyWith(
displaySpeed: 'GPS OFF',
displayHeading: 'GPS OFF'
@@ -91,9 +57,9 @@ class GpsDataManager {
}
final permissionGranted = await GpsService.requestPermissions();
print('[GpsDataManager] 🔐 GPS permission granted: $permissionGranted');
Logger.debug('GPS permission granted: $permissionGranted', 'GpsDataManager');
if (!permissionGranted) {
print('[GpsDataManager] ❌ GPS permission denied');
Logger.error('GPS permission denied', 'GpsDataManager');
_updateData(_currentData.copyWith(
displaySpeed: 'NO PERM',
displayHeading: 'NO PERM'
@@ -103,47 +69,47 @@ class GpsDataManager {
}
// Create GPS stream subscription
print('[GpsDataManager] 📡 Creating GPS stream subscription...');
Logger.debug('Creating GPS stream subscription...', 'GpsDataManager');
try {
print('[GpsDataManager] 🔧 Getting initial GPS position with long timeout...');
Logger.debug('Getting initial GPS position with long timeout...', 'GpsDataManager');
// First, get initial position with long timeout for cold start
final initialPosition = await Geolocator.getCurrentPosition(
locationSettings: const LocationSettings(
locationSettings: LocationSettings(
accuracy: LocationAccuracy.bestForNavigation,
timeLimit: Duration(seconds: 20), // Long timeout for initial fix
timeLimit: GpsConfig.INITIAL_FIX_TIMEOUT,
),
);
print('[GpsDataManager] ✅ Initial GPS fix obtained');
Logger.info('Initial GPS fix obtained', 'GpsDataManager');
_onPositionUpdate(initialPosition);
// Then start position stream with shorter timeout for updates
print('[GpsDataManager] 🔧 Starting GPS stream with short update timeout...');
Logger.debug('Starting GPS stream with short update timeout...', 'GpsDataManager');
_gpsSubscription = GpsService.positionStream.listen(
(Position position) {
print('[GpsDataManager] 📥 GPS callback triggered');
Logger.debug('GPS callback triggered', 'GpsDataManager');
_onPositionUpdate(position);
},
onError: (error) {
print('[GpsDataManager] ❌ GPS stream error callback: $error');
Logger.error('GPS stream error callback: $error', 'GpsDataManager');
_onGpsError(error);
},
onDone: () {
print('[GpsDataManager] ⚠️ GPS stream done callback - stream ended');
Logger.warn('GPS stream done callback - stream ended', 'GpsDataManager');
},
cancelOnError: false,
);
print('[GpsDataManager] ✅ GPS stream subscription created');
print('[GpsDataManager] 🔍 Subscription details: ${_gpsSubscription.runtimeType}');
Logger.info('GPS stream subscription created', 'GpsDataManager');
Logger.debug('Subscription details: ${_gpsSubscription.runtimeType}', 'GpsDataManager');
_isInitialized = true;
} catch (e, stackTrace) {
print('[GpsDataManager] ❌ GPS subscription creation failed: $e');
print('[GpsDataManager] 📚 Stack trace: $stackTrace');
Logger.error('GPS subscription creation failed: $e', 'GpsDataManager');
Logger.error('Stack trace: $stackTrace', 'GpsDataManager');
_updateData(_currentData.copyWith(
displaySpeed: 'GPS ERR',
displayHeading: 'GPS ERR'
@@ -151,12 +117,12 @@ class GpsDataManager {
_isInitialized = true; // Mark as initialized even if failed
return;
}
print('[GpsDataManager] ✅ GPS manager initialized successfully');
Logger.info('GPS manager initialized successfully', 'GpsDataManager');
}
void _onPositionUpdate(Position position) {
print('[GpsDataManager] 📡 Raw GPS update: speed=${position.speed.toStringAsFixed(2)} m/s, heading=${position.heading.toStringAsFixed(1)}°');
Logger.debug('Raw GPS update: speed=${position.speed.toStringAsFixed(2)} m/s, heading=${position.heading.toStringAsFixed(1)}°', 'GpsDataManager');
_scheduleStaleDataTimer();
@@ -165,18 +131,18 @@ class GpsDataManager {
final double speed = speedValid ? rawSpeed : 0.0;
final rawHeading = position.heading;
final bool headingValid = rawHeading.isFinite && rawHeading >= 0 && rawHeading < 360;
final bool headingValid = rawHeading.isFinite && rawHeading >= GpsConfig.VALID_HEADING_MIN && rawHeading < GpsConfig.VALID_HEADING_MAX;
final double heading = headingValid ? rawHeading : _currentData.heading;
// Basic display logic - TEMPORARILY show all speeds for indoor testing
// TODO: Re-enable low-speed logic later: (speed < 1.0 && (heading < 0.0 || heading >= 360.0)) ? '--' : ...
// TODO: Re-enable low-speed logic later: (speed < GpsConfig.LOW_SPEED_THRESHOLD && (heading < GpsConfig.VALID_HEADING_MIN || heading >= GpsConfig.VALID_HEADING_MAX)) ? '--' : ...
final displaySpeed = speed.toStringAsFixed(1);
final displayHeading = headingValid
? GpsService.formatHeading(heading)
: _currentData.displayHeading;
print('[GpsDataManager] 🔄 Processed: displaySpeed="$displaySpeed", displayHeading="$displayHeading"');
Logger.debug('Processed: displaySpeed="$displaySpeed", displayHeading="$displayHeading"', 'GpsDataManager');
final processedData = ProcessedGpsData(
speed: speed,
@@ -205,7 +171,7 @@ class GpsDataManager {
void _scheduleStaleDataTimer() {
_staleDataTimer?.cancel();
_staleDataTimer = Timer(_staleDataThreshold, _handleStaleDataTimeout);
_staleDataTimer = Timer(GpsConfig.STALE_DATA_THRESHOLD, _handleStaleDataTimeout);
}
void _handleStaleDataTimeout() {
@@ -215,10 +181,10 @@ class GpsDataManager {
return;
}
final headingStillValid = _currentData.isHeadingValid && _currentData.heading >= 0 && _currentData.heading < 360;
final headingStillValid = _currentData.isHeadingValid && _currentData.heading >= GpsConfig.VALID_HEADING_MIN && _currentData.heading < GpsConfig.VALID_HEADING_MAX;
final displayHeading = headingStillValid ? _currentData.displayHeading : '--';
print('[GpsDataManager] ⏱️ No GPS updates within ${_staleDataThreshold.inSeconds}s - marking data as stale');
Logger.warn('No GPS updates within ${GpsConfig.STALE_DATA_THRESHOLD.inSeconds}s - marking data as stale', 'GpsDataManager');
final staleData = _currentData.copyWith(
displaySpeed: '--',
@@ -232,10 +198,10 @@ class GpsDataManager {
}
void _updateData(ProcessedGpsData newData) {
print('[GpsDataManager] 📤 Broadcasting data: ${newData.displaySpeed} ${newData.displayHeading}');
Logger.debug('Broadcasting data: ${newData.displaySpeed} ${newData.displayHeading}', 'GpsDataManager');
_currentData = newData;
_dataController.add(newData);
print('[GpsDataManager] ✅ Data broadcast complete');
Logger.debug('Data broadcast complete', 'GpsDataManager');
}
// Format speed for specific unit (used by UI)
@@ -247,17 +213,17 @@ class GpsDataManager {
}
void dispose() {
print('[GpsDataManager] 🛑 Disposing GPS manager...');
Logger.info('Disposing GPS manager...', 'GpsDataManager');
if (_gpsSubscription != null) {
print('[GpsDataManager] 🛑 Cancelling GPS subscription...');
Logger.debug('Cancelling GPS subscription...', 'GpsDataManager');
_gpsSubscription?.cancel();
_gpsSubscription = null;
}
print('[GpsDataManager] 🛑 Closing data controller...');
Logger.debug('Closing data controller...', 'GpsDataManager');
_staleDataTimer?.cancel();
_staleDataTimer = null;
_dataController.close();
_isInitialized = false;
print('[GpsDataManager] 🛑 GPS manager disposed');
Logger.info('GPS manager disposed', 'GpsDataManager');
}
}
-20
View File
@@ -1,20 +0,0 @@
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];
}
}