Merge branch 'main' into main

This commit is contained in:
Lysec
2026-01-23 14:57:15 +01:00
committed by GitHub
79 changed files with 4939 additions and 831 deletions
@@ -239,3 +239,79 @@ body {
background-color: var(--accent-3) !important;
border-color: var(--accent-3) !important;
}
[data-slate-node="element"],
[data-slate-node="text"],
[data-slate-string="true"] {
color: var(--text-3) !important;
}
[class*="eyebrow"][class*="badge"][class*="expressive"] {
color: var(--accent-1) !important;
background-color: color-mix(in srgb, var(--accent-1) 15%, transparent) !important;
}
/* Outer circle (non-selected background) */
._64e6155a0667273d-outerRadioBase {
fill: var(--bg-1) !important; /* background for non-selected radios */
stroke: var(--border) !important; /* ring border */
stroke-width: 1px;
transition: stroke 0.2s ease;
}
/* Outer circle fill (selected) */
._64e6155a0667273d-outerRadioFill {
fill: var(--bg-1) !important; /* accent color for selected */
stroke: var(--border) !important;
opacity: 0; /* hidden by default */
transition: opacity 0.2s ease;
}
/* Inner dot (selected) */
._64e6155a0667273d-innerDotRadio {
fill: var(--accent-1) !important; /* contrast dot */
opacity: 0; /* hidden by default */
transition: opacity 0.2s ease;
}
/* Show outer fill + inner dot when selected (handles initial render & clicks) */
.parentSelected__6e9f8 ._64e6155a0667273d-outerRadioFill,
.parentSelected__6e9f8 ._64e6155a0667273d-innerDotRadio,
._64e6155a0667273d-outerRadioFill[aria-checked="true"],
._64e6155a0667273d-innerDotRadio[aria-checked="true"],
._64e6155a0667273d-outerRadioFill[data-selected="true"],
._64e6155a0667273d-innerDotRadio[data-selected="true"] {
opacity: 1 !important;
}
/* Hover effect for base ring */
._64e6155a0667273d-outerRadioBase:hover {
stroke: var(--accent-1) !important;
}
/* Checkbox checkmark and background for selected */
._714a9a7abaf0392a-checkboxOption[data-selected="true"] ._714a9a7abaf0392a-checkboxIndicator {
background-color: var(--bg-1) !important; /* background of selected checkbox */
border-color: var(--border) !important; /* optional: accent border */
}
._714a9a7abaf0392a-checkboxIndicator {
background-color: var(--bg-1) !important; /* background for non-selected checkboxes */
border-color: var(--border) !important; /* neutral border */
transition: background-color 0.2s ease, border-color 0.2s ease;
}
._714a9a7abaf0392a-checkboxOption[data-selected="true"] ._714a9a7abaf0392a-checkStroke,
._714a9a7abaf0392a-checkboxOption[data-selected="true"] ._714a9a7abaf0392a-dot {
color: var(--accent-1) !important; /* checkmark/dot color */
}
/* Hover effect for selected checkbox */
._714a9a7abaf0392a-checkboxOption[data-selected="true"]:hover ._714a9a7abaf0392a-checkboxIndicator {
background-color: var(--bg-1) !important; /* keep background consistent on hover */
}
._714a9a7abaf0392a-checkboxOption[data-selected="true"]:hover ._714a9a7abaf0392a-checkStroke,
._714a9a7abaf0392a-checkboxOption[data-selected="true"]:hover ._714a9a7abaf0392a-dot {
color: var(--accent-1) !important; /* keep checkmark accent color on hover */
}
+5 -5
View File
@@ -96,7 +96,7 @@
;; Basic faces
`(default ((t (:background ,bg :foreground ,on-background))))
`(cursor ((t (:background ,primary))))
`(highlight ((t (:background ,primary-container :foreground ,on-primary-container))))
`(highlight ((t (:background ,surface-container-high))))
`(region ((t (:background ,primary-container :foreground ,on-primary-container :extend t))))
`(secondary-selection ((t (:background ,secondary-container :foreground ,on-secondary-container :extend t))))
`(isearch ((t (:background ,tertiary-container :foreground ,on-tertiary-container :weight bold))))
@@ -132,7 +132,7 @@
`(show-paren-mismatch ((t (:background ,err-container :foreground ,on-err-container :weight bold))))
;; Mode line - improved status bar styling
`(mode-line ((t (:background ,surface-container :foreground ,on-surface :box nil))))
`(mode-line ((t (:background ,surface-container-high :foreground ,on-surface :box nil))))
`(mode-line-inactive ((t (:background ,surface :foreground ,on-surface-variant :box nil))))
`(mode-line-buffer-id ((t (:foreground ,primary :weight bold))))
`(mode-line-emphasis ((t (:foreground ,primary :weight bold))))
@@ -321,17 +321,17 @@
`(info-node ((t (:foreground ,tertiary :weight bold))))
;; Tabs
`(tab-bar ((t (:background ,surface-container :foreground ,on-surface :box nil))))
`(tab-bar ((t (:background ,surface-container-high :foreground ,on-surface :box nil))))
`(tab-bar-tab ((t (:background ,surface-container-high :foreground ,on-surface :weight bold :box nil))))
`(tab-bar-tab-inactive ((t (:background ,surface :foreground ,on-surface-variant :box nil))))
`(tab-line ((t (:background ,surface-container :foreground ,on-surface :box nil))))
`(tab-line ((t (:background ,surface-container-high :foreground ,on-surface :box nil))))
`(tab-line-tab ((t (:background ,surface :foreground ,on-surface-variant :box nil))))
`(tab-line-tab-current ((t (:background ,surface-container-high :foreground ,on-surface :weight bold :box nil))))
`(tab-line-tab-inactive ((t (:background ,surface :foreground ,on-surface-variant :box nil))))
`(tab-line-highlight ((t (:background ,surface-container-highest :foreground ,on-surface))))
`(centaur-tabs-default ((t (:background ,surface-container :foreground ,on-surface))))
`(centaur-tabs-default ((t (:background ,surface-container-high :foreground ,on-surface))))
`(centaur-tabs-selected ((t (:background ,surface-container-high :foreground ,on-surface :weight bold))))
`(centaur-tabs-unselected ((t (:background ,surface :foreground ,on-surface-variant))))
`(centaur-tabs-selected-modified ((t (:background ,surface-container-high :foreground ,tertiary :weight bold))))
+3
View File
@@ -354,6 +354,7 @@
"clear": "Löschen",
"clipboard": "Zwischenablage",
"close": "Schließen",
"color-muted": "Stummgeschaltet",
"colors": "Farben",
"command": "Befehl",
"connect": "Verbinden",
@@ -1375,6 +1376,8 @@
"settings-select-monitor-folder": "Monitor-Hintergrundbild-Ordner auswählen",
"settings-selector-description": "Wählen Sie Ihr Hintergrundbild aus.",
"settings-selector-position-description": "Wählen Sie aus, wo das Hintergrundbild-Auswahlfeld angezeigt wird.",
"settings-show-hidden-files-tooltip-hide": "Versteckte Dateien ausblenden",
"settings-show-hidden-files-tooltip-show": "Versteckte Dateien anzeigen",
"settings-title": "Hintergrundbild-Einstellungen",
"settings-view-mode-description": "Wählen Sie aus, wie Hintergrundbilder aus Ihrem Verzeichnis angezeigt werden.",
"settings-view-mode-label": "Ansichtsmodus",
+4
View File
@@ -354,6 +354,7 @@
"clear": "Clear",
"clipboard": "Clipboard",
"close": "Close",
"color-muted": "Muted",
"colors": "Colors",
"command": "Command",
"connect": "Connect",
@@ -436,6 +437,7 @@
"search": "Search",
"security": "Security",
"select": "Select",
"settings": "Settings",
"shortcuts": "Shortcuts",
"shutdown": "Shutdown",
"signal": "Signal",
@@ -1375,6 +1377,8 @@
"settings-select-monitor-folder": "Select monitor wallpaper folder",
"settings-selector-description": "Choose your wallpaper.",
"settings-selector-position-description": "Choose where the wallpaper selector panel appears.",
"settings-show-hidden-files-tooltip-hide": "Hide hidden files",
"settings-show-hidden-files-tooltip-show": "Show hidden files",
"settings-title": "Wallpaper settings",
"settings-view-mode-description": "Choose how wallpapers are displayed from your directory.",
"settings-view-mode-label": "Viewing mode",
+3
View File
@@ -354,6 +354,7 @@
"clear": "Borrar",
"clipboard": "Portapapeles",
"close": "Cerrar",
"color-muted": "Silenciado",
"colors": "Colores",
"command": "Comando",
"connect": "Conectar",
@@ -1375,6 +1376,8 @@
"settings-select-monitor-folder": "Seleccionar carpeta de fondos de pantalla del monitor",
"settings-selector-description": "Elige tu fondo de pantalla.",
"settings-selector-position-description": "Elige dónde aparece el panel selector de fondo de pantalla.",
"settings-show-hidden-files-tooltip-hide": "Ocultar archivos ocultos",
"settings-show-hidden-files-tooltip-show": "Mostrar archivos ocultos",
"settings-title": "Configuración del fondo de pantalla",
"settings-view-mode-description": "Elige cómo se muestran los fondos de pantalla desde tu directorio.",
"settings-view-mode-label": "Modo de visualización",
+3
View File
@@ -354,6 +354,7 @@
"clear": "Effacer",
"clipboard": "Presse-papiers",
"close": "Fermer",
"color-muted": "Muet",
"colors": "Couleurs",
"command": "Commande",
"connect": "Connecter",
@@ -1375,6 +1376,8 @@
"settings-select-monitor-folder": "Sélectionner le dossier des fonds d'écran du moniteur",
"settings-selector-description": "Choisissez votre fond d’écran.",
"settings-selector-position-description": "Choisissez l'emplacement d'affichage du panneau de sélection du fond d'écran.",
"settings-show-hidden-files-tooltip-hide": "Masquer les fichiers cachés",
"settings-show-hidden-files-tooltip-show": "Afficher les fichiers cachés",
"settings-title": "Paramètres du fond d'écran",
"settings-view-mode-description": "Choisissez comment les fonds d'écran sont affichés depuis votre répertoire.",
"settings-view-mode-label": "Mode d'affichage",
+3
View File
@@ -354,6 +354,7 @@
"clear": "Törlés",
"clipboard": "Vágólap",
"close": "Bezárás",
"color-muted": "Némítva",
"colors": "Színek",
"command": "Parancs",
"connect": "Csatlakozás",
@@ -1375,6 +1376,8 @@
"settings-select-monitor-folder": "Monitor háttérkép mappa kiválasztása",
"settings-selector-description": "Válassza ki a háttérképét.",
"settings-selector-position-description": "Válassza ki, hol jelenjen meg a háttérkép választó panel.",
"settings-show-hidden-files-tooltip-hide": "Rejtett fájlok elrejtése",
"settings-show-hidden-files-tooltip-show": "Rejtett fájlok megjelenítése",
"settings-title": "Háttérkép beállítások",
"settings-view-mode-description": "Válaszd ki, hogyan jelenjenek meg a háttérképek a könyvtáradból.",
"settings-view-mode-label": "Megtekintési mód",
+3
View File
@@ -354,6 +354,7 @@
"clear": "クリア",
"clipboard": "クリップボード",
"close": "閉じる",
"color-muted": "ミュート",
"colors": "色",
"command": "コマンド",
"connect": "接続",
@@ -1375,6 +1376,8 @@
"settings-select-monitor-folder": "ディスプレイ別の壁紙フォルダを選択",
"settings-selector-description": "壁紙を選択します。",
"settings-selector-position-description": "壁紙選択パネルの表示位置を選択します。",
"settings-show-hidden-files-tooltip-hide": "隠しファイルを隠す",
"settings-show-hidden-files-tooltip-show": "隠しファイルを表示",
"settings-title": "壁紙設定",
"settings-view-mode-description": "ディレクトリから壁紙をどのように表示するかを選択します。",
"settings-view-mode-label": "表示モード",
+3
View File
@@ -354,6 +354,7 @@
"clear": "Paqqij bike",
"clipboard": "Klîpbir",
"close": "Bigire",
"color-muted": "Bêdengkirî",
"colors": "Rengan",
"command": "Ferman",
"connect": "Girêdan",
@@ -1375,6 +1376,8 @@
"settings-select-monitor-folder": "Hilbijêre peldanka dîwarê dîmenderê",
"settings-selector-description": "Wêneyê dîwarê xwe hilbijêre.",
"settings-selector-position-description": "Cihê ku panelê hilbijêra dîwêr lê xuya dike hilbijêre.",
"settings-show-hidden-files-tooltip-hide": "Pelên veşartî veşêre",
"settings-show-hidden-files-tooltip-show": "Pelên veşartî nîşan bide",
"settings-title": "Mîhengên dîwêr",
"settings-view-mode-description": "Ji bo dîmendana dîwêrên ji pelrêça xwe rêbazek hilbijêre.",
"settings-view-mode-label": "Awayê dîtinê",
+4 -1
View File
@@ -273,7 +273,7 @@
}
},
"battery": {
"battery-health": "Batterijconditie",
"battery-health": "Accuconditie",
"battery-level": "Accuniveau",
"capacity-level": "Capaciteit: {level}",
"charging-rate": "Laadsnelheid: {rate} W",
@@ -354,6 +354,7 @@
"clear": "Wissen",
"clipboard": "Klembord",
"close": "Sluiten",
"color-muted": "Gedempt",
"colors": "Kleuren",
"command": "Commando",
"connect": "Verbinden",
@@ -1375,6 +1376,8 @@
"settings-select-monitor-folder": "Achtergrondmap voor monitor selecteren",
"settings-selector-description": "Kies je achtergrond.",
"settings-selector-position-description": "Kies waar het achtergrondselectiepaneel verschijnt.",
"settings-show-hidden-files-tooltip-hide": "Verborgen bestanden verbergen",
"settings-show-hidden-files-tooltip-show": "Verborgen bestanden weergeven",
"settings-title": "Achtergrondinstellingen",
"settings-view-mode-description": "Kies hoe achtergronden uit uw map worden weergegeven.",
"settings-view-mode-label": "Weergavemodus",
+3
View File
@@ -354,6 +354,7 @@
"clear": "Wyczyść",
"clipboard": "Schowek",
"close": "Zamknij",
"color-muted": "Wyciszony",
"colors": "Kolory",
"command": "Komenda",
"connect": "Połącz",
@@ -1375,6 +1376,8 @@
"settings-select-monitor-folder": "Wybierz folder tapet monitora",
"settings-selector-description": "Wybierz swoją tapetę.",
"settings-selector-position-description": "Wybierz, gdzie pojawia się panel wyboru tapety.",
"settings-show-hidden-files-tooltip-hide": "Ukryj ukryte pliki",
"settings-show-hidden-files-tooltip-show": "Pokaż ukryte pliki",
"settings-title": "Ustawienia tapet",
"settings-view-mode-description": "Wybierz sposób wyświetlania tapet z Twojego katalogu.",
"settings-view-mode-label": "Tryb wyświetlania",
+3
View File
@@ -354,6 +354,7 @@
"clear": "Limpar",
"clipboard": "Área de transferência",
"close": "Fechar",
"color-muted": "Silenciado",
"colors": "Cores",
"command": "Comando",
"connect": "Conectar",
@@ -1375,6 +1376,8 @@
"settings-select-monitor-folder": "Selecionar pasta de papéis de parede do monitor",
"settings-selector-description": "Escolha o seu papel de parede.",
"settings-selector-position-description": "Escolha onde o painel do seletor de papel de parede aparece.",
"settings-show-hidden-files-tooltip-hide": "Ocultar arquivos ocultos",
"settings-show-hidden-files-tooltip-show": "Mostrar ficheiros ocultos",
"settings-title": "Configurações de papel de parede",
"settings-view-mode-description": "Escolha como os papéis de parede são exibidos a partir do seu diretório.",
"settings-view-mode-label": "Modo de visualização",
+76 -73
View File
@@ -33,25 +33,25 @@
"active-window": {
"colorize-icons-description": "Применить цвета темы к иконке активного окна.",
"hide-mode-description": "Управляет поведением виджета, когда ни одно окно не активно.",
"scrolling-mode-description": "Управление включением прокрутки текста для длинных заголовков окон.",
"scrolling-mode-description": "Управление прокруткой текста для длинных заголовков окон.",
"show-app-icon-description": "Отображать иконку приложения рядом с заголовком окна.",
"show-app-icon-label": "Показывать иконку приложения"
},
"audio-visualizer": {
"color-name-description": "Выберите цвет для визуализатора.",
"color-name-label": "Цвет заливки",
"hide-when-idle-description": "Если включено, визуализатор скрыт, пока плеер активно не воспроизводит.",
"hide-when-idle-description": "Если включено, визуализатор скрыт, пока плеер не начнёт воспроизведение.",
"hide-when-idle-label": "Скрывать, когда медиа не воспроизводится",
"width-description": "Пользовательская ширина компонента."
},
"battery": {
"device-default": "Устройство отображения по умолчанию",
"device-description": "Выберите, какое устройство с батареей отображать.",
"device-label": "Батарейное устройство",
"device-description": "Выберите устройство для отображения состояния батареи.",
"device-label": "Устройство с батареей",
"hide-if-idle-description": "Скрыть виджет, когда батарея не заряжается и не разряжается.",
"hide-if-idle-label": "Скрывать в режиме простоя",
"hide-if-not-detected-description": "Скрыть виджет, если в системе не обнаружена батарея.",
"hide-if-not-detected-label": "Скрыться, если не обнаружен",
"hide-if-not-detected-label": "Скрывать при отсутствии батареи",
"low-battery-threshold-description": "Показывать предупреждение, когда уровень заряда батареи падает ниже этого процента.",
"low-battery-threshold-label": "Порог предупреждения о низком заряде батареи",
"show-noctalia-performance-description": "Отображать переключатель режима производительности Noctalia в панели батареи.<br>Отключает тени и анимации в Noctalia для снижения использования ресурсов.",
@@ -75,13 +75,13 @@
"use-custom-font-label": "Использовать пользовательский шрифт",
"use-monospaced-font-description": "Если включено, часы будут использовать моноширинный шрифт.",
"use-monospaced-font-label": "Использовать моноширинный шрифт",
"use-primary-color-description": "Если включено, это применит основной цвет для выделения.",
"use-primary-color-description": "Если включено, будет использован основной цвет для отображения часов.",
"use-primary-color-label": "Использовать основной цвет",
"vertical-bar-description": "Используйте пробел, чтобы разделить каждую часть на новую строку.",
"vertical-bar-label": "Вертикальная панель"
},
"control-center": {
"browse-file": "Обзор файла",
"browse-file": "Выбор файла",
"browse-library": "Обзор библиотеки",
"color-selection-description": "Применяет цвета темы к значкам.",
"enable-colorization-description": "Включает окрашивание для значка центра управления, применяя цвета темы.",
@@ -95,7 +95,7 @@
"collapse-condition-label": "Условие скрытия",
"color-selection-description": "Применить цвета темы к значку и тексту.",
"color-selection-label": "Выбрать цвет",
"display-command-output-description": "Введите команду для регулярного выполнения. Первая строка ее вывода будет отображаться как текст.",
"display-command-output-description": "Введите команду для регулярного выполнения. Первая строка её вывода будет отображаться как текст.",
"display-command-output-label": "Вывод команды",
"display-command-output-stream-description": "Введите команду для непрерывного выполнения.",
"dynamic-text": "Динамический текст",
@@ -220,7 +220,7 @@
"colorize-icons-description": "Применить цвета темы к иконкам панели задач.",
"hide-mode-description": "Управляет поведением виджета, когда нет соответствующих окон.",
"hide-mode-label": "Режим скрытия",
"icon-scale-description": "Задает коэффициент масштабирования для значков панели задач.",
"icon-scale-description": "Задаёт коэффициент масштабирования для значков панели задач.",
"icon-scale-label": "Масштабирование значков",
"max-width-description": "Максимальная ширина панели задач в процентах от ширины экрана.",
"max-width-label": "Максимальная ширина",
@@ -252,7 +252,7 @@
"display-mode-label": "Режим отображения"
},
"workspace": {
"character-count-description": "Количество символов для отображения из имен рабочих пространств (1-10).",
"character-count-description": "Количество символов для отображения из имён рабочих пространств (1-10).",
"character-count-label": "Количество символов",
"enable-scrollwheel-description": "Переключайтесь между рабочими пространствами с помощью колеса прокрутки мыши.",
"enable-scrollwheel-label": "Прокрутите, чтобы переключить рабочие столы",
@@ -279,7 +279,7 @@
"charging-rate": "Скорость зарядки: {rate} Вт",
"discharging-rate": "Скорость разрядки: {rate} Вт",
"health": "Здоровье: {percent}%",
"inhibit-idle-description": "Не дает системе уснуть.",
"inhibit-idle-description": "Не даёт системе уснуть.",
"no-battery-detected": "Батарея не обнаружена",
"plugged-in": "Подключено",
"power-profile": "Профиль питания",
@@ -320,7 +320,7 @@
"empty": "Примечания к выпуску пока недоступны.",
"highlight-title": "Основные изменения",
"released": "Выпущено {date}",
"subtitle-fresh": "Спасибо за установку Noctalia! Вот что входит в этот билд.",
"subtitle-fresh": "Спасибо за установку Noctalia! Вот что входит в эту сборку.",
"subtitle-updated": "Обновлено с {previousVersion}",
"title": "Что нового в {version}",
"version": "Версия {version}",
@@ -354,6 +354,7 @@
"clear": "Очистить",
"clipboard": "Буфер обмена",
"close": "Закрыть",
"color-muted": "Выключено",
"colors": "Цвета",
"command": "Команда",
"connect": "Соединить",
@@ -413,7 +414,7 @@
"notifications": "Уведомления",
"official": "Официальный",
"output": "Вывод",
"pair": "Спаровать",
"pair": "Спарить",
"paired": "Спарено",
"pairing": "Сопряжение...",
"panels": "Панели",
@@ -724,12 +725,12 @@
"monitor-override-settings-description": "Использовать пользовательские настройки для этого монитора.",
"monitor-reset-all": "Сбросить все",
"monitor-widgets-title": "Настройка виджета - {monitor}",
"monitors-desc": "Показывать панель на определенных мониторах. По умолчанию на всех, если ни один не выбран.",
"monitors-desc-new": "Настройте, на каких мониторах отображать Bar, и настройте параметры для каждого монитора.",
"monitors-desc": "Показывать панель на определённых мониторах. По умолчанию на всех, если ни один не выбран.",
"monitors-desc-new": "Настройте, на каких мониторах отображать панель, и настройте параметры для каждого монитора.",
"monitors-title": "Отображение на мониторах",
"title": "Панель",
"tray-blacklist-description": "Добавьте правила исключения трея, поддерживает подстановочные знаки (*).",
"tray-blacklist-label": ерный список",
"tray-blacklist-label": ёрный список",
"tray-blacklist-placeholder": "например, nm-applet, Fcitx*",
"tray-pin-application": "Закрепить приложение",
"tray-unpin-application": "Открепить приложение",
@@ -740,11 +741,11 @@
"color-scheme": {
"color-source-use-wallpaper-colors-description": "Создавайте цветовые схемы на основе ваших обоев. Автоматически извлекает цвета для создания целостной темы.",
"color-source-use-wallpaper-colors-label": "Использовать цвета обоев",
"dark-mode-mode-description": "Включает автоматическое переключение между светлым и темным режимами.",
"dark-mode-mode-label": "Расписание темного режима",
"dark-mode-mode-description": "Включает автоматическое переключение между светлым и тёмным режимами.",
"dark-mode-mode-label": "Расписание тёмного режима",
"dark-mode-mode-manual": "Вручную",
"dark-mode-mode-off": "Выкл",
"dark-mode-switch-description": "Переключается на более темную тему для удобства просмотра ночью.",
"dark-mode-switch-description": "Переключается на более тёмную тему для удобства просмотра ночью.",
"delete-error-description": "Не удалось удалить {scheme}",
"delete-error-title": "Ошибка удаления",
"delete-success-description": "{scheme} успешно удалена",
@@ -757,18 +758,18 @@
"download-error-download-failed": "Ошибка загрузки с кодом выхода: {code}",
"download-error-invalid-response": "Неверный формат ответа API",
"download-error-no-files": "Файлы для схемы не найдены",
"download-error-parse-failed": "Ошибка парсинга ответа API: {error}",
"download-error-parse-failed": "Ошибка обработки ответа API: {error}",
"download-error-rate-limit": "Превышен лимит запросов GitHub API",
"download-error-title": "Ошибка загрузки",
"download-fetching": "Получение доступных цветовых схем...",
"download-success-description": "{scheme} успешно загружена",
"download-success-title": "Цветовая схема загружена",
"download-title": "Загрузить цветовые схемы",
"predefined-desc": "Выберите из коллекции предопределенных цветовых схем.",
"predefined-generate-templates-label": "Генерировать шаблоны для предопределенных схем",
"predefined-title": "Предопределенные цветовые схемы",
"predefined-desc": "Выберите из коллекции предопределённых цветовых схем.",
"predefined-generate-templates-label": "Генерировать шаблоны для предопределённых схем",
"predefined-title": "Предопределённые цветовые схемы",
"templates-desc": "Применение цветов к внешним приложениям.",
"templates-filter-description": "Показать шаблоны из определенной категории.",
"templates-filter-description": "Показать шаблоны из определённой категории.",
"templates-filter-label": "Фильтровать по категории",
"templates-misc-description": "Создайте свои собственные шаблоны.",
"templates-misc-label": "Дополнительно",
@@ -844,7 +845,7 @@
"general-title": "Настольные виджеты",
"media-player-enabled-description": "Отобразить виджет медиаплеера на рабочем столе.",
"media-player-enabled-label": "Включить виджет медиаплеера",
"media-player-rounded-corners-description": "Включить скругленные углы на краях виджета.",
"media-player-rounded-corners-description": "Включить скругление углов на краях виджета.",
"media-player-show-album-art-description": "Показать обложку альбома и информацию о треках (название и исполнитель).",
"media-player-show-album-art-label": "Показывать обложку альбома и название",
"media-player-show-background-description": "Показать фоновый контейнер для виджета медиаплеера.",
@@ -871,7 +872,7 @@
"night-light-auto-schedule-description": "На основе времени заката и восхода солнца в <i>{location}</i> — рекомендуется.",
"night-light-auto-schedule-label": "Автоматическое расписание",
"night-light-desc": "Уменьшение излучения синего света, чтобы помочь вам лучше спать и уменьшить напряжение глаз.",
"night-light-enable-description": "Применить теплый цветовой фильтр для уменьшения излучения синего света.",
"night-light-enable-description": "Применить тёплый цветовой фильтр для уменьшения излучения синего света.",
"night-light-enable-label": "Включить ночной свет",
"night-light-force-activation-description": "Игнорирует расписание и немедленно применяет ночной фильтр.",
"night-light-force-activation-label": "Принудительная активация",
@@ -893,18 +894,18 @@
"appearance-background-opacity-description": "Настройка непрозрачности фона дока.",
"appearance-border-radius-description": "Измените радиус границы дока.",
"appearance-border-radius-label": "Радиус скругления границы",
"appearance-colorize-icons-description": "Применить цвета темы к иконкам приложений на доке (только для нефокусированных приложений).",
"appearance-colorize-icons-description": "Применить цвета темы к иконкам приложений на доке (только для приложений не в фокусе).",
"appearance-colorize-icons-label": "Раскрасить иконки",
"appearance-dead-opacity-description": "Настройте прозрачность значков неактивных приложений.",
"appearance-dead-opacity-label": "Мёртвая непрозрачность",
"appearance-dead-opacity-label": "Неактивные приложения",
"appearance-desc": "Настройка поведения и внешнего вида дока.",
"appearance-display-auto-hide": "Автоматически скрывать",
"appearance-display-description": "Выберите, как ведёт себя док.",
"appearance-display-description": "Выберите поведение дока.",
"appearance-display-exclusive": "Исключительно",
"appearance-floating-distance-description": "Установите расстояние между доком и краем экрана.",
"appearance-floating-distance-label": "Расстояние плавающего дока",
"appearance-hide-show-speed-description": "Настройте скорость анимации скрытия/показа дока.",
"appearance-hide-show-speed-label": "Скорость скрытия/показа",
"appearance-hide-show-speed-description": "Настройка скорости анимации скрытия/открытия дока.",
"appearance-hide-show-speed-label": "Скорость анимации",
"appearance-icon-size-description": "Настройка общего размера дока.",
"appearance-icon-size-label": "Размер дока",
"appearance-inactive-indicators-description": "Отображать индикаторы для всех приложений, а не только для активного.",
@@ -948,21 +949,21 @@
"profile-picture-label": "Фотография профиля {user}",
"profile-select-avatar": "Выбрать изображение аватара",
"profile-title": "Профиль",
"profile-tooltip": "Фото профілю",
"profile-tooltip": "Фотография профиля",
"screen-corners-desc": "Настройка скругления углов экрана и визуальных эффектов.",
"screen-corners-radius-description": "Настройка скругления углов экрана.",
"screen-corners-radius-label": "Радиус углов экрана",
"screen-corners-radius-reset": "Сбросить радиус углов экрана",
"screen-corners-show-corners-description": "Отображать скругленные углы по краю экрана.",
"screen-corners-show-corners-description": "Отображать скруглённые углы по краю экрана.",
"screen-corners-show-corners-label": "Показывать углы экрана",
"screen-corners-solid-black-description": "Использовать сплошной черный цвет вместо цвета фона панели.",
"screen-corners-solid-black-label": "Сплошные черные углы",
"screen-corners-solid-black-description": "Использовать сплошной чёрный цвет вместо цвета фона панели.",
"screen-corners-solid-black-label": "Сплошные чёрные углы",
"screen-corners-title": "Углы экрана",
"settings-copied": "Настройки скопированы в буфер обмена"
},
"hooks": {
"info-command-info-description": "• Команды выполняются через shell (sh -lc)<br>• Команды выполняются в фоновом режиме (отдельно)<br>• Кнопки 'Тест' выполняются с текущими значениями",
"info-parameters-description": "• Хук обоев: $1 = путь к обоям, $2 = имя экрана<br>• Хук переключения темы: $1 = true/false (состояние темного режима)<br>• Хуки блокировки/разблокировки экрана: Без параметров<br>• Хуки режима производительности: Без параметров<br>• Хук сеанса: $1 = действие (выключением/перезагрузкой)",
"info-parameters-description": "• Хук обоев: $1 = путь к обоям, $2 = имя экрана<br>• Хук переключения темы: $1 = true/false (состояние тёмного режима)<br>• Хуки блокировки/разблокировки экрана: Без параметров<br>• Хуки режима производительности: Без параметров<br>• Хук сеанса: $1 = действие (выключением/перезагрузкой)",
"info-parameters-label": "Доступные параметры",
"noctalia-started-description": "Команда для выполнения после завершения загрузки Noctalia.",
"noctalia-started-label": "Noctalia запущена",
@@ -986,7 +987,7 @@
"system-hooks-enable-description": "Включить или отключить все команды хуков.",
"system-hooks-enable-label": "Включить хуки",
"system-hooks-title": "Системные хуки",
"theme-changed-description": "Команда для выполнения при переключении темы между темным и светлым режимами.",
"theme-changed-description": "Команда для выполнения при переключении темы между тёмным и светлым режимами.",
"theme-changed-label": "Тема изменена",
"theme-changed-placeholder": "например, notify-send \"Тема\" \"Переключена\"",
"title": "Хуки",
@@ -1021,12 +1022,12 @@
"settings-grid-view-description": "Показывать элементы в виде сетки вместо списка.",
"settings-icon-mode-description": "Использовать нативные системные иконки вместо иконок Tabler.",
"settings-icon-mode-label": "Использовать нативные иконки",
"settings-ignore-mouse-input-description": "Отключить взаимодействие с мышью и колесо прокрутки в лаунчере.",
"settings-ignore-mouse-input-description": "Отключить взаимодействие с мышью и колесом прокрутки в лаунчере.",
"settings-ignore-mouse-input-label": "Игнорировать ввод мыши",
"settings-position-description": "Выберите, где появляется панель запуска.",
"settings-show-categories-description": "Показывать вкладки категорий для фильтрации приложений.",
"settings-show-categories-label": "Показывать категории",
"settings-show-icon-background-description": "Показывать закругленный прямоугольник в качестве фона для иконок.",
"settings-show-icon-background-description": "Показывать закруглённый прямоугольник в качестве фона для иконок.",
"settings-show-icon-background-label": "Показать фон значка",
"settings-sort-by-usage-description": "Если включено, часто запускаемые приложения появляются в списке первыми.",
"settings-sort-by-usage-label": "Сортировать по частоте использования",
@@ -1054,7 +1055,7 @@
"date-time-use-analog-label": "Использовать аналоговый стиль часов",
"date-time-week-numbers-description": "Отображает номер недели в году (например, Неделя 38) в календаре.",
"date-time-week-numbers-label": "Показывать номера недель",
"location-desc": "Получите точную погоду и расписание ночного света, установив свое местоположение.",
"location-desc": "Получите точную погоду и расписание ночного света, установив своё местоположение.",
"location-search-description": "например, Москва, RU",
"location-search-label": "Поиск местоположения",
"location-search-placeholder": "Введите название местоположения",
@@ -1086,7 +1087,7 @@
},
"network": {
"bluetooth-description": "Включить управление Bluetooth.",
"bluetooth-rssi-polling-description": "Периодически опрашивать RSSI для подключенных устройств через bluetoothctl. Может быть недоступно для всех устройств; использует минимальные ресурсы при включении.",
"bluetooth-rssi-polling-description": "Периодически опрашивать RSSI для подключённых устройств через bluetoothctl. Может быть недоступно для всех устройств; использует минимальные ресурсы при включении.",
"bluetooth-rssi-polling-label": "Опрос сигнала Bluetooth",
"desc": "Управление Wi-Fi и Bluetooth подключениями.",
"wifi-description": "Управление беспроводными сетями (требуется NetworkManager)."
@@ -1112,8 +1113,8 @@
"history-normal-urgency-label": "Сохранять нормальную срочность в истории",
"media-toast-description": "Показывать всплывающее уведомление при изменении состояния воспроизведения медиа.",
"media-toast-label": "Медиа",
"monitors-desc": "Показывать уведомления на определенных мониторах. По умолчанию на всех, если ни один не выбран.",
"settings-always-on-top-description": "Отображать уведомления поверх полноэкранных окон и других слоев.",
"monitors-desc": "Показывать уведомления на определённых мониторах. По умолчанию на всех, если ни один не выбран.",
"settings-always-on-top-description": "Отображать уведомления поверх полноэкранных окон и других слоёв.",
"settings-background-opacity-description": "Настройка непрозрачности фона уведомлений.",
"settings-desc": "Настройка внешнего вида и поведения уведомлений.",
"settings-do-not-disturb-description": "Отключить все всплывающие окна уведомлений, если включено.",
@@ -1123,8 +1124,8 @@
"sounds-desc": "Настроить звуковые эффекты и громкость уведомлений.",
"sounds-enabled-description": "Включить звуковые эффекты для входящих уведомлений.",
"sounds-enabled-label": "Включить звуки уведомлений",
"sounds-excluded-apps-description": "Пропускать воспроизведение настроенного звука уведомлений для определенных приложений, имеющих собственные встроенные звуки.",
"sounds-excluded-apps-label": "Исключенные приложения",
"sounds-excluded-apps-description": "Пропускать воспроизведение настроенного звука уведомлений для определённых приложений, имеющих собственные встроенные звуки.",
"sounds-excluded-apps-label": "Исключённые приложения",
"sounds-excluded-apps-placeholder": "discord,firefox,chrome,chromium,edge",
"sounds-files-critical-description": "Путь к звуковому файлу, воспроизводимому для уведомлений с критическим приоритетом.",
"sounds-files-critical-label": "Звук критической важности",
@@ -1154,21 +1155,21 @@
"toast-keyboard-label": "Раскладка клавиатуры"
},
"osd": {
"always-on-top-description": "Отображать OSD поверх полноэкранных окон и других слоев.",
"always-on-top-description": "Отображать OSD поверх полноэкранных окон и других слоёв.",
"always-on-top-label": "Всегда сверху",
"background-opacity-description": "Управляет прозрачностью фона OSD.",
"background-opacity-label": "Прозрачность фона",
"description": "Настройка экранных индикаторов, таких как оверлеи громкости и яркости.",
"duration-auto-hide-description": "Настройка времени, прежде чем OSD исчезнет.",
"duration-auto-hide-label": "Автоматически скрывать через",
"duration-desc": "Как долго OSD остается видимым перед автоматическим скрытием.",
"duration-desc": "Как долго OSD остаётся видимым перед автоматическим скрытием.",
"duration-title": "Таймаут автоскрытия",
"enabled-description": "Показывать изменения громкости и яркости в реальном времени.",
"enabled-label": "Включить экранное отображение (OSD)",
"events-desc": "Выберите, какие события запускают экранное меню.",
"general-desc": "Настроить видимость и поведение экранного меню (OSD).",
"location-description": "Где появляются экранные отображения.",
"monitors-desc": "Показывать OSD на определенных мониторах. По умолчанию на всех, если ни один не выбран.",
"monitors-desc": "Показывать OSD на определённых мониторах. По умолчанию на всех, если ни один не выбран.",
"title": "Экранное отображение (OSD)",
"types-brightness-description": "Показывать OSD при изменении яркости экрана.",
"types-brightness-label": "Яркость",
@@ -1232,7 +1233,7 @@
"uninstall-dialog-description": "Вы уверены, что хотите удалить {plugin}? Это удалит все данные плагина.",
"uninstall-dialog-title": "Удалить плагин",
"uninstall-error": "Не удалось удалить: {error}",
"uninstall-success": "{plugin} успешно удален",
"uninstall-success": "{plugin} успешно удалён",
"uninstalling": "Удаление {plugin}...",
"update-all": "Обновить все ({count})",
"update-all-success": "Все плагины успешно обновлены",
@@ -1248,10 +1249,10 @@
"title": "Регион"
},
"session-menu": {
"countdown-duration-description": "Установить, как долго длится таймер обратного отсчета перед выполнением действий питания.",
"countdown-duration-label": "Длительность обратного отсчета",
"enable-countdown-description": "Показывать таймер обратного отсчета перед выполнением действий питания.",
"enable-countdown-label": "Включить таймер обратного отсчета",
"countdown-duration-description": "Установить, как долго длится таймер обратного отсчёта перед выполнением действий питания.",
"countdown-duration-label": "Длительность обратного отсчёта",
"enable-countdown-description": "Показывать таймер обратного отсчёта перед выполнением действий питания.",
"enable-countdown-label": "Включить таймер обратного отсчёта",
"entries-desc": "Настроить, какие действия питания отображаются в меню сеанса и в каком порядке.",
"entries-title": "Действия питания",
"entry-settings-command-description": "Пользовательская команда для выполнения этого действия. Оставьте пустым, чтобы использовать системную команду по умолчанию.",
@@ -1316,8 +1317,8 @@
"dim-desktop-description": "Затемнять рабочий стол при открытии панелей или меню.",
"dim-desktop-label": "Затемнять рабочий стол",
"dimmer-opacity-description": "Установить уровень непрозрачности для затемнения рабочего стола.",
"dimmer-opacity-label": "Непрозрачность затемненного рабочего стола",
"dimmer-opacity-reset": "Сбросить непрозрачность затемненного рабочего стола",
"dimmer-opacity-label": "Непрозрачность затемнённого рабочего стола",
"dimmer-opacity-reset": "Сбросить непрозрачность затемнённого рабочего стола",
"panel-background-opacity-description": "Установить прозрачность фона для всех панелей (верхней панели, панели запуска, настроек и т. д.).",
"panel-background-opacity-label": "Прозрачность фона панелей",
"panels-attached-to-bar-description": "Панели прикрепляются к панели и краям экрана, создавая цельный вид со стильными инвертированными углами.",
@@ -1348,7 +1349,7 @@
"automation-random-wallpaper-description": "Запланировать смену случайных обоев через регулярные интервалы.",
"automation-scheduled-change-description": "Автоматически менять обои через регулярные интервалы.",
"automation-scheduled-change-label": "Запланированная смена",
"look-feel-edge-smoothness-description": "Применяет мягкий, растушеванный эффект к краю переходов.",
"look-feel-edge-smoothness-description": "Применяет мягкий, растушёванный эффект к краю переходов.",
"look-feel-edge-smoothness-label": "Смягчить край перехода",
"look-feel-fill-color-description": "Выберите цвет заливки, который может появиться за обоями.",
"look-feel-fill-mode-description": "Выберите, как изображение должно масштабироваться, чтобы соответствовать разрешению вашего монитора.",
@@ -1361,7 +1362,7 @@
"settings-desc": "Управление тем, как обои управляются и отображаются.",
"settings-enable-management-description": "Управлять обоями с помощью Noctalia. Снимите флажок, если предпочитаете использовать другое приложение.",
"settings-enable-management-label": "Включить управление обоями",
"settings-enable-overview-description": "Применяет размытые и затемненные обои к экрану обзора.",
"settings-enable-overview-description": "Применяет размытые и затемнённые обои к экрану обзора.",
"settings-enable-overview-label": "Включить обои обзора",
"settings-folder-description": "Путь к вашей основной папке с обоями.",
"settings-folder-label": "Папка с обоями",
@@ -1375,6 +1376,8 @@
"settings-select-monitor-folder": "Выбрать папку с обоями для монитора",
"settings-selector-description": "Выберите обои.",
"settings-selector-position-description": "Выберите, где появляется панель выбора обоев.",
"settings-show-hidden-files-tooltip-hide": "Скрыть скрытые файлы",
"settings-show-hidden-files-tooltip-show": "Показать скрытые файлы",
"settings-title": "Настройки обоев",
"settings-view-mode-description": "Выберите способ отображения обоев из вашей директории.",
"settings-view-mode-label": "Режим просмотра",
@@ -1423,7 +1426,7 @@
"setup": {
"all-done": "Готово!",
"appearance": {
"subheader": "Выберите Dark Mode и источники цвета (обои или предопределенные)."
"subheader": "Выберите режим оформления и источник палитры цветов (обои или предопределённые)."
},
"customize": {
"header": "Настройте свой опыт",
@@ -1478,7 +1481,7 @@
},
"toast": {
"airplane-mode": {
"title": "Режим полета"
"title": "Режим полёта"
},
"battery": {
"low": "Низкий заряд батареи",
@@ -1508,7 +1511,7 @@
"disabled": "Режим 'Не беспокоить' отключен",
"disabled-desc": "Отображаются все уведомления",
"enabled": "Режим 'Не беспокоить' включен",
"enabled-desc": "Вы найдете эти уведомления в своей истории"
"enabled-desc": "Вы найдёте эти уведомления в своей истории"
},
"internet-limited": "Подключено без доступа к интернету",
"keyboard-layout": {
@@ -1517,8 +1520,8 @@
},
"kofi-opened": "Страница Ko-fi открыта в вашем браузере",
"missing-control-center": {
"description": "Виджет центра управления был удален с панели. Чтобы снова получить к нему доступ с панели, вам нужно будет повторно добавить виджет. Вы также можете открыть его, нажав правой кнопкой мыши на панель",
"label": "Последний виджет центра управления удален"
"description": "Виджет центра управления был удалён с панели. Чтобы снова получить к нему доступ с панели, вам нужно будет повторно добавить виджет. Вы также можете открыть его, нажав правой кнопкой мыши на панель",
"label": "Последний виджет центра управления удалён"
},
"night-light": {
"forced": "Принудительная активация",
@@ -1531,7 +1534,7 @@
"label": "Производительность Noctalia"
},
"power-profile": {
"changed": "Профиль питания изменен",
"changed": "Профиль питания изменён",
"profile-name": "{profile}"
},
"theming-processor-failed": {
@@ -1602,7 +1605,7 @@
"search-close": "Закрыть поиск",
"session-menu": "Меню сеанса",
"show-all-devices": "Показать все устройства",
"switch-to-dark-mode": "Темный режим",
"switch-to-dark-mode": "Тёмный режим",
"switch-to-light-mode": "Светлый режим",
"unmute": "Включить звук",
"up": "Родительский каталог",
@@ -1686,7 +1689,7 @@
},
"widgets": {
"color-picker": {
"palette-description": "Выберите из широкого диапазона предопределенных цветов.",
"palette-description": "Выберите из широкого диапазона предопределённых цветов.",
"palette-label": "Палитра",
"palette-theme-colors": "Быстрый доступ к цветовой палитре вашей темы.",
"title": "Выбор цвета"
@@ -1702,22 +1705,22 @@
"common-us-date": "Формат даты США",
"common-weekday-date": "День недели с датой",
"common-weekday-month-day": "День недели, месяц и число",
"day-abbreviated": "Сокращенное название дня",
"day-abbreviated": "Сокращённое название дня",
"day-full": "Полное название дня",
"day-leading-zero": "День с ведущим нулем (01-31)",
"day-leading-zero": "День с ведущим нулём (01-31)",
"day-no-leading-zero": "День без ведущего нуля (1-31)",
"hour-leading-zero": "Час с ведущим нулем (00-23) — 24-часовой формат",
"hour-leading-zero": "Час с ведущим нулём (00-23) — 24-часовой формат",
"hour-no-leading-zero": "Час без ведущего нуля (0-23) — 24-часовой формат",
"minute-leading-zero": "Минута с ведущим нулем (00-59)",
"minute-leading-zero": "Минута с ведущим нулём (00-59)",
"minute-no-leading-zero": "Минута без ведущего нуля (0-59)",
"month-abbreviated": "Сокращенное название месяца",
"month-abbreviated": "Сокращённое название месяца",
"month-full": "Полное название месяца",
"month-number-leading-zero": "Месяц как число с ведущим нулем (01-12)",
"month-number-leading-zero": "Месяц как число с ведущим нулём (01-12)",
"month-number-no-zero": "Месяц как число без ведущего нуля (1-12)",
"second-leading-zero": "Секунда с ведущим нулем (00-59)",
"second-leading-zero": "Секунда с ведущим нулём (00-59)",
"second-no-leading-zero": "Секунда без ведущего нуля (0-59)",
"timezone-abbreviation": "Сокращение часового пояса",
"year-four-digit": "Год как четырехзначное число",
"year-four-digit": "Год как четырёхзначное число",
"year-two-digit": "Год как двухзначное число (00-99)"
},
"file-picker": {
+3
View File
@@ -354,6 +354,7 @@
"clear": "Temizle",
"clipboard": "Panoya",
"close": "Kapat",
"color-muted": "Sessiz",
"colors": "Renkler",
"command": "Komut",
"connect": "Bağlan",
@@ -1375,6 +1376,8 @@
"settings-select-monitor-folder": "Ekran duvar kâğıdı klasörünü seç",
"settings-selector-description": "Duvar kâğıdınızı seçin.",
"settings-selector-position-description": "Duvar kâğıdı seçici panelinin nerede görüneceğini seçin.",
"settings-show-hidden-files-tooltip-hide": "Gizli dosyaları gizle",
"settings-show-hidden-files-tooltip-show": "Gizli dosyaları göster",
"settings-title": "Duvar kâğıdı ayarları",
"settings-view-mode-description": "Duvar kağıtlarının dizininizden nasıl görüntüleneceğini seçin.",
"settings-view-mode-label": "Görüntüleme modu",
+3
View File
@@ -354,6 +354,7 @@
"clear": "Очистити",
"clipboard": "Буфер обміну",
"close": "Закрити",
"color-muted": "Вимкнено звук",
"colors": "Кольори",
"command": "Команда",
"connect": "Підключити",
@@ -1375,6 +1376,8 @@
"settings-select-monitor-folder": "Вибрати теку шпалер монітора",
"settings-selector-description": "Виберіть свої шпалери.",
"settings-selector-position-description": "Виберіть, де відображатиметься панель вибору шпалер.",
"settings-show-hidden-files-tooltip-hide": "Приховати приховані файли",
"settings-show-hidden-files-tooltip-show": "Показати приховані файли",
"settings-title": "Налаштування шпалер",
"settings-view-mode-description": "Оберіть спосіб відображення шпалер з вашого каталогу.",
"settings-view-mode-label": "Режим перегляду",
+20 -17
View File
@@ -192,33 +192,33 @@
"width-description": "间距宽度(像素)。"
},
"system-monitor": {
"compact-mode-description": "将统计数据以迷你条形图而非文本值的形式显示防止布局偏移。",
"compact-mode-description": "将数据以条形图显示防止布局偏移。",
"compact-mode-label": "紧凑模式",
"cpu-temperature-description": "如果可用,显示CPU温度读数。",
"cpu-temperature-label": "CPU温度",
"cpu-usage-description": "显示当前CPU使用百分比。",
"cpu-usage-label": "CPU使用率",
"cpu-temperature-description": "显示 CPU 温度(如果可用)。",
"cpu-temperature-label": "CPU 温度",
"cpu-usage-description": "以百分比显示当前 CPU 使用",
"cpu-usage-label": "CPU 使用率",
"disk-path-description": "选择要监控的磁盘挂载点。",
"disk-path-label": "磁盘路径",
"gpu-temperature-description": "显示GPU温度读数(如果可用)。",
"gpu-temperature-description": "显示 GPU 温度(如果可用)。",
"load-average-description": "显示系统平均负载。",
"load-average-label": "平均负载",
"memory-percentage-description": "以百分比而不是绝对值显示内存使用情况。",
"memory-percentage-description": "以百分比(而非绝对值显示内存用量。",
"memory-percentage-label": "内存百分比",
"memory-usage-description": "显示当前RAM使用信息。",
"memory-usage-description": "显示当前内存用量信息。",
"memory-usage-label": "内存使用率",
"network-traffic-description": "显示网络上传和下载速度。",
"network-traffic-label": "网络流量",
"storage-usage-description": "显示磁盘空间使用信息。",
"storage-usage-label": "存储使用率",
"swap-usage-description": "显示交换内存使用情况。",
"swap-usage-label": "交换内存使用量",
"use-monospace-font-description": "使用等宽字体以保持字符宽度一致。",
"storage-usage-description": "显示磁盘空间使用情况。",
"storage-usage-label": "存储用量",
"swap-usage-description": "显示 Swap 使用情况。",
"swap-usage-label": "Swap 用量",
"use-monospace-font-description": "使用等宽字体以保持字符宽度一致。",
"use-monospace-font-label": "等宽字体"
},
"taskbar": {
"colorize-icons-description": "将主题颜色应用到任务栏图标。",
"hide-mode-description": "当没有匹配窗口时控制小部件的行为。",
"hide-mode-description": "控制无匹配窗口时小部件的行为。",
"hide-mode-label": "隐藏模式",
"icon-scale-description": "设置任务栏图标的缩放比例。",
"icon-scale-label": "图标缩放",
@@ -354,6 +354,7 @@
"clear": "清除",
"clipboard": "剪贴板",
"close": "关闭",
"color-muted": "已静音",
"colors": "颜色",
"command": "命令",
"connect": "连接",
@@ -448,7 +449,7 @@
"test": "测试",
"thresholds": "阈值",
"title": "标题",
"toast": "气泡通知",
"toast": "Toast 消息",
"trusted": "可信赖的",
"uninstall": "卸载",
"unknown": "未知",
@@ -1110,7 +1111,7 @@
"history-low-urgency-label": "将低紧急度保存到历史记录",
"history-normal-urgency-description": "将正常优先级通知保存到历史记录。",
"history-normal-urgency-label": "将正常紧急度保存到历史记录",
"media-toast-description": "媒体播放状态改变时显示 Toast 提示。",
"media-toast-description": "媒体播放状态改变时显示一个 Toast。",
"media-toast-label": "媒体",
"monitors-desc": "在特定显示器上显示通知。如果未选择,则默认为全部。",
"settings-always-on-top-description": "在全屏窗口和其他图层上方显示通知。",
@@ -1150,7 +1151,7 @@
"sounds-volume-description": "调整通知声音的音量级别。",
"sounds-volume-label": "声音音量",
"toast-desc": "配置气泡通知的外观和行为。",
"toast-keyboard-description": "键盘布局改变时显示一个气泡提示。",
"toast-keyboard-description": "键盘布局改变时显示一个 Toast。",
"toast-keyboard-label": "键盘布局"
},
"osd": {
@@ -1375,6 +1376,8 @@
"settings-select-monitor-folder": "选择显示器壁纸文件夹",
"settings-selector-description": "选择你的壁纸。",
"settings-selector-position-description": "选择壁纸选择面板的显示位置。",
"settings-show-hidden-files-tooltip-hide": "不显示隐藏文件",
"settings-show-hidden-files-tooltip-show": "显示隐藏文件",
"settings-title": "壁纸设置",
"settings-view-mode-description": "选择从您的目录显示壁纸的方式。",
"settings-view-mode-label": "查看模式",
+44 -1
View File
@@ -105,6 +105,7 @@
"hide-mode-description": "控制當指令沒有輸出時,元件之可見性",
"hide-mode-expand-with-output": "有訊息輸出時展開",
"hide-mode-label": "隱藏模式",
"hide-mode-max-transparent": "最大展開但透明",
"icon-description": "從圖示庫選取一個圖示",
"ipc-identifier-description": "IPC 指令的唯一識別碼, 在使用 'qs -c noctalia-shell ipc call cb [動作] [識別碼]' 時利用此識別碼可透過 IPC 控制此按鈕。",
"ipc-identifier-label": "IPC 識別碼",
@@ -127,6 +128,8 @@
"right-click-update-text": "在點擊右鍵時更新顯示文字",
"show-icon-description": "切換小工具圖示的能見性",
"show-icon-label": "顯示圖示",
"text-stream-description": "命令列的串流內容將以文字形式顯示在按鈕上。",
"text-stream-label": "串流",
"wheel-description": "在滾輪滾動時要執行的指令<br>在指令裡使用 $delta 表示指令中滾動多少的值",
"wheel-down-description": "在滾輪下滾時要執行的指令",
"wheel-down-label": "滾輪下滾",
@@ -265,6 +268,7 @@
"show-applications-label": "顯示應用程式",
"show-labels-only-when-occupied-description": "只在工作區有視窗時顯示工作區標籤",
"show-labels-only-when-occupied-label": "佔用時顯示標籤",
"unfocused-icons-opacity-description": "設定非焦點應用程式圖示的不透明度。",
"unfocused-icons-opacity-label": "非焦點圖示的不透明度"
}
},
@@ -350,6 +354,7 @@
"clear": "清空",
"clipboard": "剪貼簿",
"close": "關閉",
"color-muted": "已靜音",
"colors": "顏色",
"command": "指令",
"connect": "連接",
@@ -368,6 +373,7 @@
"disconnected": "已斷線",
"disconnecting": "正在斷線...",
"download": "下載",
"duration": "持續時間",
"edit": "編輯",
"enabled": "已啟用",
"events": "事件",
@@ -401,6 +407,7 @@
"network": "網路",
"next": "下一首",
"night-light": "夜燈模式",
"no": "否",
"no-results": "沒有結果",
"no-summary": "沒有摘要",
"none": "無",
@@ -428,6 +435,7 @@
"scanning": "掃描中...",
"screen-corners": "畫面邊角",
"search": "搜尋",
"security": "安全",
"select": "選取",
"shortcuts": "快捷鍵",
"shutdown": "關機",
@@ -459,7 +467,8 @@
"weather-loading": "正在載入天氣...",
"week": "週",
"widgets": "小工具",
"width": "寬度"
"width": "寬度",
"yes": "是"
},
"control-center": {
"power-profile": {
@@ -779,13 +788,19 @@
"desc": "設定控制中心面板的位置和行為",
"position-description": "選擇當開啟控制中心時該從哪裡出現",
"shortcuts-custom-button-command-description": "在按鈕按下時要執行的命令",
"shortcuts-custom-button-enable-on-state-logic-description": "啟用第二個圖示與「熱區」狀態,並根據檢查命令判斷。",
"shortcuts-custom-button-enable-on-state-logic-label": "啟用狀態邏輯",
"shortcuts-custom-button-general-tooltip-text-description": "自訂按鈕的工具框提示文字",
"shortcuts-custom-button-general-tooltip-text-label": "提示框文字",
"shortcuts-custom-button-on-clicked-label": "左鍵指令",
"shortcuts-custom-button-on-middle-clicked-label": "中鍵指令",
"shortcuts-custom-button-on-right-clicked-label": "右鍵指令",
"shortcuts-custom-button-on-state-command-description": "用來檢查按鈕是否應處於「開啟」狀態的指令。傳回 0 表示應開啟,非 0 表示應關閉。",
"shortcuts-custom-button-on-state-command-label": "狀態檢查命令",
"shortcuts-custom-button-on-state-icon-description": "按鈕處於「開啓」狀態時的圖示。",
"shortcuts-custom-button-on-state-icon-label": "開啓狀態圖示",
"shortcuts-custom-button-state-checks-add": "新增狀態確認",
"shortcuts-custom-button-state-checks-command": "用於此狀態檢查的執行程式命令",
"shortcuts-custom-button-state-checks-label": "狀態確認",
"shortcuts-custom-button-state-checks-remove": "移除",
"shortcuts-custom-button-tooltip-description": "當游標移過按鈕時要顯示的提示框文字",
@@ -878,7 +893,11 @@
"dock": {
"appearance-background-opacity-description": "調整 Dock 的背景不透明度",
"appearance-border-radius-description": "調整 Dock 的邊框圓角。",
"appearance-border-radius-label": "邊框圓角",
"appearance-colorize-icons-description": "將主題色彩套用至 Dock 上的應用程式圖示(僅非焦點應用程式)。",
"appearance-colorize-icons-label": "為圖示上色",
"appearance-dead-opacity-description": "調整未執行中應用程式圖示的不透明度。",
"appearance-dead-opacity-label": "非活躍應用程式不透明度",
"appearance-desc": "自訂 Dock 的外觀及行為",
"appearance-display-auto-hide": "自動隱藏",
"appearance-display-description": "選擇 Dock 的行為",
@@ -889,6 +908,8 @@
"appearance-hide-show-speed-label": "隱藏/浮出速度",
"appearance-icon-size-description": "調整 Dock 的整體大小",
"appearance-icon-size-label": "Dock 大小",
"appearance-inactive-indicators-description": "為所有應用程式顯示指示膠囊(而不僅是正在運作的程式)。",
"appearance-inactive-indicators-label": "運作指示器",
"appearance-pinned-static-description": "總是將釘選的程式圖示固定放在左邊",
"appearance-pinned-static-label": "固定放置釘選程式",
"appearance-position-description": "選擇 Dock 該放在畫面的哪裡",
@@ -982,13 +1003,18 @@
"clipboard-desc": "在啟動器存取及整理你的剪貼簿歷史",
"execute-desc": "設定應用程式該如何啟動",
"execute-title": "執行",
"settings-annotation-tool-description": "點擊剪貼簿歷史記錄中的註記按鈕時要執行的命令,圖片將會透過管線傳送至此命令。",
"settings-annotation-tool-label": "註記工具",
"settings-annotation-tool-placeholder": "例如: gradia', 'satty -f -'",
"settings-auto-paste-description": "自動貼上選取的剪貼簿項目, 需要 wtype 套件",
"settings-auto-paste-label": "自動貼上",
"settings-clip-preview-description": "使用 >clip 命令時顯示剪貼簿內容的預覽。",
"settings-clip-preview-label": "啟用剪貼簿預覽",
"settings-clip-wrap-text-description": "將剪貼簿的文字換行顯示而不是截掉",
"settings-clip-wrap-text-label": "剪貼簿文字換行",
"settings-clipboard-history-description": "在啟動器存取先前所複製的項目",
"settings-clipboard-history-label": "啟用剪貼簿歷史",
"settings-custom-launch-prefix-description": "在命令前加上自訂啟動器前綴(例如:用「runapp」來整合 systemd)。",
"settings-custom-launch-prefix-enabled-description": "使用自訂前綴而不是預設的方式來啟動應用程式",
"settings-custom-launch-prefix-enabled-label": "啟用自訂的程式啟動前綴",
"settings-custom-launch-prefix-label": "自訂啟動前綴",
@@ -1007,6 +1033,7 @@
"settings-sort-by-usage-label": "依最常使用排序",
"settings-terminal-command-description": "打開終端機的指令, 例如: 'kitty -e'或 'gnome-terminal --'",
"settings-terminal-command-label": "終端機指令",
"settings-use-app2unit-description": "使用替代的啟動方式,以更好地管理應用程式程序並避免問題。",
"settings-use-app2unit-label": "使用 App2Unit 啟動應用程式",
"title": "啟動器"
},
@@ -1073,6 +1100,9 @@
"duration-low-urgency-label": "低急迫性",
"duration-normal-urgency-description": "設定普通急迫的通知該顯示多久",
"duration-normal-urgency-label": "普通急迫",
"duration-reset": "重設逾時時間長度",
"duration-respect-expire-description": "使用通知中設定的到期逾時時間。",
"duration-respect-expire-label": "遵循到期逾時設定",
"duration-title": "通知顯示時長",
"history-critical-urgency-description": "將非常危急的通知存放到歷史通知",
"history-critical-urgency-label": "儲存非常危急到歷史通知",
@@ -1094,11 +1124,13 @@
"sounds-desc": "設定通知所使用的音效及音量",
"sounds-enabled-description": "在有新通知時啟用音效",
"sounds-enabled-label": "啟用通知音效",
"sounds-excluded-apps-description": "針對特定具有內建音效的應用程式,略過在此設定的通知音效。",
"sounds-excluded-apps-label": "排除的應用程式",
"sounds-excluded-apps-placeholder": "discord,firefox,chrome,chromium,edge",
"sounds-files-critical-description": "作為非常危急的通知音效檔案所在的路徑",
"sounds-files-critical-label": "非常危急的音效",
"sounds-files-critical-select-title": "選擇相當危急音效檔",
"sounds-files-desc": "為不同的通知緊急程度等級設定自訂音效檔案。",
"sounds-files-low-description": "作為低急迫性的的通知音效檔案所在的路徑",
"sounds-files-low-label": "低急迫性的音效",
"sounds-files-low-select-title": "選擇低急迫性音效檔",
@@ -1186,6 +1218,7 @@
"settings-saved": "已儲存模組設定",
"settings-tooltip": "外掛模組設定",
"source-custom": "自訂來源",
"sources-add-custom": "新增自訂存儲庫",
"sources-add-dialog-description": "新增一個 GitHub 儲存庫作為模組來源",
"sources-add-dialog-error": "新增模組來源失敗",
"sources-add-dialog-name": "儲存庫名稱",
@@ -1193,6 +1226,8 @@
"sources-add-dialog-success": "成功新增外掛模組來源",
"sources-add-dialog-title": "新增外掛模組來源",
"sources-add-dialog-url": "儲存庫 URL",
"sources-description": "管理外掛套件存儲庫。",
"sources-placeholder": "我的超酷儲存庫",
"sources-remove-tooltip": "移除外掛模組來源",
"title": "外掛模組 (Plugins)",
"uninstall-dialog-description": "你確定想要移除 {plugin}? 這樣會移除所有模組的資料",
@@ -1272,8 +1307,10 @@
"appearance-desc": "自訂視覺元素, 如提示框, 邊框及陰影",
"box-border-description": "在內容區塊的外圍顯示外框",
"box-border-label": "頁面容器外框",
"box-border-radius-description": "調整主要版面區塊(例如側邊欄、卡片與內容面板)的圓角弧度。",
"box-border-radius-label": "頁面容器半徑",
"box-border-radius-reset": "重設頁面容器半徑",
"control-border-radius-description": "控制互動元素(包括按鈕、切換開關與文字欄位)的圓角弧度。",
"control-border-radius-label": "控制項半徑",
"control-border-radius-reset": "重設控制項半徑",
"desc": "自訂介面顯示的行為, 外觀及風格",
@@ -1312,6 +1349,8 @@
"automation-random-wallpaper-description": "在固定的間格隨機更換桌布",
"automation-scheduled-change-description": "在固定的間格自動更換桌布",
"automation-scheduled-change-label": "排程更換",
"look-feel-edge-smoothness-description": "在轉場邊緣套用柔和的羽化效果。",
"look-feel-edge-smoothness-label": "柔化轉場邊緣",
"look-feel-fill-color-description": "挑選一個桌布底下可能會顯示的填充實色",
"look-feel-fill-mode-description": "選擇圖像該以什麼方式縮放來符合顯示器的解析度",
"look-feel-fill-mode-label": "填充模式",
@@ -1337,6 +1376,8 @@
"settings-select-monitor-folder": "選擇顯示器所使用的桌布文件夾",
"settings-selector-description": "選擇你的桌布",
"settings-selector-position-description": "選擇挑選桌布面板要在哪裡出現",
"settings-show-hidden-files-tooltip-hide": "隱藏隱藏檔案",
"settings-show-hidden-files-tooltip-show": "顯示隱藏檔案",
"settings-title": "桌布設定",
"settings-view-mode-description": "選擇如何從您的目錄顯示桌布。",
"settings-view-mode-label": "檢視模式",
@@ -1548,6 +1589,7 @@
"next-month": "下個月",
"night-light-not-installed": "夜燈模式 (無法使用)",
"noctalia-performance-enabled": "Noctalia 效能模式",
"open-annotation-tool": "使用註記工具開啟",
"open-control-center": "控制中心",
"open-notification-history-enable-dnd": "歷史通知",
"open-settings": "設定",
@@ -1685,6 +1727,7 @@
"item": "項目",
"items": "項目",
"search-placeholder": "選擇檔案及文件夾...",
"select-current": "選擇此項",
"select-file": "選擇檔案",
"select-folder": "選擇文件夾",
"selected": "已選取:",
+1
View File
@@ -139,6 +139,7 @@
"directory": "",
"monitorDirectories": [],
"enableMultiMonitorDirectories": false,
"showHiddenFiles": false,
"viewMode": "single",
"setWallpaperOnAllMonitors": true,
"fillMode": "crop",
File diff suppressed because it is too large Load Diff
+199 -16
View File
@@ -20,30 +20,213 @@ Singleton {
property bool reloadColors: false
// Flag indicating theme colors are currently transitioning (for widgets to disable their own animations)
property bool isTransitioning: false
// Timer to reset isTransitioning after animation completes
Timer {
id: transitionTimer
interval: Style.animationSlowest + 50 // Small buffer after animation
onTriggered: root.isTransitioning = false
}
// --- Key Colors: These are the main accent colors that define your app's style
readonly property color mPrimary: customColorsData.mPrimary
readonly property color mOnPrimary: customColorsData.mOnPrimary
readonly property color mSecondary: customColorsData.mSecondary
readonly property color mOnSecondary: customColorsData.mOnSecondary
readonly property color mTertiary: customColorsData.mTertiary
readonly property color mOnTertiary: customColorsData.mOnTertiary
property color mPrimary: defaultColors.mPrimary
property color mOnPrimary: defaultColors.mOnPrimary
property color mSecondary: defaultColors.mSecondary
property color mOnSecondary: defaultColors.mOnSecondary
property color mTertiary: defaultColors.mTertiary
property color mOnTertiary: defaultColors.mOnTertiary
// --- Utility Colors: These colors serve specific, universal purposes like indicating errors
readonly property color mError: customColorsData.mError
readonly property color mOnError: customColorsData.mOnError
property color mError: defaultColors.mError
property color mOnError: defaultColors.mOnError
// --- Surface and Variant Colors: These provide additional options for surfaces and their contents, creating visual hierarchy
readonly property color mSurface: customColorsData.mSurface
readonly property color mOnSurface: customColorsData.mOnSurface
property color mSurface: defaultColors.mSurface
property color mOnSurface: defaultColors.mOnSurface
readonly property color mSurfaceVariant: customColorsData.mSurfaceVariant
readonly property color mOnSurfaceVariant: customColorsData.mOnSurfaceVariant
property color mSurfaceVariant: defaultColors.mSurfaceVariant
property color mOnSurfaceVariant: defaultColors.mOnSurfaceVariant
readonly property color mOutline: customColorsData.mOutline
readonly property color mShadow: customColorsData.mShadow
property color mOutline: defaultColors.mOutline
property color mShadow: defaultColors.mShadow
readonly property color mHover: customColorsData.mHover
readonly property color mOnHover: customColorsData.mOnHover
property color mHover: defaultColors.mHover
property color mOnHover: defaultColors.mOnHover
// --- Color transition animations ---
Behavior on mPrimary {
ColorAnimation {
duration: Style.animationSlowest
easing.type: Easing.OutCubic
}
}
Behavior on mOnPrimary {
ColorAnimation {
duration: Style.animationSlowest
easing.type: Easing.OutCubic
}
}
Behavior on mSecondary {
ColorAnimation {
duration: Style.animationSlowest
easing.type: Easing.OutCubic
}
}
Behavior on mOnSecondary {
ColorAnimation {
duration: Style.animationSlowest
easing.type: Easing.OutCubic
}
}
Behavior on mTertiary {
ColorAnimation {
duration: Style.animationSlowest
easing.type: Easing.OutCubic
}
}
Behavior on mOnTertiary {
ColorAnimation {
duration: Style.animationSlowest
easing.type: Easing.OutCubic
}
}
Behavior on mError {
ColorAnimation {
duration: Style.animationSlowest
easing.type: Easing.OutCubic
}
}
Behavior on mOnError {
ColorAnimation {
duration: Style.animationSlowest
easing.type: Easing.OutCubic
}
}
Behavior on mSurface {
ColorAnimation {
duration: Style.animationSlowest
easing.type: Easing.OutCubic
}
}
Behavior on mOnSurface {
ColorAnimation {
duration: Style.animationSlowest
easing.type: Easing.OutCubic
}
}
Behavior on mSurfaceVariant {
ColorAnimation {
duration: Style.animationSlowest
easing.type: Easing.OutCubic
}
}
Behavior on mOnSurfaceVariant {
ColorAnimation {
duration: Style.animationSlowest
easing.type: Easing.OutCubic
}
}
Behavior on mOutline {
ColorAnimation {
duration: Style.animationSlowest
easing.type: Easing.OutCubic
}
}
Behavior on mShadow {
ColorAnimation {
duration: Style.animationSlowest
easing.type: Easing.OutCubic
}
}
Behavior on mHover {
ColorAnimation {
duration: Style.animationSlowest
easing.type: Easing.OutCubic
}
}
Behavior on mOnHover {
ColorAnimation {
duration: Style.animationSlowest
easing.type: Easing.OutCubic
}
}
// Helper to start transition and update a color
function startTransition() {
root.isTransitioning = true;
transitionTimer.restart();
}
// Update colors when customColorsData changes (imperative assignment enables Behavior animations)
Connections {
target: customColorsData
function onMPrimaryChanged() {
startTransition();
root.mPrimary = customColorsData.mPrimary;
}
function onMOnPrimaryChanged() {
startTransition();
root.mOnPrimary = customColorsData.mOnPrimary;
}
function onMSecondaryChanged() {
startTransition();
root.mSecondary = customColorsData.mSecondary;
}
function onMOnSecondaryChanged() {
startTransition();
root.mOnSecondary = customColorsData.mOnSecondary;
}
function onMTertiaryChanged() {
startTransition();
root.mTertiary = customColorsData.mTertiary;
}
function onMOnTertiaryChanged() {
startTransition();
root.mOnTertiary = customColorsData.mOnTertiary;
}
function onMErrorChanged() {
startTransition();
root.mError = customColorsData.mError;
}
function onMOnErrorChanged() {
startTransition();
root.mOnError = customColorsData.mOnError;
}
function onMSurfaceChanged() {
startTransition();
root.mSurface = customColorsData.mSurface;
}
function onMOnSurfaceChanged() {
startTransition();
root.mOnSurface = customColorsData.mOnSurface;
}
function onMSurfaceVariantChanged() {
startTransition();
root.mSurfaceVariant = customColorsData.mSurfaceVariant;
}
function onMOnSurfaceVariantChanged() {
startTransition();
root.mOnSurfaceVariant = customColorsData.mOnSurfaceVariant;
}
function onMOutlineChanged() {
startTransition();
root.mOutline = customColorsData.mOutline;
}
function onMShadowChanged() {
startTransition();
root.mShadow = customColorsData.mShadow;
}
function onMHoverChanged() {
startTransition();
root.mHover = customColorsData.mHover;
}
function onMOnHoverChanged() {
startTransition();
root.mOnHover = customColorsData.mOnHover;
}
}
// --------------------------------
// Default colors: Rose Pine
+3
View File
@@ -98,6 +98,9 @@ Singleton {
"volume-low": "volume-2",
"volume-high": "volume",
"weather-sun": "sun",
"weather-moon": "moon",
"weather-moon-stars": "moon-stars",
"weather-cloud-off": "cloud-off",
"weather-cloud": "cloud",
"weather-cloud-haze": "cloud-fog",
"weather-cloud-lightning": "cloud-bolt",
+1
View File
@@ -342,6 +342,7 @@ Singleton {
property string directory: ""
property list<var> monitorDirectories: []
property bool enableMultiMonitorDirectories: false
property bool showHiddenFiles: false
property string viewMode: "single" // "single" | "recursive" | "browse"
property bool setWallpaperOnAllMonitors: true
property string fillMode: "crop"
+1
View File
@@ -83,6 +83,7 @@ Item {
border.width: Style.capsuleBorderWidth
Behavior on color {
enabled: !Color.isTransitioning
ColorAnimation {
duration: Style.animationFast
easing.type: Easing.InOutQuad
+1
View File
@@ -97,6 +97,7 @@ Item {
anchors.horizontalCenter: parent.horizontalCenter
Behavior on color {
enabled: !Color.isTransitioning
ColorAnimation {
duration: Style.animationFast
easing.type: Easing.InOutQuad
+21 -54
View File
@@ -39,10 +39,10 @@ Item {
readonly property bool hideIfNotDetected: widgetSettings.hideIfNotDetected !== undefined ? widgetSettings.hideIfNotDetected : widgetMetadata.hideIfNotDetected
readonly property bool hideIfIdle: widgetSettings.hideIfIdle !== undefined ? widgetSettings.hideIfIdle : widgetMetadata.hideIfIdle
// Only show low battery warning if device is ready (prevents false positive during initialization)
readonly property bool isLowBattery: isReady && (!charging && !isPluggedIn) && percent <= warningThreshold
readonly property bool isLowBattery: isReady && (!isCharging && !isPluggedIn) && percent <= warningThreshold
// Visibility: show if hideIfNotDetected is false, or if battery is ready (after initialization)
readonly property bool shouldShow: !hideIfNotDetected || (isReady && (hideIfIdle ? (!charging && !isPluggedIn) : true))
readonly property bool shouldShow: !hideIfNotDetected || (isReady && (hideIfIdle ? (!isCharging && !isPluggedIn) : true))
visible: shouldShow
opacity: shouldShow ? 1.0 : 0.0
@@ -109,7 +109,7 @@ Item {
if (battery.type === UPowerDeviceType.Battery && battery.isPresent !== undefined) {
return battery.isPresent;
}
return battery.ready && battery.percentage !== undefined && (battery.percentage > 0 || chargingStatus(battery.state));
return battery.ready && battery.percentage !== undefined && (battery.percentage > 0 || isCharging);
}
return false;
}
@@ -126,49 +126,21 @@ Item {
readonly property bool isReady: testMode ? true : (initializationComplete && battery && battery.ready && isDevicePresent && (battery.percentage !== undefined || hasBluetoothBattery))
readonly property real percent: testMode ? testPercent : (isReady ? (hasBluetoothBattery ? (bluetoothDevice.battery * 100) : (battery.percentage * 100)) : 0)
readonly property bool charging: testMode ? testCharging : (isReady ? chargingStatus(battery.state) : false) // Assuming not charging if battery is not ready
readonly property bool isPluggedIn: testMode ? testPluggedIn : (isReady ? getPluggedInStatus(battery.state) : false) // We can be plugged in or charging but can't both.
readonly property bool isCharging: testMode ? testCharging : (isReady ? battery.state === UPowerDeviceState.Charging : false)
readonly property bool isPluggedIn: testMode ? testPluggedIn : (isReady ? battery.state === UPowerDeviceState.FullyCharged || battery.state === UPowerDeviceState.PendingCharge : false)
property bool hasNotifiedLowBattery: false
implicitWidth: pill.width
implicitHeight: pill.height
// http://upower.freedesktop.org/docs/Device.html#Device.properties
function chargingStatus(state) {
switch (state) {
case UPowerDeviceState.Charging: // 1
// Logger.e("Battery", "Battery is charging (Battery is charging with " + (Math.floor(battery.changeRate * 10) / 10).toFixed(1) + "W)"); // debug
return true;
case UPowerDeviceState.Discharging: // 2
case UPowerDeviceState.Empty: // 3
case UPowerDeviceState.PendingDischarge: // 6
return false;
default:
return false; // unknown state 0 Fix #1417
}
}
function getPluggedInStatus(state) {
// Treat low charge rate (< 5W) as plugged in but not actively charging (grace period)
if (state === UPowerDeviceState.Charging && battery.changeRate !== undefined && Math.abs(battery.changeRate) < 5) {
return true;
}
switch (state) {
case UPowerDeviceState.FullyCharged: // 4
case UPowerDeviceState.PendingCharge: // 5
// Logger.e("Battery", "Battery is NOT charging (Power rate: " + (Math.floor(battery.changeRate * 10) / 10).toFixed(1) + "W)"); // debug
return true;
default:
return false;
}
}
function maybeNotify(currentPercent, isCharging, isPluggedIn, isReady) {
if (isReady && (!isCharging && !isPluggedIn) && !hasNotifiedLowBattery && currentPercent <= warningThreshold) {
function maybeNotify(currentPercent, charging, pluggedIn, isReady) {
if (isReady && (!charging && !pluggedIn) && !hasNotifiedLowBattery && currentPercent <= warningThreshold) {
hasNotifiedLowBattery = true;
ToastService.showWarning(I18n.tr("toast.battery.low"), I18n.tr("toast.battery.low-desc", {
"percent": Math.round(currentPercent)
}));
// Logger.e("Battery", "Low battery at " + (Math.floor(currentPercent).toFixed(1)) + "%", "isCharging: " + isCharging, "isPluggedIn: " + isPluggedIn, "isReady: " + isReady); // debug
} else if (hasNotifiedLowBattery && (isCharging || isPluggedIn || currentPercent > warningThreshold + 5)) {
} else if (hasNotifiedLowBattery && (charging || pluggedIn || currentPercent > warningThreshold + 5)) {
hasNotifiedLowBattery = false;
}
}
@@ -181,15 +153,15 @@ Item {
target: battery
function onPercentageChanged() {
if (battery) {
maybeNotify(getCurrentPercent(), chargingStatus(battery.state), getPluggedInStatus(battery.state), isReady);
maybeNotify(getCurrentPercent(), isCharging, isPluggedIn, isReady);
}
}
function onStateChanged() {
if (battery) {
if (chargingStatus(battery.state) || getPluggedInStatus(battery.state)) {
if (isCharging || isPluggedIn) {
hasNotifiedLowBattery = false;
}
maybeNotify(getCurrentPercent(), chargingStatus(battery.state), getPluggedInStatus(battery.state), isReady);
maybeNotify(getCurrentPercent(), isCharging, isPluggedIn, isReady);
}
}
}
@@ -198,7 +170,7 @@ Item {
target: bluetoothDevice
function onBatteryChanged() {
if (bluetoothDevice && hasBluetoothBattery) {
maybeNotify(bluetoothDevice.battery * 100, battery ? chargingStatus(battery.state) : false, battery ? getPluggedInStatus(battery.state) : false, true);
maybeNotify(bluetoothDevice.battery * 100, battery ? isCharging : false, battery ? isPluggedIn : false, true);
}
}
}
@@ -231,14 +203,14 @@ Item {
screen: root.screen
oppositeDirection: BarService.getPillDirection(root)
icon: testMode ? BatteryService.getIcon(testPercent, testCharging, testPluggedIn, true) : BatteryService.getIcon(percent, charging, isPluggedIn, isReady)
icon: testMode ? BatteryService.getIcon(testPercent, testCharging, testPluggedIn, true) : BatteryService.getIcon(percent, isCharging, isPluggedIn, isReady)
text: (isReady || testMode) ? Math.round(percent) : "-"
suffix: "%"
autoHide: false
forceOpen: isReady && displayMode === "alwaysShow"
forceClose: displayMode === "alwaysHide" || (initializationComplete && !isReady)
customBackgroundColor: !initializationComplete ? "transparent" : (charging ? Color.mPrimary : (isLowBattery ? Color.mError : "transparent"))
customTextIconColor: !initializationComplete ? "transparent" : (charging ? Color.mOnPrimary : (isLowBattery ? Color.mOnError : "transparent"))
customBackgroundColor: !initializationComplete ? "transparent" : (isCharging ? Color.mPrimary : (isLowBattery ? Color.mError : "transparent"))
customTextIconColor: !initializationComplete ? "transparent" : (isCharging ? Color.mOnPrimary : (isLowBattery ? Color.mOnError : "transparent"))
tooltipText: {
let lines = [];
@@ -249,35 +221,30 @@ Item {
if (!isReady || !isDevicePresent) {
return I18n.tr("battery.no-battery-detected");
}
if (battery.timeToEmpty > 0) {
if (!isPluggedIn && battery.timeToEmpty > 0) {
lines.push(I18n.tr("battery.time-left", {
"time": Time.formatVagueHumanReadableDuration(battery.timeToEmpty)
}));
}
if (battery.timeToFull > 0) {
if (!isPluggedIn && battery.timeToFull > 0) {
lines.push(I18n.tr("battery.time-until-full", {
"time": Time.formatVagueHumanReadableDuration(battery.timeToFull)
}));
}
if (battery.changeRate !== undefined) {
const rate = Math.abs(battery.changeRate);
if (charging) {
if (isPluggedIn) {
lines.push(I18n.tr("battery.plugged-in"));
} else if (isCharging) {
lines.push(I18n.tr("battery.charging-rate", {
"rate": rate.toFixed(2)
}));
} else if (isPluggedIn) {
lines.push(I18n.tr("battery.plugged-in"));
} else {
lines.push(I18n.tr("battery.discharging-rate", {
"rate": rate.toFixed(2)
}));
}
}
if (battery.healthPercentage !== undefined && battery.healthPercentage > 0) {
lines.push(I18n.tr("battery.health", {
"percent": Math.round(battery.healthPercentage)
}));
}
return lines.join("\n");
}
onClicked: PanelService.getPanel("batteryPanel", screen)?.toggle(this)
+6 -6
View File
@@ -74,27 +74,27 @@ Rectangle {
// Load Average
if (SystemStatService.loadAvg1 >= 0) {
lines.push(`${I18n.tr("system-monitor.load-average")}: ${SystemStatService.loadAvg1.toFixed(2)} ${SystemStatService.loadAvg5.toFixed(2)} ${SystemStatService.loadAvg15.toFixed(2)}`);
lines.push(`${I18n.tr("system-monitor.load-average")}: ${SystemStatService.loadAvg1.toFixed(2)} · ${SystemStatService.loadAvg5.toFixed(2)} · ${SystemStatService.loadAvg15.toFixed(2)}`);
}
// Memory
lines.push(`${I18n.tr("common.memory")}: ${Math.round(SystemStatService.memPercent)}% (${SystemStatService.formatMemoryGb(SystemStatService.memGb)})`);
lines.push(`${I18n.tr("common.memory")}: ${Math.round(SystemStatService.memPercent)}% (${SystemStatService.formatMemoryGb(SystemStatService.memGb).replace(/([0-9.]+)([A-Za-z]+)/, "$1 $2")})`);
// Swap (if available)
if (SystemStatService.swapTotalGb > 0) {
lines.push(`${I18n.tr("bar.system-monitor.swap-usage-label")}: ${Math.round(SystemStatService.swapPercent)}% (${SystemStatService.formatMemoryGb(SystemStatService.swapGb)})`);
lines.push(`${I18n.tr("bar.system-monitor.swap-usage-label")}: ${Math.round(SystemStatService.swapPercent)}% (${SystemStatService.formatMemoryGb(SystemStatService.swapGb).replace(/([0-9.]+)([A-Za-z]+)/, "$1 $2")})`);
}
// Network
lines.push(`${I18n.tr("system-monitor.download-speed")}: ${SystemStatService.formatSpeed(SystemStatService.rxSpeed)}`);
lines.push(`${I18n.tr("system-monitor.upload-speed")}: ${SystemStatService.formatSpeed(SystemStatService.txSpeed)}`);
lines.push(`${I18n.tr("system-monitor.download-speed")}: ${SystemStatService.formatSpeed(SystemStatService.rxSpeed).replace(/([0-9.]+)([A-Za-z]+)/, "$1 $2")}`);
lines.push(`${I18n.tr("system-monitor.upload-speed")}: ${SystemStatService.formatSpeed(SystemStatService.txSpeed).replace(/([0-9.]+)([A-Za-z]+)/, "$1 $2")}`);
// Disk
const diskPercent = SystemStatService.diskPercents[diskPath];
if (diskPercent !== undefined) {
const usedGb = SystemStatService.diskUsedGb[diskPath] || 0;
const sizeGb = SystemStatService.diskSizeGb[diskPath] || 0;
lines.push(`${I18n.tr("system-monitor.disk")}: ${usedGb.toFixed(1)}G / ${sizeGb.toFixed(1)}G (${diskPercent}%)`);
lines.push(`${I18n.tr("system-monitor.disk")}: ${usedGb.toFixed(1)} G / ${sizeGb.toFixed(1)} G (${diskPercent}%)`);
}
return lines.join("\n");
+292 -65
View File
@@ -100,6 +100,91 @@ Rectangle {
property int wheelAccumulatedDelta: 0
property bool wheelCooldown: false
// Drag and Drop state for visual feedback
property int dragSourceIndex: -1
property int dragTargetIndex: -1
// Track the session order of apps (transient reordering)
property var sessionAppOrder: []
function getAppKey(appData) {
if (!appData)
return null;
// prefer window object identity for running apps to distinguish instances
if (appData.window)
return appData.window;
// fallback to appId for pinned-only apps
return appData.appId;
}
function sortApps(apps) {
if (!sessionAppOrder || sessionAppOrder.length === 0) {
return apps;
}
const sorted = [];
const remaining = [...apps];
// 1. Pick apps that are in the session order
for (let i = 0; i < sessionAppOrder.length; i++) {
const key = sessionAppOrder[i];
const idx = remaining.findIndex(app => getAppKey(app) === key);
if (idx !== -1) {
sorted.push(remaining[idx]);
remaining.splice(idx, 1);
}
}
// 2. Append any new/remaining apps
remaining.forEach(app => sorted.push(app));
return sorted;
}
function reorderApps(fromIndex, toIndex) {
Logger.d("Taskbar", "Reordering apps from " + fromIndex + " to " + toIndex);
if (fromIndex === toIndex || fromIndex < 0 || toIndex < 0 || fromIndex >= combinedModel.length || toIndex >= combinedModel.length)
return;
const list = [...combinedModel];
const item = list.splice(fromIndex, 1)[0];
list.splice(toIndex, 0, item);
combinedModel = list;
sessionAppOrder = combinedModel.map(getAppKey);
savePinnedOrder();
}
function savePinnedOrder() {
const currentPinned = Settings.data.dock.pinnedApps || [];
const newPinned = [];
const seen = new Set();
// Extract pinned apps in their current visual order
combinedModel.forEach(app => {
if (app.appId && !seen.has(app.appId)) {
const isPinned = currentPinned.some(p => normalizeAppId(p) === normalizeAppId(app.appId));
if (isPinned) {
newPinned.push(app.appId);
seen.add(app.appId);
}
}
});
// Check if any pinned apps were missed (e.g. filtered out by workspace)
currentPinned.forEach(p => {
if (!seen.has(p)) {
newPinned.push(p);
seen.add(p);
}
});
if (JSON.stringify(currentPinned) !== JSON.stringify(newPinned)) {
Settings.data.dock.pinnedApps = newPinned;
}
}
// Helper function to normalize app IDs for case-insensitive matching
function normalizeAppId(appId) {
if (!appId || typeof appId !== 'string')
@@ -263,7 +348,12 @@ Rectangle {
});
}
combinedModel = runningWindows;
combinedModel = sortApps(runningWindows);
// Sync session order if needed (e.g. first run or new apps added)
if (!sessionAppOrder || sessionAppOrder.length === 0 || sessionAppOrder.length !== combinedModel.length) {
sessionAppOrder = combinedModel.map(getAppKey);
}
updateHasWindow();
}
@@ -537,6 +627,7 @@ Rectangle {
delegate: Item {
id: taskbarItem
required property var modelData
required property int index
property ShellScreen screen: root.screen
readonly property bool isRunning: modelData.window !== null
@@ -557,96 +648,232 @@ Rectangle {
Layout.preferredHeight: root.itemSize
Layout.alignment: Qt.AlignCenter
Rectangle {
id: titleBackground
visible: shouldShowTitle
anchors.centerIn: parent
width: parent.width
height: root.height
color: titleBgColor
radius: Style.radiusM
// Ensure dragged item is on top
z: (root.dragSourceIndex === index) ? 1000 : 1
Behavior on color {
ColorAnimation {
duration: Style.animationFast
easing.type: Easing.InOutQuad
property int modelIndex: index
objectName: "taskbarAppItem"
DropArea {
anchors.fill: parent
keys: ["taskbar-app"]
onEntered: function (drag) {
if (drag.source && drag.source.objectName === "taskbarAppItem") {
root.dragTargetIndex = taskbarItem.modelIndex;
}
}
onExited: function () {
if (root.dragTargetIndex === taskbarItem.modelIndex) {
root.dragTargetIndex = -1;
}
}
onDropped: function (drop) {
root.dragSourceIndex = -1;
root.dragTargetIndex = -1;
Logger.d("Taskbar", "Dropped! Source: " + (drop.source ? drop.source.objectName : "null") + " Index: " + (drop.source ? drop.source.modelIndex : "?") + " -> Target Index: " + taskbarItem.modelIndex);
if (drop.source && drop.source.objectName === "taskbarAppItem" && drop.source !== taskbarItem) {
root.reorderApps(drop.source.modelIndex, taskbarItem.modelIndex);
} else {
Logger.d("Taskbar", "Drop ignored. Source objectName: " + (drop.source ? drop.source.objectName : "null"));
}
}
}
Rectangle {
anchors.centerIn: parent
width: taskbarItem.contentWidth
Item {
id: draggableContent
width: parent.width
height: parent.height
color: "transparent"
anchors.centerIn: dragging ? undefined : parent
RowLayout {
id: itemLayout
anchors.fill: parent
spacing: taskbarItem.itemSpacing
// Visual shifting logic
readonly property bool isDragged: root.dragSourceIndex === index
property real shiftOffset: 0
Item {
Layout.preferredWidth: root.itemSize
Layout.preferredHeight: root.itemSize
Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft
// Calculate shift based on drag state
// If I am NOT the dragged item, but I am in the path of the drag
Binding on shiftOffset {
value: {
if (root.dragSourceIndex !== -1 && root.dragTargetIndex !== -1 && !draggableContent.isDragged) {
if (root.dragSourceIndex < root.dragTargetIndex) {
// Dragging Right: Items between source and target shift Left
if (index > root.dragSourceIndex && index <= root.dragTargetIndex) {
return -1 * (root.isVerticalBar ? root.itemSize : draggableContent.width); // Simple approximation, could be refined
}
} else if (root.dragSourceIndex > root.dragTargetIndex) {
// Dragging Left: Items between target and source shift Right
if (index >= root.dragTargetIndex && index < root.dragSourceIndex) {
return (root.isVerticalBar ? root.itemSize : draggableContent.width);
}
}
}
return 0;
}
}
IconImage {
id: appIcon
anchors.fill: parent
transform: Translate {
x: !root.isVerticalBar ? draggableContent.shiftOffset : 0
y: root.isVerticalBar ? draggableContent.shiftOffset : 0
source: ThemeIcons.iconForAppId(taskbarItem.modelData.appId)
smooth: true
asynchronous: true
Behavior on x {
NumberAnimation {
duration: Style.animationFast
easing.type: Easing.OutQuad
}
}
Behavior on y {
NumberAnimation {
duration: Style.animationFast
easing.type: Easing.OutQuad
}
}
}
// Apply dock shader to all taskbar icons
layer.enabled: widgetSettings.colorizeIcons !== false
layer.effect: ShaderEffect {
property color targetColor: Settings.data.colorSchemes.darkMode ? Color.mOnSurface : Color.mSurfaceVariant
property real colorizeMode: 0.0 // Dock mode (grayscale)
property bool dragging: taskbarMouseArea.drag.active
onDraggingChanged: {
if (dragging) {
root.dragSourceIndex = index;
} else {
// Don't reset immediately on release to allow drop to handle it,
// or use a timer if needed, but drop handler usually fires.
// However, if dropped outside, we need to reset.
// Let's reset if not handled by drop area quickly?
// Actually, drag.active becomes false on release.
// We might want to clear it if no drop happened.
if (root.dragSourceIndex === index) {
// Slight delay/check? For now, let DropArea handle reset on success.
// If cancelled (dropped nowhere), we should reset.
Qt.callLater(() => {
if (!taskbarMouseArea.drag.active && root.dragSourceIndex === index) {
root.dragSourceIndex = -1;
root.dragTargetIndex = -1;
}
});
}
}
}
fragmentShader: Qt.resolvedUrl(Quickshell.shellDir + "/Shaders/qsb/appicon_colorize.frag.qsb")
Drag.active: dragging
Drag.source: taskbarItem
Drag.hotSpot.x: width / 2
Drag.hotSpot.y: height / 2
Drag.keys: ["taskbar-app"]
z: dragging ? 1000 : 0
scale: dragging ? 1.05 : 1.0
Behavior on scale {
NumberAnimation {
duration: Style.animationFast
}
}
Rectangle {
id: titleBackground
visible: shouldShowTitle
anchors.centerIn: parent
width: parent.width
height: root.height
color: titleBgColor
radius: Style.radiusM
Behavior on color {
ColorAnimation {
duration: Style.animationFast
easing.type: Easing.InOutQuad
}
}
}
Rectangle {
anchors.centerIn: parent
width: taskbarItem.contentWidth
height: parent.height
color: "transparent"
RowLayout {
id: itemLayout
anchors.fill: parent
spacing: taskbarItem.itemSpacing
Item {
Layout.preferredWidth: root.itemSize
Layout.preferredHeight: root.itemSize
Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft
IconImage {
id: appIcon
anchors.fill: parent
source: ThemeIcons.iconForAppId(taskbarItem.modelData.appId)
smooth: true
asynchronous: true
// Apply dock shader to all taskbar icons
layer.enabled: widgetSettings.colorizeIcons !== false
layer.effect: ShaderEffect {
property color targetColor: Settings.data.colorSchemes.darkMode ? Color.mOnSurface : Color.mSurfaceVariant
property real colorizeMode: 0.0 // Dock mode (grayscale)
fragmentShader: Qt.resolvedUrl(Quickshell.shellDir + "/Shaders/qsb/appicon_colorize.frag.qsb")
}
}
Rectangle {
id: iconBackground
visible: !shouldShowTitle
anchors.bottomMargin: -2
anchors.bottom: parent.bottom
anchors.horizontalCenter: parent.horizontalCenter
width: Style.toOdd(root.itemSize * 0.25)
height: 4
color: taskbarItem.isFocused ? Color.mPrimary : "transparent"
radius: Math.min(Style.radiusXXS, width / 2)
}
}
Rectangle {
id: iconBackground
visible: !shouldShowTitle
anchors.bottomMargin: -2
anchors.bottom: parent.bottom
anchors.horizontalCenter: parent.horizontalCenter
width: Style.toOdd(root.itemSize * 0.25)
height: 4
color: taskbarItem.isFocused ? Color.mPrimary : "transparent"
radius: Math.min(Style.radiusXXS, width / 2)
NText {
id: titleText
visible: shouldShowTitle
Layout.preferredWidth: root.titleWidth
Layout.preferredHeight: root.itemSize
Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft
Layout.fillWidth: false
text: taskbarItem.title
elide: Text.ElideRight
verticalAlignment: Text.AlignVCenter
horizontalAlignment: Text.AlignLeft
pointSize: Style.barFontSize
color: titleFgColor
opacity: Style.opacityFull
}
}
NText {
id: titleText
visible: shouldShowTitle
Layout.preferredWidth: root.titleWidth
Layout.preferredHeight: root.itemSize
Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft
Layout.fillWidth: false
text: taskbarItem.title
elide: Text.ElideRight
verticalAlignment: Text.AlignVCenter
horizontalAlignment: Text.AlignLeft
pointSize: Style.barFontSize
color: titleFgColor
opacity: Style.opacityFull
}
}
}
MouseArea {
id: taskbarMouseArea
objectName: "taskbarMouseArea"
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton | Qt.RightButton
drag.target: draggableContent
drag.axis: root.isVerticalBar ? Drag.YAxis : Drag.XAxis
preventStealing: true
onPressed: {
// Constrain drag to roughly the taskbar area but allow some freedom
// Or just let it be free since we only care about drops
}
onReleased: {
if (draggableContent.Drag.active) {
draggableContent.Drag.drop();
}
}
onClicked: function (mouse) {
if (!modelData)
return;
+165 -53
View File
@@ -89,6 +89,7 @@ Item {
property int iconRevision: 0
property ListModel localWorkspaces: ListModel {}
property int lastFocusedWorkspaceId: -1
property real masterProgress: 0.0
property bool effectsActive: false
property color effectColor: Color.mPrimary
@@ -105,9 +106,10 @@ Item {
implicitWidth: showApplications ? (isVertical ? groupedGrid.implicitWidth : Math.round(groupedGrid.implicitWidth + horizontalPadding * hasLabel)) : (isVertical ? barHeight : computeWidth())
implicitHeight: showApplications ? (isVertical ? Math.round(groupedGrid.implicitHeight + horizontalPadding * 0.6 * hasLabel) : barHeight) : (isVertical ? computeHeight() : barHeight)
function getWorkspaceWidth(ws) {
function getWorkspaceWidth(ws, activeOverride) {
const d = Math.round(Style.capsuleHeight * root.baseDimensionRatio);
const factor = ws.isActive ? 2.2 : 1;
const isActive = activeOverride !== undefined ? activeOverride : ws.isActive;
const factor = isActive ? 2.2 : 1;
// Don't calculate text width if labels are off
if (labelMode === "none") {
@@ -129,9 +131,10 @@ Item {
return Style.toOdd(Math.max(d * factor, textWidth + padding));
}
function getWorkspaceHeight(ws) {
function getWorkspaceHeight(ws, activeOverride) {
const d = Math.round(Style.capsuleHeight * root.baseDimensionRatio);
const factor = ws.isActive ? 2.2 : 1;
const isActive = activeOverride !== undefined ? activeOverride : ws.isActive;
const factor = isActive ? 2.2 : 1;
return Style.toOdd(d * factor);
}
@@ -230,7 +233,6 @@ Item {
target: CompositorService
function onWorkspacesChanged() {
refreshWorkspaces();
root.triggerUnifiedWave();
}
function onWindowListChanged() {
if (showApplications || showLabelsOnlyWhenOccupied) {
@@ -253,8 +255,7 @@ Item {
}
function refreshWorkspaces() {
localWorkspaces.clear();
var targetList = [];
var focusedOutput = null;
if (followFocusedScreen) {
for (var i = 0; i < CompositorService.workspaces.count; i++) {
@@ -275,18 +276,51 @@ Item {
if (hideUnoccupied && !ws.isOccupied && !ws.isFocused)
continue;
// Create a plain JS object for the workspace data
var workspaceData = {
id: ws.id,
idx: ws.idx,
name: ws.name,
output: ws.output,
isFocused: ws.isFocused,
isActive: ws.isActive,
isUrgent: ws.isUrgent,
isOccupied: ws.isOccupied
};
if (showApplications) {
// For grouped mode, attach windows to each workspace
var workspaceData = Object.assign({}, ws);
workspaceData.windows = CompositorService.getWindowsForWorkspace(ws.id);
localWorkspaces.append(workspaceData);
} else {
localWorkspaces.append(ws);
}
targetList.push(workspaceData);
}
}
workspaceRepeaterHorizontal.model = localWorkspaces;
workspaceRepeaterVertical.model = localWorkspaces;
// In-place update to preserve delegates for animations
var i = 0;
while (i < localWorkspaces.count || i < targetList.length) {
if (i < localWorkspaces.count && i < targetList.length) {
var existing = localWorkspaces.get(i);
var target = targetList[i];
if (existing.id === target.id) {
// Use set() to update all properties, including arrays like 'windows'
// This is more reliable than repeated setProperty calls for complex types
localWorkspaces.set(i, target);
i++;
} else {
// ID mismatch, remove existing and re-evaluate this index
localWorkspaces.remove(i);
}
} else if (i < localWorkspaces.count) {
// Excess items in local, remove them
localWorkspaces.remove(i);
} else {
// More items in target, append them
localWorkspaces.append(targetList[i]);
i++;
}
}
updateWorkspaceFocus();
}
@@ -299,6 +333,10 @@ Item {
for (var i = 0; i < localWorkspaces.count; i++) {
const ws = localWorkspaces.get(i);
if (ws.isFocused === true) {
if (root.lastFocusedWorkspaceId !== -1 && root.lastFocusedWorkspaceId !== ws.id) {
root.triggerUnifiedWave();
}
root.lastFocusedWorkspaceId = ws.id;
root.workspaceChanged(ws.id, Color.mPrimary);
break;
}
@@ -493,9 +531,48 @@ Item {
model: localWorkspaces
Item {
id: workspacePillContainer
width: root.getWorkspaceWidth(model)
height: Style.toOdd(Style.capsuleHeight * root.baseDimensionRatio)
states: [
State {
name: "active"
when: model.isActive
PropertyChanges {
target: workspacePillContainer
width: root.getWorkspaceWidth(model, true)
}
},
State {
name: "inactive"
when: !model.isActive
PropertyChanges {
target: workspacePillContainer
width: root.getWorkspaceWidth(model, false)
}
}
]
transitions: [
Transition {
from: "inactive"
to: "active"
NumberAnimation {
property: "width"
duration: Style.animationNormal
easing.type: Easing.OutBack
}
},
Transition {
from: "active"
to: "inactive"
NumberAnimation {
property: "width"
duration: Style.animationNormal
easing.type: Easing.OutBack
}
}
]
Rectangle {
id: pill
anchors.fill: parent
@@ -559,19 +636,7 @@ Item {
}
hoverEnabled: true
}
// Material 3-inspired smooth animation for width, height, scale, color, opacity, and radius
Behavior on width {
NumberAnimation {
duration: Style.animationNormal
easing.type: Easing.OutBack
}
}
Behavior on height {
NumberAnimation {
duration: Style.animationNormal
easing.type: Easing.OutBack
}
}
// Material 3-inspired smooth animation for scale, color, opacity, and radius
Behavior on scale {
NumberAnimation {
duration: Style.animationNormal
@@ -579,6 +644,7 @@ Item {
}
}
Behavior on color {
enabled: !Color.isTransitioning
ColorAnimation {
duration: Style.animationFast
easing.type: Easing.InOutCubic
@@ -642,7 +708,46 @@ Item {
Item {
id: workspacePillContainerVertical
width: Style.toOdd(Style.capsuleHeight * root.baseDimensionRatio)
height: root.getWorkspaceHeight(model)
states: [
State {
name: "active"
when: model.isActive
PropertyChanges {
target: workspacePillContainerVertical
height: root.getWorkspaceHeight(model, true)
}
},
State {
name: "inactive"
when: !model.isActive
PropertyChanges {
target: workspacePillContainerVertical
height: root.getWorkspaceHeight(model, false)
}
}
]
transitions: [
Transition {
from: "inactive"
to: "active"
NumberAnimation {
property: "height"
duration: Style.animationNormal
easing.type: Easing.OutBack
}
},
Transition {
from: "active"
to: "inactive"
NumberAnimation {
property: "height"
duration: Style.animationNormal
easing.type: Easing.OutBack
}
}
]
Rectangle {
id: pillVertical
@@ -707,19 +812,7 @@ Item {
}
hoverEnabled: true
}
// Material 3-inspired smooth animation for width, height, scale, color, opacity, and radius
Behavior on width {
NumberAnimation {
duration: Style.animationNormal
easing.type: Easing.OutBack
}
}
Behavior on height {
NumberAnimation {
duration: Style.animationNormal
easing.type: Easing.OutBack
}
}
// Material 3-inspired smooth animation for scale, color, opacity, and radius
Behavior on scale {
NumberAnimation {
duration: Style.animationNormal
@@ -727,6 +820,7 @@ Item {
}
}
Behavior on color {
enabled: !Color.isTransitioning
ColorAnimation {
duration: Style.animationFast
easing.type: Easing.InOutCubic
@@ -788,7 +882,7 @@ Item {
required property var model
property var workspaceModel: model
property bool hasWindows: (workspaceModel?.windows?.count ?? 0) > 0
property bool hasWindows: (workspaceModel?.windows?.length > 0 || workspaceModel?.windows?.count > 0)
width: Style.toOdd((hasWindows ? groupedIconsFlow.implicitWidth : root.iconSize) + (root.isVertical ? (root.baseItemSize - root.iconSize + Style.marginXS) : Style.marginXL))
height: Style.toOdd((hasWindows ? groupedIconsFlow.implicitHeight : root.iconSize) + (root.isVertical ? Style.marginL : (root.baseItemSize - root.iconSize + Style.marginXS)))
@@ -797,6 +891,19 @@ Item {
border.color: Settings.data.bar.showOutline ? Style.capsuleBorderColor : Qt.alpha((workspaceModel.isFocused ? Color.mPrimary : Color.mOutline), root.groupedBorderOpacity)
border.width: Style.borderS
Behavior on width {
NumberAnimation {
duration: Style.animationNormal
easing.type: Easing.OutBack
}
}
Behavior on height {
NumberAnimation {
duration: Style.animationNormal
easing.type: Easing.OutBack
}
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
@@ -846,16 +953,17 @@ Item {
height: parent.height
source: {
root.iconRevision; // Force re-evaluation when revision changes
return ThemeIcons.iconForAppId(model.appId?.toLowerCase());
const win = (typeof modelData !== "undefined") ? modelData : model;
return ThemeIcons.iconForAppId(win.appId?.toLowerCase());
}
smooth: true
asynchronous: true
opacity: model.isFocused ? Style.opacityFull : unfocusedIconsOpacity
layer.enabled: root.colorizeIcons && !model.isFocused
opacity: (typeof modelData !== "undefined" ? modelData.isFocused : model.isFocused) ? Style.opacityFull : unfocusedIconsOpacity
layer.enabled: root.colorizeIcons && !(typeof modelData !== "undefined" ? modelData.isFocused : model.isFocused)
Rectangle {
id: groupedFocusIndicator
visible: model.isFocused
visible: (typeof modelData !== "undefined" ? modelData.isFocused : model.isFocused)
anchors.bottomMargin: -2
anchors.bottom: parent.bottom
anchors.horizontalCenter: parent.horizontalCenter
@@ -880,27 +988,30 @@ Item {
preventStealing: true
onPressed: mouse => {
if (!model)
const win = (typeof modelData !== "undefined") ? modelData : model;
if (!win)
return;
if (mouse.button === Qt.LeftButton) {
CompositorService.focusWindow(model);
CompositorService.focusWindow(win);
}
}
onReleased: mouse => {
if (!model)
const win = (typeof modelData !== "undefined") ? modelData : model;
if (!win)
return;
if (mouse.button === Qt.RightButton) {
mouse.accepted = true;
TooltipService.hide();
root.selectedWindowId = model.id || model.address || "";
root.selectedAppId = model.appId;
root.selectedWindowId = win.id || win.address || "";
root.selectedAppId = win.appId;
openGroupedContextMenu(groupedTaskbarItem);
}
}
onEntered: {
const win = (typeof modelData !== "undefined") ? modelData : model;
groupedTaskbarItem.itemHovered = true;
TooltipService.show(groupedTaskbarItem, model.title || model.appId || "Unknown app.", BarService.getTooltipDirection(root.screen?.name));
TooltipService.show(groupedTaskbarItem, win.title || win.appId || "Unknown app.", BarService.getTooltipDirection(root.screen?.name));
}
onExited: {
groupedTaskbarItem.itemHovered = false;
@@ -957,6 +1068,7 @@ Item {
}
Behavior on color {
enabled: !Color.isTransitioning
ColorAnimation {
duration: Style.animationFast
easing.type: Easing.InOutCubic
+1 -1
View File
@@ -87,7 +87,7 @@ NBox {
NIcon {
Layout.alignment: Qt.AlignVCenter
icon: weatherReady ? LocationService.weatherSymbolFromCode(LocationService.data.weather.current_weather.weathercode) : ""
icon: weatherReady ? LocationService.weatherSymbolFromCode(LocationService.data.weather.current_weather.weathercode, LocationService.data.weather.current_weather.is_day) : "weather-cloud-off"
pointSize: Style.fontSizeXXXL * 1.75
color: Color.mPrimary
}
@@ -63,7 +63,7 @@ DraggableDesktopWidget {
NIcon {
anchors.centerIn: parent
icon: weatherReady ? LocationService.weatherSymbolFromCode(currentWeatherCode) : "cloud"
icon: weatherReady ? LocationService.weatherSymbolFromCode(currentWeatherCode, LocationService.data.weather.current_weather.is_day) : "weather-cloud-off"
pointSize: Math.round(Style.fontSizeXXXL * 2 * widgetScale)
color: weatherReady ? Color.mPrimary : Color.mOnSurfaceVariant
}
+72 -2
View File
@@ -106,6 +106,10 @@ Loader {
// Track the session order of apps (transient reordering)
property var sessionAppOrder: []
// Drag and Drop state for visual feedback
property int dragSourceIndex: -1
property int dragTargetIndex: -1
// Revision counter to force icon re-evaluation
property int iconRevision: 0
@@ -636,7 +640,19 @@ Loader {
DropArea {
anchors.fill: parent
keys: ["dock-app"]
onEntered: function (drag) {
if (drag.source && drag.source.objectName === "dockAppButton") {
root.dragTargetIndex = appButton.modelIndex;
}
}
onExited: function () {
if (root.dragTargetIndex === appButton.modelIndex) {
root.dragTargetIndex = -1;
}
}
onDropped: function (drop) {
root.dragSourceIndex = -1;
root.dragTargetIndex = -1;
if (drop.source && drop.source.objectName === "dockAppButton" && drop.source !== appButton) {
root.reorderApps(drop.source.modelIndex, appButton.modelIndex);
}
@@ -661,6 +677,19 @@ Loader {
anchors.centerIn: dragging ? undefined : parent
property bool dragging: appMouseArea.drag.active
onDraggingChanged: {
if (dragging) {
root.dragSourceIndex = index;
} else {
// Reset if not handled by drop (e.g. dropped outside)
Qt.callLater(() => {
if (!appMouseArea.drag.active && root.dragSourceIndex === index) {
root.dragSourceIndex = -1;
root.dragTargetIndex = -1;
}
});
}
}
Drag.active: dragging
Drag.source: appButton
@@ -668,7 +697,7 @@ Loader {
Drag.hotSpot.y: height / 2
Drag.keys: ["dock-app"]
z: dragging ? 1000 : 0
z: (root.dragSourceIndex === index) ? 1000 : ((dragging ? 1000 : 0))
scale: dragging ? 1.1 : (appButton.hovered ? 1.15 : 1.0)
Behavior on scale {
NumberAnimation {
@@ -678,6 +707,47 @@ Loader {
}
}
// Visual shifting logic
readonly property bool isDragged: root.dragSourceIndex === index
property real shiftOffset: 0
Binding on shiftOffset {
value: {
if (root.dragSourceIndex !== -1 && root.dragTargetIndex !== -1 && !iconContainer.isDragged) {
if (root.dragSourceIndex < root.dragTargetIndex) {
// Dragging Forward: Items between source and target shift Backward
if (index > root.dragSourceIndex && index <= root.dragTargetIndex) {
return -1 * (root.isVertical ? iconSize + Style.marginS : iconSize + Style.marginS);
}
} else if (root.dragSourceIndex > root.dragTargetIndex) {
// Dragging Backward: Items between target and source shift Forward
if (index >= root.dragTargetIndex && index < root.dragSourceIndex) {
return (root.isVertical ? iconSize + Style.marginS : iconSize + Style.marginS);
}
}
}
return 0;
}
}
transform: Translate {
x: !root.isVertical ? iconContainer.shiftOffset : 0
y: root.isVertical ? iconContainer.shiftOffset : 0
Behavior on x {
NumberAnimation {
duration: Style.animationFast
easing.type: Easing.OutQuad
}
}
Behavior on y {
NumberAnimation {
duration: Style.animationFast
easing.type: Easing.OutQuad
}
}
}
IconImage {
id: appIcon
anchors.fill: parent
@@ -779,7 +849,7 @@ Loader {
// Only allow left-click dragging via axis control
drag.target: iconContainer
drag.axis: (pressedButtons & Qt.LeftButton) ? Drag.XAndYAxis : Drag.None
drag.axis: (pressedButtons & Qt.LeftButton) ? (root.isVertical ? Drag.YAxis : Drag.XAxis) : Drag.None
preventStealing: true
onPressed: {
+13 -3
View File
@@ -18,11 +18,14 @@ Scope {
property string errorMessage: ""
property string infoMessage: ""
property bool pamAvailable: typeof PamContext !== "undefined"
property bool fprintdAvailable: false
readonly property string pamConfigDirectory: Quickshell.env("NOCTALIA_PAM_CONFIG") ? "/etc/pam.d" : Settings.configDir + "pam"
readonly property string pamConfig: Quickshell.env("NOCTALIA_PAM_CONFIG") || "password.conf"
Component.onCompleted: {
checkFprintdProc.running = true;
if (Quickshell.env("NOCTALIA_PAM_CONFIG")) {
Logger.i("LockContext", "NOCTALIA_PAM_CONFIG is set, using system PAM config: /etc/pam.d/" + pamConfig);
} else {
@@ -36,10 +39,9 @@ Scope {
infoMessage = "";
showFailure = false;
errorMessage = "";
if (!waitingForPassword) {
pam.abort();
if (fprintdAvailable) {
occupyFingerprintSensorProc.running = true;
}
occupyFingerprintSensorProc.running = true;
} else {
occupyFingerprintSensorProc.running = false;
pam.start();
@@ -68,6 +70,14 @@ Scope {
pam.start();
}
Process {
id: checkFprintdProc
command: ["sh", "-c", "command -v fprintd-verify"]
onExited: function (exitCode) {
fprintdAvailable = (exitCode === 0);
}
}
Process {
id: occupyFingerprintSensorProc
command: ["fprintd-verify"]
+6
View File
@@ -87,6 +87,7 @@ Loader {
property bool isReady: initializationComplete && BatteryService.batteryReady
property real percent: BatteryService.batteryPercentage
property bool charging: BatteryService.batteryCharging
property bool pluggedIn: BatteryService.batteryPluggedIn
property bool batteryVisible: isReady && percent > 0 && BatteryService.hasAnyBattery()
}
@@ -279,6 +280,11 @@ Loader {
Keys.onPressed: function (event) {
if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
lockContext.tryUnlock();
event.accepted = true;
}
if (event.key === Qt.Key_Escape && panelComponent.timerActive) {
panelComponent.cancelTimer();
event.accepted = true;
}
}
+3 -3
View File
@@ -136,7 +136,7 @@ Item {
visible: batteryIndicator.isReady && BatteryService.hasAnyBattery()
NIcon {
icon: BatteryService.getIcon(Math.round(batteryIndicator.percent), batteryIndicator.charging, batteryIndicator.isReady)
icon: BatteryService.getIcon(Math.round(batteryIndicator.percent), batteryIndicator.charging, batteryIndicator.pluggedIn, batteryIndicator.isReady)
pointSize: Style.fontSizeM
color: batteryIndicator.charging ? Color.mPrimary : Color.mOnSurfaceVariant
}
@@ -327,7 +327,7 @@ Item {
NIcon {
Layout.alignment: Qt.AlignVCenter
icon: LocationService.weatherSymbolFromCode(LocationService.data.weather.current_weather.weathercode)
icon: weatherReady ? LocationService.weatherSymbolFromCode(LocationService.data.weather.current_weather.weathercode, LocationService.data.weather.current_weather.is_day) : "weather-cloud-off"
pointSize: Style.fontSizeXXXL
color: Color.mPrimary
}
@@ -461,7 +461,7 @@ Item {
visible: batteryIndicator.isReady && BatteryService.hasAnyBattery()
NIcon {
icon: BatteryService.getIcon(Math.round(batteryIndicator.percent), batteryIndicator.charging, batteryIndicator.isReady)
icon: BatteryService.getIcon(Math.round(batteryIndicator.percent), batteryIndicator.charging, batteryIndicator.pluggedIn, batteryIndicator.isReady)
pointSize: Style.fontSizeM
color: batteryIndicator.charging ? Color.mPrimary : Color.mOnSurfaceVariant
}
@@ -95,13 +95,6 @@ ShapePath {
startX: barMappedPos.x + tlRadius * tlMultX
startY: barMappedPos.y
// Smooth color animation
Behavior on fillColor {
ColorAnimation {
duration: Style.animationFast
}
}
// ========== PATH DEFINITION ==========
// Draws a rectangle with potentially inverted corners
// All coordinates are relative to startX/startY
+2 -1
View File
@@ -52,8 +52,9 @@ Item {
strokeWidth: -1 // No stroke, fill only
fillColor: shouldShow ? cornerColor : "transparent"
// Smooth color animation
// Smooth color animation (disabled during theme transitions to sync with Color.qml)
Behavior on fillColor {
enabled: !Color.isTransitioning
ColorAnimation {
duration: Style.animationFast
}
+18 -2
View File
@@ -456,6 +456,7 @@ Variants {
visible: false
opacity: 0
scale: 0.85
property bool pendingShow: false
Behavior on opacity {
NumberAnimation {
@@ -511,9 +512,15 @@ Variants {
id: contentLoader
anchors.fill: background
anchors.margins: Style.marginM
// Delay activation until background has valid dimensions to avoid negative layout sizes
active: background.width > 0 && background.height > 0
active: true
sourceComponent: panel.verticalMode ? verticalContent : horizontalContent
onWidthChanged: {
if (width > 0 && height > 0 && osdItem.pendingShow) {
osdItem.pendingShow = false;
osdItem.show();
}
}
}
Component {
@@ -743,6 +750,14 @@ Variants {
function show() {
hideTimer.stop();
visibilityTimer.stop();
// Defer show until content layout has valid geometry
if (contentLoader.width <= 0 || contentLoader.height <= 0) {
pendingShow = true;
return;
}
pendingShow = false;
osdItem.visible = true;
Qt.callLater(() => {
@@ -764,6 +779,7 @@ Variants {
function hideImmediately() {
hideTimer.stop();
visibilityTimer.stop();
pendingShow = false;
osdItem.opacity = 0;
osdItem.scale = 0.85;
osdItem.visible = false;
+12 -12
View File
@@ -113,7 +113,7 @@ SmartPanel {
readonly property bool isReady: battery && battery.ready && isDevicePresent && (battery.percentage !== undefined || hasBluetoothBattery)
readonly property int percent: isReady ? Math.round(hasBluetoothBattery ? (bluetoothDevice.battery * 100) : (battery.percentage * 100)) : -1
readonly property bool charging: isReady ? battery.state === UPowerDeviceState.Charging : false
readonly property bool isCharging: isReady ? battery.state === UPowerDeviceState.Charging : false
readonly property bool isPluggedIn: isReady ? (battery.state === UPowerDeviceState.FullyCharged || battery.state === UPowerDeviceState.PendingCharge) : false
readonly property bool healthAvailable: (isReady && battery.healthSupported) || BatteryService.healthAvailable
readonly property int healthPercent: (isReady && battery.healthSupported) ? Math.round(battery.healthPercentage) : BatteryService.healthPercent
@@ -141,22 +141,22 @@ SmartPanel {
readonly property string timeText: {
if (!isReady || !isDevicePresent)
return I18n.tr("battery.no-battery-detected");
if (charging && battery.timeToFull > 0) {
if (isPluggedIn) {
return I18n.tr("battery.plugged-in");
}
if (battery.timeToFull > 0) {
return I18n.tr("battery.time-until-full", {
"time": Time.formatVagueHumanReadableDuration(battery.timeToFull)
});
}
if (!charging && battery.timeToEmpty > 0) {
if (battery.timeToEmpty > 0) {
return I18n.tr("battery.time-left", {
"time": Time.formatVagueHumanReadableDuration(battery.timeToEmpty)
});
}
if (!charging && isPluggedIn) {
return I18n.tr("battery.plugged-in"); // i18n: Could be Plugged in, not charging? Ask maintainers if i not forgot
}
return I18n.tr("common.idle");
}
readonly property string iconName: BatteryService.getIcon(percent, charging, isPluggedIn, isReady)
readonly property string iconName: BatteryService.getIcon(percent, isCharging, isPluggedIn, isReady)
property var batteryWidgetInstance: BarService.lookupWidget("Battery", screen ? screen.name : null)
readonly property var batteryWidgetSettings: batteryWidgetInstance ? batteryWidgetInstance.widgetSettings : null
@@ -223,7 +223,7 @@ SmartPanel {
NIcon {
pointSize: Style.fontSizeXXL
color: (charging || isPluggedIn) ? Color.mPrimary : Color.mOnSurface
color: (isCharging || isPluggedIn) ? Color.mPrimary : Color.mOnSurface
icon: iconName
}
@@ -261,7 +261,7 @@ SmartPanel {
// Charge level + health/time
NBox {
Layout.fillWidth: true
height: chargeLayout.implicitHeight + Style.marginL * 2
implicitHeight: chargeLayout.implicitHeight + Style.marginL * 2
visible: isReady
ColumnLayout {
@@ -311,13 +311,14 @@ SmartPanel {
RowLayout {
Layout.fillWidth: true
spacing: Style.marginS
visible: healthAvailable
ColumnLayout {
RowLayout {
spacing: Style.marginXS
NText {
text: I18n.tr("battery.battery-health") + ":"
text: I18n.tr("battery.battery-health")
color: Color.mOnSurface
pointSize: Style.fontSizeS
}
@@ -333,7 +334,6 @@ SmartPanel {
anchors.verticalCenter: parent.verticalCenter
height: parent.height
radius: parent.radius
visible: healthAvailable
width: {
if (!healthAvailable || healthPercent <= 0)
return 0;
@@ -346,7 +346,7 @@ SmartPanel {
}
NText {
text: healthAvailable ? `${healthPercent}%` : I18n.tr("common.not-available")
text: healthPercent >= 0 ? `${healthPercent}%` : "--"
color: Color.mOnSurface
pointSize: Style.fontSizeS
font.weight: Style.fontWeightBold
@@ -164,7 +164,7 @@ NBox {
NIcon {
icon: {
var b = BluetoothService.getBatteryPercent(modelData);
return BatteryService.getIcon(b !== null ? b : 0, false, b !== null);
return BatteryService.getIcon(b !== null ? b : 0, false, false, b !== null);
}
pointSize: Style.fontSizeXS
color: getContentColor(Color.mOnSurface)
@@ -347,7 +347,7 @@ NBox {
NIcon {
icon: {
var b = BluetoothService.getBatteryPercent(modelData);
return BatteryService.getIcon(b !== null ? b : 0, false, b !== null);
return BatteryService.getIcon(b !== null ? b : 0, false, false, b !== null);
}
pointSize: Style.fontSizeXS
color: Color.mOnSurface
+8
View File
@@ -714,6 +714,14 @@ SmartPanel {
}
}
SettingsProvider {
id: settingsProvider
Component.onCompleted: {
registerProvider(this);
Logger.d("Launcher", "Registered: SettingsProvider");
}
}
// ---------------------------------------------------
panelContent: Rectangle {
id: ui
@@ -0,0 +1,110 @@
import QtQuick
import Quickshell
import Quickshell.Io
import qs.Commons
import qs.Services.UI
Item {
id: root
// Provider metadata
property string name: I18n.tr("common.settings")
property var launcher: null
property bool handleSearch: true
property string supportedLayouts: "list"
property var searchIndex: []
FileView {
id: searchIndexFile
path: Quickshell.shellDir + "/Assets/settings-search-index.json"
watchChanges: false
printErrors: false
onLoaded: {
try {
root.searchIndex = JSON.parse(text());
} catch (e) {
root.searchIndex = [];
}
}
}
function init() {
Logger.d("SettingsProvider", "Initialized");
}
function getResults(query) {
if (!query || searchIndex.length === 0)
return [];
const trimmed = query.trim();
if (!trimmed || trimmed.length < 2)
return [];
// Build searchable items with resolved translations
let items = [];
for (let j = 0; j < searchIndex.length; j++) {
const entry = searchIndex[j];
items.push({
"labelKey": entry.labelKey,
"descriptionKey": entry.descriptionKey,
"widget": entry.widget,
"tab": entry.tab,
"tabLabel": entry.tabLabel,
"subTab": entry.subTab,
"subTabLabel": entry.subTabLabel || null,
"label": I18n.tr(entry.labelKey),
"description": entry.descriptionKey ? I18n.tr(entry.descriptionKey) : "",
"subTabName": entry.subTabLabel ? I18n.tr(entry.subTabLabel) : ""
});
}
const results = FuzzySort.go(trimmed, items, {
"keys": ["label", "subTabName", "description"],
"threshold": 0.35,
"limit": 3,
"scoreFn": function (r) {
const labelScore = r[0].score;
const subTabScore = r[1].score * 1.5;
const descScore = r[2].score;
return Math.max(labelScore, subTabScore, descScore);
}
});
let launcherItems = [];
for (let i = 0; i < results.length; i++) {
const entry = results[i].obj;
const tabName = I18n.tr(entry.tabLabel);
const subTabName = entry.subTabName || "";
const breadcrumb = subTabName ? (tabName + " " + subTabName) : tabName;
launcherItems.push({
"name": entry.label,
"description": breadcrumb,
"icon": "settings",
"isTablerIcon": true,
"isImage": false,
"provider": root,
"onActivate": createActivateHandler(entry)
});
}
return launcherItems;
}
function createActivateHandler(entry) {
return function () {
if (launcher)
launcher.close();
Qt.callLater(() => {
var settingsPanel = PanelService.getPanel("settingsPanel", launcher.screen);
if (settingsPanel) {
settingsPanel.requestedEntry = entry;
settingsPanel.open();
}
});
};
}
}
@@ -103,24 +103,16 @@ ColumnLayout {
onToggled: checked => root.valueShowPinnedApps = checked
}
ColumnLayout {
spacing: Style.marginXXS
NValueSlider {
Layout.fillWidth: true
NLabel {
label: I18n.tr("bar.taskbar.icon-scale-label")
description: I18n.tr("bar.taskbar.icon-scale-description")
}
NValueSlider {
Layout.fillWidth: true
from: 0.5
to: 1
stepSize: 0.01
value: root.valueIconScale
onMoved: value => root.valueIconScale = value
text: Math.round(root.valueIconScale * 100) + "%"
}
label: I18n.tr("bar.taskbar.icon-scale-label")
description: I18n.tr("bar.taskbar.icon-scale-description")
from: 0.5
to: 1
stepSize: 0.01
value: root.valueIconScale
onMoved: value => root.valueIconScale = value
text: Math.round(root.valueIconScale * 100) + "%"
}
NToggle {
@@ -141,25 +133,17 @@ ColumnLayout {
onToggled: checked => root.valueSmartWidth = checked
}
ColumnLayout {
NValueSlider {
visible: root.valueSmartWidth && !isVerticalBar
spacing: Style.marginXXS
Layout.fillWidth: true
NLabel {
label: I18n.tr("bar.taskbar.max-width-label")
description: I18n.tr("bar.taskbar.max-width-description")
}
NValueSlider {
Layout.fillWidth: true
from: 10
to: 100
stepSize: 5
value: root.valueMaxTaskbarWidth
onMoved: value => root.valueMaxTaskbarWidth = Math.round(value)
text: Math.round(root.valueMaxTaskbarWidth) + "%"
}
label: I18n.tr("bar.taskbar.max-width-label")
description: I18n.tr("bar.taskbar.max-width-description")
from: 10
to: 100
stepSize: 5
value: root.valueMaxTaskbarWidth
onMoved: value => root.valueMaxTaskbarWidth = Math.round(value)
text: Math.round(root.valueMaxTaskbarWidth) + "%"
}
NTextInput {
+509
View File
@@ -2,6 +2,7 @@ import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import Quickshell.Io
import qs.Commons
import qs.Modules.Panels.Settings.Tabs
import qs.Modules.Panels.Settings.Tabs.About
@@ -38,14 +39,253 @@ Item {
property int currentTabIndex: 0
property var tabsModel: []
property var activeScrollView: null
property var activeTabContent: null
property bool sidebarExpanded: true
// Track if sidebar was collapsed before searching started
property bool wasCollapsedBeforeSearch: false
// Search state
property string searchText: ""
property var searchIndex: []
property var searchResults: []
property int searchSelectedIndex: 0
property string highlightLabelKey: ""
// Mouse hover suppression during keyboard navigation
property bool ignoreMouseHover: false
property real _lastMouseX: 0
property real _lastMouseY: 0
property bool _mouseInitialized: false
onSearchResultsChanged: {
searchSelectedIndex = 0;
ignoreMouseHover = true;
_mouseInitialized = false;
}
// Signal when close button is clicked
signal closeRequested
// Load search index
FileView {
id: searchIndexFile
path: Quickshell.shellDir + "/Assets/settings-search-index.json"
watchChanges: false
printErrors: false
onLoaded: {
try {
root.searchIndex = JSON.parse(text());
} catch (e) {
root.searchIndex = [];
}
}
}
// Search function
onSearchTextChanged: {
if (searchText.trim() === "") {
searchResults = [];
if (wasCollapsedBeforeSearch) {
root.sidebarExpanded = false;
wasCollapsedBeforeSearch = false;
}
return;
}
// Auto-expand sidebar when searching
if (!root.sidebarExpanded) {
if (root.activeFocus) {
// If we are typing and the sidebar is collapsed and focused, we assume the user is typing to search
wasCollapsedBeforeSearch = true;
}
root.sidebarExpanded = true;
}
if (searchIndex.length === 0)
return;
// Build searchable items with resolved translations
let items = [];
for (let j = 0; j < searchIndex.length; j++) {
const entry = searchIndex[j];
items.push({
"labelKey": entry.labelKey,
"descriptionKey": entry.descriptionKey,
"widget": entry.widget,
"tab": entry.tab,
"tabLabel": entry.tabLabel,
"subTab": entry.subTab,
"subTabLabel": entry.subTabLabel || null,
"label": I18n.tr(entry.labelKey),
"description": entry.descriptionKey ? I18n.tr(entry.descriptionKey) : "",
"subTabName": entry.subTabLabel ? I18n.tr(entry.subTabLabel) : ""
});
}
const results = FuzzySort.go(searchText.trim(), items, {
"keys": ["label", "subTabName", "description"],
"threshold": 0.35,
"limit": 20,
"scoreFn": function (r) {
// r[0]=label, r[1]=subTabName, r[2]=description
// Boost subTabName matches by 1.5x
const labelScore = r[0].score;
const subTabScore = r[1].score * 1.5;
const descScore = r[2].score;
return Math.max(labelScore, subTabScore, descScore);
}
});
let extracted = [];
for (let i = 0; i < results.length; i++) {
extracted.push(results[i].obj);
}
searchResults = extracted;
}
// Navigate to a search result
property int _pendingSubTab: -1
function navigateToResult(entry) {
if (entry.tab < 0 || entry.tab >= tabsModel.length)
return;
highlightLabelKey = entry.labelKey;
_pendingSubTab = (entry.subTab !== null && entry.subTab !== undefined) ? entry.subTab : -1;
// Check if we're already on this tab
const alreadyOnTab = (currentTabIndex === entry.tab);
currentTabIndex = entry.tab;
if (alreadyOnTab && activeTabContent) {
// Tab is already loaded, apply subtab + highlight directly
if (_pendingSubTab >= 0) {
setSubTabIndex(_pendingSubTab);
_pendingSubTab = -1;
}
highlightScrollTimer.targetKey = highlightLabelKey;
highlightScrollTimer.restart();
}
// Clear highlight after a delay
highlightClearTimer.restart();
}
function searchSelectNext() {
if (searchResults.length === 0)
return;
ignoreMouseHover = true;
_mouseInitialized = false;
searchSelectedIndex = Math.min(searchSelectedIndex + 1, searchResults.length - 1);
searchResultsList.positionViewAtIndex(searchSelectedIndex, ListView.Contain);
}
function searchSelectPrevious() {
if (searchResults.length === 0)
return;
ignoreMouseHover = true;
_mouseInitialized = false;
searchSelectedIndex = Math.max(searchSelectedIndex - 1, 0);
searchResultsList.positionViewAtIndex(searchSelectedIndex, ListView.Contain);
}
function searchActivate() {
if (searchSelectedIndex >= 0 && searchSelectedIndex < searchResults.length) {
navigateToResult(searchResults[searchSelectedIndex]);
searchInput.text = "";
}
}
// Set sub-tab on the currently loaded tab content
function setSubTabIndex(subTabIndex) {
if (activeTabContent) {
setSubTabRecursive(activeTabContent, subTabIndex);
}
}
function setSubTabRecursive(item, subTabIndex) {
if (!item)
return false;
if (item.objectName === "NTabBar") {
item.currentIndex = subTabIndex;
return true;
}
const childCount = item.children ? item.children.length : 0;
for (let i = 0; i < childCount; i++) {
if (setSubTabRecursive(item.children[i], subTabIndex))
return true;
}
return false;
}
// Find and highlight a widget by its label key
function findAndHighlightWidget(item, labelKey) {
if (!item)
return null;
// Check if this item has a matching label
if (item.hasOwnProperty("label") && item.label === I18n.tr(labelKey)) {
return item;
}
// Recursively search children
if (item.children) {
for (let i = 0; i < item.children.length; i++) {
const found = findAndHighlightWidget(item.children[i], labelKey);
if (found)
return found;
}
}
return null;
}
Timer {
id: highlightClearTimer
interval: 3000
onTriggered: root.highlightLabelKey = ""
}
Timer {
id: highlightScrollTimer
interval: 200
property string targetKey: ""
onTriggered: {
if (root.activeTabContent && targetKey) {
const widget = root.findAndHighlightWidget(root.activeTabContent, targetKey);
if (widget && root.activeScrollView) {
// Scroll widget into view
const mapped = widget.mapToItem(root.activeScrollView.contentItem, 0, 0);
const scrollBar = root.activeScrollView.ScrollBar.vertical;
if (scrollBar) {
const targetPos = (mapped.y - root.activeScrollView.height / 3) / root.activeScrollView.contentHeight;
scrollBar.position = Math.max(0, Math.min(targetPos, 1.0 - scrollBar.size));
}
// Position highlight overlay
const overlayPos = widget.mapToItem(tabContentArea, 0, 0);
highlightOverlay.x = overlayPos.x - Style.marginM;
highlightOverlay.y = overlayPos.y - Style.marginM;
highlightOverlay.width = widget.width + Style.marginM * 2;
highlightOverlay.height = widget.height + Style.marginM * 2;
highlightAnimation.restart();
}
}
targetKey = "";
}
}
// Save sidebar state when it changes
onSidebarExpandedChanged: {
ShellState.setSettingsSidebarExpanded(sidebarExpanded);
if (!sidebarExpanded) {
root.searchText = "";
searchInput.text = "";
root.forceActiveFocus();
}
}
Component.onCompleted: {
@@ -286,8 +526,43 @@ Item {
ProgramCheckerService.checkAllPrograms();
updateTabsModel();
selectTabById(requestedTab);
if (sidebarExpanded) {
Qt.callLater(() => {
if (searchInput.inputItem)
searchInput.inputItem.forceActiveFocus();
});
} else {
// Ensure root has focus so it can catch typing
Qt.callLater(() => root.forceActiveFocus());
}
}
// Handle typing when sidebar is collapsed
focus: true
Keys.onPressed: event => {
if (!sidebarExpanded && event.text.length > 0 && event.text.trim() !== "") {
// Only capture if it looks like visible text
if (event.modifiers & (Qt.ControlModifier | Qt.AltModifier | Qt.MetaModifier))
return;
// Explicitly ignore backspace and similar keys that might have text but shouldn't trigger search
if (event.key === Qt.Key_Backspace || event.key === Qt.Key_Delete || event.key === Qt.Key_Escape)
return;
wasCollapsedBeforeSearch = true;
sidebarExpanded = true;
searchInput.text = event.text;
Qt.callLater(() => {
if (searchInput.inputItem) {
searchInput.inputItem.forceActiveFocus();
// Cursor moves to end automatically usually, but let's be safe
searchInput.inputItem.cursorPosition = 1;
}
});
event.accepted = true;
}
}
// Scroll functions
function scrollDown() {
if (activeScrollView && activeScrollView.ScrollBar.vertical) {
@@ -388,6 +663,7 @@ Item {
color: toggleMouseArea.containsMouse ? Color.mHover : "transparent"
Behavior on color {
enabled: !Color.isTransitioning
ColorAnimation {
duration: Style.animationFast
easing.type: Easing.InOutQuad
@@ -427,13 +703,196 @@ Item {
}
}
// Search input
NTextInput {
id: searchInput
Layout.fillWidth: true
placeholderText: I18n.tr("common.search")
inputIconName: "search"
visible: root.sidebarExpanded
opacity: root.sidebarExpanded ? 1.0 : 0.0
Behavior on opacity {
NumberAnimation {
duration: Style.animationFast
easing.type: Easing.InOutQuad
}
}
onTextChanged: root.searchText = text
}
// Search button for collapsed sidebar
Item {
id: searchCollapsedContainer
Layout.fillWidth: true
Layout.preferredHeight: Math.round(searchCollapsedRow.implicitHeight + Style.marginS * 2)
visible: !root.sidebarExpanded
opacity: !root.sidebarExpanded ? 1.0 : 0.0
Behavior on opacity {
NumberAnimation {
duration: Style.animationFast
easing.type: Easing.InOutQuad
}
}
Rectangle {
id: searchCollapsedButton
width: Math.round(searchCollapsedRow.implicitWidth + Style.marginS * 2)
height: parent.height
anchors.left: parent.left
radius: Style.radiusS
color: searchCollapsedMouseArea.containsMouse ? Color.mHover : "transparent"
Behavior on color {
enabled: !Color.isTransitioning
ColorAnimation {
duration: Style.animationFast
easing.type: Easing.InOutQuad
}
}
RowLayout {
id: searchCollapsedRow
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
anchors.leftMargin: Style.marginS
spacing: 0
NIcon {
icon: "search"
color: searchCollapsedMouseArea.containsMouse ? Color.mOnHover : Color.mOnSurface
pointSize: Style.fontSizeXL
}
}
MouseArea {
id: searchCollapsedMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
root.sidebarExpanded = true;
root.wasCollapsedBeforeSearch = false; // Expanding manually resets this
Qt.callLater(() => searchInput.inputItem.forceActiveFocus());
}
onEntered: {
TooltipService.show(searchCollapsedButton, I18n.tr("common.search"));
}
onExited: {
TooltipService.hide();
}
}
}
}
Item {
Layout.fillWidth: true
Layout.fillHeight: true
Layout.bottomMargin: Style.marginXL
// Search results list
NListView {
id: searchResultsList
anchors.fill: parent
model: root.searchResults
spacing: Style.marginXS
visible: root.searchText.trim() !== ""
verticalPolicy: ScrollBar.AsNeeded
HoverHandler {
onPointChanged: {
if (!root._mouseInitialized) {
root._lastMouseX = point.position.x;
root._lastMouseY = point.position.y;
root._mouseInitialized = true;
return;
}
const deltaX = Math.abs(point.position.x - root._lastMouseX);
const deltaY = Math.abs(point.position.y - root._lastMouseY);
if (deltaX + deltaY >= 5) {
root.ignoreMouseHover = false;
root._lastMouseX = point.position.x;
root._lastMouseY = point.position.y;
}
}
}
delegate: Rectangle {
id: resultItem
width: searchResultsList.width - (searchResultsList.verticalScrollBarActive ? Style.marginM : 0)
height: resultColumn.implicitHeight + Style.marginS * 2
radius: Style.iRadiusS
readonly property bool selected: index === root.searchSelectedIndex
readonly property bool effectiveHover: !root.ignoreMouseHover && resultMouseArea.containsMouse
color: (effectiveHover || selected) ? Color.mHover : "transparent"
Behavior on color {
enabled: !Color.isTransitioning
ColorAnimation {
duration: Style.animationFast
easing.type: Easing.InOutQuad
}
}
ColumnLayout {
id: resultColumn
anchors.fill: parent
anchors.leftMargin: Style.marginS
anchors.rightMargin: Style.marginS
anchors.topMargin: Style.marginXS
anchors.bottomMargin: Style.marginXS
spacing: 0
NText {
text: I18n.tr(modelData.labelKey)
pointSize: Style.fontSizeM
font.weight: Style.fontWeightSemiBold
color: (resultItem.effectiveHover || resultItem.selected) ? Color.mOnHover : Color.mOnSurface
Layout.fillWidth: true
elide: Text.ElideRight
maximumLineCount: 1
}
NText {
text: {
let t = I18n.tr(modelData.tabLabel);
if (modelData.subTabLabel)
t += " " + I18n.tr(modelData.subTabLabel);
return t;
}
pointSize: Style.fontSizeXS
color: (resultItem.effectiveHover || resultItem.selected) ? Color.mOnHover : Color.mOnSurfaceVariant
Layout.fillWidth: true
elide: Text.ElideRight
maximumLineCount: 1
}
}
MouseArea {
id: resultMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onEntered: {
if (!root.ignoreMouseHover)
root.searchSelectedIndex = index;
}
onClicked: {
root.searchSelectedIndex = index;
root.navigateToResult(modelData);
searchInput.text = "";
}
}
}
}
// Tab list
NListView {
id: sidebarList
visible: root.searchText.trim() === ""
anchors.fill: parent
model: root.tabsModel
spacing: Style.marginXS
@@ -451,6 +910,7 @@ Item {
property color tabTextColor: selected ? Color.mOnPrimary : (tabItem.hovering ? Color.mOnHover : Color.mOnSurface)
Behavior on color {
enabled: !Color.isTransitioning
ColorAnimation {
duration: Style.animationFast
easing.type: Easing.InOutQuad
@@ -458,6 +918,7 @@ Item {
}
Behavior on tabTextColor {
enabled: !Color.isTransitioning
ColorAnimation {
duration: Style.animationFast
easing.type: Easing.InOutQuad
@@ -630,6 +1091,7 @@ Item {
// Tab content area
Rectangle {
id: tabContentArea
Layout.fillWidth: true
Layout.fillHeight: true
Layout.leftMargin: -Style.marginM
@@ -637,6 +1099,7 @@ Item {
color: "transparent"
Repeater {
id: contentRepeater
model: root.tabsModel
delegate: Loader {
anchors.fill: parent
@@ -674,6 +1137,16 @@ Item {
if (item && item.hasOwnProperty("screen")) {
item.screen = root.screen;
}
root.activeTabContent = item;
// Handle pending subtab + highlight from search navigation
if (root.highlightLabelKey) {
if (root._pendingSubTab >= 0) {
root.setSubTabIndex(root._pendingSubTab);
root._pendingSubTab = -1;
}
highlightScrollTimer.targetKey = root.highlightLabelKey;
highlightScrollTimer.restart();
}
}
}
}
@@ -715,6 +1188,42 @@ Item {
}
}
}
// Highlight overlay for search results
Rectangle {
id: highlightOverlay
visible: opacity > 0
opacity: 0
color: Qt.alpha(Color.mSecondary, 0.12)
border.color: Qt.alpha(Color.mSecondary, 0.4)
border.width: Style.borderM
radius: Style.radiusS
z: 100
SequentialAnimation {
id: highlightAnimation
NumberAnimation {
target: highlightOverlay
property: "opacity"
to: 1.0
duration: Style.animationFast
easing.type: Easing.OutQuad
}
PauseAnimation {
duration: 2000
}
NumberAnimation {
target: highlightOverlay
property: "opacity"
to: 0
duration: Style.animationSlowest
easing.type: Easing.InQuad
}
}
}
}
}
}
+37 -6
View File
@@ -94,6 +94,7 @@ SmartPanel {
}
property int requestedTab: SettingsPanel.Tab.General
property var requestedEntry: null
// Content state - these are synced with SettingsContent when panel opens
property int currentTabIndex: 0
@@ -148,8 +149,16 @@ SmartPanel {
// When the panel opens, initialize content
onOpened: {
if (_settingsContent) {
_settingsContent.requestedTab = requestedTab;
_settingsContent.initialize();
if (requestedEntry) {
_settingsContent.requestedTab = requestedEntry.tab;
_settingsContent.initialize();
const entry = requestedEntry;
requestedEntry = null;
Qt.callLater(() => _settingsContent.navigateToResult(entry));
} else {
_settingsContent.requestedTab = requestedTab;
_settingsContent.initialize();
}
}
}
@@ -195,11 +204,25 @@ SmartPanel {
}
function onUpPressed() {
scrollUp();
if (_settingsContent && _settingsContent.searchText.trim() !== "") {
_settingsContent.searchSelectPrevious();
} else {
scrollUp();
}
}
function onDownPressed() {
scrollDown();
if (_settingsContent && _settingsContent.searchText.trim() !== "") {
_settingsContent.searchSelectNext();
} else {
scrollDown();
}
}
function onReturnPressed() {
if (_settingsContent && _settingsContent.searchText.trim() !== "") {
_settingsContent.searchActivate();
}
}
function onPageUpPressed() {
@@ -211,11 +234,19 @@ SmartPanel {
}
function onCtrlJPressed() {
scrollDown();
if (_settingsContent && _settingsContent.searchText.trim() !== "") {
_settingsContent.searchSelectNext();
} else {
scrollDown();
}
}
function onCtrlKPressed() {
scrollUp();
if (_settingsContent && _settingsContent.searchText.trim() !== "") {
_settingsContent.searchSelectPrevious();
} else {
scrollUp();
}
}
panelContent: Rectangle {
@@ -29,14 +29,21 @@ ColumnLayout {
}
}
// Master Volume
// Output Volume
ColumnLayout {
spacing: Style.marginXXS
Layout.fillWidth: true
NLabel {
NValueSlider {
Layout.fillWidth: true
label: I18n.tr("panels.osd.types-volume-label")
description: I18n.tr("panels.audio.volumes-output-volume-description")
from: 0
to: Settings.data.audio.volumeOverdrive ? 1.5 : 1.0
value: localVolume
stepSize: 0.01
text: Math.round(AudioService.volume * 100) + "%"
onMoved: value => localVolume = value
}
Timer {
@@ -49,30 +56,6 @@ ColumnLayout {
}
}
}
NValueSlider {
Layout.fillWidth: true
from: 0
to: Settings.data.audio.volumeOverdrive ? 1.5 : 1.0
value: localVolume
stepSize: 0.01
text: Math.round(AudioService.volume * 100) + "%"
onMoved: value => localVolume = value
}
}
// Volume Feedback sound Toggle
ColumnLayout {
spacing: Style.marginS
Layout.fillWidth: true
NToggle {
label: I18n.tr("panels.audio.volumes-volume-feedback-label")
description: I18n.tr("panels.audio.volumes-volume-feedback-description")
checked: Settings.data.audio.volumeFeedback
defaultValue: Settings.getDefaultValue("audio.volumeFeedback")
onToggled: checked => Settings.data.audio.volumeFeedback = checked
}
}
// Mute Toggle
@@ -92,6 +75,24 @@ ColumnLayout {
}
}
// Volume Feedback sound Toggle
ColumnLayout {
spacing: Style.marginS
Layout.fillWidth: true
NToggle {
label: I18n.tr("panels.audio.volumes-volume-feedback-label")
description: I18n.tr("panels.audio.volumes-volume-feedback-description")
checked: Settings.data.audio.volumeFeedback
defaultValue: Settings.getDefaultValue("audio.volumeFeedback")
onToggled: checked => Settings.data.audio.volumeFeedback = checked
}
}
NDivider {
Layout.fillWidth: true
}
// Input Volume
ColumnLayout {
spacing: Style.marginXS
@@ -142,6 +143,10 @@ ColumnLayout {
}
}
NDivider {
Layout.fillWidth: true
}
// Raise maximum volume above 100%
ColumnLayout {
spacing: Style.marginS
@@ -108,23 +108,18 @@ ColumnLayout {
onToggled: checked => Settings.data.bar.showCapsule = checked
}
ColumnLayout {
NValueSlider {
Layout.fillWidth: true
spacing: Style.marginXXS
visible: Settings.data.bar.showCapsule
NValueSlider {
Layout.fillWidth: true
label: I18n.tr("panels.bar.appearance-capsule-opacity-label")
description: I18n.tr("panels.bar.appearance-capsule-opacity-description")
from: 0
to: 1
stepSize: 0.01
value: Settings.data.bar.capsuleOpacity
defaultValue: Settings.getDefaultValue("bar.capsuleOpacity")
onMoved: value => Settings.data.bar.capsuleOpacity = value
text: Math.floor(Settings.data.bar.capsuleOpacity * 100) + "%"
}
label: I18n.tr("panels.bar.appearance-capsule-opacity-label")
description: I18n.tr("panels.bar.appearance-capsule-opacity-description")
from: 0
to: 1
stepSize: 0.01
value: Settings.data.bar.capsuleOpacity
defaultValue: Settings.getDefaultValue("bar.capsuleOpacity")
onMoved: value => Settings.data.bar.capsuleOpacity = value
text: Math.floor(Settings.data.bar.capsuleOpacity * 100) + "%"
}
NToggle {
@@ -162,36 +157,28 @@ ColumnLayout {
Layout.fillWidth: true
spacing: Style.marginL
ColumnLayout {
spacing: Style.marginXXS
NValueSlider {
Layout.fillWidth: true
label: I18n.tr("panels.bar.appearance-margins-vertical")
from: 0
to: 18
stepSize: 1
value: Settings.data.bar.marginVertical
defaultValue: Settings.getDefaultValue("bar.marginVertical")
onMoved: value => Settings.data.bar.marginVertical = value
text: Settings.data.bar.marginVertical + "px"
}
NValueSlider {
Layout.fillWidth: true
label: I18n.tr("panels.bar.appearance-margins-vertical")
from: 0
to: 18
stepSize: 1
value: Settings.data.bar.marginVertical
defaultValue: Settings.getDefaultValue("bar.marginVertical")
onMoved: value => Settings.data.bar.marginVertical = value
text: Settings.data.bar.marginVertical + "px"
}
ColumnLayout {
spacing: Style.marginXXS
NValueSlider {
Layout.fillWidth: true
label: I18n.tr("panels.bar.appearance-margins-horizontal")
from: 0
to: 18
stepSize: 1
value: Settings.data.bar.marginHorizontal
defaultValue: Settings.getDefaultValue("bar.marginHorizontal")
onMoved: value => Settings.data.bar.marginHorizontal = value
text: Settings.data.bar.marginHorizontal + "px"
}
NValueSlider {
Layout.fillWidth: true
label: I18n.tr("panels.bar.appearance-margins-horizontal")
from: 0
to: 18
stepSize: 1
value: Settings.data.bar.marginHorizontal
defaultValue: Settings.getDefaultValue("bar.marginHorizontal")
onMoved: value => Settings.data.bar.marginHorizontal = value
text: Settings.data.bar.marginHorizontal + "px"
}
}
}
@@ -3,7 +3,6 @@ import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import Quickshell.Io
import "."
import qs.Commons
import qs.Services.System
import qs.Services.Theming
@@ -247,32 +246,7 @@ ColumnLayout {
label: I18n.tr("panels.color-scheme.wallpaper-method-label")
description: I18n.tr("panels.color-scheme.wallpaper-method-description")
enabled: Settings.data.colorSchemes.useWallpaperColors
model: [
{
"key": "tonal-spot",
"name": "M3-Tonal Spot" // Do not translate
},
{
"key": "content",
"name": "M3-Content" // Do not translate
},
{
"key": "fruit-salad",
"name": "M3-Fruit Salad" // Do not translate
},
{
"key": "rainbow",
"name": "M3-Rainbow" // Do not translate
},
{
"key": "vibrant",
"name": I18n.tr("common.vibrant")
},
{
"key": "faithful",
"name": I18n.tr("common.faithful")
},
]
model: TemplateProcessor.schemeTypes
currentKey: Settings.data.colorSchemes.generationMethod
onSelected: key => {
Settings.data.colorSchemes.generationMethod = key;
@@ -282,13 +256,12 @@ ColumnLayout {
NDivider {
Layout.fillWidth: true
visible: !Settings.data.colorSchemes.useWallpaperColors
}
ColumnLayout {
spacing: Style.marginM
Layout.fillWidth: true
visible: !Settings.data.colorSchemes.useWallpaperColors
enabled: !Settings.data.colorSchemes.useWallpaperColors
NHeader {
label: I18n.tr("panels.color-scheme.predefined-title")
@@ -311,6 +284,7 @@ ColumnLayout {
property string schemePath: modelData
property string schemeName: root.extractSchemeName(modelData)
opacity: enabled ? 1.0 : 0.6
Layout.fillWidth: true
Layout.alignment: Qt.AlignHCenter
height: 50 * Style.uiScaleRatio
@@ -318,7 +292,7 @@ ColumnLayout {
color: root.getSchemeColor(schemeName, "mSurface")
border.width: Style.borderL
border.color: {
if (Settings.data.colorSchemes.predefinedScheme === schemeName) {
if ((Settings.data.colorSchemes.predefinedScheme === schemeName) && schemeItem.enabled) {
return Color.mSecondary;
}
if (itemMouseArea.containsMouse) {
@@ -378,6 +352,7 @@ ColumnLayout {
MouseArea {
id: itemMouseArea
anchors.fill: parent
enabled: schemeItem.enabled
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
@@ -390,7 +365,7 @@ ColumnLayout {
}
Rectangle {
visible: (Settings.data.colorSchemes.predefinedScheme === schemeItem.schemeName)
visible: (Settings.data.colorSchemes.predefinedScheme === schemeItem.schemeName) && schemeItem.enabled
anchors.right: parent.right
anchors.top: parent.top
anchors.rightMargin: 0
+10 -18
View File
@@ -51,25 +51,17 @@ ColumnLayout {
defaultValue: Settings.getDefaultValue("general.enableLockScreenCountdown")
}
ColumnLayout {
NValueSlider {
visible: Settings.data.general.showSessionButtonsOnLockScreen && Settings.data.general.enableLockScreenCountdown
spacing: Style.marginXXS
Layout.fillWidth: true
NLabel {
label: I18n.tr("panels.session-menu.countdown-duration-label")
description: I18n.tr("panels.session-menu.countdown-duration-description")
}
NValueSlider {
Layout.fillWidth: true
from: 1000
to: 30000
stepSize: 1000
value: Settings.data.general.lockScreenCountdownDuration
onMoved: value => Settings.data.general.lockScreenCountdownDuration = value
text: Math.round(Settings.data.general.lockScreenCountdownDuration / 1000) + "s"
defaultValue: Settings.getDefaultValue("general.lockScreenCountdownDuration")
}
label: I18n.tr("panels.session-menu.countdown-duration-label")
description: I18n.tr("panels.session-menu.countdown-duration-description")
from: 1000
to: 30000
stepSize: 1000
value: Settings.data.general.lockScreenCountdownDuration
onMoved: value => Settings.data.general.lockScreenCountdownDuration = value
text: Math.round(Settings.data.general.lockScreenCountdownDuration / 1000) + "s"
defaultValue: Settings.getDefaultValue("general.lockScreenCountdownDuration")
}
}
@@ -53,23 +53,18 @@ ColumnLayout {
}
// Sound Volume
ColumnLayout {
NValueSlider {
enabled: SoundService.multimediaAvailable && (Settings.data.notifications?.sounds?.enabled ?? false)
spacing: Style.marginXXS
Layout.fillWidth: true
NValueSlider {
Layout.fillWidth: true
label: I18n.tr("panels.notifications.sounds-volume-label")
description: I18n.tr("panels.notifications.sounds-volume-description")
from: 0
to: 1
stepSize: 0.01
value: Settings.data.notifications?.sounds?.volume ?? 0.5
onMoved: value => Settings.data.notifications.sounds.volume = value
text: Math.round((Settings.data.notifications?.sounds?.volume ?? 0.5) * 100) + "%"
defaultValue: Settings.getDefaultValue("notifications.sounds.volume")
}
label: I18n.tr("panels.notifications.sounds-volume-label")
description: I18n.tr("panels.notifications.sounds-volume-description")
from: 0
to: 1
stepSize: 0.01
value: Settings.data.notifications?.sounds?.volume ?? 0.5
onMoved: value => Settings.data.notifications.sounds.volume = value
text: Math.round((Settings.data.notifications?.sounds?.volume ?? 0.5) * 100) + "%"
defaultValue: Settings.getDefaultValue("notifications.sounds.volume")
}
// Separate Sounds Toggle
@@ -105,25 +105,17 @@ ColumnLayout {
defaultValue: Settings.getDefaultValue("sessionMenu.enableCountdown")
}
ColumnLayout {
NValueSlider {
visible: Settings.data.sessionMenu.enableCountdown
spacing: Style.marginXXS
Layout.fillWidth: true
NLabel {
label: I18n.tr("panels.session-menu.countdown-duration-label")
description: I18n.tr("panels.session-menu.countdown-duration-description")
}
NValueSlider {
Layout.fillWidth: true
from: 1000
to: 30000
stepSize: 1000
value: Settings.data.sessionMenu.countdownDuration
onMoved: value => Settings.data.sessionMenu.countdownDuration = value
text: Math.round(Settings.data.sessionMenu.countdownDuration / 1000) + "s"
defaultValue: Settings.getDefaultValue("sessionMenu.countdownDuration")
}
label: I18n.tr("panels.session-menu.countdown-duration-label")
description: I18n.tr("panels.session-menu.countdown-duration-description")
from: 1000
to: 30000
stepSize: 1000
value: Settings.data.sessionMenu.countdownDuration
onMoved: value => Settings.data.sessionMenu.countdownDuration = value
text: Math.round(Settings.data.sessionMenu.countdownDuration / 1000) + "s"
defaultValue: Settings.getDefaultValue("sessionMenu.countdownDuration")
}
}
@@ -220,12 +220,12 @@ SmartPanel {
icon: "download-speed"
suffix: "%"
fillColor: Color.mPrimary
tooltipText: I18n.tr("common.download") + `: ${SystemStatService.formatSpeed(SystemStatService.rxSpeed)}`
tooltipText: I18n.tr("common.download") + `: ${SystemStatService.formatSpeed(SystemStatService.rxSpeed).replace(/([0-9.]+)([A-Za-z]+)/, "$1 $2")}`
Layout.alignment: Qt.AlignHCenter
}
NText {
text: SystemStatService.formatSpeed(SystemStatService.rxSpeed) + "/s"
text: SystemStatService.formatSpeed(SystemStatService.rxSpeed).replace(/([0-9.]+)([A-Za-z]+)/, "$1 $2") + "/s"
pointSize: Style.fontSizeXXS
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
@@ -248,12 +248,12 @@ SmartPanel {
icon: "upload-speed"
suffix: "%"
fillColor: Color.mPrimary
tooltipText: I18n.tr("common.upload") + `: ${SystemStatService.formatSpeed(SystemStatService.txSpeed)}`
tooltipText: I18n.tr("common.upload") + `: ${SystemStatService.formatSpeed(SystemStatService.txSpeed).replace(/([0-9.]+)([A-Za-z]+)/, "$1 $2")}`
Layout.alignment: Qt.AlignHCenter
}
NText {
text: SystemStatService.formatSpeed(SystemStatService.txSpeed) + "/s"
text: SystemStatService.formatSpeed(SystemStatService.txSpeed).replace(/([0-9.]+)([A-Za-z]+)/, "$1 $2") + "/s"
pointSize: Style.fontSizeXXS
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
@@ -317,7 +317,7 @@ SmartPanel {
}
NText {
text: SystemStatService.formatMemoryGb(SystemStatService.memGb)
text: SystemStatService.formatMemoryGb(SystemStatService.memGb).replace(/([0-9.]+)([A-Za-z]+)/, "$1 $2")
pointSize: Style.fontSizeXS
color: Color.mOnSurface
Layout.fillWidth: true
@@ -344,7 +344,7 @@ SmartPanel {
}
NText {
text: `${SystemStatService.formatMemoryGb(SystemStatService.swapGb)} / ${SystemStatService.formatMemoryGb(SystemStatService.swapTotalGb)}`
text: `${SystemStatService.formatMemoryGb(SystemStatService.swapGb).replace(/([0-9.]+)([A-Za-z]+)/, "$1 $2")} / ${SystemStatService.formatMemoryGb(SystemStatService.swapTotalGb).replace(/([0-9.]+)([A-Za-z]+)/, "$1 $2")}`
pointSize: Style.fontSizeXS
color: Color.mOnSurface
Layout.fillWidth: true
@@ -373,7 +373,7 @@ SmartPanel {
text: {
const usedGb = SystemStatService.diskUsedGb[panelContent.diskPath] || 0;
const sizeGb = SystemStatService.diskSizeGb[panelContent.diskPath] || 0;
return `${usedGb.toFixed(1)}G / ${sizeGb.toFixed(1)}G`;
return `${usedGb.toFixed(1)} G / ${sizeGb.toFixed(1)} G`;
}
pointSize: Style.fontSizeXS
color: Color.mOnSurface
+43 -16
View File
@@ -5,6 +5,7 @@ import Quickshell
import qs.Commons
import qs.Modules.MainScreen
import qs.Modules.Panels.Settings
import qs.Services.Theming
import qs.Services.UI
import qs.Widgets
@@ -609,20 +610,20 @@ SmartPanel {
for (var i = 0; i < directoriesList.length; i++) {
var dirPath = directoriesList[i];
combinedItems.push({
"path": dirPath,
"name": dirPath.split('/').pop(),
"isDirectory": true
});
"path": dirPath,
"name": dirPath.split('/').pop(),
"isDirectory": true
});
}
}
// Add files
for (var i = 0; i < wallpapersList.length; i++) {
combinedItems.push({
"path": wallpapersList[i],
"name": wallpapersList[i].split('/').pop(),
"isDirectory": false
});
"path": wallpapersList[i],
"name": wallpapersList[i].split('/').pop(),
"isDirectory": false
});
}
// Apply filter if text is present
@@ -686,7 +687,7 @@ SmartPanel {
var browsePath = WallpaperService.getCurrentBrowsePath(targetScreen.name);
currentBrowsePath = browsePath;
WallpaperService.scanDirectoryWithDirs(targetScreen.name, browsePath, function(result) {
WallpaperService.scanDirectoryWithDirs(targetScreen.name, browsePath, function (result) {
wallpapersList = result.files;
directoriesList = result.directories;
Logger.d("WallpaperPanel", "Browse mode: Got", wallpapersList.length, "files and", directoriesList.length, "directories for screen", targetScreen.name);
@@ -716,8 +717,10 @@ SmartPanel {
// Helper function to get icon for current view mode
function getViewModeIcon() {
var mode = Settings.data.wallpaper.viewMode;
if (mode === "single") return "folder";
if (mode === "recursive") return "folders";
if (mode === "single")
return "folder";
if (mode === "recursive")
return "folders";
return "folder-open";
}
@@ -725,9 +728,12 @@ SmartPanel {
function getViewModeTooltip() {
var mode = Settings.data.wallpaper.viewMode;
var modeName;
if (mode === "single") modeName = I18n.tr("panels.wallpaper.view-mode-single");
else if (mode === "recursive") modeName = I18n.tr("panels.wallpaper.view-mode-recursive");
else modeName = I18n.tr("panels.wallpaper.view-mode-browse");
if (mode === "single")
modeName = I18n.tr("panels.wallpaper.view-mode-single");
else if (mode === "recursive")
modeName = I18n.tr("panels.wallpaper.view-mode-recursive");
else
modeName = I18n.tr("panels.wallpaper.view-mode-browse");
return I18n.tr("panels.wallpaper.view-mode-cycle-tooltip").replace("{mode}", modeName);
}
@@ -769,6 +775,20 @@ SmartPanel {
}
// Right side: actions (view mode, hide filenames, refresh)
NComboBox {
visible: Settings.data.colorSchemes.useWallpaperColors
baseSize: 0.8
Layout.minimumWidth: 200
minimumWidth: 200
tooltip: I18n.tr("panels.color-scheme.wallpaper-method-label")
model: TemplateProcessor.schemeTypes
currentKey: Settings.data.colorSchemes.generationMethod
onSelected: key => {
Settings.data.colorSchemes.generationMethod = key;
AppThemeService.generate();
}
}
NIconButton {
icon: getViewModeIcon()
tooltipText: getViewModeTooltip()
@@ -777,12 +797,19 @@ SmartPanel {
}
NIconButton {
icon: Settings.data.wallpaper.hideWallpaperFilenames ? "eye-closed" : "eye"
icon: Settings.data.wallpaper.hideWallpaperFilenames ? "id-off" : "id"
tooltipText: Settings.data.wallpaper.hideWallpaperFilenames ? I18n.tr("panels.wallpaper.settings-hide-wallpaper-filenames-tooltip-show") : I18n.tr("panels.wallpaper.settings-hide-wallpaper-filenames-tooltip-hide")
baseSize: Style.baseWidgetSize * 0.8
onClicked: Settings.data.wallpaper.hideWallpaperFilenames = !Settings.data.wallpaper.hideWallpaperFilenames
}
NIconButton {
icon: Settings.data.wallpaper.showHiddenFiles ? "eye" : "eye-closed"
tooltipText: Settings.data.wallpaper.showHiddenFiles ? I18n.tr("panels.wallpaper.settings-show-hidden-files-tooltip-hide") : I18n.tr("panels.wallpaper.settings-show-hidden-files-tooltip-show")
baseSize: Style.baseWidgetSize * 0.8
onClicked: Settings.data.wallpaper.showHiddenFiles = !Settings.data.wallpaper.showHiddenFiles
}
NIconButton {
icon: "refresh"
tooltipText: I18n.tr("tooltips.refresh-wallpaper-list")
@@ -978,7 +1005,7 @@ SmartPanel {
NIcon {
icon: "folder"
pointSize: Style.fontSizeXXL
pointSize: Style.fontSizeXXXL
color: Color.mPrimary
Layout.alignment: Qt.AlignHCenter
}
+1 -1
View File
@@ -38,7 +38,7 @@ ghostty)
echo "theme = noctalia" >>"$CONFIG_FILE"
fi
# Only signal if ghostty is running
pgrep -x ghostty >/dev/null && pkill -SIGUSR2 ghostty || true
pgrep -f ghostty >/dev/null && pkill -SIGUSR2 ghostty || true
else
echo "Error: ghostty config file not found at $CONFIG_FILE" >&2
exit 1
+345
View File
@@ -0,0 +1,345 @@
#!/usr/bin/env python3
"""
Build settings search index from QML source files.
Parses settings tab QML files to extract searchable metadata
(i18n keys, widget types, tab/sub-tab locations).
Output: Assets/settings-search-index.json
Usage:
python Scripts/dev/build-settings-search-index.py
"""
import json
import re
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent.parent
SETTINGS_DIR = ROOT / "Modules" / "Panels" / "Settings"
TABS_DIR = SETTINGS_DIR / "Tabs"
OUTPUT = ROOT / "Assets" / "settings-search-index.json"
# Widget types that have searchable label/description
WIDGET_TYPES = (
"NToggle",
"NComboBox",
"NValueSlider",
"NSpinBox",
"NSearchableComboBox",
"NTextInputButton",
"NTextInput",
"NCheckbox",
"NLabel",
)
# Regex patterns
RE_WIDGET_OPEN = re.compile(
r"^\s*(" + "|".join(WIDGET_TYPES) + r")\s*\{", re.MULTILINE
)
RE_LABEL = re.compile(r'label:\s*I18n\.tr\("([^"]+)"')
RE_DESCRIPTION = re.compile(r'description:\s*I18n\.tr\("([^"]+)"')
def parse_component_declarations(content: str) -> dict[str, str]:
"""
Parse Component declarations from SettingsContent.qml.
Returns: component_id -> QML type name (e.g. "generalTab" -> "GeneralTab")
"""
components = {}
# Match patterns like:
# Component {
# id: generalTab
# GeneralTab {}
# }
pattern = re.compile(
r"Component\s*\{\s*\n\s*id:\s*(\w+)\s*\n\s*(\w+)\s*\{",
re.MULTILINE,
)
for m in pattern.finditer(content):
comp_id = m.group(1)
type_name = m.group(2)
components[comp_id] = type_name
return components
def parse_tabs_model_order(content: str) -> list[tuple[str, str]]:
"""
Parse updateTabsModel() to get the ordered list of (source_id, label_key) pairs.
Returns: list of (component_id, i18n_label_key) in display order.
"""
# Find the updateTabsModel function body
match = re.search(r"function updateTabsModel\(\)\s*\{", content)
if not match:
return []
func_body = content[match.end():]
# Extract tab entries: each has "label" and "source" fields
entries = []
for m in re.finditer(
r'"label":\s*"([^"]+)"[^}]*?"source":\s*(\w+)',
func_body,
re.DOTALL,
):
entries.append((m.group(2), m.group(1)))
return entries
def build_tab_mappings(content: str) -> tuple[dict[str, int], dict[str, str]]:
"""
Build mappings from QML type name to tabsModel index and label key.
Parses Component declarations and updateTabsModel() order.
Returns: (type_to_index, type_to_label)
- type_to_index: e.g. {"GeneralTab": 0, ...}
- type_to_label: e.g. {"GeneralTab": "common.general", ...}
"""
components = parse_component_declarations(content)
entries = parse_tabs_model_order(content)
type_to_index = {}
type_to_label = {}
for idx, (source_id, label_key) in enumerate(entries):
type_name = components.get(source_id)
if type_name:
type_to_index[type_name] = idx
type_to_label[type_name] = label_key
return type_to_index, type_to_label
def get_subtab_info(parent_tab_file: Path) -> tuple[list[str], list[str | None]]:
"""
Parse a parent tab file to get subtab order and labels.
Returns: (subtab_type_names, subtab_label_keys)
- subtab_type_names: list of component names like ["VolumesSubTab", ...]
- subtab_label_keys: list of i18n keys like ["common.volumes", ...] (same order)
"""
content = parent_tab_file.read_text()
# Extract NTabButton labels in order from NTabBar
labels = []
in_tabbar = False
tabbar_depth = 0
for line in content.splitlines():
stripped = line.strip()
if not in_tabbar:
if re.match(r"NTabBar\s*\{", stripped):
in_tabbar = True
tabbar_depth = 1
continue
continue
tabbar_depth += stripped.count("{") - stripped.count("}")
if tabbar_depth <= 0:
break
# Match text: I18n.tr("...") inside NTabButton
m = re.search(r'text:\s*I18n\.tr\("([^"]+)"', stripped)
if m:
labels.append(m.group(1))
# Extract subtab component names from NTabView
subtabs = []
in_tabview = False
tabview_depth = 0
for line in content.splitlines():
stripped = line.strip()
if not in_tabview:
if re.match(r"NTabView\s*\{", stripped):
in_tabview = True
tabview_depth = 1
continue
continue
tabview_depth += stripped.count("{") - stripped.count("}")
if tabview_depth <= 0:
break
# Match component instantiations like "VolumesSubTab {}" or "VolumesSubTab {"
m = re.match(r"(\w+SubTab)\s*\{", stripped)
if m:
subtabs.append(m.group(1))
# Pad labels list if shorter than subtabs (shouldn't happen, but safety)
while len(labels) < len(subtabs):
labels.append(None)
return subtabs, labels[:len(subtabs)]
def resolve_tab_info(
qml_file: Path,
type_to_index: dict[str, int],
type_to_label: dict[str, str],
) -> tuple[int | None, str | None, int | None, str | None]:
"""
Determine the tab index, tab label, sub-tab index, and sub-tab label for a QML file.
Returns (tab_index, tab_label_key, sub_tab_index, sub_tab_label_key)
"""
parent = qml_file.parent
stem = qml_file.stem
# Top-level tab files (directly in Tabs/)
if parent == TABS_DIR:
tab_index = type_to_index.get(stem)
tab_label = type_to_label.get(stem)
return tab_index, tab_label, None, None
# Sub-directory files
dir_name = parent.name # e.g. "Audio", "Bar"
parent_type = f"{dir_name}Tab" # e.g. "AudioTab"
tab_index = type_to_index.get(parent_type)
tab_label = type_to_label.get(parent_type)
if tab_index is None:
return None, None, None, None
# Skip the parent tab file itself (e.g. AudioTab.qml) — still scan for widgets
if stem.endswith("Tab") and not stem.endswith("SubTab"):
return tab_index, tab_label, None, None
# Determine sub-tab index and label from parent tab's NTabBar/NTabView
parent_tab_file = parent / f"{dir_name}Tab.qml"
if not parent_tab_file.exists():
return tab_index, tab_label, None, None
subtab_names, subtab_labels = get_subtab_info(parent_tab_file)
try:
idx = subtab_names.index(stem)
sub_label = subtab_labels[idx] if idx < len(subtab_labels) else None
return tab_index, tab_label, idx, sub_label
except ValueError:
return tab_index, tab_label, None, None
def extract_widget_blocks(content: str) -> list[tuple[str, str]]:
"""
Extract (widget_type, block_text) pairs from QML content.
Uses brace-depth tracking to capture the full widget block.
"""
results = []
lines = content.splitlines()
i = 0
while i < len(lines):
m = RE_WIDGET_OPEN.match(lines[i])
if m:
widget_type = m.group(1)
depth = 0
block_lines = []
j = i
while j < len(lines):
line = lines[j]
block_lines.append(line)
depth += line.count("{") - line.count("}")
if depth <= 0:
break
j += 1
block_text = "\n".join(block_lines)
results.append((widget_type, block_text))
i = j + 1
else:
i += 1
return results
def extract_entries(
qml_file: Path,
type_to_index: dict[str, int],
type_to_label: dict[str, str],
) -> list[dict]:
"""Extract all searchable settings entries from a QML file."""
tab_index, tab_label, sub_tab, sub_tab_label = resolve_tab_info(
qml_file, type_to_index, type_to_label
)
if tab_index is None:
return []
content = qml_file.read_text()
entries = []
for widget_type, block in extract_widget_blocks(content):
label_match = RE_LABEL.search(block)
if not label_match:
continue
label_key = label_match.group(1)
desc_match = RE_DESCRIPTION.search(block)
desc_key = desc_match.group(1) if desc_match else None
entry = {
"labelKey": label_key,
"descriptionKey": desc_key,
"widget": widget_type,
"tab": tab_index,
"tabLabel": tab_label,
"subTab": sub_tab,
}
if sub_tab_label is not None:
entry["subTabLabel"] = sub_tab_label
entries.append(entry)
return entries
def main():
if not TABS_DIR.exists():
print(f"Error: Tabs directory not found: {TABS_DIR}", file=sys.stderr)
sys.exit(1)
settings_content = SETTINGS_DIR / "SettingsContent.qml"
if not settings_content.exists():
print(f"Error: SettingsContent.qml not found: {settings_content}", file=sys.stderr)
sys.exit(1)
# Build type -> tabsModel index/label mappings from SettingsContent.qml
content = settings_content.read_text()
type_to_index, type_to_label = build_tab_mappings(content)
if not type_to_index:
print("Error: Could not parse tab model from SettingsContent.qml", file=sys.stderr)
sys.exit(1)
print(f"Parsed {len(type_to_index)} tab types from SettingsContent.qml")
all_entries = []
seen_labels = set()
# Scan all QML files in Tabs/ (recursive)
for qml_file in sorted(TABS_DIR.rglob("*.qml")):
entries = extract_entries(qml_file, type_to_index, type_to_label)
for entry in entries:
if entry["labelKey"] not in seen_labels:
seen_labels.add(entry["labelKey"])
all_entries.append(entry)
# Write output
OUTPUT.parent.mkdir(parents=True, exist_ok=True)
with open(OUTPUT, "w") as f:
json.dump(all_entries, f, indent=2)
print(f"Generated {len(all_entries)} entries -> {OUTPUT.relative_to(ROOT)}")
if __name__ == "__main__":
main()
-205
View File
@@ -1,205 +0,0 @@
#!/usr/bin/env python3
"""
Compare Noctalia's template-processor color extraction with matugen.
Usage:
./compare-matugen.py <wallpaper_path>
./compare-matugen.py ~/Pictures/Wallpapers/example.png
Compares all M3 schemes (tonal-spot, fruit-salad, rainbow, content) and shows
a table with hue differences.
"""
import argparse
import json
import subprocess
import sys
from pathlib import Path
# Add the theming lib to path
SCRIPT_DIR = Path(__file__).parent.resolve()
THEMING_DIR = SCRIPT_DIR.parent / "python" / "src" / "theming"
sys.path.insert(0, str(THEMING_DIR))
from lib.color import Color
from lib.hct import Hct
def hue_diff(h1: float, h2: float) -> float:
"""Calculate circular hue difference."""
diff = abs(h1 - h2)
return min(diff, 360.0 - diff)
def hex_to_rgb(hex_color: str) -> tuple[int, int, int]:
"""Convert hex to RGB tuple."""
h = hex_color.lstrip('#')
return tuple(int(h[i:i+2], 16) for i in (0, 2, 4))
def rgb_distance(hex1: str, hex2: str) -> float:
"""Calculate Euclidean RGB distance (0-441 range)."""
r1, g1, b1 = hex_to_rgb(hex1)
r2, g2, b2 = hex_to_rgb(hex2)
return ((r1-r2)**2 + (g1-g2)**2 + (b1-b2)**2) ** 0.5
def get_hct(hex_color: str) -> Hct:
"""Convert hex color to HCT."""
return Color.from_hex(hex_color).to_hct()
def run_our_processor(image_path: Path, scheme: str) -> dict | None:
"""Run our template-processor and return colors."""
cmd = [
sys.executable,
str(THEMING_DIR / "template-processor.py"),
str(image_path),
"--scheme-type", scheme,
"--dark"
]
try:
result = subprocess.run(cmd, capture_output=True, text=True, check=True)
data = json.loads(result.stdout)
return data.get("dark", {})
except (subprocess.CalledProcessError, json.JSONDecodeError) as e:
print(f"Error running our processor: {e}", file=sys.stderr)
return None
def run_matugen(image_path: Path, scheme: str) -> dict | None:
"""Run matugen and return colors."""
matugen_scheme = f"scheme-{scheme}"
cmd = [
"matugen", "image", str(image_path),
"--json", "hex",
"--dry-run",
"-t", matugen_scheme
]
try:
result = subprocess.run(cmd, capture_output=True, text=True, check=True)
data = json.loads(result.stdout)
colors = data.get("colors", {})
# Extract dark mode values
return {k: v.get("dark", v) for k, v in colors.items() if isinstance(v, dict)}
except subprocess.CalledProcessError as e:
print(f"Error running matugen: {e}", file=sys.stderr)
return None
except json.JSONDecodeError as e:
print(f"Error parsing matugen output: {e}", file=sys.stderr)
return None
def compare_schemes(image_path: Path) -> None:
"""Compare all M3 schemes between our processor and matugen."""
schemes = ["tonal-spot", "fruit-salad", "rainbow", "content"]
color_keys = ["primary", "secondary", "tertiary", "surface", "on_surface"]
print(f"\nComparing: {image_path.name}\n")
print("=" * 78)
# Header
print(f"{'Scheme':<12} {'Color':<14} {'Ours':<10} {'Matugen':<10} {'Diff':>10} {'Match':<10}")
print("-" * 78)
for scheme in schemes:
ours = run_our_processor(image_path, scheme)
matugen = run_matugen(image_path, scheme)
if not ours or not matugen:
print(f"{scheme}: Failed to get colors")
continue
for key in color_keys:
our_hex = ours.get(key, "")
mat_hex = matugen.get(key, "")
if not our_hex or not mat_hex:
continue
try:
our_hct = get_hct(our_hex)
mat_hct = get_hct(mat_hex)
avg_chroma = (our_hct.chroma + mat_hct.chroma) / 2
# For low-chroma colors, use RGB distance instead of hue
# (hue is meaningless for near-grayscale colors)
if avg_chroma < 15:
rgb_dist = rgb_distance(our_hex, mat_hex)
# RGB distance: 0-10 excellent, 10-25 good, 25-50 fair
if rgb_dist < 10:
match = "excellent"
elif rgb_dist < 25:
match = "good"
elif rgb_dist < 50:
match = "fair"
else:
match = "poor"
diff_str = f"{rgb_dist:>5.1f} rgb"
else:
diff = hue_diff(our_hct.hue, mat_hct.hue)
if diff < 5:
match = "excellent"
elif diff < 15:
match = "good"
elif diff < 30:
match = "fair"
else:
match = "poor"
diff_str = f"{diff:>5.1f} hue"
print(f"{scheme:<12} {key:<14} {our_hex:<10} {mat_hex:<10} {diff_str:>10} {match:<10}")
except Exception as e:
print(f"{scheme:<12} {key:<14} Error: {e}")
print("-" * 78)
# Also show source color comparison
print("\nSource Color Extraction:")
print("-" * 40)
ours = run_our_processor(image_path, "tonal-spot")
matugen = run_matugen(image_path, "tonal-spot")
if ours and matugen:
# Get source from primary at tone 40 (approximation)
our_primary = ours.get("primary", "")
mat_source = matugen.get("source_color", "")
if our_primary and mat_source:
our_hct = get_hct(our_primary)
mat_hct = get_hct(mat_source)
print(f"Our primary hue: {our_hct.hue:.1f}°")
print(f"Matugen source hue: {mat_hct.hue:.1f}°")
print(f"Difference: {hue_diff(our_hct.hue, mat_hct.hue):.1f}°")
def main() -> int:
parser = argparse.ArgumentParser(
description="Compare Noctalia template-processor with matugen"
)
parser.add_argument(
"wallpaper",
type=Path,
help="Path to wallpaper image"
)
args = parser.parse_args()
if not args.wallpaper.exists():
print(f"Error: File not found: {args.wallpaper}", file=sys.stderr)
return 1
# Check if matugen is available
try:
subprocess.run(["matugen", "--version"], capture_output=True, check=True)
except (subprocess.CalledProcessError, FileNotFoundError):
print("Error: matugen not found. Please install matugen first.", file=sys.stderr)
return 1
compare_schemes(args.wallpaper)
return 0
if __name__ == "__main__":
sys.exit(main())
+318
View File
@@ -0,0 +1,318 @@
#!/usr/bin/env python3
"""
Analyze Noctalia's template-processor color extraction.
Usage:
./template-processor-analysis.py <wallpaper_path>
./template-processor-analysis.py ~/Pictures/Wallpapers/example.png
Shows extracted colors for all scheme types and compares M3 schemes with matugen.
Scheme types:
- M3 schemes (tonal-spot, fruit-salad, rainbow, content): Compared with matugen
- vibrant: Prioritizes the most saturated colors regardless of area
- faithful: Prioritizes dominant colors by area coverage
- muted: Preserves hue but caps saturation low (for monochrome wallpapers)
"""
import argparse
import json
import subprocess
import sys
from pathlib import Path
# Add the theming lib to path
SCRIPT_DIR = Path(__file__).parent.resolve()
THEMING_DIR = SCRIPT_DIR.parent / "python" / "src" / "theming"
sys.path.insert(0, str(THEMING_DIR))
from lib.color import Color
from lib.hct import Hct
def hue_diff(h1: float, h2: float) -> float:
"""Calculate circular hue difference."""
diff = abs(h1 - h2)
return min(diff, 360.0 - diff)
def hex_to_rgb(hex_color: str) -> tuple[int, int, int]:
"""Convert hex to RGB tuple."""
h = hex_color.lstrip('#')
return tuple(int(h[i:i+2], 16) for i in (0, 2, 4))
def rgb_distance(hex1: str, hex2: str) -> float:
"""Calculate Euclidean RGB distance (0-441 range)."""
r1, g1, b1 = hex_to_rgb(hex1)
r2, g2, b2 = hex_to_rgb(hex2)
return ((r1-r2)**2 + (g1-g2)**2 + (b1-b2)**2) ** 0.5
def get_hct(hex_color: str) -> Hct:
"""Convert hex color to HCT."""
return Color.from_hex(hex_color).to_hct()
def hue_to_name(hue: float) -> str:
"""Convert hue to color name."""
if hue < 30 or hue >= 330:
return "RED"
elif hue < 60:
return "ORANGE"
elif hue < 90:
return "YELLOW"
elif hue < 150:
return "GREEN"
elif hue < 210:
return "CYAN"
elif hue < 270:
return "BLUE"
elif hue < 330:
return "PURPLE"
return "RED"
def run_our_processor(image_path: Path, scheme: str) -> dict | None:
"""Run our template-processor and return colors."""
cmd = [
sys.executable,
str(THEMING_DIR / "template-processor.py"),
str(image_path),
"--scheme-type", scheme,
"--dark"
]
try:
result = subprocess.run(cmd, capture_output=True, text=True, check=True)
data = json.loads(result.stdout)
return data.get("dark", {})
except (subprocess.CalledProcessError, json.JSONDecodeError) as e:
print(f"Error running our processor: {e}", file=sys.stderr)
return None
def run_matugen(image_path: Path, scheme: str) -> dict | None:
"""Run matugen and return colors."""
matugen_scheme = f"scheme-{scheme}"
cmd = [
"matugen", "image", str(image_path),
"--json", "hex",
"--dry-run",
"-t", matugen_scheme
]
try:
result = subprocess.run(cmd, capture_output=True, text=True, check=True)
data = json.loads(result.stdout)
colors = data.get("colors", {})
# Extract dark mode values
return {k: v.get("dark", v) for k, v in colors.items() if isinstance(v, dict)}
except subprocess.CalledProcessError as e:
print(f"Error running matugen: {e}", file=sys.stderr)
return None
except json.JSONDecodeError as e:
print(f"Error parsing matugen output: {e}", file=sys.stderr)
return None
def analyze_vibrant_faithful_muted(image_path: Path) -> None:
"""Analyze vibrant, faithful, and muted mode outputs."""
print("\n" + "=" * 78)
print("VIBRANT vs FAITHFUL vs MUTED COMPARISON")
print("=" * 78)
print()
print("Vibrant: Prioritizes the most saturated colors regardless of area")
print("Faithful: Prioritizes dominant colors by area coverage")
print("Muted: Preserves hue but caps saturation low (monochrome wallpapers)")
print()
print("-" * 78)
print(f"{'Mode':<12} {'Color':<12} {'Hex':<10} {'Hue':>8} {'Chroma':>8} {'Name':<10}")
print("-" * 78)
for scheme in ["vibrant", "faithful", "muted"]:
colors = run_our_processor(image_path, scheme)
if not colors:
print(f"{scheme}: Failed to get colors")
continue
for key in ["primary", "secondary", "tertiary"]:
hex_color = colors.get(key, "")
if not hex_color:
continue
try:
hct = get_hct(hex_color)
name = hue_to_name(hct.hue)
print(f"{scheme:<12} {key:<12} {hex_color:<10} {hct.hue:>7.1f}° {hct.chroma:>7.1f} {name:<10}")
except Exception as e:
print(f"{scheme:<12} {key:<12} Error: {e}")
print("-" * 78)
# Summary comparison
vibrant = run_our_processor(image_path, "vibrant")
faithful = run_our_processor(image_path, "faithful")
muted = run_our_processor(image_path, "muted")
if vibrant and faithful and muted:
print()
print("Summary:")
v_hct = get_hct(vibrant.get("primary", "#000000"))
f_hct = get_hct(faithful.get("primary", "#000000"))
m_hct = get_hct(muted.get("primary", "#000000"))
v_name = hue_to_name(v_hct.hue)
f_name = hue_to_name(f_hct.hue)
m_name = hue_to_name(m_hct.hue)
vf_diff = hue_diff(v_hct.hue, f_hct.hue)
print(f" Vibrant primary: {vibrant.get('primary')} ({v_name}, hue {v_hct.hue:.0f}°, chroma {v_hct.chroma:.1f})")
print(f" Faithful primary: {faithful.get('primary')} ({f_name}, hue {f_hct.hue:.0f}°, chroma {f_hct.chroma:.1f})")
print(f" Muted primary: {muted.get('primary')} ({m_name}, hue {m_hct.hue:.0f}°, chroma {m_hct.chroma:.1f})")
print(f" V-F hue diff: {vf_diff:.1f}°")
if vf_diff > 60:
print(f" → Vibrant/Faithful picked DIFFERENT color families ({v_name} vs {f_name})")
else:
print(f" → Vibrant/Faithful picked SIMILAR colors")
# Note the muted chroma reduction
if m_hct.chroma < 20:
print(f" → Muted successfully reduced chroma to {m_hct.chroma:.1f}")
else:
print(f" → Muted chroma still moderately high ({m_hct.chroma:.1f})")
def compare_m3_schemes(image_path: Path, has_matugen: bool) -> None:
"""Compare all M3 schemes between our processor and matugen."""
schemes = ["tonal-spot", "fruit-salad", "rainbow", "content", "monochrome"]
color_keys = ["primary", "secondary", "tertiary", "surface", "on_surface"]
print("\n" + "=" * 78)
print("M3 SCHEMES" + (" (compared with matugen)" if has_matugen else ""))
print("=" * 78)
if has_matugen:
# Header for comparison mode
print(f"{'Scheme':<12} {'Color':<14} {'Ours':<10} {'Matugen':<10} {'Diff':>10} {'Match':<10}")
print("-" * 78)
for scheme in schemes:
ours = run_our_processor(image_path, scheme)
matugen = run_matugen(image_path, scheme)
if not ours or not matugen:
print(f"{scheme}: Failed to get colors")
continue
for key in color_keys:
our_hex = ours.get(key, "")
mat_hex = matugen.get(key, "")
if not our_hex or not mat_hex:
continue
try:
our_hct = get_hct(our_hex)
mat_hct = get_hct(mat_hex)
avg_chroma = (our_hct.chroma + mat_hct.chroma) / 2
# For low-chroma colors, use RGB distance instead of hue
if avg_chroma < 15:
rgb_dist = rgb_distance(our_hex, mat_hex)
if rgb_dist < 10:
match = "excellent"
elif rgb_dist < 25:
match = "good"
elif rgb_dist < 50:
match = "fair"
else:
match = "poor"
diff_str = f"{rgb_dist:>5.1f} rgb"
else:
diff = hue_diff(our_hct.hue, mat_hct.hue)
if diff < 5:
match = "excellent"
elif diff < 15:
match = "good"
elif diff < 30:
match = "fair"
else:
match = "poor"
diff_str = f"{diff:>5.1f} hue"
print(f"{scheme:<12} {key:<14} {our_hex:<10} {mat_hex:<10} {diff_str:>10} {match:<10}")
except Exception as e:
print(f"{scheme:<12} {key:<14} Error: {e}")
print("-" * 78)
else:
# Header for standalone mode
print(f"{'Scheme':<12} {'Color':<14} {'Hex':<10} {'Hue':>8} {'Chroma':>8} {'Name':<10}")
print("-" * 78)
for scheme in schemes:
ours = run_our_processor(image_path, scheme)
if not ours:
print(f"{scheme}: Failed to get colors")
continue
for key in ["primary", "secondary", "tertiary"]:
our_hex = ours.get(key, "")
if not our_hex:
continue
try:
hct = get_hct(our_hex)
name = hue_to_name(hct.hue)
print(f"{scheme:<12} {key:<14} {our_hex:<10} {hct.hue:>7.1f}° {hct.chroma:>7.1f} {name:<10}")
except Exception as e:
print(f"{scheme:<12} {key:<14} Error: {e}")
print("-" * 78)
def main() -> int:
parser = argparse.ArgumentParser(
description="Analyze Noctalia template-processor color extraction"
)
parser.add_argument(
"wallpaper",
type=Path,
help="Path to wallpaper image"
)
parser.add_argument(
"--no-matugen",
action="store_true",
help="Skip matugen comparison"
)
args = parser.parse_args()
if not args.wallpaper.exists():
print(f"Error: File not found: {args.wallpaper}", file=sys.stderr)
return 1
print(f"\nAnalyzing: {args.wallpaper.name}")
# Check if matugen is available
has_matugen = False
if not args.no_matugen:
try:
subprocess.run(["matugen", "--version"], capture_output=True, check=True)
has_matugen = True
except (subprocess.CalledProcessError, FileNotFoundError):
print("Note: matugen not found, skipping M3 comparison")
# Always show vibrant vs faithful vs muted first (most useful)
analyze_vibrant_faithful_muted(args.wallpaper)
# Then show M3 schemes
compare_m3_schemes(args.wallpaper, has_matugen)
return 0
if __name__ == "__main__":
sys.exit(main())
+3 -2
View File
@@ -249,19 +249,20 @@ def _read_image_imagemagick(path: Path) -> list[RGB]:
# ppm: output as PPM format (easy to parse)
# Resize to 112x112 to match matugen's color extraction
# Use -filter Box for consistent results across ImageMagick versions
resize_spec = "112x112!"
try:
# Try 'magick convert' first (ImageMagick 7+), fallback to 'convert' (ImageMagick 6)
try:
result = subprocess.run(
['magick', 'convert', str(path), '-resize', resize_spec, '-depth', '8', 'ppm:-'],
['magick', 'convert', str(path), '-filter', 'Box', '-resize', resize_spec, '-depth', '8', 'ppm:-'],
capture_output=True,
check=True
)
except FileNotFoundError:
result = subprocess.run(
['convert', str(path), '-resize', resize_spec, '-depth', '8', 'ppm:-'],
['convert', str(path), '-filter', 'Box', '-resize', resize_spec, '-depth', '8', 'ppm:-'],
capture_output=True,
check=True
)
+141
View File
@@ -90,6 +90,34 @@ LIGHT_TONES = {
'inverse_primary': 80,
}
# Monochrome scheme uses different tone values (from material-colors library)
# Primary/tertiary get special treatment for higher contrast in grayscale
MONOCHROME_DARK_TONES = {
**DARK_TONES,
'primary': 100, # White (was 80)
'on_primary': 10, # Near-black (was 20)
'primary_container': 85, # Light gray (was 30)
'on_primary_container': 0, # Black (was 90)
'tertiary': 90, # Light gray (was 80)
'on_tertiary': 10, # Near-black (was 20)
'tertiary_container': 60, # Mid gray (was 30)
'on_tertiary_container': 0, # Black (was 90)
'secondary_container': 30, # Same as normal
}
MONOCHROME_LIGHT_TONES = {
**LIGHT_TONES,
'primary': 0, # Black (was 40)
'on_primary': 90, # Light gray (was 100)
'primary_container': 25, # Dark gray (was 90)
'on_primary_container': 100, # White (was 10)
'tertiary': 25, # Dark gray (was 40)
'on_tertiary': 90, # Light gray (was 100)
'tertiary_container': 49, # Mid gray (was 90)
'on_tertiary_container': 100, # White (was 10)
'secondary_container': 90, # Same as normal
}
# =============================================================================
# Base Scheme Class
@@ -347,6 +375,119 @@ class SchemeContent(_BaseScheme):
self.neutral_variant_palette = TonalPalette(source_color.hue, neutral_variant_chroma)
class SchemeMonochrome(_BaseScheme):
"""
Material Design 3 Monochrome scheme.
All color palettes use chroma=0.0, producing a pure grayscale theme.
Only the error color retains saturation for accessibility.
Uses special tone mappings (different from other M3 schemes) for higher
contrast in grayscale - e.g., primary is tone 100 (white) in dark mode.
Palette configuration:
- Primary: chroma 0.0 (grayscale)
- Secondary: chroma 0.0 (grayscale)
- Tertiary: chroma 0.0 (grayscale)
- Neutral: chroma 0.0 (grayscale)
- Neutral variant: chroma 0.0 (grayscale)
- Error: hue 25°, chroma 84 (vibrant red)
"""
def __init__(self, source_color: Hct):
super().__init__(source_color)
# All palettes use chroma=0 (grayscale)
# Source hue is preserved but irrelevant at chroma 0
self.primary_palette = TonalPalette(source_color.hue, 0.0)
self.secondary_palette = TonalPalette(source_color.hue, 0.0)
self.tertiary_palette = TonalPalette(source_color.hue, 0.0)
self.neutral_palette = TonalPalette(source_color.hue, 0.0)
self.neutral_variant_palette = TonalPalette(source_color.hue, 0.0)
# Error palette keeps vibrant red for accessibility
self.error_palette = TonalPalette(25.0, 84.0)
def _generate_scheme(self, is_dark: bool) -> dict[str, str]:
"""Generate scheme with monochrome-specific tone values."""
# Monochrome uses different tones for higher contrast in grayscale
tones = MONOCHROME_DARK_TONES if is_dark else MONOCHROME_LIGHT_TONES
scheme = {
# Primary colors
'primary': self.primary_palette.get_hex(tones['primary']),
'on_primary': self.primary_palette.get_hex(tones['on_primary']),
'primary_container': self.primary_palette.get_hex(tones['primary_container']),
'on_primary_container': self.primary_palette.get_hex(tones['on_primary_container']),
# Secondary colors
'secondary': self.secondary_palette.get_hex(tones['secondary']),
'on_secondary': self.secondary_palette.get_hex(tones['on_secondary']),
'secondary_container': self.secondary_palette.get_hex(tones['secondary_container']),
'on_secondary_container': self.secondary_palette.get_hex(tones['on_secondary_container']),
# Tertiary colors
'tertiary': self.tertiary_palette.get_hex(tones['tertiary']),
'on_tertiary': self.tertiary_palette.get_hex(tones['on_tertiary']),
'tertiary_container': self.tertiary_palette.get_hex(tones['tertiary_container']),
'on_tertiary_container': self.tertiary_palette.get_hex(tones['on_tertiary_container']),
# Error colors
'error': self.error_palette.get_hex(tones['error']),
'on_error': self.error_palette.get_hex(tones['on_error']),
'error_container': self.error_palette.get_hex(tones['error_container']),
'on_error_container': self.error_palette.get_hex(tones['on_error_container']),
# Surface colors
'surface': self.neutral_palette.get_hex(tones['surface']),
'on_surface': self.neutral_palette.get_hex(tones['on_surface']),
'surface_variant': self.neutral_variant_palette.get_hex(tones['surface_variant']),
'on_surface_variant': self.neutral_variant_palette.get_hex(tones['on_surface_variant']),
# Surface containers
'surface_container_lowest': self.neutral_palette.get_hex(tones['surface_container_lowest']),
'surface_container_low': self.neutral_palette.get_hex(tones['surface_container_low']),
'surface_container': self.neutral_palette.get_hex(tones['surface_container']),
'surface_container_high': self.neutral_palette.get_hex(tones['surface_container_high']),
'surface_container_highest': self.neutral_palette.get_hex(tones['surface_container_highest']),
# Outline and other
'outline': self.neutral_variant_palette.get_hex(tones['outline']),
'outline_variant': self.neutral_variant_palette.get_hex(tones['outline_variant']),
'shadow': self.neutral_palette.get_hex(tones['shadow']),
'scrim': self.neutral_palette.get_hex(tones['scrim']),
# Inverse colors
'inverse_surface': self.neutral_palette.get_hex(tones['inverse_surface']),
'inverse_on_surface': self.neutral_palette.get_hex(tones['inverse_on_surface']),
'inverse_primary': self.primary_palette.get_hex(tones['inverse_primary']),
# Background (same as surface in MD3)
'background': self.neutral_palette.get_hex(tones['surface']),
'on_background': self.neutral_palette.get_hex(tones['on_surface']),
# Surface dim and bright
'surface_dim': self.neutral_palette.get_hex(tones['surface']),
'surface_bright': self.neutral_palette.get_hex(tones['surface_container_highest'] + 5),
# Fixed colors
'primary_fixed': self.primary_palette.get_hex(90),
'primary_fixed_dim': self.primary_palette.get_hex(80),
'on_primary_fixed': self.primary_palette.get_hex(10),
'on_primary_fixed_variant': self.primary_palette.get_hex(30),
'secondary_fixed': self.secondary_palette.get_hex(90),
'secondary_fixed_dim': self.secondary_palette.get_hex(80),
'on_secondary_fixed': self.secondary_palette.get_hex(10),
'on_secondary_fixed_variant': self.secondary_palette.get_hex(30),
'tertiary_fixed': self.tertiary_palette.get_hex(90),
'tertiary_fixed_dim': self.tertiary_palette.get_hex(80),
'on_tertiary_fixed': self.tertiary_palette.get_hex(10),
'on_tertiary_fixed_variant': self.tertiary_palette.get_hex(30),
}
return scheme
# Backward compatibility alias
MaterialScheme = SchemeContent
+141 -11
View File
@@ -175,6 +175,124 @@ def _score_colors_chroma(
return result_colors
def _hue_to_family(hue: float) -> int:
"""
Map hue to perceptual color family.
Uses non-uniform ranges that match human color perception:
- 0: RED (330-30°, wraps around)
- 1: ORANGE (30-60°)
- 2: YELLOW (60-105°)
- 3: GREEN (105-190°, includes green-leaning teal)
- 4: BLUE (190-270°, includes cyan)
- 5: PURPLE (270-330°)
"""
if hue >= 330 or hue < 30:
return 0 # RED
elif hue < 60:
return 1 # ORANGE
elif hue < 105:
return 2 # YELLOW
elif hue < 190:
return 3 # GREEN (includes green-leaning teal)
elif hue < 270:
return 4 # BLUE (includes cyan)
else:
return 5 # PURPLE
def _score_colors_count(
colors_with_counts: list[tuple[RGB, int]],
) -> list[tuple[Color, float]]:
"""
Score colors prioritizing pixel count (area coverage) by hue family.
Groups colors into perceptual hue families, sums counts per family,
then picks the dominant family. This is more faithful to human perception
where we see "green" as a category, not individual shades.
Args:
colors_with_counts: List of (RGB, count) tuples from clustering
Returns:
List of (Color, score) tuples, sorted by family dominance then count
"""
MIN_CHROMA = 10.0 # Filter out near-gray colors
# First pass: collect colorful colors and group by hue family
hue_families: dict[int, list[tuple[Color, float, float, int]]] = {} # family -> [(color, hue, chroma, count), ...]
for rgb, count in colors_with_counts:
color = Color.from_rgb(rgb)
try:
hct = color.to_hct()
if hct.chroma >= MIN_CHROMA:
family = _hue_to_family(hct.hue)
if family not in hue_families:
hue_families[family] = []
hue_families[family].append((color, hct.hue, hct.chroma, count))
except (ValueError, ZeroDivisionError):
pass
# If no colorful colors found, fall back to all colors
if not hue_families:
result = []
for rgb, count in colors_with_counts:
color = Color.from_rgb(rgb)
result.append((color, float(count)))
result.sort(key=lambda x: -x[1])
return result
# Calculate total count per hue family
family_totals: list[tuple[int, int]] = []
for family, colors in hue_families.items():
total = sum(c[3] for c in colors)
family_totals.append((family, total))
# Sort families by total count (dominant family first)
family_totals.sort(key=lambda x: -x[1])
# Build result: colors from dominant families first, sorted by count within each family
result_colors = []
for family, _ in family_totals:
family_colors = hue_families[family]
# Sort by count descending, chroma as tiebreaker
family_colors.sort(key=lambda x: (-x[3], -x[2]))
for color, hue, chroma, count in family_colors:
# Score encodes family rank + count for proper ordering
family_rank = next(i for i, (f, _) in enumerate(family_totals) if f == family)
score = (len(family_totals) - family_rank) * 1000000 + count * 1000 + chroma
result_colors.append((color, score))
result_colors.sort(key=lambda x: -x[1])
return result_colors
def _score_colors_muted(
colors_with_counts: list[tuple[RGB, int]],
) -> list[tuple[Color, float]]:
"""
Score colors for muted mode - pure pixel count without chroma filtering.
Unlike count scoring which filters to chroma >= 10, this accepts all colors
including grayscale. Designed for monochrome/monotonal wallpapers where
the dominant color may have very low or zero saturation.
Args:
colors_with_counts: List of (RGB, count) tuples from clustering
Returns:
List of (Color, score) tuples, sorted by count descending
"""
result = []
for rgb, count in colors_with_counts:
color = Color.from_rgb(rgb)
result.append((color, float(count)))
result.sort(key=lambda x: -x[1])
return result
def _score_colors_population(
colors_with_counts: list[tuple[RGB, int]],
total_pixels: int
@@ -321,7 +439,8 @@ def extract_palette(
scoring: Scoring method:
- "population": matugen-like, representative colors (M3 schemes)
- "chroma": vibrant, chroma-prioritized with centroid averaging
- "chroma-representative": chroma-prioritized with actual pixels (faithful)
- "count": area-dominant, picks by pixel count (faithful mode)
- "muted": like count but without chroma filtering (monochrome wallpapers)
Returns:
List of Color objects, sorted by score
@@ -338,14 +457,20 @@ def extract_palette(
# Don't pre-filter for population scoring - let the Score algorithm filter
# This matches matugen which quantizes all pixels, then filters in scoring
filtered = sampled
elif scoring == "chroma-representative":
# Faithful mode: more clusters, no pre-filtering
# This picks actual dominant colors from the image without averaging
elif scoring == "count":
# Faithful mode: many clusters to capture color diversity, no pre-filtering
# Scoring will filter to colorful colors and pick by count
cluster_count = 48
filtered = sampled # No colorfulness filter - let scoring handle it
filtered = sampled
elif scoring == "muted":
# Muted mode: similar to count but accepts low-chroma colors
# For monochrome/monotonal wallpapers
cluster_count = 24
filtered = sampled
else:
# Vibrant mode: fewer clusters with colorfulness pre-filter
cluster_count = k
# Vibrant mode: more clusters to capture high-chroma colors that might
# otherwise get averaged away, with colorfulness pre-filter
cluster_count = 20
# Filter to colorful pixels for smoother averaged results
filtered = []
for p in sampled:
@@ -364,16 +489,21 @@ def extract_palette(
# Score colors based on method
# - chroma: centroid colors (averaged, smoother - vibrant mode)
# - chroma-representative: representative pixels with chroma scoring (faithful mode)
# - count: representative pixels by area dominance (faithful mode)
# - muted: like count but accepts low/zero chroma (monochrome wallpapers)
# - population: representative colors with Material scoring (M3 schemes)
if scoring == "chroma":
# Use centroid colors for vibrant mode (smoother, blended)
colors_for_scoring = [(c[0], c[2]) for c in clusters]
scored = _score_colors_chroma(colors_for_scoring)
elif scoring == "chroma-representative":
# Use representative colors with chroma scoring (faithful mode)
elif scoring == "count":
# Use representative colors with count scoring (faithful mode)
colors_for_scoring = [(c[1], c[2]) for c in clusters]
scored = _score_colors_chroma(colors_for_scoring)
scored = _score_colors_count(colors_for_scoring)
elif scoring == "muted":
# Use representative colors with muted scoring (no chroma filter)
colors_for_scoring = [(c[1], c[2]) for c in clusters]
scored = _score_colors_muted(colors_for_scoring)
else:
# Use representative colors for M3 schemes
colors_for_scoring = [(c[1], c[2]) for c in clusters]
+369 -4
View File
@@ -9,20 +9,22 @@ Supported scheme types:
- tonal-spot: Default Android 12-13 scheme (recommended)
- fruit-salad: Bold/playful with hue rotation
- rainbow: Chromatic accents with grayscale neutrals
- monochrome: Pure grayscale M3 scheme (chroma = 0)
- vibrant: Prioritizes the most saturated colors regardless of area
- faithful: Prioritizes dominant colors by area coverage
- muted: Preserves hue but caps saturation low (for monochrome wallpapers)
"""
from typing import Literal
from .color import Color, shift_hue, hue_distance, adjust_surface
from .contrast import ensure_contrast
from .material import SchemeTonalSpot, SchemeFruitSalad, SchemeRainbow, SchemeContent
from .material import SchemeTonalSpot, SchemeFruitSalad, SchemeRainbow, SchemeContent, SchemeMonochrome
from .palette import find_error_color
# Type aliases
ThemeMode = Literal["dark", "light"]
SchemeType = Literal["tonal-spot", "fruit-salad", "rainbow", "content", "vibrant", "faithful"]
SchemeType = Literal["tonal-spot", "fruit-salad", "rainbow", "content", "monochrome", "vibrant", "faithful", "muted"]
# Map scheme type strings to classes
SCHEME_CLASSES = {
@@ -30,7 +32,8 @@ SCHEME_CLASSES = {
"fruit-salad": SchemeFruitSalad,
"rainbow": SchemeRainbow,
"content": SchemeContent,
# "vibrant" and "faithful" uses generate_normal_* functions, not a scheme class
"monochrome": SchemeMonochrome,
# "vibrant", "faithful", and "muted" use generate_*_* functions, not a scheme class
}
@@ -484,6 +487,362 @@ def generate_normal_light(palette: list[Color]) -> dict[str, str]:
}
def generate_muted_dark(palette: list[Color]) -> dict[str, str]:
"""
Generate muted dark theme from palette.
Designed for monochrome/monotonal wallpapers - preserves the dominant hue
but caps saturation to very low values for a subtle, understated look.
Outputs same keys as Material for compatibility.
"""
# Use primary color's hue but with very low saturation
primary = palette[0] if palette else Color(128, 128, 128)
primary_h, primary_s, primary_l = primary.to_hsl()
# Derive secondary and tertiary with subtle hue shifts (monochromatic feel)
# Much smaller shifts than normal mode since we want cohesion
secondary = shift_hue(primary, 15)
tertiary = shift_hue(primary, 30)
quaternary = shift_hue(primary, 180)
error = find_error_color(palette)
# Cap saturation low - this is the key difference from normal mode
MUTED_SAT_PRIMARY = 0.15
MUTED_SAT_SECONDARY = 0.12
MUTED_SAT_TERTIARY = 0.10
MUTED_SAT_SURFACE = 0.08
h, s, l = primary.to_hsl()
primary_adjusted = Color.from_hsl(h, min(s, MUTED_SAT_PRIMARY), max(l, 0.65))
h, s, l = secondary.to_hsl()
secondary_adjusted = Color.from_hsl(h, min(s, MUTED_SAT_SECONDARY), max(l, 0.60))
h, s, l = tertiary.to_hsl()
tertiary_adjusted = Color.from_hsl(h, min(s, MUTED_SAT_TERTIARY), max(l, 0.60))
# Container colors - darker, slightly saturated versions
def make_container_dark(base: Color) -> Color:
h, s, l = base.to_hsl()
return Color.from_hsl(h, min(s + 0.05, MUTED_SAT_PRIMARY), max(l - 0.35, 0.15))
primary_container = make_container_dark(primary_adjusted)
secondary_container = make_container_dark(secondary_adjusted)
tertiary_container = make_container_dark(tertiary_adjusted)
error_container = make_container_dark(error)
# Surface: very low saturation, preserving hue for subtle tint
surface_hue = primary_h
base_surface = Color.from_hsl(surface_hue, MUTED_SAT_SURFACE, 0.5)
surface = adjust_surface(base_surface, MUTED_SAT_SURFACE, 0.12)
surface_variant = adjust_surface(base_surface, MUTED_SAT_SURFACE, 0.16)
# Surface containers - progressive lightness with minimal saturation
surface_container_lowest = adjust_surface(base_surface, MUTED_SAT_SURFACE, 0.06)
surface_container_low = adjust_surface(base_surface, MUTED_SAT_SURFACE, 0.10)
surface_container = adjust_surface(base_surface, MUTED_SAT_SURFACE, 0.20)
surface_container_high = adjust_surface(base_surface, MUTED_SAT_SURFACE, 0.18)
surface_container_highest = adjust_surface(base_surface, MUTED_SAT_SURFACE, 0.22)
# Text colors - near-neutral with slight hue tint
base_on_surface = Color.from_hsl(primary_h, 0.03, 0.95)
on_surface = ensure_contrast(base_on_surface, surface, 4.5)
base_on_surface_variant = Color.from_hsl(primary_h, 0.03, 0.80)
on_surface_variant = ensure_contrast(base_on_surface_variant, surface_variant, 4.5)
outline = ensure_contrast(Color.from_hsl(primary_h, 0.05, 0.30), surface, 3.0)
outline_variant = ensure_contrast(Color.from_hsl(primary_h, 0.05, 0.40), surface, 3.0)
# Contrasting foregrounds
dark_fg = Color.from_hsl(primary_h, 0.10, 0.12)
on_primary = ensure_contrast(dark_fg, primary_adjusted, 7.0)
on_secondary = ensure_contrast(dark_fg, secondary_adjusted, 7.0)
on_tertiary = ensure_contrast(dark_fg, tertiary_adjusted, 7.0)
on_error = ensure_contrast(dark_fg, error, 7.0)
# "On" colors for containers
on_primary_container = ensure_contrast(Color.from_hsl(primary_h, 0.05, 0.90), primary_container, 4.5, prefer_light=True)
sec_h, _, _ = secondary.to_hsl()
on_secondary_container = ensure_contrast(Color.from_hsl(sec_h, 0.05, 0.90), secondary_container, 4.5, prefer_light=True)
ter_h, _, _ = tertiary.to_hsl()
on_tertiary_container = ensure_contrast(Color.from_hsl(ter_h, 0.05, 0.90), tertiary_container, 4.5, prefer_light=True)
err_h, _, _ = error.to_hsl()
on_error_container = ensure_contrast(Color.from_hsl(err_h, 0.05, 0.90), error_container, 4.5, prefer_light=True)
# Shadow and scrim
shadow = surface
scrim = Color(0, 0, 0)
# Inverse colors
inverse_surface = Color.from_hsl(primary_h, 0.05, 0.90)
inverse_on_surface = Color.from_hsl(primary_h, 0.03, 0.15)
inverse_primary = Color.from_hsl(primary_h, min(primary_s * 0.5, MUTED_SAT_PRIMARY), 0.40)
# Background aliases
background = surface
on_background = on_surface
# Fixed colors - still muted
def make_fixed_dark(base: Color) -> tuple[Color, Color]:
h, s, _ = base.to_hsl()
fixed = Color.from_hsl(h, min(s, MUTED_SAT_PRIMARY), 0.85)
fixed_dim = Color.from_hsl(h, min(s, MUTED_SAT_PRIMARY), 0.75)
return fixed, fixed_dim
primary_fixed, primary_fixed_dim = make_fixed_dark(primary_adjusted)
secondary_fixed, secondary_fixed_dim = make_fixed_dark(secondary_adjusted)
tertiary_fixed, tertiary_fixed_dim = make_fixed_dark(tertiary_adjusted)
# "On" colors for fixed
on_primary_fixed = ensure_contrast(Color.from_hsl(primary_h, 0.05, 0.15), primary_fixed, 4.5)
on_primary_fixed_variant = ensure_contrast(Color.from_hsl(primary_h, 0.05, 0.20), primary_fixed_dim, 4.5)
on_secondary_fixed = ensure_contrast(Color.from_hsl(sec_h, 0.05, 0.15), secondary_fixed, 4.5)
on_secondary_fixed_variant = ensure_contrast(Color.from_hsl(sec_h, 0.05, 0.20), secondary_fixed_dim, 4.5)
on_tertiary_fixed = ensure_contrast(Color.from_hsl(ter_h, 0.05, 0.15), tertiary_fixed, 4.5)
on_tertiary_fixed_variant = ensure_contrast(Color.from_hsl(ter_h, 0.05, 0.20), tertiary_fixed_dim, 4.5)
# Surface dim and bright
surface_dim = adjust_surface(base_surface, MUTED_SAT_SURFACE, 0.08)
surface_bright = adjust_surface(base_surface, MUTED_SAT_SURFACE, 0.24)
return {
# Primary
"primary": primary_adjusted.to_hex(),
"on_primary": on_primary.to_hex(),
"primary_container": primary_container.to_hex(),
"on_primary_container": on_primary_container.to_hex(),
"primary_fixed": primary_fixed.to_hex(),
"primary_fixed_dim": primary_fixed_dim.to_hex(),
"on_primary_fixed": on_primary_fixed.to_hex(),
"on_primary_fixed_variant": on_primary_fixed_variant.to_hex(),
# Secondary
"secondary": secondary_adjusted.to_hex(),
"on_secondary": on_secondary.to_hex(),
"secondary_container": secondary_container.to_hex(),
"on_secondary_container": on_secondary_container.to_hex(),
"secondary_fixed": secondary_fixed.to_hex(),
"secondary_fixed_dim": secondary_fixed_dim.to_hex(),
"on_secondary_fixed": on_secondary_fixed.to_hex(),
"on_secondary_fixed_variant": on_secondary_fixed_variant.to_hex(),
# Tertiary
"tertiary": tertiary_adjusted.to_hex(),
"on_tertiary": on_tertiary.to_hex(),
"tertiary_container": tertiary_container.to_hex(),
"on_tertiary_container": on_tertiary_container.to_hex(),
"tertiary_fixed": tertiary_fixed.to_hex(),
"tertiary_fixed_dim": tertiary_fixed_dim.to_hex(),
"on_tertiary_fixed": on_tertiary_fixed.to_hex(),
"on_tertiary_fixed_variant": on_tertiary_fixed_variant.to_hex(),
# Error
"error": error.to_hex(),
"on_error": on_error.to_hex(),
"error_container": error_container.to_hex(),
"on_error_container": on_error_container.to_hex(),
# Surface
"surface": surface.to_hex(),
"on_surface": on_surface.to_hex(),
"surface_variant": surface_variant.to_hex(),
"on_surface_variant": on_surface_variant.to_hex(),
"surface_dim": surface_dim.to_hex(),
"surface_bright": surface_bright.to_hex(),
# Surface containers
"surface_container_lowest": surface_container_lowest.to_hex(),
"surface_container_low": surface_container_low.to_hex(),
"surface_container": surface_container.to_hex(),
"surface_container_high": surface_container_high.to_hex(),
"surface_container_highest": surface_container_highest.to_hex(),
# Outline and other
"outline": outline.to_hex(),
"outline_variant": outline_variant.to_hex(),
"shadow": shadow.to_hex(),
"scrim": scrim.to_hex(),
# Inverse
"inverse_surface": inverse_surface.to_hex(),
"inverse_on_surface": inverse_on_surface.to_hex(),
"inverse_primary": inverse_primary.to_hex(),
# Background
"background": background.to_hex(),
"on_background": on_background.to_hex(),
}
def generate_muted_light(palette: list[Color]) -> dict[str, str]:
"""
Generate muted light theme from palette.
Designed for monochrome/monotonal wallpapers - preserves the dominant hue
but caps saturation to very low values for a subtle, understated look.
Outputs same keys as Material for compatibility.
"""
primary = palette[0] if palette else Color(128, 128, 128)
primary_h, primary_s, _ = primary.to_hsl()
# Derive secondary and tertiary with subtle hue shifts
secondary = shift_hue(primary, 15)
tertiary = shift_hue(primary, 30)
quaternary = shift_hue(primary, 180)
error = find_error_color(palette)
# Cap saturation low
MUTED_SAT_PRIMARY = 0.15
MUTED_SAT_SECONDARY = 0.12
MUTED_SAT_TERTIARY = 0.10
MUTED_SAT_SURFACE = 0.08
h, s, l = primary.to_hsl()
primary_adjusted = Color.from_hsl(h, min(s, MUTED_SAT_PRIMARY), min(l, 0.45))
h, s, l = secondary.to_hsl()
secondary_adjusted = Color.from_hsl(h, min(s, MUTED_SAT_SECONDARY), min(l, 0.40))
h, s, l = tertiary.to_hsl()
tertiary_adjusted = Color.from_hsl(h, min(s, MUTED_SAT_TERTIARY), min(l, 0.35))
# Container colors - lighter, less saturated
def make_container_light(base: Color) -> Color:
h, s, l = base.to_hsl()
return Color.from_hsl(h, max(s - 0.05, 0.05), min(l + 0.35, 0.85))
primary_container = make_container_light(primary_adjusted)
secondary_container = make_container_light(secondary_adjusted)
tertiary_container = make_container_light(tertiary_adjusted)
error_container = make_container_light(error)
# Surface: very low saturation, preserving hue for subtle tint
surface = adjust_surface(primary, MUTED_SAT_SURFACE, 0.90)
surface_variant = adjust_surface(primary, MUTED_SAT_SURFACE, 0.78)
# Surface containers - progressive darkening with minimal saturation
surface_container_lowest = adjust_surface(primary, MUTED_SAT_SURFACE, 0.96)
surface_container_low = adjust_surface(primary, MUTED_SAT_SURFACE, 0.92)
surface_container = adjust_surface(primary, MUTED_SAT_SURFACE, 0.86)
surface_container_high = adjust_surface(primary, MUTED_SAT_SURFACE, 0.84)
surface_container_highest = adjust_surface(primary, MUTED_SAT_SURFACE, 0.80)
# Text colors - near-neutral with slight hue tint
base_on_surface = Color.from_hsl(primary_h, 0.03, 0.10)
on_surface = ensure_contrast(base_on_surface, surface, 4.5)
base_on_surface_variant = Color.from_hsl(primary_h, 0.03, 0.90)
on_surface_variant = ensure_contrast(base_on_surface_variant, surface_variant, 4.5)
# Contrasting foregrounds
light_fg = Color.from_hsl(primary_h, 0.05, 0.98)
on_primary = ensure_contrast(light_fg, primary_adjusted, 7.0)
on_secondary = ensure_contrast(light_fg, secondary_adjusted, 7.0)
on_tertiary = ensure_contrast(light_fg, tertiary_adjusted, 7.0)
on_error = ensure_contrast(light_fg, error, 7.0)
# "On" colors for containers
on_primary_container = ensure_contrast(Color.from_hsl(primary_h, 0.05, 0.15), primary_container, 4.5, prefer_light=False)
sec_h, _, _ = secondary.to_hsl()
on_secondary_container = ensure_contrast(Color.from_hsl(sec_h, 0.05, 0.15), secondary_container, 4.5, prefer_light=False)
ter_h, _, _ = tertiary.to_hsl()
on_tertiary_container = ensure_contrast(Color.from_hsl(ter_h, 0.05, 0.15), tertiary_container, 4.5, prefer_light=False)
err_h, _, _ = error.to_hsl()
on_error_container = ensure_contrast(Color.from_hsl(err_h, 0.05, 0.15), error_container, 4.5, prefer_light=False)
# Fixed colors - still muted
def make_fixed_light(base: Color) -> tuple[Color, Color]:
h, s, _ = base.to_hsl()
fixed = Color.from_hsl(h, min(s, MUTED_SAT_PRIMARY), 0.40)
fixed_dim = Color.from_hsl(h, min(s, MUTED_SAT_PRIMARY), 0.30)
return fixed, fixed_dim
primary_fixed, primary_fixed_dim = make_fixed_light(primary_adjusted)
secondary_fixed, secondary_fixed_dim = make_fixed_light(secondary_adjusted)
tertiary_fixed, tertiary_fixed_dim = make_fixed_light(tertiary_adjusted)
# "On" colors for fixed
on_primary_fixed = ensure_contrast(Color.from_hsl(primary_h, 0.05, 0.90), primary_fixed, 4.5)
on_primary_fixed_variant = ensure_contrast(Color.from_hsl(primary_h, 0.05, 0.85), primary_fixed_dim, 4.5)
on_secondary_fixed = ensure_contrast(Color.from_hsl(sec_h, 0.05, 0.90), secondary_fixed, 4.5)
on_secondary_fixed_variant = ensure_contrast(Color.from_hsl(sec_h, 0.05, 0.85), secondary_fixed_dim, 4.5)
on_tertiary_fixed = ensure_contrast(Color.from_hsl(ter_h, 0.05, 0.90), tertiary_fixed, 4.5)
on_tertiary_fixed_variant = ensure_contrast(Color.from_hsl(ter_h, 0.05, 0.85), tertiary_fixed_dim, 4.5)
# Surface dim and bright
surface_dim = adjust_surface(primary, MUTED_SAT_SURFACE, 0.82)
surface_bright = adjust_surface(primary, MUTED_SAT_SURFACE, 0.95)
# Outline
outline = ensure_contrast(Color.from_hsl(primary_h, 0.05, 0.65), surface, 3.0)
outline_variant = ensure_contrast(Color.from_hsl(primary_h, 0.05, 0.75), surface, 3.0)
shadow = Color.from_hsl(primary_h, 0.05, 0.80)
scrim = Color(0, 0, 0)
# Inverse colors
inverse_surface = Color.from_hsl(primary_h, 0.05, 0.15)
inverse_on_surface = Color.from_hsl(primary_h, 0.03, 0.90)
inverse_primary = Color.from_hsl(primary_h, min(primary_s * 0.5, MUTED_SAT_PRIMARY), 0.70)
# Background aliases
background = surface
on_background = on_surface
return {
# Primary
"primary": primary_adjusted.to_hex(),
"on_primary": on_primary.to_hex(),
"primary_container": primary_container.to_hex(),
"on_primary_container": on_primary_container.to_hex(),
"primary_fixed": primary_fixed.to_hex(),
"primary_fixed_dim": primary_fixed_dim.to_hex(),
"on_primary_fixed": on_primary_fixed.to_hex(),
"on_primary_fixed_variant": on_primary_fixed_variant.to_hex(),
# Secondary
"secondary": secondary_adjusted.to_hex(),
"on_secondary": on_secondary.to_hex(),
"secondary_container": secondary_container.to_hex(),
"on_secondary_container": on_secondary_container.to_hex(),
"secondary_fixed": secondary_fixed.to_hex(),
"secondary_fixed_dim": secondary_fixed_dim.to_hex(),
"on_secondary_fixed": on_secondary_fixed.to_hex(),
"on_secondary_fixed_variant": on_secondary_fixed_variant.to_hex(),
# Tertiary
"tertiary": tertiary_adjusted.to_hex(),
"on_tertiary": on_tertiary.to_hex(),
"tertiary_container": tertiary_container.to_hex(),
"on_tertiary_container": on_tertiary_container.to_hex(),
"tertiary_fixed": tertiary_fixed.to_hex(),
"tertiary_fixed_dim": tertiary_fixed_dim.to_hex(),
"on_tertiary_fixed": on_tertiary_fixed.to_hex(),
"on_tertiary_fixed_variant": on_tertiary_fixed_variant.to_hex(),
# Error
"error": error.to_hex(),
"on_error": on_error.to_hex(),
"error_container": error_container.to_hex(),
"on_error_container": on_error_container.to_hex(),
# Surface
"surface": surface.to_hex(),
"on_surface": on_surface.to_hex(),
"surface_variant": surface_variant.to_hex(),
"on_surface_variant": on_surface_variant.to_hex(),
"surface_dim": surface_dim.to_hex(),
"surface_bright": surface_bright.to_hex(),
# Surface containers
"surface_container_lowest": surface_container_lowest.to_hex(),
"surface_container_low": surface_container_low.to_hex(),
"surface_container": surface_container.to_hex(),
"surface_container_high": surface_container_high.to_hex(),
"surface_container_highest": surface_container_highest.to_hex(),
# Outline and other
"outline": outline.to_hex(),
"outline_variant": outline_variant.to_hex(),
"shadow": shadow.to_hex(),
"scrim": scrim.to_hex(),
# Inverse
"inverse_surface": inverse_surface.to_hex(),
"inverse_on_surface": inverse_on_surface.to_hex(),
"inverse_primary": inverse_primary.to_hex(),
# Background
"background": background.to_hex(),
"on_background": on_background.to_hex(),
}
def generate_theme(
palette: list[Color],
mode: ThemeMode,
@@ -495,7 +854,7 @@ def generate_theme(
Args:
palette: List of extracted colors
mode: "dark" or "light"
scheme_type: One of "tonal-spot", "fruit-salad", "rainbow", "vibrant", "faithful"
scheme_type: One of "tonal-spot", "fruit-salad", "rainbow", "vibrant", "faithful", "muted"
Returns:
Dictionary of color token names to hex values
@@ -507,6 +866,12 @@ def generate_theme(
return generate_normal_dark(palette)
return generate_normal_light(palette)
# Handle muted mode (low saturation, monochrome wallpapers)
if scheme_type == "muted":
if mode == "dark":
return generate_muted_dark(palette)
return generate_muted_light(palette)
# All other schemes use Material Design 3 generation
if mode == "dark":
return generate_material_dark(palette, scheme_type)
@@ -9,14 +9,16 @@ Supported scheme types:
- content: Preserves source color's chroma with temperature-based tertiary (matugen default)
- fruit-salad: Bold/playful with -50° hue rotation
- rainbow: Chromatic accents with grayscale neutrals
- monochrome: Pure grayscale M3 scheme (chroma = 0, only error has color)
- vibrant: Prioritizes the most saturated colors regardless of area coverage
- faithful: Prioritizes dominant colors by area, what you see is what you get
- muted: Preserves hue but caps saturation low (for monochrome/monotonal wallpapers)
Usage:
python3 template-processor.py IMAGE_OR_JSON [OPTIONS]
Options:
--scheme-type Scheme type: tonal-spot (default), content, fruit-salad, rainbow, vibrant, faithful
--scheme-type Scheme type: tonal-spot (default), content, fruit-salad, rainbow, monochrome, vibrant, faithful, muted
--dark Generate dark theme only
--light Generate light theme only
--both Generate both themes (default)
@@ -81,7 +83,7 @@ Examples:
# Scheme type selection
parser.add_argument(
'--scheme-type',
choices=['tonal-spot', 'content', 'fruit-salad', 'rainbow', 'vibrant', 'faithful'],
choices=['tonal-spot', 'content', 'fruit-salad', 'rainbow', 'monochrome', 'vibrant', 'faithful', 'muted'],
default='tonal-spot',
help='Color scheme type (default: tonal-spot)'
)
@@ -265,18 +267,18 @@ def main() -> int:
# This matches matugen's color extraction exactly
# - vibrant: Use k-means clustering for colorful/blended colors
# - faithful: Use Wu quantizer for primary (dominant by area), k-means for accents
# - muted: Like count but without chroma filtering (for monochrome wallpapers)
if scheme_type == "vibrant":
# K-means with chroma scoring for vibrant, blended colors
palette = extract_palette(pixels, k=5, scoring="chroma")
elif scheme_type == "faithful":
# Wu quantizer for dominant color (primary), k-means for accent colors
# This ensures primary reflects the most visually prominent area
source_argb = extract_source_color(pixels)
r, g, b = source_color_to_rgb(source_argb)
primary = Color(r, g, b)
# Get additional colors via k-means for secondary/tertiary
additional = extract_palette(pixels, k=4, scoring="chroma-representative")
palette = [primary] + additional[:4]
# K-means with count scoring - picks dominant color by area coverage
# This ensures primary reflects what you actually see in the image
palette = extract_palette(pixels, k=5, scoring="count")
elif scheme_type == "muted":
# K-means with muted scoring - accepts low/zero chroma colors
# For monochrome/monotonal wallpapers where dominant color has low saturation
palette = extract_palette(pixels, k=5, scoring="muted")
else:
# Wu quantizer + Score algorithm (matches matugen)
source_argb = extract_source_color(pixels)
+10 -1
View File
@@ -59,6 +59,13 @@ Item {
sendSocketCommand(niriCommandSocket, "Windows");
}
Timer {
id: workspaceUpdateTimer
interval: 50
repeat: false
onTriggered: updateWorkspaces()
}
function queryDisplayScales() {
sendSocketCommand(niriCommandSocket, "Outputs");
}
@@ -186,7 +193,7 @@ Item {
} else if (event.WindowsChanged) {
handleWindowsChanged(event.WindowsChanged);
} else if (event.WorkspaceActivated) {
updateWorkspaces();
workspaceUpdateTimer.restart();
} else if (event.WindowFocusChanged) {
handleWindowFocusChanged(event.WindowFocusChanged);
} else if (event.WindowLayoutsChanged) {
@@ -328,6 +335,7 @@ Item {
}
windowListChanged();
workspaceUpdateTimer.restart();
} catch (e) {
Logger.e("NiriService", "Error handling WindowOpenedOrChanged:", e);
}
@@ -348,6 +356,7 @@ Item {
windows.splice(windowIndex, 1);
windowListChanged();
workspaceUpdateTimer.restart();
}
} catch (e) {
Logger.e("NiriService", "Error handling WindowClosed:", e);
+30
View File
@@ -158,6 +158,26 @@ Item {
}
});
}
function command() {
root.screenDetector.withCurrentScreen(screen => {
var launcherPanel = PanelService.getPanel("launcherPanel", screen);
if (!launcherPanel)
return;
var searchText = launcherPanel.searchText || "";
var isInClipMode = searchText.startsWith(">cmd");
if (!launcherPanel.isPanelOpen) {
// Closed -> open in clipboard mode
launcherPanel.open();
launcherPanel.setSearchText(">cmd ");
} else if (isInClipMode) {
// Already in clipboard mode -> close
launcherPanel.close();
} else {
// In another mode -> switch to clipboard mode
launcherPanel.setSearchText(">cmd ");
}
});
}
function emoji() {
root.screenDetector.withCurrentScreen(screen => {
var launcherPanel = PanelService.getPanel("launcherPanel", screen);
@@ -617,4 +637,14 @@ Item {
Settings.data.location.name = name;
}
}
IpcHandler {
target: "systemMonitor"
function toggle() {
root.screenDetector.withCurrentScreen(screen => {
var panel = PanelService.getPanel("systemStatsPanel", screen);
panel?.toggle(null, "SystemMonitor");
});
}
}
}
+11 -8
View File
@@ -60,6 +60,13 @@ Singleton {
return primaryDevice.state !== undefined && primaryDevice.state === UPowerDeviceState.Charging;
}
readonly property bool batteryPluggedIn: {
if (!primaryDevice || !isLaptopBattery) {
return false;
}
return primaryDevice.state !== undefined && (primaryDevice.state === UPowerDeviceState.FullyCharged || primaryDevice.state === UPowerDeviceState.PendingCharge);
}
readonly property bool batteryReady: {
if (!primaryDevice) {
return false;
@@ -83,13 +90,11 @@ Singleton {
property bool healthAvailable: false
property int healthPercent: -1
property string capacityLevel: ""
function refreshHealth() {
if (!isLaptopBattery || !primaryDevice) {
healthAvailable = false;
healthPercent = -1;
capacityLevel = "";
return;
}
healthProcess.running = true;
@@ -98,6 +103,10 @@ Singleton {
Process {
id: healthProcess
command: ["sh", "-c", "upower -i $(upower -e | grep battery | head -n 1) 2>/dev/null | grep -iE 'capacity'"]
environment: ({
"LC_ALL": "C"
})
stdout: SplitParser {
onRead: function (data) {
var line = data.trim();
@@ -110,12 +119,6 @@ Singleton {
root.healthAvailable = true;
Logger.d("Battery", "Health retrieved from CLI:", root.healthPercent + "%");
}
var levelMatch = line.match(/^\s*capacity-level:\s*(.+)/i);
if (levelMatch) {
root.capacityLevel = levelMatch[1].trim();
Logger.d("Battery", "Capacity level:", root.capacityLevel);
}
}
}
}
+4 -4
View File
@@ -198,7 +198,7 @@ Singleton {
// Fetch weather data from Open-Meteo API
function fetchWeatherData(latitude, longitude, errorCallback) {
Logger.d("Location", "Fetching weather from api.open-meteo.com");
var url = "https://api.open-meteo.com/v1/forecast?latitude=" + latitude + "&longitude=" + longitude + "&current_weather=true&current=relativehumidity_2m,surface_pressure&daily=temperature_2m_max,temperature_2m_min,weathercode,sunset,sunrise&timezone=auto";
var url = "https://api.open-meteo.com/v1/forecast?latitude=" + latitude + "&longitude=" + longitude + "&current_weather=true&current=relativehumidity_2m,surface_pressure,is_day&daily=temperature_2m_max,temperature_2m_min,weathercode,sunset,sunrise&timezone=auto";
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function () {
if (xhr.readyState === XMLHttpRequest.DONE) {
@@ -237,11 +237,11 @@ Singleton {
}
// --------------------------------
function weatherSymbolFromCode(code) {
function weatherSymbolFromCode(code, isDay) {
if (code === 0)
return "weather-sun";
return isDay ? "weather-sun" : "weather-moon";
if (code === 1 || code === 2)
return "weather-cloud-sun";
return isDay ? "weather-cloud-sun" : "weather-moon-stars";
if (code === 3)
return "weather-cloud";
if (code >= 45 && code <= 48)
+1 -1
View File
@@ -146,7 +146,7 @@ Singleton {
let controllablePlayers = [];
for (var i = 0; i < finalPlayers.length; i++) {
let player = finalPlayers[i];
if (player && player.canControl) {
if (player && player.canPlay) {
controllablePlayers.push(player);
}
}
-1
View File
@@ -5,7 +5,6 @@ import Quickshell
import Quickshell.Bluetooth
import Quickshell.Io
import "../../Helpers/BluetoothUtils.js" as BluetoothUtils
import "."
import qs.Commons
import qs.Services.UI
+44 -8
View File
@@ -22,12 +22,40 @@ Singleton {
property var pendingWallpaperRequest: null
property var pendingPredefinedRequest: null
readonly property var schemeNameMap: ({
"Noctalia (default)": "Noctalia-default",
"Noctalia (legacy)": "Noctalia-legacy",
"Tokyo Night": "Tokyo-Night",
"Rose Pine": "Rosepine"
})
readonly property var schemeTypes: [
{
"key": "tonal-spot",
"name": "M3-Tonal Spot" // Do not translate
},
{
"key": "content",
"name": "M3-Content" // Do not translate
},
{
"key": "fruit-salad",
"name": "M3-Fruit Salad" // Do not translate
},
{
"key": "rainbow",
"name": "M3-Rainbow" // Do not translate
},
{
"key": "monochrome",
"name": "M3-Monochrome" // Do not translate
},
{
"key": "vibrant",
"name": I18n.tr("common.vibrant")
},
{
"key": "faithful",
"name": I18n.tr("common.faithful")
},
{
"key": "muted",
"name": I18n.tr("common.color-muted")
},
]
// Check if a template is enabled in the activeTemplates array
function isTemplateEnabled(templateId) {
@@ -279,8 +307,8 @@ Singleton {
// Get scheme type, defaulting to tonal-spot if not a recognized value
function getSchemeType() {
const method = Settings.data.colorSchemes.generationMethod;
const validTypes = ["tonal-spot", "content", "fruit-salad", "rainbow", "vibrant", "faithful"];
return validTypes.includes(method) ? method : "tonal-spot";
const validKeys = root.schemeTypes.map(scheme => scheme.key);
return validKeys.includes(method) ? method : "tonal-spot";
}
function buildGenerationScript(content, wallpaper, mode) {
@@ -380,6 +408,7 @@ Singleton {
/**
* Old path: Copy pre-rendered terminal files (backward compatibility)
* Should be removed in late february 2026
*/
function handleTerminalThemesCopy(mode, homeDir) {
const commands = [];
@@ -408,6 +437,13 @@ Singleton {
}
function getTerminalColorsTemplate(terminal, mode) {
const schemeNameMap = ({
"Noctalia (default)": "Noctalia-default",
"Noctalia (legacy)": "Noctalia-legacy",
"Tokyo Night": "Tokyo-Night",
"Rose Pine": "Rosepine"
});
let colorScheme = Settings.data.colorSchemes.predefinedScheme;
colorScheme = schemeNameMap[colorScheme] || colorScheme;
+1 -1
View File
@@ -147,7 +147,7 @@ Singleton {
"id": "discord",
"name": "Discord",
"category": "misc",
"input": "vesktop.css",
"input": "discord.css",
"clients": [
{
"name": "vesktop",
+53 -20
View File
@@ -102,6 +102,9 @@ Singleton {
root.currentBrowsePaths = {};
root.refreshWallpapersList();
}
function onShowHiddenFilesChanged() {
root.refreshWallpapersList();
}
function onUseSolidColorChanged() {
if (Settings.data.wallpaper.useSolidColor) {
var solidPath = root.createSolidColorPath(Settings.data.wallpaper.solidColor.toString());
@@ -488,28 +491,38 @@ Singleton {
// -------------------------------------------------------------------
function getCurrentBrowsePath(screenName) {
if (currentBrowsePaths[screenName] !== undefined) {
return currentBrowsePaths[screenName];
var stored = currentBrowsePaths[screenName];
var root = getMonitorDirectory(screenName);
if (root && stored.startsWith(root)) {
return stored;
}
// Stored path is outside the root directory, reset it
delete currentBrowsePaths[screenName];
}
return getMonitorDirectory(screenName);
}
function setBrowsePath(screenName, path) {
if (!screenName) return;
if (!screenName)
return;
currentBrowsePaths[screenName] = path;
browsePathChanged(screenName, path);
}
function navigateUp(screenName) {
if (!screenName) return;
if (!screenName)
return;
var currentPath = getCurrentBrowsePath(screenName);
var rootPath = getMonitorDirectory(screenName);
// Don't go above the root directory
if (currentPath === rootPath) return;
// Don't navigate if root is invalid or we're already at root
if (!rootPath || currentPath === rootPath)
return;
// Get parent directory
var parentPath = currentPath.replace(/\/[^\/]+\/?$/, "");
if (parentPath === "") parentPath = "/";
if (parentPath === "")
parentPath = rootPath;
// Don't go above root
if (!parentPath.startsWith(rootPath)) {
@@ -520,7 +533,8 @@ Singleton {
}
function navigateToRoot(screenName) {
if (!screenName) return;
if (!screenName)
return;
var rootPath = getMonitorDirectory(screenName);
setBrowsePath(screenName, rootPath);
}
@@ -529,11 +543,17 @@ Singleton {
// callback receives { files: [], directories: [] }
function scanDirectoryWithDirs(screenName, directory, callback) {
if (!directory || directory === "") {
callback({ files: [], directories: [] });
callback({
files: [],
directories: []
});
return;
}
var result = { files: [], directories: [] };
var result = {
files: [],
directories: []
};
var pendingScans = 2;
function checkComplete() {
@@ -547,13 +567,13 @@ Singleton {
}
// Scan for files
_scanDirectoryInternal(screenName, directory, false, false, function(files) {
_scanDirectoryInternal(screenName, directory, false, false, function (files) {
result.files = files;
checkComplete();
});
// Scan for directories
_scanForDirectories(directory, function(dirs) {
_scanForDirectories(directory, function (dirs) {
result.directories = dirs;
checkComplete();
});
@@ -575,14 +595,18 @@ Singleton {
var processObject = Qt.createQmlObject(processString, root, "DirScan");
processObject.exited.connect(function(exitCode) {
processObject.exited.connect(function (exitCode) {
var dirs = [];
if (exitCode === 0) {
var lines = processObject.stdout.text.split('\n');
for (var i = 0; i < lines.length; i++) {
var line = lines[i].trim();
if (line !== '') {
dirs.push(line);
var showHidden = Settings.data.wallpaper.showHiddenFiles;
var name = line.split('/').pop();
if (showHidden || !name.startsWith('.')) {
dirs.push(line);
}
}
}
}
@@ -636,7 +660,8 @@ Singleton {
wallpaperLists[screenName] = [];
wallpaperListChanged(screenName, 0);
}
if (callback) callback([]);
if (callback)
callback([]);
return;
}
@@ -646,10 +671,12 @@ Singleton {
recursiveProcesses[screenName].running = false;
recursiveProcesses[screenName].destroy();
delete recursiveProcesses[screenName];
if (updateList) scanningCount--;
if (updateList)
scanningCount--;
}
if (updateList) scanningCount++;
if (updateList)
scanningCount++;
Logger.i("Wallpaper", "Starting scan for", screenName, "in", directory, "recursive:", recursive);
// Build find command args dynamically from ImageCacheService filters
@@ -690,8 +717,9 @@ Singleton {
recursiveProcesses[screenName] = processObject;
}
var handler = function(exitCode) {
if (updateList) scanningCount--;
var handler = function (exitCode) {
if (updateList)
scanningCount--;
Logger.d("Wallpaper", "Process exited with code", exitCode, "for", screenName);
var files = [];
@@ -700,7 +728,11 @@ Singleton {
for (var i = 0; i < lines.length; i++) {
var line = lines[i].trim();
if (line !== '') {
files.push(line);
var showHidden = Settings.data.wallpaper.showHiddenFiles;
var name = line.split('/').pop();
if (showHidden || !name.startsWith('.')) {
files.push(line);
}
}
}
// Sort files for consistent ordering
@@ -735,7 +767,8 @@ Singleton {
delete recursiveProcesses[screenName];
}
if (callback) callback(files);
if (callback)
callback(files);
processObject.destroy();
};
+10 -7
View File
@@ -15,7 +15,6 @@ Rectangle {
property color textColor: Color.mOnPrimary
property color hoverColor: Color.mHover
property color textHoverColor: Color.mOnHover
property bool enabled: true
property real fontSize: Style.fontSizeM
property int fontWeight: Style.fontWeightSemiBold
property real iconSize: Style.fontSizeL
@@ -52,25 +51,26 @@ Rectangle {
// Appearance
radius: root.buttonRadius
color: {
if (!enabled)
if (!root.enabled)
return outlined ? "transparent" : Qt.lighter(Color.mSurfaceVariant, 1.2);
if (hovered)
if (root.hovered)
return hoverColor;
return outlined ? "transparent" : backgroundColor;
return root.outlined ? "transparent" : root.backgroundColor;
}
border.width: outlined ? Style.borderS : 0
border.color: {
if (!enabled)
if (!root.enabled)
return Color.mOutline;
if (hovered)
if (root.hovered)
return backgroundColor;
return outlined ? backgroundColor : "transparent";
return root.outlined ? root.backgroundColor : "transparent";
}
opacity: enabled ? 1.0 : 0.6
Behavior on color {
enabled: !Color.isTransitioning
ColorAnimation {
duration: Style.animationFast
easing.type: Easing.OutCubic
@@ -78,6 +78,7 @@ Rectangle {
}
Behavior on border.color {
enabled: !Color.isTransitioning
ColorAnimation {
duration: Style.animationFast
easing.type: Easing.OutCubic
@@ -102,6 +103,7 @@ Rectangle {
color: contentColor
Behavior on color {
enabled: !Color.isTransitioning
ColorAnimation {
duration: Style.animationFast
easing.type: Easing.OutCubic
@@ -119,6 +121,7 @@ Rectangle {
color: contentColor
Behavior on color {
enabled: !Color.isTransitioning
ColorAnimation {
duration: Style.animationFast
easing.type: Easing.OutCubic
+22 -3
View File
@@ -2,6 +2,7 @@ import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Services.UI
import qs.Widgets
RowLayout {
@@ -12,19 +13,20 @@ RowLayout {
property string label: ""
property string description: ""
property string tooltip: ""
property var model
property string currentKey: ""
property string placeholder: ""
property var defaultValue: undefined
property string settingsPath: ""
property real baseSize: 1.0
readonly property real preferredHeight: Math.round(Style.baseWidgetSize * 1.1)
readonly property real preferredHeight: Math.round(Style.baseWidgetSize * 1.1 * root.baseSize)
readonly property var comboBox: combo
signal selected(string key)
spacing: Style.marginL
Layout.fillWidth: true
opacity: enabled ? 1.0 : 0.6
// Less strict comparison with != (instead of !==) so it can properly compare int vs string (ex for FPS: 30 and "30")
@@ -159,6 +161,22 @@ RowLayout {
duration: Style.animationFast
}
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.NoButton
onEntered: {
if (root.tooltip != "") {
TooltipService.show(root, root.tooltip);
}
}
onExited: {
if (root.tooltip != "") {
TooltipService.hide();
}
}
}
}
contentItem: NText {
@@ -282,8 +300,9 @@ RowLayout {
anchors.fill: parent
hoverEnabled: true
onContainsMouseChanged: {
if (containsMouse)
if (containsMouse) {
listView.currentIndex = delegateRect.index;
}
}
onClicked: {
var item = root.getItem(delegateRect.index);
+2 -1
View File
@@ -13,7 +13,6 @@ Rectangle {
property string icon
property string tooltipText
property string tooltipDirection: "auto"
property bool enabled: true
property bool allowClickWhenDisabled: false
property bool hovering: false
@@ -42,6 +41,7 @@ Rectangle {
border.width: Style.borderS
Behavior on color {
enabled: !Color.isTransitioning
ColorAnimation {
duration: Style.animationFast
easing.type: Easing.InOutQuad
@@ -58,6 +58,7 @@ Rectangle {
y: Style.pixelAlignCenter(root.height, contentHeight)
Behavior on color {
enabled: !Color.isTransitioning
ColorAnimation {
duration: Style.animationFast
easing.type: Easing.InOutQuad
+2 -1
View File
@@ -14,7 +14,6 @@ Rectangle {
property string icon: ""
property string tooltipText: ""
property string tooltipDirection: "auto"
property bool enabled: true
property bool allowClickWhenDisabled: false
property bool hot: false
@@ -64,6 +63,7 @@ Rectangle {
border.width: Style.borderS
Behavior on color {
enabled: !Color.isTransitioning
ColorAnimation {
duration: Style.animationFast
easing.type: Easing.InOutQuad
@@ -93,6 +93,7 @@ Rectangle {
y: (root.height - height) / 2 + (height - contentHeight) / 2
Behavior on color {
enabled: !Color.isTransitioning
ColorAnimation {
duration: Style.animationFast
easing.type: Easing.InOutQuad
+2 -1
View File
@@ -14,8 +14,9 @@ ColumnLayout {
property string indicatorTooltip: ""
opacity: enabled ? 1.0 : 0.6
spacing: Style.marginXXS
visible: root.label != "" || root.description != ""
Layout.fillWidth: true
RowLayout {
-1
View File
@@ -16,7 +16,6 @@ RowLayout {
property string prefix: ""
property string label: ""
property string description: ""
property bool enabled: true
property bool hovering: false
property int baseSize: Style.baseWidgetSize
property var defaultValue: undefined
+1
View File
@@ -6,6 +6,7 @@ import qs.Widgets
Rectangle {
id: root
objectName: "NTabBar"
// Public properties
property int currentIndex: 0
+2
View File
@@ -34,6 +34,7 @@ Rectangle {
border.width: Style.borderS
Behavior on color {
enabled: !Color.isTransitioning
ColorAnimation {
duration: Style.animationFast
easing.type: Easing.OutCubic
@@ -57,6 +58,7 @@ Rectangle {
verticalAlignment: Text.AlignVCenter
Behavior on color {
enabled: !Color.isTransitioning
ColorAnimation {
duration: Style.animationFast
easing.type: Easing.OutCubic
-1
View File
@@ -11,7 +11,6 @@ ColumnLayout {
property string description: ""
property string inputIconName: ""
property bool readOnly: false
property bool enabled: true
property color labelColor: Color.mOnSurface
property color descriptionColor: Color.mOnSurfaceVariant
property string fontFamily: Settings.data.ui.fontDefault