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
+54 -10
View File
@@ -11,6 +11,12 @@ import 'baka_auth.dart';
String sichererDateiname(String titel) =>
titel.replaceAll(RegExp(r'[\\/:*?"<>|]'), '_').trim();
/// Signalisiert, dass [YtDownloadService.abbrechen] während einer laufenden
/// Anfrage aufgerufen wurde.
class _Abgebrochen implements Exception {
const _Abgebrochen();
}
/// Ergebnis eines geglückten Downloads.
class YtErgebnis {
const YtErgebnis({
@@ -41,6 +47,11 @@ class YtDownloadService extends ChangeNotifier {
String? _status;
String? _fehler;
/// Ist gesetzt, während [herunterladen] läuft. [abbrechen] löst es aus —
/// die Netzwerk-Anfrage selbst läuft im Hintergrund weiter, bis der Socket
/// sie beendet, aber die App wartet nicht länger darauf.
Completer<void>? _abbruch;
bool get laeuft => _laeuft;
/// Was gerade passiert — für die Anzeige während des Downloads.
@@ -59,6 +70,20 @@ class YtDownloadService extends ChangeNotifier {
notifyListeners();
}
/// Bricht den laufenden Download ab, falls einer läuft.
void abbrechen() {
_abbruch?.complete();
}
/// Wartet auf [aufgabe], bricht aber sofort mit [_Abgebrochen] ab, sobald
/// [abbrechen] aufgerufen wird — ohne auf das Ende von [aufgabe] zu warten.
Future<T> _abbrechbar<T>(Future<T> aufgabe, Completer<void> abbruch) {
return Future.any<T>([
aufgabe,
abbruch.future.then((_) => throw const _Abgebrochen()),
]);
}
/// Lädt [url] herunter und legt die MP3 in [zielOrdner] ab.
/// Mit [cookies] nutzt der Proxy seine YouTube-Anmeldung — nötig für
/// altersbeschränkte Videos.
@@ -67,6 +92,7 @@ class YtDownloadService extends ChangeNotifier {
{required String zielOrdner, bool cookies = true}) async {
_fehler = null;
_laeuft = true;
final abbruch = _abbruch = Completer<void>();
notifyListeners();
if (!auth.istAngemeldet) {
@@ -82,13 +108,22 @@ class YtDownloadService extends ChangeNotifier {
_melde('Proxy wird gefragt …');
final http.Response antwort;
try {
antwort = await _client
.post(
Uri.parse('$proxyUrl/api/yt-dl'),
headers: {...auth.authHeader, 'Content-Type': 'application/json'},
body: jsonEncode({'url': url, 'cookies': cookies}),
)
.timeout(const Duration(seconds: 180));
antwort = await _abbrechbar(
_client
.post(
Uri.parse('$proxyUrl/api/yt-dl'),
headers: {
...auth.authHeader,
'Content-Type': 'application/json'
},
body: jsonEncode({'url': url, 'cookies': cookies}),
)
.timeout(const Duration(seconds: 180)),
abbruch,
);
} on _Abgebrochen {
_scheitere('Abgebrochen');
return null;
} on TimeoutException {
// Anders als bei einem Verbindungsfehler ist der Proxy hier vermutlich
// erreichbar, arbeitet nur noch — "nicht erreichbar" würde in die
@@ -124,14 +159,23 @@ class YtDownloadService extends ChangeNotifier {
final datei = File('${ordner.path}/${sichererDateiname(titel)}.mp3');
try {
final mp3 = await _client
.get(Uri.parse('$proxyUrl$mp3Url'), headers: auth.authHeader)
.timeout(const Duration(seconds: 180));
final mp3 = await _abbrechbar(
_client
.get(Uri.parse('$proxyUrl$mp3Url'), headers: auth.authHeader)
.timeout(const Duration(seconds: 180)),
abbruch,
);
if (mp3.statusCode != 200) {
_scheitere('Herunterladen fehlgeschlagen (${mp3.statusCode})');
return null;
}
await datei.writeAsBytes(mp3.bodyBytes);
} on _Abgebrochen {
if (await datei.exists()) {
await datei.delete();
}
_scheitere('Abgebrochen');
return null;
} on TimeoutException {
if (await datei.exists()) {
await datei.delete();