v2.52.3 — F3-Rest: ☁️-Chip-Puls + Auto-Sync-Timer app-weit

## Chip-Puls (home_screen)
- ListenableBuilder hört auf Listenable.merge([_vm, _cloud, SyncService.laeuftNotifier]) — der ☁️-Musikserver-Chip pulsiert jetzt bei laufendem Sync (auch im Hintergrund/Auto-Sync), nicht nur beim Verbinden/Laden
- serverSyncLaeuft = SyncService.laeuftNotifier.value || CloudStatus.verbinde || serverLadt

## Auto-Sync-Timer app-weit (main.dart)
- _starteAutoSyncFallsNoetig() entfernt (F3a: kein Sync beim App-Start); stattdessen SyncService.starteAutoSyncTimer() — Timer tickt erst nach dem Intervall, Fälligkeit via istSyncFaellig
- shared_preferences-Import aus main.dart entfernt (Intervall-Logik lebt in SyncService/CloudScreen)

## Sync-Notification
- Sync-Kanal de.baka.melo.sync wird in main() angelegt (Android 8+: Channel nötig, sonst erscheint die Notification nicht)
- onDidReceiveNotificationResponse: Tipp auf 'sync_fertig'/'sync_fehler' → SyncService.syncBenachrichtigungGetippt++ → MeloHome öffnet den Cloud-Tab

