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:
Hermes (Server)
2026-08-20 19:00:16 +02:00
co-authored by Claude Opus 5
parent 4ebd0a8dd4
commit a625d70da8
10 changed files with 285 additions and 78 deletions
+2 -2
View File
@@ -5,7 +5,7 @@ import 'package:provider/provider.dart';
import '../player/audio_handler.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
/// und stehen danach offline zur Verfügung.
class DownloadsScreen extends StatelessWidget {
@@ -21,7 +21,7 @@ class DownloadsScreen extends StatelessWidget {
padding: EdgeInsets.fromLTRB(20, 16, 20, 4),
child: Align(
alignment: Alignment.centerLeft,
child: Text('Download',
child: Text('Online',
style: TextStyle(fontSize: 24, fontWeight: FontWeight.w700)),
),
),
-13
View File
@@ -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(),
);
}
}
-13
View File
@@ -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(),
);
}
}
+85 -36
View File
@@ -15,9 +15,9 @@ import 'permissions.dart';
import 'song_list.dart';
/// Startbildschirm: Kopfzeile mit Einstellungen/Suche/Musikerkennung,
/// Schnellzugriffe, darunter Shuffle + sortierbare Liederliste.
/// Bewusst ohne AppBar — die Kopfzeile ist Teil des Inhalts.
class MyMusicScreen extends StatelessWidget {
/// Schnellzugriffe, Unterreiter (Songs/Künstler/Alben) und darunter der
/// jeweilige Inhalt. Bewusst ohne AppBar — die Kopfzeile ist Teil des Inhalts.
class MyMusicScreen extends StatefulWidget {
const MyMusicScreen({super.key, this.onSearchTap, this.onFavoritesTap});
/// 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.
final VoidCallback? onFavoritesTap;
@override
State<MyMusicScreen> createState() => _MyMusicScreenState();
}
class _MyMusicScreenState extends State<MyMusicScreen> {
/// 0 = Songs, 1 = Künstler, 2 = Alben.
int _subTab = 0;
@override
Widget build(BuildContext context) {
final db = context.read<MeloDb>();
@@ -34,7 +42,7 @@ class MyMusicScreen extends StatelessWidget {
bottom: false,
child: Column(
children: [
_Header(onSearchTap: onSearchTap),
_Header(onSearchTap: widget.onSearchTap),
if (lib.permissionDenied)
MaterialBanner(
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(
child: StreamBuilder<List<Song>>(
stream: db.watchSongs(),
builder: (context, snapshot) {
if (snapshot.hasError) {
return Center(
child: Text('Fehler: ${snapshot.error}',
style: const TextStyle(color: Colors.white54)),
);
}
final songs = snapshot.data ?? const <Song>[];
return SortableSongList(
songs: songs,
storeKey: SortStore.meineMusik,
empty: lib.scanning ? const SizedBox.shrink() : const _Empty(),
);
},
),
child: switch (_subTab) {
1 => const ArtistListScreen(),
2 => const AlbumListScreen(),
_ => StreamBuilder<List<Song>>(
stream: db.watchSongs(),
builder: (context, snapshot) {
if (snapshot.hasError) {
return Center(
child: Text('Fehler: ${snapshot.error}',
style: const TextStyle(color: Colors.white54)),
);
}
final songs = snapshot.data ?? const <Song>[];
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, …).
class _QuickAccessRow extends StatelessWidget {
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
View File
@@ -107,12 +107,12 @@ class _HomeShellState extends State<HomeShell> {
Widget build(BuildContext context) {
final tabs = <Widget>[
MyMusicScreen(
onSearchTap: () => _goTo(1),
onFavoritesTap: () => _goTo(2),
onSearchTap: () => _goTo(2),
onFavoritesTap: () => _goTo(3),
),
const DownloadsScreen(),
const SearchScreen(),
const FavoritesScreen(),
const DownloadsScreen(),
];
return Scaffold(
body: Column(
@@ -127,11 +127,11 @@ class _HomeShellState extends State<HomeShell> {
items: const [
BottomNavigationBarItem(
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(
icon: Icon(Icons.favorite_border), label: 'Favoriten'),
BottomNavigationBarItem(
icon: Icon(Icons.download_outlined), label: 'Download'),
],
),
);