Apple Like Notificiation Centre

This commit is contained in:
tr1x_em committed 2025-08-09 19:57:24 +05:30
1 parent 91ed27a04b
commit 0f811def01
3 files changed
+639 -102

No files matched your search

+1 -1
View File
@@ -67,12 +67,12 @@ class ModusControlCenter(Window):
self.add_keybinding("Escape", self.hide_controlcenter)
self.network_service.connect("wifi-device-added", self.on_network_ready)
volume = 100
wlan = modus_service.sc("wlan-changed", self.wlan_changed)
bluetooth = modus_service.sc("bluetooth-changed", self.bluetooth_changed)
music = modus_service.sc("music-changed", self.audio_changed)
self.network_service.connect("wifi-device-added", self.on_network_ready)
# Store signal connections for cleanup
self._signal_connections.extend(
[
+417 -80
View File
@@ -1,13 +1,282 @@
from fabric.widgets.centerbox import CenterBox
from loguru import logger
from collections import defaultdict
from fabric.widgets.box import Box
from fabric.widgets.button import Button
from fabric.widgets.label import Label
from fabric.widgets.scrolledwindow import ScrolledWindow
from modules.notification.notification import NotificationWidget, cleanup_notification_image_cache, cleanup_all_notification_caches, cleanup_notification_specific_caches
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 widgets.wayland import WaylandWindow as Window
from fabric.widgets.image import Image
from fabric.widgets.eventbox import EventBox
from fabric.widgets.revealer import Revealer
class ExpandableNotificationGroup(Box):
def __init__(self, app_name, notifications, **kwargs):
super().__init__(
name="notification-group", orientation="v", spacing=0, **kwargs
)
self.app_name = app_name
self.notifications = notifications
self.is_expanded = False
# Create collapsed state (shows only latest notification)
self.create_collapsed_state()
def create_collapsed_state(self):
from widgets.custom_image import CustomImage
latest_notification = self.notifications[0] # Most recent notification
# Create clickable event box
self.collapsed_eventbox = EventBox(
events=["button-press-event"],
)
self.collapsed_eventbox.connect("button-press-event", self.on_clicked)
# Only create stacked effect if we have multiple notifications
num_notifications = len(self.notifications)
if num_notifications == 1:
# Single notification - no stacking needed
single_notification = Box(
name="single-notification-content",
spacing=8,
children=[
Box(
name="notification-image",
children=CustomImage(
pixbuf=self._get_notification_pixbuf(
latest_notification._notification
)
),
),
Box(
name="notification-text",
orientation="v",
v_align="center",
h_expand=True,
children=[
Box(
name="notification-summary-box",
orientation="h",
children=[
Label(
name="notification-summary",
markup=f"<b>{self.app_name}</b>",
h_align="start",
ellipsization="end",
),
],
),
Label(
name="notification-body",
markup=latest_notification._notification.summary.replace(
"\n", " "
),
h_align="start",
ellipsization="end",
),
],
),
],
)
self.collapsed_eventbox.add(single_notification)
else:
# Multiple notifications - create stacked effect
# Create container for the entire stack
stack_container = Box(
name="notification-stack-container",
orientation="v",
spacing=0,
)
# Add bottom shadow layer first (deepest)
if num_notifications >= 3:
bottom_shadow = Box(
name="stack-shadow-bottom",
# style="min-height: 16px; margin-left: 20px; margin-right: -4px; margin-bottom: -8px;",
)
stack_container.add(bottom_shadow)
# Add middle shadow layer
if num_notifications >= 2:
middle_shadow = Box(
name="stack-shadow-middle",
# style="min-height: 20px; margin-left: 10px; margin-right: -2px; margin-bottom: -12px;",
)
stack_container.add(middle_shadow)
# Add the main notification content on top
main_notification = Box(
name="stack-main-notification",
spacing=8,
children=[
Box(
name="notification-image",
children=CustomImage(
pixbuf=self._get_notification_pixbuf(
latest_notification._notification
)
),
),
Box(
name="notification-text",
orientation="v",
v_align="center",
h_expand=True,
children=[
Box(
name="notification-summary-box",
orientation="h",
children=[
Label(
name="notification-summary",
markup=f"<b>{self.app_name}</b>",
h_align="start",
ellipsization="end",
),
],
),
Label(
name="notification-body",
markup=latest_notification._notification.summary.replace(
"\n", " "
),
h_align="start",
ellipsization="end",
),
],
),
Box(
name="notification-count",
children=[
Label(
name="notification-count-label",
label=f"{len(self.notifications)}",
h_align="end",
)
],
),
],
)
stack_container.add(main_notification)
self.collapsed_eventbox.add(stack_container)
self.add(self.collapsed_eventbox)
# Create expanded state (hidden initially)
self.create_expanded_state()
def create_expanded_state(self):
self.expanded_box = Box(
name="notification-group-expanded",
orientation="v",
spacing=5,
visible=False,
)
# Header with app name and controls
# App name and show less button
header_content = Box(
orientation="h",
h_expand=True,
children=[
Label(
name="notification-group-title",
markup=f"<b>{self.app_name}</b>",
h_align="start",
h_expand=True,
),
Button(
name="notification-show-less",
label="Show less",
on_clicked=self.collapse,
h_align="end",
),
Button(
name="notification-close-all",
label="×",
on_clicked=self.close_all,
h_align="end",
),
],
)
self.expanded_box.add(header_content)
# Individual notifications
for notification in self.notifications:
notification_widget = NotificationCenterWidget(notification=notification)
self.expanded_box.add(notification_widget)
self.add(self.expanded_box)
def _get_notification_pixbuf(self, notification):
# Use the same logic as NotificationWidget
try:
if hasattr(notification, "image_pixbuf") and notification.image_pixbuf:
return notification.image_pixbuf.scale_simple(
35, 35, 2
) # GdkPixbuf.InterpType.BILINEAR = 2
except Exception:
pass
# Fallback to app icon
try:
from modules.notification.notification import cache_notification_icon
cached_app_icon = cache_notification_icon(notification.app_icon, (35, 35))
if cached_app_icon:
return cached_app_icon
except Exception:
pass
# Ultimate fallback
from modules.notification.notification import get_fallback_notification_icon
return get_fallback_notification_icon((35, 35))
def on_clicked(self, widget, event):
if event.button == 1: # Left click
self.expand()
return True
def expand(self, *args):
self.is_expanded = True
self.collapsed_eventbox.set_visible(False)
self.expanded_box.set_visible(True)
def collapse(self, *args):
self.is_expanded = False
self.collapsed_eventbox.set_visible(True)
self.expanded_box.set_visible(False)
def close_all(self, *args):
# Close all notifications in this group
for notification in self.notifications:
try:
cleanup_notification_specific_caches(
app_icon_source=getattr(notification, "app_icon_source", None),
notification_image_cache_key=getattr(
notification, "notification_image_cache_key", None
),
)
notification_service.remove_cached_notification(notification.cache_id)
except Exception as e:
logger.error(
f"Error removing notification {notification.cache_id}: {e}"
)
class NotificationCenterWidget(NotificationWidget):
@@ -15,13 +284,17 @@ class NotificationCenterWidget(NotificationWidget):
self.notification_id = notification.cache_id
super().__init__(
notification._notification, timeout_ms=0, show_close_button=True, **kwargs
notification._notification,
timeout_ms=0,
show_close_button=True,
name="notification-centre-notifs",
**kwargs,
)
def create_content(self, notification):
# Create our custom close button for notification center
from widgets.custom_image import CustomImage
self.close_button = Button(
name="notif-close-button",
image=CustomImage(icon_name="close-symbolic", icon_size=18),
@@ -34,11 +307,11 @@ class NotificationCenterWidget(NotificationWidget):
self.close_button.connect(
"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,
@@ -100,11 +373,15 @@ class NotificationCenterWidget(NotificationWidget):
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)
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}")
logger.debug(
f"Cleaned up all caches for notification center: {self.notification_id}"
)
notification_service.remove_cached_notification(self.notification_id)
except Exception as e:
logger.error(f"Error removing notification {self.notification_id}: {e}")
@@ -135,6 +412,10 @@ class NotificationCenter(Window):
NOTIFICATION_CENTER_WIDTH = 410
self.set_size_request(NOTIFICATION_CENTER_WIDTH, 600)
# Group notifications by app name
self.notification_groups = defaultdict(list)
self.group_widgets = {}
notification_service.connect(
"cached-notification-added", self.on_notification_added
)
@@ -150,27 +431,10 @@ class NotificationCenter(Window):
name="noti-center-box",
)
header = Box(
orientation="h",
spacing=10,
style="margin-bottom: 10px;",
)
header.add(
Label(
label="Notification Center",
h_align="start",
h_expand=True,
style="font-size: 16px; font-weight: bold; color: #ffffff;",
)
)
main_box.add(header)
self.scrolled = ScrolledWindow(h_expand=True, v_expand=True)
self.scrolled = ScrolledWindow(h_expand=False, v_expand=False)
self.notifications_box = Box(
v_expand=True,
h_expand=True,
v_expand=False,
h_expand=False,
style="margin: 1px 0px 1px 1px;",
orientation="v",
spacing=5,
@@ -178,84 +442,154 @@ class NotificationCenter(Window):
self.scrolled.add(self.notifications_box)
main_box.add(self.scrolled)
# No notifications label
self.not_found_label = Label(
label="No notifications",
h_align="center",
v_align="center",
h_expand=True,
v_expand=True,
style="color: #888888; font-style: italic; margin: 20px;",
visible=(notification_service.count == 0),
)
main_box.add(self.not_found_label)
# # No notifications label
# self.not_found_label = Label(
# label="No notifications",
# h_align="center",
# v_align="center",
# h_expand=True,
# v_expand=True,
# style="color: #888888; font-style: italic; margin: 20px;",
# visible=(notification_service.count == 0),
# )
# main_box.add(self.not_found_label)
self.clear_all_button = Button(
name="noti-clear-button",
label="Clear All",
label="Clear",
on_clicked=self.clear_all_notifications,
visible=(notification_service.count > 0),
)
main_box.add(self.clear_all_button)
self.button_centre_box = CenterBox(
center_children=[self.clear_all_button],
)
main_box.add(self.button_centre_box)
self.children = main_box
# Load existing notifications
for cached_notification in notification_service.cached_notifications:
self.notifications_box.add(
NotificationCenterWidget(notification=cached_notification)
)
# Load existing notifications and group them
self._rebuild_notification_groups()
self.add_keybinding("Escape", self._on_escape_pressed)
self.connect("destroy", self._on_destroy)
def _rebuild_notification_groups(self):
"""Rebuild notification groups from scratch"""
# Clear existing groups
self.notification_groups.clear()
self.group_widgets.clear()
# Clear notifications box
for child in self.notifications_box.get_children():
child.destroy()
# Group notifications by app name
for cached_notification in notification_service.cached_notifications:
app_name = cached_notification._notification.app_name
self.notification_groups[app_name].append(cached_notification)
# Create group widgets
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)
self.group_widgets[app_name] = group_widget
self.notifications_box.add(group_widget)
def on_notification_added(self, service, cached_notification):
try:
notification_widget = NotificationCenterWidget(
notification=cached_notification
)
# Insert at the beginning to show newest first
self.notifications_box.pack_start(notification_widget, False, False, 0)
notification_widget.show_all()
logger.debug(
f"Added notification widget for {cached_notification.app_name}"
)
app_name = cached_notification._notification.app_name
# Add to groups
self.notification_groups[app_name].insert(
0, cached_notification
) # Insert at beginning (newest first)
# Update or create group widget
if app_name in self.group_widgets:
# Update existing group
group_widget = self.group_widgets[app_name]
group_widget.notifications = self.notification_groups[app_name]
# Refresh the group widget
self._refresh_group_widget(group_widget)
else:
# Create new group widget
group_widget = ExpandableNotificationGroup(
app_name, self.notification_groups[app_name]
)
self.group_widgets[app_name] = group_widget
self.notifications_box.pack_start(group_widget, False, False, 0)
group_widget.show_all()
logger.debug(f"Added notification to group {app_name}")
except Exception as e:
logger.error(f"Error adding notification widget: {e}")
logger.error(f"Error adding notification to group: {e}")
def _refresh_group_widget(self, group_widget):
"""Refresh a group widget's content"""
try:
# Remove existing children
for child in group_widget.get_children():
group_widget.remove(child)
# Recreate content
group_widget.create_collapsed_state()
group_widget.show_all()
except Exception as e:
logger.error(f"Error refreshing group widget: {e}")
def on_notification_removed(self, service, cached_notification):
try:
# Find and remove the corresponding widget with cache cleanup
for child in self.notifications_box.get_children():
if (
hasattr(child, "notification_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()
break
logger.debug(
f"Removed notification widget and all caches for {cached_notification.app_name}"
app_name = cached_notification._notification.app_name
# Remove from groups
if app_name in self.notification_groups:
self.notification_groups[app_name] = [
n
for n in self.notification_groups[app_name]
if n.cache_id != cached_notification.cache_id
]
# If no more notifications for this app, remove group widget
if not self.notification_groups[app_name]:
if app_name in self.group_widgets:
group_widget = self.group_widgets[app_name]
group_widget.destroy()
del self.group_widgets[app_name]
del self.notification_groups[app_name]
else:
# Update existing group widget
group_widget = self.group_widgets[app_name]
group_widget.notifications = self.notification_groups[app_name]
self._refresh_group_widget(group_widget)
# Clean up caches
cleanup_notification_specific_caches(
app_icon_source=getattr(cached_notification, "app_icon_source", None),
notification_image_cache_key=getattr(
cached_notification, "notification_image_cache_key", None
),
)
logger.debug(f"Removed notification from group {app_name}")
except Exception as e:
logger.error(f"Error removing notification widget: {e}")
logger.error(f"Error removing notification from group: {e}")
def on_clear_all(self, service):
try:
# Clear all remaining cached notification images AND icons
# Individual notifications clean up their own images, this handles any remaining
# Clear all groups
self.notification_groups.clear()
self.group_widgets.clear()
# Clear all remaining cached notification images AND icons
cleanup_all_notification_caches()
for child in self.notifications_box.get_children():
child.destroy()
logger.debug("Cleared all notification widgets and remaining cached images")
logger.debug("Cleared all notification groups and remaining cached images")
except Exception as e:
logger.error(f"Error clearing notification widgets: {e}")
logger.error(f"Error clearing notification groups: {e}")
def on_count_changed(self, service, count=None):
current_count = notification_service.count
@@ -264,8 +598,11 @@ class NotificationCenter(Window):
self.scrolled.set_visible(current_count > 0)
def clear_all_notifications(self, *_):
# Clear all groups
self.notification_groups.clear()
self.group_widgets.clear()
# 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()
if hasattr(self, "mousecapture"):
+221 -21
View File
@@ -1,26 +1,182 @@
#noti-center-box {
background-color: alpha(#fff, 0.09);
background-color: transparent;
border: 1px solid alpha(#111, 0.3);
/* border: 1px solid alpha(#111, 0.3); */
border-radius: 10px;
padding: 20px;
margin-right: 10px;
/* padding: 20px; */
/* margin: 6px; */
}
#noti-clear-button {
min-width: 10px;
min-height: 40px;
border-radius: 10px;
#noti-clear-button label {
/* min-width: 10px; */
background-color: alpha(#202828, 0.8);
min-height: 25px;
min-width: 70px;
font-size: 15px;
font-weight: 500;
font-family: "SF Pro Rounded";
border: 1.5px solid #525155;
border-radius: 15px;
margin-top: 10px;
}
#noti-clear-button {
background-color: alpha(#888, 0.09);
}
#noti-clear-button:hover {
background-color: alpha(#fff, 0.09);
}
/* Expandable notification group styles */
#notification-group {
margin: 4px 0;
}
/* Single notification (no stacking) */
#single-notification-content {
background-color: alpha(#202828, 0.8);
border: 1.5px solid #525155;
border-radius: 12px;
padding: 12px;
transition: all 0.2s ease;
}
#single-notification-content:hover {
background-color: alpha(#999, 0.9);
border: 1.5px solid #525155;
}
/* Stacked notification container */
#notification-stack-container {
background-color: transparent;
margin: 8px 0;
}
/* Bottom shadow layer (deepest) - offset to show stacking from below */
#stack-shadow-bottom {
background-color: alpha(#202828, 0.9);
border: 1.5px solid #525155;
border-radius: 12px;
opacity: 1;
min-height: 16px;
margin-left: 20px;
margin-right: -4px;
margin-bottom: -8px;
}
/* Middle shadow layer */
#stack-shadow-middle {
background-color: alpha(#202828, 0.9);
border: 1.5px solid #525155;
border-radius: 12px;
opacity: 1;
min-height: 20px;
margin-left: 10px;
margin-right: -2px;
margin-bottom: -12px;
}
/* Main notification content (top layer) */
#stack-main-notification {
background-color: alpha(#202828, 1);
border: 1.5px solid #525155;
border-radius: 12px;
padding: 12px;
transition: all 0.2s ease;
}
/* Hover effects for stacked notifications */
#notification-stack-container:hover #stack-main-notification {
background-color: alpha(#525155, 0.1);
border: 1px solid #525155;
}
#notification-stack-container:hover #stack-shadow-middle {
opacity: 0.7;
}
#notification-stack-container:hover #stack-shadow-bottom {
opacity: 0.5;
}
/* Collapsed notification state */
#notification-content-collapsed {
background-color: alpha(#000, 0.05);
border: 1px solid #525155;
border-radius: 12px;
padding: 12px;
margin: 2px 0;
transition: all 0.2s ease;
}
#notification-content-collapsed:hover {
background-color: alpha(#525155, 0.1);
border: 1px solid #525155;
}
/* Notification count badge */
#notification-count-label {
background-color: alpha(#fff, 0.2);
color: rgba(255, 255, 255, 0.8);
border-radius: 10px;
padding: 2px 6px;
font-size: 10px;
font-weight: 500;
min-width: 16px;
}
/* Expanded notification group header */
#notification-group-header {
background-color: alpha(#000, 0.05);
border: 1px solid #525155;
border-radius: 12px 12px 0 0;
padding: 8px 12px;
margin: 2px 0 0 0;
}
#notification-group-title {
color: #ffffff;
font-weight: bold;
font-size: 14px;
}
#notification-show-less {
background-color: transparent;
color: rgba(255, 255, 255, 0.7);
border: 1px solid alpha(#fff, 0.2);
border-radius: 6px;
padding: 2px 8px;
font-size: 11px;
margin-right: 8px;
}
#notification-show-less:hover {
background-color: alpha(#fff, 0.1);
color: rgba(255, 255, 255, 0.9);
}
#notification-close-all {
background-color: transparent;
color: rgba(255, 255, 255, 0.7);
border: 1px solid alpha(#fff, 0.2);
border-radius: 6px;
padding: 2px 8px;
font-size: 12px;
min-width: 24px;
}
#notification-close-all:hover {
background-color: alpha(#ff5555, 0.2);
color: rgba(255, 255, 255, 0.9);
border-color: alpha(#ff5555, 0.4);
}
/* Expanded notification list */
#notification-group-expanded {
border: 1px solid #525155;
border-top: none;
border-radius: 0 0 12px 12px;
/* background-color: alpha(#000, 0.02); */
}
/* Stacked notification group styles */
.stacked-notification-group {
margin: 4px 0;
@@ -30,11 +186,8 @@
}
.stacked-notification-item {
background-color: alpha(#000, 0.4);
border: 0.5px solid alpha(#111, 0.4);
box-shadow:
inset 0 0 200px 0 alpha(#111, 0.5),
inset 0 0 0 0.5px alpha(#aaa, 0.4);
background-color: alpha(#000, 0.05);
border: 1px solid #525155;
border-radius: 12px;
transition: all 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
}
@@ -50,11 +203,8 @@
}
#stacked-notification-item:hover {
background-color: alpha(#111, 0.5);
box-shadow:
inset 0 0 200px 0 alpha(#111, 0.5),
inset 0 0 0 0.5px alpha(#aaa, 0.4),
0 4px 12px alpha(#000, 0.3);
background-color: alpha(#525155, 0.1);
border: 1px solid #525155;
}
/* Stack count indicator */
@@ -72,7 +222,57 @@
/* Make expanded notifications clickable with subtle hover effect */
#expanded-notification-list:hover {
background-color: alpha(#111, 0.1);
background-color: alpha(#111, 0.9);
border-radius: 8px;
transition: background-color 0.2s ease;
}
#notification-group-title {
font-family: "SF Pro Rounded";
font-size: 19px;
margin: 10px;
font-weight: bold;
}
#notification-show-less {
font-family: "SF Pro Rounded";
background-color: alpha(#111, 0.9);
font-size: 12px;
margin: 10px;
border-radius: 15px;
}
#notification-close-all {
margin: 10px 10px 10px 0;
background-color: alpha(#111, 0.9);
border-radius: 15px;
}
#notif-close-button {
background-color: transparent;
}
#notification-group-expanded {
background-color: transparent;
border: none;
}
#notification-content {
background-color: transparent;
}
#notification-count-label {
background-color: transparent;
color: white;
font-family: "SF Pro Rounded";
font-size: 13px;
}
#notification-centre-notifs {
background-color: alpha(#1c2328, 0.8);
min-height: 40px;
padding: 10px;
border: 1.5px solid #525155;
border-radius: 12px;
}
#notification-centre-notifs:last-child {
margin-bottom: 10px;
}