diff --git a/Assets/Translations/de.json b/Assets/Translations/de.json index 01311b44..5c1dfeec 100644 --- a/Assets/Translations/de.json +++ b/Assets/Translations/de.json @@ -32,16 +32,6 @@ "reset": "Eckenradius des Bildschirms zurücksetzen" } }, - "lockscreen": { - "section": { - "label": "Sperrbildschirm", - "description": "Hier kannst du deinen Sperrbildschirm konfigurieren." - }, - "lock-on-suspend": { - "label": "Beim Standby sperren", - "description": "Bildschirm automatisch sperren, wenn das System in den Standby-Modus wechselt." - } - }, "fonts": { "section": { "label": "Schriftarten", @@ -79,7 +69,8 @@ "description": "Wählen die in der Anwendungsoberfläche verwendete Sprache.", "auto-detect": "Automatisch" } - } + }, + "launch-setup-wizard": "Starte den Setup-Assistenten" }, "audio": { "title": "Audio", @@ -243,13 +234,13 @@ "widgets": { "section": { "label": "Widget-Positionierung", - "description": "Widgets per Drag & Drop neu anordnen. Abzeichen zeigen die Verwendung an: [L]inks, [M]itte, [R]echts." + "description": "Widgets per Drag & Drop neu anordnen. Symoble zeigen die Verwendung an: [L]inks, [M]itte, [R]echts." } }, "monitors": { "section": { "label": "Monitor-Anzeige", - "description": "Statusleiste auf bestimmten Monitoren anzeigen. Standard ist alle, wenn keine ausgewählt sind." + "description": "Leiste auf bestimmten Monitoren anzeigen. Standardmäßig auf allen." } }, "tray": { @@ -319,7 +310,7 @@ }, "clipboard-history": { "label": "Zwischenablage-Verlauf aktivieren", - "description": "Zugriff auf zuvor kopierte Elemente über den Starter." + "description": "Zugriff auf zuvor kopierte Elemente über den Launcher." }, "sort-by-usage": { "label": "Nach Häufigkeit sortieren", @@ -343,8 +334,8 @@ "description": "Erscheinungsbild und Verhalten von Benachrichtigungen konfigurieren." }, "do-not-disturb": { - "label": "Nicht stören", - "description": "Alle Benachrichtigungs-Popups deaktivieren, wenn aktiviert." + "label": "Bitte Nicht stören", + "description": "Alle Benachrichtigungs-Popups deaktivieren." }, "enable-osd": { "label": "Bildschirmanzeige aktivieren", @@ -384,35 +375,35 @@ "monitors": { "section": { "label": "Monitor-Anzeige", - "description": "Benachrichtigungen auf bestimmten Monitoren anzeigen. Standard ist alle, wenn keine ausgewählt sind." + "description": "Benachrichtigungen auf bestimmten Monitoren anzeigen. Standardmäßig werden sie auf allen Monitoren angezeigt" } } }, "osd": { - "title": "Bildschirmanzeige", + "title": "On-Screen Display", "description": "Bildschirm-Overlays wie Lautstärke- und Helligkeitsanzeigen konfigurieren.", "section": { "general": { "label": "Allgemein", - "description": "Sichtbarkeit und Verhalten der OSD konfigurieren." + "description": "Sichtbarkeit und Verhalten vom On-Screen Display konfigurieren." } }, "enabled": { - "label": "Bildschirmanzeige aktivieren", + "label": "On-Screen Display aktivieren", "description": "Lautstärke- und Helligkeitsänderungen in Echtzeit anzeigen." }, "always-on-top": { "label": "Immer im Vordergrund", - "description": "Bildschirmanzeige über Vollbildfenstern und anderen Ebenen anzeigen." + "description": "On-Screen Display über Vollbildfenstern und anderen Ebenen anzeigen." }, "location": { "label": "Position", - "description": "Wo Bildschirmanzeigen erscheinen." + "description": "Wo On-Screen Displays erscheinen." }, "duration": { "section": { "label": "Automatisches Ausblenden", - "description": "Wie lange die OSD sichtbar bleibt, bevor sie automatisch ausgeblendet wird." + "description": "Wie lange das On-Screen Display sichtbar bleibt, bevor es automatisch ausgeblendet wird." }, "auto-hide": { "label": "Ausblenden nach", @@ -422,7 +413,7 @@ "monitors": { "section": { "label": "Monitor-Anzeige", - "description": "OSD auf bestimmten Monitoren anzeigen. Standard ist alle, wenn keine ausgewählt sind." + "description": "On-Screen Display auf bestimmten Monitoren anzeigen. Standardmäßig auf allen angezeigt." } } }, @@ -471,7 +462,7 @@ "description": "Dauer der Übergangsanimationen in Sekunden." }, "edge-smoothness": { - "label": "Übergangskante weichzeichnen", + "label": "Übergangseffect weichzeichnen", "description": "Wendet einen weichen, gefiederten Effekt auf die Kante von Übergängen an." } }, @@ -551,15 +542,15 @@ "description": "Terminal-Emulator-Theming.", "kitty": { "description": "Schreibt {filepath} und lädt neu", - "description-missing": "Erfordert kitty Terminal" + "description-missing": "Erfordert {app} Terminal" }, "ghostty": { "description": "Schreibt {filepath} und lädt neu", - "description-missing": "Erfordert ghostty Terminal" + "description-missing": "Erfordert {app} Terminal" }, "foot": { "description": "Schreibt {filepath} und lädt neu", - "description-missing": "Erfordert foot Terminal" + "description-missing": "Erfordert {app} Terminal" } }, "programs": { @@ -567,11 +558,11 @@ "description": "Anwendungsspezifisches Theming.", "fuzzel": { "description": "Schreibt {filepath} und lädt neu", - "description-missing": "Erfordert fuzzel Starter" + "description-missing": "Erfordert die Installation von {app}" }, "vicinae": { "description": "Schreibt {filepath} und lädt neu", - "description-missing": "Erfordert {app} Starter" + "description-missing": "Erfordert die Installation von {app}" }, "discord": { "description": "Schreibt {filepath} für {client}", @@ -579,7 +570,7 @@ }, "pywalfox": { "description": "Schreibt {filepath} und führt pywalfox update aus", - "description-missing": "Erfordert pywalfox Paket" + "description-missing": "Erfordert die Installation von {app} " } }, "misc": { @@ -605,7 +596,7 @@ }, "search": { "label": "Nach einem Standort suchen", - "description": "z.B. Berlin, Deutschland", + "description": "z.B. Dortmund, Deutschland", "placeholder": "Standortnamen eingeben" } }, @@ -634,7 +625,7 @@ }, "week-numbers": { "label": "Wochennummern anzeigen", - "description": "Zeigt die Woche des Jahres (z.B. Woche 38) im Kalender an." + "description": "Zeigt die Kalender Wochen an (z.B. Woche 38)" } } }, @@ -727,7 +718,7 @@ "description_plural": "Ein Dankeschön an unsere {count} großartigen Mitwirkenden!" } }, - "support": "Unterstützen Sie uns" + "support": "Unterstütz uns" }, "hooks": { "title": "Hooks", @@ -792,6 +783,10 @@ "description": "Deaktivieren Sie alle Animationen für eine schnellere und reaktionsfreudigere Erfahrung.", "label": "UI-Animationen deaktivieren" }, + "panels-overlay": { + "label": "Panels in Overlay-Ebene öffnen", + "description": "Panels werden über Vollbildfenstern angezeigt" + }, "animation-speed": { "description": "Globale Animationsgeschwindigkeit anpassen.", "label": "Animationsgeschwindigkeit", @@ -802,10 +797,6 @@ "label": "Eckenradius", "reset": "Rahmenradius zurücksetzen" }, - "compact-lockscreen": { - "description": "Zeige nur die Login-Eingabe und Systemsteuerung, blende Wetter- und Medien-Widgets aus.", - "label": "Kompakter Sperrbildschirm" - }, "dim-desktop": { "description": "Den Desktop abdunkeln, wenn Bedienfelder oder Menüs geöffnet sind.", "label": "Dim Desktop" @@ -824,6 +815,17 @@ "description": "Tooltips in der gesamten Benutzeroberfläche aktivieren oder deaktivieren.", "label": "Tooltips anzeigen" } + }, + "lock-screen": { + "compact-lockscreen": { + "description": "Zeige nur die Login-Eingabe und Systemsteuerung, blende Wetter- und Medien-Widgets aus.", + "label": "Kompakter Sperrbildschirm" + }, + "lock-on-suspend": { + "description": "Den Bildschirm beim Suspendieren des Systems automatisch sperren.", + "label": "Sperren beim Ruhezustand" + }, + "title": "Sperrbildschirm" } }, "general": { @@ -836,7 +838,7 @@ "mainly-clear": "Überwiegend klar", "partly-cloudy": "Teilweise bewölkt", "overcast": "Bedeckt", - "fog": "Nebel", + "fog": "Nebelig", "drizzle": "Nieselregen", "snow": "Schnee", "rain-showers": "Regenschauer", @@ -852,7 +854,7 @@ "select-file": "Datei auswählen", "cancel": "Abbrechen", "search-placeholder": "Dateien und Ordner suchen...", - "select-current": "Aktuelle auswählen", + "select-current": "Aktuelles Objekt auswählen", "title": "Dateiauswahl" }, "datetime-tokens": { diff --git a/Assets/Translations/en.json b/Assets/Translations/en.json index 88acd5c9..b4814d84 100644 --- a/Assets/Translations/en.json +++ b/Assets/Translations/en.json @@ -2,6 +2,7 @@ "settings": { "general": { "title": "General", + "launch-setup-wizard": "Launch the setup wizard", "profile": { "section": { "label": "Profile", @@ -32,16 +33,6 @@ "reset": "Reset screen corners radius" } }, - "lockscreen": { - "section": { - "label": "Lock screen", - "description": "Configure lock screen behavior." - }, - "lock-on-suspend": { - "label": "Lock on suspend", - "description": "Automatically lock the screen when suspending the system." - } - }, "fonts": { "reset-scaling": "Reset scaling", "section": { @@ -806,10 +797,6 @@ "label": "Dim desktop", "description": "Dim the desktop when panels or menus are open." }, - "compact-lockscreen": { - "label": "Compact lock screen", - "description": "Show only the login input and system controls, hiding weather and media widgets." - }, "border-radius": { "label": "Border radius", "description": "Controls the corner roundness of windows, buttons, and other elements.", @@ -823,6 +810,21 @@ "animation-disable": { "label": "Disable UI Animations", "description": "Disable all animations for a faster, more responsive experience." + }, + "panels-overlay": { + "label": "Open panels in overlay layer", + "description": "Panels will appear above fullscreen windows" + } + }, + "lock-screen": { + "title": "Lock screen", + "compact-lockscreen": { + "label": "Compact lock screen", + "description": "Show only the login input and system controls, hiding weather and media widgets." + }, + "lock-on-suspend": { + "label": "Lock on suspend", + "description": "Automatically lock the screen when suspending the system." } } }, diff --git a/Assets/Translations/es.json b/Assets/Translations/es.json index 68d2010c..10bc18bc 100644 --- a/Assets/Translations/es.json +++ b/Assets/Translations/es.json @@ -32,16 +32,6 @@ "reset": "Restablecer el radio de las esquinas de la pantalla" } }, - "lockscreen": { - "section": { - "label": "Pantalla de bloqueo", - "description": "Configura el comportamiento de la pantalla de bloqueo." - }, - "lock-on-suspend": { - "label": "Bloquear al suspender", - "description": "Bloquear automáticamente la pantalla al suspender el sistema." - } - }, "fonts": { "section": { "label": "Fuentes", @@ -79,7 +69,8 @@ "description": "Selecciona el idioma utilizado en la interfaz de la aplicación.", "auto-detect": "Automático" } - } + }, + "launch-setup-wizard": "Inicie el asistente de configuración" }, "audio": { "title": "Audio", @@ -513,6 +504,13 @@ "switch": { "label": "Modo oscuro", "description": "Cambia a un tema más oscuro para una visualización más fácil por la noche." + }, + "mode": { + "description": "Permite el cambio automático entre el modo claro y el modo oscuro.", + "label": "Programación del modo oscuro", + "location": "Ubicación", + "manual": "Manual", + "off": "Apagado" } }, "predefined": { @@ -785,6 +783,10 @@ "description": "Desactiva todas las animaciones para una experiencia más rápida y con mayor capacidad de respuesta.", "label": "Desactivar animaciones de la interfaz de usuario" }, + "panels-overlay": { + "label": "Abrir paneles en capa superpuesta", + "description": "Los paneles aparecerán sobre las ventanas en pantalla completa" + }, "animation-speed": { "description": "Ajustar la velocidad global de la animación.", "label": "Velocidad de animación", @@ -795,10 +797,6 @@ "label": "Radio de borde", "reset": "Restablecer el radio del borde" }, - "compact-lockscreen": { - "description": "Mostrar solo el campo de inicio de sesión y los controles del sistema, ocultando los widgets del clima y multimedia.", - "label": "Pantalla de bloqueo compacta" - }, "dim-desktop": { "description": "Atenuar el escritorio cuando los paneles o menús estén abiertos.", "label": "Dim escritorio" @@ -817,6 +815,17 @@ "description": "Activar o desactivar los avisos emergentes en toda la interfaz.", "label": "Mostrar sugerencias" } + }, + "lock-screen": { + "compact-lockscreen": { + "description": "Mostrar solo el campo de inicio de sesión y los controles del sistema, ocultando los widgets del clima y multimedia.", + "label": "Pantalla de bloqueo compacta" + }, + "lock-on-suspend": { + "description": "Bloquear la pantalla automáticamente al suspender el sistema.", + "label": "Bloquear al suspender" + }, + "title": "Pantalla de bloqueo" } }, "widgets": { diff --git a/Assets/Translations/fr.json b/Assets/Translations/fr.json index b20b2308..790220ed 100644 --- a/Assets/Translations/fr.json +++ b/Assets/Translations/fr.json @@ -32,16 +32,6 @@ "reset": "Réinitialiser le rayon des coins de l'écran" } }, - "lockscreen": { - "section": { - "label": "Écran de verrouillage", - "description": "Configurer le comportement de l'écran de verrouillage." - }, - "lock-on-suspend": { - "label": "Verrouiller lors de la suspension", - "description": "Verrouiller automatiquement l'écran lors de la mise en veille du système." - } - }, "fonts": { "section": { "label": "Polices", @@ -79,7 +69,8 @@ "description": "Sélectionnez la langue utilisée dans l'interface de l'application.", "auto-detect": "Automatique" } - } + }, + "launch-setup-wizard": "Lancer l'assistant d'installation" }, "audio": { "title": "Audio", @@ -513,6 +504,13 @@ "switch": { "label": "Mode sombre", "description": "Passe à un thème plus sombre pour une visualisation plus facile la nuit." + }, + "mode": { + "description": "Active la commutation automatique entre le mode clair et le mode sombre.", + "label": "Programmation du mode sombre", + "location": "Emplacement", + "manual": "Manuel", + "off": "Éteint" } }, "predefined": { @@ -785,6 +783,10 @@ "description": "Désactiver toutes les animations pour une expérience plus rapide et plus réactive.", "label": "Désactiver les animations de l'interface utilisateur" }, + "panels-overlay": { + "label": "Ouvrir les panneaux en couche superposée", + "description": "Les panneaux apparaîtront au-dessus des fenêtres en plein écran" + }, "animation-speed": { "description": "Ajuster la vitesse globale de l'animation.", "label": "Vitesse d'animation", @@ -795,10 +797,6 @@ "label": "Rayon de bordure", "reset": "Réinitialiser le rayon de la bordure" }, - "compact-lockscreen": { - "description": "Afficher uniquement le champ de saisie de connexion et les commandes système, en masquant les widgets météo et multimédia.", - "label": "Écran de verrouillage compact" - }, "dim-desktop": { "description": "Atténuer le bureau lorsque des panneaux ou des menus sont ouverts.", "label": "Dim bureau" @@ -817,6 +815,17 @@ "description": "Activer ou désactiver les info-bulles dans toute l'interface.", "label": "Afficher les infobulles" } + }, + "lock-screen": { + "compact-lockscreen": { + "description": "Afficher uniquement le champ de saisie de connexion et les commandes système, en masquant les widgets météo et multimédia.", + "label": "Écran de verrouillage compact" + }, + "lock-on-suspend": { + "description": "Verrouiller automatiquement l'écran lors de la mise en veille du système.", + "label": "Verrouiller à la suspension" + }, + "title": "Écran de verrouillage" } }, "widgets": { diff --git a/Assets/Translations/pt.json b/Assets/Translations/pt.json index d78a4ec6..0be6fdf2 100644 --- a/Assets/Translations/pt.json +++ b/Assets/Translations/pt.json @@ -32,16 +32,6 @@ "reset": "Redefinir raio dos cantos da tela" } }, - "lockscreen": { - "section": { - "label": "Tela de bloqueio", - "description": "Configure o comportamento da tela de bloqueio." - }, - "lock-on-suspend": { - "label": "Bloquear ao suspender", - "description": "Bloquear automaticamente a tela ao suspender o sistema." - } - }, "fonts": { "section": { "label": "Fontes", @@ -79,7 +69,8 @@ "description": "Selecione o idioma usado na interface da aplicação.", "auto-detect": "Automático" } - } + }, + "launch-setup-wizard": "Iniciar o assistente de configuração" }, "audio": { "title": "Áudio", @@ -475,6 +466,13 @@ "switch": { "label": "Modo escuro", "description": "Muda para um tema mais escuro para facilitar a visualização à noite." + }, + "mode": { + "description": "Ativa a mudança automática entre o modo claro e o modo escuro.", + "label": "Agendamento do modo escuro", + "location": "Localização", + "manual": "Manual", + "off": "Desligado" } }, "predefined": { @@ -785,6 +783,10 @@ "description": "Desative todas as animações para uma experiência mais rápida e responsiva.", "label": "Desativar animações da interface do usuário" }, + "panels-overlay": { + "label": "Abrir painéis na camada de sobreposição", + "description": "Os painéis aparecerão sobre janelas em tela cheia" + }, "animation-speed": { "description": "Ajustar a velocidade global da animação.", "label": "Velocidade da animação", @@ -795,10 +797,6 @@ "label": "Raio da borda", "reset": "Redefinir raio da borda" }, - "compact-lockscreen": { - "description": "Mostrar apenas a entrada de login e os controles do sistema, ocultando widgets de clima e mídia.", - "label": "Tela de bloqueio compacta" - }, "dim-desktop": { "description": "Escurecer a área de trabalho quando painéis ou menus estiverem abertos.", "label": "Dim área de trabalho" @@ -817,6 +815,17 @@ "description": "Ativar ou desativar dicas de ferramentas em toda a interface.", "label": "Mostrar dicas de ferramenta" } + }, + "lock-screen": { + "compact-lockscreen": { + "description": "Mostrar apenas a entrada de login e os controles do sistema, ocultando os widgets de clima e mídia.", + "label": "Tela de bloqueio compacta" + }, + "lock-on-suspend": { + "description": "Bloquear a tela automaticamente ao suspender o sistema.", + "label": "Bloquear ao suspender" + }, + "title": "Tela de bloqueio" } }, "widgets": { diff --git a/Assets/Translations/zh-CN.json b/Assets/Translations/zh-CN.json index 0159a41a..90c4cecf 100644 --- a/Assets/Translations/zh-CN.json +++ b/Assets/Translations/zh-CN.json @@ -32,16 +32,6 @@ "reset": "重置屏幕圆角半径" } }, - "lockscreen": { - "section": { - "label": "锁屏", - "description": "配置锁屏行为。" - }, - "lock-on-suspend": { - "label": "挂起时锁定", - "description": "在系统挂起时自动锁定屏幕。" - } - }, "fonts": { "reset-scaling": "恢复默认缩放", "section": { @@ -79,7 +69,8 @@ "description": "选择应用程序界面中使用的语言。", "auto-detect": "自动检测" } - } + }, + "launch-setup-wizard": "启动安装向导" }, "audio": { "title": "音频", @@ -513,6 +504,13 @@ "switch": { "label": "深色模式", "description": "切换到更暗的主题,便于夜间观看。" + }, + "mode": { + "description": "启用自动切换浅色和深色模式。", + "label": "深色模式计划", + "location": "位置", + "manual": "手册", + "off": "关" } }, "predefined": { @@ -785,6 +783,10 @@ "description": "禁用所有动画以获得更快、更流畅的体验。", "label": "禁用 UI 动画" }, + "panels-overlay": { + "label": "在覆盖层中打开面板", + "description": "面板将显示在全屏窗口上方" + }, "animation-speed": { "description": "调整全局动画速度。", "label": "动画速度", @@ -795,10 +797,6 @@ "label": "边框半径", "reset": "重置边框半径" }, - "compact-lockscreen": { - "description": "仅显示登录输入和系统控制,隐藏天气和媒体小部件。", - "label": "紧凑型锁屏" - }, "dim-desktop": { "description": "当面板或菜单打开时,桌面变暗。", "label": "昏暗的桌面" @@ -817,6 +815,17 @@ "description": "启用或禁用整个界面的工具提示。", "label": "显示工具提示" } + }, + "lock-screen": { + "compact-lockscreen": { + "description": "仅显示登录输入和系统控制,隐藏天气和媒体小部件。", + "label": "紧凑型锁屏" + }, + "lock-on-suspend": { + "description": "系统挂起时自动锁定屏幕。", + "label": "挂起时锁定" + }, + "title": "锁屏" } }, "widgets": { diff --git a/Assets/settings-default.json b/Assets/settings-default.json index 57033237..1bf17a16 100644 --- a/Assets/settings-default.json +++ b/Assets/settings-default.json @@ -179,24 +179,23 @@ "network": { "wifiEnabled": true }, - "notifications": { - "doNotDisturb": false, - "monitors": [], - "location": "top_right", - "alwaysOnTop": false, - "lastSeenTs": 0, - "respectExpireTimeout": false, - "lowUrgencyDuration": 3, - "normalUrgencyDuration": 8, - "criticalUrgencyDuration": 15 - }, - "osd": { - "enabled": true, - "location": "top_right", - "monitors": [], - "autoHideMs": 2000, - "alwaysOnTop": false - }, + "notifications": { + "doNotDisturb": false, + "monitors": [], + "location": "top_right", + "overlayLayer": true, + "respectExpireTimeout": false, + "lowUrgencyDuration": 3, + "normalUrgencyDuration": 8, + "criticalUrgencyDuration": 15 + }, + "osd": { + "enabled": true, + "location": "top_right", + "monitors": [], + "autoHideMs": 2000, + "overlayLayer": true + }, "audio": { "volumeStep": 5, "volumeOverdrive": false, @@ -210,7 +209,8 @@ "fontFixed": "DejaVu Sans Mono", "fontDefaultScale": 1, "fontFixedScale": 1, - "tooltipsEnabled": true + "tooltipsEnabled": true, + "panelsOverlayLayer": true }, "brightness": { "brightnessStep": 5 diff --git a/Bin/calendar-events.py b/Bin/calendar-events.py new file mode 100755 index 00000000..febfe2ed --- /dev/null +++ b/Bin/calendar-events.py @@ -0,0 +1,135 @@ +#!/usr/bin/env python3 +import gi + +gi.require_version('EDataServer', '1.2') +gi.require_version('ECal', '2.0') +import json +import sys +import time +from datetime import datetime, timezone + +from gi.repository import ECal, EDataServer + +start_time = int(sys.argv[1]) +end_time = int(sys.argv[2]) + +print(f"Starting with time range: {start_time} to {end_time}", file=sys.stderr) + +all_events = [] + +def safe_get_time(ical_time): + """Safely get time from ICalTime object""" + if not ical_time: + return None + + try: + year = ical_time.get_year() + month = ical_time.get_month() + day = ical_time.get_day() + + if year < 1970 or year > 2100 or month < 1 or month > 12 or day < 1 or day > 31: + return None + + if ical_time.is_date(): + local_struct = time.struct_time((year, month, day, 0, 0, 0, 0, 0, -1)) + return int(time.mktime(local_struct)) + + hour = ical_time.get_hour() + minute = ical_time.get_minute() + second = ical_time.get_second() + + dt = datetime(year, month, day, hour, minute, second, tzinfo=timezone.utc) + return int(dt.timestamp()) + except Exception: + return None + +print("Getting registry...", file=sys.stderr) +registry = EDataServer.SourceRegistry.new_sync(None) +print("Registry obtained", file=sys.stderr) + +sources = registry.list_sources(EDataServer.SOURCE_EXTENSION_CALENDAR) +print(f"Found {len(sources)} calendar sources", file=sys.stderr) + +for source in sources: + if not source.get_enabled(): + print(f"Skipping disabled calendar: {source.get_display_name()}", file=sys.stderr) + continue + + calendar_name = source.get_display_name() + print(f"\nProcessing calendar: {calendar_name}", file=sys.stderr) + + try: + print(f" Connecting to {calendar_name}...", file=sys.stderr) + client = ECal.Client.connect_sync( + source, + ECal.ClientSourceType.EVENTS, + 30, + None + ) + print(f" Connected to {calendar_name}", file=sys.stderr) + + start_dt = datetime.fromtimestamp(start_time, tz=timezone.utc) + end_dt = datetime.fromtimestamp(end_time, tz=timezone.utc) + + start_str = start_dt.strftime("%Y%m%dT%H%M%SZ") + end_str = end_dt.strftime("%Y%m%dT%H%M%SZ") + + query = f'(occur-in-time-range? (make-time "{start_str}") (make-time "{end_str}"))' + print(f" Query: {query}", file=sys.stderr) + + print(f" Getting object list for {calendar_name}...", file=sys.stderr) + success, ical_objects = client.get_object_list_sync(query, None) + print(f" Got object list for {calendar_name}: success={success}, count={len(ical_objects) if ical_objects else 0}", file=sys.stderr) + + if not success or not ical_objects: + print(f" No events found in {calendar_name}", file=sys.stderr) + continue + + print(f" Processing {len(ical_objects)} events from {calendar_name}...", file=sys.stderr) + for idx, ical_obj in enumerate(ical_objects): + try: + if hasattr(ical_obj, 'get_summary'): + comp = ical_obj + else: + comp = ECal.Component.new_from_string(ical_obj) + + if not comp: + continue + + summary = comp.get_summary() or "(No title)" + + start_timestamp = safe_get_time(comp.get_dtstart()) + if start_timestamp is None: + continue + + end_timestamp = safe_get_time(comp.get_dtend()) + if end_timestamp is None or end_timestamp == start_timestamp: + end_timestamp = start_timestamp + 3600 + + location = comp.get_location() or "" + description = comp.get_description() or "" + + all_events.append({ + 'summary': summary, + 'start': start_timestamp, + 'end': end_timestamp, + 'location': location, + 'description': description, + 'calendar': calendar_name + }) + + if (idx + 1) % 10 == 0: + print(f" Processed {idx + 1} events from {calendar_name}...", file=sys.stderr) + except Exception as e: + print(f" Error processing event {idx} in {calendar_name}: {e}", file=sys.stderr) + continue + + print(f" Finished processing {calendar_name}, found {len([e for e in all_events if e['calendar'] == calendar_name])} events", file=sys.stderr) + + except Exception as e: + print(f" Error for {calendar_name}: {e}", file=sys.stderr) + +print(f"\nSorting {len(all_events)} total events...", file=sys.stderr) +all_events.sort(key=lambda x: x['start']) +print("Done! Outputting JSON...", file=sys.stderr) +print(json.dumps(all_events)) diff --git a/Bin/check-calendar.py b/Bin/check-calendar.py new file mode 100755 index 00000000..463d2e40 --- /dev/null +++ b/Bin/check-calendar.py @@ -0,0 +1,11 @@ +#!/usr/bin/env python3 +import gi + +gi.require_version('EDataServer', '1.2') +gi.require_version('ECal', '2.0') + +try: + from gi.repository import ECal, EDataServer + print("available") +except ImportError as e: + print(f"unavailable: {e}") diff --git a/Bin/list-calendars.py b/Bin/list-calendars.py new file mode 100755 index 00000000..12c67bd1 --- /dev/null +++ b/Bin/list-calendars.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python3 +import gi + +gi.require_version('EDataServer', '1.2') +import json + +from gi.repository import EDataServer + +registry = EDataServer.SourceRegistry.new_sync(None) +sources = registry.list_sources(EDataServer.SOURCE_EXTENSION_CALENDAR) + +calendars = [] +for source in sources: + if source.get_enabled(): + calendars.append({ + 'uid': source.get_uid(), + 'name': source.get_display_name(), + 'enabled': True + }) + +print(json.dumps(calendars)) diff --git a/Commons/I18n.qml b/Commons/I18n.qml index 4ec5e86f..59eeb922 100644 --- a/Commons/I18n.qml +++ b/Commons/I18n.qml @@ -8,7 +8,6 @@ import qs.Commons Singleton { id: root - property bool isLoaded: false property string langCode: "" property string systemDetectedLangCode: "" diff --git a/Commons/Settings.qml b/Commons/Settings.qml index 2a392176..9e7a92d8 100644 --- a/Commons/Settings.qml +++ b/Commons/Settings.qml @@ -312,8 +312,7 @@ Singleton { property bool doNotDisturb: false property list monitors: [] property string location: "top_right" - property bool alwaysOnTop: false - property real lastSeenTs: 0 + property bool overlayLayer: true property bool respectExpireTimeout: false property int lowUrgencyDuration: 3 property int normalUrgencyDuration: 8 @@ -326,7 +325,7 @@ Singleton { property string location: "top_right" property list monitors: [] property int autoHideMs: 2000 - property bool alwaysOnTop: false + property bool overlayLayer: true } // audio @@ -346,6 +345,7 @@ Singleton { property real fontDefaultScale: 1.0 property real fontFixedScale: 1.0 property bool tooltipsEnabled: true + property bool panelsOverlayLayer: true } // brightness diff --git a/Commons/TablerIcons.qml b/Commons/TablerIcons.qml index a840d72f..59b226d3 100644 --- a/Commons/TablerIcons.qml +++ b/Commons/TablerIcons.qml @@ -119,6 +119,7 @@ Singleton { "settings-notifications": "bell", "settings-osd": "picture-in-picture", "settings-about": "info-square-rounded", + "settings-lock-screen": "lock", "bluetooth": "bluetooth", "bt-device-generic": "bluetooth", "bt-device-headphones": "headphones", diff --git a/Modules/Bar/Calendar/CalendarPanel.qml b/Modules/Bar/Calendar/CalendarPanel.qml index b882324d..888552bb 100644 --- a/Modules/Bar/Calendar/CalendarPanel.qml +++ b/Modules/Bar/Calendar/CalendarPanel.qml @@ -10,6 +10,7 @@ import qs.Widgets NPanel { id: root + property ShellScreen screen readonly property var now: Time.date preferredWidth: (Settings.data.location.showWeekNumberInCalendar ? 400 : 380) * Style.uiScaleRatio @@ -347,6 +348,14 @@ NPanel { grid.year = newDate.getFullYear() grid.month = newDate.getMonth() content.isCurrentMonth = content.checkIsCurrentMonth() + const now = new Date() + const monthStart = new Date(grid.year, grid.month, 1) + const monthEnd = new Date(grid.year, grid.month + 1, 0) + + const daysBehind = Math.max(0, Math.ceil((now - monthStart) / (24 * 60 * 60 * 1000))) + const daysAhead = Math.max(0, Math.ceil((monthEnd - now) / (24 * 60 * 60 * 1000))) + + CalendarService.loadEvents(daysAhead + 30, daysBehind + 30) } } NIconButton { @@ -355,6 +364,7 @@ NPanel { grid.month = Time.date.getMonth() grid.year = Time.date.getFullYear() content.isCurrentMonth = true + CalendarService.loadEvents() } } NIconButton { @@ -364,6 +374,14 @@ NPanel { grid.year = newDate.getFullYear() grid.month = newDate.getMonth() content.isCurrentMonth = content.checkIsCurrentMonth() + const now = new Date() + const monthStart = new Date(grid.year, grid.month, 1) + const monthEnd = new Date(grid.year, grid.month + 1, 0) + + const daysBehind = Math.max(0, Math.ceil((now - monthStart) / (24 * 60 * 60 * 1000))) + const daysAhead = Math.max(0, Math.ceil((monthEnd - now) / (24 * 60 * 60 * 1000))) + + CalendarService.loadEvents(daysAhead + 30, daysBehind + 30) } } } @@ -405,6 +423,71 @@ NPanel { Layout.fillWidth: true Layout.fillHeight: true spacing: 0 + + // Helper function to check if a date has events + function hasEventsOnDate(year, month, day) { + if (!CalendarService.available || CalendarService.events.length === 0) + return false + + const targetDate = new Date(year, month, day) + const targetStart = new Date(targetDate.getFullYear(), targetDate.getMonth(), targetDate.getDate()).getTime() / 1000 + const targetEnd = targetStart + 86400 // +24 hours + + return CalendarService.events.some(event => { + // Check if event starts or overlaps with this day + return (event.start >= targetStart && event.start < targetEnd) || (event.end > targetStart && event.end <= targetEnd) || (event.start < targetStart && event.end > targetEnd) + }) + } + + // Helper function to get events for a specific date + function getEventsForDate(year, month, day) { + if (!CalendarService.available || CalendarService.events.length === 0) + return [] + + const targetDate = new Date(year, month, day) + const targetStart = Math.floor(new Date(targetDate.getFullYear(), targetDate.getMonth(), targetDate.getDate()).getTime() / 1000) + const targetEnd = targetStart + 86400 // +24 hours + + return CalendarService.events.filter(event => { + return (event.start >= targetStart && event.start < targetEnd) || (event.end > targetStart && event.end <= targetEnd) || (event.start < targetStart && event.end > targetEnd) + }) + } + + // Helper function to check if an event is all-day + function isAllDayEvent(event) { + const duration = event.end - event.start + const startDate = new Date(event.start * 1000) + const isAtMidnight = startDate.getHours() === 0 && startDate.getMinutes() === 0 + return duration === 86400 && isAtMidnight + } + + // Helper function to check if an event is multi-day + function isMultiDayEvent(event) { + if (isAllDayEvent(event)) { + return false + } + + const startDate = new Date(event.start * 1000) + const endDate = new Date(event.end * 1000) + + const startDateOnly = new Date(startDate.getFullYear(), startDate.getMonth(), startDate.getDate()) + const endDateOnly = new Date(endDate.getFullYear(), endDate.getMonth(), endDate.getDate()) + + return startDateOnly.getTime() !== endDateOnly.getTime() + } + + // Helper function to get color for a specific event + function getEventColor(event, isToday) { + if (isMultiDayEvent(event)) { + return isToday ? Color.mOnSecondary : Color.mTertiary + } else if (isAllDayEvent(event)) { + return isToday ? Color.mOnSecondary : Color.mSecondary + } else { + return isToday ? Color.mOnSecondary : Color.mPrimary + } + } + + // Column of week numbers ColumnLayout { visible: Settings.data.location.showWeekNumberInCalendar Layout.preferredWidth: visible ? Style.baseWidgetSize * 0.7 : 0 @@ -475,6 +558,55 @@ NPanel { pointSize: Style.fontSizeM font.weight: model.today ? Style.fontWeightBold : Style.fontWeightMedium } + + // Event indicator dots + Row { + visible: parent.parent.parent.parent.parent.hasEventsOnDate(model.year, model.month, model.day) + spacing: 2 + anchors.horizontalCenter: parent.horizontalCenter + anchors.bottom: parent.bottom + anchors.bottomMargin: Style.marginXS + + readonly property int currentYear: model.year + readonly property int currentMonth: model.month + readonly property int currentDay: model.day + readonly property bool isToday: model.today + + Repeater { + model: parent.parent.parent.parent.parent.parent.getEventsForDate(parent.currentYear, parent.currentMonth, parent.currentDay) + + Rectangle { + width: 4 + height: width + radius: width / 2 + color: parent.parent.parent.parent.parent.parent.getEventColor(modelData, model.today) + } + } + } + + MouseArea { + anchors.fill: parent + hoverEnabled: true + + onEntered: { + const events = parent.parent.parent.parent.parent.getEventsForDate(model.year, model.month, model.day) + if (events.length > 0) { + const summaries = events.map(e => e.summary).join('\n') + TooltipService.show(Screen, parent, summaries) + TooltipService.updateText(summaries) + } + } + + onClicked: { + const dateWithSlashes = `${model.month.toString().padStart(2, '0')}/${model.day.toString().padStart(2, '0')}/${model.year.toString().substring(2)}` + Quickshell.execDetached(["gnome-calendar", "--date", dateWithSlashes]) + } + + onExited: { + TooltipService.hide() + } + } + Behavior on color { ColorAnimation { duration: Style.animationFast diff --git a/Modules/Bar/Extras/BarPill.qml b/Modules/Bar/Extras/BarPill.qml index ba1601d3..aef33339 100644 --- a/Modules/Bar/Extras/BarPill.qml +++ b/Modules/Bar/Extras/BarPill.qml @@ -16,8 +16,7 @@ Item { property bool autoHide: false property bool forceOpen: false property bool forceClose: false - property bool disableOpen: false - property bool rightOpen: false + property bool oppositeDirection: false property bool hovered: false readonly property string barPosition: Settings.data.bar.position @@ -51,8 +50,7 @@ Item { autoHide: root.autoHide forceOpen: root.forceOpen forceClose: root.forceClose - disableOpen: root.disableOpen - rightOpen: root.rightOpen + oppositeDirection: root.oppositeDirection hovered: root.hovered density: root.density onShown: root.shown() @@ -76,8 +74,7 @@ Item { autoHide: root.autoHide forceOpen: root.forceOpen forceClose: root.forceClose - disableOpen: root.disableOpen - rightOpen: root.rightOpen + oppositeDirection: root.oppositeDirection hovered: root.hovered density: root.density onShown: root.shown() diff --git a/Modules/Bar/Extras/BarPillHorizontal.qml b/Modules/Bar/Extras/BarPillHorizontal.qml index 5a732263..e5e5ccda 100644 --- a/Modules/Bar/Extras/BarPillHorizontal.qml +++ b/Modules/Bar/Extras/BarPillHorizontal.qml @@ -18,8 +18,7 @@ Item { property bool autoHide: false property bool forceOpen: false property bool forceClose: false - property bool disableOpen: false - property bool rightOpen: false + property bool oppositeDirection: false property bool hovered: false // Effective shown state (true if hovered/animated open or forced) @@ -79,18 +78,18 @@ Item { width: revealed ? pillMaxWidth : 1 height: pillHeight - x: rightOpen ? (iconCircle.x + iconCircle.width / 2) : // Opens right - (iconCircle.x + iconCircle.width / 2) - width // Opens left + x: oppositeDirection ? (iconCircle.x + iconCircle.width / 2) : // Opens right + (iconCircle.x + iconCircle.width / 2) - width // Opens left opacity: revealed ? Style.opacityFull : Style.opacityNone color: Settings.data.bar.showCapsule ? Color.mSurfaceVariant : Color.transparent readonly property int halfPillHeight: Math.round(pillHeight * 0.5) - topLeftRadius: rightOpen ? 0 : halfPillHeight - bottomLeftRadius: rightOpen ? 0 : halfPillHeight - topRightRadius: rightOpen ? halfPillHeight : 0 - bottomRightRadius: rightOpen ? halfPillHeight : 0 + topLeftRadius: oppositeDirection ? 0 : halfPillHeight + bottomLeftRadius: oppositeDirection ? 0 : halfPillHeight + topRightRadius: oppositeDirection ? halfPillHeight : 0 + bottomRightRadius: oppositeDirection ? halfPillHeight : 0 anchors.verticalCenter: parent.verticalCenter NText { @@ -99,10 +98,10 @@ Item { x: { // Better text horizontal centering var centerX = (parent.width - width) / 2 - var offset = rightOpen ? Style.marginXS : -Style.marginXS + var offset = oppositeDirection ? Style.marginXS : -Style.marginXS if (forceOpen) { // If its force open, the icon disc background is the same color as the bg pill move text slightly - offset += rightOpen ? -Style.marginXXS : Style.marginXXS + offset += oppositeDirection ? -Style.marginXXS : Style.marginXXS } return centerX + offset } @@ -139,7 +138,7 @@ Item { color: hovered ? Color.mTertiary : Settings.data.bar.showCapsule ? Color.mSurfaceVariant : Color.transparent anchors.verticalCenter: parent.verticalCenter - x: rightOpen ? 0 : (parent.width - width) + x: oppositeDirection ? 0 : (parent.width - width) Behavior on color { ColorAnimation { @@ -245,7 +244,7 @@ Item { hovered = true root.entered() TooltipService.show(Screen, pill, root.tooltipText, BarService.getTooltipDirection(), Style.tooltipDelayLong) - if (disableOpen || forceClose) { + if (forceClose) { return } if (!forceOpen) { diff --git a/Modules/Bar/Extras/BarPillVertical.qml b/Modules/Bar/Extras/BarPillVertical.qml index 485d9732..fe94a534 100644 --- a/Modules/Bar/Extras/BarPillVertical.qml +++ b/Modules/Bar/Extras/BarPillVertical.qml @@ -16,8 +16,7 @@ Item { property bool autoHide: false property bool forceOpen: false property bool forceClose: false - property bool disableOpen: false - property bool rightOpen: false + property bool oppositeDirection: false property bool hovered: false // Bar position detection for pill direction @@ -25,8 +24,8 @@ Item { readonly property bool isVerticalBar: barPosition === "left" || barPosition === "right" // Determine pill direction based on section position - readonly property bool openDownward: rightOpen - readonly property bool openUpward: !rightOpen + readonly property bool openDownward: oppositeDirection + readonly property bool openUpward: !oppositeDirection // Effective shown state (true if animated open or forced, but not if force closed) readonly property bool revealed: !forceClose && (forceOpen || showPill) @@ -114,7 +113,7 @@ Item { var offset = openDownward ? Math.round(pillPaddingVertical * 0.75) : -Math.round(pillPaddingVertical * 0.75) if (forceOpen) { // If its force open, the icon disc background is the same color as the bg pill move text slightly - offset += rightOpen ? -Style.marginXXS : Style.marginXXS + offset += oppositeDirection ? -Style.marginXXS : Style.marginXXS } return offset } @@ -284,7 +283,7 @@ Item { hovered = true root.entered() TooltipService.show(Screen, pill, root.tooltipText, BarService.getTooltipDirection(), Style.tooltipDelayLong) - if (disableOpen || forceClose) { + if (forceClose) { return } if (!forceOpen) { diff --git a/Modules/Bar/Widgets/ActiveWindow.qml b/Modules/Bar/Widgets/ActiveWindow.qml index 2058bf1a..ae639ffb 100644 --- a/Modules/Bar/Widgets/ActiveWindow.qml +++ b/Modules/Bar/Widgets/ActiveWindow.qml @@ -45,12 +45,12 @@ Item { readonly property string windowTitle: CompositorService.getFocusedWindowTitle() || "No active window" readonly property string fallbackIcon: "user-desktop" - implicitHeight: visible ? (isVerticalBar ? calculatedVerticalDimension() : Style.barHeight) : 0 - implicitWidth: visible ? (isVerticalBar ? calculatedVerticalDimension() : dynamicWidth) : 0 + implicitHeight: visible ? (isVerticalBar ? (((!hasFocusedWindow) && hideMode === "hidden") ? 0 : calculatedVerticalDimension()) : Style.capsuleHeight) : 0 + implicitWidth: visible ? (isVerticalBar ? (((!hasFocusedWindow) && hideMode === "hidden") ? 0 : calculatedVerticalDimension()) : (((!hasFocusedWindow) && hideMode === "hidden") ? 0 : dynamicWidth)) : 0 // "visible": Always Visible, "hidden": Hide When Empty, "transparent": Transparent When Empty - visible: hideMode !== "hidden" || hasFocusedWindow - opacity: hideMode !== "transparent" || hasFocusedWindow ? 1.0 : 0 + visible: (hideMode !== "hidden" || hasFocusedWindow) || opacity > 0 + opacity: ((hideMode !== "hidden" || hasFocusedWindow) && (hideMode !== "transparent" || hasFocusedWindow)) ? 1.0 : 0.0 Behavior on opacity { NumberAnimation { duration: Style.animationNormal @@ -58,6 +58,20 @@ Item { } } + Behavior on implicitWidth { + NumberAnimation { + duration: Style.animationNormal + easing.type: Easing.InOutCubic + } + } + + Behavior on implicitHeight { + NumberAnimation { + duration: Style.animationNormal + easing.type: Easing.InOutCubic + } + } + function calculatedVerticalDimension() { return Math.round((Style.baseWidgetSize - 5) * scaling) } @@ -93,7 +107,7 @@ Item { } // Otherwise, adapt to content if (!hasFocusedWindow) { - return maxWidth + return Math.min(calculateContentWidth(), maxWidth) } // Use content width but don't exceed user-set maximum width return Math.min(calculateContentWidth(), maxWidth) @@ -155,10 +169,9 @@ Item { Rectangle { id: windowActiveRect visible: root.visible - anchors.left: parent.left anchors.verticalCenter: parent.verticalCenter - width: isVerticalBar ? root.width : dynamicWidth - height: isVerticalBar ? width : Style.capsuleHeight + width: isVerticalBar ? ((!hasFocusedWindow) && hideMode === "hidden" ? 0 : calculatedVerticalDimension()) : ((!hasFocusedWindow) && (hideMode === "hidden") ? 0 : dynamicWidth) + height: isVerticalBar ? ((!hasFocusedWindow) && hideMode === "hidden" ? 0 : calculatedVerticalDimension()) : Style.capsuleHeight radius: isVerticalBar ? width / 2 : Style.radiusM color: Settings.data.bar.showCapsule ? Color.mSurfaceVariant : Color.transparent @@ -307,6 +320,14 @@ Item { font.weight: Style.fontWeightMedium verticalAlignment: Text.AlignVCenter color: Color.mOnSurface + onTextChanged: { + if (root.scrollingMode === "always") { + titleContainer.isScrolling = false + titleContainer.isResetting = false + scrollContainer.scrollX = 0 + scrollStartTimer.restart() + } + } } // Second copy for seamless scrolling @@ -343,13 +364,6 @@ Item { easing.type: Easing.Linear } } - - Behavior on Layout.preferredWidth { - NumberAnimation { - duration: Style.animationSlow - easing.type: Easing.InOutCubic - } - } } } diff --git a/Modules/Bar/Widgets/Battery.qml b/Modules/Bar/Widgets/Battery.qml index d29c817e..e19e564f 100644 --- a/Modules/Bar/Widgets/Battery.qml +++ b/Modules/Bar/Widgets/Battery.qml @@ -87,14 +87,13 @@ Item { id: pill density: Settings.data.bar.density - rightOpen: BarService.getPillDirection(root) + oppositeDirection: BarService.getPillDirection(root) icon: testMode ? BatteryService.getIcon(testPercent, testCharging, true) : BatteryService.getIcon(percent, charging, isReady) text: (isReady || testMode) ? Math.round(percent) : "-" suffix: "%" autoHide: false forceOpen: isReady && (testMode || battery.isLaptopBattery) && displayMode === "alwaysShow" - forceClose: displayMode === "alwaysHide" - disableOpen: (!isReady || (!testMode && !battery.isLaptopBattery)) + forceClose: displayMode === "alwaysHide" || !isReady || (!testMode && !battery.isLaptopBattery) onClicked: PanelService.getPanel("batteryPanel")?.toggle(this) tooltipText: { let lines = [] diff --git a/Modules/Bar/Widgets/Bluetooth.qml b/Modules/Bar/Widgets/Bluetooth.qml index e6f50f0f..1c3fd2b0 100644 --- a/Modules/Bar/Widgets/Bluetooth.qml +++ b/Modules/Bar/Widgets/Bluetooth.qml @@ -1,27 +1,66 @@ import QtQuick -import QtQuick.Layouts -import QtQuick.Controls import Quickshell -import Quickshell.Wayland import qs.Commons import qs.Services -import qs.Widgets +import qs.Modules.Bar.Extras -NIconButton { +Item { id: root property ShellScreen screen - baseSize: Style.capsuleHeight - applyUiScale: false - density: Settings.data.bar.density - colorBg: Settings.data.bar.showCapsule ? Color.mSurfaceVariant : Color.transparent - colorFg: Color.mOnSurface - colorBorder: Color.transparent - colorBorderHover: Color.transparent - tooltipText: I18n.tr("tooltips.bluetooth-devices") - tooltipDirection: BarService.getTooltipDirection() - icon: BluetoothService.enabled ? "bluetooth" : "bluetooth-off" - onClicked: PanelService.getPanel("bluetoothPanel")?.toggle(this) - onRightClicked: PanelService.getPanel("bluetoothPanel")?.toggle(this) + // Widget properties passed from Bar.qml for per-instance settings + property string widgetId: "" + property string section: "" + property int sectionWidgetIndex: -1 + property int sectionWidgetsCount: 0 + + property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId] + property var widgetSettings: { + if (section && sectionWidgetIndex >= 0) { + var widgets = Settings.data.bar.widgets[section] + if (widgets && sectionWidgetIndex < widgets.length) { + return widgets[sectionWidgetIndex] + } + } + return {} + } + + readonly property bool isBarVertical: Settings.data.bar.position === "left" || Settings.data.bar.position === "right" + readonly property string displayMode: widgetSettings.displayMode !== undefined ? widgetSettings.displayMode : widgetMetadata.displayMode + + implicitWidth: pill.width + implicitHeight: pill.height + + BarPill { + id: pill + + density: Settings.data.bar.density + oppositeDirection: BarService.getPillDirection(root) + icon: BluetoothService.enabled ? "bluetooth" : "bluetooth-off" + text: { + if (BluetoothService.connectedDevices && BluetoothService.connectedDevices.length > 0) { + const firstDevice = BluetoothService.connectedDevices[0] + return firstDevice.name || firstDevice.deviceName + } + return "" + } + suffix: { + if (BluetoothService.connectedDevices && BluetoothService.connectedDevices.length > 1) { + return ` + ${BluetoothService.connectedDevices.length - 1}` + } + return "" + } + autoHide: false + forceOpen: !isBarVertical && root.displayMode === "alwaysShow" + forceClose: isBarVertical || root.displayMode === "alwaysHide" || BluetoothService.connectedDevices.length === 0 + onClicked: PanelService.getPanel("bluetoothPanel")?.toggle(this) + onRightClicked: PanelService.getPanel("bluetoothPanel")?.toggle(this) + tooltipText: { + if (pill.text !== "") { + return pill.text + } + return I18n.tr("tooltips.bluetooth-devices") + } + } } diff --git a/Modules/Bar/Widgets/Brightness.qml b/Modules/Bar/Widgets/Brightness.qml index 5c02e490..60efc609 100644 --- a/Modules/Bar/Widgets/Brightness.qml +++ b/Modules/Bar/Widgets/Brightness.qml @@ -77,7 +77,7 @@ Item { id: pill density: Settings.data.bar.density - rightOpen: BarService.getPillDirection(root) + oppositeDirection: BarService.getPillDirection(root) icon: getIcon() autoHide: false // Important to be false so we can hover as long as we want text: { diff --git a/Modules/Bar/Widgets/CustomButton.qml b/Modules/Bar/Widgets/CustomButton.qml index 4015c4f4..652d391a 100644 --- a/Modules/Bar/Widgets/CustomButton.qml +++ b/Modules/Bar/Widgets/CustomButton.qml @@ -45,14 +45,13 @@ Item { BarPill { id: pill - rightOpen: BarService.getPillDirection(root) + oppositeDirection: BarService.getPillDirection(root) icon: customIcon text: _dynamicText density: Settings.data.bar.density autoHide: false forceOpen: _dynamicText !== "" - forceClose: false - disableOpen: true + forceClose: true tooltipText: { if (!hasExec) { return "Custom button, configure in settings." diff --git a/Modules/Bar/Widgets/KeyboardLayout.qml b/Modules/Bar/Widgets/KeyboardLayout.qml index 6d597227..41ffd027 100644 --- a/Modules/Bar/Widgets/KeyboardLayout.qml +++ b/Modules/Bar/Widgets/KeyboardLayout.qml @@ -43,7 +43,7 @@ Item { anchors.verticalCenter: parent.verticalCenter density: Settings.data.bar.density - rightOpen: BarService.getPillDirection(root) + oppositeDirection: BarService.getPillDirection(root) icon: "keyboard" autoHide: false // Important to be false so we can hover as long as we want text: currentLayout.toUpperCase() diff --git a/Modules/Bar/Widgets/Microphone.qml b/Modules/Bar/Widgets/Microphone.qml index 86338210..6fe3d709 100644 --- a/Modules/Bar/Widgets/Microphone.qml +++ b/Modules/Bar/Widgets/Microphone.qml @@ -89,7 +89,7 @@ Item { BarPill { id: pill - rightOpen: BarService.getPillDirection(root) + oppositeDirection: BarService.getPillDirection(root) icon: getIcon() density: Settings.data.bar.density autoHide: false // Important to be false so we can hover as long as we want diff --git a/Modules/Bar/Widgets/NotificationHistory.qml b/Modules/Bar/Widgets/NotificationHistory.qml index 6d6652fe..ccceeaa3 100644 --- a/Modules/Bar/Widgets/NotificationHistory.qml +++ b/Modules/Bar/Widgets/NotificationHistory.qml @@ -31,12 +31,8 @@ NIconButton { readonly property bool showUnreadBadge: (widgetSettings.showUnreadBadge !== undefined) ? widgetSettings.showUnreadBadge : widgetMetadata.showUnreadBadge readonly property bool hideWhenZero: (widgetSettings.hideWhenZero !== undefined) ? widgetSettings.hideWhenZero : widgetMetadata.hideWhenZero - function lastSeenTs() { - return Settings.data.notifications?.lastSeenTs || 0 - } - function computeUnreadCount() { - var since = lastSeenTs() + var since = NotificationService.lastSeenTs var count = 0 var model = NotificationService.historyList for (var i = 0; i < model.count; i++) { diff --git a/Modules/Bar/Widgets/TaskbarGrouped.qml b/Modules/Bar/Widgets/TaskbarGrouped.qml new file mode 100644 index 00000000..616032c8 --- /dev/null +++ b/Modules/Bar/Widgets/TaskbarGrouped.qml @@ -0,0 +1,223 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import Quickshell.Wayland +import Quickshell.Widgets +import qs.Commons +import qs.Services +import qs.Widgets + +Item { + id: root + + property ShellScreen screen + + // Widget properties passed from Bar.qml for per-instance settings + property string widgetId: "" + property string section: "" + property int sectionWidgetIndex: -1 + property int sectionWidgetsCount: 0 + + readonly property bool isVerticalBar: Settings.data.bar.position === "left" || Settings.data.bar.position === "right" + readonly property string density: Settings.data.bar.density + readonly property real itemSize: (density === "compact") ? Style.capsuleHeight * 0.9 : Style.capsuleHeight * 0.8 + property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId] + property var widgetSettings: { + if (section && sectionWidgetIndex >= 0) { + var widgets = Settings.data.bar.widgets[section] + if (widgets && sectionWidgetIndex < widgets.length) { + return widgets[sectionWidgetIndex] + } + } + return {} + } + readonly property bool hideUnoccupied: (widgetSettings.hideUnoccupied !== undefined) ? widgetSettings.hideUnoccupied : false + property ListModel localWorkspaces: ListModel {} + + function refreshWorkspaces() { + localWorkspaces.clear() + if (!screen) + return + + const screenName = screen.name.toLowerCase() + + for (var i = 0; i < CompositorService.workspaces.count; i++) { + const ws = CompositorService.workspaces.get(i) + + if (ws.output.toLowerCase() !== screenName) + continue + if (hideUnoccupied && !ws.isOccupied && !ws.isFocused) + continue + + // Copy all properties from ws and add windows + var workspaceData = Object.assign({}, ws) + workspaceData.windows = CompositorService.getWindowsForWorkspace(ws.id) + + localWorkspaces.append(workspaceData) + } + } + + Component.onCompleted: { + refreshWorkspaces() + } + + onScreenChanged: refreshWorkspaces() + + implicitWidth: isVerticalBar ? taskbarGrid.implicitWidth + Style.marginM * 2 : Math.round(taskbarGrid.implicitWidth + Style.marginM * 2) + implicitHeight: isVerticalBar ? Math.round(taskbarGrid.implicitHeight + Style.marginM * 2) : Style.barHeight + + Connections { + target: CompositorService + + function onWorkspacesChanged() { + refreshWorkspaces() + } + + function onWindowListChanged() { + refreshWorkspaces() + } + } + + Component { + id: workspaceRepeaterDelegate + + Rectangle { + id: container + + required property var model + property var workspaceModel: model + property bool hasWindows: workspaceModel.windows.count > 0 + + radius: Style.radiusS + border.color: workspaceModel.isFocused ? Color.mPrimary : Color.mOutline + border.width: 1 + width: (hasWindows ? iconsFlow.implicitWidth : root.itemSize * 0.8) + (root.isVerticalBar ? Style.marginXS : Style.marginL) + height: (hasWindows ? iconsFlow.implicitHeight : root.itemSize * 0.8) + (root.isVerticalBar ? Style.marginL : Style.marginXS) + color: Settings.data.bar.showCapsule ? Color.mSurfaceVariant : Color.transparent + + MouseArea { + anchors.fill: parent + hoverEnabled: true + enabled: !hasWindows + cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor + onClicked: { + CompositorService.switchToWorkspace(workspaceModel) + } + } + + Flow { + id: iconsFlow + + anchors.centerIn: parent + spacing: 4 + flow: root.isVerticalBar ? Flow.TopToBottom : Flow.LeftToRight + + Repeater { + model: workspaceModel.windows + + delegate: Item { + id: taskbarItem + + property bool itemHovered: false + + width: root.itemSize * 0.8 + height: root.itemSize * 0.8 + + // Smooth scale animation on hover + scale: itemHovered ? 1.1 : 1.0 + + Behavior on scale { + NumberAnimation { + duration: Style.animationNormal + easing.type: Easing.OutBack + } + } + + IconImage { + id: appIcon + + width: parent.width + height: parent.height + source: ThemeIcons.iconForAppId(model.appId) + smooth: true + asynchronous: true + opacity: model.isFocused ? Style.opacityFull : 0.6 + layer.enabled: widgetSettings.colorizeIcons === true + + Behavior on opacity { + NumberAnimation { + duration: Style.animationNormal + easing.type: Easing.InOutCubic + } + } + + Rectangle { + id: focusIndicator + anchors.bottomMargin: -2 + anchors.bottom: parent.bottom + anchors.horizontalCenter: parent.horizontalCenter + width: model.isFocused ? 4 : 0 + height: model.isFocused ? 4 : 0 + color: model.isFocused ? Color.mPrimary : Color.transparent + radius: width * 0.5 + } + + layer.effect: ShaderEffect { + property color targetColor: Color.mOnSurface + property real colorizeMode: 0 + fragmentShader: Qt.resolvedUrl(Quickshell.shellDir + "/Shaders/qsb/appicon_colorize.frag.qsb") + } + } + + MouseArea { + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + acceptedButtons: Qt.LeftButton | Qt.RightButton + + onPressed: function (mouse) { + if (!model) { + return + } + + if (mouse.button === Qt.LeftButton) { + CompositorService.focusWindow(model) + } else if (mouse.button === Qt.RightButton) { + CompositorService.closeWindow(model) + } + } + onEntered: { + taskbarItem.itemHovered = true + TooltipService.show(Screen, taskbarItem, model.title || model.appId || "Unknown app.", BarService.getTooltipDirection()) + } + onExited: { + taskbarItem.itemHovered = false + TooltipService.hide() + } + } + } + } + } + } + } + + Flow { + id: taskbarGrid + + anchors.verticalCenter: isVerticalBar ? undefined : parent.verticalCenter + anchors.left: isVerticalBar ? undefined : parent.left + anchors.leftMargin: isVerticalBar ? 0 : Style.marginM + anchors.horizontalCenter: isVerticalBar ? parent.horizontalCenter : undefined + anchors.top: isVerticalBar ? parent.top : undefined + anchors.topMargin: isVerticalBar ? Style.marginM : 0 + + spacing: Style.marginS + flow: isVerticalBar ? Flow.TopToBottom : Flow.LeftToRight + + Repeater { + model: localWorkspaces + delegate: workspaceRepeaterDelegate + } + } +} diff --git a/Modules/Bar/Widgets/Volume.qml b/Modules/Bar/Widgets/Volume.qml index 10018771..edf75b9d 100644 --- a/Modules/Bar/Widgets/Volume.qml +++ b/Modules/Bar/Widgets/Volume.qml @@ -75,7 +75,7 @@ Item { id: pill density: Settings.data.bar.density - rightOpen: BarService.getPillDirection(root) + oppositeDirection: BarService.getPillDirection(root) icon: getIcon() autoHide: false // Important to be false so we can hover as long as we want text: Math.round(AudioService.volume * 100) diff --git a/Modules/Bar/Widgets/WiFi.qml b/Modules/Bar/Widgets/WiFi.qml index d2b996b5..25de98ae 100644 --- a/Modules/Bar/Widgets/WiFi.qml +++ b/Modules/Bar/Widgets/WiFi.qml @@ -1,46 +1,88 @@ import QtQuick -import QtQuick.Layouts -import QtQuick.Controls import Quickshell -import Quickshell.Wayland import qs.Commons import qs.Services -import qs.Widgets +import qs.Modules.Bar.Extras -NIconButton { +Item { id: root property ShellScreen screen - density: Settings.data.bar.density - baseSize: Style.capsuleHeight - applyUiScale: false - colorBg: (Settings.data.bar.showCapsule ? Color.mSurfaceVariant : Color.transparent) - colorFg: Color.mOnSurface - colorBorder: Color.transparent - colorBorderHover: Color.transparent - tooltipText: I18n.tr("tooltips.manage-wifi") - tooltipDirection: BarService.getTooltipDirection() - icon: { - try { - if (NetworkService.ethernetConnected) { - return "ethernet" + // Widget properties passed from Bar.qml for per-instance settings + property string widgetId: "" + property string section: "" + property int sectionWidgetIndex: -1 + property int sectionWidgetsCount: 0 + + property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId] + property var widgetSettings: { + if (section && sectionWidgetIndex >= 0) { + var widgets = Settings.data.bar.widgets[section] + if (widgets && sectionWidgetIndex < widgets.length) { + return widgets[sectionWidgetIndex] } - let connected = false - let signalStrength = 0 - for (const net in NetworkService.networks) { - if (NetworkService.networks[net].connected) { - connected = true - signalStrength = NetworkService.networks[net].signal - break + } + return {} + } + + readonly property bool isBarVertical: Settings.data.bar.position === "left" || Settings.data.bar.position === "right" + readonly property string displayMode: widgetSettings.displayMode !== undefined ? widgetSettings.displayMode : widgetMetadata.displayMode + + implicitWidth: pill.width + implicitHeight: pill.height + + BarPill { + id: pill + + density: Settings.data.bar.density + oppositeDirection: BarService.getPillDirection(root) + icon: { + try { + if (NetworkService.ethernetConnected) { + return "ethernet" } + let connected = false + let signalStrength = 0 + for (const net in NetworkService.networks) { + if (NetworkService.networks[net].connected) { + connected = true + signalStrength = NetworkService.networks[net].signal + break + } + } + return connected ? NetworkService.signalIcon(signalStrength) : "wifi-off" + } catch (error) { + Logger.e("Wi-Fi", "Error getting icon:", error) + return "signal_wifi_bad" } - return connected ? NetworkService.signalIcon(signalStrength) : "wifi-off" - } catch (error) { - Logger.e("Wi-Fi", "Error getting icon:", error) - return "signal_wifi_bad" + } + text: { + try { + if (NetworkService.ethernetConnected) { + return "" + } + for (const net in NetworkService.networks) { + if (NetworkService.networks[net].connected) { + return net + } + } + return "" + } catch (error) { + Logger.e("Wi-Fi", "Error getting ssid:", error) + return "error" + } + } + autoHide: false + forceOpen: !isBarVertical && root.displayMode === "alwaysShow" + forceClose: isBarVertical || root.displayMode === "alwaysHide" || !pill.text + onClicked: PanelService.getPanel("wifiPanel")?.toggle(this) + onRightClicked: PanelService.getPanel("wifiPanel")?.toggle(this) + tooltipText: { + if (pill.text !== "") { + return pill.text + } + return I18n.tr("tooltips.manage-wifi") } } - onClicked: PanelService.getPanel("wifiPanel")?.toggle(this) - onRightClicked: PanelService.getPanel("wifiPanel")?.toggle(this) } diff --git a/Modules/LockScreen/LockScreen.qml b/Modules/LockScreen/LockScreen.qml index 09a91ff8..2b8add42 100644 --- a/Modules/LockScreen/LockScreen.qml +++ b/Modules/LockScreen/LockScreen.qml @@ -17,11 +17,18 @@ Loader { id: lockScreen active: false + // Track if triggered via deprecated IPC call + property bool triggeredViaDeprecatedCall: false + Timer { id: unloadAfterUnlockTimer interval: 250 repeat: false - onTriggered: lockScreen.active = false + onTriggered: { + lockScreen.active = false + // Reset the deprecation flag when unlocking + lockScreen.triggeredViaDeprecatedCall = false + } } function scheduleUnloadAfterUnlock() { @@ -424,6 +431,64 @@ Loader { } } + // Deprecation warning (shown above error notification) + Rectangle { + width: Math.min(650, parent.width - 40) + implicitHeight: deprecationContent.implicitHeight + 24 + height: implicitHeight + anchors.horizontalCenter: parent.horizontalCenter + anchors.bottom: parent.bottom + anchors.bottomMargin: (Settings.data.general.compactLockScreen ? 320 : 400) * Style.uiScaleRatio + radius: Style.radiusL + color: Qt.alpha(Color.mTertiary, 0.95) + border.color: Color.mTertiary + border.width: 2 + visible: lockScreen.triggeredViaDeprecatedCall + opacity: visible ? 1.0 : 0.0 + + ColumnLayout { + id: deprecationContent + anchors.fill: parent + anchors.margins: 12 + spacing: 6 + + RowLayout { + Layout.alignment: Qt.AlignHCenter + spacing: 8 + + NIcon { + icon: "alert-triangle" + pointSize: Style.fontSizeL + color: Color.mOnTertiary + } + + NText { + text: "Deprecated IPC Call" + color: Color.mOnTertiary + pointSize: Style.fontSizeL + font.weight: Font.Bold + } + } + + NText { + text: "The 'lockScreen toggle' IPC call is deprecated. Use 'lockScreen lock' instead." + color: Color.mOnTertiary + pointSize: Style.fontSizeM + horizontalAlignment: Text.AlignHCenter + Layout.alignment: Qt.AlignHCenter + Layout.fillWidth: true + wrapMode: Text.WordWrap + } + } + + Behavior on opacity { + NumberAnimation { + duration: 300 + easing.type: Easing.OutCubic + } + } + } + // Error notification Rectangle { width: 450 diff --git a/Modules/Notification/Notification.qml b/Modules/Notification/Notification.qml index 00b93297..e6ea0702 100644 --- a/Modules/Notification/Notification.qml +++ b/Modules/Notification/Notification.qml @@ -45,7 +45,7 @@ Variants { screen: modelData WlrLayershell.namespace: "noctalia-notifications" - WlrLayershell.layer: (Settings.data.notifications && Settings.data.notifications.alwaysOnTop) ? WlrLayer.Overlay : WlrLayer.Top + WlrLayershell.layer: (Settings.data.notifications && Settings.data.notifications.overlayLayer) ? WlrLayer.Overlay : WlrLayer.Top color: Color.transparent @@ -161,6 +161,7 @@ Variants { // Animate when notifications are added/removed Behavior on implicitHeight { + enabled: !Settings.data.general.animationDisabled SpringAnimation { spring: 2.0 damping: 0.4 @@ -196,6 +197,7 @@ Variants { anchors.right: parent.right height: 2 color: Color.transparent + visible: !Settings.data.general.animationDisabled // Pre-calculate available width for the progress bar readonly property real availableWidth: parent.width - (2 * parent.radius) @@ -222,7 +224,7 @@ Variants { // Smooth progress animation Behavior on width { - enabled: !card.isRemoving // Disable during removal animation + enabled: !card.isRemoving && !Settings.data.general.animationDisabled // Disable during removal animation or when animations disabled NumberAnimation { duration: 100 // Quick but smooth easing.type: Easing.Linear @@ -230,7 +232,7 @@ Variants { } Behavior on x { - enabled: !card.isRemoving + enabled: !card.isRemoving && !Settings.data.general.animationDisabled NumberAnimation { duration: 100 easing.type: Easing.Linear @@ -312,14 +314,21 @@ Variants { // Animate in when the item is created Component.onCompleted: { - // Start from slide position - slideOffset = slideInOffset - scaleValue = 0.8 - opacityValue = 0.0 + if (Settings.data.general.animationDisabled) { + // No animation - set to final state immediately + slideOffset = 0 + scaleValue = 1.0 + opacityValue = 1.0 + } else { + // Start from slide position + slideOffset = slideInOffset + scaleValue = 0.8 + opacityValue = 0.0 - // Delay animation based on index for staggered effect - delayTimer.interval = animationDelay - delayTimer.start() + // Delay animation based on index for staggered effect + delayTimer.interval = animationDelay + delayTimer.start() + } } // Timer for staggered animation start @@ -341,9 +350,11 @@ Variants { return // Prevent multiple animations isRemoving = true - slideOffset = slideOutOffset - scaleValue = 0.8 - opacityValue = 0.0 + if (!Settings.data.general.animationDisabled) { + slideOffset = slideOutOffset + scaleValue = 0.8 + opacityValue = 0.0 + } } // Timer for delayed removal after animation @@ -365,6 +376,7 @@ Variants { // Animation behaviors with spring physics Behavior on scale { + enabled: !Settings.data.general.animationDisabled SpringAnimation { spring: 3 damping: 0.4 @@ -374,6 +386,7 @@ Variants { } Behavior on opacity { + enabled: !Settings.data.general.animationDisabled NumberAnimation { duration: Style.animationNormal easing.type: Easing.OutCubic @@ -381,6 +394,7 @@ Variants { } Behavior on y { + enabled: !Settings.data.general.animationDisabled SpringAnimation { spring: 2.5 damping: 0.3 diff --git a/Modules/Notification/NotificationHistoryPanel.qml b/Modules/Notification/NotificationHistoryPanel.qml index 8bf5583f..b4121199 100644 --- a/Modules/Notification/NotificationHistoryPanel.qml +++ b/Modules/Notification/NotificationHistoryPanel.qml @@ -17,7 +17,7 @@ NPanel { panelKeyboardFocus: true onOpened: function () { - Settings.data.notifications.lastSeenTs = Time.timestamp * 1000 + NotificationService.updateLastSeenTs() } panelContent: Rectangle { @@ -148,6 +148,7 @@ NPanel { border.width: Math.max(1, Style.borderS) Behavior on height { + enabled: !Settings.data.general.animationDisabled NumberAnimation { duration: Style.animationNormal easing.type: Easing.InOutQuad @@ -156,6 +157,7 @@ NPanel { // Smooth color transition on hover Behavior on color { + enabled: !Settings.data.general.animationDisabled ColorAnimation { duration: Style.animationFast } diff --git a/Modules/OSD/OSD.qml b/Modules/OSD/OSD.qml index 5c98c45d..c460b672 100644 --- a/Modules/OSD/OSD.qml +++ b/Modules/OSD/OSD.qml @@ -190,7 +190,7 @@ Variants { color: Color.transparent WlrLayershell.keyboardFocus: WlrKeyboardFocus.None - WlrLayershell.layer: (Settings.data.osd && Settings.data.osd.alwaysOnTop) ? WlrLayer.Overlay : WlrLayer.Top + WlrLayershell.layer: (Settings.data.osd && Settings.data.osd.overlayLayer) ? WlrLayer.Overlay : WlrLayer.Top exclusionMode: PanelWindow.ExclusionMode.Ignore Rectangle { diff --git a/Modules/Settings/Bar/BarWidgetSettingsDialog.qml b/Modules/Settings/Bar/BarWidgetSettingsDialog.qml index 4b35b7fd..ed7562e4 100644 --- a/Modules/Settings/Bar/BarWidgetSettingsDialog.qml +++ b/Modules/Settings/Bar/BarWidgetSettingsDialog.qml @@ -122,6 +122,7 @@ Popup { const widgetSettingsMap = { "ActiveWindow": "WidgetSettings/ActiveWindowSettings.qml", "Battery": "WidgetSettings/BatterySettings.qml", + "Bluetooth": "WidgetSettings/BluetoothSettings.qml", "Brightness": "WidgetSettings/BrightnessSettings.qml", "Clock": "WidgetSettings/ClockSettings.qml", "ControlCenter": "WidgetSettings/ControlCenterSettings.qml", @@ -133,6 +134,7 @@ Popup { "Spacer": "WidgetSettings/SpacerSettings.qml", "SystemMonitor": "WidgetSettings/SystemMonitorSettings.qml", "Volume": "WidgetSettings/VolumeSettings.qml", + "WiFi": "WidgetSettings/WiFiSettings.qml", "Workspace": "WidgetSettings/WorkspaceSettings.qml", "Taskbar": "WidgetSettings/TaskbarSettings.qml", "Tray": "WidgetSettings/TraySettings.qml" diff --git a/Modules/Settings/Bar/WidgetSettings/BluetoothSettings.qml b/Modules/Settings/Bar/WidgetSettings/BluetoothSettings.qml new file mode 100644 index 00000000..d6548476 --- /dev/null +++ b/Modules/Settings/Bar/WidgetSettings/BluetoothSettings.qml @@ -0,0 +1,40 @@ +import QtQuick +import QtQuick.Layouts +import qs.Commons +import qs.Widgets + +ColumnLayout { + id: root + spacing: Style.marginM + + // Properties to receive data from parent + property var widgetData: null + property var widgetMetadata: null + + // Local state + property string valueDisplayMode: widgetData.displayMode !== undefined ? widgetData.displayMode : widgetMetadata.displayMode + + function saveSettings() { + var settings = Object.assign({}, widgetData || {}) + settings.displayMode = valueDisplayMode + return settings + } + + NComboBox { + label: I18n.tr("bar.widget-settings.battery.display-mode.label") + description: I18n.tr("bar.widget-settings.battery.display-mode.description") + minimumWidth: 134 + model: [{ + "key": "onhover", + "name": I18n.tr("options.display-mode.on-hover") + }, { + "key": "alwaysShow", + "name": I18n.tr("options.display-mode.always-show") + }, { + "key": "alwaysHide", + "name": I18n.tr("options.display-mode.always-hide") + }] + currentKey: root.valueDisplayMode + onSelected: key => root.valueDisplayMode = key + } +} diff --git a/Modules/Settings/Bar/WidgetSettings/WiFiSettings.qml b/Modules/Settings/Bar/WidgetSettings/WiFiSettings.qml new file mode 100644 index 00000000..d6548476 --- /dev/null +++ b/Modules/Settings/Bar/WidgetSettings/WiFiSettings.qml @@ -0,0 +1,40 @@ +import QtQuick +import QtQuick.Layouts +import qs.Commons +import qs.Widgets + +ColumnLayout { + id: root + spacing: Style.marginM + + // Properties to receive data from parent + property var widgetData: null + property var widgetMetadata: null + + // Local state + property string valueDisplayMode: widgetData.displayMode !== undefined ? widgetData.displayMode : widgetMetadata.displayMode + + function saveSettings() { + var settings = Object.assign({}, widgetData || {}) + settings.displayMode = valueDisplayMode + return settings + } + + NComboBox { + label: I18n.tr("bar.widget-settings.battery.display-mode.label") + description: I18n.tr("bar.widget-settings.battery.display-mode.description") + minimumWidth: 134 + model: [{ + "key": "onhover", + "name": I18n.tr("options.display-mode.on-hover") + }, { + "key": "alwaysShow", + "name": I18n.tr("options.display-mode.always-show") + }, { + "key": "alwaysHide", + "name": I18n.tr("options.display-mode.always-hide") + }] + currentKey: root.valueDisplayMode + onSelected: key => root.valueDisplayMode = key + } +} diff --git a/Modules/Settings/SettingsPanel.qml b/Modules/Settings/SettingsPanel.qml index e5b64566..47ebc40a 100644 --- a/Modules/Settings/SettingsPanel.qml +++ b/Modules/Settings/SettingsPanel.qml @@ -26,6 +26,7 @@ NPanel { Audio, Bar, ColorScheme, + LockScreen, ControlCenter, OSD, Display, @@ -118,6 +119,11 @@ NPanel { id: userInterfaceTab UserInterfaceTab {} } + Component { + id: lockScreenTab + LockScreenTab {} + } + // Order *DOES* matter function updateTabsModel() { let newTabs = [{ @@ -150,6 +156,11 @@ NPanel { "label": "settings.launcher.title", "icon": "settings-launcher", "source": launcherTab + }, { + "id": SettingsPanel.Tab.LockScreen, + "label": "settings.lock-screen.title", + "icon": "settings-lock-screen", + "source": lockScreenTab }, { "id": SettingsPanel.Tab.Audio, "label": "settings.audio.title", diff --git a/Modules/Settings/Tabs/ColorSchemeTab.qml b/Modules/Settings/Tabs/ColorSchemeTab.qml index 314cc07d..bbd02a8e 100644 --- a/Modules/Settings/Tabs/ColorSchemeTab.qml +++ b/Modules/Settings/Tabs/ColorSchemeTab.qml @@ -159,7 +159,6 @@ ColumnLayout { label: I18n.tr("settings.color-scheme.dark-mode.switch.label") description: I18n.tr("settings.color-scheme.dark-mode.switch.description") checked: Settings.data.colorSchemes.darkMode - enabled: true onToggled: checked => { Settings.data.colorSchemes.darkMode = checked root.cacheVersion++ // Force UI update for dark/light variants @@ -241,6 +240,7 @@ ColumnLayout { NToggle { label: I18n.tr("settings.color-scheme.color-source.use-wallpaper-colors.label") description: I18n.tr("settings.color-scheme.color-source.use-wallpaper-colors.description") + enabled: ProgramCheckerService.matugenAvailable checked: Settings.data.colorSchemes.useWallpaperColors onToggled: checked => { if (checked) { @@ -665,24 +665,22 @@ ColumnLayout { } } NCheckbox { - label: "Vicinae" - description: ProgramCheckerService.vicinaeAvailable - ? I18n.tr("settings.color-scheme.templates.programs.vicinae.description", { - "filepath": "~/.local/share/vicinae/themes/matugen.toml" - }) - : I18n.tr("settings.color-scheme.templates.programs.vicinae.description-missing", { - "app": "vicinae" - }) - checked: Settings.data.templates.vicinae - enabled: ProgramCheckerService.vicinaeAvailable - opacity: ProgramCheckerService.vicinaeAvailable ? 1.0 : 0.6 - onToggled: checked => { - if (ProgramCheckerService.vicinaeAvailable) { - Settings.data.templates.vicinae = checked - AppThemeService.generate() - } - } -} + label: "Vicinae" + description: ProgramCheckerService.vicinaeAvailable ? I18n.tr("settings.color-scheme.templates.programs.vicinae.description", { + "filepath": "~/.local/share/vicinae/themes/matugen.toml" + }) : I18n.tr("settings.color-scheme.templates.programs.vicinae.description-missing", { + "app": "vicinae" + }) + checked: Settings.data.templates.vicinae + enabled: ProgramCheckerService.vicinaeAvailable + opacity: ProgramCheckerService.vicinaeAvailable ? 1.0 : 0.6 + onToggled: checked => { + if (ProgramCheckerService.vicinaeAvailable) { + Settings.data.templates.vicinae = checked + AppThemeService.generate() + } + } + } } // Miscellaneous diff --git a/Modules/Settings/Tabs/GeneralTab.qml b/Modules/Settings/Tabs/GeneralTab.qml index 40b5276d..d750f3c7 100644 --- a/Modules/Settings/Tabs/GeneralTab.qml +++ b/Modules/Settings/Tabs/GeneralTab.qml @@ -204,20 +204,24 @@ ColumnLayout { Layout.fillWidth: true label: I18n.tr("settings.general.language.select.label") description: I18n.tr("settings.general.language.select.description") - model: [ - { "key": "", "name": I18n.tr("settings.general.language.select.auto-detect") + " (" + I18n.systemDetectedLangCode + ")" } - ].concat(I18n.availableLanguages.map(function(langCode) { - return { "key": langCode, "name": langCode } - })) + model: [{ + "key": "", + "name": I18n.tr("settings.general.language.select.auto-detect") + " (" + I18n.systemDetectedLangCode + ")" + }].concat(I18n.availableLanguages.map(function (langCode) { + return { + "key": langCode, + "name": langCode + } + })) currentKey: Settings.data.general.language onSelected: key => { - Settings.data.general.language = key - if (key === "") { - I18n.detectLanguage() // Re-detect system language if "Automatic" is selected - } else { - I18n.setLanguage(key) // Set specific language - } - } + Settings.data.general.language = key + if (key === "") { + I18n.detectLanguage() // Re-detect system language if "Automatic" is selected + } else { + I18n.setLanguage(key) // Set specific language + } + } } } @@ -226,26 +230,13 @@ ColumnLayout { Layout.topMargin: Style.marginXL Layout.bottomMargin: Style.marginXL } - ColumnLayout { - spacing: Style.marginL - Layout.fillWidth: true - NHeader { - label: I18n.tr("settings.general.lockscreen.section.label") - description: I18n.tr("settings.general.lockscreen.section.description") + NButton { + visible: !DistroService.isNixOS + text: I18n.tr("settings.general.launch-setup-wizard") + onClicked: { + setupWizardLoader.active = false + setupWizardLoader.active = true } - - NToggle { - label: I18n.tr("settings.general.lockscreen.lock-on-suspend.label") - description: I18n.tr("settings.general.lockscreen.lock-on-suspend.description") - checked: Settings.data.general.lockOnSuspend - onToggled: Settings.data.general.lockOnSuspend = checked - } - } - - NDivider { - Layout.fillWidth: true - Layout.topMargin: Style.marginXL - Layout.bottomMargin: Style.marginXL } } diff --git a/Modules/Settings/Tabs/LockScreenTab.qml b/Modules/Settings/Tabs/LockScreenTab.qml new file mode 100644 index 00000000..2b9d0ea7 --- /dev/null +++ b/Modules/Settings/Tabs/LockScreenTab.qml @@ -0,0 +1,31 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import qs.Commons +import qs.Services +import qs.Widgets + +ColumnLayout { + id: root + + NToggle { + label: I18n.tr("settings.lock-screen.lock-on-suspend.label") + description: I18n.tr("settings.lock-screen.lock-on-suspend.description") + checked: Settings.data.general.lockOnSuspend + onToggled: Settings.data.general.lockOnSuspend = checked + } + + NToggle { + label: I18n.tr("settings.lock-screen.compact-lockscreen.label") + description: I18n.tr("settings.lock-screen.compact-lockscreen.description") + checked: Settings.data.general.compactLockScreen + onToggled: checked => Settings.data.general.compactLockScreen = checked + } + + NDivider { + Layout.fillWidth: true + Layout.topMargin: Style.marginXL + Layout.bottomMargin: Style.marginXL + } +} diff --git a/Modules/Settings/Tabs/NotificationsTab.qml b/Modules/Settings/Tabs/NotificationsTab.qml index beae8cdc..316b620d 100644 --- a/Modules/Settings/Tabs/NotificationsTab.qml +++ b/Modules/Settings/Tabs/NotificationsTab.qml @@ -68,8 +68,8 @@ ColumnLayout { NToggle { label: I18n.tr("settings.notifications.settings.always-on-top.label") description: I18n.tr("settings.notifications.settings.always-on-top.description") - checked: Settings.data.notifications.alwaysOnTop - onToggled: checked => Settings.data.notifications.alwaysOnTop = checked + checked: Settings.data.notifications.overlayLayer + onToggled: checked => Settings.data.notifications.overlayLayer = checked } // OSD settings moved to the dedicated OSD tab diff --git a/Modules/Settings/Tabs/OsdTab.qml b/Modules/Settings/Tabs/OsdTab.qml index edb16fd3..365b2396 100644 --- a/Modules/Settings/Tabs/OsdTab.qml +++ b/Modules/Settings/Tabs/OsdTab.qml @@ -86,8 +86,8 @@ ColumnLayout { NToggle { label: I18n.tr("settings.osd.always-on-top.label") description: I18n.tr("settings.osd.always-on-top.description") - checked: Settings.data.osd.alwaysOnTop - onToggled: checked => Settings.data.osd.alwaysOnTop = checked + checked: Settings.data.osd.overlayLayer + onToggled: checked => Settings.data.osd.overlayLayer = checked } NLabel { diff --git a/Modules/Settings/Tabs/UserInterfaceTab.qml b/Modules/Settings/Tabs/UserInterfaceTab.qml index 13557c29..e3190927 100644 --- a/Modules/Settings/Tabs/UserInterfaceTab.qml +++ b/Modules/Settings/Tabs/UserInterfaceTab.qml @@ -34,10 +34,10 @@ ColumnLayout { } NToggle { - label: I18n.tr("settings.user-interface.compact-lockscreen.label") - description: I18n.tr("settings.user-interface.compact-lockscreen.description") - checked: Settings.data.general.compactLockScreen - onToggled: checked => Settings.data.general.compactLockScreen = checked + label: I18n.tr("settings.user-interface.panels-overlay.label") + description: I18n.tr("settings.user-interface.panels-overlay.description") + checked: Settings.data.ui.panelsOverlayLayer + onToggled: checked => Settings.data.ui.panelsOverlayLayer = checked } NDivider { diff --git a/Modules/SetupWizard/SetupAppearanceStep.qml b/Modules/SetupWizard/SetupAppearanceStep.qml index e247e070..a59b2ad3 100644 --- a/Modules/SetupWizard/SetupAppearanceStep.qml +++ b/Modules/SetupWizard/SetupAppearanceStep.qml @@ -124,14 +124,14 @@ ColumnLayout { spacing: 2 NText { - text: I18n.tr("settings.color-scheme.color-source.dark-mode.label") + text: I18n.tr("settings.color-scheme.dark-mode.switch.label") pointSize: Style.fontSizeL font.weight: Style.fontWeightBold color: Color.mOnSurface } NText { - text: I18n.tr("settings.color-scheme.color-source.dark-mode.description") + text: I18n.tr("settings.color-scheme.dark-mode.switch.description") pointSize: Style.fontSizeS color: Color.mOnSurfaceVariant wrapMode: Text.WordWrap @@ -167,7 +167,7 @@ ColumnLayout { color: Color.mSurface NIcon { - icon: "color-picker" + icon: ProgramCheckerService.matugenAvailable ? "color-picker" : "alert-triangle" pointSize: Style.fontSizeL color: Color.mPrimary anchors.centerIn: parent @@ -196,7 +196,6 @@ ColumnLayout { NToggle { enabled: ProgramCheckerService.matugenAvailable - opacity: ProgramCheckerService.matugenAvailable ? 1.0 : 0.6 checked: Settings.data.colorSchemes.useWallpaperColors && ProgramCheckerService.matugenAvailable onToggled: checked => { if (!ProgramCheckerService.matugenAvailable) @@ -214,34 +213,6 @@ ColumnLayout { } } - // Matugen not available notice - RowLayout { - Layout.fillWidth: true - spacing: Style.marginS - visible: !ProgramCheckerService.matugenAvailable - - Rectangle { - width: 28 - height: 28 - radius: Style.radiusM - color: Color.mSurface - NIcon { - icon: "alert-triangle" - pointSize: Style.fontSizeL - color: Color.mPrimary - anchors.centerIn: parent - } - } - NText { - text: I18n.tr("settings.color-scheme.color-source.use-wallpaper-colors.description") - // Reuse description; availability is visually indicated - pointSize: Style.fontSizeS - color: Color.mOnSurfaceVariant - wrapMode: Text.WordWrap - Layout.fillWidth: true - } - } - // Matugen scheme type (visible when wallpaper colors enabled and matugen available) ColumnLayout { Layout.fillWidth: true diff --git a/Modules/Toast/ToastScreen.qml b/Modules/Toast/ToastScreen.qml index badb00b6..fafd9e04 100644 --- a/Modules/Toast/ToastScreen.qml +++ b/Modules/Toast/ToastScreen.qml @@ -197,7 +197,7 @@ Item { color: Color.transparent - WlrLayershell.layer: WlrLayer.Overlay + WlrLayershell.layer: (Settings.data.notifications && Settings.data.notifications.overlayLayer) ? WlrLayer.Overlay : WlrLayer.Top WlrLayershell.keyboardFocus: WlrKeyboardFocus.None exclusionMode: PanelWindow.ExclusionMode.Ignore diff --git a/Services/AppThemeService.qml b/Services/AppThemeService.qml index ac5eb72a..c54662ce 100644 --- a/Services/AppThemeService.qml +++ b/Services/AppThemeService.qml @@ -70,8 +70,8 @@ Singleton { "vicinae": { "input": "vicinae.toml", "outputs": [{ - "path": "~/.local/share/vicinae/themes/matugen.toml" - }], + "path": "~/.local/share/vicinae/themes/matugen.toml" + }], "postProcess": () => `cp -n ${Quickshell.shellDir}/Assets/noctalia.svg ~/.local/share/vicinae/themes/noctalia.svg && ${colorsApplyScript} vicinae\n` } }) diff --git a/Services/BarWidgetRegistry.qml b/Services/BarWidgetRegistry.qml index a5823db6..f825f2b5 100644 --- a/Services/BarWidgetRegistry.qml +++ b/Services/BarWidgetRegistry.qml @@ -30,6 +30,7 @@ Singleton { "Spacer": spacerComponent, "SystemMonitor": systemMonitorComponent, "Taskbar": taskbarComponent, + "TaskbarGrouped": taskbarGroupedComponent, "Tray": trayComponent, "Volume": volumeComponent, "WiFi": wiFiComponent, @@ -53,6 +54,10 @@ Singleton { "displayMode": "onhover", "warningThreshold": 30 }, + "Bluetooth": { + "allowUserSettings": true, + "displayMode": "onhover" + }, "Brightness": { "allowUserSettings": true, "displayMode": "onhover" @@ -124,11 +129,18 @@ Singleton { "hideMode": "hidden", "colorizeIcons": false }, + "TaskbarGrouped": { + "allowUserSettings": true + }, "Tray": { "allowUserSettings": true, "blacklist": [], "colorizeIcons": false }, + "WiFi": { + "allowUserSettings": true, + "displayMode": "onhover" + }, "Workspace": { "allowUserSettings": true, "labelMode": "index", @@ -216,6 +228,9 @@ Singleton { property Component taskbarComponent: Component { Taskbar {} } + property Component taskbarGroupedComponent: Component { + TaskbarGrouped {} + } function init() { Logger.i("BarWidgetRegistry", "Service started") diff --git a/Services/BluetoothService.qml b/Services/BluetoothService.qml index d6c56e79..f6a42743 100644 --- a/Services/BluetoothService.qml +++ b/Services/BluetoothService.qml @@ -21,6 +21,12 @@ Singleton { return dev && (dev.paired || dev.trusted) }) } + readonly property var connectedDevices: { + if (!adapter || !adapter.devices) { + return [] + } + return adapter.devices.values.filter(dev => dev && dev.connected) + } readonly property var allDevicesWithBattery: { if (!adapter || !adapter.devices) { diff --git a/Services/CalendarService.qml b/Services/CalendarService.qml new file mode 100644 index 00000000..b152d177 --- /dev/null +++ b/Services/CalendarService.qml @@ -0,0 +1,250 @@ +pragma Singleton + +import QtQuick +import Quickshell +import Quickshell.Io +import qs.Commons + +Singleton { + id: root + + // Core state + property var events: ([]) + property bool loading: false + property bool available: false + property string lastError: "" + property var calendars: ([]) + + // Persistent cache + property string cacheFile: Settings.cacheDir + "calendar.json" + + // Python scripts + readonly property string checkCalendarAvailableScript: Quickshell.shellDir + '/Bin/check-calendar.py' + readonly property string listCalendarsScript: Quickshell.shellDir + '/Bin/list-calendars.py' + readonly property string calendarEventsScript: Quickshell.shellDir + '/Bin/calendar-events.py' + + // Cache file handling + FileView { + id: cacheFileView + path: root.cacheFile + printErrors: false + + JsonAdapter { + id: cacheAdapter + property var cachedEvents: ([]) + property var cachedCalendars: ([]) + property string lastUpdate: "" + } + + onLoadFailed: { + cacheAdapter.cachedEvents = ([]) + cacheAdapter.cachedCalendars = ([]) + cacheAdapter.lastUpdate = "" + } + + onLoaded: { + loadFromCache() + } + } + + Component.onCompleted: { + Logger.i("Calendar", "Service initialized") + loadFromCache() + checkAvailability() + } + + // Save cache with debounce + Timer { + id: saveDebounce + interval: 1000 + onTriggered: cacheFileView.writeAdapter() + } + + function saveCache() { + saveDebounce.restart() + } + + // Load events and calendars from cache + function loadFromCache() { + if (cacheAdapter.cachedEvents && cacheAdapter.cachedEvents.length > 0) { + root.events = cacheAdapter.cachedEvents + Logger.i("Calendar", `Loaded ${cacheAdapter.cachedEvents.length} cached event(s)`) + } + + if (cacheAdapter.cachedCalendars && cacheAdapter.cachedCalendars.length > 0) { + root.calendars = cacheAdapter.cachedCalendars + Logger.i("Calendar", `Loaded ${cacheAdapter.cachedCalendars.length} cached calendar(s)`) + } + + if (cacheAdapter.lastUpdate) { + Logger.i("Calendar", `Cache last updated: ${cacheAdapter.lastUpdate}`) + } + } + + // Auto-refresh timer (every 5 minutes) + Timer { + id: refreshTimer + interval: 300000 + running: true + repeat: true + onTriggered: loadEvents() + } + + // Core functions + function checkAvailability() { + availabilityCheckProcess.running = true + } + + function loadCalendars() { + listCalendarsProcess.running = true + } + + function loadEvents(daysAhead = 31, daysBehind = 14) { + if (loading) + return + + loading = true + lastError = "" + + const now = new Date() + const startDate = new Date(now.getTime() - (daysBehind * 24 * 60 * 60 * 1000)) + const endDate = new Date(now.getTime() + (daysAhead * 24 * 60 * 60 * 1000)) + + loadEventsProcess.startTime = Math.floor(startDate.getTime() / 1000) + loadEventsProcess.endTime = Math.floor(endDate.getTime() / 1000) + loadEventsProcess.running = true + + Logger.i("Calendar", `Loading events (${daysBehind} days behind, ${daysAhead} days ahead): ${startDate.toLocaleDateString()} to ${endDate.toLocaleDateString()}`) + } + + // Helper to format date/time + function formatDateTime(timestamp) { + const date = new Date(timestamp * 1000) + return Qt.formatDateTime(date, "yyyy-MM-dd hh:mm") + } + + // Process to check for evolution-data-server libraries + Process { + id: availabilityCheckProcess + running: false + command: ["sh", "-c", "command -v python3 >/dev/null 2>&1 && python3 " + root.checkCalendarAvailableScript + " || echo 'unavailable: python3 not installed'"] + + stdout: StdioCollector { + onStreamFinished: { + const result = text.trim() + root.available = result === "available" + + if (root.available) { + Logger.i("Calendar", "EDS libraries available") + loadCalendars() + } else { + Logger.w("Calendar", "EDS libraries not available: " + result) + root.lastError = "Evolution Data Server libraries not installed" + } + } + } + + stderr: StdioCollector { + onStreamFinished: { + if (text.trim()) { + Logger.d("Calendar", "Availability check error: " + text) + root.available = false + root.lastError = "Failed to check library availability" + } + } + } + } + + // Process to list available calendars + Process { + id: listCalendarsProcess + running: false + command: ["python3", root.listCalendarsScript] + + stdout: StdioCollector { + onStreamFinished: { + try { + const result = JSON.parse(text.trim()) + root.calendars = result + cacheAdapter.cachedCalendars = result + saveCache() + + Logger.i("Calendar", `Found ${result.length} calendar(s)`) + + // Auto-load events after discovering calendars + // Only load if we have calendars and no cached events + if (result.length > 0 && root.events.length === 0) { + loadEvents() + } else if (result.length > 0) { + // If we already have cached events, load in background + loadEvents() + } + } catch (e) { + Logger.d("Calendar", "Failed to parse calendars: " + e) + root.lastError = "Failed to parse calendar list" + } + } + } + + stderr: StdioCollector { + onStreamFinished: { + if (text.trim()) { + Logger.d("Calendar", "List calendars error: " + text) + root.lastError = text.trim() + } + } + } + } + + // Process to load events + Process { + id: loadEventsProcess + running: false + property int startTime: 0 + property int endTime: 0 + + command: ["python3", root.calendarEventsScript, startTime.toString(), endTime.toString()] + + stdout: StdioCollector { + onStreamFinished: { + root.loading = false + + try { + const result = JSON.parse(text.trim()) + root.events = result + cacheAdapter.cachedEvents = result + cacheAdapter.lastUpdate = new Date().toISOString() + saveCache() + + Logger.i("Calendar", `Loaded ${result.length} event(s)`) + } catch (e) { + Logger.d("Calendar", "Failed to parse events: " + e) + root.lastError = "Failed to parse events" + + // Fall back to cached events if available + if (cacheAdapter.cachedEvents.length > 0) { + root.events = cacheAdapter.cachedEvents + Logger.i("Calendar", "Using cached events") + } + } + } + } + + stderr: StdioCollector { + onStreamFinished: { + root.loading = false + + if (text.trim()) { + Logger.d("Calendar", "Load events error: " + text) + root.lastError = text.trim() + + // Fall back to cached events if available + if (cacheAdapter.cachedEvents.length > 0) { + root.events = cacheAdapter.cachedEvents + Logger.i("Calendar", "Using cached events due to error") + } + } + } + } + } +} diff --git a/Services/CompositorService.qml b/Services/CompositorService.qml index 307dc9b7..cc752fbd 100644 --- a/Services/CompositorService.qml +++ b/Services/CompositorService.qml @@ -256,6 +256,17 @@ Singleton { return "" } + function getWindowsForWorkspace(workspaceId) { + var windowsInWs = [] + for (var i = 0; i < windows.count; i++) { + var window = windows.get(i) + if (window.workspaceId === workspaceId) { + windowsInWs.push(window) + } + } + return windowsInWs + } + // Generic workspace switching function switchToWorkspace(workspace) { if (backend && backend.switchToWorkspace) { diff --git a/Services/IPCService.qml b/Services/IPCService.qml index 3d1d3924..12c7c204 100644 --- a/Services/IPCService.qml +++ b/Services/IPCService.qml @@ -77,9 +77,26 @@ Item { IpcHandler { target: "lockScreen" - function toggle() { + + // New preferred method - lock the screen + function lock() { // Only lock if not already locked (prevents the red screen issue) // Note: No unlock via IPC for security reasons + if (!lockScreen.active) { + lockScreen.triggeredViaDeprecatedCall = false + lockScreen.active = true + } + } + + // Deprecated: Use 'lockScreen lock' instead + function toggle() { + // Mark as triggered via deprecated call - warning will show in lock screen + lockScreen.triggeredViaDeprecatedCall = true + + // Log deprecation warning for users checking logs + Logger.w("IPC", "The 'lockScreen toggle' IPC call is deprecated. Use 'lockScreen lock' instead.") + + // Still functional for backward compatibility if (!lockScreen.active) { lockScreen.active = true } diff --git a/Services/MatugenTemplates.qml b/Services/MatugenTemplates.qml index 19066213..8740aaf8 100644 --- a/Services/MatugenTemplates.qml +++ b/Services/MatugenTemplates.qml @@ -97,7 +97,8 @@ Singleton { }, { "name": "ghostty", "path": "Terminal/ghostty", - "output": "~/.config/ghostty/themes/noctalia" + "output": "~/.config/ghostty/themes/noctalia", + "post_hook": "pkill -SIGUSR2 ghostty" }, { "name": "kitty", "path": "Terminal/kitty.conf", diff --git a/Services/NotificationService.qml b/Services/NotificationService.qml index 4bb5042d..8a55af35 100644 --- a/Services/NotificationService.qml +++ b/Services/NotificationService.qml @@ -16,6 +16,10 @@ Singleton { property int maxVisible: 5 property int maxHistory: 100 property string historyFile: Quickshell.env("NOCTALIA_NOTIF_HISTORY_FILE") || (Settings.cacheDir + "notifications.json") + property string stateFile: Settings.cacheDir + "notifications-state.json" + + // State + property real lastSeenTs: 0 // Models property ListModel activeList: ListModel {} @@ -264,7 +268,7 @@ Singleton { saveHistory() } - // Persistence + // Persistence - History FileView { id: historyFileView path: historyFile @@ -281,6 +285,23 @@ Singleton { } } + // Persistence - State (lastSeenTs, etc.) + FileView { + id: stateFileView + path: stateFile + printErrors: false + onLoaded: loadState() + onLoadFailed: error => { + if (error === 2) + writeAdapter() + } + + JsonAdapter { + id: stateAdapter + property real lastSeenTs: 0 + } + } + Timer { id: saveTimer interval: 200 @@ -337,6 +358,35 @@ Singleton { } } + function loadState() { + try { + root.lastSeenTs = stateAdapter.lastSeenTs || 0 + + // Migration: if state file is empty but settings has lastSeenTs, migrate it + if (root.lastSeenTs === 0 && Settings.data.notifications && Settings.data.notifications.lastSeenTs) { + root.lastSeenTs = Settings.data.notifications.lastSeenTs + saveState() + Logger.i("Notifications", "Migrated lastSeenTs from settings to state file") + } + } catch (e) { + Logger.e("Notifications", "Load state failed:", e) + } + } + + function saveState() { + try { + stateAdapter.lastSeenTs = root.lastSeenTs + stateFileView.writeAdapter() + } catch (e) { + Logger.e("Notifications", "Save state failed:", e) + } + } + + function updateLastSeenTs() { + root.lastSeenTs = Time.timestamp * 1000 + saveState() + } + function getAppName(name) { if (!name || name.trim() === "") return "Unknown" diff --git a/Services/ProgramCheckerService.qml b/Services/ProgramCheckerService.qml index ca06dc0c..153b35c9 100644 --- a/Services/ProgramCheckerService.qml +++ b/Services/ProgramCheckerService.qml @@ -97,8 +97,8 @@ Singleton { "kittyAvailable": ["which", "kitty"], "ghosttyAvailable": ["which", "ghostty"], "footAvailable": ["which", "foot"], - "fuzzelAvailable": ["which", "fuzzel"], - "vicinaeAvailable": ["which", "vicinae"], + "fuzzelAvailable": ["which", "fuzzel"], + "vicinaeAvailable": ["which", "vicinae"], "app2unitAvailable": ["which", "app2unit"], "gpuScreenRecorderAvailable": ["sh", "-c", "command -v gpu-screen-recorder >/dev/null 2>&1 || (command -v flatpak >/dev/null 2>&1 && flatpak list --app | grep -q 'com.dec05eba.gpu_screen_recorder')"], "wlsunsetAvailable": ["which", "wlsunset"] diff --git a/Widgets/NFilePicker.qml b/Widgets/NFilePicker.qml index b9bd7863..4b82e77d 100644 --- a/Widgets/NFilePicker.qml +++ b/Widgets/NFilePicker.qml @@ -39,8 +39,8 @@ Popup { function openFilePicker() { if (!root.currentPath) root.currentPath = root.initialPath - shouldResetSelection = true - open() + shouldResetSelection = true + open() } function getFileIcon(fileName) { @@ -92,18 +92,18 @@ Popup { function formatFileSize(bytes) { if (bytes === 0) return "0 B" - const k = 1024, sizes = ["B", "KB", "MB", "GB", "TB"] - const i = Math.floor(Math.log(bytes) / Math.log(k)) - return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + " " + sizes[i] + const k = 1024, sizes = ["B", "KB", "MB", "GB", "TB"] + const i = Math.floor(Math.log(bytes) / Math.log(k)) + return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + " " + sizes[i] } function confirmSelection() { if (filePickerPanel.currentSelection.length === 0) return - root.selectedPaths = filePickerPanel.currentSelection - root.accepted(filePickerPanel.currentSelection) - root.close() + root.selectedPaths = filePickerPanel.currentSelection + root.accepted(filePickerPanel.currentSelection) + root.close() } function updateFilteredModel() { @@ -126,14 +126,14 @@ Popup { if (root.selectionMode === "folders" && !fileIsDir) continue - if (searchText === "" || fileName.toLowerCase().includes(searchText)) { - filteredModel.append({ - "fileName": fileName, - "filePath": filePath, - "fileIsDir": fileIsDir, - "fileSize": fileSize - }) - } + if (searchText === "" || fileName.toLowerCase().includes(searchText)) { + filteredModel.append({ + "fileName": fileName, + "filePath": filePath, + "fileIsDir": fileIsDir, + "fileSize": fileSize + }) + } } } @@ -165,19 +165,19 @@ Popup { focus: true Keys.onPressed: event => { - if (event.modifiers & Qt.ControlModifier && event.key === Qt.Key_F) { - filePickerPanel.showSearchBar = !filePickerPanel.showSearchBar - if (filePickerPanel.showSearchBar) - Qt.callLater(() => searchInput.forceActiveFocus()) - event.accepted = true - } else if (event.key === Qt.Key_Escape && filePickerPanel.showSearchBar) { - filePickerPanel.showSearchBar = false - filePickerPanel.searchText = "" - filePickerPanel.filterText = "" - root.updateFilteredModel() - event.accepted = true - } - } + if (event.modifiers & Qt.ControlModifier && event.key === Qt.Key_F) { + filePickerPanel.showSearchBar = !filePickerPanel.showSearchBar + if (filePickerPanel.showSearchBar) + Qt.callLater(() => searchInput.forceActiveFocus()) + event.accepted = true + } else if (event.key === Qt.Key_Escape && filePickerPanel.showSearchBar) { + filePickerPanel.showSearchBar = false + filePickerPanel.searchText = "" + filePickerPanel.filterText = "" + root.updateFilteredModel() + event.accepted = true + } + } ColumnLayout { anchors.fill: parent @@ -473,11 +473,11 @@ Popup { bottomMargin: Style.marginS ScrollBar.vertical: scrollBarComponent.createObject(gridView, { - "parent": gridView, - "x": gridView.mirrored ? 0 : gridView.width - width, - "y": 0, - "height": gridView.height - }) + "parent": gridView, + "x": gridView.mirrored ? 0 : gridView.width - width, + "y": 0, + "height": gridView.height + }) delegate: Rectangle { id: gridItem @@ -533,8 +533,8 @@ Popup { property bool isImage: { if (model.fileIsDir) return false - const ext = model.fileName.split('.').pop().toLowerCase() - return ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg', 'ico'].includes(ext) + const ext = model.fileName.split('.').pop().toLowerCase() + return ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg', 'ico'].includes(ext) } Image { @@ -574,10 +574,10 @@ Popup { color: { if (isSelected) return Color.mSecondary - else if (mouseArea.containsMouse) - return model.fileIsDir ? Color.mOnTertiary : Color.mOnTertiary - else - return model.fileIsDir ? Color.mPrimary : Color.mOnSurfaceVariant + else if (mouseArea.containsMouse) + return model.fileIsDir ? Color.mOnTertiary : Color.mOnTertiary + else + return model.fileIsDir ? Color.mPrimary : Color.mOnSurfaceVariant } anchors.centerIn: parent visible: !iconContainer.isImage || thumbnail.status !== Image.Ready @@ -608,10 +608,10 @@ Popup { color: { if (isSelected) return Color.mSecondary - else if (mouseArea.containsMouse) - return Color.mOnTertiary - else - return Color.mOnSurfaceVariant + else if (mouseArea.containsMouse) + return Color.mOnTertiary + else + return Color.mOnSurfaceVariant } pointSize: Style.fontSizeS font.weight: isSelected ? Style.fontWeightBold : Style.fontWeightRegular @@ -630,37 +630,37 @@ Popup { acceptedButtons: Qt.LeftButton | Qt.RightButton onClicked: mouse => { - if (mouse.button === Qt.LeftButton) { - if (model.fileIsDir) { - // In folder mode, single click selects the folder - if (root.selectionMode === "folders") { - filePickerPanel.currentSelection = [model.filePath] - } - // In file mode, single click on folder does nothing (must double-click to enter) - } else { - // Single click on file selects it (only in file mode) - if (root.selectionMode === "files") { - filePickerPanel.currentSelection = [model.filePath] - } - } - } - } + if (mouse.button === Qt.LeftButton) { + if (model.fileIsDir) { + // In folder mode, single click selects the folder + if (root.selectionMode === "folders") { + filePickerPanel.currentSelection = [model.filePath] + } + // In file mode, single click on folder does nothing (must double-click to enter) + } else { + // Single click on file selects it (only in file mode) + if (root.selectionMode === "files") { + filePickerPanel.currentSelection = [model.filePath] + } + } + } + } onDoubleClicked: mouse => { - if (mouse.button === Qt.LeftButton) { - if (model.fileIsDir) { - // Double-click on folder always navigates into it - folderModel.folder = "file://" + model.filePath - root.currentPath = model.filePath - } else { - // Double-click on file selects and confirms (only in file mode) - if (root.selectionMode === "files") { - filePickerPanel.currentSelection = [model.filePath] - root.confirmSelection() - } - } - } - } + if (mouse.button === Qt.LeftButton) { + if (model.fileIsDir) { + // Double-click on folder always navigates into it + folderModel.folder = "file://" + model.filePath + root.currentPath = model.filePath + } else { + // Double-click on file selects and confirms (only in file mode) + if (root.selectionMode === "files") { + filePickerPanel.currentSelection = [model.filePath] + root.confirmSelection() + } + } + } + } } } } @@ -680,9 +680,9 @@ Popup { color: { if (filePickerPanel.currentSelection.includes(model.filePath)) return Color.mSecondary - if (mouseArea.containsMouse) - return Color.mTertiary - return Color.transparent + if (mouseArea.containsMouse) + return Color.mTertiary + return Color.transparent } radius: Style.radiusS Behavior on color { @@ -728,37 +728,37 @@ Popup { acceptedButtons: Qt.LeftButton | Qt.RightButton onClicked: mouse => { - if (mouse.button === Qt.LeftButton) { - if (model.fileIsDir) { - // In folder mode, single click selects the folder - if (root.selectionMode === "folders") { - filePickerPanel.currentSelection = [model.filePath] - } - // In file mode, single click on folder does nothing (must double-click to enter) - } else { - // Single click on file selects it (only in file mode) - if (root.selectionMode === "files") { - filePickerPanel.currentSelection = [model.filePath] - } - } - } - } + if (mouse.button === Qt.LeftButton) { + if (model.fileIsDir) { + // In folder mode, single click selects the folder + if (root.selectionMode === "folders") { + filePickerPanel.currentSelection = [model.filePath] + } + // In file mode, single click on folder does nothing (must double-click to enter) + } else { + // Single click on file selects it (only in file mode) + if (root.selectionMode === "files") { + filePickerPanel.currentSelection = [model.filePath] + } + } + } + } onDoubleClicked: mouse => { - if (mouse.button === Qt.LeftButton) { - if (model.fileIsDir) { - // Double-click on folder always navigates into it - folderModel.folder = "file://" + model.filePath - root.currentPath = model.filePath - } else { - // Double-click on file selects and confirms (only in file mode) - if (root.selectionMode === "files") { - filePickerPanel.currentSelection = [model.filePath] - root.confirmSelection() - } - } - } - } + if (mouse.button === Qt.LeftButton) { + if (model.fileIsDir) { + // Double-click on folder always navigates into it + folderModel.folder = "file://" + model.filePath + root.currentPath = model.filePath + } else { + // Double-click on file selects and confirms (only in file mode) + if (root.selectionMode === "files") { + filePickerPanel.currentSelection = [model.filePath] + root.confirmSelection() + } + } + } + } } } } @@ -816,7 +816,7 @@ Popup { Component.onCompleted: { if (!root.currentPath) root.currentPath = root.initialPath - folderModel.folder = "file://" + root.currentPath + folderModel.folder = "file://" + root.currentPath } } } diff --git a/Widgets/NPanel.qml b/Widgets/NPanel.qml index 72e4905e..527f31c6 100644 --- a/Widgets/NPanel.qml +++ b/Widgets/NPanel.qml @@ -9,6 +9,8 @@ Loader { property ShellScreen screen + property bool useOverlay: Settings.data.ui.panelsOverlayLayer + property Component panelContent: null property real preferredWidth: 700 property real preferredHeight: 900 @@ -157,6 +159,7 @@ Loader { WlrLayershell.exclusionMode: ExclusionMode.Ignore WlrLayershell.namespace: "noctalia-panel" + WlrLayershell.layer: useOverlay ? WlrLayer.Overlay : WlrLayer.Top WlrLayershell.keyboardFocus: root.panelKeyboardFocus ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None Region { diff --git a/Widgets/NTextInput.qml b/Widgets/NTextInput.qml index 6043a51d..f11f9ca4 100644 --- a/Widgets/NTextInput.qml +++ b/Widgets/NTextInput.qml @@ -75,31 +75,31 @@ ColumnLayout { propagateComposedEvents: false onPressed: mouse => { - mouse.accepted = true - // Focus the input and position cursor - input.forceActiveFocus() - var inputPos = mapToItem(inputContainer, mouse.x, mouse.y) - if (inputPos.x >= 0 && inputPos.x <= inputContainer.width) { - var textPos = inputPos.x - Style.marginM - if (textPos >= 0 && textPos <= input.width) { - input.cursorPosition = input.positionAt(textPos, input.height / 2) - } - } - } + mouse.accepted = true + // Focus the input and position cursor + input.forceActiveFocus() + var inputPos = mapToItem(inputContainer, mouse.x, mouse.y) + if (inputPos.x >= 0 && inputPos.x <= inputContainer.width) { + var textPos = inputPos.x - Style.marginM + if (textPos >= 0 && textPos <= input.width) { + input.cursorPosition = input.positionAt(textPos, input.height / 2) + } + } + } onReleased: mouse => { - mouse.accepted = true - } + mouse.accepted = true + } onDoubleClicked: mouse => { - mouse.accepted = true - input.selectAll() - } + mouse.accepted = true + input.selectAll() + } onPositionChanged: mouse => { - mouse.accepted = true - } + mouse.accepted = true + } onWheel: wheel => { - wheel.accepted = true - } + wheel.accepted = true + } } // Container for the actual text field @@ -167,32 +167,32 @@ ColumnLayout { property int selectionStart: 0 onPressed: mouse => { - mouse.accepted = true - input.forceActiveFocus() - var pos = input.positionAt(mouse.x, mouse.y) - input.cursorPosition = pos - selectionStart = pos - } + mouse.accepted = true + input.forceActiveFocus() + var pos = input.positionAt(mouse.x, mouse.y) + input.cursorPosition = pos + selectionStart = pos + } onPositionChanged: mouse => { - if (mouse.buttons & Qt.LeftButton) { - mouse.accepted = true - var pos = input.positionAt(mouse.x, mouse.y) - input.select(selectionStart, pos) - } - } + if (mouse.buttons & Qt.LeftButton) { + mouse.accepted = true + var pos = input.positionAt(mouse.x, mouse.y) + input.select(selectionStart, pos) + } + } onDoubleClicked: mouse => { - mouse.accepted = true - input.selectAll() - } + mouse.accepted = true + input.selectAll() + } onReleased: mouse => { - mouse.accepted = true - } + mouse.accepted = true + } onWheel: wheel => { - wheel.accepted = true - } + wheel.accepted = true + } } } NIconButton { diff --git a/Widgets/NToggle.qml b/Widgets/NToggle.qml index 18259ed6..45ad8bc3 100644 --- a/Widgets/NToggle.qml +++ b/Widgets/NToggle.qml @@ -9,6 +9,7 @@ RowLayout { property string label: "" property string description: "" + property bool enabled: true property bool checked: false property bool hovering: false property int baseSize: Math.round(Style.baseWidgetSize * 0.8 * Style.uiScaleRatio) @@ -18,6 +19,7 @@ RowLayout { signal exited Layout.fillWidth: true + opacity: enabled ? 1.0 : 0.6 NLabel { label: root.label @@ -71,14 +73,20 @@ RowLayout { cursorShape: Qt.PointingHandCursor hoverEnabled: true onEntered: { + if (!enabled) + return hovering = true root.entered() } onExited: { + if (!enabled) + return hovering = false root.exited() } onClicked: { + if (!enabled) + return root.toggled(!root.checked) } }