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

2528 lines
94 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 '../../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/alerts/widgets/quick_swarm_report_modal.dart';
import '../../services/auth/auth_providers.dart';
import '../../services/field_test/field_test_logger.dart';
import '../../services/sensor_fusion_service.dart';
import 'widgets/current_plan_card.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;
late TextEditingController _communityEndpointController;
late TextEditingController _hereKeyController;
bool _isTestingTomTom = false;
bool _isTestingCommunity = false;
bool _isTestingHere = false;
bool _isTestingWaze = false;
bool _isTestingZyla = false;
bool _isTestingGa511 = false;
@override
void initState() {
super.initState();
_tomtomKeyController = TextEditingController(
text: ref.read(tomtomApiKeyProvider),
);
_communityKeyController = TextEditingController(
text: ref.read(communityApiKeyProvider),
);
_communityEndpointController = TextEditingController(
text: ref.read(communityEndpointUrlProvider),
);
_hereKeyController = TextEditingController(
text: ref.read(hereApiKeyProvider),
);
}
@override
void dispose() {
_tomtomKeyController.dispose();
_communityKeyController.dispose();
_communityEndpointController.dispose();
_hereKeyController.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),
_buildSwarmProviderCard(context),
const SizedBox(height: 16),
_buildZylaProviderCard(context),
const SizedBox(height: 16),
_buildGa511ProviderCard(context),
const SizedBox(height: 16),
_buildWazeProviderCard(context),
const SizedBox(height: 16),
_buildOsmSpeedCameraProviderCard(context),
const SizedBox(height: 16),
_buildHereProviderCard(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),
_buildSwarmProviderCard(context),
_buildZylaProviderCard(context),
_buildGa511ProviderCard(context),
_buildWazeProviderCard(context),
_buildOsmSpeedCameraProviderCard(context),
_buildHereProviderCard(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<Widget> 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<ThemeMode>(
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<ThemeMode> 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;
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.satellite_alt_outlined, color: colors.cyberCyan, size: 20),
const SizedBox(width: 10),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Map & Navigation Engine',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w700,
),
),
const SizedBox(height: 2),
Text(
'Powered exclusively by Amazon Location Service.',
style: TextStyle(
fontSize: 12,
color: Theme.of(context).colorScheme.onSurfaceVariant,
height: 1.3,
),
),
],
),
),
],
),
const SizedBox(height: 16),
Container(
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: colors.surfaceContainer,
borderRadius: BorderRadius.circular(10),
border: Border.all(color: colors.cyberCyan.withValues(alpha: 0.3)),
),
child: Row(
children: [
Icon(Icons.check_circle_outline, color: colors.cyberCyan, size: 20),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Amazon Location Service Active',
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 3),
Text(
'High-resolution satellite imagery, live place search, and turn-by-turn motorcycle route generation via AWS Maps V2.',
style: TextStyle(
fontSize: 11,
color: Theme.of(context).colorScheme.onSurfaceVariant,
height: 1.3,
),
),
],
),
),
],
),
),
],
),
),
);
}
Widget _buildRoadAlertsCard(BuildContext context) {
final colors = context.motorColors;
final roadAlertsEnabled = ref.watch(roadAlertsEnabledProvider);
final audioAlertsEnabled = ref.watch(audioAlertsEnabledProvider);
final audioMode = ref.watch(roadAlertAudioModeProvider);
final leanInterlockEnabled = ref.watch(leanInterlockEnabledProvider);
final complianceEnabled = ref.watch(jurisdictionComplianceEnabledProvider);
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',
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,
),
if (roadAlertsEnabled && audioAlertsEnabled) ...[
Padding(
padding: const EdgeInsets.symmetric(vertical: 4.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Audio Warning Style',
style: TextStyle(fontWeight: FontWeight.w600, fontSize: 13),
),
const SizedBox(height: 6),
SegmentedButton<RoadAlertAudioMode>(
segments: const [
ButtonSegment(
value: RoadAlertAudioMode.voice,
icon: Icon(Icons.record_voice_over, size: 16),
label: Text('Voice (TTS)', style: TextStyle(fontSize: 12)),
),
ButtonSegment(
value: RoadAlertAudioMode.tacticalTone,
icon: Icon(Icons.graphic_eq, size: 16),
label: Text('Radar Chirp', style: TextStyle(fontSize: 12)),
),
],
selected: {audioMode},
onSelectionChanged: (newSelection) {
ref
.read(roadAlertAudioModeProvider.notifier)
.setMode(newSelection.first);
},
),
],
),
),
const SizedBox(height: 4),
SwitchListTile(
activeThumbColor: colors.cyberCyan,
contentPadding: EdgeInsets.zero,
title: const Text(
'Lean-Angle Cornering Safety Interlock',
style: TextStyle(fontWeight: FontWeight.w700, fontSize: 13),
),
subtitle: Text(
'Suppresses audio warnings while bike lean angle > 15° to prevent startling the rider mid-apex. Automatically announces once upright.',
style: TextStyle(
fontSize: 11,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
value: leanInterlockEnabled,
onChanged: (val) {
ref
.read(leanInterlockEnabledProvider.notifier)
.setEnabled(val);
},
),
],
const Divider(),
SwitchListTile(
activeThumbColor: colors.cyberCyan,
contentPadding: EdgeInsets.zero,
title: const Text(
'Automated Legal Compliance',
style: TextStyle(fontWeight: FontWeight.w700, fontSize: 13),
),
subtitle: Text(
'Automatically conforms camera warnings per region (US exact pinpoints, France Danger Zones, Germany/Switzerland silent).',
style: TextStyle(
fontSize: 11,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
value: complianceEnabled,
onChanged: roadAlertsEnabled
? (val) {
ref
.read(jurisdictionComplianceEnabledProvider.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<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(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 _buildSwarmProviderCard(BuildContext context) {
final colors = context.motorColors;
final swarmStatus = ref.watch(swarmStatusProvider);
final swarmEnabled = ref.watch(swarmAlertsEnabledProvider);
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.campaign,
iconColor: Colors.blueAccent,
title: 'MotorSignal Rider Swarm (US Live Alerts)',
subtitle:
'Real-time crowdsourced police traps, road debris, and hazard alerts from nearby riders and pack members across the United States.',
status: swarmStatus,
isEnabled: swarmEnabled,
),
const SizedBox(height: 12),
SwitchListTile.adaptive(
contentPadding: EdgeInsets.zero,
title: const Text(
'Enable Rider Swarm Alerts',
style: TextStyle(fontWeight: FontWeight.w600, fontSize: 14),
),
subtitle: Text(
'Receives live police and road hazard broadcasts from other MotorSignal riders.',
style: TextStyle(
fontSize: 12,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
value: swarmEnabled,
onChanged: (val) {
ref
.read(swarmAlertsEnabledProvider.notifier)
.setEnabled(val);
},
),
const SizedBox(height: 8),
Align(
alignment: Alignment.centerRight,
child: OutlinedButton.icon(
style: OutlinedButton.styleFrom(
foregroundColor: colors.cyberCyan,
side: BorderSide(color: colors.cyberCyan),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
icon: const Icon(Icons.add_alert, size: 18),
label: const Text(
'REPORT INCIDENT',
style: TextStyle(fontSize: 12, fontWeight: FontWeight.bold),
),
onPressed: () => QuickSwarmReportModal.show(context),
),
),
],
),
),
);
}
Widget _buildOsmSpeedCameraProviderCard(BuildContext context) {
final colors = context.motorColors;
final osmStatus = ref.watch(osmSpeedCameraStatusProvider);
final usCamerasEnabled = ref.watch(usSpeedCamerasEnabledProvider);
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.camera_alt,
iconColor: Colors.purpleAccent,
title: 'US Speed Cameras (Offline OSM)',
subtitle:
'Offline speed cameras, radar traps & photo enforcement across the United States. 100% operational in mountain passes & cellular dead zones.',
status: osmStatus,
isEnabled: usCamerasEnabled,
),
const SizedBox(height: 12),
SwitchListTile.adaptive(
contentPadding: EdgeInsets.zero,
title: const Text(
'Enable US Speed Cameras',
style: TextStyle(fontWeight: FontWeight.w600, fontSize: 14),
),
subtitle: Text(
'Uses local OpenStreetMap spatial database with zero cellular data needed.',
style: TextStyle(
fontSize: 12,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
value: usCamerasEnabled,
onChanged: (val) {
ref
.read(usSpeedCamerasEnabledProvider.notifier)
.setEnabled(val);
},
),
],
),
),
);
}
Widget _buildZylaProviderCard(BuildContext context) {
final colors = context.motorColors;
final zylaStatus = ref.watch(zylaStatusProvider);
final zylaEnabled = ref.watch(zylaRoadAlertsEnabledProvider);
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.radar,
iconColor: Colors.amberAccent,
title: 'Zyla Waze Data #4814 (Backend Gateway)',
subtitle:
'Commercial licensed Waze Data feed via MotorSignal API backend. Disabled by default; client never holds credentials.',
status: zylaStatus,
isEnabled: zylaEnabled,
),
const SizedBox(height: 12),
SwitchListTile.adaptive(
contentPadding: EdgeInsets.zero,
title: const Text(
'Enable Zyla Waze Alerts',
style: TextStyle(fontWeight: FontWeight.w600, fontSize: 14),
),
subtitle: Text(
'Receives verified police, hazard, accident, and road closure data through MotorSignal API gateway.',
style: TextStyle(
fontSize: 12,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
value: zylaEnabled,
onChanged: (val) {
ref
.read(zylaRoadAlertsEnabledProvider.notifier)
.setEnabled(val);
},
),
const SizedBox(height: 8),
Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: colors.surfaceContainer,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: colors.outlineVariant),
),
child: Row(
children: [
Icon(Icons.shield_outlined, size: 16, color: colors.cyberCyan),
const SizedBox(width: 8),
Expanded(
child: Text(
'Zero credential leakage. All requests query the secure MotorSignal FastAPI backend with cell-quantized caching.',
style: TextStyle(
fontSize: 11,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
),
],
),
),
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: _isTestingZyla
? null
: () async {
setState(() => _isTestingZyla = true);
final messenger = ScaffoldMessenger.of(context);
final provider = ref.read(zylaProviderInstance);
final status = await provider.testConnection('');
ref.read(zylaStatusProvider.notifier).state = status;
setState(() => _isTestingZyla = false);
if (mounted) {
messenger.showSnackBar(
SnackBar(
content: Text(
status == RoadAlertProviderStatus.connected
? 'Zyla adapter operational!'
: 'Zyla gateway status: ${status.name}',
),
),
);
}
},
icon: _isTestingZyla
? const SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Icon(Icons.sync, size: 18),
label: const Text('Test Connection'),
),
),
],
),
),
);
}
Widget _buildGa511ProviderCard(BuildContext context) {
final colors = context.motorColors;
final ga511Status = ref.watch(ga511StatusProvider);
final ga511Enabled = ref.watch(ga511AlertsEnabledProvider);
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.account_balance,
iconColor: Colors.deepOrangeAccent,
title: 'Georgia 511 (Experimental Official Feed)',
subtitle:
'Experimental GDOT traffic, incident, construction, and closure open data via MotorSignal backend gateway. Live GDOT production credentials unverified.',
status: ga511Status,
isEnabled: ga511Enabled,
),
const SizedBox(height: 12),
SwitchListTile.adaptive(
contentPadding: EdgeInsets.zero,
title: const Text(
'Enable Georgia 511 Alerts',
style: TextStyle(fontWeight: FontWeight.w600, fontSize: 14),
),
subtitle: Text(
'Streams public department of transportation events without API keys or accounts.',
style: TextStyle(
fontSize: 12,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
value: ga511Enabled,
onChanged: (val) {
ref
.read(ga511AlertsEnabledProvider.notifier)
.setEnabled(val);
},
),
const SizedBox(height: 8),
Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: colors.surfaceContainer,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: colors.outlineVariant),
),
child: Row(
children: [
Icon(Icons.info_outline, size: 16, color: colors.cyberCyan),
const SizedBox(width: 8),
Expanded(
child: Text(
'Experimental integration. Live GDOT production credentials unverified; uses bounded caching and verified incident datasets.',
style: TextStyle(
fontSize: 11,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
),
],
),
),
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: _isTestingGa511
? null
: () async {
setState(() => _isTestingGa511 = true);
final messenger = ScaffoldMessenger.of(context);
final provider = ref.read(ga511ProviderInstance);
final status = await provider.testConnection('');
ref.read(ga511StatusProvider.notifier).state = status;
setState(() => _isTestingGa511 = false);
if (mounted) {
messenger.showSnackBar(
SnackBar(
content: Text(
status == RoadAlertProviderStatus.connected
? 'Georgia 511 feed connected!'
: 'Georgia 511 gateway status: ${status.name}',
),
),
);
}
},
icon: _isTestingGa511
? const SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Icon(Icons.sync, size: 18),
label: const Text('Test Connection'),
),
),
],
),
),
);
}
Widget _buildWazeProviderCard(BuildContext context) {
final colors = context.motorColors;
final wazeOsintStatus = ref.watch(wazeOsintStatusProvider);
final wazeOsintEnabled = ref.watch(wazeOsintEnabledProvider);
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.alt_route,
iconColor: colors.velocityBlue,
title: 'Waze Community (Experimental OSINT)',
subtitle:
'Optional community hazard & traffic feed inspired by ArgeliusLabs/waze_osint. Disabled by default; zero paid subscriptions.',
status: wazeOsintStatus,
isEnabled: wazeOsintEnabled,
),
const SizedBox(height: 12),
SwitchListTile.adaptive(
contentPadding: EdgeInsets.zero,
title: const Text(
'Enable Experimental Waze Feed',
style: TextStyle(fontWeight: FontWeight.w600, fontSize: 14),
),
subtitle: Text(
'Streams public community road alerts, hazards, and traffic (Disabled by default).',
style: TextStyle(
fontSize: 12,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
value: wazeOsintEnabled,
onChanged: (val) {
ref
.read(wazeOsintEnabledProvider.notifier)
.setEnabled(val);
},
),
const SizedBox(height: 8),
Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: colors.surfaceContainer,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: colors.outlineVariant),
),
child: Row(
children: [
Icon(Icons.info_outline, size: 16, color: colors.cyberCyan),
const SizedBox(width: 8),
Expanded(
child: Text(
'Community-sourced experimental data. No API key required.',
style: TextStyle(
fontSize: 11,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
),
],
),
),
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: _isTestingWaze
? null
: () async {
setState(() => _isTestingWaze = true);
final messenger = ScaffoldMessenger.of(context);
final provider = ref.read(wazeOsintProviderInstance);
final status = await provider.testConnection('');
ref.read(wazeOsintStatusProvider.notifier).state = status;
setState(() => _isTestingWaze = false);
if (mounted) {
messenger.showSnackBar(
const SnackBar(
content: Text('Waze OSINT adapter configured successfully!'),
),
);
}
},
icon: _isTestingWaze
? const SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Icon(Icons.sync, size: 18),
label: const Text('Test Connection'),
),
),
],
),
),
);
}
Widget _buildHereProviderCard(BuildContext context) {
final colors = context.motorColors;
final hereStatus = ref.watch(hereStatusProvider);
final hereEnabled = ref.watch(hereRoadAlertsEnabledProvider);
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.shield,
iconColor: colors.cyberCyan,
title: 'HERE Safety Cameras (US)',
subtitle:
'Commercial telematics feed for verified mobile speed traps, stationary radar, and red light cameras.',
status: hereStatus,
isEnabled: hereEnabled,
),
const SizedBox(height: 12),
SwitchListTile.adaptive(
contentPadding: EdgeInsets.zero,
title: const Text(
'Enable HERE Safety Cameras',
style: TextStyle(fontWeight: FontWeight.w600, fontSize: 14),
),
subtitle: Text(
'Commercial telematics alerts active during ride navigation.',
style: TextStyle(
fontSize: 12,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
value: hereEnabled,
onChanged: (val) {
ref
.read(hereRoadAlertsEnabledProvider.notifier)
.setEnabled(val);
},
),
const SizedBox(height: 12),
TextField(
controller: _hereKeyController,
decoration: const InputDecoration(
labelText: 'HERE REST API Key',
hintText: 'Enter your HERE platform API key',
helperText:
'Self-service key from developer.here.com (Freemium 250k queries/month).',
helperMaxLines: 2,
border: OutlineInputBorder(),
isDense: true,
),
style: const TextStyle(fontSize: 14),
onChanged: (val) {
ref.read(hereApiKeyProvider.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: _isTestingHere
? null
: () async {
setState(() => _isTestingHere = true);
final messenger = ScaffoldMessenger.of(context);
final provider = ref.read(hereProviderInstance);
final status = await provider.testConnection(
_hereKeyController.text,
);
ref.read(hereStatusProvider.notifier).state = status;
setState(() => _isTestingHere = false);
if (mounted) {
String msg = '';
if (status == RoadAlertProviderStatus.connected) {
msg = 'HERE API Connected successfully!';
} else if (status ==
RoadAlertProviderStatus.authError) {
msg = 'Authentication Error: Invalid HERE API Key.';
} else if (status ==
RoadAlertProviderStatus.notConfigured) {
msg = 'HERE API Key is empty.';
} else if (status ==
RoadAlertProviderStatus.networkError) {
msg = 'Network Error: Failed to connect to HERE API.';
} else if (status ==
RoadAlertProviderStatus.rateLimited) {
msg = 'Rate Limited: Plan limit reached.';
} else {
msg = 'Status: ${status.name}';
}
messenger.showSnackBar(SnackBar(content: Text(msg)));
}
},
icon: _isTestingHere
? const SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Icon(Icons.sync, size: 18),
label: const Text('Test Connection'),
),
),
],
),
),
);
}
Widget _buildCommunityProviderCard(BuildContext context) {
final colors = context.motorColors;
final communityStatus = ref.watch(communityStatusProvider);
final communityKey = ref.watch(communityApiKeyProvider);
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,
isEnabled: communityKey.trim().isNotEmpty,
),
const SizedBox(height: 16),
TextField(
controller: _communityKeyController,
decoration: const InputDecoration(
labelText: 'Community / RapidAPI Key',
hintText: 'Enter RapidAPI or Custom Community 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 default endpoint 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 Traffic 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);
final tomtomKey = ref.watch(tomtomApiKeyProvider);
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,
isEnabled: tomtomKey.trim().isNotEmpty,
),
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.camera_alt,
color: Colors.purpleAccent,
size: 18,
),
label: const Text('Test Camera (150m)'),
onPressed: () {
ref
.read(roadAlertServiceProvider)
.triggerTestAlert(category: RoadAlertCategory.speedCamera);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Simulated Speed Camera 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<bool>(
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<bool>(
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,
bool isEnabled = true,
}) {
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),
Wrap(
spacing: 8,
runSpacing: 4,
children: [
_buildStateBadge(
label: isEnabled ? 'Enabled' : 'Disabled',
color: isEnabled
? context.motorColors.launchGreen
: context.motorColors.outlineVariant,
icon: isEnabled
? Icons.check_circle_outline
: Icons.pause_circle_outline,
),
_buildStatusBadge(status),
],
),
],
),
),
],
);
}
Widget _buildStatusBadge(RoadAlertProviderStatus status) {
final colors = context.motorColors;
String label;
Color color;
IconData icon;
switch (status) {
case RoadAlertProviderStatus.connected:
label = 'Verified & Reachable';
color = colors.launchGreen;
icon = Icons.verified_outlined;
break;
case RoadAlertProviderStatus.configured:
label = 'Configured';
color = colors.cyberCyan;
icon = Icons.tune;
break;
case RoadAlertProviderStatus.authError:
label = 'Auth Error';
color = colors.raceRed;
icon = Icons.lock_outline;
break;
case RoadAlertProviderStatus.rateLimited:
label = 'Rate Limited';
color = colors.warningAmber;
icon = Icons.speed;
break;
case RoadAlertProviderStatus.networkError:
label = 'Unreachable';
color = colors.warningAmber;
icon = Icons.cloud_off;
break;
case RoadAlertProviderStatus.notConfigured:
label = 'Not Configured';
color = colors.outlineVariant;
icon = Icons.info_outline;
break;
}
return _buildStateBadge(label: label, color: color, icon: icon);
}
Widget _buildStateBadge({
required String label,
required Color color,
IconData? icon,
}) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
decoration: BoxDecoration(
color: color.withValues(alpha: 0.12),
border: Border.all(color: color.withValues(alpha: 0.7)),
borderRadius: BorderRadius.circular(6),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (icon != null) ...[
Icon(icon, size: 12, color: color),
const SizedBox(width: 4),
],
Text(
label,
style: TextStyle(
color: color,
fontSize: 11,
fontWeight: FontWeight.w600,
),
),
],
),
);
}
}
/// 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<String> _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<String>(
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,
),
),
],
),
);
},
),
),
);
}
}