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 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): super().__init__( name="dashboard", visible=True, orientation="v", spacing=8, all_visible=True, **kwargs, ) self.wifi = Network() self.bluetooth = Bluetooth() self.mini_player = PlayerContainerMini() self.mini_player_tile = TileSpecial( props=self.mini_player, mini_props=self.mini_player.get_mini_view() ) # Create real notifications component self.dashboard_notifications = DashboardNotifications() # Create scrolled window for notifications (matching dock notifications style) self.notifications_scrolled = ScrolledWindow( name="notification-history-scrolled-window", child=self.dashboard_notifications, h_scrollbar_policy="never", v_scrollbar_policy="automatic", min_content_size=(400, 200), max_content_size=(450, 300), h_expand=True, v_expand=True, h_align="fill", v_align="fill", propagate_width=False, propagate_height=False, ) self.tiles = Box( children=[ self.wifi, self.bluetooth, self.mini_player_tile, ], ) self.notification_container = Revealer( transition_duration=250, transition_type="slide-down", h_expand=True, child=Box( name="notification-history-popup-content", orientation="v", spacing=8, h_expand=True, v_expand=True, children=[self.notifications_scrolled], style_classes=["notification-history-popup"], ), child_revealed=True, ) self.children = [ Box(name="inner", children=self.tiles), Box( children=[ self.notification_container, ] ), ] def handle_tile_menu_expand(self, tile: str, toggle: bool): if toggle: self.notification_container.set_reveal_child(False) else: self.notification_container.set_reveal_child(True) for i in self.tiles: if i.get_name() == tile: print("found") else: if toggle: i.mini_view() i.icon.add_style_class("mini") i.icon.remove_style_class("maxi") else: i.maxi_view() i.icon.add_style_class("maxi") i.icon.remove_style_class("mini") print("search complete") class DashboardWindow(Window): visible = Property(bool, flags="read-write", default_value=False) def __init__(self, **kwargs): super().__init__( name="dashboard-window", layer="top", anchor="bottom", exclusivity="none", keyboard_mode="on-demand", child=Dashboard(), visible=False, all_visible=False, **kwargs, ) # Add escape key binding to close dashboard self.add_keybinding("Escape", lambda *_: self.close_dashboard()) self.hide() def show_dashboard(self): self.show_all() self.visible = True def close_dashboard(self): self.hide() self.visible = False def toggle_dashboard(self): if self.visible: self.close_dashboard() else: self.show_dashboard()