146 lines
4.2 KiB
Dart
146 lines
4.2 KiB
Dart
import 'dart:async';
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
import 'package:flutter_markdown_plus/flutter_markdown_plus.dart';
|
|
import 'package:url_launcher/url_launcher_string.dart';
|
|
|
|
import '../../api/emergency/emergency_notice.dart';
|
|
import '../../api/emergency/emergency_notice_client.dart';
|
|
import '../../state/app/modules/settings/bloc/settings_cubit.dart';
|
|
|
|
/// Wraps the app and surfaces a backend-independent emergency notice on cold
|
|
/// start and on resume. Transparent otherwise: renders [child] unchanged and
|
|
/// only overlays a dialog when the foreign source has an active message.
|
|
///
|
|
/// Independent of MarianumConnect and of the login state by design — this is
|
|
/// the disaster fallback for when the backend is gone. Any failure to load or
|
|
/// parse is swallowed by [EmergencyNoticeClient], so a missing/broken source
|
|
/// simply shows nothing.
|
|
class EmergencyNoticeGate extends StatefulWidget {
|
|
final Widget child;
|
|
|
|
const EmergencyNoticeGate({required this.child, super.key});
|
|
|
|
@override
|
|
State<EmergencyNoticeGate> createState() => _EmergencyNoticeGateState();
|
|
}
|
|
|
|
class _EmergencyNoticeGateState extends State<EmergencyNoticeGate>
|
|
with WidgetsBindingObserver {
|
|
final EmergencyNoticeClient _client = const EmergencyNoticeClient();
|
|
bool _showing = false;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
WidgetsBinding.instance.addObserver(this);
|
|
WidgetsBinding.instance.addPostFrameCallback((_) => unawaited(_check()));
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
WidgetsBinding.instance.removeObserver(this);
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
void didChangeAppLifecycleState(AppLifecycleState state) {
|
|
if (state == AppLifecycleState.resumed) unawaited(_check());
|
|
}
|
|
|
|
Future<void> _check() async {
|
|
if (_showing || !mounted) return;
|
|
final url = context
|
|
.read<SettingsCubit>()
|
|
.val()
|
|
.devToolsSettings
|
|
.resolveEmergencyNoticeUrl();
|
|
if (url == null) return;
|
|
|
|
final notice = await _client.fetch(url);
|
|
if (notice == null || !mounted || _showing) return;
|
|
|
|
_showing = true;
|
|
try {
|
|
await showDialog<void>(
|
|
context: context,
|
|
barrierDismissible: notice.dismissible,
|
|
builder: (_) => _EmergencyNoticeDialog(notice: notice),
|
|
);
|
|
} finally {
|
|
_showing = false;
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) => widget.child;
|
|
}
|
|
|
|
class _EmergencyNoticeDialog extends StatelessWidget {
|
|
final EmergencyNotice notice;
|
|
|
|
const _EmergencyNoticeDialog({required this.notice});
|
|
|
|
Future<void> _openLink(String? href) async {
|
|
if (href == null) return;
|
|
if (await canLaunchUrlString(href)) {
|
|
await launchUrlString(href);
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final theme = Theme.of(context);
|
|
final body = MarkdownBody(
|
|
data: notice.body,
|
|
onTapLink: (_, href, _) => unawaited(_openLink(href)),
|
|
);
|
|
|
|
if (notice.dismissible) {
|
|
return AlertDialog(
|
|
title: notice.title == null ? null : Text(notice.title!),
|
|
content: SingleChildScrollView(child: body),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.of(context).pop(),
|
|
child: const Text('Schließen'),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
// Full-screen, non-dismissible: no close affordance, back is blocked.
|
|
return PopScope(
|
|
canPop: false,
|
|
child: Dialog.fullscreen(
|
|
child: SafeArea(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(24),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
if (notice.title != null) ...[
|
|
Text(
|
|
notice.title!,
|
|
style: theme.textTheme.headlineSmall?.copyWith(
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
),
|
|
const SizedBox(height: 16),
|
|
],
|
|
Expanded(
|
|
child: SingleChildScrollView(
|
|
physics: const AlwaysScrollableScrollPhysics(),
|
|
child: body,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|