update notification

This commit is contained in:
sankalp committed 2025-03-01 21:09:53 +05:45
1 parent 93160926af
commit c0fd204b6a
1 file changed
+198 -209
+198 -209
View File
@@ -1,70 +1,49 @@
import time
import gi
gi.require_version("GdkPixbuf", "2.0")
import os
from gi.repository import GdkPixbuf, GLib, Gdk
from fabric.notifications import (
Notification,
NotificationAction,
NotificationCloseReason,
Notifications,
)
from fabric.utils import invoke_repeater
from loguru import logger
from fabric.widgets.box import Box
from fabric.widgets.button import Button
from fabric.widgets.circularprogressbar import CircularProgressBar
from fabric.widgets.image import Image
from fabric.widgets.centerbox import CenterBox
from fabric.widgets.label import Label
from fabric.widgets.overlay import Overlay
from fabric.widgets.revealer import Revealer
from fabric.widgets.wayland import WaylandWindow as Window
from gi.repository import GdkPixbuf, GLib, GObject
from snippets import Animator, CustomImage
class AnimatedCircularProgressBar(CircularProgressBar):
def __init__(self, duration=0.6, bezier_curve=(0.34, 1.56, 0.64, 1.0), **kwargs):
super().__init__(**kwargs)
self.animator = (
Animator(
bezier_curve=bezier_curve,
duration=duration,
min_value=self.min_value,
max_value=self.value,
tick_widget=self,
notify_value=lambda p, *_: self.set_value(p.value),
)
.build()
.play()
.unwrap()
)
def animate_value(self, value: float):
self.animator.pause()
self.animator.min_value = self.value
self.animator.max_value = value
self.animator.play()
return
from utils import CustomImage
import utils.icons as icons
class ActionButton(Button):
def __init__(
self, action: NotificationAction, action_number: int, total_actions: int
self, action: NotificationAction, index: int, total: int, notification_box
):
self.action = action
super().__init__(
label=action.label,
name="action-button",
h_expand=True,
on_clicked=self.on_clicked,
child=Label(name="button-label", label=action.label),
)
self.action = action
self.notification_box = notification_box
style_class = (
"start-action"
if index == 0
else "end-action"
if index == total - 1
else "middle-action"
)
self.add_style_class(style_class)
self.connect(
"enter-notify-event", lambda *_: notification_box.hover_button(self)
)
self.connect(
"leave-notify-event", lambda *_: notification_box.unhover_button(self)
)
if action_number == 0:
self.add_style_class("start-action")
elif action_number == total_actions - 1:
self.add_style_class("end-action")
else:
self.add_style_class("middle-action")
def on_clicked(self, *_):
self.action.invoke()
@@ -72,181 +51,192 @@ class ActionButton(Button):
class NotificationWidget(Box):
def __init__(self, notification: Notification, **kwargs):
def __init__(self, notification: Notification, timeout_ms=5000, **kwargs):
super().__init__(
name="notification",
spacing=8,
name="notification-box",
orientation="v",
**kwargs,
)
self._notification = notification
self._timer = None
self.anim_parts = 20
self.anim_interval = self.get_timeout() / self.anim_parts
self.timeout_in_sec = self.get_timeout() / 1000
self.time = GLib.DateTime.new_from_unix_local(time.time()).format("%m/%d")
self.connect("button-press-event", self.on_button_press)
header_container = Box(spacing=8, orientation="h")
self.progress_timeout = AnimatedCircularProgressBar(
name="notification-circular-progress-bar",
size=30,
min_value=0,
max_value=1,
radius_color=True,
value=0,
)
header_container.children = (
self.get_icon(notification.app_icon, notification.app_name, 25),
Label(
markup=str(
self._notification.summary
if self._notification.summary
else notification.app_name
),
h_align="start",
style="font-weight: 900;",
ellipsization="end",
),
)
header_container.pack_end(
Box(
v_align="start",
children=(
Overlay(
child=self.progress_timeout,
overlays=Button(
image=Image(
icon_name="window-close-symbolic",
icon_size=16,
),
style_classes="close-button",
on_clicked=lambda *_: self._notification.close(
"dismissed-by-user"
),
),
),
),
),
False,
False,
0,
)
body_container = Box(spacing=4, orientation="h")
if image_pixbuf := self._notification.image_pixbuf:
body_container.add(
Box(
v_expand=True,
v_align="center",
children=CustomImage(
pixbuf=image_pixbuf.scale_simple(
64, 64, GdkPixbuf.InterpType.BILINEAR
),
style_classes="image",
),
)
)
body_container.add(
Label(
markup=self._notification.body,
line_wrap="word-char",
v_align="start",
ellipsization="end",
max_chars_width=40,
h_align="start",
)
)
actions_container = Box(
spacing=4,
orientation="h",
name="notification-action-buttons",
children=[
ActionButton(action, i, len(self._notification.actions))
for i, action in enumerate(self._notification.actions)
],
h_align="fill",
h_expand=True,
children=[
self.create_content(notification),
self.create_action_buttons(notification),
],
)
self.notification = notification
self.timeout_ms = timeout_ms
self._timeout_id = None
self.start_timeout()
def create_header(self, notification):
app_icon = (
Image(
name="notification-icon",
image_file=notification.app_icon[7:],
size=24,
)
if "file://" in notification.app_icon
else Image(
name="notification-icon",
icon_name="dialog-information-symbolic" or notification.app_icon,
icon_size=24,
)
)
self.add(header_container)
self.add(body_container)
self.add(actions_container)
# Destroy this widget once the notification is closed
self._notification.connect(
"closed",
lambda *_: (
parent.remove(self) if (parent := self.get_parent()) else None, # type: ignore
self.destroy(),
return CenterBox(
name="notification-title",
start_children=[
Box(
spacing=4,
children=[
app_icon,
Label(
notification.app_name,
name="notification-app-name",
h_align="start",
),
invoke_repeater(
5 * 1000,
lambda: self._notification.close("expired"),
initial_call=False,
],
)
],
end_children=[self.create_close_button()],
)
def create_content(self, notification):
return Box(
name="notification-content",
spacing=8,
children=[
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)
),
),
Box(
name="notification-text",
orientation="v",
v_align="center",
children=[
Box(
name="notification-summary-box",
orientation="h",
children=[
Label(
name="notification-summary",
markup=notification.summary.replace("\n", " "),
h_align="start",
ellipsization="end",
),
Label(
name="notification-app-name",
markup=" | " + notification.app_name,
h_align="start",
ellipsization="end",
),
],
),
Label(
markup=notification.body.replace("\n", " "),
h_align="start",
ellipsization="end",
)
if notification.body
else Box(),
],
),
Box(h_expand=True),
Box(
orientation="v",
children=[
self.create_close_button(),
Box(v_expand=True),
],
),
],
)
self.start_timer()
def get_pixbuf(self, icon_path, width, height):
if icon_path.startswith("file://"):
icon_path = icon_path[7:]
def start_timer(self):
self._timer = GObject.timeout_add(200, self.update_progress)
if not os.path.exists(icon_path):
logger.warning(f"Icon path does not exist: {icon_path}")
return None
def update_progress(self):
self.progress_timeout.animate_value(
self.progress_timeout.value + 1 / self.anim_parts
)
if self.progress_timeout.value >= self.timeout_in_sec:
self.progress_timeout.value = self.timeout_in_sec
self.stop_timer()
return True # Continue the timer
def stop_timer(self):
if self._timer:
GObject.source_remove(self._timer)
self._timer = None
def get_icon(self, app_icon, app_name, size) -> Image:
app_name = (
app_name.lower().replace(" ", "-")
if isinstance(app_name, str)
else app_name
)
if isinstance(app_icon, str) and app_icon.strip():
if app_icon.startswith("file://") or app_icon.startswith("/"):
file_path = app_icon[7:] if app_icon.startswith("file://") else app_icon
try:
pixbuf = GdkPixbuf.Pixbuf.new_from_file(file_path)
scaled_pixbuf = pixbuf.scale_simple(
size, size, GdkPixbuf.InterpType.BILINEAR
)
return Image(name="notification-icon", pixbuf=scaled_pixbuf)
pixbuf = GdkPixbuf.Pixbuf.new_from_file(icon_path)
return pixbuf.scale_simple(width, height, GdkPixbuf.InterpType.BILINEAR)
except Exception as e:
print(f"DEBUG: Failed to load image from {file_path}: {e}")
logger.error(f"Failed to load or scale icon: {e}")
return None
if app_icon and not app_icon.startswith(("file://", "/")):
icon_name = app_icon
elif app_name and app_name.strip() not in ["notify-send", ""]:
icon_name = app_name.strip()
else:
icon_name = "dialog-information-symbolic"
return Image(name="notification-icon", icon_name=icon_name, icon_size=size)
def on_button_press(self, _, event):
if event.button != 1:
(self._notification.close("dismissed-by-user"),)
def get_timeout(self):
return (
self._notification.timeout if self._notification.timeout != -1 else 5 * 1000
def create_action_buttons(self, notification):
return Box(
name="notification-action-buttons",
spacing=4,
h_expand=True,
children=[
ActionButton(action, i, len(notification.actions), self)
for i, action in enumerate(notification.actions)
],
)
def create_close_button(self):
close_button = Button(
name="notif-close-button",
child=Label(name="notif-close-label", markup=icons.cancel),
on_clicked=lambda *_: self.notification.close("dismissed-by-user"),
)
close_button.connect(
"enter-notify-event", lambda *_: self.hover_button(close_button)
)
close_button.connect(
"leave-notify-event", lambda *_: self.unhover_button(close_button)
)
return close_button
def start_timeout(self):
self.stop_timeout()
self._timeout_id = GLib.timeout_add(self.timeout_ms, self.close_notification)
def stop_timeout(self):
if self._timeout_id is not None:
GLib.source_remove(self._timeout_id)
self._timeout_id = None
def close_notification(self):
self.notification.close("expired")
self.stop_timeout()
return False
def pause_timeout(self):
self.stop_timeout()
def resume_timeout(self):
self.start_timeout()
def destroy(self):
self.stop_timeout()
super().destroy()
# @staticmethod
def set_pointer_cursor(self, widget, cursor_name):
window = widget.get_window()
if window:
cursor = Gdk.Cursor.new_from_name(widget.get_display(), cursor_name)
window.set_cursor(cursor)
def hover_button(self, button):
self.pause_timeout()
self.set_pointer_cursor(button, "hand2")
def unhover_button(self, button):
self.resume_timeout()
self.set_pointer_cursor(button, "arrow")
class NotificationRevealer(Revealer):
def __init__(self, notification: Notification, **kwargs):
@@ -254,11 +244,10 @@ class NotificationRevealer(Revealer):
self._notification = notification
super().__init__(
child=Box(
style="margin: 12px;",
children=[self.notif_box],
),
transition_duration=500,
transition_type="crossfade",
transition_duration=250,
transition_type="slide-down",
)
self.connect(
@@ -289,7 +278,7 @@ class NotificationPopup(Window):
self._server.connect("notification-added", self.on_new_notification)
super().__init__(
anchor="bottom",
anchor="top right",
child=self.notifications,
layer="overlay",
all_visible=True,