This repository has been archived on 2026-08-19. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
melo-app/lib/main.dart
T
Dustin 85615d253c v2.55.3 — Code-Review-Fixes: foregroundServiceType (Android 14+), POST_NOTIFICATIONS-Check, FG-Service-Notification-Channel, try/catch _zeigeSyncNotification
CRITICAL:
- AndroidManifest: foregroundServiceType="dataSync" für BackgroundService (Android 14+ MissingForegroundServiceTypeException)
- main.dart: notificationChannelId + initialNotificationTitle + foregroundServiceTypes für FlutterBackgroundService (fehlende FG-Notification → ANR/crash nach 5s)

MEDIUM:
- sync_service.dart: POST_NOTIFICATIONS-Permission-Check vor _starteForegroundService (Android 13+)
- sync_service.dart: try/catch um _zeigeSyncNotification (plugin-dispose-Sicherheit)

flutter analyze: clean | flutter test: 194/194 passed
2026-08-06 18:17:25 +02:00

235 lines
8.4 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'dart:async';
import 'package:flutter/material.dart';
import 'dart:io' show Platform;
import 'package:audio_service/audio_service.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:flutter_background_service/flutter_background_service.dart';
import 'database/db_helper.dart';
import 'services/favoriten_service.dart';
import 'services/auth_service.dart';
import 'services/cloud_service.dart';
import 'services/melo_logger.dart';
import 'services/audio_handler.dart';
import 'services/musik_scanner.dart';
import 'services/sync_service.dart';
import 'utils/farb_theme.dart';
import 'utils/user_effekte.dart';
import 'screens/home_screen.dart';
import 'screens/login_screen.dart';
/// Globaler Notification-Plugin (wird in main() initialisiert)
final FlutterLocalNotificationsPlugin notificationsPlugin =
FlutterLocalNotificationsPlugin();
/// Globaler Timer für periodischen Auto-Scan (wird in MeloAppState.dispose gecancelt)
Timer? _autoScanTimer;
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// Logger startet sofort zeichnet ALLES auf
MeloLogger().init('2.54.1');
// ── Notifications initialisieren (Issue #9) ──
const androidInit = AndroidInitializationSettings('@mipmap/ic_launcher');
const initSettings = InitializationSettings(android: androidInit);
await notificationsPlugin.initialize(
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)
const downloadChannel = AndroidNotificationChannel(
'de.baka.melo.downloads',
'Melo Downloads',
description: 'Download-Fortschritt und Abschluss',
importance: Importance.low,
playSound: false,
enableVibration: false,
);
await notificationsPlugin
.resolvePlatformSpecificImplementation<
AndroidFlutterLocalNotificationsPlugin>()
?.createNotificationChannel(downloadChannel);
// Notification-Channel für den Cloud-Sync (Fortschritt + Abschluss)
// v2.55.2: Importance HIGH + Visibility public für Sperrbildschirm + Shadow.
// createNotificationChannel überschreibt bestehende Channel-Settings —
// ein alter Kanal mit low-Importance wird beim nächsten App-Start korrigiert.
const syncChannel = AndroidNotificationChannel(
'de.baka.melo.sync',
'Melo Sync',
description: 'Cloud-Sync-Fortschritt und Abschluss',
importance: Importance.high,
playSound: false,
enableVibration: false,
);
await notificationsPlugin
.resolvePlatformSpecificImplementation<
AndroidFlutterLocalNotificationsPlugin>()
?.createNotificationChannel(syncChannel);
// ── Background Service initialisieren (Issue #3) ──
// v2.55.3: Foreground-Service-Notification-Channel konfigurieren.
// Ohne notificationChannelId zeigt der Service eine Default-Notification
// ohne eigenen Channel — auf Android 14+ Pflicht.
const fgChannel = AndroidNotificationChannel(
'de.baka.melo.fg_service',
'Melo Hintergrunddienst',
description: 'Hält Melo im Hintergrund aktiv (Sync, Downloads)',
importance: Importance.low,
playSound: false,
enableVibration: false,
showBadge: false,
);
await notificationsPlugin
.resolvePlatformSpecificImplementation<
AndroidFlutterLocalNotificationsPlugin>()
?.createNotificationChannel(fgChannel);
await FlutterBackgroundService().configure(
iosConfiguration: IosConfiguration(),
androidConfiguration: AndroidConfiguration(
onStart: _onServiceStart,
autoStart: false,
isForegroundMode: true,
notificationChannelId: 'de.baka.melo.fg_service',
initialNotificationTitle: 'Melo Sync',
initialNotificationContent: 'Synchronisiere Musik…',
foregroundServiceTypes: [AndroidForegroundType.dataSync],
),
);
try {
await DbHelper().db;
await FavoritenService().init();
// Hintergrund-Audio-Service starten
await AudioService.init(
builder: () => MeloAudioHandler(),
config: const AudioServiceConfig(
androidNotificationChannelId: 'de.baka.melo.audio',
androidNotificationChannelName: 'Melo Wiedergabe',
androidStopForegroundOnPause: false,
),
);
MeloLogger().zustand('start_ok', {'db': 'ok', 'audio': 'ok'});
} catch (e, stack) {
MeloLogger().fehler('App-Start', e, stack);
}
// Auth initialisieren (Token aus SharedPreferences laden + online validieren)
await AuthService().initialisieren();
// Favoriten-Toggle → Server-Push (Feuer-und-Vergessen, deterministisch via
// `set` statt Toggle — verhindert Doppel-Toggle). Songs ohne cloud_id
// (nicht hochgeladen) werden übersprungen; ein Fehlschlag wird beim
// nächsten vollständigen Sync korrigiert (favoritenMerge-Vereinigung).
FavoritenService().onStatusGeaendert = (songId, neuerStatus) async {
try {
final song = await DbHelper().songNachId(songId);
final cid = song?.cloudId;
if (cid == null || cid.isEmpty) return;
await CloudService().setFavorite(cid, favorit: neuerStatus);
} catch (e) {
MeloLogger().fehler('favoriten_server_push', e);
}
};
// Android-Berechtigungen beim Start abfragen (Issue #4)
// Speicherzugriff für Musik-Scan (READ_MEDIA_AUDIO / Storage)
await MusikScanner().frageSpeicherZugriff();
// Auto-Scan: Musikordner beim Start scannen + periodisch alle 3h (Issue #6)
_starteAutoScan();
// POST_NOTIFICATIONS für Android 13+ (Notifications für Downloads, Player)
if (Platform.isAndroid) {
await Permission.notification.request();
}
// Per-User-Akzent (überschreibbar in den Einstellungen) + Begrüßungs-Sound
// 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());
}
/// Führt Musik-Scan beim Start aus + startet Timer.periodic alle 3 Stunden (Issue #6)
/// Scannt nur /Music, /Download, /Musik, /Downloads — nicht ganz /storage.
void _starteAutoScan() {
// Sofort beim Start scannen (asynchron, blockiert nicht)
MusikScanner().scanneMusikOrdner().then((songs) {
MeloLogger().zustand('auto_scan_start', {'gefunden': songs.length});
}).catchError((e) {
MeloLogger().fehler('auto_scan_start', e);
});
// Periodischer Scan alle 3 Stunden Timer wird in MeloAppState.dispose() gecancelt
_autoScanTimer = Timer.periodic(const Duration(hours: 3), (_) {
MusikScanner().scanneMusikOrdner().then((songs) {
MeloLogger().zustand('auto_scan_periodisch', {'gefunden': songs.length});
}).catchError((e) {
MeloLogger().fehler('auto_scan_periodisch', e);
});
});
}
/// Background Service Callback hält App im Vordergrund beim Download (Issue #3)
@pragma('vm:entry-point')
Future<bool> _onServiceStart(ServiceInstance service) async {
WidgetsFlutterBinding.ensureInitialized();
// Service läuft als Foreground-Service und hält den Prozess am Leben.
// Der eigentliche Download läuft im Haupt-Isolate der Service dient nur
// als WakeLock / Foreground-Garantie.
service.on('stopService').listen((_) {
service.stopSelf();
});
return true;
}
class MeloApp extends StatefulWidget {
const MeloApp({super.key});
@override
State<MeloApp> createState() => _MeloAppState();
}
class _MeloAppState extends State<MeloApp> {
@override
Widget build(BuildContext context) {
final auth = AuthService();
// ValueListenableBuilder: Akzentfarbe (per User, in Einstellungen wählbar)
// wechselt das komplette Theme live — kein App-Neustart nötig.
return ValueListenableBuilder<Color>(
valueListenable: MeloTheme.akzentNotifier,
builder: (_, akzent, __) => MaterialApp(
title: 'Melo',
debugShowCheckedModeBanner: false,
theme: MeloTheme.theme,
home: auth.istEingeloggt
? const Scaffold(body: MeloHome())
: const LoginScreen(),
),
);
}
@override
void dispose() {
_autoScanTimer?.cancel();
_autoScanTimer = null;
super.dispose();
}
}