diff --git a/backend/app/config.py b/backend/app/config.py
index 612129925..73dc3a159 100644
--- a/backend/app/config.py
+++ b/backend/app/config.py
@@ -29,6 +29,7 @@
from flask_bcrypt import Bcrypt
from flask_jwt_extended import JWTManager
from flask_apscheduler import APScheduler
+from flask_pywebpush import WebPush
import sqlite_icu
import os
@@ -170,6 +171,7 @@ def get_secret(env_var: str, default: str = None) -> str | None:
migrate = Migrate(app, db, render_as_batch=True)
bcrypt = Bcrypt(app)
jwt = JWTManager(app)
+push = WebPush(app)
socketio = SocketIO(
app,
json=app.json,
diff --git a/backend/app/models/__init__.py b/backend/app/models/__init__.py
index 9af3ad90f..662702b3d 100644
--- a/backend/app/models/__init__.py
+++ b/backend/app/models/__init__.py
@@ -18,3 +18,4 @@
from .challenge_password_reset import ChallengePasswordReset
from .oidc import OIDCLink, OIDCRequest
from .report import Report
+from .push_notification_subscription import PushNotificationSubscription
diff --git a/backend/app/models/push_notification_subscription.py b/backend/app/models/push_notification_subscription.py
new file mode 100644
index 000000000..7d57f46a2
--- /dev/null
+++ b/backend/app/models/push_notification_subscription.py
@@ -0,0 +1,41 @@
+from typing import Any, Self, TYPE_CHECKING, cast
+
+from app import db
+from sqlalchemy.orm import Mapped
+
+Model = db.Model
+if TYPE_CHECKING:
+ from app.models import Token
+ from app.helpers.db_model_base import DbModelBase
+
+ Model = DbModelBase
+
+
+class PushNotificationSubscription(Model):
+ __tablename__ = "push_notification_subscription"
+
+ endpoint: Mapped[str] = db.Column(db.String(), primary_key=True)
+ pubkey: Mapped[str] = db.Column(db.String())
+ auth: Mapped[str] = db.Column(db.String())
+
+ # Should never be an access token
+ created_by_token_id: Mapped[int] = db.Column(
+ db.Integer, db.ForeignKey("token.jti"), nullable=False
+ )
+ created_by_token: Mapped["Token"] = cast(
+ Mapped["Token"],
+ db.relationship("Token"),
+ )
+
+ def toSubsciptionInfo(self) -> dict[str, Any]:
+ return {
+ "endpoint": self.endpoint,
+ "keys": {
+ "p256dh": self.pubkey,
+ "auth": self.auth,
+ },
+ }
+
+ @classmethod
+ def find_by_user(cls, user_id: int) -> list[Self] | None:
+ return cls.query.filter(cls.created_by_token.user_id == user_id).all()
diff --git a/backend/app/models/token.py b/backend/app/models/token.py
index 1d8c5eed2..8ec1b1639 100644
--- a/backend/app/models/token.py
+++ b/backend/app/models/token.py
@@ -11,6 +11,7 @@
Model = db.Model
if TYPE_CHECKING:
+ from app.models import PushNotificationSubscription
from app.helpers.db_model_base import DbModelBase
Model = DbModelBase
@@ -53,6 +54,16 @@ class Token(Model):
lazy="selectin",
),
)
+ created_push_notification_subscriptions: Mapped[
+ List["PushNotificationSubscription"]
+ ] = cast(
+ Mapped[List["PushNotificationSubscription"]],
+ db.relationship(
+ "PushNotificationSubscription",
+ back_populates="created_by_token",
+ cascade="all, delete-orphan",
+ ),
+ )
def obj_to_dict(
self,
diff --git a/backend/pyproject.toml b/backend/pyproject.toml
index 5463c611c..fc0e4da3f 100644
--- a/backend/pyproject.toml
+++ b/backend/pyproject.toml
@@ -42,6 +42,7 @@ dependencies = [
"psycopg2-binary>=2.9.10",
"uWSGI>=2.0.28",
"uwsgi-tools>=1.1.1",
+ "flask-pywebpush>=1.1",
]
[dependency-groups]
diff --git a/backend/uv.lock b/backend/uv.lock
index 049f3bf5e..d73596f62 100644
--- a/backend/uv.lock
+++ b/backend/uv.lock
@@ -737,6 +737,19 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/d2/c4/3f329b23d769fe7628a5fc57ad36956f1fb7132cf8837be6da762b197327/Flask_Migrate-4.1.0-py3-none-any.whl", hash = "sha256:24d8051af161782e0743af1b04a152d007bad9772b2bca67b7ec1e8ceeb3910d", size = 21237, upload-time = "2025-01-10T18:51:09.527Z" },
]
+[[package]]
+name = "flask-pywebpush"
+version = "1.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "flask" },
+ { name = "pywebpush" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/3b/b3/65eefcdfbb2381cb1d58a9973bfdfb113178c6a9633eb384e0d0a39cd36b/Flask-pyWebPush-1.1.tar.gz", hash = "sha256:aa6b6391f35b77727948fd416a5bb4051fac916d53c5dccb8af35ff768741a96", size = 3487, upload-time = "2020-03-30T22:39:00.2Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/7b/df/74ca48a77785d5c5d02eb80202d59ac66ef25c880f585559968f71227df8/Flask_pyWebPush-1.1-py3-none-any.whl", hash = "sha256:018187d9deac755ebae9fd774801704b08540c50dd0856a1e8599fcbc9e57239", size = 4291, upload-time = "2020-03-30T22:38:58.626Z" },
+]
+
[[package]]
name = "flask-socketio"
version = "5.5.1"
@@ -979,6 +992,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/6c/dd/a834df6482147d48e225a49515aabc28974ad5a4ca3215c18a882565b028/html5lib-1.1-py2.py3-none-any.whl", hash = "sha256:0d78f8fde1c230e99fe37986a60526d7049ed4bf8a9fadbad5f00e22e58e041d", size = 112173, upload-time = "2020-06-22T23:32:36.781Z" },
]
+[[package]]
+name = "http-ece"
+version = "1.2.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "cryptography" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/7c/af/249d1576653b69c20b9ac30e284b63bd94af6a175d72d87813235caf2482/http_ece-1.2.1.tar.gz", hash = "sha256:8c6ab23116bbf6affda894acfd5f2ca0fb8facbcbb72121c11c75c33e7ce8cff", size = 8830, upload-time = "2024-08-08T00:10:47.301Z" }
+
[[package]]
name = "httpcore"
version = "1.0.9"
@@ -1204,6 +1226,7 @@ dependencies = [
{ name = "flask-bcrypt" },
{ name = "flask-jwt-extended" },
{ name = "flask-migrate" },
+ { name = "flask-pywebpush" },
{ name = "flask-socketio" },
{ name = "flask-sqlalchemy" },
{ name = "gevent" },
@@ -1254,6 +1277,7 @@ requires-dist = [
{ name = "flask-bcrypt", specifier = ">=1.0.1" },
{ name = "flask-jwt-extended", specifier = ">=4.7.1" },
{ name = "flask-migrate", specifier = ">=4.1.0" },
+ { name = "flask-pywebpush", specifier = ">=1.1" },
{ name = "flask-socketio", specifier = ">=5.5.1" },
{ name = "flask-sqlalchemy", specifier = ">=3.1.1" },
{ name = "gevent", specifier = ">=24.11.1" },
@@ -1889,6 +1913,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/e1/36/9c0c326fe3a4227953dfb29f5d0c8ae3b8eb8c1cd2967aa569f50cb3c61f/psycopg2_binary-2.9.11-cp314-cp314-win_amd64.whl", hash = "sha256:4012c9c954dfaccd28f94e84ab9f94e12df76b4afb22331b1f0d3154893a6316", size = 2803913, upload-time = "2025-10-10T11:13:57.058Z" },
]
+[[package]]
+name = "py-vapid"
+version = "1.9.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "cryptography" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/ff/57/5c1c61f27ce01f939443cf3f6c279a295f7ec0327b18a1cbbcfefe0b5456/py_vapid-1.9.2.tar.gz", hash = "sha256:3c8973b6cf8384ad0c9ae64d6270ccc480e0b92c702d8f5ea2cc03e6b51247f9", size = 20300, upload-time = "2024-11-19T21:55:41.859Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/73/fb/b877a221b09dabcebeb073d5e7f19244f3fa1d5aec87092c359a6049a006/py_vapid-1.9.2-py3-none-any.whl", hash = "sha256:4ccf8a00fc54f1f99f66fb543c96f2c82622508ad814b6e9225f2c26948934d7", size = 21492, upload-time = "2024-11-19T21:55:40.832Z" },
+]
+
[[package]]
name = "pycparser"
version = "2.23"
@@ -2145,6 +2181,23 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload-time = "2025-03-25T02:24:58.468Z" },
]
+[[package]]
+name = "pywebpush"
+version = "2.0.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "aiohttp" },
+ { name = "cryptography" },
+ { name = "http-ece" },
+ { name = "py-vapid" },
+ { name = "requests" },
+ { name = "six" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/41/ca/6e669bf676916d66c8c7adedc291e9a9758650f9d85ec040fda13e3c82f4/pywebpush-2.0.3.tar.gz", hash = "sha256:584878e3c243e873a22db8895505d95715bc796ef74cc1b8fe99f596174161e3", size = 25874, upload-time = "2024-11-19T21:30:20.444Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/90/8a/ecaa2a589338a038b89148b01a5db2dac53e45918342da69baca1fb058fc/pywebpush-2.0.3-py3-none-any.whl", hash = "sha256:04666441715bc547918d7668b2ac7ad5c4b5de7d0a6cf528daf61e0c4bc5431c", size = 21364, upload-time = "2024-11-19T21:30:19.312Z" },
+]
+
[[package]]
name = "pyyaml"
version = "6.0.3"
diff --git a/kitchenowl/android/app/build.gradle b/kitchenowl/android/app/build.gradle
index ae9abee29..e13c39ab1 100644
--- a/kitchenowl/android/app/build.gradle
+++ b/kitchenowl/android/app/build.gradle
@@ -31,6 +31,14 @@ if (keystorePropertiesFile.exists()) {
dependencies {
implementation 'com.google.errorprone:error_prone_annotations:2.36.0' // required by flutter_secure_storage
implementation 'com.github.spotbugs:spotbugs-annotations:4.8.6' // required by flutter_secure_storage
+ coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.5' // required by local notifications
+ implementation 'androidx.window:window:1.0.0' // required by local notifications
+ implementation 'androidx.window:window-java:1.0.0' // required by local notifications
+ implementation 'org.unifiedpush.android:embedded-fcm-distributor:3.0.0' // required by unified push
+}
+
+configurations.configureEach {
+ exclude group: 'com.google.crypto.tink', module: 'tink'
}
android {
@@ -39,6 +47,8 @@ android {
ndkVersion flutter.ndkVersion
compileOptions {
+ // Flag to enable support for the new language APIs (local notifications)
+ coreLibraryDesugaringEnabled true
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
diff --git a/kitchenowl/android/app/src/main/res/drawable-anydpi-v24/notification_icon.xml b/kitchenowl/android/app/src/main/res/drawable-anydpi-v24/notification_icon.xml
new file mode 100644
index 000000000..0b082f05a
--- /dev/null
+++ b/kitchenowl/android/app/src/main/res/drawable-anydpi-v24/notification_icon.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
diff --git a/kitchenowl/android/app/src/main/res/drawable-hdpi/notification_icon.png b/kitchenowl/android/app/src/main/res/drawable-hdpi/notification_icon.png
new file mode 100644
index 000000000..5d4c0ffd3
Binary files /dev/null and b/kitchenowl/android/app/src/main/res/drawable-hdpi/notification_icon.png differ
diff --git a/kitchenowl/android/app/src/main/res/drawable-mdpi/notification_icon.png b/kitchenowl/android/app/src/main/res/drawable-mdpi/notification_icon.png
new file mode 100644
index 000000000..11e607922
Binary files /dev/null and b/kitchenowl/android/app/src/main/res/drawable-mdpi/notification_icon.png differ
diff --git a/kitchenowl/android/app/src/main/res/drawable-xhdpi/notification_icon.png b/kitchenowl/android/app/src/main/res/drawable-xhdpi/notification_icon.png
new file mode 100644
index 000000000..3e95dddad
Binary files /dev/null and b/kitchenowl/android/app/src/main/res/drawable-xhdpi/notification_icon.png differ
diff --git a/kitchenowl/android/app/src/main/res/drawable-xxhdpi/notification_icon.png b/kitchenowl/android/app/src/main/res/drawable-xxhdpi/notification_icon.png
new file mode 100644
index 000000000..357407d0f
Binary files /dev/null and b/kitchenowl/android/app/src/main/res/drawable-xxhdpi/notification_icon.png differ
diff --git a/kitchenowl/android/app/src/main/res/raw/com_tombursch_kitchenowl_keep.xml b/kitchenowl/android/app/src/main/res/raw/com_tombursch_kitchenowl_keep.xml
new file mode 100644
index 000000000..512c34250
--- /dev/null
+++ b/kitchenowl/android/app/src/main/res/raw/com_tombursch_kitchenowl_keep.xml
@@ -0,0 +1,3 @@
+
+
\ No newline at end of file
diff --git a/kitchenowl/lib/app.dart b/kitchenowl/lib/app.dart
index 082cb9b09..1184f01ae 100644
--- a/kitchenowl/lib/app.dart
+++ b/kitchenowl/lib/app.dart
@@ -17,6 +17,7 @@ import 'package:kitchenowl/kitchenowl.dart';
import 'package:kitchenowl/router.dart';
import 'package:kitchenowl/services/api/api_service.dart';
import 'package:kitchenowl/services/background_task.dart';
+import 'package:kitchenowl/services/notification_service.dart';
import 'package:kitchenowl/services/storage/storage.dart';
import 'package:kitchenowl/services/transaction_handler.dart';
import 'package:kitchenowl/styles/colors.dart';
@@ -109,6 +110,9 @@ class _AppState extends State {
},
);
}
+
+ NotificationService.getInstance().initialize().whenComplete(
+ () => widget._settingsCubit.refreshNotificationDistributor());
}
@override
diff --git a/kitchenowl/lib/cubits/settings_cubit.dart b/kitchenowl/lib/cubits/settings_cubit.dart
index 3b40b6a5e..2e75cfee2 100644
--- a/kitchenowl/lib/cubits/settings_cubit.dart
+++ b/kitchenowl/lib/cubits/settings_cubit.dart
@@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:kitchenowl/config.dart';
import 'package:kitchenowl/kitchenowl.dart';
+import 'package:kitchenowl/services/notification_service.dart';
import 'package:kitchenowl/services/storage/storage.dart';
import 'package:package_info_plus/package_info_plus.dart';
@@ -55,6 +56,14 @@ class SettingsCubit extends Cubit {
));
}
+ Future refreshNotificationDistributor() async {
+ await Future.delayed(const Duration(seconds: 1));
+ String? distributor =
+ await NotificationService.getInstance().getDistributor();
+ print("Refresh distributor: ${distributor}");
+ emit(state.copyWith(notificationDistributor: Nullable(distributor)));
+ }
+
void setTheme(ThemeMode themeMode) {
PreferenceStorage.getInstance()
.writeInt(key: 'themeMode', value: themeMode.index);
@@ -133,6 +142,7 @@ class SettingsState extends Equatable {
final bool shoppingListTapToRemove;
final bool recentItemsCategorize;
final bool restoreLastShoppingList;
+ final String? notificationDistributor;
const SettingsState({
this.themeMode = ThemeMode.system,
@@ -144,6 +154,7 @@ class SettingsState extends Equatable {
this.shoppingListTapToRemove = true,
this.recentItemsCategorize = false,
this.restoreLastShoppingList = false,
+ this.notificationDistributor,
});
SettingsState copyWith({
@@ -156,6 +167,7 @@ class SettingsState extends Equatable {
bool? shoppingListTapToRemove,
bool? recentItemsCategorize,
bool? restoreLastShoppingList,
+ Nullable? notificationDistributor,
}) =>
SettingsState(
themeMode: themeMode ?? this.themeMode,
@@ -170,6 +182,9 @@ class SettingsState extends Equatable {
recentItemsCategorize ?? this.recentItemsCategorize,
restoreLastShoppingList:
restoreLastShoppingList ?? this.restoreLastShoppingList,
+ notificationDistributor:
+ (notificationDistributor ?? Nullable(this.notificationDistributor))
+ .value,
);
@override
@@ -183,5 +198,6 @@ class SettingsState extends Equatable {
shoppingListTapToRemove,
recentItemsCategorize,
restoreLastShoppingList,
+ notificationDistributor
];
}
diff --git a/kitchenowl/lib/pages/settings_page.dart b/kitchenowl/lib/pages/settings_page.dart
index 66b527532..5d164586e 100644
--- a/kitchenowl/lib/pages/settings_page.dart
+++ b/kitchenowl/lib/pages/settings_page.dart
@@ -20,11 +20,13 @@ import 'package:kitchenowl/pages/household_update_page.dart';
import 'package:kitchenowl/pages/reports_list_page.dart';
import 'package:kitchenowl/pages/settings_server_user_page.dart';
import 'package:kitchenowl/services/api/api_service.dart';
+import 'package:kitchenowl/services/notification_service.dart';
import 'package:kitchenowl/styles/colors.dart';
import 'package:kitchenowl/widgets/settings/color_button.dart';
import 'package:kitchenowl/widgets/sliver_expansion_tile.dart';
import 'package:kitchenowl/widgets/user_list_tile.dart';
import 'package:sliver_tools/sliver_tools.dart';
+import 'package:unifiedpush_ui/unifiedpush_ui.dart';
class SettingsPage extends StatefulWidget {
final Household? household;
@@ -381,6 +383,33 @@ class _SettingsPageState extends State {
),
),
),
+ if (!kIsWeb && (Platform.isAndroid || Platform.isLinux))
+ ListTile(
+ title: Text(
+ AppLocalizations.of(context)!.server,
+ ),
+ leading: const Icon(Icons.notifications_rounded),
+ onTap: () async {
+ state.notificationDistributor != null
+ ? await NotificationService.getInstance()
+ .unregister()
+ : await UnifiedPushUi(
+ context: context,
+ instances: [NotificationService.instanceName],
+ unifiedPushFunctions:
+ NotificationService.getInstance(),
+ showNoDistribDialog: true,
+ onNoDistribDialogDismissed: () {},
+ ).registerAppWithDialog();
+ BlocProvider.of(context)
+ .refreshNotificationDistributor();
+ },
+ trailing: Padding(
+ padding: const EdgeInsets.only(right: 8),
+ child: Text(state.notificationDistributor ??
+ AppLocalizations.of(context)!.none),
+ ),
+ ),
],
),
),
diff --git a/kitchenowl/lib/services/notification_service.dart b/kitchenowl/lib/services/notification_service.dart
new file mode 100644
index 000000000..21f4f2ecc
--- /dev/null
+++ b/kitchenowl/lib/services/notification_service.dart
@@ -0,0 +1,166 @@
+import 'dart:convert';
+import 'dart:io';
+
+import 'package:flutter/foundation.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_local_notifications/flutter_local_notifications.dart';
+import 'package:unifiedpush/unifiedpush.dart';
+import 'package:unifiedpush_platform_interface/unifiedpush_platform_interface.dart';
+import 'package:unifiedpush_storage_shared_preferences/storage.dart';
+import 'package:unifiedpush_ui/unifiedpush_ui.dart';
+
+class NotificationService extends UnifiedPushFunctions {
+ static const String instanceName = "org.tombursch.kitchenowl";
+
+ static NotificationService? _instance;
+
+ String? endpoint;
+
+ NotificationService.internal();
+
+ static NotificationService getInstance() {
+ _instance ??= NotificationService.internal();
+
+ return _instance!;
+ }
+
+ Future initialize() async {
+ debugPrint("Initializing UnifiedPush");
+ UnifiedPush.initialize(
+ onNewEndpoint: onNewEndpoint,
+ onRegistrationFailed: onRegistrationFailed,
+ onUnregistered: onUnregistered,
+ onMessage: onNotification,
+ linuxOptions: LinuxOptions(
+ dbusName: instanceName,
+ storage: UnifiedPushStorageSharedPreferences(),
+ background: false,
+ ),
+ );
+
+ try {
+ if (!kIsWeb && Platform.isAndroid) {
+ final AndroidFlutterLocalNotificationsPlugin? androidImplementation =
+ NotificationService.getInstance()
+ .flutterLocalNotificationsPlugin
+ .resolvePlatformSpecificImplementation<
+ AndroidFlutterLocalNotificationsPlugin>();
+
+ androidImplementation?.requestNotificationsPermission();
+ }
+ } on Exception catch (_) {
+ debugPrint("Exception while granting permissions");
+ }
+
+ UnifiedPush.tryUseCurrentOrDefaultDistributor().then((success) async {
+ if (success) {
+ String? distributor = await getDistributor();
+ if (distributor != null) registerApp(distributor);
+ }
+ });
+ }
+
+ Future unregister() => UnifiedPush.unregister(instanceName);
+
+ @override
+ Future getDistributor() => UnifiedPush.getDistributor();
+
+ @override
+ Future> getDistributors() => UnifiedPush.getDistributors();
+
+ @override
+ Future registerApp(String instance) =>
+ UnifiedPush.registerApp(instance);
+
+ @override
+ Future saveDistributor(String distributor) =>
+ UnifiedPush.saveDistributor(distributor);
+
+ void onNewEndpoint(PushEndpoint endpoint, String instance) {
+ this.endpoint = endpoint.url;
+ debugPrint(
+ "New endpoint: https://unifiedpush.org/test_wp.html#endpoint=${endpoint.url}&p256dh=${endpoint.pubKeySet?.pubKey}&auth=${endpoint.pubKeySet?.auth}");
+ }
+
+ void onRegistrationFailed(FailedReason reason, String instance) {
+ onUnregistered(instance);
+ }
+
+ void onUnregistered(String instance) {
+ endpoint = null;
+ debugPrint("unregistered ${instance}");
+ }
+
+ final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
+ FlutterLocalNotificationsPlugin();
+
+ static bool _notificationInitialized = false;
+
+ Map decodeMessageContentsUri(String message) {
+ List uri = Uri.decodeComponent(message).split("&");
+ Map decoded = {};
+ for (var i in uri) {
+ try {
+ decoded[i.split("=")[0]] = i.split("=")[1];
+ } on Exception {
+ debugPrint("Couldn't decode $i");
+ }
+ }
+ return decoded;
+ }
+
+ Future onNotification(
+ PushMessage message,
+ String instance,
+ ) async {
+ var payload = utf8.decode(message.content);
+
+ String title = 'KitchenOwl'; // Default title
+ String body = 'Could not get the content'; // Default body
+
+ try {
+ // Try to decode title and message (JSON)
+ Map decodedMessage = decodeMessageContentsUri(payload);
+ title = decodedMessage['title'] ?? title;
+ body = decodedMessage['message'] ?? body;
+ } catch (e) {
+ // If decoding fails, use plain payload as body
+ body = payload.isNotEmpty ? payload : 'Empty message';
+ }
+
+ if (!_notificationInitialized) _initNotifications();
+
+ var androidPlatformChannelSpecifics = const AndroidNotificationDetails(
+ 'KitchenOwl',
+ 'KitchenOwl',
+ playSound: false,
+ );
+ var platformChannelSpecifics = NotificationDetails(
+ android: androidPlatformChannelSpecifics,
+ );
+ await flutterLocalNotificationsPlugin.show(
+ DateTime.now().microsecondsSinceEpoch % 100000000,
+ title,
+ body,
+ platformChannelSpecifics,
+ );
+ return true;
+ }
+
+ void _initNotifications() async {
+ WidgetsFlutterBinding.ensureInitialized();
+
+ const AndroidInitializationSettings initializationSettingsAndroid =
+ AndroidInitializationSettings('notification_icon');
+ const LinuxInitializationSettings initializationSettingsLinux =
+ LinuxInitializationSettings(defaultActionName: 'open');
+ const InitializationSettings initializationSettings =
+ InitializationSettings(
+ android: initializationSettingsAndroid,
+ linux: initializationSettingsLinux,
+ );
+ _notificationInitialized = await flutterLocalNotificationsPlugin
+ .initialize(initializationSettings) ??
+ false;
+ }
+}
diff --git a/kitchenowl/linux/flutter/generated_plugin_registrant.cc b/kitchenowl/linux/flutter/generated_plugin_registrant.cc
index 80530edab..7d55788c5 100644
--- a/kitchenowl/linux/flutter/generated_plugin_registrant.cc
+++ b/kitchenowl/linux/flutter/generated_plugin_registrant.cc
@@ -9,7 +9,10 @@
#include
#include
#include
+#include
#include
+#include
+#include
void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) dynamic_system_colors_registrar =
@@ -21,7 +24,16 @@ void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin");
flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar);
+ g_autoptr(FlPluginRegistrar) screen_retriever_linux_registrar =
+ fl_plugin_registry_get_registrar_for_plugin(registry, "ScreenRetrieverLinuxPlugin");
+ screen_retriever_linux_plugin_register_with_registrar(screen_retriever_linux_registrar);
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);
+ g_autoptr(FlPluginRegistrar) webcrypto_registrar =
+ fl_plugin_registry_get_registrar_for_plugin(registry, "WebcryptoPlugin");
+ webcrypto_plugin_register_with_registrar(webcrypto_registrar);
+ g_autoptr(FlPluginRegistrar) window_manager_registrar =
+ fl_plugin_registry_get_registrar_for_plugin(registry, "WindowManagerPlugin");
+ window_manager_plugin_register_with_registrar(window_manager_registrar);
}
diff --git a/kitchenowl/linux/flutter/generated_plugins.cmake b/kitchenowl/linux/flutter/generated_plugins.cmake
index e5db312d1..7c386f700 100644
--- a/kitchenowl/linux/flutter/generated_plugins.cmake
+++ b/kitchenowl/linux/flutter/generated_plugins.cmake
@@ -6,7 +6,10 @@ list(APPEND FLUTTER_PLUGIN_LIST
dynamic_system_colors
file_selector_linux
flutter_secure_storage_linux
+ screen_retriever_linux
url_launcher_linux
+ webcrypto
+ window_manager
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST
diff --git a/kitchenowl/pubspec.lock b/kitchenowl/pubspec.lock
old mode 100755
new mode 100644
index 5a841a9d2..580956633
--- a/kitchenowl/pubspec.lock
+++ b/kitchenowl/pubspec.lock
@@ -371,6 +371,38 @@ packages:
url: "https://pub.dev"
source: hosted
version: "6.0.0"
+ flutter_local_notifications:
+ dependency: "direct main"
+ description:
+ name: flutter_local_notifications
+ sha256: "19ffb0a8bb7407875555e5e98d7343a633bb73707bae6c6a5f37c90014077875"
+ url: "https://pub.dev"
+ source: hosted
+ version: "19.5.0"
+ flutter_local_notifications_linux:
+ dependency: transitive
+ description:
+ name: flutter_local_notifications_linux
+ sha256: e3c277b2daab8e36ac5a6820536668d07e83851aeeb79c446e525a70710770a5
+ url: "https://pub.dev"
+ source: hosted
+ version: "6.0.0"
+ flutter_local_notifications_platform_interface:
+ dependency: transitive
+ description:
+ name: flutter_local_notifications_platform_interface
+ sha256: "277d25d960c15674ce78ca97f57d0bae2ee401c844b6ac80fcd972a9c99d09fe"
+ url: "https://pub.dev"
+ source: hosted
+ version: "9.1.0"
+ flutter_local_notifications_windows:
+ dependency: transitive
+ description:
+ name: flutter_local_notifications_windows
+ sha256: "8d658f0d367c48bd420e7cf2d26655e2d1130147bca1eea917e576ca76668aaf"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.0.3"
flutter_localizations:
dependency: "direct main"
description: flutter
@@ -628,6 +660,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.6.7"
+ json_annotation:
+ dependency: transitive
+ description:
+ name: json_annotation
+ sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1"
+ url: "https://pub.dev"
+ source: hosted
+ version: "4.9.0"
leak_tracker:
dependency: transitive
description:
@@ -892,6 +932,46 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.28.0"
+ screen_retriever:
+ dependency: transitive
+ description:
+ name: screen_retriever
+ sha256: "570dbc8e4f70bac451e0efc9c9bb19fa2d6799a11e6ef04f946d7886d2e23d0c"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.2.0"
+ screen_retriever_linux:
+ dependency: transitive
+ description:
+ name: screen_retriever_linux
+ sha256: f7f8120c92ef0784e58491ab664d01efda79a922b025ff286e29aa123ea3dd18
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.2.0"
+ screen_retriever_macos:
+ dependency: transitive
+ description:
+ name: screen_retriever_macos
+ sha256: "71f956e65c97315dd661d71f828708bd97b6d358e776f1a30d5aa7d22d78a149"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.2.0"
+ screen_retriever_platform_interface:
+ dependency: transitive
+ description:
+ name: screen_retriever_platform_interface
+ sha256: ee197f4581ff0d5608587819af40490748e1e39e648d7680ecf95c05197240c0
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.2.0"
+ screen_retriever_windows:
+ dependency: transitive
+ description:
+ name: screen_retriever_windows
+ sha256: "449ee257f03ca98a57288ee526a301a430a344a161f9202b4fcc38576716fe13"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.2.0"
share_handler:
dependency: "direct main"
description:
@@ -1161,6 +1241,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.7.7"
+ timezone:
+ dependency: transitive
+ description:
+ name: timezone
+ sha256: dd14a3b83cfd7cb19e7888f1cbc20f258b8d71b54c06f79ac585f14093a287d1
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.10.1"
transparent_image:
dependency: "direct main"
description:
@@ -1185,6 +1273,62 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.4.0"
+ unifiedpush:
+ dependency: "direct main"
+ description:
+ name: unifiedpush
+ sha256: "8ed9767f750a1dc6159a77e2171641d0cb825dc87682d1ce1b8618689b79f58e"
+ url: "https://pub.dev"
+ source: hosted
+ version: "6.2.0"
+ unifiedpush_android:
+ dependency: transitive
+ description:
+ name: unifiedpush_android
+ sha256: "2e6684e0a1a39ad8d6c7bc0d197954d50686acb1b1a7614b18ab0e0126f5492a"
+ url: "https://pub.dev"
+ source: hosted
+ version: "3.4.1"
+ unifiedpush_linux:
+ dependency: transitive
+ description:
+ name: unifiedpush_linux
+ sha256: c062d5eedd1cec70bcd33270cc4e01ae0ff6501f33d471167c06b34a968adfeb
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.0.0"
+ unifiedpush_platform_interface:
+ dependency: transitive
+ description:
+ name: unifiedpush_platform_interface
+ sha256: "83372bc8d794b8b12ef6993b518d7be907dcfc2191bdf6de0ece5c4445d89880"
+ url: "https://pub.dev"
+ source: hosted
+ version: "4.0.0"
+ unifiedpush_storage_interface:
+ dependency: transitive
+ description:
+ name: unifiedpush_storage_interface
+ sha256: b8d423a4695efc616aa21d8ab48fb5ef99d6288c68b56282b8faac1579ceabd9
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.0.0"
+ unifiedpush_storage_shared_preferences:
+ dependency: "direct main"
+ description:
+ name: unifiedpush_storage_shared_preferences
+ sha256: eda9c52bac0058f81e0d9c65e33eedc12c5901d2e68ad47fdf68175ddf00a3b4
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.0.0"
+ unifiedpush_ui:
+ dependency: "direct main"
+ description:
+ name: unifiedpush_ui
+ sha256: "1b36b2aa0bc6b61577e2661c1183bd3442969ecf77b4c78174796d324f66dd1d"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.2.0"
universal_html:
dependency: "direct main"
description:
@@ -1337,6 +1481,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.1.1"
+ webcrypto:
+ dependency: transitive
+ description:
+ name: webcrypto
+ sha256: e393b3d0b01694a8f81efecf278ed7392877130e6e7b29f578863e4f2d0b2ebd
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.5.8"
webdriver:
dependency: transitive
description:
@@ -1345,6 +1497,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.1.0"
+ webpush_encryption:
+ dependency: transitive
+ description:
+ name: webpush_encryption
+ sha256: "63046b7d6909f4a72ce3c153fa574726e257aaf21b1995ba063dc241a1b1520b"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.0.0"
win32:
dependency: transitive
description:
@@ -1361,6 +1521,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.0"
+ window_manager:
+ dependency: transitive
+ description:
+ name: window_manager
+ sha256: "7eb6d6c4164ec08e1bf978d6e733f3cebe792e2a23fb07cbca25c2872bfdbdcd"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.5.1"
xdg_directories:
dependency: transitive
description:
diff --git a/kitchenowl/pubspec.yaml b/kitchenowl/pubspec.yaml
index 26ace79a5..208b37b59 100644
--- a/kitchenowl/pubspec.yaml
+++ b/kitchenowl/pubspec.yaml
@@ -78,6 +78,12 @@ dependencies:
wakelock_plus: ^1.2.10
fuzzywuzzy: ^1.2.0
+ # Notifications
+ unifiedpush: ^6.2.0
+ unifiedpush_ui: ^0.2.0
+ unifiedpush_storage_shared_preferences: ^1.0.0
+ flutter_local_notifications: ^19.3.0
+
dev_dependencies:
flutter_test:
sdk: flutter
diff --git a/kitchenowl/windows/flutter/generated_plugin_registrant.cc b/kitchenowl/windows/flutter/generated_plugin_registrant.cc
index 6783f76c3..78824a9cd 100644
--- a/kitchenowl/windows/flutter/generated_plugin_registrant.cc
+++ b/kitchenowl/windows/flutter/generated_plugin_registrant.cc
@@ -9,8 +9,11 @@
#include
#include
#include
+#include
#include
#include
+#include
+#include
void RegisterPlugins(flutter::PluginRegistry* registry) {
DynamicColorPluginCApiRegisterWithRegistrar(
@@ -19,8 +22,14 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
registry->GetRegistrarForPlugin("FileSelectorWindows"));
FlutterSecureStorageWindowsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin"));
+ ScreenRetrieverWindowsPluginCApiRegisterWithRegistrar(
+ registry->GetRegistrarForPlugin("ScreenRetrieverWindowsPluginCApi"));
SharePlusWindowsPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi"));
UrlLauncherWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
+ WebcryptoPluginRegisterWithRegistrar(
+ registry->GetRegistrarForPlugin("WebcryptoPlugin"));
+ WindowManagerPluginRegisterWithRegistrar(
+ registry->GetRegistrarForPlugin("WindowManagerPlugin"));
}
diff --git a/kitchenowl/windows/flutter/generated_plugins.cmake b/kitchenowl/windows/flutter/generated_plugins.cmake
index a814e1474..0f0fd18f4 100644
--- a/kitchenowl/windows/flutter/generated_plugins.cmake
+++ b/kitchenowl/windows/flutter/generated_plugins.cmake
@@ -6,11 +6,15 @@ list(APPEND FLUTTER_PLUGIN_LIST
dynamic_system_colors
file_selector_windows
flutter_secure_storage_windows
+ screen_retriever_windows
share_plus
url_launcher_windows
+ webcrypto
+ window_manager
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST
+ flutter_local_notifications_windows
)
set(PLUGIN_BUNDLED_LIBRARIES)