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:
@@ -205,4 +205,26 @@ class CloudService {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Löscht alle Cloud-Songs des angemeldeten Users (inkl. Server-Dateien).
|
||||
Future<bool> loescheMusik() async {
|
||||
return _postOhneBody('/api/cloud/delete-music');
|
||||
}
|
||||
|
||||
/// Löscht ALLE Cloud-Daten des Users (Musik + Shares + Ordner).
|
||||
Future<bool> loescheAlles() async {
|
||||
return _postOhneBody('/api/cloud/delete-all');
|
||||
}
|
||||
|
||||
Future<bool> _postOhneBody(String path) async {
|
||||
try {
|
||||
final r = await http
|
||||
.post(Uri.parse('$_base$path'),
|
||||
headers: {..._authHeader, 'Content-Type': 'application/json'})
|
||||
.timeout(const Duration(seconds: 30));
|
||||
return r.statusCode == 200;
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user