From 22ce6115370a9c1768a2dff2eb25f8a6a49c62e1 Mon Sep 17 00:00:00 2001 From: Dustin Date: Wed, 5 Aug 2026 11:10:49 +0200 Subject: [PATCH] =?UTF-8?q?v2.53=20=E2=80=94=20Sprint=20E:=20Recap-Zeitr?= =?UTF-8?q?=C3=A4ume=20(Woche/Monat/Jahr)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Recap-Zeiträume - recap_service.dart: RecapZeitraum-Enum (woche/monat/jahr), berechne(zeitraum:, jetzt:) mit injizierbarem Historie-Loader (Tests ohne DB), Fallback-Kette Woche → Monat → Jahr → Gesamt-Historie, Zeitraum-Getter (kopfzeile, introTitel, introUntertitel, zeitraumLabel, shareTitel, Monatsname) - recap_screen.dart: Zeitraum-Umschalter (7 Tage / Monat / Jahr) oben im Recap, dynamische Texte („Deine Woche in Musik“, „Abspielungen in dieser Woche“, „Melo Wochen-Recap“), Seiten-Reset bei Wechsel - home_screen.dart: Recap-Karte „Recap 2026“ + „Woche · Monat · Jahr“-Untertitel - settings_screen.dart: Kachel „Recap (Woche/Monat/Jahr)“ - test/recap_service_test.dart: 12 neue Tests (Filter, Fallback-Kette, Aggregation, Label-Getter) - flutter analyze 0 Issues, Tests 123/123 --- lib/screens/home_screen.dart | 4 +- lib/screens/recap_screen.dart | 98 ++++++++++++-- lib/screens/settings_screen.dart | 2 +- lib/services/recap_service.dart | 157 ++++++++++++++++++++-- test/recap_service_test.dart | 220 +++++++++++++++++++++++++++++++ 5 files changed, 454 insertions(+), 27 deletions(-) create mode 100644 test/recap_service_test.dart diff --git a/lib/screens/home_screen.dart b/lib/screens/home_screen.dart index f79af46..acf3d2a 100644 --- a/lib/screens/home_screen.dart +++ b/lib/screens/home_screen.dart @@ -1397,7 +1397,7 @@ class _MeloHomeState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - 'Jahres-Recap ${DateTime.now().year}', + 'Recap ${DateTime.now().year}', style: const TextStyle( fontSize: 15, fontWeight: FontWeight.w700, @@ -1406,7 +1406,7 @@ class _MeloHomeState extends State { ), const SizedBox(height: 2), const Text( - 'Deine Top-Songs, Künstler & Hörzeit – wie Spotify Wrapped', + 'Woche · Monat · Jahr – deine Top-Songs, Künstler & Hörzeit', maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle(fontSize: 11, color: MeloTheme.textSekundaer), diff --git a/lib/screens/recap_screen.dart b/lib/screens/recap_screen.dart index f36571a..9bfbaf5 100644 --- a/lib/screens/recap_screen.dart +++ b/lib/screens/recap_screen.dart @@ -21,10 +21,13 @@ class _RecapScreenState extends State { final PageController _controller = PageController(); int _seite = 0; + /// Gewählter Zeitraum (7 Tage / Monat / Jahr) — Umschalter oben im Recap. + RecapZeitraum _zeitraum = RecapZeitraum.jahr; + @override void initState() { super.initState(); - _future = _service.berechne(); + _future = _service.berechne(zeitraum: _zeitraum); } @override @@ -33,6 +36,19 @@ class _RecapScreenState extends State { super.dispose(); } + /// Zeitraum wechseln: Recap neu berechnen + zurück zur ersten Seite. + void _zeitraumSetzen(RecapZeitraum z) { + if (z == _zeitraum) return; + setState(() { + _zeitraum = z; + _future = _service.berechne(zeitraum: z); + _seite = 0; + }); + if (_controller.hasClients) { + _controller.jumpToPage(0); + } + } + void _weiter() { if (_seite < 6) { _controller.nextPage( @@ -46,7 +62,7 @@ class _RecapScreenState extends State { Future _teilen(RecapDaten d) async { final text = StringBuffer() - ..writeln('🎧 Melo Jahres-Recap ${d.jahr}') + ..writeln('🎧 ${d.shareTitel}') ..writeln('─────────────────') ..writeln('🔥 Top 5 Songs:'); for (var i = 0; i < d.topSongs.length; i++) { @@ -163,6 +179,7 @@ class _RecapScreenState extends State { child: Column( children: [ _kopfzeile(d), + _zeitraumUmschalter(), Expanded( child: PageView.builder( controller: _controller, @@ -200,7 +217,7 @@ class _RecapScreenState extends State { ), const Spacer(), Text( - 'MELO RECAP ${d.jahr}', + d.kopfzeile, style: const TextStyle( fontSize: 12, fontWeight: FontWeight.w700, @@ -215,6 +232,56 @@ class _RecapScreenState extends State { ); } + /// Zeitraum-Umschalter (7 Tage / Monat / Jahr) oben im Recap. + Widget _zeitraumUmschalter() { + const optionen = [ + (RecapZeitraum.woche, '7 Tage'), + (RecapZeitraum.monat, 'Monat'), + (RecapZeitraum.jahr, 'Jahr'), + ]; + return Padding( + padding: const EdgeInsets.fromLTRB(20, 12, 20, 0), + child: Container( + padding: const EdgeInsets.all(4), + decoration: BoxDecoration( + color: Colors.white.withValues(alpha: 0.06), + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.white.withValues(alpha: 0.1)), + ), + child: Row( + children: optionen.map((o) { + final aktiv = _zeitraum == o.$1; + return Expanded( + child: GestureDetector( + onTap: () => _zeitraumSetzen(o.$1), + child: AnimatedContainer( + duration: const Duration(milliseconds: 200), + padding: const EdgeInsets.symmetric(vertical: 9), + decoration: BoxDecoration( + color: aktiv ? MeloTheme.rot : Colors.transparent, + borderRadius: BorderRadius.circular(9), + ), + child: Center( + child: Text( + o.$2, + style: TextStyle( + color: aktiv + ? Colors.white + : MeloTheme.textSekundaer, + fontSize: 12, + fontWeight: FontWeight.w700, + ), + ), + ), + ), + ), + ); + }).toList(), + ), + ), + ); + } + Widget _navigation(RecapDaten d) { return Padding( padding: const EdgeInsets.fromLTRB(20, 8, 20, 24), @@ -290,14 +357,14 @@ class _RecapScreenState extends State { children: [ const Text('🎧', style: TextStyle(fontSize: 72)), const SizedBox(height: 24), - const Text( - 'Dein Jahr in Musik', + Text( + d.introUntertitel, textAlign: TextAlign.center, - style: TextStyle(fontSize: 15, color: MeloTheme.textSekundaer), + style: const TextStyle(fontSize: 15, color: MeloTheme.textSekundaer), ), const SizedBox(height: 8), Text( - '${d.jahr}', + d.introTitel, textAlign: TextAlign.center, style: const TextStyle( fontSize: 96, @@ -442,9 +509,12 @@ class _RecapScreenState extends State { } Widget _outroSeite(RecapDaten d) { + final titel = d.zeitraum == RecapZeitraum.jahr + ? 'Dein Recap ${d.jahr}' + : 'Dein Recap · ${d.zeitraumLabel}'; return _seitenLayout( kind: 'Das war\'s', - title: 'Dein Recap ${d.jahr}', + title: titel, children: [ _Zaehler( ziel: d.gesamtAnzahl, @@ -455,8 +525,16 @@ class _RecapScreenState extends State { shadows: [Shadow(color: MeloTheme.rot, blurRadius: 40)], ), ), - const Text('Abspielungen insgesamt', - style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: MeloTheme.rot)), + Text( + switch (d.zeitraum) { + RecapZeitraum.woche => 'Abspielungen in dieser Woche', + RecapZeitraum.monat => 'Abspielungen in diesem Monat', + RecapZeitraum.jahr => 'Abspielungen insgesamt', + }, + textAlign: TextAlign.center, + style: const TextStyle( + fontSize: 18, fontWeight: FontWeight.w600, color: MeloTheme.rot), + ), const SizedBox(height: 20), const Text('Danke fürs Zuhören ♥', style: TextStyle(fontSize: 16, color: Colors.white70)), diff --git a/lib/screens/settings_screen.dart b/lib/screens/settings_screen.dart index b7494ea..27dcc40 100644 --- a/lib/screens/settings_screen.dart +++ b/lib/screens/settings_screen.dart @@ -233,7 +233,7 @@ class _SettingsScreenState extends State { ), _einstellungsKachel( icon: Icons.auto_graph, - titel: 'Jahres-Recap', + titel: 'Recap (Woche/Monat/Jahr)', untertitel: 'Deine Hörstatistiken – wie Spotify Wrapped', onTap: () { Navigator.push( diff --git a/lib/services/recap_service.dart b/lib/services/recap_service.dart index e29b8f5..c8dce44 100644 --- a/lib/services/recap_service.dart +++ b/lib/services/recap_service.dart @@ -1,6 +1,18 @@ import '../database/db_helper.dart'; import '../models/song.dart'; +/// Zeitraum für den Recap (Spotify-Wrapped-Stil) +enum RecapZeitraum { + /// Letzte 7 Tage + woche, + + /// Aktueller Kalendermonat + monat, + + /// Aktuelles Jahr + jahr, +} + /// Ein Song mit seiner Abspiel-Anzahl im Recap class RecapSongStat { final Song song; @@ -19,7 +31,17 @@ class RecapKuenstlerStat { /// Aggregierte Recap-Daten (wie Spotify Wrapped) class RecapDaten { + /// Tatsächlich verwendeter Zeitraum (nach Fallback-Kette Woche → Monat → + /// Jahr → Gesamt-Historie). Kann vom angeforderten abweichen, wenn für + /// den gewünschten Zeitraum keine Daten existieren. + final RecapZeitraum zeitraum; + + /// Anker-Jahr des Zeitraums (für „Monat“: das Jahr des Monats). final int jahr; + + /// Anker-Monat (1–12) — nur für [RecapZeitraum.monat] relevant. + final int monat; + final int gesamtAnzahl; // Gesamtzahl aller Abspielungen final int gesamtSekunden; // Geschätzte Hörzeit (Summe Songdauer je Abspielung) final String haeufigsterTag; // z.B. "Freitag" @@ -28,7 +50,9 @@ class RecapDaten { final List topKuenstler; // Top 3 RecapDaten({ + required this.zeitraum, required this.jahr, + required this.monat, required this.gesamtAnzahl, required this.gesamtSekunden, required this.haeufigsterTag, @@ -37,6 +61,48 @@ class RecapDaten { required this.topKuenstler, }); + static const List _monatsnamen = [ + 'Januar', 'Februar', 'März', 'April', 'Mai', 'Juni', + 'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember', + ]; + + String get monatsName => _monatsnamen[(monat - 1).clamp(0, 11)]; + + /// Überschrift des Recap-Titels, z.B. „MELO RECAP 2026“. + String get kopfzeile => switch (zeitraum) { + RecapZeitraum.woche => 'MELO RECAP · 7 TAGE', + RecapZeitraum.monat => 'MELO RECAP · ${monatsName.toUpperCase()}', + RecapZeitraum.jahr => 'MELO RECAP $jahr', + }; + + /// Große Zahl/Titel auf der Intro-Seite (Jahr, Monatsname oder „7 Tage“). + String get introTitel => switch (zeitraum) { + RecapZeitraum.woche => '7 Tage', + RecapZeitraum.monat => monatsName, + RecapZeitraum.jahr => '$jahr', + }; + + /// Untertitel der Intro-Seite („Deine Woche in Musik“ …). + String get introUntertitel => switch (zeitraum) { + RecapZeitraum.woche => 'Deine Woche in Musik', + RecapZeitraum.monat => 'Dein Monat in Musik', + RecapZeitraum.jahr => 'Dein Jahr in Musik', + }; + + /// Kurzlabel des Zeitraums (für Statistik-Texte). + String get zeitraumLabel => switch (zeitraum) { + RecapZeitraum.woche => 'Diese Woche', + RecapZeitraum.monat => 'Dein Monat', + RecapZeitraum.jahr => 'Dein Jahr', + }; + + /// Text für die Teilen-Funktion, z.B. „Melo Jahres-Recap 2026“. + String get shareTitel => switch (zeitraum) { + RecapZeitraum.woche => 'Melo Wochen-Recap', + RecapZeitraum.monat => 'Melo Monats-Recap $monatsName', + RecapZeitraum.jahr => 'Melo Jahres-Recap $jahr', + }; + String get hoerzeitFormatiert { final stunden = gesamtSekunden ~/ 3600; final minuten = (gesamtSekunden % 3600) ~/ 60; @@ -48,32 +114,93 @@ class RecapDaten { /// Aggregiert die lokale Abspiel-Historie zu Recap-Statistiken. /// /// Datenquelle ist die lokale `abspiel_historie`-Tabelle (nicht der Server), -/// da die History lokal aufgezeichnet wird. +/// da die History lokal aufgezeichnet wird. Unterstützt die Zeiträume +/// Woche (letzte 7 Tage), Monat (aktueller Kalendermonat) und Jahr +/// (aktuelles Jahr). Fallback: Wenn im gewünschten Zeitraum keine Daten +/// existieren, wird der nächstgrößere Zeitraum verwendet. class RecapService { final DbHelper _db = DbHelper(); + /// Injizierbarer Historie-Loader (Tests liefern Fake-Daten; Produktion + /// liest die `abspiel_historie`-Tabelle). + late final Future>> Function() _historie; + + RecapService({Future>> Function()? historie}) { + _historie = historie ?? _db.abspielHistorie; + } + static const List _wochentage = [ 'Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag', 'Sonntag', ]; - /// Berechnet den Recap. Filtert auf [jahr] (Standard: aktuelles Jahr). - /// Falls im gewählten Jahr keine Daten existieren, wird auf die - /// Gesamt-Historie zurückgegriffen. Liefert `null` bei leerer History. - Future berechne({int? jahr}) async { - final zielJahr = jahr ?? DateTime.now().year; - var rows = await _db.abspielHistorie(); + /// Berechnet den Recap für [zeitraum] (Standard: aktuelles Jahr). + /// + /// Fallback-Kette: Woche → Monat → Jahr → Gesamt-Historie. Liefert `null` + /// bei komplett leerer History. [jetzt] ist injizierbar (Tests). + Future berechne({ + RecapZeitraum zeitraum = RecapZeitraum.jahr, + DateTime? jetzt, + }) async { + final j = jetzt ?? DateTime.now(); + final rows = await _historie(); if (rows.isEmpty) return null; - final gefiltert = rows.where((r) { - final t = DateTime.tryParse(r['played_at'] as String? ?? ''); - return t != null && t.year == zielJahr; - }).toList(); + // Zeitraum-Filter in der Fallback-Reihenfolge (Woche → Monat → Jahr → + // Gesamt-Historie): erst der gewünschte Zeitraum, danach der jeweils + // nächstgrößere. + final kette = switch (zeitraum) { + RecapZeitraum.woche => const [ + RecapZeitraum.woche, RecapZeitraum.monat, RecapZeitraum.jahr, + ], + RecapZeitraum.monat => const [RecapZeitraum.monat, RecapZeitraum.jahr], + RecapZeitraum.jahr => const [RecapZeitraum.jahr], + }; + RecapZeitraum genutzterZeitraum = zeitraum; + var daten = >[]; + for (final z in kette) { + final gefiltert = rows.where(_filterFuer(z, j)).toList(); + if (gefiltert.isNotEmpty) { + genutzterZeitraum = z; + daten = gefiltert; + break; + } + } + if (daten.isEmpty) { + // Nichts im (größten) Zeitraum, aber History vorhanden → + // Gesamt-Historie (wie bisher beim Jahres-Fallback). + daten = rows; + } - // Fallback: wenn im Zieljahr nichts gehört wurde → gesamte History - final daten = gefiltert.isNotEmpty ? gefiltert : rows; + return _aggregiere(daten, genutzterZeitraum, j); + } + /// Filter-Funktion für einen Zeitraum. + bool Function(Map row) _filterFuer( + RecapZeitraum zeitraum, DateTime j) { + switch (zeitraum) { + case RecapZeitraum.woche: + final grenze = j.subtract(const Duration(days: 7)); + return (r) { + final t = DateTime.tryParse(r['played_at'] as String? ?? ''); + return t != null && !t.isBefore(grenze); + }; + case RecapZeitraum.monat: + return (r) { + final t = DateTime.tryParse(r['played_at'] as String? ?? ''); + return t != null && t.year == j.year && t.month == j.month; + }; + case RecapZeitraum.jahr: + return (r) { + final t = DateTime.tryParse(r['played_at'] as String? ?? ''); + return t != null && t.year == j.year; + }; + } + } + + RecapDaten _aggregiere( + List> daten, RecapZeitraum zeitraum, DateTime j) { // ── Aggregation ── final songCounts = {}; final songMap = {}; @@ -133,7 +260,9 @@ class RecapService { .toList(); return RecapDaten( - jahr: zielJahr, + zeitraum: zeitraum, + jahr: j.year, + monat: j.month, gesamtAnzahl: gesamtAnzahl, gesamtSekunden: gesamtSekunden, haeufigsterTag: _wochentage[maxTag], diff --git a/test/recap_service_test.dart b/test/recap_service_test.dart new file mode 100644 index 0000000..5c7c505 --- /dev/null +++ b/test/recap_service_test.dart @@ -0,0 +1,220 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:melo_app/services/recap_service.dart'; + +/// RecapService-Tests mit injiziertem Historie-Loader (kein DB-Zugriff). +/// Abgedeckt: Zeitraum-Filter (Woche/Monat/Jahr), Fallback-Kette +/// (Woche → Monat → Jahr → Gesamt), Aggregation + Label-Getter. +void main() { + // Fester Anker-Zeitpunkt: 5. August 2026, 15:00 Uhr + final jetzt = DateTime(2026, 8, 5, 15, 0, 0); + + Map zeile( + int id, + String playedAt, { + String titel = 'Song', + String kuenstler = 'Künstler', + int dauer = 200, + }) => + { + 'song_id': id, + 'played_at': playedAt, + 'titel': titel, + 'kuenstler': kuenstler, + 'dauer_sekunden': dauer, + }; + + RecapService service(List> rows) => + RecapService(historie: () async => rows); + + test('leere History → null (alle Zeiträume)', () async { + expect(await service([]).berechne(jetzt: jetzt), isNull); + expect(await service([]).berechne(zeitraum: RecapZeitraum.woche, jetzt: jetzt), + isNull); + expect( + await service([]) + .berechne(zeitraum: RecapZeitraum.monat, jetzt: jetzt), + isNull); + }); + + group('Jahr', () { + test('nur Einträge des aktuellen Jahres zählen', () async { + final rows = [ + zeile(1, '2026-03-01T10:00:00'), + zeile(2, '2026-08-01T10:00:00'), + zeile(3, '2025-12-31T23:59:00'), // anderes Jahr → raus + ]; + final d = await service(rows).berechne(jetzt: jetzt); + expect(d, isNotNull); + expect(d!.gesamtAnzahl, 2); + expect(d.zeitraum, RecapZeitraum.jahr); + }); + + test('Jahr ohne Daten → Gesamt-Historie (alle Einträge)', () async { + final rows = [ + zeile(1, '2024-01-01T10:00:00'), + zeile(2, '2023-06-15T10:00:00'), + ]; + final d = await service(rows).berechne(jetzt: jetzt); + expect(d, isNotNull); + expect(d!.gesamtAnzahl, 2); // Fallback auf Gesamt + }); + }); + + group('Monat', () { + test('nur Einträge des aktuellen Kalendermonats zählen', () async { + final rows = [ + zeile(1, '2026-08-01T10:00:00'), + zeile(2, '2026-08-05T08:00:00'), + zeile(3, '2026-07-31T23:59:00'), // Juli → raus + zeile(4, '2026-01-15T10:00:00'), // Januar → raus + ]; + final d = + await service(rows).berechne(zeitraum: RecapZeitraum.monat, jetzt: jetzt); + expect(d, isNotNull); + expect(d!.gesamtAnzahl, 2); + expect(d.zeitraum, RecapZeitraum.monat); + expect(d.monatsName, 'August'); + }); + + test('Monat ohne Daten → nächstgrößerer Zeitraum (Jahr)', () async { + final rows = [ + zeile(1, '2026-03-01T10:00:00'), + zeile(2, '2026-05-15T10:00:00'), + ]; + final d = + await service(rows).berechne(zeitraum: RecapZeitraum.monat, jetzt: jetzt); + expect(d, isNotNull); + expect(d!.zeitraum, RecapZeitraum.jahr); // Fallback Monat → Jahr + expect(d.gesamtAnzahl, 2); + }); + }); + + group('Woche', () { + test('nur Einträge der letzten 7 Tage zählen', () async { + final rows = [ + zeile(1, '2026-08-04T10:00:00'), // innerhalb 7 Tage + zeile(2, '2026-07-30T12:00:00'), // genau 6 Tage vorher → drin + zeile(3, '2026-07-29T12:00:00'), // > 7 Tage → raus + zeile(4, '2026-01-01T10:00:00'), // alt → raus + ]; + final d = + await service(rows).berechne(zeitraum: RecapZeitraum.woche, jetzt: jetzt); + expect(d, isNotNull); + expect(d!.gesamtAnzahl, 2); + expect(d.zeitraum, RecapZeitraum.woche); + }); + + test('Woche ohne Daten → Monat (nächstgrößerer Zeitraum)', () async { + // Anker 20.8.: 7-Tage-Grenze = 13.8. → 10.8. liegt im Monat, aber + // außerhalb der letzten 7 Tage. + final spaeter = DateTime(2026, 8, 20, 15, 0, 0); + final rows = [ + zeile(1, '2026-08-10T10:00:00'), + ]; + final d = await service(rows) + .berechne(zeitraum: RecapZeitraum.woche, jetzt: spaeter); + expect(d, isNotNull); + expect(d!.zeitraum, RecapZeitraum.monat); + expect(d.gesamtAnzahl, 1); + }); + + test('Woche UND Monat ohne Daten → Jahr', () async { + final rows = [ + zeile(1, '2026-03-01T10:00:00'), + ]; + final d = + await service(rows).berechne(zeitraum: RecapZeitraum.woche, jetzt: jetzt); + expect(d, isNotNull); + expect(d!.zeitraum, RecapZeitraum.jahr); + }); + }); + + group('Aggregation', () { + test('Top 5 Songs + Top 3 Künstler + Hörzeit + Nacht-%', () async { + final rows = [ + // Song A 3×, Song B 2×, Song C 1× + zeile(1, '2026-08-01T10:00:00', titel: 'A', kuenstler: 'K1', dauer: 300), + zeile(1, '2026-08-02T10:00:00', titel: 'A', kuenstler: 'K1', dauer: 300), + zeile(1, '2026-08-03T23:00:00', titel: 'A', kuenstler: 'K1', dauer: 300), // nach 22 Uhr + zeile(2, '2026-08-01T11:00:00', titel: 'B', kuenstler: 'K1', dauer: 240), + zeile(2, '2026-08-02T11:00:00', titel: 'B', kuenstler: 'K1', dauer: 240), + zeile(3, '2026-08-01T12:00:00', titel: 'C', kuenstler: 'K2', dauer: 180), + ]; + final d = + await service(rows).berechne(zeitraum: RecapZeitraum.monat, jetzt: jetzt); + expect(d, isNotNull); + expect(d!.gesamtAnzahl, 6); + expect(d.topSongs.length, 3); + expect(d.topSongs.first.song.titel, 'A'); + expect(d.topSongs.first.anzahl, 3); + expect(d.topKuenstler.length, 2); + expect(d.topKuenstler.first.name, 'K1'); + expect(d.topKuenstler.first.anzahl, 5); + // Hörzeit: 3×300 + 2×240 + 1×180 = 1560s = 26 Min + expect(d.gesamtSekunden, 1560); + expect(d.hoerzeitFormatiert, '26 Min'); + // 1 von 6 Abspielungen nach 22 Uhr → 17% + expect(d.nachtProzent, 17); + expect(d.haeufigsterTag, isNotEmpty); + }); + }); + + group('Label-Getter', () { + test('Woche: Kopfzeile, Intro, Teilen', () { + final d = RecapDaten( + zeitraum: RecapZeitraum.woche, + jahr: 2026, + monat: 8, + gesamtAnzahl: 12, + gesamtSekunden: 3600, + haeufigsterTag: 'Freitag', + nachtProzent: 30, + topSongs: [], + topKuenstler: [], + ); + expect(d.kopfzeile, 'MELO RECAP · 7 TAGE'); + expect(d.introTitel, '7 Tage'); + expect(d.introUntertitel, 'Deine Woche in Musik'); + expect(d.zeitraumLabel, 'Diese Woche'); + expect(d.shareTitel, 'Melo Wochen-Recap'); + }); + + test('Monat: Kopfzeile, Intro, Teilen', () { + final d = RecapDaten( + zeitraum: RecapZeitraum.monat, + jahr: 2026, + monat: 8, + gesamtAnzahl: 12, + gesamtSekunden: 3600, + haeufigsterTag: 'Freitag', + nachtProzent: 30, + topSongs: [], + topKuenstler: [], + ); + expect(d.kopfzeile, 'MELO RECAP · AUGUST'); + expect(d.introTitel, 'August'); + expect(d.introUntertitel, 'Dein Monat in Musik'); + expect(d.zeitraumLabel, 'Dein Monat'); + expect(d.shareTitel, 'Melo Monats-Recap August'); + }); + + test('Jahr: Kopfzeile, Intro, Teilen', () { + final d = RecapDaten( + zeitraum: RecapZeitraum.jahr, + jahr: 2026, + monat: 8, + gesamtAnzahl: 12, + gesamtSekunden: 3600, + haeufigsterTag: 'Freitag', + nachtProzent: 30, + topSongs: [], + topKuenstler: [], + ); + expect(d.kopfzeile, 'MELO RECAP 2026'); + expect(d.introTitel, '2026'); + expect(d.introUntertitel, 'Dein Jahr in Musik'); + expect(d.zeitraumLabel, 'Dein Jahr'); + expect(d.shareTitel, 'Melo Jahres-Recap 2026'); + }); + }); +}