MEDIUM: - #7 API-Key XOR-obfuskiert in app_config.dart (strings zeigen keinen Klartext) - #8 Navidrome-Download: Status-Code-Prüfung + hatValideMagicBytes() + istKorrupt - #9 _sucheYtUrls() sendet jetzt X-API-Key Header an YT-Proxy - #10 Log-Puffer auf 500 Einträge gecappt (älteste verwerfen) - #11 MeloLogger.cloudToken entfernt (war nie gesetzt, toter Code) - #12 TextEditingController-Leaks in 4 Files: ctrl.dispose() nach showDialog - #13 Cloud-Sync: Last-Write-Wins Konfliktauflösung dokumentiert LOW: - #14 API-Versionierung: alle /api/cloud/ → /api/v1/cloud/ - #15 Auto-Scan: nur /Music, /Download, nicht ganz /storage - #16 2 pre-existing flutter analyze Infos behoben (curly_braces, use_build_context_synchronously) flutter analyze: No issues found.
208 lines
6.7 KiB
Dart
208 lines
6.7 KiB
Dart
import 'dart:async';
|
||
import 'package:flutter/material.dart';
|
||
import 'dart:io' show Platform;
|
||
import 'package:audio_service/audio_service.dart';
|
||
import 'package:shared_preferences/shared_preferences.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 'utils/farb_theme.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.35');
|
||
|
||
// ── Notifications initialisieren (Issue #9) ──
|
||
const androidInit = AndroidInitializationSettings('@mipmap/ic_launcher');
|
||
const initSettings = InitializationSettings(android: androidInit);
|
||
await notificationsPlugin.initialize(
|
||
settings: initSettings,
|
||
);
|
||
|
||
// 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);
|
||
|
||
// ── Background Service initialisieren (Issue #3) ──
|
||
await FlutterBackgroundService().configure(
|
||
iosConfiguration: IosConfiguration(),
|
||
androidConfiguration: AndroidConfiguration(
|
||
onStart: _onServiceStart,
|
||
autoStart: false,
|
||
isForegroundMode: true,
|
||
),
|
||
);
|
||
|
||
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();
|
||
|
||
// 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();
|
||
}
|
||
|
||
// Auto-Sync beim Start (wenn eingeloggt und Auto-Sync aktiv)
|
||
_starteAutoSyncFallsNoetig();
|
||
|
||
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);
|
||
});
|
||
});
|
||
}
|
||
|
||
/// 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.status();
|
||
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)
|
||
@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();
|
||
|
||
return 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();
|
||
}
|
||
}
|