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 -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,