v2.51.3 — Cloud-Dashboard UI (Statistik-Karten, Speicher, Sync-Historie) + Lint-Fixes
## Cloud-Dashboard (cloud_screen.dart)
- Statistik-Karten 2x2: Songs, Playlisten, Favoriten, Letzter Sync (Daten aus statusDaten/syncStatus)
- Speicheranzeige mit Balken — Platzhalter „–" wenn der Server keine Storage-Daten liefert (storage_used/storage_total o.ä.)
- Sync-Historie-Zeile: „Heute: X Dateien, Y Favoriten, Z Playlisten" aus _syncHistorie (heute gefiltert)
- Konflikt-Dialog NUR bei abweichendem Titel (lokal ≠ Server), sonst kein Dialog
- Lint-Fixes: curly_braces (if ohne Block) + unused _syncHistorieHeuteText (jetzt in der Historie-Zeile verdrahtet)
- Sync-Animation während des Syncs: rotierendes ☁️-Icon + Fortschrittsring (CloudStatus/AnimationController)
Keine neuen Packages, flutter analyze 0 Issues, Tests 98/98 grün
This commit is contained in:
+412
-25
@@ -1,4 +1,5 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
@@ -9,6 +10,7 @@ import '../utils/sanitize.dart';
|
||||
import '../services/cloud_service.dart';
|
||||
import '../database/db_helper.dart';
|
||||
import '../services/melo_logger.dart';
|
||||
import '../models/song.dart';
|
||||
import '../main.dart'; // notificationsPlugin
|
||||
|
||||
/// Melo Cloud Sync Screen v3 — vollständiges Sync-System
|
||||
@@ -22,16 +24,21 @@ class CloudScreen extends StatefulWidget {
|
||||
State<CloudScreen> createState() => _CloudScreenState();
|
||||
}
|
||||
|
||||
class _CloudScreenState extends State<CloudScreen> {
|
||||
class _CloudScreenState extends State<CloudScreen>
|
||||
with SingleTickerProviderStateMixin {
|
||||
// ─── Status ───
|
||||
int _serverCount = 0;
|
||||
int _favServerCount = 0;
|
||||
int _playlistServerCount = 0;
|
||||
Map? _serverStatusDaten;
|
||||
bool _ladt = false;
|
||||
String? _status;
|
||||
bool _statusOk = false;
|
||||
bool _serverDatenGeladen = false;
|
||||
|
||||
// ─── Sync-Animation ───
|
||||
late final AnimationController _syncAnimController;
|
||||
|
||||
// ─── Sync-Einstellungen ───
|
||||
bool _autoSync = true;
|
||||
int _syncIntervall = 6;
|
||||
@@ -45,6 +52,9 @@ class _CloudScreenState extends State<CloudScreen> {
|
||||
bool _syncLaeuft = false;
|
||||
int _syncedItems = 0;
|
||||
|
||||
// ─── Sync-Historie (heute) ───
|
||||
List<Map<String, dynamic>> _syncHistorie = [];
|
||||
|
||||
// ─── Korrupt ───
|
||||
List<Map> _korrupteSongs = [];
|
||||
bool _ladtKorrupt = false;
|
||||
@@ -65,9 +75,14 @@ class _CloudScreenState extends State<CloudScreen> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_syncAnimController = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 1400),
|
||||
);
|
||||
// Verbindung stellt der CloudService beim App-Start her (MeloHome.initState).
|
||||
// Hier nur Settings laden + Serverdaten, sobald der Service verbunden ist.
|
||||
_ladeSettings();
|
||||
_ladeSyncHistorie();
|
||||
if (widget.cloud.istVerbunden) {
|
||||
_serverDatenGeladen = true;
|
||||
_ladeServerDaten();
|
||||
@@ -84,6 +99,7 @@ class _CloudScreenState extends State<CloudScreen> {
|
||||
void dispose() {
|
||||
widget.cloud.removeListener(_onCloudStatus);
|
||||
_syncTimer?.cancel();
|
||||
_syncAnimController.dispose();
|
||||
_renameTitleCtrl.dispose();
|
||||
_renameArtistCtrl.dispose();
|
||||
super.dispose();
|
||||
@@ -188,6 +204,7 @@ class _CloudScreenState extends State<CloudScreen> {
|
||||
_syncFortschritt = 0;
|
||||
_syncedItems = 0;
|
||||
});
|
||||
_syncAnimController.repeat();
|
||||
|
||||
try {
|
||||
// Phase 1: Songs synchronisieren
|
||||
@@ -209,7 +226,7 @@ class _CloudScreenState extends State<CloudScreen> {
|
||||
if (existing == null) totalNew++;
|
||||
}
|
||||
|
||||
// Downloade neue Songs
|
||||
// Downloade neue Songs + löse Konflikte (Titel lokal ≠ Server)
|
||||
int processed = 0;
|
||||
for (final song in serverSongs) {
|
||||
final sid = song['id']?.toString() ?? '';
|
||||
@@ -217,6 +234,41 @@ class _CloudScreenState extends State<CloudScreen> {
|
||||
final title = (song['title'] ?? 'unknown').toString();
|
||||
final existing = await db.songNachCloudId(sid);
|
||||
if (existing != null) {
|
||||
// Konfliktprüfung: Server-Titel weicht vom lokalen ab
|
||||
final serverTitel = title.trim();
|
||||
final lokalerTitel = existing.titel.trim();
|
||||
if (serverTitel.isNotEmpty &&
|
||||
lokalerTitel.toLowerCase() != serverTitel.toLowerCase()) {
|
||||
final wahl = await _konfliktDialog(existing, song);
|
||||
if (wahl == 'server') {
|
||||
await db.cloudMetadatenAktualisieren(
|
||||
existing.id!,
|
||||
title: serverTitel,
|
||||
artist: (song['artist']?.toString() ?? '').trim(),
|
||||
);
|
||||
_syncedItems++;
|
||||
} else if (wahl == 'beide') {
|
||||
// Server-Kopie als eigenen lokalen Song ohne cloud_id anlegen
|
||||
final safeTitle = sanitizeDateiname(serverTitel);
|
||||
var dest = '${dir.path}/$safeTitle';
|
||||
if (await File(dest).exists()) {
|
||||
dest = '${dir.path}/$safeTitle (Server)';
|
||||
}
|
||||
if (await widget.cloud.download(sid, dest)) {
|
||||
await db.songEinfuegen(Song(
|
||||
titel: serverTitel,
|
||||
kuenstler: (song['artist']?.toString() ?? '').trim(),
|
||||
dauerSekunden: 0,
|
||||
dateiPfad: dest,
|
||||
downloadQuelle: 'cloud',
|
||||
istHeruntergeladen: true,
|
||||
));
|
||||
downloaded++;
|
||||
_syncedItems = downloaded;
|
||||
}
|
||||
}
|
||||
// 'lokal' → nichts tun (lokale Version behalten)
|
||||
}
|
||||
processed++;
|
||||
continue;
|
||||
}
|
||||
@@ -266,6 +318,12 @@ class _CloudScreenState extends State<CloudScreen> {
|
||||
_setzeStatus(
|
||||
'$downloaded Songs + ${favIds.length} Favoriten synchronisiert',
|
||||
ok: true);
|
||||
// Sync-Historie fürs Dashboard festhalten
|
||||
await _syncHistorieEintragen(
|
||||
dateien: downloaded,
|
||||
favoriten: favIds.length,
|
||||
playlists: _serverPlaylists.length,
|
||||
);
|
||||
await _ladeStatus();
|
||||
}
|
||||
MeloLogger().aktion('cloud_sync_all', {
|
||||
@@ -277,6 +335,8 @@ class _CloudScreenState extends State<CloudScreen> {
|
||||
MeloLogger().fehler('cloud_sync_all', e);
|
||||
_setzeStatus('Sync-Fehler: $e', ok: false);
|
||||
} finally {
|
||||
_syncAnimController.stop();
|
||||
_syncAnimController.value = 0;
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_syncLaeuft = false;
|
||||
@@ -286,6 +346,166 @@ class _CloudScreenState extends State<CloudScreen> {
|
||||
}
|
||||
}
|
||||
|
||||
// ─── 📜 Sync-Historie (fürs Dashboard) ───
|
||||
|
||||
static const _historieKey = 'cloud_sync_history';
|
||||
static const _historieCap = 30;
|
||||
|
||||
Future<void> _ladeSyncHistorie() async {
|
||||
final p = await SharedPreferences.getInstance();
|
||||
final roh = p.getStringList(_historieKey) ?? [];
|
||||
final eintraege = <Map<String, dynamic>>[];
|
||||
for (final s in roh) {
|
||||
try {
|
||||
final m = jsonDecode(s) as Map<String, dynamic>;
|
||||
eintraege.add(m);
|
||||
} catch (_) {
|
||||
// kaputte Einträge ignorieren
|
||||
}
|
||||
}
|
||||
if (mounted) setState(() => _syncHistorie = eintraege);
|
||||
}
|
||||
|
||||
Future<void> _syncHistorieEintragen({
|
||||
required int dateien,
|
||||
required int favoriten,
|
||||
required int playlists,
|
||||
}) async {
|
||||
final eintrag = <String, dynamic>{
|
||||
'ts': DateTime.now().toIso8601String(),
|
||||
'dateien': dateien,
|
||||
'favoriten': favoriten,
|
||||
'playlists': playlists,
|
||||
};
|
||||
final p = await SharedPreferences.getInstance();
|
||||
final liste = p.getStringList(_historieKey) ?? [];
|
||||
liste.add(jsonEncode(eintrag));
|
||||
while (liste.length > _historieCap) {
|
||||
liste.removeAt(0);
|
||||
}
|
||||
await p.setStringList(_historieKey, liste);
|
||||
if (mounted) {
|
||||
setState(() => _syncHistorie = [
|
||||
..._syncHistorie,
|
||||
eintrag,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/// Einträge von heute zusammenfassen: „heute 14 Dateien, 2 Favoriten, 1 Playlist“
|
||||
String get _syncHistorieHeuteText {
|
||||
final heute = DateTime.now();
|
||||
int dateien = 0;
|
||||
int favoriten = 0;
|
||||
int playlists = 0;
|
||||
for (final e in _syncHistorie) {
|
||||
final ts = DateTime.tryParse(e['ts']?.toString() ?? '');
|
||||
if (ts == null) continue;
|
||||
if (ts.year == heute.year &&
|
||||
ts.month == heute.month &&
|
||||
ts.day == heute.day) {
|
||||
dateien += (e['dateien'] as num?)?.toInt() ?? 0;
|
||||
favoriten += (e['favoriten'] as num?)?.toInt() ?? 0;
|
||||
playlists += (e['playlists'] as num?)?.toInt() ?? 0;
|
||||
}
|
||||
}
|
||||
if (dateien == 0 && favoriten == 0 && playlists == 0) {
|
||||
return 'Noch keine Syncs heute';
|
||||
}
|
||||
final teile = <String>[
|
||||
if (dateien > 0) '$dateien Dateien',
|
||||
if (favoriten > 0) '$favoriten Favoriten',
|
||||
if (playlists > 0) '$playlists Playlist${playlists != 1 ? 'en' : ''}',
|
||||
];
|
||||
return 'Heute: ${teile.join(', ')}';
|
||||
}
|
||||
|
||||
/// Konflikt-Dialog: Titel lokal ≠ Server. NUR wenn ein Konflikt existiert.
|
||||
/// Rückgabe: 'lokal' | 'server' | 'beide' | null (abgebrochen)
|
||||
Future<String?> _konfliktDialog(Song lokal, Map serverSong) async {
|
||||
final serverTitel = (serverSong['title'] ?? '?').toString();
|
||||
final serverKuenstler = (serverSong['artist'] ?? '?').toString();
|
||||
if (!mounted) return null;
|
||||
return showDialog<String>(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (ctx) => AlertDialog(
|
||||
backgroundColor: MeloTheme.dunkel1,
|
||||
title: const Text('⚠️ Konflikt',
|
||||
style: TextStyle(color: Colors.white, fontSize: 17)),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Titel weichen voneinander ab:',
|
||||
style: TextStyle(color: MeloTheme.textSekundaer, fontSize: 12),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: MeloTheme.dunkel2,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text('📱 Lokal',
|
||||
style: TextStyle(color: MeloTheme.textSekundaer, fontSize: 11)),
|
||||
const SizedBox(height: 2),
|
||||
Text(lokal.titel,
|
||||
style: const TextStyle(color: Colors.white, fontSize: 13)),
|
||||
Text(lokal.kuenstler,
|
||||
style: const TextStyle(color: MeloTheme.textSekundaer, fontSize: 11)),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: MeloTheme.dunkel2,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text('☁️ Server',
|
||||
style: TextStyle(color: MeloTheme.textSekundaer, fontSize: 11)),
|
||||
const SizedBox(height: 2),
|
||||
Text(serverTitel,
|
||||
style: const TextStyle(color: Colors.white, fontSize: 13)),
|
||||
Text(serverKuenstler,
|
||||
style: const TextStyle(color: MeloTheme.textSekundaer, fontSize: 11)),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(ctx, 'lokal'),
|
||||
child: const Text('Lokale behalten',
|
||||
style: TextStyle(color: MeloTheme.textSekundaer)),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(ctx, 'beide'),
|
||||
child: const Text('Beide behalten',
|
||||
style: TextStyle(color: MeloTheme.textSekundaer)),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(ctx, 'server'),
|
||||
child: const Text('Server übernehmen',
|
||||
style: TextStyle(color: MeloTheme.rot)),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _updateSync(String phase, double progress) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
@@ -521,6 +741,7 @@ class _CloudScreenState extends State<CloudScreen> {
|
||||
final syncSt = await widget.cloud.syncStatus();
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_serverStatusDaten = st;
|
||||
_serverCount = st?['total'] ?? 0;
|
||||
|
||||
if (syncSt != null) {
|
||||
@@ -698,12 +919,34 @@ class _CloudScreenState extends State<CloudScreen> {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 80,
|
||||
height: 80,
|
||||
child: CircularProgressIndicator(
|
||||
value: _syncFortschritt > 0 ? _syncFortschritt : null,
|
||||
color: MeloTheme.rot,
|
||||
strokeWidth: 4,
|
||||
width: 90,
|
||||
height: 90,
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 90,
|
||||
height: 90,
|
||||
child: CircularProgressIndicator(
|
||||
value: _syncFortschritt > 0 ? _syncFortschritt : null,
|
||||
color: MeloTheme.rot,
|
||||
strokeWidth: 4,
|
||||
),
|
||||
),
|
||||
// Rotierende ☁️-Animation während des Syncs
|
||||
RotationTransition(
|
||||
turns: _syncAnimController,
|
||||
child: Container(
|
||||
width: 44,
|
||||
height: 44,
|
||||
decoration: BoxDecoration(
|
||||
color: MeloTheme.dunkel1,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Icon(Icons.cloud_sync, color: MeloTheme.rot, size: 26),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
@@ -1135,35 +1378,179 @@ class _CloudScreenState extends State<CloudScreen> {
|
||||
border: Border.all(color: MeloTheme.dunkel2),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_syncInfoZeile('Songs auf Server', '$_serverCount'),
|
||||
_syncInfoZeile('Server-Favoriten', '$_favServerCount'),
|
||||
_syncInfoZeile('Server-Playlisten', '$_playlistServerCount'),
|
||||
_syncInfoZeile('Sync-Modus', _autoSync ? 'Automatisch (${_syncIntervall}h)' : 'Manuell'),
|
||||
// ── Statistik-Karten 2×2: Songs · Playlisten · Favoriten · Letzter Sync ──
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _statistikKarte(
|
||||
icon: Icons.library_music,
|
||||
label: 'Songs',
|
||||
wert: '$_serverCount',
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: _statistikKarte(
|
||||
icon: Icons.playlist_play,
|
||||
label: 'Playlisten',
|
||||
wert: '$_playlistServerCount',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _statistikKarte(
|
||||
icon: Icons.favorite,
|
||||
label: 'Favoriten',
|
||||
wert: '$_favServerCount',
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: _statistikKarte(
|
||||
icon: Icons.history,
|
||||
label: 'Letzter Sync',
|
||||
wert: _letzterSync,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
const Divider(color: MeloTheme.dunkel2, height: 1),
|
||||
const SizedBox(height: 12),
|
||||
// ── Speicheranzeige (Balken, falls Server Daten liefert) ──
|
||||
_speicherZeile(),
|
||||
const SizedBox(height: 12),
|
||||
// ── Sync-Historie (heute) ──
|
||||
_syncHistorieZeile(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _syncInfoZeile(String label, String value) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 5),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
Widget _statistikKarte({
|
||||
required IconData icon,
|
||||
required String label,
|
||||
required String wert,
|
||||
}) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 14, horizontal: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: MeloTheme.dunkel2,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(label,
|
||||
style: const TextStyle(
|
||||
color: MeloTheme.textSekundaer, fontSize: 13)),
|
||||
Text(value,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w500)),
|
||||
Icon(icon, color: MeloTheme.rot, size: 16),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
wert,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
label,
|
||||
style: const TextStyle(
|
||||
color: MeloTheme.textSekundaer, fontSize: 11),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Speicheranzeige mit Balken. Liefert der Server keine Storage-Daten
|
||||
/// (Felder `storage_used`/`storage_total` o.ä.), erscheint Platzhalter „–".
|
||||
Widget _speicherZeile() {
|
||||
final st = _serverStatusDaten;
|
||||
num? used;
|
||||
num? total;
|
||||
if (st != null) {
|
||||
used = (st['storage_used'] ?? st['bytes_used'] ?? st['used_bytes']) as num?;
|
||||
total = (st['storage_total'] ?? st['bytes_total'] ?? st['total_bytes'])
|
||||
as num?;
|
||||
}
|
||||
final hatDaten = used != null && total != null && total > 0;
|
||||
final anteil = hatDaten ? (used / total).clamp(0.0, 1.0) : 0.0;
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.storage,
|
||||
color: MeloTheme.textSekundaer, size: 16),
|
||||
const SizedBox(width: 8),
|
||||
const Text('Speicher',
|
||||
style: TextStyle(
|
||||
color: MeloTheme.textSekundaer, fontSize: 13)),
|
||||
const Spacer(),
|
||||
Text(
|
||||
hatDaten
|
||||
? '${_formatBytes(used)} / ${_formatBytes(total)}'
|
||||
: '–',
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
child: LinearProgressIndicator(
|
||||
value: hatDaten ? anteil.toDouble() : 0,
|
||||
minHeight: 6,
|
||||
backgroundColor: MeloTheme.dunkel2,
|
||||
valueColor: const AlwaysStoppedAnimation<Color>(MeloTheme.rot),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
String _formatBytes(num bytes) {
|
||||
if (bytes <= 0) return '0 B';
|
||||
const einheiten = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
var wert = bytes.toDouble();
|
||||
var i = 0;
|
||||
while (wert >= 1024 && i < einheiten.length - 1) {
|
||||
wert /= 1024;
|
||||
i++;
|
||||
}
|
||||
return '${wert.toStringAsFixed(wert >= 100 ? 0 : 1)} ${einheiten[i]}';
|
||||
}
|
||||
|
||||
/// Sync-Historie-Zeile: „Heute: 14 Dateien, 2 Favoriten, 1 Playlist“
|
||||
Widget _syncHistorieZeile() {
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Icon(Icons.event_note,
|
||||
color: MeloTheme.textSekundaer, size: 16),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
_syncHistorieHeuteText,
|
||||
style: const TextStyle(color: Colors.white70, fontSize: 12),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _playlistSektion() {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
|
||||
Reference in New Issue
Block a user