378 lines
13 KiB
Python
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)
|