feat: control center
This commit is contained in:
34
sims/main.py
34
sims/main.py
@@ -25,10 +25,12 @@ from .modules.launcher.clipboard import ClipboardMenu
|
||||
from .modules.launcher.power import PowerMenu
|
||||
from .modules.launcher.screenrec import ScreenrecMenu
|
||||
from .modules.launcher.screenshot import ScreenshotMenu
|
||||
from .modules.notification_center import NotificationCenter
|
||||
from .modules.calendar import CalendarService
|
||||
from .modules.control_center import ControlCenter
|
||||
from .modules.notifications import NotificationToasts
|
||||
from .modules.stylix import get_stylix_css_path
|
||||
from .config import NOTIFICATIONS, POWER, SCREENREC, STYLIX
|
||||
from .modules.vinyl import VinylButton
|
||||
from .config import CALENDAR, NOTIFICATIONS, POWER, SCREENREC, STYLIX, VINYL
|
||||
from .services.fenster import get_i3_connection
|
||||
from .services.notification_history import NotificationHistoryService
|
||||
from .services.screenrec import ScreenrecService
|
||||
@@ -57,7 +59,6 @@ if SCREENREC.get("enable", False):
|
||||
notifications_service: Notifications | None = None
|
||||
notification_history: NotificationHistoryService | None = None
|
||||
notification_toasts: NotificationToasts | None = None
|
||||
notification_center: NotificationCenter | None = None
|
||||
if NOTIFICATIONS.get("enable", False):
|
||||
notifications_service = Notifications()
|
||||
notification_history = NotificationHistoryService(
|
||||
@@ -73,8 +74,18 @@ if NOTIFICATIONS.get("enable", False):
|
||||
width=NOTIFICATIONS.get("width", 360),
|
||||
timeout_ms=NOTIFICATIONS.get("timeout_ms", 10_000),
|
||||
)
|
||||
notification_center = NotificationCenter(
|
||||
notification_history,
|
||||
|
||||
vinyl_button: VinylButton | None = VinylButton() if VINYL.get("enable", False) else None
|
||||
calendar_service: CalendarService | None = (
|
||||
CalendarService(update_interval=120000) if CALENDAR.get("enable", True) else None
|
||||
)
|
||||
|
||||
control_center: ControlCenter | None = None
|
||||
if notification_history is not None:
|
||||
control_center = ControlCenter(
|
||||
history=notification_history,
|
||||
calendar_service=calendar_service,
|
||||
vinyl_button=vinyl_button,
|
||||
monitor=0,
|
||||
width=NOTIFICATIONS.get("center_width", 380),
|
||||
)
|
||||
@@ -87,8 +98,8 @@ if screenrec_menu is not None:
|
||||
_app_windows.append(screenrec_menu)
|
||||
if notification_toasts is not None:
|
||||
_app_windows.append(notification_toasts)
|
||||
if notification_center is not None:
|
||||
_app_windows.append(notification_center)
|
||||
if control_center is not None:
|
||||
_app_windows.append(control_center)
|
||||
app = Application("sims", *_app_windows)
|
||||
|
||||
|
||||
@@ -148,9 +159,9 @@ def screenrec_stop():
|
||||
|
||||
|
||||
@Application.action()
|
||||
def toggle_notification_center():
|
||||
if notification_center is not None:
|
||||
notification_center.toggle()
|
||||
def toggle_control_center():
|
||||
if control_center is not None:
|
||||
control_center.toggle()
|
||||
|
||||
|
||||
def _set_all_bars_rounded(rounded: bool):
|
||||
@@ -210,8 +221,7 @@ def spawn_bars():
|
||||
tray=tray if i == 0 else None,
|
||||
monitor=i,
|
||||
screenrec_service=screenrec_service if i == 0 else None,
|
||||
notification_history=notification_history if i == 0 else None,
|
||||
notification_center=notification_center if i == 0 else None,
|
||||
control_center=control_center if i == 0 else None,
|
||||
)
|
||||
bar_windows.append(bar)
|
||||
if i == 0 and bar.notmuch:
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import psutil
|
||||
from fabric.widgets.box import Box
|
||||
from fabric.widgets.label import Label
|
||||
from fabric.widgets.image import Image
|
||||
@@ -6,25 +5,20 @@ from fabric.widgets.overlay import Overlay
|
||||
from fabric.widgets.datetime import DateTime
|
||||
from fabric.widgets.centerbox import CenterBox
|
||||
from sims.modules.player import Player
|
||||
from sims.modules.vinyl import VinylButton
|
||||
from sims.modules.quick_menu import QuickMenuOpener
|
||||
from sims.modules.battery import Battery
|
||||
from sims.modules.calendar import CalendarService, CalendarPopup
|
||||
from sims.modules.notification_center import NotificationCenter
|
||||
from sims.modules.control_center import ControlCenter
|
||||
from sims.modules.notmuch import NotmuchWidget
|
||||
from sims.modules.screenrec import ScreenrecWidget
|
||||
from sims.services.notification_history import NotificationHistoryService
|
||||
from sims.widgets.notification_bell import NotificationBell
|
||||
from fabric.widgets.wayland import WaylandWindow as Window
|
||||
from fabric.system_tray.widgets import SystemTray
|
||||
from sims.widgets.fenster import FensterWorkspaces, FensterWorkspaceButton, FensterActiveWindow
|
||||
from sims.services.fenster import get_i3_connection
|
||||
from sims.widgets.fenster import FensterWorkspaces, FensterActiveWindow
|
||||
from sims.services.screenrec import ScreenrecService
|
||||
from sims.services.smart_corners import get_smart_corners_service
|
||||
from fabric.widgets.button import Button
|
||||
from fabric.widgets.circularprogressbar import CircularProgressBar
|
||||
from sims.services.system_stats import SystemStatsService
|
||||
|
||||
from sims.config import VINYL, BATTERY, BAR_HEIGHT, WINDOW_TITLE, NOTMUCH
|
||||
from sims.config import BATTERY, BAR_HEIGHT, WINDOW_TITLE, NOTMUCH
|
||||
|
||||
|
||||
class StatusBar(Window):
|
||||
@@ -34,8 +28,7 @@ class StatusBar(Window):
|
||||
tray: SystemTray | None = None,
|
||||
monitor: int = 1,
|
||||
screenrec_service: ScreenrecService | None = None,
|
||||
notification_history: NotificationHistoryService | None = None,
|
||||
notification_center: NotificationCenter | None = None,
|
||||
control_center: ControlCenter | None = None,
|
||||
):
|
||||
super().__init__(
|
||||
name="sims",
|
||||
@@ -49,29 +42,23 @@ class StatusBar(Window):
|
||||
)
|
||||
self.output = display
|
||||
self._corners_rounded = False
|
||||
self._right_flat = False
|
||||
|
||||
self.workspaces = FensterWorkspaces(
|
||||
output=display,
|
||||
name="workspaces",
|
||||
spacing=4,
|
||||
)
|
||||
# Create calendar components (refresh every 2 minutes)
|
||||
self.calendar_service = CalendarService(update_interval=120000)
|
||||
self.calendar_popup = CalendarPopup()
|
||||
self.calendar_popup_visible = False
|
||||
|
||||
# Create clickable datetime widget
|
||||
from fabric.widgets.button import Button
|
||||
datetime_widget = DateTime(name="date-time", formatters="%d %b - %H:%M")
|
||||
self.date_time = Button(
|
||||
name="date-time-button",
|
||||
child=datetime_widget,
|
||||
on_clicked=self.toggle_calendar,
|
||||
style="background: transparent; border: none; padding: 0; margin: 0; box-shadow: none;"
|
||||
on_clicked=self._on_date_time_clicked,
|
||||
style="background: transparent; border: none; padding: 0; margin: 0; box-shadow: none;",
|
||||
)
|
||||
self.control_center = control_center
|
||||
|
||||
# Connect calendar service to popup
|
||||
self.calendar_service.connect("events-changed", self.update_calendar_display)
|
||||
self.system_tray = tray
|
||||
|
||||
self.active_window = FensterActiveWindow(
|
||||
@@ -95,15 +82,6 @@ class StatusBar(Window):
|
||||
overlays=[self.cpu_progress_bar, self.progress_label],
|
||||
)
|
||||
self.player = Player()
|
||||
self.vinyl = None
|
||||
if VINYL["enable"]:
|
||||
self.vinyl = VinylButton()
|
||||
|
||||
# Create quick menu button
|
||||
self.quick_menu = QuickMenuOpener(icon_name="open-menu-symbolic")
|
||||
# Setup audio section with vinyl if enabled
|
||||
if self.vinyl:
|
||||
self.quick_menu.get_menu().setup_audio_section(vinyl_service=self.vinyl)
|
||||
|
||||
self.battery = None
|
||||
if BATTERY["enable"]:
|
||||
@@ -117,13 +95,6 @@ class StatusBar(Window):
|
||||
if screenrec_service is not None:
|
||||
self.screenrec = ScreenrecWidget(screenrec_service)
|
||||
|
||||
self.notification_bell = None
|
||||
if notification_history is not None and notification_center is not None:
|
||||
self.notification_bell = NotificationBell(
|
||||
notification_history,
|
||||
on_clicked=notification_center.toggle,
|
||||
)
|
||||
|
||||
self.status_container = Box(
|
||||
name="widgets-container",
|
||||
spacing=4,
|
||||
@@ -146,11 +117,6 @@ class StatusBar(Window):
|
||||
if self.screenrec:
|
||||
end_container_children.append(self.screenrec)
|
||||
|
||||
if self.notification_bell:
|
||||
end_container_children.append(self.notification_bell)
|
||||
|
||||
# Add quick menu button next to time
|
||||
end_container_children.append(self.quick_menu)
|
||||
end_container_children.append(self.date_time)
|
||||
|
||||
center_children = []
|
||||
@@ -183,23 +149,19 @@ class StatusBar(Window):
|
||||
)
|
||||
self.children = self.inner
|
||||
|
||||
# Create system stats service with signal-based updates
|
||||
self.system_stats_service = SystemStatsService(update_interval=3000)
|
||||
self.system_stats_service.connect("stats-changed", self.update_progress_bars)
|
||||
|
||||
# Set the bar height
|
||||
self.set_size_request(-1, BAR_HEIGHT)
|
||||
|
||||
smart_corners = get_smart_corners_service()
|
||||
smart_corners.connect("state-changed", self._on_smart_corners_changed)
|
||||
self.set_corners_rounded(not smart_corners.get(display))
|
||||
|
||||
self.show_all()
|
||||
if self.control_center is not None:
|
||||
self.control_center.add_visibility_listener(self.set_right_flat)
|
||||
|
||||
def __del__(self):
|
||||
"""Cleanup when bar is destroyed"""
|
||||
if hasattr(self, 'calendar_service'):
|
||||
self.calendar_service.stop_monitoring()
|
||||
self.show_all()
|
||||
|
||||
@property
|
||||
def corners_rounded(self) -> bool:
|
||||
@@ -219,30 +181,19 @@ class StatusBar(Window):
|
||||
return
|
||||
self.set_corners_rounded(not active)
|
||||
|
||||
def set_right_flat(self, flat: bool) -> None:
|
||||
if flat == self._right_flat:
|
||||
return
|
||||
if flat:
|
||||
self.inner.set_style("border-radius: 0 0 0 28px;")
|
||||
else:
|
||||
self.inner.set_style("")
|
||||
self._right_flat = flat
|
||||
|
||||
def update_progress_bars(self, service, cpu_percent, memory_percent):
|
||||
"""Update progress bars when system stats change"""
|
||||
self.cpu_progress_bar.value = cpu_percent
|
||||
self.ram_progress_bar.value = memory_percent
|
||||
|
||||
def toggle_calendar(self, button=None):
|
||||
"""Toggle the calendar popup when datetime is clicked"""
|
||||
from loguru import logger
|
||||
logger.info(f"[Calendar] DateTime clicked, popup_visible: {self.calendar_popup_visible}")
|
||||
|
||||
if self.calendar_popup_visible:
|
||||
logger.info("[Calendar] Hiding calendar popup")
|
||||
self.calendar_popup.set_visible(False)
|
||||
self.calendar_popup_visible = False
|
||||
else:
|
||||
logger.info("[Calendar] Showing calendar popup")
|
||||
# Use cached events - no need to refresh on click
|
||||
cached_events = self.calendar_service.get_cached_events()
|
||||
logger.info(f"[Calendar] Using {len(cached_events)} cached events")
|
||||
self.calendar_popup.update_events_display(cached_events)
|
||||
self.calendar_popup.set_visible(True)
|
||||
self.calendar_popup.show_all()
|
||||
self.calendar_popup_visible = True
|
||||
|
||||
def update_calendar_display(self, service, events):
|
||||
"""Update the calendar popup with events"""
|
||||
self.calendar_popup.update_events_display(events)
|
||||
def _on_date_time_clicked(self, _button=None):
|
||||
if self.control_center is not None:
|
||||
self.control_center.toggle()
|
||||
|
||||
@@ -2,15 +2,10 @@ import json
|
||||
import os
|
||||
import subprocess
|
||||
import shutil
|
||||
from datetime import datetime, date
|
||||
from datetime import date
|
||||
|
||||
# Add common binary paths to PATH for user binaries
|
||||
os.environ['PATH'] = '/run/current-system/sw/bin:/home/' + os.environ.get('USER', 'user') + '/.nix-profile/bin:' + os.environ.get('PATH', '')
|
||||
from fabric.widgets.box import Box
|
||||
from fabric.widgets.label import Label
|
||||
from fabric.widgets.button import Button
|
||||
from fabric.widgets.image import Image
|
||||
from fabric.widgets.wayland import WaylandWindow as Window
|
||||
from loguru import logger
|
||||
from sims.config import CALENDAR
|
||||
|
||||
@@ -203,203 +198,3 @@ class CalendarService:
|
||||
else:
|
||||
logger.info("[Calendar] Using khal subprocess")
|
||||
self.update_events_subprocess()
|
||||
|
||||
|
||||
class CalendarPopup(Window):
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(
|
||||
name="calendar-popup",
|
||||
layer="top",
|
||||
anchor="top right",
|
||||
margin="10px 10px 0px 0px", # Just a few pixels under the bar
|
||||
exclusivity="none",
|
||||
visible=False,
|
||||
all_visible=False,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
|
||||
# Events container
|
||||
self.events_box = Box(
|
||||
name="events-box",
|
||||
orientation="v",
|
||||
spacing=6,
|
||||
style="min-width: 450px; min-height: 200px;",
|
||||
)
|
||||
|
||||
# Add a test label to make sure popup is working
|
||||
test_label = Label("Calendar Events", name="calendar-title")
|
||||
|
||||
container = Box(
|
||||
orientation="v", spacing=4, children=[test_label, self.events_box]
|
||||
)
|
||||
|
||||
self.children = container
|
||||
|
||||
# Set explicit size - much bigger
|
||||
self.set_size_request(500, 400)
|
||||
|
||||
def update_events_display(self, events):
|
||||
"""Update the events display"""
|
||||
logger.info(f"[Calendar] Updating popup with {len(events)} events")
|
||||
|
||||
# Clear existing children first
|
||||
self.events_box.children = []
|
||||
|
||||
if not events:
|
||||
logger.info("[Calendar] No events, showing 'no events' message")
|
||||
no_events_label = Label("No events today", name="no-events")
|
||||
self.events_box.add(no_events_label)
|
||||
return
|
||||
|
||||
# Check current time for time indicator placement
|
||||
now = datetime.now()
|
||||
current_time = now.strftime("%H:%M")
|
||||
current_time_added = False
|
||||
|
||||
for i, event in enumerate(events):
|
||||
logger.info(f"[Calendar] Processing event {i+1} for display")
|
||||
title = event.get("title", "No title")
|
||||
start_time = event.get("start", "").split()[1] if event.get("start") else ""
|
||||
end_time = event.get("end", "").split()[1] if event.get("end") else ""
|
||||
location = event.get("location", "")
|
||||
|
||||
# Check if we should add current time indicator before this event
|
||||
if not current_time_added and start_time and start_time > current_time:
|
||||
self.add_current_time_indicator(current_time)
|
||||
current_time_added = True
|
||||
|
||||
# Format time display
|
||||
time_str = ""
|
||||
if start_time and end_time:
|
||||
time_str = f"{start_time} - {end_time}"
|
||||
elif start_time:
|
||||
time_str = start_time
|
||||
|
||||
logger.info(f"[Calendar] Creating widget for: {title} ({time_str})")
|
||||
|
||||
# Create event item with horizontal layout - time on left, content on right
|
||||
event_box = Box(
|
||||
name="event-item",
|
||||
orientation="h", # Horizontal layout
|
||||
spacing=12,
|
||||
style_classes=["event-item"],
|
||||
)
|
||||
|
||||
# Left side: Time display (fixed width for alignment)
|
||||
time_display = time_str if time_str else "All day"
|
||||
time_label = Label(
|
||||
time_display,
|
||||
name="event-time",
|
||||
style_classes=["event-time"],
|
||||
style="min-width: 100px;" # Fixed width for consistent alignment
|
||||
)
|
||||
|
||||
# Right side: Content (title and location)
|
||||
content_box = Box(
|
||||
name="event-content",
|
||||
orientation="v",
|
||||
spacing=2
|
||||
)
|
||||
|
||||
# Title (no more status prefix)
|
||||
title_label = Label(
|
||||
title,
|
||||
name="event-title",
|
||||
style_classes=["event-title"],
|
||||
)
|
||||
content_box.add(title_label)
|
||||
|
||||
if location:
|
||||
location_label = Label(
|
||||
f"📍 {location}",
|
||||
name="event-location",
|
||||
style_classes=["event-location"],
|
||||
)
|
||||
content_box.add(location_label)
|
||||
|
||||
# Add time and content to the main event box
|
||||
event_box.add(time_label)
|
||||
event_box.add(content_box)
|
||||
|
||||
self.events_box.add(event_box)
|
||||
logger.info(f"[Calendar] Added event widget to events_box")
|
||||
|
||||
# Add current time indicator at the end if not added yet
|
||||
if not current_time_added:
|
||||
self.add_current_time_indicator(current_time)
|
||||
|
||||
# Force refresh the popup display
|
||||
self.events_box.show_all()
|
||||
logger.info(f"[Calendar] Finished updating popup")
|
||||
|
||||
def add_current_time_indicator(self, current_time):
|
||||
"""Add a current time indicator to the events list"""
|
||||
time_indicator = Box(
|
||||
name="current-time-indicator",
|
||||
orientation="h",
|
||||
spacing=8,
|
||||
style_classes=["current-time-indicator"],
|
||||
)
|
||||
|
||||
# Current time label
|
||||
time_label = Label(
|
||||
current_time,
|
||||
name="current-time-label",
|
||||
style_classes=["current-time-label"],
|
||||
style="min-width: 100px; font-weight: bold;"
|
||||
)
|
||||
|
||||
# Line indicator
|
||||
line_label = Label(
|
||||
"━━━ NOW",
|
||||
name="current-time-line",
|
||||
style_classes=["current-time-line"],
|
||||
)
|
||||
|
||||
time_indicator.add(time_label)
|
||||
time_indicator.add(line_label)
|
||||
|
||||
self.events_box.add(time_indicator)
|
||||
logger.info(f"[Calendar] Added current time indicator at {current_time}")
|
||||
|
||||
|
||||
class CalendarWidget(Button):
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(
|
||||
name="calendar-widget",
|
||||
child=Image(icon_name="x-office-calendar-symbolic", icon_size=16),
|
||||
on_clicked=self.toggle_events,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
self.service = CalendarService()
|
||||
self.service.connect("events-changed", self.update_events_display)
|
||||
|
||||
# Create popup window
|
||||
self.popup = CalendarPopup()
|
||||
self.popup_visible = False
|
||||
logger.info("[Calendar] Calendar widget initialized with popup")
|
||||
|
||||
# Initial update
|
||||
self.update_events_display(self.service, self.service.events)
|
||||
|
||||
def toggle_events(self, button=None):
|
||||
"""Toggle the visibility of the events popup"""
|
||||
logger.info(f"[Calendar] Button clicked, popup_visible: {self.popup_visible}")
|
||||
|
||||
if self.popup_visible:
|
||||
logger.info("[Calendar] Hiding popup")
|
||||
self.popup.set_visible(False)
|
||||
self.popup_visible = False
|
||||
else:
|
||||
logger.info("[Calendar] Showing popup")
|
||||
# Refresh events when opening
|
||||
self.service.update_events()
|
||||
self.popup.set_visible(True)
|
||||
self.popup.show_all()
|
||||
self.popup_visible = True
|
||||
|
||||
def update_events_display(self, service, events):
|
||||
"""Update the events display in popup"""
|
||||
self.popup.update_events_display(events)
|
||||
|
||||
352
sims/modules/control_center.py
Normal file
352
sims/modules/control_center.py
Normal file
@@ -0,0 +1,352 @@
|
||||
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
|
||||
@@ -1,129 +0,0 @@
|
||||
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.services.notification_history import NotificationHistoryService
|
||||
from sims.widgets.notification_history_entry import NotificationHistoryEntryWidget
|
||||
|
||||
|
||||
class NotificationCenter(Window):
|
||||
def __init__(
|
||||
self,
|
||||
history: NotificationHistoryService,
|
||||
monitor: int = 0,
|
||||
width: int = 380,
|
||||
):
|
||||
super().__init__(
|
||||
name="notification-center",
|
||||
anchor="top right bottom",
|
||||
monitor=monitor,
|
||||
margin="0",
|
||||
exclusivity="none",
|
||||
keyboard_mode="on-demand",
|
||||
visible=False,
|
||||
)
|
||||
self._history = history
|
||||
self._width = width
|
||||
|
||||
self._empty_label = Label(
|
||||
name="notification-center-empty",
|
||||
label="No notifications",
|
||||
h_align="center",
|
||||
v_align="center",
|
||||
h_expand=True,
|
||||
v_expand=True,
|
||||
)
|
||||
|
||||
self._list = Box(
|
||||
name="notification-center-list",
|
||||
spacing=6,
|
||||
orientation="v",
|
||||
h_expand=True,
|
||||
)
|
||||
|
||||
clear_button = Button(
|
||||
name="notification-center-clear",
|
||||
label="Clear all",
|
||||
on_clicked=lambda *_: self._history.clear(),
|
||||
)
|
||||
close_button = Button(
|
||||
name="notification-center-close",
|
||||
image=Image(icon_name="window-close-symbolic", icon_size=16),
|
||||
on_clicked=lambda *_: self.hide(),
|
||||
)
|
||||
header = Box(
|
||||
name="notification-center-header",
|
||||
orientation="h",
|
||||
spacing=8,
|
||||
)
|
||||
header.pack_start(
|
||||
Label(name="notification-center-title", label="Notifications", h_align="start"),
|
||||
True,
|
||||
True,
|
||||
0,
|
||||
)
|
||||
header.pack_end(close_button, False, False, 0)
|
||||
header.pack_end(clear_button, False, False, 4)
|
||||
|
||||
scroll = ScrolledWindow(
|
||||
name="notification-center-scroll",
|
||||
h_scrollbar_policy="never",
|
||||
v_scrollbar_policy="automatic",
|
||||
child=self._list,
|
||||
h_expand=True,
|
||||
v_expand=True,
|
||||
)
|
||||
|
||||
body = Box(
|
||||
name="notification-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())
|
||||
self._refresh()
|
||||
|
||||
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()
|
||||
|
||||
def _on_key_press(self, _widget, event):
|
||||
if event.keyval == Gdk.KEY_Escape:
|
||||
self.hide()
|
||||
return True
|
||||
return False
|
||||
|
||||
def _refresh(self) -> None:
|
||||
for child in self._list.get_children():
|
||||
self._list.remove(child)
|
||||
if child is not self._empty_label:
|
||||
child.destroy()
|
||||
entries = self._history.entries
|
||||
if not entries:
|
||||
self._list.add(self._empty_label)
|
||||
self._empty_label.show_all()
|
||||
return
|
||||
for entry in entries:
|
||||
self._list.add(
|
||||
NotificationHistoryEntryWidget(
|
||||
entry, on_dismiss=self._history.remove
|
||||
)
|
||||
)
|
||||
self._list.show_all()
|
||||
@@ -1,283 +0,0 @@
|
||||
from fabric.widgets.button import Button
|
||||
from fabric.widgets.image import Image
|
||||
from fabric.widgets.box import Box
|
||||
from fabric.widgets.label import Label
|
||||
from fabric.widgets.wayland import WaylandWindow as Window
|
||||
from gi.repository import Gtk
|
||||
from loguru import logger
|
||||
|
||||
|
||||
class QuickMenuItem(Box):
|
||||
"""Base class for quick menu items"""
|
||||
def __init__(self, title, icon_name=None, **kwargs):
|
||||
super().__init__(
|
||||
orientation="h",
|
||||
spacing=12,
|
||||
name="quick-menu-item",
|
||||
**kwargs
|
||||
)
|
||||
self.set_style("padding: 8px 12px; min-width: 280px;")
|
||||
|
||||
# Icon and title on the left
|
||||
left_box = Box(orientation="h", spacing=8)
|
||||
if icon_name:
|
||||
icon = Image(icon_name=icon_name, icon_size=16)
|
||||
left_box.add(icon)
|
||||
|
||||
self.title_label = Label(title)
|
||||
self.title_label.set_style("font-size: 14px;")
|
||||
left_box.add(self.title_label)
|
||||
|
||||
self.add(left_box)
|
||||
|
||||
# Derived classes can add controls to the right side
|
||||
|
||||
|
||||
class QuickMenuToggle(QuickMenuItem):
|
||||
"""A menu item with a toggle switch"""
|
||||
def __init__(self, title, icon_name=None, active=False, on_toggle=None, **kwargs):
|
||||
super().__init__(title, icon_name, **kwargs)
|
||||
|
||||
# Create a custom toggle using a button with state tracking
|
||||
self._active = active
|
||||
self._on_toggle = on_toggle
|
||||
|
||||
# Create toggle indicator box
|
||||
self.toggle_box = Box(
|
||||
orientation="h",
|
||||
spacing=0
|
||||
)
|
||||
self.toggle_box.set_style("min-width: 44px; min-height: 24px; border-radius: 12px; padding: 2px;")
|
||||
|
||||
# Toggle indicator (circle)
|
||||
self.toggle_indicator = Label("")
|
||||
self.toggle_indicator.set_style("min-width: 20px; min-height: 20px; border-radius: 10px; background: white;")
|
||||
|
||||
self.toggle_box.add(self.toggle_indicator)
|
||||
|
||||
# Make it clickable
|
||||
self.toggle_button = Button(
|
||||
child=self.toggle_box,
|
||||
on_clicked=self._on_click
|
||||
)
|
||||
self.toggle_button.set_style("background: transparent; border: none; padding: 0;")
|
||||
|
||||
# Add spacer to push toggle to the right
|
||||
spacer = Label("", h_expand=True)
|
||||
self.add(spacer)
|
||||
self.add(self.toggle_button)
|
||||
|
||||
# Set initial state
|
||||
self._update_appearance()
|
||||
|
||||
def _on_click(self, button):
|
||||
self._active = not self._active
|
||||
self._update_appearance()
|
||||
if self._on_toggle:
|
||||
self._on_toggle(self._active)
|
||||
|
||||
def _update_appearance(self):
|
||||
if self._active:
|
||||
self.toggle_box.set_style_classes(["toggle-active"])
|
||||
self.toggle_box.set_style(
|
||||
"min-width: 44px; min-height: 24px; border-radius: 12px; padding: 2px; "
|
||||
"transition: all 0.2s;"
|
||||
)
|
||||
self.toggle_indicator.set_style(
|
||||
"min-width: 20px; min-height: 20px; border-radius: 10px; "
|
||||
"background: white; margin-left: 20px; transition: all 0.2s;"
|
||||
)
|
||||
else:
|
||||
self.toggle_box.set_style_classes(["toggle-inactive"])
|
||||
self.toggle_box.set_style(
|
||||
"min-width: 44px; min-height: 24px; border-radius: 12px; padding: 2px; "
|
||||
"transition: all 0.2s;"
|
||||
)
|
||||
self.toggle_indicator.set_style(
|
||||
"min-width: 20px; min-height: 20px; border-radius: 10px; "
|
||||
"background: white; margin-left: 0px; transition: all 0.2s;"
|
||||
)
|
||||
|
||||
def set_active(self, active):
|
||||
self._active = active
|
||||
self._update_appearance()
|
||||
|
||||
def get_active(self):
|
||||
return self._active
|
||||
|
||||
|
||||
class QuickMenuButton(QuickMenuItem):
|
||||
"""A menu item that acts as a button"""
|
||||
def __init__(self, title, icon_name=None, on_click=None, **kwargs):
|
||||
super().__init__(title, icon_name, **kwargs)
|
||||
|
||||
if on_click:
|
||||
# Make the entire item clickable
|
||||
button_overlay = Button(
|
||||
child=Box(), # Empty box as child
|
||||
on_clicked=on_click
|
||||
)
|
||||
button_overlay.set_style("background: transparent; border: none; padding: 0; margin: 0;")
|
||||
|
||||
# Add arrow indicator on the right
|
||||
arrow = Label("›")
|
||||
arrow.set_style("font-size: 18px; opacity: 0.5;")
|
||||
spacer = Label("", h_expand=True)
|
||||
self.add(spacer)
|
||||
self.add(arrow)
|
||||
|
||||
|
||||
class QuickMenuSection(Box):
|
||||
"""A section in the quick menu with optional title"""
|
||||
def __init__(self, title=None, **kwargs):
|
||||
super().__init__(
|
||||
orientation="v",
|
||||
spacing=4,
|
||||
name="quick-menu-section",
|
||||
**kwargs
|
||||
)
|
||||
|
||||
if title:
|
||||
title_label = Label(
|
||||
title,
|
||||
name="section-title"
|
||||
)
|
||||
title_label.set_style("font-size: 12px; opacity: 0.6; padding: 8px 12px 4px 12px; font-weight: bold;")
|
||||
self.add(title_label)
|
||||
|
||||
self.items_box = Box(orientation="v", spacing=2)
|
||||
self.add(self.items_box)
|
||||
|
||||
def add_item(self, item):
|
||||
self.items_box.add(item)
|
||||
|
||||
|
||||
class QuickMenu(Window):
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(
|
||||
name="quick-menu",
|
||||
layer="overlay", # Changed from 'top' to 'overlay' for better shadow support
|
||||
anchor="top right",
|
||||
margin="40px 10px 0px 0px",
|
||||
exclusivity="none",
|
||||
visible=False,
|
||||
all_visible=False,
|
||||
style_classes=["popup-window"],
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
# Main container
|
||||
self.main_box = Box(
|
||||
orientation="v",
|
||||
spacing=8,
|
||||
name="quick-menu-container"
|
||||
)
|
||||
# Remove redundant styling since it's handled in stylix.css
|
||||
pass
|
||||
|
||||
# Title
|
||||
title_box = Box(
|
||||
orientation="h",
|
||||
spacing=8
|
||||
)
|
||||
title_box.set_style("padding: 12px;")
|
||||
title = Label("Quick Menu")
|
||||
title.set_style("font-size: 16px; font-weight: bold;")
|
||||
title_box.add(title)
|
||||
|
||||
self.main_box.add(title_box)
|
||||
# Add a simple divider line
|
||||
divider = Label("")
|
||||
divider.set_style("min-height: 1px; background: rgba(255,255,255,0.1); margin: 0px 12px;")
|
||||
self.main_box.add(divider)
|
||||
|
||||
# Sections container
|
||||
self.sections_container = Box(
|
||||
orientation="v",
|
||||
spacing=8
|
||||
)
|
||||
self.sections_container.set_style("padding: 8px 0px;")
|
||||
self.main_box.add(self.sections_container)
|
||||
|
||||
self.children = self.main_box
|
||||
self.set_size_request(360, -1)
|
||||
|
||||
# Store references to dynamic items
|
||||
self.vinyl_toggle = None
|
||||
self.sections = {}
|
||||
|
||||
def add_section(self, section_id, title=None):
|
||||
"""Add a new section to the menu"""
|
||||
section = QuickMenuSection(title=title)
|
||||
self.sections[section_id] = section
|
||||
self.sections_container.add(section)
|
||||
|
||||
# Add separator before section if not the first
|
||||
if len(self.sections) > 1:
|
||||
separator = Label("")
|
||||
separator.set_style("min-height: 1px; background: rgba(255,255,255,0.1); margin: 4px 12px;")
|
||||
self.sections_container.add(separator)
|
||||
|
||||
return section
|
||||
|
||||
def setup_audio_section(self, vinyl_service=None):
|
||||
"""Setup the audio controls section"""
|
||||
audio_section = self.add_section("audio", None) # No section title since it's the only section
|
||||
|
||||
# Vinyl passthrough toggle
|
||||
if vinyl_service:
|
||||
self.vinyl_toggle = QuickMenuToggle(
|
||||
title="Vinyl Passthrough",
|
||||
icon_name="folder-music-symbolic",
|
||||
active=vinyl_service.active,
|
||||
on_toggle=lambda active: self._on_vinyl_toggle(active, vinyl_service)
|
||||
)
|
||||
audio_section.add_item(self.vinyl_toggle)
|
||||
|
||||
# Store reference to vinyl service
|
||||
self.vinyl_service = vinyl_service
|
||||
|
||||
def _on_vinyl_toggle(self, active, vinyl_service):
|
||||
"""Handle vinyl toggle"""
|
||||
logger.info(f"[QuickMenu] Vinyl toggled: {active}")
|
||||
vinyl_service.active = active
|
||||
|
||||
def setup_system_section(self):
|
||||
"""Setup system controls section"""
|
||||
# Removed for now - can add system controls later
|
||||
pass
|
||||
|
||||
def update_vinyl_state(self, active):
|
||||
"""Update vinyl toggle state from external source"""
|
||||
if self.vinyl_toggle:
|
||||
self.vinyl_toggle.set_active(active)
|
||||
|
||||
|
||||
class QuickMenuOpener(Button):
|
||||
"""Button to open the quick menu"""
|
||||
def __init__(self, icon_name="open-menu-symbolic", **kwargs):
|
||||
super().__init__(
|
||||
name="quick-menu-button",
|
||||
child=Image(icon_name=icon_name, icon_size=16),
|
||||
on_clicked=self.toggle_menu,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
self.menu = QuickMenu()
|
||||
self.menu_visible = False
|
||||
|
||||
def toggle_menu(self, button=None):
|
||||
"""Toggle the quick menu visibility"""
|
||||
if self.menu_visible:
|
||||
logger.info("[QuickMenu] Hiding menu")
|
||||
self.menu.set_visible(False)
|
||||
self.menu_visible = False
|
||||
else:
|
||||
logger.info("[QuickMenu] Showing menu")
|
||||
self.menu.set_visible(True)
|
||||
self.menu.show_all()
|
||||
self.menu_visible = True
|
||||
|
||||
def get_menu(self):
|
||||
"""Get the menu instance for configuration"""
|
||||
return self.menu
|
||||
@@ -16,51 +16,13 @@
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
/* Calendar popup */
|
||||
#calendar-popup {
|
||||
background-color: var(--window-bg);
|
||||
border: solid 2px var(--border-color);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
|
||||
animation: slide-down 200ms ease-out;
|
||||
}
|
||||
|
||||
@keyframes slide-down {
|
||||
from {
|
||||
opacity: 0;
|
||||
margin-top: -20px;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
#calendar-title {
|
||||
color: var(--foreground);
|
||||
font-weight: bold;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
#events-box {
|
||||
background-color: var(--window-bg);
|
||||
border: none; /* Remove outline */
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
#no-events {
|
||||
color: var(--light-grey);
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
/* Calendar event items */
|
||||
.event-item {
|
||||
border-radius: 6px;
|
||||
padding: 8px 12px;
|
||||
margin: 4px 0px;
|
||||
background-color: var(--module-bg);
|
||||
border: none; /* Remove outline */
|
||||
border-radius: 8px;
|
||||
padding: 8px 10px;
|
||||
margin: 2px 0px;
|
||||
background-color: var(--light-bg);
|
||||
border: none;
|
||||
transition: background-color 0.15s ease;
|
||||
}
|
||||
|
||||
|
||||
@@ -43,43 +43,100 @@
|
||||
color: var(--red);
|
||||
}
|
||||
|
||||
/* Notification center side rail */
|
||||
/* Control center side rail */
|
||||
|
||||
#notification-center {
|
||||
#control-center {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
#notification-center-body {
|
||||
background-color: var(--mid-bg);
|
||||
border-left: solid 1px var(--border-color);
|
||||
padding: 12px;
|
||||
#control-center-body {
|
||||
background-color: var(--background);
|
||||
border-left: solid 2px var(--border-color);
|
||||
border-bottom: solid 2px var(--border-color);
|
||||
border-bottom-left-radius: 28px;
|
||||
padding: 50px 12px 12px 12px;
|
||||
}
|
||||
|
||||
#notification-center-header {
|
||||
padding: 0 0 8px 0;
|
||||
#control-center-header {
|
||||
padding: 4px 8px 12px 8px;
|
||||
border-bottom: solid 1px var(--border-color);
|
||||
}
|
||||
|
||||
#notification-center-title {
|
||||
#control-center-title {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
color: var(--foreground);
|
||||
}
|
||||
|
||||
#notification-center-clear,
|
||||
#notification-center-close {
|
||||
padding: 4px 10px;
|
||||
border-radius: 0.5rem;
|
||||
#control-center-close {
|
||||
padding: 4px 8px;
|
||||
border-radius: 12px;
|
||||
background-color: var(--module-bg);
|
||||
}
|
||||
|
||||
#control-center-close:hover {
|
||||
background-color: var(--light-bg);
|
||||
}
|
||||
|
||||
#notification-center-clear:hover,
|
||||
#notification-center-close:hover {
|
||||
#control-center-sections {
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
#control-center-settings,
|
||||
#control-center-calendar,
|
||||
#control-center-notifications {
|
||||
padding: 8px;
|
||||
background-color: var(--module-bg);
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
#control-center-section-title {
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
color: var(--light-grey);
|
||||
padding: 4px 4px 8px 4px;
|
||||
}
|
||||
|
||||
#control-center-settings-row {
|
||||
padding: 4px 4px;
|
||||
}
|
||||
|
||||
#control-center-settings-label {
|
||||
font-size: 14px;
|
||||
color: var(--foreground);
|
||||
}
|
||||
|
||||
#control-center-events {
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
#control-center-no-events {
|
||||
color: var(--light-grey);
|
||||
padding: 6px 4px;
|
||||
}
|
||||
|
||||
#control-center-notifications-header {
|
||||
padding: 0 4px 6px 4px;
|
||||
}
|
||||
|
||||
#control-center-notifications-clear {
|
||||
padding: 2px 10px;
|
||||
border-radius: 8px;
|
||||
background-color: var(--light-bg);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
#control-center-notifications-clear:hover {
|
||||
background-color: var(--dark-grey);
|
||||
}
|
||||
|
||||
#notification-center-empty {
|
||||
#control-center-notifications-list {
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
#control-center-notifications-empty {
|
||||
color: var(--light-grey);
|
||||
padding: 24px;
|
||||
padding: 8px 4px;
|
||||
}
|
||||
|
||||
#notification-history-entry {
|
||||
@@ -120,25 +177,3 @@
|
||||
background-color: var(--dark-grey);
|
||||
}
|
||||
|
||||
/* Bar bell */
|
||||
|
||||
#notification-bell {
|
||||
padding: 4px;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
#notification-bell:hover {
|
||||
background-color: var(--light-bg);
|
||||
}
|
||||
|
||||
#notification-bell-badge {
|
||||
background-color: var(--red);
|
||||
color: var(--foreground);
|
||||
font-size: 10px;
|
||||
font-weight: bold;
|
||||
padding: 0 4px;
|
||||
border-radius: 8px;
|
||||
min-width: 14px;
|
||||
min-height: 14px;
|
||||
margin: -4px -4px 0 0;
|
||||
}
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
from typing import Callable
|
||||
|
||||
from fabric.widgets.button import Button
|
||||
from fabric.widgets.image import Image
|
||||
from fabric.widgets.label import Label
|
||||
from fabric.widgets.overlay import Overlay
|
||||
|
||||
from sims.services.notification_history import NotificationHistoryService
|
||||
|
||||
|
||||
class NotificationBell(Button):
|
||||
def __init__(
|
||||
self,
|
||||
history: NotificationHistoryService,
|
||||
on_clicked: Callable[[], None],
|
||||
**kwargs,
|
||||
):
|
||||
self._badge = Label(
|
||||
name="notification-bell-badge",
|
||||
label="",
|
||||
visible=False,
|
||||
v_align="start",
|
||||
h_align="end",
|
||||
)
|
||||
self._badge.set_no_show_all(True)
|
||||
|
||||
super().__init__(
|
||||
name="notification-bell",
|
||||
child=Overlay(
|
||||
child=Image(
|
||||
icon_name="preferences-system-notifications-symbolic",
|
||||
icon_size=18,
|
||||
),
|
||||
overlays=[self._badge],
|
||||
),
|
||||
on_clicked=lambda *_: on_clicked(),
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
self._history = history
|
||||
self._history.connect("changed", lambda *_: self._refresh())
|
||||
self._refresh()
|
||||
|
||||
def _refresh(self) -> None:
|
||||
count = self._history.unseen_count
|
||||
if count <= 0:
|
||||
self._badge.set_visible(False)
|
||||
return
|
||||
self._badge.set_label(str(count) if count < 10 else "9+")
|
||||
self._badge.set_visible(True)
|
||||
Reference in New Issue
Block a user