update notification
This commit is contained in:
1 file changed
+198
-209
+198
-209
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user