From bc4d52b86729f480f1a1802f9254881ab40eacfb Mon Sep 17 00:00:00 2001 From: ndonkoHenri Date: Mon, 3 Mar 2025 03:53:46 +0100 Subject: [PATCH 1/9] initial commit --- client/pubspec.lock | 17 +- client/pubspec.yaml | 2 + packages/flet_notifications/.gitignore | 31 ++ packages/flet_notifications/.metadata | 10 + packages/flet_notifications/CHANGELOG.md | 3 + packages/flet_notifications/LICENSE | 201 ++++++++++++ packages/flet_notifications/README.md | 3 + .../flet_notifications/analysis_options.yaml | 4 + .../lib/flet_notifications.dart | 3 + .../lib/src/create_control.dart | 37 +++ .../lib/src/notifications.dart | 67 ++++ .../flet_notifications/lib/src/service.dart | 45 +++ .../lib/src/utils/notifications.dart | 310 ++++++++++++++++++ packages/flet_notifications/pubspec.yaml | 27 ++ sdk/python/packages/flet/src/flet/__init__.py | 14 + .../flet/src/flet/core/notifications.py | 229 +++++++++++++ 16 files changed, 1002 insertions(+), 1 deletion(-) create mode 100644 packages/flet_notifications/.gitignore create mode 100644 packages/flet_notifications/.metadata create mode 100644 packages/flet_notifications/CHANGELOG.md create mode 100644 packages/flet_notifications/LICENSE create mode 100644 packages/flet_notifications/README.md create mode 100644 packages/flet_notifications/analysis_options.yaml create mode 100644 packages/flet_notifications/lib/flet_notifications.dart create mode 100644 packages/flet_notifications/lib/src/create_control.dart create mode 100644 packages/flet_notifications/lib/src/notifications.dart create mode 100644 packages/flet_notifications/lib/src/service.dart create mode 100644 packages/flet_notifications/lib/src/utils/notifications.dart create mode 100644 packages/flet_notifications/pubspec.yaml create mode 100644 sdk/python/packages/flet/src/flet/core/notifications.py diff --git a/client/pubspec.lock b/client/pubspec.lock index 703fccc576..81b596fa5f 100644 --- a/client/pubspec.lock +++ b/client/pubspec.lock @@ -81,6 +81,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.0" + awesome_notifications: + dependency: transitive + description: + name: awesome_notifications + sha256: d051ffb694a53da216ff13d02c8ec645d75320048262f7e6b3c1d95a4f54c902 + url: "https://pub.dev" + source: hosted + version: "0.10.0" boolean_selector: dependency: transitive description: @@ -263,7 +271,7 @@ packages: path: "../packages/flet" relative: true source: path - version: "0.27.1" + version: "0.27.4" flet_ads: dependency: "direct main" description: @@ -327,6 +335,13 @@ packages: url: "https://github.com/flet-dev/flet-map.git" source: git version: "0.1.0" + flet_notifications: + dependency: "direct main" + description: + path: "../packages/flet_notifications" + relative: true + source: path + version: "0.1.0" flet_permission_handler: dependency: "direct main" description: diff --git a/client/pubspec.yaml b/client/pubspec.yaml index c6f003aba9..d0f8d6ec1c 100644 --- a/client/pubspec.yaml +++ b/client/pubspec.yaml @@ -93,6 +93,8 @@ dependencies: url: https://github.com/flet-dev/flet-flashlight.git ref: 0.1.0 path: src/flutter/flet_flashlight + flet_notifications: + path: ../packages/flet_notifications url_strategy: ^0.2.0 cupertino_icons: ^1.0.6 diff --git a/packages/flet_notifications/.gitignore b/packages/flet_notifications/.gitignore new file mode 100644 index 0000000000..e050eb5129 --- /dev/null +++ b/packages/flet_notifications/.gitignore @@ -0,0 +1,31 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +/pubspec.lock +**/doc/api/ +.dart_tool/ +build/ +.flutter-plugins +.flutter-plugins-dependencies \ No newline at end of file diff --git a/packages/flet_notifications/.metadata b/packages/flet_notifications/.metadata new file mode 100644 index 0000000000..07d8623a38 --- /dev/null +++ b/packages/flet_notifications/.metadata @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "2e9cb0aa71a386a91f73f7088d115c0d96654829" + channel: "stable" + +project_type: package diff --git a/packages/flet_notifications/CHANGELOG.md b/packages/flet_notifications/CHANGELOG.md new file mode 100644 index 0000000000..2f5af7e43b --- /dev/null +++ b/packages/flet_notifications/CHANGELOG.md @@ -0,0 +1,3 @@ +# 0.1.0 + +Initial release of the package. \ No newline at end of file diff --git a/packages/flet_notifications/LICENSE b/packages/flet_notifications/LICENSE new file mode 100644 index 0000000000..f49a4e16e6 --- /dev/null +++ b/packages/flet_notifications/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/packages/flet_notifications/README.md b/packages/flet_notifications/README.md new file mode 100644 index 0000000000..5e948f46d5 --- /dev/null +++ b/packages/flet_notifications/README.md @@ -0,0 +1,3 @@ +# Flet `Notifications` control + +`Notifications` control to use in Flet apps. \ No newline at end of file diff --git a/packages/flet_notifications/analysis_options.yaml b/packages/flet_notifications/analysis_options.yaml new file mode 100644 index 0000000000..a5744c1cfb --- /dev/null +++ b/packages/flet_notifications/analysis_options.yaml @@ -0,0 +1,4 @@ +include: package:flutter_lints/flutter.yaml + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/packages/flet_notifications/lib/flet_notifications.dart b/packages/flet_notifications/lib/flet_notifications.dart new file mode 100644 index 0000000000..3b019b6ff7 --- /dev/null +++ b/packages/flet_notifications/lib/flet_notifications.dart @@ -0,0 +1,3 @@ +library flet_lottie; + +export "src/create_control.dart" show createControl, ensureInitialized; diff --git a/packages/flet_notifications/lib/src/create_control.dart b/packages/flet_notifications/lib/src/create_control.dart new file mode 100644 index 0000000000..8a5ee4d5d2 --- /dev/null +++ b/packages/flet_notifications/lib/src/create_control.dart @@ -0,0 +1,37 @@ +import 'package:awesome_notifications/awesome_notifications.dart'; +import 'package:flet/flet.dart'; +import 'package:flet_notifications/src/service.dart'; +import 'package:flutter/material.dart'; + +import 'notifications.dart'; + +CreateControlFactory createControl = (CreateControlArgs args) { + switch (args.control.type) { + case "notifications": + return NotificationControl( + parent: args.parent, + control: args.control, + nextChild: args.nextChild, + backend: args.backend); + default: + return null; + } +}; + +void ensureInitialized() async{ + await NotificationService.initializeLocalNotifications( + channels: [ + NotificationChannel( + channelKey: 'alerts', + channelName: 'Notification Channel Name', + channelDescription: 'Notification Channel Description', + playSound: true, + onlyAlertOnce: true, + groupAlertBehavior: GroupAlertBehavior.Children, + importance: NotificationImportance.High, + defaultPrivacy: NotificationPrivacy.Private, + defaultColor: Colors.deepPurple, + ledColor: Colors.deepPurple) + ] + ); +} diff --git a/packages/flet_notifications/lib/src/notifications.dart b/packages/flet_notifications/lib/src/notifications.dart new file mode 100644 index 0000000000..8793cf56a8 --- /dev/null +++ b/packages/flet_notifications/lib/src/notifications.dart @@ -0,0 +1,67 @@ +import 'package:flet/flet.dart'; +import 'package:flet_notifications/src/utils/notifications.dart'; +import 'package:flutter/material.dart'; + +import 'service.dart'; + +class NotificationControl extends StatefulWidget { + final Control? parent; + final Control control; + final Widget? nextChild; + final FletControlBackend backend; + + const NotificationControl( + {super.key, + required this.parent, + required this.control, + required this.nextChild, + required this.backend}); + + @override + State createState() => _NotificationControlState(); +} + +class _NotificationControlState extends State + with FletStoreMixin { + @override + void initState() { + super.initState(); + // _initializeService(); + } + + Future _initializeService() async { + var channels = parseNotificationChannels( + widget.control, "channels", Theme.of(context)); + await NotificationService.initializeLocalNotifications( + channels: channels, + languageCode: widget.control.attrString("languageCode"), + ); + } + + @override + Widget build(BuildContext context) { + debugPrint( + "Notification build: ${widget.control.id} (${widget.control.hashCode})"); + + () async { + widget.backend.subscribeMethods(widget.control.id, + (methodName, args) async { + switch (methodName) { + case "show": + var content = + notificationContentFromJSON(Theme.of(context), args["content"]); + var actionButtons = notificationActionButtonsFromJSON( + Theme.of(context), args["action_buttons"]); + if (content != null) { + debugPrint("NotificationService.showNotification"); + NotificationService.showNotification(content, + actionButtons: actionButtons); + } + } + return null; + }); + }(); + + return const SizedBox.shrink(); + } +} diff --git a/packages/flet_notifications/lib/src/service.dart b/packages/flet_notifications/lib/src/service.dart new file mode 100644 index 0000000000..fdc5cb168e --- /dev/null +++ b/packages/flet_notifications/lib/src/service.dart @@ -0,0 +1,45 @@ +import 'package:awesome_notifications/awesome_notifications.dart'; +import 'package:flutter/material.dart'; + +class NotificationService { + static Future initializeLocalNotifications( + {required List channels, String? languageCode}) async { + await AwesomeNotifications().initialize( + null, //'resource://drawable/res_app_icon',// + channels, + languageCode: languageCode, + debug: true); + + await AwesomeNotifications().isNotificationAllowed().then((isAllowed) { + if (!isAllowed) { + AwesomeNotifications().requestPermissionToSendNotifications(); + } + }); + + await AwesomeNotifications().setListeners( + onActionReceivedMethod: onActionReceivedMethod, + onDismissActionReceivedMethod: (receivedNotification) async { + debugPrint('Notification dismissed: ${receivedNotification.id}'); + }, + onNotificationDisplayedMethod: (receivedNotification) async { + debugPrint('Notification displayed: ${receivedNotification.id}'); + }, + onNotificationCreatedMethod: (receivedNotification) async { + debugPrint('Notification created: ${receivedNotification.id}'); + }, + ); + } + + static Future onActionReceivedMethod( + ReceivedAction receivedAction) async { + debugPrint('Notification Action received'); + } + + static void showNotification(NotificationContent content, + {List? actionButtons}) { + AwesomeNotifications().createNotification( + content: content, + actionButtons: actionButtons, + ); + } +} diff --git a/packages/flet_notifications/lib/src/utils/notifications.dart b/packages/flet_notifications/lib/src/utils/notifications.dart new file mode 100644 index 0000000000..beb9dd064a --- /dev/null +++ b/packages/flet_notifications/lib/src/utils/notifications.dart @@ -0,0 +1,310 @@ +import 'dart:convert'; + +import 'package:awesome_notifications/awesome_notifications.dart'; +import 'package:flet/flet.dart'; +import 'package:flutter/material.dart'; + +NotificationContent? parseNotificationContent( + Control control, String propName, ThemeData theme, + [NotificationContent? defValue]) { + var v = control.attrString(propName); + if (v == null) { + return defValue; + } + final j1 = json.decode(v); + return notificationContentFromJSON(j1, theme, defValue); +} + +NotificationContent? notificationContentFromJSON(ThemeData theme, dynamic j, + [NotificationContent? defValue]) { + if (j == null) return defValue; + + j = json.decode(j); + var id = parseInt(j['id']); + var channelKey = j['channel_key']; + + if (j == null || id == null || channelKey == null) { + return defValue; + } + + return NotificationContent( + id: id, + channelKey: channelKey, + title: j["title"], + body: j["body"], + titleLocKey: j["title_loc_key"], + bodyLocKey: j["body_loc_key"], + //titleLocArgs: , + //bodyLocArgs:, + groupKey: j["group_key"], + summary: j["summary"], + icon: j["icon"], + largeIcon: j["large_icon"], + bigPicture: j["big_picture"], + customSound: j["custom_sound"], + showWhen: parseBool(j["show_when"], true)!, + wakeUpScreen: parseBool(j["wake_up_screen"], false)!, + fullScreenIntent: parseBool(j["full_screen_intent"], false)!, + criticalAlert: parseBool(j["critical_alert"], false)!, + roundedLargeIcon: parseBool(j["rounded_large_icon"], false)!, + roundedBigPicture: parseBool(j["rounded_big_picture"], false)!, + autoDismissible: parseBool(j["auto_dismissible"], true)!, + color: parseColor(theme, j["color"]), + timeoutAfter: durationFromJSON(j["timeout_after"]), + chronometer: durationFromJSON(j["chronometer"]), + backgroundColor: parseColor(theme, j["bgcolor"]), + hideLargeIconOnExpand: parseBool(j["hide_large_icon_on_expand"], false)!, + locked: parseBool(j["locked"], false)!, + progress: parseDouble(j["progress"]), + badge: parseInt(j["badge"]), + ticker: j["ticker"], + displayOnForeground: parseBool(j["display_on_foreground"], true)!, + displayOnBackground: parseBool(j["display_on_background"], true)!, + duration: durationFromJSON(j["duration"]), + playbackSpeed: parseDouble(j["playback_speed"]), + actionType: parseActionType(j["action_type"], ActionType.Default)!, + category: parseNotificationCategory(j["category"]), + notificationLayout: + parseNotificationLayout(j["layout"], NotificationLayout.Default)!, + ); +} + +NotificationActionButton? notificationActionButtonFromJSON( + ThemeData theme, dynamic j, + [NotificationActionButton? defValue]) { + if (j == null) return defValue; + + var key = j['key']; + var label = j['label']; + + if (j == null || key == null || label == null) { + return defValue; + } + + return NotificationActionButton( + key: key, + label: label, + enabled: !parseBool(j["disabled"], false)!, + isAuthenticationRequired: parseBool(j["requires_authentication"], false)!, + isDangerousOption: parseBool(j["dangerous"], false)!, + requireInputText: parseBool(j["require_text_input"], false)!, + showInCompactView: parseBool(j["show_in_compact_view"], true)!, + autoDismissible: parseBool(j["auto_dismissible"], true)!, + color: parseColor(theme, j["color"]), + icon: j["icon"], + actionType: parseActionType(j["action_type"], ActionType.Default)!, + ); +} + +List? notificationActionButtonsFromJSON( + ThemeData theme, dynamic j, + [List? defValue]) { + if (j == null) return defValue; + + j = json.decode(j); + if (j == null) return defValue; + + var buttons = []; + for (var b in j) { + var actionButton = notificationActionButtonFromJSON(theme, b); + if (actionButton != null) { + buttons.add(actionButton); + } + } + return buttons; +} + +List parseNotificationChannels( + Control control, String propName, ThemeData theme, + [List? defValue]) { + var v = control.attrString(propName); + if (v == null) { + return defValue ?? []; + } + + final List jsonList = json.decode(v); + if (jsonList.isEmpty) { + return defValue ?? []; + } + + List channels = []; + for (var j in jsonList) { + var channel = notificationChannelFromJSON(theme, j); + if (channel != null) { + channels.add(channel); + } + } + + return channels; +} + +NotificationChannel? notificationChannelFromJSON(ThemeData theme, dynamic j, + [NotificationChannel? defValue]) { + if (j == null) return defValue; + + var channelKey = j['channel_key']; + var channelName = j['channel_name']; + var channelDescription = j['channel_description']; + + if (j == null || + channelKey == null || + channelName == null || + channelDescription == null) { + return defValue; + } + + return NotificationChannel( + channelKey: channelKey, + channelName: channelName, + channelDescription: channelDescription, + channelGroupKey: j["channel_group_key"], + channelShowBadge: parseBool(j["channel_show_badge"]), + criticalAlerts: parseBool(j["critical_alerts"]), + defaultColor: parseColor(theme, j["default_color"]), + enableLights: parseBool(j["enable_lights"]), + enableVibration: parseBool(j["enable_vibration"]), + ledColor: parseColor(theme, j["led_color"]), + ledOnMs: parseInt(j["led_on_ms"]), + ledOffMs: parseInt(j["led_off_ms"]), + onlyAlertOnce: parseBool(j["only_alert_once"]), + playSound: parseBool(j["play_sound"]), + soundSource: j["sound_source"], + groupKey: j["group_key"], + icon: j["icon"], + locked: parseBool(j["locked"], false)!, + defaultPrivacy: parseNotificationPrivacy(j["privacy"]), + groupSort: parseGroupSort(j["group_sort"]), + importance: parseNotificationImportance(j["importance"]), + defaultRingtoneType: parseRingtoneType(j["ringtone_type"]), + groupAlertBehavior: parseGroupAlertBehavior(j["group_alert_behavior"]), + ); +} + +ActionType? parseActionType(String? value, [ActionType? defaultActionType]) { + if (value == null) { + return defaultActionType; + } + const Map actionMap = { + 'default': ActionType.Default, + 'disabled': ActionType.DisabledAction, + 'keepontop': ActionType.KeepOnTop, + 'silent': ActionType.SilentAction, + 'silentbackground': ActionType.SilentBackgroundAction, + 'dismiss': ActionType.DismissAction, + }; + return actionMap[value.toLowerCase()] ?? defaultActionType; +} + +NotificationCategory? parseNotificationCategory(String? value, + [NotificationCategory? defValue]) { + if (value == null) { + return defValue; + } + const Map categoryMap = { + 'alarm': NotificationCategory.Alarm, + 'call': NotificationCategory.Call, + 'email': NotificationCategory.Email, + 'error': NotificationCategory.Error, + 'event': NotificationCategory.Event, + 'localsharing': NotificationCategory.LocalSharing, + 'message': NotificationCategory.Message, + 'missedcall': NotificationCategory.MissedCall, + 'navigation': NotificationCategory.Navigation, + 'progress': NotificationCategory.Progress, + 'promo': NotificationCategory.Promo, + 'recommendation': NotificationCategory.Recommendation, + 'reminder': NotificationCategory.Reminder, + 'service': NotificationCategory.Service, + 'social': NotificationCategory.Social, + 'status': NotificationCategory.Status, + 'stopwatch': NotificationCategory.StopWatch, + 'transport': NotificationCategory.Transport, + 'workout': NotificationCategory.Workout, + }; + return categoryMap[value.toLowerCase()] ?? defValue; +} + +NotificationLayout? parseNotificationLayout(String? value, + [NotificationLayout? defValue]) { + if (value == null) { + return defValue; + } + const Map layoutMap = { + 'default': NotificationLayout.Default, + 'bigpicture': NotificationLayout.BigPicture, + 'bigtext': NotificationLayout.BigText, + 'inbox': NotificationLayout.Inbox, + 'progressbar': NotificationLayout.ProgressBar, + 'messaging': NotificationLayout.Messaging, + 'messaginggroup': NotificationLayout.MessagingGroup, + 'mediaplayer': NotificationLayout.MediaPlayer, + }; + return layoutMap[value.toLowerCase()] ?? defValue; +} + +NotificationPrivacy? parseNotificationPrivacy(String? value, + [NotificationPrivacy? defValue]) { + if (value == null) { + return defValue; + } + const Map privacyMap = { + 'secret': NotificationPrivacy.Secret, + 'private': NotificationPrivacy.Private, + 'public': NotificationPrivacy.Public, + }; + return privacyMap[value.toLowerCase()] ?? defValue; +} + +GroupSort? parseGroupSort(String? value, [GroupSort? defValue]) { + if (value == null) { + return defValue; + } + const Map groupSortMap = { + 'ascending': GroupSort.Asc, + 'descending': GroupSort.Desc, + }; + return groupSortMap[value.toLowerCase()] ?? defValue; +} + +NotificationImportance? parseNotificationImportance(String? value, + [NotificationImportance? defValue]) { + if (value == null) { + return defValue; + } + const Map importanceMap = { + 'none': NotificationImportance.None, + 'default': NotificationImportance.Default, + 'max': NotificationImportance.Max, + 'minimum': NotificationImportance.Min, + 'high': NotificationImportance.High, + 'low': NotificationImportance.Low, + }; + return importanceMap[value.toLowerCase()] ?? defValue; +} + +DefaultRingtoneType? parseRingtoneType(String? value, + [DefaultRingtoneType? defValue]) { + if (value == null) { + return defValue; + } + const Map ringtoneMap = { + 'alarm': DefaultRingtoneType.Alarm, + 'notification': DefaultRingtoneType.Notification, + 'ringtone': DefaultRingtoneType.Ringtone, + }; + return ringtoneMap[value.toLowerCase()] ?? defValue; +} + +GroupAlertBehavior? parseGroupAlertBehavior(String? value, + [GroupAlertBehavior? defValue]) { + if (value == null) { + return defValue; + } + const Map behaviorMap = { + 'all': GroupAlertBehavior.All, + 'summary': GroupAlertBehavior.Summary, + 'children': GroupAlertBehavior.Children, + }; + return behaviorMap[value.toLowerCase()] ?? defValue; +} + diff --git a/packages/flet_notifications/pubspec.yaml b/packages/flet_notifications/pubspec.yaml new file mode 100644 index 0000000000..dea18ac8da --- /dev/null +++ b/packages/flet_notifications/pubspec.yaml @@ -0,0 +1,27 @@ +name: flet_notifications +description: Flet Notifications control +homepage: https://flet.dev +repository: https://github.com/flet-dev/flet-notifications/src/flutter/flet_notifications +version: 0.1.0 + +environment: + sdk: '>=3.2.3 <4.0.0' + flutter: ">=1.17.0" + +dependencies: + flutter: + sdk: flutter + + collection: ^1.16.0 + awesome_notifications: ^0.10.0 + + flet: + path: ../flet + + + + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^2.0.0 \ No newline at end of file diff --git a/sdk/python/packages/flet/src/flet/__init__.py b/sdk/python/packages/flet/src/flet/__init__.py index 8df93d4808..4912ccd2c7 100644 --- a/sdk/python/packages/flet/src/flet/__init__.py +++ b/sdk/python/packages/flet/src/flet/__init__.py @@ -24,6 +24,20 @@ from flet.core.animated_switcher import AnimatedSwitcher, AnimatedSwitcherTransition from flet.core.animation import Animation, AnimationCurve from flet.core.app_bar import AppBar +from flet.core.notifications import ( + Notifications, + NotificationContent, + NotificationActionButton, + NotificationActionType, + NotificationChannel, + NotificationCategory, + NotificationPrivacy, + NotificationGroupAlertBehavior, + NotificationGroupSort, + NotificationImportance, + NotificationRingtoneType, + NotificationLayout, +) from flet.core.audio import ( Audio, AudioDurationChangeEvent, diff --git a/sdk/python/packages/flet/src/flet/core/notifications.py b/sdk/python/packages/flet/src/flet/core/notifications.py new file mode 100644 index 0000000000..47f9796c3f --- /dev/null +++ b/sdk/python/packages/flet/src/flet/core/notifications.py @@ -0,0 +1,229 @@ +from dataclasses import dataclass +from enum import Enum +from typing import Any, Optional, List + +from flet.core.badge import BadgeValue +from flet.core.control import Control, OptionalNumber +from flet.core.ref import Ref +from flet.core.tooltip import TooltipValue +from flet.core.types import ColorValue, DurationValue + + +class NotificationActionType(Enum): + DEFAULT = "default" + DISABLED = "disabled" + KEEP_ON_TOP = "keepOnTop" + SILENT = "silent" + SILENT_BACKGROUND = "silentBackground" + DISMISS = "dismiss" + + +class NotificationCategory(Enum): + ALARM = "alarm" + CALL = "call" + EMAIL = "email" + ERROR = "error" + EVENT = "event" + LOCAL_SHARING = "localSharing" + MESSAGE = "message" + MISSED_CALL = "missedCall" + NAVIGATION = "navigation" + PROGRESS = "progress" + PROMO = "promo" + RECOMMENDATION = "recommendation" + REMINDER = "reminder" + SERVICE = "service" + SOCIAL = "social" + STATUS = "status" + STOPWATCH = "stopwatch" + TRANSPORT = "transport" + WORKOUT = "workout" + + +class NotificationLayout(Enum): + DEFAULT = "default" + BIG_PICTURE = "bigPicture" + BIG_TEXT = "bigText" + INBOX = "inbox" + PROGRESS_BAR = "progressBar" + MESSAGING = "messaging" + MESSAGING_GROUP = "messagingGroup" + MEDIA_PLAYER = "mediaPlayer" + + +class NotificationPrivacy(Enum): + SECRET = "secret" + PRIVATE = "private" + PUBLIC = "public" + + +class NotificationGroupSort(Enum): + ASCENDING = "ascending" + DESCENDING = "descending" + + +class NotificationImportance(Enum): + NONE = "none" + DEFAULT = "default" + MAXIMUM = "maximum" + MINIMUM = "minimum" + HIGH = "high" + LOW = "low" + + +class NotificationRingtoneType(Enum): + ALARM = "alarm" + NOTIFICATION = "notification" + RINGTONE = "ringtone" + + +class NotificationGroupAlertBehavior(Enum): + ALL = "all" + SUMMARY = "summary" + CHILDREN = "children" + + +@dataclass +class NotificationContent: + id: int + channel_key: str + title: Optional[str] = None + body: Optional[str] = None + title_loc_key: Optional[str] = None + body_loc_key: Optional[str] = None + title_loc_args: Optional[List[str]] = None + body_loc_args: Optional[List[str]] = None + group_key: Optional[str] = None + summary: Optional[str] = None + icon: Optional[str] = None + large_icon: Optional[str] = None + big_picture: Optional[str] = None + custom_sound: Optional[str] = None + show_when: Optional[bool] = True + wake_up_screen: Optional[bool] = False + full_screen_intent: Optional[bool] = False + critical_alert: Optional[bool] = False + rounded_large_icon: Optional[bool] = False + rounded_big_picture: Optional[bool] = False + auto_dismissible: Optional[bool] = True + color: Optional[ColorValue] = None + timeout_after: Optional[DurationValue] = None + chronometer: Optional[DurationValue] = None + bgcolor: Optional[ColorValue] = None + hide_large_icon_on_expand: Optional[bool] = False + locked: Optional[bool] = False + progress: OptionalNumber = None + badge: Optional[int] = None + ticker: Optional[str] = None + display_on_foreground: Optional[bool] = True + display_on_background: Optional[bool] = True + duration: Optional[DurationValue] = None + playback_speed: OptionalNumber = None + action_type: Optional[NotificationActionType] = NotificationActionType.DEFAULT + category: Optional[NotificationCategory] = None + layout: Optional[NotificationLayout] = None + + +@dataclass +class NotificationChannel: + channel_key: str + channel_name: str + channel_description: str + channel_group_key: Optional[str] = None + channel_show_badge: Optional[bool] = True + critical_alerts: Optional[bool] = False + default_color: Optional[ColorValue] = None + enable_lights: Optional[bool] = True + enable_vibration: Optional[bool] = True + led_color: Optional[ColorValue] = None + led_on_ms: Optional[int] = None + led_off_ms: Optional[int] = None + only_alert_once: Optional[bool] = False + play_sound: Optional[bool] = True + sound_source: Optional[str] = None + group_key: Optional[str] = None + icon: Optional[str] = None + locked: Optional[bool] = False + privacy: Optional[NotificationPrivacy] = None + group_sort: Optional[NotificationGroupSort] = None + importance: Optional[NotificationImportance] = None + ringtone_type: Optional[NotificationRingtoneType] = None + group_alert_behavior: Optional[NotificationGroupAlertBehavior] = None + + +@dataclass +class NotificationActionButton: + key: str + label: str + disabled: Optional[bool] = False + requires_authentication: Optional[bool] = False + dangerous: Optional[bool] = False + require_text_input: Optional[bool] = False + show_in_compact_view: Optional[bool] = True + auto_dismissible: Optional[bool] = True + color: Optional[ColorValue] = None + icon: Optional[str] = None + action_type: Optional[NotificationActionType] = None + + +class Notifications(Control): + def __init__( + self, + channels: Optional[List[NotificationChannel]] = None, + language_code: Optional[str] = None, + # + # Control + # + ref: Optional[Ref] = None, + opacity: OptionalNumber = None, + tooltip: Optional[TooltipValue] = None, + badge: Optional[BadgeValue] = None, + visible: Optional[bool] = None, + data: Any = None, + ): + Control.__init__( + self, + ref=ref, + opacity=opacity, + tooltip=tooltip, + badge=badge, + visible=visible, + data=data, + ) + + self.__channels = channels + self.language_code = language_code + + def _get_control_name(self): + return "notifications" + + def before_update(self): + super().before_update() + self._set_attr_json("channels", self.__channels) + + def show( + self, + content: NotificationContent, + actions: Optional[List[NotificationActionButton]] = None, + ): + self.invoke_method( + "show", + arguments={ + "content": self._convert_attr_json(content), + "action_buttons": self._convert_attr_json(actions), + }, + ) + + # channels∂ + @property + def channels(self) -> Optional[List[NotificationChannel]]: + return self.__channels + + # language_code + @property + def language_code(self) -> Optional[str]: + return self._get_attr("languageCode") + + @language_code.setter + def language_code(self, value: Optional[str]): + self._set_attr("languageCode", value) From d3379b24dd7cf854b4db0d62c7bec09f90b57bed Mon Sep 17 00:00:00 2001 From: ndonkoHenri Date: Mon, 3 Mar 2025 03:54:07 +0100 Subject: [PATCH 2/9] android permissions --- client/android/app/src/main/AndroidManifest.xml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/client/android/app/src/main/AndroidManifest.xml b/client/android/app/src/main/AndroidManifest.xml index 29ebfbb351..cd49b07e78 100644 --- a/client/android/app/src/main/AndroidManifest.xml +++ b/client/android/app/src/main/AndroidManifest.xml @@ -15,6 +15,8 @@ + + From 4822ca959e0fddfd45a17146501c98261fb309bc Mon Sep 17 00:00:00 2001 From: ndonkoHenri Date: Mon, 3 Mar 2025 03:54:21 +0100 Subject: [PATCH 3/9] generated files --- client/linux/flutter/generated_plugin_registrant.cc | 4 ++++ client/linux/flutter/generated_plugins.cmake | 1 + client/macos/Flutter/GeneratedPluginRegistrant.swift | 2 ++ client/windows/flutter/generated_plugin_registrant.cc | 3 +++ client/windows/flutter/generated_plugins.cmake | 1 + 5 files changed, 11 insertions(+) diff --git a/client/linux/flutter/generated_plugin_registrant.cc b/client/linux/flutter/generated_plugin_registrant.cc index ce73c487f9..0afc8bba69 100644 --- a/client/linux/flutter/generated_plugin_registrant.cc +++ b/client/linux/flutter/generated_plugin_registrant.cc @@ -7,6 +7,7 @@ #include "generated_plugin_registrant.h" #include +#include #include #include #include @@ -20,6 +21,9 @@ void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) audioplayers_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "AudioplayersLinuxPlugin"); audioplayers_linux_plugin_register_with_registrar(audioplayers_linux_registrar); + g_autoptr(FlPluginRegistrar) awesome_notifications_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "AwesomeNotificationsPlugin"); + awesome_notifications_plugin_register_with_registrar(awesome_notifications_registrar); g_autoptr(FlPluginRegistrar) media_kit_libs_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "MediaKitLibsLinuxPlugin"); media_kit_libs_linux_plugin_register_with_registrar(media_kit_libs_linux_registrar); diff --git a/client/linux/flutter/generated_plugins.cmake b/client/linux/flutter/generated_plugins.cmake index db550dddca..6348ae91e8 100644 --- a/client/linux/flutter/generated_plugins.cmake +++ b/client/linux/flutter/generated_plugins.cmake @@ -4,6 +4,7 @@ list(APPEND FLUTTER_PLUGIN_LIST audioplayers_linux + awesome_notifications media_kit_libs_linux media_kit_video record_linux diff --git a/client/macos/Flutter/GeneratedPluginRegistrant.swift b/client/macos/Flutter/GeneratedPluginRegistrant.swift index 79ace66a63..1eea9d0b35 100644 --- a/client/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/client/macos/Flutter/GeneratedPluginRegistrant.swift @@ -6,6 +6,7 @@ import FlutterMacOS import Foundation import audioplayers_darwin +import awesome_notifications import device_info_plus import geolocator_apple import media_kit_libs_macos_video @@ -25,6 +26,7 @@ import window_to_front func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { AudioplayersDarwinPlugin.register(with: registry.registrar(forPlugin: "AudioplayersDarwinPlugin")) + AwesomeNotificationsPlugin.register(with: registry.registrar(forPlugin: "AwesomeNotificationsPlugin")) DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) GeolocatorPlugin.register(with: registry.registrar(forPlugin: "GeolocatorPlugin")) MediaKitLibsMacosVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitLibsMacosVideoPlugin")) diff --git a/client/windows/flutter/generated_plugin_registrant.cc b/client/windows/flutter/generated_plugin_registrant.cc index be1578b395..0889f5d987 100644 --- a/client/windows/flutter/generated_plugin_registrant.cc +++ b/client/windows/flutter/generated_plugin_registrant.cc @@ -7,6 +7,7 @@ #include "generated_plugin_registrant.h" #include +#include #include #include #include @@ -22,6 +23,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { AudioplayersWindowsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("AudioplayersWindowsPlugin")); + AwesomeNotificationsPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("AwesomeNotificationsPluginCApi")); GeolocatorWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("GeolocatorWindows")); MediaKitLibsWindowsVideoPluginCApiRegisterWithRegistrar( diff --git a/client/windows/flutter/generated_plugins.cmake b/client/windows/flutter/generated_plugins.cmake index 61e94283e8..76d0015841 100644 --- a/client/windows/flutter/generated_plugins.cmake +++ b/client/windows/flutter/generated_plugins.cmake @@ -4,6 +4,7 @@ list(APPEND FLUTTER_PLUGIN_LIST audioplayers_windows + awesome_notifications geolocator_windows media_kit_libs_windows_video media_kit_video From 0ed4805512fb57de0554fc88df5db1dab2b2bd04 Mon Sep 17 00:00:00 2001 From: ndonkoHenri Date: Mon, 3 Mar 2025 13:43:29 +0100 Subject: [PATCH 4/9] support ios --- .../android/app/src/main/AndroidManifest.xml | 2 + client/ios/Podfile | 16 ++++- client/ios/Podfile.lock | 62 ++++++++++++------- client/ios/Runner.xcodeproj/project.pbxproj | 6 ++ client/ios/Runner/AppDelegate.swift | 12 ++++ 5 files changed, 72 insertions(+), 26 deletions(-) diff --git a/client/android/app/src/main/AndroidManifest.xml b/client/android/app/src/main/AndroidManifest.xml index cd49b07e78..3265ac214f 100644 --- a/client/android/app/src/main/AndroidManifest.xml +++ b/client/android/app/src/main/AndroidManifest.xml @@ -17,6 +17,8 @@ + + diff --git a/client/ios/Podfile b/client/ios/Podfile index 279576f388..301860ff43 100644 --- a/client/ios/Podfile +++ b/client/ios/Podfile @@ -35,7 +35,17 @@ target 'Runner' do end post_install do |installer| - installer.pods_project.targets.each do |target| - flutter_additional_ios_build_settings(target) + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings target + end + + ################ Awesome Notifications pod modification ################### + awesome_pod_file = File.expand_path(File.join('plugins', 'awesome_notifications', 'ios', 'Scripts', 'AwesomePodFile'), '.symlinks') + require awesome_pod_file + update_awesome_pod_build_settings(installer) end -end + + awesome_pod_file = File.expand_path(File.join('plugins', 'awesome_notifications', 'ios', 'Scripts', 'AwesomePodFile'), '.symlinks') + require awesome_pod_file + update_awesome_main_target_settings('Runner', File.dirname(File.realpath(__FILE__)), flutter_root) + ################ Awesome Notifications pod modification ################### \ No newline at end of file diff --git a/client/ios/Podfile.lock b/client/ios/Podfile.lock index 3686d0c2a1..9e56950ab3 100644 --- a/client/ios/Podfile.lock +++ b/client/ios/Podfile.lock @@ -1,6 +1,11 @@ PODS: - audioplayers_darwin (0.0.1): - Flutter + - awesome_notifications (0.10.0): + - Flutter + - IosAwnCore (~> 0.10.0) + - device_info_plus (0.0.1): + - Flutter - DKImagePickerController/Core (4.3.9): - DKImagePickerController/ImageDataManager - DKImagePickerController/Resource @@ -47,6 +52,7 @@ PODS: - GoogleUserMessagingPlatform (2.7.0) - integration_test (0.0.1): - Flutter + - IosAwnCore (0.10.0) - media_kit_libs_ios_video (1.0.4): - Flutter - media_kit_native_event_loop (1.0.0): @@ -90,6 +96,8 @@ PODS: DEPENDENCIES: - audioplayers_darwin (from `.symlinks/plugins/audioplayers_darwin/ios`) + - awesome_notifications (from `.symlinks/plugins/awesome_notifications/ios`) + - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) - file_picker (from `.symlinks/plugins/file_picker/ios`) - Flutter (from `Flutter`) - geolocator_apple (from `.symlinks/plugins/geolocator_apple/ios`) @@ -118,12 +126,17 @@ SPEC REPOS: - DKPhotoGallery - Google-Mobile-Ads-SDK - GoogleUserMessagingPlatform + - IosAwnCore - SDWebImage - SwiftyGif EXTERNAL SOURCES: audioplayers_darwin: :path: ".symlinks/plugins/audioplayers_darwin/ios" + awesome_notifications: + :path: ".symlinks/plugins/awesome_notifications/ios" + device_info_plus: + :path: ".symlinks/plugins/device_info_plus/ios" file_picker: :path: ".symlinks/plugins/file_picker/ios" Flutter: @@ -168,35 +181,38 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/webview_flutter_wkwebview/darwin" SPEC CHECKSUMS: - audioplayers_darwin: 877d9a4d06331c5c374595e46e16453ac7eafa40 + audioplayers_darwin: ccf9c770ee768abb07e26d90af093f7bab1c12ab + awesome_notifications: 0f432b28098d193920b11a44cfa9d2d9313a3888 + device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60 - file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655 + file_picker: 9b3292d7c8bc68c8a7bf8eb78f730e49c8efc517 Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 - geolocator_apple: 6cbaf322953988e009e5ecb481f07efece75c450 + geolocator_apple: d981750b9f47dbdb02427e1476d9a04397beb8d9 Google-Mobile-Ads-SDK: 13e6e98edfd78ad8d8a791edb927658cc260a56f - google_mobile_ads: 2a538d8e42b1813809782792e48f8cf4374c2180 + google_mobile_ads: dc2b2a5884bef7ab2b4ff30022a513df5373e208 GoogleUserMessagingPlatform: a8b56893477f67212fbc8411c139e61d463349f5 - integration_test: 252f60fa39af5e17c3aa9899d35d908a0721b573 - media_kit_libs_ios_video: a5fe24bc7875ccd6378a0978c13185e1344651c1 - media_kit_native_event_loop: e6b2ab20cf0746eb1c33be961fcf79667304fa2a - media_kit_video: 5da63f157170e5bf303bf85453b7ef6971218a2e - package_info_plus: 58f0028419748fad15bf008b270aaa8e54380b1c - path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 - permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2 - record_darwin: df0a677188e5fed18472550298e675f19ddaffbe - rive_common: c537b4eed761e903a9403d93c347b69bd7a4762f - screen_brightness_ios: 715ca807df953bf676d339f11464e438143ee625 + integration_test: 4a889634ef21a45d28d50d622cf412dc6d9f586e + IosAwnCore: 653786a911089012092ce831f2945cd339855a89 + media_kit_libs_ios_video: 5a18affdb97d1f5d466dc79988b13eff6c5e2854 + media_kit_native_event_loop: 5fba1a849a6c87a34985f1e178a0de5bd444a0cf + media_kit_video: 1746e198cb697d1ffb734b1d05ec429d1fcd1474 + package_info_plus: 580e9a5f1b6ca5594e7c9ed5f92d1dfb2a66b5e1 + path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 + permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d + record_darwin: 1630616226de4038fa17cec21b11403ca510ec3e + rive_common: dd421daaf9ae69f0125aa761dd96abd278399952 + screen_brightness_ios: 5ed898fa50fa82a26171c086ca5e28228f932576 SDWebImage: 73c6079366fea25fa4bb9640d5fb58f0893facd8 - sensors_plus: 42b9de1b8237675fa8d8121e4bb93be0f79fa61d - shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 + sensors_plus: 1c5f0a01ce21c609a4df404c4e6879d62bce287f + shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4 - torch_light: 682062fa12102172fa38b6b14c106d93b060f83e - url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe - volume_controller: 531ddf792994285c9b17f9d8a7e4dcdd29b3eae9 - wakelock_plus: 78ec7c5b202cab7761af8e2b2b3d0671be6c4ae1 - webview_flutter_wkwebview: 0982481e3d9c78fd5c6f62a002fcd24fc791f1e4 + torch_light: d093d579a221a59ef8a6b8c0eca20d52f7178087 + url_launcher_ios: 694010445543906933d732453a59da0a173ae33d + volume_controller: ca1cde542ee70fad77d388f82e9616488110942b + wakelock_plus: fd58c82b1388f4afe3fe8aa2c856503a262a5b03 + webview_flutter_wkwebview: 44d4dee7d7056d5ad185d25b38404436d56c547c -PODFILE CHECKSUM: c4c93c5f6502fe2754f48404d3594bf779584011 +PODFILE CHECKSUM: 084f8396722e64e82a94b53a4958146f7cea265c -COCOAPODS: 1.15.2 +COCOAPODS: 1.16.2 diff --git a/client/ios/Runner.xcodeproj/project.pbxproj b/client/ios/Runner.xcodeproj/project.pbxproj index 51ef87bc74..87227d4e4b 100644 --- a/client/ios/Runner.xcodeproj/project.pbxproj +++ b/client/ios/Runner.xcodeproj/project.pbxproj @@ -373,7 +373,9 @@ isa = XCBuildConfiguration; baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { + APPLICATION_EXTENSION_API_ONLY = NO; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + BUILD_LIBRARY_FOR_DISTRIBUTION = NO; CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Manual; CURRENT_PROJECT_VERSION = 1; @@ -505,7 +507,9 @@ isa = XCBuildConfiguration; baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; buildSettings = { + APPLICATION_EXTENSION_API_ONLY = NO; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + BUILD_LIBRARY_FOR_DISTRIBUTION = NO; CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Manual; CURRENT_PROJECT_VERSION = 1; @@ -531,7 +535,9 @@ isa = XCBuildConfiguration; baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { + APPLICATION_EXTENSION_API_ONLY = NO; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + BUILD_LIBRARY_FOR_DISTRIBUTION = NO; CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Manual; CURRENT_PROJECT_VERSION = 1; diff --git a/client/ios/Runner/AppDelegate.swift b/client/ios/Runner/AppDelegate.swift index b636303481..eec88ed71e 100644 --- a/client/ios/Runner/AppDelegate.swift +++ b/client/ios/Runner/AppDelegate.swift @@ -1,5 +1,7 @@ import UIKit import Flutter +import awesome_notifications +import shared_preferences_ios @main @objc class AppDelegate: FlutterAppDelegate { @@ -7,7 +9,17 @@ import Flutter _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { + GeneratedPluginRegistrant.register(with: self) + + // Register AwesomeNotifications Plugin for background actions + SwiftAwesomeNotificationsPlugin.setPluginRegistrantCallback { registry in + SwiftAwesomeNotificationsPlugin.register( + with: registry.registrar(forPlugin: "io.flutter.plugins.awesomenotifications.AwesomeNotificationsPlugin")!) + FLTSharedPreferencesPlugin.register( + with: registry.registrar(forPlugin: "io.flutter.plugins.sharedpreferences.SharedPreferencesPlugin")!) + } + return super.application(application, didFinishLaunchingWithOptions: launchOptions) } } From 02f0c9d3ba9017bdf208c0ceae6206cce0cc64c7 Mon Sep 17 00:00:00 2001 From: ndonkoHenri Date: Mon, 3 Mar 2025 18:35:44 +0100 Subject: [PATCH 5/9] update main.dart --- client/lib/main.dart | 3 +++ 1 file changed, 3 insertions(+) diff --git a/client/lib/main.dart b/client/lib/main.dart index 4f2f5e0afe..3f4a184d11 100644 --- a/client/lib/main.dart +++ b/client/lib/main.dart @@ -17,6 +17,7 @@ import 'package:flet_rive/flet_rive.dart' as flet_rive; // --FAT_CLIENT_START-- import 'package:flet_video/flet_video.dart' as flet_video; // --FAT_CLIENT_END-- +import 'package:flet_notifications/flet_notifications.dart' as flet_notifications; import 'package:flet_webview/flet_webview.dart' as flet_webview; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -47,6 +48,7 @@ void main([List? args]) async { flet_rive.ensureInitialized(); flet_webview.ensureInitialized(); flet_flashlight.ensureInitialized(); + flet_notifications.ensureInitialized(); var pageUrl = Uri.base.toString(); var assetsDir = ""; @@ -117,6 +119,7 @@ void main([List? args]) async { flet_rive.createControl, flet_webview.createControl, flet_flashlight.createControl, + flet_notifications.createControl, ], )); } From 42fd4f9e0dc6748aaa3bde6f060edacf9a07eda2 Mon Sep 17 00:00:00 2001 From: Feodor Fitsner Date: Mon, 3 Mar 2025 10:19:05 -0800 Subject: [PATCH 6/9] Fix AppDelegate.swift --- client/ios/Podfile.lock | 4 ++-- client/ios/Runner/AppDelegate.swift | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/client/ios/Podfile.lock b/client/ios/Podfile.lock index 9e56950ab3..ea3bd3f077 100644 --- a/client/ios/Podfile.lock +++ b/client/ios/Podfile.lock @@ -213,6 +213,6 @@ SPEC CHECKSUMS: wakelock_plus: fd58c82b1388f4afe3fe8aa2c856503a262a5b03 webview_flutter_wkwebview: 44d4dee7d7056d5ad185d25b38404436d56c547c -PODFILE CHECKSUM: 084f8396722e64e82a94b53a4958146f7cea265c +PODFILE CHECKSUM: f72fa9a23cb5aeeaa7768cc7b4b325a6cffa1f2a -COCOAPODS: 1.16.2 +COCOAPODS: 1.14.3 diff --git a/client/ios/Runner/AppDelegate.swift b/client/ios/Runner/AppDelegate.swift index eec88ed71e..368b2aa683 100644 --- a/client/ios/Runner/AppDelegate.swift +++ b/client/ios/Runner/AppDelegate.swift @@ -1,7 +1,7 @@ import UIKit import Flutter import awesome_notifications -import shared_preferences_ios +import shared_preferences_foundation @main @objc class AppDelegate: FlutterAppDelegate { @@ -16,7 +16,7 @@ import shared_preferences_ios SwiftAwesomeNotificationsPlugin.setPluginRegistrantCallback { registry in SwiftAwesomeNotificationsPlugin.register( with: registry.registrar(forPlugin: "io.flutter.plugins.awesomenotifications.AwesomeNotificationsPlugin")!) - FLTSharedPreferencesPlugin.register( + SharedPreferencesPlugin.register( with: registry.registrar(forPlugin: "io.flutter.plugins.sharedpreferences.SharedPreferencesPlugin")!) } From 993b58334eb7a7c0daef9c0456d266acd5af2688 Mon Sep 17 00:00:00 2001 From: Feodor Fitsner Date: Mon, 3 Mar 2025 10:36:43 -0800 Subject: [PATCH 7/9] Run _initializeService() inside widget --- .../lib/src/create_control.dart | 34 +++++++++---------- .../lib/src/notifications.dart | 2 +- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/packages/flet_notifications/lib/src/create_control.dart b/packages/flet_notifications/lib/src/create_control.dart index 8a5ee4d5d2..3c0416dbf8 100644 --- a/packages/flet_notifications/lib/src/create_control.dart +++ b/packages/flet_notifications/lib/src/create_control.dart @@ -18,20 +18,20 @@ CreateControlFactory createControl = (CreateControlArgs args) { } }; -void ensureInitialized() async{ - await NotificationService.initializeLocalNotifications( - channels: [ - NotificationChannel( - channelKey: 'alerts', - channelName: 'Notification Channel Name', - channelDescription: 'Notification Channel Description', - playSound: true, - onlyAlertOnce: true, - groupAlertBehavior: GroupAlertBehavior.Children, - importance: NotificationImportance.High, - defaultPrivacy: NotificationPrivacy.Private, - defaultColor: Colors.deepPurple, - ledColor: Colors.deepPurple) - ] - ); -} +void ensureInitialized() async {} + +// void ensureInitialized2() async { +// await NotificationService.initializeLocalNotifications(channels: [ +// NotificationChannel( +// channelKey: 'alerts', +// channelName: 'Notification Channel Name', +// channelDescription: 'Notification Channel Description', +// playSound: true, +// onlyAlertOnce: true, +// groupAlertBehavior: GroupAlertBehavior.Children, +// importance: NotificationImportance.High, +// defaultPrivacy: NotificationPrivacy.Private, +// defaultColor: Colors.deepPurple, +// ledColor: Colors.deepPurple) +// ]); +// } diff --git a/packages/flet_notifications/lib/src/notifications.dart b/packages/flet_notifications/lib/src/notifications.dart index 8793cf56a8..0fdb55b6f0 100644 --- a/packages/flet_notifications/lib/src/notifications.dart +++ b/packages/flet_notifications/lib/src/notifications.dart @@ -26,7 +26,7 @@ class _NotificationControlState extends State @override void initState() { super.initState(); - // _initializeService(); + Future.microtask(() => _initializeService()); } Future _initializeService() async { From 825aa3c9ccb22126a161bf352cd53c16c891dd92 Mon Sep 17 00:00:00 2001 From: ndonkoHenri Date: Thu, 6 Mar 2025 01:25:02 +0100 Subject: [PATCH 8/9] implement more methods --- client/ios/Podfile.lock | 2 +- .../lib/src/create_control.dart | 20 +- .../lib/src/notifications.dart | 168 ++++++++++- .../flet_notifications/lib/src/service.dart | 7 +- .../lib/src/utils/notifications.dart | 76 +++-- sdk/python/packages/flet/src/flet/__init__.py | 3 + .../packages/flet/src/flet/core/control.py | 12 +- .../flet/src/flet/core/notifications.py | 262 ++++++++++++++++-- .../packages/flet/src/flet/core/page.py | 38 ++- 9 files changed, 519 insertions(+), 69 deletions(-) diff --git a/client/ios/Podfile.lock b/client/ios/Podfile.lock index ea3bd3f077..e9725bb850 100644 --- a/client/ios/Podfile.lock +++ b/client/ios/Podfile.lock @@ -215,4 +215,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: f72fa9a23cb5aeeaa7768cc7b4b325a6cffa1f2a -COCOAPODS: 1.14.3 +COCOAPODS: 1.16.2 diff --git a/packages/flet_notifications/lib/src/create_control.dart b/packages/flet_notifications/lib/src/create_control.dart index 3c0416dbf8..e6ea9103e2 100644 --- a/packages/flet_notifications/lib/src/create_control.dart +++ b/packages/flet_notifications/lib/src/create_control.dart @@ -18,20 +18,6 @@ CreateControlFactory createControl = (CreateControlArgs args) { } }; -void ensureInitialized() async {} - -// void ensureInitialized2() async { -// await NotificationService.initializeLocalNotifications(channels: [ -// NotificationChannel( -// channelKey: 'alerts', -// channelName: 'Notification Channel Name', -// channelDescription: 'Notification Channel Description', -// playSound: true, -// onlyAlertOnce: true, -// groupAlertBehavior: GroupAlertBehavior.Children, -// importance: NotificationImportance.High, -// defaultPrivacy: NotificationPrivacy.Private, -// defaultColor: Colors.deepPurple, -// ledColor: Colors.deepPurple) -// ]); -// } +void ensureInitialized() async { + // initialization is done in NotificationControl.initState +} \ No newline at end of file diff --git a/packages/flet_notifications/lib/src/notifications.dart b/packages/flet_notifications/lib/src/notifications.dart index 0fdb55b6f0..00191f54fc 100644 --- a/packages/flet_notifications/lib/src/notifications.dart +++ b/packages/flet_notifications/lib/src/notifications.dart @@ -1,3 +1,6 @@ +import 'dart:convert'; + +import 'package:awesome_notifications/awesome_notifications.dart'; import 'package:flet/flet.dart'; import 'package:flet_notifications/src/utils/notifications.dart'; import 'package:flutter/material.dart'; @@ -35,13 +38,20 @@ class _NotificationControlState extends State await NotificationService.initializeLocalNotifications( channels: channels, languageCode: widget.control.attrString("languageCode"), + icon: widget.control.attrString("icon"), ); } + @override + void dispose() { + AwesomeNotifications().dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { debugPrint( - "Notification build: ${widget.control.id} (${widget.control.hashCode})"); + "Notifications build: ${widget.control.id} (${widget.control.hashCode})"); () async { widget.backend.subscribeMethods(widget.control.id, @@ -52,11 +62,165 @@ class _NotificationControlState extends State notificationContentFromJSON(Theme.of(context), args["content"]); var actionButtons = notificationActionButtonsFromJSON( Theme.of(context), args["action_buttons"]); + var schedule = args["schedule"] != null + ? args["schedule_parser"] == "interval" + ? notificationIntervalFromJSON(args["schedule"], + jsonDecode: true) + : notificationCalendarFromJSON(args["schedule"], + jsonDecode: true) + : null; if (content != null) { debugPrint("NotificationService.showNotification"); NotificationService.showNotification(content, - actionButtons: actionButtons); + actionButtons: actionButtons, schedule: schedule); + } + // dismissals + case "dismiss": + var id = parseInt(args["id"]); + var channelKey = args["channel_key"]; + var groupKey = args["group_key"]; + if (id != null) { + await AwesomeNotifications().dismiss(id); + } else if (channelKey != null) { + await AwesomeNotifications() + .dismissNotificationsByChannelKey(channelKey); + } else if (groupKey != null) { + await AwesomeNotifications() + .dismissNotificationsByGroupKey(groupKey); + } + break; + case "dismiss_all": + await AwesomeNotifications().dismissAllNotifications(); + + // cancellations + case "cancel": + var id = parseInt(args["id"]); + var channelKey = args["channel_key"]; + var groupKey = args["group_key"]; + if (id != null) { + await AwesomeNotifications().cancelSchedule(id); + } else if (channelKey != null) { + await AwesomeNotifications() + .cancelNotificationsByChannelKey(channelKey); + } else if (groupKey != null) { + await AwesomeNotifications() + .cancelNotificationsByGroupKey(groupKey); + } + break; + case "cancel_schedule": + var id = parseInt(args["id"]); + var channelKey = args["channel_key"]; + var groupKey = args["group_key"]; + if (id != null) { + await AwesomeNotifications().cancelSchedule(id); + } else if (channelKey != null) { + await AwesomeNotifications() + .cancelSchedulesByChannelKey(channelKey); + } else if (groupKey != null) { + await AwesomeNotifications().cancelSchedulesByGroupKey(groupKey); + } + break; + case "cancel_all_schedules": + await AwesomeNotifications().cancelAllSchedules(); + break; + + // badge_counter + case "get_badge_counter": + return await AwesomeNotifications() + .getGlobalBadgeCounter() + .then((value) => value.toString()); + case "set_badge_counter": + var value = parseInt(args["value"]); + if (value != null) { + await AwesomeNotifications().setGlobalBadgeCounter(value); } + break; + case "increment_badge_counter": + return await AwesomeNotifications() + .incrementGlobalBadgeCounter() + .then((value) => value.toString()); + case "decrement_badge_counter": + return await AwesomeNotifications() + .decrementGlobalBadgeCounter() + .then((value) => value.toString()); + case "reset_badge_counter": + await AwesomeNotifications().resetGlobalBadge(); + break; + + // channels + case "set_channel": + var notificationChannel = notificationChannelFromJSON( + Theme.of(context), args["channel"], + jsonDecode: true); + if (notificationChannel != null) { + await AwesomeNotifications().setChannel(notificationChannel, + forceUpdate: parseBool(args["force_update"], false)!); + } + break; + case "remove_channel": + var channelKey = args["channel_key"]; + if (channelKey != null) { + await AwesomeNotifications().removeChannel(channelKey); + } + break; + + // permissions + case "is_allowed": + return await AwesomeNotifications() + .isNotificationAllowed() + .then((value) => value.toString()); + case "request_permission": + return await AwesomeNotifications() + .requestPermissionToSendNotifications() + .then((value) => value.toString()); + + // time + case "get_local_timezone_identifier": + return await AwesomeNotifications() + .getLocalTimeZoneIdentifier() + .then((value) => value); + case "get_next_date": + return ""; + case "get_utc_timezone_identifier": + return await AwesomeNotifications() + .getUtcTimeZoneIdentifier() + .then((value) => value); + + // others + case "show_alarm_page": + await AwesomeNotifications().showAlarmPage(); + break; + case "get_initial_action": + return await AwesomeNotifications() + .getInitialNotificationAction( + removeFromActionEvents: + parseBool(args["remove_from_action_events"], false)!) + .then((ReceivedAction? action) => jsonEncode(action?.toMap())); + case "show_global_dnd_override_page": + await AwesomeNotifications().showGlobalDndOverridePage(); + break; + + case "get_lifecycle": + return await AwesomeNotifications() + .getAppLifeCycle() + .then((value) => value.name.toLowerCase()); + case "get_localization": + return await AwesomeNotifications() + .getLocalization() + .then((value) => value); + case "is_active_on_status_bar": + var id = parseInt(args["id"]); + if (id != null) { + return await AwesomeNotifications() + .isNotificationActiveOnStatusBar(id: id) + .then((value) => value.toString()); + } + break; + + case "get_ids_active_on_status_bar": + return await AwesomeNotifications() + .getAllActiveNotificationIdsOnStatusBar() + .then((value) => jsonEncode(value)); } return null; }); diff --git a/packages/flet_notifications/lib/src/service.dart b/packages/flet_notifications/lib/src/service.dart index fdc5cb168e..723d5a5463 100644 --- a/packages/flet_notifications/lib/src/service.dart +++ b/packages/flet_notifications/lib/src/service.dart @@ -3,9 +3,9 @@ import 'package:flutter/material.dart'; class NotificationService { static Future initializeLocalNotifications( - {required List channels, String? languageCode}) async { + {required List channels, String? languageCode, String? icon}) async { await AwesomeNotifications().initialize( - null, //'resource://drawable/res_app_icon',// + icon, channels, languageCode: languageCode, debug: true); @@ -36,10 +36,11 @@ class NotificationService { } static void showNotification(NotificationContent content, - {List? actionButtons}) { + {List? actionButtons, NotificationSchedule? schedule}) { AwesomeNotifications().createNotification( content: content, actionButtons: actionButtons, + schedule: schedule, ); } } diff --git a/packages/flet_notifications/lib/src/utils/notifications.dart b/packages/flet_notifications/lib/src/utils/notifications.dart index beb9dd064a..f105745311 100644 --- a/packages/flet_notifications/lib/src/utils/notifications.dart +++ b/packages/flet_notifications/lib/src/utils/notifications.dart @@ -17,9 +17,7 @@ NotificationContent? parseNotificationContent( NotificationContent? notificationContentFromJSON(ThemeData theme, dynamic j, [NotificationContent? defValue]) { - if (j == null) return defValue; - - j = json.decode(j); + j = j != null ? json.decode(j) : null; var id = parseInt(j['id']); var channelKey = j['channel_key']; @@ -72,10 +70,8 @@ NotificationContent? notificationContentFromJSON(ThemeData theme, dynamic j, NotificationActionButton? notificationActionButtonFromJSON( ThemeData theme, dynamic j, [NotificationActionButton? defValue]) { - if (j == null) return defValue; - - var key = j['key']; - var label = j['label']; + var key = j?['key']; + var label = j?['label']; if (j == null || key == null || label == null) { return defValue; @@ -99,9 +95,7 @@ NotificationActionButton? notificationActionButtonFromJSON( List? notificationActionButtonsFromJSON( ThemeData theme, dynamic j, [List? defValue]) { - if (j == null) return defValue; - - j = json.decode(j); + j = j != null ? json.decode(j) : null; if (j == null) return defValue; var buttons = []; @@ -139,12 +133,13 @@ List parseNotificationChannels( } NotificationChannel? notificationChannelFromJSON(ThemeData theme, dynamic j, - [NotificationChannel? defValue]) { - if (j == null) return defValue; - - var channelKey = j['channel_key']; - var channelName = j['channel_name']; - var channelDescription = j['channel_description']; + {NotificationChannel? defValue, bool jsonDecode = false}) { + if (jsonDecode && j is String) { + j = json.decode(j); + } + var channelKey = j?['channel_key']; + var channelName = j?['channel_name']; + var channelDescription = j?['channel_description']; if (j == null || channelKey == null || @@ -180,6 +175,54 @@ NotificationChannel? notificationChannelFromJSON(ThemeData theme, dynamic j, ); } +NotificationCalendar? notificationCalendarFromJSON(dynamic j, + {NotificationCalendar? defValue, bool jsonDecode = false}) { + if (jsonDecode && j is String) { + j = json.decode(j); + } + + if (j == null) { + return defValue; + } + + return NotificationCalendar( + allowWhileIdle: parseBool(j["allow_while_idle"], false)!, + preciseAlarm: parseBool(j["precise_alarm"], false)!, + repeats: parseBool(j["repeats"], false)!, + timeZone: j["time_zone"], + day: parseInt(j["day"]), + hour: parseInt(j["hour"]), + minute: parseInt(j["minute"]), + second: parseInt(j["second"]), + millisecond: parseInt(j["millisecond"]), + month: parseInt(j["month"]), + weekday: parseInt(j["weekday"]), + weekOfYear: parseInt(j["week_of_year"]), + year: parseInt(j["year"]), + era: parseInt(j["era"]), + // weekOfMonth (not fully implemented atm) + ); +} + +NotificationInterval? notificationIntervalFromJSON(dynamic j, + {NotificationInterval? defValue, bool jsonDecode = false}) { + if (jsonDecode && j is String) { + j = json.decode(j); + } + + if (j == null || j["interval"] == null) { + return defValue; + } + + return NotificationInterval( + interval: durationFromJSON(j["interval"]), + allowWhileIdle: parseBool(j["allow_while_idle"], false)!, + preciseAlarm: parseBool(j["precise_alarm"], false)!, + repeats: parseBool(j["repeats"], false)!, + timeZone: j["time_zone"], + ); +} + ActionType? parseActionType(String? value, [ActionType? defaultActionType]) { if (value == null) { return defaultActionType; @@ -307,4 +350,3 @@ GroupAlertBehavior? parseGroupAlertBehavior(String? value, }; return behaviorMap[value.toLowerCase()] ?? defValue; } - diff --git a/sdk/python/packages/flet/src/flet/__init__.py b/sdk/python/packages/flet/src/flet/__init__.py index 4912ccd2c7..6253cb85a7 100644 --- a/sdk/python/packages/flet/src/flet/__init__.py +++ b/sdk/python/packages/flet/src/flet/__init__.py @@ -37,6 +37,9 @@ NotificationImportance, NotificationRingtoneType, NotificationLayout, + NotificationLifeCycle, + NotificationCalendar, + NotificationInterval, ) from flet.core.audio import ( Audio, diff --git a/sdk/python/packages/flet/src/flet/core/control.py b/sdk/python/packages/flet/src/flet/core/control.py index 434d6a5e79..06e46d0d72 100644 --- a/sdk/python/packages/flet/src/flet/core/control.py +++ b/sdk/python/packages/flet/src/flet/core/control.py @@ -345,7 +345,10 @@ def invoke_method( arguments: Optional[Dict[str, str]] = None, wait_for_result: bool = False, wait_timeout: Optional[float] = 5, - ) -> Optional[str]: + result_type: Literal[ + "string", "int", "float", "bool", "json_encoded" + ] = "string", + ) -> Optional[Any]: assert ( self.__page ), f"{self.__class__.__qualname__} Control must be added to the page first" @@ -358,6 +361,7 @@ def invoke_method( arguments=arguments, wait_for_result=wait_for_result, wait_timeout=wait_timeout, + result_type=result_type, ) def invoke_method_async( @@ -366,7 +370,10 @@ def invoke_method_async( arguments: Optional[Dict[str, str]] = None, wait_for_result: bool = False, wait_timeout: Optional[float] = 5, - ): + result_type: Literal[ + "string", "int", "float", "bool", "json_encoded" + ] = "string", + ) -> Optional[Any]: assert ( self.__page ), f"{self.__class__.__qualname__} Control must be added to the page first" @@ -379,6 +386,7 @@ def invoke_method_async( arguments=arguments, wait_for_result=wait_for_result, wait_timeout=wait_timeout, + result_type=result_type, ) def copy_attrs(self, dest: Dict[str, Any]) -> None: diff --git a/sdk/python/packages/flet/src/flet/core/notifications.py b/sdk/python/packages/flet/src/flet/core/notifications.py index 47f9796c3f..bd328b1e03 100644 --- a/sdk/python/packages/flet/src/flet/core/notifications.py +++ b/sdk/python/packages/flet/src/flet/core/notifications.py @@ -1,6 +1,6 @@ from dataclasses import dataclass from enum import Enum -from typing import Any, Optional, List +from typing import Any, Optional, List, Union from flet.core.badge import BadgeValue from flet.core.control import Control, OptionalNumber @@ -83,6 +83,39 @@ class NotificationGroupAlertBehavior(Enum): CHILDREN = "children" +class NotificationLifeCycle(Enum): + FOREGROUND = "foreground" + BACKGROUND = "background" + TERMINATED = "terminated" + + +@dataclass +class NotificationInterval: + interval: DurationValue + time_zone: Optional[str] = None + allow_while_idle: bool = False + repeats: bool = False + precise_alarm: bool = False + + +@dataclass +class NotificationCalendar: + day: Optional[int] = None + hour: Optional[int] = None + minute: Optional[int] = None + second: Optional[int] = None + millisecond: Optional[int] = None + month: Optional[int] = None + weekday: Optional[int] = None + week_of_year: Optional[int] = None + year: Optional[int] = None + era: Optional[int] = None + time_zone: Optional[str] = None + allow_while_idle: bool = False + repeats: bool = False + precise_alarm: bool = False + + @dataclass class NotificationContent: id: int @@ -99,27 +132,27 @@ class NotificationContent: large_icon: Optional[str] = None big_picture: Optional[str] = None custom_sound: Optional[str] = None - show_when: Optional[bool] = True - wake_up_screen: Optional[bool] = False - full_screen_intent: Optional[bool] = False - critical_alert: Optional[bool] = False - rounded_large_icon: Optional[bool] = False - rounded_big_picture: Optional[bool] = False - auto_dismissible: Optional[bool] = True + show_when: bool = True + wake_up_screen: bool = False + full_screen_intent: bool = False + critical_alert: bool = False + rounded_large_icon: bool = False + rounded_big_picture: bool = False + auto_dismissible: bool = True color: Optional[ColorValue] = None timeout_after: Optional[DurationValue] = None chronometer: Optional[DurationValue] = None bgcolor: Optional[ColorValue] = None - hide_large_icon_on_expand: Optional[bool] = False + hide_large_icon_on_expand: bool = False locked: Optional[bool] = False progress: OptionalNumber = None badge: Optional[int] = None ticker: Optional[str] = None - display_on_foreground: Optional[bool] = True - display_on_background: Optional[bool] = True + display_on_foreground: bool = True + display_on_background: bool = True duration: Optional[DurationValue] = None playback_speed: OptionalNumber = None - action_type: Optional[NotificationActionType] = NotificationActionType.DEFAULT + action_type: NotificationActionType = NotificationActionType.DEFAULT category: Optional[NotificationCategory] = None layout: Optional[NotificationLayout] = None @@ -130,20 +163,20 @@ class NotificationChannel: channel_name: str channel_description: str channel_group_key: Optional[str] = None - channel_show_badge: Optional[bool] = True - critical_alerts: Optional[bool] = False + channel_show_badge: bool = True + critical_alerts: bool = False default_color: Optional[ColorValue] = None - enable_lights: Optional[bool] = True - enable_vibration: Optional[bool] = True + enable_lights: bool = True + enable_vibration: bool = True led_color: Optional[ColorValue] = None led_on_ms: Optional[int] = None led_off_ms: Optional[int] = None - only_alert_once: Optional[bool] = False - play_sound: Optional[bool] = True + only_alert_once: bool = False + play_sound: bool = True sound_source: Optional[str] = None group_key: Optional[str] = None icon: Optional[str] = None - locked: Optional[bool] = False + locked: bool = False privacy: Optional[NotificationPrivacy] = None group_sort: Optional[NotificationGroupSort] = None importance: Optional[NotificationImportance] = None @@ -169,7 +202,7 @@ class NotificationActionButton: class Notifications(Control): def __init__( self, - channels: Optional[List[NotificationChannel]] = None, + channels: List[NotificationChannel], language_code: Optional[str] = None, # # Control @@ -204,19 +237,198 @@ def before_update(self): def show( self, content: NotificationContent, - actions: Optional[List[NotificationActionButton]] = None, - ): + action_buttons: Optional[List[NotificationActionButton]] = None, + schedule: Optional[Union[NotificationCalendar, NotificationInterval]] = None, + ) -> None: self.invoke_method( "show", arguments={ "content": self._convert_attr_json(content), - "action_buttons": self._convert_attr_json(actions), + "action_buttons": self._convert_attr_json(action_buttons), + "schedule": self._convert_attr_json(schedule), + "schedule_parser": "interval" + if isinstance(schedule, NotificationInterval) + else "calendar", + }, + ) + + def dismiss( + self, + id: Optional[int] = None, + channel_key: Optional[str] = None, + group_key: Optional[str] = None, + ) -> None: + self.invoke_method( + "dismiss", + arguments={ + "id": id, + "channel_key": channel_key, + "group_key": group_key, }, ) - # channels∂ + def dismiss_all(self) -> None: + self.invoke_method("dismiss_all") + + def cancel( + self, + id: Optional[int] = None, + channel_key: Optional[str] = None, + group_key: Optional[str] = None, + ) -> None: + self.invoke_method( + "cancel", + arguments={ + "id": id, + "channel_key": channel_key, + "group_key": group_key, + }, + ) + + def cancel_schedule( + self, + id: Optional[int] = None, + channel_key: Optional[str] = None, + group_key: Optional[str] = None, + ) -> None: + self.invoke_method( + "cancel_schedule", + arguments={ + "id": id, + "channel_key": channel_key, + "group_key": group_key, + }, + ) + + def cancel_all_schedules(self) -> None: + self.invoke_method("cancel_all_schedules") + + # badge_counter + def get_badge_counter(self, wait_timeout: float = 10) -> int: + return self.invoke_method( + "get_badge_counter", + wait_for_result=True, + wait_timeout=wait_timeout, + result_type="int", + ) + + def set_badge_counter(self, value: int) -> None: + self.invoke_method("set_badge_counter", arguments={"value": str(value)}) + + def increment_badge_counter(self, wait_timeout: float = 10) -> bool: + return self.invoke_method( + "increment_badge_counter", + wait_for_result=True, + wait_timeout=wait_timeout, + result_type="bool", + ) + + def decrement_badge_counter(self, wait_timeout: float = 10) -> bool: + return self.invoke_method( + "decrement_badge_counter", + wait_for_result=True, + wait_timeout=wait_timeout, + result_type="bool", + ) + + def reset_badge_counter(self) -> None: + self.invoke_method("reset_badge_counter") + + # channels + def set_channel( + self, channel: NotificationChannel, force_update: bool = False + ) -> None: + self.invoke_method( + "set_channel", + arguments={ + "channel": self._convert_attr_json(channel), + "force_update": force_update, + }, + ) + + def remove_channel(self, channel_key: str) -> None: + self.invoke_method("remove_channel", arguments={"channel_key": channel_key}) + + # permissions + def is_allowed(self, wait_timeout: float = 10) -> bool: + return self.invoke_method( + "is_allowed", + wait_for_result=True, + wait_timeout=wait_timeout, + result_type="bool", + ) + + def request_permission(self, wait_timeout: float = 10) -> bool: + return self.invoke_method( + "request_permission", + wait_for_result=True, + wait_timeout=wait_timeout, + result_type="bool", + ) + + # time + def get_local_timezone_identifier(self, wait_timeout: float = 10) -> str: + return self.invoke_method( + "get_local_timezone_identifier", + wait_for_result=True, + wait_timeout=wait_timeout, + ) + + def get_utc_timezone_identifier(self, wait_timeout: float = 10) -> str: + return self.invoke_method( + "get_utc_timezone_identifier", + wait_for_result=True, + wait_timeout=wait_timeout, + ) + + # others + def show_alarm_page(self) -> None: + self.invoke_method("show_alarm_page") + + def get_initial_action(self, remove_from_action_events: bool = False) -> str: + return self.invoke_method( + "get_initial_action", + arguments={"remove_from_action_events": str(remove_from_action_events)}, + ) + + def show_global_dnd_override_page(self) -> None: + self.invoke_method("show_global_dnd_override_page") + + def get_lifecycle(self, wait_timeout: float = 10) -> NotificationLifeCycle: + result = self.invoke_method( + "get_app_lifecycle", + wait_for_result=True, + wait_timeout=wait_timeout, + ) + return NotificationLifeCycle(result) + + def get_localization(self, wait_timeout: float = 10) -> str: + return self.invoke_method( + "get_localization", + wait_for_result=True, + wait_timeout=wait_timeout, + ) + + def is_active_on_status_bar(self, id: int, wait_timeout: float = 10) -> bool: + return self.invoke_method( + "is_active_on_status_bar", + arguments={"id": str(id)}, + wait_for_result=True, + wait_timeout=wait_timeout, + result_type="bool", + ) + + def get_ids_active_on_status_bar(self, wait_timeout: float = 10) -> List[int]: + return self.invoke_method( + "get_ids_active_on_status_bar", + wait_for_result=True, + wait_timeout=wait_timeout, + result_type="json_encoded", + ) + + # channels @property - def channels(self) -> Optional[List[NotificationChannel]]: + def channels(self) -> List[NotificationChannel]: return self.__channels # language_code diff --git a/sdk/python/packages/flet/src/flet/core/page.py b/sdk/python/packages/flet/src/flet/core/page.py index b2b3eb8eda..ea36116329 100644 --- a/sdk/python/packages/flet/src/flet/core/page.py +++ b/sdk/python/packages/flet/src/flet/core/page.py @@ -25,6 +25,7 @@ TypeVar, Union, cast, + Literal, ) from urllib.parse import urlparse @@ -1187,7 +1188,10 @@ def _invoke_method( control_id: Optional[str] = "", wait_for_result: Optional[bool] = False, wait_timeout: Optional[float] = 5, - ) -> Optional[str]: + result_type: Literal[ + "string", "int", "float", "bool", "json_encoded" + ] = "string", + ) -> Optional[Any]: method_id = uuid.uuid4().hex # register callback @@ -1218,8 +1222,22 @@ def _invoke_method( result, err = self.__method_call_results.pop(evt) if err: raise Exception(err) + if result is None or result == "null": return None + + # parse result according to result_type + try: + if result_type == "int": + return int(result) + elif result_type == "float": + return float(result) + elif result_type == "bool": + return result == "true" + elif result_type == "json_encoded": + return json.loads(result) + except Exception: + return result return result async def _invoke_method_async( @@ -1229,7 +1247,10 @@ async def _invoke_method_async( control_id: Optional[str] = "", wait_for_result: Optional[bool] = False, wait_timeout: Optional[float] = 5, - ) -> Optional[str]: + result_type: Literal[ + "string", "int", "float", "bool", "json_encoded" + ] = "string", + ) -> Optional[Any]: method_id = uuid.uuid4().hex # register callback @@ -1264,6 +1285,19 @@ async def _invoke_method_async( raise Exception(err) if result == "null": return None + + # parse result according to result_type + try: + if result_type == "int": + return int(result) + elif result_type == "float": + return float(result) + elif result_type == "bool": + return result == "true" + elif result_type == "json_encoded": + return json.loads(result) + except Exception: + return result return result def __on_invoke_method_result(self, e) -> None: From c84d018985213c43e5ae34adbfa71b017727c03f Mon Sep 17 00:00:00 2001 From: ndonkoHenri Date: Mon, 31 Mar 2025 06:06:55 +0200 Subject: [PATCH 9/9] specify supported platforms --- .../packages/flet/src/flet/core/notifications.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/sdk/python/packages/flet/src/flet/core/notifications.py b/sdk/python/packages/flet/src/flet/core/notifications.py index bd328b1e03..cb66567012 100644 --- a/sdk/python/packages/flet/src/flet/core/notifications.py +++ b/sdk/python/packages/flet/src/flet/core/notifications.py @@ -1,12 +1,13 @@ from dataclasses import dataclass from enum import Enum -from typing import Any, Optional, List, Union +from typing import Any, List, Optional, Union from flet.core.badge import BadgeValue from flet.core.control import Control, OptionalNumber +from flet.core.exceptions import FletUnsupportedPlatformException from flet.core.ref import Ref from flet.core.tooltip import TooltipValue -from flet.core.types import ColorValue, DurationValue +from flet.core.types import ColorValue, DurationValue, PagePlatform class NotificationActionType(Enum): @@ -232,6 +233,14 @@ def _get_control_name(self): def before_update(self): super().before_update() + assert self.page is not None, "Notifications must be added to page first." + if self.page.web or self.page.platform not in [ + PagePlatform.ANDROID, + PagePlatform.IOS, + ]: + raise FletUnsupportedPlatformException( + "This control is supported on Android and iOS platforms only." + ) self._set_attr_json("channels", self.__channels) def show(