v2.47 — Cloud-Verbindung beim App-Start + Live-Status-Anzeige (Verbinde→Verbunden)

## CloudService
- CloudStatus-Enum (verbinde/verbunden/fehler), CloudService extends ChangeNotifier
- verbinde(): Login+Status beim App-Start, setzt Status, notifyListeners
- Getter status/statusText/istVerbunden/verbindungGestartet
- Bestehende Methoden behalten — nur status() → statusDaten() umbenannt (Namenskonflikt mit neuem status-Getter)

## Home-Screen
- _cloud.verbinde() in initState (feuern-und-vergessen) — Verbindung steht beim App-Start, nicht erst beim Tab-Öffnen

## Cloud-Screen
- initState verbindet nicht mehr selbst — zeigt Service-Status via ListenableBuilder
- Status wechselt live: 'Verbinde...' (gelb) → 'Verbunden' (grün) / 'Keine Verbindung' (rot)
- Serverdaten (Playlists/Favoriten/Zähler) laden automatisch, sobald Service verbunden
- Refresh-Button re-verbindet + lädt Zähler neu
- Fallback: ungestarteter Service (Einstellungen-Screen) wird beim Öffnen gestartet
This commit is contained in:
Dustin
2026-08-03 03:08:45 +02:00
parent 20ef788178
commit 319b765d08
4 changed files with 169 additions and 83 deletions
+1 -1
View File
@@ -151,7 +151,7 @@ Future<void> _starteAutoSyncFallsNoetig() async {
// Sync ausführen // Sync ausführen
MeloLogger().aktion('auto_sync_start', {}); MeloLogger().aktion('auto_sync_start', {});
final st = await cloud.status(); final st = await cloud.statusDaten();
if (st != null) { if (st != null) {
final serverSongs = await cloud.listSongs(); final serverSongs = await cloud.listSongs();
MeloLogger().aktion('auto_sync_done', MeloLogger().aktion('auto_sync_done',
+109 -80
View File
@@ -7,7 +7,6 @@ import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import '../utils/farb_theme.dart'; import '../utils/farb_theme.dart';
import '../utils/sanitize.dart'; import '../utils/sanitize.dart';
import '../services/cloud_service.dart'; import '../services/cloud_service.dart';
import '../services/auth_service.dart';
import '../database/db_helper.dart'; import '../database/db_helper.dart';
import '../services/melo_logger.dart'; import '../services/melo_logger.dart';
import '../main.dart'; // notificationsPlugin import '../main.dart'; // notificationsPlugin
@@ -31,7 +30,7 @@ class _CloudScreenState extends State<CloudScreen> {
bool _ladt = false; bool _ladt = false;
String? _status; String? _status;
bool _statusOk = false; bool _statusOk = false;
bool _verbunden = false; bool _serverDatenGeladen = false;
// ─── Sync-Einstellungen ─── // ─── Sync-Einstellungen ───
bool _autoSync = true; bool _autoSync = true;
@@ -66,15 +65,24 @@ class _CloudScreenState extends State<CloudScreen> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_status = 'Verbinde...'; // Verbindung stellt der CloudService beim App-Start her (MeloHome.initState).
_verbindeCloud(); // Hier nur Settings laden + Serverdaten, sobald der Service verbunden ist.
_ladeStatus();
_ladeSettings(); _ladeSettings();
_ladeServerDaten(); if (widget.cloud.istVerbunden) {
_serverDatenGeladen = true;
_ladeServerDaten();
_ladeStatus();
} else if (!widget.cloud.verbindungGestartet) {
// Fallback: Service wurde noch nicht gestartet (z.B. eigener Service
// aus den Einstellungen) — Verbindung hier anstoßen.
widget.cloud.verbinde();
}
widget.cloud.addListener(_onCloudStatus);
} }
@override @override
void dispose() { void dispose() {
widget.cloud.removeListener(_onCloudStatus);
_syncTimer?.cancel(); _syncTimer?.cancel();
_renameTitleCtrl.dispose(); _renameTitleCtrl.dispose();
_renameArtistCtrl.dispose(); _renameArtistCtrl.dispose();
@@ -83,16 +91,14 @@ class _CloudScreenState extends State<CloudScreen> {
// ─── Verbindung ─── // ─── Verbindung ───
Future<void> _verbindeCloud() async { /// Reagiert auf Status-Änderungen des CloudService: sobald verbunden,
final user = AuthService().benutzer; /// werden Serverdaten + Zähler nachgeladen.
if (user.isNotEmpty) { void _onCloudStatus() {
final ok = await widget.cloud.login(user); if (!mounted) return;
if (mounted) { if (widget.cloud.istVerbunden && !_serverDatenGeladen) {
setState(() { _serverDatenGeladen = true;
_verbunden = ok; _ladeServerDaten();
if (!ok) _setzeStatus('Cloud-Login fehlgeschlagen', ok: false); _ladeStatus();
});
}
} }
} }
@@ -115,7 +121,7 @@ class _CloudScreenState extends State<CloudScreen> {
} }
Future<void> _ladeServerDaten() async { Future<void> _ladeServerDaten() async {
if (!_verbunden) return; if (!widget.cloud.istVerbunden) return;
// Paralleles Laden // Paralleles Laden
await Future.wait([ await Future.wait([
_ladePlaylists(), _ladePlaylists(),
@@ -511,14 +517,11 @@ class _CloudScreenState extends State<CloudScreen> {
// ─── Status ─── // ─── Status ───
Future<void> _ladeStatus() async { Future<void> _ladeStatus() async {
final st = await widget.cloud.status(); final st = await widget.cloud.statusDaten();
final syncSt = await widget.cloud.syncStatus(); final syncSt = await widget.cloud.syncStatus();
if (!mounted) return; if (!mounted) return;
setState(() { setState(() {
_serverCount = st?['total'] ?? 0; _serverCount = st?['total'] ?? 0;
_verbunden = st != null;
_statusOk = st != null;
_status = st != null ? 'Verbunden' : 'Keine Verbindung (Token?)';
if (syncSt != null) { if (syncSt != null) {
final counts = syncSt['counts']; final counts = syncSt['counts'];
@@ -672,13 +675,20 @@ class _CloudScreenState extends State<CloudScreen> {
IconButton( IconButton(
icon: const Icon(Icons.refresh, color: Colors.grey, size: 20), icon: const Icon(Icons.refresh, color: Colors.grey, size: 20),
onPressed: () async { onPressed: () async {
await widget.cloud.verbinde();
await _ladeStatus(); await _ladeStatus();
await _ladeServerDaten(); await _ladeServerDaten();
}, },
), ),
], ],
), ),
body: _syncLaeuft ? _syncFortschrittWidget() : _contentWidget(), // ListenableBuilder: Status kommt live vom CloudService
// ('Verbinde...' → 'Verbunden'/'Keine Verbindung')
body: ListenableBuilder(
listenable: widget.cloud,
builder: (context, _) =>
_syncLaeuft ? _syncFortschrittWidget() : _contentWidget(),
),
); );
} }
@@ -774,47 +784,64 @@ class _CloudScreenState extends State<CloudScreen> {
child: CircularProgressIndicator(color: MeloTheme.rot)), child: CircularProgressIndicator(color: MeloTheme.rot)),
), ),
// Status-Text // Status-Text: transiente Meldungen (Upload-Fortschritt, Sync-Ergebnis)
if (_status != null && !_ladt) // haben Vorrang — sonst kommt der Verbindungsstatus live vom Service
Padding( // ('Verbinde...' → 'Verbunden' / 'Keine Verbindung')
padding: const EdgeInsets.only(top: 12), if (!_ladt) ...[
child: Container( Builder(builder: (context) {
width: double.infinity, final hatTransientStatus = _status != null;
padding: final verbindet = widget.cloud.status == CloudStatus.verbinde;
const EdgeInsets.symmetric(horizontal: 14, vertical: 10), final statusOk =
decoration: BoxDecoration( hatTransientStatus ? _statusOk : widget.cloud.istVerbunden;
color: _statusOk return Padding(
? const Color(0xFF0D3B1E) padding: const EdgeInsets.only(top: 12),
: const Color(0xFF3B0D0D), child: Container(
borderRadius: BorderRadius.circular(10), width: double.infinity,
), padding:
child: Row( const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
children: [ decoration: BoxDecoration(
Icon( color: statusOk
_statusOk ? const Color(0xFF0D3B1E)
? Icons.check_circle : verbindet
: Icons.info_outline, ? const Color(0xFF3B2A0D)
size: 16, : const Color(0xFF3B0D0D),
color: _statusOk borderRadius: BorderRadius.circular(10),
? const Color(0xFF4CAF50) ),
: const Color(0xFFEF5350), child: Row(
), children: [
const SizedBox(width: 8), Icon(
Expanded( statusOk
child: Text( ? Icons.check_circle
_status!, : verbindet
style: TextStyle( ? Icons.sync
color: _statusOk : Icons.info_outline,
? const Color(0xFFA5D6A7) size: 16,
: const Color(0xFFEF9A9A), color: statusOk
fontSize: 13, ? const Color(0xFF4CAF50)
: verbindet
? const Color(0xFFFFA726)
: const Color(0xFFEF5350),
),
const SizedBox(width: 8),
Expanded(
child: Text(
_status ?? widget.cloud.statusText,
style: TextStyle(
color: statusOk
? const Color(0xFFA5D6A7)
: verbindet
? const Color(0xFFFFCC80)
: const Color(0xFFEF9A9A),
fontSize: 13,
),
), ),
), ),
), ],
], ),
), ),
), );
), }),
],
const SizedBox(height: 24), const SizedBox(height: 24),
@@ -947,26 +974,28 @@ class _CloudScreenState extends State<CloudScreen> {
], ],
), ),
), ),
// Verbindungsstatus-Indikator // Verbindungsstatus-Indikator (live vom CloudService)
Container( Builder(builder: (context) {
width: 12, final farbe = widget.cloud.istVerbunden
height: 12, ? const Color(0xFF4CAF50)
decoration: BoxDecoration( : widget.cloud.status == CloudStatus.verbinde
shape: BoxShape.circle, ? const Color(0xFFFFA726)
color: _verbunden : const Color(0xFFEF5350);
? const Color(0xFF4CAF50) return Container(
: const Color(0xFFEF5350), width: 12,
boxShadow: [ height: 12,
BoxShadow( decoration: BoxDecoration(
color: (_verbunden shape: BoxShape.circle,
? const Color(0xFF4CAF50) color: farbe,
: const Color(0xFFEF5350)) boxShadow: [
.withValues(alpha: 0.5), BoxShadow(
blurRadius: 8, color: farbe.withValues(alpha: 0.5),
), blurRadius: 8,
], ),
), ],
), ),
);
}),
], ],
), ),
); );
+4
View File
@@ -34,6 +34,10 @@ class _MeloHomeState extends State<MeloHome> {
void initState() { void initState() {
super.initState(); super.initState();
_vm.ladeSongs(); _vm.ladeSongs();
// Cloud-Verbindung beim App-Start herstellen (feuern-und-vergessen).
// Der CloudService wechselt 'Verbinde...' → 'Verbunden' und benachrichtigt
// Listener — der CloudScreen zeigt den Status direkt vom Service an.
_cloud.verbinde();
} }
@override @override
+55 -2
View File
@@ -1,10 +1,14 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import '../config/app_config.dart'; import '../config/app_config.dart';
import '../services/auth_service.dart'; import '../services/auth_service.dart';
import '../services/melo_logger.dart'; import '../services/melo_logger.dart';
/// Verbindungsstatus der Cloud
enum CloudStatus { verbinde, verbunden, fehler }
/// Cloud-Sync Service für Melo Registry (v3 — vollständiges Sync-System) /// Cloud-Sync Service für Melo Registry (v3 — vollständiges Sync-System)
/// Authentifizierung via Baka-Auth JWT-Token /// Authentifizierung via Baka-Auth JWT-Token
/// ///
@@ -16,11 +20,59 @@ import '../services/melo_logger.dart';
/// 3. Lokale Änderungen werden danach per `syncAll()` hochgeladen. /// 3. Lokale Änderungen werden danach per `syncAll()` hochgeladen.
/// Merge-Strategie wird NICHT unterstützt (kein 3-Way-Merge) — es gewinnt immer /// Merge-Strategie wird NICHT unterstützt (kein 3-Way-Merge) — es gewinnt immer
/// der jüngste Timestamp. /// der jüngste Timestamp.
class CloudService { class CloudService extends ChangeNotifier {
static final http.Client _client = http.Client(); static final http.Client _client = http.Client();
static String get _base => AppConfig.cloudUrl; static String get _base => AppConfig.cloudUrl;
CloudStatus _status = CloudStatus.verbinde;
bool _verbindungGestartet = false;
/// Aktueller Verbindungsstatus der Cloud
CloudStatus get status => _status;
/// Ob die Cloud-Verbindung steht
bool get istVerbunden => _status == CloudStatus.verbunden;
/// Ob bereits ein Verbindungsversuch gestartet wurde (auch wenn er läuft)
bool get verbindungGestartet => _verbindungGestartet;
/// Anzeigetext für den Verbindungsstatus
String get statusText {
switch (_status) {
case CloudStatus.verbinde:
return 'Verbinde...';
case CloudStatus.verbunden:
return 'Verbunden';
case CloudStatus.fehler:
return 'Keine Verbindung';
}
}
/// Stellt die Cloud-Verbindung her (Login + Status-Check) und benachrichtigt
/// Listener über den neuen Status. Wird beim App-Start aufgerufen
/// (feuern-und-vergessen — blockiert nicht).
Future<void> verbinde() async {
_verbindungGestartet = true;
final user = AuthService().benutzer;
_status = CloudStatus.verbinde;
notifyListeners();
if (user.isEmpty) {
_status = CloudStatus.fehler;
notifyListeners();
return;
}
final ok = await login(user);
if (!ok) {
_status = CloudStatus.fehler;
notifyListeners();
return;
}
final st = await statusDaten();
_status = st != null ? CloudStatus.verbunden : CloudStatus.fehler;
notifyListeners();
}
/// Login mit Baka-Auth Token wird aus AuthService bezogen /// Login mit Baka-Auth Token wird aus AuthService bezogen
Future<bool> login(String user) async { Future<bool> login(String user) async {
try { try {
@@ -48,7 +100,8 @@ class CloudService {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}; };
Future<Map?> status() => _get('/api/v1/cloud/status'); /// Server-Status abrufen (Song-Zähler etc.)
Future<Map?> statusDaten() => _get('/api/v1/cloud/status');
Future<Map?> syncStatus() => _get('/api/v1/cloud/sync-status'); Future<Map?> syncStatus() => _get('/api/v1/cloud/sync-status');