This repository has been archived on 2026-08-19. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
melo-app/lib/screens/settings_screen.dart

815 lines
27 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../config/app_config.dart';
import '../utils/farb_theme.dart';
import '../utils/user_effekte.dart';
import '../services/auth_service.dart';
import '../services/cloud_service.dart';
import '../services/player_service.dart';
import '../services/sync_service.dart';
import '../services/realtime_sync_service.dart';
import '../database/db_helper.dart';
import 'cloud_screen.dart';
import 'recap_screen.dart';
import 'erweitert_screen.dart';
/// Einstellungen 2-stufig: Standard + Expertenmodus (ab v2.56).
/// Sprint D F4: alle Einstellungen funktionierend + persistiert.
class SettingsScreen extends StatefulWidget {
const SettingsScreen({super.key});
@override
State<SettingsScreen> createState() => _SettingsScreenState();
}
class _SettingsScreenState extends State<SettingsScreen> {
// ─── Standard-Einstellungen ───
String _syncModus = 'realtime'; // 'realtime' | 'manual' | 'interval'
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',
72: '3 Tage',
120: '5 Tage',
168: '1 Woche',
336: '2 Wochen',
};
static const _akzentFarben = [
Color(0xFFCC0000), // Rot (Standard)
Color(0xFF00B8A9), // Türkis
Color(0xFF1E6FD9), // Blau
Color(0xFF2E9E4F), // Grün
Color(0xFF8E44AD), // Lila
Color(0xFFE67E22), // Orange
Color(0xFFD81B60), // Pink
];
@override
void initState() {
super.initState();
_ladeEinstellungen();
_ladeSpeicherInfo();
}
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));
final modus = p.getString('sync_modus') ?? 'realtime';
int intervallStunden = p.getInt('cloud_interval_stunden') ?? 0;
if (intervallStunden <= 0) {
final alt = p.getInt('cloud_interval') ?? 0;
if (alt == 1 || alt == 3 || alt == 6 || alt == 12) {
intervallStunden = 48;
await p.setInt('cloud_interval_stunden', 48);
} else {
intervallStunden = 48;
}
}
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');
if (!mounted) return;
setState(() {
_syncModus = modus;
_cloudIntervallStunden = intervallStunden;
_sleepDefaultMin = p.getInt('sleep_timer_default_min') ?? 0;
_geschwindigkeit = speed;
_akzent = UserEffekt.farbeAusHex(akzentHex) ?? MeloTheme.akzent;
_expertenModus = experte;
_letzterSync = letzterSyncStr;
_verbindungsStatus = ''; // CloudService()-Instanz hat keinen echten Status
});
await PlayerService().setGeschwindigkeit(speed);
}
Future<void> _ladeSpeicherInfo() async {
final info = await DbHelper().speicherInfo();
if (!mounted) return;
setState(() => _speicherInfo = info);
}
// ─── Setter ────────────────────────────────────────
Future<void> _syncModusSetzen(String modus) async {
setState(() => _syncModus = modus);
final p = await SharedPreferences.getInstance();
await p.setString('sync_modus', modus);
final rs = RealtimeSyncService();
switch (modus) {
case 'realtime':
await rs.starteWennAktiviert();
break;
case 'manual':
rs.stoppe();
await p.setInt('cloud_interval', 0);
await SyncService.starteAutoSyncTimer();
break;
case 'interval':
rs.stoppe();
await p.setInt('cloud_interval', _cloudIntervallStunden);
await SyncService.starteAutoSyncTimer();
break;
}
}
Future<void> _intervallSetzen(int stunden) async {
setState(() => _cloudIntervallStunden = stunden);
final p = await SharedPreferences.getInstance();
await p.setInt('cloud_interval_stunden', stunden);
await p.setInt('cloud_interval', stunden);
await SyncService.starteAutoSyncTimer();
}
Future<void> _sleepDefaultSetzen(int min) async {
setState(() => _sleepDefaultMin = min);
final p = await SharedPreferences.getInstance();
await p.setInt('sleep_timer_default_min', min);
}
Future<void> _geschwindigkeitSetzen(double wert) async {
setState(() => _geschwindigkeit = wert);
final p = await SharedPreferences.getInstance();
await p.setDouble('playback_speed', wert);
await PlayerService().setGeschwindigkeit(wert);
}
Future<void> _akzentSetzen(Color farbe) async {
setState(() => _akzent = farbe);
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())}';
}
String get _speicherText {
final anzahl = _speicherInfo['anzahl'] ?? 0;
final bytes = _speicherInfo['bytes'] ?? 0;
if (bytes >= 1073741824) {
return '$anzahl Songs · ${(bytes / 1073741824).toStringAsFixed(2)} GB';
}
return '$anzahl Songs · ${(bytes / 1048576).toStringAsFixed(1)} MB';
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: MeloTheme.schwarz,
appBar: AppBar(
backgroundColor: MeloTheme.oberflaeche,
leading: IconButton(
icon: const Icon(Icons.arrow_back, color: Colors.white, size: 22),
onPressed: () => Navigator.pop(context),
),
title: Row(
children: [
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),
),
],
),
),
body: ListView(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
children: [
// ═══ Sektion: Standard ═══
_sektionHeader('⚙️ Standard', icon: Icons.tune),
const SizedBox(height: 4),
// ─── Verbindung ───
_einstellungsKachel(
icon: Icons.cloud_outlined,
titel: 'Cloud Sync',
untertitel: 'Auto-Sync, Upload & Download',
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => CloudScreen(cloud: CloudService()),
),
);
},
),
_einstellungsKachel(
icon: Icons.dns_outlined,
titel: 'Musikserver',
untertitel: 'Navidrome Musik-Server verbinden',
onTap: () {
Navigator.pop(context, 'server');
},
),
const SizedBox(height: 4),
// ─── Sync & Wiedergabe ───
_einstellungsGruppe([
_gruppenZeile(
label: 'Sync-Modus',
wert: _syncModusText,
inhalt: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_modusRadioTile('⚡ Echtzeit', 'realtime',
'Änderungen sofort synchronisieren (Push)'),
_modusRadioTile('👆 Manuell', 'manual',
'Nur auf Knopfdruck synchronisieren'),
_modusRadioTile('🗓️ Zeit-Intervall', 'interval',
'Automatisch nach Zeitplan synchronisieren'),
],
),
),
if (_syncModus == 'interval') ...[
const Divider(color: MeloTheme.dunkel2, height: 1),
_gruppenZeile(
label: 'Intervall',
wert: _intervallOptionen[_cloudIntervallStunden] ?? '${_cloudIntervallStunden}h',
inhalt: DropdownButtonFormField<int>(
initialValue: _intervallOptionen.containsKey(_cloudIntervallStunden)
? _cloudIntervallStunden
: 48,
dropdownColor: MeloTheme.dunkel1,
style: const TextStyle(color: Colors.white, fontSize: 13),
decoration: const InputDecoration(
contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 8),
border: OutlineInputBorder(),
),
items: _intervallOptionen.entries.map((e) {
return DropdownMenuItem<int>(
value: e.key,
child: Text(e.value),
);
}).toList(),
onChanged: (v) {
if (v != null) _intervallSetzen(v);
},
),
),
],
const Divider(color: MeloTheme.dunkel2, height: 1),
_gruppenZeile(
label: 'Sleep-Timer-Standard',
wert: _sleepDefaultMin == 0 ? 'Aus' : '$_sleepDefaultMin Min',
inhalt: Row(children: [
_wahlChip('Aus', _sleepDefaultMin == 0, () => _sleepDefaultSetzen(0)),
_wahlChip('15', _sleepDefaultMin == 15, () => _sleepDefaultSetzen(15)),
_wahlChip('30', _sleepDefaultMin == 30, () => _sleepDefaultSetzen(30)),
_wahlChip('45', _sleepDefaultMin == 45, () => _sleepDefaultSetzen(45)),
_wahlChip('60', _sleepDefaultMin == 60, () => _sleepDefaultSetzen(60)),
]),
),
const Divider(color: MeloTheme.dunkel2, height: 1),
_gruppenZeile(
label: 'Geschwindigkeit',
wert: '${_geschwindigkeit.toStringAsFixed(2)}x',
inhalt: Slider(
value: _geschwindigkeit,
min: 0.5,
max: 2.0,
divisions: 6,
activeColor: MeloTheme.akzent,
onChanged: (v) => setState(() => _geschwindigkeit = v),
onChangeEnd: _geschwindigkeitSetzen,
),
),
]),
const SizedBox(height: 4),
// ─── Aussehen ───
_einstellungsGruppe([
_gruppenZeile(
label: 'Akzentfarbe',
wert: 'Für ${AuthService().benutzer}',
inhalt: Row(children: [
for (final farbe in _akzentFarben) ...[
_akzentPunkt(farbe, _akzent == farbe, () => _akzentSetzen(farbe)),
],
]),
),
]),
const SizedBox(height: 4),
// ─── Recap ───
_einstellungsKachel(
icon: Icons.auto_graph,
titel: 'Recap (Woche/Monat/Jahr)',
untertitel: 'Deine Hörstatistiken wie Spotify Wrapped',
onTap: () {
Navigator.push(
context,
MaterialPageRoute(builder: (_) => const RecapScreen()),
);
},
),
const SizedBox(height: 4),
// ─── Abmelden ───
SizedBox(
width: double.infinity,
height: 48,
child: OutlinedButton.icon(
onPressed: () {
Navigator.pop(context, 'logout');
},
icon: const Icon(Icons.logout, size: 18),
label: const Text('Abmelden', style: TextStyle(fontSize: 14)),
style: OutlinedButton.styleFrom(
foregroundColor: MeloTheme.akzent,
side: BorderSide(color: MeloTheme.akzent),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(999)),
),
),
),
const SizedBox(height: 20),
// ─── Versions-Anzeige (sichtbar!) ───
Center(
child: Text(
'Melo v2.56.1',
style: TextStyle(fontSize: 12, color: MeloTheme.textSekundaer),
),
),
const SizedBox(height: 8),
// ═══ 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),
],
),
);
}
// ─── ─── Hilfs-Widgets ─── ───
Widget _sektionHeader(String titel, {IconData? icon}) {
return Padding(
padding: const EdgeInsets.fromLTRB(4, 16, 4, 8),
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,
),
),
],
),
);
}
Widget _einstellungsKachel({
required IconData icon,
required String titel,
required String untertitel,
required VoidCallback onTap,
}) {
return Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Material(
color: MeloTheme.dunkel1,
borderRadius: BorderRadius.circular(20),
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(20),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
border: Border.all(color: MeloTheme.dunkel2),
),
child: Row(
children: [
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: MeloTheme.dunkel2,
borderRadius: BorderRadius.circular(14),
),
child: Icon(icon, color: MeloTheme.akzent, size: 20),
),
const SizedBox(width: 14),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
titel,
style: const TextStyle(color: Colors.white, fontSize: 14, fontWeight: FontWeight.w500),
),
const SizedBox(height: 2),
Text(
untertitel,
style: const TextStyle(color: MeloTheme.textSekundaer, fontSize: 12),
),
],
),
),
const Icon(Icons.chevron_right, color: MeloTheme.textSekundaer, size: 20),
],
),
),
),
),
);
}
Widget _infoKachel({
required IconData icon,
required String titel,
required String wert,
}) {
return Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
decoration: BoxDecoration(
color: MeloTheme.dunkel1,
borderRadius: BorderRadius.circular(20),
border: Border.all(color: MeloTheme.dunkel2),
),
child: Row(
children: [
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: MeloTheme.dunkel2,
borderRadius: BorderRadius.circular(14),
),
child: Icon(icon, color: MeloTheme.akzent, size: 20),
),
const SizedBox(width: 14),
Expanded(
child: Text(
titel,
style: const TextStyle(color: Colors.white, fontSize: 14, fontWeight: FontWeight.w500),
),
),
Text(
wert,
style: const TextStyle(color: MeloTheme.textSekundaer, fontSize: 12),
),
],
),
),
);
}
Widget _einstellungsGruppe(List<Widget> kinder) {
return 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: kinder),
);
}
Widget _gruppenZeile({
required String label,
String? wert,
required Widget inhalt,
}) {
return Padding(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Text(
label,
style: const TextStyle(color: Colors.white, fontSize: 14, fontWeight: FontWeight.w500),
),
),
if (wert != null)
Text(
wert,
style: TextStyle(color: MeloTheme.akzent, fontSize: 13, fontWeight: FontWeight.w600),
),
],
),
const SizedBox(height: 10),
inhalt,
],
),
);
}
Widget _wahlChip(String label, bool aktiv, VoidCallback onTap) {
return GestureDetector(
onTap: onTap,
child: Container(
margin: const EdgeInsets.only(right: 8),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 7),
decoration: BoxDecoration(
color: aktiv ? MeloTheme.akzent : MeloTheme.dunkel2,
borderRadius: BorderRadius.circular(999),
),
child: Text(
label,
style: TextStyle(
fontSize: 12,
color: aktiv ? Colors.white : MeloTheme.textSekundaer,
),
),
),
);
}
Widget _akzentPunkt(Color farbe, bool aktiv, VoidCallback onTap) {
return GestureDetector(
onTap: onTap,
child: Container(
margin: const EdgeInsets.only(right: 10),
width: 34,
height: 34,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: farbe,
border: Border.all(
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,
),
);
}
Widget _infoZeile(String label, String wert) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: Row(
children: [
Text(
label,
style: const TextStyle(color: MeloTheme.textSekundaer, fontSize: 13),
),
const Spacer(),
Text(
wert,
style: const TextStyle(color: Colors.white70, fontSize: 13),
),
],
),
);
}
String get _syncModusText {
switch (_syncModus) {
case 'realtime':
return '⚡ Echtzeit';
case 'manual':
return '👆 Manuell';
case 'interval':
return '🗓️ Zeit';
default:
return '⚡ Echtzeit';
}
}
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(14),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
decoration: BoxDecoration(
color: ausgewaehlt ? MeloTheme.akzent.withAlpha(25) : Colors.transparent,
borderRadius: BorderRadius.circular(14),
border: Border.all(
color: ausgewaehlt ? MeloTheme.akzent : MeloTheme.dunkel2,
width: ausgewaehlt ? 1.5 : 1,
),
),
child: Row(
children: [
Icon(
ausgewaehlt ? Icons.radio_button_checked : Icons.radio_button_unchecked,
color: ausgewaehlt ? MeloTheme.akzent : MeloTheme.textSekundaer,
size: 18,
),
const SizedBox(width: 10),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
titel,
style: TextStyle(
color: ausgewaehlt ? Colors.white : MeloTheme.textSekundaer,
fontSize: 13,
fontWeight: FontWeight.w500,
),
),
Text(
beschreibung,
style: TextStyle(
color: ausgewaehlt ? Colors.white70 : MeloTheme.textSekundaer,
fontSize: 11,
),
),
],
),
),
],
),
),
),
);
}
}