Files
Modus/services/custom_notification.py
T

258 lines
8.6 KiB
Python

import json
import os
from typing import List
import gi
from gi.repository import GdkPixbuf
import config.data as data
from fabric.core.service import Property, Service, Signal
from fabric.notifications import (
Notification,
NotificationAction,
NotificationImagePixmap,
Notifications,
)
gi.require_version("Gtk", "3.0")
NOTIFICATION_CACHE_FILE = f"{data.CACHE_DIR}/notification"
class CachedNotification(Service):
@classmethod
def create_from_dict(cls, data, **kwargs):
data["timeout"] = 0
self = cls.__new__(cls)
Service.__init__(self, **kwargs)
self._notification = Notification.deserialize(data)
self.cache_id = data["cached-id"]
return self
@Signal
def removed_from_cache(self) -> None: ...
@Property(int, "read-write")
def cache_id(self) -> int:
return self._cache_id
@cache_id.setter
def cache_id(self, cache_id: int):
self._cache_id = cache_id
@Property(str, "readable")
def app_name(self) -> str:
return self._notification.app_name
@Property(str, "readable")
def app_icon(self) -> str:
return self._notification.app_icon
@Property(str, "readable")
def summary(self) -> str:
return self._notification.summary
@Property(str, "readable")
def body(self) -> str:
return self._notification.body
@Property(int, "readable")
def id(self) -> int:
return self._notification.id
@Property(int, "readable")
def replaces_id(self) -> int:
return self._notification.replaces_id
@Property(int, "readable")
def urgency(self) -> int:
return self._notification.urgency
@Property(list[NotificationAction], "readable")
def actions(self) -> list[NotificationAction]:
return self._notification.actions
@Property(NotificationImagePixmap, "readable")
def image_pixmap(self) -> NotificationImagePixmap:
return self._notification.image_pixmap # type: ignore
@Property(str, "readable")
def image_file(self) -> str:
return self._notification.image_file # type: ignore
@Property(GdkPixbuf.Pixbuf, "readable")
def image_pixbuf(self) -> GdkPixbuf.Pixbuf:
if self.image_pixmap:
return self.image_pixmap.as_pixbuf()
if self.image_file:
return GdkPixbuf.Pixbuf.new_from_file(self.image_file)
return None # type: ignore
@Property(dict, "readable")
def serialized(self) -> dict:
return {
"cached-id": self.cache_id,
"id": self.id,
"replaces-id": self.replaces_id,
"app-name": self.app_name,
"app-icon": self.app_icon,
"summary": self.summary,
"body": self.body,
"urgency": self.urgency,
"actions": [(action.identifier, action.label) for action in self.actions],
"image-file": self.image_file,
"image-pixmap": (
self.image_pixmap.serialize() if self.image_pixmap else None
),
}
def __init__(self, notification: Notification, cache_id: int, **kwargs):
super().__init__()
self._notification: Notification = notification
self._cache_id = cache_id
def remove_from_cache(self):
self.removed_from_cache.emit()
class CachedNotifications(Notifications):
"""A service to manage the cached notifications."""
@Signal
def clear_all(self) -> None:
"""Signal emitted when notifications are emptied."""
pass
@Signal
def cached_notification_added(self, notification: CachedNotification) -> None:
"""Signal emitted when a notification is cached."""
pass
@Signal
def cached_notification_removed(self, notification: CachedNotification) -> None:
"""Signal emitted when a notification is removed from cache."""
pass
@Property(List[CachedNotification], "readable")
def cached_notifications(self) -> List[CachedNotification]:
"""Return the cached notifications."""
return list(self._cached_notifications.values())
@Property(int, "readable")
def count(self) -> int:
"""Return the count of notifications."""
return self._count
@Property(bool, "read-write", default_value=False)
def dont_disturb(self) -> bool:
"""Return the pause status."""
return self._dont_disturb
@dont_disturb.setter
def dont_disturb(self, value: bool):
"""Set the pause status."""
self._dont_disturb = value
self.notify("dont-disturb")
def __init__(self, **kwargs):
super().__init__()
self._cached_notifications: dict[int, CachedNotification] = {}
self._signal_handlers = {} # Store signal handlers by notification_id
self._dont_disturb = False
self._count = 0
self.load_cached_notifications()
def load_cached_notifications(self) -> dict[int, CachedNotification]:
"""Load cached notifications from a JSON file (deserialization)."""
try:
with open(NOTIFICATION_CACHE_FILE, "r") as file:
data = json.load(file) # Load list of serialized notifications
except (FileNotFoundError, json.JSONDecodeError):
# If file doesn't exist or is corrupted, start with empty list
data = []
for notification in data:
cached_notification = CachedNotification.create_from_dict(notification)
handler_id = cached_notification.connect(
"removed-from-cache",
lambda *args: self.remove_cached_notification(
notification_id=cached_notification.cache_id
),
)
self._signal_handlers[cached_notification.cache_id] = handler_id
self._cached_notifications[cached_notification.cache_id] = (
cached_notification
)
self._count += 1
self.notify("count")
def cache_notifications(self) -> None:
"""Save cached notifications to a JSON file."""
# Ensure cache directory exists
os.makedirs(os.path.dirname(NOTIFICATION_CACHE_FILE), exist_ok=True)
serialized_data = [
notif.serialized for notif in self._cached_notifications.values()
] # Convert to serializable format
with open(NOTIFICATION_CACHE_FILE, "w") as file:
json.dump(serialized_data, file, indent=4)
def clear_all_cached_notifications(self):
"""Empty the notifications."""
for cached_notification in self._cached_notifications.values():
handler_id = self._signal_handlers.pop(cached_notification.cache_id, None)
if handler_id:
cached_notification.disconnect(handler_id)
self._cached_notifications = {}
self.cache_notifications()
self._count = 0
self.notify("count")
self.clear_all.emit()
def notification_added(self, notification_id: int) -> None:
"""Handle notification added and cache it."""
super().notification_added(notification_id)
notification = self.get_notification_from_id(notification_id)
if notification:
# Always cache notifications, regardless of DND status
# DND only affects popup display, not caching
self._count += 1
cache_id = self._count
cached_notification = CachedNotification(
notification=notification, cache_id=cache_id
)
handler_id = cached_notification.connect(
"removed-from-cache",
lambda *args: self.remove_cached_notification(notification_id=cache_id),
)
self._signal_handlers[cache_id] = handler_id
self._cached_notifications[cache_id] = cached_notification
self.cache_notifications()
self.notify("count")
self.emit("cached-notification-added", cached_notification)
def remove_cached_notification(self, notification_id: int):
"""Remove the notification of given id."""
if notification_id in self._cached_notifications:
cached_notification = self._cached_notifications.pop(
notification_id
) # Remove from cache
self.cache_notifications() # Update JSON
self._count -= 1
self.notify("count")
# Get the stored signal handler ID and disconnect it
handler_id = self._signal_handlers.pop(notification_id, None)
if handler_id:
# Disconnect the signal handler
cached_notification.disconnect(handler_id)
def toggle_dnd(self):
self.dont_disturb = not self.dont_disturb