Files
Melo/lib/shared/lauf_balken.dart
T
Hermes (Server)andClaude Opus 5 80948f1ad5 Listen zeigen, welcher Titel gerade laeuft
Bisher scrollte man durch die Bibliothek und musste raten, wo man ist —
die einzige Rueckmeldung war die Leiste ganz unten.

Drei tanzende Balken ueber dem Coverbild des laufenden Titels, sein Name
in Rot. Die Balken liegen auf dem Cover statt daneben: eine vierte Spalte
haette dem Titel Platz genommen und die Zeile bei jedem Titelwechsel in
der Breite springen lassen. Pausiert bleibt die Markierung stehen, die
Balken stehen still. Bei abgeschalteten Systemanimationen ebenfalls — eine
Endlosschleife ist genau das, was diese Einstellung vermeiden soll.

Die Markierung sitzt in SongZeile und gilt damit in einem Rutsch fuer
Meine Musik, Favoriten, Suche, Kuenstler, Kategorie und "Zuletzt
hinzugefuegt". Warteschlange und Wiedergabeliste bauen ihre Zeilen selbst
(dort steht rechts Entfernen statt Herz und Menue) und haben dasselbe
Zeichen einzeln bekommen.

Nur die laufende Zeile hoert auf den Wiedergabe-Zustand: der meldet auch
den Pufferstand und feuert mehrmals pro Sekunde. Haenge jede sichtbare
Zeile daran, baute sich die halbe Liste staendig neu auf.

Aus dem Code-Review nachgebessert: Startwert fuer den Strom, sonst
erschien die Markierung beim Zurueckscrollen einen Frame zu spaet; die
Zeile aendert beim Wechsel nicht mehr die Form ihres Aufbaus, sonst verlor
das Herz daneben sein Abo und sah kurz leer aus; die Wiedergabeliste
bekam die Markierung ueberhaupt erst (der Changelog hatte sie faelschlich
schon behauptet); der Abdunkel-Schleier nimmt den Eckenradius jetzt
entgegen statt ihn fest zu verdrahten.

Testluecke geschlossen, die der Review benannt hat: geprueft waren nur die
Regel und die Balken je fuer sich, die Verdrahtung dazwischen nicht —
genau dort sassen alle drei Fehler.

411 Tests gruen (vorher 396), flutter analyze ohne Befund, Release-APK
gebaut.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013xAHJTJM6UUqmjUgk1PUEd
2026-08-23 20:54:45 +02:00

176 lines
4.6 KiB
Dart

import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'theme.dart';
/// Drei tanzende Balken — das Zeichen dafür, dass genau dieser Titel läuft.
///
/// Bei [laeuft] `false` (pausiert) stehen sie still, statt zu verschwinden:
/// die Zeile bleibt als „hier bist du gerade" markiert, zeigt aber, dass
/// nichts vorangeht. Sind die Systemanimationen abgeschaltet, stehen sie
/// ebenfalls still — eine Endlosschleife ist genau das, was diese
/// Einstellung vermeiden soll.
class LaufBalken extends StatefulWidget {
const LaufBalken({
super.key,
required this.laeuft,
this.hoehe = 16,
this.farbe = Colors.white,
});
final bool laeuft;
final double hoehe;
final Color farbe;
@override
State<LaufBalken> createState() => _LaufBalkenState();
}
class _LaufBalkenState extends State<LaufBalken>
with SingleTickerProviderStateMixin {
static const _anzahl = 3;
/// Versatz je Balken, damit sie nicht im Gleichschritt springen.
static const _versatz = [0.0, 0.35, 0.7];
late final AnimationController _takt = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 900),
);
// Kein _richteTaktAus() in initState: die Entscheidung hängt an MediaQuery,
// und darauf darf man dort noch nicht zugreifen. didChangeDependencies
// läuft unmittelbar danach und erledigt es.
@override
void didUpdateWidget(LaufBalken alt) {
super.didUpdateWidget(alt);
if (alt.laeuft != widget.laeuft) _richteTaktAus();
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
_richteTaktAus();
}
void _richteTaktAus() {
final ruhig = MediaQuery.disableAnimationsOf(context);
if (widget.laeuft && !ruhig) {
if (!_takt.isAnimating) _takt.repeat();
} else {
_takt.stop();
}
}
@override
void dispose() {
_takt.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return SizedBox(
height: widget.hoehe,
width: widget.hoehe,
child: AnimatedBuilder(
animation: _takt,
builder: (context, _) => Row(
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
for (var i = 0; i < _anzahl; i++)
_Balken(
anteil: _anteil(i),
hoehe: widget.hoehe,
farbe: widget.farbe,
),
],
),
),
);
}
/// Höhe eines Balkens zwischen 0,3 und 1,0. Stillstehend eine feste,
/// ungleiche Form — damit man auch pausiert erkennt, was gemeint ist.
double _anteil(int i) {
if (!_takt.isAnimating) return const [0.4, 1.0, 0.6][i];
final phase = (_takt.value + _versatz[i]) * 2 * math.pi;
return 0.3 + 0.7 * (0.5 + 0.5 * math.sin(phase));
}
}
class _Balken extends StatelessWidget {
const _Balken({
required this.anteil,
required this.hoehe,
required this.farbe,
});
final double anteil;
final double hoehe;
final Color farbe;
@override
Widget build(BuildContext context) {
return Container(
width: hoehe * 0.22,
height: hoehe * anteil,
decoration: BoxDecoration(
color: farbe,
borderRadius: BorderRadius.circular(hoehe * 0.11),
),
);
}
}
/// Das Coverbild einer Songzeile mit der Lauf-Markierung darüber.
///
/// Die Balken liegen **auf** dem Cover statt daneben: eine vierte Spalte in
/// der Zeile würde dem Titel Platz nehmen, und die Zeile spränge beim
/// Titelwechsel in der Breite.
class LaufMarkierung extends StatelessWidget {
const LaufMarkierung({
super.key,
required this.markiert,
required this.spielt,
required this.child,
this.radius = MeloRadius.element,
});
/// Dieser Titel steht in der Wiedergabe — auch pausiert.
final bool markiert;
/// Die Wiedergabe läuft tatsächlich (steuert nur die Bewegung).
final bool spielt;
final Widget child;
/// Muss zum Radius des Coverbilds darunter passen, sonst hat der Schleier
/// andere Ecken als das Bild.
final double radius;
@override
Widget build(BuildContext context) {
if (!markiert) return child;
return Stack(
alignment: Alignment.center,
children: [
child,
// Abdunkeln, damit die weißen Balken auch auf hellen Covern stehen.
Positioned.fill(
child: DecoratedBox(
decoration: BoxDecoration(
color: Colors.black.withValues(alpha: 0.55),
borderRadius: BorderRadius.circular(radius),
),
),
),
LaufBalken(laeuft: spielt),
],
);
}
}