## Version
- Versionsanzeige 2.51 → 2.52.3 (settings/home/erweitert), MeloLogger.init('2.35') → '2.52.3'
This commit is contained in:
Dustin
2026-08-05 11:03:03 +02:00
parent 73aed70edd
commit 187b26603b
5 changed files with 58 additions and 53 deletions
+31 -48
View File
@@ -2,7 +2,6 @@ import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'dart:io' show Platform; import 'dart:io' show Platform;
import 'package:audio_service/audio_service.dart'; import 'package:audio_service/audio_service.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:permission_handler/permission_handler.dart'; import 'package:permission_handler/permission_handler.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:flutter_background_service/flutter_background_service.dart'; import 'package:flutter_background_service/flutter_background_service.dart';
@@ -13,7 +12,9 @@ import 'services/cloud_service.dart';
import 'services/melo_logger.dart'; import 'services/melo_logger.dart';
import 'services/audio_handler.dart'; import 'services/audio_handler.dart';
import 'services/musik_scanner.dart'; import 'services/musik_scanner.dart';
import 'services/sync_service.dart';
import 'utils/farb_theme.dart'; import 'utils/farb_theme.dart';
import 'utils/user_effekte.dart';
import 'screens/home_screen.dart'; import 'screens/home_screen.dart';
import 'screens/login_screen.dart'; import 'screens/login_screen.dart';
@@ -28,13 +29,20 @@ void main() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
// Logger startet sofort zeichnet ALLES auf // Logger startet sofort zeichnet ALLES auf
MeloLogger().init('2.35'); MeloLogger().init('2.52.3');
// ── Notifications initialisieren (Issue #9) ── // ── Notifications initialisieren (Issue #9) ──
const androidInit = AndroidInitializationSettings('@mipmap/ic_launcher'); const androidInit = AndroidInitializationSettings('@mipmap/ic_launcher');
const initSettings = InitializationSettings(android: androidInit); const initSettings = InitializationSettings(android: androidInit);
await notificationsPlugin.initialize( await notificationsPlugin.initialize(
settings: initSettings, settings: initSettings,
// Tipp auf die Sync-Abschluss-/Fehler-Notification → Cloud-Tab öffnen
onDidReceiveNotificationResponse: (response) {
if (response.payload == 'sync_fertig' ||
response.payload == 'sync_fehler') {
SyncService.syncBenachrichtigungGetippt.value++;
}
},
); );
// Notification-Channel für Downloads (Fortschritt + Abschluss) // Notification-Channel für Downloads (Fortschritt + Abschluss)
@@ -51,6 +59,20 @@ void main() async {
AndroidFlutterLocalNotificationsPlugin>() AndroidFlutterLocalNotificationsPlugin>()
?.createNotificationChannel(downloadChannel); ?.createNotificationChannel(downloadChannel);
// Notification-Channel für den Cloud-Sync (Fortschritt + Abschluss)
const syncChannel = AndroidNotificationChannel(
'de.baka.melo.sync',
'Melo Sync',
description: 'Cloud-Sync-Fortschritt und Abschluss',
importance: Importance.low,
playSound: false,
enableVibration: false,
);
await notificationsPlugin
.resolvePlatformSpecificImplementation<
AndroidFlutterLocalNotificationsPlugin>()
?.createNotificationChannel(syncChannel);
// ── Background Service initialisieren (Issue #3) ── // ── Background Service initialisieren (Issue #3) ──
await FlutterBackgroundService().configure( await FlutterBackgroundService().configure(
iosConfiguration: IosConfiguration(), iosConfiguration: IosConfiguration(),
@@ -108,8 +130,13 @@ void main() async {
await Permission.notification.request(); await Permission.notification.request();
} }
// Auto-Sync beim Start (wenn eingeloggt und Auto-Sync aktiv) // Per-User-Akzent (überschreibbar in den Einstellungen) + Begrüßungs-Sound
_starteAutoSyncFallsNoetig(); // beim App-Start (wenn Token vorhanden). Fehler sind unkritisch.
await UserEffekt.anwenden(AuthService().benutzer);
// App-weiter Auto-Sync-Timer (F3): Intervall aus `cloud_interval`
// (0 = Aus), erster Tick erst NACH dem Intervall — kein Sync beim Start.
unawaited(SyncService.starteAutoSyncTimer());
runApp(const MeloApp()); runApp(const MeloApp());
} }
@@ -134,50 +161,6 @@ void _starteAutoScan() {
}); });
} }
/// Führt Cloud-Auto-Sync beim App-Start aus, falls konfiguriert
Future<void> _starteAutoSyncFallsNoetig() async {
try {
final auth = AuthService();
if (!auth.istEingeloggt) return;
// Prüfen, ob Auto-Sync aktiviert ist
final SharedPreferences prefs =
await SharedPreferences.getInstance();
final autoSync = prefs.getBool('cloud_auto') ?? true;
if (!autoSync) return;
final cloud = CloudService();
final ok = await cloud.login(auth.benutzer);
if (!ok) return;
// Letzten Sync prüfen — nur syncen wenn nötig
final lastSync = prefs.getString('cloud_last_sync_ts');
final now = DateTime.now();
if (lastSync != null) {
final last = DateTime.tryParse(lastSync);
if (last != null) {
final interval = prefs.getInt('cloud_interval') ?? 6;
if (now.difference(last).inHours < interval) {
return; // Noch nicht fällig
}
}
}
// Sync ausführen
MeloLogger().aktion('auto_sync_start', {});
final st = await cloud.statusDaten();
if (st != null) {
final serverSongs = await cloud.listSongs();
MeloLogger().aktion('auto_sync_done',
{'songs': serverSongs.length});
}
await prefs.setString('cloud_last_sync_ts', now.toIso8601String());
} catch (e) {
MeloLogger().fehler('auto_sync_init', e);
}
}
/// Background Service Callback hält App im Vordergrund beim Download (Issue #3) /// Background Service Callback hält App im Vordergrund beim Download (Issue #3)
@pragma('vm:entry-point') @pragma('vm:entry-point')
Future<bool> _onServiceStart(ServiceInstance service) async { Future<bool> _onServiceStart(ServiceInstance service) async {
+1 -1
View File
@@ -419,7 +419,7 @@ class _EntwicklerScreenState extends State<EntwicklerScreen> {
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
children: [ children: [
_infoKarte([ _infoKarte([
('Version', 'v2.51'), ('Version', 'v2.52.3'),
('Session', MeloLogger().sessionId.isEmpty ? '' : MeloLogger().sessionId), ('Session', MeloLogger().sessionId.isEmpty ? '' : MeloLogger().sessionId),
('Songs in DB', _songAnzahl < 0 ? '' : '$_songAnzahl'), ('Songs in DB', _songAnzahl < 0 ? '' : '$_songAnzahl'),
('Auth-Server', AppConfig.authUrl), ('Auth-Server', AppConfig.authUrl),
+19 -3
View File
@@ -20,6 +20,7 @@ import 'erweitert_screen.dart';
import '../services/cloud_service.dart'; import '../services/cloud_service.dart';
import '../services/player_service.dart'; import '../services/player_service.dart';
import '../services/auth_service.dart'; import '../services/auth_service.dart';
import '../services/sync_service.dart';
import '../config/app_config.dart'; import '../config/app_config.dart';
class MeloHome extends StatefulWidget { class MeloHome extends StatefulWidget {
@@ -54,14 +55,23 @@ class _MeloHomeState extends State<MeloHome> {
}); });
// Cloud-Verbindung beim App-Start herstellen (feuern-und-vergessen). // Cloud-Verbindung beim App-Start herstellen (feuern-und-vergessen).
_cloud.verbinde(); _cloud.verbinde();
// Tipp auf die Sync-Abschluss-/Fehler-Notification → Cloud-Tab öffnen
SyncService.syncBenachrichtigungGetippt.addListener(_syncNotifGetippt);
} }
@override @override
void dispose() { void dispose() {
SyncService.syncBenachrichtigungGetippt.removeListener(_syncNotifGetippt);
_vm.dispose(); _vm.dispose();
super.dispose(); super.dispose();
} }
/// Öffnet den Cloud-Tab, wenn die Sync-Notification getippt wurde.
void _syncNotifGetippt() {
if (!mounted) return;
setState(() => _aktiverTab = 3);
}
// ─── Navigation ───────────────────────────────────── // ─── Navigation ─────────────────────────────────────
/// Öffnet den Now-Playing-Fullscreen mit Slide-up-Animation /// Öffnet den Now-Playing-Fullscreen mit Slide-up-Animation
@@ -415,7 +425,9 @@ class _MeloHomeState extends State<MeloHome> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ListenableBuilder( return ListenableBuilder(
listenable: _vm, // _vm (Songs/Status), _cloud (Verbindung) UND SyncService.laeuftNotifier
// (☁️-Chip pulsiert bei laufendem Sync, auch im Hintergrund)
listenable: Listenable.merge([_vm, _cloud, SyncService.laeuftNotifier]),
builder: (_, _) { builder: (_, _) {
if (_vm.ladt) { if (_vm.ladt) {
return const Scaffold( return const Scaffold(
@@ -454,7 +466,11 @@ class _MeloHomeState extends State<MeloHome> {
onSearch: _zeigeSuche, onSearch: _zeigeSuche,
onServer: _zeigeMusikserverDetail, onServer: _zeigeMusikserverDetail,
serverVerbunden: _vm.navidrome.istVerbunden, serverVerbunden: _vm.navidrome.istVerbunden,
serverSyncLaeuft: _cloud.status == CloudStatus.verbinde || _vm.serverLadt, // Chip pulsiert während des Syncs (SyncService-Läuft-Signal),
// beim Verbinden und solange der Server lädt.
serverSyncLaeuft: SyncService.laeuftNotifier.value ||
_cloud.status == CloudStatus.verbinde ||
_vm.serverLadt,
), ),
Expanded( Expanded(
child: ListView( child: ListView(
@@ -1059,7 +1075,7 @@ class _MeloHomeState extends State<MeloHome> {
), ),
child: Column( child: Column(
children: [ children: [
_infoZeile('Version', '2.51'), _infoZeile('Version', '2.52.3'),
const Divider(color: MeloTheme.dunkel2, height: 1), const Divider(color: MeloTheme.dunkel2, height: 1),
_infoZeile('Theme', 'Schwarz + Rot'), _infoZeile('Theme', 'Schwarz + Rot'),
const Divider(color: MeloTheme.dunkel2, height: 1), const Divider(color: MeloTheme.dunkel2, height: 1),
+1 -1
View File
@@ -138,7 +138,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
), ),
child: Column( child: Column(
children: [ children: [
_infoZeile('Version', '2.51'), _infoZeile('Version', '2.52.3'),
const Divider(color: MeloTheme.dunkel2, height: 1), const Divider(color: MeloTheme.dunkel2, height: 1),
_infoZeile('Theme', 'Schwarz + Rot'), _infoZeile('Theme', 'Schwarz + Rot'),
const Divider(color: MeloTheme.dunkel2, height: 1), const Divider(color: MeloTheme.dunkel2, height: 1),
+6
View File
@@ -60,6 +60,12 @@ class SyncService {
/// ValueNotifier, damit der Home-Screen (ListenableBuilder) mitrebuildet. /// ValueNotifier, damit der Home-Screen (ListenableBuilder) mitrebuildet.
static final ValueNotifier<bool> laeuftNotifier = ValueNotifier<bool>(false); static final ValueNotifier<bool> laeuftNotifier = ValueNotifier<bool>(false);
/// Wird inkrementiert, wenn die Sync-Abschluss-/Fehler-Notification getippt
/// wird — MeloHome hört darauf und öffnet den Cloud-Tab (main.dart
/// verdrahtet den Tipp via `onDidReceiveNotificationResponse`).
static final ValueNotifier<int> syncBenachrichtigungGetippt =
ValueNotifier<int>(0);
// ─── Auto-Sync-Timer (app-weit, F3) ─── // ─── Auto-Sync-Timer (app-weit, F3) ───
static Timer? _autoSyncTimer; static Timer? _autoSyncTimer;