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
Executable
+36
@@ -0,0 +1,36 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Git Pre-Commit Hook: Melo App Qualitäts-Gate
|
||||||
|
# Läuft automatisch vor jedem git commit
|
||||||
|
|
||||||
|
export FLUTTER="/home/dustin/development/flutter/bin/flutter"
|
||||||
|
export DART="/home/dustin/development/flutter/bin/dart"
|
||||||
|
export PATH="$HOME/development/flutter/bin:$PATH"
|
||||||
|
|
||||||
|
echo "🔍 Melo App Qualitäts-Gate gestartet..."
|
||||||
|
|
||||||
|
# 1. Dart Analyse
|
||||||
|
echo -n " Prüfe: flutter analyze ... "
|
||||||
|
cd "$(git rev-parse --show-toplevel)" || exit 1
|
||||||
|
ANALYZE_OUTPUT=$($FLUTTER analyze 2>&1)
|
||||||
|
if echo "$ANALYZE_OUTPUT" | grep -q "No issues found"; then
|
||||||
|
echo "✅"
|
||||||
|
else
|
||||||
|
echo "❌ FEHLER!"
|
||||||
|
echo "$ANALYZE_OUTPUT" | tail -20
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 2. Tests
|
||||||
|
echo -n " Prüfe: flutter test ... "
|
||||||
|
TEST_OUTPUT=$($FLUTTER test 2>&1)
|
||||||
|
if echo "$TEST_OUTPUT" | grep -q "All tests passed"; then
|
||||||
|
echo "✅"
|
||||||
|
elif echo "$TEST_OUTPUT" | grep -q "No tests found"; then
|
||||||
|
echo "⚠️ Keine Tests vorhanden (überspringe)"
|
||||||
|
else
|
||||||
|
echo "❌ Tests fehlgeschlagen!"
|
||||||
|
echo "$TEST_OUTPUT" | tail -20
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "✅ Alle Prüfungen bestanden! Commit wird freigegeben."
|
||||||
@@ -5,6 +5,13 @@ Format angelehnt an [Keep a Changelog](https://keepachangelog.com/de/1.0.0/).
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- 🌐 Navidrome-Integration (Musikserver-Verbindung): Settings-Tab bietet Login/Logout für Subsonic-API-kompatible Server
|
||||||
|
- Navidrome-Service mit sichere Credential-Speicherung (Secure Storage + SharedPreferences)
|
||||||
|
- Dependencies für Navidrome hinzugefügt: http, crypto, shared_preferences, flutter_secure_storage
|
||||||
|
|
||||||
|
## [1.0.0-alpha]
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
- Playlisten-Tab: Favoriten-Kurzeinstieg oben, Liste aller Playlisten mit Songanzahl,
|
- Playlisten-Tab: Favoriten-Kurzeinstieg oben, Liste aller Playlisten mit Songanzahl,
|
||||||
Erstellen-Dialog (FAB), Löschen mit Bestätigung
|
Erstellen-Dialog (FAB), Löschen mit Bestätigung
|
||||||
@@ -44,6 +51,15 @@ Künstler-/Alben-Browsing und automatischem Fortsetzen der Wiedergabeposition.**
|
|||||||
- "Zuletzt hinzugefügt" aus der alten Startseite ist jetzt Teil der Bibliothek (horizontale Leiste oben)
|
- "Zuletzt hinzugefügt" aus der alten Startseite ist jetzt Teil der Bibliothek (horizontale Leiste oben)
|
||||||
- MiniPlayer wird auf dem Player-Tab ausgeblendet (dort läuft schon die volle Ansicht)
|
- MiniPlayer wird auf dem Player-Tab ausgeblendet (dort läuft schon die volle Ansicht)
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- 🐛 Der APK-Build funktioniert jetzt überhaupt. Drei Dinge waren nötig:
|
||||||
|
`compileSdk = 37` und `kotlin.jvm.target.validation.mode=warning` existierten
|
||||||
|
bisher nur lokal auf dem Server und fehlten im Repo — ohne sie bricht der Build
|
||||||
|
ab, weil `permission_handler` SDK 37 verlangt. Zusätzlich hebt ein Gradle-
|
||||||
|
Workaround jetzt Plugin-Module mit zu altem `compileSdk` auf 36 an:
|
||||||
|
`on_audio_query_android` kompiliert gegen SDK 33, seine AndroidX-Abhängigkeiten
|
||||||
|
verlangen aber mindestens 34 (16 Metadaten-Konflikte).
|
||||||
|
|
||||||
### Removed
|
### Removed
|
||||||
- Alte Startseite (`home_screen.dart`) entfernt — Funktionalität in die Bibliothek verschoben
|
- Alte Startseite (`home_screen.dart`) entfernt — Funktionalität in die Bibliothek verschoben
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,32 @@
|
|||||||
# Arbeitsweise (Melo-Projekt)
|
# CLAUDE.md — Melo Projekt (Server-Klon)
|
||||||
|
|
||||||
|
## 🎯 Projekt
|
||||||
|
- **Melo**: Musik-App (Flutter, Android) — Nachfolger des alten "Melo"-Projekts
|
||||||
|
- Design: Schwarz (#0B0B10) / Rot (#c0392b) — Dark Theme, 5 Tabs
|
||||||
|
- Features (frühere Version): Player, Playlisten, Favoriten, Recaps, Cloud-Sync (JWT), Offline-Tombstones, Echtzeit-Sync (SSE)
|
||||||
|
- Git: http://git.baka-net.de/dustin/Melo.git (Repo heißt "Melo"!)
|
||||||
|
- Der Code hier ist ein BUILD-KLON für den Server (Builds + Tests)
|
||||||
|
|
||||||
|
## ⚙️ Build-Umgebung (Server!)
|
||||||
|
- Android SDK: ANDROID_HOME=/home/dustin/Android
|
||||||
|
- Flutter: /home/dustin/development/flutter/bin/flutter
|
||||||
|
- Build: flutter build apk --target-platform android-arm64
|
||||||
|
- Bekannte Fixes: compileSdk=37 gesetzt (in android/app/build.gradle.kts), kotlin.jvm.target.validation.mode=warning (gradle.properties)
|
||||||
|
- ACHTUNG: permission_handler + on_audio_query verlangen SDK 37 — kein echtes android-37 SDK (Kopie von 36!)
|
||||||
|
|
||||||
|
## 🛡️ Sicherheitsregeln
|
||||||
|
- NIE Passwörter/API-Keys in Code oder Commits
|
||||||
|
- Keine .env committen
|
||||||
|
- Bei Unsicherheit: Fragen statt raten
|
||||||
|
|
||||||
|
## 📋 CHANGELOG-PFLICHT
|
||||||
|
- Nach JEDER Aufgabe → CHANGELOG.md aktualisieren (neueste zuerst, einfach erklärt!)
|
||||||
|
- Format: ✅ Was gemacht · 🔧 Details · 🐛 Fixes
|
||||||
|
- Changelog IMMER mit dem Code committen!
|
||||||
|
|
||||||
|
## 🤖 Arbeitsweise
|
||||||
- Vor jedem Commit: @code-reviewer die Änderungen reviewen lassen
|
- Vor jedem Commit: @code-reviewer die Änderungen reviewen lassen
|
||||||
- Bei neuen Features: @planner für den Plan, @tdd-guide für Tests
|
- Bei neuen Features: @planner für den Plan, @tdd-guide für Tests
|
||||||
- Bei Sicherheitsfragen: @security-reviewer
|
- Bei Sicherheitsfragen: @security-reviewer
|
||||||
- Ponytail-Prinzip: minimale, robuste Lösungen bevorzugen
|
- Ponytail-Prinzip: minimale, robuste Lösungen bevorzugen
|
||||||
|
- Nach Abschluss: commit + push nach git.baka-net.de
|
||||||
|
|||||||
@@ -7,6 +7,15 @@
|
|||||||
|
|
||||||
# The following line activates a set of recommended lints for Flutter apps,
|
# The following line activates a set of recommended lints for Flutter apps,
|
||||||
# packages, and plugins designed to encourage good coding practices.
|
# packages, and plugins designed to encourage good coding practices.
|
||||||
|
analyzer:
|
||||||
|
exclude:
|
||||||
|
- build/**
|
||||||
|
- android/**
|
||||||
|
- ios/**
|
||||||
|
- web/**
|
||||||
|
- windows/**
|
||||||
|
- macos/**
|
||||||
|
- linux/**
|
||||||
include: package:flutter_lints/flutter.yaml
|
include: package:flutter_lints/flutter.yaml
|
||||||
|
|
||||||
linter:
|
linter:
|
||||||
|
|||||||
@@ -6,7 +6,9 @@ plugins {
|
|||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "de.baka.melo"
|
namespace = "de.baka.melo"
|
||||||
compileSdk = flutter.compileSdkVersion
|
// permission_handler_android verlangt SDK 37; android-37 ist lokal eine Kopie
|
||||||
|
// von 36 (ApiLevel=36) → jvmTarget-Validierung muss auf "warning" stehen.
|
||||||
|
compileSdk = 37
|
||||||
ndkVersion = flutter.ndkVersion
|
ndkVersion = flutter.ndkVersion
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
|
|||||||
@@ -22,6 +22,13 @@ subprojects {
|
|||||||
if (androidExt != null && androidExt.namespace == null) {
|
if (androidExt != null && androidExt.namespace == null) {
|
||||||
androidExt.namespace = project.group.toString()
|
androidExt.namespace = project.group.toString()
|
||||||
}
|
}
|
||||||
|
// on_audio_query_android kompiliert gegen SDK 33, seine AndroidX-
|
||||||
|
// Abhängigkeiten (fragment, window, ...) verlangen aber mindestens 34.
|
||||||
|
// Zu niedrige Library-Module auf 36 anheben (echtes SDK, nicht die 37-Kopie).
|
||||||
|
val libCompileSdk = androidExt?.compileSdkVersion?.removePrefix("android-")?.toIntOrNull()
|
||||||
|
if (libCompileSdk != null && libCompileSdk < 34) {
|
||||||
|
androidExt.compileSdkVersion(36)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
subprojects {
|
subprojects {
|
||||||
|
|||||||
@@ -4,3 +4,4 @@ android.useAndroidX=true
|
|||||||
android.newDsl=false
|
android.newDsl=false
|
||||||
# This builtInKotlin flag was added by the Flutter template
|
# This builtInKotlin flag was added by the Flutter template
|
||||||
android.builtInKotlin=false
|
android.builtInKotlin=false
|
||||||
|
kotlin.jvm.target.validation.mode=warning
|
||||||
|
|||||||
@@ -0,0 +1,178 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:math';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:crypto/crypto.dart';
|
||||||
|
import 'package:http/http.dart' as http;
|
||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||||
|
|
||||||
|
class SubsonicSong {
|
||||||
|
final String id;
|
||||||
|
final String titel;
|
||||||
|
final String kuenstler;
|
||||||
|
final String album;
|
||||||
|
final int dauerSekunden;
|
||||||
|
final String? coverId;
|
||||||
|
|
||||||
|
const SubsonicSong({
|
||||||
|
required this.id,
|
||||||
|
required this.titel,
|
||||||
|
this.kuenstler = '',
|
||||||
|
this.album = '',
|
||||||
|
this.dauerSekunden = 0,
|
||||||
|
this.coverId,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory SubsonicSong.fromJson(Map<String, dynamic> j) => SubsonicSong(
|
||||||
|
id: j['id'] as String,
|
||||||
|
titel: j['title'] as String? ?? 'Unbekannt',
|
||||||
|
kuenstler: j['artist'] as String? ?? '',
|
||||||
|
album: j['album'] as String? ?? '',
|
||||||
|
dauerSekunden: (j['duration'] as int?) ?? 0,
|
||||||
|
coverId: j['coverArt'] as String?,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class SubsonicAlbum {
|
||||||
|
final String id;
|
||||||
|
final String name;
|
||||||
|
final String? coverId;
|
||||||
|
final int songCount;
|
||||||
|
|
||||||
|
const SubsonicAlbum({
|
||||||
|
required this.id,
|
||||||
|
required this.name,
|
||||||
|
this.coverId,
|
||||||
|
this.songCount = 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory SubsonicAlbum.fromJson(Map<String, dynamic> j) => SubsonicAlbum(
|
||||||
|
id: j['id'] as String,
|
||||||
|
name: j['name'] as String? ?? j['title'] as String? ?? 'Unbekannt',
|
||||||
|
coverId: j['coverArt'] as String?,
|
||||||
|
songCount: (j['songCount'] as int?) ?? 0,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class NavidromeService {
|
||||||
|
final FlutterSecureStorage _secure = const FlutterSecureStorage();
|
||||||
|
|
||||||
|
String _serverUrl = '';
|
||||||
|
String _user = '';
|
||||||
|
String _password = '';
|
||||||
|
String _salt = '';
|
||||||
|
String? _token;
|
||||||
|
|
||||||
|
static const _version = '1.16.1';
|
||||||
|
static const _client = 'Melo';
|
||||||
|
|
||||||
|
void setCredentials(String url, String user, String password) {
|
||||||
|
_serverUrl = url.endsWith('/') ? url.substring(0, url.length - 1) : url;
|
||||||
|
_user = user;
|
||||||
|
_password = password;
|
||||||
|
final rng = Random.secure();
|
||||||
|
_salt = base64Encode(List.generate(16, (_) => rng.nextInt(256)));
|
||||||
|
_token = md5.convert(utf8.encode(_password + _salt)).toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> ladeGespeicherteZugangsdaten() async {
|
||||||
|
try {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
final url = prefs.getString('navidrome_url');
|
||||||
|
final user = prefs.getString('navidrome_user');
|
||||||
|
final pass = await _secure.read(key: 'navidrome_pass');
|
||||||
|
if (url != null && user != null && pass != null && url.isNotEmpty) {
|
||||||
|
setCredentials(url, user, pass);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('Fehler beim Laden der Navidrome-Zugangsdaten: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> speichereZugangsdaten(String url, String user, String password) async {
|
||||||
|
setCredentials(url, user, password);
|
||||||
|
try {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
await prefs.setString('navidrome_url', url);
|
||||||
|
await prefs.setString('navidrome_user', user);
|
||||||
|
await _secure.write(key: 'navidrome_pass', value: password);
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('Fehler beim Speichern der Navidrome-Zugangsdaten: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> loescheZugangsdaten() async {
|
||||||
|
_serverUrl = '';
|
||||||
|
_user = '';
|
||||||
|
_password = '';
|
||||||
|
_salt = '';
|
||||||
|
_token = null;
|
||||||
|
try {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
await prefs.remove('navidrome_url');
|
||||||
|
await prefs.remove('navidrome_user');
|
||||||
|
await _secure.delete(key: 'navidrome_pass');
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('Fehler beim Löschen der Navidrome-Zugangsdaten: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool get istVerbunden => _serverUrl.isNotEmpty && _user.isNotEmpty;
|
||||||
|
|
||||||
|
Uri _uri(String endpoint, [Map<String, String>? extra]) {
|
||||||
|
final params = {
|
||||||
|
'u': _user,
|
||||||
|
't': _token!,
|
||||||
|
's': _salt,
|
||||||
|
'v': _version,
|
||||||
|
'c': _client,
|
||||||
|
'f': 'json',
|
||||||
|
};
|
||||||
|
if (extra != null) params.addAll(extra);
|
||||||
|
return Uri.parse('$_serverUrl/rest/$endpoint').replace(queryParameters: params);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> ping() async {
|
||||||
|
try {
|
||||||
|
final r = await http.get(_uri('ping.view')).timeout(const Duration(seconds: 10));
|
||||||
|
return r.statusCode == 200;
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('Navidrome Ping Fehler: $e');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<SubsonicAlbum>> getAlben({int anzahl = 50}) async {
|
||||||
|
try {
|
||||||
|
final r = await http.get(_uri('getAlbumList.view', {'type': 'newest', 'size': '$anzahl'})).timeout(const Duration(seconds: 15));
|
||||||
|
if (r.statusCode != 200) return [];
|
||||||
|
final data = jsonDecode(r.body);
|
||||||
|
final list = data['subsonic-response']?['albumList']?['album'] as List? ?? [];
|
||||||
|
return list.map((j) => SubsonicAlbum.fromJson(j)).toList();
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('Navidrome getAlben Fehler: $e');
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<SubsonicSong>> getSongs(String albumId) async {
|
||||||
|
try {
|
||||||
|
final r = await http.get(_uri('getAlbum.view', {'id': albumId})).timeout(const Duration(seconds: 15));
|
||||||
|
if (r.statusCode != 200) return [];
|
||||||
|
final data = jsonDecode(r.body);
|
||||||
|
final songs = data['subsonic-response']?['album']?['song'] as List? ?? [];
|
||||||
|
return songs.map((j) => SubsonicSong.fromJson(j)).toList();
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('Navidrome getSongs Fehler: $e');
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Uri streamUrl(String songId) {
|
||||||
|
return _uri('stream.view', {'id': songId, 'format': 'raw'});
|
||||||
|
}
|
||||||
|
|
||||||
|
Uri? coverUrl(String coverId) {
|
||||||
|
return _uri('getCoverArt.view', {'id': coverId});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,15 +3,26 @@ import 'package:provider/provider.dart';
|
|||||||
|
|
||||||
import '../library/database.dart';
|
import '../library/database.dart';
|
||||||
import '../library/permissions.dart';
|
import '../library/permissions.dart';
|
||||||
|
import '../services/navidrome_service.dart';
|
||||||
import 'library_stats.dart';
|
import 'library_stats.dart';
|
||||||
|
|
||||||
/// Settings-Tab: Bibliotheks-Statistik, Berechtigungen, Über Melo.
|
/// Settings-Tab: Bibliotheks-Statistik, Berechtigungen, Navidrome, Über Melo.
|
||||||
///
|
class SettingsScreen extends StatefulWidget {
|
||||||
/// 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 {
|
|
||||||
const SettingsScreen({super.key});
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final db = context.read<MeloDb>();
|
final db = context.read<MeloDb>();
|
||||||
@@ -40,6 +51,24 @@ class SettingsScreen extends StatelessWidget {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
const Divider(height: 1),
|
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'),
|
const _SectionLabel('Berechtigungen'),
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Icons.mic_none),
|
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.
|
/// Kleine graue Überschrift über einer Einstellungs-Gruppe.
|
||||||
|
|||||||
@@ -9,7 +9,9 @@ import audio_service
|
|||||||
import audio_session
|
import audio_session
|
||||||
import device_info_plus
|
import device_info_plus
|
||||||
import file_picker_darwin
|
import file_picker_darwin
|
||||||
|
import flutter_secure_storage_darwin
|
||||||
import just_audio
|
import just_audio
|
||||||
|
import shared_preferences_foundation
|
||||||
import sqflite_darwin
|
import sqflite_darwin
|
||||||
|
|
||||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||||
@@ -17,6 +19,8 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
|||||||
AudioSessionPlugin.register(with: registry.registrar(forPlugin: "AudioSessionPlugin"))
|
AudioSessionPlugin.register(with: registry.registrar(forPlugin: "AudioSessionPlugin"))
|
||||||
DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin"))
|
DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin"))
|
||||||
FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin"))
|
FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin"))
|
||||||
|
FlutterSecureStorageDarwinPlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStorageDarwinPlugin"))
|
||||||
JustAudioPlugin.register(with: registry.registrar(forPlugin: "JustAudioPlugin"))
|
JustAudioPlugin.register(with: registry.registrar(forPlugin: "JustAudioPlugin"))
|
||||||
|
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
||||||
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
|
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
|
||||||
}
|
}
|
||||||
|
|||||||
+106
-2
@@ -218,7 +218,7 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "0.3.5+4"
|
version: "0.3.5+4"
|
||||||
crypto:
|
crypto:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: crypto
|
name: crypto
|
||||||
sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf
|
sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf
|
||||||
@@ -382,6 +382,54 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.0.0"
|
version: "6.0.0"
|
||||||
|
flutter_secure_storage:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: flutter_secure_storage
|
||||||
|
sha256: "15e8c8fe269fdf7d469b23008ab3df521c8b826ed345820532364c31bdebace6"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "11.0.0"
|
||||||
|
flutter_secure_storage_darwin:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: flutter_secure_storage_darwin
|
||||||
|
sha256: ac6d76a752de0cd738334eb4b21743fc4943f449f5b6e308f18838b048c02ac0
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.4.0"
|
||||||
|
flutter_secure_storage_linux:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: flutter_secure_storage_linux
|
||||||
|
sha256: "76fa9c841b3b1619fc5b5bc36efc7d158fa2356f223b6caeb1d0c80a54168546"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.0.2"
|
||||||
|
flutter_secure_storage_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: flutter_secure_storage_platform_interface
|
||||||
|
sha256: "788060052712555182aba55ecb5f8b6e5cb9cfe8f776c83249a61fe3ce877db4"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.3"
|
||||||
|
flutter_secure_storage_web:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: flutter_secure_storage_web
|
||||||
|
sha256: "073a62b3aeb866ab4ce795f960413948e51e5a42a9b0c8333b6daf5bb3208a1c"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.1"
|
||||||
|
flutter_secure_storage_windows:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: flutter_secure_storage_windows
|
||||||
|
sha256: "471951813a97006d899db4948acc654a4f28c440083ea08178935ce20b173ec1"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.2.2"
|
||||||
flutter_test:
|
flutter_test:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description: flutter
|
description: flutter
|
||||||
@@ -417,7 +465,7 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.2"
|
version: "2.0.2"
|
||||||
http:
|
http:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: http
|
name: http
|
||||||
sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412"
|
sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412"
|
||||||
@@ -856,6 +904,62 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.28.0"
|
version: "0.28.0"
|
||||||
|
shared_preferences:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: shared_preferences
|
||||||
|
sha256: c3025c5534b01739267eb7d76959bbc25a6d10f6988e1c2a3036940133dd10bf
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.5.5"
|
||||||
|
shared_preferences_android:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: shared_preferences_android
|
||||||
|
sha256: "0634e64bd719f89c012f392938e173521f535d3ecaf66558fa94a056d22b5cc7"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.4.27"
|
||||||
|
shared_preferences_foundation:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: shared_preferences_foundation
|
||||||
|
sha256: "4e7eaffc2b17ba398759f1151415869a34771ba11ebbccd1b0145472a619a64f"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.5.6"
|
||||||
|
shared_preferences_linux:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: shared_preferences_linux
|
||||||
|
sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.4.1"
|
||||||
|
shared_preferences_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: shared_preferences_platform_interface
|
||||||
|
sha256: "649dc798a33931919ea356c4305c2d1f81619ea6e92244070b520187b5140ef9"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.4.2"
|
||||||
|
shared_preferences_web:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: shared_preferences_web
|
||||||
|
sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.4.3"
|
||||||
|
shared_preferences_windows:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: shared_preferences_windows
|
||||||
|
sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.4.1"
|
||||||
shelf:
|
shelf:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -48,6 +48,10 @@ dependencies:
|
|||||||
permission_handler: ^13.0.1
|
permission_handler: ^13.0.1
|
||||||
device_info_plus: ^13.2.0
|
device_info_plus: ^13.2.0
|
||||||
on_audio_query: ^2.9.0
|
on_audio_query: ^2.9.0
|
||||||
|
http: ^1.1.0
|
||||||
|
crypto: ^3.0.3
|
||||||
|
shared_preferences: ^2.2.2
|
||||||
|
flutter_secure_storage: ^11.0.0
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|||||||
Reference in New Issue
Block a user