mirror of
https://github.com/GopeedLab/gopeed.git
synced 2026-11-13 11:53:41 +00:00
feat(desktop): implement native desktop notifications for task completion and errors (#1293)
This commit is contained in:
1 parent
98bd3305b7
commit
bc5c818eae
26 files changed
+285
-4
No files matched your search
@@ -17,6 +17,7 @@ import 'package:uri_to_file/uri_to_file.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
|
||||
|
||||
import '../../../../api/api.dart';
|
||||
import '../../../../api/model/create_task.dart';
|
||||
import '../../../../api/model/downloader_config.dart';
|
||||
@@ -38,6 +39,7 @@ import '../../../../util/util.dart';
|
||||
import '../../../routes/app_pages.dart';
|
||||
import '../../../rpc/rpc.dart';
|
||||
import '../../redirect/views/redirect_view.dart';
|
||||
import '../../../services/notification_service.dart';
|
||||
|
||||
const unixSocketPath = 'gopeed.sock';
|
||||
|
||||
@@ -96,6 +98,10 @@ class AppController extends GetxController with WindowListener, TrayListener {
|
||||
|
||||
_initWindows().onError((error, stackTrace) =>
|
||||
logger.w("initWindows error", error, stackTrace));
|
||||
|
||||
if (Util.isDesktop()) {
|
||||
Get.put(NotificationService());
|
||||
}
|
||||
|
||||
_initTray().onError(
|
||||
(error, stackTrace) => logger.w("initTray error", error, stackTrace));
|
||||
|
||||
@@ -498,6 +498,27 @@ class SettingView extends GetView<SettingController> {
|
||||
);
|
||||
});
|
||||
|
||||
final buildDesktopNotification = !Util.isDesktop()
|
||||
? () => null
|
||||
: _buildConfigItem('desktopNotification', () {
|
||||
return appController.downloaderConfig.value.extra.desktopNotification
|
||||
? 'on'.tr
|
||||
: 'off'.tr;
|
||||
}, (Key key) {
|
||||
return Container(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Switch(
|
||||
value: appController.downloaderConfig.value.extra.desktopNotification,
|
||||
onChanged: (bool value) async {
|
||||
appController.downloaderConfig.update((val) {
|
||||
val!.extra.desktopNotification = value;
|
||||
});
|
||||
await debounceSave();
|
||||
},
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
// http config items start
|
||||
final httpConfig = downloaderCfg.value.protocolConfig.http;
|
||||
final buildHttpUa =
|
||||
@@ -1592,6 +1613,7 @@ class SettingView extends GetView<SettingController> {
|
||||
buildBrowserExtension(),
|
||||
buildAutoStartup(),
|
||||
buildMenubarMode(),
|
||||
buildDesktopNotification(),
|
||||
]),
|
||||
)),
|
||||
Text('archives'.tr),
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
||||
import '../../api/api.dart';
|
||||
import '../../api/model/task.dart';
|
||||
import '../../util/util.dart';
|
||||
import '../modules/app/controllers/app_controller.dart';
|
||||
|
||||
class NotificationService extends GetxService {
|
||||
Timer? _timer;
|
||||
final Map<String, Status> _previousStatus = {};
|
||||
|
||||
final AppController appController = Get.find<AppController>();
|
||||
final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
|
||||
FlutterLocalNotificationsPlugin();
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
_initNotifications();
|
||||
_startPolling();
|
||||
}
|
||||
|
||||
Future<void> _initNotifications() async {
|
||||
const DarwinInitializationSettings initializationSettingsDarwin =
|
||||
DarwinInitializationSettings(
|
||||
requestAlertPermission: false,
|
||||
requestBadgePermission: false,
|
||||
requestSoundPermission: false);
|
||||
|
||||
final LinuxInitializationSettings initializationSettingsLinux =
|
||||
LinuxInitializationSettings(
|
||||
defaultActionName: 'Open notification',
|
||||
defaultIcon: AssetsLinuxIcon('assets/icon/icon.png'),
|
||||
);
|
||||
|
||||
String? windowsIconPath;
|
||||
try {
|
||||
if (Util.isWindows()) {
|
||||
final byteData = await rootBundle.load('assets/icon/icon.ico');
|
||||
final tempDir = await getTemporaryDirectory();
|
||||
final file = File('${tempDir.path}/notification_icon.ico');
|
||||
await file.writeAsBytes(byteData.buffer
|
||||
.asUint8List(byteData.offsetInBytes, byteData.lengthInBytes));
|
||||
windowsIconPath = file.path;
|
||||
}
|
||||
} catch (e) {
|
||||
// Ignore
|
||||
}
|
||||
|
||||
final WindowsInitializationSettings initializationSettingsWindows =
|
||||
WindowsInitializationSettings(
|
||||
appName: 'Gopeed',
|
||||
appUserModelId: 'com.gopeed.gopeed',
|
||||
guid: '3c1bf3f4-3d91-4eaa-a33f-8705e71cf1ce', // unique guid
|
||||
iconPath: windowsIconPath,
|
||||
);
|
||||
|
||||
final InitializationSettings initializationSettings =
|
||||
InitializationSettings(
|
||||
macOS: initializationSettingsDarwin,
|
||||
linux: initializationSettingsLinux,
|
||||
windows: initializationSettingsWindows,
|
||||
);
|
||||
|
||||
await flutterLocalNotificationsPlugin.initialize(
|
||||
settings: initializationSettings,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
_timer?.cancel();
|
||||
super.onClose();
|
||||
}
|
||||
|
||||
void _startPolling() {
|
||||
_timer = Timer.periodic(const Duration(seconds: 2), (timer) async {
|
||||
try {
|
||||
final config = appController.downloaderConfig.value;
|
||||
if (!config.extra.desktopNotification) {
|
||||
return;
|
||||
}
|
||||
|
||||
final tasks = await getTasks([
|
||||
Status.ready,
|
||||
Status.running,
|
||||
Status.pause,
|
||||
Status.wait,
|
||||
Status.error,
|
||||
Status.done,
|
||||
]);
|
||||
|
||||
for (var task in tasks) {
|
||||
final prevStatus = _previousStatus[task.id];
|
||||
final currentStatus = task.status;
|
||||
|
||||
if (prevStatus != null && prevStatus != currentStatus) {
|
||||
if (currentStatus == Status.done) {
|
||||
_showNotification(
|
||||
title: 'notificationTaskDone'.tr,
|
||||
body: task.name,
|
||||
);
|
||||
} else if (currentStatus == Status.error) {
|
||||
_showNotification(
|
||||
title: 'notificationTaskError'.tr,
|
||||
body: task.name,
|
||||
);
|
||||
}
|
||||
}
|
||||
_previousStatus[task.id] = currentStatus;
|
||||
}
|
||||
|
||||
// Clean up deleted tasks from map
|
||||
final currentTaskIds = tasks.map((t) => t.id).toSet();
|
||||
_previousStatus
|
||||
.removeWhere((id, status) => !currentTaskIds.contains(id));
|
||||
} catch (e) {
|
||||
// Ignored
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
int _notificationId = 0;
|
||||
|
||||
Future<void> _showNotification(
|
||||
{required String title, required String body}) async {
|
||||
const NotificationDetails notificationDetails = NotificationDetails(
|
||||
macOS: DarwinNotificationDetails(),
|
||||
linux: LinuxNotificationDetails(),
|
||||
windows: WindowsNotificationDetails(),
|
||||
);
|
||||
|
||||
await flutterLocalNotificationsPlugin.show(
|
||||
id: _notificationId++,
|
||||
title: title,
|
||||
body: body,
|
||||
notificationDetails: notificationDetails,
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user