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
Hermes (Server)andClaude Haiku 4.5 653bc612f5 BUG-FIX: Login auf Navidrome umgestellt (Baka-Auth + Cloud gelöscht)
Fehlerpfad (502):
- App versuchte bei Startup, sich bei altem Baka-Auth (8993/8994) anzumelden → gelöscht
- CloudService versuchte, Cloud-API zu erreichen → gelöscht
- AuthService.initialisieren() crashte bei 502

Lösung:
- LoginScreen nutzt jetzt Navidrome.ping() statt AuthService.login()
- main.dart: AuthService.initialisieren() entfernt (nicht mehr nötig)
- MeloApp: Prüft jetzt Navidrome-Credentials statt Baka-Auth-Token
- MeloApp zeigt Loading-Spinner während Credentials geladen werden

Musikserver-Chip: Nutzt jetzt nur Navidrome (nicht mehr Cloud)
Alle 202 Tests grün

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M8HvgtKSsaDZ8bjZrYBBms
2026-08-19 07:11:35 +02:00

262 lines
9.3 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 'services/navidrome_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.56.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);
}
// Navidrome-Credentials laden (nicht mehr Baka-Auth — Server gelöscht)
// Die App zeigt LoginScreen, wenn keine Credentials existieren
// 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> {
late Future<bool> _checkNavidromeCredentials;
@override
void initState() {
super.initState();
// Prüfe, ob Navidrome-Credentials gespeichert sind
_checkNavidromeCredentials = _hasNavidromeLogin();
}
Future<bool> _hasNavidromeLogin() async {
final nav = NavidromeService();
await nav.ladeGespeicherteZugangsdaten();
return nav.istVerbunden;
}
@override
Widget build(BuildContext context) {
// 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: FutureBuilder<bool>(
future: _checkNavidromeCredentials,
builder: (context, snapshot) {
if (snapshot.connectionState != ConnectionState.done) {
return const Scaffold(
backgroundColor: MeloTheme.schwarz,
body: Center(
child: CircularProgressIndicator(color: MeloTheme.rot),
),
);
}
return Scaffold(
body: snapshot.data == true ? const MeloHome() : const LoginScreen(),
);
},
),
),
);
}
@override
void dispose() {
_autoScanTimer?.cancel();
_autoScanTimer = null;
super.dispose();
}
}