Notificaitons #9

Merged
tr1xem merged 3 commits from macos into macos 2025-07-30 17:27:08 +00:00
7 changed files with 387 additions and 2 deletions

No files matched your search

+1
View File
@@ -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;
+4 -2
View File
@@ -8,6 +8,7 @@ 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 ModusNoti
# from modules.corners import Corners
from modules.panel.main import Panel
@@ -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 = ModusNoti()
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(
+203
View File
@@ -0,0 +1,203 @@
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,
)
import os
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
def get_notification_image_pixbuf(
notification: Notification,
) -> GdkPixbuf.Pixbuf | None:
width = NOTIFICATION_IMAGE_SIZE
height = NOTIFICATION_IMAGE_SIZE
try:
if notification.image_file and os.path.exists(notification.image_file):
pixbuf = GdkPixbuf.Pixbuf.new_from_file(notification.image_file)
return pixbuf.scale_simple(width, height, GdkPixbuf.InterpType.BILINEAR)
elif notification.image_pixbuf:
return notification.image_pixbuf.scale_simple(
width, height, GdkPixbuf.InterpType.BILINEAR
)
except Exception as e:
print("Failed to load image:", e)
return None
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.app_icon
body_container = Box(name="noti-image", spacing=4, orientation="h")
# Load and show image (from app_icon, image_file, or pixbuf)
pixbuf = get_notification_image_pixbuf(notification)
image_file = None
if notification.app_icon:
if notification.app_icon.startswith("file://"):
image_file = notification.app_icon[7:]
elif os.path.exists(notification.app_icon):
image_file = notification.app_icon
if image_file:
body_container.add(
CustomImage(
name="noti-image",
image_file=image_file,
size=NOTIFICATION_IMAGE_SIZE,
)
)
elif pixbuf:
body_container.add(
CustomImage(
name="noti-image",
image_pixbuf=pixbuf,
size=NOTIFICATION_IMAGE_SIZE,
)
)
else:
body_container.add(
CustomImage(
name="noti-image",
icon_name="dialog-information-symbolic",
icon_size=NOTIFICATION_IMAGE_SIZE,
)
)
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 ModusNoti(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))
)
)
+4
View File
@@ -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,
+85
View File
@@ -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
+60
View File
@@ -0,0 +1,60 @@
#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;
}
#noti-image image{
border-radius: 25px;
}
#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;
}
+30
View File
@@ -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)