Files
sims/sims/modules/control_center.py
T
2026-05-05 00:19:10 +02:00

353 lines
11 KiB
Python

from datetime import datetime
from fabric.widgets.box import Box
from fabric.widgets.button import Button
from fabric.widgets.image import Image
from fabric.widgets.label import Label
from fabric.widgets.scrolledwindow import ScrolledWindow
from fabric.widgets.wayland import WaylandWindow as Window
from gi.repository import Gdk
from sims.modules.calendar import CalendarService
from sims.modules.vinyl import VinylButton
from sims.services.notification_history import NotificationHistoryService
from sims.widgets.notification_history_entry import NotificationHistoryEntryWidget
class ControlCenter(Window):
def __init__(
self,
history: NotificationHistoryService,
calendar_service: CalendarService | None = None,
vinyl_button: VinylButton | None = None,
monitor: int = 0,
width: int = 380,
):
super().__init__(
name="control-center",
anchor="top right bottom",
monitor=monitor,
margin="0",
exclusivity="none",
keyboard_mode="on-demand",
visible=False,
)
self._history = history
self._calendar_service = calendar_service
self._vinyl_button = vinyl_button
self._width = width
self._visibility_listeners: list = []
close_button = Button(
name="control-center-close",
image=Image(icon_name="window-close-symbolic", icon_size=16),
on_clicked=lambda *_: self.hide(),
)
header = Box(
name="control-center-header",
orientation="h",
spacing=8,
)
header.pack_start(
Label(name="control-center-title", label="Control Center", h_align="start"),
True,
True,
0,
)
header.pack_end(close_button, False, False, 0)
sections: list = []
if vinyl_button is not None:
sections.append(self._build_settings_section())
if calendar_service is not None:
sections.append(self._build_calendar_section())
sections.append(self._build_notifications_section())
sections_box = Box(
name="control-center-sections",
orientation="v",
spacing=12,
children=sections,
h_expand=True,
)
scroll = ScrolledWindow(
name="control-center-scroll",
h_scrollbar_policy="never",
v_scrollbar_policy="automatic",
child=sections_box,
h_expand=True,
v_expand=True,
)
body = Box(
name="control-center-body",
orientation="v",
spacing=8,
children=[header, scroll],
h_expand=True,
v_expand=True,
)
body.set_size_request(self._width, -1)
self.add(body)
self.connect("key-press-event", self._on_key_press)
self._history.connect("changed", lambda *_: self._refresh_notifications())
if self._calendar_service is not None:
self._calendar_service.connect(
"events-changed",
lambda _service, events: self._refresh_calendar(events),
)
self._refresh_notifications()
if self._calendar_service is not None:
self._refresh_calendar(self._calendar_service.get_cached_events())
def _build_section(self, name: str, title: str | None) -> Box:
section = Box(
name=name,
orientation="v",
spacing=6,
h_expand=True,
)
if title is not None:
section.add(
Label(
name="control-center-section-title",
label=title,
h_align="start",
)
)
return section
def _build_settings_section(self) -> Box:
section = self._build_section("control-center-settings", "Settings")
row = Box(
name="control-center-settings-row",
orientation="h",
spacing=8,
)
row.pack_start(
Label(
name="control-center-settings-label",
label="Vinyl Passthrough",
h_align="start",
),
True,
True,
0,
)
row.pack_end(self._vinyl_button, False, False, 0)
section.add(row)
return section
def _build_calendar_section(self) -> Box:
section = self._build_section("control-center-calendar", "Calendar")
self._calendar_events_box = Box(
name="control-center-events",
orientation="v",
spacing=4,
h_expand=True,
)
section.add(self._calendar_events_box)
return section
def _build_notifications_section(self) -> Box:
section = self._build_section("control-center-notifications", None)
clear_button = Button(
name="control-center-notifications-clear",
label="Clear all",
on_clicked=lambda *_: self._history.clear(),
)
header = Box(
name="control-center-notifications-header",
orientation="h",
spacing=8,
)
header.pack_start(
Label(
name="control-center-section-title",
label="Notifications",
h_align="start",
),
True,
True,
0,
)
header.pack_end(clear_button, False, False, 0)
self._notifications_empty = Label(
name="control-center-notifications-empty",
label="No notifications",
h_align="start",
)
self._notifications_list = Box(
name="control-center-notifications-list",
orientation="v",
spacing=6,
h_expand=True,
)
section.add(header)
section.add(self._notifications_list)
return section
def add_visibility_listener(self, callback) -> None:
self._visibility_listeners.append(callback)
def _notify_visibility(self, visible: bool) -> None:
for callback in self._visibility_listeners:
callback(visible)
def toggle(self) -> None:
if self.get_visible():
self.hide()
else:
self.show()
def show(self) -> None: # type: ignore[override]
self._history.mark_all_seen()
super().show()
self.show_all()
self._notify_visibility(True)
def hide(self) -> None: # type: ignore[override]
super().hide()
self._notify_visibility(False)
def _on_key_press(self, _widget, event):
if event.keyval == Gdk.KEY_Escape:
self.hide()
return True
return False
def _refresh_notifications(self) -> None:
for child in self._notifications_list.get_children():
self._notifications_list.remove(child)
if child is not self._notifications_empty:
child.destroy()
entries = self._history.entries
if not entries:
self._notifications_list.add(self._notifications_empty)
self._notifications_empty.show_all()
return
for entry in entries:
self._notifications_list.add(
NotificationHistoryEntryWidget(
entry, on_dismiss=self._history.remove
)
)
self._notifications_list.show_all()
def _refresh_calendar(self, events) -> None:
for child in self._calendar_events_box.get_children():
self._calendar_events_box.remove(child)
child.destroy()
if not events:
self._calendar_events_box.add(
Label(
name="control-center-no-events",
label="No events today",
h_align="start",
)
)
self._calendar_events_box.show_all()
return
now = datetime.now()
current_time = now.strftime("%H:%M")
current_time_added = False
for event in events:
title = event.get("title", "No title")
start_raw = event.get("start", "")
end_raw = event.get("end", "")
start_time = start_raw.split()[1] if start_raw else ""
end_time = end_raw.split()[1] if end_raw else ""
location = event.get("location", "")
if not current_time_added and start_time and start_time > current_time:
self._calendar_events_box.add(self._build_now_indicator(current_time))
current_time_added = True
if start_time and end_time:
time_str = f"{start_time} - {end_time}"
elif start_time:
time_str = start_time
else:
time_str = "All day"
event_box = Box(
name="event-item",
orientation="h",
spacing=12,
style_classes=["event-item"],
)
event_box.add(
Label(
time_str,
name="event-time",
style_classes=["event-time"],
style="min-width: 90px;",
)
)
content_box = Box(
name="event-content",
orientation="v",
spacing=2,
)
content_box.add(
Label(
title,
name="event-title",
style_classes=["event-title"],
h_align="start",
)
)
if location:
content_box.add(
Label(
f"📍 {location}",
name="event-location",
style_classes=["event-location"],
h_align="start",
)
)
event_box.add(content_box)
self._calendar_events_box.add(event_box)
if not current_time_added:
self._calendar_events_box.add(self._build_now_indicator(current_time))
self._calendar_events_box.show_all()
def _build_now_indicator(self, current_time: str) -> Box:
indicator = Box(
name="current-time-indicator",
orientation="h",
spacing=8,
style_classes=["current-time-indicator"],
)
indicator.add(
Label(
current_time,
name="current-time-label",
style_classes=["current-time-label"],
style="min-width: 90px; font-weight: bold;",
)
)
indicator.add(
Label(
"━━━ NOW",
name="current-time-line",
style_classes=["current-time-line"],
)
)
return indicator