diff --git a/lib/main.dart b/lib/main.dart index 3c403ff..febe238 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -151,7 +151,7 @@ Future _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', diff --git a/lib/screens/cloud_screen.dart b/lib/screens/cloud_screen.dart index 29193c5..a0d41a9 100644 --- a/lib/screens/cloud_screen.dart +++ b/lib/screens/cloud_screen.dart @@ -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 { 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 { @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(); - _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 void dispose() { + widget.cloud.removeListener(_onCloudStatus); _syncTimer?.cancel(); _renameTitleCtrl.dispose(); _renameArtistCtrl.dispose(); @@ -83,16 +91,14 @@ class _CloudScreenState extends State { // ─── Verbindung ─── - Future _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 { } Future _ladeServerDaten() async { - if (!_verbunden) return; + if (!widget.cloud.istVerbunden) return; // Paralleles Laden await Future.wait([ _ladePlaylists(), @@ -511,14 +517,11 @@ class _CloudScreenState extends State { // ─── Status ─── Future _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 { 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,47 +784,64 @@ class _CloudScreenState extends State { 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, + // 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 + ? const Color(0xFF0D3B1E) + : verbindet + ? const Color(0xFF3B2A0D) + : const Color(0xFF3B0D0D), + borderRadius: BorderRadius.circular(10), + ), + child: Row( + children: [ + Icon( + statusOk + ? Icons.check_circle + : verbindet + ? Icons.sync + : Icons.info_outline, + size: 16, + color: statusOk + ? 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), @@ -947,26 +974,28 @@ class _CloudScreenState extends State { ], ), ), - // Verbindungsstatus-Indikator - Container( - width: 12, - height: 12, - decoration: BoxDecoration( - shape: BoxShape.circle, - color: _verbunden - ? const Color(0xFF4CAF50) - : const Color(0xFFEF5350), - boxShadow: [ - BoxShadow( - color: (_verbunden - ? const Color(0xFF4CAF50) - : const Color(0xFFEF5350)) - .withValues(alpha: 0.5), - blurRadius: 8, - ), - ], - ), - ), + // 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: farbe, + boxShadow: [ + BoxShadow( + color: farbe.withValues(alpha: 0.5), + blurRadius: 8, + ), + ], + ), + ); + }), ], ), ); diff --git a/lib/screens/home_screen.dart b/lib/screens/home_screen.dart index 81b2bfa..90333f5 100644 --- a/lib/screens/home_screen.dart +++ b/lib/screens/home_screen.dart @@ -34,6 +34,10 @@ class _MeloHomeState extends State { 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 diff --git a/lib/services/cloud_service.dart b/lib/services/cloud_service.dart index d98967f..b25b29a 100644 --- a/lib/services/cloud_service.dart +++ b/lib/services/cloud_service.dart @@ -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 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 login(String user) async { try { @@ -48,7 +100,8 @@ class CloudService { 'Content-Type': 'application/json', }; - Future status() => _get('/api/v1/cloud/status'); + /// Server-Status abrufen (Song-Zähler etc.) + Future statusDaten() => _get('/api/v1/cloud/status'); Future syncStatus() => _get('/api/v1/cloud/sync-status');