import 'package:filesize/filesize.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:hydrated_bloc/hydrated_bloc.dart'; import '../../../../routing/app_routes.dart'; import '../../../../state/app/modules/settings/bloc/settings_cubit.dart'; import '../../../../storage/dev_tools_settings.dart'; import '../../../../storage/settings.dart' as model; import '../../../../widget/centered_leading.dart'; import '../../../../widget/confirm_dialog.dart'; import '../../../../widget/debug/cache_view.dart'; import '../../../../widget/debug/json_viewer.dart'; import '../../../../widget/details_bottom_sheet.dart'; import '../widgets/endpoint_picker.dart'; import '../widgets/settings_checkbox_tile.dart'; class DevToolsSection extends StatefulWidget { final SettingsCubit settings; const DevToolsSection({required this.settings, super.key}); @override State createState() => _DevToolsSectionState(); } class _DevToolsSectionState extends State { @override Widget build(BuildContext context) => Column( children: [ ListTile( leading: const CenteredLeading(Icon(Icons.speed_outlined)), title: const Text('Performance overlays'), trailing: const Icon(Icons.arrow_right), onTap: () { showDetailsBottomSheet( context, children: (sheetCtx) => [ BlocBuilder( bloc: widget.settings, builder: (_, _) { final dev = widget.settings.val().devToolsSettings; return Column( mainAxisSize: MainAxisSize.min, children: [ SettingsCheckboxTile( icon: Icons.auto_graph_outlined, title: 'Performance graph', value: dev.showPerformanceOverlay, onChanged: (e) => widget.settings .val(write: true) .devToolsSettings .showPerformanceOverlay = e, ), SettingsCheckboxTile( icon: Icons.screen_search_desktop_outlined, title: 'Indicate offscreen layers', value: dev.checkerboardOffscreenLayers, onChanged: (e) => widget.settings .val(write: true) .devToolsSettings .checkerboardOffscreenLayers = e, ), SettingsCheckboxTile( icon: Icons.imagesearch_roller_outlined, title: 'Indicate raster cache images', value: dev.checkerboardRasterCacheImages, onChanged: (e) => widget.settings .val(write: true) .devToolsSettings .checkerboardRasterCacheImages = e, ), ], ); }, ), ], ); }, ), ListTile( leading: const CenteredLeading(Icon(Icons.cloud_outlined)), title: const Text('Marianum-Connect-Server'), subtitle: Text( MarianumConnectEndpointPicker.labelFor( widget.settings.val().devToolsSettings, ), ), trailing: const Icon(Icons.arrow_right), onTap: () => MarianumConnectEndpointPicker.show(context, widget.settings), ), BlocBuilder( bloc: widget.settings, builder: (_, _) { final override = widget.settings .val() .devToolsSettings .emergencyNoticeUrl .trim(); return ListTile( leading: const CenteredLeading(Icon(Icons.emergency_outlined)), title: const Text('Notfall-Nachricht (Quelle)'), subtitle: Text( override.isEmpty ? 'Standardquelle (im Code hinterlegt)' : override, ), trailing: const Icon(Icons.arrow_right), onTap: () => _EmergencyNoticeUrlEditor.show(context, widget.settings), ); }, ), ListTile( leading: const CenteredLeading(Icon(Icons.image_outlined)), title: const Text('Thumb-storage'), subtitle: Text( 'etwa ${filesize(PaintingBinding.instance.imageCache.currentSizeBytes)}\nLange tippen um zu löschen', ), onLongPress: () { ConfirmDialog( title: 'Thumbs cache löschen', content: 'Alle zwischengespeicherten Bilder werden gelöscht.', confirmButton: 'Unwiederruflich löschen', onConfirm: () => PaintingBinding.instance.imageCache.clear(), ).asDialog(context); }, ), ListTile( leading: const CenteredLeading( Icon(Icons.settings_applications_outlined), ), title: const Text('Settings-storage JSON dump'), subtitle: Text( 'etwa ${filesize(widget.settings.val().toJson().toString().length * 8)}\nLange tippen um zu löschen', ), onTap: () { JsonViewer.asDialog(context, widget.settings.val().toJson()); }, onLongPress: () { ConfirmDialog( title: 'Einstellungen löschen', content: 'Alle Einstellungen gehen verloren! Accountdaten sowie App-Daten sind nicht betroffen.', confirmButton: 'Unwiederruflich Löschen', onConfirm: () { context.read().reset(); }, ).asDialog(context); }, trailing: const Icon(Icons.arrow_right), ), ListTile( leading: const CenteredLeading(Icon(Icons.data_object)), title: const Text('Cache-storage JSON dump'), subtitle: FutureBuilder( future: const CacheView().totalSize(), builder: (context, snapshot) => Text( "etwa ${snapshot.hasError ? "?" : snapshot.hasData ? filesize(snapshot.data) : "..."}\nLange tippen um zu löschen", ), ), onTap: () => AppRoutes.openCacheView(context), onLongPress: () { ConfirmDialog( title: 'App-Cache löschen', content: 'Alle cache Einträge werden gelöscht. Der Cache wird bei Nutzung der App automatisch erneut aufgebaut', confirmButton: 'Unwiederruflich löschen', onConfirm: () => const CacheView().clear().then((value) => setState(() {})), ).asDialog(context); }, trailing: const Icon(Icons.arrow_right), ), ListTile( leading: const CenteredLeading(Icon(Icons.data_object)), title: const Text('BLOC-storage state cache'), subtitle: const Text('Lange tippen um zu löschen'), onLongPress: () { ConfirmDialog( title: 'BLOC-Cache löschen', content: 'Alle cache Einträge werden gelöscht. Der Cache wird bei Nutzung der App automatisch erneut aufgebaut', confirmButton: 'Unwiederruflich löschen', onConfirm: () => HydratedBloc.storage.clear(), ).asDialog(context); }, ), ], ); } /// Bottom-sheet editor for the backend-independent emergency-notice override /// URL. Empty clears the override so the compiled-in default source is used. class _EmergencyNoticeUrlEditor extends StatefulWidget { final SettingsCubit settings; const _EmergencyNoticeUrlEditor({required this.settings}); static void show(BuildContext context, SettingsCubit settings) { showDetailsBottomSheet( context, header: const ListTile(title: Text('Notfall-Nachricht (Quelle)')), children: (sheetCtx) => [_EmergencyNoticeUrlEditor(settings: settings)], ); } @override State<_EmergencyNoticeUrlEditor> createState() => _EmergencyNoticeUrlEditorState(); } class _EmergencyNoticeUrlEditorState extends State<_EmergencyNoticeUrlEditor> { late final TextEditingController _controller; String? _error; @override void initState() { super.initState(); _controller = TextEditingController( text: widget.settings.val().devToolsSettings.emergencyNoticeUrl, ); } @override void dispose() { _controller.dispose(); super.dispose(); } void _save() { final raw = _controller.text.trim(); // Empty is valid: it clears the override and falls back to the default. if (raw.isNotEmpty && DevToolsSettings.sanitizeCustomUrl(raw) == null) { setState( () => _error = DevToolsSettings.allowsHttpCustomEndpoint ? 'Ungültige URL (http(s)://host[:port]/...)' : 'Ungültige URL — nur HTTPS erlaubt', ); return; } widget.settings.val(write: true).devToolsSettings.emergencyNoticeUrl = raw; Navigator.of(context).pop(); } @override Widget build(BuildContext context) => Padding( padding: const EdgeInsets.fromLTRB(24, 0, 24, 16), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ const Padding( padding: EdgeInsets.only(bottom: 12), child: Text( 'Überschreibt die im Code hinterlegte Standardquelle. Leer ' 'lassen, um die Standardquelle zu verwenden.', style: TextStyle(fontSize: 12), ), ), TextField( controller: _controller, keyboardType: TextInputType.url, decoration: InputDecoration( labelText: 'https://...', errorText: _error, ), onChanged: (_) { if (_error != null) setState(() => _error = null); }, ), Padding( padding: const EdgeInsets.only(top: 16), child: FilledButton( onPressed: _save, child: const Text('Übernehmen'), ), ), ], ), ); }