diff --git a/Assets/Translations/de.json b/Assets/Translations/de.json
index b75e00799..ae9d2261c 100644
--- a/Assets/Translations/de.json
+++ b/Assets/Translations/de.json
@@ -1623,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"
+ }
}
}
diff --git a/Assets/Translations/en.json b/Assets/Translations/en.json
index 9052de86e..7ea6c7cca 100644
--- a/Assets/Translations/en.json
+++ b/Assets/Translations/en.json
@@ -1623,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"
+ }
}
}
diff --git a/Assets/Translations/es.json b/Assets/Translations/es.json
index fe0fd6fa0..fe04122f0 100644
--- a/Assets/Translations/es.json
+++ b/Assets/Translations/es.json
@@ -1623,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."
+ }
}
}
diff --git a/Assets/Translations/fr.json b/Assets/Translations/fr.json
index e8ab41bcd..96d668d2c 100644
--- a/Assets/Translations/fr.json
+++ b/Assets/Translations/fr.json
@@ -1623,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"
+ }
}
}
diff --git a/Assets/Translations/pt.json b/Assets/Translations/pt.json
index ac27a3b5e..55121c93e 100644
--- a/Assets/Translations/pt.json
+++ b/Assets/Translations/pt.json
@@ -1616,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"
}
}
}
diff --git a/Assets/Translations/zh-CN.json b/Assets/Translations/zh-CN.json
index fd255395f..884d6cc1e 100644
--- a/Assets/Translations/zh-CN.json
+++ b/Assets/Translations/zh-CN.json
@@ -1623,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": "先进行一些基础设置——更多选项可在“设置”中找到"
+ }
}
}
diff --git a/Assets/noctalia.svg b/Assets/noctalia.svg
new file mode 100644
index 000000000..106462259
--- /dev/null
+++ b/Assets/noctalia.svg
@@ -0,0 +1,65 @@
+
+
+
+
diff --git a/Assets/settings-default.json b/Assets/settings-default.json
index b8a822597..8d3d6ce87 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 d0308487b..11767999f 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()
+
+ // Last, 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/Modules/SetupWizard/SetupAppearanceStep.qml b/Modules/SetupWizard/SetupAppearanceStep.qml
new file mode 100644
index 000000000..32903902b
--- /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 000000000..e5e0dcc13
--- /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 000000000..aa915f724
--- /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 000000000..2a7a5c1b2
--- /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/Services/AppThemeService.qml b/Services/AppThemeService.qml
index 53f7fe75b..d8fa9589c 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/Widgets/NImageCached.qml b/Widgets/NImageCached.qml
index 1343828b9..edeb4baf4 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/shell.qml b/shell.qml
index 39b76224c..66d15d51c 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
+ }
+ }
+ }
+ }
}