From bececc35da08ea36d293eb7d98c6b55fe2f1d4d2 Mon Sep 17 00:00:00 2001 From: tr1x_em Date: Wed, 30 Jul 2025 22:13:14 +0530 Subject: [PATCH] Initial notification --- main.css | 1 + main.py | 22 +++--- modules/notification.py | 159 ++++++++++++++++++++++++++++++++++++++++ modules/panel/main.py | 4 + services/modus.py | 85 +++++++++++++++++++++ styles/notification.css | 57 ++++++++++++++ utils/roam.py | 30 ++++++++ 7 files changed, 348 insertions(+), 10 deletions(-) create mode 100644 modules/notification.py create mode 100644 services/modus.py create mode 100644 styles/notification.css create mode 100644 utils/roam.py diff --git a/main.css b/main.css index 9be58bb..61e5b2d 100644 --- a/main.css +++ b/main.css @@ -6,6 +6,7 @@ @import url("./styles/launcher.css"); @import url("./styles/osd.css"); @import url("./styles/about.css"); +@import url("./styles/notification.css"); * { all: unset; diff --git a/main.py b/main.py index 72602a4..95fb2a4 100644 --- a/main.py +++ b/main.py @@ -8,19 +8,20 @@ from modules.dock import Dock from modules.launcher.main import Launcher from modules.osd import OSD from modules.about import About +from modules.notification import EnvNoti # from modules.corners import Corners from modules.panel.main import Panel from modules.switcher import ApplicationSwitcher -for log in [ - "fabric.hyprland.widgets", - "fabric.audio.service", - "fabric.bluetooth.service", - "services.network", - "utils.wayland", -]: - logger.disable(log) +# for log in [ +# "fabric.hyprland.widgets", +# "fabric.audio.service", +# "fabric.bluetooth.service", +# "services.network", +# "utils.wayland", +# ]: +# logger.disable(log) if __name__ == "__main__": @@ -29,12 +30,13 @@ if __name__ == "__main__": # Load configuration from config.data import load_config - About().toggle(None) + # About().toggle(None) config = load_config() panel = Panel() # corners = Corners() dock = Dock() + envnoti = EnvNoti() switcher = ApplicationSwitcher() launcher = Launcher() panel.launcher = launcher @@ -49,7 +51,7 @@ if __name__ == "__main__": _ = css_file.connect("changed", lambda *_: set_css()) # Make sure corners is added to the app - app = Application(f"{APP_NAME}", panel, dock, switcher, launcher, osd) + app = Application(f"{APP_NAME}", panel, dock, switcher, launcher, envnoti, osd) def set_css(): app.set_stylesheet_from_file( diff --git a/modules/notification.py b/modules/notification.py new file mode 100644 index 0000000..523393a --- /dev/null +++ b/modules/notification.py @@ -0,0 +1,159 @@ +from typing import cast + +from fabric import Application +from fabric.widgets.box import Box +from fabric.widgets.label import Label +from fabric.widgets.image import Image +from fabric.widgets.button import Button +from utils.wayland import WaylandWindow as Window +from fabric.notifications import ( + NotificationAction, + Notifications, + Notification, + NotificationImagePixmap, +) +from fabric.utils import invoke_repeater, get_relative_path + +from utils.roam import modus_service + +from utils.custom_image import CustomImage + +from gi.repository import GdkPixbuf # type: ignore + + +NOTIFICATION_WIDTH = 360 +NOTIFICATION_IMAGE_SIZE = 48 +NOTIFICATION_TIMEOUT = 10 * 1000 + + +class NotificationWidget(Box): + def __init__(self, notification: Notification, **kwargs): + super().__init__( + size=(NOTIFICATION_WIDTH, -1), + name="notification", + spacing=8, + orientation="v", + **kwargs, + ) + + self._notification = notification + + self.image = notification.image_pixbuf + body_container = Box(spacing=4, orientation="h") + + if self._notification.image_pixbuf: + body_container.add( + CustomImage( + name="noti-image", + pixbuf=self.image.scale_simple( # type: ignore + NOTIFICATION_IMAGE_SIZE, + NOTIFICATION_IMAGE_SIZE, + GdkPixbuf.InterpType.BILINEAR, + ), + ) + ) + + body_container.add( + Box( + spacing=4, + orientation="v", + children=[ + Box( + orientation="h", + children=[ + Label( + markup=self._notification.summary, # type: ignore + ellipsization="middle", + style_classes="summary", + ) + ], + h_expand=True, + v_expand=True, + ), + Label( + label=self._notification.body, # type: ignore + line_wrap="word-char", + v_align="start", + h_align="start", + style_classes="body", + ), + ], + h_expand=True, + v_expand=True, + ) + ) + + self.add(body_container) + + if actions := self._notification.actions: + actions = cast(list[NotificationAction], actions) # type: ignore + self.add( + Box( + spacing=4, + orientation="h", + children=[ + Button( + label=action.label, + h_expand=True, + name="notification-action", + v_expand=True, + on_clicked=lambda *_, action=action: ( + action.invoke(), + action.parent.close("dismissed-by-user"), + ), + ) + for action in actions # type: ignore + ], + ) + ) + + self._notification.connect( + "closed", + self.destroy_noti, + ) + + invoke_repeater( + NOTIFICATION_TIMEOUT, + lambda: self._notification.close("expired"), + initial_call=False, + ) + + def close_noti(self, *_): + modus_service.remove_notification(self._notification["id"]) + self._notification.close() + self.destroy() + + def destroy_noti(self, *_): + parent.remove(self) if (parent := self.get_parent()) else None, # type: ignore + self.destroy() + + +class EnvNoti(Window): + def __init__(self, **kwargs): + super().__init__( + margin="8px 8px 8px 8px", + name="notification-window", + title="Notification Center", + layer="overlay", + anchor="top right", + child=Box( + size=2, + spacing=4, + orientation="v", + ).build( + lambda viewport, _: Notifications( + on_notification_added=self.on_notification_added + ) + ), + **kwargs, + ) + + def on_notification_added(self, notifs_service, nid): + modus_service.cache_notification(notifs_service.get_notification_from_id(nid)) + if modus_service.dont_disturb == "True": + return + self.get_child().add( + NotificationWidget( + cast(Notification, notifs_service.get_notification_from_id(nid)) + ) + ) diff --git a/modules/panel/main.py b/modules/panel/main.py index a6661ed..95cea33 100644 --- a/modules/panel/main.py +++ b/modules/panel/main.py @@ -13,6 +13,8 @@ from modules.panel.components.enhanced_system_tray import apply_enhanced_system_ from modules.panel.components.indicators import Indicators from modules.panel.components.menubar import MenuBar from modules.panel.components.recording_indicator import RecordingIndicator +from utils.roam import modus_service +from modules.notification import EnvNoti # Apply enhanced system tray icon handling apply_enhanced_system_tray() @@ -49,6 +51,8 @@ class Panel(Window): self.tray = SystemTray(name="system-tray", spacing=4, icon_size=20) + # modus_service.connect("show-notificationcenter-changed", envnoticenter.toggle_mousecapture) + self.tray_revealer = Revealer( name="tray-revealer", child=self.tray, diff --git a/services/modus.py b/services/modus.py new file mode 100644 index 0000000..27ced60 --- /dev/null +++ b/services/modus.py @@ -0,0 +1,85 @@ +# type: ignore +from fabric.core.service import Service, Signal, Property +from fabric.notifications import Notification +from typing import List + + +class ModusService(Service): + @Signal + def notification_count_changed(self, value: int) -> None: ... + @Signal + def dont_disturb_changed(self, value: bool) -> None: ... + @Signal + def show_notificationcenter_changed(self, value: bool) -> None: ... + @Property(bool, flags="read-write", default_value=False) + def show_notificationcenter(self) -> bool: + return self._show_notificationcenter + + @Property(str, flags="read-write") + def notification_count(self) -> int: + return self._notification_count + + @Property(str, flags="read-write") + def dont_disturb(self) -> bool: + return self._dont_disturb + + @dont_disturb.setter + def dont_disturb(self, value: bool): + if value != self._dont_disturb: + self._dont_disturb = value + self.dont_disturb_changed(value) + + @show_notificationcenter.setter + def show_notificationcenter(self, value: bool): + if value != self._show_notificationcenter: + self._show_notificationcenter = value + self.show_notificationcenter_changed(value) + + def sc(self, signal_name: str, callback: callable, def_value="..."): + self.connect(signal_name, callback) + return def_value + + def __init__(self): + super().__init__() + self._notifications = [] + self._notification_count = len(self._notifications) + self._dont_disturb = False + self._show_notificationcenter = False + + self.notifications = [] + + def remove_notification(self, id: int): + item = next((p for p in self._notifications if p["id"] == id), None) + if item is None: + return + index = self._notifications.index(item) + + self._notifications.pop(index) + self._notification_count -= 1 + self.notification_count_changed(self._notification_count) + if self._notification_count == 0: + self.clear_all_changed(True) + + def cache_notification(self, data: Notification): + existing_data = self._notifications + serialized_data = data.serialize() + serialized_data.update({"id": self._notification_count + 1}) + existing_data.append(serialized_data) + + self._notification_count += 1 + self._notifications = existing_data + self.notification_count_changed(self._notification_count) + + def clear_all_notifications(self): + self._notifications = [] + self._notification_count = 0 + + self.clear_all_changed(True) + self.notification_count_changed(self._notification_count) + + def get_deserialized(self) -> List[Notification]: + if len(self.notifications) <= 0: + self.notifications = [ + Notification.deserialize(data) for data in self._notifications + ] + return self.notifications diff --git a/styles/notification.css b/styles/notification.css new file mode 100644 index 0000000..52a5465 --- /dev/null +++ b/styles/notification.css @@ -0,0 +1,57 @@ + +#notification { + padding: 0.8rem; + background-color: alpha(#000, 0.3); + border: .5px solid alpha(#111, 0.4); + box-shadow: inset 0 0 200px 0 alpha(#111, 0.5), inset 0 0 0 .5px alpha(#aaa, 0.4); + border-radius: 1.2rem; +} + +#notification .summary { + font-size: 20px; + font-weight: bold; +} + +#notification .body { + color: #ddd; + margin-top: 0.5rem; + font-weight: normal; +} + +#noti-image { + border-radius: 7px; +} + +#env-noti-center { + border-radius: 1.25rem; +} + +#notification-action{ + background-color: #e0e0e0; + border: none; + border-radius: 6px; + padding: 6px 12px; + font-weight: 500; + font-size: 0.9rem; + transition: background-color 0.2s ease, color 0.2s ease; +} +#notification-action label { + color: #000000; +} +#notification-action:hover label { + color: #ffffff; +} + +#notification-action:hover { + background-color: #007aff; /* macOS-style blue */ + color: #ffffff; +} + +#notification-center { + background-color: alpha(#000, 0.3); + box-shadow: inset 0 0 0 1px alpha(#aaa, 0.4); + border: 1px solid alpha(#111, 0.4); + border-radius: 1.25rem; + padding: 1rem; +} + diff --git a/utils/roam.py b/utils/roam.py new file mode 100644 index 0000000..0b05951 --- /dev/null +++ b/utils/roam.py @@ -0,0 +1,30 @@ +import sys +import os + +# from hyprpy import Hyprland +from fabric.audio import Audio +from fabric.notifications import Notifications +from services.modus import ModusService +from loguru import logger + + +global modus_service +try: + modus_service = ModusService() +except Exception as e: + logger.error("[Main] Failed to create ModusService:", e) + sys.exit(1) + +# global notification_service +# try: +# notification_service = Notifications() +# except Exception as e: +# logger.error("[Main] Failed to create NotificationService:", e) +# sys.exit(1) + +global audio_service +try: + audio_service = Audio() +except Exception as e: + logger.error("[Main] Failed to create AudioService:", e) + sys.exit(1)