import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../library/database.dart'; import '../library/permissions.dart'; import '../services/cache_manager.dart'; import '../services/navidrome_service.dart'; import '../services/offline_mode.dart'; import 'library_stats.dart'; /// Settings-Tab: Bibliotheks-Statistik, Berechtigungen, Navidrome, Über Melo. class SettingsScreen extends StatefulWidget { const SettingsScreen({super.key}); @override State createState() => _SettingsScreenState(); } class _SettingsScreenState extends State { late final NavidromeService _navidrome = NavidromeService(); late final CacheManager _cache = CacheManager(); int _cacheSize = 0; @override void initState() { super.initState(); _navidrome.ladeGespeicherteZugangsdaten(); _cache.init().then((_) => _updateCacheSize()); } Future _updateCacheSize() async { final size = await _cache.getCacheSize(); if (mounted) setState(() => _cacheSize = size); } @override Widget build(BuildContext context) { final db = context.read(); return Scaffold( appBar: AppBar(title: const Text('Settings')), body: ListView( children: [ const _SectionLabel('Bibliothek'), StreamBuilder>( stream: db.watchSongs(), builder: (context, snapshot) { final songs = snapshot.data ?? const []; return Column( children: [ ListTile( leading: const Icon(Icons.library_music), title: Text('${songs.length} Songs in der Bibliothek'), ), ListTile( leading: const Icon(Icons.schedule), title: Text('${formatTotalDuration(songs)} Gesamtspieldauer'), ), ], ); }, ), const Divider(height: 1), const _SectionLabel('Musikserver'), ListTile( leading: const Icon(Icons.cloud_circle), title: const Text('🌐 Navidrome'), subtitle: _navidrome.istVerbunden ? const Text('✅ Verbunden') : const Text('Nicht verbunden'), trailing: _navidrome.istVerbunden ? IconButton( icon: const Icon(Icons.logout), onPressed: () => _trennNavidrome(), ) : IconButton( icon: const Icon(Icons.login), onPressed: () => _zeigeNavidromeDialog(), ), ), ListTile( leading: const Icon(Icons.wifi_off), title: const Text('📴 Offline-Modus'), subtitle: const Text('Nur gecachte Lieder abspielen'), trailing: Consumer( builder: (context, offlineMode, _) { return Switch( value: offlineMode.enabled, onChanged: (value) => offlineMode.setEnabled(value), ); }, ), ), ListTile( leading: const Icon(Icons.storage), title: const Text('🗄️ Cache-Speicher'), subtitle: Text(_formatBytes(_cacheSize)), trailing: _cacheSize > 0 ? IconButton( tooltip: 'Cache löschen', icon: const Icon(Icons.delete), onPressed: () => _clearCache(), ) : null, ), const Divider(height: 1), const _SectionLabel('Berechtigungen'), ListTile( leading: const Icon(Icons.mic_none), title: const Text('Musik-Berechtigung'), trailing: TextButton( onPressed: openMusicPermissionSettings, child: const Text('Einstellungen öffnen'), ), ), const Divider(height: 1), const _SectionLabel('Über Melo'), const ListTile( leading: Icon(Icons.info_outline), title: Text('Melo'), subtitle: Text('Deine Musik. Offline. Kein Abo.'), ), ], ), ); } void _zeigeNavidromeDialog() { final urlCtrl = TextEditingController(); final userCtrl = TextEditingController(); final passCtrl = TextEditingController(); bool verbindet = false; String? fehler; showDialog( context: context, builder: (ctx) => StatefulBuilder( builder: (ctx, setDialogState) => AlertDialog( backgroundColor: Colors.grey.shade900, title: const Text('🌐 Navidrome Verbinden', style: TextStyle(color: Colors.white, fontSize: 16)), content: Column( mainAxisSize: MainAxisSize.min, children: [ TextField( controller: urlCtrl, style: const TextStyle(color: Colors.white), decoration: const InputDecoration( labelText: 'Server-URL', hintText: 'https://musik.example.com', labelStyle: TextStyle(color: Colors.grey), hintStyle: TextStyle(color: Colors.grey), border: OutlineInputBorder(), ), ), const SizedBox(height: 8), TextField( controller: userCtrl, style: const TextStyle(color: Colors.white), decoration: const InputDecoration( labelText: 'Benutzer', labelStyle: TextStyle(color: Colors.grey), border: OutlineInputBorder(), ), ), const SizedBox(height: 8), TextField( controller: passCtrl, style: const TextStyle(color: Colors.white), obscureText: true, decoration: const InputDecoration( labelText: 'Passwort', labelStyle: TextStyle(color: Colors.grey), border: OutlineInputBorder(), ), ), if (fehler != null) ...[ const SizedBox(height: 8), Text(fehler!, style: const TextStyle(color: Colors.red, fontSize: 12)), ], if (verbindet) const Padding( padding: EdgeInsets.only(top: 12), child: SizedBox( width: 20, height: 20, child: CircularProgressIndicator( strokeWidth: 2, color: Colors.redAccent), ), ), ], ), actions: [ TextButton( onPressed: () => Navigator.pop(ctx), child: const Text('Abbrechen')), TextButton( onPressed: verbindet ? null : () async { setDialogState(() => verbindet = true); _navidrome.setCredentials( urlCtrl.text, userCtrl.text, passCtrl.text); final ok = await _navidrome.ping(); setDialogState(() => verbindet = false); if (ok && ctx.mounted) { await _navidrome.speichereZugangsdaten( urlCtrl.text, userCtrl.text, passCtrl.text); if (ctx.mounted) Navigator.pop(ctx); if (mounted) setState(() {}); } else if (ctx.mounted) { setDialogState(() => fehler = '❌ Verbindung fehlgeschlagen\nPrüfe URL + Zugangsdaten'); } }, child: const Text('Verbinden', style: TextStyle(color: Colors.redAccent)), ), ], ), ), ).then((_) { urlCtrl.dispose(); userCtrl.dispose(); passCtrl.dispose(); }); } Future _trennNavidrome() async { await _navidrome.loescheZugangsdaten(); if (mounted) setState(() {}); } Future _clearCache() async { await _cache.clearCache(); if (mounted) { setState(() => _cacheSize = 0); ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Cache gelöscht')), ); } } String _formatBytes(int bytes) { if (bytes < 1024) return '$bytes B'; if (bytes < 1024 * 1024) return '${(bytes / 1024).toStringAsFixed(1)} KB'; return '${(bytes / (1024 * 1024)).toStringAsFixed(1)} MB'; } } /// Kleine graue Überschrift über einer Einstellungs-Gruppe. class _SectionLabel extends StatelessWidget { const _SectionLabel(this.label); final String label; @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.fromLTRB(16, 16, 16, 4), child: Text( label, style: const TextStyle( color: Colors.white54, fontSize: 12, fontWeight: FontWeight.bold, ), ), ); } }