UX-Simulation, Teil 3: Abbrechen, Badge, Versionsnummer, Wisch-Strecke

Die vier zuletzt zurückgestellten Punkte aus der UX-Simulation:

- Abbrechen-Knopf für hängende YouTube-Downloads (Future.any gegen ein
  Abbruch-Signal, Anfrage läuft im Hintergrund aus statt die App zu
  blockieren)
- Punkt am inaktiven Download-Unterreiter, wenn dort etwas im Hintergrund
  läuft (SubTabs bekommt einen optionalen badges-Parameter)
- Versionsnummer unter "Über Melo" (neue Abhängigkeit: package_info_plus)
- Mini-Player führt beim Wischen jetzt sichtbar mit und wertet zusätzlich
  zum Schwung auch die zurückgelegte Strecke aus — ein langsames, aber
  weites Wischen wurde vorher komplett ignoriert

Dabei drei bestehende Widget-Tests korrigiert, die den Download-Tab ohne
Provider<DownloadService> aufbauten (hauptmenue_test.dart,
home_shell_test.dart, online_screen_test.dart) — lief bisher nur, weil
niemand danach fragte; das neue Badge tut das jetzt app-weit.

550 Tests grün (vorher 539), flutter analyze ohne Befund.
This commit is contained in:
Hermes (Server)
2026-08-25 10:25:43 +02:00
parent 8f80362f08
commit c2f31dee01
16 changed files with 392 additions and 33 deletions
+52 -7
View File
@@ -42,19 +42,40 @@ class MiniPlayer extends StatelessWidget {
}
}
class _Leiste extends StatelessWidget {
class _Leiste extends StatefulWidget {
const _Leiste({required this.handler, required this.item});
final MeloAudioHandler handler;
final MediaItem item;
@override
State<_Leiste> createState() => _LeisteState();
}
class _LeisteState extends State<_Leiste> {
/// Wie weit der Finger die Leiste gerade zieht — nur zur Anzeige, die
/// eigentliche Entscheidung fällt erst bei [onVerticalDragEnd]/
/// [onHorizontalDragEnd]. `dy` bleibt <= 0: nach unten ist unbelegt, ein
/// Mitführen dorthin würde Wirkung vortäuschen, die es nicht gibt.
Offset _versatz = Offset.zero;
bool _ziehtGerade = false;
void _oeffne(BuildContext context) => Navigator.of(context).push(
MaterialPageRoute(builder: (_) => const NowPlayingScreen()),
);
void _zurueckschnappen() {
setState(() {
_ziehtGerade = false;
_versatz = Offset.zero;
});
}
@override
Widget build(BuildContext context) {
final texte = Theme.of(context).textTheme;
final handler = widget.handler;
final item = widget.item;
return DecoratedBox(
decoration: const BoxDecoration(
color: MeloTheme.surface,
@@ -68,14 +89,30 @@ class _Leiste extends StatelessWidget {
children: [
GestureDetector(
// Wischen wie in jeder anderen Musik-App: nach oben öffnet die
// Vollbild-Wiedergabe, seitwärts wechselt den Titel.
// Vollbild-Wiedergabe, seitwärts wechselt den Titel. Die Leiste
// folgt dabei sichtbar dem Finger — sonst blieb ein
// bedächtiges, aber weites Wischen (unter der Schwung-Schwelle)
// ganz ohne Reaktion.
onVerticalDragUpdate: (details) => setState(() {
_ziehtGerade = true;
final dy = _versatz.dy + details.delta.dy;
_versatz = Offset(_versatz.dx, dy > 0 ? 0 : dy);
}),
onVerticalDragEnd: (details) {
if (oeffnetVollbild(details.velocity.pixelsPerSecond.dy)) {
_oeffne(context);
}
final oeffnet = oeffnetVollbildMitStrecke(
_versatz.dy, details.velocity.pixelsPerSecond.dy);
_zurueckschnappen();
if (oeffnet) _oeffne(context);
},
onHorizontalDragUpdate: (details) => setState(() {
_ziehtGerade = true;
_versatz = Offset(_versatz.dx + details.delta.dx, _versatz.dy);
}),
onHorizontalDragEnd: (details) {
switch (titelWechsel(details.velocity.pixelsPerSecond.dx)) {
final wechsel = titelWechselMitStrecke(
_versatz.dx, details.velocity.pixelsPerSecond.dx);
_zurueckschnappen();
switch (wechsel) {
case Wechsel.naechster:
handler.skipToNext();
case Wechsel.vorheriger:
@@ -86,7 +123,15 @@ class _Leiste extends StatelessWidget {
},
child: InkWell(
onTap: () => _oeffne(context),
child: SizedBox(
child: AnimatedContainer(
// Während des Ziehens sofort folgen (keine Verzögerung);
// beim Loslassen ohne Wirkung sichtbar zurückschnappen.
duration: _ziehtGerade
? Duration.zero
: MeloMotion.ruhig(context, MeloMotion.normal),
curve: MeloMotion.curve,
transform:
Matrix4.translationValues(_versatz.dx, _versatz.dy, 0),
height: MiniPlayer.hoehe,
child: Row(
children: [
+25
View File
@@ -23,3 +23,28 @@ Wechsel? titelWechsel(double geschwindigkeit) {
/// Wiedergabe aus Versehen wegschiebt.
bool oeffnetVollbild(double geschwindigkeit) =>
geschwindigkeit < -wischSchwelle;
/// Ab welcher zurückgelegten Strecke (in Pixeln) eine langsame, aber
/// eindeutige Wischbewegung auch ohne Schwung zählt.
///
/// Ohne diese Schwelle bekam ein bedächtiges, aber weites Wischen (unter
/// [wischSchwelle] Pixel pro Sekunde) überhaupt keine Reaktion — für den
/// Nutzer sah das aus wie ein nicht reagierender Bildschirm statt "zu
/// langsam gewischt".
const wischStreckeSchwelle = 80.0;
/// Wie [titelWechsel], aber eine ausreichend weite Bewegung zählt zusätzlich
/// auch bei geringem Schwung.
Wechsel? titelWechselMitStrecke(double strecke, double geschwindigkeit) {
final ausSchwung = titelWechsel(geschwindigkeit);
if (ausSchwung != null) return ausSchwung;
if (strecke.abs() <= wischStreckeSchwelle) return null;
return strecke < 0 ? Wechsel.naechster : Wechsel.vorheriger;
}
/// Wie [oeffnetVollbild], aber eine ausreichend weite Bewegung nach oben
/// zählt zusätzlich auch bei geringem Schwung.
bool oeffnetVollbildMitStrecke(double strecke, double geschwindigkeit) {
if (oeffnetVollbild(geschwindigkeit)) return true;
return strecke < -wischStreckeSchwelle;
}