SetupWizard: initial commit

This commit is contained in:
lysec
2025-10-15 18:01:08 +02:00
parent 7343d2403a
commit 514fdaa4cc
15 changed files with 2228 additions and 4 deletions

View File

@@ -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"
}
}
}
}

View File

@@ -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"
}
}
}

View File

@@ -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"
}
}
}
}

View File

@@ -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"
}
}
}

View File

@@ -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"
}
}
}

View File

@@ -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": "先进行一些基础设置——更多选项可在“设置”中找到"
}
}
}

View File

@@ -64,7 +64,8 @@
"screenRadiusRatio": 1,
"animationSpeed": 1,
"animationDisabled": false,
"compactLockScreen": false
"compactLockScreen": false,
"setupCompleted": false
},
"location": {
"name": "Tokyo",

View File

@@ -185,6 +185,7 @@ Singleton {
property real animationSpeed: 1.0
property bool animationDisabled: false
property bool compactLockScreen: false
property bool setupCompleted: false
}
// location

View File

@@ -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)
}
}
}
}
}
}
}

View File

@@ -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
}
}
}
}

View File

@@ -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<string> wallpapersList: []
property list<string> 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]
}
}
}
}
}

View File

@@ -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
}
}
}

View File

@@ -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")

View File

@@ -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
}
}
}
}

View File

@@ -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()
}
}
}
}
}
}