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 {
);
}
}
+60 -40
View File
@@ -6,8 +6,7 @@ import '../utils/user_effekte.dart';
import '../config/app_config.dart';
import 'home_screen.dart';
/// Melo Login-Screen Schwarz+Rot Design
/// Baka-Auth + Navidrome-Credentials
/// Melo Login-Screen Melo Dark Fusion Design
class LoginScreen extends StatefulWidget {
const LoginScreen({super.key});
@@ -74,9 +73,7 @@ class _LoginScreenState extends State<LoginScreen>
if (result.erfolg) {
_setzeStatus('✅ Login erfolgreich!', ok: true);
// Per-User-Akzent (überschreibbar in den Einstellungen) + Begrüßung
await UserEffekt.anwenden(user);
// Navidrome automatisch mit gleichen Credentials syncen
await NavidromeService().speichereZugangsdaten(
AppConfig.navidromeUrl, user, pass);
await Future.delayed(const Duration(milliseconds: 600));
@@ -110,6 +107,8 @@ class _LoginScreenState extends State<LoginScreen>
}
Widget _buildLogin() {
final akzent = MeloTheme.akzent;
return Column(
mainAxisSize: MainAxisSize.min,
children: [
@@ -123,11 +122,11 @@ class _LoginScreenState extends State<LoginScreen>
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
color: MeloTheme.dunkel1,
borderRadius: BorderRadius.circular(20),
borderRadius: BorderRadius.circular(24),
border: Border.all(color: MeloTheme.dunkel2),
boxShadow: [
BoxShadow(
color: MeloTheme.rot.withValues(alpha: 0.15),
color: akzent.withValues(alpha: 0.15),
blurRadius: 30,
offset: const Offset(0, 8),
),
@@ -142,6 +141,7 @@ class _LoginScreenState extends State<LoginScreen>
color: Colors.white,
fontSize: 20,
fontWeight: FontWeight.w700,
letterSpacing: -0.5,
),
),
const SizedBox(height: 4),
@@ -172,37 +172,55 @@ class _LoginScreenState extends State<LoginScreen>
),
const SizedBox(height: 24),
// Login Button
// Login Button Gradient Pill
SizedBox(
width: double.infinity,
height: 50,
child: ElevatedButton(
onPressed: _ladt ? null : _login,
style: ElevatedButton.styleFrom(
backgroundColor: MeloTheme.rot,
disabledBackgroundColor: MeloTheme.dunkel2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(14),
child: DecoratedBox(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(999),
gradient: LinearGradient(
colors: [akzent, Color.lerp(akzent, Colors.black, 0.4)!],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
elevation: 0,
boxShadow: [
BoxShadow(
color: akzent.withValues(alpha: 0.3),
blurRadius: 16,
offset: const Offset(0, 4),
),
],
),
child: _ladt
? const SizedBox(
width: 22,
height: 22,
child: CircularProgressIndicator(
strokeWidth: 2.5,
color: Colors.white,
child: ElevatedButton(
onPressed: _ladt ? null : _login,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.transparent,
disabledBackgroundColor: Colors.transparent,
shadowColor: Colors.transparent,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(999),
),
elevation: 0,
),
child: _ladt
? const SizedBox(
width: 22,
height: 22,
child: CircularProgressIndicator(
strokeWidth: 2.5,
color: Colors.white,
),
)
: const Text(
'Anmelden',
style: TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
)
: const Text(
'Anmelden',
style: TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
),
),
),
@@ -235,6 +253,8 @@ class _LoginScreenState extends State<LoginScreen>
}
Widget _logoBereich({bool klein = false}) {
final akzent = MeloTheme.akzent;
return Column(
children: [
// Icon
@@ -243,14 +263,14 @@ class _LoginScreenState extends State<LoginScreen>
height: klein ? 64 : 80,
decoration: BoxDecoration(
shape: BoxShape.circle,
gradient: const LinearGradient(
colors: [Color(0xFFCC0000), Color(0xFF660000)],
gradient: LinearGradient(
colors: [akzent, Color.lerp(akzent, Colors.black, 0.5)!],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
boxShadow: [
BoxShadow(
color: MeloTheme.rot.withValues(alpha: 0.4),
color: akzent.withValues(alpha: 0.4),
blurRadius: 24,
offset: const Offset(0, 6),
),
@@ -277,7 +297,7 @@ class _LoginScreenState extends State<LoginScreen>
Text(
'Musik. Für immer.',
style: TextStyle(
color: MeloTheme.rot.withValues(alpha: 0.8),
color: akzent.withValues(alpha: 0.8),
fontSize: 13,
letterSpacing: 2,
fontWeight: FontWeight.w400,
@@ -295,7 +315,7 @@ class _LoginScreenState extends State<LoginScreen>
color: _statusOk
? const Color(0xFF0D3B1E)
: const Color(0xFF3B0D0D),
borderRadius: BorderRadius.circular(10),
borderRadius: BorderRadius.circular(14),
border: Border.all(
color: _statusOk ? const Color(0xFF2E7D32) : const Color(0xFF7D2E2E),
),
@@ -348,15 +368,15 @@ class _LoginScreenState extends State<LoginScreen>
filled: true,
fillColor: MeloTheme.dunkel2,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderRadius: BorderRadius.circular(14),
borderSide: BorderSide.none,
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(color: MeloTheme.rot, width: 1.5),
borderRadius: BorderRadius.circular(14),
borderSide: BorderSide(color: MeloTheme.akzent, width: 1.5),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderRadius: BorderRadius.circular(14),
borderSide: const BorderSide(color: MeloTheme.dunkel2),
),
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
+1 -1
View File
@@ -591,7 +591,7 @@ class _NowPlayingScreenState extends State<NowPlayingScreen>
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [Color(0xFF1A0A0A), MeloTheme.schwarz],
colors: [MeloTheme.dunkel1, MeloTheme.schwarz],
),
),
);
+233 -145
View File
@@ -13,10 +13,8 @@ import 'cloud_screen.dart';
import 'recap_screen.dart';
import 'erweitert_screen.dart';
/// Vollwertiger Einstellungen-Screen kein Popup mehr.
/// Sprint D F4: alle Einstellungen funktionierend + persistiert
/// (Auto-Sync-Intervall, Sleep-Timer-Standard, Wiedergabegeschwindigkeit,
/// Akzentfarbe pro User, Speicher-Info).
/// Einstellungen 2-stufig: Standard + Expertenmodus (ab v2.56).
/// Sprint D F4: alle Einstellungen funktionierend + persistiert.
class SettingsScreen extends StatefulWidget {
const SettingsScreen({super.key});
@@ -25,14 +23,19 @@ class SettingsScreen extends StatefulWidget {
}
class _SettingsScreenState extends State<SettingsScreen> {
// ─── Einstellungen (aus SharedPreferences geladen) ───
// ─── Standard-Einstellungen ───
String _syncModus = 'realtime'; // 'realtime' | 'manual' | 'interval'
int _cloudIntervallStunden = 48; // 48/72/120/168/336
int _sleepDefaultMin = 0; // 0 = Aus
int _cloudIntervallStunden = 48;
int _sleepDefaultMin = 0;
double _geschwindigkeit = 1.0;
Color _akzent = MeloTheme.akzent;
Map<String, int> _speicherInfo = const {'anzahl': 0, 'bytes': 0};
// ─── Expertenmodus ───
bool _expertenModus = false;
String? _letzterSync;
String? _verbindungsStatus;
/// Verfügbare Zeit-Intervalle für Modus 3 (in Stunden).
static const _intervallOptionen = {
48: '2 Tage',
@@ -62,13 +65,10 @@ class _SettingsScreenState extends State<SettingsScreen> {
Future<void> _ladeEinstellungen() async {
final p = await SharedPreferences.getInstance();
final speed = p.getDouble('playback_speed') ?? 1.0;
final akzentHex =
p.getString(UserEffekt.akzentKey(AuthService().benutzer));
// Sync-Modus laden (mit Migration von altem cloud_interval)
final akzentHex = p.getString(UserEffekt.akzentKey(AuthService().benutzer));
final modus = p.getString('sync_modus') ?? 'realtime';
int intervallStunden = p.getInt('cloud_interval_stunden') ?? 0;
if (intervallStunden <= 0) {
// Migration: alter cloud_interval (1/3/6/12h) → 48h default
final alt = p.getInt('cloud_interval') ?? 0;
if (alt == 1 || alt == 3 || alt == 6 || alt == 12) {
intervallStunden = 48;
@@ -77,10 +77,17 @@ class _SettingsScreenState extends State<SettingsScreen> {
intervallStunden = 48;
}
}
// Prüfe, ob der Modus gültig ist
if (!['realtime', 'manual', 'interval'].contains(modus)) {
await p.setString('sync_modus', 'realtime');
}
// Expertenmodus laden (Key: 'experten_modus')
final experte = p.getBool('experten_modus') ?? false;
// Sync-Info für erweiterte Sektion
final letzterSyncStr = p.getString('letzter_sync');
final syncStat = CloudService().status;
if (!mounted) return;
setState(() {
_syncModus = modus;
@@ -88,8 +95,10 @@ class _SettingsScreenState extends State<SettingsScreen> {
_sleepDefaultMin = p.getInt('sleep_timer_default_min') ?? 0;
_geschwindigkeit = speed;
_akzent = UserEffekt.farbeAusHex(akzentHex) ?? MeloTheme.akzent;
_expertenModus = experte;
_letzterSync = letzterSyncStr;
_verbindungsStatus = syncStat == CloudStatus.verbunden ? 'Verbunden' : 'Getrennt';
});
// Speed-Default auch auf den Player anwenden (gilt fürs Abspielen)
await PlayerService().setGeschwindigkeit(speed);
}
@@ -99,30 +108,24 @@ class _SettingsScreenState extends State<SettingsScreen> {
setState(() => _speicherInfo = info);
}
// ─── Setter (persistieren + anwenden) ───
// ─── Setter ────────────────────────────────────────
/// Setzt den Sync-Modus und persistiert ihn.
/// Startet/stoppt den Realtime-Service bzw. den Timer entsprechend.
Future<void> _syncModusSetzen(String modus) async {
setState(() => _syncModus = modus);
final p = await SharedPreferences.getInstance();
await p.setString('sync_modus', modus);
// Realtime-Service entsprechend starten/stoppen
final rs = RealtimeSyncService();
switch (modus) {
case 'realtime':
// SSE sofort starten
await rs.starteWennAktiviert();
break;
case 'manual':
// Kein Auto-Sync — SSE stoppen, Timer deaktivieren
rs.stoppe();
await p.setInt('cloud_interval', 0); // alten Timer deaktivieren
await p.setInt('cloud_interval', 0);
await SyncService.starteAutoSyncTimer();
break;
case 'interval':
// Intervall-Timer — SSE stoppen, Timer mit cloud_interval_stunden starten
rs.stoppe();
await p.setInt('cloud_interval', _cloudIntervallStunden);
await SyncService.starteAutoSyncTimer();
@@ -130,12 +133,10 @@ class _SettingsScreenState extends State<SettingsScreen> {
}
}
/// Setzt das Cloud-Intervall (nur für Modus 3: interval).
Future<void> _intervallSetzen(int stunden) async {
setState(() => _cloudIntervallStunden = stunden);
final p = await SharedPreferences.getInstance();
await p.setInt('cloud_interval_stunden', stunden);
// Timer neu starten (der cloud_interval-Wert wird aktualisiert)
await p.setInt('cloud_interval', stunden);
await SyncService.starteAutoSyncTimer();
}
@@ -155,12 +156,18 @@ class _SettingsScreenState extends State<SettingsScreen> {
Future<void> _akzentSetzen(Color farbe) async {
setState(() => _akzent = farbe);
MeloTheme.akzent = farbe; // live — main.dart rebuildet das Theme
MeloTheme.akzent = farbe;
final p = await SharedPreferences.getInstance();
await p.setString(
UserEffekt.akzentKey(AuthService().benutzer), _hexVonFarbe(farbe));
}
Future<void> _expertenModusSetzen(bool wert) async {
setState(() => _expertenModus = wert);
final p = await SharedPreferences.getInstance();
await p.setBool('experten_modus', wert);
}
String _hexVonFarbe(Color c) {
String hx(int w) => w.toRadixString(16).padLeft(2, '0').toUpperCase();
return '${hx((c.r * 255).round())}${hx((c.g * 255).round())}${hx((c.b * 255).round())}';
@@ -180,16 +187,16 @@ class _SettingsScreenState extends State<SettingsScreen> {
return Scaffold(
backgroundColor: MeloTheme.schwarz,
appBar: AppBar(
backgroundColor: MeloTheme.dunkel1,
backgroundColor: MeloTheme.oberflaeche,
leading: IconButton(
icon: const Icon(Icons.arrow_back, color: Colors.white, size: 22),
onPressed: () => Navigator.pop(context),
),
title: const Row(
title: Row(
children: [
Icon(Icons.settings, color: MeloTheme.rot, size: 20),
SizedBox(width: 10),
Text(
Icon(Icons.settings, color: MeloTheme.akzent, size: 20),
const SizedBox(width: 10),
const Text(
'Einstellungen',
style: TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.w600),
),
@@ -199,8 +206,11 @@ class _SettingsScreenState extends State<SettingsScreen> {
body: ListView(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
children: [
// ─── Sektion: Verbindung ───
_sektionHeader('Verbindung'),
// ═══ Sektion: Standard ═══
_sektionHeader('⚙️ Standard', icon: Icons.tune),
const SizedBox(height: 4),
// ─── Verbindung ───
_einstellungsKachel(
icon: Icons.cloud_outlined,
titel: 'Cloud Sync',
@@ -219,14 +229,13 @@ class _SettingsScreenState extends State<SettingsScreen> {
titel: 'Musikserver',
untertitel: 'Navidrome Musik-Server verbinden',
onTap: () {
// Signal zum Öffnen des Server-Browsers
Navigator.pop(context, 'server');
},
),
const SizedBox(height: 8),
// ─── Sektion: Cloud-Sync ───
_sektionHeader('Cloud-Sync'),
const SizedBox(height: 4),
// ─── Sync & Wiedergabe ───
_einstellungsGruppe([
_gruppenZeile(
label: 'Sync-Modus',
@@ -284,7 +293,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
),
const Divider(color: MeloTheme.dunkel2, height: 1),
_gruppenZeile(
label: 'Wiedergabegeschwindigkeit',
label: 'Geschwindigkeit',
wert: '${_geschwindigkeit.toStringAsFixed(2)}x',
inhalt: Slider(
value: _geschwindigkeit,
@@ -297,10 +306,10 @@ class _SettingsScreenState extends State<SettingsScreen> {
),
),
]),
const SizedBox(height: 8),
// ─── Sektion: Aussehen ───
_sektionHeader('Aussehen'),
const SizedBox(height: 4),
// ─── Aussehen ───
_einstellungsGruppe([
_gruppenZeile(
label: 'Akzentfarbe',
@@ -312,15 +321,10 @@ class _SettingsScreenState extends State<SettingsScreen> {
]),
),
]),
const SizedBox(height: 8),
// ─── Sektion: Daten & Privatsphäre ───
_sektionHeader('Daten & Privatsphäre'),
_infoKachel(
icon: Icons.storage_outlined,
titel: 'Lokale Musik',
wert: _speicherText,
),
const SizedBox(height: 4),
// ─── Recap ───
_einstellungsKachel(
icon: Icons.auto_graph,
titel: 'Recap (Woche/Monat/Jahr)',
@@ -332,77 +336,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
);
},
),
const SizedBox(height: 8),
// ─── Sektion: Erweitert (Technik) ───
_sektionHeader('Erweitert'),
_einstellungsKachel(
icon: Icons.handyman_outlined,
titel: 'Erweitert',
untertitel: 'Logs, Diagnose, Entwickler & Scanner',
onTap: () async {
// Erweitert-Screen öffnen. „Musik scannen“ (onScan) schließt den
// Erweitert-Screen mit dem Ergebnis 'scanner' → hier awaiten und
// das Ergebnis an MeloHome weiterreichen, das den Scanner startet.
final result = await Navigator.push<String>(
context,
MaterialPageRoute(
builder: (_) => ErweitertScreen(
onScan: () {
Navigator.pop(context, 'scanner');
},
),
),
);
if (!mounted || result != 'scanner') return;
Navigator.pop(this.context, 'scanner');
},
),
_einstellungsKachel(
icon: Icons.article_outlined,
titel: 'Logs',
untertitel: 'App-Logbuch im Speicher ansehen',
onTap: () {
Navigator.push(
context,
MaterialPageRoute(builder: (_) => const LogViewerScreen()),
);
},
),
_einstellungsKachel(
icon: Icons.bug_report_outlined,
titel: 'Entwickler',
untertitel: 'Version, URLs & technische Details',
onTap: () {
Navigator.push(
context,
MaterialPageRoute(builder: (_) => const EntwicklerScreen()),
);
},
),
const SizedBox(height: 8),
// ─── Sektion: Info ───
_sektionHeader('Info'),
Container(
decoration: BoxDecoration(
color: MeloTheme.dunkel1,
borderRadius: BorderRadius.circular(14),
border: Border.all(color: MeloTheme.dunkel2),
),
child: Column(
children: [
_infoZeile('Version', '2.52.3'),
const Divider(color: MeloTheme.dunkel2, height: 1),
_infoZeile('Theme', 'Schwarz + Akzent'),
const Divider(color: MeloTheme.dunkel2, height: 1),
_infoZeile('Auth', AppConfig.authUrl),
const Divider(color: MeloTheme.dunkel2, height: 1),
_infoZeile('Cloud', AppConfig.cloudUrl),
],
),
),
const SizedBox(height: 24),
const SizedBox(height: 4),
// ─── Abmelden ───
SizedBox(
@@ -415,29 +350,180 @@ class _SettingsScreenState extends State<SettingsScreen> {
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)),
),
),
),
const SizedBox(height: 20),
// ═══ Expertenmodus ═══
Container(
decoration: BoxDecoration(
color: MeloTheme.dunkel1,
borderRadius: BorderRadius.circular(20),
border: Border.all(color: MeloTheme.dunkel2),
),
child: SwitchListTile(
key: const Key('experten_modus'),
title: const Text(
'Expertenmodus',
style: TextStyle(color: Colors.white, fontSize: 15, fontWeight: FontWeight.w600),
),
subtitle: const Text(
'Erweiterte Einstellungen & Diagnose',
style: TextStyle(color: MeloTheme.textSekundaer, fontSize: 12),
),
value: _expertenModus,
activeTrackColor: MeloTheme.akzent,
activeThumbColor: MeloTheme.akzent,
onChanged: _expertenModusSetzen,
secondary: Icon(Icons.science_outlined, color: MeloTheme.akzent, size: 22),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
),
),
// ═══ Erweiterte Sektion (nur wenn Expertenmodus an) ═══
if (_expertenModus) ...[
const SizedBox(height: 4),
_sektionHeader('🔬 Erweitert', icon: Icons.science),
// ─── Storage-Info ───
_infoKachel(
icon: Icons.storage_outlined,
titel: 'Lokale Musik',
wert: _speicherText,
),
// ─── Sync-Details ───
Container(
margin: const EdgeInsets.only(bottom: 8),
decoration: BoxDecoration(
color: MeloTheme.dunkel1,
borderRadius: BorderRadius.circular(20),
border: Border.all(color: MeloTheme.dunkel2),
),
child: Column(
children: [
_infoZeile('Letzter Sync', _letzterSync ?? 'Nie'),
const Divider(color: MeloTheme.dunkel2, height: 1),
_infoZeile('Modus', _syncModusText),
const Divider(color: MeloTheme.dunkel2, height: 1),
_infoZeile('Verbindung', _verbindungsStatus ?? ''),
],
),
),
// ─── Server-URL-Anzeige ───
Container(
margin: const EdgeInsets.only(bottom: 8),
decoration: BoxDecoration(
color: MeloTheme.dunkel1,
borderRadius: BorderRadius.circular(20),
border: Border.all(color: MeloTheme.dunkel2),
),
child: Column(
children: [
_infoZeile('Auth-URL', AppConfig.authUrl),
const Divider(color: MeloTheme.dunkel2, height: 1),
_infoZeile('Cloud-URL', AppConfig.cloudUrl),
const Divider(color: MeloTheme.dunkel2, height: 1),
_infoZeile('Musikserver', AppConfig.navidromeUrl),
],
),
),
// ─── Logs & DB-Wartung ───
_einstellungsKachel(
icon: Icons.article_outlined,
titel: 'Logs',
untertitel: 'App-Logbuch im Speicher ansehen',
onTap: () {
Navigator.push(
context,
MaterialPageRoute(builder: (_) => const LogViewerScreen()),
);
},
),
_einstellungsKachel(
icon: Icons.handyman_outlined,
titel: 'DB-Wartung & Diagnose',
untertitel: 'Datenbank-Optimierung, Scanner & mehr',
onTap: () async {
final result = await Navigator.push<String>(
context,
MaterialPageRoute(
builder: (_) => ErweitertScreen(
onScan: () {
Navigator.pop(context, 'scanner');
},
),
),
);
if (!mounted || result != 'scanner') return;
Navigator.pop(this.context, 'scanner');
},
),
_einstellungsKachel(
icon: Icons.bug_report_outlined,
titel: 'Entwickler',
untertitel: 'Version, URLs & technische Details',
onTap: () {
Navigator.push(
context,
MaterialPageRoute(builder: (_) => const EntwicklerScreen()),
);
},
),
const SizedBox(height: 4),
// ─── Info ───
Container(
decoration: BoxDecoration(
color: MeloTheme.dunkel1,
borderRadius: BorderRadius.circular(20),
border: Border.all(color: MeloTheme.dunkel2),
),
child: Column(
children: [
_infoZeile('Version', '2.56.0'),
const Divider(color: MeloTheme.dunkel2, height: 1),
_infoZeile('Theme', 'Melo Dark Fusion'),
],
),
),
],
const SizedBox(height: 32),
],
),
);
}
Widget _sektionHeader(String titel) {
// ─── ─── Hilfs-Widgets ─── ───
Widget _sektionHeader(String titel, {IconData? icon}) {
return Padding(
padding: const EdgeInsets.fromLTRB(4, 16, 4, 8),
child: Text(
titel,
style: const TextStyle(
color: MeloTheme.textSekundaer,
fontSize: 11,
fontWeight: FontWeight.w600,
letterSpacing: 1.2,
),
child: Row(
children: [
if (icon != null) ...[
Icon(icon, size: 16, color: MeloTheme.akzent),
const SizedBox(width: 6),
],
Text(
titel,
style: const TextStyle(
color: MeloTheme.textSekundaer,
fontSize: 12,
fontWeight: FontWeight.w600,
letterSpacing: 1.0,
),
),
],
),
);
}
@@ -452,14 +538,14 @@ class _SettingsScreenState extends State<SettingsScreen> {
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(
@@ -469,9 +555,9 @@ class _SettingsScreenState extends State<SettingsScreen> {
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(
@@ -499,7 +585,6 @@ class _SettingsScreenState extends State<SettingsScreen> {
);
}
/// Informations-Kachel (nicht antippbar) — z.B. Speicher-Info.
Widget _infoKachel({
required IconData icon,
required String titel,
@@ -511,7 +596,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
decoration: BoxDecoration(
color: MeloTheme.dunkel1,
borderRadius: BorderRadius.circular(14),
borderRadius: BorderRadius.circular(20),
border: Border.all(color: MeloTheme.dunkel2),
),
child: Row(
@@ -521,9 +606,9 @@ class _SettingsScreenState extends State<SettingsScreen> {
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(
@@ -542,13 +627,12 @@ class _SettingsScreenState extends State<SettingsScreen> {
);
}
/// Gruppe aus mehreren Einstellungs-Zeilen in einer abgerundeten Box.
Widget _einstellungsGruppe(List<Widget> kinder) {
return Container(
margin: const EdgeInsets.only(bottom: 8),
decoration: BoxDecoration(
color: MeloTheme.dunkel1,
borderRadius: BorderRadius.circular(14),
borderRadius: BorderRadius.circular(20),
border: Border.all(color: MeloTheme.dunkel2),
),
child: Column(children: kinder),
@@ -595,7 +679,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 7),
decoration: BoxDecoration(
color: aktiv ? MeloTheme.akzent : MeloTheme.dunkel2,
borderRadius: BorderRadius.circular(16),
borderRadius: BorderRadius.circular(999),
),
child: Text(
label,
@@ -622,6 +706,12 @@ class _SettingsScreenState extends State<SettingsScreen> {
color: aktiv ? Colors.white : Colors.transparent,
width: 2.5,
),
boxShadow: aktiv ? [
BoxShadow(
color: farbe.withValues(alpha: 0.4),
blurRadius: 12,
),
] : null,
),
child: aktiv ? const Icon(Icons.check, size: 18, color: Colors.white) : null,
),
@@ -647,7 +737,6 @@ class _SettingsScreenState extends State<SettingsScreen> {
);
}
/// Anzeigetext für den aktuellen Sync-Modus.
String get _syncModusText {
switch (_syncModus) {
case 'realtime':
@@ -661,19 +750,18 @@ class _SettingsScreenState extends State<SettingsScreen> {
}
}
/// Radio-ähnliches Kachel-Widget für Sync-Modus-Auswahl.
Widget _modusRadioTile(String titel, String wert, String beschreibung) {
final ausgewaehlt = _syncModus == wert;
return Padding(
padding: const EdgeInsets.only(bottom: 4),
child: InkWell(
onTap: () => _syncModusSetzen(wert),
borderRadius: BorderRadius.circular(10),
borderRadius: BorderRadius.circular(14),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
decoration: BoxDecoration(
color: ausgewaehlt ? MeloTheme.akzent.withAlpha(25) : Colors.transparent,
borderRadius: BorderRadius.circular(10),
borderRadius: BorderRadius.circular(14),
border: Border.all(
color: ausgewaehlt ? MeloTheme.akzent : MeloTheme.dunkel2,
width: ausgewaehlt ? 1.5 : 1,