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
MeloLogger().aktion('auto_sync_start', {});
final st = await cloud.status();
final st = await cloud.statusDaten();
if (st != null) {
final serverSongs = await cloud.listSongs();
MeloLogger().aktion('auto_sync_done',
+69 -40
View File
@@ -7,7 +7,6 @@ import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import '../utils/farb_theme.dart';
import '../utils/sanitize.dart';
import '../services/cloud_service.dart';
import '../services/auth_service.dart';
import '../database/db_helper.dart';
import '../services/melo_logger.dart';
import '../main.dart'; // notificationsPlugin
@@ -31,7 +30,7 @@ class _CloudScreenState extends State<CloudScreen> {
bool _ladt = false;
String? _status;
bool _statusOk = false;
bool _verbunden = false;
bool _serverDatenGeladen = false;
// ─── Sync-Einstellungen ───
bool _autoSync = true;
@@ -66,15 +65,24 @@ class _CloudScreenState extends State<CloudScreen> {
@override
void initState() {
super.initState();
_status = 'Verbinde...';
_verbindeCloud();
_ladeStatus();
// Verbindung stellt der CloudService beim App-Start her (MeloHome.initState).
// Hier nur Settings laden + Serverdaten, sobald der Service verbunden ist.
_ladeSettings();
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
void dispose() {
widget.cloud.removeListener(_onCloudStatus);
_syncTimer?.cancel();
_renameTitleCtrl.dispose();
_renameArtistCtrl.dispose();
@@ -83,16 +91,14 @@ class _CloudScreenState extends State<CloudScreen> {
// ─── Verbindung ───
Future<void> _verbindeCloud() async {
final user = AuthService().benutzer;
if (user.isNotEmpty) {
final ok = await widget.cloud.login(user);
if (mounted) {
setState(() {
_verbunden = ok;
if (!ok) _setzeStatus('Cloud-Login fehlgeschlagen', ok: false);
});
}
/// Reagiert auf Status-Änderungen des CloudService: sobald verbunden,
/// werden Serverdaten + Zähler nachgeladen.
void _onCloudStatus() {
if (!mounted) return;
if (widget.cloud.istVerbunden && !_serverDatenGeladen) {
_serverDatenGeladen = true;
_ladeServerDaten();
_ladeStatus();
}
}
@@ -115,7 +121,7 @@ class _CloudScreenState extends State<CloudScreen> {
}
Future<void> _ladeServerDaten() async {
if (!_verbunden) return;
if (!widget.cloud.istVerbunden) return;
// Paralleles Laden
await Future.wait([
_ladePlaylists(),
@@ -511,14 +517,11 @@ class _CloudScreenState extends State<CloudScreen> {
// ─── Status ───
Future<void> _ladeStatus() async {
final st = await widget.cloud.status();
final st = await widget.cloud.statusDaten();
final syncSt = await widget.cloud.syncStatus();
if (!mounted) return;
setState(() {
_serverCount = st?['total'] ?? 0;
_verbunden = st != null;
_statusOk = st != null;
_status = st != null ? 'Verbunden' : 'Keine Verbindung (Token?)';
if (syncSt != null) {
final counts = syncSt['counts'];
@@ -672,13 +675,20 @@ class _CloudScreenState extends State<CloudScreen> {
IconButton(
icon: const Icon(Icons.refresh, color: Colors.grey, size: 20),
onPressed: () async {
await widget.cloud.verbinde();
await _ladeStatus();
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,38 +784,53 @@ class _CloudScreenState extends State<CloudScreen> {
child: CircularProgressIndicator(color: MeloTheme.rot)),
),
// Status-Text
if (_status != null && !_ladt)
Padding(
// Status-Text: transiente Meldungen (Upload-Fortschritt, Sync-Ergebnis)
// haben Vorrang — sonst kommt der Verbindungsstatus live vom Service
// ('Verbinde...' → 'Verbunden' / 'Keine Verbindung')
if (!_ladt) ...[
Builder(builder: (context) {
final hatTransientStatus = _status != null;
final verbindet = widget.cloud.status == CloudStatus.verbinde;
final statusOk =
hatTransientStatus ? _statusOk : widget.cloud.istVerbunden;
return Padding(
padding: const EdgeInsets.only(top: 12),
child: Container(
width: double.infinity,
padding:
const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
decoration: BoxDecoration(
color: _statusOk
color: statusOk
? const Color(0xFF0D3B1E)
: verbindet
? const Color(0xFF3B2A0D)
: const Color(0xFF3B0D0D),
borderRadius: BorderRadius.circular(10),
),
child: Row(
children: [
Icon(
_statusOk
statusOk
? Icons.check_circle
: verbindet
? Icons.sync
: Icons.info_outline,
size: 16,
color: _statusOk
color: statusOk
? const Color(0xFF4CAF50)
: verbindet
? const Color(0xFFFFA726)
: const Color(0xFFEF5350),
),
const SizedBox(width: 8),
Expanded(
child: Text(
_status!,
_status ?? widget.cloud.statusText,
style: TextStyle(
color: _statusOk
color: statusOk
? const Color(0xFFA5D6A7)
: verbindet
? const Color(0xFFFFCC80)
: const Color(0xFFEF9A9A),
fontSize: 13,
),
@@ -814,7 +839,9 @@ class _CloudScreenState extends State<CloudScreen> {
],
),
),
),
);
}),
],
const SizedBox(height: 24),
@@ -947,26 +974,28 @@ class _CloudScreenState extends State<CloudScreen> {
],
),
),
// Verbindungsstatus-Indikator
Container(
// Verbindungsstatus-Indikator (live vom CloudService)
Builder(builder: (context) {
final farbe = widget.cloud.istVerbunden
? const Color(0xFF4CAF50)
: widget.cloud.status == CloudStatus.verbinde
? const Color(0xFFFFA726)
: const Color(0xFFEF5350);
return Container(
width: 12,
height: 12,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: _verbunden
? const Color(0xFF4CAF50)
: const Color(0xFFEF5350),
color: farbe,
boxShadow: [
BoxShadow(
color: (_verbunden
? const Color(0xFF4CAF50)
: const Color(0xFFEF5350))
.withValues(alpha: 0.5),
color: farbe.withValues(alpha: 0.5),
blurRadius: 8,
),
],
),
),
);
}),
],
),
);
+4
View File
@@ -34,6 +34,10 @@ class _MeloHomeState extends State<MeloHome> {
void initState() {
super.initState();
_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
+55 -2
View File
@@ -1,10 +1,14 @@
import 'dart:convert';
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:http/http.dart' as http;
import '../config/app_config.dart';
import '../services/auth_service.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)
/// Authentifizierung via Baka-Auth JWT-Token
///
@@ -16,11 +20,59 @@ import '../services/melo_logger.dart';
/// 3. Lokale Änderungen werden danach per `syncAll()` hochgeladen.
/// Merge-Strategie wird NICHT unterstützt (kein 3-Way-Merge) — es gewinnt immer
/// der jüngste Timestamp.
class CloudService {
class CloudService extends ChangeNotifier {
static final http.Client _client = http.Client();
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
Future<bool> login(String user) async {
try {
@@ -48,7 +100,8 @@ class CloudService {
'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');