From b3aedc53f94dd98f0d9bb5f132ef911f0413f85e Mon Sep 17 00:00:00 2001 From: "Hermes (Server)" Date: Fri, 31 Jul 2026 15:39:19 +0200 Subject: [PATCH] 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) --- lib/services/cloud_service.dart | 22 +++ lib/widgets/cloud_einstellungen.dart | 194 +++++++++++++++++++++++++ lib/widgets/halte_zum_bestaetigen.dart | 109 ++++++++++++++ 3 files changed, 325 insertions(+) create mode 100644 lib/widgets/cloud_einstellungen.dart create mode 100644 lib/widgets/halte_zum_bestaetigen.dart diff --git a/lib/services/cloud_service.dart b/lib/services/cloud_service.dart index 47401e5..067d9f6 100644 --- a/lib/services/cloud_service.dart +++ b/lib/services/cloud_service.dart @@ -205,4 +205,26 @@ class CloudService { return false; } } + + /// Löscht alle Cloud-Songs des angemeldeten Users (inkl. Server-Dateien). + Future loescheMusik() async { + return _postOhneBody('/api/cloud/delete-music'); + } + + /// Löscht ALLE Cloud-Daten des Users (Musik + Shares + Ordner). + Future loescheAlles() async { + return _postOhneBody('/api/cloud/delete-all'); + } + + Future _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; + } + } } diff --git a/lib/widgets/cloud_einstellungen.dart b/lib/widgets/cloud_einstellungen.dart new file mode 100644 index 0000000..4ab5959 --- /dev/null +++ b/lib/widgets/cloud_einstellungen.dart @@ -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 createState() => _CloudEinstellungenState(); +} + +class _CloudEinstellungenState extends State { + bool _autoSync = true; + int _syncIntervall = 6; // Stunden + Timer? _syncTimer; + + @override + void initState() { + super.initState(); + _ladeSettings(); + } + + @override + void dispose() { + _syncTimer?.cancel(); + super.dispose(); + } + + Future _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( + 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)), + ), + ], + ), + ); + } +} diff --git a/lib/widgets/halte_zum_bestaetigen.dart b/lib/widgets/halte_zum_bestaetigen.dart new file mode 100644 index 0000000..124793c --- /dev/null +++ b/lib/widgets/halte_zum_bestaetigen.dart @@ -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 createState() => _HalteZumBestaetigenState(); +} + +class _HalteZumBestaetigenState extends State { + 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, + ), + ), + ], + ), + ), + ); + } +}