Files
Modus/modules/dashboard/notifications.py
T

378 lines
13 KiB
Python

"""
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)