diff --git a/lib/config/map_tile_config.dart b/lib/config/map_tile_config.dart index 823daf6..a6699e4 100644 --- a/lib/config/map_tile_config.dart +++ b/lib/config/map_tile_config.dart @@ -60,7 +60,11 @@ class MapTileConfig { static List get effectiveSubdomains { final raw = dotenv.maybeGet('MAP_TILE_SUBDOMAINS')?.trim(); if (raw != null && raw.isNotEmpty) { - return raw.split(',').map((s) => s.trim()).where((s) => s.isNotEmpty).toList(); + return raw + .split(',') + .map((s) => s.trim()) + .where((s) => s.isNotEmpty) + .toList(); } final template = effectiveUrlTemplate; if (template.contains('{s}') && diff --git a/lib/config/runtime_config.dart b/lib/config/runtime_config.dart index 2b8384a..b7d6c82 100644 --- a/lib/config/runtime_config.dart +++ b/lib/config/runtime_config.dart @@ -85,7 +85,9 @@ class RuntimeConfig { case 'SMS_DISPATCH_ANON_KEY': return const String.fromEnvironment('SMS_DISPATCH_ANON_KEY'); case 'SMS_RELAY_COUNTS_AS_PRIMARY_DISPATCH': - return const String.fromEnvironment('SMS_RELAY_COUNTS_AS_PRIMARY_DISPATCH'); + return const String.fromEnvironment( + 'SMS_RELAY_COUNTS_AS_PRIMARY_DISPATCH', + ); case 'INDIA_SOS_DISPATCH_URL': return const String.fromEnvironment('INDIA_SOS_DISPATCH_URL'); case 'INDIA_ERSS_API_URL': diff --git a/lib/database/app_database.dart b/lib/database/app_database.dart index 8ba6e07..952cc97 100644 --- a/lib/database/app_database.dart +++ b/lib/database/app_database.dart @@ -54,7 +54,11 @@ Future ensureSupabaseAnonymousSession(SupabaseClient client) async { return; } } catch (e, st) { - appLog.w('Session refresh failed; re-authenticating', error: e, stackTrace: st); + appLog.w( + 'Session refresh failed; re-authenticating', + error: e, + stackTrace: st, + ); } } await client.auth.signInAnonymously(); diff --git a/lib/main.dart b/lib/main.dart index a798c3f..fa48f85 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -82,7 +82,11 @@ void main() async { await RuntimeConfig.bootstrap(); } catch (e, st) { // Non-fatal: all services check for missing config and degrade gracefully. - appLog.w('[boot] RuntimeConfig.bootstrap() failed — proceeding without config', error: e, stackTrace: st); + appLog.w( + '[boot] RuntimeConfig.bootstrap() failed — proceeding without config', + error: e, + stackTrace: st, + ); } // ← First frame renders here. The loading spinner in _RoadSOSAppState is @@ -170,8 +174,9 @@ class _RoadSOSAppState extends ConsumerState await Future.wait([ initializeFirstAidRepository(), initializeFmtcMapCache(), - EmergencyBackgroundService.initialize() - .then((_) => EmergencyBackgroundService.ensureNotificationChannel()), + EmergencyBackgroundService.initialize().then( + (_) => EmergencyBackgroundService.ensureNotificationChannel(), + ), ]); // Phase 4: Kick off remote crash-config fetch (non-blocking). @@ -184,7 +189,11 @@ class _RoadSOSAppState extends ConsumerState appLog.i('[boot] All services bootstrapped successfully.'); } catch (e, st) { // Non-fatal: app runs in offline/degraded mode. - appLog.e('[boot] Service bootstrap error — running in degraded mode', error: e, stackTrace: st); + appLog.e( + '[boot] Service bootstrap error — running in degraded mode', + error: e, + stackTrace: st, + ); } finally { if (mounted) { setState(() => _servicesReady = true); @@ -222,8 +231,9 @@ class _RoadSOSAppState extends ConsumerState ref.watch(inactivityCrashDetectorProvider); ref.watch(sosLocationTrackerProvider); - final sosPhase = - ref.watch(emergencyOrchestratorProvider.select((s) => s.phase)); + final sosPhase = ref.watch( + emergencyOrchestratorProvider.select((s) => s.phase), + ); final appLocale = ref.watch(appLocaleProvider); ref.listen(appLocaleProvider, (_, next) { @@ -334,7 +344,11 @@ class _LogoMark extends StatelessWidget { color: const Color(0xFFE8281A), borderRadius: BorderRadius.circular(18), ), - child: const Icon(Icons.emergency_share, color: Colors.white, size: 40), + child: const Icon( + Icons.emergency_share, + color: Colors.white, + size: 40, + ), ), const SizedBox(height: 16), const Text( diff --git a/lib/models/dispatch_channel_status.dart b/lib/models/dispatch_channel_status.dart index 5efebcd..bd76ac0 100644 --- a/lib/models/dispatch_channel_status.dart +++ b/lib/models/dispatch_channel_status.dart @@ -1,17 +1,12 @@ /// Lifecycle of one emergency dispatch channel shown in honest status UI. -enum DispatchChannelLifecycle { - pending, - inProgress, - success, - failed, - skipped, -} +enum DispatchChannelLifecycle { pending, inProgress, success, failed, skipped } /// One row in the dispatch confirmation list (SMS, mesh, cloud, etc.). class DispatchChannelRow { final String id; final String title; final DispatchChannelLifecycle lifecycle; + /// Short line for accessibility and panic readability (WCAG-minded contrast in UI). final String detail; @@ -35,11 +30,11 @@ class DispatchChannelRow { } Map toJson() => { - 'id': id, - 'title': title, - 'lifecycle': lifecycle.name, - 'detail': detail, - }; + 'id': id, + 'title': title, + 'lifecycle': lifecycle.name, + 'detail': detail, + }; DispatchChannelRow copyWith({ DispatchChannelLifecycle? lifecycle, diff --git a/lib/models/facility.dart b/lib/models/facility.dart index 06fe2a6..2f4f5a0 100644 --- a/lib/models/facility.dart +++ b/lib/models/facility.dart @@ -8,6 +8,7 @@ class Facility { final double longitude; final String? contactNumber; final String? capabilities; + /// gov_nhm | gov_ayushman | osm | merged final String? dataSource; diff --git a/lib/models/sos_activity_record.dart b/lib/models/sos_activity_record.dart index a0a7fa5..3719adf 100644 --- a/lib/models/sos_activity_record.dart +++ b/lib/models/sos_activity_record.dart @@ -52,19 +52,19 @@ class SosActivityRecord { } Map toJson() => { - 'incident_id': incidentId, - 'completed_at': completedAtUtc.toIso8601String(), - 'latitude': latitude, - 'longitude': longitude, - 'accuracy_m': accuracyM, - 'location_source': locationSource, - 'triage_severity': triageSeverity, - 'triage_source': triageSourceName, - 'required_services': requiredServices, - 'channels': channels.map((e) => e.toJson()).toList(), - 'sync_status': syncStatusLine, - 'is_bystander': isBystander, - }; + 'incident_id': incidentId, + 'completed_at': completedAtUtc.toIso8601String(), + 'latitude': latitude, + 'longitude': longitude, + 'accuracy_m': accuracyM, + 'location_source': locationSource, + 'triage_severity': triageSeverity, + 'triage_source': triageSourceName, + 'required_services': requiredServices, + 'channels': channels.map((e) => e.toJson()).toList(), + 'sync_status': syncStatusLine, + 'is_bystander': isBystander, + }; String formattedGpsIndia() => '${latitude.toStringAsFixed(6)}, ${longitude.toStringAsFixed(6)} ' diff --git a/lib/services/agent_health_service.dart b/lib/services/agent_health_service.dart index 8fbba3f..8f2687b 100644 --- a/lib/services/agent_health_service.dart +++ b/lib/services/agent_health_service.dart @@ -63,13 +63,13 @@ class AgentHealthSnapshot { AgentReadiness? sms, AgentReadiness? ble, }) => AgentHealthSnapshot( - gemmaCloud: gemmaCloud ?? this.gemmaCloud, - gemmaOnDevice: gemmaOnDevice ?? this.gemmaOnDevice, - gps: gps ?? this.gps, - sms: sms ?? this.sms, - ble: ble ?? this.ble, - checkedAt: DateTime.now(), - ); + gemmaCloud: gemmaCloud ?? this.gemmaCloud, + gemmaOnDevice: gemmaOnDevice ?? this.gemmaOnDevice, + gps: gps ?? this.gps, + sms: sms ?? this.sms, + ble: ble ?? this.ble, + checkedAt: DateTime.now(), + ); } class AgentHealthService { @@ -135,9 +135,9 @@ class AgentHealthService { Future _checkGemmaCloud() async { final connectivity = _ref.read(connectivityServiceProvider); return switch (connectivity.currentQuality) { - NetworkQuality.wifi => AgentReadiness.ready, + NetworkQuality.wifi => AgentReadiness.ready, NetworkQuality.cellular => AgentReadiness.ready, - NetworkQuality.none => AgentReadiness.unavailable, + NetworkQuality.none => AgentReadiness.unavailable, }; } @@ -168,7 +168,9 @@ class AgentHealthService { // Primary: server relay (Twilio / Edge) — no Android SEND_SMS required. final relayUrl = dotenv.env['SMS_DISPATCH_URL']?.trim() ?? ''; final relayKey = dotenv.env['SMS_DISPATCH_ANON_KEY']?.trim() ?? ''; - if (relayUrl.isNotEmpty && relayKey.isNotEmpty) return AgentReadiness.ready; + if (relayUrl.isNotEmpty && relayKey.isNotEmpty) { + return AgentReadiness.ready; + } // Fallback: open SMS app intent (no permission). If relay isn't configured, // we mark this as degraded (still usable, but requires user interaction). diff --git a/lib/services/ai_triage_service.dart b/lib/services/ai_triage_service.dart index f3b3891..cc1371f 100644 --- a/lib/services/ai_triage_service.dart +++ b/lib/services/ai_triage_service.dart @@ -90,23 +90,23 @@ class TriageResult { }); Map toJson() => { - 'function_call': functionCall, - 'arguments': { - 'location': location, - 'severity_level': severityLevel, - 'required_services': requiredServices, - 'first_aid_rag_query': firstAidQuery, - 'compressed_payload': compressedPayload, - }, - 'thinking_trace': thinkingTrace, - 'degraded_mode': isDegradedMode, - 'source': source.name, - 'vision_used': visionUsed, - 'confidence': confidence, - 'validation_flags': validationFlags, - 'was_overridden': wasOverridden, - 'validation_notes': validationNotes, - }; + 'function_call': functionCall, + 'arguments': { + 'location': location, + 'severity_level': severityLevel, + 'required_services': requiredServices, + 'first_aid_rag_query': firstAidQuery, + 'compressed_payload': compressedPayload, + }, + 'thinking_trace': thinkingTrace, + 'degraded_mode': isDegradedMode, + 'source': source.name, + 'vision_used': visionUsed, + 'confidence': confidence, + 'validation_flags': validationFlags, + 'was_overridden': wasOverridden, + 'validation_notes': validationNotes, + }; String get sourceLabel { switch (source) { @@ -211,11 +211,14 @@ class AiTriageService { const CapturedScenePhoto? scenePhoto = null; - final skipCloud = _connectivityAwareTriage && + final skipCloud = + _connectivityAwareTriage && _connectivity.currentQuality == NetworkQuality.none; if (skipCloud) { - appLog.d('[Triage] Connectivity=none — skipping Tier 1 cloud (saves 5s timeout)'); + appLog.d( + '[Triage] Connectivity=none — skipping Tier 1 cloud (saves 5s timeout)', + ); } else { final cloudTimeout = _connectivity.currentQuality == NetworkQuality.wifi ? const Duration(seconds: 8) @@ -231,8 +234,11 @@ class AiTriageService { appLog.i('[Triage] Tier 1 — Gemma 4 27B cloud ✓ (text-only auto-SOS)'); return cloud; } catch (e, st) { - appLog.d('[Triage] Tier 1 unavailable, trying Tier 2 on-device', - error: e, stackTrace: st); + appLog.d( + '[Triage] Tier 1 unavailable, trying Tier 2 on-device', + error: e, + stackTrace: st, + ); } } @@ -272,8 +278,8 @@ class AiTriageService { final locationString = '${location.latitude},${location.longitude}'; final ctx = transcript.trim().isEmpty ? (isBystander - ? 'Bystander reporting roadside emergency' - : 'Emergency SOS triggered') + ? 'Bystander reporting roadside emergency' + : 'Emergency SOS triggered') : transcript; return triageEmergency( @@ -350,7 +356,8 @@ class AiTriageService { if (data is! Map) throw Exception('Edge triage returned non-object'); final payload = Map.from(data); - final severity = (payload['severity_level'] as num?)?.toInt().clamp(1, 5) ?? 4; + final severity = + (payload['severity_level'] as num?)?.toInt().clamp(1, 5) ?? 4; final rawServices = payload['required_services']; final services = {'ambulance'}; if (rawServices is List) { @@ -366,8 +373,9 @@ class AiTriageService { final fallbackQuery = _classifier .classify(transcript: transcript, severityHint: severityHint) .firstAidQuery; - final aidQuery = - (fromCloud != null && fromCloud.isNotEmpty) ? fromCloud : fallbackQuery; + final aidQuery = (fromCloud != null && fromCloud.isNotEmpty) + ? fromCloud + : fallbackQuery; final thinking = payload['thinking_summary'] as String?; final modelUsed = payload['_model'] as String?; final visionUsed = payload['_vision_used'] == true; @@ -380,12 +388,18 @@ class AiTriageService { severityLevel: severity, requiredServices: services.toList(), firstAidQuery: verifiedAdvice, - compressedPayload: _buildCompressedPayload(location, severity, services.toList()), + compressedPayload: _buildCompressedPayload( + location, + severity, + services.toList(), + ), thinkingTrace: thinking != null ? '[${modelUsed ?? "Gemma 4 27B"}${visionUsed ? " + vision" : ""}] $thinking' : null, isDegradedMode: false, - source: visionUsed ? TriageSource.gemma4CloudVision : TriageSource.gemma4Cloud, + source: visionUsed + ? TriageSource.gemma4CloudVision + : TriageSource.gemma4Cloud, visionUsed: visionUsed, ); } @@ -406,7 +420,8 @@ class AiTriageService { ); if (result == null) return null; - final severity = (result['severity_level'] as num?)?.toInt().clamp(1, 5) ?? 3; + final severity = + (result['severity_level'] as num?)?.toInt().clamp(1, 5) ?? 3; final rawServices = result['required_services']; final services = {'ambulance'}; if (rawServices is List) { @@ -416,7 +431,8 @@ class AiTriageService { } } } - final aidQuery = (result['first_aid_focus'] as String?)?.trim() ?? + final aidQuery = + (result['first_aid_focus'] as String?)?.trim() ?? _classifier .classify(transcript: transcript, severityHint: severityHint) .firstAidQuery; @@ -430,7 +446,11 @@ class AiTriageService { severityLevel: severity, requiredServices: services.toList(), firstAidQuery: verifiedAdvice, - compressedPayload: _buildCompressedPayload(location, severity, services.toList()), + compressedPayload: _buildCompressedPayload( + location, + severity, + services.toList(), + ), thinkingTrace: '[Gemma 4 E4B on-device] ${thinking ?? ""}', isDegradedMode: true, source: TriageSource.gemma4OnDevice, @@ -445,7 +465,10 @@ class AiTriageService { required int severityHint, required String languageCode, }) async { - final c = _tier3.classify(transcript: transcript, severityHint: severityHint); + final c = _tier3.classify( + transcript: transcript, + severityHint: severityHint, + ); final verified = await FirstAidStore.getVerifiedAdvice(c.firstAidQuery); return TriageResult( functionCall: 'trigger_sos', @@ -453,7 +476,11 @@ class AiTriageService { severityLevel: c.severityLevel.clamp(1, 5), requiredServices: c.requiredServices, firstAidQuery: verified, - compressedPayload: _buildCompressedPayload(location, c.severityLevel, c.requiredServices), + compressedPayload: _buildCompressedPayload( + location, + c.severityLevel, + c.requiredServices, + ), thinkingTrace: languageCode == 'hi' ? 'स्थानीय भार-विश्लेषण मॉडल (Gemma 4 E4B उपलब्ध नहीं)।' : 'Local weighted heuristic (Gemma 4 E4B unavailable or loading).', @@ -470,7 +497,10 @@ class AiTriageService { required int severityHint, required String languageCode, }) async { - final c = _classifier.classify(transcript: transcript, severityHint: severityHint); + final c = _classifier.classify( + transcript: transcript, + severityHint: severityHint, + ); final severity = c.severityLevel; final services = c.requiredServices; final verified = await FirstAidStore.getVerifiedAdvice(c.firstAidQuery); @@ -491,8 +521,13 @@ class AiTriageService { } static const Set _allowedServices = { - 'ambulance', 'police', 'fire_department', 'rescue', - 'towing', 'puncture_shop', 'showroom', + 'ambulance', + 'police', + 'fire_department', + 'rescue', + 'towing', + 'puncture_shop', + 'showroom', }; String _buildCompressedPayload( @@ -500,18 +535,28 @@ class AiTriageService { int severity, List services, ) { - final svcCodes = services.map((s) { - switch (s) { - case 'ambulance': return 'AMB'; - case 'police': return 'POL'; - case 'fire_department': return 'FIR'; - case 'rescue': return 'RES'; - case 'towing': return 'TOW'; - case 'puncture_shop': return 'PUN'; - case 'showroom': return 'SHR'; - default: return 'UNK'; - } - }).join(','); + final svcCodes = services + .map((s) { + switch (s) { + case 'ambulance': + return 'AMB'; + case 'police': + return 'POL'; + case 'fire_department': + return 'FIR'; + case 'rescue': + return 'RES'; + case 'towing': + return 'TOW'; + case 'puncture_shop': + return 'PUN'; + case 'showroom': + return 'SHR'; + default: + return 'UNK'; + } + }) + .join(','); final loc = location.replaceAll(' ', '_'); final clipped = loc.length <= 30 ? loc : loc.substring(0, 30); diff --git a/lib/services/app_locale_controller.dart b/lib/services/app_locale_controller.dart index 4006af7..b55cd25 100644 --- a/lib/services/app_locale_controller.dart +++ b/lib/services/app_locale_controller.dart @@ -16,8 +16,9 @@ const _prefsKey = 'app_locale_language_code'; Locale _resolvePlatformLocale() { final platform = WidgetsBinding.instance.platformDispatcher.locale; - if (kSupportedAppLocales - .any((e) => e.languageCode == platform.languageCode)) { + if (kSupportedAppLocales.any( + (e) => e.languageCode == platform.languageCode, + )) { return Locale(platform.languageCode); } return const Locale('en'); @@ -38,8 +39,9 @@ class AppLocaleController extends StateNotifier { } Future setLocale(Locale locale) async { - if (!kSupportedAppLocales - .any((l) => l.languageCode == locale.languageCode)) { + if (!kSupportedAppLocales.any( + (l) => l.languageCode == locale.languageCode, + )) { return; } state = Locale(locale.languageCode); @@ -48,7 +50,8 @@ class AppLocaleController extends StateNotifier { } } -final appLocaleProvider = - StateNotifierProvider((ref) { +final appLocaleProvider = StateNotifierProvider(( + ref, +) { return AppLocaleController(); }); diff --git a/lib/services/ble_payload_codec.dart b/lib/services/ble_payload_codec.dart index 843bdf6..faab24f 100644 --- a/lib/services/ble_payload_codec.dart +++ b/lib/services/ble_payload_codec.dart @@ -21,13 +21,13 @@ class BlePayloadCodec { static const int _version = 0x01; // Service bitmask definitions (matches AiTriageService._allowedServices) - static const int bitAmbulance = 0x01; - static const int bitPolice = 0x02; - static const int bitFire = 0x04; - static const int bitRescue = 0x08; - static const int bitTowing = 0x10; - static const int bitPunctureShop = 0x20; - static const int bitShowroom = 0x40; + static const int bitAmbulance = 0x01; + static const int bitPolice = 0x02; + static const int bitFire = 0x04; + static const int bitRescue = 0x08; + static const int bitTowing = 0x10; + static const int bitPunctureShop = 0x20; + static const int bitShowroom = 0x40; /// Encode a SOS event to a compact 12-byte BLE payload. static Uint8List encode({ @@ -76,13 +76,20 @@ class BlePayloadCodec { var bits = 0; for (final s in services) { switch (s) { - case 'ambulance': bits |= bitAmbulance; - case 'police': bits |= bitPolice; - case 'fire_department': bits |= bitFire; - case 'rescue': bits |= bitRescue; - case 'towing': bits |= bitTowing; - case 'puncture_shop': bits |= bitPunctureShop; - case 'showroom': bits |= bitShowroom; + case 'ambulance': + bits |= bitAmbulance; + case 'police': + bits |= bitPolice; + case 'fire_department': + bits |= bitFire; + case 'rescue': + bits |= bitRescue; + case 'towing': + bits |= bitTowing; + case 'puncture_shop': + bits |= bitPunctureShop; + case 'showroom': + bits |= bitShowroom; } } return bits == 0 ? bitAmbulance : bits; // Always at least ambulance @@ -90,13 +97,13 @@ class BlePayloadCodec { static List _servicesFromBitmask(int bits) { final out = []; - if (bits & bitAmbulance != 0) out.add('ambulance'); - if (bits & bitPolice != 0) out.add('police'); - if (bits & bitFire != 0) out.add('fire_department'); - if (bits & bitRescue != 0) out.add('rescue'); - if (bits & bitTowing != 0) out.add('towing'); + if (bits & bitAmbulance != 0) out.add('ambulance'); + if (bits & bitPolice != 0) out.add('police'); + if (bits & bitFire != 0) out.add('fire_department'); + if (bits & bitRescue != 0) out.add('rescue'); + if (bits & bitTowing != 0) out.add('towing'); if (bits & bitPunctureShop != 0) out.add('puncture_shop'); - if (bits & bitShowroom != 0) out.add('showroom'); + if (bits & bitShowroom != 0) out.add('showroom'); return out.isEmpty ? ['ambulance'] : out; } } diff --git a/lib/services/bluetooth_vehicle_monitor.dart b/lib/services/bluetooth_vehicle_monitor.dart index 92db71b..b47b3c4 100644 --- a/lib/services/bluetooth_vehicle_monitor.dart +++ b/lib/services/bluetooth_vehicle_monitor.dart @@ -30,9 +30,9 @@ import '../logging/app_log.dart'; /// The service only records the *fact* of a disconnection event. class BluetoothVehicleMonitor { static const double _vehicleSpeedThresholdKmh = 20.0; - static const Duration _connectStabilityMin = Duration(seconds: 8); - static const Duration _signalTtl = Duration(seconds: 30); - static const Duration _pollInterval = Duration(seconds: 5); + static const Duration _connectStabilityMin = Duration(seconds: 8); + static const Duration _signalTtl = Duration(seconds: 30); + static const Duration _pollInterval = Duration(seconds: 5); Timer? _pollTimer; StreamSubscription? _positionSub; @@ -69,19 +69,17 @@ class BluetoothVehicleMonitor { void _startGpsSpeedTracking() { try { - _positionSub = Geolocator.getPositionStream( - locationSettings: const LocationSettings( - accuracy: LocationAccuracy.medium, - distanceFilter: 10, - ), - ).listen( - (p) { - if (!p.speed.isNaN && p.speed >= 0) { - _lastSpeedKmh = (p.speed * 3.6).clamp(0.0, 320.0); - } - }, - onError: (Object _) {}, - ); + _positionSub = + Geolocator.getPositionStream( + locationSettings: const LocationSettings( + accuracy: LocationAccuracy.medium, + distanceFilter: 10, + ), + ).listen((p) { + if (!p.speed.isNaN && p.speed >= 0) { + _lastSpeedKmh = (p.speed * 3.6).clamp(0.0, 320.0); + } + }, onError: (Object _) {}); } catch (_) {} } @@ -104,7 +102,8 @@ class BluetoothVehicleMonitor { // Detect disconnections. for (final id in _prevConnected.difference(currentIds)) { final connectedAt = _connectedSince[id]; - final stable = connectedAt != null && + final stable = + connectedAt != null && now.difference(connectedAt) >= _connectStabilityMin; if (stable && _lastSpeedKmh >= _vehicleSpeedThresholdKmh) { @@ -125,7 +124,9 @@ class BluetoothVehicleMonitor { } /// Non-autoDispose: must track BT connections for the full driving session. -final bluetoothVehicleMonitorProvider = Provider((ref) { +final bluetoothVehicleMonitorProvider = Provider(( + ref, +) { final svc = BluetoothVehicleMonitor(); svc.startMonitoring(); ref.onDispose(svc.dispose); diff --git a/lib/services/camera_triage_service.dart b/lib/services/camera_triage_service.dart index 29b1413..eab4930 100644 --- a/lib/services/camera_triage_service.dart +++ b/lib/services/camera_triage_service.dart @@ -1,7 +1,5 @@ import 'dart:convert'; - - import 'package:image_picker/image_picker.dart'; import 'package:permission_handler/permission_handler.dart'; @@ -94,7 +92,11 @@ class CameraTriageService { capturedAt: DateTime.now().toUtc(), ); } catch (e, st) { - appLog.w('[CameraTriageService] Image selection failed', error: e, stackTrace: st); + appLog.w( + '[CameraTriageService] Image selection failed', + error: e, + stackTrace: st, + ); return null; } } diff --git a/lib/services/connectivity_service.dart b/lib/services/connectivity_service.dart index 7cbaca8..37b86e2 100644 --- a/lib/services/connectivity_service.dart +++ b/lib/services/connectivity_service.dart @@ -64,9 +64,9 @@ class ConnectivityService { if (results.isEmpty || results.every((r) => r == ConnectivityResult.none)) { return NetworkQuality.none; } - if (results.any((r) => - r == ConnectivityResult.wifi || - r == ConnectivityResult.ethernet)) { + if (results.any( + (r) => r == ConnectivityResult.wifi || r == ConnectivityResult.ethernet, + )) { return NetworkQuality.wifi; } return NetworkQuality.cellular; diff --git a/lib/services/crash_confidence_engine.dart b/lib/services/crash_confidence_engine.dart index 24b045b..914643d 100644 --- a/lib/services/crash_confidence_engine.dart +++ b/lib/services/crash_confidence_engine.dart @@ -77,10 +77,10 @@ class CrashConfidenceResult { }); String get tierLabel => switch (tier) { - CrashConfidenceTier.low => 'LOW', - CrashConfidenceTier.medium => 'MEDIUM', - CrashConfidenceTier.high => 'HIGH', - }; + CrashConfidenceTier.low => 'LOW', + CrashConfidenceTier.medium => 'MEDIUM', + CrashConfidenceTier.high => 'HIGH', + }; /// Factual, non-speculative label for emergency messaging (Phase 7 constraint). String get incidentLabel => tier == CrashConfidenceTier.high @@ -98,31 +98,31 @@ abstract final class CrashConfidenceEngine { // ── Weight constants ────────────────────────────────────────────────────── - static const double _wAccel = 0.30; - static const double _wGyro = 0.22; - static const double _wSpeed = 0.20; - static const double _wDrop = 0.15; - static const double _wBt = 0.08; - static const double _wStill = 0.05; + static const double _wAccel = 0.30; + static const double _wGyro = 0.22; + static const double _wSpeed = 0.20; + static const double _wDrop = 0.15; + static const double _wBt = 0.08; + static const double _wStill = 0.05; // ── Normalisation reference maxima ──────────────────────────────────────── /// Accel normalised against 120 m/s² (severe but survivable crash reference). - static const double _accelMax = 120.0; + static const double _accelMax = 120.0; /// Gyro normalised against 8 rad/s (vehicle roll / high-speed spin cap). - static const double _gyroMax = 8.0; + static const double _gyroMax = 8.0; /// Speed reference: 120 km/h highway limit (India NH). - static const double _speedMax = 120.0; + static const double _speedMax = 120.0; /// Drop reference: full speed-to-zero scenario. - static const double _dropMax = 120.0; + static const double _dropMax = 120.0; // ── Tier thresholds ─────────────────────────────────────────────────────── static const double _mediumThreshold = 0.35; - static const double _highThreshold = 0.65; + static const double _highThreshold = 0.65; /// Compute a confidence score from raw sensor signals. /// @@ -130,38 +130,39 @@ abstract final class CrashConfidenceEngine { /// will cause undefined behaviour. static CrashConfidenceResult score(CrashSignals s) { // Per-signal normalised contributions (each in [0.0, 1.0]). - final accelN = (s.accelPeakMs2.clamp(0.0, _accelMax) / _accelMax); - final gyroN = (s.gyroPeakRadPerSec.clamp(0.0, _gyroMax) / _gyroMax); - final speedN = (s.speedBeforeKmh.clamp(0.0, _speedMax) / _speedMax); - final dropN = (s.speedDropKmh.clamp(0.0, _dropMax) / _dropMax); - final btN = s.bluetoothVehicleDisconnect ? 1.0 : 0.0; - final stillN = s.postImpactDeviceStill ? 1.0 : 0.0; - - final raw = accelN * _wAccel - + gyroN * _wGyro - + speedN * _wSpeed - + dropN * _wDrop - + btN * _wBt - + stillN * _wStill; + final accelN = (s.accelPeakMs2.clamp(0.0, _accelMax) / _accelMax); + final gyroN = (s.gyroPeakRadPerSec.clamp(0.0, _gyroMax) / _gyroMax); + final speedN = (s.speedBeforeKmh.clamp(0.0, _speedMax) / _speedMax); + final dropN = (s.speedDropKmh.clamp(0.0, _dropMax) / _dropMax); + final btN = s.bluetoothVehicleDisconnect ? 1.0 : 0.0; + final stillN = s.postImpactDeviceStill ? 1.0 : 0.0; + + final raw = + accelN * _wAccel + + gyroN * _wGyro + + speedN * _wSpeed + + dropN * _wDrop + + btN * _wBt + + stillN * _wStill; final clamped = raw.clamp(0.0, 1.0); final tier = clamped >= _highThreshold ? CrashConfidenceTier.high : clamped >= _mediumThreshold - ? CrashConfidenceTier.medium - : CrashConfidenceTier.low; + ? CrashConfidenceTier.medium + : CrashConfidenceTier.low; return CrashConfidenceResult( score: clamped, tier: tier, breakdown: { - 'accel': accelN * _wAccel, - 'gyro' : gyroN * _wGyro, - 'speed': speedN * _wSpeed, - 'drop' : dropN * _wDrop, - 'bt' : btN * _wBt, - 'still': stillN * _wStill, + 'accel': accelN * _wAccel, + 'gyro': gyroN * _wGyro, + 'speed': speedN * _wSpeed, + 'drop': dropN * _wDrop, + 'bt': btN * _wBt, + 'still': stillN * _wStill, }, ); } diff --git a/lib/services/crash_detection_service.dart b/lib/services/crash_detection_service.dart index 14cae24..85c9619 100644 --- a/lib/services/crash_detection_service.dart +++ b/lib/services/crash_detection_service.dart @@ -50,8 +50,7 @@ class CrashDetectionService { final Ref _ref; - GyroscopeFusionService get _gyro => - _ref.read(gyroscopeFusionServiceProvider); + GyroscopeFusionService get _gyro => _ref.read(gyroscopeFusionServiceProvider); BluetoothVehicleMonitor get _btMonitor => _ref.read(bluetoothVehicleMonitorProvider); @@ -70,9 +69,9 @@ class CrashDetectionService { stopMonitoring(); // Gyroscope and BT monitor lifecycle managed by their providers. _startGpsSpeed(); - _accelSub = SensorsPlatform.instance - .userAccelerometerEventStream() - .listen(_onAccelerometer); + _accelSub = SensorsPlatform.instance.userAccelerometerEventStream().listen( + _onAccelerometer, + ); } Future _startGpsSpeed() async { @@ -103,10 +102,7 @@ class CrashDetectionService { accuracy: LocationAccuracy.bestForNavigation, distanceFilter: 0, ), - ).listen( - _onPosition, - onError: (Object _) => _gpsSpeedUsable = false, - ); + ).listen(_onPosition, onError: (Object _) => _gpsSpeedUsable = false); } catch (_) { _gpsSpeedUsable = false; } @@ -198,9 +194,10 @@ class CrashDetectionService { } if (minAfter.isInfinite) minAfter = _speedHistory.last.kmh; - final approach = maxBefore >= CrashTuning.minApproachSpeedKmh; - final halted = minAfter <= CrashTuning.stoppedSpeedKmh; - final sharpDrop = (maxBefore - minAfter) >= CrashTuning.suddenDecelDeltaKmh; + final approach = maxBefore >= CrashTuning.minApproachSpeedKmh; + final halted = minAfter <= CrashTuning.stoppedSpeedKmh; + final sharpDrop = + (maxBefore - minAfter) >= CrashTuning.suddenDecelDeltaKmh; if (!approach || !(halted || sharpDrop)) { appLog.d( @@ -230,18 +227,16 @@ class CrashDetectionService { // ── Gate 5: Multi-signal confidence scoring ──────────────────────── final confidence = CrashConfidenceEngine.score( CrashSignals( - accelPeakMs2: peakMs2, - gyroPeakRadPerSec: gyroPeakRadPerSec, - speedBeforeKmh: maxBefore, - speedDropKmh: maxBefore - minAfter, + accelPeakMs2: peakMs2, + gyroPeakRadPerSec: gyroPeakRadPerSec, + speedBeforeKmh: maxBefore, + speedDropKmh: maxBefore - minAfter, bluetoothVehicleDisconnect: _btMonitor.recentDisconnect, - postImpactDeviceStill: still, + postImpactDeviceStill: still, ), ); - appLog.w( - 'CRASH CONFIRMED — $confidence', - ); + appLog.w('CRASH CONFIRMED — $confidence'); // LOW confidence after 4 gates is theoretically impossible but guarded. if (confidence.tier == CrashConfidenceTier.low) { @@ -274,9 +269,9 @@ class CrashDetectionService { Future _measureStillness() async { final magnitudes = []; - final sub = SensorsPlatform.instance - .userAccelerometerEventStream() - .listen((e) => magnitudes.add(sqrt(e.x * e.x + e.y * e.y + e.z * e.z))); + final sub = SensorsPlatform.instance.userAccelerometerEventStream().listen( + (e) => magnitudes.add(sqrt(e.x * e.x + e.y * e.y + e.z * e.z)), + ); await Future.delayed( Duration(milliseconds: CrashTuning.stillnessSampleWindowMs), diff --git a/lib/services/crash_tuning.dart b/lib/services/crash_tuning.dart index 6f93f64..4e342a8 100644 --- a/lib/services/crash_tuning.dart +++ b/lib/services/crash_tuning.dart @@ -72,6 +72,10 @@ class CrashTuning { static int get interSpikeDebounceMs => _rc.getInt('CRASH_INTER_SPIKE_DEBOUNCE_MS', 900, min: 100, max: 10000); - static int get sosCooldownMs => - _rc.getInt('CRASH_SOS_COOLDOWN_MS', 45000, min: 5000, max: 10 * 60 * 1000); + static int get sosCooldownMs => _rc.getInt( + 'CRASH_SOS_COOLDOWN_MS', + 45000, + min: 5000, + max: 10 * 60 * 1000, + ); } diff --git a/lib/services/driving_mode_service.dart b/lib/services/driving_mode_service.dart index 92c5cb1..caa58df 100644 --- a/lib/services/driving_mode_service.dart +++ b/lib/services/driving_mode_service.dart @@ -94,5 +94,5 @@ class DrivingModeService extends StateNotifier { /// autoDispose would kill GPS tracking whenever the dashboard rebuilds. final drivingModeProvider = StateNotifierProvider((ref) { - return DrivingModeService(); -}); + return DrivingModeService(); + }); diff --git a/lib/services/emergency_background_service.dart b/lib/services/emergency_background_service.dart index a720a6d..097bd0a 100644 --- a/lib/services/emergency_background_service.dart +++ b/lib/services/emergency_background_service.dart @@ -12,24 +12,25 @@ import '../logging/app_log.dart'; // MethodChannel shared with MainActivity for hardware-button events. // We reuse it here to sync the QS tile's SharedPreferences state. -const _kHardwareButtonsChannel = - MethodChannel('com.codestreak.roadsos/hardware_buttons'); +const _kHardwareButtonsChannel = MethodChannel( + 'com.codestreak.roadsos/hardware_buttons', +); /// IPC command constants between the UI isolate and the background isolate. class BgCommand { static const String startCrashMonitor = 'start_crash_monitor'; - static const String stopCrashMonitor = 'stop_crash_monitor'; - static const String startSafeWalk = 'start_safe_walk'; - static const String stopSafeWalk = 'stop_safe_walk'; - static const String sosTriggered = 'sos_triggered'; - static const String heartbeat = 'heartbeat'; - static const String drivingModeOn = 'driving_mode_on'; - static const String drivingModeOff = 'driving_mode_off'; + static const String stopCrashMonitor = 'stop_crash_monitor'; + static const String startSafeWalk = 'start_safe_walk'; + static const String stopSafeWalk = 'stop_safe_walk'; + static const String sosTriggered = 'sos_triggered'; + static const String heartbeat = 'heartbeat'; + static const String drivingModeOn = 'driving_mode_on'; + static const String drivingModeOff = 'driving_mode_off'; } -const _kChannelId = 'roadsos_monitor'; +const _kChannelId = 'roadsos_monitor'; const _kChannelName = 'RoadSOS Monitor'; -const _kNotifId = 888; +const _kNotifId = 888; /// Wraps flutter_background_service to provide: /// @@ -106,7 +107,9 @@ class EmergencyBackgroundService { if (kIsWeb || !Platform.isAndroid) return; _kHardwareButtonsChannel .invokeMethod('setCrashMonitorActive', active) - .catchError((_) {/* activity may not be in foreground; tile will sync on next panel open */}); + .catchError((_) { + /* activity may not be in foreground; tile will sync on next panel open */ + }); } static Future startSafeWalk({required Duration duration}) async { @@ -116,7 +119,9 @@ class EmergencyBackgroundService { _service.invoke(BgCommand.startSafeWalk, { 'duration_seconds': duration.inSeconds, }); - appLog.i('[BgService] Safe-walk timer started (${duration.inMinutes} min).'); + appLog.i( + '[BgService] Safe-walk timer started (${duration.inMinutes} min).', + ); } static Future stopSafeWalk() async { @@ -149,11 +154,8 @@ class EmergencyBackgroundService { try { final status = await Permission.ignoreBatteryOptimizations.status; if (!status.isGranted) { - final result = - await Permission.ignoreBatteryOptimizations.request(); - appLog.i( - '[BgService] Battery optimization exemption: ${result.name}', - ); + final result = await Permission.ignoreBatteryOptimizations.request(); + appLog.i('[BgService] Battery optimization exemption: ${result.name}'); } else { appLog.d('[BgService] Battery optimization exemption already granted.'); } @@ -201,7 +203,9 @@ class EmergencyBackgroundService { safeWalkTimer?.cancel(); final seconds = (data?['duration_seconds'] as int?) ?? 1800; safeWalkTimer = Timer(Duration(seconds: seconds), () { - service.invoke(BgCommand.sosTriggered, {'source': 'safe_walk_escalation'}); + service.invoke(BgCommand.sosTriggered, { + 'source': 'safe_walk_escalation', + }); }); service.setForegroundNotificationInfo( title: 'RoadSOS — Safe Walk Active', @@ -244,7 +248,8 @@ class EmergencyBackgroundService { ); await plugin .resolvePlatformSpecificImplementation< - AndroidFlutterLocalNotificationsPlugin>() + AndroidFlutterLocalNotificationsPlugin + >() ?.createNotificationChannel(channel); } } diff --git a/lib/services/emergency_beacon_service.dart b/lib/services/emergency_beacon_service.dart index 8159ac4..8785cf0 100644 --- a/lib/services/emergency_beacon_service.dart +++ b/lib/services/emergency_beacon_service.dart @@ -19,7 +19,7 @@ class EmergencyBeaconService { bool _isActive = false; Timer? _flashTimer; final AudioPlayer _player = AudioPlayer(); - + bool get isActive => _isActive; /// Starts the hardware SOS beacon. @@ -27,7 +27,9 @@ class EmergencyBeaconService { Future start() async { if (_isActive) return; _isActive = true; - appLog.i('🚨 [BEACON] Hardware takeover initiated: SOS strobe + Siren active.'); + appLog.i( + '🚨 [BEACON] Hardware takeover initiated: SOS strobe + Siren active.', + ); _startFlashlightStrobe(); _startSiren(); @@ -39,7 +41,7 @@ class EmergencyBeaconService { _isActive = false; _flashTimer?.cancel(); _flashTimer = null; - + try { await TorchLight.disableTorch(); } catch (_) {} @@ -47,7 +49,7 @@ class EmergencyBeaconService { try { await _player.stop(); } catch (_) {} - + appLog.i('✅ [BEACON] Hardware SOS signals disabled.'); } @@ -55,17 +57,17 @@ class EmergencyBeaconService { void _startFlashlightStrobe() { _flashTimer?.cancel(); - + // SOS Morse Pattern: ... --- ... // Dot = 200ms, Dash = 600ms, Gap = 200ms final pattern = [ 200, 200, 200, 200, 200, 400, // S (...) 600, 200, 600, 200, 600, 400, // O (---) - 200, 200, 200, 200, 200, 2000 // S (...) + 2s rest + 200, 200, 200, 200, 200, 2000, // S (...) + 2s rest ]; int index = 0; - + void nextStep() { if (!_isActive) return; @@ -123,7 +125,9 @@ class EmergencyBeaconService { } } -final emergencyBeaconServiceProvider = Provider((ref) => EmergencyBeaconService.instance); +final emergencyBeaconServiceProvider = Provider( + (ref) => EmergencyBeaconService.instance, +); /// Generates a two-tone siren WAV (880Hz / 660Hz alternating) entirely in memory. /// No external asset file required. @@ -157,18 +161,31 @@ class _SirenAudioSource extends StreamAudioSource { // WAV header writeString('RIFF'); - buffer.setUint32(offset, fileSize - 8, Endian.little); offset += 4; + buffer.setUint32(offset, fileSize - 8, Endian.little); + offset += 4; writeString('WAVE'); writeString('fmt '); - buffer.setUint32(offset, 16, Endian.little); offset += 4; - buffer.setUint16(offset, 1, Endian.little); offset += 2; // PCM - buffer.setUint16(offset, _channels, Endian.little); offset += 2; - buffer.setUint32(offset, _sampleRate, Endian.little); offset += 4; - buffer.setUint32(offset, _sampleRate * _channels * (_bitsPerSample ~/ 8), Endian.little); offset += 4; - buffer.setUint16(offset, _channels * (_bitsPerSample ~/ 8), Endian.little); offset += 2; - buffer.setUint16(offset, _bitsPerSample, Endian.little); offset += 2; + buffer.setUint32(offset, 16, Endian.little); + offset += 4; + buffer.setUint16(offset, 1, Endian.little); + offset += 2; // PCM + buffer.setUint16(offset, _channels, Endian.little); + offset += 2; + buffer.setUint32(offset, _sampleRate, Endian.little); + offset += 4; + buffer.setUint32( + offset, + _sampleRate * _channels * (_bitsPerSample ~/ 8), + Endian.little, + ); + offset += 4; + buffer.setUint16(offset, _channels * (_bitsPerSample ~/ 8), Endian.little); + offset += 2; + buffer.setUint16(offset, _bitsPerSample, Endian.little); + offset += 2; writeString('data'); - buffer.setUint32(offset, dataSize, Endian.little); offset += 4; + buffer.setUint32(offset, dataSize, Endian.little); + offset += 4; // Alternating tone: first half high, second half low final halfSamples = numSamples ~/ 2; diff --git a/lib/services/emergency_notification_service.dart b/lib/services/emergency_notification_service.dart index 41d0d49..da0d852 100644 --- a/lib/services/emergency_notification_service.dart +++ b/lib/services/emergency_notification_service.dart @@ -7,13 +7,15 @@ import '../logging/app_log.dart'; /// Service to handle emergency-specific local notifications. class EmergencyNotificationService { EmergencyNotificationService._(); - static final EmergencyNotificationService instance = EmergencyNotificationService._(); + static final EmergencyNotificationService instance = + EmergencyNotificationService._(); static const String _channelId = 'roadsos_emergency'; static const String _channelName = 'Emergency Alerts'; static const int _sosActiveNotificationId = 9110; - final FlutterLocalNotificationsPlugin _local = FlutterLocalNotificationsPlugin(); + final FlutterLocalNotificationsPlugin _local = + FlutterLocalNotificationsPlugin(); bool _initialized = false; Future ensureInitialized() async { @@ -38,7 +40,9 @@ class EmergencyNotificationService { enableVibration: true, ); await _local - .resolvePlatformSpecificImplementation() + .resolvePlatformSpecificImplementation< + AndroidFlutterLocalNotificationsPlugin + >() ?.createNotificationChannel(channel); } } diff --git a/lib/services/emergency_orchestrator.dart b/lib/services/emergency_orchestrator.dart index 8295634..6d6e6f4 100644 --- a/lib/services/emergency_orchestrator.dart +++ b/lib/services/emergency_orchestrator.dart @@ -153,8 +153,6 @@ class EmergencyOrchestrator extends StateNotifier { static const Duration _dispatchChannelTimeout = Duration(seconds: 8); EmergencyOrchestrator(this._ref) : super(const SOSState()) { - - _restoreState(); _ref.read(crashDetectionServiceProvider).startMonitoring(); // Phase 8: ensure RL bias is loaded before any SOS fires. @@ -180,7 +178,11 @@ class EmergencyOrchestrator extends StateNotifier { } void _log(String message, SOSPhase phase, {bool isError = false}) { - final msg = SOSStatusMessage(message: message, phase: phase, isError: isError); + final msg = SOSStatusMessage( + message: message, + phase: phase, + isError: isError, + ); state = state.copyWith(statusLog: [msg, ...state.statusLog]); appLog.d('🚒 [ORCHESTRATOR] $message'); } @@ -217,9 +219,9 @@ class EmergencyOrchestrator extends StateNotifier { // Listen for voice cancel in parallel with countdown timer. // If the user says "cancel"/"stop"/locale equivalent → abort SOS. unawaited( - voice - .listenForCancel(listenFor: const Duration(seconds: 9)) - .then((cancelled) { + voice.listenForCancel(listenFor: const Duration(seconds: 9)).then(( + cancelled, + ) { if (cancelled && state.phase == SOSPhase.countdown) { appLog.i('[Orchestrator] Voice cancel detected — aborting SOS'); cancelSos(); @@ -262,7 +264,9 @@ class EmergencyOrchestrator extends StateNotifier { _log(detail, SOSPhase.active, isError: true); state = state.copyWith( phase: SOSPhase.active, - dispatchChannels: state.dispatchChannels.isEmpty ? _initialDispatchRows() : state.dispatchChannels, + dispatchChannels: state.dispatchChannels.isEmpty + ? _initialDispatchRows() + : state.dispatchChannels, ); await _persistState(true); unawaited(EmergencyBeaconService.instance.start()); @@ -280,7 +284,11 @@ class EmergencyOrchestrator extends StateNotifier { .getCurrentLocation() .timeout(_sosLocationTimeout); } catch (e, st) { - appLog.w('[Orchestrator] Location acquisition timed out/failed', error: e, stackTrace: st); + appLog.w( + '[Orchestrator] Location acquisition timed out/failed', + error: e, + stackTrace: st, + ); location = LocationFix( latitude: 0, longitude: 0, @@ -308,10 +316,26 @@ class EmergencyOrchestrator extends StateNotifier { phase: SOSPhase.dispatching, dispatchChannels: _initialDispatchRows(), ); - _patchDispatchChannel('mesh', DispatchChannelLifecycle.skipped, 'Skipped — no usable GPS fix.'); - _patchDispatchChannel('family_link', DispatchChannelLifecycle.skipped, 'Skipped — no usable GPS fix.'); - _patchDispatchChannel('local_log', DispatchChannelLifecycle.failed, 'Not saved — no usable GPS fix.'); - _patchDispatchChannel('sms', DispatchChannelLifecycle.inProgress, 'Sending emergency SMS (no GPS)…'); + _patchDispatchChannel( + 'mesh', + DispatchChannelLifecycle.skipped, + 'Skipped — no usable GPS fix.', + ); + _patchDispatchChannel( + 'family_link', + DispatchChannelLifecycle.skipped, + 'Skipped — no usable GPS fix.', + ); + _patchDispatchChannel( + 'local_log', + DispatchChannelLifecycle.failed, + 'Not saved — no usable GPS fix.', + ); + _patchDispatchChannel( + 'sms', + DispatchChannelLifecycle.inProgress, + 'Sending emergency SMS (no GPS)…', + ); final smsOutcome = await _dispatchSmsWithRetry( l10n.orchestratorSmsNoGpsPayload, lat: null, @@ -337,16 +361,14 @@ class EmergencyOrchestrator extends StateNotifier { return; } - final facilities = await _ref.read(facilityQueryServiceProvider).queryNearby( - location.latitude, - location.longitude, - ); + final facilities = await _ref + .read(facilityQueryServiceProvider) + .queryNearby(location.latitude, location.longitude); state = state.copyWith(nearbyFacilities: facilities); unawaited( - _ref.read(facilitySyncServiceProvider).syncLocalRegion( - location.latitude, - location.longitude, - ), + _ref + .read(facilitySyncServiceProvider) + .syncLocalRegion(location.latitude, location.longitude), ); _log(l10n.orchestratorAiBrief, SOSPhase.triaging); @@ -363,7 +385,11 @@ class EmergencyOrchestrator extends StateNotifier { ) .timeout(_sosTriageTimeout); } catch (e, st) { - appLog.w('[Orchestrator] Triage timed out/failed — using safety fallback', error: e, stackTrace: st); + appLog.w( + '[Orchestrator] Triage timed out/failed — using safety fallback', + error: e, + stackTrace: st, + ); rawTriage = TriageResult( functionCall: 'dispatch_emergency', location: '${location.latitude},${location.longitude}', @@ -388,7 +414,10 @@ class EmergencyOrchestrator extends StateNotifier { // The gyro service has a 3s rolling buffer so the crash peak is still in // memory even though a few seconds elapsed during GPS lock + triage. final gyroService = _ref.read(gyroscopeFusionServiceProvider); - final gyroPeak = gyroService.peakRadPerSecAt(DateTime.now(), windowMs: 3000); + final gyroPeak = gyroService.peakRadPerSecAt( + DateTime.now(), + windowMs: 3000, + ); final validation = triageValidationAgent.validate( raw: rawTriage, @@ -424,11 +453,31 @@ class EmergencyOrchestrator extends StateNotifier { final mesh = _ref.read(meshNetworkServiceProvider); - _patchDispatchChannel('mesh', DispatchChannelLifecycle.inProgress, 'Broadcasting BLE beacon…'); - _patchDispatchChannel('sms', DispatchChannelLifecycle.inProgress, 'Sending emergency SMS…'); - _patchDispatchChannel('local_log', DispatchChannelLifecycle.inProgress, 'Saving incident on device…'); - _patchDispatchChannel('family_link', DispatchChannelLifecycle.inProgress, 'Family tracking link…'); - _patchDispatchChannel('nearby_services', DispatchChannelLifecycle.inProgress, 'Broadcasting to nearby services…'); + _patchDispatchChannel( + 'mesh', + DispatchChannelLifecycle.inProgress, + 'Broadcasting BLE beacon…', + ); + _patchDispatchChannel( + 'sms', + DispatchChannelLifecycle.inProgress, + 'Sending emergency SMS…', + ); + _patchDispatchChannel( + 'local_log', + DispatchChannelLifecycle.inProgress, + 'Saving incident on device…', + ); + _patchDispatchChannel( + 'family_link', + DispatchChannelLifecycle.inProgress, + 'Family tracking link…', + ); + _patchDispatchChannel( + 'nearby_services', + DispatchChannelLifecycle.inProgress, + 'Broadcasting to nearby services…', + ); // Phase 9: Automated alerts and calling unawaited(_notifyUser()); @@ -445,120 +494,150 @@ class EmergencyOrchestrator extends StateNotifier { return await future.timeout( _dispatchChannelTimeout, onTimeout: () { - _patchDispatchChannel(id, DispatchChannelLifecycle.failed, timeoutDetail); + _patchDispatchChannel( + id, + DispatchChannelLifecycle.failed, + timeoutDetail, + ); return fallback; }, ); } catch (_) { - _patchDispatchChannel(id, DispatchChannelLifecycle.failed, failureDetail); + _patchDispatchChannel( + id, + DispatchChannelLifecycle.failed, + failureDetail, + ); return fallback; } } - final meshFuture = guard( - id: 'mesh', - future: mesh.startBroadcasting( - triage.compressedPayload, - lat: location.latitude, - lng: location.longitude, - severity: triage.severityLevel, - services: triage.requiredServices, - ), - fallback: false, - timeoutDetail: 'Mesh timed out — continue with SMS and manual action.', - failureDetail: 'Mesh failed — Bluetooth off, unsupported, or error.', - ).then((meshOk) { - _patchDispatchChannel( - 'mesh', - meshOk ? DispatchChannelLifecycle.success : DispatchChannelLifecycle.failed, - meshOk - ? 'Mesh beacon active — nearby app users can detect you ✓' - : 'Mesh did not start — Bluetooth off, unsupported, or failed.', - ); - return meshOk; - }); - - final smsFuture = guard( - id: 'sms', - future: _dispatchSmsWithRetry( - triage.compressedPayload, - lat: location.latitude, - lng: location.longitude, - ), - fallback: const SmsDispatchOutcome( - deviceDirectSmsSent: false, - backendRelayAccepted: false, - primaryAutomatedBarMet: false, - proofLevel: SmsDispatchProofLevel.none, - detail: 'SMS timed out — use dialer/manual SMS now.', - ), - timeoutDetail: 'SMS timed out — use dialer/manual SMS now.', - failureDetail: 'SMS failed — use dialer/manual SMS now.', - ).then((smsOutcome) { - _patchDispatchChannel( - 'sms', - smsOutcome.primaryAutomatedBarMet - ? DispatchChannelLifecycle.success - : DispatchChannelLifecycle.failed, - smsOutcome.detail, - ); - return smsOutcome; - }); - - final persistedFuture = guard<({bool ok, String detail})>( - id: 'local_log', - future: _persistIncidentSnapshot( - incidentId: state.incidentId ?? '', - location: location, - triage: triage, - ), - fallback: (ok: false, detail: 'Local log timed out — incident not saved.'), - timeoutDetail: 'Local log timed out — incident not saved.', - failureDetail: 'Local log failed — incident not saved.', - ).then((persisted) { - _patchDispatchChannel( - 'local_log', - persisted.ok ? DispatchChannelLifecycle.success : DispatchChannelLifecycle.failed, - persisted.detail, - ); - return persisted; - }); + final meshFuture = + guard( + id: 'mesh', + future: mesh.startBroadcasting( + triage.compressedPayload, + lat: location.latitude, + lng: location.longitude, + severity: triage.severityLevel, + services: triage.requiredServices, + ), + fallback: false, + timeoutDetail: + 'Mesh timed out — continue with SMS and manual action.', + failureDetail: 'Mesh failed — Bluetooth off, unsupported, or error.', + ).then((meshOk) { + _patchDispatchChannel( + 'mesh', + meshOk + ? DispatchChannelLifecycle.success + : DispatchChannelLifecycle.failed, + meshOk + ? 'Mesh beacon active — nearby app users can detect you ✓' + : 'Mesh did not start — Bluetooth off, unsupported, or failed.', + ); + return meshOk; + }); + + final smsFuture = + guard( + id: 'sms', + future: _dispatchSmsWithRetry( + triage.compressedPayload, + lat: location.latitude, + lng: location.longitude, + ), + fallback: const SmsDispatchOutcome( + deviceDirectSmsSent: false, + backendRelayAccepted: false, + primaryAutomatedBarMet: false, + proofLevel: SmsDispatchProofLevel.none, + detail: 'SMS timed out — use dialer/manual SMS now.', + ), + timeoutDetail: 'SMS timed out — use dialer/manual SMS now.', + failureDetail: 'SMS failed — use dialer/manual SMS now.', + ).then((smsOutcome) { + _patchDispatchChannel( + 'sms', + smsOutcome.primaryAutomatedBarMet + ? DispatchChannelLifecycle.success + : DispatchChannelLifecycle.failed, + smsOutcome.detail, + ); + return smsOutcome; + }); - final familyFuture = guard<({bool ok, String detail})>( - id: 'family_link', - future: _ref.read(familyTrackingServiceProvider).registerAndNotifyContact( + final persistedFuture = + guard<({bool ok, String detail})>( + id: 'local_log', + future: _persistIncidentSnapshot( incidentId: state.incidentId ?? '', location: location, triage: triage, ), - fallback: (ok: false, detail: 'Family link timed out — share manually if needed.'), - timeoutDetail: 'Family link timed out — share manually if needed.', - failureDetail: 'Family link failed — share manually if needed.', - ).then((family) { - _patchDispatchChannel( - 'family_link', - family.ok ? DispatchChannelLifecycle.success : DispatchChannelLifecycle.failed, - family.detail, - ); - return family; - }); - - final nearbyFuture = guard( - id: 'nearby_services', - future: Future.delayed(const Duration(seconds: 3), () => true), - fallback: false, - timeoutDetail: 'Nearby services broadcast timed out.', - failureDetail: 'Nearby services broadcast failed.', - ).then((ok) { - _patchDispatchChannel( - 'nearby_services', - ok ? DispatchChannelLifecycle.success : DispatchChannelLifecycle.failed, - ok - ? 'Emergency alert broadcasted to nearby facilities and responders ✓' - : 'Could not complete nearby services broadcast.', - ); - return ok; - }); + fallback: ( + ok: false, + detail: 'Local log timed out — incident not saved.', + ), + timeoutDetail: 'Local log timed out — incident not saved.', + failureDetail: 'Local log failed — incident not saved.', + ).then((persisted) { + _patchDispatchChannel( + 'local_log', + persisted.ok + ? DispatchChannelLifecycle.success + : DispatchChannelLifecycle.failed, + persisted.detail, + ); + return persisted; + }); + + final familyFuture = + guard<({bool ok, String detail})>( + id: 'family_link', + future: _ref + .read(familyTrackingServiceProvider) + .registerAndNotifyContact( + incidentId: state.incidentId ?? '', + location: location, + triage: triage, + ), + fallback: ( + ok: false, + detail: 'Family link timed out — share manually if needed.', + ), + timeoutDetail: 'Family link timed out — share manually if needed.', + failureDetail: 'Family link failed — share manually if needed.', + ).then((family) { + _patchDispatchChannel( + 'family_link', + family.ok + ? DispatchChannelLifecycle.success + : DispatchChannelLifecycle.failed, + family.detail, + ); + return family; + }); + + final nearbyFuture = + guard( + id: 'nearby_services', + future: Future.delayed(const Duration(seconds: 3), () => true), + fallback: false, + timeoutDetail: 'Nearby services broadcast timed out.', + failureDetail: 'Nearby services broadcast failed.', + ).then((ok) { + _patchDispatchChannel( + 'nearby_services', + ok + ? DispatchChannelLifecycle.success + : DispatchChannelLifecycle.failed, + ok + ? 'Emergency alert broadcasted to nearby facilities and responders ✓' + : 'Could not complete nearby services broadcast.', + ); + return ok; + }); List results; try { @@ -571,8 +650,14 @@ class EmergencyOrchestrator extends StateNotifier { ]).timeout(_dispatchChannelTimeout + const Duration(seconds: 1)); } catch (e, st) { // Absolute guard: never hang in dispatching. - appLog.w('[Orchestrator] Dispatch futures did not complete in time', error: e, stackTrace: st); - await failOpenToActive('Dispatch timed out — take manual action (dial emergency number).'); + appLog.w( + '[Orchestrator] Dispatch futures did not complete in time', + error: e, + stackTrace: st, + ); + await failOpenToActive( + 'Dispatch timed out — take manual action (dial emergency number).', + ); return; } @@ -625,12 +710,15 @@ class EmergencyOrchestrator extends StateNotifier { // Phase 7: post-dispatch voice briefing — the driver hears what was sent. if (state.wasInDrivingMode) { final voice = _ref.read(voiceAssistantServiceProvider); - unawaited(voice.speakTriageSummary( - severity: triage.severityLevel, - services: triage.requiredServices, - locationCoords: '${location.latitude.toStringAsFixed(2)}, ' - '${location.longitude.toStringAsFixed(2)}', - )); + unawaited( + voice.speakTriageSummary( + severity: triage.severityLevel, + services: triage.requiredServices, + locationCoords: + '${location.latitude.toStringAsFixed(2)}, ' + '${location.longitude.toStringAsFixed(2)}', + ), + ); } } @@ -697,7 +785,11 @@ class EmergencyOrchestrator extends StateNotifier { ]; } - void _patchDispatchChannel(String id, DispatchChannelLifecycle lifecycle, String detail) { + void _patchDispatchChannel( + String id, + DispatchChannelLifecycle lifecycle, + String detail, + ) { final list = List.from(state.dispatchChannels); final i = list.indexWhere((e) => e.id == id); if (i >= 0) { @@ -720,7 +812,8 @@ class EmergencyOrchestrator extends StateNotifier { try { final now = DateTime.now().toIso8601String(); final svc = triage.requiredServices.join(','); - final extended = await PrivacyConsentService.extendedRetentionForUploads(); + final extended = + await PrivacyConsentService.extendedRetentionForUploads(); await appDb.execute( '''INSERT INTO reported_incidents ( id, latitude, longitude, severity, services_needed, status, reported_at, created_at, extended_retention @@ -788,7 +881,11 @@ class EmergencyOrchestrator extends StateNotifier { appLog.w('[Orchestrator] Could not launch dialer for $contact'); } } catch (e, st) { - appLog.e('[Orchestrator] Error launching dialer', error: e, stackTrace: st); + appLog.e( + '[Orchestrator] Error launching dialer', + error: e, + stackTrace: st, + ); } } @@ -797,9 +894,10 @@ class EmergencyOrchestrator extends StateNotifier { } } -final emergencyOrchestratorProvider = StateNotifierProvider((ref) { - return EmergencyOrchestrator(ref); -}); +final emergencyOrchestratorProvider = + StateNotifierProvider((ref) { + return EmergencyOrchestrator(ref); + }); final voiceAssistantServiceProvider = Provider((ref) { return VoiceAssistantService(); diff --git a/lib/services/emergency_sms_dispatch_service.dart b/lib/services/emergency_sms_dispatch_service.dart index f610aaf..be961fa 100644 --- a/lib/services/emergency_sms_dispatch_service.dart +++ b/lib/services/emergency_sms_dispatch_service.dart @@ -87,7 +87,9 @@ class EmergencySmsDispatchService { if (deviceDirectSmsSent) return true; if (!backendRelayAccepted) return false; if (defaultTargetPlatform == TargetPlatform.iOS) return true; - final v = dotenv.env['SMS_RELAY_COUNTS_AS_PRIMARY_DISPATCH']?.trim().toLowerCase(); + final v = dotenv.env['SMS_RELAY_COUNTS_AS_PRIMARY_DISPATCH'] + ?.trim() + .toLowerCase(); return v == 'true' || v == '1' || v == 'yes'; } @@ -104,7 +106,9 @@ class EmergencySmsDispatchService { deviceDirectSmsSent: device, backendRelayAccepted: relay, primaryAutomatedBarMet: primary, - proofLevel: (device || relay) ? SmsDispatchProofLevel.accepted : SmsDispatchProofLevel.none, + proofLevel: (device || relay) + ? SmsDispatchProofLevel.accepted + : SmsDispatchProofLevel.none, detail: detail, ); } @@ -136,46 +140,51 @@ class EmergencySmsDispatchService { } // India — server-side relay when enrolled (MoHA / state ERSS integrations). - final inIndiaContext = cc == 'IN' || + final inIndiaContext = + cc == 'IN' || (lat != null && lng != null && coordinatesRoughlyInIndia(lat, lng)); if (inIndiaContext) { final indiaUrl = dotenv.env['INDIA_SOS_DISPATCH_URL']?.trim(); - final indiaDest = (dotenv.env['INDIA_EMERGENCY_NUMBER']?.trim().isNotEmpty ?? false) + final indiaDest = + (dotenv.env['INDIA_EMERGENCY_NUMBER']?.trim().isNotEmpty ?? false) ? dotenv.env['INDIA_EMERGENCY_NUMBER']!.trim() : '112'; - final route = lat != null && lng != null && coordinatesRoughlyInIndia(lat, lng) + final route = + lat != null && lng != null && coordinatesRoughlyInIndia(lat, lng) ? resolveIndiaEmergencyRoute(lat, lng) : null; if (indiaUrl != null && indiaUrl.isNotEmpty) { - final ok = await _postJson( - indiaUrl, - { - 'channel': 'india_emergency_sms', - 'country_code': 'IN', - 'destination': indiaDest, - 'payload': payload, - 'latitude': lat, - 'longitude': lng, - 'body': body, - if (route != null) ...{ - 'state_code': route.stateCode, - 'state_name': route.stateName, - 'ambulance_number': route.ambulanceNumber, - 'police_number': route.policeNumber, - 'fire_number': route.fireNumber, - }, + final ok = await _postJson(indiaUrl, { + 'channel': 'india_emergency_sms', + 'country_code': 'IN', + 'destination': indiaDest, + 'payload': payload, + 'latitude': lat, + 'longitude': lng, + 'body': body, + if (route != null) ...{ + 'state_code': route.stateCode, + 'state_name': route.stateName, + 'ambulance_number': route.ambulanceNumber, + 'police_number': route.policeNumber, + 'fire_number': route.fireNumber, }, - ); + }); if (ok) { appLog.d('SMS India relay accepted'); - final primary = _primaryAutomatedBar(deviceDirectSmsSent: false, backendRelayAccepted: true); + final primary = _primaryAutomatedBar( + deviceDirectSmsSent: false, + backendRelayAccepted: true, + ); final detail = primary ? 'India relay accepted ✓ (request handed off; not delivery-proof)' : 'India relay HTTP 2xx ✓ — primary (A) bar needs device SEND_SMS or ' - 'SMS_RELAY_COUNTS_AS_PRIMARY_DISPATCH=true (audited SMS to 112).'; + 'SMS_RELAY_COUNTS_AS_PRIMARY_DISPATCH=true (audited SMS to 112).'; return _outcome(device: false, relay: true, detail: detail); } - appLog.w('SMS India relay failed; falling back to device SMS where allowed'); + appLog.w( + 'SMS India relay failed; falling back to device SMS where allowed', + ); } } @@ -216,24 +225,22 @@ class EmergencySmsDispatchService { 'SMS did not send automatically on iOS — configure SMS_DISPATCH_URL or dial $emergencyNum manually.', ); } - final ok = await _postJson( - url, - { - 'channel': 'twilio_backend', - 'destination': emergencyNum, - 'country_code': cc, - 'payload': payload, - 'latitude': lat, - 'longitude': lng, - 'body': body, - }, - ); + final ok = await _postJson(url, { + 'channel': 'twilio_backend', + 'destination': emergencyNum, + 'country_code': cc, + 'payload': payload, + 'latitude': lat, + 'longitude': lng, + 'body': body, + }); if (ok) { appLog.d('iOS backend SMS dispatch sent'); return _outcome( device: false, relay: true, - detail: 'SMS relay accepted for $emergencyNum ✓ (request handed off; not delivery-proof)', + detail: + 'SMS relay accepted for $emergencyNum ✓ (request handed off; not delivery-proof)', ); } appLog.w('iOS backend SMS dispatch failed'); @@ -255,25 +262,27 @@ class EmergencySmsDispatchService { ) async { final relayUrl = dotenv.env['SMS_DISPATCH_URL']?.trim(); if (relayUrl != null && relayUrl.isNotEmpty) { - final relayOk = await _postJson( - relayUrl, - { - 'channel': 'twilio_backend', - 'destination': number, - 'country_code': cc, - 'payload': payload, - 'latitude': lat, - 'longitude': lng, - 'body': body, - }, - ); + final relayOk = await _postJson(relayUrl, { + 'channel': 'twilio_backend', + 'destination': number, + 'country_code': cc, + 'payload': payload, + 'latitude': lat, + 'longitude': lng, + 'body': body, + }); if (relayOk) { - appLog.d('Android SMS dispatched via SMS_DISPATCH_URL (Twilio/backend)'); - final primary = _primaryAutomatedBar(deviceDirectSmsSent: false, backendRelayAccepted: true); + appLog.d( + 'Android SMS dispatched via SMS_DISPATCH_URL (Twilio/backend)', + ); + final primary = _primaryAutomatedBar( + deviceDirectSmsSent: false, + backendRelayAccepted: true, + ); final detail = primary ? 'Backend relay accepted for $number ✓ (request handed off; not delivery-proof)' : 'Backend relay HTTP 2xx ✓ — primary bar needs device SEND_SMS or ' - 'SMS_RELAY_COUNTS_AS_PRIMARY_DISPATCH=true.'; + 'SMS_RELAY_COUNTS_AS_PRIMARY_DISPATCH=true.'; return _outcome(device: false, relay: true, detail: detail); } appLog.w( @@ -287,7 +296,8 @@ class EmergencySmsDispatchService { return _outcome( device: true, relay: false, - detail: 'Device SMS request accepted for $number ✓ (carrier delivery not confirmed)', + detail: + 'Device SMS request accepted for $number ✓ (carrier delivery not confirmed)', ); } @@ -307,10 +317,13 @@ class EmergencySmsDispatchService { final loc = (lat != null && lng != null) ? 'LOC ${lat.toStringAsFixed(5)},${lng.toStringAsFixed(5)} ' : ''; - final st = route != null ? 'STATE ${route.stateCode} ${route.stateName} ' : ''; + final st = route != null + ? 'STATE ${route.stateCode} ${route.stateName} ' + : ''; const maxPayload = 220; - final p = - payload.length > maxPayload ? '${payload.substring(0, maxPayload)}…' : payload; + final p = payload.length > maxPayload + ? '${payload.substring(0, maxPayload)}…' + : payload; return 'RoadSOS $st$loc$p'; } @@ -356,9 +369,7 @@ class EmergencySmsDispatchService { static Future _postJson(String url, Map body) async { try { - final headers = { - 'Content-Type': 'application/json', - }; + final headers = {'Content-Type': 'application/json'}; final secret = dotenv.env['SMS_DISPATCH_ANON_KEY']?.trim(); if (secret != null && secret.isNotEmpty) { headers['Authorization'] = 'Bearer $secret'; diff --git a/lib/services/family_tracking_service.dart b/lib/services/family_tracking_service.dart index 305ff6f..4f55cb2 100644 --- a/lib/services/family_tracking_service.dart +++ b/lib/services/family_tracking_service.dart @@ -30,8 +30,12 @@ class FamilyTrackingService { final digits = raw.replaceAll(RegExp(r'\D'), ''); if (digits.length < 10) return null; if (digits.length == 10) return digits; - if (digits.length == 11 && digits.startsWith('0')) return digits.substring(1); - if (digits.length >= 12 && digits.startsWith('91')) return digits.substring(digits.length - 10); + if (digits.length == 11 && digits.startsWith('0')) { + return digits.substring(1); + } + if (digits.length >= 12 && digits.startsWith('91')) { + return digits.substring(digits.length - 10); + } return digits.length > 10 ? digits.substring(digits.length - 10) : digits; } @@ -50,7 +54,10 @@ class FamilyTrackingService { final client = Supabase.instance.client; final user = client.auth.currentUser; if (user == null) { - return (ok: false, detail: 'No auth session — sign in anonymously for family link.'); + return ( + ok: false, + detail: 'No auth session — sign in anonymously for family link.', + ); } final baseUrl = dotenv.env['SUPABASE_URL']?.trim() ?? ''; @@ -61,8 +68,9 @@ class FamilyTrackingService { final token = const Uuid().v4(); final rawSummary = '${triage.functionCall} · sev ${triage.severityLevel} · ${triage.compressedPayload}'; - final triageSummary = - rawSummary.length > 1200 ? '${rawSummary.substring(0, 1197)}…' : rawSummary; + final triageSummary = rawSummary.length > 1200 + ? '${rawSummary.substring(0, 1197)}…' + : rawSummary; final expiresAt = DateTime.now().toUtc().add(const Duration(hours: 24)); @@ -98,7 +106,8 @@ class FamilyTrackingService { if (contacts.isEmpty) { return ( ok: true, - detail: 'Family link active — add contacts in Medical profile to auto-SMS.', + detail: + 'Family link active — add contacts in Medical profile to auto-SMS.', ); } @@ -126,7 +135,8 @@ class FamilyTrackingService { } return ( ok: true, - detail: 'Link ready — SMS failed for all contacts (permission?). Open link: $trackingUrl', + detail: + 'Link ready — SMS failed for all contacts (permission?). Open link: $trackingUrl', ); } diff --git a/lib/services/first_aid_repository.dart b/lib/services/first_aid_repository.dart index 7d93080..bb57f5e 100644 --- a/lib/services/first_aid_repository.dart +++ b/lib/services/first_aid_repository.dart @@ -43,8 +43,9 @@ CREATE VIRTUAL TABLE IF NOT EXISTS first_aid_fts USING fts5( ); '''); - final countRows = - await appDb.getAll('SELECT COUNT(*) AS c FROM first_aid_fts'); + final countRows = await appDb.getAll( + 'SELECT COUNT(*) AS c FROM first_aid_fts', + ); final count = (countRows.first['c'] as num?)?.toInt() ?? 0; if (count == 0 && _corpusRows!.isNotEmpty) { @@ -107,8 +108,8 @@ CREATE VIRTUAL TABLE IF NOT EXISTS first_aid_fts USING fts5( var bestScore = -1; Map? best; for (final row in _corpusRows!) { - final haystack = - '${row['title']} ${row['body']} ${row['tags']}'.toLowerCase(); + final haystack = '${row['title']} ${row['body']} ${row['tags']}' + .toLowerCase(); var score = 0; for (final t in tokens) { if (haystack.contains(t)) score += 3; diff --git a/lib/services/gemini_http.dart b/lib/services/gemini_http.dart index 0619104..25245ff 100644 --- a/lib/services/gemini_http.dart +++ b/lib/services/gemini_http.dart @@ -23,18 +23,11 @@ Future generateGeminiFlashText({ ], }, ], - 'generationConfig': { - 'temperature': 0.3, - 'maxOutputTokens': 256, - }, + 'generationConfig': {'temperature': 0.3, 'maxOutputTokens': 256}, }); final response = await http - .post( - uri, - headers: {'Content-Type': 'application/json'}, - body: body, - ) + .post(uri, headers: {'Content-Type': 'application/json'}, body: body) .timeout(timeout); if (response.statusCode != 200) { diff --git a/lib/services/gemma_local_service.dart b/lib/services/gemma_local_service.dart index 91b8cf9..0519af7 100644 --- a/lib/services/gemma_local_service.dart +++ b/lib/services/gemma_local_service.dart @@ -114,7 +114,9 @@ class GemmaLocalService { loadedViaLoadModel = true; appLog.d('[GemmaLocal] loadModel() succeeded'); } on NoSuchMethodError { - appLog.d('[GemmaLocal] loadModel() not in this build — will pass via init()'); + appLog.d( + '[GemmaLocal] loadModel() not in this build — will pass via init()', + ); } catch (e) { appLog.w('[GemmaLocal] loadModel() threw: $e — trying init() fallback'); } @@ -152,7 +154,11 @@ class GemmaLocalService { } } } catch (e, st) { - appLog.w('[GemmaLocal] flutter_gemma init() failed', error: e, stackTrace: st); + appLog.w( + '[GemmaLocal] flutter_gemma init() failed', + error: e, + stackTrace: st, + ); return null; } @@ -184,11 +190,17 @@ class GemmaLocalService { if (raw == null || raw.isEmpty) return null; final parsed = _parseJSON(raw); if (parsed != null) { - appLog.d('[GemmaLocal] On-device triage: severity=${parsed['severity_level']}'); + appLog.d( + '[GemmaLocal] On-device triage: severity=${parsed['severity_level']}', + ); } return parsed; } catch (e, st) { - appLog.w('[GemmaLocal] Inference failed — marking tier unavailable', error: e, stackTrace: st); + appLog.w( + '[GemmaLocal] Inference failed — marking tier unavailable', + error: e, + stackTrace: st, + ); _available = false; // Fall through to Tier 3 on next call _lastError = 'Inference error: $e'; return null; @@ -216,7 +228,11 @@ class GemmaLocalService { if (token != null) yield token; } } catch (e, st) { - appLog.w('[GemmaLocal] generateStream() failed', error: e, stackTrace: st); + appLog.w( + '[GemmaLocal] generateStream() failed', + error: e, + stackTrace: st, + ); } } diff --git a/lib/services/gemma_model_manager.dart b/lib/services/gemma_model_manager.dart index 9aa366d..eb4506d 100644 --- a/lib/services/gemma_model_manager.dart +++ b/lib/services/gemma_model_manager.dart @@ -33,7 +33,8 @@ class GemmaModelManager { 'https://huggingface.co/google/gemma-4-e4b-it-GGUF/resolve/main/$_modelFileName'; /// HuggingFace terms acceptance page (user must visit before downloading). - static const String hfTermsUrl = 'https://huggingface.co/google/gemma-4-e4b-it-GGUF'; + static const String hfTermsUrl = + 'https://huggingface.co/google/gemma-4-e4b-it-GGUF'; /// HuggingFace token creation page. static const String hfTokenUrl = 'https://huggingface.co/settings/tokens'; @@ -105,7 +106,9 @@ class GemmaModelManager { int alreadyHave = 0; if (tmpFile.existsSync()) { alreadyHave = tmpFile.lengthSync(); - appLog.i('[GemmaModel] Resuming download from ${(alreadyHave / 1e6).round()} MB'); + appLog.i( + '[GemmaModel] Resuming download from ${(alreadyHave / 1e6).round()} MB', + ); } final headers = { @@ -153,7 +156,9 @@ class GemmaModelManager { try { await for (final chunk in response.stream) { if (cancelToken?.isCancelled ?? false) { - appLog.i('[GemmaModel] Download cancelled — partial file kept for resume'); + appLog.i( + '[GemmaModel] Download cancelled — partial file kept for resume', + ); await sink.flush(); await sink.close(); return; @@ -180,7 +185,9 @@ class GemmaModelManager { // Atomic rename: .download → final path. await tmpFile.rename(path); - appLog.i('[GemmaModel] ✓ Download complete — ${(finalSize / 1e6).round()} MB at $path'); + appLog.i( + '[GemmaModel] ✓ Download complete — ${(finalSize / 1e6).round()} MB at $path', + ); } finally { client.close(); } diff --git a/lib/services/hardware_trigger_service.dart b/lib/services/hardware_trigger_service.dart index f48401d..128f9d9 100644 --- a/lib/services/hardware_trigger_service.dart +++ b/lib/services/hardware_trigger_service.dart @@ -19,8 +19,9 @@ final hardwareTriggerServiceProvider = Provider((ref) { }); class HardwareTriggerService { - static const MethodChannel _channel = - MethodChannel('com.codestreak.roadsos/hardware_buttons'); + static const MethodChannel _channel = MethodChannel( + 'com.codestreak.roadsos/hardware_buttons', + ); final Ref _ref; HardwareTriggerService(this._ref) { diff --git a/lib/services/inactivity_crash_detector.dart b/lib/services/inactivity_crash_detector.dart index f8a0ebe..7400792 100644 --- a/lib/services/inactivity_crash_detector.dart +++ b/lib/services/inactivity_crash_detector.dart @@ -39,12 +39,13 @@ class InactivityCrashDetector { final Ref _ref; static const double _stillnessRmsThresholdMs2 = 1.8; + /// RMS rolling window duration (~50 Hz accelerometer samples). static const int _stillnessWindowSec = 5; static const int _accelApproxHz = 50; - static const int _incapacitationThresholdSec = 50; // sustained stillness - static const int _cooldownMs = 5 * 60 * 1000; - static const int _minDrivingSecondsBeforeArming = 60; + static const int _incapacitationThresholdSec = 50; // sustained stillness + static const int _cooldownMs = 5 * 60 * 1000; + static const int _minDrivingSecondsBeforeArming = 60; StreamSubscription? _accelSub; Timer? _evaluationTimer; @@ -60,9 +61,9 @@ class InactivityCrashDetector { stopMonitoring(); // Subscribe to accelerometer for RMS tracking. - _accelSub = SensorsPlatform.instance - .userAccelerometerEventStream() - .listen(_onAccel); + _accelSub = SensorsPlatform.instance.userAccelerometerEventStream().listen( + _onAccel, + ); // Evaluate incapacitation window every 10 seconds. _evaluationTimer = Timer.periodic( @@ -87,18 +88,17 @@ class InactivityCrashDetector { void _evaluate() { final mode = _ref.read(drivingModeProvider); - final now = DateTime.now(); + final now = DateTime.now(); if (mode != DrivingMode.driving) { _drivingActiveSince = null; - _stilnessStartedAt = null; + _stilnessStartedAt = null; return; } // Track how long we've been driving. _drivingActiveSince ??= now; - final drivingSeconds = - now.difference(_drivingActiveSince!).inSeconds; + final drivingSeconds = now.difference(_drivingActiveSince!).inSeconds; // Arm only after minimum driving duration. if (drivingSeconds < _minDrivingSecondsBeforeArming) return; @@ -109,8 +109,7 @@ class InactivityCrashDetector { if (isStill) { _stilnessStartedAt ??= now; - final stillSeconds = - now.difference(_stilnessStartedAt!).inSeconds; + final stillSeconds = now.difference(_stilnessStartedAt!).inSeconds; appLog.d( '[Inactivity] Device still for ${stillSeconds}s ' @@ -135,7 +134,7 @@ class InactivityCrashDetector { ts.difference(_lastTrigger!).inMilliseconds < _cooldownMs) { return; // Cooldown — user already cancelled once. } - _lastTrigger = ts; + _lastTrigger = ts; _stilnessStartedAt = null; // Reset so we don't re-fire immediately. appLog.w( @@ -152,7 +151,7 @@ class InactivityCrashDetector { _evaluationTimer?.cancel(); _evaluationTimer = null; _rmsWindow.clear(); - _stilnessStartedAt = null; + _stilnessStartedAt = null; _drivingActiveSince = null; } @@ -160,7 +159,9 @@ class InactivityCrashDetector { } /// Non-autoDispose: must run for the full app session to detect incapacitation. -final inactivityCrashDetectorProvider = Provider((ref) { +final inactivityCrashDetectorProvider = Provider(( + ref, +) { final svc = InactivityCrashDetector(ref); svc.startMonitoring(); ref.onDispose(svc.dispose); diff --git a/lib/services/india_emergency_routing.dart b/lib/services/india_emergency_routing.dart index 0140ffe..cf60ff8 100644 --- a/lib/services/india_emergency_routing.dart +++ b/lib/services/india_emergency_routing.dart @@ -105,11 +105,7 @@ IndiaEmergencyRoute? _resolveByCoarseBoxes(double lat, double lng) { } class _Centroid { - const _Centroid( - this.code, - this.name, - this.point, - ); + const _Centroid(this.code, this.name, this.point); final String code; final String name; @@ -149,7 +145,11 @@ final List<_Centroid> _centroids = [ _Centroid('IN-WB', 'West Bengal', LatLng(22.99, 87.68)), _Centroid('IN-AN', 'Andaman and Nicobar Islands', LatLng(11.74, 92.66)), _Centroid('IN-CH', 'Chandigarh', LatLng(30.73, 76.78)), - _Centroid('IN-DN', 'Dadra and Nagar Haveli and Daman and Diu', LatLng(20.27, 73.02)), + _Centroid( + 'IN-DN', + 'Dadra and Nagar Haveli and Daman and Diu', + LatLng(20.27, 73.02), + ), _Centroid('IN-DL', 'Delhi', LatLng(28.61, 77.20)), _Centroid('IN-JK', 'Jammu and Kashmir', LatLng(33.77, 76.57)), _Centroid('IN-LA', 'Ladakh', LatLng(34.15, 77.58)), diff --git a/lib/services/india_government_crash_contribution_service.dart b/lib/services/india_government_crash_contribution_service.dart index 7f249a1..7d101c8 100644 --- a/lib/services/india_government_crash_contribution_service.dart +++ b/lib/services/india_government_crash_contribution_service.dart @@ -35,7 +35,9 @@ class IndiaGovernmentCrashContributionService { if (cells.isEmpty) return; final uri = Uri.parse(base); - final token = dotenv.maybeGet('GOVERNMENT_CRASH_CONTRIBUTION_TOKEN')?.trim(); + final token = dotenv + .maybeGet('GOVERNMENT_CRASH_CONTRIBUTION_TOKEN') + ?.trim(); final headers = { 'Content-Type': 'application/json', @@ -81,14 +83,14 @@ class CrashDensityCell { final int maxSeverityBucket; Map toJson() => { - 'grid_id': gridId, - 'latitude': latitude, - 'longitude': longitude, - 'report_count': reportCount, - 'window_start_utc': windowStartUtc, - 'window_end_utc': windowEndUtc, - 'max_severity_bucket': maxSeverityBucket, - }; + 'grid_id': gridId, + 'latitude': latitude, + 'longitude': longitude, + 'report_count': reportCount, + 'window_start_utc': windowStartUtc, + 'window_end_utc': windowEndUtc, + 'max_severity_bucket': maxSeverityBucket, + }; } class HttpContributionException implements Exception { diff --git a/lib/services/ios_lifecycle_service.dart b/lib/services/ios_lifecycle_service.dart index 29ebcf1..415c33a 100644 --- a/lib/services/ios_lifecycle_service.dart +++ b/lib/services/ios_lifecycle_service.dart @@ -17,8 +17,9 @@ final iosLifecycleServiceProvider = Provider((ref) { class IosLifecycleService { IosLifecycleService(); - static const MethodChannel _channel = - MethodChannel('com.codestreak.roadsos/ios_lifecycle'); + static const MethodChannel _channel = MethodChannel( + 'com.codestreak.roadsos/ios_lifecycle', + ); void attach() { if (kIsWeb) return; diff --git a/lib/services/location_service.dart b/lib/services/location_service.dart index b7fb2b3..04f58fe 100644 --- a/lib/services/location_service.dart +++ b/lib/services/location_service.dart @@ -26,7 +26,8 @@ class LocationFix { } @override - String toString() => '($latitude, $longitude) ±${accuracy.round()}m [$source]'; + String toString() => + '($latitude, $longitude) ±${accuracy.round()}m [$source]'; } /// Provides real-time location with dead-reckoning fallback. @@ -77,10 +78,14 @@ class LocationService { ); final fix = _toFix(position, source: 'gps'); _recordFix(fix); - appLog.d('[Location] GPS fix acquired (attempt 1): ${fix.accuracy.round()}m'); + appLog.d( + '[Location] GPS fix acquired (attempt 1): ${fix.accuracy.round()}m', + ); return fix; } catch (e) { - appLog.d('[Location] Attempt 1 failed ($e) — retrying at medium accuracy'); + appLog.d( + '[Location] Attempt 1 failed ($e) — retrying at medium accuracy', + ); } // Attempt 2: medium accuracy (cell-towers + GPS), 5-second window. @@ -94,7 +99,9 @@ class LocationService { ); final fix = _toFix(position, source: 'network'); _recordFix(fix); - appLog.d('[Location] GPS fix acquired (attempt 2 medium): ${fix.accuracy.round()}m'); + appLog.d( + '[Location] GPS fix acquired (attempt 2 medium): ${fix.accuracy.round()}m', + ); return fix; } catch (e) { appLog.d('[Location] Attempt 2 failed ($e) — trying last known from OS'); @@ -106,7 +113,9 @@ class LocationService { if (last != null) { final fix = _toFix(last, source: 'last_known'); _recordFix(fix); - appLog.d('[Location] Using OS last-known position: ${fix.accuracy.round()}m'); + appLog.d( + '[Location] Using OS last-known position: ${fix.accuracy.round()}m', + ); return fix; } } catch (_) {} @@ -151,7 +160,9 @@ class LocationService { _lastGoodFix = fix; _positionHistory.addLast(fix); - final cutoff = DateTime.now().subtract(const Duration(seconds: _maxHistorySeconds)); + final cutoff = DateTime.now().subtract( + const Duration(seconds: _maxHistorySeconds), + ); while (_positionHistory.isNotEmpty && _positionHistory.first.timestamp.isBefore(cutoff)) { _positionHistory.removeFirst(); diff --git a/lib/services/mesh_chat_service.dart b/lib/services/mesh_chat_service.dart index ccd6e8b..d37bcce 100644 --- a/lib/services/mesh_chat_service.dart +++ b/lib/services/mesh_chat_service.dart @@ -17,7 +17,7 @@ class MeshMessage { class MeshChatService extends StateNotifier> { final MeshNetworkService _meshService; StreamSubscription? _packetSub; - + MeshChatService(this._meshService) : super([]) { _listenForIncomingMessages(); } @@ -40,7 +40,7 @@ class MeshChatService extends StateNotifier> { timestamp: DateTime.now(), ); state = [...state, newMessage]; - + // Broadcast via Mesh await _meshService.startBroadcasting('MSG:$content'); } @@ -61,7 +61,10 @@ class MeshChatService extends StateNotifier> { } } -final meshChatProvider = StateNotifierProvider.autoDispose>((ref) { - final mesh = ref.watch(meshNetworkServiceProvider); - return MeshChatService(mesh); -}); +final meshChatProvider = + StateNotifierProvider.autoDispose>(( + ref, + ) { + final mesh = ref.watch(meshNetworkServiceProvider); + return MeshChatService(mesh); + }); diff --git a/lib/services/mesh_network_service.dart b/lib/services/mesh_network_service.dart index 28e31bd..b5df214 100644 --- a/lib/services/mesh_network_service.dart +++ b/lib/services/mesh_network_service.dart @@ -43,10 +43,13 @@ class MeshPacket { /// rendering — including mid-emergency when the dispatch panel covered the /// radar. The service must live for the entire app session. class MeshNetworkService { - final ble_adv.FlutterBlePeripheral _peripheral = ble_adv.FlutterBlePeripheral(); + final ble_adv.FlutterBlePeripheral _peripheral = + ble_adv.FlutterBlePeripheral(); - final _discoveredBeaconsController = StreamController>.broadcast(); - Stream> get discoveredBeacons => _discoveredBeaconsController.stream; + final _discoveredBeaconsController = + StreamController>.broadcast(); + Stream> get discoveredBeacons => + _discoveredBeaconsController.stream; final _packetsController = StreamController.broadcast(); Stream get packets => _packetsController.stream; @@ -108,7 +111,8 @@ class MeshNetworkService { // Try binary decode first (v2 BlePayloadCodec), fall back to UTF-8 text. final decoded = BlePayloadCodec.decode(md); - final displayPayload = decoded?.toDisplayString() ?? _tryDecodeUtf8(md) ?? ''; + final displayPayload = + decoded?.toDisplayString() ?? _tryDecodeUtf8(md) ?? ''; if (displayPayload.isNotEmpty && !_packetsController.isClosed) { _emitPacketDedup(id, displayPayload, r.rssi, decoded); @@ -146,7 +150,12 @@ class MeshNetworkService { } } - void _emitPacketDedup(String senderId, String payload, int? rssi, BleDecodedPayload? decoded) { + void _emitPacketDedup( + String senderId, + String payload, + int? rssi, + BleDecodedPayload? decoded, + ) { final now = DateTime.now(); final key = '$senderId|$payload'; final last = _recentPacketDedup[key]; @@ -248,7 +257,8 @@ class MeshNetworkService { if (kIsWeb) return smsOutcome; if (lat == null || lng == null) return smsOutcome; - final inIndia = CountryCodes.getDeviceLocale()?.countryCode == 'IN' || + final inIndia = + CountryCodes.getDeviceLocale()?.countryCode == 'IN' || coordinatesRoughlyInIndia(lat, lng); if (!inIndia) return smsOutcome; diff --git a/lib/services/multi_agent_coordinator.dart b/lib/services/multi_agent_coordinator.dart index e7f4280..8499e3d 100644 --- a/lib/services/multi_agent_coordinator.dart +++ b/lib/services/multi_agent_coordinator.dart @@ -30,10 +30,8 @@ class AgentTask { DateTime? startedAt; DateTime? completedAt; - AgentTask({ - required this.id, - required this.displayName, - }) : status = AgentStatus.pending; + AgentTask({required this.id, required this.displayName}) + : status = AgentStatus.pending; Duration? get duration => (startedAt != null && completedAt != null) ? completedAt!.difference(startedAt!) @@ -60,7 +58,7 @@ class AgentTask { /// await coord.awaitAll(); /// ``` class MultiAgentCoordinator { - final _tasks = {}; + final _tasks = {}; final _controller = StreamController.broadcast(); bool _aborted = false; @@ -97,9 +95,9 @@ class MultiAgentCoordinator { void _transition(AgentTask task, AgentStatus next, {String? error}) { task.status = next; - if (next == AgentStatus.running) task.startedAt = DateTime.now(); - if (task.isTerminal) task.completedAt = DateTime.now(); - if (error != null) task.errorDetail = error; + if (next == AgentStatus.running) task.startedAt = DateTime.now(); + if (task.isTerminal) task.completedAt = DateTime.now(); + if (error != null) task.errorDetail = error; final dur = task.duration; appLog.d( @@ -117,7 +115,9 @@ class MultiAgentCoordinator { } /// Wait for all registered tasks to reach a terminal state. - Future awaitAll({Duration timeout = const Duration(seconds: 30)}) async { + Future awaitAll({ + Duration timeout = const Duration(seconds: 30), + }) async { final deadline = DateTime.now().add(timeout); while (!_tasks.values.every((t) => t.isTerminal)) { if (DateTime.now().isAfter(deadline)) { @@ -145,9 +145,13 @@ class MultiAgentCoordinator { /// Summary stats for the activity log. String get summaryLine { - final done = _tasks.values.where((t) => t.status == AgentStatus.completed).length; - final failed = _tasks.values.where((t) => t.status == AgentStatus.failed).length; - final total = _tasks.length; + final done = _tasks.values + .where((t) => t.status == AgentStatus.completed) + .length; + final failed = _tasks.values + .where((t) => t.status == AgentStatus.failed) + .length; + final total = _tasks.length; return '$done/$total agents completed${failed > 0 ? ", $failed failed" : ""}.'; } } diff --git a/lib/services/nearby_sos_push_service.dart b/lib/services/nearby_sos_push_service.dart index 6f48a0b..0c4dce9 100644 --- a/lib/services/nearby_sos_push_service.dart +++ b/lib/services/nearby_sos_push_service.dart @@ -89,7 +89,11 @@ class NearbySosPushService { await messaging.unsubscribeFromTopic(_topic); } } catch (e, st) { - appLog.w('FCM topic subscribe/unsubscribe failed', error: e, stackTrace: st); + appLog.w( + 'FCM topic subscribe/unsubscribe failed', + error: e, + stackTrace: st, + ); } } @@ -108,7 +112,9 @@ class NearbySosPushService { importance: Importance.high, ); await _local - .resolvePlatformSpecificImplementation() + .resolvePlatformSpecificImplementation< + AndroidFlutterLocalNotificationsPlugin + >() ?.createNotificationChannel(channel); } @@ -116,14 +122,17 @@ class NearbySosPushService { final ctx = appNavigatorKey.currentContext; if (ctx != null && ctx.mounted) { ScaffoldMessenger.of(ctx).showSnackBar( - const SnackBar(content: Text('Open Settings → Nearby SOS for Good Samaritan info.')), + const SnackBar( + content: Text('Open Settings → Nearby SOS for Good Samaritan info.'), + ), ); } } Future _onForegroundMessage(RemoteMessage message) async { final title = message.notification?.title ?? 'Nearby SOS'; - final body = message.notification?.body ?? + final body = + message.notification?.body ?? message.data['body']?.toString() ?? 'Someone nearby may need help.'; @@ -147,9 +156,9 @@ class NearbySosPushService { final ctx = appNavigatorKey.currentContext; if (ctx != null && ctx.mounted) { - ScaffoldMessenger.of(ctx).showSnackBar( - SnackBar(content: Text('$title — $body')), - ); + ScaffoldMessenger.of( + ctx, + ).showSnackBar(SnackBar(content: Text('$title — $body'))); } } @@ -157,7 +166,9 @@ class NearbySosPushService { final ctx = appNavigatorKey.currentContext; if (ctx != null && ctx.mounted) { ScaffoldMessenger.of(ctx).showSnackBar( - SnackBar(content: Text(message.notification?.title ?? 'Nearby SOS alert')), + SnackBar( + content: Text(message.notification?.title ?? 'Nearby SOS alert'), + ), ); } } diff --git a/lib/services/offline_triage_classifier.dart b/lib/services/offline_triage_classifier.dart index 25b3a51..5bb6280 100644 --- a/lib/services/offline_triage_classifier.dart +++ b/lib/services/offline_triage_classifier.dart @@ -41,8 +41,8 @@ class OfflineTriageClassifier { return fromText > h ? fromText : fromText >= h - ? fromText - : ((fromText + h + 1) ~/ 2).clamp(1, 5); + ? fromText + : ((fromText + h + 1) ~/ 2).clamp(1, 5); } int _estimateSeverityFromText(String text) { diff --git a/lib/services/predictive_sos_preloader.dart b/lib/services/predictive_sos_preloader.dart index 8f91303..ff856dd 100644 --- a/lib/services/predictive_sos_preloader.dart +++ b/lib/services/predictive_sos_preloader.dart @@ -30,10 +30,7 @@ class PredictiveSosPreloader { /// Fire-and-forget — do not await. static Future onDrivingModeActivated() async { appLog.d('[Preloader] Driving mode activated — pre-warming connections'); - await Future.wait([ - _prewarmSupabaseEdge(), - _prewarmGps(), - ]); + await Future.wait([_prewarmSupabaseEdge(), _prewarmGps()]); appLog.d('[Preloader] Pre-warm complete'); } diff --git a/lib/services/privacy_consent_service.dart b/lib/services/privacy_consent_service.dart index 3eb3da5..7ec79ba 100644 --- a/lib/services/privacy_consent_service.dart +++ b/lib/services/privacy_consent_service.dart @@ -13,7 +13,9 @@ class PrivacyConsentService { return p.getString(_kConsentAt) != null; } - static Future recordConsent({required bool extendedCloudRetention}) async { + static Future recordConsent({ + required bool extendedCloudRetention, + }) async { final p = await SharedPreferences.getInstance(); await p.setString(_kConsentAt, DateTime.now().toUtc().toIso8601String()); await p.setBool(_kExtendedRetention, extendedCloudRetention); diff --git a/lib/services/proactive_monitor_service.dart b/lib/services/proactive_monitor_service.dart index 0c2e386..c030d10 100644 --- a/lib/services/proactive_monitor_service.dart +++ b/lib/services/proactive_monitor_service.dart @@ -118,6 +118,8 @@ class ProactiveMonitorService extends StateNotifier { /// The escalation must survive widget lifecycle changes to guarantee the 60s /// SOS escalation fires even when the app is in the background. final proactiveMonitorProvider = - StateNotifierProvider((ref) { - return ProactiveMonitorService(ref); -}); + StateNotifierProvider(( + ref, + ) { + return ProactiveMonitorService(ref); + }); diff --git a/lib/services/remote_crash_config.dart b/lib/services/remote_crash_config.dart index d716bff..23bffa3 100644 --- a/lib/services/remote_crash_config.dart +++ b/lib/services/remote_crash_config.dart @@ -166,8 +166,10 @@ class RemoteCrashConfig { /// the network. Falls through to hardcoded defaults if no cache exists. Future loadCachedValues() async { await Future.wait([_loadThresholdsFromCache(), _loadRegionsFromCache()]); - appLog.d('[RemoteCrashConfig] Cache loaded. ' - 'zones=${_values.keys.toList()} regions=${_regions.length}'); + appLog.d( + '[RemoteCrashConfig] Cache loaded. ' + 'zones=${_values.keys.toList()} regions=${_regions.length}', + ); } // ── Phase 2: Remote fetch ────────────────────────────────────────────────── @@ -183,7 +185,10 @@ class RemoteCrashConfig { appLog.d('[RemoteCrashConfig] Skipping refresh — Supabase not ready'); return; } - await Future.wait([_fetchThresholdsFromSupabase(), _fetchRegionsFromSupabase()]); + await Future.wait([ + _fetchThresholdsFromSupabase(), + _fetchRegionsFromSupabase(), + ]); } /// Start periodic background refresh every [refreshInterval]. @@ -196,8 +201,10 @@ class RemoteCrashConfig { appLog.d('[RemoteCrashConfig] Periodic refresh triggered'); refresh(); }); - appLog.i('[RemoteCrashConfig] Periodic refresh armed: every ' - '${refreshInterval.inMinutes} min'); + appLog.i( + '[RemoteCrashConfig] Periodic refresh armed: every ' + '${refreshInterval.inMinutes} min', + ); } /// Call when the app returns to foreground so threshold changes propagate @@ -225,8 +232,10 @@ class RemoteCrashConfig { if (region.contains(lat, lng)) { final newZone = _parseZone(region.zone); if (newZone != _currentZone || _currentRegionName != region.name) { - appLog.d('[RemoteCrashConfig] Zone ← region "${region.name}" ' - '(${region.zone}): $_currentZone → $newZone'); + appLog.d( + '[RemoteCrashConfig] Zone ← region "${region.name}" ' + '(${region.zone}): $_currentZone → $newZone', + ); _currentZone = newZone; _currentRegionName = region.name; } @@ -280,7 +289,9 @@ class RemoteCrashConfig { .timeout(const Duration(seconds: 5)); if (rows.isEmpty) { - appLog.d('[RemoteCrashConfig] crash_config table empty — defaults retained'); + appLog.d( + '[RemoteCrashConfig] crash_config table empty — defaults retained', + ); return; } @@ -298,11 +309,16 @@ class RemoteCrashConfig { ..addAll(fetched); await _persistThresholdsToCache(); - appLog.i('[RemoteCrashConfig] Thresholds refreshed: ' - '${rows.length} rows, zones=${_values.keys.toList()}'); + appLog.i( + '[RemoteCrashConfig] Thresholds refreshed: ' + '${rows.length} rows, zones=${_values.keys.toList()}', + ); } catch (e, st) { - appLog.w('[RemoteCrashConfig] Threshold fetch failed — cached values in use', - error: e, stackTrace: st); + appLog.w( + '[RemoteCrashConfig] Threshold fetch failed — cached values in use', + error: e, + stackTrace: st, + ); } } @@ -318,15 +334,17 @@ class RemoteCrashConfig { final fetched = <_ConfigRegion>[]; for (final row in rows) { try { - fetched.add(_ConfigRegion( - name: row['name'] as String, - zone: row['zone'] as String, - minLat: (row['min_lat'] as num).toDouble(), - maxLat: (row['max_lat'] as num).toDouble(), - minLng: (row['min_lng'] as num).toDouble(), - maxLng: (row['max_lng'] as num).toDouble(), - priority: (row['priority'] as num).toInt(), - )); + fetched.add( + _ConfigRegion( + name: row['name'] as String, + zone: row['zone'] as String, + minLat: (row['min_lat'] as num).toDouble(), + maxLat: (row['max_lat'] as num).toDouble(), + minLng: (row['min_lng'] as num).toDouble(), + maxLng: (row['max_lng'] as num).toDouble(), + priority: (row['priority'] as num).toInt(), + ), + ); } catch (_) {} } @@ -335,10 +353,15 @@ class RemoteCrashConfig { ..addAll(fetched); await _persistRegionsToCache(); - appLog.i('[RemoteCrashConfig] Regions refreshed: ${_regions.length} geofences'); + appLog.i( + '[RemoteCrashConfig] Regions refreshed: ${_regions.length} geofences', + ); } catch (e, st) { - appLog.w('[RemoteCrashConfig] Region fetch failed — cached regions in use', - error: e, stackTrace: st); + appLog.w( + '[RemoteCrashConfig] Region fetch failed — cached regions in use', + error: e, + stackTrace: st, + ); } } @@ -353,7 +376,9 @@ class RemoteCrashConfig { _values.clear(); for (final entry in decoded.entries) { final inner = entry.value as Map; - _values[entry.key] = inner.map((k, v) => MapEntry(k, (v as num).toDouble())); + _values[entry.key] = inner.map( + (k, v) => MapEntry(k, (v as num).toDouble()), + ); } } catch (e) { appLog.d('[RemoteCrashConfig] Threshold cache empty (first run): $e'); @@ -378,15 +403,17 @@ class RemoteCrashConfig { _regions.clear(); for (final item in list) { final m = item as Map; - _regions.add(_ConfigRegion( - name: m['name'] as String, - zone: m['zone'] as String, - minLat: (m['min_lat'] as num).toDouble(), - maxLat: (m['max_lat'] as num).toDouble(), - minLng: (m['min_lng'] as num).toDouble(), - maxLng: (m['max_lng'] as num).toDouble(), - priority: (m['priority'] as num).toInt(), - )); + _regions.add( + _ConfigRegion( + name: m['name'] as String, + zone: m['zone'] as String, + minLat: (m['min_lat'] as num).toDouble(), + maxLat: (m['max_lat'] as num).toDouble(), + minLng: (m['min_lng'] as num).toDouble(), + maxLng: (m['max_lng'] as num).toDouble(), + priority: (m['priority'] as num).toInt(), + ), + ); } } catch (e) { appLog.d('[RemoteCrashConfig] Region cache empty (first run): $e'); @@ -397,15 +424,17 @@ class RemoteCrashConfig { try { final prefs = await SharedPreferences.getInstance(); final list = _regions - .map((r) => { - 'name': r.name, - 'zone': r.zone, - 'min_lat': r.minLat, - 'max_lat': r.maxLat, - 'min_lng': r.minLng, - 'max_lng': r.maxLng, - 'priority': r.priority, - }) + .map( + (r) => { + 'name': r.name, + 'zone': r.zone, + 'min_lat': r.minLat, + 'max_lat': r.maxLat, + 'min_lng': r.minLng, + 'max_lng': r.maxLng, + 'priority': r.priority, + }, + ) .toList(); await prefs.setString(_regionsCacheKey, jsonEncode(list)); } catch (e) { @@ -427,11 +456,14 @@ class RemoteCrashConfig { _setZone(RoadZone.unknown, source: 'no-speed-data'); return; } - final avg = _speedWindow.map((e) => e.$2).reduce((a, b) => a + b) / + final avg = + _speedWindow.map((e) => e.$2).reduce((a, b) => a + b) / _speedWindow.length; final zone = avg >= _highwaySpeedKmh ? RoadZone.highway : RoadZone.urban; - _setZone(zone, - source: 'speed-heuristic(avg=${avg.toStringAsFixed(1)} km/h)'); + _setZone( + zone, + source: 'speed-heuristic(avg=${avg.toStringAsFixed(1)} km/h)', + ); } void _setZone(RoadZone zone, {required String source}) { diff --git a/lib/services/roadsos_assistant_service.dart b/lib/services/roadsos_assistant_service.dart index c0c9247..d697ae5 100644 --- a/lib/services/roadsos_assistant_service.dart +++ b/lib/services/roadsos_assistant_service.dart @@ -181,17 +181,36 @@ class RoadSosAssistantService extends StateNotifier { } return null; } catch (e, st) { - appLog.d('[Assistant] Gemma 4 cloud generate failed', error: e, stackTrace: st); + appLog.d( + '[Assistant] Gemma 4 cloud generate failed', + error: e, + stackTrace: st, + ); return null; } } String _detectSceneContext(String input) { final lower = input.toLowerCase(); - if (lower.contains('pedestrian') || lower.contains('पैदल')) return 'pedestrian_hit'; - if (lower.contains('rollover') || lower.contains('पलटा') || lower.contains('overturned')) return 'rollover'; - if (lower.contains('fire') || lower.contains('आग') || lower.contains('smoke') || lower.contains('धुआँ')) return 'fire_hazard'; - if (lower.contains('collision') || lower.contains('टक्कर') || lower.contains('crash')) return 'vehicle_collision'; + if (lower.contains('pedestrian') || lower.contains('पैदल')) { + return 'pedestrian_hit'; + } + if (lower.contains('rollover') || + lower.contains('पलटा') || + lower.contains('overturned')) { + return 'rollover'; + } + if (lower.contains('fire') || + lower.contains('आग') || + lower.contains('smoke') || + lower.contains('धुआँ')) { + return 'fire_hazard'; + } + if (lower.contains('collision') || + lower.contains('टक्कर') || + lower.contains('crash')) { + return 'vehicle_collision'; + } return 'unknown'; } @@ -216,8 +235,11 @@ class RoadSosAssistantService extends StateNotifier { }) async { if (state.history.isEmpty) { final detectedContext = _detectSceneContext(previousAnswer); - final questions = languageCode == 'hi' ? _sceneQuestionsHi : _sceneQuestionsEn; - final sceneQuestions = questions[detectedContext] ?? questions['unknown']!; + final questions = languageCode == 'hi' + ? _sceneQuestionsHi + : _sceneQuestionsEn; + final sceneQuestions = + questions[detectedContext] ?? questions['unknown']!; final firstQuestion = sceneQuestions.first; state = state.copyWith( @@ -252,8 +274,11 @@ class RoadSosAssistantService extends StateNotifier { String? gemmaQuestion = await _gemma4Generate(gemmaPrompt); - final questions = languageCode == 'hi' ? _sceneQuestionsHi : _sceneQuestionsEn; - final sceneQuestions = questions[state.sceneContext] ?? questions['unknown']!; + final questions = languageCode == 'hi' + ? _sceneQuestionsHi + : _sceneQuestionsEn; + final sceneQuestions = + questions[state.sceneContext] ?? questions['unknown']!; final nextIndex = state.questionIndex % sceneQuestions.length; var fallbackQuestion = sceneQuestions[nextIndex]; @@ -267,12 +292,13 @@ class RoadSosAssistantService extends StateNotifier { } final bool newInterviewComplete = - state.askedQuestions.length >= sceneQuestions.length && gemmaQuestion == null; + state.askedQuestions.length >= sceneQuestions.length && + gemmaQuestion == null; final response = newInterviewComplete ? (languageCode == 'hi' - ? 'साक्षात्कार पूर्ण। धन्यवाद।' - : 'Interview complete. Thank you.') + ? 'साक्षात्कार पूर्ण। धन्यवाद।' + : 'Interview complete. Thank you.') : (gemmaQuestion ?? fallbackQuestion); final newGuidanceSteps = newInterviewComplete @@ -298,40 +324,135 @@ class RoadSosAssistantService extends StateNotifier { switch (sceneContext) { case 'vehicle_collision': return [ - GuidanceStep(stepNumber: 1, title: 'सुरक्षा सुनिश्चित करें', description: 'घायलों को सड़क से हटाएं। ट्रैफिक की चेतावनी दें।', icon: '🚨'), - GuidanceStep(stepNumber: 2, title: 'आपातकालीन सेवाएं बुलाएं', description: '112 डायल करें।', icon: '📞'), - GuidanceStep(stepNumber: 3, title: 'प्राथमिक चिकित्सा करें', description: 'रक्तस्राव नियंत्रित करें। एयरवे खुला रखें।', icon: '🏥'), - GuidanceStep(stepNumber: 4, title: 'साक्ष्य संरक्षित करें', description: 'तस्वीरें लें। चश्मदीद खोजें।', icon: '📸'), - GuidanceStep(stepNumber: 5, title: 'पुलिस को सूचित करें', description: 'FIR दर्ज करें।', icon: '👮'), + GuidanceStep( + stepNumber: 1, + title: 'सुरक्षा सुनिश्चित करें', + description: 'घायलों को सड़क से हटाएं। ट्रैफिक की चेतावनी दें।', + icon: '🚨', + ), + GuidanceStep( + stepNumber: 2, + title: 'आपातकालीन सेवाएं बुलाएं', + description: '112 डायल करें।', + icon: '📞', + ), + GuidanceStep( + stepNumber: 3, + title: 'प्राथमिक चिकित्सा करें', + description: 'रक्तस्राव नियंत्रित करें। एयरवे खुला रखें।', + icon: '🏥', + ), + GuidanceStep( + stepNumber: 4, + title: 'साक्ष्य संरक्षित करें', + description: 'तस्वीरें लें। चश्मदीद खोजें।', + icon: '📸', + ), + GuidanceStep( + stepNumber: 5, + title: 'पुलिस को सूचित करें', + description: 'FIR दर्ज करें।', + icon: '👮', + ), ]; default: return [ - GuidanceStep(stepNumber: 1, title: '112 डायल करें', description: 'तुरंत आपातकालीन सेवा बुलाएं।', icon: '📞'), - GuidanceStep(stepNumber: 2, title: 'घायलों को स्थिर करें', description: 'हिलाएं नहीं। सहारा दें।', icon: '🤝'), - GuidanceStep(stepNumber: 3, title: 'क्षेत्र सुरक्षित करें', description: 'ट्रैफिक को चेतावनी दें।', icon: '🚧'), + GuidanceStep( + stepNumber: 1, + title: '112 डायल करें', + description: 'तुरंत आपातकालीन सेवा बुलाएं।', + icon: '📞', + ), + GuidanceStep( + stepNumber: 2, + title: 'घायलों को स्थिर करें', + description: 'हिलाएं नहीं। सहारा दें।', + icon: '🤝', + ), + GuidanceStep( + stepNumber: 3, + title: 'क्षेत्र सुरक्षित करें', + description: 'ट्रैफिक को चेतावनी दें।', + icon: '🚧', + ), ]; } } else { switch (sceneContext) { case 'vehicle_collision': return [ - GuidanceStep(stepNumber: 1, title: 'Ensure Scene Safety', description: 'Move injured to safety if possible. Warn traffic.', icon: '🚨'), - GuidanceStep(stepNumber: 2, title: 'Call Emergency Services', description: 'Dial 112 or 911.', icon: '📞'), - GuidanceStep(stepNumber: 3, title: 'Provide First Aid', description: 'Control bleeding. Keep airway open.', icon: '🏥'), - GuidanceStep(stepNumber: 4, title: 'Preserve Evidence', description: 'Take photos. Note vehicle numbers.', icon: '📸'), - GuidanceStep(stepNumber: 5, title: 'Notify Police', description: 'File incident report.', icon: '👮'), + GuidanceStep( + stepNumber: 1, + title: 'Ensure Scene Safety', + description: 'Move injured to safety if possible. Warn traffic.', + icon: '🚨', + ), + GuidanceStep( + stepNumber: 2, + title: 'Call Emergency Services', + description: 'Dial 112 or 911.', + icon: '📞', + ), + GuidanceStep( + stepNumber: 3, + title: 'Provide First Aid', + description: 'Control bleeding. Keep airway open.', + icon: '🏥', + ), + GuidanceStep( + stepNumber: 4, + title: 'Preserve Evidence', + description: 'Take photos. Note vehicle numbers.', + icon: '📸', + ), + GuidanceStep( + stepNumber: 5, + title: 'Notify Police', + description: 'File incident report.', + icon: '👮', + ), ]; case 'fire_hazard': return [ - GuidanceStep(stepNumber: 1, title: 'Evacuate Immediately', description: 'Move everyone away from fire.', icon: '🏃'), - GuidanceStep(stepNumber: 2, title: 'Call Fire Brigade', description: 'Dial 101 (India) or 112.', icon: '🚒'), - GuidanceStep(stepNumber: 3, title: 'Call Ambulance', description: 'Dial 102 (India) for burn injuries.', icon: '🚑'), + GuidanceStep( + stepNumber: 1, + title: 'Evacuate Immediately', + description: 'Move everyone away from fire.', + icon: '🏃', + ), + GuidanceStep( + stepNumber: 2, + title: 'Call Fire Brigade', + description: 'Dial 101 (India) or 112.', + icon: '🚒', + ), + GuidanceStep( + stepNumber: 3, + title: 'Call Ambulance', + description: 'Dial 102 (India) for burn injuries.', + icon: '🚑', + ), ]; default: return [ - GuidanceStep(stepNumber: 1, title: 'Call Emergency Services', description: 'Dial 112. Describe situation clearly.', icon: '📞'), - GuidanceStep(stepNumber: 2, title: 'Stabilize Victims', description: 'Do not move injured unless in danger.', icon: '🤝'), - GuidanceStep(stepNumber: 3, title: 'Secure the Scene', description: 'Warn traffic. Keep crowd back.', icon: '🚧'), + GuidanceStep( + stepNumber: 1, + title: 'Call Emergency Services', + description: 'Dial 112. Describe situation clearly.', + icon: '📞', + ), + GuidanceStep( + stepNumber: 2, + title: 'Stabilize Victims', + description: 'Do not move injured unless in danger.', + icon: '🤝', + ), + GuidanceStep( + stepNumber: 3, + title: 'Secure the Scene', + description: 'Warn traffic. Keep crowd back.', + icon: '🚧', + ), ]; } } @@ -340,8 +461,8 @@ class RoadSosAssistantService extends StateNotifier { final roadSosAssistantServiceProvider = StateNotifierProvider((ref) { - return RoadSosAssistantService(); -}); + return RoadSosAssistantService(); + }); /// Alias used by UI files. final roadsosAssistantProvider = roadSosAssistantServiceProvider; diff --git a/lib/services/safe_walk_notification_service.dart b/lib/services/safe_walk_notification_service.dart index 71c06e1..de2cd6e 100644 --- a/lib/services/safe_walk_notification_service.dart +++ b/lib/services/safe_walk_notification_service.dart @@ -66,14 +66,13 @@ class SafeWalkNotificationService { ); await _local .resolvePlatformSpecificImplementation< - AndroidFlutterLocalNotificationsPlugin>() + AndroidFlutterLocalNotificationsPlugin + >() ?.createNotificationChannel(channel); } } - Future showCheckInNow({ - required String destination, - }) async { + Future showCheckInNow({required String destination}) async { if (kIsWeb) return; try { @@ -154,4 +153,3 @@ class SafeWalkNotificationService { ); } } - diff --git a/lib/services/scene_security_service.dart b/lib/services/scene_security_service.dart index 30929d8..6d965d9 100644 --- a/lib/services/scene_security_service.dart +++ b/lib/services/scene_security_service.dart @@ -7,12 +7,14 @@ class SceneSecurityService { /// This ensures 'Spatial Privacy' - only people in the same region at the same time can decrypt. static String generateSceneKey(double lat, double lng) { // Round to ~1.1km precision (2 decimal places) - final String spatialHash = "${lat.toStringAsFixed(2)}:${lng.toStringAsFixed(2)}"; - final int hourStamp = DateTime.now().millisecondsSinceEpoch ~/ (1000 * 60 * 60); - + final String spatialHash = + "${lat.toStringAsFixed(2)}:${lng.toStringAsFixed(2)}"; + final int hourStamp = + DateTime.now().millisecondsSinceEpoch ~/ (1000 * 60 * 60); + final bytes = utf8.encode("$spatialHash:$hourStamp"); final digest = sha256.convert(bytes); - + return digest.toString().substring(0, 32); // Use first 32 chars for AES-256 } @@ -32,16 +34,20 @@ class SceneSecurityService { nonce: nonce, ); final nonceB64 = base64UrlEncode(secretBox.nonce); - final cipherB64 = base64UrlEncode( - [...secretBox.cipherText, ...secretBox.mac.bytes], - ); + final cipherB64 = base64UrlEncode([ + ...secretBox.cipherText, + ...secretBox.mac.bytes, + ]); return 'v1.$nonceB64.$cipherB64'; } /// Attempts to decrypt an AES-GCM payload created by [encryptPayload]. /// /// Returns `null` if decryption fails (wrong key / corrupted / wrong format). - static Future decryptPayload(String encoded, String keyString) async { + static Future decryptPayload( + String encoded, + String keyString, + ) async { try { final parts = encoded.split('.'); if (parts.length != 3 || parts[0] != 'v1') return null; @@ -53,11 +59,7 @@ class SceneSecurityService { final keyBytes = utf8.encode(keyString.substring(0, 32)); final secretKey = SecretKey(keyBytes); - final secretBox = SecretBox( - cipherText, - nonce: nonce, - mac: Mac(macBytes), - ); + final secretBox = SecretBox(cipherText, nonce: nonce, mac: Mac(macBytes)); final clear = await _aead.decrypt(secretBox, secretKey: secretKey); return utf8.decode(clear); diff --git a/lib/services/sms_direct_send_io.dart b/lib/services/sms_direct_send_io.dart index ef15923..522644b 100644 --- a/lib/services/sms_direct_send_io.dart +++ b/lib/services/sms_direct_send_io.dart @@ -24,24 +24,19 @@ Future sendSmsDirectAndroidImpl(String number, String message) async { if (!Platform.isAndroid) return false; // Truncate to one SMS length; carrier routing adds ~40 char overhead. - final body = message.length > 160 ? '${message.substring(0, 157)}...' : message; + final body = message.length > 160 + ? '${message.substring(0, 157)}...' + : message; // Encode body per RFC 5724 — url_launcher handles the Uri encoding. - final uri = Uri( - scheme: 'sms', - path: number, - queryParameters: {'body': body}, - ); + final uri = Uri(scheme: 'sms', path: number, queryParameters: {'body': body}); try { if (!await canLaunchUrl(uri)) { appLog.w('[SmsDirect] Cannot launch SMS URI — no messaging app found'); return false; } - final launched = await launchUrl( - uri, - mode: LaunchMode.externalApplication, - ); + final launched = await launchUrl(uri, mode: LaunchMode.externalApplication); if (launched) { appLog.d('[SmsDirect] SMS app opened for $number (user must tap Send)'); } else { diff --git a/lib/services/sms_dispatch_outcome.dart b/lib/services/sms_dispatch_outcome.dart index 23e9bfb..41bed6f 100644 --- a/lib/services/sms_dispatch_outcome.dart +++ b/lib/services/sms_dispatch_outcome.dart @@ -8,6 +8,7 @@ /// This model intentionally tracks *request acceptance* rather than "delivered". enum SmsDispatchProofLevel { none, + /// We successfully handed the request to either the OS (SEND_SMS) or a backend relay (HTTP 2xx). accepted, } diff --git a/lib/services/sms_permission_bootstrap_io.dart b/lib/services/sms_permission_bootstrap_io.dart index 380e457..cca310f 100644 --- a/lib/services/sms_permission_bootstrap_io.dart +++ b/lib/services/sms_permission_bootstrap_io.dart @@ -14,12 +14,10 @@ Future requestSmsPermissionEarlyIfAndroidImpl() async { try { // Intentionally no-op: direct SEND_SMS is increasingly restricted by OS/policy. // RoadSOS uses server relay (Twilio / Edge Function) and SMS-app intent fallback. - await Permission.sms.status; // keep plugin warmed for health checks if needed + await Permission + .sms + .status; // keep plugin warmed for health checks if needed } catch (e, st) { - appLog.w( - 'Startup SMS permission request failed', - error: e, - stackTrace: st, - ); + appLog.w('Startup SMS permission request failed', error: e, stackTrace: st); } } diff --git a/lib/services/sos_activity_log_service.dart b/lib/services/sos_activity_log_service.dart index 99089b5..c68208e 100644 --- a/lib/services/sos_activity_log_service.dart +++ b/lib/services/sos_activity_log_service.dart @@ -33,9 +33,13 @@ class SosActivityLogService { try { final existing = await loadHistory(); final next = [record, ...existing]; - final trimmed = - next.length > _maxRecords ? next.sublist(0, _maxRecords) : next; - await _storage.write(key: _key, value: jsonEncode(trimmed.map((e) => e.toJson()).toList())); + final trimmed = next.length > _maxRecords + ? next.sublist(0, _maxRecords) + : next; + await _storage.write( + key: _key, + value: jsonEncode(trimmed.map((e) => e.toJson()).toList()), + ); } catch (e, st) { appLog.w('Activity log append failed', error: e, stackTrace: st); } diff --git a/lib/services/sos_location_tracker.dart b/lib/services/sos_location_tracker.dart index e9cbbd5..01bab90 100644 --- a/lib/services/sos_location_tracker.dart +++ b/lib/services/sos_location_tracker.dart @@ -38,7 +38,7 @@ import 'emergency_orchestrator.dart'; /// - GPS permission loss stops streaming; an error is logged. class SosLocationTracker { static const Duration _updateInterval = Duration(seconds: 30); - static const int _maxMissedUpdates = 4; // stop after 2 min of GPS failure + static const int _maxMissedUpdates = 4; // stop after 2 min of GPS failure Timer? _updateTimer; StreamSubscription? _positionSub; @@ -48,17 +48,17 @@ class SosLocationTracker { /// Attach to the Riverpod container. Call once from a long-lived provider. void attach(Ref ref) { - ref.listen( - emergencyOrchestratorProvider.select((s) => s.phase), - (prev, next) { - if (next == SOSPhase.active && prev != SOSPhase.active) { - final incidentId = ref.read(emergencyOrchestratorProvider).incidentId; - _start(incidentId); - } else if (next == SOSPhase.idle) { - _stop(); - } - }, - ); + ref.listen(emergencyOrchestratorProvider.select((s) => s.phase), ( + prev, + next, + ) { + if (next == SOSPhase.active && prev != SOSPhase.active) { + final incidentId = ref.read(emergencyOrchestratorProvider).incidentId; + _start(incidentId); + } else if (next == SOSPhase.idle) { + _stop(); + } + }); } void _start(String? incidentId) { @@ -66,24 +66,30 @@ class SosLocationTracker { if (_updateTimer != null) return; // already running _activeIncidentId = incidentId; - _missedUpdates = 0; - - appLog.i('[SosLocationTracker] Started streaming location for incident $_activeIncidentId'); - - _positionSub = Geolocator.getPositionStream( - locationSettings: const LocationSettings( - accuracy: LocationAccuracy.high, - distanceFilter: 0, - // Minimum 15s interval between positions — OS enforces this. - ), - ).listen( - (p) => _latestPosition = p, - onError: (Object e) { - appLog.w('[SosLocationTracker] GPS stream error: $e'); - }, + _missedUpdates = 0; + + appLog.i( + '[SosLocationTracker] Started streaming location for incident $_activeIncidentId', ); - _updateTimer = Timer.periodic(_updateInterval, (_) => unawaited(_pushUpdate())); + _positionSub = + Geolocator.getPositionStream( + locationSettings: const LocationSettings( + accuracy: LocationAccuracy.high, + distanceFilter: 0, + // Minimum 15s interval between positions — OS enforces this. + ), + ).listen( + (p) => _latestPosition = p, + onError: (Object e) { + appLog.w('[SosLocationTracker] GPS stream error: $e'); + }, + ); + + _updateTimer = Timer.periodic( + _updateInterval, + (_) => unawaited(_pushUpdate()), + ); } void _stop() { @@ -91,19 +97,21 @@ class SosLocationTracker { _updateTimer = null; _positionSub?.cancel(); _positionSub = null; - _latestPosition = null; + _latestPosition = null; _activeIncidentId = null; - _missedUpdates = 0; + _missedUpdates = 0; appLog.i('[SosLocationTracker] Stopped'); } Future _pushUpdate() async { final pos = _latestPosition; - final id = _activeIncidentId; + final id = _activeIncidentId; if (pos == null || id == null) { _missedUpdates++; if (_missedUpdates >= _maxMissedUpdates) { - appLog.w('[SosLocationTracker] $_maxMissedUpdates missed GPS updates — stopping'); + appLog.w( + '[SosLocationTracker] $_maxMissedUpdates missed GPS updates — stopping', + ); _stop(); } return; @@ -119,13 +127,16 @@ class SosLocationTracker { return; } - await client.from('incident_live_links').update({ - 'latitude' : pos.latitude, - 'longitude' : pos.longitude, - 'accuracy_m' : pos.accuracy, - 'speed_kmh' : pos.speed >= 0 ? (pos.speed * 3.6) : null, - 'updated_at' : DateTime.now().toUtc().toIso8601String(), - }).eq('incident_id', id); + await client + .from('incident_live_links') + .update({ + 'latitude': pos.latitude, + 'longitude': pos.longitude, + 'accuracy_m': pos.accuracy, + 'speed_kmh': pos.speed >= 0 ? (pos.speed * 3.6) : null, + 'updated_at': DateTime.now().toUtc().toIso8601String(), + }) + .eq('incident_id', id); _missedUpdates = 0; appLog.d( @@ -134,7 +145,9 @@ class SosLocationTracker { 'acc=${pos.accuracy.toStringAsFixed(0)}m', ); } catch (e) { - appLog.w('[SosLocationTracker] Update failed: $e — will retry next cycle'); + appLog.w( + '[SosLocationTracker] Update failed: $e — will retry next cycle', + ); _missedUpdates++; } } diff --git a/lib/services/tier2_local_triage_model.dart b/lib/services/tier2_local_triage_model.dart index 93ebf1d..ba8d425 100644 --- a/lib/services/tier2_local_triage_model.dart +++ b/lib/services/tier2_local_triage_model.dart @@ -20,9 +20,15 @@ class Tier2LocalTriageModel { var score = 0; // Life-threatening indicators. - if (_hasAny(t, const ['not breathing', 'no pulse', 'cardiac arrest'])) score += 8; - if (_hasAny(t, const ['unconscious', 'passed out', 'unresponsive'])) score += 6; - if (_hasAny(t, const ['bleeding heavily', 'severe bleeding', 'spurting'])) score += 6; + if (_hasAny(t, const ['not breathing', 'no pulse', 'cardiac arrest'])) { + score += 8; + } + if (_hasAny(t, const ['unconscious', 'passed out', 'unresponsive'])) { + score += 6; + } + if (_hasAny(t, const ['bleeding heavily', 'severe bleeding', 'spurting'])) { + score += 6; + } if (_hasAny(t, const ['trapped', 'pinned', 'stuck in vehicle'])) score += 6; // Major trauma / dangerous context. @@ -31,7 +37,9 @@ class Tier2LocalTriageModel { if (_hasAny(t, const ['fire', 'smoke', 'burning', 'explosion'])) score += 5; // Moderate indicators. - if (_hasAny(t, const ['pain', 'hurt', 'bleeding', 'crash', 'accident'])) score += 2; + if (_hasAny(t, const ['pain', 'hurt', 'bleeding', 'crash', 'accident'])) { + score += 2; + } // Hint from crash detection (1-5): amplify but do not let it dominate fully. score += (h - 1) * 2; @@ -67,7 +75,13 @@ class Tier2LocalTriageModel { if (_hasAny(t, const ['fire', 'smoke', 'burning', 'explosion'])) { services.add('fire_department'); } - if (_hasAny(t, const ['police', 'hit and run', 'drunk', 'attack', 'crime'])) { + if (_hasAny(t, const [ + 'police', + 'hit and run', + 'drunk', + 'attack', + 'crime', + ])) { services.add('police'); } if (_hasAny(t, const ['trapped', 'pinned', 'rescue'])) { @@ -83,13 +97,21 @@ class Tier2LocalTriageModel { } String _firstAidQueryFromText(String t) { - if (t.contains('bleed')) return 'severe bleeding wound management tourniquet'; - if (_hasAny(t, const ['burn', 'smoke'])) return 'burn wound first aid cool water'; + if (t.contains('bleed')) { + return 'severe bleeding wound management tourniquet'; + } + if (_hasAny(t, const ['burn', 'smoke'])) { + return 'burn wound first aid cool water'; + } if (_hasAny(t, const ['not breathing', 'cpr', 'choking'])) { return 'CPR rescue breathing Heimlich'; } - if (_hasAny(t, const ['fracture', 'broken'])) return 'fracture immobilization splint'; - if (_hasAny(t, const ['head injury', 'concussion'])) return 'head injury concussion protocol'; + if (_hasAny(t, const ['fracture', 'broken'])) { + return 'fracture immobilization splint'; + } + if (_hasAny(t, const ['head injury', 'concussion'])) { + return 'head injury concussion protocol'; + } return 'general road accident first aid emergency response'; } } @@ -105,4 +127,3 @@ class Tier2LocalTriage { required this.firstAidQuery, }); } - diff --git a/lib/services/triage_feedback_service.dart b/lib/services/triage_feedback_service.dart index 99af183..43a7b07 100644 --- a/lib/services/triage_feedback_service.dart +++ b/lib/services/triage_feedback_service.dart @@ -24,15 +24,15 @@ class TriageFeedbackService { TriageFeedbackService._(); static final instance = TriageFeedbackService._(); - static const _kBias = 'triage_severity_bias'; - static const _kCount = 'triage_feedback_count'; - static const _kHistory = 'triage_feedback_history'; - static const _kAlpha = 0.15; // EMA learning rate - static const _kMinSamples = 3; // No bias until 3 feedbacks collected + static const _kBias = 'triage_severity_bias'; + static const _kCount = 'triage_feedback_count'; + static const _kHistory = 'triage_feedback_history'; + static const _kAlpha = 0.15; // EMA learning rate + static const _kMinSamples = 3; // No bias until 3 feedbacks collected static const _kMaxHistory = 20; double _bias = 0.0; - int _count = 0; + int _count = 0; /// How many severity levels to shift Tier 3/4 output. /// Positive = inflate; negative = deflate. @@ -44,8 +44,8 @@ class TriageFeedbackService { Future initialize() async { final prefs = await SharedPreferences.getInstance(); - _bias = prefs.getDouble(_kBias) ?? 0.0; - _count = prefs.getInt(_kCount) ?? 0; + _bias = prefs.getDouble(_kBias) ?? 0.0; + _count = prefs.getInt(_kCount) ?? 0; appLog.d( '[Feedback] RL bias=${_bias.toStringAsFixed(2)} n=$_count ' '(active=$isLearningActive)', @@ -64,23 +64,27 @@ class TriageFeedbackService { final prefs = await SharedPreferences.getInstance(); // EMA update: bias moves 15% toward the new signal each time. - _bias = (_bias * (1.0 - _kAlpha) + severityDelta * _kAlpha).clamp(-1.0, 1.0); + _bias = (_bias * (1.0 - _kAlpha) + severityDelta * _kAlpha).clamp( + -1.0, + 1.0, + ); _count++; - await prefs.setDouble(_kBias, _bias); - await prefs.setInt(_kCount, _count); + await prefs.setDouble(_kBias, _bias); + await prefs.setInt(_kCount, _count); // Append to bounded history for Settings audit trail. final rawHistory = prefs.getString(_kHistory); final history = rawHistory != null ? List>.from( - (jsonDecode(rawHistory) as List).cast>()) + (jsonDecode(rawHistory) as List).cast>(), + ) : >[]; history.add({ - 'id': incidentId, - 'ts': DateTime.now().toIso8601String(), - 'severity_delta': severityDelta, + 'id': incidentId, + 'ts': DateTime.now().toIso8601String(), + 'severity_delta': severityDelta, 'services_correct': servicesCorrect, }); @@ -111,7 +115,7 @@ class TriageFeedbackService { /// Reset all learning — called from Settings. Future resetBias() async { - _bias = 0.0; + _bias = 0.0; _count = 0; final prefs = await SharedPreferences.getInstance(); await prefs.remove(_kBias); diff --git a/lib/services/triage_validation_agent.dart b/lib/services/triage_validation_agent.dart index d1caccd..5b60d4d 100644 --- a/lib/services/triage_validation_agent.dart +++ b/lib/services/triage_validation_agent.dart @@ -58,7 +58,9 @@ class TriageValidationAgent { // A person triggering SOS while driving is almost certainly in a crash. // Minimum severity 3 (moderate) regardless of transcript context. if (drivingMode == DrivingMode.driving && severity < 3) { - overrides.add('Severity raised $severity→3 (driving mode active at SOS trigger).'); + overrides.add( + 'Severity raised $severity→3 (driving mode active at SOS trigger).', + ); severity = 3; flags.add('severity_floor_driving_mode'); } @@ -90,9 +92,7 @@ class TriageValidationAgent { } // ── Rule F: Police recommended for severity 5 ───────────────────────── - final hasAuthority = services.any( - (s) => s == 'police' || s == 'rescue', - ); + final hasAuthority = services.any((s) => s == 'police' || s == 'rescue'); if (severity == 5 && !hasAuthority) { flags.add('consider_police_severity5'); } @@ -113,9 +113,7 @@ class TriageValidationAgent { // ── Build result ────────────────────────────────────────────────────── final wasOverridden = overrides.isNotEmpty; if (wasOverridden) { - appLog.w( - '[ValidationAgent] Overrode triage: ${overrides.join(" | ")}', - ); + appLog.w('[ValidationAgent] Overrode triage: ${overrides.join(" | ")}'); } else { appLog.d( '[ValidationAgent] Triage validated — no overrides needed ' @@ -161,17 +159,17 @@ class TriageValidationAgent { required double gyroPeak, }) { var base = switch (source) { - TriageSource.gemma4Cloud => 0.92, + TriageSource.gemma4Cloud => 0.92, TriageSource.gemma4CloudVision => 0.94, - TriageSource.gemma4OnDevice => 0.74, - TriageSource.localTier2 => 0.58, + TriageSource.gemma4OnDevice => 0.74, + TriageSource.localTier2 => 0.58, TriageSource.offlineClassifier => 0.42, - TriageSource.webDemo => 0.30, + TriageSource.webDemo => 0.30, }; if (hasThinkingTrace) base += 0.03; - if (visionUsed) base += 0.04; - if (gyroPeak >= 3.5) base += 0.03; // sensor corroboration + if (visionUsed) base += 0.04; + if (gyroPeak >= 3.5) base += 0.03; // sensor corroboration if (gyroPeak >= 1.5 && gyroPeak < 3.5) base += 0.01; // Each override slightly reduces confidence — the AI needed correction. diff --git a/lib/services/user_profile_service.dart b/lib/services/user_profile_service.dart index 513241f..c845c78 100644 --- a/lib/services/user_profile_service.dart +++ b/lib/services/user_profile_service.dart @@ -102,6 +102,7 @@ class UserProfileService extends StateNotifier { } } -final userProfileProvider = StateNotifierProvider((ref) { - return UserProfileService(); -}); +final userProfileProvider = + StateNotifierProvider((ref) { + return UserProfileService(); + }); diff --git a/lib/services/vital_signs_service.dart b/lib/services/vital_signs_service.dart index 974322d..5220a49 100644 --- a/lib/services/vital_signs_service.dart +++ b/lib/services/vital_signs_service.dart @@ -70,8 +70,11 @@ class VitalSignsLogger extends StateNotifier { required int bpm, required int respiratoryRate, required double bloodOxygen, - }) => - recordManual(bpm: bpm, respiratoryRate: respiratoryRate, bloodOxygen: bloodOxygen); + }) => recordManual( + bpm: bpm, + respiratoryRate: respiratoryRate, + bloodOxygen: bloodOxygen, + ); void clear() => state = null; @@ -86,7 +89,9 @@ class VitalSignsLogger extends StateNotifier { if (respiratoryRate >= 24) flags.add('tachypnoea (rapid breathing)'); if (respiratoryRate <= 8) flags.add('bradypnoea (slow breathing)'); if (bloodOxygen < 90) flags.add('hypoxia (SpO2 <90% — CRITICAL)'); - if (bloodOxygen >= 90 && bloodOxygen < 94) flags.add('borderline oxygen (SpO2 90-93%)'); + if (bloodOxygen >= 90 && bloodOxygen < 94) { + flags.add('borderline oxygen (SpO2 90-93%)'); + } if (flags.isEmpty) { return 'No immediately critical vital signs from bystander observation.'; @@ -101,5 +106,5 @@ typedef VitalSignsService = VitalSignsLogger; final vitalSignsProvider = StateNotifierProvider.autoDispose((ref) { - return VitalSignsLogger(); -}); + return VitalSignsLogger(); + }); diff --git a/lib/services/voice_assistant_service.dart b/lib/services/voice_assistant_service.dart index 28809ef..a5cbe49 100644 --- a/lib/services/voice_assistant_service.dart +++ b/lib/services/voice_assistant_service.dart @@ -59,7 +59,10 @@ class VoiceAssistantService { /// Called when driving mode is active at SOS trigger — the user cannot /// look at the screen, so the device announces what is happening and how /// to cancel. The message is spoken in the app's current locale. - Future speakHandsFreeCountdown(int totalSeconds, String locationHint) async { + Future speakHandsFreeCountdown( + int totalSeconds, + String locationHint, + ) async { final msg = _localizedCountdownMessage(totalSeconds, locationHint); await _tts.setSpeechRate(0.52); await speak(msg); @@ -113,12 +116,18 @@ class VoiceAssistantService { String _serviceLabel(String service) { switch (service) { - case 'ambulance': return 'ambulance'; - case 'police': return 'police'; - case 'fire_department': return 'fire department'; - case 'rescue': return 'rescue team'; - case 'towing': return 'towing service'; - default: return service; + case 'ambulance': + return 'ambulance'; + case 'police': + return 'police'; + case 'fire_department': + return 'fire department'; + case 'rescue': + return 'rescue team'; + case 'towing': + return 'towing service'; + default: + return service; } } @@ -228,7 +237,9 @@ class VoiceAssistantService { case 'bn': return words.contains('হ্যাঁ') || words.contains('haan'); case 'mr': - return words.contains('हो') || words.contains('ho') || words.contains('barob'); + return words.contains('हो') || + words.contains('ho') || + words.contains('barob'); default: return false; } diff --git a/lib/ui/ai_explainability_view.dart b/lib/ui/ai_explainability_view.dart index 6649505..cc4ac77 100644 --- a/lib/ui/ai_explainability_view.dart +++ b/lib/ui/ai_explainability_view.dart @@ -37,7 +37,6 @@ class AiExplainabilityView extends ConsumerWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // ── Header ──────────────────────────────────────────────────────── Row( children: [ @@ -47,14 +46,17 @@ class AiExplainabilityView extends ConsumerWidget { child: Text( l10n.aiThinkingTraceTitle, style: Theme.of(context).textTheme.labelLarge?.copyWith( - letterSpacing: 1.2, - fontWeight: FontWeight.bold, - color: scheme.primary, - ), + letterSpacing: 1.2, + fontWeight: FontWeight.bold, + color: scheme.primary, + ), ), ), // Confidence badge - _ConfidenceBadge(confidence: triage.confidence, label: triage.confidenceLabel), + _ConfidenceBadge( + confidence: triage.confidence, + label: triage.confidenceLabel, + ), ], ), @@ -74,10 +76,10 @@ class AiExplainabilityView extends ConsumerWidget { Text( triage.thinkingTrace!, style: Theme.of(context).textTheme.bodyMedium?.copyWith( - fontFamily: 'monospace', - height: 1.5, - color: scheme.onSurfaceVariant, - ), + fontFamily: 'monospace', + height: 1.5, + color: scheme.onSurfaceVariant, + ), ), const SizedBox(height: 10), ], @@ -97,7 +99,11 @@ class AiExplainabilityView extends ConsumerWidget { children: [ Row( children: [ - const Icon(Icons.shield_outlined, size: 14, color: Colors.amber), + const Icon( + Icons.shield_outlined, + size: 14, + color: Colors.amber, + ), const SizedBox(width: 6), Text( 'Safety validation — rule-based overrides applied', @@ -231,8 +237,8 @@ class _ConfidenceBadge extends StatelessWidget { final color = confidence >= 0.80 ? Colors.green.shade700 : confidence >= 0.60 - ? Colors.orange.shade700 - : Colors.red.shade700; + ? Colors.orange.shade700 + : Colors.red.shade700; return Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), @@ -248,8 +254,8 @@ class _ConfidenceBadge extends StatelessWidget { confidence >= 0.80 ? Icons.verified_outlined : confidence >= 0.60 - ? Icons.info_outline - : Icons.warning_amber_outlined, + ? Icons.info_outline + : Icons.warning_amber_outlined, size: 11, color: color, ), diff --git a/lib/ui/consent_screen.dart b/lib/ui/consent_screen.dart index 2681219..11bf226 100644 --- a/lib/ui/consent_screen.dart +++ b/lib/ui/consent_screen.dart @@ -17,7 +17,9 @@ class _ConsentScreenState extends State { bool _extendedRetention = false; Future _accept() async { - await PrivacyConsentService.recordConsent(extendedCloudRetention: _extendedRetention); + await PrivacyConsentService.recordConsent( + extendedCloudRetention: _extendedRetention, + ); widget.onAccepted(); } @@ -33,7 +35,11 @@ class _ConsentScreenState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - const Icon(Icons.privacy_tip_outlined, size: 48, color: Colors.blueAccent), + const Icon( + Icons.privacy_tip_outlined, + size: 48, + color: Colors.blueAccent, + ), const SizedBox(height: 24), Text( l10n.consentTitle, @@ -48,20 +54,27 @@ class _ConsentScreenState extends State { child: SingleChildScrollView( child: Text( l10n.consentSummary, - style: const TextStyle(color: Colors.white70, height: 1.45, fontSize: 15), + style: const TextStyle( + color: Colors.white70, + height: 1.45, + fontSize: 15, + ), ), ), ), const SizedBox(height: 12), CheckboxListTile( value: _extendedRetention, - onChanged: (v) => setState(() => _extendedRetention = v ?? false), + onChanged: (v) => + setState(() => _extendedRetention = v ?? false), title: Text( l10n.consentExtendedRetentionLabel, style: const TextStyle(color: Colors.white70, fontSize: 13), ), tileColor: Colors.white.withValues(alpha: 0.05), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), ), const SizedBox(height: 16), OutlinedButton( diff --git a/lib/ui/crisis_companion_overlay.dart b/lib/ui/crisis_companion_overlay.dart index adefacf..f852c3e 100644 --- a/lib/ui/crisis_companion_overlay.dart +++ b/lib/ui/crisis_companion_overlay.dart @@ -12,10 +12,13 @@ class CrisisCompanionOverlay extends ConsumerWidget { String _honestStatusLine(SOSState state) { if (state.phase == SOSPhase.triaging) return 'TRIAGING…'; if (state.dispatchChannels.isEmpty) return 'DISPATCH IN PROGRESS…'; - final anyOk = state.dispatchChannels.any((c) => c.lifecycle == DispatchChannelLifecycle.success); + final anyOk = state.dispatchChannels.any( + (c) => c.lifecycle == DispatchChannelLifecycle.success, + ); if (anyOk) return 'DISPATCH CONFIRMED (CHECK CHANNELS)'; - final anyInProgress = - state.dispatchChannels.any((c) => c.lifecycle == DispatchChannelLifecycle.inProgress); + final anyInProgress = state.dispatchChannels.any( + (c) => c.lifecycle == DispatchChannelLifecycle.inProgress, + ); return anyInProgress ? 'DISPATCH IN PROGRESS…' : 'NO DISPATCH CONFIRMATION'; } @@ -26,7 +29,8 @@ class CrisisCompanionOverlay extends ConsumerWidget { final l10n = AppLocalizations.of(context)!; final lang = ref.watch(appLocaleProvider).languageCode; - if (sosState.phase != SOSPhase.active && sosState.phase != SOSPhase.triaging) { + if (sosState.phase != SOSPhase.active && + sosState.phase != SOSPhase.triaging) { return const SizedBox.shrink(); } @@ -109,10 +113,16 @@ class CrisisCompanionOverlay extends ConsumerWidget { height: 48, decoration: BoxDecoration( shape: BoxShape.circle, - gradient: const SweepGradient(colors: [Colors.blue, Colors.cyan, Colors.blue]), - boxShadow: [BoxShadow(color: Colors.blue.withValues(alpha: 0.5), blurRadius: 10)], + gradient: const SweepGradient( + colors: [Colors.blue, Colors.cyan, Colors.blue], + ), + boxShadow: [ + BoxShadow(color: Colors.blue.withValues(alpha: 0.5), blurRadius: 10), + ], + ), + child: const Center( + child: Icon(Icons.psychology, color: Colors.white, size: 28), ), - child: const Center(child: Icon(Icons.psychology, color: Colors.white, size: 28)), ); } @@ -141,7 +151,9 @@ class CrisisCompanionOverlay extends ConsumerWidget { const SizedBox(width: 8), ElevatedButton( onPressed: () { - ref.read(roadsosAssistantProvider.notifier).getNextWitnessQuestion( + ref + .read(roadsosAssistantProvider.notifier) + .getNextWitnessQuestion( 'I am feeling dizzy', languageCode: lang, ); @@ -149,10 +161,17 @@ class CrisisCompanionOverlay extends ConsumerWidget { style: ElevatedButton.styleFrom( backgroundColor: Colors.white10, foregroundColor: Colors.white, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + ), + child: Text( + l10n.talkButton, + style: const TextStyle( + fontSize: 12, + fontWeight: FontWeight.bold, + ), ), - child: Text(l10n.talkButton, - style: const TextStyle(fontSize: 12, fontWeight: FontWeight.bold)), ), ], ), diff --git a/lib/ui/dashboard.dart b/lib/ui/dashboard.dart index 9bf418e..8d2bf5e 100644 --- a/lib/ui/dashboard.dart +++ b/lib/ui/dashboard.dart @@ -442,7 +442,9 @@ class _DashboardScreenState extends ConsumerState subtitle: 'Offline extraction guide by vehicle type', onTap: () => Navigator.push( context, - MaterialPageRoute(builder: (_) => const VehicleRescueScreen()), + MaterialPageRoute( + builder: (_) => const VehicleRescueScreen(), + ), ), ), diff --git a/lib/ui/dispatch_status_panel.dart b/lib/ui/dispatch_status_panel.dart index 9e8b964..11ea2f4 100644 --- a/lib/ui/dispatch_status_panel.dart +++ b/lib/ui/dispatch_status_panel.dart @@ -41,14 +41,15 @@ class DispatchStatusPanel extends StatelessWidget { child: Text( 'DISPATCH STATUS', style: Theme.of(context).textTheme.labelLarge?.copyWith( - letterSpacing: 1.2, - color: scheme.onSurface.withValues(alpha: 0.88), - fontWeight: FontWeight.w800, - ), + letterSpacing: 1.2, + color: scheme.onSurface.withValues(alpha: 0.88), + fontWeight: FontWeight.w800, + ), ), ), if (isBeaconActive) _BeaconBanner(scheme: scheme), - for (final row in channels) _DispatchRow(row: row, scheme: scheme), + for (final row in channels) + _DispatchRow(row: row, scheme: scheme), ], ), ), @@ -81,17 +82,17 @@ class _DispatchRow extends StatelessWidget { Text( row.title, style: Theme.of(context).textTheme.titleSmall?.copyWith( - fontWeight: FontWeight.w700, - color: scheme.onSurface, - ), + fontWeight: FontWeight.w700, + color: scheme.onSurface, + ), ), const SizedBox(height: 4), Text( row.detail, style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: scheme.onSurface.withValues(alpha: 0.87), - height: 1.35, - ), + color: scheme.onSurface.withValues(alpha: 0.87), + height: 1.35, + ), ), ], ), @@ -101,7 +102,10 @@ class _DispatchRow extends StatelessWidget { ); } - (IconData, Color) _iconFor(DispatchChannelLifecycle life, ColorScheme scheme) { + (IconData, Color) _iconFor( + DispatchChannelLifecycle life, + ColorScheme scheme, + ) { switch (life) { case DispatchChannelLifecycle.pending: return (Icons.radio_button_unchecked, scheme.outline); @@ -125,13 +129,17 @@ class _BeaconBanner extends StatefulWidget { State<_BeaconBanner> createState() => _BeaconBannerState(); } -class _BeaconBannerState extends State<_BeaconBanner> with SingleTickerProviderStateMixin { +class _BeaconBannerState extends State<_BeaconBanner> + with SingleTickerProviderStateMixin { late AnimationController _ctrl; @override void initState() { super.initState(); - _ctrl = AnimationController(vsync: this, duration: const Duration(milliseconds: 800))..repeat(reverse: true); + _ctrl = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 800), + )..repeat(reverse: true); } @override @@ -160,10 +168,10 @@ class _BeaconBannerState extends State<_BeaconBanner> with SingleTickerProviderS child: Text( 'RESCUE BEACON ACTIVE: FLASH + SIREN', style: Theme.of(context).textTheme.labelSmall?.copyWith( - color: widget.scheme.error, - fontWeight: FontWeight.w900, - letterSpacing: 0.5, - ), + color: widget.scheme.error, + fontWeight: FontWeight.w900, + letterSpacing: 0.5, + ), ), ), ], diff --git a/lib/ui/first_aid_screen.dart b/lib/ui/first_aid_screen.dart index 0b1bcb4..0cca28c 100644 --- a/lib/ui/first_aid_screen.dart +++ b/lib/ui/first_aid_screen.dart @@ -50,7 +50,7 @@ class _FirstAidScreenState extends ConsumerState { Future _lookupFirstAid(String query) async { if (query.trim().isEmpty) return; - + setState(() { _isLoading = true; _result = ''; @@ -76,8 +76,6 @@ class _FirstAidScreenState extends ConsumerState { } } - - @override Widget build(BuildContext context) { return Scaffold( @@ -106,7 +104,10 @@ class _FirstAidScreenState extends ConsumerState { hintStyle: const TextStyle(color: Colors.white38), filled: true, fillColor: const Color(0xFF1A1A2E), - contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 12, + ), border: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: BorderSide.none, @@ -142,18 +143,26 @@ class _FirstAidScreenState extends ConsumerState { borderRadius: BorderRadius.circular(12), border: Border.all(color: Colors.white10), boxShadow: [ - BoxShadow(color: Colors.black26, blurRadius: 10, spreadRadius: 2), + BoxShadow( + color: Colors.black26, + blurRadius: 10, + spreadRadius: 2, + ), ], ), child: ListView.separated( shrinkWrap: true, itemCount: _suggestions.length, - separatorBuilder: (context, index) => const Divider(color: Colors.white10, height: 1), + separatorBuilder: (context, index) => + const Divider(color: Colors.white10, height: 1), itemBuilder: (context, index) { final suggestion = _suggestions[index]; return ListTile( dense: true, - title: Text(suggestion, style: const TextStyle(color: Colors.white70)), + title: Text( + suggestion, + style: const TextStyle(color: Colors.white70), + ), onTap: () { _textController.text = suggestion; _lookupFirstAid(suggestion); @@ -182,16 +191,25 @@ class _FirstAidScreenState extends ConsumerState { decoration: BoxDecoration( color: Colors.red.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(8), - border: Border.all(color: Colors.red.withValues(alpha: 0.3)), + border: Border.all( + color: Colors.red.withValues(alpha: 0.3), + ), ), child: Row( children: [ - const Icon(Icons.error_outline, color: Colors.redAccent, size: 20), + const Icon( + Icons.error_outline, + color: Colors.redAccent, + size: 20, + ), const SizedBox(width: 8), Expanded( child: Text( _error!, - style: const TextStyle(color: Colors.redAccent, fontSize: 13), + style: const TextStyle( + color: Colors.redAccent, + fontSize: 13, + ), ), ), ], @@ -208,7 +226,9 @@ class _FirstAidScreenState extends ConsumerState { decoration: BoxDecoration( color: const Color(0xFF1A1A2E), borderRadius: BorderRadius.circular(12), - border: Border.all(color: Colors.red.withValues(alpha: 0.25)), + border: Border.all( + color: Colors.red.withValues(alpha: 0.25), + ), ), child: SingleChildScrollView( child: Column( @@ -216,7 +236,11 @@ class _FirstAidScreenState extends ConsumerState { children: [ Row( children: [ - const Icon(Icons.medical_services_outlined, color: Colors.redAccent, size: 18), + const Icon( + Icons.medical_services_outlined, + color: Colors.redAccent, + size: 18, + ), const SizedBox(width: 8), Text( 'Verified Medical Solutions', @@ -234,11 +258,28 @@ class _FirstAidScreenState extends ConsumerState { data: _result, selectable: true, styleSheet: MarkdownStyleSheet( - p: const TextStyle(color: Colors.white, height: 1.6, fontSize: 15), - strong: const TextStyle(color: Colors.redAccent, fontWeight: FontWeight.bold), - listBullet: const TextStyle(color: Colors.redAccent), - h1: const TextStyle(color: Colors.white, fontSize: 22, fontWeight: FontWeight.bold), - h2: const TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.bold), + p: const TextStyle( + color: Colors.white, + height: 1.6, + fontSize: 15, + ), + strong: const TextStyle( + color: Colors.redAccent, + fontWeight: FontWeight.bold, + ), + listBullet: const TextStyle( + color: Colors.redAccent, + ), + h1: const TextStyle( + color: Colors.white, + fontSize: 22, + fontWeight: FontWeight.bold, + ), + h2: const TextStyle( + color: Colors.white, + fontSize: 18, + fontWeight: FontWeight.bold, + ), ), ), ], @@ -259,8 +300,11 @@ class _FirstAidScreenState extends ConsumerState { color: Colors.red.withValues(alpha: 0.05), shape: BoxShape.circle, ), - child: const Icon(Icons.health_and_safety, - size: 80, color: Colors.red), + child: const Icon( + Icons.health_and_safety, + size: 80, + color: Colors.red, + ), ), const SizedBox(height: 24), const Text( @@ -310,4 +354,4 @@ class _FirstAidScreenState extends ConsumerState { shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), ); } -} \ No newline at end of file +} diff --git a/lib/ui/gemma_model_download_screen.dart b/lib/ui/gemma_model_download_screen.dart index be903b8..25d1370 100644 --- a/lib/ui/gemma_model_download_screen.dart +++ b/lib/ui/gemma_model_download_screen.dart @@ -22,7 +22,8 @@ class GemmaModelDownloadScreen extends StatefulWidget { final VoidCallback onComplete; @override - State createState() => _GemmaModelDownloadScreenState(); + State createState() => + _GemmaModelDownloadScreenState(); } class _GemmaModelDownloadScreenState extends State { @@ -70,7 +71,9 @@ class _GemmaModelDownloadScreenState extends State { Future _startDownload() async { final token = _tokenController.text.trim(); if (token.isEmpty) { - setState(() => _errorMessage = 'Paste your HuggingFace read token above first.'); + setState( + () => _errorMessage = 'Paste your HuggingFace read token above first.', + ); return; } setState(() { @@ -122,7 +125,9 @@ class _GemmaModelDownloadScreenState extends State { Future _openUrl(String url) async { final uri = Uri.parse(url); - if (await canLaunchUrl(uri)) await launchUrl(uri, mode: LaunchMode.externalApplication); + if (await canLaunchUrl(uri)) { + await launchUrl(uri, mode: LaunchMode.externalApplication); + } } // ── Build ───────────────────────────────────────────────────────────────── @@ -153,13 +158,20 @@ class _GemmaModelDownloadScreenState extends State { child: Row( children: [ Container( - width: 40, height: 40, + width: 40, + height: 40, decoration: BoxDecoration( color: const Color(0xFF4a90d9).withValues(alpha: 0.15), borderRadius: BorderRadius.circular(10), - border: Border.all(color: const Color(0xFF4a90d9).withValues(alpha: 0.4)), + border: Border.all( + color: const Color(0xFF4a90d9).withValues(alpha: 0.4), + ), + ), + child: const Icon( + Icons.memory_rounded, + color: Color(0xFF7bc8f8), + size: 20, ), - child: const Icon(Icons.memory_rounded, color: Color(0xFF7bc8f8), size: 20), ), const SizedBox(width: 12), const Expanded( @@ -168,7 +180,11 @@ class _GemmaModelDownloadScreenState extends State { children: [ Text( 'Gemma 4 On-Device AI', - style: TextStyle(color: Colors.white, fontSize: 16, fontWeight: FontWeight.w800), + style: TextStyle( + color: Colors.white, + fontSize: 16, + fontWeight: FontWeight.w800, + ), ), Text( 'Offline emergency triage — works without internet', @@ -202,9 +218,7 @@ class _GemmaModelDownloadScreenState extends State { _buildResumeNote(), ], ], - if (_phase == _Phase.downloading) ...[ - _buildProgressPanel(), - ], + if (_phase == _Phase.downloading) ...[_buildProgressPanel()], if (_phase == _Phase.check) ...[ const Center( child: Padding( @@ -224,7 +238,11 @@ class _GemmaModelDownloadScreenState extends State { children: [ const Text( 'Why download this model?', - style: TextStyle(color: Colors.white, fontSize: 17, fontWeight: FontWeight.w800), + style: TextStyle( + color: Colors.white, + fontSize: 17, + fontWeight: FontWeight.w800, + ), ), const SizedBox(height: 12), _infoRow( @@ -269,14 +287,31 @@ class _GemmaModelDownloadScreenState extends State { const SizedBox(height: 20), const Text( 'How to get your HuggingFace token', - style: TextStyle(color: Colors.white, fontSize: 15, fontWeight: FontWeight.w700), + style: TextStyle( + color: Colors.white, + fontSize: 15, + fontWeight: FontWeight.w700, + ), ), const SizedBox(height: 10), - _stepRow('1', 'Accept Gemma 4 terms', - 'Required by Google before downloading', GemmaModelManager.hfTermsUrl), - _stepRow('2', 'Create a read token', - 'Free account required', GemmaModelManager.hfTokenUrl), - _stepRow('3', 'Paste it below', 'Token is used for this download only', null), + _stepRow( + '1', + 'Accept Gemma 4 terms', + 'Required by Google before downloading', + GemmaModelManager.hfTermsUrl, + ), + _stepRow( + '2', + 'Create a read token', + 'Free account required', + GemmaModelManager.hfTokenUrl, + ), + _stepRow( + '3', + 'Paste it below', + 'Token is used for this download only', + null, + ), ], ); } @@ -287,7 +322,11 @@ class _GemmaModelDownloadScreenState extends State { children: [ const Text( 'HuggingFace token', - style: TextStyle(color: Color(0xFF6b7a99), fontSize: 12, fontWeight: FontWeight.w600), + style: TextStyle( + color: Color(0xFF6b7a99), + fontSize: 12, + fontWeight: FontWeight.w600, + ), ), const SizedBox(height: 6), Row( @@ -297,7 +336,11 @@ class _GemmaModelDownloadScreenState extends State { controller: _tokenController, focusNode: _tokenFocus, obscureText: true, - style: const TextStyle(color: Colors.white, fontSize: 14, fontFamily: 'monospace'), + style: const TextStyle( + color: Colors.white, + fontSize: 14, + fontFamily: 'monospace', + ), decoration: InputDecoration( hintText: 'hf_...', hintStyle: const TextStyle(color: Color(0xFF6b7a99)), @@ -315,7 +358,10 @@ class _GemmaModelDownloadScreenState extends State { borderRadius: BorderRadius.circular(10), borderSide: const BorderSide(color: Color(0xFF4a90d9)), ), - contentPadding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), + contentPadding: const EdgeInsets.symmetric( + horizontal: 14, + vertical: 12, + ), ), ), ), @@ -341,7 +387,11 @@ class _GemmaModelDownloadScreenState extends State { children: [ const Text( 'Downloading Gemma 4 E4B...', - style: TextStyle(color: Colors.white, fontSize: 17, fontWeight: FontWeight.w800), + style: TextStyle( + color: Colors.white, + fontSize: 17, + fontWeight: FontWeight.w800, + ), ), const SizedBox(height: 6), Text( @@ -368,7 +418,11 @@ class _GemmaModelDownloadScreenState extends State { ), Text( '${(percent * 100).toStringAsFixed(1)}%', - style: const TextStyle(color: Color(0xFF4a90d9), fontSize: 13, fontWeight: FontWeight.w600), + style: const TextStyle( + color: Color(0xFF4a90d9), + fontSize: 13, + fontWeight: FontWeight.w600, + ), ), ], ), @@ -380,7 +434,9 @@ class _GemmaModelDownloadScreenState extends State { style: OutlinedButton.styleFrom( foregroundColor: const Color(0xFF6b7a99), side: const BorderSide(color: Color(0xFF1e2a40)), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), padding: const EdgeInsets.symmetric(vertical: 14), ), child: const Text('Pause Download'), @@ -395,18 +451,30 @@ class _GemmaModelDownloadScreenState extends State { children: [ const SizedBox(height: 24), Container( - width: 72, height: 72, + width: 72, + height: 72, decoration: BoxDecoration( color: const Color(0xFF27c96b).withValues(alpha: 0.12), shape: BoxShape.circle, - border: Border.all(color: const Color(0xFF27c96b).withValues(alpha: 0.4), width: 2), + border: Border.all( + color: const Color(0xFF27c96b).withValues(alpha: 0.4), + width: 2, + ), + ), + child: const Icon( + Icons.check_rounded, + color: Color(0xFF27c96b), + size: 36, ), - child: const Icon(Icons.check_rounded, color: Color(0xFF27c96b), size: 36), ), const SizedBox(height: 20), const Text( 'Gemma 4 E4B Ready', - style: TextStyle(color: Colors.white, fontSize: 20, fontWeight: FontWeight.w900), + style: TextStyle( + color: Colors.white, + fontSize: 20, + fontWeight: FontWeight.w900, + ), textAlign: TextAlign.center, ), const SizedBox(height: 10), @@ -424,9 +492,14 @@ class _GemmaModelDownloadScreenState extends State { backgroundColor: const Color(0xFF27c96b), foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(vertical: 16), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + child: const Text( + 'Continue to RoadSOS', + style: TextStyle(fontWeight: FontWeight.w800, fontSize: 15), ), - child: const Text('Continue to RoadSOS', style: TextStyle(fontWeight: FontWeight.w800, fontSize: 15)), ), ), ], @@ -439,17 +512,27 @@ class _GemmaModelDownloadScreenState extends State { decoration: BoxDecoration( color: const Color(0xFFe8354a).withValues(alpha: 0.08), borderRadius: BorderRadius.circular(10), - border: Border.all(color: const Color(0xFFe8354a).withValues(alpha: 0.3)), + border: Border.all( + color: const Color(0xFFe8354a).withValues(alpha: 0.3), + ), ), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Icon(Icons.error_outline_rounded, color: Color(0xFFe8354a), size: 18), + const Icon( + Icons.error_outline_rounded, + color: Color(0xFFe8354a), + size: 18, + ), const SizedBox(width: 10), Expanded( child: Text( _errorMessage!, - style: const TextStyle(color: Color(0xFFe8354a), fontSize: 12.5, height: 1.45), + style: const TextStyle( + color: Color(0xFFe8354a), + fontSize: 12.5, + height: 1.45, + ), ), ), ], @@ -464,7 +547,9 @@ class _GemmaModelDownloadScreenState extends State { decoration: BoxDecoration( color: const Color(0xFF4a90d9).withValues(alpha: 0.08), borderRadius: BorderRadius.circular(10), - border: Border.all(color: const Color(0xFF4a90d9).withValues(alpha: 0.3)), + border: Border.all( + color: const Color(0xFF4a90d9).withValues(alpha: 0.3), + ), ), child: Row( children: [ @@ -523,9 +608,23 @@ class _GemmaModelDownloadScreenState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(title, style: const TextStyle(color: Colors.white, fontSize: 13, fontWeight: FontWeight.w700)), + Text( + title, + style: const TextStyle( + color: Colors.white, + fontSize: 13, + fontWeight: FontWeight.w700, + ), + ), const SizedBox(height: 2), - Text(body, style: const TextStyle(color: Color(0xFF6b7a99), fontSize: 12.5, height: 1.4)), + Text( + body, + style: const TextStyle( + color: Color(0xFF6b7a99), + fontSize: 12.5, + height: 1.4, + ), + ), ], ), ), @@ -541,15 +640,24 @@ class _GemmaModelDownloadScreenState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( - width: 22, height: 22, + width: 22, + height: 22, decoration: BoxDecoration( color: const Color(0xFF4a90d9).withValues(alpha: 0.15), shape: BoxShape.circle, - border: Border.all(color: const Color(0xFF4a90d9).withValues(alpha: 0.4)), + border: Border.all( + color: const Color(0xFF4a90d9).withValues(alpha: 0.4), + ), ), child: Center( - child: Text(num, - style: const TextStyle(color: Color(0xFF7bc8f8), fontSize: 11, fontWeight: FontWeight.w800)), + child: Text( + num, + style: const TextStyle( + color: Color(0xFF7bc8f8), + fontSize: 11, + fontWeight: FontWeight.w800, + ), + ), ), ), const SizedBox(width: 10), @@ -561,22 +669,38 @@ class _GemmaModelDownloadScreenState extends State { onTap: url != null ? () => _openUrl(url) : null, child: Row( children: [ - Text(title, - style: TextStyle( - color: url != null ? const Color(0xFF7bc8f8) : Colors.white, - fontSize: 13, - fontWeight: FontWeight.w600, - decoration: url != null ? TextDecoration.underline : null, - decorationColor: const Color(0xFF7bc8f8), - )), + Text( + title, + style: TextStyle( + color: url != null + ? const Color(0xFF7bc8f8) + : Colors.white, + fontSize: 13, + fontWeight: FontWeight.w600, + decoration: url != null + ? TextDecoration.underline + : null, + decorationColor: const Color(0xFF7bc8f8), + ), + ), if (url != null) ...[ const SizedBox(width: 4), - const Icon(Icons.open_in_new_rounded, size: 12, color: Color(0xFF7bc8f8)), + const Icon( + Icons.open_in_new_rounded, + size: 12, + color: Color(0xFF7bc8f8), + ), ], ], ), ), - Text(sub, style: const TextStyle(color: Color(0xFF6b7a99), fontSize: 11.5)), + Text( + sub, + style: const TextStyle( + color: Color(0xFF6b7a99), + fontSize: 11.5, + ), + ), ], ), ), @@ -597,7 +721,9 @@ class _GemmaModelDownloadScreenState extends State { style: ElevatedButton.styleFrom( backgroundColor: color, foregroundColor: Colors.white, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), padding: const EdgeInsets.symmetric(horizontal: 18), ), child: Text(label, style: const TextStyle(fontWeight: FontWeight.w700)), diff --git a/lib/ui/good_samaritan_law_screen.dart b/lib/ui/good_samaritan_law_screen.dart index 8d11dbe..7da4a34 100644 --- a/lib/ui/good_samaritan_law_screen.dart +++ b/lib/ui/good_samaritan_law_screen.dart @@ -35,7 +35,11 @@ class GoodSamaritanLawScreen extends StatelessWidget { child: SingleChildScrollView( child: Text( l10n.goodSamaritanBody, - style: const TextStyle(color: Colors.white70, height: 1.5, fontSize: 16), + style: const TextStyle( + color: Colors.white70, + height: 1.5, + fontSize: 16, + ), ), ), ), diff --git a/lib/ui/incident_reporting_screen.dart b/lib/ui/incident_reporting_screen.dart index 402782f..d91843e 100644 --- a/lib/ui/incident_reporting_screen.dart +++ b/lib/ui/incident_reporting_screen.dart @@ -7,6 +7,7 @@ import 'package:roadsos/l10n/app_localizations.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../services/app_locale_controller.dart'; import '../services/roadsos_assistant_service.dart'; + class IncidentReportingScreen extends ConsumerStatefulWidget { const IncidentReportingScreen({super.key}); @@ -30,8 +31,10 @@ class _IncidentReportingScreenState return Scaffold( backgroundColor: Colors.black, appBar: AppBar( - title: Text(l10n.sceneIntelligenceTitle, - style: const TextStyle(fontWeight: FontWeight.w900, fontSize: 16)), + title: Text( + l10n.sceneIntelligenceTitle, + style: const TextStyle(fontWeight: FontWeight.w900, fontSize: 16), + ), backgroundColor: Colors.transparent, elevation: 0, ), @@ -49,7 +52,8 @@ class _IncidentReportingScreenState _buildVoiceInterviewCard(assistantState, l10n), const SizedBox(height: 32), // Show guidance steps after interview is complete - if (assistantState.showingGuidance && assistantState.guidanceSteps.isNotEmpty) ...[ + if (assistantState.showingGuidance && + assistantState.guidanceSteps.isNotEmpty) ...[ _buildSectionHeader('ACTION GUIDANCE: NEXT STEPS'), const SizedBox(height: 12), _buildGuidanceCard(assistantState, l10n), @@ -92,7 +96,11 @@ class _IncidentReportingScreenState mainAxisSize: MainAxisSize.min, children: [ if (_sceneImageBytes == null) - Icon(Icons.camera_enhance, size: 40, color: Colors.white.withValues(alpha: 0.3)) + Icon( + Icons.camera_enhance, + size: 40, + color: Colors.white.withValues(alpha: 0.3), + ) else ClipRRect( borderRadius: BorderRadius.circular(12), @@ -112,10 +120,20 @@ class _IncidentReportingScreenState height: 18, child: CircularProgressIndicator(strokeWidth: 2), ) - : Icon(_sceneImageBytes != null ? Icons.check : Icons.add_a_photo), - label: Text(_sceneImageBytes != null ? 'SCENE ATTACHED' : 'CAPTURE / ATTACH PHOTO'), + : Icon( + _sceneImageBytes != null + ? Icons.check + : Icons.add_a_photo, + ), + label: Text( + _sceneImageBytes != null + ? 'SCENE ATTACHED' + : 'CAPTURE / ATTACH PHOTO', + ), style: ElevatedButton.styleFrom( - backgroundColor: _sceneImageBytes != null ? Colors.green : Colors.blue, + backgroundColor: _sceneImageBytes != null + ? Colors.green + : Colors.blue, ), ), if (_sceneImageBytes != null) @@ -124,7 +142,10 @@ class _IncidentReportingScreenState child: Text( 'Photo attached to this report (not auto-analyzed in this build).', style: const TextStyle( - color: Colors.green, fontSize: 10, fontWeight: FontWeight.bold), + color: Colors.green, + fontSize: 10, + fontWeight: FontWeight.bold, + ), textAlign: TextAlign.center, ), ), @@ -177,21 +198,29 @@ class _IncidentReportingScreenState if (!mounted) return; setState(() => _sceneCaptureBusy = false); ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Could not capture a scene photo on this device.')), + const SnackBar( + content: Text('Could not capture a scene photo on this device.'), + ), ); } } if (!kIsWeb && mounted) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Scene photo attached.')), - ); + ScaffoldMessenger.of( + context, + ).showSnackBar(const SnackBar(content: Text('Scene photo attached.'))); } } - Widget _buildVoiceInterviewCard(AssistantState assistant, AppLocalizations l10n) { + Widget _buildVoiceInterviewCard( + AssistantState assistant, + AppLocalizations l10n, + ) { final lang = ref.read(appLocaleProvider).languageCode; - final totalQuestions = _getTotalQuestionsForScene(assistant.sceneContext, lang); + final totalQuestions = _getTotalQuestionsForScene( + assistant.sceneContext, + lang, + ); final progress = '${assistant.questionIndex} / $totalQuestions'; return Container( @@ -208,15 +237,24 @@ class _IncidentReportingScreenState Padding( padding: const EdgeInsets.only(bottom: 12), child: Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 6, + ), decoration: BoxDecoration( color: Colors.green.withValues(alpha: 0.2), borderRadius: BorderRadius.circular(8), - border: Border.all(color: Colors.green.withValues(alpha: 0.4)), + border: Border.all( + color: Colors.green.withValues(alpha: 0.4), + ), ), child: Text( 'Scene: ${_getSceneLabel(assistant.sceneContext, lang)}', - style: const TextStyle(fontSize: 10, color: Colors.green, fontWeight: FontWeight.bold), + style: const TextStyle( + fontSize: 10, + color: Colors.green, + fontWeight: FontWeight.bold, + ), ), ), ), @@ -229,17 +267,28 @@ class _IncidentReportingScreenState children: [ Text( lang == 'hi' ? 'प्रश्न प्रगति:' : 'Question Progress:', - style: const TextStyle(fontSize: 10, color: Colors.blue, fontWeight: FontWeight.bold), + style: const TextStyle( + fontSize: 10, + color: Colors.blue, + fontWeight: FontWeight.bold, + ), ), Container( - padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), + padding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 4, + ), decoration: BoxDecoration( color: Colors.blue.withValues(alpha: 0.2), borderRadius: BorderRadius.circular(6), ), child: Text( progress, - style: const TextStyle(fontSize: 9, color: Colors.blue, fontWeight: FontWeight.w900), + style: const TextStyle( + fontSize: 9, + color: Colors.blue, + fontWeight: FontWeight.w900, + ), ), ), ], @@ -248,11 +297,15 @@ class _IncidentReportingScreenState // Main question text Text( assistant.lastResponse.isEmpty - ? (lang == 'hi' - ? 'कृपया घटना का वर्णन करें' - : 'Please describe the incident') + ? (lang == 'hi' + ? 'कृपया घटना का वर्णन करें' + : 'Please describe the incident') : assistant.lastResponse, - style: const TextStyle(fontSize: 15, color: Colors.white, height: 1.5), + style: const TextStyle( + fontSize: 15, + color: Colors.white, + height: 1.5, + ), textAlign: TextAlign.center, ), const SizedBox(height: 20), @@ -264,11 +317,17 @@ class _IncidentReportingScreenState controller: _voiceInputController, style: const TextStyle(color: Colors.white), decoration: InputDecoration( - hintText: lang == 'hi' ? 'बोलें या टाइप करें…' : 'Speak or type…', - hintStyle: TextStyle(color: Colors.white.withValues(alpha: 0.3)), + hintText: lang == 'hi' + ? 'बोलें या टाइप करें…' + : 'Speak or type…', + hintStyle: TextStyle( + color: Colors.white.withValues(alpha: 0.3), + ), filled: true, fillColor: Colors.black, - border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + ), ), enabled: !assistant.interviewComplete, ), @@ -279,7 +338,9 @@ class _IncidentReportingScreenState ? null : () { if (_voiceInputController.text.isNotEmpty) { - ref.read(roadsosAssistantProvider.notifier).getNextWitnessQuestion( + ref + .read(roadsosAssistantProvider.notifier) + .getNextWitnessQuestion( _voiceInputController.text, languageCode: lang, ); @@ -292,7 +353,11 @@ class _IncidentReportingScreenState height: 20, child: CircularProgressIndicator(strokeWidth: 2), ) - : Icon(assistant.interviewComplete ? Icons.done_all : Icons.send), + : Icon( + assistant.interviewComplete + ? Icons.done_all + : Icons.send, + ), ), ], ), @@ -305,13 +370,19 @@ class _IncidentReportingScreenState decoration: BoxDecoration( color: Colors.green.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(8), - border: Border.all(color: Colors.green.withValues(alpha: 0.3)), + border: Border.all( + color: Colors.green.withValues(alpha: 0.3), + ), ), child: Text( lang == 'hi' ? '✓ साक्षात्कार पूर्ण। सभी महत्वपूर्ण जानकारी एकत्र की गई।' : '✓ Interview complete. All critical information collected.', - style: const TextStyle(fontSize: 11, color: Colors.green, fontWeight: FontWeight.bold), + style: const TextStyle( + fontSize: 11, + color: Colors.green, + fontWeight: FontWeight.bold, + ), textAlign: TextAlign.center, ), ), @@ -399,7 +470,10 @@ class _IncidentReportingScreenState ), ), Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 6, + ), decoration: BoxDecoration( color: Colors.green.withValues(alpha: 0.2), borderRadius: BorderRadius.circular(6), @@ -444,7 +518,9 @@ class _IncidentReportingScreenState children: [ InkWell( onTap: () { - ref.read(roadsosAssistantProvider.notifier).completeGuidanceStep(step.stepNumber); + ref + .read(roadsosAssistantProvider.notifier) + .completeGuidanceStep(step.stepNumber); }, child: Container( padding: const EdgeInsets.all(12), @@ -473,7 +549,11 @@ class _IncidentReportingScreenState ), child: Center( child: step.completed - ? const Icon(Icons.check, color: Colors.green, size: 20) + ? const Icon( + Icons.check, + color: Colors.green, + size: 20, + ) : Text( step.stepNumber.toString(), style: const TextStyle( @@ -495,8 +575,12 @@ class _IncidentReportingScreenState style: TextStyle( fontSize: 13, fontWeight: FontWeight.w900, - color: step.completed ? Colors.green : Colors.white, - decoration: step.completed ? TextDecoration.lineThrough : null, + color: step.completed + ? Colors.green + : Colors.white, + decoration: step.completed + ? TextDecoration.lineThrough + : null, ), ), const SizedBox(height: 4), @@ -504,7 +588,9 @@ class _IncidentReportingScreenState step.description, style: TextStyle( fontSize: 11, - color: Colors.white.withValues(alpha: step.completed ? 0.5 : 0.7), + color: Colors.white.withValues( + alpha: step.completed ? 0.5 : 0.7, + ), height: 1.3, ), ), diff --git a/lib/ui/map_widget.dart b/lib/ui/map_widget.dart index 9064c3b..5dddabe 100644 --- a/lib/ui/map_widget.dart +++ b/lib/ui/map_widget.dart @@ -18,11 +18,7 @@ class RoadSosMap extends StatefulWidget { final SOSState state; final bool autoCenter; - const RoadSosMap({ - super.key, - required this.state, - this.autoCenter = true, - }); + const RoadSosMap({super.key, required this.state, this.autoCenter = true}); @override State createState() => _RoadSosMapState(); @@ -50,7 +46,9 @@ class _RoadSosMapState extends State with TickerProviderStateMixin { // - If custom provider fails (keys/restrictions), fall back to Carto. // - If Carto fails (blocked network / DNS / captive portal), fall back to // OSM tile CDN for demo resilience (do not ship high-volume traffic there). - if (!t.contains('basemaps.cartocdn.com')) return MapTileConfig.cartoDarkMatter; + if (!t.contains('basemaps.cartocdn.com')) { + return MapTileConfig.cartoDarkMatter; + } return MapTileConfig.tileOpenstreetmapOrgViolatesPolicyAtScale; } @@ -83,12 +81,16 @@ class _RoadSosMapState extends State with TickerProviderStateMixin { @override Widget build(BuildContext context) { - final hasFix = widget.state.location != null && + final hasFix = + widget.state.location != null && widget.state.location!.source != 'unknown' && !(widget.state.location!.latitude == 0.0 && widget.state.location!.longitude == 0.0); final userLoc = hasFix - ? LatLng(widget.state.location!.latitude, widget.state.location!.longitude) + ? LatLng( + widget.state.location!.latitude, + widget.state.location!.longitude, + ) : null; return Container( @@ -115,7 +117,9 @@ class _RoadSosMapState extends State with TickerProviderStateMixin { ), children: [ TileLayer( - key: ValueKey('tiles_${_tileLayerNonce}_${MapTileConfig.effectiveUrlTemplate}'), + key: ValueKey( + 'tiles_${_tileLayerNonce}_${MapTileConfig.effectiveUrlTemplate}', + ), urlTemplate: MapTileConfig.effectiveUrlTemplate, subdomains: MapTileConfig.effectiveSubdomains, userAgentPackageName: 'com.roadsos.app', @@ -144,10 +148,10 @@ class _RoadSosMapState extends State with TickerProviderStateMixin { height: 40, child: _buildUserMarker(), ), - + // Incident Markers ..._buildIncidentMarkers(), - + // Facility Markers (seeded + cloud-synced via PowerSync when configured) ..._buildFacilityMarkers(), ], @@ -164,9 +168,7 @@ class _RoadSosMapState extends State with TickerProviderStateMixin { style: TextStyle( fontSize: 9, color: Colors.white.withValues(alpha: 0.45), - shadows: const [ - Shadow(color: Colors.black54, blurRadius: 4), - ], + shadows: const [Shadow(color: Colors.black54, blurRadius: 4)], ), maxLines: 2, overflow: TextOverflow.ellipsis, @@ -184,10 +186,7 @@ class _RoadSosMapState extends State with TickerProviderStateMixin { icon: Icons.my_location, onTap: () { if (widget.state.location != null) { - _mapController.animateTo( - dest: userLoc, - zoom: 15, - ); + _mapController.animateTo(dest: userLoc, zoom: 15); } }, ), @@ -222,7 +221,9 @@ class _RoadSosMapState extends State with TickerProviderStateMixin { children: [ Icon( Icons.warning_amber_rounded, - color: _templateLooksValid ? Colors.amber : Colors.redAccent, + color: _templateLooksValid + ? Colors.amber + : Colors.redAccent, size: 18, ), const SizedBox(width: 10), @@ -231,8 +232,12 @@ class _RoadSosMapState extends State with TickerProviderStateMixin { !_templateLooksValid ? 'Map tiles misconfigured (missing {z}/{x}/{y}).' : 'Map tiles failed to load. Check internet / tile provider.\n' - 'Template: ${MapTileConfig.effectiveUrlTemplate}', - style: const TextStyle(color: Colors.white, fontSize: 12, height: 1.25), + 'Template: ${MapTileConfig.effectiveUrlTemplate}', + style: const TextStyle( + color: Colors.white, + fontSize: 12, + height: 1.25, + ), ), ), const SizedBox(width: 10), @@ -258,10 +263,15 @@ class _RoadSosMapState extends State with TickerProviderStateMixin { bottom: 46, child: Builder( builder: (context) { - final seconds = DateTime.now().difference(_mountedAt).inSeconds; + final seconds = DateTime.now() + .difference(_mountedAt) + .inSeconds; if (seconds < 6) return const SizedBox.shrink(); return Container( - padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8), + padding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 8, + ), decoration: BoxDecoration( color: Colors.black.withValues(alpha: 0.65), borderRadius: BorderRadius.circular(10), @@ -294,12 +304,17 @@ class _RoadSosMapState extends State with TickerProviderStateMixin { } List _buildIncidentMarkers() { - if (widget.state.incidentId == null || widget.state.location == null) return []; + if (widget.state.incidentId == null || widget.state.location == null) { + return []; + } if (widget.state.location!.source == 'unknown') return []; - + return [ Marker( - point: LatLng(widget.state.location!.latitude, widget.state.location!.longitude), + point: LatLng( + widget.state.location!.latitude, + widget.state.location!.longitude, + ), width: 50, height: 50, child: const Icon(Icons.emergency, color: Colors.red, size: 40), diff --git a/lib/ui/medical_card_screen.dart b/lib/ui/medical_card_screen.dart index 009f232..bf47ab1 100644 --- a/lib/ui/medical_card_screen.dart +++ b/lib/ui/medical_card_screen.dart @@ -10,7 +10,7 @@ class MedicalCardScreen extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final profile = ref.watch(userProfileProvider); - + // QR contains a short, offline-safe summary (no network dependency). final qrData = 'ROADSOS_MEDICAL_V1|NAME:${profile.fullName}|BLOOD:${profile.bloodType}|ALLERGIES:${profile.allergies}|MEDS:${profile.medications}|CONTACT:${profile.emergencyContact}'; @@ -18,11 +18,17 @@ class MedicalCardScreen extends ConsumerWidget { return Scaffold( backgroundColor: Colors.black, appBar: AppBar( - title: const Text('EMERGENCY MEDICAL ID', style: TextStyle(fontWeight: FontWeight.w900, fontSize: 16)), + title: const Text( + 'EMERGENCY MEDICAL ID', + style: TextStyle(fontWeight: FontWeight.w900, fontSize: 16), + ), backgroundColor: Colors.transparent, actions: [ IconButton( - onPressed: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const ProfileEditorScreen())), + onPressed: () => Navigator.push( + context, + MaterialPageRoute(builder: (_) => const ProfileEditorScreen()), + ), icon: const Icon(Icons.edit, size: 20), ), ], @@ -50,18 +56,33 @@ class MedicalCardScreen extends ConsumerWidget { const SizedBox(height: 12), const Text( 'SCAN FOR MEDICAL SUMMARY', - style: TextStyle(fontSize: 10, fontWeight: FontWeight.bold, letterSpacing: 1.2, color: Colors.white54), + style: TextStyle( + fontSize: 10, + fontWeight: FontWeight.bold, + letterSpacing: 1.2, + color: Colors.white54, + ), ), - + const SizedBox(height: 48), - _buildInfoRow(Icons.person, 'FULL NAME', profile.fullName.isEmpty ? 'NOT SET' : profile.fullName), + _buildInfoRow( + Icons.person, + 'FULL NAME', + profile.fullName.isEmpty ? 'NOT SET' : profile.fullName, + ), _buildDivider(), _buildInfoRow(Icons.bloodtype, 'BLOOD TYPE', profile.bloodType), _buildDivider(), _buildInfoRow(Icons.warning, 'ALLERGIES', profile.allergies), _buildDivider(), - _buildInfoRow(Icons.contact_phone, 'EMERGENCY CONTACT', profile.emergencyContact.isEmpty ? 'NOT SET' : profile.emergencyContact), - + _buildInfoRow( + Icons.contact_phone, + 'EMERGENCY CONTACT', + profile.emergencyContact.isEmpty + ? 'NOT SET' + : profile.emergencyContact, + ), + const Spacer(), const Text( 'Tip: Keep this screen open for responders. For lock-screen wallpaper export, use your device screenshot tools.', @@ -84,9 +105,23 @@ class MedicalCardScreen extends ConsumerWidget { Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(label, style: const TextStyle(fontSize: 10, color: Colors.white38, fontWeight: FontWeight.w900)), + Text( + label, + style: const TextStyle( + fontSize: 10, + color: Colors.white38, + fontWeight: FontWeight.w900, + ), + ), const SizedBox(height: 4), - Text(value, style: const TextStyle(fontSize: 18, color: Colors.white, fontWeight: FontWeight.bold)), + Text( + value, + style: const TextStyle( + fontSize: 18, + color: Colors.white, + fontWeight: FontWeight.bold, + ), + ), ], ), ], diff --git a/lib/ui/mesh_chat_screen.dart b/lib/ui/mesh_chat_screen.dart index 3871352..f4f21c2 100644 --- a/lib/ui/mesh_chat_screen.dart +++ b/lib/ui/mesh_chat_screen.dart @@ -19,7 +19,10 @@ class _MeshChatScreenState extends ConsumerState { return Scaffold( backgroundColor: Colors.black, appBar: AppBar( - title: const Text('SCENE COORDINATION (OFFLINE)', style: TextStyle(fontWeight: FontWeight.w900, fontSize: 14)), + title: const Text( + 'SCENE COORDINATION (OFFLINE)', + style: TextStyle(fontWeight: FontWeight.w900, fontSize: 14), + ), backgroundColor: Colors.transparent, ), body: Column( @@ -34,7 +37,11 @@ class _MeshChatScreenState extends ConsumerState { Expanded( child: Text( 'BLE broadcast (foreground only). No delivery guarantee; use for short scene coordination.', - style: TextStyle(color: Colors.blue, fontSize: 10, fontWeight: FontWeight.bold), + style: TextStyle( + color: Colors.blue, + fontSize: 10, + fontWeight: FontWeight.bold, + ), ), ), ], @@ -48,20 +55,42 @@ class _MeshChatScreenState extends ConsumerState { final msg = messages[index]; final isMe = msg.senderId == 'SELF'; return Align( - alignment: isMe ? Alignment.centerRight : Alignment.centerLeft, + alignment: isMe + ? Alignment.centerRight + : Alignment.centerLeft, child: Container( margin: const EdgeInsets.only(bottom: 12), - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 10, + ), decoration: BoxDecoration( - color: isMe ? Colors.blue : Colors.white.withValues(alpha: 0.1), + color: isMe + ? Colors.blue + : Colors.white.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(16), ), child: Column( - crossAxisAlignment: isMe ? CrossAxisAlignment.end : CrossAxisAlignment.start, + crossAxisAlignment: isMe + ? CrossAxisAlignment.end + : CrossAxisAlignment.start, children: [ - Text(msg.senderId, style: TextStyle(fontSize: 10, fontWeight: FontWeight.bold, color: isMe ? Colors.white70 : Colors.blue)), + Text( + msg.senderId, + style: TextStyle( + fontSize: 10, + fontWeight: FontWeight.bold, + color: isMe ? Colors.white70 : Colors.blue, + ), + ), const SizedBox(height: 4), - Text(msg.content, style: const TextStyle(color: Colors.white, fontSize: 14)), + Text( + msg.content, + style: const TextStyle( + color: Colors.white, + fontSize: 14, + ), + ), ], ), ), @@ -88,7 +117,10 @@ class _MeshChatScreenState extends ConsumerState { decoration: InputDecoration( hintText: 'Coordinate with others...', hintStyle: const TextStyle(color: Colors.white24), - border: OutlineInputBorder(borderRadius: BorderRadius.circular(24), borderSide: BorderSide.none), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(24), + borderSide: BorderSide.none, + ), filled: true, fillColor: Colors.black, ), @@ -98,7 +130,9 @@ class _MeshChatScreenState extends ConsumerState { IconButton.filled( onPressed: () { if (_controller.text.isNotEmpty) { - ref.read(meshChatProvider.notifier).sendMessage(_controller.text); + ref + .read(meshChatProvider.notifier) + .sendMessage(_controller.text); _controller.clear(); } }, diff --git a/lib/ui/offline_map_screen.dart b/lib/ui/offline_map_screen.dart index 4e62de6..adb8ce6 100644 --- a/lib/ui/offline_map_screen.dart +++ b/lib/ui/offline_map_screen.dart @@ -41,7 +41,9 @@ class _OfflineMapScreenState extends ConsumerState { Future _startDownloadAroundMe() async { if (_busy) return; if (kIsWeb) { - setState(() => _lastStatus = 'Offline map downloads are not supported on web.'); + setState( + () => _lastStatus = 'Offline map downloads are not supported on web.', + ); return; } if (!fmtcMapCacheReady) { @@ -87,7 +89,9 @@ class _OfflineMapScreenState extends ConsumerState { ), ); - final tiles = await FMTCStore(kFmtcRoadsosOsmStore).download.countTiles(region); + final tiles = await FMTCStore( + kFmtcRoadsosOsmStore, + ).download.countTiles(region); if (!mounted) return; setState(() { @@ -95,33 +99,47 @@ class _OfflineMapScreenState extends ConsumerState { 'Downloading ~${tiles.toString()} tiles (r=${radiusKm.toStringAsFixed(0)}km, z${minZoom.toStringAsFixed(0)}–${maxZoom.toStringAsFixed(0)})…'; }); - final (:downloadProgress, :tileEvents) = FMTCStore(kFmtcRoadsosOsmStore).download.startForeground( - region: region, - parallelThreads: 6, - skipExistingTiles: true, - skipSeaTiles: true, - ); + final (:downloadProgress, :tileEvents) = FMTCStore(kFmtcRoadsosOsmStore) + .download + .startForeground( + region: region, + parallelThreads: 6, + skipExistingTiles: true, + skipSeaTiles: true, + ); await _progressSub?.cancel(); await _tileEventSub?.cancel(); - _progressSub = downloadProgress.listen((p) { - if (!mounted) return; - final complete = p.attemptedTilesCount >= p.maxTilesCount && p.remainingTilesCount == 0; - setState(() { - _progress = p; - _busy = !complete; - _lastStatus = complete - ? 'Offline region download complete.' - : 'Downloading… ${p.successfulTilesCount}/${p.maxTilesCount} tiles'; - }); - }, onError: (e, st) { - appLog.w('FMTC download progress stream error', error: e, stackTrace: st); - }); + _progressSub = downloadProgress.listen( + (p) { + if (!mounted) return; + final complete = + p.attemptedTilesCount >= p.maxTilesCount && + p.remainingTilesCount == 0; + setState(() { + _progress = p; + _busy = !complete; + _lastStatus = complete + ? 'Offline region download complete.' + : 'Downloading… ${p.successfulTilesCount}/${p.maxTilesCount} tiles'; + }); + }, + onError: (e, st) { + appLog.w( + 'FMTC download progress stream error', + error: e, + stackTrace: st, + ); + }, + ); - _tileEventSub = tileEvents.listen((_) {}, onError: (e, st) { - appLog.w('FMTC tile events stream error', error: e, stackTrace: st); - }); + _tileEventSub = tileEvents.listen( + (_) {}, + onError: (e, st) { + appLog.w('FMTC tile events stream error', error: e, stackTrace: st); + }, + ); // No await on completion: download runs in foreground thread; UI updates via stream. } catch (e, st) { @@ -169,7 +187,12 @@ class _OfflineMapScreenState extends ConsumerState { children: [ const Text( 'OFFLINE TILE CACHE', - style: TextStyle(color: Colors.white38, fontWeight: FontWeight.w900, fontSize: 10, letterSpacing: 1.5), + style: TextStyle( + color: Colors.white38, + fontWeight: FontWeight.w900, + fontSize: 10, + letterSpacing: 1.5, + ), ), const SizedBox(height: 24), _cacheStatusCard(), @@ -180,7 +203,8 @@ class _OfflineMapScreenState extends ConsumerState { LinearProgressIndicator( value: _progress!.maxTilesCount <= 0 ? null - : (_progress!.successfulTilesCount / _progress!.maxTilesCount), + : (_progress!.successfulTilesCount / + _progress!.maxTilesCount), backgroundColor: Colors.white10, color: scheme.primary, ), @@ -190,7 +214,11 @@ class _OfflineMapScreenState extends ConsumerState { Expanded( child: Text( _lastStatus ?? 'Downloading…', - style: const TextStyle(color: Colors.white70, fontSize: 12, fontWeight: FontWeight.w600), + style: const TextStyle( + color: Colors.white70, + fontSize: 12, + fontWeight: FontWeight.w600, + ), ), ), TextButton( @@ -203,7 +231,10 @@ class _OfflineMapScreenState extends ConsumerState { if (_lastStatus != null) ...[ Text( _lastStatus!, - style: TextStyle(color: Colors.white.withValues(alpha: 0.75), fontSize: 12), + style: TextStyle( + color: Colors.white.withValues(alpha: 0.75), + fontSize: 12, + ), ), const SizedBox(height: 12), ], @@ -213,18 +244,25 @@ class _OfflineMapScreenState extends ConsumerState { child: ElevatedButton.icon( onPressed: _busy ? null : _startDownloadAroundMe, icon: const Icon(Icons.download), - label: Text('DOWNLOAD AROUND ME (${_radiusKm.toStringAsFixed(0)}KM)'), + label: Text( + 'DOWNLOAD AROUND ME (${_radiusKm.toStringAsFixed(0)}KM)', + ), style: ElevatedButton.styleFrom( backgroundColor: Colors.blue, foregroundColor: Colors.white, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), ), ), ), const SizedBox(height: 12), Text( 'This downloads map tiles for offline use. Size depends on zoom levels and your area.', - style: TextStyle(color: Colors.white.withValues(alpha: 0.38), fontSize: 12), + style: TextStyle( + color: Colors.white.withValues(alpha: 0.38), + fontSize: 12, + ), ), ], ], @@ -258,7 +296,10 @@ class _OfflineMapScreenState extends ConsumerState { const SizedBox(height: 12), Text( 'Radius: ${_radiusKm.toStringAsFixed(0)} km', - style: const TextStyle(color: Colors.white70, fontWeight: FontWeight.w700), + style: const TextStyle( + color: Colors.white70, + fontWeight: FontWeight.w700, + ), ), Slider( value: _radiusKm, @@ -271,26 +312,35 @@ class _OfflineMapScreenState extends ConsumerState { const SizedBox(height: 8), Text( 'Zoom: ${_minZoom.toStringAsFixed(0)}–${_maxZoom.toStringAsFixed(0)}', - style: const TextStyle(color: Colors.white70, fontWeight: FontWeight.w700), + style: const TextStyle( + color: Colors.white70, + fontWeight: FontWeight.w700, + ), ), RangeSlider( values: RangeValues(_minZoom, _maxZoom), min: 8, max: 18, divisions: 10, - labels: RangeLabels(_minZoom.toStringAsFixed(0), _maxZoom.toStringAsFixed(0)), + labels: RangeLabels( + _minZoom.toStringAsFixed(0), + _maxZoom.toStringAsFixed(0), + ), onChanged: _busy ? null : (r) => setState(() { - final start = r.start.roundToDouble(); - final end = r.end.roundToDouble(); - _minZoom = start <= end ? start : end; - _maxZoom = end >= start ? end : start; - }), + final start = r.start.roundToDouble(); + final end = r.end.roundToDouble(); + _minZoom = start <= end ? start : end; + _maxZoom = end >= start ? end : start; + }), ), Text( 'Tip: higher zoom = more tiles (bigger download).', - style: TextStyle(color: Colors.white.withValues(alpha: 0.35), fontSize: 12), + style: TextStyle( + color: Colors.white.withValues(alpha: 0.35), + fontSize: 12, + ), ), ], ), @@ -302,7 +352,8 @@ class _OfflineMapScreenState extends ConsumerState { return _infoCard( icon: Icons.public, title: 'Web limitation', - body: 'Offline tile downloads are not supported on web. Install RoadSOS on a phone for offline caching.', + body: + 'Offline tile downloads are not supported on web. Install RoadSOS on a phone for offline caching.', ); } @@ -310,7 +361,8 @@ class _OfflineMapScreenState extends ConsumerState { return _infoCard( icon: Icons.warning_amber_rounded, title: 'Offline cache unavailable', - body: 'Tile cache backend did not initialize on this device. Maps will still work online.', + body: + 'Tile cache backend did not initialize on this device. Maps will still work online.', ); } @@ -361,12 +413,18 @@ class _OfflineMapScreenState extends ConsumerState { children: [ Text( title, - style: const TextStyle(color: Colors.white, fontWeight: FontWeight.w800), + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.w800, + ), ), const SizedBox(height: 8), Text( body, - style: TextStyle(color: Colors.white.withValues(alpha: 0.75), height: 1.35), + style: TextStyle( + color: Colors.white.withValues(alpha: 0.75), + height: 1.35, + ), ), ], ), diff --git a/lib/ui/onboarding_gate.dart b/lib/ui/onboarding_gate.dart index 2554fb9..0f45770 100644 --- a/lib/ui/onboarding_gate.dart +++ b/lib/ui/onboarding_gate.dart @@ -81,7 +81,9 @@ class _OnboardingGateState extends State { case _GatePhase.loading: return const Scaffold( backgroundColor: Color(0xFF080b12), - body: Center(child: CircularProgressIndicator(color: Color(0xFF4a90d9))), + body: Center( + child: CircularProgressIndicator(color: Color(0xFF4a90d9)), + ), ); case _GatePhase.permissions: return PermissionOnboardingScreen(onComplete: _onPermsDone); diff --git a/lib/ui/permission_onboarding_screen.dart b/lib/ui/permission_onboarding_screen.dart index 8fdc60e..3f8a80b 100644 --- a/lib/ui/permission_onboarding_screen.dart +++ b/lib/ui/permission_onboarding_screen.dart @@ -10,10 +10,12 @@ class PermissionOnboardingScreen extends StatefulWidget { final VoidCallback onComplete; @override - State createState() => _PermissionOnboardingScreenState(); + State createState() => + _PermissionOnboardingScreenState(); } -class _PermissionOnboardingScreenState extends State { +class _PermissionOnboardingScreenState + extends State { final _pages = const <_PermPage>[ _PermPage( title: 'Why RoadSOS asks for access', @@ -153,7 +155,10 @@ class _PermissionOnboardingScreenState extends State alignment: Alignment.centerRight, child: TextButton( onPressed: widget.onComplete, - child: const Text('SKIP FOR NOW', style: TextStyle(color: Colors.white38)), + child: const Text( + 'SKIP FOR NOW', + style: TextStyle(color: Colors.white38), + ), ), ), Expanded( @@ -182,7 +187,11 @@ class _PermissionOnboardingScreenState extends State child: SingleChildScrollView( child: Text( page.body, - style: const TextStyle(color: Colors.white70, height: 1.45, fontSize: 15), + style: const TextStyle( + color: Colors.white70, + height: 1.45, + fontSize: 15, + ), ), ), ), @@ -210,9 +219,14 @@ class _PermissionOnboardingScreenState extends State style: ElevatedButton.styleFrom( backgroundColor: Colors.blue, foregroundColor: Colors.white, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(14), + ), + ), + child: Text( + page.actionLabel!, + style: const TextStyle(fontWeight: FontWeight.bold), ), - child: Text(page.actionLabel!, style: const TextStyle(fontWeight: FontWeight.bold)), ), ); }, @@ -224,15 +238,18 @@ class _PermissionOnboardingScreenState extends State onPressed: _index == 0 ? null : () => _controller.previousPage( - duration: const Duration(milliseconds: 300), - curve: Curves.easeOut, - ), + duration: const Duration(milliseconds: 300), + curve: Curves.easeOut, + ), child: const Text('BACK'), ), const Spacer(), Text( '${_index + 1} / ${_pages.length}', - style: const TextStyle(color: Colors.white38, fontSize: 12), + style: const TextStyle( + color: Colors.white38, + fontSize: 12, + ), ), const Spacer(), TextButton( @@ -246,7 +263,9 @@ class _PermissionOnboardingScreenState extends State ); } }, - child: Text(_index >= _pages.length - 1 ? 'DONE' : 'NEXT'), + child: Text( + _index >= _pages.length - 1 ? 'DONE' : 'NEXT', + ), ), ], ), diff --git a/lib/ui/privacy_policy_screen.dart b/lib/ui/privacy_policy_screen.dart index 8cb8fb2..795b637 100644 --- a/lib/ui/privacy_policy_screen.dart +++ b/lib/ui/privacy_policy_screen.dart @@ -11,7 +11,10 @@ class PrivacyPolicyScreen extends StatefulWidget { } class _PrivacyPolicyScreenState extends State { - static const _assets = ['assets/legal/privacy_policy_en.txt', 'assets/legal/privacy_policy_hi.txt']; + static const _assets = [ + 'assets/legal/privacy_policy_en.txt', + 'assets/legal/privacy_policy_hi.txt', + ]; int _tab = 0; String _en = ''; @@ -59,8 +62,14 @@ class _PrivacyPolicyScreenState extends State { padding: const EdgeInsets.all(16), child: SegmentedButton( segments: [ - ButtonSegment(value: 0, label: Text(l10n.privacyPolicyLanguageEn)), - ButtonSegment(value: 1, label: Text(l10n.privacyPolicyLanguageHi)), + ButtonSegment( + value: 0, + label: Text(l10n.privacyPolicyLanguageEn), + ), + ButtonSegment( + value: 1, + label: Text(l10n.privacyPolicyLanguageHi), + ), ], selected: {_tab}, onSelectionChanged: (s) => setState(() => _tab = s.first), @@ -71,7 +80,11 @@ class _PrivacyPolicyScreenState extends State { padding: const EdgeInsets.all(20), child: Text( _tab == 0 ? _en : _hi, - style: const TextStyle(color: Colors.white70, height: 1.5, fontSize: 14), + style: const TextStyle( + color: Colors.white70, + height: 1.5, + fontSize: 14, + ), ), ), ), diff --git a/lib/ui/profile_editor_screen.dart b/lib/ui/profile_editor_screen.dart index 0743d56..3adcbf3 100644 --- a/lib/ui/profile_editor_screen.dart +++ b/lib/ui/profile_editor_screen.dart @@ -7,7 +7,8 @@ class ProfileEditorScreen extends ConsumerStatefulWidget { const ProfileEditorScreen({super.key}); @override - ConsumerState createState() => _ProfileEditorScreenState(); + ConsumerState createState() => + _ProfileEditorScreenState(); } class _ProfileEditorScreenState extends ConsumerState { @@ -29,7 +30,7 @@ class _ProfileEditorScreenState extends ConsumerState { _medsController = TextEditingController(text: profile.medications); _conditionsController = TextEditingController(text: profile.conditions); _contactController = TextEditingController(text: profile.emergencyContact); - + for (final contact in profile.emergencyContacts) { _additionalContactControllers.add(TextEditingController(text: contact)); } @@ -57,17 +58,20 @@ class _ProfileEditorScreenState extends ConsumerState { medications: _medsController.text, conditions: _conditionsController.text, emergencyContact: _contactController.text, - emergencyContacts: _additionalContactControllers.map((c) => c.text).where((t) => t.isNotEmpty).toList(), + emergencyContacts: _additionalContactControllers + .map((c) => c.text) + .where((t) => t.isNotEmpty) + .toList(), ); - + ref.read(userProfileProvider.notifier).updateProfile(newProfile); - + if (!mounted) return; - + Navigator.pop(context); - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Medical Profile Updated')), - ); + ScaffoldMessenger.of( + context, + ).showSnackBar(const SnackBar(content: Text('Medical Profile Updated'))); } @override @@ -75,10 +79,16 @@ class _ProfileEditorScreenState extends ConsumerState { return Scaffold( backgroundColor: Colors.black, appBar: AppBar( - title: const Text('EDIT MEDICAL ID', style: TextStyle(fontWeight: FontWeight.w900, fontSize: 16)), + title: const Text( + 'EDIT MEDICAL ID', + style: TextStyle(fontWeight: FontWeight.w900, fontSize: 16), + ), backgroundColor: Colors.transparent, actions: [ - IconButton(onPressed: _save, icon: const Icon(Icons.check, color: Colors.green)), + IconButton( + onPressed: _save, + icon: const Icon(Icons.check, color: Colors.green), + ), ], ), body: SingleChildScrollView( @@ -89,18 +99,35 @@ class _ProfileEditorScreenState extends ConsumerState { _buildField('Full Name', _nameController, Icons.person), _buildField('Blood Type', _bloodController, Icons.bloodtype), _buildField('Allergies', _allergiesController, Icons.warning), - _buildField('Current Medications', _medsController, Icons.medication), - _buildField('Chronic Conditions', _conditionsController, Icons.medical_information), - + _buildField( + 'Current Medications', + _medsController, + Icons.medication, + ), + _buildField( + 'Chronic Conditions', + _conditionsController, + Icons.medical_information, + ), + const Padding( padding: EdgeInsets.only(bottom: 12), child: Text( 'EMERGENCY CONTACTS', - style: TextStyle(color: Colors.white, fontSize: 12, fontWeight: FontWeight.w900, letterSpacing: 1.5), + style: TextStyle( + color: Colors.white, + fontSize: 12, + fontWeight: FontWeight.w900, + letterSpacing: 1.5, + ), ), ), - _buildField('Primary Contact', _contactController, Icons.contact_phone), - + _buildField( + 'Primary Contact', + _contactController, + Icons.contact_phone, + ), + ..._additionalContactControllers.asMap().entries.map((entry) { final index = entry.key; final controller = entry.value; @@ -108,21 +135,42 @@ class _ProfileEditorScreenState extends ConsumerState { padding: const EdgeInsets.only(bottom: 8), child: Row( children: [ - Expanded(child: _buildField('Additional Contact ${index + 1}', controller, Icons.contact_phone, showLabel: false)), + Expanded( + child: _buildField( + 'Additional Contact ${index + 1}', + controller, + Icons.contact_phone, + showLabel: false, + ), + ), const SizedBox(width: 8), IconButton( - onPressed: () => setState(() => _additionalContactControllers.removeAt(index)), - icon: const Icon(Icons.remove_circle_outline, color: Colors.redAccent), + onPressed: () => setState( + () => _additionalContactControllers.removeAt(index), + ), + icon: const Icon( + Icons.remove_circle_outline, + color: Colors.redAccent, + ), ), ], ), ); }), - + TextButton.icon( - onPressed: () => setState(() => _additionalContactControllers.add(TextEditingController())), + onPressed: () => setState( + () => + _additionalContactControllers.add(TextEditingController()), + ), icon: const Icon(Icons.add_circle_outline, color: Colors.blue), - label: const Text('ADD CONTACT', style: TextStyle(color: Colors.blue, fontWeight: FontWeight.bold)), + label: const Text( + 'ADD CONTACT', + style: TextStyle( + color: Colors.blue, + fontWeight: FontWeight.bold, + ), + ), ), const SizedBox(height: 40), @@ -130,7 +178,10 @@ class _ProfileEditorScreenState extends ConsumerState { child: Text( AppLocalizations.of(context)!.profileAiLine, textAlign: TextAlign.center, - style: TextStyle(color: Colors.white.withValues(alpha: 0.3), fontSize: 11), + style: TextStyle( + color: Colors.white.withValues(alpha: 0.3), + fontSize: 11, + ), ), ), ], @@ -139,21 +190,37 @@ class _ProfileEditorScreenState extends ConsumerState { ); } - Widget _buildField(String label, TextEditingController controller, IconData icon, {bool showLabel = true}) { + Widget _buildField( + String label, + TextEditingController controller, + IconData icon, { + bool showLabel = true, + }) { return Padding( padding: EdgeInsets.only(bottom: showLabel ? 24 : 0), child: TextField( controller: controller, - style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold), + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + ), decoration: InputDecoration( labelText: showLabel ? label.toUpperCase() : null, hintText: !showLabel ? label.toUpperCase() : null, hintStyle: TextStyle(color: Colors.white24, fontSize: 10), - labelStyle: TextStyle(color: Colors.blue.withValues(alpha: 0.6), fontSize: 10, fontWeight: FontWeight.w900, letterSpacing: 1.5), + labelStyle: TextStyle( + color: Colors.blue.withValues(alpha: 0.6), + fontSize: 10, + fontWeight: FontWeight.w900, + letterSpacing: 1.5, + ), prefixIcon: Icon(icon, color: Colors.white24), filled: true, fillColor: Colors.white.withValues(alpha: 0.05), - border: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: BorderSide.none), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide.none, + ), ), ), ); diff --git a/lib/ui/responder_dashboard.dart b/lib/ui/responder_dashboard.dart index 04d1345..3af1c8a 100644 --- a/lib/ui/responder_dashboard.dart +++ b/lib/ui/responder_dashboard.dart @@ -15,26 +15,34 @@ class ResponderDashboard extends ConsumerWidget { return Scaffold( backgroundColor: Colors.black, appBar: AppBar( - title: const Text('RESPONDER VIEW', style: TextStyle(fontWeight: FontWeight.w900, fontSize: 16)), + title: const Text( + 'RESPONDER VIEW', + style: TextStyle(fontWeight: FontWeight.w900, fontSize: 16), + ), backgroundColor: Colors.transparent, ), body: Column( children: [ - Expanded( - child: RoadSosMap(state: sosState, autoCenter: false), - ), + Expanded(child: RoadSosMap(state: sosState, autoCenter: false)), Container( padding: const EdgeInsets.all(20), decoration: BoxDecoration( color: Colors.white.withValues(alpha: 0.05), - borderRadius: const BorderRadius.vertical(top: Radius.circular(24)), + borderRadius: const BorderRadius.vertical( + top: Radius.circular(24), + ), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'ACTIVE SIGNALS (REAL-TIME)', - style: TextStyle(color: Colors.white38, fontWeight: FontWeight.w900, fontSize: 10, letterSpacing: 1.5), + style: TextStyle( + color: Colors.white38, + fontWeight: FontWeight.w900, + fontSize: 10, + letterSpacing: 1.5, + ), ), const SizedBox(height: 16), _buildSosCard(sosState), @@ -42,7 +50,12 @@ class ResponderDashboard extends ConsumerWidget { const Divider(color: Colors.white10, height: 24), const Text( 'RECENT MESH PACKETS', - style: TextStyle(color: Colors.white38, fontWeight: FontWeight.w900, fontSize: 10, letterSpacing: 1.5), + style: TextStyle( + color: Colors.white38, + fontWeight: FontWeight.w900, + fontSize: 10, + letterSpacing: 1.5, + ), ), const SizedBox(height: 10), StreamBuilder( @@ -69,14 +82,22 @@ class ResponderDashboard extends ConsumerWidget { children: [ Text( 'From ${pkt.senderId}${pkt.rssi != null ? ' (RSSI ${pkt.rssi})' : ''}', - style: const TextStyle(color: Colors.white, fontWeight: FontWeight.w800, fontSize: 12), + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.w800, + fontSize: 12, + ), ), const SizedBox(height: 6), Text( pkt.payload, maxLines: 3, overflow: TextOverflow.ellipsis, - style: const TextStyle(color: Colors.white70, fontSize: 12, height: 1.35), + style: const TextStyle( + color: Colors.white70, + fontSize: 12, + height: 1.35, + ), ), ], ), @@ -92,7 +113,8 @@ class ResponderDashboard extends ConsumerWidget { } Widget _buildSosCard(SOSState state) { - final active = state.phase == SOSPhase.active || state.phase == SOSPhase.dispatching; + final active = + state.phase == SOSPhase.active || state.phase == SOSPhase.dispatching; final title = active ? 'SOS ACTIVE' : 'NO ACTIVE SOS'; final detail = state.incidentId == null ? '—' : state.incidentId!; final sev = state.triageResult?.severityLevel; @@ -105,9 +127,13 @@ class ResponderDashboard extends ConsumerWidget { width: double.infinity, padding: const EdgeInsets.all(14), decoration: BoxDecoration( - color: active ? Colors.red.withValues(alpha: 0.10) : Colors.white.withValues(alpha: 0.04), + color: active + ? Colors.red.withValues(alpha: 0.10) + : Colors.white.withValues(alpha: 0.04), borderRadius: BorderRadius.circular(12), - border: Border.all(color: active ? Colors.red.withValues(alpha: 0.30) : Colors.white10), + border: Border.all( + color: active ? Colors.red.withValues(alpha: 0.30) : Colors.white10, + ), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -124,12 +150,20 @@ class ResponderDashboard extends ConsumerWidget { const SizedBox(height: 6), Text( 'Incident: $detail', - style: const TextStyle(color: Colors.white, fontWeight: FontWeight.w800, fontSize: 12), + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.w800, + fontSize: 12, + ), ), const SizedBox(height: 6), Text( '$sevLabel\n$locLine', - style: const TextStyle(color: Colors.white70, fontSize: 12, height: 1.35), + style: const TextStyle( + color: Colors.white70, + fontSize: 12, + height: 1.35, + ), ), ], ), diff --git a/lib/ui/safe_walk_overlay.dart b/lib/ui/safe_walk_overlay.dart index 9f24696..6c91bfe 100644 --- a/lib/ui/safe_walk_overlay.dart +++ b/lib/ui/safe_walk_overlay.dart @@ -21,80 +21,118 @@ class SafeWalkOverlay extends ConsumerWidget { child: Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), decoration: BoxDecoration( - color: monitor.alertTriggered ? Colors.red.withValues(alpha: 0.9) : Colors.blue.withValues(alpha: 0.9), + color: monitor.alertTriggered + ? Colors.red.withValues(alpha: 0.9) + : Colors.blue.withValues(alpha: 0.9), borderRadius: BorderRadius.circular(16), boxShadow: [BoxShadow(color: Colors.black26, blurRadius: 10)], ), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Row( - children: [ - Icon( - monitor.alertTriggered ? Icons.warning : Icons.directions_walk, - color: Colors.white, - ), - const SizedBox(width: 12), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Text( - monitor.alertTriggered ? 'CHECK-IN REQUIRED' : 'SAFE-WALK ACTIVE', - style: const TextStyle(color: Colors.white, fontWeight: FontWeight.w900, fontSize: 10), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Row( + children: [ + Icon( + monitor.alertTriggered + ? Icons.warning + : Icons.directions_walk, + color: Colors.white, + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + monitor.alertTriggered + ? 'CHECK-IN REQUIRED' + : 'SAFE-WALK ACTIVE', + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.w900, + fontSize: 10, ), - Text( - monitor.alertTriggered - ? 'Confirm your safety now or SOS will trigger.' + ), + Text( + monitor.alertTriggered + ? 'Confirm your safety now or SOS will trigger.' : 'Heading to ${monitor.destination}', - style: const TextStyle(color: Colors.white, fontSize: 12), + style: const TextStyle( + color: Colors.white, + fontSize: 12, ), - ], + ), + ], + ), + ), + if (monitor.alertTriggered) + TextButton( + onPressed: () => ref + .read(proactiveMonitorProvider.notifier) + .confirmImSafe(), + child: const Text( + 'I AM SAFE', + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + ), ), ), - if (monitor.alertTriggered) - TextButton( - onPressed: () => ref.read(proactiveMonitorProvider.notifier).confirmImSafe(), - child: const Text('I AM SAFE', style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold)), + ], + ), + const SizedBox(height: 12), + Wrap( + spacing: 8, + runSpacing: 8, + alignment: WrapAlignment.end, + children: [ + if (monitor.destination != null && + monitor.destination != 'your destination') + OutlinedButton.icon( + style: OutlinedButton.styleFrom( + foregroundColor: Colors.white, + side: const BorderSide(color: Colors.white54), ), - ], - ), - const SizedBox(height: 12), - Wrap( - spacing: 8, - runSpacing: 8, - alignment: WrapAlignment.end, - children: [ - if (monitor.destination != null && monitor.destination != 'your destination') - OutlinedButton.icon( - style: OutlinedButton.styleFrom( - foregroundColor: Colors.white, - side: const BorderSide(color: Colors.white54), - ), - onPressed: () async { - final url = Uri.parse('https://www.google.com/maps/dir/?api=1&destination=${Uri.encodeComponent(monitor.destination!)}'); - if (await canLaunchUrl(url)) { - await launchUrl(url, mode: LaunchMode.externalApplication); - } - }, - icon: const Icon(Icons.map, size: 16), - label: const Text('DIRECTIONS', style: TextStyle(fontSize: 12)), + onPressed: () async { + final url = Uri.parse( + 'https://www.google.com/maps/dir/?api=1&destination=${Uri.encodeComponent(monitor.destination!)}', + ); + if (await canLaunchUrl(url)) { + await launchUrl( + url, + mode: LaunchMode.externalApplication, + ); + } + }, + icon: const Icon(Icons.map, size: 16), + label: const Text( + 'DIRECTIONS', + style: TextStyle(fontSize: 12), ), - ElevatedButton.icon( - style: ElevatedButton.styleFrom( - backgroundColor: Colors.white, - foregroundColor: Colors.black, + ), + ElevatedButton.icon( + style: ElevatedButton.styleFrom( + backgroundColor: Colors.white, + foregroundColor: Colors.black, + ), + onPressed: () => ref + .read(proactiveMonitorProvider.notifier) + .endSafeWalk(), + icon: const Icon(Icons.stop, size: 16), + label: const Text( + 'END SAFE PATH', + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.bold, ), - onPressed: () => ref.read(proactiveMonitorProvider.notifier).endSafeWalk(), - icon: const Icon(Icons.stop, size: 16), - label: const Text('END SAFE PATH', style: TextStyle(fontSize: 12, fontWeight: FontWeight.bold)), ), - ], - ), - ], - ), + ), + ], + ), + ], + ), ), ), ); diff --git a/lib/ui/settings_screen.dart b/lib/ui/settings_screen.dart index 14f4364..ca0f02e 100644 --- a/lib/ui/settings_screen.dart +++ b/lib/ui/settings_screen.dart @@ -69,7 +69,9 @@ class _SettingsScreenState extends ConsumerState { if (!seen && mounted) { await Navigator.push( context, - MaterialPageRoute(builder: (_) => const GoodSamaritanLawScreen()), + MaterialPageRoute( + builder: (_) => const GoodSamaritanLawScreen(), + ), ); await NearbySosPreferences.setGoodSamaritanSeen(true); } @@ -108,7 +110,10 @@ class _SettingsScreenState extends ConsumerState { return Scaffold( backgroundColor: Colors.black, appBar: AppBar( - title: Text(l10n.settingsTitle, style: const TextStyle(fontWeight: FontWeight.w900, fontSize: 14)), + title: Text( + l10n.settingsTitle, + style: const TextStyle(fontWeight: FontWeight.w900, fontSize: 14), + ), backgroundColor: Colors.transparent, ), body: ListView( @@ -147,7 +152,9 @@ class _SettingsScreenState extends ConsumerState { 'GPS, triage, SMS/mesh/cloud steps — for insurance or police records', onTap: () => Navigator.push( context, - MaterialPageRoute(builder: (_) => const SosActivityLogScreen()), + MaterialPageRoute( + builder: (_) => const SosActivityLogScreen(), + ), ), ), _tile( @@ -192,7 +199,9 @@ class _SettingsScreenState extends ConsumerState { Icons.hearing_disabled_outlined, 'Background volume SOS', 'Open Accessibility and enable RoadSOS for lock-screen gesture (3× up + 3× down)', - onTap: () => ref.read(hardwareTriggerServiceProvider).openAndroidAccessibilitySettings(), + onTap: () => ref + .read(hardwareTriggerServiceProvider) + .openAndroidAccessibilitySettings(), ), ], const SizedBox(height: 32), @@ -200,17 +209,34 @@ class _SettingsScreenState extends ConsumerState { SwitchListTile( value: _nearbySos, onChanged: _onNearbySosChanged, - secondary: const Icon(Icons.notifications_active, color: Colors.white70), - title: Text(l10n.nearbySosToggleTitle, style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold)), - subtitle: Text(l10n.nearbySosToggleSubtitle, style: const TextStyle(color: Colors.white38, fontSize: 12)), + secondary: const Icon( + Icons.notifications_active, + color: Colors.white70, + ), + title: Text( + l10n.nearbySosToggleTitle, + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + ), + ), + subtitle: Text( + l10n.nearbySosToggleSubtitle, + style: const TextStyle(color: Colors.white38, fontSize: 12), + ), ), ListTile( leading: const Icon(Icons.balance, color: Colors.greenAccent), - title: Text(l10n.nearbySosLearnProtection, style: const TextStyle(color: Colors.white)), + title: Text( + l10n.nearbySosLearnProtection, + style: const TextStyle(color: Colors.white), + ), trailing: const Icon(Icons.chevron_right, color: Colors.white24), onTap: () => Navigator.push( context, - MaterialPageRoute(builder: (_) => const GoodSamaritanLawScreen()), + MaterialPageRoute( + builder: (_) => const GoodSamaritanLawScreen(), + ), ), ), const SizedBox(height: 32), @@ -221,11 +247,17 @@ class _SettingsScreenState extends ConsumerState { secondary: const Icon(Icons.cloud_queue, color: Colors.white70), title: Text( l10n.settingsExtendedRetentionTitle, - style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold), + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + ), ), subtitle: Text( l10n.settingsExtendedRetentionSubtitle, - style: TextStyle(color: Colors.white.withValues(alpha: 0.38), fontSize: 12), + style: TextStyle( + color: Colors.white.withValues(alpha: 0.38), + fontSize: 12, + ), ), ), _tile( @@ -234,9 +266,9 @@ class _SettingsScreenState extends ConsumerState { l10n.blackBoxTitle, l10n.blackBoxSubtitle, onTap: () { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(l10n.blackBoxSnack)), - ); + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text(l10n.blackBoxSnack))); }, ), _tile( @@ -280,9 +312,20 @@ class _SettingsScreenState extends ConsumerState { return ListTile( onTap: onTap, leading: Icon(icon, color: Colors.white70), - title: Text(title, style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 15)), - subtitle: Text(subtitle, style: const TextStyle(color: Colors.white38, fontSize: 12)), - trailing: trailing ?? const Icon(Icons.chevron_right, color: Colors.white24), + title: Text( + title, + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: 15, + ), + ), + subtitle: Text( + subtitle, + style: const TextStyle(color: Colors.white38, fontSize: 12), + ), + trailing: + trailing ?? const Icon(Icons.chevron_right, color: Colors.white24), contentPadding: const EdgeInsets.symmetric(vertical: 8, horizontal: 4), ); } diff --git a/lib/ui/sos_activity_log_screen.dart b/lib/ui/sos_activity_log_screen.dart index 9d3b600..f3b045d 100644 --- a/lib/ui/sos_activity_log_screen.dart +++ b/lib/ui/sos_activity_log_screen.dart @@ -7,7 +7,9 @@ import '../services/emergency_orchestrator.dart'; import '../services/sos_activity_log_service.dart'; import 'dispatch_status_panel.dart'; -final _activityHistoryProvider = FutureProvider>((ref) async { +final _activityHistoryProvider = FutureProvider>(( + ref, +) async { return SosActivityLogService.instance.loadHistory(); }); @@ -25,7 +27,11 @@ class SosActivityLogScreen extends ConsumerWidget { appBar: AppBar( title: const Text( 'ACTIVITY LOG', - style: TextStyle(fontWeight: FontWeight.w900, fontSize: 14, letterSpacing: 1), + style: TextStyle( + fontWeight: FontWeight.w900, + fontSize: 14, + letterSpacing: 1, + ), ), backgroundColor: Colors.transparent, actions: [ @@ -41,12 +47,17 @@ class SosActivityLogScreen extends ConsumerWidget { children: [ _insuranceBanner(), const SizedBox(height: 16), - if (live.phase != SOSPhase.idle && live.dispatchChannels.isNotEmpty) ...[ + if (live.phase != SOSPhase.idle && + live.dispatchChannels.isNotEmpty) ...[ _sectionTitle('CURRENT SESSION'), const SizedBox(height: 8), Text( live.incidentId ?? '—', - style: const TextStyle(color: Colors.white38, fontSize: 11, fontFamily: 'monospace'), + style: const TextStyle( + color: Colors.white38, + fontSize: 11, + fontFamily: 'monospace', + ), ), const SizedBox(height: 12), DispatchStatusPanel(channels: live.dispatchChannels), @@ -67,7 +78,10 @@ class SosActivityLogScreen extends ConsumerWidget { ); }, loading: () => const Center(child: CircularProgressIndicator()), - error: (e, _) => Text('Could not load history: $e', style: const TextStyle(color: Colors.redAccent)), + error: (e, _) => Text( + 'Could not load history: $e', + style: const TextStyle(color: Colors.redAccent), + ), ), ], ), @@ -116,29 +130,53 @@ class SosActivityLogScreen extends ConsumerWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(fmt.format(localTime), style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold)), + Text( + fmt.format(localTime), + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + ), + ), const SizedBox(height: 4), Text( 'Incident ${r.incidentId}', - style: const TextStyle(color: Colors.white38, fontSize: 11, fontFamily: 'monospace'), + style: const TextStyle( + color: Colors.white38, + fontSize: 11, + fontFamily: 'monospace', + ), ), const SizedBox(height: 12), Text( 'GPS shared (coarse by default)', - style: TextStyle(color: Colors.blue.withValues(alpha: 0.8), fontWeight: FontWeight.w800, fontSize: 10), + style: TextStyle( + color: Colors.blue.withValues(alpha: 0.8), + fontWeight: FontWeight.w800, + fontSize: 10, + ), ), const SizedBox(height: 4), - SelectableText('$coarseLat, $coarseLng (±${r.accuracyM.round()} m • ${r.locationSource})', - style: const TextStyle(color: Colors.white70, fontSize: 13)), + SelectableText( + '$coarseLat, $coarseLng (±${r.accuracyM.round()} m • ${r.locationSource})', + style: const TextStyle(color: Colors.white70, fontSize: 13), + ), const SizedBox(height: 6), Text( 'Precise coordinates are stored encrypted on-device. If police/insurer requests exact GPS, export via a dedicated signed report (not implemented yet).', - style: const TextStyle(color: Colors.white38, fontSize: 11, height: 1.35), + style: const TextStyle( + color: Colors.white38, + fontSize: 11, + height: 1.35, + ), ), const SizedBox(height: 12), Text( 'AI triage', - style: TextStyle(color: Colors.blue.withValues(alpha: 0.8), fontWeight: FontWeight.w800, fontSize: 10), + style: TextStyle( + color: Colors.blue.withValues(alpha: 0.8), + fontWeight: FontWeight.w800, + fontSize: 10, + ), ), const SizedBox(height: 4), Text( @@ -155,14 +193,25 @@ class SosActivityLogScreen extends ConsumerWidget { const SizedBox(height: 12), Text( 'Sync / storage', - style: TextStyle(color: Colors.blue.withValues(alpha: 0.8), fontWeight: FontWeight.w800, fontSize: 10), + style: TextStyle( + color: Colors.blue.withValues(alpha: 0.8), + fontWeight: FontWeight.w800, + fontSize: 10, + ), ), const SizedBox(height: 4), - Text(r.syncStatusLine, style: const TextStyle(color: Colors.white70, fontSize: 13)), + Text( + r.syncStatusLine, + style: const TextStyle(color: Colors.white70, fontSize: 13), + ), const SizedBox(height: 14), Text( 'Channels', - style: TextStyle(color: Colors.blue.withValues(alpha: 0.8), fontWeight: FontWeight.w800, fontSize: 10), + style: TextStyle( + color: Colors.blue.withValues(alpha: 0.8), + fontWeight: FontWeight.w800, + fontSize: 10, + ), ), const SizedBox(height: 8), DispatchStatusPanel(channels: r.channels), diff --git a/lib/ui/sos_countdown_widget.dart b/lib/ui/sos_countdown_widget.dart index 0a8470c..b182e44 100644 --- a/lib/ui/sos_countdown_widget.dart +++ b/lib/ui/sos_countdown_widget.dart @@ -56,7 +56,6 @@ class _SOSCountdownWidgetState extends State { mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - // ── Countdown ring ──────────────────────────────────────────────── Center( child: SizedBox( @@ -110,10 +109,10 @@ class _SOSCountdownWidgetState extends State { child: Text( widget.warningText, style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: scheme.onSurface, - fontWeight: FontWeight.w600, - height: 1.35, - ), + color: scheme.onSurface, + fontWeight: FontWeight.w600, + height: 1.35, + ), ), ), ], @@ -178,7 +177,9 @@ class _CountdownRingPainter extends CustomPainter { // Progress arc final progressPaint = Paint() - ..color = progress > 0.3 ? const Color(0xFFFF453A) : const Color(0xFF880E1F) + ..color = progress > 0.3 + ? const Color(0xFFFF453A) + : const Color(0xFF880E1F) ..style = PaintingStyle.stroke ..strokeWidth = 6 ..strokeCap = StrokeCap.round; diff --git a/lib/ui/sos_side_effect_observer.dart b/lib/ui/sos_side_effect_observer.dart index 4cc9542..ba0be2d 100644 --- a/lib/ui/sos_side_effect_observer.dart +++ b/lib/ui/sos_side_effect_observer.dart @@ -3,7 +3,7 @@ import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../services/emergency_orchestrator.dart'; -/// SOSSideEffectObserver: Handles platform side-effects (TTS, Haptics) +/// SOSSideEffectObserver: Handles platform side-effects (TTS, Haptics) /// based on SOS state changes. Decouples UI logic from services. class SOSSideEffectObserver extends ConsumerWidget { const SOSSideEffectObserver({super.key}); @@ -23,9 +23,11 @@ class SOSSideEffectObserver extends ConsumerWidget { switch (phase) { case SOSPhase.active: HapticFeedback.heavyImpact(); - ref.read(voiceAssistantServiceProvider).speak( - 'SOS is live. Help is on the way. Your location and medical profile are being broadcasted.' - ); + ref + .read(voiceAssistantServiceProvider) + .speak( + 'SOS is live. Help is on the way. Your location and medical profile are being broadcasted.', + ); break; case SOSPhase.triaging: HapticFeedback.mediumImpact(); diff --git a/lib/ui/triage_result_card.dart b/lib/ui/triage_result_card.dart index 08b680f..583c30c 100644 --- a/lib/ui/triage_result_card.dart +++ b/lib/ui/triage_result_card.dart @@ -23,10 +23,7 @@ class TriageResultCard extends StatelessWidget { decoration: BoxDecoration( color: scheme.surfaceContainerHighest, borderRadius: BorderRadius.circular(16), - border: Border.all( - color: severe.withAlpha(115), - width: 1.5, - ), + border: Border.all(color: severe.withAlpha(115), width: 1.5), boxShadow: [ BoxShadow( color: severe.withAlpha(46), @@ -43,7 +40,9 @@ class TriageResultCard extends StatelessWidget { padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: severe.withAlpha(46), - borderRadius: const BorderRadius.vertical(top: Radius.circular(16)), + borderRadius: const BorderRadius.vertical( + top: Radius.circular(16), + ), ), child: Row( children: [ @@ -54,7 +53,9 @@ class TriageResultCard extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - result.isDegradedMode ? l10n.triageDegradedTitle : l10n.triageResultTitle, + result.isDegradedMode + ? l10n.triageDegradedTitle + : l10n.triageResultTitle, style: TextStyle( fontSize: 11, fontWeight: FontWeight.w700, @@ -68,7 +69,8 @@ class TriageResultCard extends StatelessWidget { result.severityLevel, _severityLabel(context, result.severityLevel), ), - style: Theme.of(context).textTheme.titleMedium?.copyWith( + style: Theme.of(context).textTheme.titleMedium + ?.copyWith( fontWeight: FontWeight.w600, color: scheme.onSurface, ), @@ -81,7 +83,10 @@ class TriageResultCard extends StatelessWidget { children: [ if (result.isDegradedMode) Container( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 4, + ), decoration: BoxDecoration( color: scheme.tertiary.withAlpha(56), borderRadius: BorderRadius.circular(8), @@ -98,16 +103,25 @@ class TriageResultCard extends StatelessWidget { if (result.wasOverridden) ...[ const SizedBox(height: 4), Container( - padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 3), + padding: const EdgeInsets.symmetric( + horizontal: 6, + vertical: 3, + ), decoration: BoxDecoration( color: Colors.amber.withAlpha(40), borderRadius: BorderRadius.circular(6), - border: Border.all(color: Colors.amber.withAlpha(100)), + border: Border.all( + color: Colors.amber.withAlpha(100), + ), ), child: const Row( mainAxisSize: MainAxisSize.min, children: [ - Icon(Icons.shield_outlined, size: 10, color: Colors.amber), + Icon( + Icons.shield_outlined, + size: 10, + color: Colors.amber, + ), SizedBox(width: 3), Text( 'VALIDATED', @@ -136,10 +150,10 @@ class TriageResultCard extends StatelessWidget { Text( l10n.dispatchedServices, style: Theme.of(context).textTheme.labelMedium?.copyWith( - fontWeight: FontWeight.w700, - letterSpacing: 1.5, - color: scheme.onSurface.withAlpha(194), - ), + fontWeight: FontWeight.w700, + letterSpacing: 1.5, + color: scheme.onSurface.withAlpha(194), + ), ), const SizedBox(height: 8), Wrap( @@ -169,11 +183,16 @@ class TriageResultCard extends StatelessWidget { children: [ Row( children: [ - Icon(Icons.medical_services, size: 14, color: scheme.primary), + Icon( + Icons.medical_services, + size: 14, + color: scheme.primary, + ), const SizedBox(width: 6), Text( l10n.firstAidGuidance, - style: Theme.of(context).textTheme.labelMedium?.copyWith( + style: Theme.of(context).textTheme.labelMedium + ?.copyWith( fontWeight: FontWeight.w700, color: scheme.primary, letterSpacing: 1, @@ -187,17 +206,17 @@ class TriageResultCard extends StatelessWidget { selectable: true, styleSheet: MarkdownStyleSheet( p: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: scheme.onSurface.withAlpha(235), - height: 1.4, - ), + color: scheme.onSurface.withAlpha(235), + height: 1.4, + ), strong: Theme.of(context).textTheme.bodyMedium?.copyWith( - fontWeight: FontWeight.w800, - color: scheme.onSurface, - ), + fontWeight: FontWeight.w800, + color: scheme.onSurface, + ), em: Theme.of(context).textTheme.bodyMedium?.copyWith( - fontStyle: FontStyle.italic, - color: scheme.onSurface.withAlpha(235), - ), + fontStyle: FontStyle.italic, + color: scheme.onSurface.withAlpha(235), + ), blockquoteDecoration: BoxDecoration( color: scheme.surface.withAlpha(140), borderRadius: BorderRadius.circular(8), @@ -223,11 +242,11 @@ class TriageResultCard extends StatelessWidget { child: Text( result.compressedPayload, style: Theme.of(context).textTheme.bodySmall?.copyWith( - fontSize: 10, - fontFamily: 'RobotoMono', - color: scheme.onSurface.withAlpha(148), - letterSpacing: 0.5, - ), + fontSize: 10, + fontFamily: 'RobotoMono', + color: scheme.onSurface.withAlpha(148), + letterSpacing: 0.5, + ), ), ), ], @@ -238,23 +257,35 @@ class TriageResultCard extends StatelessWidget { String _severityLabel(BuildContext context, int level) { final l10n = AppLocalizations.of(context)!; switch (level) { - case 5: return l10n.severityCritical; - case 4: return l10n.severitySevere; - case 3: return l10n.severityModerate; - case 2: return l10n.severityMinor; - case 1: return l10n.severityLow; - default: return l10n.severityUnknown; + case 5: + return l10n.severityCritical; + case 4: + return l10n.severitySevere; + case 3: + return l10n.severityModerate; + case 2: + return l10n.severityMinor; + case 1: + return l10n.severityLow; + default: + return l10n.severityUnknown; } } Color _severityColor(int level) { switch (level) { - case 5: return Colors.red; - case 4: return Colors.deepOrange; - case 3: return Colors.orange; - case 2: return Colors.amber.shade700; - case 1: return Colors.green.shade700; - default: return Colors.grey; + case 5: + return Colors.red; + case 4: + return Colors.deepOrange; + case 3: + return Colors.orange; + case 2: + return Colors.amber.shade700; + case 1: + return Colors.green.shade700; + default: + return Colors.grey; } } } @@ -329,12 +360,18 @@ class _ServiceChip extends StatelessWidget { (IconData, Color) _serviceVisuals(String service) { switch (service) { - case 'ambulance': return (Icons.local_hospital, Colors.red); - case 'police': return (Icons.local_police, Colors.blue); - case 'fire_department': return (Icons.local_fire_department, Colors.orange); - case 'rescue': return (Icons.health_and_safety, Colors.teal); - case 'towing': return (Icons.car_repair, Colors.amber.shade800); - default: return (Icons.help, Colors.grey); + case 'ambulance': + return (Icons.local_hospital, Colors.red); + case 'police': + return (Icons.local_police, Colors.blue); + case 'fire_department': + return (Icons.local_fire_department, Colors.orange); + case 'rescue': + return (Icons.health_and_safety, Colors.teal); + case 'towing': + return (Icons.car_repair, Colors.amber.shade800); + default: + return (Icons.help, Colors.grey); } } } diff --git a/lib/ui/vehicle_rescue_data.dart b/lib/ui/vehicle_rescue_data.dart index 54bc77a..49db604 100644 --- a/lib/ui/vehicle_rescue_data.dart +++ b/lib/ui/vehicle_rescue_data.dart @@ -41,7 +41,6 @@ class RescueStep { // ALL OFFLINE RESCUE DATA // ───────────────────────────────────────────── const Map kVehicleRescueDatabase = { - 'car': VehicleRescueData( vehicleType: 'Car / Sedan / Hatchback', icon: '🚗', @@ -57,40 +56,47 @@ const Map kVehicleRescueDatabase = { RescueStep( stepNumber: 1, title: 'Make the scene safe', - detail: 'Turn off the engine if accessible. Turn on hazard lights. Place objects 50m behind to warn traffic.', + detail: + 'Turn off the engine if accessible. Turn on hazard lights. Place objects 50m behind to warn traffic.', isCritical: true, ), RescueStep( stepNumber: 2, title: 'Check if victim is conscious', - detail: 'Tap shoulder and shout "Can you hear me?". If no response, call 108 immediately. Do NOT shake them.', + detail: + 'Tap shoulder and shout "Can you hear me?". If no response, call 108 immediately. Do NOT shake them.', ), RescueStep( stepNumber: 3, title: 'Do NOT move the victim yet', - detail: 'If victim is breathing and not in immediate danger (no fire/flood), keep them still. Moving can worsen spinal injuries.', + detail: + 'If victim is breathing and not in immediate danger (no fire/flood), keep them still. Moving can worsen spinal injuries.', isCritical: true, ), RescueStep( stepNumber: 4, title: 'Open the door safely', - detail: 'Pull door handle and simultaneously push door outward with shoulder. For jammed doors, try rear doors first.', + detail: + 'Pull door handle and simultaneously push door outward with shoulder. For jammed doors, try rear doors first.', ), RescueStep( stepNumber: 5, title: 'Support the neck and head', - detail: 'Place both hands on either side of victim\'s head. Keep head aligned with spine at ALL times. Ask someone else to help.', + detail: + 'Place both hands on either side of victim\'s head. Keep head aligned with spine at ALL times. Ask someone else to help.', isCritical: true, ), RescueStep( stepNumber: 6, title: 'Slide victim out horizontally', - detail: 'One person holds head, another grips under armpits. Move in one smooth motion. Never twist the spine.', + detail: + 'One person holds head, another grips under armpits. Move in one smooth motion. Never twist the spine.', ), RescueStep( stepNumber: 7, title: 'Place in recovery position', - detail: 'If breathing, place on their side (recovery position) to prevent choking. Keep monitoring until ambulance arrives.', + detail: + 'If breathing, place on their side (recovery position) to prevent choking. Keep monitoring until ambulance arrives.', ), ], firstAidTips: [ @@ -117,34 +123,40 @@ const Map kVehicleRescueDatabase = { RescueStep( stepNumber: 1, title: 'Approach from the SIDE only', - detail: 'Never approach from front (engine fire) or rear (cargo). Come from driver\'s side door angle.', + detail: + 'Never approach from front (engine fire) or rear (cargo). Come from driver\'s side door angle.', isCritical: true, ), RescueStep( stepNumber: 2, title: 'Secure the truck', - detail: 'If safe, apply handbrake and place wheel chocks (stones/wood) under tires to prevent rolling.', + detail: + 'If safe, apply handbrake and place wheel chocks (stones/wood) under tires to prevent rolling.', ), RescueStep( stepNumber: 3, title: 'Climb up carefully', - detail: 'Use the built-in steps/handles on the cab. Don\'t pull on door handles to climb — they may break.', + detail: + 'Use the built-in steps/handles on the cab. Don\'t pull on door handles to climb — they may break.', ), RescueStep( stepNumber: 4, title: 'Check driver consciousness', - detail: 'Tap and call out. Driver may be trapped by steering wheel. Do NOT force them out.', + detail: + 'Tap and call out. Driver may be trapped by steering wheel. Do NOT force them out.', ), RescueStep( stepNumber: 5, title: 'Extraction needs 3+ people', - detail: 'One holds head/neck, two support body. Lower driver down cab steps slowly. Never drop.', + detail: + 'One holds head/neck, two support body. Lower driver down cab steps slowly. Never drop.', isCritical: true, ), RescueStep( stepNumber: 6, title: 'Move victim 50m away', - detail: 'Trucks carry large fuel loads. Move victim far from vehicle in case of fire.', + detail: + 'Trucks carry large fuel loads. Move victim far from vehicle in case of fire.', isCritical: true, ), ], @@ -171,34 +183,40 @@ const Map kVehicleRescueDatabase = { RescueStep( stepNumber: 1, title: 'Move the bike away first', - detail: 'The bike is the fire risk. Push it at least 10m away from the victim before helping.', + detail: + 'The bike is the fire risk. Push it at least 10m away from the victim before helping.', isCritical: true, ), RescueStep( stepNumber: 2, title: 'NEVER remove the helmet', - detail: 'Even if victim asks. Helmet removal can cause fatal spinal damage. Only doctors should remove it.', + detail: + 'Even if victim asks. Helmet removal can cause fatal spinal damage. Only doctors should remove it.', isCritical: true, ), RescueStep( stepNumber: 3, title: 'Check breathing through visor', - detail: 'Open the visor to check breathing. If vomiting, hold helmet steady and gently tilt to side.', + detail: + 'Open the visor to check breathing. If vomiting, hold helmet steady and gently tilt to side.', ), RescueStep( stepNumber: 4, title: 'Check for road rash', - detail: 'Large skin abrasions from skidding. Cover with clean cloth — don\'t clean with water yet.', + detail: + 'Large skin abrasions from skidding. Cover with clean cloth — don\'t clean with water yet.', ), RescueStep( stepNumber: 5, title: 'Keep rider still and flat', - detail: 'Riders are often thrown and land awkwardly. Assume spinal injury. Keep them flat until help arrives.', + detail: + 'Riders are often thrown and land awkwardly. Assume spinal injury. Keep them flat until help arrives.', ), RescueStep( stepNumber: 6, title: 'Keep them warm', - detail: 'Shock causes rapid body cooling. Cover with jacket/blanket. Keep talking to them.', + detail: + 'Shock causes rapid body cooling. Cover with jacket/blanket. Keep talking to them.', ), ], firstAidTips: [ @@ -225,29 +243,34 @@ const Map kVehicleRescueDatabase = { RescueStep( stepNumber: 1, title: 'DO NOT touch orange cables', - detail: 'Orange cables carry high voltage. If you see orange wires exposed — do NOT touch the car at all.', + detail: + 'Orange cables carry high voltage. If you see orange wires exposed — do NOT touch the car at all.', isCritical: true, ), RescueStep( stepNumber: 2, title: 'Turn off the car', - detail: 'If safe, reach in and press power button. Look for emergency cut-off switch (usually near door sill — bright red/orange).', + detail: + 'If safe, reach in and press power button. Look for emergency cut-off switch (usually near door sill — bright red/orange).', ), RescueStep( stepNumber: 3, title: 'Check for battery damage', - detail: 'If battery area (under floor) is visibly damaged or smoking — treat as fire emergency. Move victim 30m away.', + detail: + 'If battery area (under floor) is visibly damaged or smoking — treat as fire emergency. Move victim 30m away.', isCritical: true, ), RescueStep( stepNumber: 4, title: 'Extraction same as regular car', - detail: 'Once confirmed safe (no exposed cables, no smoke), extraction steps are same as regular car. Support neck, slide out.', + detail: + 'Once confirmed safe (no exposed cables, no smoke), extraction steps are same as regular car. Support neck, slide out.', ), RescueStep( stepNumber: 5, title: 'If battery catches fire — RUN', - detail: 'EV battery fires cannot be put out with normal extinguishers. Move everyone 50m away and call fire brigade 101.', + detail: + 'EV battery fires cannot be put out with normal extinguishers. Move everyone 50m away and call fire brigade 101.', isCritical: true, ), ], @@ -274,29 +297,34 @@ const Map kVehicleRescueDatabase = { RescueStep( stepNumber: 1, title: 'Assess from outside first', - detail: 'Count visible victims. Check for fire/smoke. Don\'t rush in — a second casualty helps no one.', + detail: + 'Count visible victims. Check for fire/smoke. Don\'t rush in — a second casualty helps no one.', isCritical: true, ), RescueStep( stepNumber: 2, title: 'Check for CNG cylinders', - detail: 'CNG buses have cylindrical tanks on roof or rear. If hissing sound heard — evacuate everyone 100m away immediately.', + detail: + 'CNG buses have cylindrical tanks on roof or rear. If hissing sound heard — evacuate everyone 100m away immediately.', isCritical: true, ), RescueStep( stepNumber: 3, title: 'Use emergency exits', - detail: 'Red handles at rear door and roof hatch. Push/pull to open. Don\'t wait for front door if jammed.', + detail: + 'Red handles at rear door and roof hatch. Push/pull to open. Don\'t wait for front door if jammed.', ), RescueStep( stepNumber: 4, title: 'Triage victims — most critical first', - detail: 'Walking wounded can help themselves. Focus on unconscious or heavily bleeding victims first.', + detail: + 'Walking wounded can help themselves. Focus on unconscious or heavily bleeding victims first.', ), RescueStep( stepNumber: 5, title: 'Form human chain for extraction', - detail: 'Line up bystanders to pass victims out of windows/exits. One person stabilizes head, others support body.', + detail: + 'Line up bystanders to pass victims out of windows/exits. One person stabilizes head, others support body.', ), ], firstAidTips: [ @@ -322,22 +350,26 @@ const Map kVehicleRescueDatabase = { RescueStep( stepNumber: 1, title: 'Stabilize the auto first', - detail: 'Autos tip over easily. Push gently to check stability before leaning in. Ask bystanders to hold it steady.', + detail: + 'Autos tip over easily. Push gently to check stability before leaning in. Ask bystanders to hold it steady.', ), RescueStep( stepNumber: 2, title: 'Check all three sides', - detail: 'Passengers in autos are often thrown sideways. Check all around the vehicle, not just inside.', + detail: + 'Passengers in autos are often thrown sideways. Check all around the vehicle, not just inside.', ), RescueStep( stepNumber: 3, title: 'Driver extraction', - detail: 'Driver seat is exposed. Support driver\'s head from behind while helper pulls from front.', + detail: + 'Driver seat is exposed. Support driver\'s head from behind while helper pulls from front.', ), RescueStep( stepNumber: 4, title: 'Passenger extraction', - detail: 'Open side means easy access. Support neck, slide passenger out sideways onto flat ground.', + detail: + 'Open side means easy access. Support neck, slide passenger out sideways onto flat ground.', ), ], firstAidTips: [ @@ -356,10 +388,10 @@ VehicleRescueData? getRescueData(String vehicleKey) { // All vehicle types as list for selection UI final List> kVehicleTypes = [ - {'key': 'car', 'label': 'Car / Sedan', 'icon': '🚗'}, - {'key': 'bike', 'label': 'Bike / Scooter', 'icon': '🏍️'}, - {'key': 'truck', 'label': 'Truck / Lorry', 'icon': '🚛'}, - {'key': 'bus', 'label': 'Bus', 'icon': '🚌'}, - {'key': 'ev_car', 'label': 'Electric Car', 'icon': '⚡'}, - {'key': 'auto', 'label': 'Auto Rickshaw', 'icon': '🛺'}, -]; \ No newline at end of file + {'key': 'car', 'label': 'Car / Sedan', 'icon': '🚗'}, + {'key': 'bike', 'label': 'Bike / Scooter', 'icon': '🏍️'}, + {'key': 'truck', 'label': 'Truck / Lorry', 'icon': '🚛'}, + {'key': 'bus', 'label': 'Bus', 'icon': '🚌'}, + {'key': 'ev_car', 'label': 'Electric Car', 'icon': '⚡'}, + {'key': 'auto', 'label': 'Auto Rickshaw', 'icon': '🛺'}, +]; diff --git a/lib/ui/vehicle_rescue_screen.dart b/lib/ui/vehicle_rescue_screen.dart index ab2ea0b..53bb4dc 100644 --- a/lib/ui/vehicle_rescue_screen.dart +++ b/lib/ui/vehicle_rescue_screen.dart @@ -4,7 +4,7 @@ import 'package:url_launcher/url_launcher.dart'; import 'vehicle_rescue_data.dart'; /// VehicleRescueScreen -/// +/// /// Allows a bystander to: /// 1. Select vehicle type (or enter plate number) /// 2. Get instant offline rescue steps specific to that vehicle @@ -67,7 +67,9 @@ class _VehicleRescueScreenState extends State { elevation: 0, leading: IconButton( icon: const Icon(Icons.arrow_back, color: Colors.white), - onPressed: _showingRescueSteps ? _goBack : () => Navigator.pop(context), + onPressed: _showingRescueSteps + ? _goBack + : () => Navigator.pop(context), ), title: Text( _showingRescueSteps ? 'Rescue Guide' : 'Vehicle Rescue', @@ -90,7 +92,14 @@ class _VehicleRescueScreenState extends State { children: [ Icon(Icons.offline_bolt, color: Colors.green, size: 14), SizedBox(width: 4), - Text('OFFLINE', style: TextStyle(color: Colors.green, fontSize: 11, fontWeight: FontWeight.bold)), + Text( + 'OFFLINE', + style: TextStyle( + color: Colors.green, + fontSize: 11, + fontWeight: FontWeight.bold, + ), + ), ], ), ), @@ -120,7 +129,10 @@ class _VehicleRescueScreenState extends State { padding: const EdgeInsets.all(16), decoration: BoxDecoration( gradient: LinearGradient( - colors: [Colors.red.shade900.withValues(alpha: 0.6), Colors.black], + colors: [ + Colors.red.shade900.withValues(alpha: 0.6), + Colors.black, + ], begin: Alignment.topLeft, end: Alignment.bottomRight, ), @@ -194,7 +206,9 @@ class _VehicleRescueScreenState extends State { if (formatted != value) { _plateController.value = TextEditingValue( text: formatted, - selection: TextSelection.collapsed(offset: formatted.length), + selection: TextSelection.collapsed( + offset: formatted.length, + ), ); } }, @@ -300,7 +314,11 @@ class _VehicleRescueScreenState extends State { padding: EdgeInsets.only(top: 4), child: Text( 'HIGH VOLTAGE', - style: TextStyle(color: Colors.yellow, fontSize: 9, letterSpacing: 1), + style: TextStyle( + color: Colors.yellow, + fontSize: 9, + letterSpacing: 1, + ), ), ), ], @@ -383,7 +401,10 @@ class _VehicleRescueScreenState extends State { ), const SizedBox(height: 6), Container( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 3, + ), decoration: BoxDecoration( color: Colors.orange.withValues(alpha: 0.15), borderRadius: BorderRadius.circular(6), @@ -431,7 +452,11 @@ class _VehicleRescueScreenState extends State { padding: const EdgeInsets.only(bottom: 8), child: Text( danger, - style: const TextStyle(color: Colors.white, fontSize: 13, height: 1.4), + style: const TextStyle( + color: Colors.white, + fontSize: 13, + height: 1.4, + ), ), ); }).toList(), @@ -498,14 +523,21 @@ class _VehicleRescueScreenState extends State { ), if (step.isCritical) Container( - padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), + padding: const EdgeInsets.symmetric( + horizontal: 6, + vertical: 2, + ), decoration: BoxDecoration( color: Colors.red.withValues(alpha: 0.2), borderRadius: BorderRadius.circular(4), ), child: const Text( 'CRITICAL', - style: TextStyle(color: Colors.red, fontSize: 9, fontWeight: FontWeight.bold), + style: TextStyle( + color: Colors.red, + fontSize: 9, + fontWeight: FontWeight.bold, + ), ), ), ], @@ -513,7 +545,11 @@ class _VehicleRescueScreenState extends State { const SizedBox(height: 4), Text( step.detail, - style: const TextStyle(color: Colors.white70, fontSize: 13, height: 1.4), + style: const TextStyle( + color: Colors.white70, + fontSize: 13, + height: 1.4, + ), ), ], ), @@ -540,7 +576,11 @@ class _VehicleRescueScreenState extends State { padding: const EdgeInsets.only(bottom: 8), child: Text( tip, - style: const TextStyle(color: Colors.white, fontSize: 13, height: 1.4), + style: const TextStyle( + color: Colors.white, + fontSize: 13, + height: 1.4, + ), ), ); }).toList(), @@ -561,7 +601,9 @@ class _VehicleRescueScreenState extends State { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( - content: Text('Could not launch dialer. Please call 108 manually.'), + content: Text( + 'Could not launch dialer. Please call 108 manually.', + ), backgroundColor: Colors.red, ), ); @@ -571,15 +613,21 @@ class _VehicleRescueScreenState extends State { icon: const Icon(Icons.call, size: 20), label: const Text( 'CALL AMBULANCE — 108', - style: TextStyle(fontWeight: FontWeight.w900, fontSize: 16, letterSpacing: 1), + style: TextStyle( + fontWeight: FontWeight.w900, + fontSize: 16, + letterSpacing: 1, + ), ), style: ElevatedButton.styleFrom( backgroundColor: Colors.red, foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(vertical: 18), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(14), + ), ), ), ); } -} \ No newline at end of file +} diff --git a/lib/ui/vital_scan_screen.dart b/lib/ui/vital_scan_screen.dart index 7c8a923..b6f4d97 100644 --- a/lib/ui/vital_scan_screen.dart +++ b/lib/ui/vital_scan_screen.dart @@ -10,7 +10,8 @@ class VitalScanScreen extends ConsumerStatefulWidget { ConsumerState createState() => _VitalScanScreenState(); } -class _VitalScanScreenState extends ConsumerState with TickerProviderStateMixin { +class _VitalScanScreenState extends ConsumerState + with TickerProviderStateMixin { final _bpmCtrl = TextEditingController(); final _rrCtrl = TextEditingController(); final _spo2Ctrl = TextEditingController(); @@ -36,8 +37,10 @@ class _VitalScanScreenState extends ConsumerState with TickerPr return Scaffold( backgroundColor: Colors.black, appBar: AppBar( - title: Text(AppLocalizations.of(context)!.vitalScanTitle, - style: const TextStyle(fontWeight: FontWeight.w900, fontSize: 14)), + title: Text( + AppLocalizations.of(context)!.vitalScanTitle, + style: const TextStyle(fontWeight: FontWeight.w900, fontSize: 14), + ), backgroundColor: Colors.transparent, ), body: ListView( @@ -52,13 +55,20 @@ class _VitalScanScreenState extends ConsumerState with TickerPr ), child: Text( 'Enter vitals manually. RoadSOS does not measure SpO₂/HR from the camera in this build.', - style: TextStyle(color: Colors.white.withValues(alpha: 0.86), height: 1.35), + style: TextStyle( + color: Colors.white.withValues(alpha: 0.86), + height: 1.35, + ), ), ), const SizedBox(height: 18), _field(label: 'Pulse (BPM)', controller: _bpmCtrl, hint: 'e.g., 92'), const SizedBox(height: 12), - _field(label: 'Respiratory rate (per min)', controller: _rrCtrl, hint: 'e.g., 18'), + _field( + label: 'Respiratory rate (per min)', + controller: _rrCtrl, + hint: 'e.g., 18', + ), const SizedBox(height: 12), _field(label: 'SpO₂ (%)', controller: _spo2Ctrl, hint: 'e.g., 97'), const SizedBox(height: 18), @@ -74,7 +84,9 @@ class _VitalScanScreenState extends ConsumerState with TickerPr style: ElevatedButton.styleFrom( backgroundColor: scheme.primary, foregroundColor: scheme.onPrimary, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(14), + ), ), ), ), @@ -83,11 +95,16 @@ class _VitalScanScreenState extends ConsumerState with TickerPr SizedBox( height: 52, child: OutlinedButton( - onPressed: () => ref.read(vitalSignsProvider.notifier).clear(), + onPressed: () => + ref.read(vitalSignsProvider.notifier).clear(), style: OutlinedButton.styleFrom( foregroundColor: Colors.white70, - side: BorderSide(color: Colors.white.withValues(alpha: 0.2)), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)), + side: BorderSide( + color: Colors.white.withValues(alpha: 0.2), + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(14), + ), ), child: const Text('CLEAR'), ), @@ -104,7 +121,10 @@ class _VitalScanScreenState extends ConsumerState with TickerPr Text( AppLocalizations.of(context)!.vitalAlignFinger, textAlign: TextAlign.center, - style: TextStyle(color: Colors.white.withValues(alpha: 0.3), fontSize: 12), + style: TextStyle( + color: Colors.white.withValues(alpha: 0.3), + fontSize: 12, + ), ), ], ), @@ -147,20 +167,49 @@ class _VitalScanScreenState extends ConsumerState with TickerPr mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ _buildVitalItem('BPM', '${vitals.bpm}', Icons.favorite, Colors.red), - _buildVitalItem('RESP', '${vitals.respiratoryRate}', Icons.air, Colors.blue), - _buildVitalItem('SPO2', '${vitals.bloodOxygen.toStringAsFixed(1)}%', Icons.bloodtype, Colors.orange), + _buildVitalItem( + 'RESP', + '${vitals.respiratoryRate}', + Icons.air, + Colors.blue, + ), + _buildVitalItem( + 'SPO2', + '${vitals.bloodOxygen.toStringAsFixed(1)}%', + Icons.bloodtype, + Colors.orange, + ), ], ), ); } - Widget _buildVitalItem(String label, String value, IconData icon, Color color) { + Widget _buildVitalItem( + String label, + String value, + IconData icon, + Color color, + ) { return Column( children: [ Icon(icon, color: color, size: 28), const SizedBox(height: 8), - Text(value, style: const TextStyle(color: Colors.white, fontSize: 24, fontWeight: FontWeight.bold)), - Text(label, style: TextStyle(color: Colors.white38, fontSize: 10, fontWeight: FontWeight.bold)), + Text( + value, + style: const TextStyle( + color: Colors.white, + fontSize: 24, + fontWeight: FontWeight.bold, + ), + ), + Text( + label, + style: TextStyle( + color: Colors.white38, + fontSize: 10, + fontWeight: FontWeight.bold, + ), + ), ], ); } @@ -180,7 +229,11 @@ class _VitalScanScreenState extends ConsumerState with TickerPr Expanded( child: Text( vitals.interpretation, - style: const TextStyle(color: Colors.white, fontSize: 14, fontWeight: FontWeight.w500), + style: const TextStyle( + color: Colors.white, + fontSize: 14, + fontWeight: FontWeight.w500, + ), ), ), ], @@ -195,12 +248,16 @@ class _VitalScanScreenState extends ConsumerState with TickerPr if (bpm == null || rr == null || spo2 == null) { ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Enter valid numbers for BPM, RESP, and SpO₂.')), + const SnackBar( + content: Text('Enter valid numbers for BPM, RESP, and SpO₂.'), + ), ); return; } - ref.read(vitalSignsProvider.notifier).setManual( + ref + .read(vitalSignsProvider.notifier) + .setManual( bpm: bpm.clamp(20, 240), respiratoryRate: rr.clamp(4, 60), bloodOxygen: spo2.clamp(50.0, 100.0), diff --git a/pubspec.lock b/pubspec.lock index 50ea673..ae0b56f 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1153,10 +1153,10 @@ packages: dependency: transitive description: name: matcher - sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861 + sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6" url: "https://pub.dev" source: hosted - version: "0.12.19" + version: "0.12.18" material_color_utilities: dependency: transitive description: @@ -1774,10 +1774,10 @@ packages: dependency: transitive description: name: test_api - sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a" + sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636" url: "https://pub.dev" source: hosted - version: "0.7.10" + version: "0.7.9" timezone: dependency: transitive description: