updates
This commit is contained in:
11 files changed
+1570
-650
No files matched your search
@@ -1,70 +1,89 @@
|
||||
import json
|
||||
import locale
|
||||
import os
|
||||
import uuid
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from fabric.utils.helpers import get_relative_path
|
||||
from fabric.widgets.box import Box
|
||||
from fabric.widgets.button import Button
|
||||
from fabric.widgets.centerbox import CenterBox
|
||||
from fabric.widgets.label import Label
|
||||
from fabric.widgets.scrolledwindow import ScrolledWindow
|
||||
from gi.repository import GdkPixbuf, GLib, Gtk
|
||||
from loguru import logger
|
||||
from gi.repository import GLib, Gtk, Gdk
|
||||
|
||||
import config.data as data
|
||||
import utils.icons as icons
|
||||
from modules.notification_popup import NotificationBox, cache_notification_pixbuf, load_scaled_pixbuf
|
||||
from modules.notification_popup import NotificationBox, load_scaled_pixbuf, get_shared_notification_history
|
||||
from utils.custom_image import CustomImage
|
||||
from utils.wayland import WaylandWindow as Window
|
||||
|
||||
PERSISTENT_DIR = f"/tmp/{data.APP_NAME}/notifications"
|
||||
PERSISTENT_HISTORY_FILE = os.path.join(PERSISTENT_DIR, "notification_history.json")
|
||||
CONFIG_FILE = get_relative_path("../../../config/assets/config.json")
|
||||
|
||||
|
||||
class NotificationIndicator(Button):
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(name="button-bar-notifications", **kwargs)
|
||||
|
||||
# Create the notification icon
|
||||
|
||||
self.notification_icon = Label(
|
||||
name="notification-icon",
|
||||
markup=icons.notifications
|
||||
name="notification-icon", markup=icons.notifications
|
||||
)
|
||||
|
||||
# Add the icon directly
|
||||
self.add(self.notification_icon)
|
||||
|
||||
# Connect click handler
|
||||
self.connect("clicked", self.on_clicked)
|
||||
|
||||
# Set default tooltip
|
||||
self.set_tooltip_text("Notifications")
|
||||
|
||||
# Popup window for showing notifications
|
||||
self.popup_window = None
|
||||
|
||||
|
||||
self._connect_to_dnd_state()
|
||||
|
||||
def on_clicked(self, _button):
|
||||
"""Handle click to show/hide notification popup."""
|
||||
if self.popup_window and self.popup_window.get_visible():
|
||||
self.popup_window.set_visible(False)
|
||||
else:
|
||||
self.show_notifications_popup()
|
||||
|
||||
def show_notifications_popup(self):
|
||||
"""Show popup window with notification history."""
|
||||
if self.popup_window:
|
||||
self.popup_window.destroy()
|
||||
|
||||
print(f"[NotificationIndicator] Opening notification history popup")
|
||||
|
||||
# Create popup window using the notification history directly
|
||||
self.popup_window = NotificationHistoryWindow()
|
||||
self.popup_window.show_all()
|
||||
|
||||
def _connect_to_dnd_state(self):
|
||||
try:
|
||||
|
||||
self.notification_history = get_shared_notification_history()
|
||||
|
||||
|
||||
self.notification_history.connect('dnd-state-changed', self._on_dnd_state_changed)
|
||||
|
||||
|
||||
self._update_icon_for_dnd_state(self.notification_history.do_not_disturb_enabled)
|
||||
except Exception as e:
|
||||
print(f"Could not connect to DND state: {e}")
|
||||
|
||||
def _on_dnd_state_changed(self, notification_history, dnd_enabled):
|
||||
self._update_icon_for_dnd_state(dnd_enabled)
|
||||
|
||||
def _update_icon_for_dnd_state(self, dnd_enabled):
|
||||
if dnd_enabled:
|
||||
self.notification_icon.set_markup(icons.notifications_off)
|
||||
self.set_tooltip_text("Do Not Disturb enabled")
|
||||
else:
|
||||
self.notification_icon.set_markup(icons.notifications)
|
||||
self.set_tooltip_text("Notifications")
|
||||
|
||||
|
||||
class HistoricalNotification(object):
|
||||
def __init__(self, id, app_icon, summary, body, app_name, timestamp, cached_image_path=None):
|
||||
def __init__(
|
||||
self, id, app_icon, summary, body, app_name, timestamp, cached_image_path=None
|
||||
):
|
||||
self.id = id
|
||||
self.app_icon = app_icon
|
||||
self.summary = summary
|
||||
@@ -79,11 +98,7 @@ class HistoricalNotification(object):
|
||||
|
||||
class NotificationHistory(Box):
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(
|
||||
name="notification-history",
|
||||
orientation="v",
|
||||
**kwargs
|
||||
)
|
||||
super().__init__(name="notification-history", orientation="v", **kwargs)
|
||||
|
||||
self.containers = []
|
||||
self.header_label = Label(
|
||||
@@ -148,7 +163,10 @@ class NotificationHistory(Box):
|
||||
propagate_width=False,
|
||||
propagate_height=False,
|
||||
)
|
||||
self.scrolled_window_viewport_box = Box(orientation="v", children=[self.notifications_list, self.no_notifications_box])
|
||||
self.scrolled_window_viewport_box = Box(
|
||||
orientation="v",
|
||||
children=[self.notifications_list, self.no_notifications_box],
|
||||
)
|
||||
self.scrolled_window.add_with_viewport(self.scrolled_window_viewport_box)
|
||||
self.persistent_notifications = []
|
||||
self.add(self.history_header)
|
||||
@@ -157,13 +175,72 @@ class NotificationHistory(Box):
|
||||
self._cleanup_orphan_cached_images()
|
||||
self.schedule_midnight_update()
|
||||
|
||||
|
||||
self._connect_to_shared_history()
|
||||
|
||||
|
||||
self._load_and_sync_dnd_state()
|
||||
|
||||
self.LIMITED_APPS_HISTORY = ["Spotify"]
|
||||
|
||||
def _connect_to_shared_history(self):
|
||||
try:
|
||||
shared_history = get_shared_notification_history()
|
||||
shared_history.connect('notification-added', self._on_shared_notification_added)
|
||||
except Exception as e:
|
||||
print(f"Could not connect to shared notification history: {e}")
|
||||
|
||||
def _on_shared_notification_added(self, shared_history):
|
||||
GLib.idle_add(self._refresh_history)
|
||||
|
||||
def _refresh_history(self):
|
||||
try:
|
||||
|
||||
for child in self.notifications_list.get_children()[:]:
|
||||
self.notifications_list.remove(child)
|
||||
if hasattr(child, 'destroy'):
|
||||
child.destroy()
|
||||
|
||||
self.containers.clear()
|
||||
|
||||
|
||||
if os.path.exists(PERSISTENT_HISTORY_FILE):
|
||||
with open(PERSISTENT_HISTORY_FILE, "r") as f:
|
||||
self.persistent_notifications = json.load(f)
|
||||
|
||||
|
||||
for note in reversed(self.persistent_notifications[-50:]):
|
||||
self._add_historical_notification(note)
|
||||
|
||||
self.update_no_notifications_label_visibility()
|
||||
except Exception as e:
|
||||
print(f"Error refreshing notification history: {e}")
|
||||
|
||||
def _load_and_sync_dnd_state(self):
|
||||
try:
|
||||
|
||||
dnd_enabled = False
|
||||
if os.path.exists(CONFIG_FILE):
|
||||
with open(CONFIG_FILE, "r") as f:
|
||||
config_data = json.load(f)
|
||||
dnd_enabled = config_data.get("dnd_enabled", False)
|
||||
|
||||
|
||||
shared_history = get_shared_notification_history()
|
||||
shared_history.do_not_disturb_enabled = dnd_enabled
|
||||
|
||||
|
||||
self.header_switch.set_active(dnd_enabled)
|
||||
self.do_not_disturb_enabled = dnd_enabled
|
||||
|
||||
except Exception as e:
|
||||
print(f"Could not sync DND state: {e}")
|
||||
|
||||
def get_ordinal(self, n):
|
||||
if 11 <= (n % 100) <= 13:
|
||||
return 'th'
|
||||
return "th"
|
||||
else:
|
||||
return {1: 'st', 2: 'nd', 3: 'rd'}.get(n % 10, 'th')
|
||||
return {1: "st", 2: "nd", 3: "rd"}.get(n % 10, "th")
|
||||
|
||||
def get_date_header(self, dt):
|
||||
now = datetime.now()
|
||||
@@ -176,9 +253,9 @@ class NotificationHistory(Box):
|
||||
else:
|
||||
original_locale = locale.getlocale(locale.LC_TIME)
|
||||
try:
|
||||
locale.setlocale(locale.LC_TIME, ('en_US', 'UTF-8'))
|
||||
locale.setlocale(locale.LC_TIME, ("en_US", "UTF-8"))
|
||||
except locale.Error:
|
||||
locale.setlocale(locale.LC_TIME, 'C')
|
||||
locale.setlocale(locale.LC_TIME, "C")
|
||||
try:
|
||||
day = dt.day
|
||||
ordinal = self.get_ordinal(day)
|
||||
@@ -193,7 +270,9 @@ class NotificationHistory(Box):
|
||||
|
||||
def schedule_midnight_update(self):
|
||||
now = datetime.now()
|
||||
next_midnight = datetime.combine(now.date() + timedelta(days=1), datetime.min.time())
|
||||
next_midnight = datetime.combine(
|
||||
now.date() + timedelta(days=1), datetime.min.time()
|
||||
)
|
||||
delta_seconds = (next_midnight - now).total_seconds()
|
||||
GLib.timeout_add_seconds(int(delta_seconds), self.on_midnight)
|
||||
|
||||
@@ -212,7 +291,7 @@ class NotificationHistory(Box):
|
||||
h_align="center",
|
||||
h_expand=True,
|
||||
)
|
||||
]
|
||||
],
|
||||
)
|
||||
|
||||
def rebuild_with_separators(self):
|
||||
@@ -225,7 +304,9 @@ class NotificationHistory(Box):
|
||||
|
||||
current_date_header = None
|
||||
last_date_header = None
|
||||
for container in sorted(self.containers, key=lambda x: x.arrival_time, reverse=True):
|
||||
for container in sorted(
|
||||
self.containers, key=lambda x: x.arrival_time, reverse=True
|
||||
):
|
||||
arrival_time = container.arrival_time
|
||||
date_header = self.get_date_header(arrival_time)
|
||||
if date_header != current_date_header:
|
||||
@@ -245,56 +326,58 @@ class NotificationHistory(Box):
|
||||
|
||||
def on_do_not_disturb_changed(self, switch, pspec):
|
||||
self.do_not_disturb_enabled = switch.get_active()
|
||||
logger.info(f"Do Not Disturb mode {'enabled' if self.do_not_disturb_enabled else 'disabled'}")
|
||||
|
||||
|
||||
|
||||
try:
|
||||
shared_history = get_shared_notification_history()
|
||||
shared_history.set_do_not_disturb_enabled(self.do_not_disturb_enabled)
|
||||
except Exception as e:
|
||||
print(f"Could not update shared DND state: {e}")
|
||||
|
||||
def clear_history(self, *args):
|
||||
for child in self.notifications_list.get_children()[:]:
|
||||
container = child
|
||||
notif_box = container.notification_box if hasattr(container, "notification_box") else None
|
||||
notif_box = (
|
||||
container.notification_box
|
||||
if hasattr(container, "notification_box")
|
||||
else None
|
||||
)
|
||||
if notif_box:
|
||||
notif_box.destroy(from_history_delete=True)
|
||||
self.notifications_list.remove(child)
|
||||
child.destroy()
|
||||
|
||||
# Clear notifications from the fabric service
|
||||
try:
|
||||
# Clear all notifications from the fabric service
|
||||
# Note: fabric Notifications doesn't have clear_all_notifications method
|
||||
# so we just clear our local history
|
||||
logger.info("Notification history cleared.")
|
||||
except Exception as e:
|
||||
logger.error(f"Error clearing notification history: {e}")
|
||||
|
||||
# Also clear persistent file if it exists
|
||||
|
||||
if os.path.exists(PERSISTENT_HISTORY_FILE):
|
||||
try:
|
||||
os.remove(PERSISTENT_HISTORY_FILE)
|
||||
logger.info("Notification history cleared and persistent file deleted.")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error deleting persistent history file: {e}")
|
||||
print(f"Error deleting persistent history file: {e}")
|
||||
|
||||
self.persistent_notifications = []
|
||||
self.containers = []
|
||||
self.rebuild_with_separators()
|
||||
|
||||
def _load_persistent_history(self):
|
||||
"""Load notifications from persistent file."""
|
||||
if not os.path.exists(PERSISTENT_DIR):
|
||||
os.makedirs(PERSISTENT_DIR, exist_ok=True)
|
||||
|
||||
# Load from persistent file
|
||||
|
||||
if os.path.exists(PERSISTENT_HISTORY_FILE):
|
||||
try:
|
||||
with open(PERSISTENT_HISTORY_FILE, "r") as f:
|
||||
self.persistent_notifications = json.load(f)
|
||||
logger.info(f"Loading {len(self.persistent_notifications)} notifications from persistent history")
|
||||
|
||||
# Add notifications to display (in reverse order to show newest first)
|
||||
for note in reversed(self.persistent_notifications[-50:]): # Last 50 notifications
|
||||
|
||||
|
||||
for note in reversed(self.persistent_notifications[-50:]):
|
||||
self._add_historical_notification(note)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error loading persistent history: {e}")
|
||||
print(f"Error loading persistent history: {e}")
|
||||
self.persistent_notifications = []
|
||||
else:
|
||||
self.persistent_notifications = []
|
||||
@@ -306,7 +389,7 @@ class NotificationHistory(Box):
|
||||
with open(PERSISTENT_HISTORY_FILE, "w") as f:
|
||||
json.dump(self.persistent_notifications, f)
|
||||
except Exception as e:
|
||||
logger.error(f"Error saving persistent history: {e}")
|
||||
print(f"Error saving persistent history: {e}")
|
||||
|
||||
def update_no_notifications_label_visibility(self):
|
||||
has_notifications = bool(self.containers)
|
||||
@@ -314,35 +397,39 @@ class NotificationHistory(Box):
|
||||
self.notifications_list.set_visible(has_notifications)
|
||||
|
||||
def _cleanup_orphan_cached_images(self):
|
||||
logger.debug("Starting orphan cached image cleanup.")
|
||||
|
||||
if not os.path.exists(PERSISTENT_DIR):
|
||||
logger.debug("Cache directory does not exist, skipping cleanup.")
|
||||
|
||||
return
|
||||
|
||||
cached_files = [f for f in os.listdir(PERSISTENT_DIR) if f.startswith("notification_") and f.endswith(".png")]
|
||||
cached_files = [
|
||||
f
|
||||
for f in os.listdir(PERSISTENT_DIR)
|
||||
if f.startswith("notification_") and f.endswith(".png")
|
||||
]
|
||||
if not cached_files:
|
||||
logger.debug("No cached image files found, skipping cleanup.")
|
||||
|
||||
return
|
||||
|
||||
history_uuids = {note.get("id") for note in self.persistent_notifications if note.get("id")}
|
||||
history_uuids = {
|
||||
note.get("id") for note in self.persistent_notifications if note.get("id")
|
||||
}
|
||||
deleted_count = 0
|
||||
for cached_file in cached_files:
|
||||
try:
|
||||
uuid_from_filename = cached_file[len("notification_"):-len(".png")]
|
||||
uuid_from_filename = cached_file[len("notification_") : -len(".png")]
|
||||
if uuid_from_filename not in history_uuids:
|
||||
cache_file_path = os.path.join(PERSISTENT_DIR, cached_file)
|
||||
os.remove(cache_file_path)
|
||||
logger.info(f"Deleted orphan cached image: {cache_file_path}")
|
||||
deleted_count += 1
|
||||
else:
|
||||
logger.debug(f"Cached image {cached_file} found in history, keeping it.")
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing cached file {cached_file} during cleanup: {e}")
|
||||
|
||||
if deleted_count > 0:
|
||||
logger.info(f"Orphan cached image cleanup finished. Deleted {deleted_count} images.")
|
||||
else:
|
||||
logger.info("Orphan cached image cleanup finished. No orphan images found.")
|
||||
deleted_count += 1
|
||||
|
||||
except Exception as e:
|
||||
print(
|
||||
f"Error processing cached file {cached_file} during cleanup: {e}"
|
||||
)
|
||||
|
||||
|
||||
|
||||
def _add_historical_notification(self, note):
|
||||
hist_notif = HistoricalNotification(
|
||||
@@ -388,11 +475,9 @@ class NotificationHistory(Box):
|
||||
name="notification-image",
|
||||
orientation="v",
|
||||
children=[
|
||||
CustomImage(
|
||||
pixbuf=load_scaled_pixbuf(hist_box, 48, 48)
|
||||
),
|
||||
CustomImage(pixbuf=load_scaled_pixbuf(hist_box, 48, 48)),
|
||||
Box(v_expand=True),
|
||||
]
|
||||
],
|
||||
)
|
||||
self.hist_notif_summary_label = Label(
|
||||
name="notification-summary",
|
||||
@@ -408,23 +493,41 @@ class NotificationHistory(Box):
|
||||
ellipsization="end",
|
||||
)
|
||||
|
||||
self.hist_notif_body_label = Label(
|
||||
name="notification-body",
|
||||
markup=hist_notif.body,
|
||||
h_align="start",
|
||||
ellipsization="end",
|
||||
line_wrap="word-char",
|
||||
) if hist_notif.body else Box()
|
||||
self.hist_notif_body_label.set_single_line_mode(True) if hist_notif.body else None
|
||||
self.hist_notif_body_label = (
|
||||
Label(
|
||||
name="notification-body",
|
||||
markup=hist_notif.body,
|
||||
h_align="start",
|
||||
ellipsization="end",
|
||||
line_wrap="word-char",
|
||||
)
|
||||
if hist_notif.body
|
||||
else Box()
|
||||
)
|
||||
self.hist_notif_body_label.set_single_line_mode(
|
||||
True
|
||||
) if hist_notif.body else None
|
||||
|
||||
self.hist_notif_summary_box = Box(
|
||||
name="notification-summary-box",
|
||||
orientation="h",
|
||||
children=[
|
||||
self.hist_notif_summary_label,
|
||||
Box(name="notif-sep", h_expand=False, v_expand=False, h_align="center", v_align="center"),
|
||||
Box(
|
||||
name="notif-sep",
|
||||
h_expand=False,
|
||||
v_expand=False,
|
||||
h_align="center",
|
||||
v_align="center",
|
||||
),
|
||||
self.hist_notif_app_name_label,
|
||||
Box(name="notif-sep", h_expand=False, v_expand=False, h_align="center", v_align="center"),
|
||||
Box(
|
||||
name="notif-sep",
|
||||
h_expand=False,
|
||||
v_expand=False,
|
||||
h_align="center",
|
||||
v_align="center",
|
||||
),
|
||||
self.hist_time_label,
|
||||
],
|
||||
)
|
||||
@@ -441,7 +544,9 @@ class NotificationHistory(Box):
|
||||
self.hist_notif_close_button = Button(
|
||||
name="notif-close-button",
|
||||
child=Label(name="notif-close-label", markup=icons.cancel),
|
||||
on_clicked=lambda *_: self.delete_historical_notification(hist_notif.id, container),
|
||||
on_clicked=lambda *_: self.delete_historical_notification(
|
||||
hist_notif.id, container
|
||||
),
|
||||
)
|
||||
self.hist_notif_close_button_box = Box(
|
||||
orientation="v",
|
||||
@@ -482,9 +587,8 @@ class NotificationHistory(Box):
|
||||
|
||||
if removed_from_list:
|
||||
self.persistent_notifications = new_persistent_notifications
|
||||
logger.info(f"Notification with ID {target_note_id_str} was marked for removal from persistent_notifications list.")
|
||||
else:
|
||||
logger.warning(f"Notification with ID {target_note_id_str} was NOT found in persistent_notifications list. The list remains unchanged.")
|
||||
|
||||
|
||||
|
||||
self._save_persistent_history()
|
||||
container.destroy()
|
||||
@@ -494,10 +598,10 @@ class NotificationHistory(Box):
|
||||
|
||||
class NotificationHistoryWindow(Window):
|
||||
def __init__(self):
|
||||
# Create the notification history widget directly
|
||||
|
||||
self.notification_history = NotificationHistory()
|
||||
|
||||
# Create scrolled window with proper sizing
|
||||
|
||||
scrolled = ScrolledWindow(
|
||||
name="notifications-scrolled",
|
||||
child=self.notification_history,
|
||||
@@ -508,7 +612,7 @@ class NotificationHistoryWindow(Window):
|
||||
h_expand=True,
|
||||
v_expand=True,
|
||||
propagate_width=False,
|
||||
propagate_height=False
|
||||
propagate_height=False,
|
||||
)
|
||||
|
||||
main_box = Box(
|
||||
@@ -517,20 +621,17 @@ class NotificationHistoryWindow(Window):
|
||||
spacing=8,
|
||||
h_expand=True,
|
||||
v_expand=True,
|
||||
children=[scrolled]
|
||||
children=[scrolled],
|
||||
)
|
||||
|
||||
# Determine popup position based on dock position
|
||||
|
||||
dock_position = data.DOCK_POSITION
|
||||
popup_anchor = self._get_popup_anchor(dock_position)
|
||||
|
||||
print(f"[NotificationPopup] Dock position: {dock_position}, Popup anchor: {popup_anchor}")
|
||||
|
||||
# Get margin based on dock position
|
||||
margin = self._get_popup_margin(dock_position)
|
||||
|
||||
super().__init__(
|
||||
name="notifications-popup-window",
|
||||
anchor=popup_anchor,
|
||||
margin=margin,
|
||||
child=main_box,
|
||||
@@ -538,89 +639,75 @@ class NotificationHistoryWindow(Window):
|
||||
exclusive=False,
|
||||
keyboard_mode="on-demand",
|
||||
visible=True,
|
||||
all_visible=True
|
||||
all_visible=True,
|
||||
)
|
||||
|
||||
# Auto-hide when clicking outside or pressing escape
|
||||
|
||||
self.connect("button-press-event", self.on_button_press)
|
||||
self.connect("key-press-event", self.on_key_press)
|
||||
|
||||
# Make sure the window can receive key events
|
||||
|
||||
self.set_can_focus(True)
|
||||
self.grab_focus()
|
||||
|
||||
# Set tooltip with keyboard shortcuts
|
||||
self.set_tooltip_text("Keyboard shortcuts:\n• Escape: Close popup\n• Ctrl+D: Toggle DND\n• Ctrl+A: Clear all notifications")
|
||||
|
||||
self.set_tooltip_text(
|
||||
"Keyboard shortcuts:\n• Escape: Close popup\n• Ctrl+D: Toggle DND\n• Ctrl+A: Clear all notifications"
|
||||
)
|
||||
|
||||
def _get_popup_anchor(self, dock_position):
|
||||
"""Determine popup anchor position based on dock position."""
|
||||
# Position popup on the opposite side of the dock for better UX
|
||||
anchor_map = {
|
||||
"Top": "top", # Dock at top -> popup at top right
|
||||
"Bottom": "bottom", # Dock at bottom -> popup at bottom right
|
||||
"Left": "left", # Dock at left -> popup at top left
|
||||
"Right": "right" # Dock at right -> popup at top right
|
||||
"Top": "top", # Dock at top -> popup at top right
|
||||
"Bottom": "bottom", # Dock at bottom -> popup at bottom right
|
||||
"Left": "left", # Dock at left -> popup at top left
|
||||
"Right": "right", # Dock at right -> popup at top right
|
||||
}
|
||||
return anchor_map.get(dock_position, "bottom") # Default fallback
|
||||
return anchor_map.get(dock_position, "bottom")
|
||||
|
||||
def _get_popup_margin(self, dock_position):
|
||||
"""Get appropriate margin based on dock position."""
|
||||
# Add margin to avoid overlapping with dock
|
||||
margin_map = {
|
||||
"Top": "60px 10px 10px 10px", # Top dock -> margin from top
|
||||
"Bottom": "10px 10px 60px 10px", # Bottom dock -> margin from bottom
|
||||
"Left": "10px 10px 10px 60px", # Left dock -> margin from left
|
||||
"Right": "10px 60px 10px 10px" # Right dock -> margin from right
|
||||
"Top": "60px 10px 10px 10px",
|
||||
"Bottom": "10px 10px 60px 10px",
|
||||
"Left": "10px 10px 10px 60px",
|
||||
"Right": "10px 60px 10px 10px",
|
||||
}
|
||||
return margin_map.get(dock_position, "10px 10px 10px 10px") # Default fallback
|
||||
return margin_map.get(dock_position, "10px 10px 10px 10px")
|
||||
|
||||
def refresh_popup(self):
|
||||
"""Refresh the popup content by updating the notification history."""
|
||||
# The notification history will automatically update itself
|
||||
# when notifications are added/removed from the service
|
||||
pass
|
||||
|
||||
def on_key_press(self, _widget, event):
|
||||
"""Handle key press events."""
|
||||
from gi.repository import Gdk
|
||||
|
||||
# Check if Escape key was pressed
|
||||
if event.keyval == Gdk.KEY_Escape:
|
||||
print(f"[NotificationPopup] Escape key pressed, closing popup")
|
||||
self.set_visible(False)
|
||||
return True # Event handled
|
||||
|
||||
# Check for Ctrl+D to toggle DND
|
||||
self.set_visible(False)
|
||||
return True
|
||||
|
||||
|
||||
elif event.state & Gdk.ModifierType.CONTROL_MASK and event.keyval == Gdk.KEY_d:
|
||||
print(f"[NotificationPopup] Ctrl+D pressed, toggling DND")
|
||||
# Toggle DND via the notification history
|
||||
|
||||
current_dnd = self.notification_history.do_not_disturb_enabled
|
||||
self.notification_history.header_switch.set_active(not current_dnd)
|
||||
return True # Event handled
|
||||
return True
|
||||
|
||||
|
||||
# Check for Ctrl+A to clear all notifications
|
||||
elif event.state & Gdk.ModifierType.CONTROL_MASK and event.keyval == Gdk.KEY_a:
|
||||
print(f"[NotificationPopup] Ctrl+A pressed, clearing all notifications")
|
||||
|
||||
self.notification_history.clear_history()
|
||||
return True # Event handled
|
||||
return True
|
||||
|
||||
return False # Let other handlers process the event
|
||||
|
||||
def on_button_press(self, _widget, _event):
|
||||
"""Hide popup when clicking outside."""
|
||||
# This is a simple implementation - in a real scenario you'd want
|
||||
# more sophisticated outside-click detection
|
||||
return False
|
||||
|
||||
def on_button_press(self, _widget, _event):
|
||||
return False
|
||||
|
||||
|
||||
class Notifications(Box):
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(
|
||||
name="notifications",
|
||||
orientation="h" if not data.VERTICAL else "v",
|
||||
spacing=4,
|
||||
children=[NotificationIndicator()],
|
||||
**kwargs
|
||||
**kwargs,
|
||||
)
|
||||
self.show_all()
|
||||
@@ -93,7 +93,6 @@ class PluginManager:
|
||||
"network",
|
||||
"bluetooth",
|
||||
"otp",
|
||||
"notifications",
|
||||
"password",
|
||||
"bookmarks",
|
||||
"process",
|
||||
|
||||
File renamed without changes.
@@ -291,9 +291,7 @@ class ProcessPlugin(PluginBase):
|
||||
)
|
||||
mem_indicator = "💾" if memory_percent > 80 else ""
|
||||
|
||||
subtitle = f"CPU: {cpu_percent:.1f}%{cpu_indicator} | Memory: {
|
||||
memory_mb:.1f
|
||||
}MB ({memory_percent:.1f}%){mem_indicator}"
|
||||
subtitle = f"CPU: {cpu_percent:.1f}%{cpu_indicator} | Memory: {memory_mb:.1f}MB ({memory_percent:.1f}%){mem_indicator}"
|
||||
|
||||
# Choose icon based on CPU usage
|
||||
if cpu_percent > 50:
|
||||
@@ -344,11 +342,6 @@ class ProcessPlugin(PluginBase):
|
||||
if self._kill_process(pid, silent=True):
|
||||
killed_count += 1
|
||||
|
||||
print(
|
||||
f"✓ Terminated {killed_count}/{len(child_pids)} processes for '{
|
||||
app_name
|
||||
}'"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
print(f"✗ Error killing process group: {e}")
|
||||
@@ -365,10 +358,6 @@ class ProcessPlugin(PluginBase):
|
||||
# Wait a bit for graceful termination
|
||||
try:
|
||||
proc.wait(timeout=3)
|
||||
if not silent:
|
||||
print(
|
||||
f"✓ Successfully terminated process '{proc_name}' (PID: {pid})"
|
||||
)
|
||||
return True
|
||||
except psutil.TimeoutExpired:
|
||||
# Force kill if graceful termination failed
|
||||
@@ -497,9 +486,7 @@ class ProcessPlugin(PluginBase):
|
||||
"🔥" if cpu_percent > 80 else "⚡" if cpu_percent > 50 else ""
|
||||
)
|
||||
mem_indicator = "💾" if memory_percent > 80 else ""
|
||||
new_subtitle = f"CPU: {cpu_percent:.1f}%{cpu_indicator} | Memory: {
|
||||
memory_mb:.1f
|
||||
}MB ({memory_percent:.1f}%){mem_indicator}"
|
||||
new_subtitle = f"CPU: {cpu_percent:.1f}%{cpu_indicator} | Memory: {memory_mb:.1f}MB ({memory_percent:.1f}%){mem_indicator}"
|
||||
|
||||
# Find and update the subtitle label widget
|
||||
self._find_and_update_subtitle_label(result_item, new_subtitle)
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
import json
|
||||
import locale
|
||||
import os
|
||||
import uuid
|
||||
from datetime import datetime, timedelta
|
||||
from datetime import datetime
|
||||
|
||||
from fabric.notifications.service import Notification, NotificationAction, Notifications
|
||||
from fabric.utils.helpers import get_relative_path
|
||||
from fabric.widgets.box import Box
|
||||
from fabric.widgets.button import Button
|
||||
from fabric.widgets.centerbox import CenterBox
|
||||
from fabric.widgets.image import Image
|
||||
from fabric.widgets.label import Label
|
||||
from fabric.widgets.revealer import Revealer
|
||||
from fabric.widgets.scrolledwindow import ScrolledWindow
|
||||
from gi.repository import GdkPixbuf, GLib, Gtk
|
||||
from gi.repository import GdkPixbuf, GLib, GObject, Gtk
|
||||
from loguru import logger
|
||||
|
||||
import config.data as data
|
||||
@@ -22,31 +21,33 @@ from utils.wayland import WaylandWindow as Window
|
||||
|
||||
PERSISTENT_DIR = f"/tmp/{data.APP_NAME}/notifications"
|
||||
PERSISTENT_HISTORY_FILE = os.path.join(PERSISTENT_DIR, "notification_history.json")
|
||||
CONFIG_FILE = get_relative_path("../config/assets/config.json")
|
||||
|
||||
# Global shared notification history instance for DND state
|
||||
_shared_notification_history = None
|
||||
|
||||
|
||||
def get_shared_notification_history():
|
||||
global _shared_notification_history
|
||||
if _shared_notification_history is None:
|
||||
_shared_notification_history = NotificationHistory()
|
||||
return _shared_notification_history
|
||||
|
||||
|
||||
def cache_notification_pixbuf(notification_box):
|
||||
"""
|
||||
Saves a scaled pixbuf (48x48) in the cache directory and returns the cache file path.
|
||||
"""
|
||||
notification = notification_box.notification
|
||||
if notification.image_pixbuf:
|
||||
os.makedirs(PERSISTENT_DIR, exist_ok=True)
|
||||
cache_file = os.path.join(
|
||||
PERSISTENT_DIR, f"notification_{notification_box.uuid}.png"
|
||||
)
|
||||
logger.debug(
|
||||
f"Caching image for notification {notification.id} to: {cache_file}"
|
||||
)
|
||||
|
||||
try:
|
||||
scaled = notification.image_pixbuf.scale_simple(
|
||||
48, 48, GdkPixbuf.InterpType.BILINEAR
|
||||
)
|
||||
scaled.savev(cache_file, "png", [], [])
|
||||
logger.info(
|
||||
f"Successfully cached image for notification {notification.id} to: {
|
||||
cache_file
|
||||
}"
|
||||
)
|
||||
|
||||
return cache_file
|
||||
except Exception as e:
|
||||
logger.error(f"Error caching image for notification {notification.id}: {e}")
|
||||
@@ -57,14 +58,8 @@ def cache_notification_pixbuf(notification_box):
|
||||
|
||||
|
||||
def load_scaled_pixbuf(notification_box, width, height):
|
||||
"""
|
||||
Loads and scales a pixbuf for a notification_box, prioritizing cached images.
|
||||
"""
|
||||
notification = notification_box.notification
|
||||
if not hasattr(notification_box, "notification") or notification is None:
|
||||
logger.error(
|
||||
"load_scaled_pixbuf: notification_box.notification is None or not set!"
|
||||
)
|
||||
return None
|
||||
|
||||
pixbuf = None
|
||||
@@ -84,11 +79,7 @@ def load_scaled_pixbuf(notification_box, width, height):
|
||||
pixbuf = pixbuf.scale_simple(
|
||||
width, height, GdkPixbuf.InterpType.BILINEAR
|
||||
)
|
||||
logger.info(
|
||||
f"Successfully loaded cached image from: {
|
||||
notification_box.cached_image_path
|
||||
} for notification {notification.id}"
|
||||
)
|
||||
|
||||
return pixbuf
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
@@ -103,28 +94,15 @@ def load_scaled_pixbuf(notification_box, width, height):
|
||||
)
|
||||
|
||||
if notification.image_pixbuf:
|
||||
logger.debug(
|
||||
f"Loading image directly from notification.image_pixbuf for notification {
|
||||
notification.id
|
||||
}"
|
||||
)
|
||||
pixbuf = notification.image_pixbuf.scale_simple(
|
||||
width, height, GdkPixbuf.InterpType.BILINEAR
|
||||
)
|
||||
return pixbuf
|
||||
|
||||
logger.debug(
|
||||
f"No image_pixbuf or cached image found, trying app icon for notification {
|
||||
notification.id
|
||||
}"
|
||||
)
|
||||
return get_app_icon_pixbuf(notification.app_icon, width, height)
|
||||
|
||||
|
||||
def get_app_icon_pixbuf(icon_path, width, height):
|
||||
"""
|
||||
Loads and scales a pixbuf from an app icon path.
|
||||
"""
|
||||
if not icon_path:
|
||||
return None
|
||||
if icon_path.startswith("file://"):
|
||||
@@ -207,11 +185,7 @@ class NotificationBox(Box):
|
||||
cache_path = cache_notification_pixbuf(self)
|
||||
if cache_path:
|
||||
self.cached_image_path = cache_path
|
||||
logger.debug(
|
||||
f"NotificationBox {self.uuid}: Cached image path set to: {
|
||||
self.cached_image_path
|
||||
}"
|
||||
)
|
||||
|
||||
else:
|
||||
logger.warning(
|
||||
f"NotificationBox {
|
||||
@@ -232,9 +206,6 @@ class NotificationBox(Box):
|
||||
|
||||
self._destroyed = False
|
||||
self._is_history = False
|
||||
logger.debug(
|
||||
f"NotificationBox {self.uuid} created for notification {notification.id}"
|
||||
)
|
||||
|
||||
def set_is_history(self, is_history):
|
||||
self._is_history = is_history
|
||||
@@ -420,13 +391,6 @@ class NotificationBox(Box):
|
||||
return False
|
||||
|
||||
def destroy(self, from_history_delete=False):
|
||||
logger.debug(
|
||||
f"NotificationBox destroy called for notification: {
|
||||
self.notification.id
|
||||
}, from_history_delete: {from_history_delete}, is_history: {
|
||||
self._is_history
|
||||
}"
|
||||
)
|
||||
if (
|
||||
hasattr(self, "cached_image_path")
|
||||
and self.cached_image_path
|
||||
@@ -454,14 +418,25 @@ class NotificationBox(Box):
|
||||
|
||||
|
||||
class NotificationHistory(Box):
|
||||
__gsignals__ = {
|
||||
"dnd-state-changed": (GObject.SignalFlags.RUN_FIRST, None, (bool,)),
|
||||
"notification-added": (GObject.SignalFlags.RUN_FIRST, None, ()),
|
||||
}
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(name="notification-history", orientation="v", **kwargs)
|
||||
self.do_not_disturb_enabled = False
|
||||
self.persistent_notifications = []
|
||||
self._load_persistent_history()
|
||||
self._load_dnd_state()
|
||||
|
||||
def set_do_not_disturb_enabled(self, enabled):
|
||||
if self.do_not_disturb_enabled != enabled:
|
||||
self.do_not_disturb_enabled = enabled
|
||||
self._save_dnd_state()
|
||||
self.emit("dnd-state-changed", enabled)
|
||||
|
||||
def _load_persistent_history(self):
|
||||
"""Load notifications from persistent file."""
|
||||
if not os.path.exists(PERSISTENT_DIR):
|
||||
os.makedirs(PERSISTENT_DIR, exist_ok=True)
|
||||
|
||||
@@ -480,8 +455,33 @@ class NotificationHistory(Box):
|
||||
else:
|
||||
self.persistent_notifications = []
|
||||
|
||||
def _load_dnd_state(self):
|
||||
try:
|
||||
if os.path.exists(CONFIG_FILE):
|
||||
with open(CONFIG_FILE, "r") as f:
|
||||
config_data = json.load(f)
|
||||
self.do_not_disturb_enabled = config_data.get("dnd_enabled", False)
|
||||
logger.info(f"Loaded DND state: {self.do_not_disturb_enabled}")
|
||||
except Exception as e:
|
||||
logger.error(f"Error loading DND state: {e}")
|
||||
self.do_not_disturb_enabled = False
|
||||
|
||||
def _save_dnd_state(self):
|
||||
try:
|
||||
config_data = {}
|
||||
if os.path.exists(CONFIG_FILE):
|
||||
with open(CONFIG_FILE, "r") as f:
|
||||
config_data = json.load(f)
|
||||
|
||||
config_data["dnd_enabled"] = self.do_not_disturb_enabled
|
||||
|
||||
with open(CONFIG_FILE, "w") as f:
|
||||
json.dump(config_data, f, indent=4)
|
||||
logger.debug(f"Saved DND state: {self.do_not_disturb_enabled}")
|
||||
except Exception as e:
|
||||
logger.error(f"Error saving DND state: {e}")
|
||||
|
||||
def _save_persistent_history(self):
|
||||
"""Save notifications to persistent file."""
|
||||
try:
|
||||
with open(PERSISTENT_HISTORY_FILE, "w") as f:
|
||||
json.dump(self.persistent_notifications, f, indent=2)
|
||||
@@ -494,11 +494,9 @@ class NotificationHistory(Box):
|
||||
logger.error(f"Error saving persistent history: {e}")
|
||||
|
||||
def add_notification(self, notification_box):
|
||||
"""Add a notification to the persistent history."""
|
||||
try:
|
||||
notification = notification_box.notification
|
||||
|
||||
# Convert to persistent format
|
||||
hist_data = {
|
||||
"id": notification_box.uuid,
|
||||
"app_icon": getattr(
|
||||
@@ -512,28 +510,18 @@ class NotificationHistory(Box):
|
||||
notification_box, "cached_image_path", None
|
||||
),
|
||||
}
|
||||
|
||||
# Add to persistent notifications (keep last 100)
|
||||
self.persistent_notifications.append(hist_data)
|
||||
if len(self.persistent_notifications) > 100:
|
||||
# Remove oldest notifications but keep their cached images
|
||||
old_notif = self.persistent_notifications.pop(0)
|
||||
# Don't delete cached image here as it might be in use
|
||||
|
||||
# Save to file
|
||||
self._save_persistent_history()
|
||||
|
||||
logger.info(
|
||||
f"Added notification to history: {hist_data['summary']} from {
|
||||
hist_data['app_name']
|
||||
}"
|
||||
)
|
||||
self.emit("notification-added")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error adding notification to history: {e}")
|
||||
|
||||
def clear_history_for_app(self, app_name):
|
||||
"""Clear history for a specific app."""
|
||||
try:
|
||||
original_count = len(self.persistent_notifications)
|
||||
self.persistent_notifications = [
|
||||
@@ -639,9 +627,6 @@ class NotificationContainer(Box):
|
||||
def on_new_notification(self, fabric_notif, id):
|
||||
notification_history_instance = self.notification_history
|
||||
if notification_history_instance.do_not_disturb_enabled:
|
||||
logger.info(
|
||||
"Do Not Disturb mode enabled: adding notification directly to history."
|
||||
)
|
||||
notification = fabric_notif.get_notification_from_id(id)
|
||||
new_box = NotificationBox(notification)
|
||||
if notification.image_pixbuf:
|
||||
@@ -670,7 +655,6 @@ class NotificationContainer(Box):
|
||||
existing_notification_index
|
||||
)
|
||||
self.stack.remove(old_notification_box)
|
||||
# Add old notification to history before destroying
|
||||
old_notification_box.set_is_history(True)
|
||||
notification_history_instance.add_notification(old_notification_box)
|
||||
old_notification_box.destroy()
|
||||
@@ -712,26 +696,19 @@ class NotificationContainer(Box):
|
||||
self.main_revealer.set_reveal_child(True)
|
||||
self.update_navigation_buttons()
|
||||
|
||||
logger.info(
|
||||
f"[NotificationContainer] New notification from {notification.app_name}"
|
||||
)
|
||||
|
||||
def show_previous(self, *args):
|
||||
"""Show the previous notification in the stack."""
|
||||
if self.current_index > 0:
|
||||
self.current_index -= 1
|
||||
self.stack.set_visible_child(self.notifications[self.current_index])
|
||||
self.update_navigation_buttons()
|
||||
|
||||
def show_next(self, *args):
|
||||
"""Show the next notification in the stack."""
|
||||
if self.current_index < len(self.notifications) - 1:
|
||||
self.current_index += 1
|
||||
self.stack.set_visible_child(self.notifications[self.current_index])
|
||||
self.update_navigation_buttons()
|
||||
|
||||
def update_navigation_buttons(self):
|
||||
"""Update navigation button states and visibility."""
|
||||
self.prev_button.set_sensitive(self.current_index > 0)
|
||||
self.next_button.set_sensitive(self.current_index < len(self.notifications) - 1)
|
||||
should_reveal = len(self.notifications) > 1
|
||||
@@ -744,7 +721,6 @@ class NotificationContainer(Box):
|
||||
return
|
||||
self._destroyed_notifications.add(notification.id)
|
||||
try:
|
||||
logger.info(f"Notification {notification.id} closing with reason: {reason}")
|
||||
notif_to_remove = None
|
||||
for i, notif_box in enumerate(self.notifications):
|
||||
if notif_box.notification.id == notification.id:
|
||||
@@ -757,17 +733,10 @@ class NotificationContainer(Box):
|
||||
|
||||
notification_history_instance = self.notification_history
|
||||
|
||||
# Always add to history regardless of close reason
|
||||
logger.info(
|
||||
f"Adding notification {notification.id} to history (reason: {
|
||||
reason_str
|
||||
})"
|
||||
)
|
||||
notif_box.set_is_history(True)
|
||||
notification_history_instance.add_notification(notif_box)
|
||||
notif_box.stop_timeout()
|
||||
|
||||
# Only destroy if explicitly dismissed by user
|
||||
if reason_str == "NotificationCloseReason.DISMISSED_BY_USER":
|
||||
logger.info(
|
||||
f"User dismissed notification {
|
||||
@@ -823,7 +792,6 @@ class NotificationContainer(Box):
|
||||
logger.error(f"Error closing notification: {e}")
|
||||
|
||||
def _destroy_container(self):
|
||||
"""Clean up the container when all notifications are closed."""
|
||||
try:
|
||||
self.notifications.clear()
|
||||
self._destroyed_notifications.clear()
|
||||
@@ -838,7 +806,6 @@ class NotificationContainer(Box):
|
||||
return False
|
||||
|
||||
def pause_and_reset_all_timeouts(self):
|
||||
"""Pause timeouts for all notifications."""
|
||||
if self._is_destroying:
|
||||
return
|
||||
for notification in self.notifications[:]:
|
||||
@@ -849,7 +816,6 @@ class NotificationContainer(Box):
|
||||
logger.error(f"Error pausing timeout: {e}")
|
||||
|
||||
def resume_all_timeouts(self):
|
||||
"""Resume timeouts for all notifications."""
|
||||
if self._is_destroying:
|
||||
return
|
||||
for notification in self.notifications[:]:
|
||||
@@ -860,7 +826,6 @@ class NotificationContainer(Box):
|
||||
logger.error(f"Error resuming timeout: {e}")
|
||||
|
||||
def close_all_notifications(self, *args):
|
||||
"""Close all notifications."""
|
||||
notifications_to_close = self.notifications.copy()
|
||||
for notification_box in notifications_to_close:
|
||||
notification_box.notification.close("dismissed-by-user")
|
||||
@@ -871,7 +836,6 @@ class NotificationPopup(Window):
|
||||
y_pos = data.NOTIF_POS.lower() if hasattr(data, "NOTIF_POS") else "top"
|
||||
x_pos = "right"
|
||||
|
||||
# Simple positioning logic - can be enhanced later
|
||||
if hasattr(data, "DOCK_POSITION") and data.DOCK_POSITION in ["Top", "Bottom"]:
|
||||
x_pos = "right"
|
||||
|
||||
@@ -888,7 +852,9 @@ class NotificationPopup(Window):
|
||||
self.widgets = kwargs.get("widgets", None)
|
||||
|
||||
self.notification_history = (
|
||||
self.widgets.notification_history if self.widgets else NotificationHistory()
|
||||
self.widgets.notification_history
|
||||
if self.widgets
|
||||
else get_shared_notification_history()
|
||||
)
|
||||
self.notification_container = NotificationContainer(
|
||||
notification_history_instance=self.notification_history,
|
||||
|
||||
Reference in new issue
Block a user