reworked developer tools with diagnostics, storage viewers and release-capable debug overlays
This commit is contained in:
@@ -0,0 +1,222 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
|
||||
import '../../../api/marianumconnect/auth/device_token_name.dart';
|
||||
import '../../../api/marianumconnect/auth/token_storage.dart';
|
||||
import '../../../api/marianumconnect/marianumconnect_endpoint.dart';
|
||||
import '../../../api/marianumconnect/queries/telemetry_heartbeat/telemetry_device_id.dart';
|
||||
import '../../../background/widget_background_task.dart';
|
||||
import '../../../extensions/date_time.dart';
|
||||
import '../../../model/account_data.dart';
|
||||
import '../../../model/endpoint_data.dart';
|
||||
import '../../../push/push_registration_store.dart';
|
||||
import '../../../push/push_registration_type.dart';
|
||||
import '../../../state/app/modules/account/bloc/account_bloc.dart';
|
||||
import '../../../state/app/modules/settings/bloc/settings_cubit.dart';
|
||||
import '../../../utils/clipboard_helper.dart';
|
||||
import '../../../widget/app_progress_indicator.dart';
|
||||
import '../../../widget/async_action_button.dart';
|
||||
import '../../../widget/centered_leading.dart';
|
||||
import '../../../widget_data/widget_sync.dart';
|
||||
import 'widgets/push_status_sheet.dart';
|
||||
|
||||
typedef _Row = ({String label, String value});
|
||||
typedef _Section = ({String title, List<_Row> rows});
|
||||
|
||||
/// Read-only snapshot of build, endpoint, session, push and widget state for
|
||||
/// bug reports. Secrets (passwords, bearer token) are never shown.
|
||||
class DiagnosticsPage extends StatefulWidget {
|
||||
const DiagnosticsPage({super.key});
|
||||
|
||||
@override
|
||||
State<DiagnosticsPage> createState() => _DiagnosticsPageState();
|
||||
}
|
||||
|
||||
class _DiagnosticsPageState extends State<DiagnosticsPage> {
|
||||
late Future<List<_Section>> _sections = _collect();
|
||||
|
||||
Future<List<_Section>> _collect() async {
|
||||
final settings = context.read<SettingsCubit>().val();
|
||||
final accountStatus = context.read<AccountBloc>().state.status;
|
||||
final account = AccountData();
|
||||
const tokens = MarianumConnectTokenStorage();
|
||||
final push = PushRegistrationStore();
|
||||
|
||||
Future<String> safe(Future<Object?> Function() read) async {
|
||||
try {
|
||||
final value = await read().timeout(const Duration(seconds: 5));
|
||||
return switch (value) {
|
||||
null => '–',
|
||||
bool() => value ? 'ja' : 'nein',
|
||||
DateTime() => '${value.formatDateTime()} (${value.formatRelative()})',
|
||||
_ => '$value',
|
||||
};
|
||||
} on Object catch (e) {
|
||||
return 'Fehler: $e';
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<_Row>> rows(Map<String, Future<Object?> Function()> reads) =>
|
||||
Future.wait(
|
||||
reads.entries.map(
|
||||
(e) async => (label: e.key, value: await safe(e.value)),
|
||||
),
|
||||
);
|
||||
|
||||
final info = await PackageInfo.fromPlatform();
|
||||
return [
|
||||
(
|
||||
title: 'App',
|
||||
rows: await rows({
|
||||
'Version': () async => '${info.version} (${info.buildNumber})',
|
||||
'Paket': () async => info.packageName,
|
||||
'Build-Modus': () async => kReleaseMode
|
||||
? 'Release'
|
||||
: kProfileMode
|
||||
? 'Profile'
|
||||
: 'Debug',
|
||||
'Plattform': () async =>
|
||||
'${Platform.operatingSystem} ${Platform.operatingSystemVersion}',
|
||||
'Gerät': DeviceTokenName.resolve,
|
||||
'Dart': () async => Platform.version.split(' ').first,
|
||||
}),
|
||||
),
|
||||
(
|
||||
title: 'Server',
|
||||
rows: await rows({
|
||||
'Marianum Connect': () async => MarianumConnectEndpoint.current(),
|
||||
'Nextcloud': () async =>
|
||||
'https://${EndpointData().nextcloud().full()}',
|
||||
'Notfall-Nachricht': () async =>
|
||||
settings.devToolsSettings.resolveEmergencyNoticeUrl(),
|
||||
}),
|
||||
),
|
||||
(
|
||||
title: 'Sitzung',
|
||||
rows: await rows({
|
||||
'Status': () async => accountStatus.name,
|
||||
'Benutzer': () async =>
|
||||
account.isPopulated() ? account.getUsername() : null,
|
||||
'Demo-Modus': () async => account.isDemo,
|
||||
'Login Flow v2': () async => account.usesLoginFlow,
|
||||
'App-Passwort (Dateien)': () async => account.hasAppPassword(),
|
||||
'App-Passwort (Talk)': () async => account.hasAppPasswordTalk(),
|
||||
'Session-Epoche': () async => account.sessionEpoch,
|
||||
'MC-Token-ID': tokens.readTokenId,
|
||||
'MC-Token läuft ab': tokens.readExpiresAt,
|
||||
}),
|
||||
),
|
||||
(
|
||||
title: 'Push',
|
||||
rows: await rows({
|
||||
'Benachrichtigungen': () async =>
|
||||
settings.notificationSettings.enabled,
|
||||
'FCM-Token': FirebaseMessaging.instance.getToken,
|
||||
for (final type in PushRegistrationType.values) ...{
|
||||
'Registriert (${type.name})': () => push.isRegistered(type),
|
||||
'Letzte Registrierung (${type.name})': () =>
|
||||
push.lastRegistrationAt(type),
|
||||
'Letzter Fehler (${type.name})': () =>
|
||||
push.lastRegistrationError(type),
|
||||
},
|
||||
}),
|
||||
),
|
||||
(
|
||||
title: 'Sonstiges',
|
||||
rows: await rows({
|
||||
'Telemetrie-Geräte-ID': TelemetryDeviceId.resolve,
|
||||
'Widget zuletzt abgerufen': WidgetSync.getFetchedAt,
|
||||
}),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
String _asText(List<_Section> sections) => [
|
||||
for (final s in sections) ...[
|
||||
'## ${s.title}',
|
||||
for (final r in s.rows) '${r.label}: ${r.value}',
|
||||
'',
|
||||
],
|
||||
].join('\n');
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Diagnose'),
|
||||
actions: [
|
||||
IconButton(
|
||||
tooltip: 'Neu laden',
|
||||
icon: const Icon(Icons.refresh),
|
||||
onPressed: () => setState(() {
|
||||
_sections = _collect();
|
||||
}),
|
||||
),
|
||||
FutureBuilder(
|
||||
future: _sections,
|
||||
builder: (context, snapshot) => IconButton(
|
||||
tooltip: 'Alles kopieren',
|
||||
icon: const Icon(Icons.copy_all_outlined),
|
||||
onPressed: snapshot.hasData
|
||||
? () => copyToClipboard(context, _asText(snapshot.data!))
|
||||
: null,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: FutureBuilder(
|
||||
future: _sections,
|
||||
builder: (context, snapshot) {
|
||||
final sections = snapshot.data;
|
||||
if (sections == null) {
|
||||
return const Center(child: AppProgressIndicator.large());
|
||||
}
|
||||
return ListView(
|
||||
children: [
|
||||
for (final section in sections) ...[
|
||||
ListTile(
|
||||
dense: true,
|
||||
title: Text(
|
||||
section.title,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
for (final row in section.rows)
|
||||
ListTile(
|
||||
dense: true,
|
||||
title: Text(row.label),
|
||||
subtitle: SelectableText(row.value),
|
||||
onLongPress: () => copyToClipboard(context, row.value),
|
||||
),
|
||||
if (section.title == 'Push')
|
||||
ListTile(
|
||||
leading: const CenteredLeading(
|
||||
Icon(Icons.notifications_active_outlined),
|
||||
),
|
||||
title: const Text('Push-Status prüfen'),
|
||||
trailing: const Icon(Icons.arrow_right),
|
||||
onTap: () => showPushStatusSheet(context),
|
||||
),
|
||||
if (section.title == 'Sonstiges')
|
||||
AsyncListTile(
|
||||
leading: const CenteredLeading(Icon(Icons.widgets_outlined)),
|
||||
title: const Text('Widget jetzt aktualisieren'),
|
||||
closeOnSuccess: false,
|
||||
onPressed: WidgetBackgroundTask.requestImmediateRefresh,
|
||||
),
|
||||
const Divider(),
|
||||
],
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user