From 514fdaa4ccb3421f501e0dc869ccabcfc25172e1 Mon Sep 17 00:00:00 2001 From: lysec Date: Wed, 15 Oct 2025 18:01:08 +0200 Subject: [PATCH] SetupWizard: initial commit --- Assets/Translations/de.json | 30 ++ Assets/Translations/en.json | 29 + Assets/Translations/es.json | 30 ++ Assets/Translations/fr.json | 31 ++ Assets/Translations/pt.json | 31 ++ Assets/Translations/zh-CN.json | 31 ++ Assets/settings-default.json | 3 +- Commons/Settings.qml | 1 + Modules/SetupWizard/SetupAppearanceStep.qml | 555 ++++++++++++++++++++ Modules/SetupWizard/SetupCustomizeStep.qml | 522 ++++++++++++++++++ Modules/SetupWizard/SetupWallpaperStep.qml | 507 ++++++++++++++++++ Modules/SetupWizard/SetupWizard.qml | 414 +++++++++++++++ Services/AppThemeService.qml | 2 +- Widgets/NImageCached.qml | 20 +- shell.qml | 26 + 15 files changed, 2228 insertions(+), 4 deletions(-) create mode 100644 Modules/SetupWizard/SetupAppearanceStep.qml create mode 100644 Modules/SetupWizard/SetupCustomizeStep.qml create mode 100644 Modules/SetupWizard/SetupWallpaperStep.qml create mode 100644 Modules/SetupWizard/SetupWizard.qml diff --git a/Assets/Translations/de.json b/Assets/Translations/de.json index 6a5bb0bd0..3bfd4dd60 100644 --- a/Assets/Translations/de.json +++ b/Assets/Translations/de.json @@ -1621,6 +1621,36 @@ "missing-control-center": { "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 7f6cf5c74..bd8545e4b 100644 --- a/Assets/Translations/en.json +++ b/Assets/Translations/en.json @@ -1622,5 +1622,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 9ced10d2a..e33bbb3b6 100644 --- a/Assets/Translations/es.json +++ b/Assets/Translations/es.json @@ -1622,5 +1622,35 @@ "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 ajustes básicos para empezar - el resto está en Configuración" } + } +} } \ No newline at end of file diff --git a/Assets/Translations/fr.json b/Assets/Translations/fr.json index 1d2d95682..c613046d1 100644 --- a/Assets/Translations/fr.json +++ b/Assets/Translations/fr.json @@ -1623,4 +1623,35 @@ "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 118939a8f..e825b83e9 100644 --- a/Assets/Translations/pt.json +++ b/Assets/Translations/pt.json @@ -1629,4 +1629,35 @@ "uninstall-failed": "Falha na desinstalação" } } + , + "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 d64db2ffb..9aa297190 100644 --- a/Assets/Translations/zh-CN.json +++ b/Assets/Translations/zh-CN.json @@ -1623,4 +1623,35 @@ "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/settings-default.json b/Assets/settings-default.json index b8a822597..99ef9c6de 100644 --- a/Assets/settings-default.json +++ b/Assets/settings-default.json @@ -64,7 +64,8 @@ "screenRadiusRatio": 1, "animationSpeed": 1, "animationDisabled": false, - "compactLockScreen": false + "compactLockScreen": false, + "setupCompleted": false }, "location": { "name": "Tokyo", diff --git a/Commons/Settings.qml b/Commons/Settings.qml index d0308487b..2349a292c 100644 --- a/Commons/Settings.qml +++ b/Commons/Settings.qml @@ -185,6 +185,7 @@ Singleton { property real animationSpeed: 1.0 property bool animationDisabled: false property bool compactLockScreen: false + property bool setupCompleted: false } // location diff --git a/Modules/SetupWizard/SetupAppearanceStep.qml b/Modules/SetupWizard/SetupAppearanceStep.qml new file mode 100644 index 000000000..15c8c1eea --- /dev/null +++ b/Modules/SetupWizard/SetupAppearanceStep.qml @@ -0,0 +1,555 @@ +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 + spacing: Style.marginM + + 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: Style.marginXS + + NText { + text: I18n.tr("setup.appearance.header") + pointSize: Style.fontSizeXL + font.weight: Style.fontWeightBold + color: Color.mOnSurface + } + + 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.cacheVersionroot.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.cacheVersionfunction () { + 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") + })() + } + Rectangle { + width: 14 + height: 14 + radius: width * 0.5 + color: (root.cacheVersionfunction () { + 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") + })() + } + Rectangle { + width: 14 + height: 14 + radius: width * 0.5 + color: (root.cacheVersionfunction () { + 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") + })() + } + Rectangle { + width: 14 + height: 14 + radius: width * 0.5 + color: (root.cacheVersionfunction () { + 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") + })() + } + } + + 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..a1854f5f5 --- /dev/null +++ b/Modules/SetupWizard/SetupCustomizeStep.qml @@ -0,0 +1,522 @@ +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 + spacing: Style.marginM + + 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: Style.marginXS + + NText { + text: I18n.tr("setup.customize.header") + pointSize: Style.fontSizeXL + font.weight: Style.fontWeightBold + color: Color.mOnSurface + } + + 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 + } + } + } + + GridLayout { + Layout.fillWidth: true + columns: 2 + rowSpacing: Style.marginS + columnSpacing: 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: 48 + 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(72, densityText.implicitWidth + Style.marginM) + + 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 + } + } + } + + RowLayout { + Layout.fillWidth: true + spacing: Style.marginM + NText { + text: "80%" + 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) + "%" + } + NText { + text: "120%" + pointSize: Style.fontSizeS + color: Color.mOnSurfaceVariant + } + } + } + + // 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..fa1225c29 --- /dev/null +++ b/Modules/SetupWizard/SetupWallpaperStep.qml @@ -0,0 +1,507 @@ +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 + 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.mOnSurface + } + + 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..d34d6013f --- /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: "https://assets.noctalia.dev/noctalia-logo.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.general.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..46edcf475 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 @@ -89,6 +90,10 @@ ShellRoot { HooksService.init() BluetoothService.init() BatteryService.init() + + if (Settings && Settings.data && Settings.data.general && !Settings.data.general.setupCompleted) { + setupWizardLoader.active = true + } } Background {} @@ -166,6 +171,27 @@ ShellRoot { id: batteryPanel objectName: "batteryPanel" } + + // Lazy-load Setup Wizard to save memory + Component { + id: setupWizardComponent + SetupWizard { + id: setupWizardPanel + objectName: "setupWizardPanel" + } + } + + Loader { + id: setupWizardLoader + active: false + asynchronous: true + sourceComponent: setupWizardComponent + onLoaded: { + if (setupWizardLoader.item && setupWizardLoader.item.open) { + setupWizardLoader.item.open() + } + } + } } } }