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:
@@ -10,6 +10,8 @@ import 'package:melo/downloads/downloads_screen.dart';
|
||||
import 'package:melo/library/category_service.dart';
|
||||
import 'package:melo/library/database.dart';
|
||||
import 'package:melo/services/baka_auth.dart';
|
||||
import 'package:melo/services/download_service.dart';
|
||||
import 'package:melo/services/navidrome_service.dart';
|
||||
import 'package:melo/services/yt_download_service.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
@@ -40,6 +42,11 @@ Widget _wrap(BakaAuth auth,
|
||||
client: MockClient((_) async => http.Response('', 500)),
|
||||
),
|
||||
),
|
||||
// Nur fürs Badge im Unterreiter-Kopf gebraucht (dienst.laeuft) — der
|
||||
// Download-Tab beobachtet ihn app-weit, unabhängig vom Unterreiter.
|
||||
ChangeNotifierProvider<DownloadService>(
|
||||
create: (_) => DownloadService(db: db, navidrome: NavidromeService()),
|
||||
),
|
||||
],
|
||||
child: const MaterialApp(home: Scaffold(body: DownloadsScreen())),
|
||||
);
|
||||
|
||||
@@ -8,7 +8,9 @@ import 'package:melo/library/playlist_service.dart';
|
||||
import 'package:melo/main.dart';
|
||||
import 'package:melo/player/audio_handler.dart';
|
||||
import 'package:melo/services/baka_auth.dart';
|
||||
import 'package:melo/services/download_service.dart';
|
||||
import 'package:melo/services/melo_cloud_service.dart';
|
||||
import 'package:melo/services/navidrome_service.dart';
|
||||
import 'package:melo/services/offline_mode.dart';
|
||||
import 'package:melo/services/sync_service.dart';
|
||||
import 'package:melo/services/yt_download_service.dart';
|
||||
@@ -39,6 +41,7 @@ void main() {
|
||||
late AppSettings einstellungen;
|
||||
late OfflineMode offline;
|
||||
late YtDownloadService ytDownload;
|
||||
late DownloadService downloads;
|
||||
late SyncService sync;
|
||||
MeloAudioHandler? handler;
|
||||
|
||||
@@ -52,6 +55,7 @@ void main() {
|
||||
einstellungen = AppSettings();
|
||||
offline = OfflineMode();
|
||||
ytDownload = YtDownloadService(auth: auth);
|
||||
downloads = DownloadService(db: db, navidrome: NavidromeService());
|
||||
sync = SyncService(db: db, cloud: MeloCloudService(auth: auth));
|
||||
});
|
||||
|
||||
@@ -83,6 +87,7 @@ void main() {
|
||||
ChangeNotifierProvider<OfflineMode>.value(value: offline),
|
||||
ChangeNotifierProvider<BakaAuth>.value(value: auth),
|
||||
ChangeNotifierProvider<YtDownloadService>.value(value: ytDownload),
|
||||
ChangeNotifierProvider<DownloadService>.value(value: downloads),
|
||||
ChangeNotifierProvider<SyncService>.value(value: sync),
|
||||
],
|
||||
child: MaterialApp(theme: MeloTheme.dark, home: const HomeShell()),
|
||||
|
||||
@@ -8,7 +8,9 @@ import 'package:melo/library/playlist_service.dart';
|
||||
import 'package:melo/main.dart';
|
||||
import 'package:melo/player/audio_handler.dart';
|
||||
import 'package:melo/services/baka_auth.dart';
|
||||
import 'package:melo/services/download_service.dart';
|
||||
import 'package:melo/services/melo_cloud_service.dart';
|
||||
import 'package:melo/services/navidrome_service.dart';
|
||||
import 'package:melo/services/offline_mode.dart';
|
||||
import 'package:melo/services/sync_service.dart';
|
||||
import 'package:melo/services/yt_download_service.dart';
|
||||
@@ -49,6 +51,8 @@ void main() {
|
||||
ChangeNotifierProvider<BakaAuth>(create: (_) => auth),
|
||||
ChangeNotifierProvider<YtDownloadService>(
|
||||
create: (_) => YtDownloadService(auth: auth)),
|
||||
ChangeNotifierProvider<DownloadService>(
|
||||
create: (_) => DownloadService(db: db, navidrome: NavidromeService())),
|
||||
ChangeNotifierProvider<SyncService>(
|
||||
create: (_) => SyncService(
|
||||
db: db, cloud: MeloCloudService(auth: auth))),
|
||||
|
||||
@@ -49,4 +49,47 @@ void main() {
|
||||
expect(oeffnetVollbild(0), isFalse);
|
||||
});
|
||||
});
|
||||
|
||||
group('titelWechselMitStrecke', () {
|
||||
test('kräftiger Schwung reicht wie bisher, egal wie kurz die Strecke',
|
||||
() {
|
||||
expect(titelWechselMitStrecke(-5, -1200), Wechsel.naechster);
|
||||
expect(titelWechselMitStrecke(5, 1200), Wechsel.vorheriger);
|
||||
});
|
||||
|
||||
test('eine weite, aber langsame Bewegung wechselt trotzdem', () {
|
||||
expect(titelWechselMitStrecke(-wischStreckeSchwelle - 1, 0),
|
||||
Wechsel.naechster);
|
||||
expect(titelWechselMitStrecke(wischStreckeSchwelle + 1, 0),
|
||||
Wechsel.vorheriger);
|
||||
});
|
||||
|
||||
test('kurz und langsam wechselt nichts', () {
|
||||
expect(titelWechselMitStrecke(20, 80), isNull);
|
||||
});
|
||||
|
||||
test('genau auf der Streckenschwelle wechselt es noch nicht', () {
|
||||
expect(titelWechselMitStrecke(wischStreckeSchwelle, 0), isNull);
|
||||
});
|
||||
});
|
||||
|
||||
group('oeffnetVollbildMitStrecke', () {
|
||||
test('kräftiger Schwung reicht wie bisher, egal wie kurz die Strecke',
|
||||
() {
|
||||
expect(oeffnetVollbildMitStrecke(-5, -1200), isTrue);
|
||||
});
|
||||
|
||||
test('eine weite, aber langsame Bewegung nach oben öffnet trotzdem', () {
|
||||
expect(oeffnetVollbildMitStrecke(-wischStreckeSchwelle - 1, 0), isTrue);
|
||||
});
|
||||
|
||||
test('dieselbe Strecke nach unten öffnet nicht — nur oben ist belegt',
|
||||
() {
|
||||
expect(oeffnetVollbildMitStrecke(wischStreckeSchwelle + 1, 0), isFalse);
|
||||
});
|
||||
|
||||
test('kurz und langsam öffnet nichts', () {
|
||||
expect(oeffnetVollbildMitStrecke(-20, -80), isFalse);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -74,6 +74,28 @@ void main() {
|
||||
'Der Server braucht zu lange — bitte später erneut versuchen');
|
||||
});
|
||||
|
||||
test('abbrechen() beendet einen hängenden Download sofort', () async {
|
||||
// Antwortet nie — genau der Fall, in dem der Nutzer heute nicht mehr
|
||||
// herankam.
|
||||
final haengt = Completer<http.Response>();
|
||||
final dienst = YtDownloadService(
|
||||
auth: await _angemeldeteAuth(),
|
||||
client: MockClient((_) => haengt.future),
|
||||
);
|
||||
|
||||
final ergebnisFuture = dienst.herunterladen('https://youtu.be/dQw4w9WgXcQ',
|
||||
zielOrdner: ziel.path);
|
||||
// Der Service braucht einen Mikrotask, um _abbruch zu setzen, bevor
|
||||
// abbrechen() etwas zu vervollständigen hat.
|
||||
await Future<void>.delayed(Duration.zero);
|
||||
dienst.abbrechen();
|
||||
final ergebnis = await ergebnisFuture;
|
||||
|
||||
expect(ergebnis, isNull);
|
||||
expect(dienst.fehler, 'Abgebrochen');
|
||||
expect(dienst.laeuft, isFalse);
|
||||
});
|
||||
|
||||
test('eine tote Verbindung zum Proxy meldet sich anders als ein Timeout',
|
||||
() async {
|
||||
final dienst = YtDownloadService(
|
||||
|
||||
@@ -81,6 +81,57 @@ void main() {
|
||||
expect(find.byType(InkWell), findsNWidgets(2));
|
||||
});
|
||||
|
||||
bool istPunkt(Widget w) =>
|
||||
w is Container && (w.decoration as BoxDecoration?)?.shape == BoxShape.circle;
|
||||
|
||||
testWidgets(
|
||||
'ein Punkt zeigt an, dass im anderen Reiter etwas im Hintergrund läuft',
|
||||
(tester) async {
|
||||
await tester.pumpWidget(_wrap(SubTabs(
|
||||
labels: const ['YouTube', 'Server'],
|
||||
index: 0,
|
||||
badges: const [false, true],
|
||||
onChanged: (_) {},
|
||||
)));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(
|
||||
find.descendant(
|
||||
of: find.ancestor(
|
||||
of: find.text('Server'), matching: find.byType(AnimatedContainer)),
|
||||
matching: find.byWidgetPredicate(istPunkt),
|
||||
),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(
|
||||
find.descendant(
|
||||
of: find.ancestor(
|
||||
of: find.text('YouTube'),
|
||||
matching: find.byType(AnimatedContainer)),
|
||||
matching: find.byWidgetPredicate(istPunkt),
|
||||
),
|
||||
findsNothing,
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('ohne badges-Angabe bleibt ein Reiter ohne Punkt', (tester) async {
|
||||
await tester.pumpWidget(_wrap(SubTabs(
|
||||
labels: const ['Lieder', 'Kategorie'],
|
||||
index: 0,
|
||||
onChanged: (_) {},
|
||||
)));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(
|
||||
find.descendant(
|
||||
of: find.ancestor(
|
||||
of: find.text('Lieder'), matching: find.byType(AnimatedContainer)),
|
||||
matching: find.byWidgetPredicate(istPunkt),
|
||||
),
|
||||
findsNothing,
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('der Wechsel wird animiert, nicht gesprungen', (tester) async {
|
||||
await tester.pumpWidget(_wrap(SubTabs(
|
||||
labels: const ['Lieder', 'Kategorie'],
|
||||
|
||||
Reference in New Issue
Block a user