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 b5b1475d5d v2.42.1 — HIGH: Timer-Periodic-Leak behoben
- MeloApp von StatelessWidget zu StatefulWidget konvertiert
- _autoScanTimer als globale Variable gespeichert
- Timer wird in _MeloAppState.dispose() gecancelt
2026-08-02 17:00:23 +02:00

207 lines
6.6 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: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)
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();
}
}