forked from cwtch.im/cwtch-ui
make NixNotificationManager using flutter_local_notification
This commit is contained in:
parent
6859780873
commit
7509c20a62
BIN
assets/knott.png
BIN
assets/knott.png
Binary file not shown.
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 51 KiB |
|
@ -165,10 +165,10 @@ class CwtchNotifier {
|
||||||
var notification = data["notification"];
|
var notification = data["notification"];
|
||||||
|
|
||||||
if (notification == "SimpleEvent") {
|
if (notification == "SimpleEvent") {
|
||||||
notificationManager.notify(notificationSimple ?? "New Message");
|
notificationManager.notify(notificationSimple ?? "New Message", "", 0);
|
||||||
} else if (notification == "ContactInfo") {
|
} else if (notification == "ContactInfo") {
|
||||||
var contact = profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(identifier);
|
var contact = profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(identifier);
|
||||||
notificationManager.notify((notificationConversationInfo ?? "New Message from %1").replaceFirst("%1", (contact?.nickname ?? senderHandle.toString())));
|
notificationManager.notify((notificationConversationInfo ?? "New Message from %1").replaceFirst("%1", (contact?.nickname ?? senderHandle.toString())), data["ProfileOnion"], identifier);
|
||||||
}
|
}
|
||||||
|
|
||||||
profileCN.getProfile(data["ProfileOnion"])?.newMessage(
|
profileCN.getProfile(data["ProfileOnion"])?.newMessage(
|
||||||
|
@ -242,10 +242,10 @@ class CwtchNotifier {
|
||||||
profileCN.getProfile(data["ProfileOnion"])?.newMessage(identifier, idx, timestampSent, senderHandle, senderImage, isAuto, data["Data"], contenthash, selectedProfile, selectedConversation);
|
profileCN.getProfile(data["ProfileOnion"])?.newMessage(identifier, idx, timestampSent, senderHandle, senderImage, isAuto, data["Data"], contenthash, selectedProfile, selectedConversation);
|
||||||
|
|
||||||
if (notification == "SimpleEvent") {
|
if (notification == "SimpleEvent") {
|
||||||
notificationManager.notify(notificationSimple ?? "New Message");
|
notificationManager.notify(notificationSimple ?? "New Message", "", 0);
|
||||||
} else if (notification == "ContactInfo") {
|
} else if (notification == "ContactInfo") {
|
||||||
var contact = profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(identifier);
|
var contact = profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(identifier);
|
||||||
notificationManager.notify((notificationConversationInfo ?? "New Message from %1").replaceFirst("%1", (contact?.nickname ?? senderHandle.toString())));
|
notificationManager.notify((notificationConversationInfo ?? "New Message from %1").replaceFirst("%1", (contact?.nickname ?? senderHandle.toString())), data["ProfileOnion"], identifier);
|
||||||
}
|
}
|
||||||
appState.notifyProfileUnread();
|
appState.notifyProfileUnread();
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ import 'dart:convert';
|
||||||
import 'package:cwtch/config.dart';
|
import 'package:cwtch/config.dart';
|
||||||
import 'package:cwtch/notification_manager.dart';
|
import 'package:cwtch/notification_manager.dart';
|
||||||
import 'package:cwtch/themes/cwtch.dart';
|
import 'package:cwtch/themes/cwtch.dart';
|
||||||
|
import 'package:cwtch/views/doublecolview.dart';
|
||||||
import 'package:cwtch/views/messageview.dart';
|
import 'package:cwtch/views/messageview.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:cwtch/cwtch/ffi.dart';
|
import 'package:cwtch/cwtch/ffi.dart';
|
||||||
|
@ -17,8 +18,11 @@ import 'cwtch/cwtch.dart';
|
||||||
import 'cwtch/cwtchNotifier.dart';
|
import 'cwtch/cwtchNotifier.dart';
|
||||||
import 'licenses.dart';
|
import 'licenses.dart';
|
||||||
import 'models/appstate.dart';
|
import 'models/appstate.dart';
|
||||||
|
import 'models/contactlist.dart';
|
||||||
|
import 'models/profile.dart';
|
||||||
import 'models/profilelist.dart';
|
import 'models/profilelist.dart';
|
||||||
import 'models/servers.dart';
|
import 'models/servers.dart';
|
||||||
|
import 'views/contactsview.dart';
|
||||||
import 'views/profilemgrview.dart';
|
import 'views/profilemgrview.dart';
|
||||||
import 'views/splashView.dart';
|
import 'views/splashView.dart';
|
||||||
import 'dart:io' show Platform, exit, sleep;
|
import 'dart:io' show Platform, exit, sleep;
|
||||||
|
@ -78,10 +82,10 @@ class FlwtchState extends State<Flwtch> with WindowListener {
|
||||||
var cwtchNotifier = new CwtchNotifier(profs, globalSettings, globalErrorHandler, globalTorStatus, NullNotificationsManager(), globalAppState, globalServersList);
|
var cwtchNotifier = new CwtchNotifier(profs, globalSettings, globalErrorHandler, globalTorStatus, NullNotificationsManager(), globalAppState, globalServersList);
|
||||||
cwtch = CwtchGomobile(cwtchNotifier);
|
cwtch = CwtchGomobile(cwtchNotifier);
|
||||||
} else if (Platform.isLinux) {
|
} else if (Platform.isLinux) {
|
||||||
var cwtchNotifier = new CwtchNotifier(profs, globalSettings, globalErrorHandler, globalTorStatus, newDesktopNotificationsManager(), globalAppState, globalServersList);
|
var cwtchNotifier = new CwtchNotifier(profs, globalSettings, globalErrorHandler, globalTorStatus, newDesktopNotificationsManager(_notificationSelectConvo), globalAppState, globalServersList);
|
||||||
cwtch = CwtchFfi(cwtchNotifier);
|
cwtch = CwtchFfi(cwtchNotifier);
|
||||||
} else {
|
} else {
|
||||||
var cwtchNotifier = new CwtchNotifier(profs, globalSettings, globalErrorHandler, globalTorStatus, newDesktopNotificationsManager(), globalAppState, globalServersList);
|
var cwtchNotifier = new CwtchNotifier(profs, globalSettings, globalErrorHandler, globalTorStatus, newDesktopNotificationsManager(_notificationSelectConvo), globalAppState, globalServersList);
|
||||||
cwtch = CwtchFfi(cwtchNotifier);
|
cwtch = CwtchFfi(cwtchNotifier);
|
||||||
}
|
}
|
||||||
print("initState: invoking cwtch.Start()");
|
print("initState: invoking cwtch.Start()");
|
||||||
|
@ -182,36 +186,43 @@ class FlwtchState extends State<Flwtch> with WindowListener {
|
||||||
// coder beware: args["RemotePeer"] is actually a handle, and could be eg a groupID
|
// coder beware: args["RemotePeer"] is actually a handle, and could be eg a groupID
|
||||||
Future<void> _externalNotificationClicked(MethodCall call) async {
|
Future<void> _externalNotificationClicked(MethodCall call) async {
|
||||||
var args = jsonDecode(call.arguments);
|
var args = jsonDecode(call.arguments);
|
||||||
var profile = profs.getProfile(args["ProfileOnion"])!;
|
_notificationSelectConvo(args["ProfileOnion"], args["Handle"]);
|
||||||
var convo = profile.contactList.getContact(args["Handle"])!;
|
}
|
||||||
|
|
||||||
|
Future<void> _notificationSelectConvo(String profileOnion, int convoId) async {
|
||||||
|
var profile = profs.getProfile(profileOnion)!;
|
||||||
|
var convo = profile.contactList.getContact(convoId)!;
|
||||||
|
if (profileOnion.isEmpty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
Provider.of<AppState>(navKey.currentContext!, listen: false).initialScrollIndex = convo.unreadMessages;
|
Provider.of<AppState>(navKey.currentContext!, listen: false).initialScrollIndex = convo.unreadMessages;
|
||||||
convo.unreadMessages = 0;
|
convo.unreadMessages = 0;
|
||||||
|
|
||||||
// single pane mode pushes; double pane mode reads AppState.selectedProfile/Conversation
|
// Clear nav path back to root
|
||||||
var isLandscape = Provider.of<AppState>(navKey.currentContext!, listen: false).isLandscape(navKey.currentContext!);
|
while (navKey.currentState!.canPop()) {
|
||||||
if (Provider.of<Settings>(navKey.currentContext!, listen: false).uiColumns(isLandscape).length == 1) {
|
navKey.currentState!.pop();
|
||||||
while (navKey.currentState!.canPop()) {
|
|
||||||
print("messageview already open; popping before pushing replacement");
|
|
||||||
navKey.currentState!.pop();
|
|
||||||
}
|
|
||||||
navKey.currentState?.push(
|
|
||||||
MaterialPageRoute<void>(
|
|
||||||
builder: (BuildContext builderContext) {
|
|
||||||
return MultiProvider(
|
|
||||||
providers: [
|
|
||||||
ChangeNotifierProvider.value(value: profile),
|
|
||||||
ChangeNotifierProvider.value(value: convo),
|
|
||||||
],
|
|
||||||
builder: (context, child) => MessageView(),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
//dual pane
|
|
||||||
Provider.of<AppState>(navKey.currentContext!, listen: false).selectedProfile = args["ProfileOnion"];
|
|
||||||
Provider.of<AppState>(navKey.currentContext!, listen: false).selectedConversation = args["Handle"];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Provider.of<AppState>(navKey.currentContext!, listen: false).selectedConversation = null;
|
||||||
|
Provider.of<AppState>(navKey.currentContext!, listen: false).selectedProfile = profileOnion;
|
||||||
|
Provider.of<AppState>(navKey.currentContext!, listen: false).selectedConversation = convoId;
|
||||||
|
|
||||||
|
Navigator.of(navKey.currentContext!).push(
|
||||||
|
MaterialPageRoute<void>(
|
||||||
|
settings: RouteSettings(name: "conversations"),
|
||||||
|
builder: (BuildContext buildcontext) {
|
||||||
|
return OrientationBuilder(builder: (orientationBuilderContext, orientation) {
|
||||||
|
return MultiProvider(
|
||||||
|
providers: [ChangeNotifierProvider<ProfileInfoState>.value(value: profile), ChangeNotifierProvider<ContactListState>.value(value: profile.contactList)],
|
||||||
|
builder: (innercontext, widget) {
|
||||||
|
var appState = Provider.of<AppState>(navKey.currentContext!);
|
||||||
|
var settings = Provider.of<Settings>(navKey.currentContext!);
|
||||||
|
return settings.uiColumns(appState.isLandscape(innercontext)).length > 1 ? DoubleColumnView() : MessageView();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// using windowManager flutter plugin until proper lifecycle management lands in desktop
|
// using windowManager flutter plugin until proper lifecycle management lands in desktop
|
||||||
|
|
|
@ -1,38 +1,24 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:cwtch/main.dart';
|
import 'package:cwtch/main.dart';
|
||||||
import 'package:win_toast/win_toast.dart';
|
import 'package:win_toast/win_toast.dart';
|
||||||
import 'package:desktop_notifications/desktop_notifications.dart';
|
import 'package:desktop_notifications/desktop_notifications.dart';
|
||||||
|
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||||
import 'package:path/path.dart' as path;
|
import 'package:path/path.dart' as path;
|
||||||
|
|
||||||
import 'config.dart';
|
import 'config.dart';
|
||||||
|
|
||||||
// NotificationsManager provides a wrapper around platform specific notifications logic.
|
// NotificationsManager provides a wrapper around platform specific notifications logic.
|
||||||
abstract class NotificationsManager {
|
abstract class NotificationsManager {
|
||||||
Future<void> notify(String message);
|
Future<void> notify(String message, String profile, int conversationId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// NullNotificationsManager ignores all notification requests
|
// NullNotificationsManager ignores all notification requests
|
||||||
class NullNotificationsManager implements NotificationsManager {
|
class NullNotificationsManager implements NotificationsManager {
|
||||||
@override
|
@override
|
||||||
Future<void> notify(String message) async {}
|
Future<void> notify(String message, String profile, int conversationId) async {}
|
||||||
}
|
|
||||||
|
|
||||||
// LinuxNotificationsManager uses the desktop_notifications package to implement
|
|
||||||
// the standard dbus-powered linux desktop notifications.
|
|
||||||
class LinuxNotificationsManager implements NotificationsManager {
|
|
||||||
int previous_id = 0;
|
|
||||||
late NotificationsClient client;
|
|
||||||
|
|
||||||
LinuxNotificationsManager(NotificationsClient client) {
|
|
||||||
this.client = client;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> notify(String message) async {
|
|
||||||
var iconPath = Uri.file(path.join(path.current, "cwtch.png"));
|
|
||||||
client.notify(message, appName: "cwtch", appIcon: iconPath.toString(), replacesId: this.previous_id).then((Notification value) => previous_id = value.id);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Windows Notification Manager uses https://pub.dev/packages/desktoasts to implement
|
// Windows Notification Manager uses https://pub.dev/packages/desktoasts to implement
|
||||||
|
@ -47,7 +33,7 @@ class WindowsNotificationManager implements NotificationsManager {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> notify(String message) async {
|
Future<void> notify(String message, String profile, int conversationId) async {
|
||||||
if (initialized && !globalAppState.focus) {
|
if (initialized && !globalAppState.focus) {
|
||||||
if (!active) {
|
if (!active) {
|
||||||
active = true;
|
active = true;
|
||||||
|
@ -64,16 +50,73 @@ class WindowsNotificationManager implements NotificationsManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
NotificationsManager newDesktopNotificationsManager() {
|
class NotificationPayload {
|
||||||
if (Platform.isLinux) {
|
late String profileOnion;
|
||||||
|
late int convoId;
|
||||||
|
|
||||||
|
NotificationPayload(String po, int cid) {
|
||||||
|
profileOnion = po;
|
||||||
|
convoId = cid;
|
||||||
|
}
|
||||||
|
|
||||||
|
NotificationPayload.fromJson(Map<String, dynamic> json)
|
||||||
|
: profileOnion = json['profileOnion'],
|
||||||
|
convoId = json['convoId'];
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => {
|
||||||
|
'profileOnion': profileOnion,
|
||||||
|
'convoId': convoId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// FlutterLocalNotificationsPlugin based NotificationManager that handles Linux and MacOS
|
||||||
|
// Todo: it can also handle Android, do we want to migrate away from our manual solution?
|
||||||
|
class NixNotificationManager implements NotificationsManager {
|
||||||
|
late FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin;
|
||||||
|
late Future<void> Function(String, int) notificationSelectConvo;
|
||||||
|
|
||||||
|
NixNotificationManager(Future<void> Function(String, int) notificationSelectConvo) {
|
||||||
|
this.notificationSelectConvo = notificationSelectConvo;
|
||||||
|
flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();
|
||||||
|
final MacOSInitializationSettings initializationSettingsMacOS = MacOSInitializationSettings();
|
||||||
|
final LinuxInitializationSettings initializationSettingsLinux =
|
||||||
|
LinuxInitializationSettings(
|
||||||
|
defaultActionName: 'Open notification',
|
||||||
|
defaultIcon: AssetsLinuxIcon('assets/knott.png'),
|
||||||
|
);
|
||||||
|
|
||||||
|
final InitializationSettings initializationSettings = InitializationSettings(android: null, iOS: null, macOS: initializationSettingsMacOS, linux: initializationSettingsLinux);
|
||||||
|
|
||||||
|
flutterLocalNotificationsPlugin.resolvePlatformSpecificImplementation<MacOSFlutterLocalNotificationsPlugin>()?.requestPermissions(
|
||||||
|
alert: true,
|
||||||
|
badge: true,
|
||||||
|
sound: true,
|
||||||
|
);
|
||||||
|
|
||||||
|
scheduleMicrotask(() async {
|
||||||
|
await flutterLocalNotificationsPlugin.initialize(initializationSettings, onSelectNotification: selectNotification);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void selectNotification(String? payloadJson) async {
|
||||||
|
if (payloadJson != null) {
|
||||||
|
Map<String, dynamic> payloadMap = jsonDecode(payloadJson);
|
||||||
|
var payload = NotificationPayload.fromJson(payloadMap);
|
||||||
|
notificationSelectConvo(payload.profileOnion, payload.convoId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> notify(String message, String profile, int conversationId) async {
|
||||||
|
await flutterLocalNotificationsPlugin.show(0, 'Cwtch', message, null, payload: jsonEncode(NotificationPayload(profile, conversationId)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NotificationsManager newDesktopNotificationsManager(Future<void> Function(String profileOnion, int convoId) notificationSelectConvo) {
|
||||||
|
if (Platform.isLinux || Platform.isMacOS) {
|
||||||
try {
|
try {
|
||||||
// Test that we can actually access DBUS. Otherwise return a null
|
return NixNotificationManager(notificationSelectConvo);
|
||||||
// notifications manager...
|
|
||||||
NotificationsClient client = NotificationsClient();
|
|
||||||
client.getCapabilities();
|
|
||||||
return LinuxNotificationsManager(client);
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
EnvironmentConfig.debugLog("Attempted to access DBUS for notifications but failed. Switching off notifications.");
|
EnvironmentConfig.debugLog("Failed to create NixNotificationManager. Switching off notifications.");
|
||||||
}
|
}
|
||||||
} else if (Platform.isWindows) {
|
} else if (Platform.isWindows) {
|
||||||
try {
|
try {
|
||||||
|
@ -82,5 +125,6 @@ NotificationsManager newDesktopNotificationsManager() {
|
||||||
EnvironmentConfig.debugLog("Failed to create Windows desktoasts notification manager");
|
EnvironmentConfig.debugLog("Failed to create Windows desktoasts notification manager");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return NullNotificationsManager();
|
return NullNotificationsManager();
|
||||||
}
|
}
|
||||||
|
|
39
pubspec.lock
39
pubspec.lock
|
@ -189,14 +189,14 @@ packages:
|
||||||
name: dbus
|
name: dbus
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.5.6"
|
version: "0.6.8"
|
||||||
desktop_notifications:
|
desktop_notifications:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: desktop_notifications
|
name: desktop_notifications
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.5.0"
|
version: "0.6.0"
|
||||||
fake_async:
|
fake_async:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -256,6 +256,27 @@ packages:
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.0-rc.9"
|
version: "3.0.0-rc.9"
|
||||||
|
flutter_local_notifications:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: flutter_local_notifications
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "9.3.2"
|
||||||
|
flutter_local_notifications_linux:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: flutter_local_notifications_linux
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "0.4.1+1"
|
||||||
|
flutter_local_notifications_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: flutter_local_notifications_platform_interface
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "5.0.0"
|
||||||
flutter_localizations:
|
flutter_localizations:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description: flutter
|
description: flutter
|
||||||
|
@ -519,13 +540,6 @@ packages:
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.5"
|
version: "2.0.5"
|
||||||
pedantic:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: pedantic
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "1.11.1"
|
|
||||||
petitparser:
|
petitparser:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -671,6 +685,13 @@ packages:
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.4.3"
|
version: "0.4.3"
|
||||||
|
timezone:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: timezone
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "0.8.0"
|
||||||
timing:
|
timing:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
|
@ -34,7 +34,6 @@ dependencies:
|
||||||
cupertino_icons: ^1.0.0
|
cupertino_icons: ^1.0.0
|
||||||
ffi: ^1.0.0
|
ffi: ^1.0.0
|
||||||
path_provider: ^2.0.0
|
path_provider: ^2.0.0
|
||||||
desktop_notifications: 0.5.0
|
|
||||||
crypto: 3.0.1
|
crypto: 3.0.1
|
||||||
|
|
||||||
glob: any
|
glob: any
|
||||||
|
@ -42,8 +41,10 @@ dependencies:
|
||||||
file_picker: ^4.3.2
|
file_picker: ^4.3.2
|
||||||
file_picker_desktop: ^1.1.0
|
file_picker_desktop: ^1.1.0
|
||||||
url_launcher: ^6.0.18
|
url_launcher: ^6.0.18
|
||||||
win_toast: ^0.0.2
|
|
||||||
window_manager: ^0.1.4
|
window_manager: ^0.1.4
|
||||||
|
desktop_notifications: 0.6.0
|
||||||
|
win_toast: ^0.0.2
|
||||||
|
flutter_local_notifications: 9.3.2
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
msix: ^2.1.3
|
msix: ^2.1.3
|
||||||
|
|
Loading…
Reference in New Issue