Tab-Struktur nach Referenz-UI: Online-Tab + Unterreiter Songs/Kuenstler/Alben
Untere Navigation folgt jetzt den Referenzbildern: Meine Musik / Online / Suchen / Favoriten. Der bisherige "Download"-Tab heisst "Online" und bekommt als Naechstes den YouTube-Downloader. In "Meine Musik" gibt es Unterreiter (Songs/Kuenstler/Alben) im Pillen-Stil der Referenz. Sie wechseln nur den Inhalt statt einen Bildschirm zu oeffnen; die Schnellzugriff-Kacheln "Kuenstler"/"Alben" entfallen dadurch, ebenso die damit tot gewordenen ArtistsScreen/AlbumsScreen-Huellen. 158 Tests gruen, flutter analyze ohne Befund. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018NLW1q1hzEC9goDQ1dJ98d
This commit is contained in:
co-authored by
Claude Opus 5
parent
4ebd0a8dd4
commit
a625d70da8
@@ -5,6 +5,25 @@ Format angelehnt an [Keep a Changelog](https://keepachangelog.com/de/1.0.0/).
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
### 🧭 Neue Tab-Struktur nach deinen Referenzbildern (2026-08-20)
|
||||||
|
- 🔀 **Untere Leiste wie auf deinem Bild:** Die Reihenfolge ist jetzt
|
||||||
|
**Meine Musik · Online · Suchen · Favoriten**. Der frühere Reiter
|
||||||
|
"Download" heißt jetzt **Online** (Musiknoten-Symbol) — dort kommt als
|
||||||
|
Nächstes der YouTube-Downloader dazu.
|
||||||
|
- 🗂️ **Unterreiter in "Meine Musik":** Über der Liste sitzen jetzt
|
||||||
|
**Songs · Künstler · Alben** als Pillen — genau wie in der Referenz. Der
|
||||||
|
aktive Reiter ist hell, die anderen sind nur Text.
|
||||||
|
Wichtig: Ein Unterreiter **wechselt nur den Inhalt** und öffnet keinen
|
||||||
|
neuen Bildschirm — Suchfeld und Schnellzugriffe bleiben stehen.
|
||||||
|
- 🧹 **Aufgeräumt:** Die Schnellzugriff-Kacheln "Künstler" und "Alben" sind
|
||||||
|
weg, weil sie jetzt Unterreiter sind. Übrig bleiben — wie in der Referenz —
|
||||||
|
**Favoriten · Wiedergabelisten · Zuletzt**.
|
||||||
|
- 🔧 **Details:** `MyMusicScreen` ist jetzt zustandsbehaftet (`_subTab`), neues
|
||||||
|
`_SubTabs`-Widget; die dadurch toten `ArtistsScreen`/`AlbumsScreen`-Hüllen
|
||||||
|
wurden entfernt (`ArtistListScreen`/`AlbumListScreen` bleiben und werden
|
||||||
|
direkt eingebettet).
|
||||||
|
- ✅ **Getestet:** 158 Tests grün (4 neue), `flutter analyze` ohne Befund.
|
||||||
|
|
||||||
### 🎼 Songtext, Warteschlange, Sperrbildschirm-Cover und gleiche Lautstärke (2026-08-20)
|
### 🎼 Songtext, Warteschlange, Sperrbildschirm-Cover und gleiche Lautstärke (2026-08-20)
|
||||||
- 📝 **Automatischer Songtext:** Beim Scan wird der Songtext aus dem Tag der
|
- 📝 **Automatischer Songtext:** Beim Scan wird der Songtext aus dem Tag der
|
||||||
Datei mitgelesen und in der Wiedergabe sofort angezeigt — **ohne Server und
|
Datei mitgelesen und in der Wiedergabe sofort angezeigt — **ohne Server und
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import 'package:provider/provider.dart';
|
|||||||
import '../player/audio_handler.dart';
|
import '../player/audio_handler.dart';
|
||||||
import '../services/navidrome_service.dart';
|
import '../services/navidrome_service.dart';
|
||||||
|
|
||||||
/// Download-Tab: durchsucht den verbundenen Navidrome-Server. Abgespielte
|
/// Online-Tab: durchsucht den verbundenen Navidrome-Server. Abgespielte
|
||||||
/// Titel werden vom [MeloAudioHandler] automatisch lokal zwischengespeichert
|
/// Titel werden vom [MeloAudioHandler] automatisch lokal zwischengespeichert
|
||||||
/// und stehen danach offline zur Verfügung.
|
/// und stehen danach offline zur Verfügung.
|
||||||
class DownloadsScreen extends StatelessWidget {
|
class DownloadsScreen extends StatelessWidget {
|
||||||
@@ -21,7 +21,7 @@ class DownloadsScreen extends StatelessWidget {
|
|||||||
padding: EdgeInsets.fromLTRB(20, 16, 20, 4),
|
padding: EdgeInsets.fromLTRB(20, 16, 20, 4),
|
||||||
child: Align(
|
child: Align(
|
||||||
alignment: Alignment.centerLeft,
|
alignment: Alignment.centerLeft,
|
||||||
child: Text('Download',
|
child: Text('Online',
|
||||||
style: TextStyle(fontSize: 24, fontWeight: FontWeight.w700)),
|
style: TextStyle(fontSize: 24, fontWeight: FontWeight.w700)),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -67,16 +67,3 @@ class _AlbumSongsScreen extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Alben-Übersicht als eigener Bildschirm (Schnellzugriff "Alben").
|
|
||||||
class AlbumsScreen extends StatelessWidget {
|
|
||||||
const AlbumsScreen({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Scaffold(
|
|
||||||
appBar: AppBar(title: const Text('Alben')),
|
|
||||||
body: const AlbumListScreen(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -59,16 +59,3 @@ class _ArtistSongsScreen extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Künstler-Übersicht als eigener Bildschirm (Schnellzugriff "Künstler").
|
|
||||||
class ArtistsScreen extends StatelessWidget {
|
|
||||||
const ArtistsScreen({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Scaffold(
|
|
||||||
appBar: AppBar(title: const Text('Künstler')),
|
|
||||||
body: const ArtistListScreen(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -15,9 +15,9 @@ import 'permissions.dart';
|
|||||||
import 'song_list.dart';
|
import 'song_list.dart';
|
||||||
|
|
||||||
/// Startbildschirm: Kopfzeile mit Einstellungen/Suche/Musikerkennung,
|
/// Startbildschirm: Kopfzeile mit Einstellungen/Suche/Musikerkennung,
|
||||||
/// Schnellzugriffe, darunter Shuffle + sortierbare Liederliste.
|
/// Schnellzugriffe, Unterreiter (Songs/Künstler/Alben) und darunter der
|
||||||
/// Bewusst ohne AppBar — die Kopfzeile ist Teil des Inhalts.
|
/// jeweilige Inhalt. Bewusst ohne AppBar — die Kopfzeile ist Teil des Inhalts.
|
||||||
class MyMusicScreen extends StatelessWidget {
|
class MyMusicScreen extends StatefulWidget {
|
||||||
const MyMusicScreen({super.key, this.onSearchTap, this.onFavoritesTap});
|
const MyMusicScreen({super.key, this.onSearchTap, this.onFavoritesTap});
|
||||||
|
|
||||||
/// Wechselt zum Suche-Tab; die Suche ist ein eigener Tab, kein eigener Screen.
|
/// Wechselt zum Suche-Tab; die Suche ist ein eigener Tab, kein eigener Screen.
|
||||||
@@ -26,6 +26,14 @@ class MyMusicScreen extends StatelessWidget {
|
|||||||
/// Wechselt zum Favoriten-Tab.
|
/// Wechselt zum Favoriten-Tab.
|
||||||
final VoidCallback? onFavoritesTap;
|
final VoidCallback? onFavoritesTap;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<MyMusicScreen> createState() => _MyMusicScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MyMusicScreenState extends State<MyMusicScreen> {
|
||||||
|
/// 0 = Songs, 1 = Künstler, 2 = Alben.
|
||||||
|
int _subTab = 0;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final db = context.read<MeloDb>();
|
final db = context.read<MeloDb>();
|
||||||
@@ -34,7 +42,7 @@ class MyMusicScreen extends StatelessWidget {
|
|||||||
bottom: false,
|
bottom: false,
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
_Header(onSearchTap: onSearchTap),
|
_Header(onSearchTap: widget.onSearchTap),
|
||||||
if (lib.permissionDenied)
|
if (lib.permissionDenied)
|
||||||
MaterialBanner(
|
MaterialBanner(
|
||||||
backgroundColor: Colors.red.shade900,
|
backgroundColor: Colors.red.shade900,
|
||||||
@@ -73,25 +81,34 @@ class MyMusicScreen extends StatelessWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
_QuickAccessRow(onFavoritesTap: onFavoritesTap),
|
_QuickAccessRow(onFavoritesTap: widget.onFavoritesTap),
|
||||||
|
_SubTabs(
|
||||||
|
index: _subTab,
|
||||||
|
onChanged: (i) => setState(() => _subTab = i),
|
||||||
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: StreamBuilder<List<Song>>(
|
child: switch (_subTab) {
|
||||||
stream: db.watchSongs(),
|
1 => const ArtistListScreen(),
|
||||||
builder: (context, snapshot) {
|
2 => const AlbumListScreen(),
|
||||||
if (snapshot.hasError) {
|
_ => StreamBuilder<List<Song>>(
|
||||||
return Center(
|
stream: db.watchSongs(),
|
||||||
child: Text('Fehler: ${snapshot.error}',
|
builder: (context, snapshot) {
|
||||||
style: const TextStyle(color: Colors.white54)),
|
if (snapshot.hasError) {
|
||||||
);
|
return Center(
|
||||||
}
|
child: Text('Fehler: ${snapshot.error}',
|
||||||
final songs = snapshot.data ?? const <Song>[];
|
style: const TextStyle(color: Colors.white54)),
|
||||||
return SortableSongList(
|
);
|
||||||
songs: songs,
|
}
|
||||||
storeKey: SortStore.meineMusik,
|
final songs = snapshot.data ?? const <Song>[];
|
||||||
empty: lib.scanning ? const SizedBox.shrink() : const _Empty(),
|
return SortableSongList(
|
||||||
);
|
songs: songs,
|
||||||
},
|
storeKey: SortStore.meineMusik,
|
||||||
),
|
empty:
|
||||||
|
lib.scanning ? const SizedBox.shrink() : const _Empty(),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -154,6 +171,52 @@ class _Header extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Unterreiter im Stil der Referenz-UI: der aktive Reiter ist eine helle
|
||||||
|
/// Pille, die übrigen sind nur Text.
|
||||||
|
class _SubTabs extends StatelessWidget {
|
||||||
|
const _SubTabs({required this.index, required this.onChanged});
|
||||||
|
|
||||||
|
final int index;
|
||||||
|
final ValueChanged<int> onChanged;
|
||||||
|
|
||||||
|
static const _labels = ['Songs', 'Künstler', 'Alben'];
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(12, 12, 12, 4),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
for (var i = 0; i < _labels.length; i++)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(right: 8),
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: () => onChanged(i),
|
||||||
|
child: Container(
|
||||||
|
padding:
|
||||||
|
const EdgeInsets.symmetric(horizontal: 18, vertical: 9),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: i == index ? Colors.white : Colors.transparent,
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
_labels[i],
|
||||||
|
style: TextStyle(
|
||||||
|
color: i == index ? Colors.black : Colors.white70,
|
||||||
|
fontSize: 15,
|
||||||
|
fontWeight:
|
||||||
|
i == index ? FontWeight.w600 : FontWeight.w400,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Waagerecht scrollende Schnellzugriffe (Favoriten, Wiedergabelisten, …).
|
/// Waagerecht scrollende Schnellzugriffe (Favoriten, Wiedergabelisten, …).
|
||||||
class _QuickAccessRow extends StatelessWidget {
|
class _QuickAccessRow extends StatelessWidget {
|
||||||
const _QuickAccessRow({this.onFavoritesTap});
|
const _QuickAccessRow({this.onFavoritesTap});
|
||||||
@@ -210,20 +273,6 @@ class _QuickAccessRow extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
_QuickCard(
|
|
||||||
icon: Icons.person,
|
|
||||||
label: 'Künstler',
|
|
||||||
onTap: () => Navigator.of(context).push(
|
|
||||||
MaterialPageRoute(builder: (_) => const ArtistsScreen()),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
_QuickCard(
|
|
||||||
icon: Icons.album,
|
|
||||||
label: 'Alben',
|
|
||||||
onTap: () => Navigator.of(context).push(
|
|
||||||
MaterialPageRoute(builder: (_) => const AlbumsScreen()),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
+6
-6
@@ -107,12 +107,12 @@ class _HomeShellState extends State<HomeShell> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final tabs = <Widget>[
|
final tabs = <Widget>[
|
||||||
MyMusicScreen(
|
MyMusicScreen(
|
||||||
onSearchTap: () => _goTo(1),
|
onSearchTap: () => _goTo(2),
|
||||||
onFavoritesTap: () => _goTo(2),
|
onFavoritesTap: () => _goTo(3),
|
||||||
),
|
),
|
||||||
|
const DownloadsScreen(),
|
||||||
const SearchScreen(),
|
const SearchScreen(),
|
||||||
const FavoritesScreen(),
|
const FavoritesScreen(),
|
||||||
const DownloadsScreen(),
|
|
||||||
];
|
];
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
body: Column(
|
body: Column(
|
||||||
@@ -127,11 +127,11 @@ class _HomeShellState extends State<HomeShell> {
|
|||||||
items: const [
|
items: const [
|
||||||
BottomNavigationBarItem(
|
BottomNavigationBarItem(
|
||||||
icon: Icon(Icons.headphones), label: 'Meine Musik'),
|
icon: Icon(Icons.headphones), label: 'Meine Musik'),
|
||||||
BottomNavigationBarItem(icon: Icon(Icons.search), label: 'Suche'),
|
BottomNavigationBarItem(
|
||||||
|
icon: Icon(Icons.music_note_outlined), label: 'Online'),
|
||||||
|
BottomNavigationBarItem(icon: Icon(Icons.search), label: 'Suchen'),
|
||||||
BottomNavigationBarItem(
|
BottomNavigationBarItem(
|
||||||
icon: Icon(Icons.favorite_border), label: 'Favoriten'),
|
icon: Icon(Icons.favorite_border), label: 'Favoriten'),
|
||||||
BottomNavigationBarItem(
|
|
||||||
icon: Icon(Icons.download_outlined), label: 'Download'),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
+8
-8
@@ -620,10 +620,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: matcher
|
name: matcher
|
||||||
sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861
|
sha256: "31bd099b47c10cd1aeb55146a2d46ce0277630ecef3f7dae54ad7873f36696cd"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.12.19"
|
version: "0.12.20"
|
||||||
material_color_utilities:
|
material_color_utilities:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -636,10 +636,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: meta
|
name: meta
|
||||||
sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349"
|
sha256: "307249ce4ff29d58a18e97f6345f539382eb9c9c29ecda628900f31de0443dd9"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.18.0"
|
version: "1.19.0"
|
||||||
mime:
|
mime:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1113,10 +1113,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: test_api
|
name: test_api
|
||||||
sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e"
|
sha256: "2a122cbe059f8b610d3a5415f42e255b6c17b1f21eee1d960f31080237fb4f11"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.7.11"
|
version: "0.7.12"
|
||||||
typed_data:
|
typed_data:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1137,10 +1137,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: vector_math
|
name: vector_math
|
||||||
sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b
|
sha256: f36f9f3be64c6198714492bb455c11056e33e2f85d9a0b676a48301e44fdcf47
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.2.0"
|
version: "2.4.2"
|
||||||
vm_service:
|
vm_service:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import 'package:drift/native.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:melo/library/category_service.dart';
|
||||||
|
import 'package:melo/library/database.dart';
|
||||||
|
import 'package:melo/library/library_service.dart';
|
||||||
|
import 'package:melo/library/playlist_service.dart';
|
||||||
|
import 'package:melo/main.dart';
|
||||||
|
import 'package:melo/player/audio_handler.dart';
|
||||||
|
import 'package:melo/services/offline_mode.dart';
|
||||||
|
import 'package:melo/settings/app_settings.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
setUp(() => SharedPreferences.setMockInitialValues({}));
|
||||||
|
|
||||||
|
testWidgets('Untere Navigation folgt der Referenz-UI: '
|
||||||
|
'Meine Musik, Online, Suchen, Favoriten', (tester) async {
|
||||||
|
final db = MeloDb(NativeDatabase.memory());
|
||||||
|
final lib = LibraryService(db);
|
||||||
|
final playlists = PlaylistService(db);
|
||||||
|
final handler = MeloAudioHandler(db: db);
|
||||||
|
|
||||||
|
await tester.pumpWidget(MultiProvider(
|
||||||
|
providers: [
|
||||||
|
Provider<MeloDb>.value(value: db),
|
||||||
|
ChangeNotifierProvider<LibraryService>.value(value: lib),
|
||||||
|
ChangeNotifierProvider<PlaylistService>.value(value: playlists),
|
||||||
|
Provider<MeloAudioHandler>.value(value: handler),
|
||||||
|
ChangeNotifierProvider<CategoryService>(create: (_) => CategoryService(db)),
|
||||||
|
ChangeNotifierProvider<AppSettings>(create: (_) => AppSettings()),
|
||||||
|
ChangeNotifierProvider<OfflineMode>(create: (_) => OfflineMode()),
|
||||||
|
],
|
||||||
|
child: const MaterialApp(home: HomeShell()),
|
||||||
|
));
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
final bar = tester.widget<BottomNavigationBar>(
|
||||||
|
find.byType(BottomNavigationBar));
|
||||||
|
expect(bar.items.map((i) => i.label).toList(),
|
||||||
|
['Meine Musik', 'Online', 'Suchen', 'Favoriten']);
|
||||||
|
|
||||||
|
handler.dispose();
|
||||||
|
await db.close();
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
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/category_service.dart';
|
||||||
|
import 'package:melo/library/database.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:melo/settings/app_settings.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),
|
||||||
|
ChangeNotifierProvider<CategoryService>(create: (_) => CategoryService(db)),
|
||||||
|
ChangeNotifierProvider<AppSettings>(create: (_) => AppSettings()),
|
||||||
|
],
|
||||||
|
child: MaterialApp(home: Scaffold(body: child)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _insert(
|
||||||
|
MeloDb db, {
|
||||||
|
required String id,
|
||||||
|
required String title,
|
||||||
|
String? artist,
|
||||||
|
String? album,
|
||||||
|
}) {
|
||||||
|
return db.into(db.songs).insert(SongsCompanion.insert(
|
||||||
|
id: id,
|
||||||
|
path: '/$id.mp3',
|
||||||
|
title: title,
|
||||||
|
artist: Value(artist),
|
||||||
|
album: Value(album),
|
||||||
|
dateAddedMs: 0,
|
||||||
|
updatedAtMs: 0,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
setUp(() => SharedPreferences.setMockInitialValues({}));
|
||||||
|
|
||||||
|
testWidgets('Meine Musik hat die Unterreiter Songs, Künstler und Alben',
|
||||||
|
(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', artist: 'Neoni', album: 'Roses');
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
_wrap(db, lib, playlists, handler, const MyMusicScreen()));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.text('Songs'), findsOneWidget);
|
||||||
|
expect(find.text('Künstler'), findsOneWidget);
|
||||||
|
expect(find.text('Alben'), findsOneWidget);
|
||||||
|
|
||||||
|
handler.dispose();
|
||||||
|
await db.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('Unterreiter Künstler zeigt die Künstlerliste statt der Songs',
|
||||||
|
(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', artist: 'Neoni', album: 'Roses');
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
_wrap(db, lib, playlists, handler, const MyMusicScreen()));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
await tester.tap(find.text('Künstler'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.text('Neoni'), findsOneWidget);
|
||||||
|
// Unterreiter wechseln den Inhalt, sie öffnen keinen neuen Bildschirm —
|
||||||
|
// Suchfeld und Reiter bleiben sichtbar.
|
||||||
|
expect(find.text('Titel, Künstler und Alben suchen'), findsOneWidget);
|
||||||
|
expect(find.text('Songs'), findsOneWidget);
|
||||||
|
expect(find.text('Shuffle-Wiedergabe'), findsNothing);
|
||||||
|
|
||||||
|
handler.dispose();
|
||||||
|
await db.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('Unterreiter Alben zeigt die Albenliste', (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', artist: 'Neoni', album: 'Roses');
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
_wrap(db, lib, playlists, handler, const MyMusicScreen()));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
await tester.tap(find.text('Alben'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.text('Roses'), findsOneWidget);
|
||||||
|
expect(find.text('Titel, Künstler und Alben suchen'), findsOneWidget);
|
||||||
|
expect(find.text('Songs'), findsOneWidget);
|
||||||
|
|
||||||
|
handler.dispose();
|
||||||
|
await db.close();
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user