diff --git a/lib/main.dart b/lib/main.dart index 82fac43..9ead63a 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -4,6 +4,7 @@ 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 'database/db_helper.dart'; import 'services/favoriten_service.dart'; import 'services/auth_service.dart'; @@ -15,12 +16,37 @@ 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(); + 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); + try { await DbHelper().db; await FavoritenService().init(); diff --git a/lib/screens/cloud_screen.dart b/lib/screens/cloud_screen.dart index 58449aa..69af4f0 100644 --- a/lib/screens/cloud_screen.dart +++ b/lib/screens/cloud_screen.dart @@ -3,11 +3,13 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:path_provider/path_provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; +import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import '../utils/farb_theme.dart'; import '../services/cloud_service.dart'; import '../services/auth_service.dart'; import '../database/db_helper.dart'; import '../services/melo_logger.dart'; +import '../main.dart'; // notificationsPlugin /// Melo Cloud Sync Screen v3 — vollständiges Sync-System /// Playlisten, Favoriten, Auto-Sync, Persistent Login, Benutzerdefinierte Namen @@ -398,6 +400,9 @@ class _CloudScreenState extends State { // ─── Upload / Download ─── + static const String _cloudChannelId = 'de.baka.melo.downloads'; + static const int _cloudNotifyId = 200; + Future _upload() async { setState(() => _ladt = true); _setzeStatus('Suche lokale Songs...'); @@ -413,12 +418,59 @@ class _CloudScreenState extends State { } final files = dir.listSync().whereType().where( (f) => f.path.endsWith('.mp3') || f.path.endsWith('.m4a')); + final fileList = files.toList(); int count = 0; - for (final f in files) { + for (int i = 0; i < fileList.length; i++) { + final f = fileList[i]; _setzeStatus('Upload: ${f.path.split('/').last}...'); + // Progress-Notification + if (Platform.isAndroid) { + notificationsPlugin.show( + id: _cloudNotifyId, + title: 'Melo Cloud Upload', + body: '${i + 1} / ${fileList.length}: ${f.path.split('/').last}', + notificationDetails: NotificationDetails( + android: AndroidNotificationDetails( + _cloudChannelId, + 'Melo Downloads', + channelDescription: 'Cloud Upload-Fortschritt', + importance: Importance.low, + priority: Priority.low, + onlyAlertOnce: true, + showProgress: true, + maxProgress: fileList.length, + progress: i, + ongoing: true, + autoCancel: false, + ), + ), + ); + } final sid = await widget.cloud.upload(f.path, f.path.split('/').last); if (sid != null) count++; } + // Abschluss-Notification + if (Platform.isAndroid) { + notificationsPlugin.cancel(id: _cloudNotifyId); + final ok = count > 0; + notificationsPlugin.show( + id: _cloudNotifyId + 1, + title: ok ? 'Cloud Upload fertig' : 'Cloud Upload fehlgeschlagen', + body: ok + ? '$count / ${fileList.length} Songs hochgeladen ✅' + : 'Kein Song konnte hochgeladen werden ❌', + notificationDetails: NotificationDetails( + android: AndroidNotificationDetails( + _cloudChannelId, + 'Melo Downloads', + channelDescription: 'Cloud Upload-Abschluss', + importance: Importance.defaultImportance, + priority: Priority.defaultPriority, + autoCancel: true, + ), + ), + ); + } await _ladeStatus(); if (mounted) { setState(() => _ladt = false); @@ -427,6 +479,9 @@ class _CloudScreenState extends State { } } catch (e) { MeloLogger().fehler('cloud_upload_path', e); + if (Platform.isAndroid) { + notificationsPlugin.cancel(id: _cloudNotifyId); + } if (mounted) { setState(() { _ladt = false; diff --git a/lib/services/download_service.dart b/lib/services/download_service.dart index 7eef276..0b089c5 100644 --- a/lib/services/download_service.dart +++ b/lib/services/download_service.dart @@ -4,14 +4,17 @@ import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:http/http.dart' as http; import 'package:path_provider/path_provider.dart'; +import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import '../models/song.dart'; import '../database/db_helper.dart'; import '../utils/audio_validator.dart'; import 'melo_logger.dart'; import '../config/app_config.dart'; +import '../main.dart'; // für notificationsPlugin /// Download-Service: Lädt YouTube-Audio über den yt-proxy herunter. /// Nutzt ChangeNotifier für UI-Updates via ListenableBuilder. +/// Issue #9: Fortschritt per System-Notification (ProgressBar). class DownloadService extends ChangeNotifier { static final http.Client _client = http.Client(); @@ -23,6 +26,10 @@ class DownloadService extends ChangeNotifier { static String get _apiKey => AppConfig.ytProxyApiKey; static Map get _authHeader => {'X-API-Key': _apiKey}; + static const String _channelId = 'de.baka.melo.downloads'; + static const int _notifyProgressId = 100; + static const int _notifyCompleteId = 101; + final DbHelper _db = DbHelper(); bool _ladt = false; @@ -91,12 +98,14 @@ class DownloadService extends ChangeNotifier { int erfolgreich = 0; _aktuellerTitel = '0/${urls.length} – Starte...'; notifyListeners(); + _showProgress(0, urls.length); for (int i = 0; i < urls.length; i++) { if (sollAbbrechen) break; _aktuellerTitel = '${i + 1}/${urls.length} – ${urls[i]._titelKurz()}'; notifyListeners(); + _showProgress(i, urls.length, titel: urls[i]._titelKurz()); MeloLogger().zustand('download_einzeln_start', { 'index': i, @@ -137,6 +146,7 @@ class DownloadService extends ChangeNotifier { _aktuellerTitel = '✅ $erfolgreich/${urls.length} Songs geladen'; } notifyListeners(); + _completeNotification(erfolgreich, urls.length, abgebrochen: sollAbbrechen); MeloLogger().zustand('download_batch_ende', { 'erfolgreich': erfolgreich, 'gesamt': urls.length, @@ -458,6 +468,66 @@ class DownloadService extends ChangeNotifier { return _downloadEinzeln(song.ytUrl!); } + // ─── #9: Notification-Progress ───────────────── + + /// Zeigt oder aktualisiert eine Fortschritts-Notification. + /// [current] = aktueller Index (1-basiert), [total] = Gesamtanzahl. + void _showProgress(int current, int total, {String? titel}) { + if (!Platform.isAndroid) return; + notificationsPlugin.show( + id: _notifyProgressId, + title: 'Melo lädt herunter', + body: titel ?? '$current / $total Songs', + notificationDetails: NotificationDetails( + android: AndroidNotificationDetails( + _channelId, + 'Melo Downloads', + channelDescription: 'Download-Fortschritt', + importance: Importance.low, + priority: Priority.low, + onlyAlertOnce: true, + showProgress: true, + maxProgress: total, + progress: current, + ongoing: true, + autoCancel: false, + ), + ), + ); + } + + /// Zeigt Abschluss-Notification (Erfolg oder Fehler). + void _completeNotification(int erfolgreich, int gesamt, {bool abgebrochen = false}) { + if (!Platform.isAndroid) return; + // Fortschritts-Notification abbrechen + notificationsPlugin.cancel(id: _notifyProgressId); + + final titel = abgebrochen + ? 'Download abgebrochen' + : (erfolgreich > 0 ? 'Download fertig' : 'Download fehlgeschlagen'); + final body = abgebrochen + ? '$erfolgreich / $gesamt Songs geladen' + : (erfolgreich > 0 + ? '$erfolgreich / $gesamt Songs geladen ✅' + : 'Kein Song konnte geladen werden ❌'); + + notificationsPlugin.show( + id: _notifyCompleteId, + title: titel, + body: body, + notificationDetails: NotificationDetails( + android: AndroidNotificationDetails( + _channelId, + 'Melo Downloads', + channelDescription: 'Download-Abschluss', + importance: Importance.defaultImportance, + priority: Priority.defaultPriority, + autoCancel: true, + ), + ), + ); + } + @override void dispose() { abbrechen(); diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 2270315..766ed2f 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -7,6 +7,7 @@ import Foundation import audio_service import audio_session +import flutter_local_notifications import flutter_secure_storage_darwin import just_audio import shared_preferences_foundation @@ -15,6 +16,7 @@ import sqflite_darwin func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { AudioServicePlugin.register(with: registry.registrar(forPlugin: "AudioServicePlugin")) AudioSessionPlugin.register(with: registry.registrar(forPlugin: "AudioSessionPlugin")) + FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin")) FlutterSecureStorageDarwinPlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStorageDarwinPlugin")) JustAudioPlugin.register(with: registry.registrar(forPlugin: "JustAudioPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) diff --git a/pubspec.lock b/pubspec.lock index 34dc967..3225fda 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -97,6 +97,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.7" + dbus: + dependency: transitive + description: + name: dbus + sha256: "792974a4007974fbc5c1b5433eb2330a9db3e368c3f906253af4c007d0f49a91" + url: "https://pub.dev" + source: hosted + version: "0.7.13" fake_async: dependency: transitive description: @@ -158,6 +166,46 @@ packages: url: "https://pub.dev" source: hosted version: "5.0.0" + flutter_local_notifications: + dependency: "direct main" + description: + name: flutter_local_notifications + sha256: "9375211fd7df9c070504ac4db7047c7fae857e238291433b770cfe696b5c357a" + url: "https://pub.dev" + source: hosted + version: "22.2.0" + flutter_local_notifications_linux: + dependency: transitive + description: + name: flutter_local_notifications_linux + sha256: "9ca97e63776f29ab1b955725c09999fc2c150523269db150c39274f2a43c5a8b" + url: "https://pub.dev" + source: hosted + version: "8.0.1" + flutter_local_notifications_platform_interface: + dependency: transitive + description: + name: flutter_local_notifications_platform_interface + sha256: "945a438a4779f3aca3a948718d8a4048cbe9f9c8c797492c00abd5d39112bf37" + url: "https://pub.dev" + source: hosted + version: "12.1.0" + flutter_local_notifications_web: + dependency: transitive + description: + name: flutter_local_notifications_web + sha256: "516afaf97a2d1e67a036c6617321b00d205d72f7a67b6eccf936cd565f985878" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + flutter_local_notifications_windows: + dependency: transitive + description: + name: flutter_local_notifications_windows + sha256: "6f43bdd03b171b7a90f22647506fea33e2bb12294b7c7c7a3d690e960a382945" + url: "https://pub.dev" + source: hosted + version: "3.1.1" flutter_secure_storage: dependency: "direct main" description: @@ -472,6 +520,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.2.1" + petitparser: + dependency: transitive + description: + name: petitparser + sha256: "91bd59303e9f769f108f8df05e371341b15d59e995e6806aefab827b58336675" + url: "https://pub.dev" + source: hosted + version: "7.0.2" platform: dependency: transitive description: @@ -669,6 +725,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.11" + timezone: + dependency: transitive + description: + name: timezone + sha256: "981d1020d6ef8fe1e7b3de5054e5b25579ae7c403d7734adc508ffc47668e9cb" + url: "https://pub.dev" + source: hosted + version: "0.11.1" typed_data: dependency: transitive description: @@ -725,6 +789,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.0" + xml: + dependency: transitive + description: + name: xml + sha256: "67f0aff7be013d107995e9b75bf4e7f2c3ef2dfdb2c8e68024bba0a7fd5756a4" + url: "https://pub.dev" + source: hosted + version: "7.0.1" yaml: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 4f1ccad..b78e0c4 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -29,6 +29,7 @@ dependencies: http: ^1.2.0 crypto: ^3.0.6 flutter_secure_storage: ^10.3.1 + flutter_local_notifications: ^22.2.0 dev_dependencies: flutter_test: diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index d1aac24..f513871 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -8,6 +8,7 @@ list(APPEND FLUTTER_PLUGIN_LIST ) list(APPEND FLUTTER_FFI_PLUGIN_LIST + flutter_local_notifications_windows jni )