v2.56 — UI/Design-Rework: Melo Dark Fusion (neue Startseite, 2-stufige Einstellungen, Design-System)

- Design-System: MeloDesignTokens ThemeExtension, Outfit-Font, Farben #0B0B10/#14141C/#1C1C26
- Akzentfarbe-User-Einstellung behalten (MeloTheme.akzentNotifier), Gradient aus Akzent
- Startseite: Hero-Weiterhören-Karte mit Cover, Titel, Glow-Play-Button
- 2-stufige Einstellungen: Standard + Expertenmodus (SharedPreferences Key 'experten_modus')
- Komponenten: Pill-Buttons (radius 999), Card-Radius 20, Glow-Shadow, PressScale-Widget
- Login: Gradient-Button mit Akzent-Glow
- 199 Tests grün (194 bestehend + 5 neu): ThemeExtension, Expertenmodus, Akzent, PressScale, Hero
- flutter analyze lib/ + test/ = 0 Issues
- KEIN APK
This commit is contained in:
Dustin
2026-08-06 19:56:02 +02:00
parent 85615d253c
commit 79b15a3cb8
14 changed files with 1078 additions and 364 deletions
+233 -92
View File
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'dart:async';
import 'dart:io';
import '../viewmodels/melo_home_viewmodel.dart';
import '../models/song.dart';
import '../models/playlist.dart';
@@ -9,6 +10,7 @@ import '../widgets/melo_header.dart';
import '../widgets/statistik_card.dart';
import '../widgets/tag_leiste.dart';
import '../widgets/song_tile.dart';
import '../widgets/press_scale.dart';
import '../widgets/recent_widget.dart';
import 'download_screen.dart';
import 'cloud_screen.dart';
@@ -78,13 +80,11 @@ class _MeloHomeState extends State<MeloHome> with WidgetsBindingObserver {
super.didChangeAppLifecycleState(state);
switch (state) {
case AppLifecycleState.resumed:
// App in den Vordergrund → Realtime fortsetzen
_realtimeSync.fortsetzen();
case AppLifecycleState.paused:
case AppLifecycleState.detached:
case AppLifecycleState.inactive:
case AppLifecycleState.hidden:
// App im Hintergrund → Realtime pausieren (Batterie sparen)
_realtimeSync.pausiere();
}
}
@@ -175,7 +175,6 @@ class _MeloHomeState extends State<MeloHome> with WidgetsBindingObserver {
final suchModus = teile.length > 1 ? teile[1] : 'Alle';
if (suchtext.isEmpty) return;
// Tag-Namen die zum Suchtext passen
final matchingTags = _vm.tags
.where((t) => t['name'] != 'Alle' && t['name']!.toLowerCase().contains(suchtext))
.map((t) => t['name']!)
@@ -190,7 +189,6 @@ class _MeloHomeState extends State<MeloHome> with WidgetsBindingObserver {
return tag != null && s.tagIds!.contains(tag.id);
});
}
// Alle
if (s.titel.toLowerCase().contains(suchtext)) return true;
if (s.kuenstler.toLowerCase().contains(suchtext)) return true;
return s.tagIds != null && matchingTags.any((name) {
@@ -252,14 +250,11 @@ class _MeloHomeState extends State<MeloHome> with WidgetsBindingObserver {
Future<void> _zeigeServerBrowser() async {
if (_vm.navidrome.istVerbunden) {
// Trennen: auch gespeicherte Zugangsdaten löschen (SharedPreferences +
// SecureStorage) — sonst ist der Chip nach App-Neustart wieder grün.
await _vm.navidrome.loescheZugangsdaten();
_vm.navidromeAlben.clear();
if (mounted) setState(() {});
return;
}
// Verbinden: einfacher Login-Dialog
final urlCtrl = TextEditingController(text: AppConfig.navidromeUrl);
final userCtrl = TextEditingController();
final passCtrl = TextEditingController();
@@ -305,8 +300,6 @@ class _MeloHomeState extends State<MeloHome> with WidgetsBindingObserver {
});
}
/// Detail-Dialog zum ☁️ Musikserver-Status (Feature: Umbenennung von
/// „Navidrome verbinden“ → „Musikserver“).
Future<void> _zeigeMusikserverDetail() async {
final verbunden = _vm.navidrome.istVerbunden;
await showDialog<void>(
@@ -349,7 +342,7 @@ class _MeloHomeState extends State<MeloHome> with WidgetsBindingObserver {
Text(
verbunden
? 'Verbunden mit Navidrome · musik.baka-net.de'
: 'Nicht mit dem Musikserver verbunden.\nTippe auf „Verbinden, um Alben zu durchstöbern.',
: 'Nicht mit dem Musikserver verbunden.\nTippe auf „Verbinden", um Alben zu durchstöbern.',
style: const TextStyle(color: MeloTheme.textSekundaer, fontSize: 13),
),
],
@@ -360,7 +353,7 @@ class _MeloHomeState extends State<MeloHome> with WidgetsBindingObserver {
TextButton(
onPressed: () {
Navigator.pop(ctx);
_zeigeServerBrowser(); // trennt (Credentials leeren)
_zeigeServerBrowser();
},
child: const Text('Trennen', style: TextStyle(color: MeloTheme.textSekundaer)),
),
@@ -421,7 +414,6 @@ class _MeloHomeState extends State<MeloHome> with WidgetsBindingObserver {
}
void _zeigeDownloadDialog() {
// „Lied+“ (YT-Download) als eigenen Screen öffnen
Navigator.push(
context,
MaterialPageRoute(
@@ -433,7 +425,6 @@ class _MeloHomeState extends State<MeloHome> with WidgetsBindingObserver {
);
}
/// Korrupten Song mit bekannter ytUrl neu herunterladen
Future<void> _erneutHerunterladen(Song song) async {
final neu = await _vm.downloader.reDownload(song);
if (!mounted) return;
@@ -448,8 +439,6 @@ class _MeloHomeState extends State<MeloHome> with WidgetsBindingObserver {
@override
Widget build(BuildContext context) {
return ListenableBuilder(
// _vm (Songs/Status), _cloud (Verbindung) UND SyncService.laeuftNotifier
// (☁️-Chip pulsiert bei laufendem Sync, auch im Hintergrund)
listenable: Listenable.merge([_vm, _cloud, SyncService.laeuftNotifier]),
builder: (_, _) {
if (_vm.ladt) {
@@ -489,8 +478,6 @@ class _MeloHomeState extends State<MeloHome> with WidgetsBindingObserver {
onSearch: _zeigeSuche,
onServer: _zeigeMusikserverDetail,
serverVerbunden: _vm.navidrome.istVerbunden,
// Chip pulsiert während des Syncs (SyncService-Läuft-Signal),
// beim Verbinden und solange der Server lädt.
serverSyncLaeuft: SyncService.laeuftNotifier.value ||
_cloud.status == CloudStatus.verbinde ||
_vm.serverLadt,
@@ -499,6 +486,8 @@ class _MeloHomeState extends State<MeloHome> with WidgetsBindingObserver {
child: ListView(
padding: const EdgeInsets.only(bottom: 8),
children: [
// ═══ Hero „Weiterhören"-Karte ═══
_weiterhoerenHeroKarte(),
// Weiterhören / Zuletzt gehört
RecentWidget(songs: _vm.letzteSongs, onPlay: _vm.spieleSong),
// Jahres-Recap (wie Spotify Wrapped)
@@ -526,6 +515,153 @@ class _MeloHomeState extends State<MeloHome> with WidgetsBindingObserver {
);
}
/// Hero „Weiterhören" zeigt den aktuellen/last Song als große Card mit Glow
Widget _weiterhoerenHeroKarte() {
final aktuellerSong = PlayerService().aktuellerSong;
// Fallback: letzten gespielten Song verwenden
final song = aktuellerSong ?? (_vm.letzteSongs.isNotEmpty ? _vm.letzteSongs.first : null);
if (song == null) return const SizedBox.shrink();
final hatCover = song.coverPfad != null && File(song.coverPfad!).existsSync();
final akzent = MeloTheme.akzent;
return Padding(
padding: const EdgeInsets.fromLTRB(20, 8, 20, 12),
child: GestureDetector(
onTap: () => _oeffneNowPlaying(),
child: AnimatedScale(
scale: 1.0,
duration: const Duration(milliseconds: 120),
curve: Curves.easeOut,
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
gradient: LinearGradient(
colors: [
akzent.withValues(alpha: 0.3),
MeloTheme.schwarz,
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
border: Border.all(
color: akzent.withValues(alpha: 0.35),
),
boxShadow: [
BoxShadow(
color: akzent.withValues(alpha: 0.25),
blurRadius: 30,
offset: const Offset(0, 8),
),
],
),
child: Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
// Cover
ClipRRect(
borderRadius: BorderRadius.circular(14),
child: Container(
width: 64,
height: 64,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
akzent,
Color.lerp(akzent, Colors.black, 0.5)!,
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
),
child: hatCover
? Image.file(File(song.coverPfad!), fit: BoxFit.cover)
: const Center(
child: Text('', style: TextStyle(fontSize: 28, color: Colors.white)),
),
),
),
const SizedBox(width: 16),
// Infos
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Weiterhören',
key: Key('weiterhoeren_label'),
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: MeloTheme.textSekundaer,
letterSpacing: 0.5,
),
),
const SizedBox(height: 4),
Text(
song.titel,
key: const Key('weiterhoeren_titel'),
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontSize: 17,
fontWeight: FontWeight.w600,
color: Colors.white,
letterSpacing: -0.5,
),
),
const SizedBox(height: 2),
Text(
song.kuenstler,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontSize: 13,
color: MeloTheme.textSekundaer,
),
),
],
),
),
// Play-Button mit Glow
_playButton(akzent),
],
),
),
),
),
),
);
}
/// Press-Scale Play-Button mit Glow (Mikro-Interaktion)
Widget _playButton(Color akzent) {
return PressScale(
onTap: () => _oeffneNowPlaying(),
child: Container(
width: 48,
height: 48,
decoration: BoxDecoration(
shape: BoxShape.circle,
gradient: LinearGradient(
colors: [akzent, Color.lerp(akzent, Colors.black, 0.4)!],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
boxShadow: [
BoxShadow(
color: akzent.withValues(alpha: 0.35),
blurRadius: 20,
offset: const Offset(0, 4),
),
],
),
child: const Icon(Icons.play_arrow_rounded, color: Colors.white, size: 28),
),
);
}
/// ⭐ Favoriten-Schnellzugriff auf dem Start-Tab (horizontale Chips)
Widget _favoritenSchnellzugriff() {
final favoriten = _vm.songs
@@ -578,7 +714,7 @@ class _MeloHomeState extends State<MeloHome> with WidgetsBindingObserver {
padding: const EdgeInsets.symmetric(horizontal: 10),
decoration: BoxDecoration(
color: MeloTheme.dunkel1,
borderRadius: BorderRadius.circular(10),
borderRadius: BorderRadius.circular(20),
border: Border.all(color: MeloTheme.dunkel2),
),
child: Row(
@@ -587,8 +723,13 @@ class _MeloHomeState extends State<MeloHome> with WidgetsBindingObserver {
Container(
width: 28, height: 28,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(6),
color: MeloTheme.rot.withValues(alpha: 0.3),
borderRadius: BorderRadius.circular(8),
gradient: LinearGradient(
colors: [
MeloTheme.akzent.withValues(alpha: 0.4),
MeloTheme.akzent.withValues(alpha: 0.1),
],
),
),
child: const Center(child: Text('', style: TextStyle(fontSize: 12))),
),
@@ -648,7 +789,6 @@ class _MeloHomeState extends State<MeloHome> with WidgetsBindingObserver {
],
),
Row(children: [
// „Lied+“ (YT-Download) in die Bibliothek integriert
_bibButton(Icons.add_circle_outline, 'Lied +', _zeigeDownloadDialog),
const SizedBox(width: 8),
_bibButton(Icons.search, 'Suche', _zeigeSuche),
@@ -666,13 +806,13 @@ class _MeloHomeState extends State<MeloHome> with WidgetsBindingObserver {
padding: const EdgeInsets.symmetric(horizontal: 12),
decoration: BoxDecoration(
color: MeloTheme.dunkel1,
borderRadius: BorderRadius.circular(12),
borderRadius: BorderRadius.circular(20),
border: Border.all(color: MeloTheme.dunkel2),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon, size: 16, color: MeloTheme.rot),
Icon(icon, size: 16, color: MeloTheme.akzent),
const SizedBox(width: 6),
Text(label, style: const TextStyle(fontSize: 12, color: Colors.white70, fontWeight: FontWeight.w500)),
],
@@ -696,8 +836,8 @@ class _MeloHomeState extends State<MeloHome> with WidgetsBindingObserver {
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 7),
decoration: BoxDecoration(
color: aktiv ? MeloTheme.rot : MeloTheme.dunkel1,
borderRadius: BorderRadius.circular(16),
color: aktiv ? MeloTheme.akzent : MeloTheme.dunkel1,
borderRadius: BorderRadius.circular(999),
),
child: Text(
seg,
@@ -752,7 +892,6 @@ class _MeloHomeState extends State<MeloHome> with WidgetsBindingObserver {
}
}
/// Gruppiert Songs nach einem Schlüssel (Album, Künstler, Jahr, Genre)
Map<String, List<Song>> _gruppiereNach(String Function(Song) schluessel) {
final gruppen = <String, List<Song>>{};
for (final s in _vm.songs) {
@@ -763,7 +902,6 @@ class _MeloHomeState extends State<MeloHome> with WidgetsBindingObserver {
return gruppen;
}
/// Expandierbare Gruppen-Liste (Alben / Künstler / Jahre / Genres)
Widget _gruppenListe(Map<String, List<Song>> gruppen) {
final keys = gruppen.keys.toList()..sort((a, b) => a.toLowerCase().compareTo(b.toLowerCase()));
if (keys.isEmpty) {
@@ -779,14 +917,14 @@ class _MeloHomeState extends State<MeloHome> with WidgetsBindingObserver {
margin: const EdgeInsets.only(bottom: 6),
decoration: BoxDecoration(
color: MeloTheme.dunkel1,
borderRadius: BorderRadius.circular(12),
borderRadius: BorderRadius.circular(20),
border: Border.all(color: MeloTheme.dunkel2),
),
child: Theme(
data: Theme.of(context).copyWith(dividerColor: Colors.transparent),
child: ExpansionTile(
tilePadding: const EdgeInsets.symmetric(horizontal: 14),
leading: const Icon(Icons.folder_outlined, color: MeloTheme.rot, size: 18),
leading: Icon(Icons.folder_outlined, color: MeloTheme.akzent, size: 18),
title: Text(
key,
style: const TextStyle(color: Colors.white, fontSize: 14, fontWeight: FontWeight.w500),
@@ -824,7 +962,6 @@ class _MeloHomeState extends State<MeloHome> with WidgetsBindingObserver {
);
}
/// 🏷 Tags-Segment: Tag-Liste mit Zählern; Tipp filtert die Songliste
Widget _tagsSegment() {
final eintraege = _vm.tagCounts.entries.toList()
..sort((a, b) => b.value.compareTo(a.value));
@@ -840,9 +977,9 @@ class _MeloHomeState extends State<MeloHome> with WidgetsBindingObserver {
padding: const EdgeInsets.only(bottom: 6),
child: Material(
color: MeloTheme.dunkel1,
borderRadius: BorderRadius.circular(12),
borderRadius: BorderRadius.circular(20),
child: InkWell(
borderRadius: BorderRadius.circular(12),
borderRadius: BorderRadius.circular(20),
onTap: () {
_vm.aktiveTags = {e.key};
setState(() => _bibSegment = 'Songs');
@@ -850,12 +987,12 @@ class _MeloHomeState extends State<MeloHome> with WidgetsBindingObserver {
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 13),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
borderRadius: BorderRadius.circular(20),
border: Border.all(color: MeloTheme.dunkel2),
),
child: Row(
children: [
const Icon(Icons.label_outline, color: MeloTheme.rot, size: 18),
Icon(Icons.label_outline, color: MeloTheme.akzent, size: 18),
const SizedBox(width: 12),
Expanded(
child: Text(
@@ -879,7 +1016,6 @@ class _MeloHomeState extends State<MeloHome> with WidgetsBindingObserver {
);
}
/// 📋 Playlists-Segment: Playlist-Liste, Tipp zeigt die Songs
Widget _playlistsSegment() {
return FutureBuilder<List<Playlist>>(
future: _vm.playlists.allePlaylists(),
@@ -903,19 +1039,19 @@ class _MeloHomeState extends State<MeloHome> with WidgetsBindingObserver {
padding: const EdgeInsets.only(bottom: 6),
child: Material(
color: MeloTheme.dunkel1,
borderRadius: BorderRadius.circular(12),
borderRadius: BorderRadius.circular(20),
child: InkWell(
borderRadius: BorderRadius.circular(12),
borderRadius: BorderRadius.circular(20),
onTap: () => _zeigePlaylistSongs(pl),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 13),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
borderRadius: BorderRadius.circular(20),
border: Border.all(color: MeloTheme.dunkel2),
),
child: Row(
children: [
const Icon(Icons.queue_music, color: MeloTheme.rot, size: 18),
Icon(Icons.queue_music, color: MeloTheme.akzent, size: 18),
const SizedBox(width: 12),
Expanded(
child: Text(
@@ -956,7 +1092,7 @@ class _MeloHomeState extends State<MeloHome> with WidgetsBindingObserver {
: ListView.builder(
itemCount: songs.length,
itemBuilder: (_, i) => ListTile(
leading: const Icon(Icons.music_note, color: MeloTheme.rot, size: 18),
leading: Icon(Icons.music_note, color: MeloTheme.akzent, size: 18),
title: Text(songs[i].titel, style: const TextStyle(color: Colors.white, fontSize: 14)),
subtitle: Text(songs[i].kuenstler, style: const TextStyle(color: MeloTheme.textSekundaer, fontSize: 11)),
onTap: () {
@@ -971,7 +1107,6 @@ class _MeloHomeState extends State<MeloHome> with WidgetsBindingObserver {
);
}
/// ⬇ Downloads-Segment: lokale Songs + „Lied+“-Einstieg
Widget _downloadsSegment() {
final downloads = _vm.songs.where((s) => s.istHeruntergeladen).toList();
return Column(
@@ -984,15 +1119,20 @@ class _MeloHomeState extends State<MeloHome> with WidgetsBindingObserver {
width: double.infinity,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
decoration: BoxDecoration(
gradient: const LinearGradient(colors: [Color(0xFF3A0000), Color(0xFF1A0000)]),
borderRadius: BorderRadius.circular(14),
border: Border.all(color: MeloTheme.rot.withValues(alpha: 0.45)),
gradient: LinearGradient(
colors: [
MeloTheme.akzent.withValues(alpha: 0.25),
MeloTheme.akzent.withValues(alpha: 0.05),
],
),
borderRadius: BorderRadius.circular(20),
border: Border.all(color: MeloTheme.akzent.withValues(alpha: 0.4)),
),
child: const Row(
child: Row(
children: [
Icon(Icons.add_circle_outline, color: MeloTheme.rot, size: 22),
SizedBox(width: 12),
Expanded(
Icon(Icons.add_circle_outline, color: MeloTheme.akzent, size: 22),
const SizedBox(width: 12),
const Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@@ -1003,7 +1143,7 @@ class _MeloHomeState extends State<MeloHome> with WidgetsBindingObserver {
],
),
),
Icon(Icons.chevron_right, color: MeloTheme.rot, size: 20),
Icon(Icons.chevron_right, color: MeloTheme.akzent, size: 20),
],
),
),
@@ -1023,7 +1163,7 @@ class _MeloHomeState extends State<MeloHome> with WidgetsBindingObserver {
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.play_circle_outline, size: 64, color: MeloTheme.dunkel2),
Icon(Icons.play_circle_outline, size: 64, color: MeloTheme.dunkel2.withValues(alpha: 0.5)),
const SizedBox(height: 16),
const Text(
'Nichts spielt gerade',
@@ -1036,13 +1176,8 @@ class _MeloHomeState extends State<MeloHome> with WidgetsBindingObserver {
style: TextStyle(fontSize: 13, color: MeloTheme.textSekundaer),
),
const SizedBox(height: 20),
ElevatedButton(
FilledButton(
onPressed: () => setState(() => _aktiverTab = 1),
style: ElevatedButton.styleFrom(
backgroundColor: MeloTheme.rot,
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
),
child: const Text('Zur Bibliothek'),
),
],
@@ -1093,14 +1228,14 @@ class _MeloHomeState extends State<MeloHome> with WidgetsBindingObserver {
Container(
decoration: BoxDecoration(
color: MeloTheme.dunkel1,
borderRadius: BorderRadius.circular(14),
borderRadius: BorderRadius.circular(20),
border: Border.all(color: MeloTheme.dunkel2),
),
child: Column(
children: [
_infoZeile('Version', '2.52.3'),
_infoZeile('Version', '2.56.0'),
const Divider(color: MeloTheme.dunkel2, height: 1),
_infoZeile('Theme', 'Schwarz + Rot'),
_infoZeile('Theme', 'Melo Dark Fusion'),
const Divider(color: MeloTheme.dunkel2, height: 1),
_infoZeile('Musikserver', AppConfig.navidromeUrl),
const Divider(color: MeloTheme.dunkel2, height: 1),
@@ -1124,9 +1259,9 @@ class _MeloHomeState extends State<MeloHome> with WidgetsBindingObserver {
icon: const Icon(Icons.logout, size: 18),
label: const Text('Abmelden', style: TextStyle(fontSize: 14)),
style: OutlinedButton.styleFrom(
foregroundColor: MeloTheme.rot,
side: const BorderSide(color: MeloTheme.rot),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)),
foregroundColor: MeloTheme.akzent,
side: BorderSide(color: MeloTheme.akzent),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(999)),
),
),
),
@@ -1150,14 +1285,14 @@ class _MeloHomeState extends State<MeloHome> with WidgetsBindingObserver {
padding: const EdgeInsets.only(bottom: 8),
child: Material(
color: MeloTheme.dunkel1,
borderRadius: BorderRadius.circular(14),
borderRadius: BorderRadius.circular(20),
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(14),
borderRadius: BorderRadius.circular(20),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(14),
borderRadius: BorderRadius.circular(20),
border: Border.all(color: MeloTheme.dunkel2),
),
child: Row(
@@ -1167,9 +1302,9 @@ class _MeloHomeState extends State<MeloHome> with WidgetsBindingObserver {
height: 40,
decoration: BoxDecoration(
color: MeloTheme.dunkel2,
borderRadius: BorderRadius.circular(10),
borderRadius: BorderRadius.circular(14),
),
child: Icon(icon, color: MeloTheme.rot, size: 20),
child: Icon(icon, color: MeloTheme.akzent, size: 20),
),
const SizedBox(width: 14),
Expanded(
@@ -1266,16 +1401,15 @@ class _MeloHomeState extends State<MeloHome> with WidgetsBindingObserver {
Widget _bottomNav() {
return Container(
decoration: const BoxDecoration(
border: Border(top: BorderSide(color: MeloTheme.dunkel1)),
decoration: BoxDecoration(
border: Border(top: BorderSide(color: MeloTheme.oberflaeche, width: 0)),
),
child: NavigationBar(
backgroundColor: MeloTheme.schwarz,
indicatorColor: MeloTheme.rot.withValues(alpha: 0.25),
indicatorColor: MeloTheme.akzent.withValues(alpha: 0.25),
selectedIndex: _aktiverTab,
onDestinationSelected: (i) {
if (i == 2) {
// ▶️ Jetzt läuft: Fullscreen öffnen, wenn etwas spielt
if (PlayerService().aktuellerSong != null) {
_oeffneNowPlaying();
return;
@@ -1309,7 +1443,7 @@ class _MeloHomeState extends State<MeloHome> with WidgetsBindingObserver {
children: [
Text(titel, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: Colors.white)),
Row(children: [
Text('${songs.length} Titel${_vm.aktiveTags.isNotEmpty ? ' (gefiltert)' : ''}', style: TextStyle(fontSize: 12, color: MeloTheme.rot)),
Text('${songs.length} Titel${_vm.aktiveTags.isNotEmpty ? ' (gefiltert)' : ''}', style: TextStyle(fontSize: 12, color: MeloTheme.akzent)),
const SizedBox(width: 8),
GestureDetector(
onTap: _scanMusik,
@@ -1319,12 +1453,12 @@ class _MeloHomeState extends State<MeloHome> with WidgetsBindingObserver {
border: Border.all(color: MeloTheme.dunkel2),
borderRadius: BorderRadius.circular(8),
),
child: const Row(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.refresh, size: 12, color: MeloTheme.rot),
SizedBox(width: 4),
Text('Scannen', style: TextStyle(fontSize: 11, color: MeloTheme.rot)),
Icon(Icons.refresh, size: 12, color: MeloTheme.akzent),
const SizedBox(width: 4),
Text('Scannen', style: TextStyle(fontSize: 11, color: MeloTheme.akzent)),
],
),
),
@@ -1364,22 +1498,20 @@ class _MeloHomeState extends State<MeloHome> with WidgetsBindingObserver {
);
}
/// Hilfs-Widget für Such-Modus-Chips
Widget _suchChip(String label, bool aktiv, VoidCallback onTap) {
return GestureDetector(
onTap: onTap,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 5),
decoration: BoxDecoration(
color: aktiv ? MeloTheme.rot : MeloTheme.dunkel2,
borderRadius: BorderRadius.circular(12),
color: aktiv ? MeloTheme.akzent : MeloTheme.dunkel2,
borderRadius: BorderRadius.circular(999),
),
child: Text(label, style: TextStyle(fontSize: 11, color: aktiv ? Colors.white : MeloTheme.textSekundaer)),
),
);
}
/// Karte „Jahres-Recap“ öffnet den Wrapped-artigen Recap-Screen
Widget _recapKarte() {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8),
@@ -1393,13 +1525,17 @@ class _MeloHomeState extends State<MeloHome> with WidgetsBindingObserver {
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [Color(0xFF3A0000), Color(0xFF1A0000), Color(0xFF0D0D0D)],
gradient: LinearGradient(
colors: [
MeloTheme.akzent.withValues(alpha: 0.2),
MeloTheme.akzent.withValues(alpha: 0.05),
MeloTheme.schwarz,
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(14),
border: Border.all(color: MeloTheme.rot.withValues(alpha: 0.45)),
borderRadius: BorderRadius.circular(20),
border: Border.all(color: MeloTheme.akzent.withValues(alpha: 0.35)),
),
child: Row(
children: [
@@ -1407,8 +1543,8 @@ class _MeloHomeState extends State<MeloHome> with WidgetsBindingObserver {
width: 42,
height: 42,
decoration: BoxDecoration(
color: MeloTheme.rot.withValues(alpha: 0.18),
borderRadius: BorderRadius.circular(12),
color: MeloTheme.akzent.withValues(alpha: 0.18),
borderRadius: BorderRadius.circular(14),
),
child: const Center(
child: Text('🎧', style: TextStyle(fontSize: 20)),
@@ -1437,7 +1573,7 @@ class _MeloHomeState extends State<MeloHome> with WidgetsBindingObserver {
],
),
),
const Icon(Icons.chevron_right, color: MeloTheme.rot, size: 22),
Icon(Icons.chevron_right, color: MeloTheme.akzent, size: 22),
],
),
),
@@ -1445,16 +1581,20 @@ class _MeloHomeState extends State<MeloHome> with WidgetsBindingObserver {
);
}
/// Banner "💌 Seit 2008" erscheint nach 10 Playbacks
Widget _botschaftBanner() {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
decoration: BoxDecoration(
gradient: const LinearGradient(colors: [Color(0xFF2A0000), Color(0xFF1A0000)]),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: MeloTheme.rot.withValues(alpha: 0.4)),
gradient: LinearGradient(
colors: [
MeloTheme.akzent.withValues(alpha: 0.2),
MeloTheme.akzent.withValues(alpha: 0.05),
],
),
borderRadius: BorderRadius.circular(20),
border: Border.all(color: MeloTheme.akzent.withValues(alpha: 0.35)),
),
child: Row(
children: [
@@ -1481,3 +1621,4 @@ class _MeloHomeState extends State<MeloHome> with WidgetsBindingObserver {
);
}
}