MotorSignal-App/lib/features/settings/widgets/evolution_settings_view.dart

1221 lines
49 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.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 '../../../services/alerts/road_alert_service.dart';
import '../../../services/osm_oauth_service.dart';
import '../../../services/sensor_fusion_service.dart';
import '../../../services/field_test/field_test_logger.dart';
import '../../../database/database.dart';
import '../../rides/ride_state_machine.dart';
import '../../../core/theme/evolution_theme_config.dart';
import '../../../core/app_info.dart';
class EvolutionSettingsView extends ConsumerStatefulWidget {
const EvolutionSettingsView({super.key});
@override
ConsumerState<EvolutionSettingsView> createState() => _EvolutionSettingsViewState();
}
class _EvolutionSettingsViewState extends ConsumerState<EvolutionSettingsView> {
late TextEditingController _tomtomKeyController;
late TextEditingController _communityKeyController;
late TextEditingController _communityEndpointController;
bool _isTestingTomTom = false;
bool _isTestingCommunity = false;
@override
void initState() {
super.initState();
_tomtomKeyController = TextEditingController(
text: ref.read(tomtomApiKeyProvider),
);
_communityKeyController = TextEditingController(
text: ref.read(communityApiKeyProvider),
);
_communityEndpointController = TextEditingController(
text: ref.read(communityEndpointUrlProvider),
);
}
@override
void dispose() {
_tomtomKeyController.dispose();
_communityKeyController.dispose();
_communityEndpointController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final mapType = ref.watch(mapProviderTypeNotifierProvider);
final themeMode = ref.watch(dashboardThemeModeProvider);
final roadAlertsEnabled = ref.watch(roadAlertsEnabledProvider);
final audioAlertsEnabled = ref.watch(audioAlertsEnabledProvider);
final alertRadiusMeters = ref.watch(alertRadiusMetersProvider);
final enabledCategories = ref.watch(enabledCategoriesProvider);
final osmAuth = ref.watch(osmOAuthServiceProvider);
final radiusMiles = (alertRadiusMeters / 1609.34).toStringAsFixed(1);
final isLandscape = MediaQuery.of(context).orientation == Orientation.landscape;
return Scaffold(
backgroundColor: Colors.black,
appBar: AppBar(
toolbarHeight: isLandscape ? 36 : 52,
backgroundColor: Colors.transparent,
elevation: 0,
centerTitle: true,
title: Text(
'SETTINGS & SYSTEM CONFIG',
style: TextStyle(
fontFamily: 'Sora',
fontSize: isLandscape ? 13 : 16,
fontWeight: FontWeight.w900,
color: Colors.white,
letterSpacing: 0.12,
),
),
),
body: LayoutBuilder(
builder: (context, constraints) {
final isLandscape = constraints.maxWidth > constraints.maxHeight;
final themeToggleBar = Container(
margin: EdgeInsets.only(bottom: isLandscape ? 6 : 12),
padding: EdgeInsets.symmetric(horizontal: 12, vertical: isLandscape ? 6 : 12),
decoration: BoxDecoration(
color: const Color(0xFF0D0D0D),
borderRadius: BorderRadius.circular(14),
border: Border.all(color: const Color(0xFF1F1F1F), width: 1.2),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Row(
children: [
Icon(Icons.palette_outlined, color: Color(0xFF00E5FF), size: 18),
SizedBox(width: 8),
Text(
'ACTIVE THEME:',
style: TextStyle(
fontFamily: 'Inter',
fontSize: 11,
fontWeight: FontWeight.w800,
color: Colors.white70,
),
),
],
),
SegmentedButton<DashboardThemeMode>(
style: const ButtonStyle(
visualDensity: VisualDensity.compact,
),
segments: const [
ButtonSegment(
value: DashboardThemeMode.classic,
label: Text('Classic', style: TextStyle(fontSize: 11)),
),
ButtonSegment(
value: DashboardThemeMode.evolution,
label: Text('Evolution', style: TextStyle(fontSize: 11)),
),
],
selected: {themeMode},
onSelectionChanged: (Set<DashboardThemeMode> selection) {
ref.read(dashboardThemeModeProvider.notifier).setThemeMode(selection.first);
},
),
],
),
);
final osmAccountCard = _buildOsmAccountCard(context, ref, osmAuth);
final mapEngineCard = _buildMapEngineCard(context, ref, mapType);
final alertConfigCard = _buildAlertConfigCard(
context,
ref,
roadAlertsEnabled,
audioAlertsEnabled,
enabledCategories,
alertRadiusMeters,
radiusMiles,
);
final sensorsCard = _buildSensorsCalibrationCard(context, ref);
final tomtomCard = _buildTomTomCard(context, ref);
final communityCard = _buildCommunityCard(context, ref);
final dataCard = _buildDataManagementCard(context, ref);
final engineeringCard = _buildEngineeringDiagnosticsCard(context, ref);
final versionFooter = _buildVersionFooter();
if (isLandscape) {
return SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
children: [
themeToggleBar,
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Column(
children: [
sensorsCard,
const SizedBox(height: 16),
mapEngineCard,
const SizedBox(height: 16),
osmAccountCard,
const SizedBox(height: 16),
tomtomCard,
],
),
),
const SizedBox(width: 16),
Expanded(
child: Column(
children: [
alertConfigCard,
const SizedBox(height: 16),
communityCard,
const SizedBox(height: 16),
engineeringCard,
const SizedBox(height: 16),
dataCard,
],
),
),
],
),
const SizedBox(height: 24),
versionFooter,
],
),
);
}
return SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
themeToggleBar,
sensorsCard,
const SizedBox(height: 16),
engineeringCard,
const SizedBox(height: 16),
dataCard,
const SizedBox(height: 16),
osmAccountCard,
const SizedBox(height: 16),
mapEngineCard,
const SizedBox(height: 16),
alertConfigCard,
const SizedBox(height: 16),
communityCard,
const SizedBox(height: 16),
tomtomCard,
const SizedBox(height: 24),
versionFooter,
const SizedBox(height: 24),
],
),
);
},
),
);
}
Widget _buildOsmAccountCard(BuildContext context, WidgetRef ref, OsmAuthState osmAuth) {
final isAuthed = osmAuth.isAuthenticated;
final user = osmAuth.user;
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color(0xFF0D0D0D),
borderRadius: BorderRadius.circular(16),
border: Border.all(color: const Color(0xFF1F1F1F), width: 1.2),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Row(
children: [
Icon(Icons.public, color: Color(0xFF00E5FF), size: 18),
SizedBox(width: 8),
Text(
'OPENSTREETMAP ACCOUNT',
style: TextStyle(
fontFamily: 'Sora',
fontSize: 12,
fontWeight: FontWeight.w800,
color: Color(0xFF00E5FF),
letterSpacing: 0.1,
),
),
],
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
decoration: BoxDecoration(
color: isAuthed
? const Color(0xFF00E5FF).withValues(alpha: 0.15)
: Colors.white10,
borderRadius: BorderRadius.circular(8),
),
child: Text(
isAuthed ? 'CONNECTED' : 'DISCONNECTED',
style: TextStyle(
fontFamily: 'Inter',
fontSize: 9,
fontWeight: FontWeight.w800,
color: isAuthed ? const Color(0xFF00E5FF) : Colors.white38,
),
),
),
],
),
const SizedBox(height: 12),
if (isAuthed && user != null) ...[
Row(
children: [
CircleAvatar(
radius: 20,
backgroundColor: Colors.white12,
backgroundImage: user.avatarUrl != null ? NetworkImage(user.avatarUrl!) : null,
child: user.avatarUrl == null
? const Icon(Icons.person, color: Color(0xFF00E5FF), size: 22)
: null,
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
user.displayName,
style: const TextStyle(
fontFamily: 'Inter',
fontSize: 14,
fontWeight: FontWeight.w700,
color: Colors.white,
),
),
Text(
'Changesets: ${user.changesetCount} • Sync Enabled',
style: const TextStyle(
fontFamily: 'Inter',
fontSize: 11,
color: Colors.white54,
),
),
],
),
),
],
),
const SizedBox(height: 12),
OutlinedButton.icon(
style: OutlinedButton.styleFrom(
foregroundColor: Colors.redAccent,
side: const BorderSide(color: Colors.redAccent, width: 0.8),
minimumSize: const Size(double.infinity, 38),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
),
icon: const Icon(Icons.link_off, size: 16),
label: const Text('Disconnect OpenStreetMap', style: TextStyle(fontSize: 12, fontWeight: FontWeight.bold)),
onPressed: () => ref.read(osmOAuthServiceProvider.notifier).logout(),
),
] else ...[
const Text(
'Link your OpenStreetMap account via OAuth 2.0 PKCE to upload GPS traces and sync ride telemetry.',
style: TextStyle(fontFamily: 'Inter', fontSize: 11, color: Colors.white60, height: 1.4),
),
const SizedBox(height: 12),
ElevatedButton.icon(
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF00E5FF),
foregroundColor: Colors.black,
minimumSize: const Size(double.infinity, 40),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
),
icon: const Icon(Icons.login, size: 18),
label: const Text(
'Connect OpenStreetMap Account',
style: TextStyle(fontFamily: 'Inter', fontSize: 12, fontWeight: FontWeight.w800),
),
onPressed: osmAuth.isLoading
? null
: () => ref.read(osmOAuthServiceProvider.notifier).startLoginFlow(),
),
],
if (osmAuth.errorMessage != null) ...[
const SizedBox(height: 8),
Text(
osmAuth.errorMessage!,
style: const TextStyle(color: Colors.redAccent, fontSize: 10),
),
],
],
),
);
}
Widget _buildMapEngineCard(BuildContext context, WidgetRef ref, AppMapType mapType) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color(0xFF0D0D0D),
borderRadius: BorderRadius.circular(16),
border: Border.all(color: const Color(0xFF1F1F1F), width: 1.2),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'MAP ENGINE & RENDERING',
style: TextStyle(
fontFamily: 'Sora',
fontSize: 12,
fontWeight: FontWeight.w800,
color: Color(0xFF00E5FF),
letterSpacing: 0.1,
),
),
const SizedBox(height: 12),
const Text(
'Active Map Engine',
style: TextStyle(fontFamily: 'Inter', fontSize: 11, fontWeight: FontWeight.w700, color: Colors.white70),
),
const SizedBox(height: 8),
_buildEngineOption(
title: 'OpenStreetMap',
selected: mapType == AppMapType.openStreetMap,
onTap: () => ref.read(mapProviderTypeNotifierProvider.notifier).setMapProviderType(AppMapType.openStreetMap),
),
const SizedBox(height: 6),
_buildEngineOption(
title: 'Google Maps',
selected: mapType == AppMapType.googleMaps,
onTap: () => ref.read(mapProviderTypeNotifierProvider.notifier).setMapProviderType(AppMapType.googleMaps),
),
const SizedBox(height: 16),
const Text(
'Rendering Preferences',
style: TextStyle(fontFamily: 'Inter', fontSize: 11, fontWeight: FontWeight.w700, color: Colors.white70),
),
const SizedBox(height: 8),
_buildSwitchRow(Icons.apartment, '3D Buildings', true, (_) {}),
_buildSwitchRow(Icons.terrain, 'Terrain Topo Data', true, (_) {}),
_buildSwitchRow(Icons.traffic, 'Live Traffic Overlay', true, (_) {}),
_buildSwitchRow(Icons.play_circle_outline, 'Replay Preview', false, (_) {}),
],
),
);
}
Widget _buildAlertConfigCard(
BuildContext context,
WidgetRef ref,
bool roadAlertsEnabled,
bool audioAlertsEnabled,
Set<RoadAlertCategory> enabledCategories,
double alertRadiusMeters,
String radiusMiles,
) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color(0xFF0D0D0D),
borderRadius: BorderRadius.circular(16),
border: Border.all(color: const Color(0xFF1F1F1F), width: 1.2),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'ROAD ALERT CONFIGURATION',
style: TextStyle(
fontFamily: 'Sora',
fontSize: 12,
fontWeight: FontWeight.w800,
color: Color(0xFF00E5FF),
letterSpacing: 0.1,
),
),
const SizedBox(height: 12),
const Text(
'Alert Categories',
style: TextStyle(fontFamily: 'Inter', fontSize: 11, fontWeight: FontWeight.w700, color: Colors.white70),
),
const SizedBox(height: 8),
_buildSwitchRow(
Icons.local_police_outlined,
'Police Presence',
enabledCategories.contains(RoadAlertCategory.police),
(v) => ref.read(enabledCategoriesProvider.notifier).toggleCategory(RoadAlertCategory.police, v),
),
_buildSwitchRow(
Icons.camera_alt_outlined,
'Speed Cameras',
enabledCategories.contains(RoadAlertCategory.speedCamera),
(v) => ref.read(enabledCategoriesProvider.notifier).toggleCategory(RoadAlertCategory.speedCamera, v),
),
_buildSwitchRow(
Icons.warning_amber_rounded,
'Road Hazards',
enabledCategories.contains(RoadAlertCategory.hazard),
(v) => ref.read(enabledCategoriesProvider.notifier).toggleCategory(RoadAlertCategory.hazard, v),
),
_buildSwitchRow(
Icons.car_crash_outlined,
'Traffic Incidents',
enabledCategories.contains(RoadAlertCategory.traffic),
(v) => ref.read(enabledCategoriesProvider.notifier).toggleCategory(RoadAlertCategory.traffic, v),
),
const SizedBox(height: 16),
const Text(
'Radius Configuration',
style: TextStyle(fontFamily: 'Inter', fontSize: 11, fontWeight: FontWeight.w700, color: Colors.white70),
),
const SizedBox(height: 8),
Center(
child: Text(
'$radiusMiles MILE',
style: const TextStyle(
fontFamily: 'JetBrains Mono',
fontSize: 22,
fontWeight: FontWeight.w900,
color: Colors.white,
),
),
),
const SizedBox(height: 8),
Row(
children: [
IconButton(
icon: const Icon(Icons.remove_circle_outline, color: Color(0xFF00E5FF)),
onPressed: alertRadiusMeters > 500
? () => ref.read(alertRadiusMetersProvider.notifier).setRadius(alertRadiusMeters - 500.0)
: null,
),
Expanded(
child: LinearProgressIndicator(
value: (alertRadiusMeters / 5000.0).clamp(0.1, 1.0),
backgroundColor: const Color(0xFF1F1F1F),
valueColor: const AlwaysStoppedAnimation<Color>(Color(0xFF00E5FF)),
minHeight: 8,
borderRadius: BorderRadius.circular(4),
),
),
IconButton(
icon: const Icon(Icons.add_circle_outline, color: Color(0xFF00E5FF)),
onPressed: alertRadiusMeters < 5000
? () => ref.read(alertRadiusMetersProvider.notifier).setRadius(alertRadiusMeters + 500.0)
: null,
),
],
),
const Center(
child: Text(
'Distance to detect upcoming incidents',
style: TextStyle(fontFamily: 'Inter', fontSize: 9, color: Colors.white38),
),
),
const SizedBox(height: 12),
SizedBox(
width: double.infinity,
child: OutlinedButton.icon(
icon: const Icon(Icons.volume_up, size: 16),
label: const Text('TRIGGER TEST ALERT (TTS + HUD)'),
style: OutlinedButton.styleFrom(
foregroundColor: const Color(0xFF00E5FF),
side: const BorderSide(color: Color(0xFF00E5FF), width: 1),
padding: const EdgeInsets.symmetric(vertical: 10),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
onPressed: () => ref.read(roadAlertServiceProvider).triggerTestAlert(),
),
),
],
),
);
}
Widget _buildEngineOption({required String title, required bool selected, required VoidCallback onTap}) {
return InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(8),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
decoration: BoxDecoration(
color: selected ? const Color(0xFF00252A) : const Color(0xFF141414),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: selected ? const Color(0xFF00E5FF) : const Color(0xFF222222),
width: selected ? 1.4 : 1.0,
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
title,
style: TextStyle(
fontFamily: 'Inter',
fontSize: 12,
fontWeight: FontWeight.w700,
color: selected ? Colors.white : Colors.white60,
),
),
if (selected) const Icon(Icons.check, color: Color(0xFF00E5FF), size: 16),
],
),
),
);
}
Widget _buildSwitchRow(IconData icon, String title, bool value, ValueChanged<bool> onChanged) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 2),
child: Row(
children: [
Icon(icon, color: Colors.white60, size: 16),
const SizedBox(width: 8),
Expanded(
child: Text(
title,
style: const TextStyle(fontFamily: 'Inter', fontSize: 11, fontWeight: FontWeight.w600, color: Colors.white),
),
),
Switch(
value: value,
activeThumbColor: const Color(0xFF00E5FF),
onChanged: onChanged,
),
],
),
);
}
Widget _buildSensorsCalibrationCard(BuildContext context, WidgetRef ref) {
final leanAsync = ref.watch(leanAngleStreamProvider);
final currentLean = leanAsync.value ?? 0.0;
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color(0xFF0D0D0D),
borderRadius: BorderRadius.circular(16),
border: Border.all(color: const Color(0xFF1F1F1F), width: 1.2),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Row(
children: [
Icon(Icons.screen_rotation, color: Color(0xFF00E5FF), size: 18),
SizedBox(width: 8),
Text(
'SENSORS & MOUNT CALIBRATION',
style: TextStyle(
fontFamily: 'Inter',
fontSize: 12,
fontWeight: FontWeight.w800,
color: Colors.white,
letterSpacing: 0.1,
),
),
],
),
const SizedBox(height: 8),
const Text(
'Place motorcycle upright on level ground. Zeroes the roll and pitch offsets for your phone mount angle.',
style: TextStyle(fontFamily: 'Inter', fontSize: 10, color: Colors.white60, height: 1.3),
),
const SizedBox(height: 12),
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
decoration: BoxDecoration(
color: const Color(0xFF141414),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: const Color(0xFF222222)),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'CURRENT LEAN ANGLE:',
style: TextStyle(fontFamily: 'Inter', fontSize: 10, fontWeight: FontWeight.bold, color: Colors.white54),
),
Text(
'${currentLean.toStringAsFixed(1)}°',
style: const TextStyle(fontFamily: 'Inter', fontSize: 14, fontWeight: FontWeight.w900, color: Color(0xFF00E5FF)),
),
],
),
),
const SizedBox(height: 12),
SizedBox(
width: double.infinity,
child: ElevatedButton.icon(
icon: const Icon(Icons.tune, size: 16),
label: const Text('CALIBRATE UPRIGHT ZERO'),
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF00E5FF),
foregroundColor: Colors.black,
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
textStyle: const TextStyle(fontFamily: 'Inter', fontSize: 11, fontWeight: FontWeight.w900, letterSpacing: 0.8),
),
onPressed: () {
final session = ref.read(rideStateMachineProvider);
if (session.state == RideState.riding) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Cannot calibrate while a ride is actively recording.')),
);
return;
}
ref.read(sensorFusionServiceProvider).calibrateUpright();
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Sensors Calibrated! Zero mount angle saved.')),
);
},
),
),
],
),
);
}
Widget _buildDataManagementCard(BuildContext context, WidgetRef ref) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color(0xFF0D0D0D),
borderRadius: BorderRadius.circular(16),
border: Border.all(color: const Color(0xFF1F1F1F), width: 1.2),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Row(
children: [
Icon(Icons.storage_outlined, color: Color(0xFF00E5FF), size: 18),
SizedBox(width: 8),
Text(
'DATA & STORAGE',
style: TextStyle(
fontFamily: 'Inter',
fontSize: 12,
fontWeight: FontWeight.w800,
color: Colors.white,
letterSpacing: 0.1,
),
),
],
),
const SizedBox(height: 12),
InkWell(
onTap: () => context.go('/settings/diagnostics'),
borderRadius: BorderRadius.circular(8),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
decoration: BoxDecoration(
color: const Color(0xFF141414),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: const Color(0xFF222222)),
),
child: const Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Icon(Icons.monitor_heart, color: Color(0xFF00E5FF), size: 16),
SizedBox(width: 8),
Text(
'Sensor Diagnostics & Raw Telemetry',
style: TextStyle(fontFamily: 'Inter', fontSize: 11, fontWeight: FontWeight.w700, color: Colors.white),
),
],
),
Icon(Icons.chevron_right, color: Colors.white54, size: 18),
],
),
),
),
const SizedBox(height: 10),
SizedBox(
width: double.infinity,
child: OutlinedButton.icon(
icon: const Icon(Icons.delete_forever, color: Colors.redAccent, size: 16),
label: const Text('CLEAR ALL RIDE HISTORY'),
style: OutlinedButton.styleFrom(
foregroundColor: Colors.redAccent,
side: const BorderSide(color: Colors.redAccent, width: 1),
padding: const EdgeInsets.symmetric(vertical: 10),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
textStyle: const TextStyle(fontFamily: 'Inter', fontSize: 11, fontWeight: FontWeight.w800, letterSpacing: 0.5),
),
onPressed: () async {
final session = ref.read(rideStateMachineProvider);
if (session.state == RideState.riding) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Cannot clear history while a ride is actively recording.')),
);
return;
}
final confirmed = await showDialog<bool>(
context: context,
builder: (ctx) => AlertDialog(
backgroundColor: const Color(0xFF141414),
title: const Text('Clear All Ride History?'),
content: const Text('This will permanently delete all saved rides, telemetry points, and events. This action cannot be undone.'),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx, false),
child: const Text('CANCEL'),
),
TextButton(
onPressed: () => Navigator.pop(ctx, true),
child: const Text('CLEAR ALL', style: TextStyle(color: Colors.redAccent, fontWeight: FontWeight.bold)),
),
],
),
);
if (confirmed == true && context.mounted) {
final db = ref.read(databaseProvider);
await db.transaction(() async {
await db.delete(db.telemetrySamples).go();
await db.delete(db.rideEvents).go();
await db.delete(db.performanceRuns).go();
await db.delete(db.rides).go();
});
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('All ride history cleared.')),
);
}
}
},
),
),
],
),
);
}
Widget _buildEngineeringDiagnosticsCard(BuildContext context, WidgetRef ref) {
final isEnabled = ref.watch(fieldTestModeEnabledProvider);
final activeSession = ref.watch(activeFieldTestSessionProvider);
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color(0xFF0D0D0D),
borderRadius: BorderRadius.circular(16),
border: Border.all(color: const Color(0xFF1F1F1F), width: 1.2),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Icon(Icons.biotech, color: isEnabled ? Colors.greenAccent : const Color(0xFF00E5FF), size: 18),
const SizedBox(width: 8),
const Text(
'ENGINEERING & DIAGNOSTICS',
style: TextStyle(
fontFamily: 'Inter',
fontSize: 12,
fontWeight: FontWeight.w800,
color: Colors.white,
letterSpacing: 0.1,
),
),
],
),
Switch(
value: isEnabled,
activeThumbColor: Colors.greenAccent,
onChanged: (val) {
ref.read(fieldTestModeEnabledProvider.notifier).setEnabled(val);
},
),
],
),
const SizedBox(height: 6),
const Text(
'High-resolution black-box recorder for sensor fusion, GPS, and Kalman filter diagnostics.',
style: TextStyle(fontFamily: 'Inter', fontSize: 10, color: Colors.white60, height: 1.3),
),
const SizedBox(height: 12),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text('RECORDER STATUS:', style: TextStyle(fontFamily: 'Inter', color: Colors.white54, fontSize: 10, fontWeight: FontWeight.bold)),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
decoration: BoxDecoration(
color: activeSession != null
? Colors.greenAccent.withValues(alpha: 0.15)
: (isEnabled ? const Color(0xFF00E5FF).withValues(alpha: 0.15) : Colors.white10),
borderRadius: BorderRadius.circular(6),
border: Border.all(
color: activeSession != null
? Colors.greenAccent
: (isEnabled ? const Color(0xFF00E5FF) : Colors.white24),
),
),
child: Text(
activeSession != null
? 'RECORDING (Ride #$activeSession)'
: (isEnabled ? 'ARMED (Auto-starts on Ride)' : 'OFF'),
style: TextStyle(
fontFamily: 'Inter',
fontSize: 10,
fontWeight: FontWeight.bold,
color: activeSession != null
? Colors.greenAccent
: (isEnabled ? const Color(0xFF00E5FF) : Colors.white54),
),
),
),
],
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: ElevatedButton.icon(
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF00E5FF).withValues(alpha: 0.15),
foregroundColor: const Color(0xFF00E5FF),
side: const BorderSide(color: Color(0xFF00E5FF)),
padding: const EdgeInsets.symmetric(vertical: 10),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
icon: const Icon(Icons.assessment, size: 16),
label: const Text('VIEW REPORTS', style: TextStyle(fontFamily: 'Inter', fontSize: 10, fontWeight: FontWeight.w800)),
onPressed: () => context.go('/settings/field-test-report'),
),
),
const SizedBox(width: 8),
Expanded(
child: ElevatedButton.icon(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.white10,
foregroundColor: Colors.white,
side: const BorderSide(color: Colors.white24),
padding: const EdgeInsets.symmetric(vertical: 10),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
icon: const Icon(Icons.stream, size: 16),
label: const Text('LIVE STREAM', style: TextStyle(fontFamily: 'Inter', fontSize: 10, fontWeight: FontWeight.w800)),
onPressed: () => context.go('/settings/field-test-live'),
),
),
],
),
],
),
);
}
Widget _buildTomTomCard(BuildContext context, WidgetRef ref) {
final tomtomStatus = ref.watch(tomtomStatusProvider);
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color(0xFF0D0D0D),
borderRadius: BorderRadius.circular(16),
border: Border.all(color: const Color(0xFF1F1F1F), width: 1.2),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Row(
children: [
Icon(Icons.traffic, color: Color(0xFF00E5FF), size: 18),
SizedBox(width: 8),
Text(
'TOMTOM TRAFFIC PROVIDER',
style: TextStyle(
fontFamily: 'Inter',
fontSize: 12,
fontWeight: FontWeight.w800,
color: Colors.white,
letterSpacing: 0.1,
),
),
],
),
_buildStatusBadge(tomtomStatus),
],
),
const SizedBox(height: 8),
const Text(
'Supplies real-time accidents, hazards, road closures, and construction feeds.',
style: TextStyle(fontFamily: 'Inter', fontSize: 10, color: Colors.white60, height: 1.3),
),
const SizedBox(height: 12),
TextField(
controller: _tomtomKeyController,
decoration: InputDecoration(
labelText: 'TomTom API Key',
labelStyle: const TextStyle(fontFamily: 'Inter', fontSize: 11, color: Colors.white60),
hintText: 'Enter your TomTom API Key',
hintStyle: const TextStyle(fontFamily: 'Inter', fontSize: 11, color: Colors.white30),
filled: true,
fillColor: const Color(0xFF141414),
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8), borderSide: const BorderSide(color: Color(0xFF222222))),
enabledBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(8), borderSide: const BorderSide(color: Color(0xFF222222))),
focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(8), borderSide: const BorderSide(color: Color(0xFF00E5FF))),
isDense: true,
),
style: const TextStyle(fontFamily: 'Inter', fontSize: 12, color: Colors.white),
onChanged: (val) {
ref.read(tomtomApiKeyProvider.notifier).setApiKey(val);
},
),
const SizedBox(height: 12),
Align(
alignment: Alignment.centerRight,
child: ElevatedButton.icon(
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF00E5FF).withValues(alpha: 0.15),
foregroundColor: const Color(0xFF00E5FF),
side: const BorderSide(color: Color(0xFF00E5FF)),
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
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);
String msg = status == RoadAlertProviderStatus.connected
? 'TomTom API Connected successfully!'
: 'Status: ${status.name}';
messenger.showSnackBar(SnackBar(content: Text(msg)));
},
icon: _isTestingTomTom
? const SizedBox(width: 14, height: 14, child: CircularProgressIndicator(strokeWidth: 2, color: Color(0xFF00E5FF)))
: const Icon(Icons.sync, size: 16),
label: const Text('TEST CONNECTION', style: TextStyle(fontFamily: 'Inter', fontSize: 10, fontWeight: FontWeight.w800)),
),
),
],
),
);
}
Widget _buildCommunityCard(BuildContext context, WidgetRef ref) {
final communityStatus = ref.watch(communityStatusProvider);
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color(0xFF0D0D0D),
borderRadius: BorderRadius.circular(16),
border: Border.all(color: const Color(0xFF1F1F1F), width: 1.2),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Row(
children: [
Icon(Icons.local_police, color: Color(0xFF00E5FF), size: 18),
SizedBox(width: 8),
Text(
'COMMUNITY TRAFFIC PROVIDER',
style: TextStyle(
fontFamily: 'Inter',
fontSize: 12,
fontWeight: FontWeight.w800,
color: Colors.white,
letterSpacing: 0.1,
),
),
],
),
_buildStatusBadge(communityStatus),
],
),
const SizedBox(height: 8),
const Text(
'Live crowdsourced speed traps, police radars, and hazard reports.',
style: TextStyle(fontFamily: 'Inter', fontSize: 10, color: Colors.white60, height: 1.3),
),
const SizedBox(height: 12),
TextField(
controller: _communityKeyController,
decoration: InputDecoration(
labelText: 'Community API Key',
labelStyle: const TextStyle(fontFamily: 'Inter', fontSize: 11, color: Colors.white60),
hintText: 'Enter API Key',
hintStyle: const TextStyle(fontFamily: 'Inter', fontSize: 11, color: Colors.white30),
filled: true,
fillColor: const Color(0xFF141414),
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8), borderSide: const BorderSide(color: Color(0xFF222222))),
enabledBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(8), borderSide: const BorderSide(color: Color(0xFF222222))),
focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(8), borderSide: const BorderSide(color: Color(0xFF00E5FF))),
isDense: true,
),
style: const TextStyle(fontFamily: 'Inter', fontSize: 12, color: Colors.white),
onChanged: (val) {
ref.read(communityApiKeyProvider.notifier).setApiKey(val);
},
),
const SizedBox(height: 10),
TextField(
controller: _communityEndpointController,
decoration: InputDecoration(
labelText: 'Custom Endpoint URL (Optional)',
labelStyle: const TextStyle(fontFamily: 'Inter', fontSize: 11, color: Colors.white60),
hintText: 'e.g. https://google-map-places.p.rapidapi.com',
hintStyle: const TextStyle(fontFamily: 'Inter', fontSize: 11, color: Colors.white30),
filled: true,
fillColor: const Color(0xFF141414),
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8), borderSide: const BorderSide(color: Color(0xFF222222))),
enabledBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(8), borderSide: const BorderSide(color: Color(0xFF222222))),
focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(8), borderSide: const BorderSide(color: Color(0xFF00E5FF))),
isDense: true,
),
style: const TextStyle(fontFamily: 'Inter', fontSize: 12, color: Colors.white),
onChanged: (val) {
ref.read(communityEndpointUrlProvider.notifier).setEndpoint(val);
},
),
const SizedBox(height: 12),
Align(
alignment: Alignment.centerRight,
child: ElevatedButton.icon(
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF00E5FF).withValues(alpha: 0.15),
foregroundColor: const Color(0xFF00E5FF),
side: const BorderSide(color: Color(0xFF00E5FF)),
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
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);
String msg = status == RoadAlertProviderStatus.connected
? 'Community API Connected successfully!'
: 'Status: ${status.name}';
messenger.showSnackBar(SnackBar(content: Text(msg)));
},
icon: _isTestingCommunity
? const SizedBox(width: 14, height: 14, child: CircularProgressIndicator(strokeWidth: 2, color: Color(0xFF00E5FF)))
: const Icon(Icons.sync, size: 16),
label: const Text('TEST CONNECTION', style: TextStyle(fontFamily: 'Inter', fontSize: 10, fontWeight: FontWeight.w800)),
),
),
],
),
);
}
Widget _buildStatusBadge(RoadAlertProviderStatus status) {
Color color = Colors.white30;
String text = 'OFF';
if (status == RoadAlertProviderStatus.connected) {
color = const Color(0xFF69F0AE);
text = 'CONNECTED';
} else if (status == RoadAlertProviderStatus.authError) {
color = const Color(0xFFFF5252);
text = 'AUTH ERROR';
} else if (status == RoadAlertProviderStatus.rateLimited) {
color = const Color(0xFFFFD740);
text = 'RATE LIMITED';
} else if (status == RoadAlertProviderStatus.networkError) {
color = const Color(0xFFFF5252);
text = 'NETWORK ERROR';
}
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
decoration: BoxDecoration(
color: color.withValues(alpha: 0.15),
borderRadius: BorderRadius.circular(6),
border: Border.all(color: color, width: 0.8),
),
child: Text(
text,
style: TextStyle(
fontFamily: 'Inter',
fontSize: 9,
fontWeight: FontWeight.w800,
color: color,
),
),
);
}
Widget _buildVersionFooter() {
return FutureBuilder<String>(
future: AppInfo.versionString(),
builder: (context, snapshot) {
final ver = snapshot.data ?? AppInfo.fallbackVersion;
return Center(
child: Column(
children: [
Text(
'MotorSignal v$ver',
style: const TextStyle(
fontFamily: 'Inter',
fontSize: 12,
fontWeight: FontWeight.w700,
color: Colors.white54,
letterSpacing: 1.0,
),
),
const SizedBox(height: 2),
const Text(
'High-Performance Motorcycle Telemetry Engine',
style: TextStyle(
fontFamily: 'Inter',
fontSize: 9,
color: Colors.white30,
),
),
],
),
);
},
);
}
}