v2.35 — Vollständiges Sync-System: Playlisten, Favoriten, Auto-Sync, Persistent Login, Benutzerdefinierte Namen

## Server (melo_cloud.py v3.0.0)
- Neue DB-Tabellen: user_playlists, user_playlist_songs, user_favorites, user_history, sync_meta
- Playlist-Endpunkte: GET/POST /api/cloud/playlists, GET /api/cloud/playlists/<id>, POST songs, DELETE songs, PUT positions
- Favoriten-Endpunkte: GET/POST /api/cloud/favorites, POST /api/cloud/favorites/toggle
- History-Endpunkte: GET/POST /api/cloud/history (mit Dedup)
- Rename-Endpunkt: POST /api/cloud/rename (custom_title/custom_artist)
- Sync-Endpunkte: POST /api/cloud/sync-all, GET /api/cloud/sync-status
- delete-all erweitert um Playlists + Favorites + History

## App
- db_helper.dart: Migration v4→v5 (cloud_id, cloud_title, cloud_artist, server_favorites, sync_metadata)
- auth_service.dart: Token-Validierung beim Start (online check + Offline-Fallback), Persistent Login
- cloud_service.dart: Neue Methoden für Playlists (CRUD), Favorites (get/sync/toggle), Rename, History, syncAll/syncStatus
- cloud_screen.dart: Komplett-Rewrite mit Sync-Modus (Manuell/Auto), Intervall-Auswahl, Sync-Fortschritt-Anzeige, Playlist-Verwaltung, Favoriten-Anzeige + Umbenennen, Sync-Info-Karte, nächster Sync
- main.dart: Auto-Sync beim App-Start (nur wenn konfiguriert + fällig)

## Flutter Analyze
- 0 neue Issues — nur 7 pre-existing
This commit is contained in:
Dustin
2026-08-01 19:32:35 +02:00
parent 2b06a8e496
commit 15993b1d0a
5 changed files with 1551 additions and 463 deletions
+95 -1
View File
@@ -20,7 +20,7 @@ class DbHelper {
final pfad = await getDatabasesPath();
return openDatabase(
p.join(pfad, 'melo.db'),
version: 4,
version: 5,
onCreate: (db, version) async {
await db.execute('''
CREATE TABLE songs (
@@ -107,6 +107,33 @@ class DbHelper {
// Spalte existiert bereits ignorieren
}
}
if (oldVersion < 5) {
try {
await db.execute('ALTER TABLE songs ADD COLUMN cloud_id TEXT');
} catch (_) {}
try {
await db.execute('ALTER TABLE songs ADD COLUMN cloud_title TEXT');
} catch (_) {}
try {
await db.execute('ALTER TABLE songs ADD COLUMN cloud_artist TEXT');
} catch (_) {}
try {
await db.execute('''
CREATE TABLE IF NOT EXISTS server_favorites (
cloud_id TEXT PRIMARY KEY,
favorited_at TEXT
)
''');
} catch (_) {}
try {
await db.execute('''
CREATE TABLE IF NOT EXISTS sync_metadata (
key TEXT PRIMARY KEY,
value TEXT
)
''');
} catch (_) {}
}
},
);
}
@@ -356,6 +383,73 @@ class DbHelper {
return rows.map((r) => Song.fromMap(r)).toList();
}
/// Cloud-ID für einen Song speichern (Verknüpfung lokal ↔ Cloud)
Future<void> cloudIdSetzen(int songId, String cloudId,
{String? cloudTitle, String? cloudArtist}) async {
final d = await db;
final update = <String, dynamic>{'cloud_id': cloudId};
if (cloudTitle != null) update['cloud_title'] = cloudTitle;
if (cloudArtist != null) update['cloud_artist'] = cloudArtist;
await d.update('songs', update, where: 'id = ?', whereArgs: [songId]);
}
/// Sync-Metadaten lesen
Future<String?> syncMetaGet(String key) async {
final d = await db;
final rows = await d.query('sync_metadata',
where: 'key = ?', whereArgs: [key]);
if (rows.isEmpty) return null;
return rows.first['value'] as String?;
}
/// Sync-Metadaten setzen
Future<void> syncMetaSet(String key, String value) async {
final d = await db;
await d.insert('sync_metadata', {'key': key, 'value': value},
conflictAlgorithm: ConflictAlgorithm.replace);
}
/// Server-Favoriten abrufen (alle cloud_ids)
Future<Set<String>> serverFavoritesGet() async {
final d = await db;
final rows = await d.query('server_favorites');
return rows.map((r) => r['cloud_id'] as String).toSet();
}
/// Server-Favoriten ersetzen (komplette Liste)
Future<void> serverFavoritesSet(List<String> cloudIds) async {
final d = await db;
await d.transaction((txn) async {
await txn.delete('server_favorites');
final now = DateTime.now().toIso8601String();
for (final cid in cloudIds) {
await txn.insert('server_favorites',
{'cloud_id': cid, 'favorited_at': now});
}
});
}
/// Song per cloud_id finden
Future<Song?> songNachCloudId(String cloudId) async {
final d = await db;
final rows = await d.query('songs',
where: 'cloud_id = ?', whereArgs: [cloudId]);
if (rows.isEmpty) return null;
return Song.fromMap(rows.first);
}
/// Benutzerdefinierte Metadaten für Cloud-Song aktualisieren
Future<void> cloudMetadatenAktualisieren(int songId,
{String? title, String? artist}) async {
final d = await db;
final update = <String, dynamic>{};
if (title != null) update['cloud_title'] = title;
if (artist != null) update['cloud_artist'] = artist;
if (update.isNotEmpty) {
await d.update('songs', update, where: 'id = ?', whereArgs: [songId]);
}
}
Future<void> loeschen() async {
final d = await db;
await d.transaction((txn) async {