This repository has been archived on 2026-08-19. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
melo-app/lib/screens/cloud_screen.dart
T

624 lines
21 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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();
_status = 'Verbinde...';
_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);
}