added guardian login with views for their assigned childs
This commit is contained in:
@@ -4,15 +4,19 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import '../../../../api/marianumcloud/cloud_users/cloud_users_actions.dart';
|
||||
import '../../../../api/marianumconnect/queries/auth_logout/auth_logout.dart';
|
||||
import '../../../../model/account_data.dart';
|
||||
import '../../../../push/push_registration.dart';
|
||||
import '../../../../routing/app_routes.dart';
|
||||
import '../../../../session/session.dart';
|
||||
import '../../../../session/session_lifecycle.dart';
|
||||
import '../../../../session/session_manager.dart';
|
||||
import '../../../../state/app/modules/account/bloc/account_bloc.dart';
|
||||
import '../../../../state/app/modules/account/bloc/account_state.dart';
|
||||
import '../../../../state/app/modules/capabilities/bloc/capabilities_cubit.dart';
|
||||
import '../../../../widget/app_progress_indicator.dart';
|
||||
import '../../../../widget/async_action_button.dart';
|
||||
import '../../../../widget/avatar_actions_sheet.dart';
|
||||
import '../../../../widget/centered_leading.dart';
|
||||
import '../../../../widget/child_switcher.dart';
|
||||
import '../../../../widget/confirm_dialog.dart';
|
||||
import '../../../../widget/demo_restricted.dart';
|
||||
import '../../../../widget/user_avatar.dart';
|
||||
@@ -21,14 +25,53 @@ import '../../../../widget/user_avatar.dart';
|
||||
// every Settings rebuild doesn't re-issue the OCS request.
|
||||
String? _cachedDisplayName;
|
||||
|
||||
class AccountSection extends StatefulWidget {
|
||||
class AccountSection extends StatelessWidget {
|
||||
const AccountSection({super.key});
|
||||
|
||||
@override
|
||||
State<AccountSection> createState() => _AccountSectionState();
|
||||
Widget build(BuildContext context) => switch (SessionManager().current) {
|
||||
GuardianSession(:final email) => _GuardianAccount(email: email),
|
||||
_ => const _SchoolAccount(),
|
||||
};
|
||||
}
|
||||
|
||||
class _AccountSectionState extends State<AccountSection> {
|
||||
/// Guardians have no Nextcloud profile: show the e-mail and linked children.
|
||||
class _GuardianAccount extends StatelessWidget {
|
||||
final String email;
|
||||
|
||||
const _GuardianAccount({required this.email});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final children = context.watch<CapabilitiesCubit>().state.children;
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
ListTile(
|
||||
contentPadding: const EdgeInsets.fromLTRB(16, 12, 16, 4),
|
||||
leading: const CenteredLeading(Icon(Icons.family_restroom_outlined)),
|
||||
title: const Text('Elternkonto'),
|
||||
subtitle: Text(email),
|
||||
trailing: TextButton.icon(
|
||||
icon: const Icon(Icons.logout_outlined, size: 18),
|
||||
label: const Text('Abmelden'),
|
||||
onPressed: () => _confirmLogout(context),
|
||||
),
|
||||
),
|
||||
for (final child in children) ChildTile(child: child),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SchoolAccount extends StatefulWidget {
|
||||
const _SchoolAccount();
|
||||
|
||||
@override
|
||||
State<_SchoolAccount> createState() => _SchoolAccountState();
|
||||
}
|
||||
|
||||
class _SchoolAccountState extends State<_SchoolAccount> {
|
||||
int _avatarVersion = 0;
|
||||
bool _avatarBusy = false;
|
||||
String? _displayName = _cachedDisplayName;
|
||||
@@ -42,9 +85,7 @@ class _AccountSectionState extends State<AccountSection> {
|
||||
Future<void> _loadDisplayName() async {
|
||||
try {
|
||||
final info = await GetUserInfo().run();
|
||||
_cachedDisplayName = info.displayName.isEmpty
|
||||
? null
|
||||
: info.displayName;
|
||||
_cachedDisplayName = info.displayName.isEmpty ? null : info.displayName;
|
||||
if (!mounted) return;
|
||||
setState(() => _displayName = _cachedDisplayName);
|
||||
} catch (_) {
|
||||
@@ -84,13 +125,17 @@ class _AccountSectionState extends State<AccountSection> {
|
||||
setState(() => _avatarBusy = false);
|
||||
if (!ok) return;
|
||||
|
||||
invalidateAvatarCache(id: AccountData().getUsername(), isGroup: false);
|
||||
invalidateAvatarCache(
|
||||
id: SessionManager().requireNextcloud().username,
|
||||
isGroup: false,
|
||||
);
|
||||
setState(() => _avatarVersion++);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final username = AccountData().getUsername();
|
||||
final nextcloud = SessionManager().requireNextcloud();
|
||||
final username = nextcloud.username;
|
||||
final displayName = _displayName;
|
||||
final theme = Theme.of(context);
|
||||
|
||||
@@ -109,8 +154,10 @@ class _AccountSectionState extends State<AccountSection> {
|
||||
children: [
|
||||
Center(
|
||||
child: GestureDetector(
|
||||
onTap: () =>
|
||||
AppRoutes.openLargeProfilePicture(context, username),
|
||||
onTap: () => AppRoutes.openLargeProfilePicture(
|
||||
context,
|
||||
username,
|
||||
),
|
||||
child: UserAvatar(
|
||||
key: ValueKey(_avatarVersion),
|
||||
id: username,
|
||||
@@ -164,7 +211,7 @@ class _AccountSectionState extends State<AccountSection> {
|
||||
TextButton.icon(
|
||||
icon: const Icon(Icons.logout_outlined, size: 18),
|
||||
label: const Text('Abmelden'),
|
||||
onPressed: () => _showLogoutDialog(context),
|
||||
onPressed: () => _confirmLogout(context),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -172,13 +219,11 @@ class _AccountSectionState extends State<AccountSection> {
|
||||
// Nur für Login-Flow-Konten (2FA) sichtbar — Passwort-Konten heilen
|
||||
// sich still über das App-Passwort-Minting und sollen von dem ganzen
|
||||
// Flow-Mechanismus nichts mitbekommen.
|
||||
if (!AccountData().isDemo && AccountData().usesLoginFlow)
|
||||
if (!SessionManager().isDemo && nextcloud.usesLoginFlow)
|
||||
AsyncListTile(
|
||||
leading: const Icon(Icons.cloud_sync_outlined),
|
||||
title: const Text('Nextcloud neu verbinden'),
|
||||
subtitle: const Text(
|
||||
'Bei Anmeldeproblemen in Talk oder Dateien',
|
||||
),
|
||||
subtitle: const Text('Bei Anmeldeproblemen in Talk oder Dateien'),
|
||||
closeOnSuccess: false,
|
||||
onPressed: _reconnectNextcloud,
|
||||
),
|
||||
@@ -197,35 +242,29 @@ class _AccountSectionState extends State<AccountSection> {
|
||||
const SnackBar(content: Text('Nextcloud-Verbindung erneuert.')),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _showLogoutDialog(BuildContext context) async {
|
||||
// Flip AccountBloc state only after the dialog fully closes: doing it from
|
||||
// inside removeData (the previous approach) raced AsyncDialogAction's
|
||||
// pop(true) against the listener's popUntil(isFirst) and could leave the
|
||||
// navigator in an inconsistent state.
|
||||
final confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (dialogContext) => ConfirmDialog(
|
||||
title: 'Abmelden?',
|
||||
content: 'Möchtest du dich wirklich abmelden?',
|
||||
confirmButton: 'Abmelden',
|
||||
onConfirmAsync: _performLogout,
|
||||
),
|
||||
);
|
||||
if (confirmed != true || !context.mounted) return;
|
||||
context.read<AccountBloc>().setStatus(AccountStatus.loggedOut);
|
||||
}
|
||||
Future<void> _confirmLogout(BuildContext context) async {
|
||||
// Flip AccountBloc state only after the dialog fully closes: doing it from
|
||||
// inside the sign-out (the previous approach) raced AsyncDialogAction's
|
||||
// pop(true) against the listener's popUntil(isFirst) and could leave the
|
||||
// navigator in an inconsistent state.
|
||||
final confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (dialogContext) => ConfirmDialog(
|
||||
title: 'Abmelden?',
|
||||
content: 'Möchtest du dich wirklich abmelden?',
|
||||
confirmButton: 'Abmelden',
|
||||
onConfirmAsync: _performLogout,
|
||||
),
|
||||
);
|
||||
if (confirmed != true || !context.mounted) return;
|
||||
context.read<AccountBloc>().setStatus(AccountStatus.loggedOut);
|
||||
}
|
||||
|
||||
// Ordered teardown: unregister push at Nextcloud + proxy and revoke the app
|
||||
// password (while Nextcloud credentials are still available), THEN revoke the
|
||||
// MC bearer token, and finally wipe local credentials. Each step is
|
||||
// best-effort so an offline logout still reaches a clean local state.
|
||||
Future<void> _performLogout() async {
|
||||
await PushRegistration().logoutCleanup();
|
||||
await AuthLogout().run();
|
||||
await AccountData().removeData();
|
||||
_cachedDisplayName = null;
|
||||
}
|
||||
Future<void> _performLogout() async {
|
||||
await SessionLifecycle.signOut();
|
||||
_cachedDisplayName = null;
|
||||
}
|
||||
|
||||
class _AvatarEditBadge extends StatelessWidget {
|
||||
@@ -253,11 +292,7 @@ class _AvatarEditBadge extends StatelessWidget {
|
||||
color: theme.colorScheme.onPrimary,
|
||||
),
|
||||
)
|
||||
: Icon(
|
||||
Icons.edit,
|
||||
size: 14,
|
||||
color: theme.colorScheme.onPrimary,
|
||||
),
|
||||
: Icon(Icons.edit, size: 14, color: theme.colorScheme.onPrimary),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -0,0 +1,205 @@
|
||||
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 '../../../../session/session_manager.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 NotificationsSection extends StatelessWidget {
|
||||
const NotificationsSection({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final settings = context.watch<SettingsCubit>();
|
||||
return _PushSettings(
|
||||
settings: settings,
|
||||
capabilities: context.read<CapabilitiesCubit>(),
|
||||
enabled: settings.val().notificationSettings.enabled,
|
||||
devMode: settings.val().devToolsEnabled,
|
||||
// The status checklist describes the Nextcloud chain (app passwords,
|
||||
// keypair, general/talk registrations); a direct registration has none
|
||||
// of these links.
|
||||
showChainStatus: SessionManager().hasNextcloud,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 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;
|
||||
final bool showChainStatus;
|
||||
|
||||
const _PushSettings({
|
||||
required this.settings,
|
||||
required this.capabilities,
|
||||
required this.enabled,
|
||||
required this.devMode,
|
||||
required this.showChainStatus,
|
||||
});
|
||||
|
||||
@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 {
|
||||
if (!widget.showChainStatus) return;
|
||||
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: widget.showChainStatus
|
||||
? 'Benachrichtigungen bei neuen Talk-Nachrichten erhalten'
|
||||
: 'Benachrichtigungen der Schule erhalten',
|
||||
value: widget.enabled,
|
||||
beforeCheckbox: _inlineStatusIcon(report),
|
||||
onChanged: _onToggle,
|
||||
),
|
||||
// Detail entry only when there is a problem to fix or for developers.
|
||||
if (widget.showChainStatus && (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();
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,8 @@
|
||||
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 {
|
||||
@@ -42,180 +35,7 @@ class TalkSection extends StatelessWidget {
|
||||
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();
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,35 +1,49 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../access/access_requirement.dart';
|
||||
import '../../../session/session_manager.dart';
|
||||
import 'sections/about_section.dart';
|
||||
import 'sections/account_section.dart';
|
||||
import 'sections/appearance_section.dart';
|
||||
import 'sections/files_section.dart';
|
||||
import 'sections/modules_section.dart';
|
||||
import 'sections/notifications_section.dart';
|
||||
import 'sections/talk_section.dart';
|
||||
import 'sections/timetable_section.dart';
|
||||
|
||||
class Settings extends StatelessWidget {
|
||||
const Settings({super.key});
|
||||
|
||||
/// Sections in display order with the backend identities they need;
|
||||
/// sections the session cannot use are left out.
|
||||
static const List<(Widget, Set<AccessRequirement>)> _sections = [
|
||||
(AccountSection(), {}),
|
||||
(AppearanceSection(), {}),
|
||||
(ModulesSection(), {}),
|
||||
(TimetableSection(), {}),
|
||||
(NotificationsSection(), {}),
|
||||
(TalkSection(), {AccessRequirement.nextcloud}),
|
||||
(FilesSection(), {AccessRequirement.nextcloud}),
|
||||
(AboutSection(), {}),
|
||||
];
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Scaffold(
|
||||
appBar: AppBar(title: const Text('Einstellungen')),
|
||||
body: ListView(
|
||||
children: const [
|
||||
AccountSection(),
|
||||
Divider(),
|
||||
AppearanceSection(),
|
||||
Divider(),
|
||||
ModulesSection(),
|
||||
Divider(),
|
||||
TimetableSection(),
|
||||
Divider(),
|
||||
TalkSection(),
|
||||
Divider(),
|
||||
FilesSection(),
|
||||
Divider(),
|
||||
AboutSection(),
|
||||
],
|
||||
),
|
||||
);
|
||||
Widget build(BuildContext context) {
|
||||
final session = SessionManager().current;
|
||||
final visible = [
|
||||
for (final (section, requirements) in _sections)
|
||||
if (requirements.areMetBy(session)) section,
|
||||
];
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Einstellungen')),
|
||||
body: ListView(
|
||||
children: [
|
||||
for (final (i, section) in visible.indexed) ...[
|
||||
if (i > 0) const Divider(),
|
||||
section,
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user