Complete v2.0 ultra-lightweight rebuild with premium color themes
🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'providers/location_provider.dart';
|
||||
import 'providers/settings_provider.dart';
|
||||
import 'screens/speedometer_screen.dart';
|
||||
|
||||
void main() {
|
||||
runApp(const SpeedometerApp());
|
||||
}
|
||||
|
||||
class SpeedometerApp extends StatelessWidget {
|
||||
const SpeedometerApp({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
SystemChrome.setPreferredOrientations([
|
||||
DeviceOrientation.portraitUp,
|
||||
DeviceOrientation.portraitDown,
|
||||
DeviceOrientation.landscapeLeft,
|
||||
DeviceOrientation.landscapeRight,
|
||||
]);
|
||||
|
||||
return MultiProvider(
|
||||
providers: [
|
||||
ChangeNotifierProvider(create: (_) => SettingsProvider()),
|
||||
ChangeNotifierProvider(create: (_) => LocationProvider()),
|
||||
],
|
||||
child: Consumer<SettingsProvider>(
|
||||
builder: (context, settings, child) {
|
||||
return MaterialApp(
|
||||
title: 'GPS Speedometer',
|
||||
debugShowCheckedModeBanner: false,
|
||||
theme: _buildTheme(settings.isDarkMode),
|
||||
home: const SpeedometerScreen(),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
ThemeData _buildTheme(bool isDarkMode) {
|
||||
return ThemeData(
|
||||
useMaterial3: true,
|
||||
brightness: isDarkMode ? Brightness.dark : Brightness.light,
|
||||
colorScheme: ColorScheme.fromSeed(
|
||||
seedColor: Colors.blue,
|
||||
brightness: isDarkMode ? Brightness.dark : Brightness.light,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,110 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
enum SpeedUnit { kmh, mph, knots }
|
||||
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 {
|
||||
switch (_speedUnit) {
|
||||
case SpeedUnit.kmh:
|
||||
return 'km/h';
|
||||
case SpeedUnit.mph:
|
||||
return 'mph';
|
||||
case SpeedUnit.knots:
|
||||
return 'knots';
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
switch (_speedUnit) {
|
||||
case SpeedUnit.kmh:
|
||||
return speedMps * 3.6;
|
||||
case SpeedUnit.mph:
|
||||
return speedMps * 2.237;
|
||||
case SpeedUnit.knots:
|
||||
return speedMps * 1.944;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../providers/settings_provider.dart';
|
||||
|
||||
class SettingsScreen extends StatelessWidget {
|
||||
const SettingsScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Settings'),
|
||||
),
|
||||
body: Consumer<SettingsProvider>(
|
||||
builder: (context, settings, child) {
|
||||
return ListView(
|
||||
children: [
|
||||
_buildSectionHeader(context, 'Display'),
|
||||
SwitchListTile(
|
||||
title: const Text('Dark Mode'),
|
||||
subtitle: const Text('Switch between light and dark themes'),
|
||||
value: settings.isDarkMode,
|
||||
onChanged: settings.setDarkMode,
|
||||
),
|
||||
|
||||
ListTile(
|
||||
title: const Text('Speed Unit'),
|
||||
subtitle: Text('Current: ${settings.speedUnitString}'),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
onTap: () => _showSpeedUnitDialog(context, settings),
|
||||
),
|
||||
|
||||
ListTile(
|
||||
title: const Text('Speedometer Style'),
|
||||
subtitle: Text(settings.speedometerStyle == SpeedometerStyle.digital
|
||||
? 'Digital Display'
|
||||
: 'Analog Gauge'),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
onTap: () => _showSpeedometerStyleDialog(context, settings),
|
||||
),
|
||||
|
||||
const Divider(),
|
||||
_buildSectionHeader(context, 'Information Display'),
|
||||
|
||||
SwitchListTile(
|
||||
title: const Text('Show Coordinates'),
|
||||
subtitle: const Text('Display current latitude and longitude'),
|
||||
value: settings.showCoordinates,
|
||||
onChanged: settings.setShowCoordinates,
|
||||
),
|
||||
|
||||
SwitchListTile(
|
||||
title: const Text('Show Distance'),
|
||||
subtitle: const Text('Display total trip distance'),
|
||||
value: settings.showDistance,
|
||||
onChanged: settings.setShowDistance,
|
||||
),
|
||||
|
||||
SwitchListTile(
|
||||
title: const Text('Show Trip Time'),
|
||||
subtitle: const Text('Display elapsed trip time'),
|
||||
value: settings.showTripTime,
|
||||
onChanged: settings.setShowTripTime,
|
||||
),
|
||||
|
||||
SwitchListTile(
|
||||
title: const Text('Show GPS Accuracy'),
|
||||
subtitle: const Text('Display current GPS accuracy'),
|
||||
value: settings.showAccuracy,
|
||||
onChanged: settings.setShowAccuracy,
|
||||
),
|
||||
|
||||
const Divider(),
|
||||
_buildSectionHeader(context, 'About'),
|
||||
|
||||
ListTile(
|
||||
title: const Text('GPS Speedometer'),
|
||||
subtitle: const Text('Version 1.0.0\nCreated with Claude Code'),
|
||||
leading: const Icon(Icons.info_outline),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSectionHeader(BuildContext context, String title) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 16, 16, 8),
|
||||
child: Text(
|
||||
title,
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showSpeedUnitDialog(BuildContext context, SettingsProvider settings) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Speed Unit'),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
RadioListTile<SpeedUnit>(
|
||||
title: const Text('Kilometers per hour (km/h)'),
|
||||
value: SpeedUnit.kmh,
|
||||
groupValue: settings.speedUnit,
|
||||
onChanged: (value) {
|
||||
if (value != null) {
|
||||
settings.setSpeedUnit(value);
|
||||
Navigator.pop(context);
|
||||
}
|
||||
},
|
||||
),
|
||||
RadioListTile<SpeedUnit>(
|
||||
title: const Text('Miles per hour (mph)'),
|
||||
value: SpeedUnit.mph,
|
||||
groupValue: settings.speedUnit,
|
||||
onChanged: (value) {
|
||||
if (value != null) {
|
||||
settings.setSpeedUnit(value);
|
||||
Navigator.pop(context);
|
||||
}
|
||||
},
|
||||
),
|
||||
RadioListTile<SpeedUnit>(
|
||||
title: const Text('Knots'),
|
||||
value: SpeedUnit.knots,
|
||||
groupValue: settings.speedUnit,
|
||||
onChanged: (value) {
|
||||
if (value != null) {
|
||||
settings.setSpeedUnit(value);
|
||||
Navigator.pop(context);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showSpeedometerStyleDialog(BuildContext context, SettingsProvider settings) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Speedometer Style'),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
RadioListTile<SpeedometerStyle>(
|
||||
title: const Text('Digital Display'),
|
||||
subtitle: const Text('Clean numeric display'),
|
||||
value: SpeedometerStyle.digital,
|
||||
groupValue: settings.speedometerStyle,
|
||||
onChanged: (value) {
|
||||
if (value != null) {
|
||||
settings.setSpeedometerStyle(value);
|
||||
Navigator.pop(context);
|
||||
}
|
||||
},
|
||||
),
|
||||
RadioListTile<SpeedometerStyle>(
|
||||
title: const Text('Analog Gauge'),
|
||||
subtitle: const Text('Classic circular speedometer'),
|
||||
value: SpeedometerStyle.analog,
|
||||
groupValue: settings.speedometerStyle,
|
||||
onChanged: (value) {
|
||||
if (value != null) {
|
||||
settings.setSpeedometerStyle(value);
|
||||
Navigator.pop(context);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../providers/location_provider.dart';
|
||||
import '../providers/settings_provider.dart';
|
||||
import '../widgets/digital_speedometer.dart';
|
||||
import '../widgets/analog_speedometer.dart';
|
||||
import '../widgets/info_panel.dart';
|
||||
import '../widgets/speed_unit_selector.dart';
|
||||
import 'settings_screen.dart';
|
||||
|
||||
class SpeedometerScreen extends StatefulWidget {
|
||||
const SpeedometerScreen({super.key});
|
||||
|
||||
@override
|
||||
State<SpeedometerScreen> createState() => _SpeedometerScreenState();
|
||||
}
|
||||
|
||||
class _SpeedometerScreenState extends State<SpeedometerScreen> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
context.read<LocationProvider>().initializeLocation();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('GPS Speedometer'),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.refresh),
|
||||
onPressed: () {
|
||||
context.read<LocationProvider>().resetTrip();
|
||||
},
|
||||
tooltip: 'Reset Trip',
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.settings),
|
||||
onPressed: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (context) => const SettingsScreen()),
|
||||
);
|
||||
},
|
||||
tooltip: 'Settings',
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Consumer2<LocationProvider, SettingsProvider>(
|
||||
builder: (context, locationProvider, settingsProvider, child) {
|
||||
if (!locationProvider.hasLocationPermission) {
|
||||
return _buildPermissionError(locationProvider.errorMessage);
|
||||
}
|
||||
|
||||
if (locationProvider.errorMessage.isNotEmpty) {
|
||||
return _buildError(locationProvider.errorMessage);
|
||||
}
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 3,
|
||||
child: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _buildSpeedometer(locationProvider, settingsProvider),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 8.0),
|
||||
child: const SpeedUnitSelector(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: InfoPanel(
|
||||
locationProvider: locationProvider,
|
||||
settingsProvider: settingsProvider,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSpeedometer(LocationProvider locationProvider, SettingsProvider settingsProvider) {
|
||||
final speed = settingsProvider.convertSpeed(locationProvider.currentSpeed);
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
final newStyle = settingsProvider.speedometerStyle == SpeedometerStyle.digital
|
||||
? SpeedometerStyle.analog
|
||||
: SpeedometerStyle.digital;
|
||||
settingsProvider.setSpeedometerStyle(newStyle);
|
||||
},
|
||||
child: settingsProvider.speedometerStyle == SpeedometerStyle.digital
|
||||
? DigitalSpeedometer(
|
||||
speed: speed,
|
||||
unit: settingsProvider.speedUnitString,
|
||||
)
|
||||
: AnalogSpeedometer(
|
||||
speed: speed,
|
||||
unit: settingsProvider.speedUnitString,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPermissionError(String message) {
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.location_off,
|
||||
size: 64,
|
||||
color: Colors.red,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Location Permission Required',
|
||||
style: Theme.of(context).textTheme.headlineSmall,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
message,
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
context.read<LocationProvider>().initializeLocation();
|
||||
},
|
||||
child: const Text('Retry'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildError(String message) {
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.error_outline,
|
||||
size: 64,
|
||||
color: Colors.orange,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'GPS Error',
|
||||
style: Theme.of(context).textTheme.headlineSmall,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
message,
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
context.read<LocationProvider>().initializeLocation();
|
||||
},
|
||||
child: const Text('Retry'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
import 'dart:math';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class AnalogSpeedometer extends StatelessWidget {
|
||||
final double speed;
|
||||
final String unit;
|
||||
|
||||
const AnalogSpeedometer({
|
||||
super.key,
|
||||
required this.speed,
|
||||
required this.unit,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 250,
|
||||
height: 250,
|
||||
child: CustomPaint(
|
||||
painter: SpeedometerPainter(
|
||||
speed: speed,
|
||||
theme: Theme.of(context),
|
||||
),
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const SizedBox(height: 40),
|
||||
Text(
|
||||
speed.toStringAsFixed(0),
|
||||
style: const TextStyle(
|
||||
fontSize: 36,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontFeatures: [FontFeature.tabularFigures()],
|
||||
),
|
||||
),
|
||||
Text(
|
||||
unit,
|
||||
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SpeedometerPainter extends CustomPainter {
|
||||
final double speed;
|
||||
final ThemeData theme;
|
||||
static const double maxSpeed = 120.0;
|
||||
|
||||
const SpeedometerPainter({
|
||||
required this.speed,
|
||||
required this.theme,
|
||||
});
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final center = Offset(size.width / 2, size.height / 2);
|
||||
final radius = size.width / 2 - 20;
|
||||
|
||||
_drawSpeedometerArc(canvas, center, radius);
|
||||
_drawTicks(canvas, center, radius);
|
||||
_drawNeedle(canvas, center, radius);
|
||||
_drawCenterCircle(canvas, center);
|
||||
}
|
||||
|
||||
void _drawSpeedometerArc(Canvas canvas, Offset center, double radius) {
|
||||
final paint = Paint()
|
||||
..color = theme.colorScheme.outline.withOpacity(0.3)
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = 8
|
||||
..strokeCap = StrokeCap.round;
|
||||
|
||||
const startAngle = pi * 0.75;
|
||||
const sweepAngle = pi * 1.5;
|
||||
|
||||
canvas.drawArc(
|
||||
Rect.fromCircle(center: center, radius: radius),
|
||||
startAngle,
|
||||
sweepAngle,
|
||||
false,
|
||||
paint,
|
||||
);
|
||||
|
||||
final progressPaint = Paint()
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = 8
|
||||
..strokeCap = StrokeCap.round;
|
||||
|
||||
final progress = (speed / maxSpeed).clamp(0.0, 1.0);
|
||||
final progressSweep = sweepAngle * progress;
|
||||
|
||||
if (progress > 0) {
|
||||
progressPaint.color = _getSpeedColor(speed);
|
||||
canvas.drawArc(
|
||||
Rect.fromCircle(center: center, radius: radius),
|
||||
startAngle,
|
||||
progressSweep,
|
||||
false,
|
||||
progressPaint,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _drawTicks(Canvas canvas, Offset center, double radius) {
|
||||
final paint = Paint()
|
||||
..color = theme.colorScheme.onSurface
|
||||
..strokeWidth = 2;
|
||||
|
||||
const tickCount = 13;
|
||||
const startAngle = pi * 0.75;
|
||||
const sweepAngle = pi * 1.5;
|
||||
|
||||
for (int i = 0; i < tickCount; i++) {
|
||||
final angle = startAngle + (sweepAngle * i / (tickCount - 1));
|
||||
final isMainTick = i % 3 == 0;
|
||||
|
||||
final outerRadius = radius - 5;
|
||||
final innerRadius = outerRadius - (isMainTick ? 15 : 8);
|
||||
|
||||
final outerPoint = Offset(
|
||||
center.dx + cos(angle) * outerRadius,
|
||||
center.dy + sin(angle) * outerRadius,
|
||||
);
|
||||
|
||||
final innerPoint = Offset(
|
||||
center.dx + cos(angle) * innerRadius,
|
||||
center.dy + sin(angle) * innerRadius,
|
||||
);
|
||||
|
||||
paint.strokeWidth = isMainTick ? 3 : 2;
|
||||
canvas.drawLine(outerPoint, innerPoint, paint);
|
||||
|
||||
if (isMainTick) {
|
||||
final speed = (maxSpeed * i / (tickCount - 1)).round();
|
||||
final textPainter = TextPainter(
|
||||
text: TextSpan(
|
||||
text: speed.toString(),
|
||||
style: TextStyle(
|
||||
color: theme.colorScheme.onSurface,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
textDirection: TextDirection.ltr,
|
||||
);
|
||||
textPainter.layout();
|
||||
|
||||
final textRadius = innerRadius - 15;
|
||||
final textOffset = Offset(
|
||||
center.dx + cos(angle) * textRadius - textPainter.width / 2,
|
||||
center.dy + sin(angle) * textRadius - textPainter.height / 2,
|
||||
);
|
||||
|
||||
textPainter.paint(canvas, textOffset);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _drawNeedle(Canvas canvas, Offset center, double radius) {
|
||||
final progress = (speed / maxSpeed).clamp(0.0, 1.0);
|
||||
const startAngle = pi * 0.75;
|
||||
const sweepAngle = pi * 1.5;
|
||||
final needleAngle = startAngle + (sweepAngle * progress);
|
||||
|
||||
final needlePaint = Paint()
|
||||
..color = _getSpeedColor(speed)
|
||||
..strokeWidth = 4
|
||||
..strokeCap = StrokeCap.round;
|
||||
|
||||
final needleLength = radius * 0.7;
|
||||
final needleEnd = Offset(
|
||||
center.dx + cos(needleAngle) * needleLength,
|
||||
center.dy + sin(needleAngle) * needleLength,
|
||||
);
|
||||
|
||||
canvas.drawLine(center, needleEnd, needlePaint);
|
||||
}
|
||||
|
||||
void _drawCenterCircle(Canvas canvas, Offset center) {
|
||||
final paint = Paint()
|
||||
..color = theme.colorScheme.primary
|
||||
..style = PaintingStyle.fill;
|
||||
|
||||
canvas.drawCircle(center, 8, paint);
|
||||
}
|
||||
|
||||
Color _getSpeedColor(double speed) {
|
||||
if (speed < 30) {
|
||||
return Colors.green;
|
||||
} else if (speed < 60) {
|
||||
return Colors.orange;
|
||||
} else {
|
||||
return Colors.red;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(covariant CustomPainter oldDelegate) {
|
||||
return oldDelegate is SpeedometerPainter && oldDelegate.speed != speed;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class DigitalSpeedometer extends StatelessWidget {
|
||||
final double speed;
|
||||
final String unit;
|
||||
|
||||
const DigitalSpeedometer({
|
||||
super.key,
|
||||
required this.speed,
|
||||
required this.unit,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final speedText = speed.toStringAsFixed(0);
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 16),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: theme.colorScheme.outline,
|
||||
width: 2,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
color: theme.colorScheme.surface,
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
speedText,
|
||||
style: TextStyle(
|
||||
fontSize: 80,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: _getSpeedColor(speed, theme),
|
||||
fontFeatures: const [FontFeature.tabularFigures()],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
unit,
|
||||
style: theme.textTheme.headlineMedium?.copyWith(
|
||||
color: theme.colorScheme.onSurface.withOpacity(0.7),
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
_buildSpeedIndicator(speed, theme),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Color _getSpeedColor(double speed, ThemeData theme) {
|
||||
if (speed < 30) {
|
||||
return Colors.green;
|
||||
} else if (speed < 60) {
|
||||
return Colors.orange;
|
||||
} else {
|
||||
return Colors.red;
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildSpeedIndicator(double speed, ThemeData theme) {
|
||||
final maxSpeed = 120.0;
|
||||
final progress = (speed / maxSpeed).clamp(0.0, 1.0);
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
Text(
|
||||
'Speed Progress',
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: theme.colorScheme.onSurface.withOpacity(0.7),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
width: 200,
|
||||
height: 8,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
color: theme.colorScheme.outline.withOpacity(0.3),
|
||||
),
|
||||
child: FractionallySizedBox(
|
||||
alignment: Alignment.centerLeft,
|
||||
widthFactor: progress,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
color: _getSpeedColor(speed, theme),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'${(progress * 100).toStringAsFixed(0)}%',
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.colorScheme.onSurface.withOpacity(0.5),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../providers/location_provider.dart';
|
||||
import '../providers/settings_provider.dart';
|
||||
|
||||
class InfoPanel extends StatelessWidget {
|
||||
final LocationProvider locationProvider;
|
||||
final SettingsProvider settingsProvider;
|
||||
|
||||
const InfoPanel({
|
||||
super.key,
|
||||
required this.locationProvider,
|
||||
required this.settingsProvider,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
children: [
|
||||
if (settingsProvider.showCoordinates ||
|
||||
settingsProvider.showDistance ||
|
||||
settingsProvider.showTripTime ||
|
||||
settingsProvider.showAccuracy)
|
||||
Expanded(
|
||||
child: GridView.count(
|
||||
crossAxisCount: 2,
|
||||
childAspectRatio: 2.5,
|
||||
crossAxisSpacing: 8,
|
||||
mainAxisSpacing: 8,
|
||||
children: _buildInfoCards(context),
|
||||
),
|
||||
)
|
||||
else
|
||||
Expanded(
|
||||
child: Center(
|
||||
child: Text(
|
||||
'Enable additional metrics in settings',
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.6),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
List<Widget> _buildInfoCards(BuildContext context) {
|
||||
final cards = <Widget>[];
|
||||
|
||||
if (settingsProvider.showCoordinates) {
|
||||
cards.add(_buildInfoCard(
|
||||
context,
|
||||
'Location',
|
||||
'${locationProvider.latitude?.toStringAsFixed(6) ?? '--'}\n${locationProvider.longitude?.toStringAsFixed(6) ?? '--'}',
|
||||
Icons.location_on,
|
||||
));
|
||||
}
|
||||
|
||||
if (settingsProvider.showDistance) {
|
||||
final distanceKm = locationProvider.totalDistance / 1000;
|
||||
final distanceText = distanceKm < 1
|
||||
? '${locationProvider.totalDistance.toStringAsFixed(0)} m'
|
||||
: '${distanceKm.toStringAsFixed(2)} km';
|
||||
|
||||
cards.add(_buildInfoCard(
|
||||
context,
|
||||
'Distance',
|
||||
distanceText,
|
||||
Icons.straighten,
|
||||
));
|
||||
}
|
||||
|
||||
if (settingsProvider.showTripTime) {
|
||||
final duration = locationProvider.tripDuration;
|
||||
final hours = duration.inHours;
|
||||
final minutes = duration.inMinutes % 60;
|
||||
final seconds = duration.inSeconds % 60;
|
||||
|
||||
String timeText;
|
||||
if (hours > 0) {
|
||||
timeText = '${hours.toString().padLeft(2, '0')}:${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}';
|
||||
} else {
|
||||
timeText = '${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}';
|
||||
}
|
||||
|
||||
cards.add(_buildInfoCard(
|
||||
context,
|
||||
'Trip Time',
|
||||
timeText,
|
||||
Icons.timer,
|
||||
));
|
||||
}
|
||||
|
||||
if (settingsProvider.showAccuracy) {
|
||||
final accuracy = locationProvider.accuracy;
|
||||
final accuracyText = accuracy != null
|
||||
? '±${accuracy.toStringAsFixed(0)} m'
|
||||
: '--';
|
||||
|
||||
cards.add(_buildInfoCard(
|
||||
context,
|
||||
'Accuracy',
|
||||
accuracyText,
|
||||
Icons.gps_fixed,
|
||||
));
|
||||
}
|
||||
|
||||
return cards;
|
||||
}
|
||||
|
||||
Widget _buildInfoCard(BuildContext context, String title, String value, IconData icon) {
|
||||
return Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
icon,
|
||||
size: 20,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
title,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Flexible(
|
||||
child: Text(
|
||||
value,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
fontFamily: 'monospace',
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../providers/settings_provider.dart';
|
||||
|
||||
class SpeedUnitSelector extends StatelessWidget {
|
||||
const SpeedUnitSelector({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Consumer<SettingsProvider>(
|
||||
builder: (context, settings, child) {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: SpeedUnit.values.map((unit) {
|
||||
final isSelected = settings.speedUnit == unit;
|
||||
return Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4.0),
|
||||
child: ElevatedButton(
|
||||
onPressed: () => settings.setSpeedUnit(unit),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: isSelected
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: Theme.of(context).colorScheme.surface,
|
||||
foregroundColor: isSelected
|
||||
? Theme.of(context).colorScheme.onPrimary
|
||||
: Theme.of(context).colorScheme.onSurface,
|
||||
elevation: isSelected ? 4 : 1,
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
),
|
||||
child: Text(
|
||||
_getUnitDisplayText(unit),
|
||||
style: TextStyle(
|
||||
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
String _getUnitDisplayText(SpeedUnit unit) {
|
||||
switch (unit) {
|
||||
case SpeedUnit.kmh:
|
||||
return 'km/h';
|
||||
case SpeedUnit.mph:
|
||||
return 'mph';
|
||||
case SpeedUnit.knots:
|
||||
return 'knots';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<!-- The INTERNET permission is required for development. Specifically,
|
||||
the Flutter tool needs it to communicate with the running application
|
||||
to allow setting breakpoints, to provide hot reload, etc.
|
||||
-->
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
</manifest>
|
||||
@@ -0,0 +1,51 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<!-- Location permissions for GPS speedometer -->
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
||||
<!-- Optional: for background location access -->
|
||||
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
|
||||
|
||||
<application
|
||||
android:label="gps_speedo"
|
||||
android:name="${applicationName}"
|
||||
android:icon="@mipmap/ic_launcher">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:launchMode="singleTop"
|
||||
android:taskAffinity=""
|
||||
android:theme="@style/LaunchTheme"
|
||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||||
android:hardwareAccelerated="true"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
<!-- Specifies an Android theme to apply to this Activity as soon as
|
||||
the Android process has started. This theme is visible to the user
|
||||
while the Flutter UI initializes. After that, this theme continues
|
||||
to determine the Window background behind the Flutter UI. -->
|
||||
<meta-data
|
||||
android:name="io.flutter.embedding.android.NormalTheme"
|
||||
android:resource="@style/NormalTheme"
|
||||
/>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<!-- Don't delete the meta-data below.
|
||||
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
||||
<meta-data
|
||||
android:name="flutterEmbedding"
|
||||
android:value="2" />
|
||||
</application>
|
||||
<!-- Required to query activities that can process text, see:
|
||||
https://developer.android.com/training/package-visibility and
|
||||
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
|
||||
|
||||
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
|
||||
<queries>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.PROCESS_TEXT"/>
|
||||
<data android:mimeType="text/plain"/>
|
||||
</intent>
|
||||
</queries>
|
||||
</manifest>
|
||||
@@ -0,0 +1,34 @@
|
||||
package io.flutter.plugins;
|
||||
|
||||
import androidx.annotation.Keep;
|
||||
import androidx.annotation.NonNull;
|
||||
import io.flutter.Log;
|
||||
|
||||
import io.flutter.embedding.engine.FlutterEngine;
|
||||
|
||||
/**
|
||||
* Generated file. Do not edit.
|
||||
* This file is generated by the Flutter tool based on the
|
||||
* plugins that support the Android platform.
|
||||
*/
|
||||
@Keep
|
||||
public final class GeneratedPluginRegistrant {
|
||||
private static final String TAG = "GeneratedPluginRegistrant";
|
||||
public static void registerWith(@NonNull FlutterEngine flutterEngine) {
|
||||
try {
|
||||
flutterEngine.getPlugins().add(new com.baseflow.geolocator.GeolocatorPlugin());
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Error registering plugin geolocator_android, com.baseflow.geolocator.GeolocatorPlugin", e);
|
||||
}
|
||||
try {
|
||||
flutterEngine.getPlugins().add(new com.baseflow.permissionhandler.PermissionHandlerPlugin());
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Error registering plugin permission_handler_android, com.baseflow.permissionhandler.PermissionHandlerPlugin", e);
|
||||
}
|
||||
try {
|
||||
flutterEngine.getPlugins().add(new io.flutter.plugins.sharedpreferences.SharedPreferencesPlugin());
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Error registering plugin shared_preferences_android, io.flutter.plugins.sharedpreferences.SharedPreferencesPlugin", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package com.example.gps_speedo
|
||||
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
|
||||
class MainActivity : FlutterActivity()
|
||||
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Modify this file to customize your launch splash screen -->
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="?android:colorBackground" />
|
||||
|
||||
<!-- You can insert your own image assets here -->
|
||||
<!-- <item>
|
||||
<bitmap
|
||||
android:gravity="center"
|
||||
android:src="@mipmap/launch_image" />
|
||||
</item> -->
|
||||
</layer-list>
|
||||
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Modify this file to customize your launch splash screen -->
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="@android:color/white" />
|
||||
|
||||
<!-- You can insert your own image assets here -->
|
||||
<!-- <item>
|
||||
<bitmap
|
||||
android:gravity="center"
|
||||
android:src="@mipmap/launch_image" />
|
||||
</item> -->
|
||||
</layer-list>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 544 B |
Binary file not shown.
|
After Width: | Height: | Size: 442 B |
Binary file not shown.
|
After Width: | Height: | Size: 721 B |
Binary file not shown.
|
After Width: | Height: | Size: 1.0 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.4 KiB |
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
|
||||
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||
<!-- Show a splash screen on the activity. Automatically removed when
|
||||
the Flutter engine draws its first frame -->
|
||||
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||
</style>
|
||||
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||
This theme determines the color of the Android Window while your
|
||||
Flutter UI initializes, as well as behind your Flutter UI while its
|
||||
running.
|
||||
|
||||
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
||||
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||
<item name="android:windowBackground">?android:colorBackground</item>
|
||||
</style>
|
||||
</resources>
|
||||
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
|
||||
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||
<!-- Show a splash screen on the activity. Automatically removed when
|
||||
the Flutter engine draws its first frame -->
|
||||
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||
</style>
|
||||
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||
This theme determines the color of the Android Window while your
|
||||
Flutter UI initializes, as well as behind your Flutter UI while its
|
||||
running.
|
||||
|
||||
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
||||
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||
<item name="android:windowBackground">?android:colorBackground</item>
|
||||
</style>
|
||||
</resources>
|
||||
@@ -0,0 +1,7 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<!-- The INTERNET permission is required for development. Specifically,
|
||||
the Flutter tool needs it to communicate with the running application
|
||||
to allow setting breakpoints, to provide hot reload, etc.
|
||||
-->
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
</manifest>
|
||||
Reference in New Issue
Block a user