410 lines
14 KiB
Python
410 lines
14 KiB
Python
from fabric.notifications import Notification
|
|
from services import notification_service
|
|
from fabric.widgets.box import Box
|
|
from fabric.widgets.button import Button
|
|
from fabric.widgets.label import Label
|
|
from fabric.widgets.image import Image
|
|
from fabric.widgets.scrolledwindow import ScrolledWindow
|
|
from fabric.widgets.revealer import Revealer
|
|
from loguru import logger
|
|
import utils.icons as icons
|
|
from gi.repository import GdkPixbuf, GLib
|
|
from utils import CustomImage
|
|
import os
|
|
from gi.repository import Gtk
|
|
from fabric.widgets.centerbox import CenterBox
|
|
|
|
|
|
class NotificationCenter(Box):
|
|
def __init__(self, **kwargs):
|
|
super().__init__(
|
|
name="notification-center",
|
|
visible=False,
|
|
all_visible=False,
|
|
**kwargs,
|
|
)
|
|
|
|
self.launcher = kwargs["launcher"]
|
|
self.notification_service = notification_service
|
|
|
|
# Main container for notifications
|
|
self.notifications = Box(
|
|
v_expand=True,
|
|
h_expand=True,
|
|
orientation="v",
|
|
spacing=8,
|
|
)
|
|
self.header_switch = Gtk.Switch(name="matugen-switcher")
|
|
self.header_switch.set_vexpand(False)
|
|
self.header_switch.set_valign(Gtk.Align.CENTER)
|
|
self.header_switch.set_active(self.notification_service.dont_disturb)
|
|
self.do_not_disturb_enabled = self.notification_service.dont_disturb
|
|
self.header_switch.connect("notify::active", self.on_do_not_disturb_changed)
|
|
self.dnd_label = Label(name="dnd-label", markup=icons.notifications_off)
|
|
# Header with clear all button
|
|
header = CenterBox(
|
|
h_expand=True,
|
|
start_children=[self.header_switch, self.dnd_label],
|
|
center_children=[
|
|
Label(
|
|
label="Notifications",
|
|
name="nhh",
|
|
)
|
|
],
|
|
end_children=[
|
|
Button(
|
|
name="nhh-button",
|
|
child=Label(name="nhh-button-label", markup=icons.trash),
|
|
on_clicked=lambda *args: self.clear_all_notifications(),
|
|
)
|
|
],
|
|
)
|
|
|
|
self.scrolledwindow = ScrolledWindow(
|
|
name="scrolled-window",
|
|
child=self.notifications,
|
|
min_content_size=(400, 300),
|
|
max_content_size=(400, 300),
|
|
v_expand=True,
|
|
h_expand=True,
|
|
)
|
|
|
|
# Main container
|
|
main_container = Box(
|
|
v_expand=True,
|
|
h_expand=True,
|
|
orientation="v",
|
|
children=[header, self.scrolledwindow],
|
|
)
|
|
|
|
self.add(main_container)
|
|
|
|
# Connect to notification service signals
|
|
self.notification_service.connect(
|
|
"notification-added", self.on_new_notification
|
|
)
|
|
self.notification_service.connect("clear_all", self.on_clear_all)
|
|
|
|
# Load existing notifications
|
|
self.load_notifications()
|
|
|
|
def load_notifications(self):
|
|
"""Load existing notifications from the service."""
|
|
notifications = self.notification_service.get_deserialized()
|
|
if not notifications:
|
|
# Create a container box to better center the message
|
|
container = Box(
|
|
name="no-notifications-container",
|
|
orientation="v",
|
|
h_align="center",
|
|
v_align="center",
|
|
h_expand=True,
|
|
v_expand=True,
|
|
)
|
|
|
|
# Show a message if no notifications
|
|
label = Label(
|
|
name="no-notifications-label",
|
|
markup=icons.notifications_clear,
|
|
v_align="fill",
|
|
h_align="fill",
|
|
v_expand=True,
|
|
h_expand=True,
|
|
justification="center",
|
|
)
|
|
|
|
container.add(label)
|
|
self.notifications.add(container)
|
|
return
|
|
|
|
for notification in notifications:
|
|
self.add_notification(notification)
|
|
|
|
def toggle_notification_body(self, revealer, chevron_label):
|
|
"""Toggle the visibility of notification body."""
|
|
is_revealed = revealer.get_reveal_child()
|
|
revealer.set_reveal_child(not is_revealed)
|
|
# Update chevron icon
|
|
chevron_label.set_markup(
|
|
icons.chevron_up if not is_revealed else icons.chevron_down
|
|
)
|
|
|
|
def add_notification(self, notification: Notification):
|
|
"""Add a notification to the center."""
|
|
# Remove empty state container if it exists
|
|
for child in self.notifications.get_children():
|
|
if child.get_name() == "no-notifications-container":
|
|
self.notifications.remove(child)
|
|
break
|
|
|
|
# Get timestamp from notification object
|
|
timestamp = getattr(notification, 'timestamp', GLib.get_real_time() / 1000000)
|
|
time_str = GLib.DateTime.new_from_unix_local(timestamp).format("%I:%M %p")
|
|
|
|
# Create the notification header (summary) box
|
|
header_box = Box(
|
|
name="notification-header",
|
|
# h_expand=True,
|
|
spacing=8,
|
|
children=[
|
|
Box(
|
|
orientation="h",
|
|
h_expand=True,
|
|
spacing=8,
|
|
style="min-width: 0;",
|
|
children=[
|
|
# Summary
|
|
Label(
|
|
name="notification-app-name",
|
|
markup=notification.app_name + " | ",
|
|
h_align="start",
|
|
ellipsization="end",
|
|
),
|
|
Label(
|
|
markup=notification.summary.replace("\n", " "),
|
|
h_align="start",
|
|
ellipsization="end",
|
|
),
|
|
Label(
|
|
name="notification-time",
|
|
markup=f" | {time_str}",
|
|
h_align="end",
|
|
style="color: var(--outline);",
|
|
),
|
|
],
|
|
),
|
|
# Add chevron button if there's content to show
|
|
*(
|
|
[
|
|
Button(
|
|
name="chevron-button",
|
|
child=Label(
|
|
name="chevron-label", markup=icons.chevron_down
|
|
),
|
|
)
|
|
]
|
|
if notification.body or notification.image_pixbuf
|
|
else []
|
|
),
|
|
Button(
|
|
name="notif-close-button",
|
|
child=Label(name="notif-close-label", markup=icons.cancel),
|
|
on_clicked=lambda *args: self.remove_notification(notification),
|
|
),
|
|
],
|
|
)
|
|
|
|
# Create content box that will be revealed
|
|
# Create content box in vertical layout for body + actions
|
|
content_box = Box(
|
|
name="notification-content",
|
|
spacing=8,
|
|
orientation="v",
|
|
)
|
|
|
|
# Create horizontal box for image and body
|
|
body_box = Box(
|
|
name="notification-body-box",
|
|
spacing=8,
|
|
orientation="h",
|
|
)
|
|
|
|
# Add image if available
|
|
if notification.image_pixbuf:
|
|
image_box = Box(
|
|
name="notification-image",
|
|
children=[
|
|
CustomImage(
|
|
pixbuf=notification.image_pixbuf.scale_simple(
|
|
48, 48, GdkPixbuf.InterpType.BILINEAR
|
|
)
|
|
if notification.image_pixbuf
|
|
else self.get_pixbuf(notification.app_icon, 48, 48)
|
|
),
|
|
],
|
|
)
|
|
body_box.add(image_box)
|
|
|
|
# Add body text if available
|
|
if notification.body:
|
|
body_box.add(
|
|
Label(
|
|
markup=notification.body.replace("\n", " "),
|
|
name="notification-body",
|
|
h_align="start",
|
|
ellipsization="end",
|
|
)
|
|
)
|
|
|
|
# Add body box to content
|
|
content_box.add(body_box)
|
|
|
|
# Add action buttons if available
|
|
if notification.actions:
|
|
action_box = Box(
|
|
name="notification-action-buttons",
|
|
spacing=4,
|
|
h_expand=True,
|
|
)
|
|
for i, action in enumerate(notification.actions):
|
|
style_class = (
|
|
"start-action"
|
|
if i == 0
|
|
else "end-action"
|
|
if i == len(notification.actions) - 1
|
|
else "middle-action"
|
|
)
|
|
button = Button(
|
|
name="action-button",
|
|
h_expand=True,
|
|
child=Label(name="button-label", label=action.label),
|
|
on_clicked=lambda btn, act=action, notif=notification: [
|
|
act.invoke(),
|
|
self.notification_service.remove_notification(notif.id),
|
|
self.remove_notification(notif)
|
|
],
|
|
)
|
|
button.add_style_class(style_class)
|
|
action_box.add(button)
|
|
content_box.add(action_box)
|
|
|
|
# Create revealer for content
|
|
body_revealer = None
|
|
if notification.body or notification.image_pixbuf:
|
|
body_revealer = Revealer(
|
|
name="body-revealer",
|
|
transition_type="slide-down",
|
|
reveal_child=False,
|
|
child=content_box,
|
|
)
|
|
|
|
# Create main notification box
|
|
notification_box = Box(
|
|
name="notification-box",
|
|
orientation="v",
|
|
h_expand=True,
|
|
style="margin: 4px; padding: 16px; border-radius: 20px; background-color: var(--surface-container-low); min-width: 200px;",
|
|
children=[header_box],
|
|
)
|
|
|
|
# Add body revealer if exists
|
|
if body_revealer:
|
|
notification_box.add(body_revealer)
|
|
# Get the chevron button from header_box children
|
|
chevron_button = [
|
|
c for c in header_box.get_children() if c.get_name() == "chevron-button"
|
|
][0]
|
|
chevron_label = chevron_button.get_child()
|
|
chevron_button.connect(
|
|
"clicked",
|
|
lambda *args: self.toggle_notification_body(
|
|
body_revealer, chevron_label
|
|
),
|
|
)
|
|
|
|
self.notifications.add(notification_box)
|
|
|
|
def on_do_not_disturb_changed(self, switch, pspec):
|
|
"""Handle Do Not Disturb toggle."""
|
|
self.do_not_disturb_enabled = switch.get_active()
|
|
self.notification_service.dont_disturb = self.do_not_disturb_enabled
|
|
|
|
def on_new_notification(self, fabric_notif, id):
|
|
"""Handle new notification event."""
|
|
notification = fabric_notif.get_notification_from_id(id)
|
|
self.add_notification(notification)
|
|
|
|
def remove_notification(self, notification: Notification):
|
|
"""Remove a notification from the center."""
|
|
self.notification_service.remove_notification(notification.id)
|
|
# Find and remove the notification box
|
|
for child in self.notifications.get_children():
|
|
if child.get_name() == "notification-box":
|
|
# Get the header box
|
|
header_box = child.get_children()[0]
|
|
# Get the text container box
|
|
text_box = header_box.get_children()[0]
|
|
# Get the summary label (second child of text box)
|
|
summary_label = text_box.get_children()[1]
|
|
if summary_label.get_text() == notification.summary:
|
|
self.notifications.remove(child)
|
|
break
|
|
|
|
def clear_all_notifications(self, *args):
|
|
"""Clear all notifications."""
|
|
self.notification_service.clear_all_notifications()
|
|
# Remove all child widgets
|
|
for child in self.notifications.get_children():
|
|
self.notifications.remove(child)
|
|
|
|
# Add empty state
|
|
container = Box(
|
|
name="no-notifications-container",
|
|
orientation="v",
|
|
h_align="center",
|
|
v_align="center",
|
|
h_expand=True,
|
|
v_expand=True,
|
|
)
|
|
|
|
label = Label(
|
|
name="no-notifications-label",
|
|
markup=icons.notifications_clear,
|
|
v_align="fill",
|
|
h_align="fill",
|
|
v_expand=True,
|
|
h_expand=True,
|
|
justification="center",
|
|
)
|
|
|
|
container.add(label)
|
|
self.notifications.add(container)
|
|
|
|
def on_clear_all(self, *args):
|
|
"""Handle clear all event from service."""
|
|
# Remove all child widgets
|
|
for child in self.notifications.get_children():
|
|
self.notifications.remove(child)
|
|
|
|
# Add empty state
|
|
container = Box(
|
|
name="no-notifications-container",
|
|
orientation="v",
|
|
h_align="center",
|
|
v_align="center",
|
|
h_expand=True,
|
|
v_expand=True,
|
|
)
|
|
|
|
label = Label(
|
|
name="no-notifications-label",
|
|
markup=icons.notifications_clear,
|
|
v_align="fill",
|
|
h_align="fill",
|
|
v_expand=True,
|
|
h_expand=True,
|
|
justification="center",
|
|
)
|
|
|
|
container.add(label)
|
|
self.notifications.add(container)
|
|
|
|
def get_pixbuf(self, icon_path, width, height):
|
|
if icon_path.startswith("file://"):
|
|
icon_path = icon_path[7:]
|
|
|
|
if not os.path.exists(icon_path):
|
|
logger.warning(f"Icon path does not exist: {icon_path}")
|
|
return None
|
|
|
|
try:
|
|
pixbuf = GdkPixbuf.Pixbuf.new_from_file(icon_path)
|
|
return pixbuf.scale_simple(width, height, GdkPixbuf.InterpType.BILINEAR)
|
|
except Exception as e:
|
|
logger.error(f"Failed to load or scale icon: {e}")
|
|
return None
|
|
|
|
def open_center(self):
|
|
"""Open the notification center."""
|
|
self.show_all()
|