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)),
),
],
),
);
}
}
+109
View File
@@ -0,0 +1,109 @@
import 'dart:async';
import 'package:flutter/material.dart';
import '../utils/farb_theme.dart';
/// Bestätigungs-Button: 3 Sekunden gedrückt halten, dann feuert [onBestaetigt].
/// Loslassen vor Ablauf bricht ab (Fortschrittsbalken geht zurück).
class HalteZumBestaetigen extends StatefulWidget {
final String text;
final Color farbe;
final VoidCallback onBestaetigt;
final Duration dauer;
const HalteZumBestaetigen({
super.key,
required this.text,
required this.onBestaetigt,
this.farbe = MeloTheme.rot,
this.dauer = const Duration(seconds: 3),
});
@override
State<HalteZumBestaetigen> createState() => _HalteZumBestaetigenState();
}
class _HalteZumBestaetigenState extends State<HalteZumBestaetigen> {
double _fortschritt = 0;
Timer? _timer;
bool _aktiv = false;
void _starte() {
if (_aktiv) return;
_aktiv = true;
final schritte = widget.dauer.inMilliseconds ~/ 50;
_timer = Timer.periodic(const Duration(milliseconds: 50), (t) {
setState(() => _fortschritt += 1 / schritte);
if (_fortschritt >= 1) {
t.cancel();
_timer = null;
_aktiv = false;
widget.onBestaetigt();
}
});
}
void _stoppe() {
_timer?.cancel();
_timer = null;
_aktiv = false;
if (_fortschritt > 0 && _fortschritt < 1) {
setState(() => _fortschritt = 0);
}
}
@override
void dispose() {
_timer?.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTapDown: (_) => _starte(),
onTapUp: (_) => _stoppe(),
onTapCancel: _stoppe,
child: Container(
width: double.infinity,
decoration: BoxDecoration(
color: widget.farbe.withValues(alpha: 0.12),
border: Border.all(color: widget.farbe, width: 1.5),
borderRadius: BorderRadius.circular(10),
),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.lock_clock, color: widget.farbe, size: 18),
const SizedBox(width: 8),
Flexible(
child: Text(
widget.text,
style: TextStyle(
color: widget.farbe,
fontSize: 13,
fontWeight: FontWeight.w600),
),
),
],
),
const SizedBox(height: 8),
ClipRRect(
borderRadius: BorderRadius.circular(4),
child: LinearProgressIndicator(
value: _fortschritt,
minHeight: 6,
backgroundColor: MeloTheme.dunkel2,
color: widget.farbe,
),
),
],
),
),
);
}
}