mirror of
https://github.com/noctalia-dev/noctalia-shell.git
synced 2026-05-11 17:08:27 +08:00
Merge branch 'main' into main
This commit is contained in:
@@ -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 */
|
||||
}
|
||||
@@ -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))))
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "表示モード",
|
||||
|
||||
@@ -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ê",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
@@ -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": {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "Режим перегляду",
|
||||
|
||||
@@ -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": "查看模式",
|
||||
|
||||
@@ -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": "已選取:",
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -83,6 +83,7 @@ Item {
|
||||
border.width: Style.capsuleBorderWidth
|
||||
|
||||
Behavior on color {
|
||||
enabled: !Color.isTransitioning
|
||||
ColorAnimation {
|
||||
duration: Style.animationFast
|
||||
easing.type: Easing.InOutQuad
|
||||
|
||||
@@ -97,6 +97,7 @@ Item {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
Behavior on color {
|
||||
enabled: !Color.isTransitioning
|
||||
ColorAnimation {
|
||||
duration: Style.animationFast
|
||||
easing.type: Easing.InOutQuad
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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,6 +648,124 @@ Rectangle {
|
||||
Layout.preferredHeight: root.itemSize
|
||||
Layout.alignment: Qt.AlignCenter
|
||||
|
||||
// Ensure dragged item is on top
|
||||
z: (root.dragSourceIndex === index) ? 1000 : 1
|
||||
|
||||
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"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: draggableContent
|
||||
width: parent.width
|
||||
height: parent.height
|
||||
anchors.centerIn: dragging ? undefined : parent
|
||||
|
||||
// Visual shifting logic
|
||||
readonly property bool isDragged: root.dragSourceIndex === index
|
||||
property real shiftOffset: 0
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
transform: Translate {
|
||||
x: !root.isVerticalBar ? draggableContent.shiftOffset : 0
|
||||
y: root.isVerticalBar ? draggableContent.shiftOffset : 0
|
||||
|
||||
Behavior on x {
|
||||
NumberAnimation {
|
||||
duration: Style.animationFast
|
||||
easing.type: Easing.OutQuad
|
||||
}
|
||||
}
|
||||
Behavior on y {
|
||||
NumberAnimation {
|
||||
duration: Style.animationFast
|
||||
easing.type: Easing.OutQuad
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
@@ -640,13 +849,31 @@ Rectangle {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
targetList.push(workspaceData);
|
||||
}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
localWorkspaces.append(ws);
|
||||
// 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++;
|
||||
}
|
||||
}
|
||||
}
|
||||
workspaceRepeaterHorizontal.model = localWorkspaces;
|
||||
workspaceRepeaterVertical.model = localWorkspaces;
|
||||
|
||||
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
|
||||
|
||||
@@ -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
@@ -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: {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
} 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"]
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,17 +103,10 @@ ColumnLayout {
|
||||
onToggled: checked => root.valueShowPinnedApps = checked
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
spacing: Style.marginXXS
|
||||
Layout.fillWidth: true
|
||||
|
||||
NLabel {
|
||||
label: I18n.tr("bar.taskbar.icon-scale-label")
|
||||
description: I18n.tr("bar.taskbar.icon-scale-description")
|
||||
}
|
||||
|
||||
NValueSlider {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("bar.taskbar.icon-scale-label")
|
||||
description: I18n.tr("bar.taskbar.icon-scale-description")
|
||||
from: 0.5
|
||||
to: 1
|
||||
stepSize: 0.01
|
||||
@@ -121,7 +114,6 @@ ColumnLayout {
|
||||
onMoved: value => root.valueIconScale = value
|
||||
text: Math.round(root.valueIconScale * 100) + "%"
|
||||
}
|
||||
}
|
||||
|
||||
NToggle {
|
||||
Layout.fillWidth: true
|
||||
@@ -141,18 +133,11 @@ 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
|
||||
@@ -160,7 +145,6 @@ ColumnLayout {
|
||||
onMoved: value => root.valueMaxTaskbarWidth = Math.round(value)
|
||||
text: Math.round(root.valueMaxTaskbarWidth) + "%"
|
||||
}
|
||||
}
|
||||
|
||||
NTextInput {
|
||||
id: titleWidthInput
|
||||
|
||||
@@ -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,6 +526,41 @@ 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
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,10 +149,18 @@ SmartPanel {
|
||||
// When the panel opens, initialize content
|
||||
onOpened: {
|
||||
if (_settingsContent) {
|
||||
if (requestedEntry) {
|
||||
_settingsContent.requestedTab = requestedEntry.tab;
|
||||
_settingsContent.initialize();
|
||||
const entry = requestedEntry;
|
||||
requestedEntry = null;
|
||||
Qt.callLater(() => _settingsContent.navigateToResult(entry));
|
||||
} else {
|
||||
_settingsContent.requestedTab = requestedTab;
|
||||
_settingsContent.initialize();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Scroll functions - delegate to content
|
||||
function scrollDown() {
|
||||
@@ -195,12 +204,26 @@ SmartPanel {
|
||||
}
|
||||
|
||||
function onUpPressed() {
|
||||
if (_settingsContent && _settingsContent.searchText.trim() !== "") {
|
||||
_settingsContent.searchSelectPrevious();
|
||||
} else {
|
||||
scrollUp();
|
||||
}
|
||||
}
|
||||
|
||||
function onDownPressed() {
|
||||
if (_settingsContent && _settingsContent.searchText.trim() !== "") {
|
||||
_settingsContent.searchSelectNext();
|
||||
} else {
|
||||
scrollDown();
|
||||
}
|
||||
}
|
||||
|
||||
function onReturnPressed() {
|
||||
if (_settingsContent && _settingsContent.searchText.trim() !== "") {
|
||||
_settingsContent.searchActivate();
|
||||
}
|
||||
}
|
||||
|
||||
function onPageUpPressed() {
|
||||
scrollPageUp();
|
||||
@@ -211,12 +234,20 @@ SmartPanel {
|
||||
}
|
||||
|
||||
function onCtrlJPressed() {
|
||||
if (_settingsContent && _settingsContent.searchText.trim() !== "") {
|
||||
_settingsContent.searchSelectNext();
|
||||
} else {
|
||||
scrollDown();
|
||||
}
|
||||
}
|
||||
|
||||
function onCtrlKPressed() {
|
||||
if (_settingsContent && _settingsContent.searchText.trim() !== "") {
|
||||
_settingsContent.searchSelectPrevious();
|
||||
} else {
|
||||
scrollUp();
|
||||
}
|
||||
}
|
||||
|
||||
panelContent: Rectangle {
|
||||
id: panelContent
|
||||
|
||||
@@ -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,13 +108,9 @@ ColumnLayout {
|
||||
onToggled: checked => Settings.data.bar.showCapsule = checked
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginXXS
|
||||
visible: Settings.data.bar.showCapsule
|
||||
|
||||
NValueSlider {
|
||||
Layout.fillWidth: true
|
||||
visible: Settings.data.bar.showCapsule
|
||||
label: I18n.tr("panels.bar.appearance-capsule-opacity-label")
|
||||
description: I18n.tr("panels.bar.appearance-capsule-opacity-description")
|
||||
from: 0
|
||||
@@ -125,7 +121,6 @@ ColumnLayout {
|
||||
onMoved: value => Settings.data.bar.capsuleOpacity = value
|
||||
text: Math.floor(Settings.data.bar.capsuleOpacity * 100) + "%"
|
||||
}
|
||||
}
|
||||
|
||||
NToggle {
|
||||
Layout.fillWidth: true
|
||||
@@ -162,9 +157,6 @@ ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginL
|
||||
|
||||
ColumnLayout {
|
||||
spacing: Style.marginXXS
|
||||
|
||||
NValueSlider {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("panels.bar.appearance-margins-vertical")
|
||||
@@ -176,10 +168,6 @@ ColumnLayout {
|
||||
onMoved: value => Settings.data.bar.marginVertical = value
|
||||
text: Settings.data.bar.marginVertical + "px"
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
spacing: Style.marginXXS
|
||||
|
||||
NValueSlider {
|
||||
Layout.fillWidth: true
|
||||
@@ -194,7 +182,6 @@ ColumnLayout {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NToggle {
|
||||
Layout.fillWidth: true
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -51,18 +51,11 @@ 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
|
||||
@@ -71,5 +64,4 @@ ColumnLayout {
|
||||
text: Math.round(Settings.data.general.lockScreenCountdownDuration / 1000) + "s"
|
||||
defaultValue: Settings.getDefaultValue("general.lockScreenCountdownDuration")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,12 +53,8 @@ ColumnLayout {
|
||||
}
|
||||
|
||||
// Sound Volume
|
||||
ColumnLayout {
|
||||
enabled: SoundService.multimediaAvailable && (Settings.data.notifications?.sounds?.enabled ?? false)
|
||||
spacing: Style.marginXXS
|
||||
Layout.fillWidth: true
|
||||
|
||||
NValueSlider {
|
||||
enabled: SoundService.multimediaAvailable && (Settings.data.notifications?.sounds?.enabled ?? false)
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("panels.notifications.sounds-volume-label")
|
||||
description: I18n.tr("panels.notifications.sounds-volume-description")
|
||||
@@ -70,7 +66,6 @@ ColumnLayout {
|
||||
text: Math.round((Settings.data.notifications?.sounds?.volume ?? 0.5) * 100) + "%"
|
||||
defaultValue: Settings.getDefaultValue("notifications.sounds.volume")
|
||||
}
|
||||
}
|
||||
|
||||
// Separate Sounds Toggle
|
||||
NToggle {
|
||||
|
||||
@@ -105,18 +105,11 @@ 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
|
||||
@@ -125,5 +118,4 @@ ColumnLayout {
|
||||
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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Executable
+345
@@ -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()
|
||||
@@ -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())
|
||||
Executable
+318
@@ -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())
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 + "¤t_weather=true¤t=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 + "¤t_weather=true¤t=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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -147,7 +147,7 @@ Singleton {
|
||||
"id": "discord",
|
||||
"name": "Discord",
|
||||
"category": "misc",
|
||||
"input": "vesktop.css",
|
||||
"input": "discord.css",
|
||||
"clients": [
|
||||
{
|
||||
"name": "vesktop",
|
||||
|
||||
@@ -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,17 +595,21 @@ 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 !== '') {
|
||||
var showHidden = Settings.data.wallpaper.showHiddenFiles;
|
||||
var name = line.split('/').pop();
|
||||
if (showHidden || !name.startsWith('.')) {
|
||||
dirs.push(line);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
callback(dirs);
|
||||
processObject.destroy();
|
||||
});
|
||||
@@ -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,9 +728,13 @@ Singleton {
|
||||
for (var i = 0; i < lines.length; i++) {
|
||||
var line = lines[i].trim();
|
||||
if (line !== '') {
|
||||
var showHidden = Settings.data.wallpaper.showHiddenFiles;
|
||||
var name = line.split('/').pop();
|
||||
if (showHidden || !name.startsWith('.')) {
|
||||
files.push(line);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Sort files for consistent ordering
|
||||
files.sort();
|
||||
|
||||
@@ -735,7 +767,8 @@ Singleton {
|
||||
delete recursiveProcesses[screenName];
|
||||
}
|
||||
|
||||
if (callback) callback(files);
|
||||
if (callback)
|
||||
callback(files);
|
||||
processObject.destroy();
|
||||
};
|
||||
|
||||
|
||||
+10
-7
@@ -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
@@ -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,9 +300,10 @@ RowLayout {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
onContainsMouseChanged: {
|
||||
if (containsMouse)
|
||||
if (containsMouse) {
|
||||
listView.currentIndex = delegateRect.index;
|
||||
}
|
||||
}
|
||||
onClicked: {
|
||||
var item = root.getItem(delegateRect.index);
|
||||
if (item && item.key !== undefined) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -6,6 +6,7 @@ import qs.Widgets
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
objectName: "NTabBar"
|
||||
|
||||
// Public properties
|
||||
property int currentIndex: 0
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user