From 25e5342b0fc3c4a8e418b772f317fcb350961d33 Mon Sep 17 00:00:00 2001 From: S4NKALP Date: Sun, 27 Jul 2025 01:29:00 +0545 Subject: [PATCH] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor(dashboard):=20mov?= =?UTF-8?q?e=20notification=20components=20to=20separate=20module?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/dashboard/__init__.py | 3 +- modules/dashboard/main.py | 371 +--------------------------- modules/dashboard/notifications.py | 377 +++++++++++++++++++++++++++++ 3 files changed, 380 insertions(+), 371 deletions(-) create mode 100644 modules/dashboard/notifications.py diff --git a/modules/dashboard/__init__.py b/modules/dashboard/__init__.py index 0cb5d35..9d69267 100644 --- a/modules/dashboard/__init__.py +++ b/modules/dashboard/__init__.py @@ -4,5 +4,6 @@ Provides a dashboard interface with tiles and notifications. """ from .main import DashboardWindow +from .notifications import DashboardNotifications, DashboardNotificationItem -__all__ = ["DashboardWindow"] +__all__ = ["DashboardWindow", "DashboardNotifications", "DashboardNotificationItem"] diff --git a/modules/dashboard/main.py b/modules/dashboard/main.py index b9fe01c..f2a976b 100644 --- a/modules/dashboard/main.py +++ b/modules/dashboard/main.py @@ -1,385 +1,16 @@ from fabric.widgets.box import Box -from fabric.widgets.label import Label -from fabric.widgets.button import Button from fabric.widgets.revealer import Revealer from fabric.widgets.scrolledwindow import ScrolledWindow -from fabric.widgets.centerbox import CenterBox from fabric.core.service import Property -from gi.repository import GLib, Gtk -import os -import json -from datetime import datetime from modules.dashboard.bluetooth import Bluetooth from modules.dashboard.network import Network from modules.dashboard.tile import TileSpecial from modules.dashboard.player_mini import PlayerContainerMini -from utils.notification_utils import ( - get_shared_notification_history, - PERSISTENT_HISTORY_FILE, - create_historical_notification_from_data, - compute_time_label, -) +from modules.dashboard.notifications import DashboardNotifications from utils.wayland import WaylandWindow as Window -import utils.icons as icons - - -class DashboardNotificationItem(Box): - """Individual notification item for dashboard""" - - def __init__(self, notification_data, **kwargs): - super().__init__( - name="notification-container", - orientation="v", - h_align="fill", - h_expand=True, - **kwargs - ) - - self.notification_data = notification_data - - # Create historical notification from data - hist_notif = create_historical_notification_from_data(notification_data) - - # Create timestamp - try: - arrival_time = datetime.fromisoformat(hist_notif.timestamp) - except Exception: - arrival_time = datetime.now() - - # Create time label - self.time_label = Label( - name="notification-timestamp", - markup=compute_time_label(arrival_time), - h_align="start", - ellipsization="end", - ) - - # Create notification image box - # For dashboard, we'll use a simple icon instead of trying to load cached images - self.notif_image_box = Box( - name="notification-image", - orientation="v", - children=[ - Label(name="notification-icon", markup=icons.notifications), - Box(v_expand=True), - ], - ) - - # Create summary label - self.notif_summary_label = Label( - name="notification-summary", - markup=hist_notif.summary, - h_align="start", - ellipsization="end", - ) - - # Create app name label - self.notif_app_name_label = Label( - name="notification-app-name", - markup=f"{hist_notif.app_name}", - h_align="start", - ellipsization="end", - ) - - # Create body label - self.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() - ) - if hist_notif.body: - self.notif_body_label.set_single_line_mode(True) - - # Create summary box with separators - self.notif_summary_box = Box( - name="notification-summary-box", - orientation="h", - children=[ - self.notif_summary_label, - Box( - name="notif-sep", - h_expand=False, - v_expand=False, - h_align="center", - v_align="center", - ), - self.notif_app_name_label, - Box( - name="notif-sep", - h_expand=False, - v_expand=False, - h_align="center", - v_align="center", - ), - self.time_label, - ], - ) - - # Create text box - self.notif_text_box = Box( - name="notification-text", - orientation="v", - v_align="center", - h_expand=True, - children=[ - self.notif_summary_box, - self.notif_body_label, - ], - ) - - # Create close button - self.notif_close_button = Button( - name="notif-close-button", - child=Label(name="notif-close-label", markup=icons.cancel), - on_clicked=lambda *_: self.remove_notification(), - ) - - self.notif_close_button_box = Box( - orientation="v", - children=[ - self.notif_close_button, - Box(v_expand=True), - ], - ) - - # Create main content box - content_box = Box( - name="notification-box-hist", - spacing=8, - children=[ - self.notif_image_box, - self.notif_text_box, - self.notif_close_button_box, - ], - ) - - self.add(content_box) - - def remove_notification(self): - """Remove this notification from the dashboard""" - if self.get_parent(): - self.get_parent().remove(self) - self.destroy() - - -class DashboardNotifications(Box): - """Dashboard notifications container that shows real notifications""" - - def __init__(self, **kwargs): - super().__init__( - name="notification-history", - orientation="v", - **kwargs - ) - - self.notification_items = [] - self.max_notifications = 5 # Limit to 5 notifications in dashboard - - # Create header components (matching dock notifications) - self.header_label = Label( - name="nhh", - label="Notifications", - h_align="start", - h_expand=True, - ) - self.header_switch = Gtk.Switch(name="dnd-switch") - self.header_switch.set_vexpand(False) - self.header_switch.set_valign(Gtk.Align.CENTER) - self.header_switch.set_active(False) - self.header_clean = Button( - name="nhh-button", - child=Label(name="nhh-button-label", markup=icons.trash), - on_clicked=self.clear_history, - ) - self.do_not_disturb_enabled = False - self.header_switch.connect("notify::active", self.on_do_not_disturb_changed) - self.dnd_label = Label(name="dnd-label", markup=icons.notifications_off) - - self.history_header = CenterBox( - name="notification-history-header", - spacing=8, - start_children=[self.header_switch, self.dnd_label], - center_children=[self.header_label], - end_children=[self.header_clean], - ) - - # Create notifications list - self.notifications_list = Box( - name="notifications-list", - orientation="v", - spacing=4, - h_expand=True, - v_expand=True, - h_align="fill", - v_align="fill", - ) - - # Create "no notifications" label - self.no_notifications_label = Label( - name="no-notif", - markup=icons.notifications_clear, - v_align="fill", - h_align="fill", - v_expand=True, - h_expand=True, - justification="center", - ) - self.no_notifications_box = Box( - name="no-notifications-box", - v_align="fill", - h_align="fill", - v_expand=True, - h_expand=True, - children=[self.no_notifications_label], - ) - - # Add components to main container - self.add(self.history_header) - self.add(self.notifications_list) - - # Add the no notifications box initially to the notifications list - self.notifications_list.add(self.no_notifications_box) - - # Connect to shared notification history - self._connect_to_notifications() - - # Load and sync DND state - self._load_and_sync_dnd_state() - - # Load initial notifications - self._load_recent_notifications() - - def _connect_to_notifications(self): - """Connect to the shared notification history for real-time updates""" - try: - self.shared_history = get_shared_notification_history() - self.shared_history.connect("notification-added", self._on_notification_added) - self.shared_history.connect("dnd-state-changed", self._on_dnd_state_changed) - except Exception as e: - print(f"Could not connect to notification history: {e}") - - def _on_notification_added(self, shared_history): - """Handle new notification added""" - GLib.idle_add(self._refresh_notifications) - - def _on_dnd_state_changed(self, notification_history, dnd_enabled): - """Handle DND state change""" - self.do_not_disturb_enabled = dnd_enabled - self.header_switch.set_active(dnd_enabled) - - def _load_and_sync_dnd_state(self): - """Load and sync DND state from shared history""" - try: - if hasattr(self, 'shared_history'): - dnd_enabled = self.shared_history.do_not_disturb_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 on_do_not_disturb_changed(self, switch, pspec): - """Handle DND switch toggle""" - self.do_not_disturb_enabled = switch.get_active() - - try: - if hasattr(self, 'shared_history'): - self.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): - """Clear all notifications""" - # Clear notification items - for item in self.notification_items[:]: - self.notifications_list.remove(item) - item.destroy() - self.notification_items.clear() - - # Clear persistent history - try: - if hasattr(self, 'shared_history'): - self.shared_history.clear_history() - except Exception as e: - print(f"Could not clear shared history: {e}") - - # Update visibility - self._update_no_notifications_visibility() - - def _load_recent_notifications(self): - """Load recent notifications from persistent storage""" - try: - if os.path.exists(PERSISTENT_HISTORY_FILE): - with open(PERSISTENT_HISTORY_FILE, "r") as f: - notifications = json.load(f) - - # Get the most recent notifications (up to max_notifications) - recent_notifications = notifications[-self.max_notifications:] - - for notif_data in reversed(recent_notifications): - self._add_notification_item(notif_data) - - except Exception as e: - print(f"Error loading recent notifications: {e}") - - def _refresh_notifications(self): - """Refresh the notification list""" - # Clear existing notifications - for item in self.notification_items[:]: - self.notifications_list.remove(item) - item.destroy() - self.notification_items.clear() - - # Reload recent notifications - self._load_recent_notifications() - - # Update visibility - self._update_no_notifications_visibility() - - def _add_notification_item(self, notification_data): - """Add a notification item to the dashboard""" - # Remove no notifications box if it's showing - if self.no_notifications_box in self.notifications_list.get_children(): - self.notifications_list.remove(self.no_notifications_box) - - if len(self.notification_items) >= self.max_notifications: - # Remove oldest notification - oldest = self.notification_items.pop(0) - self.notifications_list.remove(oldest) - oldest.destroy() - - # Create new notification item - item = DashboardNotificationItem(notification_data) - self.notification_items.append(item) - self.notifications_list.add(item) - - # Update visibility - self._update_no_notifications_visibility() - - # Show all to ensure visibility - self.show_all() - - def _update_no_notifications_visibility(self): - """Update visibility of no notifications label""" - has_notifications = bool(self.notification_items) - - if has_notifications: - # Hide no notifications box - if self.no_notifications_box in self.notifications_list.get_children(): - self.notifications_list.remove(self.no_notifications_box) - else: - # Show no notifications box - if self.no_notifications_box not in self.notifications_list.get_children(): - self.notifications_list.add(self.no_notifications_box) - class Dashboard(Box): def __init__(self, **kwargs): diff --git a/modules/dashboard/notifications.py b/modules/dashboard/notifications.py new file mode 100644 index 0000000..b5c6256 --- /dev/null +++ b/modules/dashboard/notifications.py @@ -0,0 +1,377 @@ +""" +Dashboard notification components for Modus. +Provides notification display functionality for the dashboard. +""" + +from fabric.widgets.box import Box +from fabric.widgets.label import Label +from fabric.widgets.button import Button +from fabric.widgets.centerbox import CenterBox +from gi.repository import GLib, Gtk +import os +import json +from datetime import datetime + +from utils.notification_utils import ( + get_shared_notification_history, + PERSISTENT_HISTORY_FILE, + create_historical_notification_from_data, + compute_time_label, +) + +import utils.icons as icons + + +class DashboardNotificationItem(Box): + """Individual notification item for dashboard""" + + def __init__(self, notification_data, **kwargs): + super().__init__( + name="notification-container", + orientation="v", + h_align="fill", + h_expand=True, + **kwargs + ) + + self.notification_data = notification_data + + # Create historical notification from data + hist_notif = create_historical_notification_from_data(notification_data) + + # Create timestamp + try: + arrival_time = datetime.fromisoformat(hist_notif.timestamp) + except Exception: + arrival_time = datetime.now() + + # Create time label + self.time_label = Label( + name="notification-timestamp", + markup=compute_time_label(arrival_time), + h_align="start", + ellipsization="end", + ) + + # Create notification image box + # For dashboard, we'll use a simple icon instead of trying to load cached images + self.notif_image_box = Box( + name="notification-image", + orientation="v", + children=[ + Label(name="notification-icon", markup=icons.notifications), + Box(v_expand=True), + ], + ) + + # Create summary label + self.notif_summary_label = Label( + name="notification-summary", + markup=hist_notif.summary, + h_align="start", + ellipsization="end", + ) + + # Create app name label + self.notif_app_name_label = Label( + name="notification-app-name", + markup=f"{hist_notif.app_name}", + h_align="start", + ellipsization="end", + ) + + # Create body label + self.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() + ) + if hist_notif.body: + self.notif_body_label.set_single_line_mode(True) + + # Create summary box with separators + self.notif_summary_box = Box( + name="notification-summary-box", + orientation="h", + children=[ + self.notif_summary_label, + Box( + name="notif-sep", + h_expand=False, + v_expand=False, + h_align="center", + v_align="center", + ), + self.notif_app_name_label, + Box( + name="notif-sep", + h_expand=False, + v_expand=False, + h_align="center", + v_align="center", + ), + self.time_label, + ], + ) + + # Create text box + self.notif_text_box = Box( + name="notification-text", + orientation="v", + v_align="center", + h_expand=True, + children=[ + self.notif_summary_box, + self.notif_body_label, + ], + ) + + # Create close button + self.notif_close_button = Button( + name="notif-close-button", + child=Label(name="notif-close-label", markup=icons.cancel), + on_clicked=lambda *_: self.remove_notification(), + ) + + self.notif_close_button_box = Box( + orientation="v", + children=[ + self.notif_close_button, + Box(v_expand=True), + ], + ) + + # Create main content box + content_box = Box( + name="notification-box-hist", + spacing=8, + children=[ + self.notif_image_box, + self.notif_text_box, + self.notif_close_button_box, + ], + ) + + self.add(content_box) + + def remove_notification(self): + """Remove this notification from the dashboard""" + if self.get_parent(): + self.get_parent().remove(self) + self.destroy() + + +class DashboardNotifications(Box): + """Dashboard notifications container that shows real notifications""" + + def __init__(self, **kwargs): + super().__init__( + name="notification-history", + orientation="v", + **kwargs + ) + + self.notification_items = [] + self.max_notifications = 5 # Limit to 5 notifications in dashboard + + # Create header components (matching dock notifications) + self.header_label = Label( + name="nhh", + label="Notifications", + h_align="start", + h_expand=True, + ) + self.header_switch = Gtk.Switch(name="dnd-switch") + self.header_switch.set_vexpand(False) + self.header_switch.set_valign(Gtk.Align.CENTER) + self.header_switch.set_active(False) + self.header_clean = Button( + name="nhh-button", + child=Label(name="nhh-button-label", markup=icons.trash), + on_clicked=self.clear_history, + ) + self.do_not_disturb_enabled = False + self.header_switch.connect("notify::active", self.on_do_not_disturb_changed) + self.dnd_label = Label(name="dnd-label", markup=icons.notifications_off) + + self.history_header = CenterBox( + name="notification-history-header", + spacing=8, + start_children=[self.header_switch, self.dnd_label], + center_children=[self.header_label], + end_children=[self.header_clean], + ) + + # Create notifications list + self.notifications_list = Box( + name="notifications-list", + orientation="v", + spacing=4, + h_expand=True, + v_expand=True, + h_align="fill", + v_align="fill", + ) + + # Create "no notifications" label + self.no_notifications_label = Label( + name="no-notif", + markup=icons.notifications_clear, + v_align="fill", + h_align="fill", + v_expand=True, + h_expand=True, + justification="center", + ) + self.no_notifications_box = Box( + name="no-notifications-box", + v_align="fill", + h_align="fill", + v_expand=True, + h_expand=True, + children=[self.no_notifications_label], + ) + + # Add components to main container + self.add(self.history_header) + self.add(self.notifications_list) + + # Add the no notifications box initially to the notifications list + self.notifications_list.add(self.no_notifications_box) + + # Connect to shared notification history + self._connect_to_notifications() + + # Load and sync DND state + self._load_and_sync_dnd_state() + + # Load initial notifications + self._load_recent_notifications() + + def _connect_to_notifications(self): + """Connect to the shared notification history for real-time updates""" + try: + self.shared_history = get_shared_notification_history() + self.shared_history.connect("notification-added", self._on_notification_added) + self.shared_history.connect("dnd-state-changed", self._on_dnd_state_changed) + except Exception as e: + print(f"Could not connect to notification history: {e}") + + def _on_notification_added(self, shared_history): + """Handle new notification added""" + GLib.idle_add(self._refresh_notifications) + + def _on_dnd_state_changed(self, notification_history, dnd_enabled): + """Handle DND state change""" + self.do_not_disturb_enabled = dnd_enabled + self.header_switch.set_active(dnd_enabled) + + def _load_and_sync_dnd_state(self): + """Load and sync DND state from shared history""" + try: + if hasattr(self, 'shared_history'): + dnd_enabled = self.shared_history.do_not_disturb_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 on_do_not_disturb_changed(self, switch, pspec): + """Handle DND switch toggle""" + self.do_not_disturb_enabled = switch.get_active() + + try: + if hasattr(self, 'shared_history'): + self.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): + """Clear all notifications""" + # Clear notification items + for item in self.notification_items[:]: + self.notifications_list.remove(item) + item.destroy() + self.notification_items.clear() + + # Clear persistent history + try: + if hasattr(self, 'shared_history'): + self.shared_history.clear_history() + except Exception as e: + print(f"Could not clear shared history: {e}") + + # Update visibility + self._update_no_notifications_visibility() + + def _load_recent_notifications(self): + """Load recent notifications from persistent storage""" + try: + if os.path.exists(PERSISTENT_HISTORY_FILE): + with open(PERSISTENT_HISTORY_FILE, "r") as f: + notifications = json.load(f) + + # Get the most recent notifications (up to max_notifications) + recent_notifications = notifications[-self.max_notifications:] + + for notif_data in reversed(recent_notifications): + self._add_notification_item(notif_data) + + except Exception as e: + print(f"Error loading recent notifications: {e}") + + def _refresh_notifications(self): + """Refresh the notification list""" + # Clear existing notifications + for item in self.notification_items[:]: + self.notifications_list.remove(item) + item.destroy() + self.notification_items.clear() + + # Reload recent notifications + self._load_recent_notifications() + + # Update visibility + self._update_no_notifications_visibility() + + def _add_notification_item(self, notification_data): + """Add a notification item to the dashboard""" + # Remove no notifications box if it's showing + if self.no_notifications_box in self.notifications_list.get_children(): + self.notifications_list.remove(self.no_notifications_box) + + if len(self.notification_items) >= self.max_notifications: + # Remove oldest notification + oldest = self.notification_items.pop(0) + self.notifications_list.remove(oldest) + oldest.destroy() + + # Create new notification item + item = DashboardNotificationItem(notification_data) + self.notification_items.append(item) + self.notifications_list.add(item) + + # Update visibility + self._update_no_notifications_visibility() + + # Show all to ensure visibility + self.show_all() + + def _update_no_notifications_visibility(self): + """Update visibility of no notifications label""" + has_notifications = bool(self.notification_items) + + if has_notifications: + # Hide no notifications box + if self.no_notifications_box in self.notifications_list.get_children(): + self.notifications_list.remove(self.no_notifications_box) + else: + # Show no notifications box + if self.no_notifications_box not in self.notifications_list.get_children(): + self.notifications_list.add(self.no_notifications_box)