diff --git a/Assets/ColorScheme/Eldritch/Eldritch.json b/Assets/ColorScheme/Eldritch/Eldritch.json
new file mode 100644
index 00000000..3fedfeef
--- /dev/null
+++ b/Assets/ColorScheme/Eldritch/Eldritch.json
@@ -0,0 +1,34 @@
+{
+ "dark": {
+ "mPrimary": "#37f499",
+ "mOnPrimary": "#171928",
+ "mSecondary": "#04d1f9",
+ "mOnSecondary": "#171928",
+ "mTertiary": "#a48cf2",
+ "mOnTertiary": "#171928",
+ "mError": "#f16c75",
+ "mOnError": "#171928",
+ "mSurface": "#212337",
+ "mOnSurface": "#ebfafa",
+ "mSurfaceVariant": "#292e42",
+ "mOnSurfaceVariant": "#ABB4DA",
+ "mOutline": "#3b4261",
+ "mShadow": "#414868"
+ },
+ "light": {
+ "mPrimary": "#37f499",
+ "mOnPrimary": "#171928",
+ "mSecondary": "#04d1f9",
+ "mOnSecondary": "#171928",
+ "mTertiary": "#a48cf2",
+ "mOnTertiary": "#171928",
+ "mError": "#f16c75",
+ "mOnError": "#171928",
+ "mSurface": "#ffffff",
+ "mOnSurface": "#171928",
+ "mSurfaceVariant": "#f2f4f8",
+ "mOnSurfaceVariant": "#3b4261",
+ "mOutline": "#b0b6c3",
+ "mShadow": "#e0e3e8"
+ }
+}
diff --git a/Assets/ColorScheme/Eldritch/terminal/foot/Eldritch-dark b/Assets/ColorScheme/Eldritch/terminal/foot/Eldritch-dark
new file mode 100644
index 00000000..dc0c5b58
--- /dev/null
+++ b/Assets/ColorScheme/Eldritch/terminal/foot/Eldritch-dark
@@ -0,0 +1,23 @@
+[colors]
+foreground=ebfafa
+background=212337
+regular0=21222c
+regular1=f9515d
+regular2=37f499
+regular3=e9f941
+regular4=9071f4
+regular5=f265b5
+regular6=04d1f9
+regular7=ebfafa
+bright0=7081d0
+bright1=f16c75
+bright2=69F8B3
+bright3=f1fc79
+bright4=a48cf2
+bright5=FD92CE
+bright6=66e4fd
+bright7=ffffff
+selection-foreground=ebfafa
+selection-background=bf4f8e
+cursor=37f499 f8f8f2
+
diff --git a/Assets/ColorScheme/Eldritch/terminal/foot/Eldritch-light b/Assets/ColorScheme/Eldritch/terminal/foot/Eldritch-light
new file mode 100644
index 00000000..90602968
--- /dev/null
+++ b/Assets/ColorScheme/Eldritch/terminal/foot/Eldritch-light
@@ -0,0 +1,22 @@
+[colors]
+foreground=212337
+background=ebfafa
+regular0=ebfafa # black → white
+regular1=ba1a1a # red (darker)
+regular2=1a7f4c # green (darker)
+regular3=9e8c13 # yellow (darker)
+regular4=3a3e8c # blue (darker)
+regular5=8c2a6c # magenta (darker)
+regular6=1a6c8c # cyan (darker)
+regular7=212337 # white → black
+bright0=7081d0 # keep as accent
+bright1=d23b3b # lighter red
+bright2=37f499 # original green
+bright3=e9f941 # original yellow
+bright4=9071f4 # original blue
+bright5=f265b5 # original magenta
+bright6=04d1f9 # original cyan
+bright7=212337 # black
+selection-foreground=212337
+selection-background=bf4f8e
+cursor=1a7f4c 212337
diff --git a/Assets/ColorScheme/Eldritch/terminal/ghostty/Eldritch-dark b/Assets/ColorScheme/Eldritch/terminal/ghostty/Eldritch-dark
new file mode 100644
index 00000000..e6b33937
--- /dev/null
+++ b/Assets/ColorScheme/Eldritch/terminal/ghostty/Eldritch-dark
@@ -0,0 +1,21 @@
+palette = 0=#21222c
+palette = 1=#f9515d
+palette = 2=#37f499
+palette = 3=#e9f941
+palette = 4=#9071f4
+palette = 5=#f265b5
+palette = 6=#04d1f9
+palette = 7=#ebfafa
+palette = 8=#7081d0
+palette = 9=#f16c75
+palette = 10=#69F8B3
+palette = 11=#f1fc79
+palette = 12=#a48cf2
+palette = 13=#FD92CE
+palette = 14=#66e4fd
+palette = 15=#ffffff
+background = 212337
+foreground = ebfafa
+cursor-color = 37f499
+selection-background = bf4f8e
+selection-foreground = ebfafa
diff --git a/Assets/ColorScheme/Eldritch/terminal/ghostty/Eldritch-light b/Assets/ColorScheme/Eldritch/terminal/ghostty/Eldritch-light
new file mode 100644
index 00000000..24b6cba5
--- /dev/null
+++ b/Assets/ColorScheme/Eldritch/terminal/ghostty/Eldritch-light
@@ -0,0 +1,22 @@
+palette = 0=#f8f8fa # base background (was dark, now light)
+palette = 1=#d7263d # red (less saturated)
+palette = 2=#1eb980 # green (cooler, lighter)
+palette = 3=#f7c948 # yellow (warmer)
+palette = 4=#5e60ce # blue (lighter)
+palette = 5=#c77dff # magenta (lighter)
+palette = 6=#38a1db # cyan (lighter)
+palette = 7=#21222c # base foreground (was background, now dark)
+palette = 8=#bfc9e3 # bright black (light gray)
+palette = 9=#f9515d # bright red
+palette = 10=#37f499 # bright green
+palette = 11=#e9f941 # bright yellow
+palette = 12=#9071f4 # bright blue
+palette = 13=#f265b5 # bright magenta
+palette = 14=#04d1f9 # bright cyan
+palette = 15=#21222c # bright white (dark for contrast)
+background = f8f8fa
+foreground = 21222c
+cursor-color = 5e60ce
+selection-background = d7e3fa
+selection-foreground = 21222c
+
diff --git a/Assets/ColorScheme/Eldritch/terminal/kitty/Eldritch-dark.conf b/Assets/ColorScheme/Eldritch/terminal/kitty/Eldritch-dark.conf
new file mode 100644
index 00000000..8decd1c9
--- /dev/null
+++ b/Assets/ColorScheme/Eldritch/terminal/kitty/Eldritch-dark.conf
@@ -0,0 +1,22 @@
+color0 #21222c
+color1 #f9515d
+color2 #37f499
+color3 #e9f941
+color4 #9071f4
+color5 #f265b5
+color6 #04d1f9
+color7 #ebfafa
+color8 #7081d0
+color9 #f16c75
+color10 #69F8B3
+color11 #f1fc79
+color12 #a48cf2
+color13 #FD92CE
+color14 #66e4fd
+color15 #ffffff
+background #212337
+selection_foreground #ebfafa
+cursor #37f499
+cursor_text_color #f8f8f2
+foreground #ebfafa
+selection_background #bf4f8e
diff --git a/Assets/ColorScheme/Eldritch/terminal/kitty/Eldritch-light.conf b/Assets/ColorScheme/Eldritch/terminal/kitty/Eldritch-light.conf
new file mode 100644
index 00000000..eb75e925
--- /dev/null
+++ b/Assets/ColorScheme/Eldritch/terminal/kitty/Eldritch-light.conf
@@ -0,0 +1,22 @@
+color0 #ebfafa
+color1 #f9515d
+color2 #37f499
+color3 #e9f941
+color4 #9071f4
+color5 #f265b5
+color6 #04d1f9
+color7 #212337
+color8 #7081d0
+color9 #f16c75
+color10 #69F8B3
+color11 #f1fc79
+color12 #a48cf2
+color13 #FD92CE
+color14 #66e4fd
+color15 #323449
+background #ebfafa
+selection_foreground #ebfafa
+cursor #212337
+cursor_text_color #ebfafa
+foreground #212337
+selection_background #bf4f8e
diff --git a/Assets/Translations/de.json b/Assets/Translations/de.json
index 6a5bb0bd..ae9d2261 100644
--- a/Assets/Translations/de.json
+++ b/Assets/Translations/de.json
@@ -1322,7 +1322,8 @@
"bottom_left": "Unten links",
"bottom_right": "Unten rechts",
"bottom_center": "Unten mittig",
- "top_center": "Oben mittig"
+ "top_center": "Oben mittig",
+ "center": "Mitte"
},
"quickSettingsStyle": {
"modern": "Modern",
@@ -1622,5 +1623,34 @@
"label": "Letztes Control-Center-Widget entfernt",
"description": "Das Control-Center-Widget wurde aus der Leiste entfernt. Um es erneut über die Leiste zu öffnen, fügen Sie das Widget wieder hinzu. Sie können es auch durch Rechtsklick auf die Leiste öffnen."
}
+ },
+ "setup": {
+ "customize": {
+ "header": "Erlebnis anpassen",
+ "subheader": "Leistenposition, Dichte, Skalierung und mehr einstellen."
+ },
+ "appearance": {
+ "header": "Erscheinungsbild",
+ "subheader": "Dunkelmodus und Farbquellen wählen (Matugen oder vordefiniert)."
+ },
+ "wallpaper": {
+ "header": "Wähle dein Hintergrundbild",
+ "subheader": "Bestimme die Stimmung mit einem schönen Hintergrund.",
+ "select-prompt": "Wähle unten ein Hintergrundbild",
+ "preview-error": "Bild konnte nicht geladen werden",
+ "none-in-dir": "Keine Hintergrundbilder im Verzeichnis gefunden",
+ "no-dir": "Kein Hintergrundbild-Verzeichnis ausgewählt",
+ "no-valid": "Keine gültigen Bilddateien gefunden in: {dir}",
+ "choose-dir": "Wähle ein Verzeichnis mit deinen Hintergrundbildern",
+ "dir": {
+ "label": "Hintergrundbild-Verzeichnis",
+ "description": "Wähle den Ordner mit deinen Hintergrundbildern",
+ "browse": "Ordner auswählen",
+ "select-title": "Hintergrundbild-Ordner wählen"
+ }
+ },
+ "welcome": {
+ "note": "Nur ein paar Grundeinstellungen – alle Optionen findest du in den Einstellungen"
+ }
}
-}
\ No newline at end of file
+}
diff --git a/Assets/Translations/en.json b/Assets/Translations/en.json
index 7f6cf5c7..7ea6c7cc 100644
--- a/Assets/Translations/en.json
+++ b/Assets/Translations/en.json
@@ -1305,7 +1305,8 @@
"bottom_left": "Bottom left",
"bottom_right": "Bottom right",
"bottom_center": "Bottom center",
- "top_center": "Top center"
+ "top_center": "Top center",
+ "center": "Center"
},
"quickSettingsStyle": {
"modern": "Modern",
@@ -1622,5 +1623,34 @@
"lifespan": "Extended lifespan ({percent}%)",
"disabled": "Battery manager disabled"
}
+ },
+ "setup": {
+ "customize": {
+ "header": "Customize your experience",
+ "subheader": "Adjust bar position, density, scaling and more."
+ },
+ "appearance": {
+ "header": "Appearance",
+ "subheader": "Choose dark mode and color sources (Matugen or predefined)."
+ },
+ "wallpaper": {
+ "header": "Choose your wallpaper",
+ "subheader": "Set the mood with a beautiful background.",
+ "select-prompt": "Select a wallpaper below",
+ "preview-error": "Failed to load image",
+ "none-in-dir": "No wallpapers found in directory",
+ "no-dir": "No wallpaper directory selected",
+ "no-valid": "No valid image files found in: {dir}",
+ "choose-dir": "Choose a directory containing your wallpaper images",
+ "dir": {
+ "label": "Wallpaper directory",
+ "description": "Choose the folder containing your wallpapers",
+ "browse": "Browse for wallpaper folder",
+ "select-title": "Select wallpaper folder"
+ }
+ },
+ "welcome": {
+ "note": "Just a few basics to get you started - full options are in Settings"
+ }
}
-}
\ No newline at end of file
+}
diff --git a/Assets/Translations/es.json b/Assets/Translations/es.json
index 9ced10d2..fe04122f 100644
--- a/Assets/Translations/es.json
+++ b/Assets/Translations/es.json
@@ -1305,7 +1305,8 @@
"bottom_left": "Inferior izquierda",
"bottom_right": "Inferior derecha",
"bottom_center": "Inferior central",
- "top_center": "Superior central"
+ "top_center": "Superior central",
+ "center": "Centro"
},
"quickSettingsStyle": {
"modern": "Moderno",
@@ -1622,5 +1623,34 @@
"lifespan": "Vida útil prolongada ({percent}%)",
"disabled": "Administrador de batería deshabilitado"
}
+ },
+ "setup": {
+ "customize": {
+ "header": "Personaliza tu experiencia",
+ "subheader": "Ajusta la posición de la barra, densidad, escala y más."
+ },
+ "appearance": {
+ "header": "Apariencia",
+ "subheader": "Elige modo oscuro y fuentes de color (Matugen o predefinido)."
+ },
+ "wallpaper": {
+ "header": "Elige tu fondo",
+ "subheader": "Define el ambiente con un bonito fondo.",
+ "select-prompt": "Selecciona un fondo abajo",
+ "preview-error": "No se pudo cargar la imagen",
+ "none-in-dir": "No se encontraron fondos en el directorio",
+ "no-dir": "No se seleccionó un directorio de fondos",
+ "no-valid": "No se encontraron imágenes válidas en: {dir}",
+ "choose-dir": "Elige un directorio que contenga tus fondos",
+ "dir": {
+ "label": "Directorio de fondos",
+ "description": "Elige la carpeta que contiene tus fondos",
+ "browse": "Seleccionar carpeta",
+ "select-title": "Seleccionar carpeta de fondos"
+ }
+ },
+ "welcome": {
+ "note": "Solo algunos conceptos básicos para empezar: todas las opciones están en Configuración."
+ }
}
-}
\ No newline at end of file
+}
diff --git a/Assets/Translations/fr.json b/Assets/Translations/fr.json
index 1d2d9568..96d668d2 100644
--- a/Assets/Translations/fr.json
+++ b/Assets/Translations/fr.json
@@ -1305,7 +1305,8 @@
"bottom_left": "En bas à gauche",
"bottom_right": "En bas à droite",
"bottom_center": "En bas au centre",
- "top_center": "En haut au centre"
+ "top_center": "En haut au centre",
+ "center": "Centre"
},
"quickSettingsStyle": {
"modern": "Moderne",
@@ -1622,5 +1623,34 @@
"lifespan": "Durée de vie prolongée ({percent}%)",
"disabled": "Gestionnaire de batterie désactivé"
}
+ },
+ "setup": {
+ "customize": {
+ "header": "Personnaliser votre expérience",
+ "subheader": "Ajustez la position de la barre, la densité, l'échelle et plus encore."
+ },
+ "appearance": {
+ "header": "Apparence",
+ "subheader": "Choisissez le mode sombre et la source des couleurs (Matugen ou prédéfinie)."
+ },
+ "wallpaper": {
+ "header": "Choisissez votre fond d'écran",
+ "subheader": "Définissez l'ambiance avec un joli fond.",
+ "select-prompt": "Sélectionnez un fond ci-dessous",
+ "preview-error": "Échec du chargement de l'image",
+ "none-in-dir": "Aucun fond d'écran trouvé dans le répertoire",
+ "no-dir": "Aucun répertoire de fonds d'écran sélectionné",
+ "no-valid": "Aucun fichier image valide trouvé dans : {dir}",
+ "choose-dir": "Choisissez un répertoire contenant vos fonds d'écran",
+ "dir": {
+ "label": "Répertoire des fonds d'écran",
+ "description": "Choisissez le dossier contenant vos fonds d'écran",
+ "browse": "Parcourir le dossier",
+ "select-title": "Sélectionner le dossier des fonds d'écran"
+ }
+ },
+ "welcome": {
+ "note": "Quelques réglages de base pour démarrer — toutes les options sont dans Paramètres"
+ }
}
-}
\ No newline at end of file
+}
diff --git a/Assets/Translations/pt.json b/Assets/Translations/pt.json
index 118939a8..55121c93 100644
--- a/Assets/Translations/pt.json
+++ b/Assets/Translations/pt.json
@@ -1303,7 +1303,8 @@
"bottom_left": "Inferior esquerdo",
"bottom_right": "Inferior direito",
"bottom_center": "Centro inferior",
- "top_center": "Centro superior"
+ "top_center": "Centro superior",
+ "center": "Centro"
},
"quickSettingsStyle": {
"modern": "Moderno",
@@ -1615,18 +1616,41 @@
"discharging-rate": "Taxa de descarregamento: {rate} W.",
"charging": "Carregando.",
"discharging": "Descarregando.",
- "battery-manager": {
- "title": "Limite da bateria",
- "set-success-desc": "Limite da bateria definido para {percent}%",
- "initial-setup": "Configuração inicial necessária",
- "set-failed": "Falha ao definir o limite da bateria",
- "install-success": "Instalado com sucesso",
- "install-missing": "Arquivos necessários ausentes",
- "install-unsupported": "Sistema não suportado",
- "install-failed": "Falha na instalação",
- "uninstall-setup": "Desinstalando, autenticação necessária",
- "uninstall-success": "Desinstalado com sucesso",
- "uninstall-failed": "Falha na desinstalação"
+ "panel": {
+ "balanced": "Balanceado ({percent}%)",
+ "disabled": "Gerenciador de bateria desativado",
+ "full": "Capacidade máxima ({percent}%)",
+ "lifespan": "Vida útil prolongada ({percent}%)",
+ "title": "Limite de carga"
+ }
+ },
+ "setup": {
+ "customize": {
+ "header": "Personalizar a sua experiência",
+ "subheader": "Ajuste a posição da barra, densidade, escala e mais."
+ },
+ "appearance": {
+ "header": "Aparência",
+ "subheader": "Escolha o modo escuro e as fontes de cores (Matugen ou predefinidas)."
+ },
+ "wallpaper": {
+ "header": "Escolha o seu papel de parede",
+ "subheader": "Defina o ambiente com um belo fundo.",
+ "select-prompt": "Selecione um papel de parede abaixo",
+ "preview-error": "Falha ao carregar a imagem",
+ "none-in-dir": "Nenhum papel de parede encontrado no diretório",
+ "no-dir": "Nenhum diretório de papéis de parede selecionado",
+ "no-valid": "Nenhuma imagem válida encontrada em: {dir}",
+ "choose-dir": "Escolha um diretório contendo seus papéis de parede",
+ "dir": {
+ "label": "Diretório de papéis de parede",
+ "description": "Escolha a pasta que contém seus papéis de parede",
+ "browse": "Procurar pasta",
+ "select-title": "Selecionar pasta de papéis de parede"
+ }
+ },
+ "welcome": {
+ "note": "Apenas alguns ajustes básicos para começar - o restante está em Configurações"
}
}
-}
\ No newline at end of file
+}
diff --git a/Assets/Translations/zh-CN.json b/Assets/Translations/zh-CN.json
index d64db2ff..884d6cc1 100644
--- a/Assets/Translations/zh-CN.json
+++ b/Assets/Translations/zh-CN.json
@@ -1305,7 +1305,8 @@
"bottom_left": "左下角",
"bottom_right": "右下角",
"bottom_center": "底部居中",
- "top_center": "顶部居中"
+ "top_center": "顶部居中",
+ "center": "居中"
},
"quickSettingsStyle": {
"modern": "现代",
@@ -1622,5 +1623,34 @@
"lifespan": "延长寿命 ({percent}%)",
"disabled": "电池管理器已禁用"
}
+ },
+ "setup": {
+ "customize": {
+ "header": "自定义体验",
+ "subheader": "调整状态栏位置、密度、缩放等。"
+ },
+ "appearance": {
+ "header": "外观",
+ "subheader": "选择深色模式与配色来源(Matugen 或预设)。"
+ },
+ "wallpaper": {
+ "header": "选择你的壁纸",
+ "subheader": "用精美壁纸营造氛围。",
+ "select-prompt": "在下方选择一张壁纸",
+ "preview-error": "图片加载失败",
+ "none-in-dir": "目录中未找到壁纸",
+ "no-dir": "未选择壁纸目录",
+ "no-valid": "在 {dir} 中未找到有效图片文件",
+ "choose-dir": "选择包含壁纸图片的目录",
+ "dir": {
+ "label": "壁纸目录",
+ "description": "选择存放壁纸的文件夹",
+ "browse": "浏览文件夹",
+ "select-title": "选择壁纸文件夹"
+ }
+ },
+ "welcome": {
+ "note": "先进行一些基础设置——更多选项可在“设置”中找到"
+ }
}
-}
\ No newline at end of file
+}
diff --git a/Assets/noctalia.svg b/Assets/noctalia.svg
new file mode 100644
index 00000000..10646225
--- /dev/null
+++ b/Assets/noctalia.svg
@@ -0,0 +1,65 @@
+
+
+
+
diff --git a/Assets/settings-default.json b/Assets/settings-default.json
index b8a82259..8d3d6ce8 100644
--- a/Assets/settings-default.json
+++ b/Assets/settings-default.json
@@ -1,5 +1,6 @@
{
- "settingsVersion": 15,
+ "settingsVersion": 16,
+ "setupCompleted": false,
"bar": {
"position": "top",
"backgroundOpacity": 1,
diff --git a/Commons/Settings.qml b/Commons/Settings.qml
index d0308487..22a1c2ff 100644
--- a/Commons/Settings.qml
+++ b/Commons/Settings.qml
@@ -14,6 +14,7 @@ Singleton {
readonly property alias data: adapter
property bool isLoaded: false
property bool directoriesCreated: false
+ property int settingsVersion: 16
// Define our app directories
// Default config directory: ~/.config/noctalia
@@ -100,6 +101,9 @@ Singleton {
// Emit the signal
root.settingsLoaded()
+
+ // Finally, update our local settings version
+ adapter.settingsVersion = settingsVersion
}
}
onLoadFailed: function (error) {
@@ -125,7 +129,8 @@ Singleton {
JsonAdapter {
id: adapter
- property int settingsVersion: 15
+ property int settingsVersion: root.settingsVersion
+ property bool setupCompleted: false
// bar
property JsonObject bar: JsonObject {
diff --git a/Commons/TablerIcons.qml b/Commons/TablerIcons.qml
index 3522e30d..a840d72f 100644
--- a/Commons/TablerIcons.qml
+++ b/Commons/TablerIcons.qml
@@ -128,6 +128,12 @@ Singleton {
"bt-device-watch": "device-watch",
"bt-device-speaker": "device-speaker",
"bt-device-tv": "device-tv",
+ "antenna-bars-1": "antenna-bars-1",
+ "antenna-bars-2": "antenna-bars-2",
+ "antenna-bars-3": "antenna-bars-3",
+ "antenna-bars-4": "antenna-bars-4",
+ "antenna-bars-5": "antenna-bars-5",
+ "antenna-bars-off": "antenna-bars-off",
"noctalia": "noctalia",
"hyprland": "hyprland",
"filepicker-folder": "folder",
diff --git a/Modules/Bar/Bluetooth/BluetoothDevicesList.qml b/Modules/Bar/Bluetooth/BluetoothDevicesList.qml
index 8e144c31..311bfeb4 100644
--- a/Modules/Bar/Bluetooth/BluetoothDevicesList.qml
+++ b/Modules/Bar/Bluetooth/BluetoothDevicesList.qml
@@ -109,7 +109,7 @@ ColumnLayout {
NIcon {
visible: modelData.signalStrength > 0 && !modelData.pairing && !modelData.blocked
- text: BluetoothService.getSignalIcon(modelData)
+ icon: BluetoothService.getSignalIcon(modelData)
pointSize: Style.fontSizeXS
color: getContentColor(Color.mOnSurface)
}
diff --git a/Modules/ControlCenter/ControlCenterPanel.qml b/Modules/ControlCenter/ControlCenterPanel.qml
index 599dddb6..45cf6a9c 100644
--- a/Modules/ControlCenter/ControlCenterPanel.qml
+++ b/Modules/ControlCenter/ControlCenterPanel.qml
@@ -46,8 +46,8 @@ NPanel {
// Positioning
readonly property string controlCenterPosition: Settings.data.controlCenter.position
- panelAnchorHorizontalCenter: controlCenterPosition !== "close_to_bar_button" && controlCenterPosition.endsWith("_center")
- panelAnchorVerticalCenter: false
+ panelAnchorHorizontalCenter: controlCenterPosition !== "close_to_bar_button" && (controlCenterPosition.endsWith("_center") || controlCenterPosition === "center")
+ panelAnchorVerticalCenter: controlCenterPosition === "center"
panelAnchorLeft: controlCenterPosition !== "close_to_bar_button" && controlCenterPosition.endsWith("_left")
panelAnchorRight: controlCenterPosition !== "close_to_bar_button" && controlCenterPosition.endsWith("_right")
panelAnchorBottom: controlCenterPosition !== "close_to_bar_button" && controlCenterPosition.startsWith("bottom_")
diff --git a/Modules/Settings/Tabs/ColorSchemeTab.qml b/Modules/Settings/Tabs/ColorSchemeTab.qml
index b38d521e..969c8769 100644
--- a/Modules/Settings/Tabs/ColorSchemeTab.qml
+++ b/Modules/Settings/Tabs/ColorSchemeTab.qml
@@ -336,7 +336,6 @@ ColumnLayout {
NIcon {
icon: "check"
pointSize: Style.fontSizeXS
- font.weight: Style.fontWeightBold
color: Color.mOnSecondary
anchors.centerIn: parent
}
diff --git a/Modules/Settings/Tabs/ControlCenterTab.qml b/Modules/Settings/Tabs/ControlCenterTab.qml
index 4bf62cfb..5fbbbf9e 100644
--- a/Modules/Settings/Tabs/ControlCenterTab.qml
+++ b/Modules/Settings/Tabs/ControlCenterTab.qml
@@ -143,6 +143,9 @@ ColumnLayout {
}, {
"key": "top_center",
"name": I18n.tr("options.control-center.position.top_center")
+ }, {
+ "key": "center",
+ "name": I18n.tr("options.control-center.position.center")
}]
currentKey: Settings.data.controlCenter.position
onSelected: function (key) {
diff --git a/Modules/SetupWizard/SetupAppearanceStep.qml b/Modules/SetupWizard/SetupAppearanceStep.qml
new file mode 100644
index 00000000..32903902
--- /dev/null
+++ b/Modules/SetupWizard/SetupAppearanceStep.qml
@@ -0,0 +1,557 @@
+import QtQuick
+import QtQuick.Layouts
+import QtQuick.Controls
+import Quickshell.Io
+import qs.Commons
+import qs.Services
+import qs.Widgets
+
+ColumnLayout {
+ id: root
+ spacing: Style.marginM
+
+ function extractSchemeName(path) {
+ var basename = path.split('/').pop()
+ return basename.replace('.json', '')
+ }
+
+ // Cache for scheme colors (mirrors ColorSchemeTab approach)
+ property var schemeColorsCache: ({})
+ property int cacheVersion: 0
+
+ function getSchemeColor(schemeName, key) {
+ try {
+ var mode = Settings.data.colorSchemes.darkMode ? "dark" : "light"
+ var data = schemeColorsCache[schemeName]
+ if (data && data[mode] && data[mode][key])
+ return data[mode][key]
+ } catch (e) {
+
+ }
+ return Color.mSurfaceVariant
+ }
+
+ // Match ColorSchemeTab helpers
+ function schemeLoaded(schemeName, jsonData) {
+ var value = jsonData || {}
+ schemeColorsCache[schemeName] = value
+ cacheVersion++
+ Logger.log("SetupAppearanceStep", `Loaded scheme ${schemeName}`)
+ }
+
+ Connections {
+ target: ColorSchemeService
+ function onSchemesChanged() {
+ Logger.log("SetupAppearanceStep", `Color schemes changed: ${ColorSchemeService.schemes.length}`)
+ schemeColorsCache = {}
+ cacheVersion++
+ }
+ }
+
+ // Beautiful header with icon
+ RowLayout {
+ Layout.fillWidth: true
+ Layout.bottomMargin: Style.marginL
+ spacing: Style.marginM
+
+ Rectangle {
+ width: 40
+ height: 40
+ radius: Style.radiusL
+ color: Color.mSurfaceVariant
+ opacity: 0.6
+
+ NIcon {
+ icon: "palette"
+ pointSize: Style.fontSizeL
+ color: Color.mPrimary
+ anchors.centerIn: parent
+ }
+ }
+
+ ColumnLayout {
+ Layout.fillWidth: true
+ spacing: Style.marginXS
+
+ NText {
+ text: I18n.tr("setup.appearance.header")
+ pointSize: Style.fontSizeXL
+ font.weight: Style.fontWeightBold
+ color: Color.mPrimary
+ }
+
+ NText {
+ text: I18n.tr("setup.appearance.subheader")
+ pointSize: Style.fontSizeM
+ color: Color.mOnSurfaceVariant
+ }
+ }
+ }
+
+ ScrollView {
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+ clip: true
+ contentWidth: availableWidth
+ ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
+ ScrollBar.vertical.policy: ScrollBar.AsNeeded
+
+ ColumnLayout {
+ width: parent.width
+ spacing: Style.marginM
+
+ // Dark Mode Toggle
+ RowLayout {
+ Layout.fillWidth: true
+ spacing: Style.marginM
+
+ Rectangle {
+ width: 28
+ height: 28
+ radius: Style.radiusM
+ color: Color.mSurface
+
+ NIcon {
+ icon: "moon"
+ pointSize: Style.fontSizeL
+ color: Color.mPrimary
+ anchors.centerIn: parent
+ }
+ }
+
+ ColumnLayout {
+ Layout.fillWidth: true
+ spacing: 2
+
+ NText {
+ text: I18n.tr("settings.color-scheme.color-source.dark-mode.label")
+ pointSize: Style.fontSizeL
+ font.weight: Style.fontWeightBold
+ color: Color.mOnSurface
+ }
+
+ NText {
+ text: I18n.tr("settings.color-scheme.color-source.dark-mode.description")
+ pointSize: Style.fontSizeS
+ color: Color.mOnSurfaceVariant
+ wrapMode: Text.WordWrap
+ Layout.fillWidth: true
+ }
+ }
+
+ NToggle {
+ checked: Settings.data.colorSchemes.darkMode
+ onToggled: checked => Settings.data.colorSchemes.darkMode = checked
+ }
+ }
+
+ // Divider
+ Rectangle {
+ Layout.fillWidth: true
+ Layout.preferredHeight: 1
+ color: Color.mOutline
+ opacity: 0.2
+ Layout.topMargin: Style.marginS
+ Layout.bottomMargin: Style.marginS
+ }
+
+ // Wallpaper Colors Toggle
+ RowLayout {
+ Layout.fillWidth: true
+ spacing: Style.marginM
+
+ Rectangle {
+ width: 28
+ height: 28
+ radius: Style.radiusM
+ color: Color.mSurface
+
+ NIcon {
+ icon: "color-picker"
+ pointSize: Style.fontSizeL
+ color: Color.mPrimary
+ anchors.centerIn: parent
+ }
+ }
+
+ ColumnLayout {
+ Layout.fillWidth: true
+ spacing: 2
+
+ NText {
+ text: I18n.tr("settings.color-scheme.color-source.use-wallpaper-colors.label")
+ pointSize: Style.fontSizeL
+ font.weight: Style.fontWeightBold
+ color: Color.mOnSurface
+ }
+
+ NText {
+ text: I18n.tr("settings.color-scheme.color-source.use-wallpaper-colors.description")
+ pointSize: Style.fontSizeS
+ color: Color.mOnSurfaceVariant
+ wrapMode: Text.WordWrap
+ Layout.fillWidth: true
+ }
+ }
+
+ NToggle {
+ enabled: ProgramCheckerService.matugenAvailable
+ opacity: ProgramCheckerService.matugenAvailable ? 1.0 : 0.6
+ checked: Settings.data.colorSchemes.useWallpaperColors && ProgramCheckerService.matugenAvailable
+ onToggled: checked => {
+ if (!ProgramCheckerService.matugenAvailable)
+ return
+ if (checked) {
+ Settings.data.colorSchemes.useWallpaperColors = true
+ AppThemeService.generate()
+ } else {
+ Settings.data.colorSchemes.useWallpaperColors = false
+ if (Settings.data.colorSchemes.predefinedScheme) {
+ ColorSchemeService.applyScheme(Settings.data.colorSchemes.predefinedScheme)
+ }
+ }
+ }
+ }
+ }
+
+ // Matugen not available notice
+ RowLayout {
+ Layout.fillWidth: true
+ spacing: Style.marginS
+ visible: !ProgramCheckerService.matugenAvailable
+
+ Rectangle {
+ width: 28
+ height: 28
+ radius: Style.radiusM
+ color: Color.mSurface
+ NIcon {
+ icon: "alert-triangle"
+ pointSize: Style.fontSizeL
+ color: Color.mPrimary
+ anchors.centerIn: parent
+ }
+ }
+ NText {
+ text: I18n.tr("settings.color-scheme.color-source.use-wallpaper-colors.description")
+ // Reuse description; availability is visually indicated
+ pointSize: Style.fontSizeS
+ color: Color.mOnSurfaceVariant
+ wrapMode: Text.WordWrap
+ Layout.fillWidth: true
+ }
+ }
+
+ // Matugen scheme type (visible when wallpaper colors enabled and matugen available)
+ ColumnLayout {
+ Layout.fillWidth: true
+ spacing: Style.marginM
+ visible: Settings.data.colorSchemes.useWallpaperColors && ProgramCheckerService.matugenAvailable
+
+ RowLayout {
+ Layout.fillWidth: true
+ spacing: Style.marginS
+
+ Rectangle {
+ width: 28
+ height: 28
+ radius: Style.radiusM
+ color: Color.mSurface
+
+ NIcon {
+ icon: "wand"
+ pointSize: Style.fontSizeL
+ color: Color.mPrimary
+ anchors.centerIn: parent
+ }
+ }
+
+ ColumnLayout {
+ Layout.fillWidth: true
+ spacing: 2
+
+ NText {
+ text: I18n.tr("settings.color-scheme.color-source.matugen-scheme-type.label")
+ pointSize: Style.fontSizeL
+ font.weight: Style.fontWeightBold
+ color: Color.mOnSurface
+ }
+
+ NText {
+ text: I18n.tr("settings.color-scheme.color-source.matugen-scheme-type.description")
+ pointSize: Style.fontSizeS
+ color: Color.mOnSurfaceVariant
+ }
+ }
+ }
+
+ // Matugen scheme options styled like bar position buttons
+ GridLayout {
+ Layout.fillWidth: true
+ columns: 2
+ rowSpacing: Style.marginS
+ columnSpacing: Style.marginS
+
+ Repeater {
+ model: [{
+ "key": "scheme-content",
+ "name": "Content"
+ }, {
+ "key": "scheme-expressive",
+ "name": "Expressive"
+ }, {
+ "key": "scheme-fidelity",
+ "name": "Fidelity"
+ }, {
+ "key": "scheme-fruit-salad",
+ "name": "Fruit Salad"
+ }, {
+ "key": "scheme-monochrome",
+ "name": "Monochrome"
+ }, {
+ "key": "scheme-neutral",
+ "name": "Neutral"
+ }, {
+ "key": "scheme-rainbow",
+ "name": "Rainbow"
+ }, {
+ "key": "scheme-tonal-spot",
+ "name": "Tonal Spot"
+ }]
+ delegate: Rectangle {
+ Layout.fillWidth: true
+ Layout.preferredHeight: 48
+ radius: Style.radiusM
+ border.width: 1
+
+ property bool isActive: Settings.data.colorSchemes.matugenSchemeType === modelData.key
+
+ color: (hoverHandler.hovered || isActive) ? Color.mPrimary : Color.mSurfaceVariant
+ border.color: (hoverHandler.hovered || isActive) ? Color.mPrimary : Color.mOutline
+ opacity: (hoverHandler.hovered || isActive) ? 1.0 : 0.8
+
+ NText {
+ text: modelData.name
+ pointSize: Style.fontSizeM
+ font.weight: (hoverHandler.hovered || parent.isActive) ? Style.fontWeightBold : Style.fontWeightMedium
+ color: (hoverHandler.hovered || parent.isActive) ? Color.mOnPrimary : Color.mOnSurface
+ anchors.centerIn: parent
+ }
+
+ HoverHandler {
+ id: hoverHandler
+ }
+ MouseArea {
+ anchors.fill: parent
+ cursorShape: Qt.PointingHandCursor
+ onClicked: {
+ Settings.data.colorSchemes.matugenSchemeType = modelData.key
+ AppThemeService.generate()
+ }
+ }
+
+ Behavior on color {
+ ColorAnimation {
+ duration: Style.animationFast
+ }
+ }
+ Behavior on border.color {
+ ColorAnimation {
+ duration: Style.animationFast
+ }
+ }
+ Behavior on opacity {
+ NumberAnimation {
+ duration: Style.animationFast
+ }
+ }
+ }
+ }
+ }
+ }
+
+ // Divider
+ Rectangle {
+ Layout.fillWidth: true
+ Layout.preferredHeight: 1
+ color: Color.mOutline
+ opacity: 0.2
+ Layout.topMargin: Style.marginS
+ Layout.bottomMargin: Style.marginS
+ visible: !Settings.data.colorSchemes.useWallpaperColors
+ }
+
+ // Predefined schemes section (visible when wallpaper colors disabled)
+ ColumnLayout {
+ Layout.fillWidth: true
+ spacing: Style.marginM
+ visible: !Settings.data.colorSchemes.useWallpaperColors
+
+ RowLayout {
+ Layout.fillWidth: true
+ spacing: Style.marginS
+
+ Rectangle {
+ width: 28
+ height: 28
+ radius: Style.radiusM
+ color: Color.mSurface
+
+ NIcon {
+ icon: "palette"
+ pointSize: Style.fontSizeL
+ color: Color.mPrimary
+ anchors.centerIn: parent
+ }
+ }
+
+ ColumnLayout {
+ Layout.fillWidth: true
+ spacing: 2
+
+ NText {
+ text: I18n.tr("settings.color-scheme.predefined.section.label")
+ pointSize: Style.fontSizeL
+ font.weight: Style.fontWeightBold
+ color: Color.mOnSurface
+ }
+
+ NText {
+ text: I18n.tr("settings.color-scheme.predefined.section.description")
+ pointSize: Style.fontSizeS
+ color: Color.mOnSurfaceVariant
+ }
+ }
+ }
+
+ // Predefined schemes Grid (matches ColorSchemeTab)
+ GridLayout {
+ id: schemesGrid
+ columns: Math.max(2, Math.floor((parent.width - Style.marginM * 2) / 180))
+ rowSpacing: Style.marginM
+ columnSpacing: Style.marginM
+ Layout.fillWidth: true
+
+ Repeater {
+ model: ColorSchemeService.schemes
+
+ delegate: Rectangle {
+ id: schemeItem
+
+ property string schemePath: modelData
+ property string schemeName: root.extractSchemeName(modelData)
+
+ Layout.fillWidth: true
+ Layout.alignment: Qt.AlignHCenter
+ height: 50
+ radius: Style.radiusS
+ color: root.cacheVersion >= 0 ? root.getSchemeColor(schemeName, "mSurface") : root.getSchemeColor(schemeName, "mSurface")
+ border.width: Math.max(1, Style.borderL)
+ border.color: itemMouseArea.containsMouse ? Color.mTertiary : (Settings.data.colorSchemes.predefinedScheme === schemeName ? Color.mSecondary : Color.mOutline)
+
+ RowLayout {
+ anchors.fill: parent
+ anchors.margins: Style.marginL
+ spacing: Style.marginXS
+
+ NText {
+ text: schemeItem.schemeName
+ pointSize: Style.fontSizeS
+ font.weight: Style.fontWeightMedium
+ color: Color.mOnSurface
+ Layout.fillWidth: true
+ elide: Text.ElideRight
+ verticalAlignment: Text.AlignVCenter
+ wrapMode: Text.WordWrap
+ maximumLineCount: 1
+ }
+
+ Rectangle {
+ width: 14
+ height: 14
+ radius: width * 0.5
+ color: root.cacheVersion >= 0 ? (function () {
+ var mode = Settings.data.colorSchemes.darkMode ? "dark" : "light"
+ var cached = root.schemeColorsCache[schemeItem.schemeName]
+ return (cached && cached[mode] && cached[mode].mPrimary) || root.getSchemeColor(schemeItem.schemeName, "mPrimary")
+ })() : Color.mPrimary
+ }
+ Rectangle {
+ width: 14
+ height: 14
+ radius: width * 0.5
+ color: root.cacheVersion >= 0 ? (function () {
+ var mode = Settings.data.colorSchemes.darkMode ? "dark" : "light"
+ var cached = root.schemeColorsCache[schemeItem.schemeName]
+ return (cached && cached[mode] && cached[mode].mSecondary) || root.getSchemeColor(schemeItem.schemeName, "mSecondary")
+ })() : Color.mSecondary
+ }
+ Rectangle {
+ width: 14
+ height: 14
+ radius: width * 0.5
+ color: root.cacheVersion >= 0 ? (function () {
+ var mode = Settings.data.colorSchemes.darkMode ? "dark" : "light"
+ var cached = root.schemeColorsCache[schemeItem.schemeName]
+ return (cached && cached[mode] && cached[mode].mTertiary) || root.getSchemeColor(schemeItem.schemeName, "mTertiary")
+ })() : Color.mTertiary
+ }
+ Rectangle {
+ width: 14
+ height: 14
+ radius: width * 0.5
+ color: root.cacheVersion >= 0 ? (function () {
+ var mode = Settings.data.colorSchemes.darkMode ? "dark" : "light"
+ var cached = root.schemeColorsCache[schemeItem.schemeName]
+ return (cached && cached[mode] && cached[mode].mError) || root.getSchemeColor(schemeItem.schemeName, "mError")
+ })() : Color.mError
+ }
+ }
+
+ MouseArea {
+ id: itemMouseArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: {
+ Settings.data.colorSchemes.useWallpaperColors = false
+ Settings.data.colorSchemes.predefinedScheme = schemeItem.schemeName
+ ColorSchemeService.applyScheme(Settings.data.colorSchemes.predefinedScheme)
+ }
+ }
+ }
+ }
+ }
+ }
+
+ // Bottom spacer
+ Item {
+ Layout.fillWidth: true
+ Layout.preferredHeight: Style.marginL
+ }
+ }
+ }
+
+ // Hidden loader to populate schemeColorsCache from files
+ Item {
+ visible: false
+ Repeater {
+ model: ColorSchemeService.schemes
+ delegate: Item {
+ FileView {
+ path: modelData
+ blockLoading: false
+ onLoaded: {
+ var schemeName = root.extractSchemeName(path)
+ try {
+ var jsonData = JSON.parse(text())
+ root.schemeLoaded(schemeName, jsonData)
+ } catch (e) {
+ root.schemeLoaded(schemeName, null)
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/Modules/SetupWizard/SetupCustomizeStep.qml b/Modules/SetupWizard/SetupCustomizeStep.qml
new file mode 100644
index 00000000..e5e0dcc1
--- /dev/null
+++ b/Modules/SetupWizard/SetupCustomizeStep.qml
@@ -0,0 +1,508 @@
+import QtQuick
+import QtQuick.Layouts
+import QtQuick.Controls
+import Quickshell
+import qs.Commons
+import qs.Services
+import qs.Widgets
+
+ColumnLayout {
+ id: root
+
+ property real selectedScaleRatio: 1.0
+ property string selectedBarPosition: "top"
+ property bool selectedDimDesktop: true
+
+ signal scaleRatioChanged(real ratio)
+ signal barPositionChanged(string position)
+ signal dimDesktopChanged(bool dim)
+
+ spacing: Style.marginM
+
+ // Beautiful header with icon
+ RowLayout {
+ Layout.fillWidth: true
+ Layout.bottomMargin: Style.marginL
+ spacing: Style.marginM
+
+ Rectangle {
+ width: 40
+ height: 40
+ radius: Style.radiusL
+ color: Color.mSurfaceVariant
+ opacity: 0.6
+
+ NIcon {
+ icon: "palette"
+ pointSize: Style.fontSizeL
+ color: Color.mPrimary
+ anchors.centerIn: parent
+ }
+ }
+
+ ColumnLayout {
+ Layout.fillWidth: true
+ spacing: Style.marginXS
+
+ NText {
+ text: I18n.tr("setup.customize.header")
+ pointSize: Style.fontSizeXL
+ font.weight: Style.fontWeightBold
+ color: Color.mPrimary
+ }
+
+ NText {
+ text: I18n.tr("setup.customize.subheader")
+ pointSize: Style.fontSizeM
+ color: Color.mOnSurfaceVariant
+ }
+ }
+ }
+
+ ScrollView {
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+ clip: true
+ contentWidth: availableWidth
+ ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
+ ScrollBar.vertical.policy: ScrollBar.AsNeeded
+
+ ColumnLayout {
+ width: parent.width
+ spacing: Style.marginM
+
+ // Bar Position section
+ ColumnLayout {
+ Layout.fillWidth: true
+ spacing: Style.marginM
+
+ RowLayout {
+ Layout.fillWidth: true
+ spacing: Style.marginS
+
+ Rectangle {
+ width: 28
+ height: 28
+ radius: Style.radiusM
+ color: Color.mSurface
+
+ NIcon {
+ icon: "layout-2"
+ pointSize: Style.fontSizeL
+ color: Color.mPrimary
+ anchors.centerIn: parent
+ }
+ }
+
+ ColumnLayout {
+ Layout.fillWidth: true
+ spacing: 2
+
+ NText {
+ text: I18n.tr("settings.bar.appearance.position.label")
+ pointSize: Style.fontSizeL
+ font.weight: Style.fontWeightBold
+ color: Color.mOnSurface
+ }
+
+ NText {
+ text: I18n.tr("settings.bar.appearance.position.description")
+ pointSize: Style.fontSizeS
+ color: Color.mOnSurfaceVariant
+ }
+ }
+ }
+
+ RowLayout {
+ Layout.fillWidth: true
+ spacing: Style.marginS
+
+ Repeater {
+ model: [{
+ "key": "top",
+ "name": I18n.tr("options.bar.position.top"),
+ "icon": "arrow-up"
+ }, {
+ "key": "bottom",
+ "name": I18n.tr("options.bar.position.bottom"),
+ "icon": "arrow-down"
+ }, {
+ "key": "left",
+ "name": I18n.tr("options.bar.position.left"),
+ "icon": "arrow-left"
+ }, {
+ "key": "right",
+ "name": I18n.tr("options.bar.position.right"),
+ "icon": "arrow-right"
+ }]
+ delegate: Rectangle {
+ Layout.fillWidth: true
+ Layout.preferredHeight: 40
+ radius: Style.radiusM
+ border.width: 1
+
+ property bool isActive: selectedBarPosition === modelData.key
+
+ color: (hoverHandler.hovered || isActive) ? Color.mPrimary : Color.mSurfaceVariant
+ border.color: (hoverHandler.hovered || isActive) ? Color.mPrimary : Color.mOutline
+ opacity: (hoverHandler.hovered || isActive) ? 1.0 : 0.8
+
+ NText {
+ text: modelData.name
+ pointSize: Style.fontSizeM
+ font.weight: (hoverHandler.hovered || parent.isActive) ? Style.fontWeightBold : Style.fontWeightMedium
+ color: (hoverHandler.hovered || parent.isActive) ? Color.mOnPrimary : Color.mOnSurface
+ anchors.centerIn: parent
+ }
+
+ HoverHandler {
+ id: hoverHandler
+ }
+ MouseArea {
+ anchors.fill: parent
+ cursorShape: Qt.PointingHandCursor
+ onClicked: {
+ selectedBarPosition = modelData.key
+ barPositionChanged(modelData.key)
+ }
+ }
+
+ Behavior on color {
+ ColorAnimation {
+ duration: Style.animationFast
+ }
+ }
+ Behavior on border.color {
+ ColorAnimation {
+ duration: Style.animationFast
+ }
+ }
+ Behavior on opacity {
+ NumberAnimation {
+ duration: Style.animationFast
+ }
+ }
+ }
+ }
+ }
+ }
+
+ // Divider
+ Rectangle {
+ Layout.fillWidth: true
+ Layout.preferredHeight: 1
+ color: Color.mOutline
+ opacity: 0.2
+ Layout.topMargin: Style.marginS
+ Layout.bottomMargin: Style.marginS
+ }
+
+ // Dim Desktop section
+ RowLayout {
+ Layout.fillWidth: true
+ spacing: Style.marginM
+
+ Rectangle {
+ width: 32
+ height: 32
+ radius: Style.radiusM
+ color: Color.mSurface
+ NIcon {
+ icon: "moon"
+ pointSize: Style.fontSizeL
+ color: Color.mPrimary
+ anchors.centerIn: parent
+ }
+ }
+
+ ColumnLayout {
+ Layout.fillWidth: true
+ spacing: 2
+ NText {
+ text: I18n.tr("settings.user-interface.dim-desktop.label")
+ pointSize: Style.fontSizeL
+ font.weight: Style.fontWeightBold
+ color: Color.mOnSurface
+ }
+ NText {
+ text: I18n.tr("settings.user-interface.dim-desktop.description")
+ pointSize: Style.fontSizeS
+ color: Color.mOnSurfaceVariant
+ wrapMode: Text.WordWrap
+ Layout.fillWidth: true
+ }
+ }
+
+ NToggle {
+ checked: selectedDimDesktop
+ onToggled: function (checked) {
+ selectedDimDesktop = checked
+ dimDesktopChanged(checked)
+ }
+ }
+ }
+
+ // Divider
+ Rectangle {
+ Layout.fillWidth: true
+ Layout.preferredHeight: 1
+ color: Color.mOutline
+ opacity: 0.2
+ Layout.topMargin: Style.marginS
+ Layout.bottomMargin: Style.marginS
+ }
+
+ // Bar Density section
+ RowLayout {
+ Layout.fillWidth: true
+ spacing: Style.marginM
+
+ Rectangle {
+ width: 32
+ height: 32
+ radius: Style.radiusM
+ color: Color.mSurface
+ NIcon {
+ icon: "minimize"
+ pointSize: Style.fontSizeL
+ color: Color.mPrimary
+ anchors.centerIn: parent
+ }
+ }
+
+ ColumnLayout {
+ Layout.fillWidth: true
+ spacing: 2
+ NText {
+ text: I18n.tr("settings.bar.appearance.density.label")
+ pointSize: Style.fontSizeL
+ font.weight: Style.fontWeightBold
+ color: Color.mOnSurface
+ }
+ NText {
+ text: I18n.tr("settings.bar.appearance.density.description")
+ pointSize: Style.fontSizeS
+ color: Color.mOnSurfaceVariant
+ wrapMode: Text.WordWrap
+ Layout.fillWidth: true
+ }
+ }
+
+ RowLayout {
+ spacing: Style.marginS
+ Repeater {
+ model: [{
+ "key": "mini",
+ "name": I18n.tr("options.bar.density.mini")
+ }, {
+ "key": "compact",
+ "name": I18n.tr("options.bar.density.compact")
+ }, {
+ "key": "default",
+ "name": I18n.tr("options.bar.density.default")
+ }, {
+ "key": "comfortable",
+ "name": I18n.tr("options.bar.density.comfortable")
+ }]
+ delegate: Rectangle {
+ radius: 16
+ border.width: 1
+ Layout.preferredHeight: 32
+ Layout.preferredWidth: Math.max(90, densityText.implicitWidth + Style.marginXL * 2)
+
+ property bool isActive: Settings.data.bar.density === modelData.key
+
+ color: (hoverHandler.hovered || isActive) ? Color.mPrimary : Color.mSurfaceVariant
+ border.color: (hoverHandler.hovered || isActive) ? Color.mPrimary : Color.mOutline
+ opacity: (hoverHandler.hovered || isActive) ? 1.0 : 0.8
+
+ NText {
+ id: densityText
+ text: modelData.name
+ pointSize: Style.fontSizeS
+ font.weight: (hoverHandler.hovered || parent.isActive) ? Style.fontWeightBold : Style.fontWeightMedium
+ color: (hoverHandler.hovered || parent.isActive) ? Color.mOnPrimary : Color.mOnSurface
+ anchors.centerIn: parent
+ }
+
+ HoverHandler {
+ id: hoverHandler
+ }
+ MouseArea {
+ anchors.fill: parent
+ cursorShape: Qt.PointingHandCursor
+ onClicked: {
+ Settings.data.bar.density = modelData.key
+ }
+ }
+
+ Behavior on color {
+ ColorAnimation {
+ duration: Style.animationFast
+ }
+ }
+ Behavior on border.color {
+ ColorAnimation {
+ duration: Style.animationFast
+ }
+ }
+ Behavior on opacity {
+ NumberAnimation {
+ duration: Style.animationFast
+ }
+ }
+ }
+ }
+ }
+ }
+
+ // Divider
+ Rectangle {
+ Layout.fillWidth: true
+ Layout.preferredHeight: 1
+ color: Color.mOutline
+ opacity: 0.2
+ Layout.topMargin: Style.marginS
+ Layout.bottomMargin: Style.marginS
+ }
+
+ // UI Scale section
+ ColumnLayout {
+ Layout.fillWidth: true
+ spacing: Style.marginM
+
+ RowLayout {
+ Layout.fillWidth: true
+ spacing: Style.marginS
+ Rectangle {
+ width: 32
+ height: 32
+ radius: Style.radiusM
+ color: Color.mSurface
+ NIcon {
+ icon: "maximize"
+ pointSize: Style.fontSizeL
+ color: Color.mPrimary
+ anchors.centerIn: parent
+ }
+ }
+ ColumnLayout {
+ Layout.fillWidth: true
+ spacing: 2
+ NText {
+ text: I18n.tr("settings.user-interface.scaling.label")
+ pointSize: Style.fontSizeL
+ font.weight: Style.fontWeightBold
+ color: Color.mOnSurface
+ }
+ NText {
+ text: I18n.tr("settings.user-interface.scaling.description")
+ pointSize: Style.fontSizeS
+ color: Color.mOnSurfaceVariant
+ }
+ }
+ }
+
+ NValueSlider {
+ Layout.fillWidth: true
+ from: 0.8
+ to: 1.2
+ stepSize: 0.05
+ value: selectedScaleRatio
+ onMoved: function (value) {
+ selectedScaleRatio = value
+ scaleRatioChanged(value)
+ }
+ text: Math.floor(selectedScaleRatio * 100) + "%"
+ }
+ }
+
+ // Divider
+ Rectangle {
+ Layout.fillWidth: true
+ Layout.preferredHeight: 1
+ color: Color.mOutline
+ opacity: 0.2
+ Layout.topMargin: Style.marginS
+ Layout.bottomMargin: Style.marginS
+ }
+
+ // Bar Floating toggle
+ RowLayout {
+ Layout.fillWidth: true
+ spacing: Style.marginM
+ Rectangle {
+ width: 32
+ height: 32
+ radius: Style.radiusM
+ color: Color.mSurface
+ NIcon {
+ icon: "layout-2"
+ pointSize: Style.fontSizeL
+ color: Color.mPrimary
+ anchors.centerIn: parent
+ }
+ }
+ ColumnLayout {
+ Layout.fillWidth: true
+ spacing: 2
+ NText {
+ text: I18n.tr("settings.bar.appearance.floating.label")
+ pointSize: Style.fontSizeL
+ font.weight: Style.fontWeightBold
+ color: Color.mOnSurface
+ }
+ NText {
+ text: I18n.tr("settings.bar.appearance.floating.description")
+ pointSize: Style.fontSizeS
+ color: Color.mOnSurfaceVariant
+ wrapMode: Text.WordWrap
+ Layout.fillWidth: true
+ }
+ }
+ NToggle {
+ checked: Settings.data.bar.floating
+ onToggled: function (checked) {
+ Settings.data.bar.floating = checked
+ }
+ }
+ }
+
+ // Divider
+ Rectangle {
+ Layout.fillWidth: true
+ Layout.preferredHeight: 1
+ color: Color.mOutline
+ opacity: 0.2
+ Layout.topMargin: Style.marginS
+ Layout.bottomMargin: Style.marginS
+ }
+
+ // Bar Background Opacity
+ ColumnLayout {
+ Layout.fillWidth: true
+ spacing: Style.marginM
+ NLabel {
+ label: I18n.tr("settings.bar.appearance.background-opacity.label")
+ description: I18n.tr("settings.bar.appearance.background-opacity.description")
+ }
+ NValueSlider {
+ Layout.fillWidth: true
+ from: 0
+ to: 1
+ stepSize: 0.01
+ value: Settings.data.bar.backgroundOpacity
+ onMoved: function (value) {
+ Settings.data.bar.backgroundOpacity = value
+ }
+ text: Math.floor(Settings.data.bar.backgroundOpacity * 100) + "%"
+ }
+ }
+
+ Item {
+ Layout.fillWidth: true
+ Layout.preferredHeight: Style.marginL
+ }
+ }
+ }
+}
diff --git a/Modules/SetupWizard/SetupWallpaperStep.qml b/Modules/SetupWizard/SetupWallpaperStep.qml
new file mode 100644
index 00000000..aa915f72
--- /dev/null
+++ b/Modules/SetupWizard/SetupWallpaperStep.qml
@@ -0,0 +1,508 @@
+import QtQuick
+import QtQuick.Layouts
+import QtQuick.Controls
+import QtQuick.Effects
+import Quickshell
+import Quickshell.Io
+import qs.Commons
+import qs.Services
+import qs.Widgets
+import "../../Helpers/FuzzySort.js" as FuzzySort
+
+ColumnLayout {
+ id: root
+
+ property string selectedDirectory: ""
+ property string selectedWallpaper: ""
+
+ signal directoryChanged(string directory)
+ signal wallpaperChanged(string wallpaper)
+
+ spacing: Style.marginL
+
+ // Beautiful header with icon
+ ColumnLayout {
+ Layout.fillWidth: true
+ Layout.bottomMargin: Style.marginL
+ spacing: Style.marginM
+
+ RowLayout {
+ spacing: Style.marginM
+
+ Rectangle {
+ width: 40
+ height: 40
+ radius: Style.radiusL
+ color: Color.mSurfaceVariant
+ opacity: 0.6
+
+ NIcon {
+ icon: "image"
+ pointSize: Style.fontSizeL
+ color: Color.mPrimary
+ anchors.centerIn: parent
+ }
+ }
+
+ ColumnLayout {
+ spacing: Style.marginXS
+
+ NText {
+ text: I18n.tr("setup.wallpaper.header")
+ pointSize: Style.fontSizeXL
+ font.weight: Style.fontWeightBold
+ color: Color.mPrimary
+ }
+
+ NText {
+ text: I18n.tr("setup.wallpaper.subheader")
+ pointSize: Style.fontSizeM
+ color: Color.mOnSurfaceVariant
+ }
+ }
+ }
+ }
+
+ // Large preview with rounded corners and shadow effect
+ Rectangle {
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+ Layout.minimumHeight: 180
+ color: Color.mSurfaceVariant
+ radius: Style.radiusL
+ border.color: selectedWallpaper !== "" ? Color.mPrimary : Color.mOutline
+ border.width: selectedWallpaper !== "" ? 2 : 1
+ clip: true
+
+ // Mirror WallpaperPanel approach with rounded shader mask
+ NImageCached {
+ id: previewCached
+ anchors.fill: parent
+ anchors.margins: 4
+ maxCacheDimension: 512
+ cacheFolder: Settings.cacheDirImagesWallpapers
+ imagePath: selectedWallpaper !== "" ? "file://" + selectedWallpaper : ""
+ visible: false // used as texture source for the shader
+ }
+
+ ShaderEffect {
+ anchors.fill: parent
+ anchors.margins: 4
+ property var source: ShaderEffectSource {
+ sourceItem: previewCached
+ hideSource: true
+ live: true
+ recursive: false
+ format: ShaderEffectSource.RGBA
+ }
+ property real itemWidth: width
+ property real itemHeight: height
+ property real cornerRadius: Style.radiusL
+ property real imageOpacity: 1.0
+ fragmentShader: Qt.resolvedUrl(Quickshell.shellDir + "/Shaders/qsb/rounded_image.frag.qsb")
+ supportsAtlasTextures: false
+ blending: true
+ }
+
+ // Loading placeholder
+ Rectangle {
+ anchors.fill: parent
+ color: Color.mSurfaceVariant
+ radius: Style.radiusL
+ visible: (previewCached.status === Image.Loading || previewCached.status === Image.Null) && selectedWallpaper !== ""
+
+ NIcon {
+ icon: "image"
+ pointSize: Style.fontSizeXXL
+ color: Color.mOnSurfaceVariant
+ anchors.centerIn: parent
+ }
+ }
+
+ // Error placeholder
+ Rectangle {
+ anchors.fill: parent
+ color: Color.mError
+ opacity: 0.1
+ radius: Style.radiusL
+ visible: previewCached.status === Image.Error && selectedWallpaper !== ""
+
+ ColumnLayout {
+ anchors.centerIn: parent
+ spacing: Style.marginS
+
+ NIcon {
+ icon: "alert-circle"
+ pointSize: Style.fontSizeXXL
+ color: Color.mError
+ Layout.alignment: Qt.AlignHCenter
+ }
+
+ NText {
+ text: I18n.tr("setup.wallpaper.preview-error")
+ pointSize: Style.fontSizeS
+ color: Color.mError
+ Layout.alignment: Qt.AlignHCenter
+ }
+ }
+ }
+
+ NBusyIndicator {
+ anchors.centerIn: parent
+ visible: (previewCached.status === Image.Loading || previewCached.status === Image.Null) && selectedWallpaper !== ""
+ running: visible
+ size: 28
+ }
+
+ ColumnLayout {
+ anchors.centerIn: parent
+ spacing: Style.marginL
+ visible: selectedWallpaper === ""
+ opacity: 0.6
+
+ Rectangle {
+ Layout.alignment: Qt.AlignHCenter
+ width: 64
+ height: 64
+ radius: width / 2
+ color: Color.mPrimary
+ opacity: 0.15
+
+ NIcon {
+ icon: "sparkles"
+ pointSize: Style.fontSizeXXL
+ color: Color.mPrimary
+ anchors.centerIn: parent
+ }
+ }
+
+ NText {
+ text: I18n.tr("setup.wallpaper.select-prompt")
+ pointSize: Style.fontSizeL
+ color: Color.mOnSurfaceVariant
+ Layout.alignment: Qt.AlignHCenter
+ font.weight: Style.fontWeightMedium
+ }
+ }
+
+ Behavior on border.color {
+ ColorAnimation {
+ duration: Style.animationFast
+ }
+ }
+ }
+
+ // Wallpaper gallery strip
+ Item {
+ Layout.fillWidth: true
+ Layout.preferredHeight: 90
+ visible: filteredWallpapers.length > 0
+
+ ScrollView {
+ anchors.fill: parent
+ clip: true
+ ScrollBar.horizontal.policy: ScrollBar.AsNeeded
+ ScrollBar.vertical.policy: ScrollBar.AlwaysOff
+
+ RowLayout {
+ spacing: Style.marginM
+ height: parent.height
+
+ Repeater {
+ model: filteredWallpapers
+ delegate: Rectangle {
+ Layout.preferredWidth: 120
+ Layout.preferredHeight: 80
+ color: Color.mSurface
+ radius: Style.radiusM
+ border.color: selectedWallpaper === modelData ? Color.mPrimary : Color.mOutline
+ border.width: selectedWallpaper === modelData ? 2 : 1
+ clip: true
+
+ // Cached thumbnail (used as shader source)
+ NImageCached {
+ id: thumbCached
+ anchors.fill: parent
+ anchors.margins: 3
+ maxCacheDimension: 256
+ cacheFolder: Settings.cacheDirImagesWallpapers
+ imagePath: "file://" + modelData
+ visible: false
+ }
+
+ ShaderEffect {
+ anchors.fill: parent
+ anchors.margins: 3
+ property var source: ShaderEffectSource {
+ sourceItem: thumbCached
+ hideSource: true
+ live: true
+ recursive: false
+ format: ShaderEffectSource.RGBA
+ }
+ property real itemWidth: width
+ property real itemHeight: height
+ property real cornerRadius: Style.radiusM - 3
+ property real imageOpacity: 1.0
+ fragmentShader: Qt.resolvedUrl(Quickshell.shellDir + "/Shaders/qsb/rounded_image.frag.qsb")
+ supportsAtlasTextures: false
+ blending: true
+ }
+
+ // Loading state
+ Rectangle {
+ anchors.fill: parent
+ color: Color.mSurfaceVariant
+ radius: Style.radiusM
+ visible: thumbCached.status === Image.Loading
+
+ NIcon {
+ icon: "image"
+ pointSize: Style.fontSizeL
+ color: Color.mOnSurfaceVariant
+ anchors.centerIn: parent
+ }
+ }
+
+ // Error state
+ Rectangle {
+ anchors.fill: parent
+ color: Color.mSurfaceVariant
+ radius: Style.radiusM
+ visible: thumbCached.status === Image.Error
+
+ NIcon {
+ icon: "image"
+ pointSize: Style.fontSizeL
+ color: Color.mOnSurfaceVariant
+ anchors.centerIn: parent
+ }
+ }
+
+ NBusyIndicator {
+ anchors.centerIn: parent
+ visible: thumbCached.status === Image.Loading || thumbCached.status === Image.Null
+ running: visible
+ size: 18
+ }
+
+ Rectangle {
+ anchors.fill: parent
+ color: Color.mPrimary
+ opacity: hoverHandler.hovered ? 0.1 : 0
+ radius: Style.radiusM
+ Behavior on opacity {
+ NumberAnimation {
+ duration: Style.animationFast
+ }
+ }
+ }
+
+ Rectangle {
+ visible: selectedWallpaper === modelData
+ anchors.right: parent.right
+ anchors.bottom: parent.bottom
+ anchors.margins: 6
+ width: 24
+ height: 24
+ radius: width / 2
+ color: Color.mPrimary
+
+ NIcon {
+ icon: "check"
+ pointSize: Style.fontSizeS
+ color: Color.mOnPrimary
+ anchors.centerIn: parent
+ }
+ }
+
+ HoverHandler {
+ id: hoverHandler
+ }
+
+ TapHandler {
+ onTapped: {
+ selectedWallpaper = modelData
+ wallpaperChanged(modelData)
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ // Helpful info card
+ Rectangle {
+ Layout.fillWidth: true
+ Layout.preferredHeight: 80
+ color: Color.mSurfaceVariant
+ radius: Style.radiusM
+ opacity: 0.4
+ visible: filteredWallpapers.length === 0
+
+ RowLayout {
+ anchors.fill: parent
+ anchors.margins: Style.marginL
+ spacing: Style.marginM
+
+ NIcon {
+ icon: "folder-open"
+ pointSize: Style.fontSizeL
+ color: Color.mOnSurfaceVariant
+ }
+
+ ColumnLayout {
+ Layout.fillWidth: true
+ spacing: Style.marginXS
+ NText {
+ text: filteredWallpapers.length === 0 && selectedDirectory !== "" ? I18n.tr("setup.wallpaper.none-in-dir") : I18n.tr("setup.wallpaper.no-dir")
+ pointSize: Style.fontSizeM
+ font.weight: Style.fontWeightBold
+ color: Color.mOnSurfaceVariant
+ }
+ NText {
+ text: selectedDirectory !== "" ? I18n.tr("setup.wallpaper.no-valid", {
+ "dir": selectedDirectory
+ }) : I18n.tr("setup.wallpaper.choose-dir")
+ pointSize: Style.fontSizeS
+ color: Color.mOnSurfaceVariant
+ wrapMode: Text.WordWrap
+ Layout.fillWidth: true
+ opacity: 0.8
+ }
+ }
+ }
+ }
+
+ // Directory selection
+ ColumnLayout {
+ Layout.fillWidth: true
+ spacing: Style.marginM
+
+ NTextInputButton {
+ id: wallpaperPathInput
+ label: I18n.tr("setup.wallpaper.dir.label")
+ description: I18n.tr("setup.wallpaper.dir.description")
+ text: selectedDirectory
+ buttonIcon: "folder-open"
+ buttonTooltip: I18n.tr("setup.wallpaper.dir.browse")
+ Layout.fillWidth: true
+ onInputEditingFinished: {
+ selectedDirectory = text
+ directoryChanged(text)
+ }
+ onButtonClicked: directoryPicker.open()
+ }
+ }
+
+ // Internal properties and functions
+ property list wallpapersList: []
+ property list filteredWallpapers: []
+
+ function updateFilteredWallpapers() {
+ filteredWallpapers = wallpapersList
+ }
+
+ function refreshWallpapers() {
+ if (!selectedDirectory || selectedDirectory === "") {
+ wallpapersList = []
+ filteredWallpapers = []
+ return
+ }
+ if (typeof WallpaperService !== "undefined" && WallpaperService.getWallpapersList) {
+ var wallpapers = WallpaperService.getWallpapersList(Screen.name)
+ wallpapersList = wallpapers
+ updateFilteredWallpapers()
+ if (wallpapersList.length > 0 && selectedWallpaper === "") {
+ selectedWallpaper = wallpapersList[0]
+ }
+ } else {
+ readDirectoryImages(selectedDirectory)
+ }
+ }
+
+ function readDirectoryImages(directoryPath) {
+ directoryScanner.command = ["find", directoryPath, "-type", "f", "\\(-iname", "*.jpg", "-o", "-iname", "*.jpeg", "-o", "-iname", "*.png", "-o", "-iname", "*.bmp", "-o", "-iname", "*.webp", "-o", "-iname", "*.svg", "\\)"]
+ directoryScanner.running = true
+ return []
+ }
+
+ onSelectedDirectoryChanged: {
+ if (typeof Settings !== "undefined" && Settings.data && Settings.data.wallpaper) {
+ Settings.data.wallpaper.directory = selectedDirectory
+ }
+ if (typeof WallpaperService !== "undefined" && WallpaperService.refreshWallpapersList) {
+ WallpaperService.refreshWallpapersList()
+ }
+ Qt.callLater(refreshWallpapers)
+ }
+
+ Connections {
+ target: WallpaperService
+ enabled: typeof WallpaperService !== "undefined"
+ function onWallpaperListChanged(screenName, count) {
+ if (screenName === Screen.name) {
+ Qt.callLater(refreshWallpapers)
+ }
+ }
+ }
+
+ Timer {
+ id: initialRefreshTimer
+ interval: 1000
+ running: false
+ repeat: false
+ onTriggered: refreshWallpapers()
+ }
+
+ Component.onCompleted: {
+ if (typeof Settings !== "undefined" && Settings.data && Settings.data.wallpaper && Settings.data.wallpaper.directory) {
+ selectedDirectory = Settings.data.wallpaper.directory
+ } else {
+ selectedDirectory = Quickshell.env("HOME") + "/Pictures/Wallpapers"
+ }
+ if (typeof WallpaperService !== "undefined" && WallpaperService.currentWallpaper) {
+ selectedWallpaper = WallpaperService.currentWallpaper
+ }
+ initialRefreshTimer.start()
+ }
+
+ NFilePicker {
+ id: directoryPicker
+ selectionMode: "folders"
+ title: I18n.tr("setup.wallpaper.dir.select-title")
+ initialPath: selectedDirectory || Quickshell.env("HOME") + "/Pictures"
+ onAccepted: paths => {
+ if (paths.length > 0) {
+ selectedDirectory = paths[0]
+ directoryChanged(paths[0])
+ }
+ }
+ }
+
+ Process {
+ id: directoryScanner
+ command: ["find", "", "-type", "f", "\\(-iname", "*.jpg", "-o", "-iname", "*.jpeg", "-o", "-iname", "*.png", "-o", "-iname", "*.bmp", "-o", "-iname", "*.webp", "-o", "-iname", "*.svg", "\\)"]
+ running: false
+ stdout: StdioCollector {}
+ stderr: StdioCollector {}
+ onExited: function (exitCode) {
+ if (exitCode === 0) {
+ var lines = stdout.text.split('\n')
+ var images = []
+ for (var i = 0; i < lines.length; i++) {
+ var line = lines[i].trim()
+ if (line !== '') {
+ images.push(line)
+ }
+ }
+ wallpapersList = images
+ updateFilteredWallpapers()
+ if (wallpapersList.length > 0 && selectedWallpaper === "") {
+ selectedWallpaper = wallpapersList[0]
+ }
+ }
+ }
+ }
+}
diff --git a/Modules/SetupWizard/SetupWizard.qml b/Modules/SetupWizard/SetupWizard.qml
new file mode 100644
index 00000000..2a7a5c1b
--- /dev/null
+++ b/Modules/SetupWizard/SetupWizard.qml
@@ -0,0 +1,414 @@
+import QtQuick
+import QtQuick.Layouts
+import QtQuick.Controls
+import Quickshell
+import Quickshell.Wayland
+import qs.Commons
+import qs.Services
+import qs.Widgets
+
+NPanel {
+ id: root
+
+ preferredWidth: 520
+ preferredHeight: 600
+ preferredWidthRatio: 0.4
+ preferredHeightRatio: 0.6
+ panelAnchorHorizontalCenter: true
+ panelAnchorVerticalCenter: true
+ panelKeyboardFocus: true
+
+ // Prevent closing during setup
+ backgroundClickEnabled: false
+ draggable: false
+
+ property int currentStep: 0
+ property int totalSteps: 4
+
+ // Setup wizard data
+ property string selectedWallpaperDirectory: Settings.defaultWallpapersDirectory
+ property string selectedWallpaper: ""
+ property real selectedScaleRatio: 1.0
+ property string selectedBarPosition: "top"
+ property bool selectedDimDesktop: true
+
+ panelContent: Component {
+ Item {
+ id: container
+ anchors.fill: parent
+
+ ColumnLayout {
+ id: wizardContent
+ anchors.fill: parent
+ anchors.margins: Style.marginXL
+ spacing: Style.marginL
+
+ // Override ESC key to prevent closing during setup
+ Shortcut {
+ sequences: ["Escape"]
+ enabled: root.active
+ onActivated: {
+
+ // Do nothing - prevent ESC from closing the setup wizard
+ }
+ context: Qt.WindowShortcut
+ }
+
+ // Step content - takes most of the space
+ Item {
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+ Layout.minimumHeight: 300
+
+ StackLayout {
+ id: stepStack
+ anchors.fill: parent
+ currentIndex: currentStep
+
+ // Step 0: Welcome - Beautiful centered design
+ Item {
+ ColumnLayout {
+ anchors.centerIn: parent
+ width: Math.min(parent.width - Style.marginXL * 2, 420)
+ spacing: Style.marginXL
+
+ // Logo with subtle glow effect
+ Item {
+ Layout.fillWidth: true
+ Layout.preferredHeight: 120
+ Layout.alignment: Qt.AlignHCenter
+
+ Rectangle {
+ anchors.centerIn: parent
+ width: 120
+ height: 120
+ radius: width / 2
+ color: Color.mPrimary
+ opacity: 0.08
+ scale: 1.3
+ }
+
+ Image {
+ anchors.centerIn: parent
+ width: 110
+ height: 110
+ source: Qt.resolvedUrl(Quickshell.shellDir + "/Assets/noctalia.svg")
+ fillMode: Image.PreserveAspectFit
+ smooth: true
+
+ Rectangle {
+ anchors.fill: parent
+ color: Color.mSurfaceVariant
+ radius: width / 2
+ border.color: Color.mOutline
+ border.width: 2
+ visible: parent.status === Image.Error
+
+ NIcon {
+ icon: "sparkles"
+ pointSize: Style.fontSizeXXL * 1.5
+ color: Color.mPrimary
+ anchors.centerIn: parent
+ }
+ }
+
+ // Subtle pulse animation
+ SequentialAnimation on scale {
+ running: true
+ loops: Animation.Infinite
+ NumberAnimation {
+ from: 1.0
+ to: 1.05
+ duration: 2000
+ easing.type: Easing.InOutQuad
+ }
+ NumberAnimation {
+ from: 1.05
+ to: 1.0
+ duration: 2000
+ easing.type: Easing.InOutQuad
+ }
+ }
+ }
+ }
+
+ // Welcome text with gradient feel
+ ColumnLayout {
+ Layout.fillWidth: true
+ Layout.alignment: Qt.AlignHCenter
+ spacing: Style.marginM
+
+ NText {
+ text: "Welcome to Noctalia! ✨"
+ pointSize: Style.fontSizeXXL * 1.4
+ font.weight: Style.fontWeightBold
+ color: Color.mOnSurface
+ Layout.fillWidth: true
+ horizontalAlignment: Text.AlignHCenter
+ }
+
+ NText {
+ text: "Let's make your desktop uniquely yours"
+ pointSize: Style.fontSizeL
+ color: Color.mOnSurfaceVariant
+ Layout.fillWidth: true
+ horizontalAlignment: Text.AlignHCenter
+ wrapMode: Text.WordWrap
+ }
+
+ // Friendly subtext
+ Rectangle {
+ Layout.fillWidth: true
+ Layout.topMargin: Style.marginL
+ Layout.preferredHeight: childrenRect.height + Style.marginM * 2
+ color: Color.mSurfaceVariant
+ radius: Style.radiusL
+ opacity: 0.4
+
+ NText {
+ anchors.centerIn: parent
+ width: parent.width - Style.marginL * 2
+ text: I18n.tr("setup.welcome.note")
+ pointSize: Style.fontSizeM
+ color: Color.mOnSurfaceVariant
+ horizontalAlignment: Text.AlignHCenter
+ wrapMode: Text.WordWrap
+ }
+ }
+ }
+ }
+ }
+
+ // Step 1: Wallpaper Setup
+ SetupWallpaperStep {
+ id: step1
+ selectedDirectory: root.selectedWallpaperDirectory
+ selectedWallpaper: root.selectedWallpaper
+ onDirectoryChanged: function (directory) {
+ root.selectedWallpaperDirectory = directory
+ root.applyWallpaperSettings()
+ }
+ onWallpaperChanged: function (wallpaper) {
+ root.selectedWallpaper = wallpaper
+ root.applyWallpaperSettings()
+ }
+ }
+
+ // Step 2: UI Configuration
+ SetupCustomizeStep {
+ id: step2
+ selectedScaleRatio: root.selectedScaleRatio
+ selectedBarPosition: root.selectedBarPosition
+ selectedDimDesktop: root.selectedDimDesktop
+ onScaleRatioChanged: function (ratio) {
+ root.selectedScaleRatio = ratio
+ root.applyUISettings()
+ }
+ onBarPositionChanged: function (position) {
+ root.selectedBarPosition = position
+ root.applyUISettings()
+ }
+ onDimDesktopChanged: function (dim) {
+ root.selectedDimDesktop = dim
+ root.applyUISettings()
+ }
+ }
+
+ // Step 3: Appearance - Dark mode and color source
+ SetupAppearanceStep {
+ id: step3
+ }
+ }
+ }
+
+ // Elegant divider
+ Rectangle {
+ Layout.fillWidth: true
+ Layout.preferredHeight: 1
+ color: Color.mOutline
+ opacity: 0.2
+ }
+
+ // Modern progress indicator with labels
+ Item {
+ Layout.fillWidth: true
+ Layout.preferredHeight: 32
+
+ RowLayout {
+ anchors.centerIn: parent
+ spacing: Style.marginM
+
+ Repeater {
+ model: [{
+ "icon": "sparkles",
+ "label": "Welcome"
+ }, {
+ "icon": "image",
+ "label": "Wallpaper"
+ }, {
+ "icon": "settings",
+ "label": "Customize"
+ }, {
+ "icon": "palette",
+ "label": "Appearance"
+ }]
+ delegate: RowLayout {
+ spacing: Style.marginS
+
+ Rectangle {
+ width: 24
+ height: 24
+ radius: width / 2
+ color: index <= currentStep ? Color.mPrimary : Color.mSurfaceVariant
+ border.color: index === currentStep ? Color.mPrimary : "transparent"
+ border.width: index === currentStep ? 2 : 0
+
+ NIcon {
+ icon: modelData.icon
+ pointSize: Style.fontSizeS
+ color: index <= currentStep ? Color.mOnPrimary : Color.mOnSurfaceVariant
+ anchors.centerIn: parent
+ }
+
+ Behavior on color {
+ ColorAnimation {
+ duration: Style.animationNormal
+ }
+ }
+ }
+
+ NText {
+ text: modelData.label
+ pointSize: Style.fontSizeS
+ color: index <= currentStep ? Color.mPrimary : Color.mOnSurfaceVariant
+ font.weight: index === currentStep ? Style.fontWeightBold : Style.fontWeightRegular
+
+ Behavior on color {
+ ColorAnimation {
+ duration: Style.animationNormal
+ }
+ }
+ }
+
+ // Connector line
+ Rectangle {
+ width: 40
+ height: 2
+ radius: 1
+ color: index < currentStep ? Color.mPrimary : Color.mSurfaceVariant
+ visible: index < totalSteps - 1
+
+ Behavior on color {
+ ColorAnimation {
+ duration: Style.animationNormal
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ // Smooth navigation buttons
+ Item {
+ Layout.fillWidth: true
+ Layout.preferredHeight: 44
+ Layout.topMargin: Style.marginS
+
+ RowLayout {
+ anchors.fill: parent
+ spacing: Style.marginM
+
+ NButton {
+ text: "Skip Setup"
+ outlined: true
+ visible: currentStep === 0
+ Layout.preferredHeight: 44
+ onClicked: {
+ root.completeSetup()
+ }
+ }
+
+ Item {
+ Layout.fillWidth: true
+ }
+
+ NButton {
+ text: "← Back"
+ outlined: true
+ visible: currentStep > 0
+ Layout.preferredHeight: 44
+ onClicked: {
+ if (currentStep > 0) {
+ currentStep--
+ }
+ }
+ }
+
+ NButton {
+ text: currentStep === totalSteps - 1 ? "All Done!" : "Continue →"
+ Layout.preferredHeight: 44
+ onClicked: {
+ if (currentStep < totalSteps - 1) {
+ currentStep++
+ } else {
+ root.completeSetup()
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ function completeSetup() {
+ Logger.log("SetupWizard", "Completing setup with selected options")
+
+ if (selectedWallpaperDirectory !== Settings.data.wallpaper.directory) {
+ Settings.data.wallpaper.directory = selectedWallpaperDirectory
+ WallpaperService.refreshWallpapersList()
+ }
+
+ if (selectedWallpaper !== "") {
+ WallpaperService.changeWallpaper(selectedWallpaper, undefined)
+ }
+
+ Settings.data.general.scaleRatio = selectedScaleRatio
+ Settings.data.bar.position = selectedBarPosition
+ Settings.data.general.dimDesktop = selectedDimDesktop
+ Settings.data.setupCompleted = true
+
+ Settings.saveImmediate()
+ Logger.log("SetupWizard", "Setup completed successfully")
+ root.close()
+ }
+
+ function applyWallpaperSettings() {
+ if (selectedWallpaperDirectory !== Settings.data.wallpaper.directory) {
+ Settings.data.wallpaper.directory = selectedWallpaperDirectory
+ WallpaperService.refreshWallpapersList()
+ }
+
+ if (selectedWallpaper !== "") {
+ WallpaperService.changeWallpaper(selectedWallpaper, undefined)
+ }
+ }
+
+ function applyUISettings() {
+ Settings.data.general.scaleRatio = selectedScaleRatio
+ Settings.data.bar.position = selectedBarPosition
+ Settings.data.general.dimDesktop = selectedDimDesktop
+ }
+
+ Component.onCompleted: {
+ Logger.log("SetupWizard", "Setup wizard opened")
+ // Initialize selections from existing settings to avoid overwriting user config
+ if (Settings && Settings.data) {
+ selectedScaleRatio = Settings.data.general.scaleRatio
+ selectedBarPosition = Settings.data.bar.position
+ selectedDimDesktop = Settings.data.general.dimDesktop
+ selectedWallpaperDirectory = Settings.data.wallpaper.directory || Settings.defaultWallpapersDirectory
+ }
+ }
+}
diff --git a/Modules/Wallpaper/WallpaperPanel.qml b/Modules/Wallpaper/WallpaperPanel.qml
index be3a526b..2dcfe18e 100644
--- a/Modules/Wallpaper/WallpaperPanel.qml
+++ b/Modules/Wallpaper/WallpaperPanel.qml
@@ -423,7 +423,6 @@ NPanel {
NIcon {
icon: "check"
pointSize: Style.fontSizeM
- font.weight: Style.fontWeightBold
color: Color.mOnSecondary
anchors.centerIn: parent
}
diff --git a/Services/AppThemeService.qml b/Services/AppThemeService.qml
index 53f7fe75..d8fa9589 100644
--- a/Services/AppThemeService.qml
+++ b/Services/AppThemeService.qml
@@ -105,8 +105,8 @@ Singleton {
// Wallpaper Colors Generation
// --------------------------------------------------------------------------------
function generateFromWallpaper() {
- Logger.log("AppThemeService", "Generating from wallpaper on screen:", Screen.name)
+ // Logger.log("AppThemeService", "Generating from wallpaper on screen:", Screen.name)
const wp = WallpaperService.getWallpaper(Screen.name).replace(/'/g, "'\\''")
if (!wp) {
Logger.error("AppThemeService", "No wallpaper found")
diff --git a/Services/BatteryService.qml b/Services/BatteryService.qml
index cf64b853..0e2bea2a 100644
--- a/Services/BatteryService.qml
+++ b/Services/BatteryService.qml
@@ -60,7 +60,6 @@ Singleton {
if (enabled) {
setChargingMode(BatteryService.ChargingMode.Full)
} else {
- BatteryService.chargingMode = BatteryService.ChargingMode.Disabled
BatteryService.initialSetter = true
ToastService.showNotice(I18n.tr("toast.battery-manager.title"), I18n.tr("toast.battery-manager.uninstall-setup"))
PanelService.getPanel("batteryPanel")?.toggle(this)
@@ -194,6 +193,7 @@ Singleton {
Logger.log("BatteryService", "Battery Manager uninstalled successfully")
ToastService.showNotice(I18n.tr("toast.battery-manager.title"), I18n.tr("toast.battery-manager.uninstall-success"))
Settings.data.battery.chargingMode = BatteryService.chargingMode
+ BatteryService.chargingMode = BatteryService.ChargingMode.Disabled
cleanupProcess.running = true
} else {
ToastService.showError(I18n.tr("toast.battery-manager.title"), I18n.tr("toast.battery-manager.uninstall-failed"))
diff --git a/Services/BluetoothService.qml b/Services/BluetoothService.qml
index c5e17d62..2911ee34 100644
--- a/Services/BluetoothService.qml
+++ b/Services/BluetoothService.qml
@@ -171,22 +171,22 @@ Singleton {
function getSignalIcon(device) {
if (!device || device.signalStrength === undefined || device.signalStrength <= 0) {
- return "signal_cellular_null"
+ return "antenna-bars-off"
}
var signal = device.signalStrength
if (signal >= 80) {
- return "signal_cellular_4_bar"
+ return "antenna-bars-5"
}
if (signal >= 60) {
- return "signal_cellular_3_bar"
+ return "antenna-bars-4"
}
if (signal >= 40) {
- return "signal_cellular_2_bar"
+ return "antenna-bars-3"
}
if (signal >= 20) {
- return "signal_cellular_1_bar"
+ return "antenna-bars-2"
}
- return "signal_cellular_0_bar"
+ return "antenna-bars-1"
}
function isDeviceBusy(device) {
diff --git a/Services/MediaService.qml b/Services/MediaService.qml
index 006285c9..b0a4eb16 100644
--- a/Services/MediaService.qml
+++ b/Services/MediaService.qml
@@ -38,12 +38,19 @@ Singleton {
let allPlayers = Mpris.players.values
let finalPlayers = []
const genericBrowsers = ["firefox", "chromium", "chrome"]
+ const blacklist = (Settings.data.audio && Settings.data.audio.mprisBlacklist) ? Settings.data.audio.mprisBlacklist : []
// Separate players into specific and generic lists
let specificPlayers = []
let genericPlayers = []
for (var i = 0; i < allPlayers.length; i++) {
const identity = String(allPlayers[i].identity || "").toLowerCase()
+ const match = blacklist.find(b => {
+ const s = String(b || "").toLowerCase()
+ return s && (identity.includes(s))
+ })
+ if (match)
+ continue
if (genericBrowsers.some(b => identity.includes(b))) {
genericPlayers.push(allPlayers[i])
} else {
diff --git a/Widgets/NCheckbox.qml b/Widgets/NCheckbox.qml
index f27694e6..9fe63be9 100644
--- a/Widgets/NCheckbox.qml
+++ b/Widgets/NCheckbox.qml
@@ -61,7 +61,6 @@ RowLayout {
icon: "check"
color: root.activeOnColor
pointSize: Math.max(Style.fontSizeXS, root.baseSize * 0.5)
- font.weight: Style.fontWeightBold
}
MouseArea {
diff --git a/Widgets/NFilePicker.qml b/Widgets/NFilePicker.qml
index 4fd7875e..cc318f02 100644
--- a/Widgets/NFilePicker.qml
+++ b/Widgets/NFilePicker.qml
@@ -624,7 +624,6 @@ Popup {
NIcon {
icon: "filepicker-check"
pointSize: Style.fontSizeS
- font.weight: Style.fontWeightBold
color: Color.mOnSecondary
anchors.centerIn: parent
}
diff --git a/Widgets/NImageCached.qml b/Widgets/NImageCached.qml
index 1343828b..edeb4baf 100644
--- a/Widgets/NImageCached.qml
+++ b/Widgets/NImageCached.qml
@@ -31,8 +31,9 @@ Image {
}
onCachePathChanged: {
if (imageHash && cachePath) {
- // Try to load the cached version, failure will be detected below in onStatusChanged
- source = cachePath
+ // Check if cache file exists before trying to load it
+ cacheChecker.command = ["test", "-f", cachePath]
+ cacheChecker.running = true
}
}
onStatusChanged: {
@@ -48,4 +49,19 @@ Image {
})
}
}
+
+ // Check if cache file exists to avoid warnings
+ Process {
+ id: cacheChecker
+ running: false
+ onExited: function (exitCode) {
+ if (exitCode === 0 && root.cachePath) {
+ // Cache file exists, load it
+ root.source = root.cachePath
+ } else if (root.imagePath) {
+ // Cache doesn't exist, load original directly
+ root.source = root.imagePath
+ }
+ }
+ }
}
diff --git a/Widgets/NReorderCheckboxes.qml b/Widgets/NReorderCheckboxes.qml
index 9a8c1abc..a7d95927 100644
--- a/Widgets/NReorderCheckboxes.qml
+++ b/Widgets/NReorderCheckboxes.qml
@@ -217,7 +217,6 @@ Item {
icon: "check"
color: root.activeOnColor
pointSize: Math.max(Style.fontSizeXS, root.baseSize * 0.5)
- font.weight: Style.fontWeightBold
}
MouseArea {
diff --git a/flake.nix b/flake.nix
index 54381f04..d2046bb1 100644
--- a/flake.nix
+++ b/flake.nix
@@ -49,9 +49,13 @@
nixpkgs.legacyPackages.${pkgs.system}.app2unit;
};
- nixosModules.default = {pkgs, ...}: {
+ nixosModules.default = {
+ pkgs,
+ lib,
+ ...
+ }: {
imports = [./nix/nixos-module.nix];
- services.noctalia-shell.package = self.packages.${pkgs.system}.default;
+ services.noctalia-shell.package = lib.mkDefault self.packages.${pkgs.system}.default;
};
};
}
diff --git a/shell.qml b/shell.qml
index 39b76224..66d15d51 100644
--- a/shell.qml
+++ b/shell.qml
@@ -40,6 +40,7 @@ import qs.Modules.OSD
import qs.Modules.Settings
import qs.Modules.Toast
import qs.Modules.Wallpaper
+import qs.Modules.SetupWizard
ShellRoot {
id: shellRoot
@@ -168,4 +169,32 @@ ShellRoot {
}
}
}
+
+ // ------------------------------
+ // Setup Wizard
+ Loader {
+ id: setupWizardLoader
+ active: false
+ asynchronous: true
+ sourceComponent: SetupWizard {}
+ onLoaded: {
+ if (setupWizardLoader.item && setupWizardLoader.item.open) {
+ setupWizardLoader.item.open()
+ }
+ }
+ }
+
+ Connections {
+ target: Settings
+ function onSettingsLoaded() {
+ // Only open the setup wizard for new users
+ if (!Settings.data.setupCompleted) {
+ if (Settings.data.settingsVersion >= Settings.settingsVersion) {
+ setupWizardLoader.active = true
+ } else {
+ Settings.data.setupCompleted = true
+ }
+ }
+ }
+ }
}