623 lines
21 KiB
Dart
623 lines
21 KiB
Dart
import 'dart:async';
|
||
import 'dart:io';
|
||
import 'package:flutter/material.dart';
|
||
import 'package:path_provider/path_provider.dart';
|
||
import 'package:shared_preferences/shared_preferences.dart';
|
||
import '../utils/farb_theme.dart';
|
||
import '../services/cloud_service.dart';
|
||
import '../services/auth_service.dart';
|
||
import '../services/melo_logger.dart';
|
||
|
||
class CloudScreen extends StatefulWidget {
|
||
final CloudService cloud;
|
||
const CloudScreen({super.key, required this.cloud});
|
||
|
||
@override
|
||
State<CloudScreen> createState() => _CloudScreenState();
|
||
}
|
||
|
||
class _CloudScreenState extends State<CloudScreen> {
|
||
int _serverCount = 0;
|
||
bool _ladt = false;
|
||
String? _status;
|
||
bool _statusOk = false;
|
||
bool _autoSync = true;
|
||
int _syncIntervall = 6;
|
||
Timer? _syncTimer;
|
||
String _letzterSync = 'Nie';
|
||
List<Map> _korrupteSongs = [];
|
||
bool _ladtKorrupt = false;
|
||
bool _hatGeprueft = false;
|
||
|
||
@override
|
||
void initState() {
|
||
super.initState();
|
||
_verbindeCloud();
|
||
_ladeStatus();
|
||
_ladeSettings();
|
||
}
|
||
|
||
@override
|
||
void dispose() {
|
||
_syncTimer?.cancel();
|
||
super.dispose();
|
||
}
|
||
|
||
Future<void> _verbindeCloud() async {
|
||
final user = AuthService().benutzer;
|
||
if (user.isNotEmpty) {
|
||
final ok = await widget.cloud.login(user);
|
||
if (!ok && mounted) {
|
||
setState(() => _setzeStatus('Cloud-Login fehlgeschlagen', ok: false));
|
||
}
|
||
}
|
||
}
|
||
|
||
Future<void> _ladeSettings() async {
|
||
final p = await SharedPreferences.getInstance();
|
||
final letzter = p.getString('cloud_last_sync');
|
||
if (mounted) {
|
||
setState(() {
|
||
_autoSync = p.getBool('cloud_auto') ?? true;
|
||
_syncIntervall = p.getInt('cloud_interval') ?? 6;
|
||
_letzterSync = letzter ?? 'Nie';
|
||
});
|
||
}
|
||
_starteAutoSync();
|
||
}
|
||
|
||
void _starteAutoSync() {
|
||
_syncTimer?.cancel();
|
||
if (!_autoSync || _syncIntervall == 0) return;
|
||
_syncTimer = Timer.periodic(
|
||
Duration(hours: _syncIntervall),
|
||
(_) => _autoSyncDurchfuehren(),
|
||
);
|
||
}
|
||
|
||
Future<void> _autoSyncDurchfuehren() async {
|
||
await _download();
|
||
final p = await SharedPreferences.getInstance();
|
||
final now = DateTime.now();
|
||
final zeit = '${now.hour.toString().padLeft(2, '0')}:${now.minute.toString().padLeft(2, '0')}';
|
||
await p.setString('cloud_last_sync', zeit);
|
||
if (mounted) setState(() => _letzterSync = zeit);
|
||
}
|
||
|
||
Future<void> _ladeStatus() async {
|
||
final st = await widget.cloud.status();
|
||
if (!mounted) return;
|
||
setState(() {
|
||
_serverCount = st?['total'] ?? 0;
|
||
_statusOk = st != null;
|
||
_status = st != null ? 'Verbunden' : 'Keine Verbindung (Token?)';
|
||
});
|
||
}
|
||
|
||
Future<void> _ladeKorrupteSongs() async {
|
||
setState(() { _ladtKorrupt = true; _hatGeprueft = false; });
|
||
try {
|
||
final corrupted = await widget.cloud.getCorrupted();
|
||
if (mounted) setState(() { _korrupteSongs = corrupted; _hatGeprueft = true; });
|
||
} catch (e) {
|
||
MeloLogger().fehler('cloud_corrupted_laden', e);
|
||
if (mounted) setState(() => _hatGeprueft = true);
|
||
} finally {
|
||
if (mounted) setState(() => _ladtKorrupt = false);
|
||
}
|
||
}
|
||
|
||
Future<void> _upload() async {
|
||
setState(() => _ladt = true);
|
||
_setzeStatus('Suche lokale Songs...');
|
||
try {
|
||
final dir = Directory('${(await getApplicationDocumentsDirectory()).path}/music');
|
||
if (!await dir.exists()) {
|
||
setState(() { _ladt = false; _setzeStatus('Keine lokalen Songs', ok: false); });
|
||
return;
|
||
}
|
||
final files = dir.listSync().whereType<File>().where((f) =>
|
||
f.path.endsWith('.mp3') || f.path.endsWith('.m4a'));
|
||
int count = 0;
|
||
for (final f in files) {
|
||
_setzeStatus('Upload: ${f.path.split('/').last}...');
|
||
final sid = await widget.cloud.upload(f.path, f.path.split('/').last);
|
||
if (sid != null) count++;
|
||
}
|
||
await _ladeStatus();
|
||
if (mounted) {
|
||
setState(() { _ladt = false; });
|
||
_setzeStatus('$count Songs hochgeladen', ok: count > 0);
|
||
MeloLogger().aktion('cloud_upload', {'count': count});
|
||
}
|
||
} catch (e) {
|
||
MeloLogger().fehler('cloud_upload_path', e);
|
||
if (mounted) setState(() { _ladt = false; _setzeStatus('Fehler beim Upload', ok: false); });
|
||
}
|
||
}
|
||
|
||
Future<void> _download() async {
|
||
setState(() { _ladt = true; });
|
||
_setzeStatus('Vergleiche mit Server...');
|
||
try {
|
||
final dir = Directory('${(await getApplicationDocumentsDirectory()).path}/music');
|
||
if (!await dir.exists()) await dir.create(recursive: true);
|
||
final localFiles = dir.listSync().whereType<File>()
|
||
.map((f) => f.path.split('/').last).toSet();
|
||
final serverSongs = await widget.cloud.listSongs();
|
||
int downloaded = 0;
|
||
for (final song in serverSongs) {
|
||
final title = (song['title'] ?? 'unknown').toString();
|
||
if (localFiles.contains(title)) continue;
|
||
_setzeStatus('Download: $title...');
|
||
final sid = song['id'].toString();
|
||
final dest = '${dir.path}/$title';
|
||
if (await widget.cloud.download(sid, dest)) downloaded++;
|
||
}
|
||
await _ladeStatus();
|
||
if (mounted) {
|
||
setState(() { _ladt = false; });
|
||
_setzeStatus('$downloaded Songs heruntergeladen', ok: true);
|
||
MeloLogger().aktion('cloud_download', {'count': downloaded});
|
||
}
|
||
} catch (e) {
|
||
MeloLogger().fehler('cloud_download_path', e);
|
||
if (mounted) setState(() { _ladt = false; _setzeStatus('Fehler beim Download', ok: false); });
|
||
}
|
||
}
|
||
|
||
void _setzeStatus(String msg, {bool ok = false}) {
|
||
if (mounted) setState(() { _status = msg; _statusOk = ok; });
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Scaffold(
|
||
backgroundColor: MeloTheme.schwarz,
|
||
appBar: AppBar(
|
||
backgroundColor: MeloTheme.dunkel1,
|
||
leading: IconButton(
|
||
icon: const Icon(Icons.arrow_back, color: Colors.white, size: 22),
|
||
onPressed: () => Navigator.pop(context),
|
||
),
|
||
title: const Row(
|
||
children: [
|
||
Text('☁️', style: TextStyle(fontSize: 20)),
|
||
SizedBox(width: 8),
|
||
Text('Cloud Sync', style: TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.w600)),
|
||
],
|
||
),
|
||
actions: [
|
||
IconButton(
|
||
icon: const Icon(Icons.refresh, color: Colors.grey, size: 20),
|
||
onPressed: _ladeStatus,
|
||
),
|
||
],
|
||
),
|
||
body: SingleChildScrollView(
|
||
padding: const EdgeInsets.all(20),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
// ─── Status-Karte ───
|
||
_statusKarte(),
|
||
const SizedBox(height: 20),
|
||
|
||
// ─── Upload / Download Buttons ───
|
||
Row(
|
||
children: [
|
||
Expanded(child: _aktionsButton(
|
||
icon: Icons.upload_rounded,
|
||
label: 'Upload',
|
||
beschreibung: 'Lokale Songs → Server',
|
||
onTap: _upload,
|
||
)),
|
||
const SizedBox(width: 12),
|
||
Expanded(child: _aktionsButton(
|
||
icon: Icons.download_rounded,
|
||
label: 'Download',
|
||
beschreibung: 'Server → Lokal',
|
||
onTap: _download,
|
||
)),
|
||
],
|
||
),
|
||
|
||
// Lade-Indikator
|
||
if (_ladt)
|
||
const Padding(
|
||
padding: EdgeInsets.only(top: 16),
|
||
child: Center(child: CircularProgressIndicator(color: MeloTheme.rot)),
|
||
),
|
||
|
||
// Status-Text
|
||
if (_status != null && !_ladt)
|
||
Padding(
|
||
padding: const EdgeInsets.only(top: 12),
|
||
child: Container(
|
||
width: double.infinity,
|
||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
|
||
decoration: BoxDecoration(
|
||
color: _statusOk ? const Color(0xFF0D3B1E) : const Color(0xFF3B0D0D),
|
||
borderRadius: BorderRadius.circular(10),
|
||
),
|
||
child: Row(
|
||
children: [
|
||
Icon(
|
||
_statusOk ? Icons.check_circle : Icons.info_outline,
|
||
size: 16,
|
||
color: _statusOk ? const Color(0xFF4CAF50) : const Color(0xFFEF5350),
|
||
),
|
||
const SizedBox(width: 8),
|
||
Expanded(
|
||
child: Text(
|
||
_status!,
|
||
style: TextStyle(
|
||
color: _statusOk ? const Color(0xFFA5D6A7) : const Color(0xFFEF9A9A),
|
||
fontSize: 13,
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
|
||
const SizedBox(height: 24),
|
||
|
||
// ─── Sync-Einstellungen ───
|
||
_sektionsHeader('⚙ Sync-Einstellungen'),
|
||
const SizedBox(height: 8),
|
||
|
||
// Auto-Sync Toggle
|
||
Container(
|
||
decoration: BoxDecoration(
|
||
color: MeloTheme.dunkel1,
|
||
borderRadius: BorderRadius.circular(14),
|
||
border: Border.all(color: MeloTheme.dunkel2),
|
||
),
|
||
child: SwitchListTile(
|
||
title: const Text('Auto-Sync', style: TextStyle(color: Colors.white, fontSize: 14)),
|
||
subtitle: Text(
|
||
_autoSync ? 'Automatisch synchronisieren' : 'Nur manuell',
|
||
style: const TextStyle(color: MeloTheme.textSekundaer, fontSize: 12),
|
||
),
|
||
value: _autoSync,
|
||
activeColor: MeloTheme.rot,
|
||
secondary: const Icon(Icons.sync, color: MeloTheme.rot, size: 22),
|
||
onChanged: (v) async {
|
||
setState(() => _autoSync = v);
|
||
(await SharedPreferences.getInstance()).setBool('cloud_auto', v);
|
||
_starteAutoSync();
|
||
},
|
||
),
|
||
),
|
||
const SizedBox(height: 12),
|
||
|
||
// Sync-Intervall – schöne Segmented Buttons
|
||
const Text(
|
||
'Intervall',
|
||
style: TextStyle(color: MeloTheme.textSekundaer, fontSize: 12, fontWeight: FontWeight.w500),
|
||
),
|
||
const SizedBox(height: 8),
|
||
_intervallAuswahl(),
|
||
const SizedBox(height: 20),
|
||
|
||
// ─── Korrupte Songs (vom Server) ───
|
||
_sektionsHeader('⚠️ Defekte Musik (Server)'),
|
||
const SizedBox(height: 8),
|
||
_korrupteSektion(),
|
||
|
||
const SizedBox(height: 20),
|
||
|
||
// Letzter Sync
|
||
Container(
|
||
width: double.infinity,
|
||
padding: const EdgeInsets.all(14),
|
||
decoration: BoxDecoration(
|
||
color: MeloTheme.dunkel1,
|
||
borderRadius: BorderRadius.circular(12),
|
||
border: Border.all(color: MeloTheme.dunkel2),
|
||
),
|
||
child: Row(
|
||
children: [
|
||
const Icon(Icons.history, color: MeloTheme.textSekundaer, size: 18),
|
||
const SizedBox(width: 10),
|
||
const Text('Letzter Sync: ', style: TextStyle(color: MeloTheme.textSekundaer, fontSize: 13)),
|
||
Text(
|
||
_letzterSync,
|
||
style: const TextStyle(color: Colors.white, fontSize: 13, fontWeight: FontWeight.w500),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
const SizedBox(height: 32),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _statusKarte() {
|
||
return Container(
|
||
width: double.infinity,
|
||
padding: const EdgeInsets.all(20),
|
||
decoration: BoxDecoration(
|
||
gradient: const LinearGradient(
|
||
colors: [Color(0xFF1A0000), Color(0xFF0D0D0D)],
|
||
begin: Alignment.topLeft,
|
||
end: Alignment.bottomRight,
|
||
),
|
||
borderRadius: BorderRadius.circular(18),
|
||
border: Border.all(color: MeloTheme.rot.withValues(alpha: 0.3)),
|
||
),
|
||
child: Row(
|
||
children: [
|
||
Container(
|
||
width: 48,
|
||
height: 48,
|
||
decoration: BoxDecoration(
|
||
color: MeloTheme.rot.withValues(alpha: 0.15),
|
||
borderRadius: BorderRadius.circular(14),
|
||
),
|
||
child: const Icon(Icons.storage_rounded, color: MeloTheme.rot, size: 24),
|
||
),
|
||
const SizedBox(width: 16),
|
||
Expanded(
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Text(
|
||
'$_serverCount',
|
||
style: const TextStyle(
|
||
color: Colors.white,
|
||
fontSize: 28,
|
||
fontWeight: FontWeight.w700,
|
||
),
|
||
),
|
||
const Text(
|
||
'Songs auf dem Server',
|
||
style: TextStyle(color: MeloTheme.textSekundaer, fontSize: 13),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
// Verbindungsstatus-Indikator
|
||
Container(
|
||
width: 12,
|
||
height: 12,
|
||
decoration: BoxDecoration(
|
||
shape: BoxShape.circle,
|
||
color: _statusOk ? const Color(0xFF4CAF50) : const Color(0xFFEF5350),
|
||
boxShadow: [
|
||
BoxShadow(
|
||
color: (_statusOk ? const Color(0xFF4CAF50) : const Color(0xFFEF5350)).withValues(alpha: 0.5),
|
||
blurRadius: 8,
|
||
),
|
||
],
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _aktionsButton({
|
||
required IconData icon,
|
||
required String label,
|
||
required String beschreibung,
|
||
required VoidCallback onTap,
|
||
}) {
|
||
return Material(
|
||
color: MeloTheme.dunkel1,
|
||
borderRadius: BorderRadius.circular(16),
|
||
child: InkWell(
|
||
onTap: _ladt ? null : onTap,
|
||
borderRadius: BorderRadius.circular(16),
|
||
child: Container(
|
||
padding: const EdgeInsets.symmetric(vertical: 20, horizontal: 16),
|
||
decoration: BoxDecoration(
|
||
borderRadius: BorderRadius.circular(16),
|
||
border: Border.all(color: MeloTheme.dunkel2),
|
||
),
|
||
child: Column(
|
||
children: [
|
||
Icon(icon, color: MeloTheme.rot, size: 28),
|
||
const SizedBox(height: 8),
|
||
Text(label, style: const TextStyle(color: Colors.white, fontSize: 15, fontWeight: FontWeight.w600)),
|
||
const SizedBox(height: 4),
|
||
Text(beschreibung,
|
||
textAlign: TextAlign.center,
|
||
style: const TextStyle(color: MeloTheme.textSekundaer, fontSize: 11)),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _intervallAuswahl() {
|
||
final optionen = [
|
||
_IntervallOption('Manuell', 0, Icons.block),
|
||
_IntervallOption('3 Std', 3, Icons.timer),
|
||
_IntervallOption('6 Std', 6, Icons.timer),
|
||
_IntervallOption('12 Std', 12, Icons.timer),
|
||
];
|
||
|
||
return Row(
|
||
children: optionen.map((opt) {
|
||
final istAktiv = _syncIntervall == opt.wert;
|
||
return Expanded(
|
||
child: Padding(
|
||
padding: EdgeInsets.only(
|
||
right: opt != optionen.last ? 8 : 0,
|
||
),
|
||
child: GestureDetector(
|
||
onTap: () async {
|
||
setState(() => _syncIntervall = opt.wert);
|
||
(await SharedPreferences.getInstance()).setInt('cloud_interval', opt.wert);
|
||
_starteAutoSync();
|
||
},
|
||
child: AnimatedContainer(
|
||
duration: const Duration(milliseconds: 250),
|
||
padding: const EdgeInsets.symmetric(vertical: 14, horizontal: 6),
|
||
decoration: BoxDecoration(
|
||
color: istAktiv ? MeloTheme.rot : MeloTheme.dunkel1,
|
||
borderRadius: BorderRadius.circular(12),
|
||
border: Border.all(
|
||
color: istAktiv ? MeloTheme.rot : MeloTheme.dunkel2,
|
||
),
|
||
),
|
||
child: Column(
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: [
|
||
Icon(
|
||
opt.icon,
|
||
size: 16,
|
||
color: istAktiv ? Colors.white : MeloTheme.textSekundaer,
|
||
),
|
||
const SizedBox(height: 6),
|
||
Text(
|
||
opt.label,
|
||
textAlign: TextAlign.center,
|
||
style: TextStyle(
|
||
fontSize: 11,
|
||
fontWeight: FontWeight.w600,
|
||
color: istAktiv ? Colors.white : MeloTheme.textSekundaer,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}).toList(),
|
||
);
|
||
}
|
||
|
||
Widget _sektionsHeader(String titel) {
|
||
return Text(
|
||
titel,
|
||
style: const TextStyle(
|
||
color: MeloTheme.textSekundaer,
|
||
fontSize: 11,
|
||
fontWeight: FontWeight.w600,
|
||
letterSpacing: 1.2,
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _korrupteSektion() {
|
||
return Container(
|
||
width: double.infinity,
|
||
padding: const EdgeInsets.all(14),
|
||
decoration: BoxDecoration(
|
||
color: MeloTheme.dunkel1,
|
||
borderRadius: BorderRadius.circular(14),
|
||
border: Border.all(color: MeloTheme.dunkel2),
|
||
),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
// Button zum Abrufen
|
||
Row(
|
||
children: [
|
||
Expanded(
|
||
child: GestureDetector(
|
||
onTap: _ladtKorrupt ? null : _ladeKorrupteSongs,
|
||
child: Container(
|
||
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 14),
|
||
decoration: BoxDecoration(
|
||
color: MeloTheme.rot.withValues(alpha: 0.1),
|
||
borderRadius: BorderRadius.circular(10),
|
||
),
|
||
child: Row(
|
||
mainAxisAlignment: MainAxisAlignment.center,
|
||
children: [
|
||
if (_ladtKorrupt)
|
||
const SizedBox(
|
||
width: 14, height: 14,
|
||
child: CircularProgressIndicator(
|
||
color: MeloTheme.rot, strokeWidth: 2,
|
||
),
|
||
)
|
||
else
|
||
const Icon(Icons.warning_amber_rounded, color: MeloTheme.rot, size: 16),
|
||
const SizedBox(width: 8),
|
||
Text(
|
||
_ladtKorrupt ? 'Prüfe...' : 'Auf defekte Musik prüfen',
|
||
style: const TextStyle(
|
||
color: MeloTheme.rot, fontSize: 13, fontWeight: FontWeight.w600,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
|
||
// Ergebnisliste
|
||
if (_korrupteSongs.isNotEmpty) ...[
|
||
const SizedBox(height: 12),
|
||
const Divider(color: MeloTheme.dunkel2, height: 1),
|
||
const SizedBox(height: 8),
|
||
Text(
|
||
'${_korrupteSongs.length} defekte Songs gefunden:',
|
||
style: const TextStyle(
|
||
color: Color(0xFFEF9A9A), fontSize: 12, fontWeight: FontWeight.w500,
|
||
),
|
||
),
|
||
const SizedBox(height: 8),
|
||
..._korrupteSongs.map((s) => Padding(
|
||
padding: const EdgeInsets.only(bottom: 6),
|
||
child: Row(
|
||
children: [
|
||
const Text('⚠️', style: TextStyle(fontSize: 13)),
|
||
const SizedBox(width: 8),
|
||
Expanded(
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Text(
|
||
s['title']?.toString() ?? 'Unbekannt',
|
||
style: const TextStyle(
|
||
color: Colors.white70, fontSize: 12,
|
||
decoration: TextDecoration.lineThrough,
|
||
),
|
||
),
|
||
if (s['reason'] != null)
|
||
Text(
|
||
s['reason'].toString(),
|
||
style: const TextStyle(
|
||
color: MeloTheme.textSekundaer, fontSize: 10,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
],
|
||
),
|
||
)),
|
||
] else if (!_ladtKorrupt && _hatGeprueft && _korrupteSongs.isEmpty)
|
||
const Padding(
|
||
padding: EdgeInsets.only(top: 8),
|
||
child: Text(
|
||
'Keine defekten Songs auf dem Server',
|
||
style: TextStyle(color: Color(0xFFA5D6A7), fontSize: 12),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
class _IntervallOption {
|
||
final String label;
|
||
final int wert;
|
||
final IconData icon;
|
||
const _IntervallOption(this.label, this.wert, this.icon);
|
||
}
|