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>
232 lines
7.7 KiB
Dart
232 lines
7.7 KiB
Dart
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,
|
||
);
|
||
},
|
||
),
|
||
),
|
||
],
|
||
);
|
||
}
|
||
}
|