mirror of
https://github.com/GopeedLab/gopeed.git
synced 2026-11-04 23:57:54 +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
@@ -28,6 +28,7 @@ android {
|
||||
ndkVersion flutter.ndkVersion
|
||||
|
||||
compileOptions {
|
||||
coreLibraryDesugaringEnabled true
|
||||
sourceCompatibility JavaVersion.VERSION_17
|
||||
targetCompatibility JavaVersion.VERSION_17
|
||||
}
|
||||
@@ -83,4 +84,5 @@ flutter {
|
||||
|
||||
dependencies {
|
||||
implementation(name: 'libgopeed', ext: 'aar')
|
||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.4'
|
||||
}
|
||||
@@ -89,6 +89,7 @@ class ExtraConfig {
|
||||
bool defaultBtClient;
|
||||
bool notifyWhenNewVersion;
|
||||
bool autoStartTasks;
|
||||
bool desktopNotification;
|
||||
List<DownloadCategory> downloadCategories;
|
||||
|
||||
ExtraConfigBt bt = ExtraConfigBt();
|
||||
@@ -102,6 +103,7 @@ class ExtraConfig {
|
||||
this.defaultBtClient = true,
|
||||
this.notifyWhenNewVersion = true,
|
||||
this.autoStartTasks = false,
|
||||
this.desktopNotification = true,
|
||||
this.downloadCategories = const [],
|
||||
});
|
||||
|
||||
|
||||
@@ -19,8 +19,7 @@ DownloaderConfig _$DownloaderConfigFromJson(Map<String, dynamic> json) =>
|
||||
..proxy = ProxyConfig.fromJson(json['proxy'] as Map<String, dynamic>)
|
||||
..webhook =
|
||||
WebhookConfig.fromJson(json['webhook'] as Map<String, dynamic>?)
|
||||
..script =
|
||||
ScriptConfig.fromJson(json['script'] as Map<String, dynamic>?)
|
||||
..script = ScriptConfig.fromJson(json['script'] as Map<String, dynamic>?)
|
||||
..autoTorrent = AutoTorrentConfig.fromJson(
|
||||
json['autoTorrent'] as Map<String, dynamic>?)
|
||||
..archive =
|
||||
@@ -91,6 +90,7 @@ ExtraConfig _$ExtraConfigFromJson(Map<String, dynamic> json) => ExtraConfig(
|
||||
defaultBtClient: json['defaultBtClient'] as bool? ?? true,
|
||||
notifyWhenNewVersion: json['notifyWhenNewVersion'] as bool? ?? true,
|
||||
autoStartTasks: json['autoStartTasks'] as bool? ?? false,
|
||||
desktopNotification: json['desktopNotification'] as bool? ?? true,
|
||||
downloadCategories: (json['downloadCategories'] as List<dynamic>?)
|
||||
?.map((e) => DownloadCategory.fromJson(e as Map<String, dynamic>))
|
||||
.toList() ??
|
||||
@@ -109,6 +109,7 @@ Map<String, dynamic> _$ExtraConfigToJson(ExtraConfig instance) =>
|
||||
'defaultBtClient': instance.defaultBtClient,
|
||||
'notifyWhenNewVersion': instance.notifyWhenNewVersion,
|
||||
'autoStartTasks': instance.autoStartTasks,
|
||||
'desktopNotification': instance.desktopNotification,
|
||||
'downloadCategories':
|
||||
instance.downloadCategories.map((e) => e.toJson()).toList(),
|
||||
'bt': instance.bt.toJson(),
|
||||
@@ -156,8 +157,7 @@ Map<String, dynamic> _$WebhookConfigToJson(WebhookConfig instance) =>
|
||||
'urls': instance.urls,
|
||||
};
|
||||
|
||||
ScriptConfig _$ScriptConfigFromJson(Map<String, dynamic> json) =>
|
||||
ScriptConfig(
|
||||
ScriptConfig _$ScriptConfigFromJson(Map<String, dynamic> json) => ScriptConfig(
|
||||
enable: json['enable'] as bool? ?? false,
|
||||
paths:
|
||||
(json['paths'] as List<dynamic>?)?.map((e) => e as String).toList() ??
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -164,5 +164,8 @@ const deDE = {
|
||||
'categoryPath': 'Kategoriepfad',
|
||||
'builtInCategory': 'Integrierte Kategorie kann nicht gelöscht werden',
|
||||
'selectCategory': 'Kategorie auswählen',
|
||||
'desktopNotification': 'Desktop-Benachrichtigungen',
|
||||
'notificationTaskDone': 'Aufgabe abgeschlossen',
|
||||
'notificationTaskError': 'Aufgabenfehler',
|
||||
},
|
||||
};
|
||||
@@ -193,5 +193,8 @@ const enUS = {
|
||||
'Task "@name" is waiting for URL update. Do you want to update it with the new URL?',
|
||||
'pendingUpdateYes': 'Update Task',
|
||||
'pendingUpdateNo': 'Create New Task',
|
||||
'desktopNotification': 'Desktop Notifications',
|
||||
'notificationTaskDone': 'Task completed',
|
||||
'notificationTaskError': 'Task error',
|
||||
},
|
||||
};
|
||||
@@ -142,5 +142,8 @@ const esES = {
|
||||
'categoryPath': 'Ruta de categoría',
|
||||
'builtInCategory': 'La categoría incorporada no se puede eliminar',
|
||||
'selectCategory': 'Seleccionar categoría',
|
||||
'desktopNotification': 'Notificaciones de escritorio',
|
||||
'notificationTaskDone': 'Tarea completada',
|
||||
'notificationTaskError': 'Error de tarea',
|
||||
},
|
||||
};
|
||||
@@ -93,5 +93,8 @@ const faIR = {
|
||||
'launchAtStartup': 'راهاندازی در استارتآپ',
|
||||
'runAsMenubarApp': 'اجرا به عنوان برنامه نوار منو',
|
||||
'runAsMenubarAppDesc': 'پنهان کردن آیکون Dock و اجرا فقط در نوار منو',
|
||||
'desktopNotification': 'اعلانهای دسکتاپ',
|
||||
'notificationTaskDone': 'وظیفه تکمیل شد',
|
||||
'notificationTaskError': 'خطای وظیفه',
|
||||
}
|
||||
};
|
||||
@@ -135,5 +135,8 @@ const frFR = {
|
||||
'categoryPath': 'Chemin de la catégorie',
|
||||
'builtInCategory': 'La catégorie intégrée ne peut pas être supprimée',
|
||||
'selectCategory': 'Sélectionner une catégorie',
|
||||
'desktopNotification': 'Notifications de bureau',
|
||||
'notificationTaskDone': 'Tâche terminée',
|
||||
'notificationTaskError': 'Erreur de tâche',
|
||||
},
|
||||
};
|
||||
@@ -156,5 +156,8 @@ const huHU = {
|
||||
'categoryPath': 'Kategória útvonala',
|
||||
'builtInCategory': 'A beépített kategória nem törölhető',
|
||||
'selectCategory': 'Kategória kiválasztása',
|
||||
'desktopNotification': 'Asztali értesítések',
|
||||
'notificationTaskDone': 'Feladat befejezve',
|
||||
'notificationTaskError': 'Feladat hiba',
|
||||
},
|
||||
};
|
||||
@@ -142,5 +142,8 @@ const idID = {
|
||||
'categoryPath': 'Path Kategori',
|
||||
'builtInCategory': 'Kategori bawaan tidak dapat dihapus',
|
||||
'selectCategory': 'Pilih Kategori',
|
||||
'desktopNotification': 'Notifikasi Desktop',
|
||||
'notificationTaskDone': 'Tugas selesai',
|
||||
'notificationTaskError': 'Tugas error',
|
||||
},
|
||||
};
|
||||
@@ -130,5 +130,8 @@ const itIT = {
|
||||
'categoryPath': 'Percorso categoria',
|
||||
'builtInCategory': 'La categoria predefinita non può essere eliminata',
|
||||
'selectCategory': 'Seleziona categoria',
|
||||
'desktopNotification': 'Notifiche Desktop',
|
||||
'notificationTaskDone': 'Attività completata',
|
||||
'notificationTaskError': 'Errore attività',
|
||||
},
|
||||
};
|
||||
@@ -95,5 +95,8 @@ const jaJP = {
|
||||
'launchAtStartup': '起動時に起動',
|
||||
'runAsMenubarApp': 'メニューバーアプリとして実行',
|
||||
'runAsMenubarAppDesc': 'Dockアイコンを非表示にして、メニューバーのみで実行',
|
||||
'desktopNotification': 'デスクトップ通知',
|
||||
'notificationTaskDone': 'タスク完了',
|
||||
'notificationTaskError': 'タスクエラー',
|
||||
}
|
||||
};
|
||||
@@ -127,5 +127,8 @@ const plPL = {
|
||||
'categoryPath': 'Ścieżka kategorii',
|
||||
'builtInCategory': 'Wbudowanej kategorii nie można usunąć',
|
||||
'selectCategory': 'Wybierz kategorię',
|
||||
'desktopNotification': 'Powiadomienia na pulpicie',
|
||||
'notificationTaskDone': 'Zadanie zakończone',
|
||||
'notificationTaskError': 'Błąd zadania',
|
||||
},
|
||||
};
|
||||
@@ -153,5 +153,8 @@ const ptBR = {
|
||||
'categoryPath': 'Caminho da Categoria',
|
||||
'builtInCategory': 'Categoria integrada não pode ser excluída',
|
||||
'selectCategory': 'Selecionar Categoria',
|
||||
'desktopNotification': 'Notificações na área de trabalho',
|
||||
'notificationTaskDone': 'Tarefa concluída',
|
||||
'notificationTaskError': 'Erro na tarefa',
|
||||
},
|
||||
};
|
||||
@@ -152,5 +152,8 @@ const ruRU = {
|
||||
'categoryPath': 'Путь категории',
|
||||
'builtInCategory': 'Встроенную категорию нельзя удалить',
|
||||
'selectCategory': 'Выбрать категорию',
|
||||
'desktopNotification': 'Уведомления',
|
||||
'notificationTaskDone': 'Задача завершена',
|
||||
'notificationTaskError': 'Ошибка задачи',
|
||||
}
|
||||
};
|
||||
@@ -129,5 +129,8 @@ const taTA = {
|
||||
'categoryPath': 'வகை பாதை',
|
||||
'builtInCategory': 'உள்ளமைக்கப்பட்ட வகையை நீக்க முடியாது',
|
||||
'selectCategory': 'வகையைத் தேர்ந்தெடுக்கவும்',
|
||||
'desktopNotification': 'கணினி அறிவிப்புகள்',
|
||||
'notificationTaskDone': 'பணி முடிந்தது',
|
||||
'notificationTaskError': 'பணி பிழை',
|
||||
},
|
||||
};
|
||||
@@ -193,5 +193,8 @@ const trTR = {
|
||||
'"@name" işlemi URL güncellemesi bekliyor. Yeni URL ile güncellemek istiyor musunuz?',
|
||||
'pendingUpdateYes': 'İşlemi Güncelle',
|
||||
'pendingUpdateNo': 'Yeni İşlem Oluştur',
|
||||
'desktopNotification': 'Masaüstü Bildirimleri',
|
||||
'notificationTaskDone': 'Görev tamamlandı',
|
||||
'notificationTaskError': 'Görev hatası',
|
||||
},
|
||||
};
|
||||
@@ -152,5 +152,8 @@ const ukUA = {
|
||||
'categoryPath': 'Шлях категорії',
|
||||
'builtInCategory': 'Вбудовану категорію неможливо видалити',
|
||||
'selectCategory': 'Вибрати категорію',
|
||||
'desktopNotification': 'Сповіщення',
|
||||
'notificationTaskDone': 'Завдання завершено',
|
||||
'notificationTaskError': 'Помилка завдання',
|
||||
},
|
||||
};
|
||||
@@ -122,5 +122,8 @@ const viVN = {
|
||||
'launchAtStartup': 'Khởi động cùng hệ thống',
|
||||
'runAsMenubarApp': 'Chạy như ứng dụng thanh menu',
|
||||
'runAsMenubarAppDesc': 'Ẩn biểu tượng Dock và chỉ chạy trong thanh menu',
|
||||
'desktopNotification': 'Thông báo trên màn hình',
|
||||
'notificationTaskDone': 'Nhiệm vụ hoàn thành',
|
||||
'notificationTaskError': 'Lỗi nhiệm vụ',
|
||||
},
|
||||
};
|
||||
@@ -188,5 +188,8 @@ const zhCN = {
|
||||
'pendingUpdateConfirm': '任务 "@name" 正在等待更新地址,是否使用新地址更新该任务?',
|
||||
'pendingUpdateYes': '更新任务',
|
||||
'pendingUpdateNo': '创建新任务',
|
||||
'desktopNotification': '桌面通知',
|
||||
'notificationTaskDone': '任务完成',
|
||||
'notificationTaskError': '任务失败',
|
||||
}
|
||||
};
|
||||
@@ -147,5 +147,8 @@ const zhTW = {
|
||||
'categoryPath': '分類路徑',
|
||||
'builtInCategory': '內建分類無法刪除',
|
||||
'selectCategory': '選擇分類',
|
||||
'desktopNotification': '桌面通知',
|
||||
'notificationTaskDone': '任務完成',
|
||||
'notificationTaskError': '任務失敗',
|
||||
}
|
||||
};
|
||||
@@ -265,6 +265,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.6"
|
||||
dbus:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: dbus
|
||||
sha256: d0c98dcd4f5169878b6cf8f6e0a52403a9dff371a3e2f019697accbf6f44a270
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.12"
|
||||
desktop_drop:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -406,6 +414,38 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.3"
|
||||
flutter_local_notifications:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_local_notifications
|
||||
sha256: "2b50e938a275e1ad77352d6a25e25770f4130baa61eaf02de7a9a884680954ad"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "20.1.0"
|
||||
flutter_local_notifications_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_local_notifications_linux
|
||||
sha256: dce0116868cedd2cdf768af0365fc37ff1cbef7c02c4f51d0587482e625868d0
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.0.0"
|
||||
flutter_local_notifications_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_local_notifications_platform_interface
|
||||
sha256: "23de31678a48c084169d7ae95866df9de5c9d2a44be3e5915a2ff067aeeba899"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "10.0.0"
|
||||
flutter_local_notifications_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_local_notifications_windows
|
||||
sha256: e97a1a3016512437d9c0b12fae7d1491c3c7b9aa7f03a69b974308840656b02a
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.1"
|
||||
flutter_localizations:
|
||||
dependency: "direct main"
|
||||
description: flutter
|
||||
@@ -1212,6 +1252,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.7"
|
||||
timezone:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: timezone
|
||||
sha256: dd14a3b83cfd7cb19e7888f1cbc20f258b8d71b54c06f79ac585f14093a287d1
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.10.1"
|
||||
timing:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
@@ -82,6 +82,7 @@ dependencies:
|
||||
ref: cf08af829f4a4145634f8a047108f505fdbe5eaa
|
||||
flutter_svg: ^2.0.17
|
||||
dart_ipc: ^1.0.1
|
||||
flutter_local_notifications: 20.1.0
|
||||
dependency_overrides:
|
||||
permission_handler_windows:
|
||||
git:
|
||||
|
||||
Reference in new issue
Block a user