Neue Melo-UI Phase 1: 4 Tabs (Meine Musik/Suche/Favoriten/Download) + Sortierung

- Meine Musik als Startbildschirm ohne Topbar: Kopfzeile (Einstellungen,
  Suche, Musikerkennung), Schnellzugriffe, Shuffle + Sortieren, Liederliste
- Sortierung nach Zuletzt hinzugefuegt / Name (A-Z-#) / Wie oft abgespielt,
  auf- und absteigend, pro Liste gespeichert (SortStore)
- Favoriten als eigener Tab mit gleicher Shuffle-/Sortier-Leiste
- Download-Tab uebernimmt den Navidrome-Server-Browser
- DB-Schema 3: playCount, gezaehlt beim Titelstart
- Theme auf #0B0B10 / #c0392b
- Scan-Aktionen aus der entfallenen Topbar jetzt in den Einstellungen

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011snPXPafPC5i87o68H14W7
This commit is contained in:
2026-08-20 18:00:53 +02:00
co-authored by Claude Opus 5
parent 5d08da1086
commit 2667613a2e
24 changed files with 1413 additions and 372 deletions
+149
View File
@@ -0,0 +1,149 @@
import 'package:drift/drift.dart';
import 'package:drift/native.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:melo/library/database.dart';
import 'package:melo/library/favorites_screen.dart';
import 'package:melo/library/library_service.dart';
import 'package:melo/library/my_music_screen.dart';
import 'package:melo/library/playlist_service.dart';
import 'package:melo/player/audio_handler.dart';
import 'package:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
Widget _wrap(MeloDb db, LibraryService lib, PlaylistService playlists,
MeloAudioHandler handler, Widget child) {
return MultiProvider(
providers: [
Provider<MeloDb>.value(value: db),
ChangeNotifierProvider<LibraryService>.value(value: lib),
ChangeNotifierProvider<PlaylistService>.value(value: playlists),
Provider<MeloAudioHandler>.value(value: handler),
],
child: MaterialApp(home: Scaffold(body: child)),
);
}
Future<void> _insert(
MeloDb db, {
required String id,
required String title,
int dateAddedMs = 0,
int playCount = 0,
}) {
return db.into(db.songs).insert(SongsCompanion.insert(
id: id,
path: '/$id.mp3',
title: title,
dateAddedMs: dateAddedMs,
updatedAtMs: 0,
playCount: Value(playCount),
));
}
void main() {
setUp(() => SharedPreferences.setMockInitialValues({}));
testWidgets('Meine Musik zeigt Kopfzeile, Schnellzugriffe und Shuffle',
(tester) async {
final db = MeloDb(NativeDatabase.memory());
final lib = LibraryService(db);
final playlists = PlaylistService(db);
final handler = MeloAudioHandler(db: db);
await _insert(db, id: '1', title: 'Song A');
await tester.pumpWidget(
_wrap(db, lib, playlists, handler, const MyMusicScreen()));
await tester.pumpAndSettle();
expect(find.text('Titel, Künstler und Alben suchen'), findsOneWidget);
expect(find.text('Favoriten'), findsOneWidget);
expect(find.text('Wiedergabelisten'), findsOneWidget);
expect(find.text('Shuffle-Wiedergabe'), findsOneWidget);
expect(find.text('Song A'), findsOneWidget);
handler.dispose();
await db.close();
});
testWidgets('Sortier-Menü bietet alle drei Kriterien und sortiert die Liste',
(tester) async {
final db = MeloDb(NativeDatabase.memory());
final lib = LibraryService(db);
final playlists = PlaylistService(db);
final handler = MeloAudioHandler(db: db);
await _insert(db, id: '1', title: 'Zenit', dateAddedMs: 300, playCount: 9);
await _insert(db, id: '2', title: 'Anfang', dateAddedMs: 100, playCount: 1);
await tester.pumpWidget(
_wrap(db, lib, playlists, handler, const MyMusicScreen()));
await tester.pumpAndSettle();
// Standard: zuletzt hinzugefügt zuerst.
var titles = tester.widgetList<Text>(find.byType(Text))
.map((t) => t.data)
.where((t) => t == 'Zenit' || t == 'Anfang')
.toList();
expect(titles.first, 'Zenit');
await tester.tap(find.byIcon(Icons.swap_vert));
await tester.pumpAndSettle();
expect(find.text('Zuletzt hinzugefügt'), findsOneWidget);
expect(find.text('Name (AZ#)'), findsOneWidget);
expect(find.text('Wie oft abgespielt'), findsOneWidget);
await tester.tap(find.text('Name (AZ#)'));
await tester.pumpAndSettle();
await tester.tap(find.text('Übernehmen'));
await tester.pumpAndSettle();
titles = tester.widgetList<Text>(find.byType(Text))
.map((t) => t.data)
.where((t) => t == 'Zenit' || t == 'Anfang')
.toList();
expect(titles.first, 'Anfang');
handler.dispose();
await db.close();
});
testWidgets('Favoriten-Tab zeigt nur favorisierte Songs mit Shuffle',
(tester) async {
final db = MeloDb(NativeDatabase.memory());
final lib = LibraryService(db);
final playlists = PlaylistService(db);
final handler = MeloAudioHandler(db: db);
await _insert(db, id: '1', title: 'Geliebt');
await _insert(db, id: '2', title: 'Egal');
await db.setFavorite('1', true);
await tester.pumpWidget(
_wrap(db, lib, playlists, handler, const FavoritesScreen()));
await tester.pumpAndSettle();
expect(find.text('Shuffle-Wiedergabe'), findsOneWidget);
expect(find.text('Geliebt'), findsOneWidget);
expect(find.text('Egal'), findsNothing);
handler.dispose();
await db.close();
});
testWidgets('Favoriten-Tab zeigt einen Hinweis, wenn nichts favorisiert ist',
(tester) async {
final db = MeloDb(NativeDatabase.memory());
final lib = LibraryService(db);
final playlists = PlaylistService(db);
final handler = MeloAudioHandler(db: db);
await tester.pumpWidget(
_wrap(db, lib, playlists, handler, const FavoritesScreen()));
await tester.pumpAndSettle();
expect(find.text('Noch keine Favoriten'), findsOneWidget);
handler.dispose();
await db.close();
});
}