Auswahl-Modus: Auf den Server laden (nur in Meine Musik)
This commit is contained in:
@@ -120,6 +120,7 @@ class _MyMusicScreenState extends State<MyMusicScreen> {
|
||||
return SortableSongList(
|
||||
songs: songs,
|
||||
storeKey: SortStore.meineMusik,
|
||||
serverUpload: true,
|
||||
empty:
|
||||
lib.scanning ? const SizedBox.shrink() : const _Empty(),
|
||||
);
|
||||
|
||||
@@ -7,6 +7,7 @@ import '../library/playlist_service.dart';
|
||||
import '../library/song_media.dart';
|
||||
import '../player/audio_handler.dart';
|
||||
import '../playlists/create_playlist_dialog.dart';
|
||||
import '../services/sync_service.dart';
|
||||
import '../settings/app_settings.dart';
|
||||
import 'auswahl.dart';
|
||||
import 'theme.dart';
|
||||
@@ -22,6 +23,7 @@ class AuswahlLeiste extends StatelessWidget {
|
||||
required this.onAbbrechen,
|
||||
required this.onWiedergabeliste,
|
||||
required this.onWarteschlange,
|
||||
this.onServerLaden,
|
||||
});
|
||||
|
||||
final int anzahl;
|
||||
@@ -29,6 +31,10 @@ class AuswahlLeiste extends StatelessWidget {
|
||||
final VoidCallback onWiedergabeliste;
|
||||
final VoidCallback onWarteschlange;
|
||||
|
||||
/// Nur gesetzt, wo der Upload hingehört („Meine Musik"). Sonst erschiene
|
||||
/// die Aktion in allen fünf Ansichten, die diese Leiste benutzen.
|
||||
final VoidCallback? onServerLaden;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
@@ -49,6 +55,12 @@ class AuswahlLeiste extends StatelessWidget {
|
||||
style: const TextStyle(fontWeight: FontWeight.w600),
|
||||
),
|
||||
),
|
||||
if (onServerLaden != null)
|
||||
IconButton(
|
||||
tooltip: 'Auf den Server laden',
|
||||
icon: const Icon(Icons.cloud_upload),
|
||||
onPressed: onServerLaden,
|
||||
),
|
||||
IconButton(
|
||||
tooltip: 'Zur Warteschlange hinzufügen',
|
||||
icon: const Icon(Icons.queue),
|
||||
@@ -159,3 +171,18 @@ Future<void> fuegeZuWarteschlangeHinzu(
|
||||
: '$angehaengt von ${gewaehlte.length} Titeln angehängt'),
|
||||
));
|
||||
}
|
||||
|
||||
/// Lädt [gewaehlte] zum Melo-Server und meldet das Ergebnis.
|
||||
///
|
||||
/// Wohnt neben [fuegeZuWiedergabelisteHinzu]: dieselbe Bauart, dieselbe Art
|
||||
/// Rückmeldung.
|
||||
Future<void> ladeAufServer(
|
||||
BuildContext context, List<Song> gewaehlte) async {
|
||||
final sync = context.read<SyncService>();
|
||||
final messenger = ScaffoldMessenger.of(context);
|
||||
final ergebnis = await sync.ladeAusgewaehlteHoch(gewaehlte);
|
||||
if (!context.mounted) return;
|
||||
messenger.showSnackBar(
|
||||
SnackBar(content: Text(sync.fehler ?? ergebnis.meldung)),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ class SortableSongList extends StatefulWidget {
|
||||
required this.songs,
|
||||
required this.storeKey,
|
||||
this.empty,
|
||||
this.serverUpload = false,
|
||||
});
|
||||
|
||||
final List<Song> songs;
|
||||
@@ -31,6 +32,10 @@ class SortableSongList extends StatefulWidget {
|
||||
/// Wird statt der Liste gezeigt, wenn [songs] leer ist.
|
||||
final Widget? empty;
|
||||
|
||||
/// Ob der Auswahl-Modus „Auf den Server laden" anbietet. Standard `false`:
|
||||
/// dieses Widget steckt in fünf Ansichten, gemeint ist nur „Meine Musik".
|
||||
final bool serverUpload;
|
||||
|
||||
@override
|
||||
State<SortableSongList> createState() => _SortableSongListState();
|
||||
}
|
||||
@@ -67,6 +72,11 @@ class _SortableSongListState extends State<SortableSongList> {
|
||||
if (mounted) _beendeAuswahl();
|
||||
}
|
||||
|
||||
Future<void> _aufServer(List<Song> gewaehlte) async {
|
||||
await ladeAufServer(context, gewaehlte);
|
||||
if (mounted) _beendeAuswahl();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_scroll.dispose();
|
||||
@@ -196,6 +206,9 @@ class _SortableSongListState extends State<SortableSongList> {
|
||||
onAbbrechen: _beendeAuswahl,
|
||||
onWiedergabeliste: () => _inWiedergabeliste(_gewaehlte(sorted)),
|
||||
onWarteschlange: () => _inWarteschlange(_gewaehlte(sorted)),
|
||||
onServerLaden: widget.serverUpload
|
||||
? () => _aufServer(_gewaehlte(sorted))
|
||||
: null,
|
||||
)
|
||||
else
|
||||
Padding(
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
import 'package:drift/drift.dart' show Value, driftRuntimeOptions;
|
||||
import 'package:drift/native.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:http/testing.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:melo/library/category_service.dart';
|
||||
import 'package:melo/library/database.dart';
|
||||
import 'package:melo/library/playlist_service.dart';
|
||||
import 'package:melo/services/baka_auth.dart';
|
||||
import 'package:melo/services/melo_cloud_service.dart';
|
||||
import 'package:melo/services/sync_service.dart';
|
||||
import 'package:melo/settings/app_settings.dart';
|
||||
import 'package:melo/shared/sort_store.dart';
|
||||
import 'package:melo/shared/sortable_song_list.dart';
|
||||
|
||||
class _MemorySpeicher implements TokenSpeicher {
|
||||
_MemorySpeicher(this.werte);
|
||||
final Map<String, String> werte;
|
||||
@override
|
||||
Future<String?> lesen(String key) async => werte[key];
|
||||
@override
|
||||
Future<void> schreiben(String key, String wert) async => werte[key] = wert;
|
||||
@override
|
||||
Future<void> loeschen(String key) async => werte.remove(key);
|
||||
}
|
||||
|
||||
/// Merkt sich nur, was hochgeladen werden sollte. Der echte Upload braucht
|
||||
/// Dateien und einen Server — hier geht es um den Weg vom Knopf zum Dienst.
|
||||
class _FakeSync extends SyncService {
|
||||
_FakeSync(MeloDb db)
|
||||
: super(
|
||||
db: db,
|
||||
cloud: MeloCloudService(
|
||||
auth: BakaAuth(speicher: _MemorySpeicher({})),
|
||||
client: MockClient(
|
||||
(_) async => http.Response('{"status":"ok"}', 200)),
|
||||
),
|
||||
);
|
||||
|
||||
final hochgeladen = <String>[];
|
||||
|
||||
@override
|
||||
Future<UploadErgebnis> ladeAusgewaehlteHoch(List<Song> songs) async {
|
||||
hochgeladen.addAll([for (final s in songs) s.id]);
|
||||
return const UploadErgebnis(hochgeladen: 1);
|
||||
}
|
||||
}
|
||||
|
||||
/// „Auf den Server laden" gehört in „Meine Musik" — und **nur** dorthin.
|
||||
/// [SortableSongList] wird in fünf Ansichten benutzt; ohne Scoping erschiene
|
||||
/// die Aktion auch bei Favoriten, Wiedergabelisten und Titellisten.
|
||||
///
|
||||
/// Aufbau bewusst im Testkörper, nicht in `setUp` (siehe auswahl_modus_test).
|
||||
void main() {
|
||||
final lieder = [
|
||||
for (var i = 0; i < 3; i++)
|
||||
Song(
|
||||
id: 'song-$i',
|
||||
path: '/music/$i.mp3',
|
||||
title: 'Titel $i',
|
||||
artist: 'Neoni',
|
||||
dateAddedMs: i,
|
||||
updatedAtMs: 0,
|
||||
deleted: false,
|
||||
playCount: 0,
|
||||
categoriesEdited: false,
|
||||
metadataEdited: false,
|
||||
),
|
||||
];
|
||||
|
||||
Future<void> beruhige(WidgetTester tester) => tester.pumpAndSettle(
|
||||
const Duration(milliseconds: 100),
|
||||
EnginePhase.sendSemanticsUpdate,
|
||||
const Duration(seconds: 5),
|
||||
);
|
||||
|
||||
/// Räumt den Widget-Baum kontrolliert ab, statt es dem Testrahmen am
|
||||
/// Testende zu überlassen: sonst bleibt ein von drift/SnackBar beim
|
||||
/// Aufräumen geplanter Timer offen (siehe auswahl_modus_test.dart /
|
||||
/// BACKLOG.md — derselbe Hänger, dieselbe Ursache).
|
||||
Future<void> aufraeumen(WidgetTester tester) async {
|
||||
await tester.pumpWidget(const SizedBox());
|
||||
await tester.pump(const Duration(milliseconds: 1));
|
||||
}
|
||||
|
||||
Future<_FakeSync> pumpe(WidgetTester tester,
|
||||
{required bool serverUpload}) async {
|
||||
driftRuntimeOptions.dontWarnAboutMultipleDatabases = true;
|
||||
SharedPreferences.setMockInitialValues({});
|
||||
final db = MeloDb(NativeDatabase.memory());
|
||||
// Kein db.close(): drift plant dabei eine Aufräum-Aufgabe (Timer) ein,
|
||||
// die der Testrahmen nicht mehr abarbeitet (siehe BACKLOG.md und
|
||||
// auswahl_modus_test.dart — derselbe Hänger, dieselbe Ursache).
|
||||
final sync = _FakeSync(db);
|
||||
final einstellungen = AppSettings();
|
||||
await einstellungen.init();
|
||||
for (final song in lieder) {
|
||||
await db.into(db.songs).insert(SongsCompanion.insert(
|
||||
id: song.id,
|
||||
path: song.path,
|
||||
title: song.title,
|
||||
artist: Value(song.artist),
|
||||
dateAddedMs: song.dateAddedMs,
|
||||
updatedAtMs: 0,
|
||||
));
|
||||
}
|
||||
await tester.pumpWidget(
|
||||
MultiProvider(
|
||||
providers: [
|
||||
Provider<MeloDb>.value(value: db),
|
||||
ChangeNotifierProvider<CategoryService>.value(
|
||||
value: CategoryService(db)),
|
||||
ChangeNotifierProvider<PlaylistService>.value(
|
||||
value: PlaylistService(db)),
|
||||
ChangeNotifierProvider<AppSettings>.value(value: einstellungen),
|
||||
// Ohne diesen Provider stürbe schon das erste Antippen der neuen
|
||||
// Aktion in ladeAufServer mit ProviderNotFoundException.
|
||||
ChangeNotifierProvider<SyncService>.value(value: sync),
|
||||
],
|
||||
child: MaterialApp(
|
||||
home: Scaffold(
|
||||
body: SortableSongList(
|
||||
songs: lieder,
|
||||
storeKey: SortStore.meineMusik,
|
||||
serverUpload: serverUpload,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await beruhige(tester);
|
||||
await tester.longPress(find.text('Titel 0'));
|
||||
await beruhige(tester);
|
||||
return sync;
|
||||
}
|
||||
|
||||
testWidgets('in „Meine Musik" erscheint die Server-Aktion', (tester) async {
|
||||
await pumpe(tester, serverUpload: true);
|
||||
|
||||
expect(find.byTooltip('Auf den Server laden'), findsOneWidget);
|
||||
|
||||
await aufraeumen(tester);
|
||||
});
|
||||
|
||||
testWidgets('in den übrigen Ansichten erscheint sie nicht', (tester) async {
|
||||
await pumpe(tester, serverUpload: false);
|
||||
|
||||
// Der Auswahl-Modus läuft, die beiden Bestands-Aktionen sind da …
|
||||
expect(find.byTooltip('Zur Warteschlange hinzufügen'), findsOneWidget);
|
||||
// … die neue nicht.
|
||||
expect(find.byTooltip('Auf den Server laden'), findsNothing);
|
||||
|
||||
await aufraeumen(tester);
|
||||
});
|
||||
|
||||
testWidgets('das Antippen reicht die Auswahl an den Upload weiter',
|
||||
(tester) async {
|
||||
final sync = await pumpe(tester, serverUpload: true);
|
||||
|
||||
await tester.tap(find.byTooltip('Auf den Server laden'));
|
||||
await beruhige(tester);
|
||||
|
||||
// Ein sichtbarer Knopf ist noch keine Funktion: geprüft wird, dass genau
|
||||
// der lang gedrückte Titel bei ladeAusgewaehlteHoch ankommt.
|
||||
expect(sync.hochgeladen, ['song-0']);
|
||||
expect(find.text('1 hochgeladen'), findsOneWidget);
|
||||
|
||||
await aufraeumen(tester);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user