diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 8966d00..8855c96 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -13,7 +13,9 @@ "Bash(cp:*)", "Bash(flutter:*)", "Bash(powershell:*)", - "WebFetch(domain:docs.anthropic.com)" + "WebFetch(domain:docs.anthropic.com)", + "WebSearch", + "WebFetch(domain:pub.dev)" ], "defaultMode": "acceptEdits" }, diff --git a/CLAUDE.md b/CLAUDE.md index 8b68af5..55f7905 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -3,8 +3,8 @@ ## Project Overview A customizable Flutter GPS speedometer app targeting Android devices, emphasizing modularity and user customization. -## Current Status: v1.1 Complete  -**Enhanced GPS speedometer with improved user experience and customization options.** +## Current Status: v2.1.0 Complete  +**GPS speedometer with floating window overlay functionality and optimized visual design.** ### Architecture Implemented - **State Management:** Provider pattern @@ -31,10 +31,18 @@ Architecture: Provider + Consumer pattern for reactive state management Permissions: Location services with graceful degradation ``` -## Recent Updates (v1.1) +## Recent Updates (v2.1.0) ### ✅ Completed Features -- [x] **Portrait/Landscape Support**: Automatic orientation detection implemented +- [x] **Floating Window Overlay**: System-level overlay window for persistent speed display +- [x] **Split Decimal Display**: Main speed prominent, decimal fraction smaller and faded +- [x] **Optimized Content Sizing**: Maximized readability with improved flex ratios +- [x] **Navigation Icon Trigger**: Tap compass area to launch floating window +- [x] **Multiple Close Options**: Overlay close button and long-press navigation area +- [x] **Android Permissions**: SYSTEM_ALERT_WINDOW and foreground service support + +### Previous Updates (v1.1) +- [x] **Portrait/Landscape Support**: Automatic orientation detection implemented - [x] **Interactive Gauge**: Click gauge face to switch digital/analog modes - [x] **Extended Speed Units**: Added knots support with quick selector buttons diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 2c835a7..7f8462a 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -5,8 +5,10 @@ - - + + + + + + + + FlutterOverlayWindow.closeOverlay(), + child: Container( + width: 24, + height: 24, + decoration: BoxDecoration( + color: Colors.red.withOpacity(0.8), + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.white, width: 1), + ), + child: const Icon( + Icons.close, + color: Colors.white, + size: 16, + ), + ), + ), + ), + ], + ), + ), + ), + )); } class SpeedoApp extends StatelessWidget { @@ -46,17 +99,19 @@ class SpeedometerScreen extends StatefulWidget { State createState() => _SpeedometerScreenState(); } -class _SpeedometerScreenState extends State { +class _SpeedometerScreenState extends State with WidgetsBindingObserver { double _speed = 0.0; double _heading = -1.0; SpeedUnit _currentUnit = SpeedUnit.kmh; int _currentThemeIndex = 0; StreamSubscription? _positionSubscription; String _errorMessage = ''; + bool _isInBackground = false; @override void initState() { super.initState(); + WidgetsBinding.instance.addObserver(this); _initializeGps(); _enableWakelock(); } @@ -71,11 +126,45 @@ class _SpeedometerScreenState extends State { @override void dispose() { + WidgetsBinding.instance.removeObserver(this); _positionSubscription?.cancel(); WakelockPlus.disable(); super.dispose(); } + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + super.didChangeAppLifecycleState(state); + + switch (state) { + case AppLifecycleState.paused: + // App went to background (home key pressed or task switch) + if (!_isInBackground) { + _isInBackground = true; + _handleBackgroundTransition(); + } + break; + case AppLifecycleState.resumed: + // App came back to foreground + _isInBackground = false; + break; + case AppLifecycleState.inactive: + case AppLifecycleState.detached: + case AppLifecycleState.hidden: + break; + } + } + + void _handleBackgroundTransition() { + // When app goes to background, show floating window if GPS is active and no error + print('App went to background, checking conditions for floating window...'); + print('Speed: $_speed, Error: $_errorMessage'); + // Temporarily disable auto-trigger for debugging + // if (_speed >= 0 && _errorMessage.isEmpty) { + // _showFloatingWindow(); + // } + } + Future _initializeGps() async { try { final serviceEnabled = await GpsService.isLocationServiceEnabled(); @@ -124,6 +213,38 @@ class _SpeedometerScreenState extends State { _currentThemeIndex = ColorThemes.getNextThemeIndex(_currentThemeIndex); }); } + + Future _showFloatingWindow() async { + try { + // Share current unit with overlay + await FlutterOverlayWindow.shareData({ + 'action': 'updateUnit', + 'unitIndex': _currentUnit.index, + }); + + // Show the overlay with safer positioning + await FlutterOverlayWindow.showOverlay( + enableDrag: true, + overlayTitle: "GPS Speedometer", + overlayContent: 'GPS Speedometer Overlay', + flag: OverlayFlag.defaultFlag, + visibility: NotificationVisibility.visibilityPublic, + positionGravity: PositionGravity.none, + width: 180, + height: 100, + ); + } catch (e) { + print('Error showing floating window: $e'); + } + } + + Future _closeFloatingWindow() async { + try { + await FlutterOverlayWindow.closeOverlay(); + } catch (e) { + print('Error closing floating window: $e'); + } + } String _getSpeedDisplayText(double displaySpeed) { if (_speed < 1.0 && (_heading < 0.0 || _heading >= 360.0)) { @@ -132,6 +253,51 @@ class _SpeedometerScreenState extends State { return displaySpeed.toStringAsFixed(1); } + Widget _buildSpeedDisplay(String speedText, ColorTheme currentTheme, double fontSize, {bool isLandscape = false}) { + if (speedText == '--') { + return Text( + speedText, + style: TextStyle( + fontSize: fontSize, + fontWeight: FontWeight.w300, + color: currentTheme.speedText, + fontFamily: 'DIN1451Alt', + ), + ); + } + + // Split speed into integral and decimal parts + final parts = speedText.split('.'); + final integralPart = parts[0]; + final decimalPart = parts.length > 1 ? '.${parts[1]}' : '.0'; + + return Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.baseline, + textBaseline: TextBaseline.alphabetic, + children: [ + Text( + integralPart, + style: TextStyle( + fontSize: fontSize, + fontWeight: FontWeight.w300, + color: currentTheme.speedText, + fontFamily: 'DIN1451Alt', + ), + ), + Text( + decimalPart, + style: TextStyle( + fontSize: fontSize * 0.5, // 60% of main font size + fontWeight: FontWeight.w300, + color: currentTheme.speedTextSub, + fontFamily: 'DIN1451Alt', + ), + ), + ], + ); + } + @override Widget build(BuildContext context) { final displaySpeed = _currentUnit.convert(_speed); @@ -165,7 +331,7 @@ class _SpeedometerScreenState extends State { child: Container( width: double.infinity, color: Colors.transparent, - padding: const EdgeInsets.all(12), + padding: const EdgeInsets.all(4), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ @@ -183,33 +349,25 @@ class _SpeedometerScreenState extends State { ) else ...[ Flexible( - flex: 3, + flex: 4, child: GestureDetector( - onTap: _cycleUnit, + onTap: _cycleTheme, // Tap speed to cycle theme child: FittedBox( - fit: BoxFit.scaleDown, - child: Text( - speedText, - style: TextStyle( - fontSize: 160, - fontWeight: FontWeight.w300, - color: currentTheme.speedText, - fontFamily: 'DIN1451Alt', - ), - ), + fit: BoxFit.contain, + child: _buildSpeedDisplay(speedText, currentTheme, 200), ), ), ), Flexible( flex: 1, child: GestureDetector( - onTap: _cycleUnit, + onTap: _cycleUnit, // clicking on unit to cycle unit is more intuitive child: FittedBox( - fit: BoxFit.scaleDown, + fit: BoxFit.contain, child: Text( _currentUnit.label, style: TextStyle( - fontSize: 64, + fontSize: 80, color: currentTheme.unitText, fontFamily: 'DIN1451Alt', ), @@ -222,37 +380,41 @@ class _SpeedometerScreenState extends State { ), ), ), - // Compass area - compact but fully tappable for theme cycling + // Compass area - compact but fully tappable for floating window Expanded( flex: 3, child: GestureDetector( - onTap: _cycleTheme, + onTap: _showFloatingWindow, // Tap navigation icon to show floating window + onLongPress: _closeFloatingWindow, // Long press to close floating window child: Container( width: double.infinity, color: Colors.transparent, - padding: const EdgeInsets.all(16), + padding: const EdgeInsets.all(2), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Flexible( - flex: 2, + flex: 3, child: Transform.rotate( angle: (_heading >= 0 && _heading < 360) ? (_heading * math.pi / 180.0) : 0, - child: Icon( - Icons.navigation, - size: 100, - color: currentTheme.headingText, + child: FittedBox( + fit: BoxFit.contain, + child: Icon( + Icons.navigation, + size: 80, + color: currentTheme.headingText, + ), ), ), ), Flexible( flex: 1, child: FittedBox( - fit: BoxFit.scaleDown, + fit: BoxFit.contain, child: Text( GpsService.formatHeading(_heading), style: TextStyle( - fontSize: 96, + fontSize: 50, color: currentTheme.headingText, fontFamily: 'DIN1451Alt', ), @@ -273,11 +435,11 @@ class _SpeedometerScreenState extends State { children: [ // Speed area - takes majority of space, precise tap targets on text only Expanded( - flex: 65, + flex: 66, child: Container( height: double.infinity, color: Colors.transparent, - padding: const EdgeInsets.all(16), + padding: const EdgeInsets.all(3), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ @@ -295,20 +457,12 @@ class _SpeedometerScreenState extends State { ) else ...[ Flexible( - flex: 3, + flex: 4, child: GestureDetector( - onTap: _cycleUnit, + onTap: _cycleTheme, child: FittedBox( - fit: BoxFit.scaleDown, - child: Text( - speedText, - style: TextStyle( - fontSize: 120, - fontWeight: FontWeight.w300, - color: currentTheme.speedText, - fontFamily: 'DIN1451Alt', - ), - ), + fit: BoxFit.contain, + child: _buildSpeedDisplay(speedText, currentTheme, 160, isLandscape: true), ), ), ), @@ -317,11 +471,11 @@ class _SpeedometerScreenState extends State { child: GestureDetector( onTap: _cycleUnit, child: FittedBox( - fit: BoxFit.scaleDown, + fit: BoxFit.contain, child: Text( _currentUnit.label, style: TextStyle( - fontSize: 24, + fontSize: 50, color: currentTheme.unitText, fontFamily: 'DIN1451Alt', ), @@ -334,35 +488,41 @@ class _SpeedometerScreenState extends State { ), ), ), - // Compass area - smaller but fully tappable for theme cycling + // Compass area - smaller but fully tappable for floating window Expanded( - flex: 35, + flex: 33, child: GestureDetector( - onTap: _cycleTheme, + onTap: _showFloatingWindow, + onLongPress: _closeFloatingWindow, // Long press to close floating window child: Container( height: double.infinity, color: Colors.transparent, - padding: const EdgeInsets.all(16), + padding: const EdgeInsets.all(2), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Flexible( - flex: 2, - child: Icon( - Icons.navigation, - size: 32, - color: currentTheme.headingText, + flex: 3, + child: Transform.rotate( + angle: (_heading >= 0 && _heading < 360) ? (_heading * math.pi / 180.0) : 0, + child: FittedBox( + fit: BoxFit.contain, + child: Icon( + Icons.navigation, + size: 60, + color: currentTheme.headingText, + ), + ), ), ), - const SizedBox(height: 8), Flexible( flex: 1, child: FittedBox( - fit: BoxFit.scaleDown, + fit: BoxFit.contain, child: Text( GpsService.formatHeading(_heading), style: TextStyle( - fontSize: 18, + fontSize: 30, color: currentTheme.headingText, fontFamily: 'DIN1451Alt', ), @@ -377,4 +537,81 @@ class _SpeedometerScreenState extends State { ], ); } +} + +class SimpleOverlaySpeedometer extends StatelessWidget { + const SimpleOverlaySpeedometer({super.key}); + + @override + Widget build(BuildContext context) { + print("SimpleOverlaySpeedometer build() called"); + return Material( + color: Colors.transparent, + child: Container( + width: 200, + height: 120, + decoration: BoxDecoration( + color: Colors.red.withOpacity(0.9), // Highly visible red background + borderRadius: BorderRadius.circular(16), + border: Border.all(color: Colors.yellow, width: 3), // Bright yellow border + ), + child: Stack( + children: [ + // Test content - much more visible + Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text( + 'FLOATING', + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + const Text( + 'WINDOW', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Colors.yellow, + ), + ), + const Text( + 'WORKING!', + style: TextStyle( + fontSize: 12, + color: Colors.white, + ), + ), + ], + ), + ), + // Close button + Positioned( + top: 2, + right: 2, + child: GestureDetector( + onTap: () => FlutterOverlayWindow.closeOverlay(), + child: Container( + width: 20, + height: 20, + decoration: BoxDecoration( + color: Color.fromARGB(255, 255, 171, 16), // call the current background colour defined as in `color_themes.dart` + borderRadius: BorderRadius.circular(10), + ), + child: const Icon( + Icons.close, + color: Colors.white, + size: 12, + ), + ), + ), + ), + ], + ), + ), + ); + } } \ No newline at end of file diff --git a/pubspec.lock b/pubspec.lock index 1ad5014..497bdd7 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -134,6 +134,14 @@ packages: url: "https://pub.dev" source: hosted version: "6.0.0" + flutter_overlay_window: + dependency: "direct main" + description: + name: flutter_overlay_window + sha256: "17988420249da68e421d1b44b511e6113d3de7a9e52c3fbd6d99730508af4580" + url: "https://pub.dev" + source: hosted + version: "0.5.0" flutter_test: dependency: "direct dev" description: flutter diff --git a/pubspec.yaml b/pubspec.yaml index b334477..4bc70ca 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: gps_speedo description: "Speedo - A Flutter application." publish_to: 'none' -version: 2.0.0+1 +version: 2.1.0+2 environment: sdk: ^3.9.0 @@ -20,6 +20,9 @@ dependencies: # Keep screen awake during navigation wakelock_plus: ^1.2.8 + + # Floating window overlay capability + flutter_overlay_window: ^0.5.0 dev_dependencies: