Merge branch 'main' into MediaMiniImprovements

This commit is contained in:
Sighthesia
2025-10-21 21:47:25 +08:00
committed by GitHub
57 changed files with 1670 additions and 498 deletions
+42 -40
View File
@@ -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} <b>großartigen</b> 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": {
+16 -14
View File
@@ -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."
}
}
},
+24 -15
View File
@@ -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": {
+24 -15
View File
@@ -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": {
+24 -15
View File
@@ -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": {
+24 -15
View File
@@ -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": {
+19 -19
View File
@@ -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
+135
View File
@@ -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))
+11
View File
@@ -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}")
+21
View File
@@ -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))
-1
View File
@@ -8,7 +8,6 @@ import qs.Commons
Singleton {
id: root
property bool isLoaded: false
property string langCode: ""
property string systemDetectedLangCode: ""
+3 -3
View File
@@ -312,8 +312,7 @@ Singleton {
property bool doNotDisturb: false
property list<string> 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<string> 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
+1
View File
@@ -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",
+132
View File
@@ -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
+3 -6
View File
@@ -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()
+11 -12
View File
@@ -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) {
+5 -6
View File
@@ -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) {
+29 -15
View File
@@ -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
}
}
}
}
+2 -3
View File
@@ -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 = []
+56 -17
View File
@@ -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")
}
}
}
+1 -1
View File
@@ -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: {
+2 -3
View File
@@ -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."
+1 -1
View File
@@ -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()
+1 -1
View File
@@ -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
+1 -5
View File
@@ -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++) {
+223
View File
@@ -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
}
}
}
+1 -1
View File
@@ -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)
+73 -31
View File
@@ -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)
}
+66 -1
View File
@@ -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
+27 -13
View File
@@ -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
@@ -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
}
+1 -1
View File
@@ -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 {
@@ -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"
@@ -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
}
}
@@ -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
}
}
+11
View File
@@ -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",
+17 -19
View File
@@ -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
+22 -31
View File
@@ -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
}
}
+31
View File
@@ -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
}
}
+2 -2
View File
@@ -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
+2 -2
View File
@@ -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 {
+4 -4
View File
@@ -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 {
+3 -32
View File
@@ -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
+1 -1
View File
@@ -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
+2 -2
View File
@@ -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`
}
})
+15
View File
@@ -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")
+6
View File
@@ -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) {
+250
View File
@@ -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")
}
}
}
}
}
}
+11
View File
@@ -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) {
+18 -1
View File
@@ -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
}
+2 -1
View File
@@ -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",
+51 -1
View File
@@ -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"
+2 -2
View File
@@ -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"]
+106 -106
View File
@@ -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
}
}
}
+3
View File
@@ -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 {
+39 -39
View File
@@ -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 {
+8
View File
@@ -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)
}
}