From f886b0a42e0b34cc5cd6fa9ed7e83f98aa441a45 Mon Sep 17 00:00:00 2001 From: Dustin Date: Wed, 5 Aug 2026 07:54:22 +0200 Subject: [PATCH] =?UTF-8?q?v2.51.1=20=E2=80=94=20Technik=20ausgelagert:=20?= =?UTF-8?q?Erweitert-Sektion=20(Logs,=20Diagnose,=20Entwickler,=20Scanner)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Erweitert - Neuer ErweitertScreen (lib/screens/erweitert_screen.dart): Logs, Diagnosedaten-Toggle, Entwickler, Musik scannen - Neuer LogViewerScreen: In-Memory-Logbuch mit Kategorie-Filter (MeloLogger bekommt Anzeige-Puffer, der beim Senden nicht geleert wird) - Neuer EntwicklerScreen: Version, Session-ID, DB-Songzahl, Server-URLs, Test-Log senden - Einstellungen-Screen: 'Erweitert'-Sektion eingebaut, Diagnose-Toggle dorthin verschoben, Version auf 2.51 ## Einstellungen - 'Server verbinden' → 'Musikserver' umbenannt - Abmelden/Info/Cloud Sync unverändert --- lib/screens/erweitert_screen.dart | 548 ++++++++++++++++++++++++++++++ lib/screens/settings_screen.dart | 88 ++--- lib/services/melo_logger.dart | 15 + 3 files changed, 609 insertions(+), 42 deletions(-) create mode 100644 lib/screens/erweitert_screen.dart diff --git a/lib/screens/erweitert_screen.dart b/lib/screens/erweitert_screen.dart new file mode 100644 index 0000000..d3963ae --- /dev/null +++ b/lib/screens/erweitert_screen.dart @@ -0,0 +1,548 @@ +import 'package:flutter/material.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import '../config/app_config.dart'; +import '../utils/farb_theme.dart'; +import '../services/melo_logger.dart'; +import '../database/db_helper.dart'; + +/// „Erweitert“ – technische Bereiche, die selten gebraucht werden: +/// Logs, Diagnosedaten, Entwickler-Optionen, Scanner. +/// Erreichbar über „Mehr“-Tab und über den Einstellungen-Screen. +class ErweitertScreen extends StatefulWidget { + /// Wird aufgerufen, wenn der Nutzer „Musik scannen“ antippt. + /// Kann null sein (dann erscheint nur ein Hinweis). + final VoidCallback? onScan; + + const ErweitertScreen({super.key, this.onScan}); + + @override + State createState() => _ErweitertScreenState(); +} + +class _ErweitertScreenState extends State { + bool _diagnose = AppConfig.sendeDiagnosedaten; + + @override + void initState() { + super.initState(); + _ladeDiagnose(); + } + + Future _ladeDiagnose() async { + final p = await SharedPreferences.getInstance(); + if (mounted) { + setState( + () => _diagnose = p.getBool('diagnose_daten') ?? AppConfig.sendeDiagnosedaten); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: MeloTheme.schwarz, + appBar: AppBar( + backgroundColor: MeloTheme.dunkel1, + leading: IconButton( + icon: const Icon(Icons.arrow_back, color: Colors.white, size: 22), + onPressed: () => Navigator.pop(context), + ), + title: const Row( + children: [ + Icon(Icons.handyman_outlined, color: MeloTheme.rot, size: 20), + SizedBox(width: 10), + Text( + 'Erweitert', + style: TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.w600), + ), + ], + ), + ), + body: ListView( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + children: [ + _sektionHeader('Technik'), + _einstellungsKachel( + icon: Icons.article_outlined, + titel: 'Logs', + untertitel: 'App-Logbuch im Speicher ansehen', + onTap: () { + Navigator.push( + context, + MaterialPageRoute(builder: (_) => const LogViewerScreen()), + ); + }, + ), + _einstellungsKachel( + icon: Icons.bug_report_outlined, + titel: 'Entwickler', + untertitel: 'Version, URLs & technische Details', + onTap: () { + Navigator.push( + context, + MaterialPageRoute(builder: (_) => const EntwicklerScreen()), + ); + }, + ), + const SizedBox(height: 8), + _sektionHeader('Daten'), + Container( + decoration: BoxDecoration( + color: MeloTheme.dunkel1, + borderRadius: BorderRadius.circular(14), + border: Border.all(color: MeloTheme.dunkel2), + ), + child: SwitchListTile( + title: const Text( + 'Diagnosedaten senden', + style: TextStyle(color: Colors.white, fontSize: 14), + ), + subtitle: const Text( + 'Absturz-Logs & anonyme Nutzungsdaten', + style: TextStyle(color: MeloTheme.textSekundaer, fontSize: 12), + ), + value: _diagnose, + activeThumbColor: MeloTheme.rot, + onChanged: (v) async { + setState(() => _diagnose = v); + AppConfig.sendeDiagnosedaten = v; + final p = await SharedPreferences.getInstance(); + await p.setBool('diagnose_daten', v); + }, + secondary: const Icon(Icons.shield_outlined, color: MeloTheme.rot, size: 22), + ), + ), + const SizedBox(height: 8), + _sektionHeader('Wartung'), + _einstellungsKachel( + icon: Icons.refresh, + titel: 'Musik scannen', + untertitel: 'Lokale Ordner nach neuer Musik durchsuchen', + onTap: widget.onScan ?? + () { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Scannen ist hier nicht verfügbar')), + ); + }, + ), + const SizedBox(height: 24), + ], + ), + ); + } + + Widget _sektionHeader(String titel) { + return Padding( + padding: const EdgeInsets.fromLTRB(4, 16, 4, 8), + child: Text( + titel, + style: const TextStyle( + color: MeloTheme.textSekundaer, + fontSize: 11, + fontWeight: FontWeight.w600, + letterSpacing: 1.2, + ), + ), + ); + } + + Widget _einstellungsKachel({ + required IconData icon, + required String titel, + required String untertitel, + required VoidCallback onTap, + }) { + return Padding( + padding: const EdgeInsets.only(bottom: 8), + child: Material( + color: MeloTheme.dunkel1, + borderRadius: BorderRadius.circular(14), + child: InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(14), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(14), + border: Border.all(color: MeloTheme.dunkel2), + ), + child: Row( + children: [ + Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: MeloTheme.dunkel2, + borderRadius: BorderRadius.circular(10), + ), + child: Icon(icon, color: MeloTheme.rot, size: 20), + ), + const SizedBox(width: 14), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + titel, + style: const TextStyle(color: Colors.white, fontSize: 14, fontWeight: FontWeight.w500), + ), + const SizedBox(height: 2), + Text( + untertitel, + style: const TextStyle(color: MeloTheme.textSekundaer, fontSize: 12), + ), + ], + ), + ), + const Icon(Icons.chevron_right, color: MeloTheme.textSekundaer, size: 20), + ], + ), + ), + ), + ), + ); + } +} + +/// Zeigt das In-Memory-Logbuch der App (MeloLogger-Anzeige-Puffer). +class LogViewerScreen extends StatefulWidget { + const LogViewerScreen({super.key}); + + @override + State createState() => _LogViewerScreenState(); +} + +class _LogViewerScreenState extends State { + List> _eintraege = []; + String _filter = 'Alle'; + + static const _kategorien = ['Alle', 'aktion', 'netzwerk', 'fehler', 'crash', 'performance', 'zustand', 'user']; + + @override + void initState() { + super.initState(); + _laden(); + } + + void _laden() { + setState(() => _eintraege = MeloLogger().anzeigeEintraege.reversed.toList()); + } + + String _zeit(String iso) { + final dt = DateTime.tryParse(iso); + if (dt == null) return iso; + final h = dt.hour.toString().padLeft(2, '0'); + final m = dt.minute.toString().padLeft(2, '0'); + final s = dt.second.toString().padLeft(2, '0'); + return '$h:$m:$s'; + } + + @override + Widget build(BuildContext context) { + final gefiltert = _filter == 'Alle' + ? _eintraege + : _eintraege.where((e) => e['kategorie'] == _filter).toList(); + + return Scaffold( + backgroundColor: MeloTheme.schwarz, + appBar: AppBar( + backgroundColor: MeloTheme.dunkel1, + leading: IconButton( + icon: const Icon(Icons.arrow_back, color: Colors.white, size: 22), + onPressed: () => Navigator.pop(context), + ), + title: const Text( + 'Logs', + style: TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.w600), + ), + actions: [ + IconButton( + icon: const Icon(Icons.refresh, color: Colors.grey, size: 20), + onPressed: _laden, + ), + ], + ), + body: Column( + children: [ + // Kategorie-Filter + SizedBox( + height: 40, + child: ListView( + scrollDirection: Axis.horizontal, + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), + children: _kategorien.map((k) { + final aktiv = _filter == k; + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 3), + child: GestureDetector( + onTap: () => setState(() => _filter = k), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: aktiv ? MeloTheme.rot : MeloTheme.dunkel1, + borderRadius: BorderRadius.circular(14), + ), + child: Text( + k, + style: TextStyle( + fontSize: 11, + color: aktiv ? Colors.white : MeloTheme.textSekundaer, + fontWeight: FontWeight.w500, + ), + ), + ), + ), + ); + }).toList(), + ), + ), + const Divider(color: MeloTheme.dunkel2, height: 1), + Expanded( + child: gefiltert.isEmpty + ? const Center( + child: Text( + 'Noch keine Log-Einträge', + style: TextStyle(color: MeloTheme.textSekundaer, fontSize: 13), + ), + ) + : ListView.builder( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + itemCount: gefiltert.length, + itemBuilder: (_, i) { + final e = gefiltert[i]; + final kategorie = e['kategorie']?.toString() ?? '?'; + final farbe = switch (kategorie) { + 'fehler' || 'crash' => const Color(0xFFEF9A9A), + 'netzwerk' => const Color(0xFFFFCC80), + 'performance' => const Color(0xFF90CAF9), + _ => Colors.white70, + }; + final details = (e['details'] as Map?) ?? const {}; + return Container( + margin: const EdgeInsets.only(bottom: 6), + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: MeloTheme.dunkel1, + borderRadius: BorderRadius.circular(10), + border: Border.all(color: MeloTheme.dunkel2), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Text( + _zeit(e['zeit']?.toString() ?? ''), + style: const TextStyle(color: MeloTheme.textSekundaer, fontSize: 10), + ), + const SizedBox(width: 8), + Container( + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), + decoration: BoxDecoration( + color: MeloTheme.dunkel2, + borderRadius: BorderRadius.circular(8), + ), + child: Text( + kategorie, + style: const TextStyle(color: MeloTheme.textSekundaer, fontSize: 9), + ), + ), + ], + ), + const SizedBox(height: 4), + Text( + e['aktion']?.toString() ?? '', + style: TextStyle(color: farbe, fontSize: 12, fontWeight: FontWeight.w500), + ), + if (details.isNotEmpty) ...[ + const SizedBox(height: 2), + Text( + details.toString(), + style: const TextStyle(color: MeloTheme.textSekundaer, fontSize: 10), + maxLines: 3, + overflow: TextOverflow.ellipsis, + ), + ], + ], + ), + ); + }, + ), + ), + ], + ), + ); + } +} + +/// Entwickler-Screen: technische Details & Debug-Aktionen. +class EntwicklerScreen extends StatefulWidget { + const EntwicklerScreen({super.key}); + + @override + State createState() => _EntwicklerScreenState(); +} + +class _EntwicklerScreenState extends State { + int _songAnzahl = -1; + + @override + void initState() { + super.initState(); + _ladeDaten(); + } + + Future _ladeDaten() async { + try { + final songs = await DbHelper().alleSongs(); + if (mounted) setState(() => _songAnzahl = songs.length); + } catch (_) { + if (mounted) setState(() => _songAnzahl = 0); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: MeloTheme.schwarz, + appBar: AppBar( + backgroundColor: MeloTheme.dunkel1, + leading: IconButton( + icon: const Icon(Icons.arrow_back, color: Colors.white, size: 22), + onPressed: () => Navigator.pop(context), + ), + title: const Text( + 'Entwickler', + style: TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.w600), + ), + ), + body: ListView( + padding: const EdgeInsets.all(16), + children: [ + _infoKarte([ + ('Version', 'v2.51'), + ('Session', MeloLogger().sessionId.isEmpty ? '—' : MeloLogger().sessionId), + ('Songs in DB', _songAnzahl < 0 ? '…' : '$_songAnzahl'), + ('Auth-Server', AppConfig.authUrl), + ('Cloud-Server', AppConfig.cloudUrl), + ('Musikserver', AppConfig.navidromeUrl), + ]), + const SizedBox(height: 16), + _einstellungsKachel( + icon: Icons.article_outlined, + titel: 'Logs ansehen', + untertitel: 'App-Logbuch öffnen', + onTap: () { + Navigator.push( + context, + MaterialPageRoute(builder: (_) => const LogViewerScreen()), + ); + }, + ), + const SizedBox(height: 8), + _einstellungsKachel( + icon: Icons.send_outlined, + titel: 'Test-Log senden', + untertitel: 'Erzeugt einen manuellen Log-Eintrag', + onTap: () { + MeloLogger().manuell('Test aus dem Entwickler-Screen'); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Test-Log erzeugt')), + ); + }, + ), + ], + ), + ); + } + + Widget _infoKarte(List<(String, String)> zeilen) { + return Container( + decoration: BoxDecoration( + color: MeloTheme.dunkel1, + borderRadius: BorderRadius.circular(14), + border: Border.all(color: MeloTheme.dunkel2), + ), + child: Column( + children: [ + for (var i = 0; i < zeilen.length; i++) ...[ + if (i > 0) const Divider(color: MeloTheme.dunkel2, height: 1), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + child: Row( + children: [ + Text( + zeilen[i].$1, + style: const TextStyle(color: MeloTheme.textSekundaer, fontSize: 13), + ), + const Spacer(), + Flexible( + child: Text( + zeilen[i].$2, + textAlign: TextAlign.right, + style: const TextStyle(color: Colors.white70, fontSize: 13), + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ), + ], + ], + ), + ); + } + + Widget _einstellungsKachel({ + required IconData icon, + required String titel, + required String untertitel, + required VoidCallback onTap, + }) { + return Material( + color: MeloTheme.dunkel1, + borderRadius: BorderRadius.circular(14), + child: InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(14), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(14), + border: Border.all(color: MeloTheme.dunkel2), + ), + child: Row( + children: [ + Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: MeloTheme.dunkel2, + borderRadius: BorderRadius.circular(10), + ), + child: Icon(icon, color: MeloTheme.rot, size: 20), + ), + const SizedBox(width: 14), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + titel, + style: const TextStyle(color: Colors.white, fontSize: 14, fontWeight: FontWeight.w500), + ), + const SizedBox(height: 2), + Text( + untertitel, + style: const TextStyle(color: MeloTheme.textSekundaer, fontSize: 12), + ), + ], + ), + ), + const Icon(Icons.chevron_right, color: MeloTheme.textSekundaer, size: 20), + ], + ), + ), + ), + ); + } +} diff --git a/lib/screens/settings_screen.dart b/lib/screens/settings_screen.dart index 552653d..ea7f110 100644 --- a/lib/screens/settings_screen.dart +++ b/lib/screens/settings_screen.dart @@ -1,9 +1,9 @@ import 'package:flutter/material.dart'; -import 'package:shared_preferences/shared_preferences.dart'; import '../config/app_config.dart'; import '../utils/farb_theme.dart'; import 'cloud_screen.dart'; import 'recap_screen.dart'; +import 'erweitert_screen.dart'; import '../services/cloud_service.dart'; /// Vollwertiger Einstellungen-Screen – kein Popup mehr @@ -15,19 +15,6 @@ class SettingsScreen extends StatefulWidget { } class _SettingsScreenState extends State { - bool _diagnose = AppConfig.sendeDiagnosedaten; - - @override - void initState() { - super.initState(); - _ladeDiagnose(); - } - - Future _ladeDiagnose() async { - final p = await SharedPreferences.getInstance(); - if (mounted) setState(() => _diagnose = p.getBool('diagnose_daten') ?? AppConfig.sendeDiagnosedaten); - } - @override Widget build(BuildContext context) { return Scaffold( @@ -69,8 +56,8 @@ class _SettingsScreenState extends State { ), _einstellungsKachel( icon: Icons.dns_outlined, - titel: 'Server verbinden', - untertitel: 'Navidrome Musik-Server', + titel: 'Musikserver', + untertitel: 'Navidrome Musik-Server verbinden', onTap: () { // Signal zum Öffnen des Server-Browsers Navigator.pop(context, 'server'); @@ -92,31 +79,48 @@ class _SettingsScreenState extends State { }, ), const SizedBox(height: 8), - Container( - decoration: BoxDecoration( - color: MeloTheme.dunkel1, - borderRadius: BorderRadius.circular(14), - border: Border.all(color: MeloTheme.dunkel2), - ), - child: SwitchListTile( - title: const Text( - 'Diagnosedaten senden', - style: TextStyle(color: Colors.white, fontSize: 14), - ), - subtitle: const Text( - 'Absturz-Logs & anonyme Nutzungsdaten', - style: TextStyle(color: MeloTheme.textSekundaer, fontSize: 12), - ), - value: _diagnose, - activeThumbColor: MeloTheme.rot, - onChanged: (v) async { - setState(() => _diagnose = v); - AppConfig.sendeDiagnosedaten = v; - final p = await SharedPreferences.getInstance(); - p.setBool('diagnose_daten', v); - }, - secondary: const Icon(Icons.bug_report_outlined, color: MeloTheme.rot, size: 22), - ), + + // ─── Sektion: Erweitert (Technik) ─── + _sektionHeader('Erweitert'), + _einstellungsKachel( + icon: Icons.handyman_outlined, + titel: 'Erweitert', + untertitel: 'Logs, Diagnose, Entwickler & Scanner', + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (_) => ErweitertScreen( + onScan: () { + // Zurück zu MeloHome, das den Scanner startet + Navigator.pop(context, 'scanner'); + }, + ), + ), + ); + }, + ), + _einstellungsKachel( + icon: Icons.article_outlined, + titel: 'Logs', + untertitel: 'App-Logbuch im Speicher ansehen', + onTap: () { + Navigator.push( + context, + MaterialPageRoute(builder: (_) => const LogViewerScreen()), + ); + }, + ), + _einstellungsKachel( + icon: Icons.bug_report_outlined, + titel: 'Entwickler', + untertitel: 'Version, URLs & technische Details', + onTap: () { + Navigator.push( + context, + MaterialPageRoute(builder: (_) => const EntwicklerScreen()), + ); + }, ), const SizedBox(height: 8), @@ -130,7 +134,7 @@ class _SettingsScreenState extends State { ), child: Column( children: [ - _infoZeile('Version', '2.31'), + _infoZeile('Version', '2.51'), const Divider(color: MeloTheme.dunkel2, height: 1), _infoZeile('Theme', 'Schwarz + Rot'), const Divider(color: MeloTheme.dunkel2, height: 1), diff --git a/lib/services/melo_logger.dart b/lib/services/melo_logger.dart index 8ff8fa4..8df9eba 100644 --- a/lib/services/melo_logger.dart +++ b/lib/services/melo_logger.dart @@ -18,11 +18,21 @@ class MeloLogger { String _sessionId = ''; String _version = '2.7'; final List> _eintraege = []; + // Anzeige-Puffer: bleibt erhalten, auch wenn Einträge gesendet wurden + // (für den Log-Viewer unter „Mehr → Erweitert → Logs“). + final List> _anzeigeEintraege = []; int _logId = 0; Timer? _flushTimer; String get _serverUrl => '${AppConfig.logUrl}/api/log'; + /// Alle bisher aufgezeichneten Log-Einträge (nur Anzeige, nie geleert). + List> get anzeigeEintraege => + List.unmodifiable(_anzeigeEintraege); + + /// Aktuelle Session-ID (für den Entwickler-Screen). + String get sessionId => _sessionId; + void init(String version) { if (_initialisiert) return; _initialisiert = true; @@ -63,6 +73,11 @@ class MeloLogger { 'aktion': aktion, 'details': details ?? {}, }); + // Anzeige-Puffer (nie leeren — nur älteste verwerfen) + if (_anzeigeEintraege.length >= _maxEintraege) { + _anzeigeEintraege.removeAt(0); + } + _anzeigeEintraege.add(_eintraege.last); } void aktion(String name, [Map? details]) {