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:
+233
-92
@@ -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 {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user