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.52.3'), ('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), ], ), ), ), ); } }