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 '../../core/app_info.dart'; import '../../core/responsive/viewport_size.dart'; import '../../core/theme/theme_extensions.dart'; import '../../core/theme/theme_mode_provider.dart'; import '../../core/theme/app_typography.dart'; import '../../database/database.dart'; import '../../features/rides/ride_state_machine.dart'; import '../../services/alerts/road_alert_models.dart'; import '../../services/alerts/road_alert_providers.dart'; import '../../services/alerts/road_alert_service.dart'; import '../../services/auth/auth_providers.dart'; import '../../services/field_test/field_test_logger.dart'; import '../../services/map/map_provider_interface.dart'; import '../../services/map/map_providers.dart'; import '../../services/osm_oauth_service.dart'; import '../../services/sensor_fusion_service.dart'; import 'widgets/current_plan_card.dart'; class SettingsScreen extends ConsumerStatefulWidget { const SettingsScreen({super.key}); @override ConsumerState createState() => _SettingsScreenState(); } class _SettingsScreenState extends ConsumerState { 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 snapshot = context.responsive; final isWide = snapshot.isExpanded && !snapshot.isShortViewport; return Scaffold( appBar: AppBar(title: const Text('Settings')), body: SafeArea( top: false, child: isWide ? _buildWideLayout(context) : _buildCompactLayout(context), ), ); } Widget _buildWideLayout(BuildContext context) { return SingleChildScrollView( padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 16.0), child: Center( child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 1200), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Left Column: Account, Plan, Appearance, Sensors Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ _buildSectionHeader('Membership & Entitlement'), const SizedBox(height: 10), const CurrentPlanCard(), const SizedBox(height: 24), _buildSectionHeader('Account & Cloud Identity'), const SizedBox(height: 10), _buildAccountCard(context), const SizedBox(height: 24), _buildSectionHeader('Appearance & Display Theme'), const SizedBox(height: 10), _buildAppearanceCard(context), const SizedBox(height: 24), _buildSectionHeader('Sensors & Calibration'), const SizedBox(height: 10), _buildSensorsCard(context), ], ), ), const SizedBox(width: 24), // Right Column: Maps, Road Alerts, Data & Diagnostics Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ _buildSectionHeader('Maps & Navigation Engine'), const SizedBox(height: 10), _buildMapsCard(context), const SizedBox(height: 24), _buildSectionHeader('Road Alerts & Incidents'), const SizedBox(height: 10), _buildRoadAlertsCard(context), const SizedBox(height: 16), _buildCommunityProviderCard(context), const SizedBox(height: 16), _buildTomTomProviderCard(context), if (kDebugMode) ...[ const SizedBox(height: 16), _buildDeveloperAlertsCard(context), ], const SizedBox(height: 24), _buildSectionHeader('Data & Diagnostics'), const SizedBox(height: 10), _buildDiagnosticsLinkCard(context), const SizedBox(height: 12), _buildClearHistoryCard(context), const SizedBox(height: 16), _buildFieldTestSection(context), ], ), ), ], ), const SizedBox(height: 32), const _AppVersionFooter(), const SizedBox(height: 16), ], ), ), ), ); } Widget _buildCompactLayout(BuildContext context) { return ListView( padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), children: [ _buildSettingsSection( context, title: 'Membership & Entitlement', icon: Icons.workspace_premium_outlined, initiallyExpanded: true, children: const [CurrentPlanCard()], ), _buildSettingsSection( context, title: 'Account & Cloud Identity', icon: Icons.account_circle_outlined, initiallyExpanded: true, children: [_buildAccountCard(context)], ), _buildSettingsSection( context, title: 'Appearance & Sensors', icon: Icons.tune, children: [_buildAppearanceCard(context), _buildSensorsCard(context)], ), _buildSettingsSection( context, title: 'Maps & Navigation', icon: Icons.map_outlined, children: [_buildMapsCard(context)], ), _buildSettingsSection( context, title: 'Road Alerts & Providers', icon: Icons.warning_amber_outlined, children: [ _buildRoadAlertsCard(context), _buildCommunityProviderCard(context), _buildTomTomProviderCard(context), if (kDebugMode) _buildDeveloperAlertsCard(context), ], ), _buildSettingsSection( context, title: 'Data & Diagnostics', icon: Icons.fact_check_outlined, children: [ _buildDiagnosticsLinkCard(context), _buildClearHistoryCard(context), _buildFieldTestSection(context), ], ), const SizedBox(height: 28), const _AppVersionFooter(), const SizedBox(height: 16), ], ); } Widget _buildSettingsSection( BuildContext context, { required String title, required IconData icon, required List children, bool initiallyExpanded = false, }) { final colors = context.motorColors; return Padding( padding: const EdgeInsets.only(bottom: 8), child: ExpansionTile( initiallyExpanded: initiallyExpanded, tilePadding: const EdgeInsets.symmetric(horizontal: 12), childrenPadding: const EdgeInsets.fromLTRB(0, 0, 0, 8), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(10), side: BorderSide(color: colors.outlineVariant), ), collapsedShape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(10), side: BorderSide(color: colors.outlineVariant), ), leading: Icon(icon, color: colors.cyberCyan), title: Text( title, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w800), ), children: [ for (var i = 0; i < children.length; i++) ...[ if (i > 0) const SizedBox(height: 12), children[i], ], ], ), ); } Widget _buildSectionHeader(String title) { return Text( title, style: TextStyle( fontFamily: AppTypography.fontHeadline, fontSize: 13, fontWeight: FontWeight.w700, letterSpacing: 0.2, color: context.motorColors.cyberCyan, ), ); } Widget _buildAccountCard(BuildContext context) { final colors = context.motorColors; return Card( clipBehavior: Clip.antiAlias, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(14), side: BorderSide(color: colors.outlineVariant), ), child: Consumer( builder: (context, ref, _) { final authState = ref.watch(authNotifierProvider); final isAuthed = authState.isAuthenticated; final user = authState.user; return InkWell( key: const ValueKey('settings_account_card'), onTap: () => context.go('/settings/account'), child: Padding( padding: const EdgeInsets.all(16.0), child: Row( children: [ Icon( isAuthed ? Icons.account_circle : Icons.account_circle_outlined, color: isAuthed ? colors.cyberCyan : colors.outline, size: 28, ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( isAuthed ? (user?.displayLabel ?? 'Rider Account') : 'Sign In / Cloud Account', style: const TextStyle( fontSize: 14, fontWeight: FontWeight.w700, ), ), const SizedBox(height: 2), Text( isAuthed ? (user?.email ?? 'Cloud Identity Active • Offline rides protected') : 'Optional cloud identity • Local riding always active', style: TextStyle( fontSize: 12, color: Theme.of( context, ).colorScheme.onSurfaceVariant, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), ], ), ), const SizedBox(width: 8), Container( padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 3, ), decoration: BoxDecoration( color: isAuthed ? colors.cyberCyan.withValues(alpha: 0.15) : colors.outlineVariant.withValues(alpha: 0.15), borderRadius: BorderRadius.circular(6), border: Border.all( color: isAuthed ? colors.cyberCyan : colors.outlineVariant, ), ), child: Text( isAuthed ? 'SIGNED IN' : 'LOCAL', style: TextStyle( fontFamily: 'Inter', fontSize: 10, fontWeight: FontWeight.bold, color: isAuthed ? colors.cyberCyan : colors.outline, letterSpacing: 0.5, ), ), ), const SizedBox(width: 6), Icon(Icons.chevron_right, color: colors.outline, size: 20), ], ), ), ); }, ), ); } Widget _buildAppearanceCard(BuildContext context) { final colors = context.motorColors; return Card( clipBehavior: Clip.antiAlias, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(14), side: BorderSide(color: colors.outlineVariant), ), child: Padding( padding: const EdgeInsets.all(16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Icon( Icons.brightness_medium_outlined, color: colors.cyberCyan, size: 20, ), const SizedBox(width: 10), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'Display Appearance', style: TextStyle( fontSize: 14, fontWeight: FontWeight.w700, ), ), const SizedBox(height: 2), Text( 'System default, Daylight Light mode, or Cockpit OLED Dark mode.', style: TextStyle( fontSize: 12, color: Theme.of(context).colorScheme.onSurfaceVariant, height: 1.3, ), ), ], ), ), ], ), const SizedBox(height: 16), Consumer( builder: (context, ref, _) { final appThemeMode = ref.watch(appThemeModeProvider); return SizedBox( width: double.infinity, child: SegmentedButton( showSelectedIcon: false, style: const ButtonStyle( tapTargetSize: MaterialTapTargetSize.shrinkWrap, visualDensity: VisualDensity.compact, ), segments: const [ ButtonSegment( value: ThemeMode.system, label: Text( 'System', style: TextStyle( fontSize: 12, fontWeight: FontWeight.bold, ), ), icon: Icon(Icons.settings_brightness, size: 16), ), ButtonSegment( value: ThemeMode.light, label: Text( 'Light', style: TextStyle( fontSize: 12, fontWeight: FontWeight.bold, ), ), icon: Icon(Icons.light_mode, size: 16), ), ButtonSegment( value: ThemeMode.dark, label: Text( 'Dark', style: TextStyle( fontSize: 12, fontWeight: FontWeight.bold, ), ), icon: Icon(Icons.dark_mode, size: 16), ), ], selected: {appThemeMode}, onSelectionChanged: (Set selection) { ref .read(appThemeModeProvider.notifier) .setThemeMode(selection.first); }, ), ); }, ), ], ), ), ); } Widget _buildSensorsCard(BuildContext context) { final colors = context.motorColors; return Card( clipBehavior: Clip.antiAlias, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(14), side: BorderSide(color: colors.outlineVariant), ), child: Padding( padding: const EdgeInsets.all(16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Icon(Icons.screen_rotation, color: colors.cyberCyan, size: 20), const SizedBox(width: 10), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'Calibrate Mount Angle', style: TextStyle( fontSize: 14, fontWeight: FontWeight.w700, ), ), const SizedBox(height: 2), Text( 'Place the motorcycle upright and tap Calibrate to zero the lean angle sensor.', style: TextStyle( fontSize: 12, color: Theme.of(context).colorScheme.onSurfaceVariant, height: 1.3, ), ), ], ), ), ], ), const SizedBox(height: 14), Align( alignment: Alignment.centerRight, child: ElevatedButton.icon( style: ElevatedButton.styleFrom( backgroundColor: colors.cyberCyan, foregroundColor: Colors.black, minimumSize: const Size(120, 48), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), ), ), icon: const Icon(Icons.tune, size: 18), label: const Text( 'CALIBRATE', style: TextStyle(fontSize: 12, fontWeight: FontWeight.w800), ), onPressed: () async { 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(); if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Sensors Calibrated upright!'), ), ); } }, ), ), ], ), ), ); } Widget _buildMapsCard(BuildContext context) { final colors = context.motorColors; final mapType = ref.watch(mapProviderTypeNotifierProvider); return Card( clipBehavior: Clip.antiAlias, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(14), side: BorderSide(color: colors.outlineVariant), ), child: Padding( padding: const EdgeInsets.all(16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Icon(Icons.map_outlined, color: colors.cyberCyan, size: 20), const SizedBox(width: 10), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'Active Map Engine', style: TextStyle( fontSize: 14, fontWeight: FontWeight.w700, ), ), const SizedBox(height: 2), Text( 'Select map rendering backend for live map, route preview, and replay.', style: TextStyle( fontSize: 12, color: Theme.of(context).colorScheme.onSurfaceVariant, height: 1.3, ), ), ], ), ), ], ), const SizedBox(height: 16), SizedBox( width: double.infinity, child: SegmentedButton( showSelectedIcon: false, style: const ButtonStyle( tapTargetSize: MaterialTapTargetSize.shrinkWrap, visualDensity: VisualDensity.compact, ), segments: const [ ButtonSegment( value: AppMapType.openStreetMap, label: FittedBox( fit: BoxFit.scaleDown, child: Text( 'OpenStreetMap', style: TextStyle( fontSize: 12, fontWeight: FontWeight.bold, ), ), ), icon: Icon(Icons.public, size: 16), ), ButtonSegment( value: AppMapType.googleMaps, label: FittedBox( fit: BoxFit.scaleDown, child: Text( 'Google Maps', style: TextStyle( fontSize: 12, fontWeight: FontWeight.bold, ), ), ), icon: Icon(Icons.map_outlined, size: 16), ), ], selected: {mapType}, onSelectionChanged: (Set selection) { ref .read(mapProviderTypeNotifierProvider.notifier) .setMapProviderType(selection.first); }, ), ), const SizedBox(height: 16), const Divider(), const SizedBox(height: 8), Consumer( builder: (context, ref, _) { final osmAuth = ref.watch(osmOAuthServiceProvider); final isAuthed = osmAuth.isAuthenticated; final user = osmAuth.user; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Expanded( child: Text( 'OpenStreetMap Account', overflow: TextOverflow.ellipsis, style: TextStyle( fontWeight: FontWeight.w600, fontSize: 13, ), ), ), const SizedBox(width: 8), Chip( label: Text( isAuthed ? 'Connected' : 'Disconnected', style: TextStyle( fontSize: 10, fontWeight: FontWeight.bold, color: isAuthed ? colors.cyberCyan : colors.outline, ), ), backgroundColor: isAuthed ? colors.cyberCyan.withValues(alpha: 0.15) : colors.outlineVariant.withValues(alpha: 0.2), visualDensity: VisualDensity.compact, padding: EdgeInsets.zero, ), ], ), const SizedBox(height: 6), if (isAuthed && user != null) ...[ ListTile( contentPadding: EdgeInsets.zero, leading: CircleAvatar( backgroundImage: user.avatarUrl != null ? NetworkImage(user.avatarUrl!) : null, child: user.avatarUrl == null ? const Icon(Icons.person) : null, ), title: Text( user.displayName, style: const TextStyle(fontWeight: FontWeight.bold), ), subtitle: Text( 'Changesets: ${user.changesetCount} • GPS Trace Sync Enabled', style: const TextStyle(fontSize: 11), ), trailing: TextButton.icon( style: TextButton.styleFrom( foregroundColor: colors.raceRed, ), icon: const Icon(Icons.link_off, size: 16), label: const Text( 'Disconnect', style: TextStyle(fontSize: 12), ), onPressed: () => ref .read(osmOAuthServiceProvider.notifier) .logout(), ), ), ] else ...[ Text( 'Sign in to OpenStreetMap via OAuth 2.0 PKCE to upload ride GPX tracks and contribute traces.', style: TextStyle( fontSize: 11, color: Theme.of(context).colorScheme.onSurfaceVariant, ), ), const SizedBox(height: 10), ElevatedButton.icon( style: ElevatedButton.styleFrom( backgroundColor: colors.cyberCyan, foregroundColor: Colors.black, minimumSize: const Size(double.infinity, 48), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), ), ), icon: const Icon(Icons.login, size: 18), label: const Text( 'Connect OpenStreetMap', style: TextStyle( fontWeight: FontWeight.bold, fontSize: 12, ), ), onPressed: osmAuth.isLoading ? null : () => ref .read(osmOAuthServiceProvider.notifier) .startLoginFlow(), ), ], ], ); }, ), ], ), ), ); } Widget _buildRoadAlertsCard(BuildContext context) { final colors = context.motorColors; final roadAlertsEnabled = ref.watch(roadAlertsEnabledProvider); final audioAlertsEnabled = ref.watch(audioAlertsEnabledProvider); final alertRadiusMeters = ref.watch(alertRadiusMetersProvider); final enabledCategories = ref.watch(enabledCategoriesProvider); return Card( clipBehavior: Clip.antiAlias, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(14), side: BorderSide(color: colors.outlineVariant), ), child: Padding( padding: const EdgeInsets.all(16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ SwitchListTile( activeThumbColor: colors.cyberCyan, contentPadding: EdgeInsets.zero, title: const Text( 'Road Alerts', style: TextStyle(fontWeight: FontWeight.w700, fontSize: 14), ), subtitle: Text( 'Display real-time police, hazards, and traffic incidents on map.', style: TextStyle( fontSize: 11, color: Theme.of(context).colorScheme.onSurfaceVariant, ), ), value: roadAlertsEnabled, onChanged: (val) { ref.read(roadAlertsEnabledProvider.notifier).setEnabled(val); }, ), const Divider(), SwitchListTile( activeThumbColor: colors.cyberCyan, contentPadding: EdgeInsets.zero, title: const Text( 'Audio Warnings (TTS)', style: TextStyle(fontWeight: FontWeight.w700, fontSize: 14), ), subtitle: Text( 'Announce approaching hazards and enforcement over helmet headset.', style: TextStyle( fontSize: 11, color: Theme.of(context).colorScheme.onSurfaceVariant, ), ), value: audioAlertsEnabled, onChanged: roadAlertsEnabled ? (val) { ref .read(audioAlertsEnabledProvider.notifier) .setEnabled(val); } : null, ), const Divider(), Padding( padding: const EdgeInsets.symmetric(vertical: 6.0), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'Alert Radius', style: TextStyle( fontWeight: FontWeight.w700, fontSize: 14, ), ), const SizedBox(height: 2), Text( 'Distance to detect upcoming incidents', style: TextStyle( fontSize: 11, color: Theme.of( context, ).colorScheme.onSurfaceVariant, ), ), ], ), ), const SizedBox(width: 12), DropdownButton( 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(vertical: 6.0), child: Text( 'Alert Categories', style: TextStyle(fontWeight: FontWeight.w700, fontSize: 14), ), ), ...RoadAlertCategory.values.map((cat) { final isChecked = enabledCategories.contains(cat); return CheckboxListTile( activeColor: colors.cyberCyan, checkColor: Colors.black, dense: true, visualDensity: VisualDensity.compact, contentPadding: EdgeInsets.zero, title: Text( cat.displayName, style: const TextStyle( fontSize: 13, fontWeight: FontWeight.w500, ), ), secondary: Icon(cat.icon, color: cat.color, size: 20), value: isChecked, onChanged: roadAlertsEnabled ? (val) { ref .read(enabledCategoriesProvider.notifier) .toggleCategory(cat, val ?? true); } : null, ); }), ], ), ), ); } Widget _buildCommunityProviderCard(BuildContext context) { final colors = context.motorColors; final communityStatus = ref.watch(communityStatusProvider); return Card( clipBehavior: Clip.antiAlias, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(14), side: BorderSide(color: colors.outlineVariant), ), child: Padding( padding: const EdgeInsets.all(16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildProviderHeader( icon: Icons.local_police, iconColor: colors.velocityBlue, title: 'Community Traffic Provider', subtitle: 'Supplies Police, Speed Cameras, and Crowd Hazards.', status: communityStatus, ), const SizedBox(height: 16), TextField( controller: _communityKeyController, decoration: const InputDecoration( labelText: 'Community / RapidAPI Key', hintText: 'Enter OpenWebNinja, RapidAPI, or Community API Key', helperText: 'Supplies Police, Speed Cameras, and Crowd Hazards.', helperMaxLines: 2, border: OutlineInputBorder(), isDense: true, ), style: const TextStyle(fontSize: 14), onChanged: (val) { ref.read(communityApiKeyProvider.notifier).setApiKey(val); }, ), const SizedBox(height: 10), TextField( controller: _communityEndpointController, decoration: const InputDecoration( labelText: 'Custom Endpoint URL (Optional)', hintText: 'e.g. https://google-map-places.p.rapidapi.com or custom URL', helperText: 'Leave blank for automatic OpenWebNinja / RapidAPI detection.', helperMaxLines: 2, border: OutlineInputBorder(), isDense: true, ), style: const TextStyle(fontSize: 13), onChanged: (val) { ref .read(communityEndpointUrlProvider.notifier) .setEndpoint(val); }, ), const SizedBox(height: 12), Align( alignment: Alignment.centerRight, child: ElevatedButton.icon( style: ElevatedButton.styleFrom( minimumSize: const Size(140, 48), 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); if (mounted) { String msg = ''; if (status == RoadAlertProviderStatus.connected) { msg = 'Community / OpenWebNinja API Connected successfully!'; } else if (status == RoadAlertProviderStatus.authError) { msg = 'Authentication Error: Invalid or unauthorized key.'; } else if (status == RoadAlertProviderStatus.notConfigured) { msg = 'API Key is empty.'; } else if (status == RoadAlertProviderStatus.networkError) { msg = 'Network Error: Check internet connection or endpoint.'; } else if (status == RoadAlertProviderStatus.rateLimited) { msg = 'Rate Limited: Plan limit reached.'; } else { msg = 'Status: ${status.name}'; } messenger.showSnackBar(SnackBar(content: Text(msg))); } }, icon: _isTestingCommunity ? const SizedBox( width: 16, height: 16, child: CircularProgressIndicator(strokeWidth: 2), ) : const Icon(Icons.sync, size: 18), label: const Text('Test Connection'), ), ), ], ), ), ); } Widget _buildTomTomProviderCard(BuildContext context) { final colors = context.motorColors; final tomtomStatus = ref.watch(tomtomStatusProvider); return Card( clipBehavior: Clip.antiAlias, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(14), side: BorderSide(color: colors.outlineVariant), ), child: Padding( padding: const EdgeInsets.all(16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildProviderHeader( icon: Icons.security, iconColor: colors.cyberCyan, title: 'TomTom Traffic Provider', subtitle: 'Supplies Accidents, Hazards, Closures & Construction data.', status: tomtomStatus, ), const SizedBox(height: 16), TextField( controller: _tomtomKeyController, decoration: const InputDecoration( labelText: 'TomTom API Key', hintText: 'Enter your free TomTom API Key', helperText: 'Supplies Accidents, Hazards, Closures & Construction data.', helperMaxLines: 2, border: OutlineInputBorder(), isDense: true, ), style: const TextStyle(fontSize: 14), onChanged: (val) { ref.read(tomtomApiKeyProvider.notifier).setApiKey(val); }, ), const SizedBox(height: 12), Align( alignment: Alignment.centerRight, child: ElevatedButton.icon( style: ElevatedButton.styleFrom( minimumSize: const Size(140, 48), 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); if (mounted) { String msg = ''; if (status == RoadAlertProviderStatus.connected) { msg = 'TomTom API Connected successfully!'; } else if (status == RoadAlertProviderStatus.authError) { msg = 'Authentication Error: Invalid Key.'; } else if (status == RoadAlertProviderStatus.notConfigured) { msg = 'Key is empty.'; } else if (status == RoadAlertProviderStatus.networkError) { msg = 'Network Error: Failed to connect to API.'; } else if (status == RoadAlertProviderStatus.rateLimited) { msg = 'Rate Limited: Too many requests.'; } else { msg = 'Status: ${status.name}'; } messenger.showSnackBar(SnackBar(content: Text(msg))); } }, icon: _isTestingTomTom ? const SizedBox( width: 16, height: 16, child: CircularProgressIndicator(strokeWidth: 2), ) : const Icon(Icons.sync, size: 18), label: const Text('Test Connection'), ), ), ], ), ), ); } Widget _buildDeveloperAlertsCard(BuildContext context) { return Card( color: Colors.orange.withValues(alpha: 0.08), shape: RoundedRectangleBorder( side: const BorderSide(color: Colors.orangeAccent), borderRadius: BorderRadius.circular(14), ), child: Padding( padding: const EdgeInsets.all(16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ const Icon( Icons.bug_report, color: Colors.orangeAccent, size: 20, ), const SizedBox(width: 10), Expanded( child: Text( 'Road Alert Test Mode (Debug Only)', style: const TextStyle( fontSize: 14, fontWeight: FontWeight.w700, color: Colors.orangeAccent, ), ), ), ], ), const SizedBox(height: 8), Text( 'Generate simulated test alerts for real-device testing.', style: TextStyle( fontSize: 12, color: Theme.of(context).colorScheme.onSurfaceVariant, ), ), const SizedBox(height: 14), Wrap( spacing: 8, runSpacing: 8, children: [ ElevatedButton.icon( style: ElevatedButton.styleFrom( minimumSize: const Size(0, 44), ), icon: const Icon( Icons.local_police, color: Colors.blueAccent, size: 18, ), label: const Text('Test Police (100m)'), onPressed: () { ref .read(roadAlertServiceProvider) .triggerTestAlert(category: RoadAlertCategory.police); ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Simulated Police Alert sent.'), ), ); }, ), ElevatedButton.icon( style: ElevatedButton.styleFrom( minimumSize: const Size(0, 44), ), icon: const Icon( Icons.warning, color: Colors.orangeAccent, size: 18, ), label: const Text('Test Hazard (250m)'), onPressed: () { ref .read(roadAlertServiceProvider) .triggerTestAlert(category: RoadAlertCategory.hazard); ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Simulated Hazard Alert sent.'), ), ); }, ), ], ), ], ), ), ); } Widget _buildDiagnosticsLinkCard(BuildContext context) { final colors = context.motorColors; return Card( clipBehavior: Clip.antiAlias, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(14), side: BorderSide(color: colors.outlineVariant), ), child: InkWell( onTap: () => context.go('/settings/diagnostics'), child: Padding( padding: const EdgeInsets.all(16.0), child: Row( children: [ Icon(Icons.monitor_heart, color: colors.cyberCyan, size: 20), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'Sensor Diagnostics', style: TextStyle( fontSize: 14, fontWeight: FontWeight.w700, ), ), const SizedBox(height: 2), Text( 'View raw IMU and GPS values in real time.', style: TextStyle( fontSize: 12, color: Theme.of(context).colorScheme.onSurfaceVariant, ), ), ], ), ), Icon(Icons.chevron_right, color: colors.outline, size: 20), ], ), ), ), ); } Widget _buildClearHistoryCard(BuildContext context) { final colors = context.motorColors; return Card( clipBehavior: Clip.antiAlias, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(14), side: BorderSide(color: colors.outlineVariant), ), child: Padding( padding: const EdgeInsets.all(16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Icon(Icons.delete_forever, color: colors.raceRed, size: 20), const SizedBox(width: 10), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'Clear Ride History', style: TextStyle( fontSize: 14, fontWeight: FontWeight.w700, ), ), const SizedBox(height: 2), Text( 'Permanently delete all recorded rides and performance runs on this device.', style: TextStyle( fontSize: 12, color: Theme.of(context).colorScheme.onSurfaceVariant, height: 1.3, ), ), ], ), ), ], ), const SizedBox(height: 14), Align( alignment: Alignment.centerRight, child: ElevatedButton( style: ElevatedButton.styleFrom( backgroundColor: colors.raceRed.withValues(alpha: 0.15), foregroundColor: colors.raceRed, side: BorderSide( color: colors.raceRed.withValues(alpha: 0.6), ), minimumSize: const Size(120, 48), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), ), ), 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( context: context, builder: (ctx) => AlertDialog( 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: Text( 'CLEAR ALL', style: TextStyle( color: colors.raceRed, fontWeight: FontWeight.bold, ), ), ), ], ), ); if (confirmed == true && 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.'), ), ); } } }, child: const Text('Clear History'), ), ), ], ), ), ); } Widget _buildFieldTestSection(BuildContext context) { final colors = context.motorColors; final isEnabled = ref.watch(fieldTestModeEnabledProvider); final activeSession = ref.watch(activeFieldTestSessionProvider); final logger = ref.watch(fieldTestLoggerProvider); return Card( clipBehavior: Clip.antiAlias, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(14), side: BorderSide(color: colors.outlineVariant), ), child: Padding( padding: const EdgeInsets.all(16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Expanded( child: Row( children: [ Icon( Icons.biotech, color: isEnabled ? colors.launchGreen : colors.outline, size: 24, ), const SizedBox(width: 10), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'Field Test Mode', style: TextStyle( fontSize: 15, fontWeight: FontWeight.w700, ), ), Text( 'Silent black-box diagnostic recorder', style: TextStyle( fontSize: 11, color: Theme.of( context, ).colorScheme.onSurfaceVariant, ), overflow: TextOverflow.ellipsis, ), ], ), ), ], ), ), const SizedBox(width: 8), Switch( value: isEnabled, activeThumbColor: colors.launchGreen, onChanged: (val) { ref .read(fieldTestModeEnabledProvider.notifier) .setEnabled(val); }, ), ], ), const Divider(height: 24), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( 'Logger Status:', style: TextStyle( color: Theme.of(context).colorScheme.onSurfaceVariant, fontSize: 13, ), ), Container( padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 3, ), decoration: BoxDecoration( color: activeSession != null ? colors.launchGreen.withValues(alpha: 0.15) : (isEnabled ? colors.cyberCyan.withValues(alpha: 0.15) : colors.outlineVariant.withValues(alpha: 0.15)), borderRadius: BorderRadius.circular(6), border: Border.all( color: activeSession != null ? colors.launchGreen : (isEnabled ? colors.cyberCyan : colors.outlineVariant), ), ), child: Text( activeSession != null ? 'RECORDING (Ride #$activeSession)' : (isEnabled ? 'ARMED (Auto-starts on Ride)' : 'OFF'), style: TextStyle( fontFamily: 'Inter', fontSize: 11, fontWeight: FontWeight.bold, color: activeSession != null ? colors.launchGreen : (isEnabled ? colors.cyberCyan : colors.outline), ), ), ), ], ), const SizedBox(height: 16), Wrap( spacing: 8, runSpacing: 8, children: [ ElevatedButton.icon( style: ElevatedButton.styleFrom( backgroundColor: colors.launchGreen.withValues(alpha: 0.2), foregroundColor: colors.launchGreen, side: BorderSide(color: colors.launchGreen), minimumSize: const Size(130, 48), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), ), ), icon: const Icon(Icons.fact_check, size: 18), label: const Text( 'Pre-Ride Check', style: TextStyle(fontSize: 12, fontWeight: FontWeight.bold), ), onPressed: () => context.go('/settings/field-test-pre-ride-check'), ), ElevatedButton.icon( style: ElevatedButton.styleFrom( backgroundColor: colors.cyberCyan.withValues(alpha: 0.15), foregroundColor: colors.cyberCyan, side: BorderSide(color: colors.cyberCyan), minimumSize: const Size(120, 48), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), ), ), icon: const Icon(Icons.assessment, size: 18), label: const Text( 'Reports', style: TextStyle(fontSize: 12, fontWeight: FontWeight.bold), ), onPressed: () => context.go('/settings/field-test-report'), ), OutlinedButton.icon( style: OutlinedButton.styleFrom( foregroundColor: Theme.of(context).colorScheme.onSurface, side: BorderSide(color: colors.outlineVariant), minimumSize: const Size(110, 48), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), ), ), icon: const Icon(Icons.receipt_long, size: 18), label: const Text( 'Live Log', style: TextStyle(fontSize: 12, fontWeight: FontWeight.bold), ), onPressed: () => context.go('/settings/field-test-live-log'), ), ], ), const SizedBox(height: 8), Align( alignment: Alignment.centerRight, child: TextButton.icon( style: TextButton.styleFrom(foregroundColor: colors.raceRed), icon: const Icon(Icons.delete_sweep, size: 16), label: const Text( 'Clear Field Test Logs', style: TextStyle(fontSize: 11, fontWeight: FontWeight.bold), ), onPressed: () async { final session = ref.read(rideStateMachineProvider); if (session.state == RideState.riding) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text( 'Cannot clear logs while a ride is recording.', ), ), ); return; } final confirm = await showDialog( context: context, builder: (ctx) => AlertDialog( title: const Text('Clear All Field Test Logs?'), content: const Text( 'This will delete all stored black-box diagnostic sessions. Standard ride history will NOT be affected.', ), actions: [ TextButton( onPressed: () => Navigator.pop(ctx, false), child: const Text('CANCEL'), ), TextButton( onPressed: () => Navigator.pop(ctx, true), child: Text( 'CLEAR', style: TextStyle( color: colors.raceRed, fontWeight: FontWeight.bold, ), ), ), ], ), ); if (confirm == true && context.mounted) { await logger.clearAllSessions(); if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Field test logs cleared.'), ), ); } } }, ), ), ], ), ), ); } Widget _buildProviderHeader({ required IconData icon, required Color iconColor, required String title, required String subtitle, required RoadAlertProviderStatus status, }) { return Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Icon(icon, color: iconColor, size: 20), const SizedBox(width: 10), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( title, style: const TextStyle( fontSize: 14, fontWeight: FontWeight.w700, ), softWrap: true, ), const SizedBox(height: 2), Text( subtitle, style: TextStyle( fontSize: 12, color: Theme.of(context).colorScheme.onSurfaceVariant, height: 1.3, ), softWrap: true, ), const SizedBox(height: 8), Align( alignment: Alignment.centerLeft, child: _buildStatusBadge(status), ), ], ), ), ], ); } Widget _buildStatusBadge(RoadAlertProviderStatus status) { final colors = context.motorColors; String label; Color color; switch (status) { case RoadAlertProviderStatus.connected: label = 'Connected'; color = colors.launchGreen; break; case RoadAlertProviderStatus.configured: label = 'Configured'; color = colors.cyberCyan; break; case RoadAlertProviderStatus.authError: label = 'Auth Error'; color = colors.raceRed; break; case RoadAlertProviderStatus.rateLimited: label = 'Rate Limited'; color = colors.warningAmber; break; case RoadAlertProviderStatus.networkError: label = 'Network Error'; color = colors.warningAmber; break; case RoadAlertProviderStatus.notConfigured: label = 'Not Set'; color = colors.outlineVariant; 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, ), ), ); } } /// Read-only footer that surfaces the app's build version inside Settings. class _AppVersionFooter extends StatefulWidget { const _AppVersionFooter(); @override State<_AppVersionFooter> createState() => _AppVersionFooterState(); } class _AppVersionFooterState extends State<_AppVersionFooter> { late Future _versionFuture; @override void initState() { super.initState(); _versionFuture = AppInfo.versionString(); } void _showIdentityDialog() { final identity = AppInfo.identity; final colors = context.motorColors; showDialog( context: context, builder: (ctx) => AlertDialog( title: Row( children: [ Icon(Icons.fingerprint, color: colors.cyberCyan), const SizedBox(width: 8), const Text( 'Software Identity', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), ), ], ), content: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ _diagRow('Application', 'MotorSignal'), _diagRow('Version', identity.semanticVersion), _diagRow('Build Number', '${identity.buildNumber}'), _diagRow('Release Channel', identity.releaseChannel), _diagRow('Telemetry Schema', '${identity.telemetrySchemaVersion}'), _diagRow('Export Schema', '${identity.exportSchemaVersion}'), _diagRow( 'Git Commit', identity.gitCommit.length > 12 ? identity.gitCommit.substring(0, 12) : identity.gitCommit, ), _diagRow('Build Timestamp', identity.buildTimestamp), ], ), actions: [ TextButton( onPressed: () => Navigator.of(ctx).pop(), child: Text( 'CLOSE', style: TextStyle( color: colors.cyberCyan, fontWeight: FontWeight.bold, ), ), ), ], ), ); } Widget _diagRow(String label, String value) { return Padding( padding: const EdgeInsets.symmetric(vertical: 4.0), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( label, style: TextStyle( color: Theme.of(context).colorScheme.onSurfaceVariant, fontSize: 12, ), ), Text( value, style: TextStyle( color: context.motorColors.cyberCyan, fontWeight: FontWeight.bold, fontSize: 12, fontFamily: 'monospace', ), ), ], ), ); } @override Widget build(BuildContext context) { return Center( child: InkWell( onTap: _showIdentityDialog, borderRadius: BorderRadius.circular(8), child: FutureBuilder( future: _versionFuture, builder: (context, snapshot) { final text = snapshot.data ?? AppInfo.fallbackVersion; return Padding( padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16), child: Column( children: [ Text( 'Motorcycle Dash v$text', textAlign: TextAlign.center, style: TextStyle( color: Theme.of(context).colorScheme.onSurfaceVariant, fontSize: 12, letterSpacing: 0.4, ), ), const SizedBox(height: 2), Text( 'Tap for forensic build identity', style: TextStyle( color: Theme.of( context, ).colorScheme.onSurfaceVariant.withValues(alpha: 0.6), fontSize: 9, ), ), ], ), ); }, ), ), ); } }