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,35 @@
import 'package:flutter/material.dart';
import '../../../models/country.dart';
/// Kompakter Header, der Flagge und Name eines [Country] anzeigt.
class CountryHeader extends StatelessWidget {
const CountryHeader({super.key, required this.country});
final Country country;
/// Wählt `nameDe`/`nameEn` (AP1-Datenfeld, keine ARB-Übersetzung die
/// Notrufnummern-Datenbasis kennt bewusst nur diese zwei Sprachen, siehe
/// `docs/plan.md` Abschnitt 4) passend zur aktuellen App-Locale aus.
String _localizedName(BuildContext context) {
final languageCode = Localizations.localeOf(context).languageCode;
return languageCode == 'de' ? country.nameDe : country.nameEn;
}
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 12),
child: Row(
children: [
Text(country.flag, style: const TextStyle(fontSize: 32)),
const SizedBox(width: 12),
Text(
_localizedName(context),
style: Theme.of(context).textTheme.titleLarge,
),
],
),
);
}
}
@@ -0,0 +1,110 @@
import 'package:flutter/material.dart';
import '../../../l10n/app_localizations.dart';
import '../../../models/emergency_number.dart';
import '../../../models/emergency_type.dart';
import '../../../services/dialer_service.dart';
/// Großer, gut antippbarer Button für eine einzelne [EmergencyNumber].
///
/// Löst den Anruf direkt über [DialerService] aus dafür ist kein
/// Callback nach oben nötig, da der Button keine weitere UI-Logik
/// beeinflusst. Kann der Wähler nicht geöffnet werden, zeigt der Button
/// die Nummer als einfachen Fallback in einer [SnackBar] an.
class EmergencyButton extends StatelessWidget {
const EmergencyButton({
super.key,
required this.emergencyNumber,
this.dialerService = const DialerService(),
});
final EmergencyNumber emergencyNumber;
final DialerService dialerService;
IconData get _icon {
switch (emergencyNumber.type) {
case EmergencyType.general:
return Icons.emergency;
case EmergencyType.police:
return Icons.local_police;
case EmergencyType.fire:
return Icons.local_fire_department;
case EmergencyType.ambulance:
return Icons.local_hospital;
case EmergencyType.seaRescue:
return Icons.directions_boat;
case EmergencyType.mountainRescue:
return Icons.terrain;
}
}
Future<void> _onTap(BuildContext context) async {
final success = await dialerService.call(emergencyNumber.number);
if (!success && context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(emergencyNumber.number)),
);
}
}
/// Wählt `labelDe`/`labelEn` (AP1-Datenfeld, keine ARB-Übersetzung die
/// Notrufnummern-Datenbasis kennt bewusst nur diese zwei Sprachen, siehe
/// `docs/plan.md` Abschnitt 4) passend zur aktuellen App-Locale aus.
String _localizedNumberLabel(BuildContext context) {
final languageCode = Localizations.localeOf(context).languageCode;
return languageCode == 'de' ? emergencyNumber.labelDe : emergencyNumber.labelEn;
}
@override
Widget build(BuildContext context) {
final label = _localizedNumberLabel(context);
final semanticLabel = AppLocalizations.of(
context,
)!.emergencyButtonSemanticLabel(label, emergencyNumber.number);
return Semantics(
button: true,
label: semanticLabel,
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: SizedBox(
width: double.infinity,
height: 96,
child: ElevatedButton(
onPressed: () => _onTap(context),
style: ElevatedButton.styleFrom(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
),
child: Tooltip(
message: semanticLabel,
child: Row(
children: [
Icon(_icon, size: 40),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
label,
style: Theme.of(context).textTheme.titleMedium,
),
Text(
emergencyNumber.number,
style: Theme.of(context).textTheme.headlineMedium,
),
],
),
),
],
),
),
),
),
),
);
}
}