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:
+1
-1
@@ -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',
|
||||||
|
|||||||
@@ -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();
|
||||||
|
if (widget.cloud.istVerbunden) {
|
||||||
|
_serverDatenGeladen = true;
|
||||||
_ladeServerDaten();
|
_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,38 +784,53 @@ 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')
|
||||||
|
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),
|
padding: const EdgeInsets.only(top: 12),
|
||||||
child: Container(
|
child: Container(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
padding:
|
padding:
|
||||||
const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
|
const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: _statusOk
|
color: statusOk
|
||||||
? const Color(0xFF0D3B1E)
|
? const Color(0xFF0D3B1E)
|
||||||
|
: verbindet
|
||||||
|
? const Color(0xFF3B2A0D)
|
||||||
: const Color(0xFF3B0D0D),
|
: const Color(0xFF3B0D0D),
|
||||||
borderRadius: BorderRadius.circular(10),
|
borderRadius: BorderRadius.circular(10),
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Icon(
|
Icon(
|
||||||
_statusOk
|
statusOk
|
||||||
? Icons.check_circle
|
? Icons.check_circle
|
||||||
|
: verbindet
|
||||||
|
? Icons.sync
|
||||||
: Icons.info_outline,
|
: Icons.info_outline,
|
||||||
size: 16,
|
size: 16,
|
||||||
color: _statusOk
|
color: statusOk
|
||||||
? const Color(0xFF4CAF50)
|
? const Color(0xFF4CAF50)
|
||||||
|
: verbindet
|
||||||
|
? const Color(0xFFFFA726)
|
||||||
: const Color(0xFFEF5350),
|
: const Color(0xFFEF5350),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
_status!,
|
_status ?? widget.cloud.statusText,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: _statusOk
|
color: statusOk
|
||||||
? const Color(0xFFA5D6A7)
|
? const Color(0xFFA5D6A7)
|
||||||
|
: verbindet
|
||||||
|
? const Color(0xFFFFCC80)
|
||||||
: const Color(0xFFEF9A9A),
|
: const Color(0xFFEF9A9A),
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
),
|
),
|
||||||
@@ -814,7 +839,9 @@ class _CloudScreenState extends State<CloudScreen> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
|
||||||
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) {
|
||||||
|
final farbe = widget.cloud.istVerbunden
|
||||||
|
? const Color(0xFF4CAF50)
|
||||||
|
: widget.cloud.status == CloudStatus.verbinde
|
||||||
|
? const Color(0xFFFFA726)
|
||||||
|
: const Color(0xFFEF5350);
|
||||||
|
return Container(
|
||||||
width: 12,
|
width: 12,
|
||||||
height: 12,
|
height: 12,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
shape: BoxShape.circle,
|
shape: BoxShape.circle,
|
||||||
color: _verbunden
|
color: farbe,
|
||||||
? const Color(0xFF4CAF50)
|
|
||||||
: const Color(0xFFEF5350),
|
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: (_verbunden
|
color: farbe.withValues(alpha: 0.5),
|
||||||
? const Color(0xFF4CAF50)
|
|
||||||
: const Color(0xFFEF5350))
|
|
||||||
.withValues(alpha: 0.5),
|
|
||||||
blurRadius: 8,
|
blurRadius: 8,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
|
}),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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');
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user