7 Commits

Author SHA1 Message Date
Mikkeli Matlock 63ee536ef1 minor formatting update 2025-08-25 23:06:46 +09:00
Mikkeli Matlock 2c2267e5ae v2.1.0: Add floating window overlay functionality
Major Features:
- Floating overlay window with system-level persistence
- Split decimal display (main speed prominent, decimal smaller/faded)
- Optimized content sizing for maximum readability
- Navigation icon trigger for overlay activation
- Multiple close options (overlay button + long-press navigation)

Technical Implementation:
- Added flutter_overlay_window dependency
- Android SYSTEM_ALERT_WINDOW permissions
- Overlay service configuration in AndroidManifest
- Enhanced flex ratios for better space utilization
- BoxFit.contain for improved content scaling

UX Improvements:
- Streamlined overlay launch (removed permission prompts)
- Cleaner visual hierarchy with split speed display
- Responsive design for both portrait and landscape
- Professional overlay styling matching app theme

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-25 23:03:31 +09:00
Mikkeli Matlock e6ffb00b2a Updated speed displays 2025-08-25 07:41:49 +09:00
Mikkeli Matlock b3e85983e7 Changed GPS force update timeout to 5s 2025-08-25 07:30:35 +09:00
Mikkeli Matlock 89ce53bb1b Improve GPS performance and heading display
- Reduce GPS distanceFilter to 0 for faster updates
- Add timeLimit to ensure regular position updates
- Implement cached heading to avoid showing N/A when GPS heading unavailable
- Add speed display logic: show "--" when stationary with no heading data
- Add rotating navigation icon that points in heading direction
- Fix degree symbol in heading format display

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-25 07:26:20 +09:00
Mikkeli Matlock 56327c00ec Added Yuuparo light palette 2025-08-25 07:09:08 +09:00
Mikkeli Matlock 20a7691ac2 Cleaning up Kotlin messes 2025-08-24 23:52:49 +09:00
9 changed files with 383 additions and 76 deletions
+3 -1
View File
@@ -13,7 +13,9 @@
"Bash(cp:*)", "Bash(cp:*)",
"Bash(flutter:*)", "Bash(flutter:*)",
"Bash(powershell:*)", "Bash(powershell:*)",
"WebFetch(domain:docs.anthropic.com)" "WebFetch(domain:docs.anthropic.com)",
"WebSearch",
"WebFetch(domain:pub.dev)"
], ],
"defaultMode": "acceptEdits" "defaultMode": "acceptEdits"
}, },
+11 -3
View File
@@ -3,8 +3,8 @@
## Project Overview ## Project Overview
A customizable Flutter GPS speedometer app targeting Android devices, emphasizing modularity and user customization. A customizable Flutter GPS speedometer app targeting Android devices, emphasizing modularity and user customization.
## Current Status: v1.1 Complete  ## Current Status: v2.1.0 Complete 
**Enhanced GPS speedometer with improved user experience and customization options.** **GPS speedometer with floating window overlay functionality and optimized visual design.**
### Architecture Implemented ### Architecture Implemented
- **State Management:** Provider pattern - **State Management:** Provider pattern
@@ -31,9 +31,17 @@ Architecture: Provider + Consumer pattern for reactive state management
Permissions: Location services with graceful degradation Permissions: Location services with graceful degradation
``` ```
## Recent Updates (v1.1) ## Recent Updates (v2.1.0)
### ✅ Completed Features ### ✅ Completed Features
- [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] **Portrait/Landscape Support**: Automatic orientation detection implemented
- [x] **Interactive Gauge**: Click gauge face to switch digital/analog modes - [x] **Interactive Gauge**: Click gauge face to switch digital/analog modes
- [x] **Extended Speed Units**: Added knots support with quick selector buttons - [x] **Extended Speed Units**: Added knots support with quick selector buttons
+10 -2
View File
@@ -5,8 +5,10 @@
<!-- Optional: for background location access --> <!-- Optional: for background location access -->
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
<!-- TODO: Enable for display-over-apps functionality --> <!-- Overlay window permissions for floating speedometer -->
<!-- <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" /> --> <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
<application <application
android:label="Speedo" android:label="Speedo"
@@ -34,6 +36,12 @@
<category android:name="android.intent.category.LAUNCHER"/> <category android:name="android.intent.category.LAUNCHER"/>
</intent-filter> </intent-filter>
</activity> </activity>
<!-- Service for overlay window -->
<service
android:name="flutter.overlay.window.flutter_overlay_window.OverlayService"
android:exported="false" />
<!-- Don't delete the meta-data below. <!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java --> This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data <meta-data
+15 -2
View File
@@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
class ColorTheme { class ColorTheme {
final Color background; final Color background;
final Color speedText; final Color speedText;
final Color speedTextSub;
final Color headingText; final Color headingText;
final Color unitText; final Color unitText;
final String name; final String name;
@@ -10,6 +11,7 @@ class ColorTheme {
const ColorTheme({ const ColorTheme({
required this.background, required this.background,
required this.speedText, required this.speedText,
required this.speedTextSub,
required this.headingText, required this.headingText,
required this.unitText, required this.unitText,
required this.name, required this.name,
@@ -22,13 +24,15 @@ class ColorThemes {
name: 'Yuzuki', name: 'Yuzuki',
background: Color.fromARGB(255, 114, 85, 109), background: Color.fromARGB(255, 114, 85, 109),
speedText: Color.fromARGB(255, 246, 224, 255), speedText: Color.fromARGB(255, 246, 224, 255),
speedTextSub: Color.fromARGB(255, 245, 217, 245),
headingText: Color.fromARGB(255, 244, 201, 255), headingText: Color.fromARGB(255, 244, 201, 255),
unitText: Color.fromARGB(255, 244, 201, 255), unitText: Color.fromARGB(255, 244, 201, 255),
), ),
ColorTheme( ColorTheme(
name: 'Yuubari', name: 'Yuuparo',
background: Color.fromARGB(255, 85, 99, 112), background: Color.fromARGB(255, 85, 99, 112),
speedText: Color.fromARGB(255, 255, 166, 0), speedText: Color.fromARGB(255, 255, 171, 16),
speedTextSub: Color.fromARGB(255, 245, 204, 116),
headingText: Color.fromARGB(255, 228, 235, 238), headingText: Color.fromARGB(255, 228, 235, 238),
unitText: Color.fromARGB(255, 141, 196, 184), unitText: Color.fromARGB(255, 141, 196, 184),
), ),
@@ -36,9 +40,18 @@ class ColorThemes {
name: 'Yuzuki Light', name: 'Yuzuki Light',
background: Color.fromARGB(255, 246, 224, 255), background: Color.fromARGB(255, 246, 224, 255),
speedText: Color.fromARGB(255, 114, 85, 109), speedText: Color.fromARGB(255, 114, 85, 109),
speedTextSub: Color.fromARGB(255, 161, 138, 170),
headingText: Color.fromARGB(255, 140, 94, 151), headingText: Color.fromARGB(255, 140, 94, 151),
unitText: Color.fromARGB(255, 114, 85, 109), 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, 255, 157, 65),
unitText: Color.fromARGB(255, 98, 124, 118),
),
]; ];
static ColorTheme getTheme(int index) { static ColorTheme getTheme(int index) {
+15 -5
View File
@@ -3,11 +3,13 @@ import 'package:geolocator/geolocator.dart';
import 'package:permission_handler/permission_handler.dart'; import 'package:permission_handler/permission_handler.dart';
class GpsService { class GpsService {
static double? _lastValidHeading;
static Stream<Position> get positionStream { static Stream<Position> get positionStream {
return Geolocator.getPositionStream( return Geolocator.getPositionStream(
locationSettings: const LocationSettings( locationSettings: const LocationSettings(
accuracy: LocationAccuracy.bestForNavigation, accuracy: LocationAccuracy.bestForNavigation,
distanceFilter: 1, distanceFilter: 0,
), ),
); );
} }
@@ -21,16 +23,24 @@ class GpsService {
return await Geolocator.isLocationServiceEnabled(); return await Geolocator.isLocationServiceEnabled();
} }
static double getCachedHeading(double heading) {
if (heading >= 0 && heading <= 360) {
_lastValidHeading = heading;
return heading;
}
return _lastValidHeading ?? 0.0;
}
static String getCompassDirection(double heading) { static String getCompassDirection(double heading) {
if (heading < 0) return 'N/A'; final cachedHeading = getCachedHeading(heading);
const directions = ['N', 'NE', 'E', 'SE', 'S', 'SW', 'W', 'NW']; const directions = ['N', 'NE', 'E', 'SE', 'S', 'SW', 'W', 'NW'];
final index = ((heading + 22.5) / 45).floor() % 8; final index = ((cachedHeading + 22.5) / 45).floor() % 8;
return directions[index]; return directions[index];
} }
static String formatHeading(double heading) { static String formatHeading(double heading) {
if (heading < 0) return 'N/A'; final cachedHeading = getCachedHeading(heading);
return '${heading.round()} ${getCompassDirection(heading)}'; return '${cachedHeading.round()} ${getCompassDirection(heading)}'; // don't add that degree symbol, this is cooler
} }
} }
+309 -55
View File
@@ -1,19 +1,73 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'dart:async'; import 'dart:async';
import 'package:geolocator/geolocator.dart'; import 'package:geolocator/geolocator.dart';
import 'package:wakelock_plus/wakelock_plus.dart'; import 'package:wakelock_plus/wakelock_plus.dart';
import 'package:flutter_overlay_window/flutter_overlay_window.dart';
import 'speed_units.dart'; import 'speed_units.dart';
import 'gps_service.dart'; import 'gps_service.dart';
import 'color_themes.dart'; import 'color_themes.dart';
import 'dart:math' as math; // Import for math.pi
void main() { void main() {
runApp(const SpeedoApp()); runApp(const SpeedoApp());
} }
// Entry point for future overlay mode // Entry point for overlay window
void mainOverlay() { @pragma("vm:entry-point")
// TODO: Initialize overlay-specific setup void overlayMain() {
runApp(const SpeedoApp(isOverlayMode: true)); runApp(MaterialApp(
debugShowCheckedModeBanner: false,
home: Scaffold(
backgroundColor: Colors.transparent,
body: Container(
width: 180,
height: 100,
decoration: BoxDecoration(
color: Colors.black.withOpacity(0.8),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.green.withOpacity(0.7), width: 2),
),
child: Stack(
children: [
const Center(
child: Text(
'GPS\nSPEEDO',
textAlign: TextAlign.center,
style: TextStyle(
color: Colors.green,
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
),
// Close button
Positioned(
top: 4,
right: 4,
child: GestureDetector(
onTap: () => 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 { class SpeedoApp extends StatelessWidget {
@@ -45,17 +99,19 @@ class SpeedometerScreen extends StatefulWidget {
State<SpeedometerScreen> createState() => _SpeedometerScreenState(); State<SpeedometerScreen> createState() => _SpeedometerScreenState();
} }
class _SpeedometerScreenState extends State<SpeedometerScreen> { class _SpeedometerScreenState extends State<SpeedometerScreen> with WidgetsBindingObserver {
double _speed = 0.0; double _speed = 0.0;
double _heading = -1.0; double _heading = -1.0;
SpeedUnit _currentUnit = SpeedUnit.kmh; SpeedUnit _currentUnit = SpeedUnit.kmh;
int _currentThemeIndex = 0; int _currentThemeIndex = 0;
StreamSubscription<Position>? _positionSubscription; StreamSubscription<Position>? _positionSubscription;
String _errorMessage = ''; String _errorMessage = '';
bool _isInBackground = false;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
WidgetsBinding.instance.addObserver(this);
_initializeGps(); _initializeGps();
_enableWakelock(); _enableWakelock();
} }
@@ -70,11 +126,45 @@ class _SpeedometerScreenState extends State<SpeedometerScreen> {
@override @override
void dispose() { void dispose() {
WidgetsBinding.instance.removeObserver(this);
_positionSubscription?.cancel(); _positionSubscription?.cancel();
WakelockPlus.disable(); WakelockPlus.disable();
super.dispose(); 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<void> _initializeGps() async { Future<void> _initializeGps() async {
try { try {
final serviceEnabled = await GpsService.isLocationServiceEnabled(); final serviceEnabled = await GpsService.isLocationServiceEnabled();
@@ -91,7 +181,13 @@ class _SpeedometerScreenState extends State<SpeedometerScreen> {
_positionSubscription = GpsService.positionStream.listen( _positionSubscription = GpsService.positionStream.listen(
_onPositionUpdate, _onPositionUpdate,
onError: (error) => setState(() => _errorMessage = 'GPS error: $error'), onError: (error) {
// Only show persistent GPS errors, not temporary signal issues
final errorString = error.toString().toLowerCase();
if (!errorString.contains('timeout') && !errorString.contains('temporarily')) {
setState(() => _errorMessage = 'GPS error: $error');
}
},
); );
} catch (e) { } catch (e) {
setState(() => _errorMessage = 'Failed to initialize GPS'); setState(() => _errorMessage = 'Failed to initialize GPS');
@@ -118,10 +214,94 @@ class _SpeedometerScreenState extends State<SpeedometerScreen> {
}); });
} }
Future<void> _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<void> _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)) {
return '--';
}
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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final displaySpeed = _currentUnit.convert(_speed); final displaySpeed = _currentUnit.convert(_speed);
final speedText = displaySpeed.toStringAsFixed(0); final speedText = _getSpeedDisplayText(displaySpeed);
final currentTheme = ColorThemes.getTheme(_currentThemeIndex); final currentTheme = ColorThemes.getTheme(_currentThemeIndex);
return Scaffold( return Scaffold(
@@ -151,7 +331,7 @@ class _SpeedometerScreenState extends State<SpeedometerScreen> {
child: Container( child: Container(
width: double.infinity, width: double.infinity,
color: Colors.transparent, color: Colors.transparent,
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(4),
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
@@ -169,33 +349,25 @@ class _SpeedometerScreenState extends State<SpeedometerScreen> {
) )
else ...[ else ...[
Flexible( Flexible(
flex: 3, flex: 4,
child: GestureDetector( child: GestureDetector(
onTap: _cycleUnit, onTap: _cycleTheme, // Tap speed to cycle theme
child: FittedBox( child: FittedBox(
fit: BoxFit.scaleDown, fit: BoxFit.contain,
child: Text( child: _buildSpeedDisplay(speedText, currentTheme, 200),
speedText,
style: TextStyle(
fontSize: 200,
fontWeight: FontWeight.w300,
color: currentTheme.speedText,
fontFamily: 'DIN1451Alt',
),
),
), ),
), ),
), ),
Flexible( Flexible(
flex: 1, flex: 1,
child: GestureDetector( child: GestureDetector(
onTap: _cycleUnit, onTap: _cycleUnit, // clicking on unit to cycle unit is more intuitive
child: FittedBox( child: FittedBox(
fit: BoxFit.scaleDown, fit: BoxFit.contain,
child: Text( child: Text(
_currentUnit.label, _currentUnit.label,
style: TextStyle( style: TextStyle(
fontSize: 64, fontSize: 80,
color: currentTheme.unitText, color: currentTheme.unitText,
fontFamily: 'DIN1451Alt', fontFamily: 'DIN1451Alt',
), ),
@@ -208,34 +380,41 @@ class _SpeedometerScreenState extends State<SpeedometerScreen> {
), ),
), ),
), ),
// Compass area - compact but fully tappable for theme cycling // Compass area - compact but fully tappable for floating window
Expanded( Expanded(
flex: 3, flex: 3,
child: GestureDetector( child: GestureDetector(
onTap: _cycleTheme, onTap: _showFloatingWindow, // Tap navigation icon to show floating window
onLongPress: _closeFloatingWindow, // Long press to close floating window
child: Container( child: Container(
width: double.infinity, width: double.infinity,
color: Colors.transparent, color: Colors.transparent,
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(2),
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Flexible( Flexible(
flex: 2, flex: 3,
child: Transform.rotate(
angle: (_heading >= 0 && _heading < 360) ? (_heading * math.pi / 180.0) : 0,
child: FittedBox(
fit: BoxFit.contain,
child: Icon( child: Icon(
Icons.navigation, Icons.navigation,
size: 100, size: 80,
color: currentTheme.headingText, color: currentTheme.headingText,
), ),
), ),
),
),
Flexible( Flexible(
flex: 1, flex: 1,
child: FittedBox( child: FittedBox(
fit: BoxFit.scaleDown, fit: BoxFit.contain,
child: Text( child: Text(
GpsService.formatHeading(_heading), GpsService.formatHeading(_heading),
style: TextStyle( style: TextStyle(
fontSize: 96, fontSize: 50,
color: currentTheme.headingText, color: currentTheme.headingText,
fontFamily: 'DIN1451Alt', fontFamily: 'DIN1451Alt',
), ),
@@ -256,11 +435,11 @@ class _SpeedometerScreenState extends State<SpeedometerScreen> {
children: [ children: [
// Speed area - takes majority of space, precise tap targets on text only // Speed area - takes majority of space, precise tap targets on text only
Expanded( Expanded(
flex: 65, flex: 66,
child: Container( child: Container(
height: double.infinity, height: double.infinity,
color: Colors.transparent, color: Colors.transparent,
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(3),
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
@@ -278,20 +457,12 @@ class _SpeedometerScreenState extends State<SpeedometerScreen> {
) )
else ...[ else ...[
Flexible( Flexible(
flex: 3, flex: 4,
child: GestureDetector( child: GestureDetector(
onTap: _cycleUnit, onTap: _cycleTheme,
child: FittedBox( child: FittedBox(
fit: BoxFit.scaleDown, fit: BoxFit.contain,
child: Text( child: _buildSpeedDisplay(speedText, currentTheme, 160, isLandscape: true),
speedText,
style: TextStyle(
fontSize: 120,
fontWeight: FontWeight.w300,
color: currentTheme.speedText,
fontFamily: 'DIN1451Alt',
),
),
), ),
), ),
), ),
@@ -300,11 +471,11 @@ class _SpeedometerScreenState extends State<SpeedometerScreen> {
child: GestureDetector( child: GestureDetector(
onTap: _cycleUnit, onTap: _cycleUnit,
child: FittedBox( child: FittedBox(
fit: BoxFit.scaleDown, fit: BoxFit.contain,
child: Text( child: Text(
_currentUnit.label, _currentUnit.label,
style: TextStyle( style: TextStyle(
fontSize: 24, fontSize: 50,
color: currentTheme.unitText, color: currentTheme.unitText,
fontFamily: 'DIN1451Alt', fontFamily: 'DIN1451Alt',
), ),
@@ -317,35 +488,41 @@ class _SpeedometerScreenState extends State<SpeedometerScreen> {
), ),
), ),
), ),
// Compass area - smaller but fully tappable for theme cycling // Compass area - smaller but fully tappable for floating window
Expanded( Expanded(
flex: 35, flex: 33,
child: GestureDetector( child: GestureDetector(
onTap: _cycleTheme, onTap: _showFloatingWindow,
onLongPress: _closeFloatingWindow, // Long press to close floating window
child: Container( child: Container(
height: double.infinity, height: double.infinity,
color: Colors.transparent, color: Colors.transparent,
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(2),
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Flexible( Flexible(
flex: 2, flex: 3,
child: Transform.rotate(
angle: (_heading >= 0 && _heading < 360) ? (_heading * math.pi / 180.0) : 0,
child: FittedBox(
fit: BoxFit.contain,
child: Icon( child: Icon(
Icons.navigation, Icons.navigation,
size: 32, size: 60,
color: currentTheme.headingText, color: currentTheme.headingText,
), ),
), ),
const SizedBox(height: 8), ),
),
Flexible( Flexible(
flex: 1, flex: 1,
child: FittedBox( child: FittedBox(
fit: BoxFit.scaleDown, fit: BoxFit.contain,
child: Text( child: Text(
GpsService.formatHeading(_heading), GpsService.formatHeading(_heading),
style: TextStyle( style: TextStyle(
fontSize: 18, fontSize: 30,
color: currentTheme.headingText, color: currentTheme.headingText,
fontFamily: 'DIN1451Alt', fontFamily: 'DIN1451Alt',
), ),
@@ -361,3 +538,80 @@ class _SpeedometerScreenState extends State<SpeedometerScreen> {
); );
} }
} }
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,
),
),
),
),
],
),
),
);
}
}
+8
View File
@@ -134,6 +134,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "6.0.0" 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: flutter_test:
dependency: "direct dev" dependency: "direct dev"
description: flutter description: flutter
+5 -1
View File
@@ -2,7 +2,7 @@ name: gps_speedo
description: "Speedo - A Flutter application." description: "Speedo - A Flutter application."
publish_to: 'none' publish_to: 'none'
version: 2.0.0+1 version: 2.1.0+2
environment: environment:
sdk: ^3.9.0 sdk: ^3.9.0
@@ -21,6 +21,10 @@ dependencies:
# Keep screen awake during navigation # Keep screen awake during navigation
wakelock_plus: ^1.2.8 wakelock_plus: ^1.2.8
# Floating window overlay capability
flutter_overlay_window: ^0.5.0
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
sdk: flutter sdk: flutter