Files
emergency/lib/features/home/home_screen.dart
T
rootandClaude Sonnet 5 e3aa088bc2 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>
2026-07-24 11:29:38 +02:00

232 lines
7.7 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../../l10n/app_localizations.dart';
import '../../models/country.dart';
import '../../models/emergency_number.dart';
import '../../models/emergency_type.dart';
import '../../providers/emergency_provider.dart';
import '../../providers/emergency_status.dart';
import 'country_picker_screen.dart';
import 'widgets/country_header.dart';
import 'widgets/emergency_button.dart';
/// Startbildschirm mit Länder-Header und Notruf-Buttons.
///
/// AP5 (State-Integration): Land und Ladezustand kommen jetzt aus dem
/// [EmergencyProvider] (SIM/GPS/Cache-Ermittlungskette aus AP4), nicht mehr
/// aus einem hartkodierten ISO-Code (AP3).
class HomeScreen extends StatefulWidget {
const HomeScreen({super.key});
@override
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
@override
void initState() {
super.initState();
// Einmaliger Start der Ermittlungskette nach dem ersten Frame: init()
// ruft synchron (vor dem ersten await) notifyListeners() auf, was
// während der laufenden Baumkonstruktion (initState) zu "setState()
// called during build" führen würde daher erst im Post-Frame-Callback.
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) context.read<EmergencyProvider>().init();
});
}
/// Globaler Panikmodus-Button (Plan Abschnitt 2 + 6): 112 funktioniert als
/// GSM-weiter Fallback in den meisten Mobilfunknetzen und muss unabhängig
/// vom Lade-/Fehlerzustand der Ländererkennung immer erreichbar sein.
static const _panicNumber = EmergencyNumber(
type: EmergencyType.general,
number: '112',
labelDe: 'Internationaler Notruf',
labelEn: 'International Emergency',
);
/// True, wenn das erkannte Land bereits eine `general`-Nummer "112" führt
/// (z. B. DE) dann würde der Panik-Button dieselbe Nummer doppelt zeigen.
bool _countryAlreadyHasPanicNumber(Country country) {
return country.numbers.any(
(n) => n.type == EmergencyType.general && n.number == _panicNumber.number,
);
}
bool _showPanicButton(EmergencyProvider provider) {
final country = provider.country;
if (provider.status == EmergencyStatus.ready && country != null) {
return !_countryAlreadyHasPanicNumber(country);
}
// Ladezustand, Fehler, countryUnknown oder Land nicht in Datenbasis:
// Panik-Button ist die einzig sichere Konstante, also immer sichtbar.
return true;
}
/// Öffnet den `CountryPickerScreen` (Plan Abschnitt 7 + 8, AP6: manuelle
/// Länderauswahl als Fallback, wenn das Land nicht automatisch ermittelbar
/// ist bzw. jederzeit als bewusste Nutzerentscheidung).
void _openCountryPicker(BuildContext context) {
Navigator.of(context).push(MaterialPageRoute(builder: (_) => const CountryPickerScreen()));
}
Widget _buildContent(BuildContext context, EmergencyProvider provider) {
final l10n = AppLocalizations.of(context)!;
switch (provider.status) {
case EmergencyStatus.initializing:
case EmergencyStatus.locating:
return const Center(child: CircularProgressIndicator());
case EmergencyStatus.error:
return _StatusMessage(
message: provider.errorMessage ?? l10n.errorGeneric,
retryLabel: l10n.retryButton,
selectCountryLabel: l10n.selectCountryButton,
onRetry: () => context.read<EmergencyProvider>().retry(),
onSelectCountry: () => _openCountryPicker(context),
);
// permissionDenied wird bewusst wie countryUnknown behandelt, siehe
// Doc-Kommentar in EmergencyProvider.
case EmergencyStatus.permissionDenied:
case EmergencyStatus.countryUnknown:
return _StatusMessage(
message: l10n.countryUnknownMessage,
retryLabel: l10n.retryButton,
selectCountryLabel: l10n.selectCountryButton,
onRetry: () => context.read<EmergencyProvider>().retry(),
onSelectCountry: () => _openCountryPicker(context),
);
case EmergencyStatus.ready:
final country = provider.country;
if (country == null) {
return _StatusMessage(message: l10n.countryNotInDatabaseMessage);
}
return _CountryContent(country: country);
}
}
@override
Widget build(BuildContext context) {
final provider = context.watch<EmergencyProvider>();
final l10n = AppLocalizations.of(context)!;
return Scaffold(
appBar: AppBar(
title: Text(l10n.appTitle),
actions: [
IconButton(
key: const Key('countryPickerButton'),
icon: const Icon(Icons.public),
tooltip: l10n.selectCountryButton,
onPressed: () => _openCountryPicker(context),
),
],
),
body: SafeArea(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
if (_showPanicButton(provider))
EmergencyButton(
key: const Key('panicButton'),
emergencyNumber: _panicNumber,
),
Expanded(child: _buildContent(context, provider)),
],
),
),
),
);
}
}
/// Kurzer Hinweistext für Fehler-/Unknown-/Fallback-Zustände, optional mit
/// "Erneut versuchen"-Button.
class _StatusMessage extends StatelessWidget {
const _StatusMessage({
required this.message,
this.retryLabel,
this.selectCountryLabel,
this.onRetry,
this.onSelectCountry,
});
final String message;
final String? retryLabel;
final String? selectCountryLabel;
final VoidCallback? onRetry;
/// Öffnet den `CountryPickerScreen`, damit der Nutzer sein Land manuell
/// auswählen kann, wenn es nicht automatisch ermittelbar war (Plan
/// Abschnitt 7, "Land nicht ermittelbar → Panik-Button + Länder-Suchfeld").
final VoidCallback? onSelectCountry;
@override
Widget build(BuildContext context) {
return Center(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(message, textAlign: TextAlign.center),
if (onRetry != null) ...[
const SizedBox(height: 12),
ElevatedButton(onPressed: onRetry, child: Text(retryLabel!)),
],
if (onSelectCountry != null) ...[
const SizedBox(height: 12),
TextButton.icon(
onPressed: onSelectCountry,
icon: const Icon(Icons.public),
label: Text(selectCountryLabel!),
),
],
],
),
),
);
}
}
/// Länder-Header + sortierte Liste der landesspezifischen Notruf-Buttons.
class _CountryContent extends StatelessWidget {
const _CountryContent({required this.country});
final Country country;
@override
Widget build(BuildContext context) {
// general zuoberst, danach der Rest in JSON-Reihenfolge.
final numbers = [
...country.numbers.where((n) => n.type == EmergencyType.general),
...country.numbers.where((n) => n.type != EmergencyType.general),
];
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
CountryHeader(country: country),
Expanded(
child: ListView.builder(
itemCount: numbers.length,
itemBuilder: (context, index) {
final number = numbers[index];
return EmergencyButton(
key: ValueKey(number.type),
emergencyNumber: number,
);
},
),
),
],
);
}
}