v2.46 — Jahres-Recap (Spotify-Wrapped-Stil) mit Hörstatistiken
## Neuer Recap-Screen - lib/screens/recap_screen.dart: Full-Screen mit 7 animierten Seiten (Intro, Top 5 Songs, Top 3 Künstler, Hörzeit, häufigster Tag, Nachtaktive %, Gesamtzahl + Teilen) - Wrapped-Stil: Verlaufs-Gradienten, große Zahlen mit Count-up-Animation, Fade+Scale-Seitenwechsel, Fortschritts-Punkte, Weiter-Button - Teilen-Button kopiert Recap-Text in die Zwischenablage (kein neues Package) ## Datenbasis - lib/services/recap_service.dart: aggregiert lokale Abspiel-Historie zu Top-5-Songs, Top-3-Künstler, Hörzeit, Wochentag, Nacht-%, Gesamtzahl - Fallback auf Gesamt-Historie wenn im Zieljahr keine Daten; freundlicher Leer-Zustand ohne History ## DB + Tracking - db_helper.dart: Migration v6→v7 mit neuer Tabelle abspiel_historie (song_id, played_at) + Backfill aus wiedergabe_verlauf - player_service.dart: registriert jeden Abspielvorgang in der Historie ## Einstieg - Home-Screen: Karte 'Jahres-Recap 2026' unter der Statistik-Card - Einstellungen: Kachel 'Jahres-Recap' in Sektion Daten & Privatsphäre flutter analyze: 0 Issues, Tests 4/4
This commit is contained in:
@@ -20,7 +20,7 @@ class DbHelper {
|
||||
final pfad = await getDatabasesPath();
|
||||
return openDatabase(
|
||||
p.join(pfad, 'melo.db'),
|
||||
version: 6,
|
||||
version: 7,
|
||||
onCreate: (db, version) async {
|
||||
await db.execute('''
|
||||
CREATE TABLE songs (
|
||||
@@ -87,6 +87,18 @@ class DbHelper {
|
||||
FOREIGN KEY (song_id) REFERENCES songs(id) ON DELETE CASCADE
|
||||
)
|
||||
''');
|
||||
await db.execute('''
|
||||
CREATE TABLE abspiel_historie (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
song_id INTEGER NOT NULL,
|
||||
played_at TEXT NOT NULL,
|
||||
FOREIGN KEY (song_id) REFERENCES songs(id) ON DELETE CASCADE
|
||||
)
|
||||
''');
|
||||
await db.execute(
|
||||
'CREATE INDEX IF NOT EXISTS i_hist_song ON abspiel_historie(song_id)');
|
||||
await db.execute(
|
||||
'CREATE INDEX IF NOT EXISTS i_hist_played ON abspiel_historie(played_at)');
|
||||
},
|
||||
onUpgrade: (db, oldVersion, newVersion) async {
|
||||
if (oldVersion < 2) {
|
||||
@@ -142,6 +154,30 @@ class DbHelper {
|
||||
try { await db.execute('ALTER TABLE songs ADD COLUMN genre TEXT'); } catch (_) {}
|
||||
try { await db.execute('ALTER TABLE songs ADD COLUMN track TEXT'); } catch (_) {}
|
||||
}
|
||||
if (oldVersion < 7) {
|
||||
try {
|
||||
await db.execute('''
|
||||
CREATE TABLE IF NOT EXISTS abspiel_historie (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
song_id INTEGER NOT NULL,
|
||||
played_at TEXT NOT NULL,
|
||||
FOREIGN KEY (song_id) REFERENCES songs(id) ON DELETE CASCADE
|
||||
)
|
||||
''');
|
||||
await db.execute(
|
||||
'CREATE INDEX IF NOT EXISTS i_hist_song ON abspiel_historie(song_id)');
|
||||
await db.execute(
|
||||
'CREATE INDEX IF NOT EXISTS i_hist_played ON abspiel_historie(played_at)');
|
||||
// Backfill: bisherige "zuletzt abgespielt"-Einträge als Historie übernehmen
|
||||
await db.rawInsert('''
|
||||
INSERT OR IGNORE INTO abspiel_historie (song_id, played_at)
|
||||
SELECT song_id, zuletzt_abgespielt FROM wiedergabe_verlauf
|
||||
WHERE zuletzt_abgespielt IS NOT NULL
|
||||
''');
|
||||
} catch (_) {
|
||||
// Historie-Tabelle konnte nicht angelegt werden – ignorieren
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -224,6 +260,36 @@ class DbHelper {
|
||||
return rows.map((r) => Song.fromMap(r)).toList();
|
||||
}
|
||||
|
||||
// ─── Abspiel-Historie (für Jahres-Recap) ─────────
|
||||
|
||||
/// Registriert einen Abspielvorgang (Zeitpunkt wird jetzt gesetzt).
|
||||
/// Dient als Datenbasis für den Jahres-Recap.
|
||||
Future<void> abspielRegistrieren(int songId) async {
|
||||
final d = await db;
|
||||
await d.insert('abspiel_historie', {
|
||||
'song_id': songId,
|
||||
'played_at': DateTime.now().toIso8601String(),
|
||||
});
|
||||
}
|
||||
|
||||
/// Rohe Abspiel-Historie: song_id + played_at (für Recap-Aggregation)
|
||||
Future<List<Map<String, dynamic>>> abspielHistorie() async {
|
||||
final d = await db;
|
||||
final rows = await d.rawQuery('''
|
||||
SELECT h.song_id, h.played_at, s.titel, s.kuenstler, s.dauer_sekunden
|
||||
FROM abspiel_historie h
|
||||
JOIN songs s ON s.id = h.song_id
|
||||
ORDER BY h.played_at ASC
|
||||
''');
|
||||
return rows;
|
||||
}
|
||||
|
||||
/// Löscht die komplette Abspiel-Historie (für "Alle Daten löschen")
|
||||
Future<void> abspielHistorieLoeschen() async {
|
||||
final d = await db;
|
||||
await d.delete('abspiel_historie');
|
||||
}
|
||||
|
||||
/// Markiert einen Song als korrupt
|
||||
Future<void> alsKorruptMarkieren(int songId) async {
|
||||
final d = await db;
|
||||
@@ -463,6 +529,7 @@ class DbHelper {
|
||||
Future<void> loeschen() async {
|
||||
final d = await db;
|
||||
await d.transaction((txn) async {
|
||||
await txn.delete('abspiel_historie');
|
||||
await txn.delete('wiedergabe_verlauf');
|
||||
await txn.delete('song_tags');
|
||||
await txn.delete('playlist_songs');
|
||||
|
||||
Reference in New Issue
Block a user