v2.53 — Sprint E: Recap-Zeiträume (Woche/Monat/Jahr)
## 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
This commit is contained in:
@@ -21,10 +21,13 @@ class _RecapScreenState extends State<RecapScreen> {
|
||||
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<RecapScreen> {
|
||||
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<RecapScreen> {
|
||||
|
||||
Future<void> _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<RecapScreen> {
|
||||
child: Column(
|
||||
children: [
|
||||
_kopfzeile(d),
|
||||
_zeitraumUmschalter(),
|
||||
Expanded(
|
||||
child: PageView.builder(
|
||||
controller: _controller,
|
||||
@@ -200,7 +217,7 @@ class _RecapScreenState extends State<RecapScreen> {
|
||||
),
|
||||
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<RecapScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
/// 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<RecapScreen> {
|
||||
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<RecapScreen> {
|
||||
}
|
||||
|
||||
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<RecapScreen> {
|
||||
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)),
|
||||
|
||||
Reference in New Issue
Block a user