This repository has been archived on 2026-08-19. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
melo-app/lib/screens/erweitert_screen.dart
Dustin 187b26603b v2.52.3 — F3-Rest: ☁️-Chip-Puls + Auto-Sync-Timer app-weit
## Chip-Puls (home_screen)
- ListenableBuilder hört auf Listenable.merge([_vm, _cloud, SyncService.laeuftNotifier]) — der ☁️-Musikserver-Chip pulsiert jetzt bei laufendem Sync (auch im Hintergrund/Auto-Sync), nicht nur beim Verbinden/Laden
- serverSyncLaeuft = SyncService.laeuftNotifier.value || CloudStatus.verbinde || serverLadt

## Auto-Sync-Timer app-weit (main.dart)
- _starteAutoSyncFallsNoetig() entfernt (F3a: kein Sync beim App-Start); stattdessen SyncService.starteAutoSyncTimer() — Timer tickt erst nach dem Intervall, Fälligkeit via istSyncFaellig
- shared_preferences-Import aus main.dart entfernt (Intervall-Logik lebt in SyncService/CloudScreen)

## Sync-Notification
- Sync-Kanal de.baka.melo.sync wird in main() angelegt (Android 8+: Channel nötig, sonst erscheint die Notification nicht)
- onDidReceiveNotificationResponse: Tipp auf 'sync_fertig'/'sync_fehler' → SyncService.syncBenachrichtigungGetippt++ → MeloHome öffnet den Cloud-Tab

## Version
- Versionsanzeige 2.51 → 2.52.3 (settings/home/erweitert), MeloLogger.init('2.35') → '2.52.3'
2026-08-05 11:03:03 +02:00

549 lines
19 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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.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),
],
),
),
),
);
}
}