Feature: Cloud-Daten loeschen in Einstellungen (3s-Halten)

- Einstellungen > Cloud-Daten loeschen: 'Nur Musik' oder 'Komplett vom Server'
- 3-Sekunden-Gedrueckthalten-Button zur Bestaetigung (Fortschrittsbalken)
- Warnhinweis: Wird SOFORT geloescht, nicht rueckgaengig machbar
- Server: delete-music/delete-all Endpunkte mit Dedup-sicherem Registry-Cleanup
- Loeschen nur des eigenen Kontos (User aus Token, kein IDOR)
This commit is contained in:
Hermes (Server)
2026-07-31 15:39:19 +02:00
parent 519bdd4e23
commit b3aedc53f9
3 changed files with 325 additions and 0 deletions
+194
View File
@@ -0,0 +1,194 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../services/cloud_service.dart';
import '../utils/farb_theme.dart';
import 'halte_zum_bestaetigen.dart';
/// Cloud Sync Einstellungen eigenständiger Dialog
class CloudEinstellungen extends StatefulWidget {
const CloudEinstellungen({super.key});
@override
State<CloudEinstellungen> createState() => _CloudEinstellungenState();
}
class _CloudEinstellungenState extends State<CloudEinstellungen> {
bool _autoSync = true;
int _syncIntervall = 6; // Stunden
Timer? _syncTimer;
@override
void initState() {
super.initState();
_ladeSettings();
}
@override
void dispose() {
_syncTimer?.cancel();
super.dispose();
}
Future<void> _ladeSettings() async {
final p = await SharedPreferences.getInstance();
if (mounted) {
setState(() {
_autoSync = p.getBool('cloud_auto') ?? true;
_syncIntervall = p.getInt('cloud_interval') ?? 6;
});
}
}
void _starteAutoSync() {
_syncTimer?.cancel();
if (!_autoSync || _syncIntervall == 0) return;
_syncTimer = Timer.periodic(
Duration(hours: _syncIntervall),
(_) => _triggerSync(),
);
}
void _triggerSync() {
// Wird vom CloudService in cloud_screen.dart erledigt
}
@override
Widget build(BuildContext context) {
return AlertDialog(
backgroundColor: MeloTheme.dunkel1,
title: const Row(
children: [
Icon(Icons.cloud, color: MeloTheme.rot, size: 20),
SizedBox(width: 8),
Text('☁ Cloud Sync', style: TextStyle(color: Colors.white, fontSize: 16)),
],
),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
SwitchListTile(
title: const Text('Auto-Sync', style: TextStyle(color: Colors.white, fontSize: 14)),
subtitle: Text(_autoSync ? 'Aktiviert' : 'Aus',
style: const TextStyle(color: Colors.grey, fontSize: 12)),
value: _autoSync,
activeColor: MeloTheme.rot,
onChanged: (v) async {
setState(() => _autoSync = v);
final p = await SharedPreferences.getInstance();
p.setBool('cloud_auto', v);
_starteAutoSync();
},
),
const Divider(color: MeloTheme.dunkel2),
...['Manuell', 'Alle 3h', 'Alle 6h', 'Alle 12h'].asMap().entries.map((e) {
final vals = [0, 3, 6, 12];
final val = vals[e.key];
return RadioListTile<int>(
dense: true,
title: Text(e.value,
style: const TextStyle(color: Colors.white, fontSize: 13)),
value: val,
groupValue: _syncIntervall,
activeColor: MeloTheme.rot,
onChanged: (v) async {
if (v == null) return;
setState(() => _syncIntervall = v);
final p = await SharedPreferences.getInstance();
p.setInt('cloud_interval', v);
_starteAutoSync();
},
);
}),
const SizedBox(height: 8),
SizedBox(
width: double.infinity,
child: OutlinedButton.icon(
icon: const Icon(Icons.sync, size: 16, color: MeloTheme.rot),
label: const Text('Jetzt synchronisieren',
style: TextStyle(color: MeloTheme.rot, fontSize: 13)),
style: OutlinedButton.styleFrom(
side: const BorderSide(color: MeloTheme.rot),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
onPressed: () {
Navigator.pop(context, true); // Signal zum Sync
},
),
),
if (CloudService().istAngemeldet) ...[
const Divider(color: MeloTheme.dunkel2),
Align(
alignment: Alignment.centerLeft,
child: Text('Cloud-Daten löschen',
style: TextStyle(color: Colors.red.shade300, fontSize: 12, fontWeight: FontWeight.w600)),
),
ListTile(
dense: true,
leading: const Icon(Icons.music_off, color: Colors.red, size: 18),
title: const Text('Nur Musik löschen',
style: TextStyle(color: Colors.white, fontSize: 13)),
subtitle: const Text('Alle Cloud-Songs entfernen',
style: TextStyle(color: Colors.grey, fontSize: 11)),
onTap: () => _zeigeLoeschDialog(komplett: false),
),
ListTile(
dense: true,
leading: const Icon(Icons.delete_sweep, color: Colors.red, size: 18),
title: const Text('Komplett vom Server löschen',
style: TextStyle(color: Colors.white, fontSize: 13)),
subtitle: const Text('Musik + Shares + Ordner (unwiderruflich)',
style: TextStyle(color: Colors.grey, fontSize: 11)),
onTap: () => _zeigeLoeschDialog(komplett: true),
),
],
],
),
);
}
/// Lösch-Dialog: Warnt vor sofortigem Löschen, Bestätigung per 3s-Halten.
void _zeigeLoeschDialog({required bool komplett}) {
showDialog(
context: context,
builder: (ctx) => AlertDialog(
backgroundColor: MeloTheme.dunkel1,
title: Text(komplett ? '☠️ Komplett löschen?' : '🎵 Musik löschen?',
style: const TextStyle(color: Colors.white, fontSize: 15)),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text(
'⚠️ Wird SOFORT gelöscht und kann nicht rückgängig gemacht werden!',
style: TextStyle(color: Colors.orangeAccent, fontSize: 13),
textAlign: TextAlign.center,
),
const SizedBox(height: 12),
HalteZumBestaetigen(
text: '3 Sekunden gedrückt halten…',
farbe: Colors.red,
onBestaetigt: () async {
Navigator.pop(ctx);
final ok = komplett
? await CloudService().loescheAlles()
: await CloudService().loescheMusik();
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text(ok
? '✅ Cloud-Daten gelöscht'
: '❌ Löschen fehlgeschlagen bist du angemeldet?'),
));
},
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx),
child: const Text('Abbrechen', style: TextStyle(color: Colors.grey)),
),
],
),
);
}
}