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
@@ -0,0 +1,73 @@
import 'dart:convert';
import 'package:flutter/services.dart' show rootBundle;
import '../models/country.dart';
/// Lädt und cacht die statische Notrufnummern-Datenbasis aus
/// `assets/data/emergency_numbers.json`.
///
/// Die Datei wird nur beim ersten [load]-Aufruf eingelesen und geparst;
/// danach liefert [lookup] aus dem In-Memory-Cache.
class EmergencyNumberRepository {
EmergencyNumberRepository({this.assetPath = 'assets/data/emergency_numbers.json'});
final String assetPath;
Map<String, Country>? _countriesByIsoCode;
/// True, sobald [load] erfolgreich durchgelaufen ist.
bool get isLoaded => _countriesByIsoCode != null;
/// Liest und parst die JSON-Datei einmalig ein.
///
/// Wird [load] mehrfach aufgerufen, geschieht nach dem ersten
/// erfolgreichen Durchlauf nichts mehr (Cache bleibt bestehen).
///
/// Wirft eine [FormatException], wenn das JSON fehlerhaft ist das ist
/// ein Programmierfehler (kaputtes Asset) und soll beim Start auffallen,
/// nicht still verschluckt werden.
Future<void> load() async {
if (isLoaded) return;
final raw = await rootBundle.loadString(assetPath);
final decoded = jsonDecode(raw);
if (decoded is! Map<String, dynamic>) {
throw const FormatException(
'emergency_numbers.json: erwartetes Top-Level-Objekt fehlt',
);
}
final parsed = <String, Country>{};
for (final entry in decoded.entries) {
final isoCode = entry.key.toUpperCase();
parsed[isoCode] = Country.fromJson(isoCode, entry.value as Map<String, dynamic>);
}
_countriesByIsoCode = parsed;
}
/// Sucht ein Land anhand seines ISO 3166-1 alpha-2 Codes (case-insensitive).
///
/// Gibt `null` zurück, wenn [load] noch nicht aufgerufen wurde oder das
/// Land nicht in der Datenbasis vorhanden ist bewusst keine Exception,
/// damit Aufrufer den Fallback-Pfad (z. B. globaler 112/911-Button) ohne
/// try/catch abbilden können.
Country? lookup(String isoCode) {
final countries = _countriesByIsoCode;
if (countries == null) return null;
return countries[isoCode.toUpperCase()];
}
/// Alle Länder der Datenbasis, alphabetisch nach [Country.nameDe] sortiert
/// (`docs/plan.md`, Abschnitt 8, AP6: Grundlage für den `CountryPickerScreen`).
///
/// Liefert eine leere Liste, wenn [load] noch nicht (erfolgreich)
/// durchgelaufen ist bewusst kein Fehler, analog zu [lookup].
List<Country> get allCountries {
final countries = _countriesByIsoCode;
if (countries == null) return const [];
final sorted = countries.values.toList()..sort((a, b) => a.nameDe.compareTo(b.nameDe));
return sorted;
}
}