♻️ refactor(notification): improve notification caching and history management
This commit is contained in:
5 files changed
+312
-192
No files matched your search
+14
-35
@@ -1,37 +1,16 @@
|
||||
{
|
||||
"wallpapers_dir": "~/Pictures/Wallpapers/",
|
||||
"dock_position": "Bottom",
|
||||
"terminal_command": "kitty -e",
|
||||
"dock_enabled": true,
|
||||
"dock_auto_hide": true,
|
||||
"dock_always_occluded": false,
|
||||
"dock_icon_size": 52,
|
||||
"window_switcher_items_per_row": 10,
|
||||
"hide_special_workspace": true,
|
||||
"dock_hide_special_workspace_apps": true,
|
||||
"dock_preview_apps": false,
|
||||
"notification_timeout": "5s",
|
||||
"notification_ignored_apps": [
|
||||
"Hyprshot"
|
||||
],
|
||||
"dock_workspace_visible": true,
|
||||
"dock_metrics_visible": true,
|
||||
"dock_battery_visible": true,
|
||||
"dock_date_time_visible": true,
|
||||
"dock_controls_visible": true,
|
||||
"dock_indicators_visible": true,
|
||||
"dock_notifications_visible": true,
|
||||
"dock_tray_visible": true,
|
||||
"dock_applications_visible": true,
|
||||
"dock_language_visible": true,
|
||||
"metrics_disks": [
|
||||
"/"
|
||||
],
|
||||
"metrics_visible": {
|
||||
"cpu": true,
|
||||
"ram": true,
|
||||
"disk": true,
|
||||
"swap": true,
|
||||
"gpu": false
|
||||
}
|
||||
"wallpapers_dir": "~/Pictures/Wallpapers/",
|
||||
"dock_position": "Bottom",
|
||||
"terminal_command": "kitty -e",
|
||||
"dock_enabled": true,
|
||||
"dock_auto_hide": true,
|
||||
"dock_always_occluded": false,
|
||||
"dock_icon_size": 52,
|
||||
"window_switcher_items_per_row": 10,
|
||||
"hide_special_workspace": true,
|
||||
"dock_hide_special_workspace_apps": true,
|
||||
"dock_preview_apps": false,
|
||||
"notification_timeout": "5s",
|
||||
"notification_ignored_apps": ["Hyprshot"],
|
||||
"notification_limited_apps_history": ["Spotify"]
|
||||
}
|
||||
+9
-47
@@ -10,8 +10,6 @@ gi.require_version("Gtk", "3.0")
|
||||
APP_NAME = "modus"
|
||||
APP_NAME_CAP = "Modus"
|
||||
|
||||
ALLOWED_PLAYERS = ["vlc", "cmus", "firefox", "spotify", "chromium", "vivaldi", "brave"]
|
||||
|
||||
|
||||
def parse_timeout_string(timeout_str):
|
||||
"""
|
||||
@@ -90,30 +88,16 @@ if os.path.exists(CONFIG_FILE):
|
||||
DOCK_HIDE_SPECIAL_WORKSPACE_APPS = config.get(
|
||||
"dock_hide_special_workspace_apps", True
|
||||
)
|
||||
DOCK_PREVIEW_APPS = config.get("dock_preview_apps", False)
|
||||
|
||||
NOTIFICATION_TIMEOUT_STR = config.get("notification_timeout", "5s")
|
||||
NOTIFICATION_TIMEOUT = parse_timeout_string(NOTIFICATION_TIMEOUT_STR)
|
||||
NOTIFICATION_IGNORED_APPS = config.get("notification_ignored_apps", ["Hyprshot"])
|
||||
|
||||
DOCK_COMPONENTS_VISIBILITY = {
|
||||
"workspace": config.get("dock_workspace_visible", True),
|
||||
"metrics": config.get("dock_metrics_visible", True),
|
||||
"battery": config.get("dock_battery_visible", True),
|
||||
"date_time": config.get("dock_date_time_visible", True),
|
||||
"controls": config.get("dock_controls_visible", True),
|
||||
"indicators": config.get("dock_indicators_visible", True),
|
||||
"notifications": config.get("dock_notifications_visible", True),
|
||||
"systray": config.get("dock_tray_visible", True),
|
||||
"applications": config.get("dock_applications_visible", True),
|
||||
"language": config.get("dock_language_visible", True),
|
||||
}
|
||||
|
||||
METRICS_DISKS = config.get("metrics_disks", ["/"])
|
||||
METRICS_VISIBLE = config.get(
|
||||
"metrics_visible",
|
||||
{"cpu": True, "ram": True, "disk": True, "swap": True, "gpu": False},
|
||||
NOTIFICATION_IGNORED_APPS_HISTORY = config.get(
|
||||
"notification_ignored_apps_history", ["Hyprshot"]
|
||||
)
|
||||
NOTIFICATION_LIMITED_APPS_HISTORY = config.get(
|
||||
"notification_limited_apps_history", ["Spotify"]
|
||||
)
|
||||
|
||||
else:
|
||||
WALLPAPERS_DIR = WALLPAPERS_DIR_DEFAULT
|
||||
DOCK_POSITION = "Bottom"
|
||||
@@ -122,34 +106,12 @@ else:
|
||||
DOCK_AUTO_HIDE = True
|
||||
TERMINAL_COMMAND = "kitty -e"
|
||||
DOCK_THEME = "Pills"
|
||||
DOCK_ICON_SIZE = 52
|
||||
DOCK_ICON_SIZE = 60
|
||||
WINDOW_SWITCHER_ITEMS_PER_ROW = 10
|
||||
HIDE_SPECIAL_WORKSPACE = True
|
||||
DOCK_HIDE_SPECIAL_WORKSPACE_APPS = True
|
||||
DOCK_PREVIEW_APPS = False
|
||||
|
||||
NOTIFICATION_TIMEOUT_STR = "5s"
|
||||
NOTIFICATION_TIMEOUT = parse_timeout_string(NOTIFICATION_TIMEOUT_STR)
|
||||
NOTIFICATION_IGNORED_APPS = ["Hyprshot"]
|
||||
|
||||
DOCK_COMPONENTS_VISIBILITY = {
|
||||
"workspace": True,
|
||||
"metrics": True,
|
||||
"battery": True,
|
||||
"date_time": True,
|
||||
"controls": True,
|
||||
"indicators": True,
|
||||
"notifications": True,
|
||||
"systray": True,
|
||||
"applications": True,
|
||||
"language": True,
|
||||
}
|
||||
|
||||
METRICS_DISKS = ["/"]
|
||||
METRICS_VISIBLE = {
|
||||
"cpu": True,
|
||||
"ram": True,
|
||||
"disk": True,
|
||||
"swap": True,
|
||||
"gpu": False,
|
||||
}
|
||||
NOTIFICATION_IGNORED_APPS_HISTORY = ["Hyprshot"]
|
||||
NOTIFICATION_LIMITED_APPS_HISTORY = ["Spotify"]
|
||||
@@ -240,22 +240,35 @@ def find_existing_cached_image(notification_id):
|
||||
"""Find existing cached image for a notification ID, regardless of hash - prioritizes restart scenarios"""
|
||||
try:
|
||||
if not os.path.exists(NOTIFICATION_IMAGE_CACHE_DIR):
|
||||
logger.debug(f"Notification image cache directory does not exist: {NOTIFICATION_IMAGE_CACHE_DIR}")
|
||||
logger.debug(
|
||||
f"Notification image cache directory does not exist: {
|
||||
NOTIFICATION_IMAGE_CACHE_DIR
|
||||
}"
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
# Look for any cached image that starts with this notification ID
|
||||
matching_files = []
|
||||
for filename in os.listdir(NOTIFICATION_IMAGE_CACHE_DIR):
|
||||
if filename.startswith(f"notif_{notification_id}_") and filename.endswith(".png"):
|
||||
if filename.startswith(f"notif_{notification_id}_") and filename.endswith(
|
||||
".png"
|
||||
):
|
||||
matching_files.append(filename)
|
||||
|
||||
|
||||
if not matching_files:
|
||||
logger.debug(f"No cached images found for notification ID: {notification_id}")
|
||||
logger.debug(
|
||||
f"No cached images found for notification ID: {notification_id}"
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
# Sort by modification time to get the most recent cache file
|
||||
matching_files.sort(key=lambda f: os.path.getmtime(os.path.join(NOTIFICATION_IMAGE_CACHE_DIR, f)), reverse=True)
|
||||
|
||||
matching_files.sort(
|
||||
key=lambda f: os.path.getmtime(
|
||||
os.path.join(NOTIFICATION_IMAGE_CACHE_DIR, f)
|
||||
),
|
||||
reverse=True,
|
||||
)
|
||||
|
||||
# Try to load the most recent cached image
|
||||
for filename in matching_files:
|
||||
cache_path = os.path.join(NOTIFICATION_IMAGE_CACHE_DIR, filename)
|
||||
@@ -263,16 +276,28 @@ def find_existing_cached_image(notification_id):
|
||||
cached_pixbuf = GdkPixbuf.Pixbuf.new_from_file(cache_path)
|
||||
if cached_pixbuf:
|
||||
cache_key = filename[:-4] # Remove .png extension
|
||||
logger.debug(f"Found existing cached image for notification {notification_id}: {cache_key}")
|
||||
logger.debug(
|
||||
f"Found existing cached image for notification {
|
||||
notification_id
|
||||
}: {cache_key}"
|
||||
)
|
||||
return cached_pixbuf
|
||||
except Exception as e:
|
||||
logger.debug(f"Failed to load cached image {filename}: {e}")
|
||||
continue
|
||||
|
||||
logger.debug(f"Found {len(matching_files)} cache files for notification {notification_id} but none could be loaded")
|
||||
|
||||
logger.debug(
|
||||
f"Found {len(matching_files)} cache files for notification {
|
||||
notification_id
|
||||
} but none could be loaded"
|
||||
)
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.debug(f"Failed to find existing cached image for notification {notification_id}: {e}")
|
||||
logger.debug(
|
||||
f"Failed to find existing cached image for notification {notification_id}: {
|
||||
e
|
||||
}"
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
@@ -286,12 +311,18 @@ def get_notification_image_cache_key(notification_id, image_pixbuf):
|
||||
image_hash = hashlib.md5(pixel_data).hexdigest()[:8]
|
||||
return f"notif_{notification_id}_{image_hash}"
|
||||
except Exception as pixel_error:
|
||||
logger.debug(f"Failed to get pixel data for cache key (temp file likely gone): {pixel_error}")
|
||||
logger.debug(
|
||||
f"Failed to get pixel data for cache key (temp file likely gone): {
|
||||
pixel_error
|
||||
}"
|
||||
)
|
||||
# Fallback to notification ID + dimensions if pixel data fails
|
||||
try:
|
||||
width = image_pixbuf.get_width()
|
||||
height = image_pixbuf.get_height()
|
||||
dimension_hash = hashlib.md5(f"{width}x{height}".encode()).hexdigest()[:8]
|
||||
dimension_hash = hashlib.md5(
|
||||
f"{width}x{height}".encode()
|
||||
).hexdigest()[:8]
|
||||
return f"notif_{notification_id}_{dimension_hash}"
|
||||
except Exception:
|
||||
# Ultimate fallback using just notification ID and timestamp
|
||||
@@ -327,26 +358,30 @@ def cache_notification_image(notification_id, image_pixbuf, size=(64, 64)):
|
||||
return cache_path, cache_key
|
||||
except Exception as scale_error:
|
||||
# Handle cases where pixbuf is corrupted or references invalid files
|
||||
logger.debug(f"Failed to scale/save pixbuf (likely temp file gone): {scale_error}")
|
||||
|
||||
logger.debug(
|
||||
f"Failed to scale/save pixbuf (likely temp file gone): {scale_error}"
|
||||
)
|
||||
|
||||
# Try to get pixel data directly to create a valid cache key even if file is gone
|
||||
try:
|
||||
# If we can't scale but pixbuf exists, create a fallback cache entry
|
||||
fallback_cache_key = f"fallback_{notification_id}_{int(time.time())}"
|
||||
fallback_cache_path = os.path.join(NOTIFICATION_IMAGE_CACHE_DIR, f"{fallback_cache_key}.png")
|
||||
|
||||
fallback_cache_path = os.path.join(
|
||||
NOTIFICATION_IMAGE_CACHE_DIR, f"{fallback_cache_key}.png"
|
||||
)
|
||||
|
||||
# Create a fallback image based on notification ID
|
||||
fallback_pixbuf = get_fallback_notification_icon(size)
|
||||
if fallback_pixbuf:
|
||||
fallback_pixbuf.savev(fallback_cache_path, "png", [], [])
|
||||
logger.debug(f"Created fallback cached image: {fallback_cache_key}")
|
||||
return fallback_cache_path, fallback_cache_key
|
||||
|
||||
|
||||
except Exception as fallback_error:
|
||||
logger.debug(f"Fallback cache creation failed: {fallback_error}")
|
||||
|
||||
|
||||
return None, None
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to cache notification image: {e}")
|
||||
return None, None
|
||||
@@ -376,19 +411,33 @@ def cleanup_notification_image_cache(cache_key=None):
|
||||
notification_id_pattern = cache_key[:-2] # Remove "_*"
|
||||
cleaned_count = 0
|
||||
for filename in os.listdir(NOTIFICATION_IMAGE_CACHE_DIR):
|
||||
if filename.startswith(notification_id_pattern) and filename.endswith(".png"):
|
||||
if filename.startswith(
|
||||
notification_id_pattern
|
||||
) and 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}")
|
||||
logger.debug(
|
||||
f"Cleaned up cached notification image: {filename}"
|
||||
)
|
||||
cleaned_count += 1
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to cleanup cache file {filename}: {e}")
|
||||
logger.warning(
|
||||
f"Failed to cleanup cache file {filename}: {e}"
|
||||
)
|
||||
if cleaned_count > 0:
|
||||
logger.info(f"Cleaned up {cleaned_count} cached images for notification ID pattern: {notification_id_pattern}")
|
||||
logger.info(
|
||||
f"Cleaned up {
|
||||
cleaned_count
|
||||
} cached images for notification ID pattern: {
|
||||
notification_id_pattern
|
||||
}"
|
||||
)
|
||||
else:
|
||||
# Remove specific cached image
|
||||
cache_path = os.path.join(NOTIFICATION_IMAGE_CACHE_DIR, f"{cache_key}.png")
|
||||
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}")
|
||||
@@ -468,15 +517,25 @@ def verify_cache_persistence():
|
||||
try:
|
||||
icon_cache_files = []
|
||||
image_cache_files = []
|
||||
|
||||
|
||||
if os.path.exists(NOTIFICATION_ICON_CACHE_DIR):
|
||||
icon_cache_files = [f for f in os.listdir(NOTIFICATION_ICON_CACHE_DIR) if f.endswith('.png')]
|
||||
|
||||
icon_cache_files = [
|
||||
f for f in os.listdir(NOTIFICATION_ICON_CACHE_DIR) if f.endswith(".png")
|
||||
]
|
||||
|
||||
if os.path.exists(NOTIFICATION_IMAGE_CACHE_DIR):
|
||||
image_cache_files = [f for f in os.listdir(NOTIFICATION_IMAGE_CACHE_DIR) if f.endswith('.png')]
|
||||
|
||||
logger.info(f"Cache persistence check: {len(icon_cache_files)} icons, {len(image_cache_files)} images cached")
|
||||
|
||||
image_cache_files = [
|
||||
f
|
||||
for f in os.listdir(NOTIFICATION_IMAGE_CACHE_DIR)
|
||||
if f.endswith(".png")
|
||||
]
|
||||
|
||||
logger.info(
|
||||
f"Cache persistence check: {len(icon_cache_files)} icons, {
|
||||
len(image_cache_files)
|
||||
} images cached"
|
||||
)
|
||||
|
||||
# Test loading a few cached items to verify they work
|
||||
for cache_file in icon_cache_files[:2]: # Test first 2 icon files
|
||||
try:
|
||||
@@ -486,7 +545,7 @@ def verify_cache_persistence():
|
||||
logger.debug(f"Successfully verified cached icon: {cache_file}")
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to load cached icon {cache_file}: {e}")
|
||||
|
||||
|
||||
for cache_file in image_cache_files[:2]: # Test first 2 image files
|
||||
try:
|
||||
cache_path = os.path.join(NOTIFICATION_IMAGE_CACHE_DIR, cache_file)
|
||||
@@ -495,9 +554,9 @@ def verify_cache_persistence():
|
||||
logger.debug(f"Successfully verified cached image: {cache_file}")
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to load cached image {cache_file}: {e}")
|
||||
|
||||
|
||||
return len(icon_cache_files) + len(image_cache_files) > 0
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to verify cache persistence: {e}")
|
||||
return False
|
||||
@@ -507,11 +566,11 @@ def migrate_persistent_notifications():
|
||||
"""Migrate persistent notifications to use cached assets when temp files are gone"""
|
||||
try:
|
||||
from services.modus import notification_service
|
||||
|
||||
|
||||
migrated_count = 0
|
||||
for cached_notification in notification_service.cached_notifications:
|
||||
notification = cached_notification._notification
|
||||
|
||||
|
||||
# Check if notification has image_pixbuf but temp file might be gone
|
||||
if hasattr(notification, "image_pixbuf") and notification.image_pixbuf:
|
||||
try:
|
||||
@@ -523,13 +582,23 @@ def migrate_persistent_notifications():
|
||||
if hasattr(notification, "app_icon") and notification.app_icon:
|
||||
cache_notification_icon(notification.app_icon, (35, 35))
|
||||
migrated_count += 1
|
||||
logger.debug(f"Migrated notification for {notification.app_name} to use cached app icon")
|
||||
logger.debug(
|
||||
f"Migrated notification for {
|
||||
notification.app_name
|
||||
} to use cached app icon"
|
||||
)
|
||||
except Exception as cache_error:
|
||||
logger.debug(f"Failed to cache app icon for {notification.app_name}: {cache_error}")
|
||||
|
||||
logger.debug(
|
||||
f"Failed to cache app icon for {notification.app_name}: {
|
||||
cache_error
|
||||
}"
|
||||
)
|
||||
|
||||
if migrated_count > 0:
|
||||
logger.info(f"Migrated {migrated_count} persistent notifications to use cached assets")
|
||||
|
||||
logger.info(
|
||||
f"Migrated {migrated_count} persistent notifications to use cached assets"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to migrate persistent notifications: {e}")
|
||||
|
||||
@@ -557,22 +626,38 @@ def preload_notification_assets(notification):
|
||||
cache_notification_icon(notification.app_icon, (35, 35))
|
||||
logger.debug(f"Preloaded app icon for: {notification.app_name}")
|
||||
except Exception as icon_error:
|
||||
logger.debug(f"Failed to preload app icon for {notification.app_name}: {icon_error}")
|
||||
logger.debug(
|
||||
f"Failed to preload app icon for {notification.app_name}: {
|
||||
icon_error
|
||||
}"
|
||||
)
|
||||
|
||||
# Preload notification image if present (with resilient handling)
|
||||
if hasattr(notification, "image_pixbuf") and notification.image_pixbuf:
|
||||
try:
|
||||
notification_id = getattr(notification, "id", int(time.time()))
|
||||
|
||||
|
||||
# Try to cache the image, but don't fail if temp files are gone
|
||||
cache_path, cache_key = cache_notification_image(notification_id, notification.image_pixbuf, (35, 35))
|
||||
cache_path, cache_key = cache_notification_image(
|
||||
notification_id, notification.image_pixbuf, (35, 35)
|
||||
)
|
||||
if cache_path and cache_key:
|
||||
logger.debug(f"Preloaded notification image for: {notification.app_name}")
|
||||
logger.debug(
|
||||
f"Preloaded notification image for: {notification.app_name}"
|
||||
)
|
||||
else:
|
||||
logger.debug(f"Skipped preloading notification image for {notification.app_name} (temp file likely gone)")
|
||||
|
||||
logger.debug(
|
||||
f"Skipped preloading notification image for {
|
||||
notification.app_name
|
||||
} (temp file likely gone)"
|
||||
)
|
||||
|
||||
except Exception as image_error:
|
||||
logger.debug(f"Failed to preload notification image for {notification.app_name}: {image_error}")
|
||||
logger.debug(
|
||||
f"Failed to preload notification image for {
|
||||
notification.app_name
|
||||
}: {image_error}"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to preload notification assets: {e}")
|
||||
@@ -598,7 +683,9 @@ class ActionButton(Button):
|
||||
style_class = (
|
||||
"start-action"
|
||||
if index == 0
|
||||
else "end-action" if index == total - 1 else "middle-action"
|
||||
else "end-action"
|
||||
if index == total - 1
|
||||
else "middle-action"
|
||||
)
|
||||
self.add_style_class(style_class)
|
||||
self.connect(
|
||||
@@ -659,8 +746,10 @@ class NotificationWidget(Box):
|
||||
"""Create notification header with optimized cached app icon"""
|
||||
try:
|
||||
# Use optimized caching for header app icon (24x24 for header display)
|
||||
cached_app_icon_pixbuf = cache_notification_icon(notification.app_icon, (24, 24))
|
||||
|
||||
cached_app_icon_pixbuf = cache_notification_icon(
|
||||
notification.app_icon, (24, 24)
|
||||
)
|
||||
|
||||
if cached_app_icon_pixbuf:
|
||||
# Create image from cached pixbuf for consistent display
|
||||
app_icon = CustomImage(pixbuf=cached_app_icon_pixbuf)
|
||||
@@ -810,26 +899,34 @@ class NotificationWidget(Box):
|
||||
def _get_notification_pixbuf(self, notification):
|
||||
"""Optimized notification pixbuf with cached-first approach for restart scenarios"""
|
||||
notification_id = getattr(notification, "id", int(time.time()))
|
||||
|
||||
|
||||
# Priority 1: ALWAYS check for existing cached image first (especially important for restarts)
|
||||
if notification_id:
|
||||
existing_cached_image = find_existing_cached_image(notification_id)
|
||||
if existing_cached_image:
|
||||
# Store the cache key for cleanup when manually closed
|
||||
self.notification_image_cache_key = f"notif_{notification_id}_*" # Wildcard for cleanup
|
||||
logger.debug(f"Using existing cached image for notification: {notification_id}, cache_key: {self.notification_image_cache_key}")
|
||||
self.notification_image_cache_key = (
|
||||
f"notif_{notification_id}_*" # Wildcard for cleanup
|
||||
)
|
||||
logger.debug(
|
||||
f"Using existing cached image for notification: {
|
||||
notification_id
|
||||
}, cache_key: {self.notification_image_cache_key}"
|
||||
)
|
||||
return existing_cached_image
|
||||
|
||||
|
||||
try:
|
||||
# Priority 2: Try to process current pixbuf only if we have no cached version
|
||||
if hasattr(notification, "image_pixbuf") and notification.image_pixbuf:
|
||||
try:
|
||||
cache_key = get_notification_image_cache_key(notification_id, notification.image_pixbuf)
|
||||
cache_key = get_notification_image_cache_key(
|
||||
notification_id, notification.image_pixbuf
|
||||
)
|
||||
cached_image = get_cached_notification_image(cache_key)
|
||||
if cached_image:
|
||||
logger.debug(f"Using cached notification image: {cache_key}")
|
||||
return cached_image
|
||||
|
||||
|
||||
# Try to cache the image now
|
||||
cache_path, cache_key = cache_notification_image(
|
||||
notification_id, notification.image_pixbuf, (35, 35)
|
||||
@@ -841,7 +938,9 @@ class NotificationWidget(Box):
|
||||
# Load and return the newly cached image
|
||||
cached_pixbuf = get_cached_notification_image(cache_key)
|
||||
if cached_pixbuf:
|
||||
logger.debug(f"Cached and using notification image: {cache_key}")
|
||||
logger.debug(
|
||||
f"Cached and using notification image: {cache_key}"
|
||||
)
|
||||
return cached_pixbuf
|
||||
|
||||
# Direct fallback if caching succeeded but loading failed
|
||||
@@ -850,9 +949,11 @@ class NotificationWidget(Box):
|
||||
35, 35, GdkPixbuf.InterpType.BILINEAR
|
||||
)
|
||||
except Exception as image_error:
|
||||
logger.debug(f"Notification image processing failed (temp file likely gone): {image_error}")
|
||||
logger.debug(
|
||||
f"Notification image processing failed (temp file likely gone): {image_error}"
|
||||
)
|
||||
# Continue to app icon fallback
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.debug(f"Failed to process notification image: {e}")
|
||||
|
||||
@@ -908,7 +1009,11 @@ class NotificationWidget(Box):
|
||||
def _manual_close(self):
|
||||
"""Handle manual close button click - mark for cache cleanup"""
|
||||
self._should_cleanup_cache = True
|
||||
logger.debug(f"Manual close triggered for notification with cache_key: {getattr(self, 'notification_image_cache_key', 'None')} and app_icon: {getattr(self, 'app_icon_source', 'None')}")
|
||||
logger.debug(
|
||||
f"Manual close triggered for notification with cache_key: {
|
||||
getattr(self, 'notification_image_cache_key', 'None')
|
||||
} and app_icon: {getattr(self, 'app_icon_source', 'None')}"
|
||||
)
|
||||
self.notification.close("dismissed-by-user")
|
||||
|
||||
def destroy(self):
|
||||
@@ -1171,9 +1276,6 @@ class ModusNoti(Window):
|
||||
self.TRANSITION_DELAY = 100 # Smoother transition timing
|
||||
self.DEBOUNCE_DELAY = 50 # Prevent rapid fire notifications
|
||||
|
||||
# Initialize ignored apps list from config
|
||||
self.ignored_apps = data.NOTIFICATION_IGNORED_APPS
|
||||
|
||||
self._server.connect("notification-added", self.on_new_notification)
|
||||
super().__init__(
|
||||
anchor="top right",
|
||||
@@ -1188,10 +1290,6 @@ class ModusNoti(Window):
|
||||
def on_new_notification(self, fabric_notif, id):
|
||||
notification: Notification = fabric_notif.get_notification_from_id(id)
|
||||
|
||||
# Check if the notification should be ignored completely (ignored apps)
|
||||
if notification.app_name in self.ignored_apps:
|
||||
return
|
||||
|
||||
if self._server.dont_disturb or modus_service.dont_disturb:
|
||||
# Notification is already cached by the service, just don't show popup
|
||||
return
|
||||
|
||||
@@ -26,6 +26,7 @@ from modules.notification.notification import (
|
||||
from services.modus import notification_service
|
||||
from widgets.custom_image import CustomImage
|
||||
from widgets.wayland import WaylandWindow as Window
|
||||
from config import data
|
||||
|
||||
|
||||
class ExpandableNotificationGroup(Box):
|
||||
@@ -310,24 +311,36 @@ class ExpandableNotificationGroup(Box):
|
||||
def _get_notification_pixbuf(self, notification):
|
||||
"""Use cached-first approach prioritizing existing cache over pixbuf processing"""
|
||||
notification_id = getattr(notification, "id", None)
|
||||
|
||||
|
||||
# Priority 1: ALWAYS check for existing cached image first (critical for restart scenarios)
|
||||
if notification_id:
|
||||
existing_cached_image = find_existing_cached_image(notification_id)
|
||||
if existing_cached_image:
|
||||
logger.debug(f"Using existing cached image in center for notification: {notification_id}")
|
||||
logger.debug(
|
||||
f"Using existing cached image in center for notification: {
|
||||
notification_id
|
||||
}"
|
||||
)
|
||||
return existing_cached_image
|
||||
|
||||
|
||||
try:
|
||||
# Priority 2: Try to process current pixbuf only if we have no cached version
|
||||
if hasattr(notification, "image_pixbuf") and notification.image_pixbuf and notification_id:
|
||||
if (
|
||||
hasattr(notification, "image_pixbuf")
|
||||
and notification.image_pixbuf
|
||||
and notification_id
|
||||
):
|
||||
try:
|
||||
cache_key = get_notification_image_cache_key(notification_id, notification.image_pixbuf)
|
||||
cache_key = get_notification_image_cache_key(
|
||||
notification_id, notification.image_pixbuf
|
||||
)
|
||||
cached_image = get_cached_notification_image(cache_key)
|
||||
if cached_image:
|
||||
logger.debug(f"Using cached notification image in center: {cache_key}")
|
||||
logger.debug(
|
||||
f"Using cached notification image in center: {cache_key}"
|
||||
)
|
||||
return cached_image
|
||||
|
||||
|
||||
# Try to cache the image now
|
||||
cache_path, cache_key = cache_notification_image(
|
||||
notification_id, notification.image_pixbuf, (35, 35)
|
||||
@@ -335,17 +348,26 @@ class ExpandableNotificationGroup(Box):
|
||||
if cache_path and cache_key:
|
||||
cached_image = get_cached_notification_image(cache_key)
|
||||
if cached_image:
|
||||
logger.debug(f"Cached and using notification image in center: {cache_key}")
|
||||
logger.debug(
|
||||
f"Cached and using notification image in center: {cache_key}"
|
||||
)
|
||||
return cached_image
|
||||
|
||||
# Direct scaling fallback if caching works but loading fails
|
||||
logger.debug("Using direct scaling for notification image in center")
|
||||
return notification.image_pixbuf.scale_simple(35, 35, 2) # GdkPixbuf.InterpType.BILINEAR = 2
|
||||
|
||||
logger.debug(
|
||||
"Using direct scaling for notification image in center"
|
||||
)
|
||||
# GdkPixbuf.InterpType.BILINEAR = 2
|
||||
return notification.image_pixbuf.scale_simple(35, 35, 2)
|
||||
|
||||
except Exception as image_error:
|
||||
logger.debug(f"Notification image processing failed in center (temp file likely gone): {image_error}")
|
||||
logger.debug(
|
||||
f"Notification image processing failed in center (temp file likely gone): {
|
||||
image_error
|
||||
}"
|
||||
)
|
||||
# Continue to app icon fallback
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.debug(f"Failed to process notification image in center: {e}")
|
||||
|
||||
@@ -356,7 +378,9 @@ class ExpandableNotificationGroup(Box):
|
||||
if app_icon_source:
|
||||
cached_app_icon = cache_notification_icon(app_icon_source, (35, 35))
|
||||
if cached_app_icon:
|
||||
logger.debug(f"Using cached app icon in center for: {app_icon_source}")
|
||||
logger.debug(
|
||||
f"Using cached app icon in center for: {app_icon_source}"
|
||||
)
|
||||
return cached_app_icon
|
||||
except Exception as e:
|
||||
logger.debug(f"Failed to get cached app icon in center: {e}")
|
||||
@@ -425,15 +449,17 @@ class ExpandableNotificationGroup(Box):
|
||||
# Get notification ID for cache cleanup
|
||||
notification_id = getattr(notification._notification, "id", None)
|
||||
app_icon_source = getattr(notification._notification, "app_icon", None)
|
||||
|
||||
|
||||
# Clean up caches using notification ID pattern
|
||||
if notification_id:
|
||||
cleanup_notification_specific_caches(
|
||||
app_icon_source=app_icon_source,
|
||||
notification_image_cache_key=f"notif_{notification_id}_*"
|
||||
notification_image_cache_key=f"notif_{notification_id}_*",
|
||||
)
|
||||
logger.debug(f"Cleaned up caches for notification ID: {notification_id}")
|
||||
|
||||
logger.debug(
|
||||
f"Cleaned up caches for notification ID: {notification_id}"
|
||||
)
|
||||
|
||||
notification_service.remove_cached_notification(notification.cache_id)
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
@@ -446,15 +472,17 @@ class ExpandableNotificationGroup(Box):
|
||||
# Get notification ID for cache cleanup
|
||||
notification_id = getattr(notification._notification, "id", None)
|
||||
app_icon_source = getattr(notification._notification, "app_icon", None)
|
||||
|
||||
|
||||
# Clean up caches using notification ID pattern
|
||||
if notification_id:
|
||||
cleanup_notification_specific_caches(
|
||||
app_icon_source=app_icon_source,
|
||||
notification_image_cache_key=f"notif_{notification_id}_*"
|
||||
notification_image_cache_key=f"notif_{notification_id}_*",
|
||||
)
|
||||
logger.debug(f"Cleaned up caches for notification ID: {notification_id}")
|
||||
|
||||
logger.debug(
|
||||
f"Cleaned up caches for notification ID: {notification_id}"
|
||||
)
|
||||
|
||||
notification_service.remove_cached_notification(notification.cache_id)
|
||||
logger.debug(f"Closed single notification: {notification.cache_id}")
|
||||
except Exception as e:
|
||||
@@ -583,15 +611,17 @@ class NotificationCenterWidget(NotificationWidget):
|
||||
# Get notification ID for cache cleanup
|
||||
notification_id = getattr(self.notification, "id", None)
|
||||
app_icon_source = getattr(self.notification, "app_icon", None)
|
||||
|
||||
|
||||
# Clean up caches using notification ID pattern
|
||||
if notification_id:
|
||||
cleanup_notification_specific_caches(
|
||||
app_icon_source=app_icon_source,
|
||||
notification_image_cache_key=f"notif_{notification_id}_*"
|
||||
notification_image_cache_key=f"notif_{notification_id}_*",
|
||||
)
|
||||
logger.debug(f"Cleaned up caches for notification center ID: {notification_id}")
|
||||
|
||||
logger.debug(
|
||||
f"Cleaned up caches for notification center ID: {notification_id}"
|
||||
)
|
||||
|
||||
notification_service.remove_cached_notification(self.notification_id)
|
||||
except Exception as e:
|
||||
logger.error(f"Error removing notification {self.notification_id}: {e}")
|
||||
@@ -696,31 +726,53 @@ class NotificationCenter(Window):
|
||||
for cached_notification in notification_service.cached_notifications:
|
||||
app_name = cached_notification._notification.app_name
|
||||
notification_id = getattr(cached_notification._notification, "id", None)
|
||||
|
||||
logger.debug(f"Rebuilding notification for {app_name} with ID: {notification_id}")
|
||||
|
||||
|
||||
# Skip ignored apps during rebuild
|
||||
if app_name in data.NOTIFICATION_IGNORED_APPS_HISTORY:
|
||||
continue
|
||||
|
||||
# Preload assets for each cached notification to ensure display consistency
|
||||
preload_notification_assets(cached_notification._notification)
|
||||
|
||||
|
||||
# Check if we can find cached images for this notification
|
||||
if notification_id:
|
||||
existing_image = find_existing_cached_image(notification_id)
|
||||
if existing_image:
|
||||
logger.debug(f"✓ Found cached image for notification {notification_id} during rebuild")
|
||||
logger.debug(
|
||||
f"✓ Found cached image for notification {
|
||||
notification_id
|
||||
} during rebuild"
|
||||
)
|
||||
else:
|
||||
logger.debug(f"✗ No cached image found for notification {notification_id} during rebuild")
|
||||
|
||||
logger.debug(
|
||||
f"✗ No cached image found for notification {
|
||||
notification_id
|
||||
} during rebuild"
|
||||
)
|
||||
|
||||
self.notification_groups[app_name].append(cached_notification)
|
||||
rebuild_count += 1
|
||||
|
||||
logger.info(f"Rebuilt {rebuild_count} notifications into {len(self.notification_groups)} groups")
|
||||
logger.info(
|
||||
f"Rebuilt {rebuild_count} notifications into {
|
||||
len(self.notification_groups)
|
||||
} groups"
|
||||
)
|
||||
|
||||
# Create group widgets
|
||||
# Create group widgets and handle limited apps
|
||||
for app_name, notifications in self.notification_groups.items():
|
||||
# Sort notifications by timestamp (newest first)
|
||||
notifications.sort(key=lambda n: getattr(n, "timestamp", 0), reverse=True)
|
||||
|
||||
group_widget = ExpandableNotificationGroup(app_name, notifications)
|
||||
# Handle limited apps history - only keep 5 notifications during rebuild
|
||||
if app_name in data.NOTIFICATION_LIMITED_APPS_HISTORY:
|
||||
if len(notifications) > 5:
|
||||
# Keep only the 5 most recent notifications
|
||||
self.notification_groups[app_name] = notifications[:5]
|
||||
|
||||
group_widget = ExpandableNotificationGroup(
|
||||
app_name, self.notification_groups[app_name]
|
||||
)
|
||||
self.group_widgets[app_name] = group_widget
|
||||
self.notifications_box.add(group_widget)
|
||||
|
||||
@@ -728,6 +780,10 @@ class NotificationCenter(Window):
|
||||
try:
|
||||
app_name = cached_notification._notification.app_name
|
||||
|
||||
# Check if this app should be ignored for history (don't add to notification center)
|
||||
if app_name in data.NOTIFICATION_IGNORED_APPS_HISTORY:
|
||||
return
|
||||
|
||||
# Preload assets for notification center display (ensure caching consistency)
|
||||
preload_notification_assets(cached_notification._notification)
|
||||
|
||||
@@ -736,6 +792,23 @@ class NotificationCenter(Window):
|
||||
0, cached_notification
|
||||
) # Insert at beginning (newest first)
|
||||
|
||||
# Handle limited apps history - only keep 5 notifications
|
||||
if app_name in data.NOTIFICATION_LIMITED_APPS_HISTORY:
|
||||
notifications_for_app = self.notification_groups[app_name]
|
||||
if len(notifications_for_app) > 5:
|
||||
# Remove oldest notifications beyond the limit
|
||||
excess_notifications = notifications_for_app[5:]
|
||||
self.notification_groups[app_name] = notifications_for_app[:5]
|
||||
|
||||
# Remove excess notifications from the service cache
|
||||
for excess_notification in excess_notifications:
|
||||
try:
|
||||
notification_service.remove_cached_notification(
|
||||
excess_notification.cache_id
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error removing excess notification: {e}")
|
||||
|
||||
# Update or create group widget
|
||||
if app_name in self.group_widgets:
|
||||
# Update existing group
|
||||
@@ -826,7 +899,7 @@ class NotificationCenter(Window):
|
||||
# No notifications label removed - only update clear button and scrolled visibility
|
||||
self.clear_all_button.set_visible(current_count > 0)
|
||||
self.scrolled.set_visible(current_count > 0)
|
||||
|
||||
|
||||
# Auto-close notification center when no notifications remain
|
||||
if current_count == 0 and hasattr(self, "mousecapture"):
|
||||
self.mousecapture.hide_child_window()
|
||||
|
||||
@@ -3,7 +3,7 @@ import json
|
||||
import os
|
||||
from typing import List
|
||||
|
||||
# Fabric imports
|
||||
# Fabric imports
|
||||
import gi
|
||||
from gi.repository import GdkPixbuf
|
||||
|
||||
@@ -224,7 +224,15 @@ class CachedNotifications(Notifications):
|
||||
notification = self.get_notification_from_id(notification_id)
|
||||
|
||||
if notification:
|
||||
# Always cache notifications, regardless of DND status
|
||||
# Import here to avoid circular imports
|
||||
from config import data
|
||||
|
||||
# Check if this app should be ignored for history (don't cache)
|
||||
if notification.app_name in data.NOTIFICATION_IGNORED_APPS_HISTORY:
|
||||
# Don't cache notifications from ignored apps, but still allow popup display
|
||||
return
|
||||
|
||||
# Cache notifications (except for ignored apps)
|
||||
# DND only affects popup display, not caching
|
||||
self._count += 1
|
||||
cache_id = self._count
|
||||
@@ -258,7 +266,7 @@ class CachedNotifications(Notifications):
|
||||
if handler_id:
|
||||
# Disconnect the signal handler
|
||||
cached_notification.disconnect(handler_id)
|
||||
|
||||
|
||||
# Emit signal to notify UI that notification was removed
|
||||
self.emit("cached-notification-removed", cached_notification)
|
||||
|
||||
|
||||
Reference in new issue
Block a user