flet-android-notifications 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,22 @@
1
+ Metadata-Version: 2.4
2
+ Name: flet-android-notifications
3
+ Version: 0.1.0
4
+ Summary: Native local notifications for Flet apps (Android)
5
+ Author-email: Alex Stoica <alexstoica@protonmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/alex-stoica/flet-android-notifications
8
+ Project-URL: Repository, https://github.com/alex-stoica/flet-android-notifications
9
+ Project-URL: Issues, https://github.com/alex-stoica/flet-android-notifications/issues
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.10
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
18
+ Requires-Python: >=3.10
19
+ Description-Content-Type: text/markdown
20
+ Requires-Dist: flet>=0.80.5
21
+
22
+ See the [full documentation on GitHub](https://github.com/alex-stoica/flet-android-notifications).
@@ -0,0 +1 @@
1
+ See the [full documentation on GitHub](https://github.com/alex-stoica/flet-android-notifications).
@@ -0,0 +1,35 @@
1
+ [project]
2
+ name = "flet-android-notifications"
3
+ version = "0.1.0"
4
+ description = "Native local notifications for Flet apps (Android)"
5
+ requires-python = ">=3.10"
6
+ readme = "README.md"
7
+ license = "MIT"
8
+ authors = [
9
+ {name = "Alex Stoica", email = "alexstoica@protonmail.com"},
10
+ ]
11
+ classifiers = [
12
+ "Development Status :: 3 - Alpha",
13
+ "Intended Audience :: Developers",
14
+ "Programming Language :: Python :: 3",
15
+ "Programming Language :: Python :: 3.10",
16
+ "Programming Language :: Python :: 3.11",
17
+ "Programming Language :: Python :: 3.12",
18
+ "Programming Language :: Python :: 3.13",
19
+ "Topic :: Software Development :: Libraries :: Python Modules",
20
+ ]
21
+ dependencies = [
22
+ "flet>=0.80.5",
23
+ ]
24
+
25
+ [project.urls]
26
+ Homepage = "https://github.com/alex-stoica/flet-android-notifications"
27
+ Repository = "https://github.com/alex-stoica/flet-android-notifications"
28
+ Issues = "https://github.com/alex-stoica/flet-android-notifications/issues"
29
+
30
+ [tool.setuptools.package-data]
31
+ "flutter.flet_android_notifications" = ["**/*"]
32
+
33
+ [build-system]
34
+ requires = ["setuptools"]
35
+ build-backend = "setuptools.build_meta"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1 @@
1
+ from .flet_android_notifications import FletAndroidNotifications, NotificationError
@@ -0,0 +1,112 @@
1
+ import json
2
+ import flet as ft
3
+ from typing import Optional
4
+
5
+
6
+ class NotificationError(Exception):
7
+ """Raised when a notification operation fails on the native side."""
8
+
9
+ pass
10
+
11
+
12
+ @ft.control("flet_android_notifications")
13
+ class FletAndroidNotifications(ft.Service):
14
+ on_notification_tap: Optional[ft.ControlEventHandler["FletAndroidNotifications"]] = None
15
+
16
+ def _check_error(self, result):
17
+ """Check if Dart returned an error and raise if so."""
18
+ if isinstance(result, str) and result.startswith("error:"):
19
+ raise NotificationError(result[6:])
20
+ return result
21
+
22
+ async def show_notification(
23
+ self,
24
+ notification_id: int,
25
+ title: str,
26
+ body: str,
27
+ *,
28
+ payload: str = "",
29
+ actions: Optional[list[dict]] = None,
30
+ channel_id: str = "flet_notifications",
31
+ channel_name: str = "Flet Notifications",
32
+ channel_description: str = "Notifications from Flet app",
33
+ importance: str = "high",
34
+ play_sound: bool = True,
35
+ enable_vibration: bool = True,
36
+ ):
37
+ """Show an Android notification.
38
+
39
+ Args:
40
+ notification_id: Unique integer ID for this notification.
41
+ title: Notification title.
42
+ body: Notification body text.
43
+ payload: Arbitrary string returned in on_notification_tap event.
44
+ actions: List of action buttons shown on the notification. Each is
45
+ a dict with "id" and "title" keys, e.g.
46
+ [{"id": "approve", "title": "Approve"}, {"id": "deny", "title": "Deny"}].
47
+ The tapped action's id is returned as "action_id" in the
48
+ on_notification_tap event data (JSON string).
49
+ channel_id: Android notification channel ID.
50
+ channel_name: Human-readable channel name (shown in system settings).
51
+ channel_description: Channel description (shown in system settings).
52
+ importance: One of "none", "min", "low", "default", "high", "max".
53
+ play_sound: Whether to play the default notification sound.
54
+ enable_vibration: Whether to vibrate on notification.
55
+
56
+ Raises:
57
+ NotificationError: If the native side reports an error.
58
+ """
59
+ result = await self._invoke_method(
60
+ method_name="show_notification",
61
+ arguments={
62
+ "id": notification_id,
63
+ "title": title,
64
+ "body": body,
65
+ "payload": payload,
66
+ "actions": actions or [],
67
+ "channel_id": channel_id,
68
+ "channel_name": channel_name,
69
+ "channel_description": channel_description,
70
+ "importance": importance,
71
+ "play_sound": play_sound,
72
+ "enable_vibration": enable_vibration,
73
+ },
74
+ )
75
+ return self._check_error(result)
76
+
77
+ async def cancel(self, notification_id: int):
78
+ """Cancel a specific notification by ID.
79
+
80
+ Raises:
81
+ NotificationError: If the native side reports an error.
82
+ """
83
+ result = await self._invoke_method(
84
+ method_name="cancel",
85
+ arguments={"id": notification_id},
86
+ )
87
+ return self._check_error(result)
88
+
89
+ async def cancel_all(self):
90
+ """Cancel all active notifications.
91
+
92
+ Raises:
93
+ NotificationError: If the native side reports an error.
94
+ """
95
+ result = await self._invoke_method(
96
+ method_name="cancel_all",
97
+ )
98
+ return self._check_error(result)
99
+
100
+ async def request_permissions(self):
101
+ """Request notification permissions (required on Android 13+).
102
+
103
+ Returns:
104
+ "true" if granted, "false" if denied.
105
+
106
+ Raises:
107
+ NotificationError: If the native side reports an error.
108
+ """
109
+ result = await self._invoke_method(
110
+ method_name="request_permissions",
111
+ )
112
+ return self._check_error(result)
@@ -0,0 +1,22 @@
1
+ Metadata-Version: 2.4
2
+ Name: flet-android-notifications
3
+ Version: 0.1.0
4
+ Summary: Native local notifications for Flet apps (Android)
5
+ Author-email: Alex Stoica <alexstoica@protonmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/alex-stoica/flet-android-notifications
8
+ Project-URL: Repository, https://github.com/alex-stoica/flet-android-notifications
9
+ Project-URL: Issues, https://github.com/alex-stoica/flet-android-notifications/issues
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.10
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
18
+ Requires-Python: >=3.10
19
+ Description-Content-Type: text/markdown
20
+ Requires-Dist: flet>=0.80.5
21
+
22
+ See the [full documentation on GitHub](https://github.com/alex-stoica/flet-android-notifications).
@@ -0,0 +1,13 @@
1
+ README.md
2
+ pyproject.toml
3
+ src/flet_android_notifications/__init__.py
4
+ src/flet_android_notifications/flet_android_notifications.py
5
+ src/flet_android_notifications.egg-info/PKG-INFO
6
+ src/flet_android_notifications.egg-info/SOURCES.txt
7
+ src/flet_android_notifications.egg-info/dependency_links.txt
8
+ src/flet_android_notifications.egg-info/requires.txt
9
+ src/flet_android_notifications.egg-info/top_level.txt
10
+ src/flutter/flet_android_notifications/pubspec.yaml
11
+ src/flutter/flet_android_notifications/lib/flet_android_notifications.dart
12
+ src/flutter/flet_android_notifications/lib/src/extension.dart
13
+ src/flutter/flet_android_notifications/lib/src/notifications_service.dart
@@ -0,0 +1,2 @@
1
+ flet_android_notifications
2
+ flutter
@@ -0,0 +1,3 @@
1
+ library flet_android_notifications;
2
+
3
+ export "src/extension.dart" show Extension;
@@ -0,0 +1,15 @@
1
+ import 'package:flet/flet.dart';
2
+
3
+ import 'notifications_service.dart';
4
+
5
+ class Extension extends FletExtension {
6
+ @override
7
+ FletService? createService(Control control) {
8
+ switch (control.type) {
9
+ case "flet_android_notifications":
10
+ return NotificationsService(control: control);
11
+ default:
12
+ return null;
13
+ }
14
+ }
15
+ }
@@ -0,0 +1,185 @@
1
+ import 'dart:async';
2
+ import 'dart:convert';
3
+ import 'package:flet/flet.dart';
4
+ import 'package:flutter_local_notifications/flutter_local_notifications.dart';
5
+
6
+ class NotificationsService extends FletService {
7
+ NotificationsService({required super.control});
8
+
9
+ final FlutterLocalNotificationsPlugin _plugin =
10
+ FlutterLocalNotificationsPlugin();
11
+ Completer<bool>? _initCompleter;
12
+ DateTime? _lastShowTime;
13
+
14
+ @override
15
+ void init() {
16
+ super.init();
17
+ control.addInvokeMethodListener(_onMethod);
18
+ _ensureInitialized();
19
+ }
20
+
21
+ @override
22
+ void dispose() {
23
+ control.removeInvokeMethodListener(_onMethod);
24
+ super.dispose();
25
+ }
26
+
27
+ Future<bool> _ensureInitialized() async {
28
+ if (_initCompleter != null) return _initCompleter!.future;
29
+ _initCompleter = Completer<bool>();
30
+
31
+ try {
32
+ const androidSettings =
33
+ AndroidInitializationSettings('@mipmap/ic_launcher');
34
+ const initSettings = InitializationSettings(android: androidSettings);
35
+
36
+ final result = await _plugin.initialize(
37
+ initSettings,
38
+ onDidReceiveNotificationResponse: (response) {
39
+ final hasAction = response.actionId != null && response.actionId!.isNotEmpty;
40
+ // Debounce only body taps — Samsung OneUI fires phantom body taps
41
+ // on show, but action button presses are always intentional.
42
+ if (!hasAction &&
43
+ _lastShowTime != null &&
44
+ DateTime.now().difference(_lastShowTime!).inSeconds < 3) {
45
+ return;
46
+ }
47
+ control.triggerEvent("notification_tap", jsonEncode({
48
+ "payload": response.payload ?? "",
49
+ "action_id": response.actionId ?? "",
50
+ }));
51
+ },
52
+ );
53
+ _initCompleter!.complete(result ?? false);
54
+ } catch (e) {
55
+ _initCompleter!.complete(false);
56
+ }
57
+
58
+ return _initCompleter!.future;
59
+ }
60
+
61
+ Importance _parseImportance(String value) {
62
+ switch (value) {
63
+ case "none":
64
+ return Importance.none;
65
+ case "min":
66
+ return Importance.min;
67
+ case "low":
68
+ return Importance.low;
69
+ case "default":
70
+ return Importance.defaultImportance;
71
+ case "high":
72
+ return Importance.high;
73
+ case "max":
74
+ return Importance.max;
75
+ default:
76
+ return Importance.high;
77
+ }
78
+ }
79
+
80
+ Priority _priorityFromImportance(Importance importance) {
81
+ switch (importance) {
82
+ case Importance.none:
83
+ case Importance.min:
84
+ return Priority.min;
85
+ case Importance.low:
86
+ return Priority.low;
87
+ case Importance.defaultImportance:
88
+ return Priority.defaultPriority;
89
+ case Importance.high:
90
+ return Priority.high;
91
+ case Importance.max:
92
+ return Priority.max;
93
+ default:
94
+ return Priority.defaultPriority;
95
+ }
96
+ }
97
+
98
+ Future<dynamic> _onMethod(String name, dynamic args) async {
99
+ try {
100
+ switch (name) {
101
+ case "show_notification":
102
+ final a = Map<String, dynamic>.from(args as Map);
103
+ final importance = _parseImportance(a["importance"] as String);
104
+ await _showNotification(
105
+ a["id"] as int,
106
+ a["title"] as String,
107
+ a["body"] as String,
108
+ payload: a["payload"] as String,
109
+ channelId: a["channel_id"] as String,
110
+ channelName: a["channel_name"] as String,
111
+ channelDescription: a["channel_description"] as String,
112
+ importance: importance,
113
+ priority: _priorityFromImportance(importance),
114
+ playSound: a["play_sound"] as bool,
115
+ enableVibration: a["enable_vibration"] as bool,
116
+ actions: (a["actions"] as List<dynamic>)
117
+ .map((action) => AndroidNotificationAction(
118
+ action["id"] as String,
119
+ action["title"] as String,
120
+ cancelNotification: true,
121
+ showsUserInterface: true,
122
+ ))
123
+ .toList(),
124
+ );
125
+ return "ok";
126
+ case "cancel":
127
+ final a = Map<String, dynamic>.from(args as Map);
128
+ await _plugin.cancel(a["id"] as int);
129
+ return "ok";
130
+ case "cancel_all":
131
+ await _plugin.cancelAll();
132
+ return "ok";
133
+ case "request_permissions":
134
+ final granted = await _requestPermissions();
135
+ return granted.toString();
136
+ }
137
+ return null;
138
+ } catch (e) {
139
+ return "error:$e";
140
+ }
141
+ }
142
+
143
+ Future<void> _showNotification(
144
+ int id,
145
+ String title,
146
+ String body, {
147
+ required String payload,
148
+ required String channelId,
149
+ required String channelName,
150
+ required String channelDescription,
151
+ required Importance importance,
152
+ required Priority priority,
153
+ required bool playSound,
154
+ required bool enableVibration,
155
+ required List<AndroidNotificationAction> actions,
156
+ }) async {
157
+ await _ensureInitialized();
158
+ _lastShowTime = DateTime.now();
159
+
160
+ final androidDetails = AndroidNotificationDetails(
161
+ channelId,
162
+ channelName,
163
+ channelDescription: channelDescription,
164
+ importance: importance,
165
+ priority: priority,
166
+ playSound: playSound,
167
+ enableVibration: enableVibration,
168
+ actions: actions,
169
+ );
170
+ final details = NotificationDetails(android: androidDetails);
171
+
172
+ await _plugin.show(id, title, body, details, payload: payload);
173
+ }
174
+
175
+ Future<bool> _requestPermissions() async {
176
+ await _ensureInitialized();
177
+
178
+ final android = _plugin.resolvePlatformSpecificImplementation<
179
+ AndroidFlutterLocalNotificationsPlugin>();
180
+ if (android == null) return false;
181
+
182
+ final granted = await android.requestNotificationsPermission();
183
+ return granted ?? false;
184
+ }
185
+ }
@@ -0,0 +1,19 @@
1
+ name: flet_android_notifications
2
+ description: Local notifications for Flet
3
+ version: 0.1.0
4
+ publish_to: none
5
+
6
+ environment:
7
+ sdk: '>=3.3.0 <4.0.0'
8
+ flutter: ">=1.17.0"
9
+
10
+ dependencies:
11
+ flet: ^0.80.2
12
+ flutter:
13
+ sdk: flutter
14
+ flutter_local_notifications: ^19.0.0
15
+
16
+ dev_dependencies:
17
+ flutter_test:
18
+ sdk: flutter
19
+ flutter_lints: ^3.0.0