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
Dustin 36c744d6e3 v2.31 — Login, Einstellungen & Cloud Sync Redesign
- Login-Screen im Melo-Design (Schwarz+Rot) mit Baka-Auth JWT
- AuthService fuer Token-Management ueber baka-net.de/auth
- Registrierungs-Modus + 'Ohne Login fortfahren'
- Navidrome-Credentials optional im Login aufklappbar

- SettingsScreen ersetzt AlertDialog
- BottomNav navigiert statt Dialog zu triggern
- Cloud Sync, Server verbinden, Diagnosedaten, App-Info, Abmelden

- CloudEinstellungen-Dialog entfernt, alles in CloudScreen konsolidiert
- Hardcode login('Baka','') entfernt → AuthService.benutzer
- Gradient-Statuskarte, animierte Segment-Intervall-Auswahl
- JWT Authorization statt Klartext-Passwort

- AppConfig: API-Key defaultValue entfernt (kein Fallback-Leak)
- Build crasht ohne --dart-define MELO_API_KEY

Build: split APK (arm64 19.6M, armeabi 17.1M, x86_64 21.1M)
2026-08-01 15:03:22 +02:00

481 lines
16 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';
@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) {
await widget.cloud.login(user);
}
}
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;
_status = st != null ? 'Verbunden' : 'Keine Verbindung';
_statusOk = st != null;
});
}
Future<void> _upload() async {
setState(() => _ladt = true);
_setzeStatus('Suche lokale Songs...');
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});
}
}
Future<void> _download() async {
setState(() => _ladt = true);
_setzeStatus('Vergleiche mit Server...');
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'].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});
}
}
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: 12),
// 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,
),
);
}
}
class _IntervallOption {
final String label;
final int wert;
final IconData icon;
const _IntervallOption(this.label, this.wert, this.icon);
}