Notrufnummern-Datenbasis, Ländererkennung, State-Management und Lokalisierung (AP1-AP7)

Implementiert den kompletten Kern-Flow der Emergency App gemäß docs/plan.md:
- Notrufnummern-Datenbasis für 199 Länder + Modelle/Repository (AP1)
- DialerService für tel:-Notrufe ohne Auto-Dial (AP2)
- Statisches UI mit Panik-Button-Fallback (AP3)
- Priorisierte Ländererkennung: SIM/Netz -> GPS/Geocoding -> Cache (AP4)
- EmergencyProvider verdrahtet Repository/LocationService mit der UI (AP5)
- Manuelle Länderauswahl als Fallback samt eigener Priorität (AP6)
- UI-Lokalisierung (de/en/fr/es), an erkanntes Land gekoppelt (AP7)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
root
2026-07-24 11:29:38 +02:00
co-authored by Claude Sonnet 5
parent 40e95cc495
commit e3aa088bc2
54 changed files with 7550 additions and 145 deletions
+125
View File
@@ -0,0 +1,125 @@
import 'package:flutter/foundation.dart';
import '../models/country.dart';
import '../repositories/emergency_number_repository.dart';
import '../services/location_service.dart';
import 'emergency_status.dart';
/// Orchestriert Repository + Ländererkennung für die UI
/// (`docs/plan.md`, Abschnitt 5 + 6 + 8, AP5).
///
/// Kennt keinen `BuildContext`; die UI (`HomeScreen`) beobachtet [status]/
/// [country]/[errorMessage] über `ChangeNotifier` und ruft [init]/[retry] auf.
///
/// **Entscheidung zu `EmergencyStatus.permissionDenied`:** `LocationService`
/// liefert bewusst nur `String? isoCode` ohne Fehlergrund (AP4-Entscheidung,
/// um die Ermittlungskette einfach zu halten). Eine zusätzliche, direkte
/// Abfrage z. B. von `Geolocator.checkPermission()` hier im Provider würde
/// eine plattformnahe Geolocator-Abhängigkeit in die State-Schicht ziehen,
/// obwohl das reine Wissen um "GPS-Berechtigung verweigert" den eigentlichen
/// Ermittlungs-Flow gar nicht beeinflusst (SIM/Cache können trotzdem einen
/// Treffer liefern → dann ist der Status weiterhin `ready`, nicht
/// `permissionDenied`). Das würde Zuständigkeiten vermischen, ohne den
/// Nutzer:innen im Ladezustand einen Mehrwert zu bieten. Deshalb bleibt
/// `permissionDenied` für AP5 unbenutzt; der Fall "GPS verweigert und keine
/// andere Quelle liefert einen Code" wird korrekt (und ausreichend) über
/// `countryUnknown` abgebildet. Eine gezieltere Unterscheidung kann bei
/// Bedarf in einem späteren AP (Banner/UX-Feinschliff) ergänzt werden.
class EmergencyProvider extends ChangeNotifier {
// Bewusst keine initializing formals (`this._repository`): Die externen
// Konstruktorparameter sollen lesbar `repository`/`locationService`
// heißen, die Felder aber `_repository`/`_locationService` (privat).
EmergencyProvider({required EmergencyNumberRepository repository, required LocationService locationService})
: _repository = repository, // ignore: prefer_initializing_formals
_locationService = locationService; // ignore: prefer_initializing_formals
final EmergencyNumberRepository _repository;
final LocationService _locationService;
EmergencyStatus _status = EmergencyStatus.initializing;
Country? _country;
String? _errorMessage;
bool _disposed = false;
EmergencyStatus get status => _status;
Country? get country => _country;
String? get errorMessage => _errorMessage;
/// Exponiert das Repository für die UI (`docs/plan.md`, Abschnitt 8, AP6):
/// Der `CountryPickerScreen` braucht Zugriff auf die bereits geladene
/// Länderliste (`EmergencyNumberRepository.allCountries`), ohne eine
/// zweite, unabhängige Repository-Instanz zu erzeugen oder das Asset
/// erneut zu laden (`load()` ist idempotent, aber unnötig).
EmergencyNumberRepository get repository => _repository;
@override
void dispose() {
_disposed = true;
super.dispose();
}
/// Wie [notifyListeners], aber ein no-op, wenn der Provider inzwischen
/// disposed wurde kann passieren, wenn [init]/[retry] noch auf eine
/// echte Asset-/Plugin-I/O wartet (z. B. asset load, GPS), während die
/// UI (z. B. `HomeScreen`) längst nicht mehr im Widgetbaum ist.
void _notifyIfActive() {
if (!_disposed) notifyListeners();
}
/// Startet die Ermittlungskette: Datenbasis laden → Ländercode ermitteln
/// → Land nachschlagen. Wirft nie; Fehler landen im `error`-Status.
Future<void> init() async {
_status = EmergencyStatus.initializing;
_country = null;
_errorMessage = null;
_notifyIfActive();
try {
await _repository.load();
} catch (e) {
_status = EmergencyStatus.error;
_errorMessage = e.toString();
_notifyIfActive();
return;
}
_status = EmergencyStatus.locating;
_notifyIfActive();
final isoCode = await _locationService.resolveCountryCode();
if (isoCode == null) {
_status = EmergencyStatus.countryUnknown;
_country = null;
_notifyIfActive();
return;
}
// country bleibt `null`, wenn der ermittelte isoCode nicht in der
// Datenbasis vorhanden ist (Plan Abschnitt 7) Status ist dennoch
// `ready`, die UI fällt dann auf den globalen Panik-Button zurück.
_country = _repository.lookup(isoCode);
_status = EmergencyStatus.ready;
_notifyIfActive();
}
/// Wiederholt die komplette Ermittlungskette (kein separater Zustand nötig).
Future<void> retry() => init();
/// Setzt die manuelle Länderauswahl des Nutzers (`docs/plan.md`,
/// Abschnitt 8, AP6: `CountryPickerScreen`) und stößt die
/// Ermittlungskette danach sofort erneut an, damit [isoCode] unmittelbar
/// wirksam wird (die manuelle Auswahl hat in [LocationService] höchste
/// Priorität vor SIM/GPS/Cache).
Future<void> selectCountryManually(String isoCode) async {
await _locationService.setManualCountryCode(isoCode);
await init();
}
/// Löscht die manuelle Länderauswahl wieder und ermittelt das Land danach
/// erneut automatisch (SIM → GPS → Cache) erlaubt dem Nutzer, zur
/// automatischen Erkennung zurückzukehren (`docs/plan.md`, Abschnitt 8, AP6).
Future<void> clearManualSelection() async {
await _locationService.setManualCountryCode(null);
await init();
}
}