Zuletzt zeigt zuletzt Gehörtes statt neu Hinzugefügtes; Herz sitzt jetzt neben dem Songnamen
- MeloDb.watchRecentlyPlayed(): wertet PlaybackHistory statt dateAddedMs aus, ein mehrfach gehörter Song erscheint nur einmal, an der Position seines jüngsten Abspielens (Subquery mit GROUP BY songId / MAX(playedAtMs)). - "Zuletzt" (Karte + RecentlyPlayedScreen, vormals RecentlyAddedScreen) in my_music_screen.dart nutzt jetzt watchRecentlyPlayed() statt watchRecent(). - Herz im Vollbild-Player: aus der AppBar-actions-Row entfernt, sitzt jetzt direkt neben dem Songtitel in _Angaben (now_playing_screen.dart). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012A2pmbnVNPiHdyf2GW8eLP
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
90b63c467e
commit
2cabe36a6c
@@ -0,0 +1,73 @@
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:drift/native.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:melo/library/database.dart';
|
||||
|
||||
Future<void> _insertSong(MeloDb db, String id, {bool deleted = false}) {
|
||||
return db.into(db.songs).insert(SongsCompanion.insert(
|
||||
id: id,
|
||||
path: '/$id.mp3',
|
||||
title: id,
|
||||
dateAddedMs: 0,
|
||||
updatedAtMs: 0,
|
||||
deleted: Value(deleted),
|
||||
));
|
||||
}
|
||||
|
||||
Future<void> _play(MeloDb db, String songId, int playedAtMs) {
|
||||
return db.into(db.playbackHistory).insert(PlaybackHistoryCompanion.insert(
|
||||
songId: songId,
|
||||
positionMs: 0,
|
||||
playedAtMs: playedAtMs,
|
||||
));
|
||||
}
|
||||
|
||||
void main() {
|
||||
test('ein mehrfach gespielter Song erscheint nur einmal, an der Position '
|
||||
'seines jüngsten Abspielens', () async {
|
||||
final db = MeloDb(NativeDatabase.memory());
|
||||
await _insertSong(db, 'a');
|
||||
await _insertSong(db, 'b');
|
||||
await _play(db, 'a', 1000);
|
||||
await _play(db, 'b', 2000);
|
||||
await _play(db, 'a', 3000); // a erneut gespielt, jetzt jüngster Play
|
||||
|
||||
final result = await db.watchRecentlyPlayed().first;
|
||||
|
||||
expect(result.map((s) => s.id).toList(), ['a', 'b']);
|
||||
|
||||
await db.close();
|
||||
});
|
||||
|
||||
test('mehrere Songs erscheinen absteigend nach ihrem letzten Abspielen',
|
||||
() async {
|
||||
final db = MeloDb(NativeDatabase.memory());
|
||||
await _insertSong(db, 'a');
|
||||
await _insertSong(db, 'b');
|
||||
await _insertSong(db, 'c');
|
||||
await _play(db, 'a', 1000);
|
||||
await _play(db, 'b', 3000);
|
||||
await _play(db, 'c', 2000);
|
||||
|
||||
final result = await db.watchRecentlyPlayed().first;
|
||||
|
||||
expect(result.map((s) => s.id).toList(), ['b', 'c', 'a']);
|
||||
|
||||
await db.close();
|
||||
});
|
||||
|
||||
test('ein gelöschter Song mit Wiedergabe-Historie erscheint nicht',
|
||||
() async {
|
||||
final db = MeloDb(NativeDatabase.memory());
|
||||
await _insertSong(db, 'a', deleted: true);
|
||||
await _insertSong(db, 'b');
|
||||
await _play(db, 'a', 2000);
|
||||
await _play(db, 'b', 1000);
|
||||
|
||||
final result = await db.watchRecentlyPlayed().first;
|
||||
|
||||
expect(result.map((s) => s.id).toList(), ['b']);
|
||||
|
||||
await db.close();
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
import 'package:audio_service/audio_service.dart';
|
||||
import 'package:drift/drift.dart' show Value, driftRuntimeOptions;
|
||||
import 'package:drift/native.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.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/library/song_list.dart';
|
||||
import 'package:melo/player/audio_handler.dart';
|
||||
import 'package:melo/player/now_playing_screen.dart';
|
||||
import 'package:melo/settings/app_settings.dart';
|
||||
import 'package:melo/shared/favorite_button.dart';
|
||||
|
||||
/// Ersetzt in [MeloAudioHandler.loadPlaylist] den echten just_audio-Zugriff —
|
||||
/// dieselbe Klasse wie in song_list_navigation_test.dart, dort erprobt.
|
||||
class _FakeAudioHandler extends MeloAudioHandler {
|
||||
_FakeAudioHandler(MeloDb db) : super(db: db);
|
||||
|
||||
@override
|
||||
Future<int> loadPlaylist(List<MediaItem> items, {int startIndex = 0}) async {
|
||||
queue.add(items);
|
||||
if (items.isNotEmpty) mediaItem.add(items[startIndex]);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
void main() {
|
||||
late MeloDb db;
|
||||
late CategoryService kategorien;
|
||||
late PlaylistService playlists;
|
||||
late AppSettings einstellungen;
|
||||
late _FakeAudioHandler handler;
|
||||
|
||||
void stelleKanaeleRuhig() {
|
||||
final messenger =
|
||||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger;
|
||||
messenger.setMockMethodCallHandler(
|
||||
const MethodChannel('plugins.flutter.io/path_provider'),
|
||||
(call) async => '/tmp/melo-test',
|
||||
);
|
||||
messenger.setMockMethodCallHandler(
|
||||
const MethodChannel('plugins.it_nomads.com/flutter_secure_storage'),
|
||||
(call) async => null,
|
||||
);
|
||||
}
|
||||
|
||||
setUp(() async {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
driftRuntimeOptions.dontWarnAboutMultipleDatabases = true;
|
||||
SharedPreferences.setMockInitialValues({});
|
||||
stelleKanaeleRuhig();
|
||||
db = MeloDb(NativeDatabase.memory());
|
||||
kategorien = CategoryService(db);
|
||||
playlists = PlaylistService(db);
|
||||
einstellungen = AppSettings();
|
||||
await einstellungen.init();
|
||||
handler = _FakeAudioHandler(db);
|
||||
await db.into(db.songs).insert(SongsCompanion.insert(
|
||||
id: 'song-1',
|
||||
path: '/music/roses.mp3',
|
||||
title: 'Roses',
|
||||
artist: const Value('Neoni'),
|
||||
dateAddedMs: 0,
|
||||
updatedAtMs: 0,
|
||||
));
|
||||
});
|
||||
|
||||
tearDown(() {
|
||||
handler.dispose();
|
||||
kategorien.dispose();
|
||||
playlists.dispose();
|
||||
});
|
||||
|
||||
Song derSong() => Song(
|
||||
id: 'song-1',
|
||||
path: '/music/roses.mp3',
|
||||
title: 'Roses',
|
||||
artist: 'Neoni',
|
||||
dateAddedMs: 0,
|
||||
updatedAtMs: 0,
|
||||
deleted: false,
|
||||
playCount: 0,
|
||||
categoriesEdited: false,
|
||||
metadataEdited: false,
|
||||
);
|
||||
|
||||
// Öffnet den Vollbild-Player über denselben Weg wie in
|
||||
// song_list_navigation_test.dart (Tap auf eine SongZeile) — dieser Weg ist
|
||||
// dort nachweislich unproblematisch, im Unterschied zu einem direkten
|
||||
// Aufbau von NowPlayingScreen mit vorab befülltem MediaItem-Stream.
|
||||
Future<void> oeffnePlayer(WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
MultiProvider(
|
||||
providers: [
|
||||
Provider<MeloDb>.value(value: db),
|
||||
Provider<MeloAudioHandler>.value(value: handler),
|
||||
ChangeNotifierProvider<CategoryService>.value(value: kategorien),
|
||||
ChangeNotifierProvider<PlaylistService>.value(value: playlists),
|
||||
ChangeNotifierProvider<AppSettings>.value(value: einstellungen),
|
||||
],
|
||||
child: MaterialApp(
|
||||
home: Scaffold(
|
||||
body: SongZeile(
|
||||
song: derSong(),
|
||||
warteschlange: [derSong()],
|
||||
index: 0,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.tap(find.text('Roses'));
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
||||
Future<void> raeumeAuf(WidgetTester tester) async {
|
||||
// Ohne diesen Abschluss bleibt ein drift-Stream-Abo (Favoriten-Herz)
|
||||
// beim Abbau des Baums als offener Fake-Timer stehen — dieselbe
|
||||
// Aufräum-Zeile wie in song_list_navigation_test.dart.
|
||||
await tester.pumpWidget(const SizedBox());
|
||||
await tester.pump(const Duration(milliseconds: 1));
|
||||
}
|
||||
|
||||
testWidgets(
|
||||
'Herz sitzt neben dem Songnamen, nicht mehr in der AppBar',
|
||||
(tester) async {
|
||||
await oeffnePlayer(tester);
|
||||
|
||||
expect(find.byType(NowPlayingScreen), findsOneWidget);
|
||||
|
||||
// Nicht mehr in der AppBar:
|
||||
final herzInAppBar = find.descendant(
|
||||
of: find.byType(AppBar),
|
||||
matching: find.byType(FavoriteButton),
|
||||
);
|
||||
expect(herzInAppBar, findsNothing);
|
||||
|
||||
// Aber sichtbar auf dem Bildschirm, in derselben Row wie der Titel.
|
||||
final titelRow = find
|
||||
.ancestor(of: find.text('Roses'), matching: find.byType(Row))
|
||||
.first;
|
||||
final herzBeimTitel = find.descendant(
|
||||
of: titelRow,
|
||||
matching: find.byType(FavoriteButton),
|
||||
);
|
||||
expect(herzBeimTitel, findsOneWidget);
|
||||
|
||||
await raeumeAuf(tester);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user