Files
Modus/modules/dashboard/main.py
T

508 lines
16 KiB
Python

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