feat(navidrome): Musikserver-Integration (Subsonic API)
- NavidromeService mit Credential-Verwaltung (secure storage) - Navidrome-Settings in Settings-Tab: Login/Logout für Server-Verbindung - Dependencies hinzugefügt: http, crypto, shared_preferences, flutter_secure_storage - Tests: 70/70 grün - Analyzer: ✅ keine Fehler Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XJzQjUtnvYUtHdnTs3iCru
This commit is contained in:
co-authored by
Claude Haiku 4.5
parent
2163c83409
commit
06be0a7065
@@ -3,15 +3,26 @@ import 'package:provider/provider.dart';
|
||||
|
||||
import '../library/database.dart';
|
||||
import '../library/permissions.dart';
|
||||
import '../services/navidrome_service.dart';
|
||||
import 'library_stats.dart';
|
||||
|
||||
/// Settings-Tab: Bibliotheks-Statistik, Berechtigungen, Über Melo.
|
||||
///
|
||||
/// Kein Cloud-/Account-System vorhanden (Phase 2) — daher keine
|
||||
/// Platzhalter-Einträge für Profil/Sync/Geräte, die aktuell ins Leere laufen.
|
||||
class SettingsScreen extends StatelessWidget {
|
||||
/// Settings-Tab: Bibliotheks-Statistik, Berechtigungen, Navidrome, Über Melo.
|
||||
class SettingsScreen extends StatefulWidget {
|
||||
const SettingsScreen({super.key});
|
||||
|
||||
@override
|
||||
State<SettingsScreen> createState() => _SettingsScreenState();
|
||||
}
|
||||
|
||||
class _SettingsScreenState extends State<SettingsScreen> {
|
||||
late final NavidromeService _navidrome = NavidromeService();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_navidrome.ladeGespeicherteZugangsdaten();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final db = context.read<MeloDb>();
|
||||
@@ -40,6 +51,24 @@ class SettingsScreen extends StatelessWidget {
|
||||
},
|
||||
),
|
||||
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(),
|
||||
),
|
||||
),
|
||||
const Divider(height: 1),
|
||||
const _SectionLabel('Berechtigungen'),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.mic_none),
|
||||
@@ -60,6 +89,113 @@ class SettingsScreen extends StatelessWidget {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
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<void> _trennNavidrome() async {
|
||||
await _navidrome.loescheZugangsdaten();
|
||||
if (mounted) setState(() {});
|
||||
}
|
||||
}
|
||||
|
||||
/// Kleine graue Überschrift über einer Einstellungs-Gruppe.
|
||||
|
||||
Reference in New Issue
Block a user