MotorSignal-App/lib/features/settings/settings_screen.dart

579 lines
25 KiB
Dart

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../services/sensor_fusion_service.dart';
import '../../services/map/map_provider_interface.dart';
import '../../services/map/map_providers.dart';
import '../../services/alerts/road_alert_models.dart';
import '../../services/alerts/road_alert_providers.dart';
import '../../database/database.dart';
class SettingsScreen extends ConsumerStatefulWidget {
const SettingsScreen({super.key});
@override
ConsumerState<SettingsScreen> createState() => _SettingsScreenState();
}
class _SettingsScreenState extends ConsumerState<SettingsScreen> {
late TextEditingController _tomtomKeyController;
late TextEditingController _communityKeyController;
bool _isTestingTomTom = false;
bool _isTestingCommunity = false;
@override
void initState() {
super.initState();
_tomtomKeyController = TextEditingController(text: ref.read(tomtomApiKeyProvider));
_communityKeyController = TextEditingController(text: ref.read(communityApiKeyProvider));
}
@override
void dispose() {
_tomtomKeyController.dispose();
_communityKeyController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final mapType = ref.watch(mapProviderTypeNotifierProvider);
final roadAlertsEnabled = ref.watch(roadAlertsEnabledProvider);
final audioAlertsEnabled = ref.watch(audioAlertsEnabledProvider);
final alertRadiusMeters = ref.watch(alertRadiusMetersProvider);
final enabledCategories = ref.watch(enabledCategoriesProvider);
final tomtomStatus = ref.watch(tomtomStatusProvider);
final communityStatus = ref.watch(communityStatusProvider);
return Scaffold(
appBar: AppBar(title: const Text('Settings')),
body: ListView(
padding: const EdgeInsets.all(16.0),
children: [
const Text(
'Maps & Navigation Engine',
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold, color: Colors.cyanAccent),
),
const SizedBox(height: 16),
Card(
clipBehavior: Clip.antiAlias,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Row(
children: [
Icon(Icons.map, color: Colors.cyanAccent),
SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Active Map Engine', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
SizedBox(height: 4),
Text('Select map rendering backend for live map, route preview, and replay.', style: TextStyle(fontSize: 13, color: Colors.white70)),
],
),
),
],
),
const SizedBox(height: 16),
SegmentedButton<AppMapType>(
segments: const [
ButtonSegment(
value: AppMapType.openStreetMap,
label: Text('OpenStreetMap (Primary)'),
icon: Icon(Icons.public),
),
ButtonSegment(
value: AppMapType.googleMaps,
label: Text('Google Maps'),
icon: Icon(Icons.map_outlined),
),
],
selected: {mapType},
onSelectionChanged: (Set<AppMapType> selection) {
ref
.read(mapProviderTypeNotifierProvider.notifier)
.setMapProviderType(selection.first);
},
),
],
),
),
),
const SizedBox(height: 32),
const Text(
'Road Alerts & Incidents',
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold, color: Colors.cyanAccent),
),
const SizedBox(height: 16),
Card(
clipBehavior: Clip.antiAlias,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SwitchListTile(
activeThumbColor: Colors.cyanAccent,
title: const Text('Road Alerts', style: TextStyle(fontWeight: FontWeight.bold)),
subtitle: const Text('Display real-time police, hazards, and traffic incidents on map.'),
value: roadAlertsEnabled,
onChanged: (val) {
ref.read(roadAlertsEnabledProvider.notifier).setEnabled(val);
},
),
const Divider(),
SwitchListTile(
activeThumbColor: Colors.cyanAccent,
title: const Text('Audio Warnings (TTS)', style: TextStyle(fontWeight: FontWeight.bold)),
subtitle: const Text('Announce approaching hazards and enforcement over helmet headset.'),
value: audioAlertsEnabled,
onChanged: roadAlertsEnabled
? (val) {
ref.read(audioAlertsEnabledProvider.notifier).setEnabled(val);
}
: null,
),
const Divider(),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Alert Radius', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16)),
SizedBox(height: 4),
Text('Distance to detect upcoming incidents', style: TextStyle(fontSize: 12, color: Colors.white70)),
],
),
DropdownButton<double>(
value: alertRadiusMeters,
items: const [
DropdownMenuItem(value: 152.4, child: Text('500 ft')),
DropdownMenuItem(value: 304.8, child: Text('1,000 ft')),
DropdownMenuItem(value: 402.3, child: Text('1/4 Mile')),
DropdownMenuItem(value: 804.7, child: Text('1/2 Mile')),
DropdownMenuItem(value: 1609.3, child: Text('1 Mile')),
],
onChanged: roadAlertsEnabled
? (val) {
if (val != null) {
ref.read(alertRadiusMetersProvider.notifier).setRadius(val);
}
}
: null,
),
],
),
),
const Divider(),
const Padding(
padding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
child: Text('Alert Categories', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16)),
),
...RoadAlertCategory.values.map((cat) {
final isChecked = enabledCategories.contains(cat);
return CheckboxListTile(
activeColor: Colors.cyanAccent,
checkColor: Colors.black,
title: Text(cat.displayName),
secondary: Icon(cat.icon, color: cat.color),
value: isChecked,
onChanged: roadAlertsEnabled
? (val) {
ref
.read(enabledCategoriesProvider.notifier)
.toggleCategory(cat, val ?? true);
}
: null,
);
}),
],
),
),
),
const SizedBox(height: 16),
// Community Traffic Provider Card
Card(
clipBehavior: Clip.antiAlias,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const Icon(Icons.local_police, color: Colors.blueAccent),
const SizedBox(width: 12),
const Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Community Traffic Provider', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
SizedBox(height: 4),
Text('Supplies Police, Speed Cameras, and Crowd Hazards.', style: TextStyle(fontSize: 12, color: Colors.white70)),
],
),
),
_buildStatusBadge(communityStatus),
],
),
const SizedBox(height: 16),
TextField(
controller: _communityKeyController,
decoration: const InputDecoration(
labelText: 'Community Provider API Key',
hintText: 'Enter API Key for Community Traffic Feed',
border: OutlineInputBorder(),
),
onChanged: (val) {
ref.read(communityApiKeyProvider.notifier).setApiKey(val);
},
),
const SizedBox(height: 12),
Align(
alignment: Alignment.centerRight,
child: ElevatedButton.icon(
onPressed: _isTestingCommunity
? null
: () async {
setState(() => _isTestingCommunity = true);
final messenger = ScaffoldMessenger.of(context);
final provider = ref.read(communityProviderInstance);
final status = await provider.testConnection(_communityKeyController.text);
ref.read(communityStatusProvider.notifier).state = status;
setState(() => _isTestingCommunity = false);
if (mounted) {
String msg = 'Community API Connected successfully!';
if (status == RoadAlertProviderStatus.authError) msg = 'Authentication Error: Invalid Key.';
if (status == RoadAlertProviderStatus.notConfigured) msg = 'Key is empty.';
messenger.showSnackBar(SnackBar(content: Text(msg)));
}
},
icon: _isTestingCommunity
? const SizedBox(width: 16, height: 16, child: CircularProgressIndicator(strokeWidth: 2))
: const Icon(Icons.sync),
label: const Text('Test Connection'),
),
),
],
),
),
),
const SizedBox(height: 16),
// TomTom Provider API Key Card
Card(
clipBehavior: Clip.antiAlias,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const Icon(Icons.security, color: Colors.cyanAccent),
const SizedBox(width: 12),
const Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('TomTom Traffic Provider', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
SizedBox(height: 4),
Text('Supplies Accidents, Hazards, Closures & Construction data.', style: TextStyle(fontSize: 12, color: Colors.white70)),
],
),
),
_buildStatusBadge(tomtomStatus),
],
),
const SizedBox(height: 16),
TextField(
controller: _tomtomKeyController,
decoration: const InputDecoration(
labelText: 'TomTom API Key',
hintText: 'Enter your free TomTom API Key',
border: OutlineInputBorder(),
),
onChanged: (val) {
ref.read(tomtomApiKeyProvider.notifier).setApiKey(val);
},
),
const SizedBox(height: 12),
Align(
alignment: Alignment.centerRight,
child: ElevatedButton.icon(
onPressed: _isTestingTomTom
? null
: () async {
setState(() => _isTestingTomTom = true);
final messenger = ScaffoldMessenger.of(context);
final provider = ref.read(tomtomProviderInstance);
final status = await provider.testConnection(_tomtomKeyController.text);
ref.read(tomtomStatusProvider.notifier).state = status;
setState(() => _isTestingTomTom = false);
if (mounted) {
String msg = 'TomTom API Connected successfully!';
if (status == RoadAlertProviderStatus.authError) msg = 'Authentication Error: Invalid Key.';
if (status == RoadAlertProviderStatus.notConfigured) msg = 'Key is empty.';
messenger.showSnackBar(SnackBar(content: Text(msg)));
}
},
icon: _isTestingTomTom
? const SizedBox(width: 16, height: 16, child: CircularProgressIndicator(strokeWidth: 2))
: const Icon(Icons.sync),
label: const Text('Test Connection'),
),
),
],
),
),
),
// Developer Test Mode Section (Debug Mode Only)
if (kDebugMode) ...[
const SizedBox(height: 32),
const Text(
'Developer Options',
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold, color: Colors.orangeAccent),
),
const SizedBox(height: 16),
Card(
color: Colors.orange.withValues(alpha: 0.1),
shape: RoundedRectangleBorder(
side: const BorderSide(color: Colors.orangeAccent),
borderRadius: BorderRadius.circular(12),
),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Row(
children: [
Icon(Icons.bug_report, color: Colors.orangeAccent),
SizedBox(width: 12),
Text('Road Alert Test Mode', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: Colors.orangeAccent)),
],
),
const SizedBox(height: 8),
const Text('Generate simulated test alerts for real-device testing (Debug Build Only).', style: TextStyle(fontSize: 12, color: Colors.white70)),
const SizedBox(height: 16),
Wrap(
spacing: 8,
runSpacing: 8,
children: [
ElevatedButton.icon(
icon: const Icon(Icons.local_police, color: Colors.blueAccent, size: 18),
label: const Text('Test Police (100m)'),
onPressed: () {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Simulated Police Alert 100m ahead triggered.')));
},
),
ElevatedButton.icon(
icon: const Icon(Icons.warning, color: Colors.orangeAccent, size: 18),
label: const Text('Test Hazard (250m)'),
onPressed: () {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Simulated Hazard Alert 250m ahead triggered.')));
},
),
],
),
],
),
),
),
],
const SizedBox(height: 32),
const Text(
'Sensors',
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold, color: Colors.cyanAccent),
),
const SizedBox(height: 16),
Card(
clipBehavior: Clip.antiAlias,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Icon(Icons.screen_rotation, color: Colors.cyanAccent),
const SizedBox(width: 12),
const Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Calibrate Mount Angle',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
SizedBox(height: 4),
Text(
'Place the motorcycle perfectly upright and tap this to zero the lean angle sensor.',
style: TextStyle(fontSize: 13, color: Colors.white70, height: 1.3),
),
],
),
),
],
),
const SizedBox(height: 16),
Align(
alignment: Alignment.centerRight,
child: ElevatedButton(
onPressed: () async {
ref.read(sensorFusionServiceProvider).calibrateUpright();
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Sensors Calibrated!')),
);
}
},
child: const Text('Calibrate'),
),
),
],
),
),
),
const SizedBox(height: 32),
const Text(
'Data',
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold, color: Colors.cyanAccent),
),
const SizedBox(height: 16),
Card(
clipBehavior: Clip.antiAlias,
child: InkWell(
onTap: () => context.go('/settings/diagnostics'),
child: const Padding(
padding: EdgeInsets.all(16.0),
child: Row(
children: [
Icon(Icons.monitor_heart, color: Colors.cyanAccent),
SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Sensor Diagnostics', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
SizedBox(height: 4),
Text('View raw IMU and GPS values.', style: TextStyle(fontSize: 13, color: Colors.white70)),
],
),
),
Icon(Icons.chevron_right, color: Colors.white54),
],
),
),
),
),
const SizedBox(height: 16),
Card(
clipBehavior: Clip.antiAlias,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Icon(Icons.delete_forever, color: Colors.redAccent),
const SizedBox(width: 12),
const Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Clear Ride History', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
SizedBox(height: 4),
Text('Permanently delete all recorded rides and performance runs.', style: TextStyle(fontSize: 13, color: Colors.white70, height: 1.3)),
],
),
),
],
),
const SizedBox(height: 16),
Align(
alignment: Alignment.centerRight,
child: ElevatedButton(
style: ElevatedButton.styleFrom(backgroundColor: Colors.red.withValues(alpha: 0.2)),
onPressed: () async {
final db = ref.read(databaseProvider);
await db.delete(db.rides).go();
await db.delete(db.performanceRuns).go();
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('All data cleared.')),
);
}
},
child: const Text('Clear', style: TextStyle(color: Colors.redAccent)),
),
),
],
),
),
),
],
),
);
}
Widget _buildStatusBadge(RoadAlertProviderStatus status) {
String label;
Color color;
switch (status) {
case RoadAlertProviderStatus.connected:
label = 'Connected';
color = Colors.greenAccent;
break;
case RoadAlertProviderStatus.configured:
label = 'Configured';
color = Colors.cyanAccent;
break;
case RoadAlertProviderStatus.authError:
label = 'Auth Error';
color = Colors.redAccent;
break;
case RoadAlertProviderStatus.rateLimited:
label = 'Rate Limited';
color = Colors.orangeAccent;
break;
case RoadAlertProviderStatus.networkError:
label = 'Network Error';
color = Colors.amberAccent;
break;
case RoadAlertProviderStatus.notConfigured:
label = 'Not Set';
color = Colors.white38;
break;
}
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: color.withValues(alpha: 0.15),
border: Border.all(color: color),
borderRadius: BorderRadius.circular(8),
),
child: Text(label, style: TextStyle(color: color, fontSize: 11, fontWeight: FontWeight.bold)),
);
}
}