222 lines
7.7 KiB
Dart
222 lines
7.7 KiB
Dart
import 'dart:async';
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
|
|
import '../../../../push/push_registration.dart';
|
|
import '../../../../push/push_status.dart';
|
|
import '../../../../routing/app_routes.dart';
|
|
import '../../../../state/app/modules/capabilities/bloc/capabilities_cubit.dart';
|
|
import '../../../../state/app/modules/settings/bloc/settings_cubit.dart';
|
|
import '../../../../widget/centered_leading.dart';
|
|
import '../widgets/push_status_sheet.dart';
|
|
import '../widgets/settings_checkbox_tile.dart';
|
|
|
|
class TalkSection extends StatelessWidget {
|
|
const TalkSection({super.key});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final settings = context.watch<SettingsCubit>();
|
|
final talkSettings = settings.val().talkSettings;
|
|
return Column(
|
|
children: [
|
|
SettingsCheckboxTile(
|
|
icon: Icons.star_border,
|
|
title: 'Favoriten im Talk nach oben sortieren',
|
|
value: talkSettings.sortFavoritesToTop,
|
|
onChanged: (e) =>
|
|
settings.val(write: true).talkSettings.sortFavoritesToTop = e,
|
|
),
|
|
SettingsCheckboxTile(
|
|
icon: Icons.mark_email_unread_outlined,
|
|
title: 'Ungelesene Chats nach oben sortieren',
|
|
value: talkSettings.sortUnreadToTop,
|
|
onChanged: (e) =>
|
|
settings.val(write: true).talkSettings.sortUnreadToTop = e,
|
|
),
|
|
ListTile(
|
|
leading: const Icon(Icons.wallpaper_outlined),
|
|
title: const Text('Chat-Hintergrund'),
|
|
subtitle: const Text('Bild, Farbe und Darstellung anpassen'),
|
|
trailing: const Icon(Icons.arrow_right),
|
|
onTap: () => AppRoutes.openChatBackgroundSettings(context),
|
|
),
|
|
_PushSettings(
|
|
settings: settings,
|
|
capabilities: context.read<CapabilitiesCubit>(),
|
|
enabled: settings.val().notificationSettings.enabled,
|
|
devMode: settings.val().devToolsEnabled,
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
/// The push area: the enable switch carries an at-a-glance health icon (green
|
|
/// check / red X) right before the checkbox, and the detailed status checklist
|
|
/// is hidden — it only surfaces when the chain is broken or the developer mode
|
|
/// is on. Owns the [PushStatusReport] loading so the inline icon and the detail
|
|
/// entry share one source of truth.
|
|
class _PushSettings extends StatefulWidget {
|
|
final SettingsCubit settings;
|
|
final CapabilitiesCubit capabilities;
|
|
final bool enabled;
|
|
final bool devMode;
|
|
|
|
const _PushSettings({
|
|
required this.settings,
|
|
required this.capabilities,
|
|
required this.enabled,
|
|
required this.devMode,
|
|
});
|
|
|
|
@override
|
|
State<_PushSettings> createState() => _PushSettingsState();
|
|
}
|
|
|
|
class _PushSettingsState extends State<_PushSettings>
|
|
with WidgetsBindingObserver {
|
|
PushStatusReport? _report;
|
|
|
|
/// True while a (de)registration triggered by the switch is in flight. The
|
|
/// report collected in that window still reflects the pre-registration state,
|
|
/// so the status is shown as "loading" instead of briefly flashing red.
|
|
bool _busy = false;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
WidgetsBinding.instance.addObserver(this);
|
|
unawaited(_load());
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
WidgetsBinding.instance.removeObserver(this);
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
void didUpdateWidget(covariant _PushSettings oldWidget) {
|
|
super.didUpdateWidget(oldWidget);
|
|
// Toggling the setting changes several links at once — re-collect.
|
|
if (oldWidget.enabled != widget.enabled) unawaited(_load());
|
|
}
|
|
|
|
@override
|
|
void didChangeAppLifecycleState(AppLifecycleState state) {
|
|
// The OS permission can change while the app is backgrounded.
|
|
if (state == AppLifecycleState.resumed) unawaited(_load());
|
|
}
|
|
|
|
Future<void> _load() async {
|
|
final caps = widget.capabilities.state;
|
|
final report = await collectPushStatus(
|
|
settingEnabled: widget.settings.val().notificationSettings.enabled,
|
|
capabilityPush: caps.pushNotifications,
|
|
capabilitiesLoaded: caps.loaded,
|
|
);
|
|
if (!mounted) return;
|
|
setState(() => _report = report);
|
|
}
|
|
|
|
void _onToggle(bool enabled) {
|
|
widget.settings.val(write: true).notificationSettings.enabled = enabled;
|
|
// Turning off does NOT unregister: the device stays subscribed so silent
|
|
// sync pushes keep arriving; the message handler and iOS NSE suppress only
|
|
// the visible notification (via the mirrored flag). Enabling (re-)registers
|
|
// and ensures the OS permission.
|
|
if (!enabled) return;
|
|
setState(() => _busy = true);
|
|
final messenger = ScaffoldMessenger.of(context);
|
|
unawaited(() async {
|
|
try {
|
|
// Only register when the OS permission isn't explicitly denied —
|
|
// otherwise NC + proxy would push into the void.
|
|
if (await PushRegistration.requestOsPermission()) {
|
|
await PushRegistration().register();
|
|
} else {
|
|
messenger.showSnackBar(
|
|
const SnackBar(
|
|
content: Text(
|
|
'Die Benachrichtigungsberechtigung wurde in den '
|
|
'Systemeinstellungen deaktiviert. Bitte aktiviere sie dort, um '
|
|
'Push-Benachrichtigungen zu erhalten.',
|
|
),
|
|
),
|
|
);
|
|
}
|
|
} finally {
|
|
if (mounted) await _load();
|
|
if (mounted) setState(() => _busy = false);
|
|
}
|
|
}());
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final report = _report;
|
|
final broken =
|
|
widget.enabled && !_busy && report != null && !report.chainHealthy;
|
|
return Column(
|
|
children: [
|
|
SettingsCheckboxTile(
|
|
icon: Icons.notifications_active_outlined,
|
|
title: 'Push-Benachrichtigungen',
|
|
subtitle: 'Benachrichtigungen bei neuen Talk-Nachrichten erhalten',
|
|
value: widget.enabled,
|
|
beforeCheckbox: _inlineStatusIcon(report),
|
|
onChanged: _onToggle,
|
|
),
|
|
// Detail entry only when there is a problem to fix or for developers.
|
|
if (broken || widget.devMode) _detailTile(error: broken),
|
|
],
|
|
);
|
|
}
|
|
|
|
/// Health icon shown before the checkbox — a spinner while a registration is
|
|
/// in flight, otherwise the green/red verdict once the report has loaded.
|
|
Widget? _inlineStatusIcon(PushStatusReport? report) {
|
|
if (_busy) {
|
|
return const SizedBox(
|
|
width: 18,
|
|
height: 18,
|
|
child: CircularProgressIndicator(strokeWidth: 2),
|
|
);
|
|
}
|
|
if (!widget.enabled || report == null) return null;
|
|
final healthy = report.chainHealthy;
|
|
return Icon(
|
|
healthy ? Icons.check_circle : Icons.cancel,
|
|
color: healthy ? Colors.green : Theme.of(context).colorScheme.error,
|
|
semanticLabel: healthy ? 'Push in Ordnung' : 'Push funktioniert nicht',
|
|
);
|
|
}
|
|
|
|
/// The full status checklist entry — same list-tile footprint whether broken
|
|
/// or not; a problem is signalled only through the error-colored icon/text.
|
|
Widget _detailTile({required bool error}) {
|
|
final color = error ? Theme.of(context).colorScheme.error : null;
|
|
final textStyle = color == null ? null : TextStyle(color: color);
|
|
return ListTile(
|
|
leading: CenteredLeading(
|
|
Icon(Icons.monitor_heart_outlined, color: color),
|
|
),
|
|
title: Text('Push-Status', style: textStyle),
|
|
subtitle: Text(
|
|
error
|
|
? 'Ein Schritt in der Zustellkette ist unterbrochen'
|
|
: 'Registrierung und Zustellung im Detail',
|
|
style: textStyle,
|
|
),
|
|
trailing: Icon(Icons.arrow_right, color: color),
|
|
// The sheet can re-register; re-collect on close so the dot reflects it.
|
|
onTap: () async {
|
|
await showPushStatusSheet(context);
|
|
if (mounted) await _load();
|
|
},
|
|
);
|
|
}
|
|
}
|