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:
@@ -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),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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),
|
||||||
|
|||||||
@@ -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]) {
|
||||||
|
|||||||
Reference in New Issue
Block a user