v2.51.1 — Technik ausgelagert: Erweitert-Sektion (Logs, Diagnose, Entwickler, Scanner)

## 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
This commit is contained in:
Dustin
2026-08-05 07:54:22 +02:00
parent 0439ce3ed2
commit f886b0a42e
3 changed files with 609 additions and 42 deletions
+548
View File
@@ -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<ErweitertScreen> createState() => _ErweitertScreenState();
}
class _ErweitertScreenState extends State<ErweitertScreen> {
bool _diagnose = AppConfig.sendeDiagnosedaten;
@override
void initState() {
super.initState();
_ladeDiagnose();
}
Future<void> _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<LogViewerScreen> createState() => _LogViewerScreenState();
}
class _LogViewerScreenState extends State<LogViewerScreen> {
List<Map<String, dynamic>> _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<EntwicklerScreen> createState() => _EntwicklerScreenState();
}
class _EntwicklerScreenState extends State<EntwicklerScreen> {
int _songAnzahl = -1;
@override
void initState() {
super.initState();
_ladeDaten();
}
Future<void> _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),
],
),
),
),
);
}
}
+46 -42
View File
@@ -1,9 +1,9 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../config/app_config.dart'; import '../config/app_config.dart';
import '../utils/farb_theme.dart'; import '../utils/farb_theme.dart';
import 'cloud_screen.dart'; import 'cloud_screen.dart';
import 'recap_screen.dart'; import 'recap_screen.dart';
import 'erweitert_screen.dart';
import '../services/cloud_service.dart'; import '../services/cloud_service.dart';
/// Vollwertiger Einstellungen-Screen kein Popup mehr /// Vollwertiger Einstellungen-Screen kein Popup mehr
@@ -15,19 +15,6 @@ class SettingsScreen extends StatefulWidget {
} }
class _SettingsScreenState extends State<SettingsScreen> { class _SettingsScreenState extends State<SettingsScreen> {
bool _diagnose = AppConfig.sendeDiagnosedaten;
@override
void initState() {
super.initState();
_ladeDiagnose();
}
Future<void> _ladeDiagnose() async {
final p = await SharedPreferences.getInstance();
if (mounted) setState(() => _diagnose = p.getBool('diagnose_daten') ?? AppConfig.sendeDiagnosedaten);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
@@ -69,8 +56,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
), ),
_einstellungsKachel( _einstellungsKachel(
icon: Icons.dns_outlined, icon: Icons.dns_outlined,
titel: 'Server verbinden', titel: 'Musikserver',
untertitel: 'Navidrome Musik-Server', untertitel: 'Navidrome Musik-Server verbinden',
onTap: () { onTap: () {
// Signal zum Öffnen des Server-Browsers // Signal zum Öffnen des Server-Browsers
Navigator.pop(context, 'server'); Navigator.pop(context, 'server');
@@ -92,31 +79,48 @@ class _SettingsScreenState extends State<SettingsScreen> {
}, },
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
Container(
decoration: BoxDecoration( // ─── Sektion: Erweitert (Technik) ───
color: MeloTheme.dunkel1, _sektionHeader('Erweitert'),
borderRadius: BorderRadius.circular(14), _einstellungsKachel(
border: Border.all(color: MeloTheme.dunkel2), icon: Icons.handyman_outlined,
), titel: 'Erweitert',
child: SwitchListTile( untertitel: 'Logs, Diagnose, Entwickler & Scanner',
title: const Text( onTap: () {
'Diagnosedaten senden', Navigator.push(
style: TextStyle(color: Colors.white, fontSize: 14), context,
), MaterialPageRoute(
subtitle: const Text( builder: (_) => ErweitertScreen(
'Absturz-Logs & anonyme Nutzungsdaten', onScan: () {
style: TextStyle(color: MeloTheme.textSekundaer, fontSize: 12), // Zurück zu MeloHome, das den Scanner startet
), Navigator.pop(context, 'scanner');
value: _diagnose, },
activeThumbColor: MeloTheme.rot, ),
onChanged: (v) async { ),
setState(() => _diagnose = v); );
AppConfig.sendeDiagnosedaten = v; },
final p = await SharedPreferences.getInstance(); ),
p.setBool('diagnose_daten', v); _einstellungsKachel(
}, icon: Icons.article_outlined,
secondary: const Icon(Icons.bug_report_outlined, color: MeloTheme.rot, size: 22), 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), const SizedBox(height: 8),
@@ -130,7 +134,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
), ),
child: Column( child: Column(
children: [ children: [
_infoZeile('Version', '2.31'), _infoZeile('Version', '2.51'),
const Divider(color: MeloTheme.dunkel2, height: 1), const Divider(color: MeloTheme.dunkel2, height: 1),
_infoZeile('Theme', 'Schwarz + Rot'), _infoZeile('Theme', 'Schwarz + Rot'),
const Divider(color: MeloTheme.dunkel2, height: 1), const Divider(color: MeloTheme.dunkel2, height: 1),
+15
View File
@@ -18,11 +18,21 @@ class MeloLogger {
String _sessionId = ''; String _sessionId = '';
String _version = '2.7'; String _version = '2.7';
final List<Map<String, dynamic>> _eintraege = []; final List<Map<String, dynamic>> _eintraege = [];
// Anzeige-Puffer: bleibt erhalten, auch wenn Einträge gesendet wurden
// (für den Log-Viewer unter „Mehr → Erweitert → Logs“).
final List<Map<String, dynamic>> _anzeigeEintraege = [];
int _logId = 0; int _logId = 0;
Timer? _flushTimer; Timer? _flushTimer;
String get _serverUrl => '${AppConfig.logUrl}/api/log'; String get _serverUrl => '${AppConfig.logUrl}/api/log';
/// Alle bisher aufgezeichneten Log-Einträge (nur Anzeige, nie geleert).
List<Map<String, dynamic>> get anzeigeEintraege =>
List.unmodifiable(_anzeigeEintraege);
/// Aktuelle Session-ID (für den Entwickler-Screen).
String get sessionId => _sessionId;
void init(String version) { void init(String version) {
if (_initialisiert) return; if (_initialisiert) return;
_initialisiert = true; _initialisiert = true;
@@ -63,6 +73,11 @@ class MeloLogger {
'aktion': aktion, 'aktion': aktion,
'details': details ?? {}, '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<String, dynamic>? details]) { void aktion(String name, [Map<String, dynamic>? details]) {