Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e6ffb00b2a | |||
| b3e85983e7 | |||
| 89ce53bb1b | |||
| 56327c00ec | |||
| 20a7691ac2 |
@@ -26,9 +26,9 @@ class ColorThemes {
|
|||||||
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),
|
||||||
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),
|
||||||
),
|
),
|
||||||
@@ -39,6 +39,13 @@ class ColorThemes {
|
|||||||
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),
|
||||||
|
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
@@ -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)}';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+21
-4
@@ -5,6 +5,7 @@ import 'package:wakelock_plus/wakelock_plus.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());
|
||||||
@@ -91,7 +92,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 +125,17 @@ class _SpeedometerScreenState extends State<SpeedometerScreen> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String _getSpeedDisplayText(double displaySpeed) {
|
||||||
|
if (_speed < 1.0 && (_heading < 0.0 || _heading >= 360.0)) {
|
||||||
|
return '--';
|
||||||
|
}
|
||||||
|
return displaySpeed.toStringAsFixed(1);
|
||||||
|
}
|
||||||
|
|
||||||
@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 +165,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(12),
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
@@ -177,7 +191,7 @@ class _SpeedometerScreenState extends State<SpeedometerScreen> {
|
|||||||
child: Text(
|
child: Text(
|
||||||
speedText,
|
speedText,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 200,
|
fontSize: 160,
|
||||||
fontWeight: FontWeight.w300,
|
fontWeight: FontWeight.w300,
|
||||||
color: currentTheme.speedText,
|
color: currentTheme.speedText,
|
||||||
fontFamily: 'DIN1451Alt',
|
fontFamily: 'DIN1451Alt',
|
||||||
@@ -222,12 +236,15 @@ class _SpeedometerScreenState extends State<SpeedometerScreen> {
|
|||||||
children: [
|
children: [
|
||||||
Flexible(
|
Flexible(
|
||||||
flex: 2,
|
flex: 2,
|
||||||
|
child: Transform.rotate(
|
||||||
|
angle: (_heading >= 0 && _heading < 360) ? (_heading * math.pi / 180.0) : 0,
|
||||||
child: Icon(
|
child: Icon(
|
||||||
Icons.navigation,
|
Icons.navigation,
|
||||||
size: 100,
|
size: 100,
|
||||||
color: currentTheme.headingText,
|
color: currentTheme.headingText,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
Flexible(
|
Flexible(
|
||||||
flex: 1,
|
flex: 1,
|
||||||
child: FittedBox(
|
child: FittedBox(
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ dependencies:
|
|||||||
# Keep screen awake during navigation
|
# Keep screen awake during navigation
|
||||||
wakelock_plus: ^1.2.8
|
wakelock_plus: ^1.2.8
|
||||||
|
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
|
|||||||
Reference in New Issue
Block a user