mirror of
https://github.com/noctalia-dev/noctalia-shell.git
synced 2026-05-11 17:08:27 +08:00
Merge branch 'main' into MediaMiniImprovements
This commit is contained in:
+42
-40
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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
|
||||
|
||||
Executable
+135
@@ -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))
|
||||
Executable
+11
@@ -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}")
|
||||
Executable
+21
@@ -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))
|
||||
@@ -8,7 +8,6 @@ import qs.Commons
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
|
||||
property bool isLoaded: false
|
||||
property string langCode: ""
|
||||
property string systemDetectedLangCode: ""
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 = []
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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++) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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`
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user