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)
This commit is contained in:
+370
-225
@@ -1,21 +1,16 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:just_audio/just_audio.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';
|
||||
import '../models/song.dart';
|
||||
import '../database/db_helper.dart';
|
||||
import '../services/id3_reader.dart';
|
||||
|
||||
class CloudScreen extends StatefulWidget {
|
||||
final CloudService cloud;
|
||||
final VoidCallback onSongsChanged;
|
||||
const CloudScreen({super.key, required this.cloud, required this.onSongsChanged});
|
||||
const CloudScreen({super.key, required this.cloud});
|
||||
|
||||
@override
|
||||
State<CloudScreen> createState() => _CloudScreenState();
|
||||
@@ -25,193 +20,144 @@ class _CloudScreenState extends State<CloudScreen> {
|
||||
int _serverCount = 0;
|
||||
bool _ladt = false;
|
||||
String? _status;
|
||||
bool _statusOk = false;
|
||||
bool _autoSync = true;
|
||||
int _syncIntervall = 6;
|
||||
String _aktuellerDownload = '';
|
||||
int _downloadFortschritt = 0;
|
||||
int _downloadGesamt = 0;
|
||||
Timer? _syncTimer;
|
||||
String _letzterSync = 'Nie';
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_verbindeCloud();
|
||||
_ladeStatus();
|
||||
_ladeSettings();
|
||||
}
|
||||
|
||||
Future<void> _ladeStatus() async {
|
||||
final p = await SharedPreferences.getInstance();
|
||||
final nutzer = p.getString('melo_nutzer') ?? '';
|
||||
await widget.cloud.restoreLogin();
|
||||
if (nutzer.isEmpty || !widget.cloud.istAngemeldet) {
|
||||
if (mounted) setState(() { _serverCount = 0; _status = 'Nicht angemeldet'; });
|
||||
return;
|
||||
@override
|
||||
void dispose() {
|
||||
_syncTimer?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _verbindeCloud() async {
|
||||
final user = AuthService().benutzer;
|
||||
if (user.isNotEmpty) {
|
||||
await widget.cloud.login(user);
|
||||
}
|
||||
final st = await widget.cloud.status();
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_serverCount = st?['total'] ?? 0;
|
||||
_status = st != null ? 'Verbunden ($nutzer)' : 'Keine Verbindung';
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _ladeSettings() async {
|
||||
final p = await SharedPreferences.getInstance();
|
||||
if (mounted) setState(() {
|
||||
_autoSync = p.getBool('cloud_auto') ?? true;
|
||||
_syncIntervall = p.getInt('cloud_interval') ?? 6;
|
||||
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; _status = 'Keine lokalen Songs'; });
|
||||
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; _status = '$count Songs hochgeladen'; });
|
||||
setState(() { _ladt = false; });
|
||||
_setzeStatus('$count Songs hochgeladen', ok: count > 0);
|
||||
MeloLogger().aktion('cloud_upload', {'count': count});
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _download() async {
|
||||
setState(() {
|
||||
_ladt = true;
|
||||
_status = null;
|
||||
_aktuellerDownload = 'Lade Liste...';
|
||||
_downloadFortschritt = 0;
|
||||
_downloadGesamt = 0;
|
||||
});
|
||||
|
||||
// Auf Android: Music/Melo (getExternalStorageDirectory)
|
||||
// Auf iOS: Documents/music (Sandbox, sichtbar in Dateien-App)
|
||||
String basePath;
|
||||
if (Platform.isIOS) {
|
||||
basePath = '${(await getApplicationDocumentsDirectory()).path}/music';
|
||||
} else {
|
||||
// Wenn voller Speicherzugriff: Downloads/Melo (sichtbar)
|
||||
final p = await SharedPreferences.getInstance();
|
||||
if (p.getBool('manage_storage') == true) {
|
||||
final d = await getDownloadsDirectory();
|
||||
basePath = d != null ? '${d.path}/Melo' : '${(await getApplicationDocumentsDirectory()).path}/music';
|
||||
} else {
|
||||
final ext = await getExternalStorageDirectory();
|
||||
basePath = ext != null ? '${ext.path}/Melo' : '${(await getApplicationDocumentsDirectory()).path}/music';
|
||||
}
|
||||
}
|
||||
final dir = Directory(basePath);
|
||||
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();
|
||||
|
||||
// Filter: nur neue Songs
|
||||
final neue = serverSongs.where((s) {
|
||||
final title = s['title'].toString();
|
||||
return !localFiles.contains(title);
|
||||
}).toList();
|
||||
|
||||
if (neue.isEmpty) {
|
||||
if (mounted) setState(() { _ladt = false; _status = 'Alle Songs bereits lokal'; });
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() => _downloadGesamt = neue.length);
|
||||
final db = DbHelper();
|
||||
|
||||
for (int i = 0; i < neue.length; i++) {
|
||||
final song = neue[i];
|
||||
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();
|
||||
|
||||
// Dateinamen säubern (p.basename verhindert Pfad-Traversal via Titel)
|
||||
final safeTitle = p.basename(title).replaceAll(RegExp(r'[^\w\s\.-]'), '_').trim();
|
||||
final dateiName = safeTitle.endsWith('.mp3') || safeTitle.endsWith('.m4a')
|
||||
? safeTitle : '$safeTitle.mp3';
|
||||
final dest = '${dir.path}/$dateiName';
|
||||
|
||||
if (mounted) setState(() {
|
||||
_aktuellerDownload = title;
|
||||
_downloadFortschritt = i + 1;
|
||||
});
|
||||
|
||||
final ok = await widget.cloud.download(sid, dest);
|
||||
if (ok) {
|
||||
// Dauer auslesen
|
||||
int dauer = 0;
|
||||
try {
|
||||
final ap = AudioPlayer();
|
||||
await ap.setFilePath(dest).timeout(const Duration(milliseconds: 2000));
|
||||
dauer = ap.duration?.inSeconds ?? 0;
|
||||
await ap.dispose();
|
||||
} catch (_) {}
|
||||
|
||||
// ID3-Metadaten & Cover aus der heruntergeladenen Datei extrahieren
|
||||
final id3 = Id3Reader.lesen(dest);
|
||||
String? coverPfad;
|
||||
if (id3['cover'] != null && id3['cover'] is List<int>) {
|
||||
try {
|
||||
final coversDir = Directory('${(await getApplicationDocumentsDirectory()).path}/covers');
|
||||
if (!await coversDir.exists()) await coversDir.create(recursive: true);
|
||||
final coverFile = File('${coversDir.path}/cloud_$sid.jpg');
|
||||
await coverFile.writeAsBytes(id3['cover'] as List<int>);
|
||||
coverPfad = coverFile.path;
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
try {
|
||||
await db.songEinfuegen(Song(
|
||||
titel: (id3['titel'] as String).isNotEmpty
|
||||
? id3['titel']
|
||||
: title.replaceAll('.mp3', '').replaceAll('.m4a', ''),
|
||||
kuenstler: (id3['kuenstler'] as String).isNotEmpty
|
||||
? id3['kuenstler']
|
||||
: 'Melo Cloud',
|
||||
album: id3['album'] ?? '',
|
||||
dauerSekunden: dauer,
|
||||
dateiPfad: dest,
|
||||
coverPfad: coverPfad,
|
||||
downloadQuelle: 'cloud',
|
||||
istHeruntergeladen: true,
|
||||
));
|
||||
} catch (_) {}
|
||||
}
|
||||
final dest = '${dir.path}/$title';
|
||||
if (await widget.cloud.download(sid, dest)) downloaded++;
|
||||
}
|
||||
|
||||
await _ladeStatus();
|
||||
widget.onSongsChanged(); // Musik-Tab aktualisieren
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_ladt = false;
|
||||
_aktuellerDownload = '';
|
||||
_status = '${_downloadGesamt} Songs heruntergeladen';
|
||||
});
|
||||
MeloLogger().aktion('cloud_download', {'count': _downloadGesamt});
|
||||
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,
|
||||
title: const Row(children: [
|
||||
Text('☁️', style: TextStyle(fontSize: 20)),
|
||||
SizedBox(width: 8),
|
||||
Text('Cloud Sync', style: TextStyle(color: Colors.white, fontSize: 18)),
|
||||
]),
|
||||
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),
|
||||
@@ -219,14 +165,118 @@ class _CloudScreenState extends State<CloudScreen> {
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Padding(
|
||||
body: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Status-Karte
|
||||
// ─── 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(16),
|
||||
padding: const EdgeInsets.all(14),
|
||||
decoration: BoxDecoration(
|
||||
color: MeloTheme.dunkel1,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
@@ -234,102 +284,197 @@ class _CloudScreenState extends State<CloudScreen> {
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.storage, color: MeloTheme.rot, size: 28),
|
||||
const SizedBox(width: 12),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('$_serverCount Songs auf Server',
|
||||
style: const TextStyle(color: Colors.white, fontSize: 16, fontWeight: FontWeight.w600)),
|
||||
Text(_status ?? 'Lädt...',
|
||||
style: const TextStyle(color: Colors.grey, fontSize: 12)),
|
||||
],
|
||||
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: 16),
|
||||
// Upload / Download
|
||||
Row(
|
||||
children: [
|
||||
Expanded(child: _btn(Icons.upload, 'Upload', _upload)),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(child: _btn(Icons.download, 'Download', _download)),
|
||||
],
|
||||
),
|
||||
// Download-Fortschritt
|
||||
if (_ladt && _aktuellerDownload.isNotEmpty) ...[
|
||||
const SizedBox(height: 12),
|
||||
LinearProgressIndicator(
|
||||
value: _downloadGesamt > 0 ? _downloadFortschritt / _downloadGesamt : null,
|
||||
backgroundColor: MeloTheme.dunkel2,
|
||||
valueColor: const AlwaysStoppedAnimation(MeloTheme.rot),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
'$_downloadFortschritt/$_downloadGesamt: $_aktuellerDownload',
|
||||
style: const TextStyle(color: Colors.grey, fontSize: 12),
|
||||
maxLines: 2, overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
if (_status != null && !_ladt)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8),
|
||||
child: Text(_status!, style: const TextStyle(color: Colors.grey, fontSize: 13)),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
// Sync-Einstellungen
|
||||
const Divider(color: MeloTheme.dunkel2),
|
||||
const Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text('⚙ Sync-Einstellungen', style: TextStyle(color: Colors.grey, fontSize: 11)),
|
||||
),
|
||||
SwitchListTile(
|
||||
dense: true,
|
||||
title: const Text('Auto-Sync', style: TextStyle(color: Colors.white, fontSize: 13)),
|
||||
value: _autoSync,
|
||||
activeColor: MeloTheme.rot,
|
||||
onChanged: (v) async {
|
||||
setState(() => _autoSync = v);
|
||||
(await SharedPreferences.getInstance()).setBool('cloud_auto', v);
|
||||
},
|
||||
),
|
||||
Row(
|
||||
children: ['Manuell', 'Alle 3h', 'Alle 6h', 'Alle 12h'].asMap().entries.map((e) {
|
||||
final vals = [0, 3, 6, 12];
|
||||
return Expanded(
|
||||
child: ChoiceChip(
|
||||
label: Text(e.value, style: TextStyle(fontSize: 10, color: _syncIntervall == vals[e.key] ? Colors.white : Colors.grey)),
|
||||
selected: _syncIntervall == vals[e.key],
|
||||
selectedColor: MeloTheme.rot,
|
||||
backgroundColor: MeloTheme.dunkel2,
|
||||
onSelected: (v) async {
|
||||
setState(() => _syncIntervall = vals[e.key]);
|
||||
(await SharedPreferences.getInstance()).setInt('cloud_interval', vals[e.key]);
|
||||
},
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _btn(IconData icon, String label, VoidCallback onTap) {
|
||||
return ElevatedButton.icon(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: MeloTheme.dunkel1,
|
||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
side: const BorderSide(color: MeloTheme.dunkel2),
|
||||
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)),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
onPressed: _ladt ? null : onTap,
|
||||
icon: Icon(icon, size: 18, color: MeloTheme.rot),
|
||||
label: Text(label, style: const TextStyle(color: Colors.white, fontSize: 14)),
|
||||
);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user