import 'package:flutter/material.dart'; import 'package:shared_preferences/shared_preferences.dart'; import '../config/app_config.dart'; import '../utils/farb_theme.dart'; import '../utils/user_effekte.dart'; import '../services/auth_service.dart'; import '../services/cloud_service.dart'; import '../services/player_service.dart'; import '../services/sync_service.dart'; import '../services/realtime_sync_service.dart'; import '../database/db_helper.dart'; import 'cloud_screen.dart'; import 'recap_screen.dart'; import 'erweitert_screen.dart'; /// Vollwertiger Einstellungen-Screen – kein Popup mehr. /// Sprint D F4: alle Einstellungen funktionierend + persistiert /// (Auto-Sync-Intervall, Sleep-Timer-Standard, Wiedergabegeschwindigkeit, /// Akzentfarbe pro User, Speicher-Info). class SettingsScreen extends StatefulWidget { const SettingsScreen({super.key}); @override State createState() => _SettingsScreenState(); } class _SettingsScreenState extends State { // ─── Einstellungen (aus SharedPreferences geladen) ─── String _syncModus = 'realtime'; // 'realtime' | 'manual' | 'interval' int _cloudIntervallStunden = 48; // 48/72/120/168/336 int _sleepDefaultMin = 0; // 0 = Aus double _geschwindigkeit = 1.0; Color _akzent = MeloTheme.akzent; Map _speicherInfo = const {'anzahl': 0, 'bytes': 0}; /// Verfügbare Zeit-Intervalle für Modus 3 (in Stunden). static const _intervallOptionen = { 48: '2 Tage', 72: '3 Tage', 120: '5 Tage', 168: '1 Woche', 336: '2 Wochen', }; static const _akzentFarben = [ Color(0xFFCC0000), // Rot (Standard) Color(0xFF00B8A9), // Türkis Color(0xFF1E6FD9), // Blau Color(0xFF2E9E4F), // Grün Color(0xFF8E44AD), // Lila Color(0xFFE67E22), // Orange Color(0xFFD81B60), // Pink ]; @override void initState() { super.initState(); _ladeEinstellungen(); _ladeSpeicherInfo(); } Future _ladeEinstellungen() async { final p = await SharedPreferences.getInstance(); final speed = p.getDouble('playback_speed') ?? 1.0; final akzentHex = p.getString(UserEffekt.akzentKey(AuthService().benutzer)); // Sync-Modus laden (mit Migration von altem cloud_interval) final modus = p.getString('sync_modus') ?? 'realtime'; int intervallStunden = p.getInt('cloud_interval_stunden') ?? 0; if (intervallStunden <= 0) { // Migration: alter cloud_interval (1/3/6/12h) → 48h default final alt = p.getInt('cloud_interval') ?? 0; if (alt == 1 || alt == 3 || alt == 6 || alt == 12) { intervallStunden = 48; await p.setInt('cloud_interval_stunden', 48); } else { intervallStunden = 48; } } // Prüfe, ob der Modus gültig ist if (!['realtime', 'manual', 'interval'].contains(modus)) { await p.setString('sync_modus', 'realtime'); } if (!mounted) return; setState(() { _syncModus = modus; _cloudIntervallStunden = intervallStunden; _sleepDefaultMin = p.getInt('sleep_timer_default_min') ?? 0; _geschwindigkeit = speed; _akzent = UserEffekt.farbeAusHex(akzentHex) ?? MeloTheme.akzent; }); // Speed-Default auch auf den Player anwenden (gilt fürs Abspielen) await PlayerService().setGeschwindigkeit(speed); } Future _ladeSpeicherInfo() async { final info = await DbHelper().speicherInfo(); if (!mounted) return; setState(() => _speicherInfo = info); } // ─── Setter (persistieren + anwenden) ─── /// Setzt den Sync-Modus und persistiert ihn. /// Startet/stoppt den Realtime-Service bzw. den Timer entsprechend. Future _syncModusSetzen(String modus) async { setState(() => _syncModus = modus); final p = await SharedPreferences.getInstance(); await p.setString('sync_modus', modus); // Realtime-Service entsprechend starten/stoppen switch (modus) { case 'realtime': // SSE sofort starten final rs = RealtimeSyncService(); await rs.starteWennAktiviert(); break; case 'manual': // Kein Auto-Sync — Timer stoppen await p.setInt('cloud_interval', 0); // alten Timer deaktivieren await SyncService.starteAutoSyncTimer(); break; case 'interval': // Intervall-Timer mit cloud_interval_stunden starten await p.setInt('cloud_interval', _cloudIntervallStunden); await SyncService.starteAutoSyncTimer(); break; } } /// Setzt das Cloud-Intervall (nur für Modus 3: interval). Future _intervallSetzen(int stunden) async { setState(() => _cloudIntervallStunden = stunden); final p = await SharedPreferences.getInstance(); await p.setInt('cloud_interval_stunden', stunden); // Timer neu starten (der cloud_interval-Wert wird aktualisiert) await p.setInt('cloud_interval', stunden); await SyncService.starteAutoSyncTimer(); } Future _sleepDefaultSetzen(int min) async { setState(() => _sleepDefaultMin = min); final p = await SharedPreferences.getInstance(); await p.setInt('sleep_timer_default_min', min); } Future _geschwindigkeitSetzen(double wert) async { setState(() => _geschwindigkeit = wert); final p = await SharedPreferences.getInstance(); await p.setDouble('playback_speed', wert); await PlayerService().setGeschwindigkeit(wert); } Future _akzentSetzen(Color farbe) async { setState(() => _akzent = farbe); MeloTheme.akzent = farbe; // live — main.dart rebuildet das Theme final p = await SharedPreferences.getInstance(); await p.setString( UserEffekt.akzentKey(AuthService().benutzer), _hexVonFarbe(farbe)); } String _hexVonFarbe(Color c) { String hx(int w) => w.toRadixString(16).padLeft(2, '0').toUpperCase(); return '${hx((c.r * 255).round())}${hx((c.g * 255).round())}${hx((c.b * 255).round())}'; } String get _speicherText { final anzahl = _speicherInfo['anzahl'] ?? 0; final bytes = _speicherInfo['bytes'] ?? 0; if (bytes >= 1073741824) { return '$anzahl Songs · ${(bytes / 1073741824).toStringAsFixed(2)} GB'; } return '$anzahl Songs · ${(bytes / 1048576).toStringAsFixed(1)} MB'; } @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.settings, color: MeloTheme.rot, size: 20), SizedBox(width: 10), Text( 'Einstellungen', style: TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.w600), ), ], ), ), body: ListView( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), children: [ // ─── Sektion: Verbindung ─── _sektionHeader('Verbindung'), _einstellungsKachel( icon: Icons.cloud_outlined, titel: 'Cloud Sync', untertitel: 'Auto-Sync, Upload & Download', onTap: () { Navigator.push( context, MaterialPageRoute( builder: (_) => CloudScreen(cloud: CloudService()), ), ); }, ), _einstellungsKachel( icon: Icons.dns_outlined, titel: 'Musikserver', untertitel: 'Navidrome Musik-Server verbinden', onTap: () { // Signal zum Öffnen des Server-Browsers Navigator.pop(context, 'server'); }, ), const SizedBox(height: 8), // ─── Sektion: Cloud-Sync ─── _sektionHeader('Cloud-Sync'), _einstellungsGruppe([ _gruppenZeile( label: 'Sync-Modus', wert: _syncModusText, inhalt: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _modusRadioTile('⚡ Echtzeit', 'realtime', 'Änderungen sofort synchronisieren (Push)'), _modusRadioTile('👆 Manuell', 'manual', 'Nur auf Knopfdruck synchronisieren'), _modusRadioTile('🗓️ Zeit-Intervall', 'interval', 'Automatisch nach Zeitplan synchronisieren'), ], ), ), if (_syncModus == 'interval') ...[ const Divider(color: MeloTheme.dunkel2, height: 1), _gruppenZeile( label: 'Intervall', wert: _intervallOptionen[_cloudIntervallStunden] ?? '${_cloudIntervallStunden}h', inhalt: DropdownButtonFormField( initialValue: _intervallOptionen.containsKey(_cloudIntervallStunden) ? _cloudIntervallStunden : 48, dropdownColor: MeloTheme.dunkel1, style: const TextStyle(color: Colors.white, fontSize: 13), decoration: const InputDecoration( contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 8), border: OutlineInputBorder(), ), items: _intervallOptionen.entries.map((e) { return DropdownMenuItem( value: e.key, child: Text(e.value), ); }).toList(), onChanged: (v) { if (v != null) _intervallSetzen(v); }, ), ), ], const Divider(color: MeloTheme.dunkel2, height: 1), _gruppenZeile( label: 'Sleep-Timer-Standard', wert: _sleepDefaultMin == 0 ? 'Aus' : '$_sleepDefaultMin Min', inhalt: Row(children: [ _wahlChip('Aus', _sleepDefaultMin == 0, () => _sleepDefaultSetzen(0)), _wahlChip('15', _sleepDefaultMin == 15, () => _sleepDefaultSetzen(15)), _wahlChip('30', _sleepDefaultMin == 30, () => _sleepDefaultSetzen(30)), _wahlChip('45', _sleepDefaultMin == 45, () => _sleepDefaultSetzen(45)), _wahlChip('60', _sleepDefaultMin == 60, () => _sleepDefaultSetzen(60)), ]), ), const Divider(color: MeloTheme.dunkel2, height: 1), _gruppenZeile( label: 'Wiedergabegeschwindigkeit', wert: '${_geschwindigkeit.toStringAsFixed(2)}x', inhalt: Slider( value: _geschwindigkeit, min: 0.5, max: 2.0, divisions: 6, activeColor: MeloTheme.akzent, onChanged: (v) => setState(() => _geschwindigkeit = v), onChangeEnd: _geschwindigkeitSetzen, ), ), ]), const SizedBox(height: 8), // ─── Sektion: Aussehen ─── _sektionHeader('Aussehen'), _einstellungsGruppe([ _gruppenZeile( label: 'Akzentfarbe', wert: 'Für ${AuthService().benutzer}', inhalt: Row(children: [ for (final farbe in _akzentFarben) ...[ _akzentPunkt(farbe, _akzent == farbe, () => _akzentSetzen(farbe)), ], ]), ), ]), const SizedBox(height: 8), // ─── Sektion: Daten & Privatsphäre ─── _sektionHeader('Daten & Privatsphäre'), _infoKachel( icon: Icons.storage_outlined, titel: 'Lokale Musik', wert: _speicherText, ), _einstellungsKachel( icon: Icons.auto_graph, titel: 'Recap (Woche/Monat/Jahr)', untertitel: 'Deine Hörstatistiken – wie Spotify Wrapped', onTap: () { Navigator.push( context, MaterialPageRoute(builder: (_) => const RecapScreen()), ); }, ), const SizedBox(height: 8), // ─── Sektion: Erweitert (Technik) ─── _sektionHeader('Erweitert'), _einstellungsKachel( icon: Icons.handyman_outlined, titel: 'Erweitert', untertitel: 'Logs, Diagnose, Entwickler & Scanner', onTap: () async { // Erweitert-Screen öffnen. „Musik scannen“ (onScan) schließt den // Erweitert-Screen mit dem Ergebnis 'scanner' → hier awaiten und // das Ergebnis an MeloHome weiterreichen, das den Scanner startet. final result = await Navigator.push( context, MaterialPageRoute( builder: (_) => ErweitertScreen( onScan: () { Navigator.pop(context, 'scanner'); }, ), ), ); if (!mounted || result != 'scanner') return; Navigator.pop(this.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), // ─── Sektion: Info ─── _sektionHeader('Info'), Container( decoration: BoxDecoration( color: MeloTheme.dunkel1, borderRadius: BorderRadius.circular(14), border: Border.all(color: MeloTheme.dunkel2), ), child: Column( children: [ _infoZeile('Version', '2.52.3'), const Divider(color: MeloTheme.dunkel2, height: 1), _infoZeile('Theme', 'Schwarz + Akzent'), const Divider(color: MeloTheme.dunkel2, height: 1), _infoZeile('Auth', AppConfig.authUrl), const Divider(color: MeloTheme.dunkel2, height: 1), _infoZeile('Cloud', AppConfig.cloudUrl), ], ), ), const SizedBox(height: 24), // ─── Abmelden ─── SizedBox( width: double.infinity, height: 48, child: OutlinedButton.icon( onPressed: () { Navigator.pop(context, 'logout'); }, icon: const Icon(Icons.logout, size: 18), label: const Text('Abmelden', style: TextStyle(fontSize: 14)), style: OutlinedButton.styleFrom( foregroundColor: MeloTheme.rot, side: const BorderSide(color: MeloTheme.rot), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)), ), ), ), const SizedBox(height: 32), ], ), ); } 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), ], ), ), ), ), ); } /// Informations-Kachel (nicht antippbar) — z.B. Speicher-Info. Widget _infoKachel({ required IconData icon, required String titel, required String wert, }) { return Padding( padding: const EdgeInsets.only(bottom: 8), child: Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), decoration: BoxDecoration( color: MeloTheme.dunkel1, 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: Text( titel, style: const TextStyle(color: Colors.white, fontSize: 14, fontWeight: FontWeight.w500), ), ), Text( wert, style: const TextStyle(color: MeloTheme.textSekundaer, fontSize: 12), ), ], ), ), ); } /// Gruppe aus mehreren Einstellungs-Zeilen in einer abgerundeten Box. Widget _einstellungsGruppe(List kinder) { return Container( margin: const EdgeInsets.only(bottom: 8), decoration: BoxDecoration( color: MeloTheme.dunkel1, borderRadius: BorderRadius.circular(14), border: Border.all(color: MeloTheme.dunkel2), ), child: Column(children: kinder), ); } Widget _gruppenZeile({ required String label, String? wert, required Widget inhalt, }) { return Padding( padding: const EdgeInsets.fromLTRB(16, 12, 16, 12), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Expanded( child: Text( label, style: const TextStyle(color: Colors.white, fontSize: 14, fontWeight: FontWeight.w500), ), ), if (wert != null) Text( wert, style: TextStyle(color: MeloTheme.akzent, fontSize: 13, fontWeight: FontWeight.w600), ), ], ), const SizedBox(height: 10), inhalt, ], ), ); } Widget _wahlChip(String label, bool aktiv, VoidCallback onTap) { return GestureDetector( onTap: onTap, child: Container( margin: const EdgeInsets.only(right: 8), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 7), decoration: BoxDecoration( color: aktiv ? MeloTheme.akzent : MeloTheme.dunkel2, borderRadius: BorderRadius.circular(16), ), child: Text( label, style: TextStyle( fontSize: 12, color: aktiv ? Colors.white : MeloTheme.textSekundaer, ), ), ), ); } Widget _akzentPunkt(Color farbe, bool aktiv, VoidCallback onTap) { return GestureDetector( onTap: onTap, child: Container( margin: const EdgeInsets.only(right: 10), width: 34, height: 34, decoration: BoxDecoration( shape: BoxShape.circle, color: farbe, border: Border.all( color: aktiv ? Colors.white : Colors.transparent, width: 2.5, ), ), child: aktiv ? const Icon(Icons.check, size: 18, color: Colors.white) : null, ), ); } Widget _infoZeile(String label, String wert) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), child: Row( children: [ Text( label, style: const TextStyle(color: MeloTheme.textSekundaer, fontSize: 13), ), const Spacer(), Text( wert, style: const TextStyle(color: Colors.white70, fontSize: 13), ), ], ), ); } /// Anzeigetext für den aktuellen Sync-Modus. String get _syncModusText { switch (_syncModus) { case 'realtime': return '⚡ Echtzeit'; case 'manual': return '👆 Manuell'; case 'interval': return '🗓️ Zeit'; default: return '⚡ Echtzeit'; } } /// Radio-ähnliches Kachel-Widget für Sync-Modus-Auswahl. Widget _modusRadioTile(String titel, String wert, String beschreibung) { final ausgewaehlt = _syncModus == wert; return Padding( padding: const EdgeInsets.only(bottom: 4), child: InkWell( onTap: () => _syncModusSetzen(wert), borderRadius: BorderRadius.circular(10), child: Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8), decoration: BoxDecoration( color: ausgewaehlt ? MeloTheme.akzent.withAlpha(25) : Colors.transparent, borderRadius: BorderRadius.circular(10), border: Border.all( color: ausgewaehlt ? MeloTheme.akzent : MeloTheme.dunkel2, width: ausgewaehlt ? 1.5 : 1, ), ), child: Row( children: [ Icon( ausgewaehlt ? Icons.radio_button_checked : Icons.radio_button_unchecked, color: ausgewaehlt ? MeloTheme.akzent : MeloTheme.textSekundaer, size: 18, ), const SizedBox(width: 10), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( titel, style: TextStyle( color: ausgewaehlt ? Colors.white : MeloTheme.textSekundaer, fontSize: 13, fontWeight: FontWeight.w500, ), ), Text( beschreibung, style: TextStyle( color: ausgewaehlt ? Colors.white70 : MeloTheme.textSekundaer, fontSize: 11, ), ), ], ), ), ], ), ), ), ); } }