Addded Caching System
This commit is contained in:
1 parent
4852fa01e2
commit
e7fac41852
3 files changed
+466
-25
No files matched your search
@@ -1,4 +1,6 @@
|
|||||||
import os
|
import os
|
||||||
|
import hashlib
|
||||||
|
import time
|
||||||
|
|
||||||
from fabric.utils import get_relative_path
|
from fabric.utils import get_relative_path
|
||||||
from gi.repository import Gdk, GdkPixbuf, GLib, Gtk # type: ignore
|
from gi.repository import Gdk, GdkPixbuf, GLib, Gtk # type: ignore
|
||||||
@@ -25,6 +27,306 @@ from services.modus import notification_service
|
|||||||
NOTIFICATION_WIDTH = 360
|
NOTIFICATION_WIDTH = 360
|
||||||
NOTIFICATION_IMAGE_SIZE = 48
|
NOTIFICATION_IMAGE_SIZE = 48
|
||||||
|
|
||||||
|
NOTIFICATION_WIDTH = 360
|
||||||
|
NOTIFICATION_IMAGE_SIZE = 48
|
||||||
|
|
||||||
|
# Notification icon cache directory (for small notification and app icons)
|
||||||
|
NOTIFICATION_ICON_CACHE_DIR = os.path.join(data.CACHE_DIR, "notification_icons")
|
||||||
|
# Notification image cache directory (for large notification images)
|
||||||
|
NOTIFICATION_IMAGE_CACHE_DIR = os.path.join(data.CACHE_DIR, "notification_images")
|
||||||
|
|
||||||
|
def ensure_notification_cache_dirs():
|
||||||
|
"""Ensure notification cache directories exist"""
|
||||||
|
os.makedirs(NOTIFICATION_ICON_CACHE_DIR, exist_ok=True)
|
||||||
|
os.makedirs(NOTIFICATION_IMAGE_CACHE_DIR, exist_ok=True)
|
||||||
|
|
||||||
|
def cleanup_old_cache_files():
|
||||||
|
"""Clean up old notification cache files (older than 7 days) from icon cache only"""
|
||||||
|
try:
|
||||||
|
if not os.path.exists(NOTIFICATION_ICON_CACHE_DIR):
|
||||||
|
return
|
||||||
|
|
||||||
|
current_time = time.time()
|
||||||
|
week_ago = current_time - (7 * 24 * 60 * 60) # 7 days
|
||||||
|
|
||||||
|
for filename in os.listdir(NOTIFICATION_ICON_CACHE_DIR):
|
||||||
|
filepath = os.path.join(NOTIFICATION_ICON_CACHE_DIR, filename)
|
||||||
|
try:
|
||||||
|
if os.path.isfile(filepath):
|
||||||
|
file_mtime = os.path.getmtime(filepath)
|
||||||
|
if file_mtime < week_ago:
|
||||||
|
os.unlink(filepath)
|
||||||
|
logger.debug(f"Cleaned up old notification icon cache: {filename}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Failed to cleanup cache file {filename}: {e}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Failed to cleanup notification cache: {e}")
|
||||||
|
|
||||||
|
def get_cache_key(source_data, size=None):
|
||||||
|
"""Generate a cache key from source data and optional size"""
|
||||||
|
if isinstance(source_data, str):
|
||||||
|
# For file paths
|
||||||
|
cache_input = source_data
|
||||||
|
if size:
|
||||||
|
cache_input += f"_{size[0]}x{size[1]}"
|
||||||
|
else:
|
||||||
|
# For pixbuf data - use hash of pixel data
|
||||||
|
try:
|
||||||
|
pixel_data = source_data.get_pixels()
|
||||||
|
cache_input = hashlib.md5(pixel_data).hexdigest()
|
||||||
|
if size:
|
||||||
|
cache_input += f"_{size[0]}x{size[1]}"
|
||||||
|
except Exception:
|
||||||
|
# Fallback to timestamp if pixel data fails
|
||||||
|
cache_input = f"pixbuf_{int(time.time())}"
|
||||||
|
|
||||||
|
return hashlib.md5(cache_input.encode()).hexdigest()
|
||||||
|
|
||||||
|
def save_pixbuf_to_cache(pixbuf, cache_key, cache_dir):
|
||||||
|
"""Save a pixbuf to the specified cache directory"""
|
||||||
|
try:
|
||||||
|
ensure_notification_cache_dirs()
|
||||||
|
cache_path = os.path.join(cache_dir, f"{cache_key}.png")
|
||||||
|
|
||||||
|
# Don't overwrite existing cache
|
||||||
|
if os.path.exists(cache_path):
|
||||||
|
return cache_path
|
||||||
|
|
||||||
|
pixbuf.savev(cache_path, "png", [], [])
|
||||||
|
logger.debug(f"Cached notification icon: {cache_key}")
|
||||||
|
return cache_path
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Failed to cache notification icon: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_cached_pixbuf(cache_key, fallback_size=(48, 48), cache_dir=None):
|
||||||
|
"""Get a cached pixbuf or return None if not found"""
|
||||||
|
if cache_dir is None:
|
||||||
|
cache_dir = NOTIFICATION_ICON_CACHE_DIR
|
||||||
|
|
||||||
|
try:
|
||||||
|
cache_path = os.path.join(cache_dir, f"{cache_key}.png")
|
||||||
|
if os.path.exists(cache_path):
|
||||||
|
logger.debug(f"Using cached notification icon: {cache_key}")
|
||||||
|
return GdkPixbuf.Pixbuf.new_from_file_at_scale(
|
||||||
|
cache_path, fallback_size[0], fallback_size[1], True
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Failed to load cached notification icon: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def cache_notification_icon(source, size=(48, 48)):
|
||||||
|
"""Cache a notification icon from local sources (file, pixbuf) - PERMANENT cache"""
|
||||||
|
try:
|
||||||
|
ensure_notification_cache_dirs()
|
||||||
|
|
||||||
|
# Handle different source types
|
||||||
|
if isinstance(source, str):
|
||||||
|
cache_key = get_cache_key(source, size)
|
||||||
|
|
||||||
|
# Check if already cached
|
||||||
|
cached_pixbuf = get_cached_pixbuf(cache_key, size, NOTIFICATION_ICON_CACHE_DIR)
|
||||||
|
if cached_pixbuf:
|
||||||
|
return cached_pixbuf
|
||||||
|
|
||||||
|
# Load and cache the icon (local only)
|
||||||
|
if source.startswith('file://'):
|
||||||
|
# Local file URL
|
||||||
|
file_path = source[7:]
|
||||||
|
pixbuf = load_and_cache_local_icon(file_path, cache_key, size)
|
||||||
|
elif os.path.exists(source):
|
||||||
|
# Direct file path
|
||||||
|
pixbuf = load_and_cache_local_icon(source, cache_key, size)
|
||||||
|
else:
|
||||||
|
# Icon name - try to resolve from theme
|
||||||
|
pixbuf = load_and_cache_theme_icon(source, cache_key, size)
|
||||||
|
|
||||||
|
return pixbuf
|
||||||
|
|
||||||
|
elif hasattr(source, 'scale_simple'):
|
||||||
|
# Already a pixbuf
|
||||||
|
cache_key = get_cache_key(source, size)
|
||||||
|
|
||||||
|
# Check if already cached
|
||||||
|
cached_pixbuf = get_cached_pixbuf(cache_key, size, NOTIFICATION_ICON_CACHE_DIR)
|
||||||
|
if cached_pixbuf:
|
||||||
|
return cached_pixbuf
|
||||||
|
|
||||||
|
# Scale and cache
|
||||||
|
scaled_pixbuf = source.scale_simple(size[0], size[1], GdkPixbuf.InterpType.BILINEAR)
|
||||||
|
save_pixbuf_to_cache(scaled_pixbuf, cache_key, NOTIFICATION_ICON_CACHE_DIR)
|
||||||
|
return scaled_pixbuf
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Failed to cache notification icon: {e}")
|
||||||
|
|
||||||
|
# Return fallback
|
||||||
|
return get_fallback_notification_icon(size)
|
||||||
|
|
||||||
|
def load_and_cache_local_icon(file_path, cache_key, size):
|
||||||
|
"""Load a local icon file and cache it"""
|
||||||
|
try:
|
||||||
|
if os.path.exists(file_path):
|
||||||
|
pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_scale(
|
||||||
|
file_path, size[0], size[1], True
|
||||||
|
)
|
||||||
|
save_pixbuf_to_cache(pixbuf, cache_key, NOTIFICATION_ICON_CACHE_DIR)
|
||||||
|
return pixbuf
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Failed to load local notification icon {file_path}: {e}")
|
||||||
|
|
||||||
|
return get_fallback_notification_icon(size)
|
||||||
|
|
||||||
|
def load_and_cache_theme_icon(icon_name, cache_key, size):
|
||||||
|
"""Load an icon from the current theme and cache it"""
|
||||||
|
try:
|
||||||
|
icon_theme = Gtk.IconTheme.get_default()
|
||||||
|
icon_info = icon_theme.lookup_icon(icon_name, min(size), 0)
|
||||||
|
|
||||||
|
if icon_info:
|
||||||
|
pixbuf = icon_info.load_icon()
|
||||||
|
if pixbuf:
|
||||||
|
scaled_pixbuf = pixbuf.scale_simple(size[0], size[1], GdkPixbuf.InterpType.BILINEAR)
|
||||||
|
save_pixbuf_to_cache(scaled_pixbuf, cache_key, NOTIFICATION_ICON_CACHE_DIR)
|
||||||
|
return scaled_pixbuf
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Failed to load theme notification icon {icon_name}: {e}")
|
||||||
|
|
||||||
|
return get_fallback_notification_icon(size)
|
||||||
|
|
||||||
|
def get_fallback_notification_icon(size=(48, 48)):
|
||||||
|
"""Get the fallback notification icon"""
|
||||||
|
try:
|
||||||
|
fallback_path = get_relative_path("../../config/assets/icons/notification.png")
|
||||||
|
return GdkPixbuf.Pixbuf.new_from_file_at_scale(
|
||||||
|
fallback_path, size[0], size[1], True
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Failed to load fallback notification icon: {e}")
|
||||||
|
# Create a simple colored rectangle as ultimate fallback
|
||||||
|
try:
|
||||||
|
return GdkPixbuf.Pixbuf.new(GdkPixbuf.Colorspace.RGB, True, 8, size[0], size[1])
|
||||||
|
except:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_notification_image_cache_key(notification_id, image_pixbuf):
|
||||||
|
"""Generate a cache key for notification images"""
|
||||||
|
try:
|
||||||
|
# Use notification ID and image hash for unique key
|
||||||
|
if hasattr(image_pixbuf, 'get_pixels'):
|
||||||
|
pixel_data = image_pixbuf.get_pixels()
|
||||||
|
image_hash = hashlib.md5(pixel_data).hexdigest()[:8]
|
||||||
|
else:
|
||||||
|
image_hash = str(int(time.time()))
|
||||||
|
return f"notif_{notification_id}_{image_hash}"
|
||||||
|
except Exception:
|
||||||
|
return f"notif_{notification_id}_{int(time.time())}"
|
||||||
|
|
||||||
|
def cache_notification_image(notification_id, image_pixbuf, size=(64, 64)):
|
||||||
|
"""Cache a notification image pixbuf - PERMANENT cache until manual clear"""
|
||||||
|
try:
|
||||||
|
ensure_notification_cache_dirs()
|
||||||
|
|
||||||
|
cache_key = get_notification_image_cache_key(notification_id, image_pixbuf)
|
||||||
|
cache_path = os.path.join(NOTIFICATION_IMAGE_CACHE_DIR, f"{cache_key}.png")
|
||||||
|
|
||||||
|
# Don't overwrite existing cache
|
||||||
|
if os.path.exists(cache_path):
|
||||||
|
return cache_path, cache_key
|
||||||
|
|
||||||
|
# Scale and save the image
|
||||||
|
scaled_pixbuf = image_pixbuf.scale_simple(size[0], size[1], GdkPixbuf.InterpType.BILINEAR)
|
||||||
|
scaled_pixbuf.savev(cache_path, "png", [], [])
|
||||||
|
logger.debug(f"Cached notification image: {cache_key}")
|
||||||
|
return cache_path, cache_key
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Failed to cache notification image: {e}")
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
def get_cached_notification_image(cache_key):
|
||||||
|
"""Get a cached notification image or return None if not found"""
|
||||||
|
try:
|
||||||
|
cache_path = os.path.join(NOTIFICATION_IMAGE_CACHE_DIR, f"{cache_key}.png")
|
||||||
|
if os.path.exists(cache_path):
|
||||||
|
logger.debug(f"Using cached notification image: {cache_key}")
|
||||||
|
return GdkPixbuf.Pixbuf.new_from_file(cache_path)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Failed to load cached notification image: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def cleanup_notification_image_cache(cache_key=None):
|
||||||
|
"""Clean up notification image cache - specific key or all"""
|
||||||
|
try:
|
||||||
|
ensure_notification_cache_dirs()
|
||||||
|
|
||||||
|
if cache_key:
|
||||||
|
# Remove specific cached image
|
||||||
|
cache_path = os.path.join(NOTIFICATION_IMAGE_CACHE_DIR, f"{cache_key}.png")
|
||||||
|
if os.path.exists(cache_path):
|
||||||
|
os.unlink(cache_path)
|
||||||
|
logger.debug(f"Cleaned up cached notification image: {cache_key}")
|
||||||
|
else:
|
||||||
|
# Remove all cached images
|
||||||
|
for filename in os.listdir(NOTIFICATION_IMAGE_CACHE_DIR):
|
||||||
|
if filename.endswith('.png'):
|
||||||
|
filepath = os.path.join(NOTIFICATION_IMAGE_CACHE_DIR, filename)
|
||||||
|
try:
|
||||||
|
os.unlink(filepath)
|
||||||
|
logger.debug(f"Cleaned up cached notification image: {filename}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Failed to cleanup cache file {filename}: {e}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Failed to cleanup notification image cache: {e}")
|
||||||
|
|
||||||
|
def cleanup_notification_specific_caches(app_icon_source=None, notification_image_cache_key=None):
|
||||||
|
"""Clean up caches specific to a notification (both app icon and notification image)"""
|
||||||
|
try:
|
||||||
|
# Clean up notification image cache
|
||||||
|
if notification_image_cache_key:
|
||||||
|
cleanup_notification_image_cache(notification_image_cache_key)
|
||||||
|
|
||||||
|
# Clean up app icon cache for this specific source
|
||||||
|
if app_icon_source:
|
||||||
|
cache_key = get_cache_key(app_icon_source, (24, 24)) # Standard app icon size
|
||||||
|
cache_path = os.path.join(NOTIFICATION_ICON_CACHE_DIR, f"{cache_key}.png")
|
||||||
|
if os.path.exists(cache_path):
|
||||||
|
os.unlink(cache_path)
|
||||||
|
logger.debug(f"Cleaned up cached app icon: {cache_key}")
|
||||||
|
|
||||||
|
# Also clean 35x35 version used in notifications
|
||||||
|
cache_key_35 = get_cache_key(app_icon_source, (35, 35))
|
||||||
|
cache_path_35 = os.path.join(NOTIFICATION_ICON_CACHE_DIR, f"{cache_key_35}.png")
|
||||||
|
if os.path.exists(cache_path_35):
|
||||||
|
os.unlink(cache_path_35)
|
||||||
|
logger.debug(f"Cleaned up cached app icon (35x35): {cache_key_35}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Failed to cleanup notification specific caches: {e}")
|
||||||
|
|
||||||
|
def cleanup_all_notification_caches():
|
||||||
|
"""Clean up ALL notification caches (icons and images)"""
|
||||||
|
try:
|
||||||
|
# Clean icon cache
|
||||||
|
if os.path.exists(NOTIFICATION_ICON_CACHE_DIR):
|
||||||
|
for filename in os.listdir(NOTIFICATION_ICON_CACHE_DIR):
|
||||||
|
if filename.endswith('.png'):
|
||||||
|
filepath = os.path.join(NOTIFICATION_ICON_CACHE_DIR, filename)
|
||||||
|
try:
|
||||||
|
os.unlink(filepath)
|
||||||
|
logger.debug(f"Cleaned up cached notification icon: {filename}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Failed to cleanup icon cache file {filename}: {e}")
|
||||||
|
|
||||||
|
# Clean image cache
|
||||||
|
cleanup_notification_image_cache()
|
||||||
|
logger.info("Cleaned up all notification caches")
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Failed to cleanup all notification caches: {e}")
|
||||||
|
|
||||||
|
# Initialize cache on module load
|
||||||
|
ensure_notification_cache_dirs()
|
||||||
|
cleanup_old_cache_files()
|
||||||
|
|
||||||
|
|
||||||
def smooth_revealer_animation(revealer: SlideRevealer, duration: int = 350):
|
def smooth_revealer_animation(revealer: SlideRevealer, duration: int = 350):
|
||||||
"""Configure revealer for macOS-like smooth animation"""
|
"""Configure revealer for macOS-like smooth animation"""
|
||||||
@@ -57,6 +359,8 @@ class ActionButton(Button):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def on_clicked(self, *_):
|
def on_clicked(self, *_):
|
||||||
|
# Mark for cache cleanup when action button is clicked
|
||||||
|
self.notification_box._should_cleanup_cache = True
|
||||||
self.action.invoke()
|
self.action.invoke()
|
||||||
self.action.parent.close("dismissed-by-user")
|
self.action.parent.close("dismissed-by-user")
|
||||||
|
|
||||||
@@ -72,6 +376,9 @@ class NotificationWidget(Box):
|
|||||||
self.show_close_button = show_close_button
|
self.show_close_button = show_close_button
|
||||||
self.close_button = None
|
self.close_button = None
|
||||||
self._is_hovered = False
|
self._is_hovered = False
|
||||||
|
self.notification_image_cache_key = None # Track cached image for cleanup
|
||||||
|
self.app_icon_source = notification.app_icon # Track app icon source for cleanup
|
||||||
|
self._should_cleanup_cache = False # Only cleanup cache on manual dismissal
|
||||||
|
|
||||||
super().__init__(
|
super().__init__(
|
||||||
size=(NOTIFICATION_WIDTH, -1),
|
size=(NOTIFICATION_WIDTH, -1),
|
||||||
@@ -187,9 +494,7 @@ class NotificationWidget(Box):
|
|||||||
name="notification-close-button",
|
name="notification-close-button",
|
||||||
image=CustomImage(icon_name="close-symbolic", icon_size=18),
|
image=CustomImage(icon_name="close-symbolic", icon_size=18),
|
||||||
visible=True, # Initially hidden
|
visible=True, # Initially hidden
|
||||||
on_clicked=lambda *_: self.notification.close(
|
on_clicked=lambda *_: self._manual_close(),
|
||||||
"dismissed-by-user"
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
Box(v_expand=True),
|
Box(v_expand=True),
|
||||||
],
|
],
|
||||||
@@ -212,32 +517,65 @@ class NotificationWidget(Box):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
def get_pixbuf(self, icon_path, width, height):
|
def get_pixbuf(self, icon_path, width, height):
|
||||||
if icon_path.startswith("file://") and os.path.exists(icon_path):
|
"""Get pixbuf with caching support"""
|
||||||
|
try:
|
||||||
|
# Use the icon caching system
|
||||||
|
cached_pixbuf = cache_notification_icon(icon_path, (width, height))
|
||||||
|
if cached_pixbuf:
|
||||||
|
return cached_pixbuf
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Failed to get cached pixbuf for {icon_path}: {e}")
|
||||||
|
|
||||||
|
# Fallback to original method if caching fails
|
||||||
|
if icon_path.startswith("file://"):
|
||||||
icon_path = icon_path[7:]
|
icon_path = icon_path[7:]
|
||||||
|
|
||||||
if not os.path.exists(icon_path):
|
if not os.path.exists(icon_path):
|
||||||
logger.warning(f"Icon path does not exist: {icon_path}")
|
logger.warning(f"Icon path does not exist: {icon_path}")
|
||||||
icon_path = get_relative_path("../../config/assets/icons/notification.png")
|
return get_fallback_notification_icon((width, height))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
pixbuf = GdkPixbuf.Pixbuf.new_from_file(icon_path)
|
pixbuf = GdkPixbuf.Pixbuf.new_from_file(icon_path)
|
||||||
return pixbuf.scale_simple(width, height, GdkPixbuf.InterpType.BILINEAR)
|
return pixbuf.scale_simple(width, height, GdkPixbuf.InterpType.BILINEAR)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Failed to load or scale icon: {e}")
|
logger.error(f"Failed to load or scale icon: {e}")
|
||||||
return None
|
return get_fallback_notification_icon((width, height))
|
||||||
|
|
||||||
def _get_notification_pixbuf(self, notification):
|
def _get_notification_pixbuf(self, notification):
|
||||||
"""Safely get notification pixbuf, fallback to app icon if needed"""
|
"""Safely get notification pixbuf with caching for image notifications"""
|
||||||
try:
|
try:
|
||||||
if hasattr(notification, 'image_pixbuf') and notification.image_pixbuf:
|
if hasattr(notification, 'image_pixbuf') and notification.image_pixbuf:
|
||||||
|
# Try to get/cache the notification image
|
||||||
|
notification_id = getattr(notification, 'id', int(time.time()))
|
||||||
|
cache_path, cache_key = cache_notification_image(
|
||||||
|
notification_id, notification.image_pixbuf, (64, 64)
|
||||||
|
)
|
||||||
|
|
||||||
|
if cache_path and cache_key:
|
||||||
|
# Store cache key for reference (but don't auto-delete)
|
||||||
|
self.notification_image_cache_key = cache_key
|
||||||
|
# Load the cached image
|
||||||
|
cached_pixbuf = get_cached_notification_image(cache_key)
|
||||||
|
if cached_pixbuf:
|
||||||
|
return cached_pixbuf.scale_simple(35, 35, GdkPixbuf.InterpType.BILINEAR)
|
||||||
|
|
||||||
|
# Fallback to direct scaling if caching fails
|
||||||
return notification.image_pixbuf.scale_simple(
|
return notification.image_pixbuf.scale_simple(
|
||||||
35, 35, GdkPixbuf.InterpType.BILINEAR
|
35, 35, GdkPixbuf.InterpType.BILINEAR
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"Failed to get notification image: {e}")
|
logger.warning(f"Failed to get notification image: {e}")
|
||||||
|
|
||||||
# Fallback to app icon
|
# Fallback to cached app icon
|
||||||
return self.get_pixbuf(notification.app_icon, 35, 35)
|
try:
|
||||||
|
cached_app_icon = cache_notification_icon(notification.app_icon, (35, 35))
|
||||||
|
if cached_app_icon:
|
||||||
|
return cached_app_icon
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Failed to get cached app icon: {e}")
|
||||||
|
|
||||||
|
# Ultimate fallback
|
||||||
|
return get_fallback_notification_icon((35, 35))
|
||||||
|
|
||||||
def create_action_buttons(self, notification):
|
def create_action_buttons(self, notification):
|
||||||
return Box(
|
return Box(
|
||||||
@@ -271,8 +609,22 @@ class NotificationWidget(Box):
|
|||||||
if not self._is_hovered: # Only resume if not hovered
|
if not self._is_hovered: # Only resume if not hovered
|
||||||
self.start_timeout()
|
self.start_timeout()
|
||||||
|
|
||||||
|
def _manual_close(self):
|
||||||
|
"""Handle manual close button click - mark for cache cleanup"""
|
||||||
|
self._should_cleanup_cache = True
|
||||||
|
self.notification.close("dismissed-by-user")
|
||||||
|
|
||||||
def destroy(self):
|
def destroy(self):
|
||||||
self.stop_timeout()
|
self.stop_timeout()
|
||||||
|
# Only clean up caches if this was a manual dismissal
|
||||||
|
if self._should_cleanup_cache:
|
||||||
|
cleanup_notification_specific_caches(
|
||||||
|
app_icon_source=getattr(self, 'app_icon_source', None),
|
||||||
|
notification_image_cache_key=getattr(self, 'notification_image_cache_key', None)
|
||||||
|
)
|
||||||
|
logger.debug(f"Cleaned up caches for manually dismissed notification")
|
||||||
|
else:
|
||||||
|
logger.debug(f"Preserved caches for timeout/auto-dismissed notification")
|
||||||
super().destroy()
|
super().destroy()
|
||||||
|
|
||||||
# @staticmethod
|
# @staticmethod
|
||||||
@@ -384,6 +736,8 @@ class NotificationRevealer(SlideRevealer):
|
|||||||
self._apply_transform(current_offset, opacity, scale)
|
self._apply_transform(current_offset, opacity, scale)
|
||||||
|
|
||||||
if progress >= 1.0:
|
if progress >= 1.0:
|
||||||
|
# Mark notification for cache cleanup on swipe dismissal
|
||||||
|
self.notif_box._should_cleanup_cache = True
|
||||||
# Trigger the actual notification dismissal immediately
|
# Trigger the actual notification dismissal immediately
|
||||||
try:
|
try:
|
||||||
self.notification.close("dismissed-by-user")
|
self.notification.close("dismissed-by-user")
|
||||||
@@ -546,6 +900,9 @@ class ModusNoti(Window):
|
|||||||
):
|
):
|
||||||
self.notification_state = NotificationState.HIDING
|
self.notification_state = NotificationState.HIDING
|
||||||
|
|
||||||
|
# Mark for cache cleanup when forcibly hiding for new notification
|
||||||
|
self.current_notification.notif_box._should_cleanup_cache = True
|
||||||
|
|
||||||
# Force close the current notification to trigger hiding animation
|
# Force close the current notification to trigger hiding animation
|
||||||
try:
|
try:
|
||||||
self.current_notification.notification.close("dismissed-by-user")
|
self.current_notification.notification.close("dismissed-by-user")
|
||||||
@@ -634,6 +991,11 @@ class ModusNoti(Window):
|
|||||||
pass # Ignore errors if notification is already closed
|
pass # Ignore errors if notification is already closed
|
||||||
self.notification_queue.clear()
|
self.notification_queue.clear()
|
||||||
|
|
||||||
|
# Also clean current notification if showing
|
||||||
|
if self.current_notification:
|
||||||
|
# Mark for cache cleanup when clearing queue
|
||||||
|
self.current_notification.notif_box._should_cleanup_cache = True
|
||||||
|
|
||||||
# Cancel any pending transition timer
|
# Cancel any pending transition timer
|
||||||
if self._transition_timer_id:
|
if self._transition_timer_id:
|
||||||
GLib.source_remove(self._transition_timer_id)
|
GLib.source_remove(self._transition_timer_id)
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ from fabric.widgets.box import Box
|
|||||||
from fabric.widgets.button import Button
|
from fabric.widgets.button import Button
|
||||||
from fabric.widgets.label import Label
|
from fabric.widgets.label import Label
|
||||||
from fabric.widgets.scrolledwindow import ScrolledWindow
|
from fabric.widgets.scrolledwindow import ScrolledWindow
|
||||||
from modules.notification.notification import NotificationWidget
|
from modules.notification.notification import NotificationWidget, cleanup_notification_image_cache, cleanup_all_notification_caches, cleanup_notification_specific_caches
|
||||||
from services.modus import notification_service
|
from services.modus import notification_service
|
||||||
from widgets.wayland import WaylandWindow as Window
|
from widgets.wayland import WaylandWindow as Window
|
||||||
from fabric.widgets.image import Image
|
from fabric.widgets.image import Image
|
||||||
@@ -18,21 +18,79 @@ class NotificationCenterWidget(NotificationWidget):
|
|||||||
notification._notification, timeout_ms=0, show_close_button=True, **kwargs
|
notification._notification, timeout_ms=0, show_close_button=True, **kwargs
|
||||||
)
|
)
|
||||||
|
|
||||||
def create_close_button(self):
|
def create_content(self, notification):
|
||||||
close_button = Button(
|
# Create our custom close button for notification center
|
||||||
|
from widgets.custom_image import CustomImage
|
||||||
|
|
||||||
|
self.close_button = Button(
|
||||||
name="notif-close-button",
|
name="notif-close-button",
|
||||||
child=Image(
|
image=CustomImage(icon_name="close-symbolic", icon_size=18),
|
||||||
icon_name="dialog-cancel-symbolic", name="notif-close-label", size=5
|
visible=True, # Always visible in notification center
|
||||||
),
|
|
||||||
on_clicked=self._on_close_clicked,
|
on_clicked=self._on_close_clicked,
|
||||||
)
|
)
|
||||||
close_button.connect(
|
self.close_button.connect(
|
||||||
"enter-notify-event", lambda *_: self.hover_button(close_button)
|
"enter-notify-event", lambda *_: self.hover_button(self.close_button)
|
||||||
)
|
)
|
||||||
close_button.connect(
|
self.close_button.connect(
|
||||||
"leave-notify-event", lambda *_: self.unhover_button(close_button)
|
"leave-notify-event", lambda *_: self.unhover_button(self.close_button)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create the content box manually with our custom close button
|
||||||
|
from fabric.widgets.box import Box
|
||||||
|
from fabric.widgets.label import Label
|
||||||
|
|
||||||
|
return Box(
|
||||||
|
name="notification-content",
|
||||||
|
spacing=8,
|
||||||
|
children=[
|
||||||
|
Box(
|
||||||
|
name="notification-image",
|
||||||
|
children=CustomImage(
|
||||||
|
pixbuf=self._get_notification_pixbuf(notification)
|
||||||
|
),
|
||||||
|
),
|
||||||
|
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(
|
||||||
|
markup=notification.body.replace("\n", " "),
|
||||||
|
h_align="start",
|
||||||
|
ellipsization="end",
|
||||||
|
)
|
||||||
|
if notification.body
|
||||||
|
else Label(
|
||||||
|
markup="",
|
||||||
|
h_align="start",
|
||||||
|
ellipsization="end",
|
||||||
|
)
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Box(h_expand=True),
|
||||||
|
Box(
|
||||||
|
orientation="v",
|
||||||
|
children=[
|
||||||
|
self.close_button, # Use our custom close button
|
||||||
|
Box(v_expand=True),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
)
|
)
|
||||||
return close_button
|
|
||||||
|
|
||||||
# Override to disable the action buttons
|
# Override to disable the action buttons
|
||||||
def create_action_buttons(self, notification):
|
def create_action_buttons(self, notification):
|
||||||
@@ -40,11 +98,16 @@ class NotificationCenterWidget(NotificationWidget):
|
|||||||
|
|
||||||
def _on_close_clicked(self, *args):
|
def _on_close_clicked(self, *args):
|
||||||
try:
|
try:
|
||||||
|
# Clean up this notification's cached icons AND images
|
||||||
|
cleanup_notification_specific_caches(
|
||||||
|
app_icon_source=getattr(self, 'app_icon_source', None),
|
||||||
|
notification_image_cache_key=getattr(self, 'notification_image_cache_key', None)
|
||||||
|
)
|
||||||
|
logger.debug(f"Cleaned up all caches for notification center: {self.notification_id}")
|
||||||
|
|
||||||
notification_service.remove_cached_notification(self.notification_id)
|
notification_service.remove_cached_notification(self.notification_id)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error removing notification {self.notification_id}: {e}")
|
logger.error(f"Error removing notification {self.notification_id}: {e}")
|
||||||
finally:
|
|
||||||
self.destroy()
|
|
||||||
|
|
||||||
# Override to disable timeout functionality
|
# Override to disable timeout functionality
|
||||||
def start_timeout(self):
|
def start_timeout(self):
|
||||||
@@ -162,25 +225,35 @@ class NotificationCenter(Window):
|
|||||||
|
|
||||||
def on_notification_removed(self, service, cached_notification):
|
def on_notification_removed(self, service, cached_notification):
|
||||||
try:
|
try:
|
||||||
# Find and remove the corresponding widget
|
# Find and remove the corresponding widget with cache cleanup
|
||||||
for child in self.notifications_box.get_children():
|
for child in self.notifications_box.get_children():
|
||||||
if (
|
if (
|
||||||
hasattr(child, "notification_id")
|
hasattr(child, "notification_id")
|
||||||
and child.notification_id == cached_notification.cache_id
|
and child.notification_id == cached_notification.cache_id
|
||||||
):
|
):
|
||||||
|
# Clean up the widget's cached icons AND images before destroying
|
||||||
|
cleanup_notification_specific_caches(
|
||||||
|
app_icon_source=getattr(child, 'app_icon_source', None),
|
||||||
|
notification_image_cache_key=getattr(child, 'notification_image_cache_key', None)
|
||||||
|
)
|
||||||
|
logger.debug(f"Cleaned up all caches for removed notification: {child.notification_id}")
|
||||||
|
|
||||||
child.destroy()
|
child.destroy()
|
||||||
break
|
break
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f"Removed notification widget for {cached_notification.app_name}"
|
f"Removed notification widget and all caches for {cached_notification.app_name}"
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error removing notification widget: {e}")
|
logger.error(f"Error removing notification widget: {e}")
|
||||||
|
|
||||||
def on_clear_all(self, service):
|
def on_clear_all(self, service):
|
||||||
try:
|
try:
|
||||||
|
# Clear all remaining cached notification images AND icons
|
||||||
|
# Individual notifications clean up their own images, this handles any remaining
|
||||||
|
cleanup_all_notification_caches()
|
||||||
for child in self.notifications_box.get_children():
|
for child in self.notifications_box.get_children():
|
||||||
child.destroy()
|
child.destroy()
|
||||||
logger.debug("Cleared all notification widgets")
|
logger.debug("Cleared all notification widgets and remaining cached images")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error clearing notification widgets: {e}")
|
logger.error(f"Error clearing notification widgets: {e}")
|
||||||
|
|
||||||
@@ -191,6 +264,9 @@ class NotificationCenter(Window):
|
|||||||
self.scrolled.set_visible(current_count > 0)
|
self.scrolled.set_visible(current_count > 0)
|
||||||
|
|
||||||
def clear_all_notifications(self, *_):
|
def clear_all_notifications(self, *_):
|
||||||
|
# Clear all remaining cached notification images AND icons when clear all is clicked
|
||||||
|
# Individual notifications clean up their own images, this handles any remaining
|
||||||
|
cleanup_all_notification_caches() # Clear ALL caches (icons + images)
|
||||||
notification_service.clear_all_cached_notifications()
|
notification_service.clear_all_cached_notifications()
|
||||||
if hasattr(self, "mousecapture"):
|
if hasattr(self, "mousecapture"):
|
||||||
self.mousecapture.hide_child_window()
|
self.mousecapture.hide_child_window()
|
||||||
|
|||||||
@@ -258,6 +258,9 @@ class CachedNotifications(Notifications):
|
|||||||
if handler_id:
|
if handler_id:
|
||||||
# Disconnect the signal handler
|
# Disconnect the signal handler
|
||||||
cached_notification.disconnect(handler_id)
|
cached_notification.disconnect(handler_id)
|
||||||
|
|
||||||
|
# Emit signal to notify UI that notification was removed
|
||||||
|
self.emit("cached-notification-removed", cached_notification)
|
||||||
|
|
||||||
def toggle_dnd(self):
|
def toggle_dnd(self):
|
||||||
self.dont_disturb = not self.dont_disturb
|
self.dont_disturb = not self.dont_disturb
|
||||||
Reference in new issue
Block a user