Implement complete GPS speedometer functionality
Features implemented: - Real-time GPS speed tracking with permission handling - Customizable speed units (km/h, mph) with persistent settings - Digital and analog speedometer display styles - Dark/light theme support with system integration - Optional metrics: coordinates, distance, trip time, GPS accuracy - Trip reset functionality and error handling - Modular architecture with Provider state management Technical details: - Added dependencies: geolocator, permission_handler, provider, shared_preferences - Configured Android location permissions (FINE_LOCATION, COARSE_LOCATION) - Portrait orientation lock for mobile-first experience - Extensible settings system for future customization options - Clean separation of concerns: providers, screens, widgets 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,136 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:geolocator/geolocator.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
|
||||
class LocationProvider extends ChangeNotifier {
|
||||
Position? _currentPosition;
|
||||
double _currentSpeed = 0.0;
|
||||
double _totalDistance = 0.0;
|
||||
DateTime? _tripStartTime;
|
||||
bool _isTracking = false;
|
||||
bool _hasLocationPermission = false;
|
||||
String _errorMessage = '';
|
||||
StreamSubscription<Position>? _positionStreamSubscription;
|
||||
Position? _previousPosition;
|
||||
|
||||
Position? get currentPosition => _currentPosition;
|
||||
double get currentSpeed => _currentSpeed;
|
||||
double get totalDistance => _totalDistance;
|
||||
DateTime? get tripStartTime => _tripStartTime;
|
||||
bool get isTracking => _isTracking;
|
||||
bool get hasLocationPermission => _hasLocationPermission;
|
||||
String get errorMessage => _errorMessage;
|
||||
|
||||
double? get latitude => _currentPosition?.latitude;
|
||||
double? get longitude => _currentPosition?.longitude;
|
||||
double? get accuracy => _currentPosition?.accuracy;
|
||||
|
||||
Duration get tripDuration {
|
||||
if (_tripStartTime == null) return Duration.zero;
|
||||
return DateTime.now().difference(_tripStartTime!);
|
||||
}
|
||||
|
||||
Future<void> initializeLocation() async {
|
||||
await _checkLocationPermission();
|
||||
if (_hasLocationPermission) {
|
||||
await _startLocationTracking();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _checkLocationPermission() async {
|
||||
final permission = await Permission.locationWhenInUse.status;
|
||||
|
||||
if (permission.isDenied) {
|
||||
final result = await Permission.locationWhenInUse.request();
|
||||
_hasLocationPermission = result.isGranted;
|
||||
} else if (permission.isPermanentlyDenied) {
|
||||
_hasLocationPermission = false;
|
||||
_errorMessage = 'Location permission is permanently denied. Please enable it in settings.';
|
||||
} else {
|
||||
_hasLocationPermission = permission.isGranted;
|
||||
}
|
||||
|
||||
if (!_hasLocationPermission) {
|
||||
_errorMessage = 'Location permission is required for the speedometer to work.';
|
||||
} else {
|
||||
_errorMessage = '';
|
||||
}
|
||||
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> _startLocationTracking() async {
|
||||
if (!_hasLocationPermission) return;
|
||||
|
||||
try {
|
||||
final isLocationServiceEnabled = await Geolocator.isLocationServiceEnabled();
|
||||
if (!isLocationServiceEnabled) {
|
||||
_errorMessage = 'Location services are disabled. Please enable them.';
|
||||
notifyListeners();
|
||||
return;
|
||||
}
|
||||
|
||||
const locationSettings = LocationSettings(
|
||||
accuracy: LocationAccuracy.bestForNavigation,
|
||||
distanceFilter: 0,
|
||||
);
|
||||
|
||||
_positionStreamSubscription = Geolocator.getPositionStream(
|
||||
locationSettings: locationSettings,
|
||||
).listen(
|
||||
_onLocationUpdate,
|
||||
onError: (error) {
|
||||
_errorMessage = 'Location error: $error';
|
||||
notifyListeners();
|
||||
},
|
||||
);
|
||||
|
||||
_isTracking = true;
|
||||
_tripStartTime = DateTime.now();
|
||||
notifyListeners();
|
||||
} catch (e) {
|
||||
_errorMessage = 'Failed to start location tracking: $e';
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
void _onLocationUpdate(Position position) {
|
||||
_currentPosition = position;
|
||||
_currentSpeed = position.speed;
|
||||
|
||||
if (_previousPosition != null) {
|
||||
final distance = Geolocator.distanceBetween(
|
||||
_previousPosition!.latitude,
|
||||
_previousPosition!.longitude,
|
||||
position.latitude,
|
||||
position.longitude,
|
||||
);
|
||||
_totalDistance += distance;
|
||||
}
|
||||
|
||||
_previousPosition = position;
|
||||
_errorMessage = '';
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void resetTrip() {
|
||||
_totalDistance = 0.0;
|
||||
_tripStartTime = DateTime.now();
|
||||
_previousPosition = _currentPosition;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> stopTracking() async {
|
||||
await _positionStreamSubscription?.cancel();
|
||||
_positionStreamSubscription = null;
|
||||
_isTracking = false;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
stopTracking();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
enum SpeedUnit { kmh, mph }
|
||||
enum SpeedometerStyle { digital, analog }
|
||||
|
||||
class SettingsProvider extends ChangeNotifier {
|
||||
bool _isDarkMode = false;
|
||||
SpeedUnit _speedUnit = SpeedUnit.kmh;
|
||||
SpeedometerStyle _speedometerStyle = SpeedometerStyle.digital;
|
||||
bool _showCoordinates = false;
|
||||
bool _showDistance = false;
|
||||
bool _showTripTime = false;
|
||||
bool _showAccuracy = false;
|
||||
|
||||
bool get isDarkMode => _isDarkMode;
|
||||
SpeedUnit get speedUnit => _speedUnit;
|
||||
SpeedometerStyle get speedometerStyle => _speedometerStyle;
|
||||
bool get showCoordinates => _showCoordinates;
|
||||
bool get showDistance => _showDistance;
|
||||
bool get showTripTime => _showTripTime;
|
||||
bool get showAccuracy => _showAccuracy;
|
||||
|
||||
String get speedUnitString => _speedUnit == SpeedUnit.kmh ? 'km/h' : 'mph';
|
||||
|
||||
SettingsProvider() {
|
||||
_loadSettings();
|
||||
}
|
||||
|
||||
Future<void> _loadSettings() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
_isDarkMode = prefs.getBool('isDarkMode') ?? false;
|
||||
_speedUnit = SpeedUnit.values[prefs.getInt('speedUnit') ?? 0];
|
||||
_speedometerStyle = SpeedometerStyle.values[prefs.getInt('speedometerStyle') ?? 0];
|
||||
_showCoordinates = prefs.getBool('showCoordinates') ?? false;
|
||||
_showDistance = prefs.getBool('showDistance') ?? false;
|
||||
_showTripTime = prefs.getBool('showTripTime') ?? false;
|
||||
_showAccuracy = prefs.getBool('showAccuracy') ?? false;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> setDarkMode(bool value) async {
|
||||
_isDarkMode = value;
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setBool('isDarkMode', value);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> setSpeedUnit(SpeedUnit unit) async {
|
||||
_speedUnit = unit;
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setInt('speedUnit', unit.index);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> setSpeedometerStyle(SpeedometerStyle style) async {
|
||||
_speedometerStyle = style;
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setInt('speedometerStyle', style.index);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> setShowCoordinates(bool value) async {
|
||||
_showCoordinates = value;
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setBool('showCoordinates', value);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> setShowDistance(bool value) async {
|
||||
_showDistance = value;
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setBool('showDistance', value);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> setShowTripTime(bool value) async {
|
||||
_showTripTime = value;
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setBool('showTripTime', value);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> setShowAccuracy(bool value) async {
|
||||
_showAccuracy = value;
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setBool('showAccuracy', value);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
double convertSpeed(double speedMps) {
|
||||
if (_speedUnit == SpeedUnit.kmh) {
|
||||
return speedMps * 3.6;
|
||||
} else {
|
||||
return speedMps * 2.237;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user