Revert "Remove need for polkit, launch any ArchUpdater update through terminal"

This reverts commit 299add4a15.
This commit is contained in:
LemmyCook 2025-08-29 20:50:28 -04:00
parent 299add4a15
commit 3496169c68
19 changed files with 807 additions and 1240 deletions

View file

@ -17,6 +17,14 @@ Singleton {
} }
} }
function _getStackTrace() {
try {
throw new Error("Stack trace")
} catch (e) {
return e.stack
}
}
function log(...args) { function log(...args) {
var msg = _formatMessage(...args) var msg = _formatMessage(...args)
console.log(msg) console.log(msg)
@ -31,4 +39,20 @@ Singleton {
var msg = _formatMessage(...args) var msg = _formatMessage(...args)
console.error(msg) console.error(msg)
} }
function callStack() {
var stack = _getStackTrace()
Logger.log("Debug", "--------------------------")
Logger.log("Debug", "Current call stack")
// Split the stack into lines and log each one
var stackLines = stack.split('\n')
for (var i = 0; i < stackLines.length; i++) {
var line = stackLines[i].trim() // Remove leading/trailing whitespace
if (line.length > 0) {
// Only log non-empty lines
Logger.log("Debug", `- ${line}`)
}
}
Logger.log("Debug", "--------------------------")
}
} }

View file

@ -89,23 +89,16 @@ Singleton {
reload() reload()
} }
onLoaded: function () { onLoaded: function () {
Qt.callLater(function () { if (!isLoaded) {
// Some stuff like wallpaper setup and settings validation should just be executed once on startup Logger.log("Settings", "----------------------------")
// And not on every reload Logger.log("Settings", "Settings loaded successfully")
if (!isLoaded) { isLoaded = true
Logger.log("Settings", "JSON completed loading")
if (adapter.wallpaper.current !== "") {
Logger.log("Settings", "Set current wallpaper", adapter.wallpaper.current)
WallpaperService.setCurrentWallpaper(adapter.wallpaper.current, true)
}
// Validate monitor configurations, only once Qt.callLater(function () {
// if none of the configured monitors exist, clear the lists // Some stuff like settings validation should just be executed once on startup and not on every reload
validateMonitorConfigurations() validateMonitorConfigurations()
})
isLoaded = true }
}
})
} }
onLoadFailed: function (error) { onLoadFailed: function (error) {
if (error.toString().includes("No such file") || error === 2) if (error.toString().includes("No such file") || error === 2)
@ -171,22 +164,13 @@ Singleton {
// wallpaper // wallpaper
property JsonObject wallpaper: JsonObject { property JsonObject wallpaper: JsonObject {
property string directory: "/usr/share/wallpapers" property string directory: "/usr/share/wallpapers"
property string current: "" property bool enableMultiMonitorDirectories: false
property bool isRandom: false property bool setWallpaperOnAllMonitors: true
property int randomInterval: 300 property bool randomEnabled: false
property JsonObject swww property int randomIntervalSec: 300 // 5 min
property int transitionDuration: 1500 // 1500 ms
onDirectoryChanged: WallpaperService.listWallpapers() property string transitionType: "fade"
onIsRandomChanged: WallpaperService.toggleRandomWallpaper() property list<var> monitors: []
onRandomIntervalChanged: WallpaperService.restartRandomWallpaperTimer()
swww: JsonObject {
property bool enabled: false
property string resizeMethod: "crop"
property int transitionFps: 60
property string transitionType: "random"
property real transitionDuration: 1.1
}
} }
// applauncher // applauncher
@ -234,19 +218,10 @@ Singleton {
property string fontDefault: "Roboto" // Default font for all text property string fontDefault: "Roboto" // Default font for all text
property string fontFixed: "DejaVu Sans Mono" // Fixed width font for terminal property string fontFixed: "DejaVu Sans Mono" // Fixed width font for terminal
property string fontBillboard: "Inter" // Large bold font for clocks and prominent displays property string fontBillboard: "Inter" // Large bold font for clocks and prominent displays
property list<var> monitorsScaling: []
// Legacy compatibility
property string fontFamily: fontDefault // Keep for backward compatibility
// Idle inhibitor state
property bool idleInhibitorEnabled: false property bool idleInhibitorEnabled: false
} }
// Scaling (not stored inside JsonObject, or it crashes)
property var monitorsScaling: {
}
// brightness // brightness
property JsonObject brightness: JsonObject { property JsonObject brightness: JsonObject {
property int brightnessStep: 5 property int brightnessStep: 5

View file

@ -15,9 +15,8 @@ NPanel {
// When the panel opens // When the panel opens
onOpened: { onOpened: {
console.log("ArchUpdaterPanel: Panel opened, refreshing package lists...") ArchUpdaterService.doPoll()
// Always refresh when panel opens to ensure we have the latest data ArchUpdaterService.doAurPoll()
ArchUpdaterService.forceRefresh()
} }
panelContent: Rectangle { panelContent: Rectangle {
@ -48,19 +47,6 @@ NPanel {
Layout.fillWidth: true Layout.fillWidth: true
} }
// Reset button (only show if update failed)
NIconButton {
visible: ArchUpdaterService.updateFailed
icon: "refresh"
tooltipText: "Reset update state"
sizeRatio: 0.8
colorBg: Color.mError
colorFg: Color.mOnError
onClicked: {
ArchUpdaterService.resetUpdateState()
}
}
NIconButton { NIconButton {
icon: "close" icon: "close"
tooltipText: "Close" tooltipText: "Close"
@ -73,10 +59,8 @@ NPanel {
Layout.fillWidth: true Layout.fillWidth: true
} }
// Update summary (only show when packages are available) // Update summary
NText { NText {
visible: !ArchUpdaterService.updateInProgress && !ArchUpdaterService.updateFailed && !ArchUpdaterService.busy
&& !ArchUpdaterService.aurBusy && ArchUpdaterService.totalUpdates > 0
text: ArchUpdaterService.totalUpdates + " package" + (ArchUpdaterService.totalUpdates !== 1 ? "s" : "") + " can be updated" text: ArchUpdaterService.totalUpdates + " package" + (ArchUpdaterService.totalUpdates !== 1 ? "s" : "") + " can be updated"
font.pointSize: Style.fontSizeL * scaling font.pointSize: Style.fontSizeL * scaling
font.weight: Style.fontWeightMedium font.weight: Style.fontWeightMedium
@ -84,184 +68,16 @@ NPanel {
Layout.fillWidth: true Layout.fillWidth: true
} }
// Package selection info (only show when not updating and have packages) // Package selection info
NText { NText {
visible: !ArchUpdaterService.updateInProgress && !ArchUpdaterService.updateFailed && !ArchUpdaterService.busy
&& !ArchUpdaterService.aurBusy && ArchUpdaterService.totalUpdates > 0
text: ArchUpdaterService.selectedPackagesCount + " of " + ArchUpdaterService.totalUpdates + " packages selected" text: ArchUpdaterService.selectedPackagesCount + " of " + ArchUpdaterService.totalUpdates + " packages selected"
font.pointSize: Style.fontSizeS * scaling font.pointSize: Style.fontSizeS * scaling
color: Color.mOnSurfaceVariant color: Color.mOnSurfaceVariant
Layout.fillWidth: true Layout.fillWidth: true
} }
// Update in progress state // Unified list
ColumnLayout {
Layout.fillWidth: true
Layout.fillHeight: true
visible: ArchUpdaterService.updateInProgress
spacing: Style.marginM * scaling
Item {
Layout.fillHeight: true
} // Spacer
NIcon {
text: "hourglass_empty"
font.pointSize: Style.fontSizeXXXL * scaling
color: Color.mPrimary
Layout.alignment: Qt.AlignHCenter
}
NText {
text: "Update in progress"
font.pointSize: Style.fontSizeL * scaling
color: Color.mOnSurface
Layout.alignment: Qt.AlignHCenter
}
NText {
text: "Please check your terminal window for update progress and prompts."
font.pointSize: Style.fontSizeNormal * scaling
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.Wrap
Layout.maximumWidth: 280 * scaling
}
Item {
Layout.fillHeight: true
} // Spacer
}
// Update failed state
Item {
Layout.fillWidth: true
Layout.fillHeight: true
visible: ArchUpdaterService.updateFailed
ColumnLayout {
anchors.centerIn: parent
spacing: Style.marginM * scaling
NIcon {
text: "error_outline"
font.pointSize: Style.fontSizeXXXL * scaling
color: Color.mError
Layout.alignment: Qt.AlignHCenter
}
NText {
text: "Update failed"
font.pointSize: Style.fontSizeL * scaling
color: Color.mOnSurface
Layout.alignment: Qt.AlignHCenter
}
NText {
text: "Check your terminal for error details and try again."
font.pointSize: Style.fontSizeNormal * scaling
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.Wrap
Layout.maximumWidth: 280 * scaling
}
// Prominent refresh button
NIconButton {
icon: "refresh"
tooltipText: "Refresh and try again"
sizeRatio: 1.2
colorBg: Color.mPrimary
colorFg: Color.mOnPrimary
onClicked: {
ArchUpdaterService.resetUpdateState()
}
Layout.alignment: Qt.AlignHCenter
Layout.topMargin: Style.marginL * scaling
}
}
}
// No updates available state
Item {
Layout.fillWidth: true
Layout.fillHeight: true
visible: !ArchUpdaterService.updateInProgress && !ArchUpdaterService.updateFailed && !ArchUpdaterService.busy
&& !ArchUpdaterService.aurBusy && ArchUpdaterService.totalUpdates === 0
ColumnLayout {
anchors.centerIn: parent
spacing: Style.marginM * scaling
NIcon {
text: "check_circle"
font.pointSize: Style.fontSizeXXXL * scaling
color: Color.mPrimary
Layout.alignment: Qt.AlignHCenter
}
NText {
text: "System is up to date"
font.pointSize: Style.fontSizeL * scaling
color: Color.mOnSurface
Layout.alignment: Qt.AlignHCenter
}
NText {
text: "All packages are current. Check back later for updates."
font.pointSize: Style.fontSizeNormal * scaling
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.Wrap
Layout.maximumWidth: 280 * scaling
}
}
}
// Checking for updates state
Item {
Layout.fillWidth: true
Layout.fillHeight: true
visible: (ArchUpdaterService.busy || ArchUpdaterService.aurBusy) && !ArchUpdaterService.updateInProgress
&& !ArchUpdaterService.updateFailed
ColumnLayout {
anchors.centerIn: parent
spacing: Style.marginM * scaling
NIcon {
text: "refresh"
font.pointSize: Style.fontSizeXXXL * scaling
color: Color.mPrimary
Layout.alignment: Qt.AlignHCenter
}
NText {
text: "Checking for updates"
font.pointSize: Style.fontSizeL * scaling
color: Color.mOnSurface
Layout.alignment: Qt.AlignHCenter
}
NText {
text: "Scanning package databases for available updates..."
font.pointSize: Style.fontSizeNormal * scaling
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.Wrap
Layout.maximumWidth: 280 * scaling
}
}
}
// Package list (only show when not in any special state)
NBox { NBox {
visible: !ArchUpdaterService.updateInProgress && !ArchUpdaterService.updateFailed && !ArchUpdaterService.busy
&& !ArchUpdaterService.aurBusy && ArchUpdaterService.totalUpdates > 0
Layout.fillWidth: true Layout.fillWidth: true
Layout.fillHeight: true Layout.fillHeight: true
@ -348,49 +164,50 @@ NPanel {
} }
} }
// Action buttons (only show when not updating) // Action buttons
RowLayout { RowLayout {
visible: !ArchUpdaterService.updateInProgress && !ArchUpdaterService.updateFailed
Layout.fillWidth: true Layout.fillWidth: true
spacing: Style.marginL * scaling spacing: Style.marginL * scaling
NIconButton { NIconButton {
icon: "refresh" icon: "refresh"
tooltipText: "Refresh package lists" tooltipText: "Check for updates"
onClicked: { onClicked: {
ArchUpdaterService.forceRefresh() ArchUpdaterService.doPoll()
ArchUpdaterService.doAurPoll()
} }
colorBg: Color.mSurfaceVariant colorBg: Color.mSurfaceVariant
colorFg: Color.mOnSurface colorFg: Color.mOnSurface
Layout.fillWidth: true Layout.fillWidth: true
enabled: !ArchUpdaterService.busy && !ArchUpdaterService.aurBusy
} }
NIconButton { NIconButton {
icon: "system_update_alt" icon: ArchUpdaterService.updateInProgress ? "hourglass_empty" : "system_update_alt"
tooltipText: "Update all packages" tooltipText: ArchUpdaterService.updateInProgress ? "Update in progress..." : "Update all packages"
enabled: ArchUpdaterService.totalUpdates > 0 enabled: !ArchUpdaterService.updateInProgress
onClicked: { onClicked: {
ArchUpdaterService.runUpdate() ArchUpdaterService.runUpdate()
root.close() root.close()
} }
colorBg: ArchUpdaterService.totalUpdates > 0 ? Color.mPrimary : Color.mSurfaceVariant colorBg: ArchUpdaterService.updateInProgress ? Color.mSurfaceVariant : Color.mPrimary
colorFg: ArchUpdaterService.totalUpdates > 0 ? Color.mOnPrimary : Color.mOnSurfaceVariant colorFg: ArchUpdaterService.updateInProgress ? Color.mOnSurfaceVariant : Color.mOnPrimary
Layout.fillWidth: true Layout.fillWidth: true
} }
NIconButton { NIconButton {
icon: "check_box" icon: ArchUpdaterService.updateInProgress ? "hourglass_empty" : "check_box"
tooltipText: "Update selected packages" tooltipText: ArchUpdaterService.updateInProgress ? "Update in progress..." : "Update selected packages"
enabled: ArchUpdaterService.selectedPackagesCount > 0 enabled: !ArchUpdaterService.updateInProgress && ArchUpdaterService.selectedPackagesCount > 0
onClicked: { onClicked: {
if (ArchUpdaterService.selectedPackagesCount > 0) { if (ArchUpdaterService.selectedPackagesCount > 0) {
ArchUpdaterService.runSelectiveUpdate() ArchUpdaterService.runSelectiveUpdate()
root.close() root.close()
} }
} }
colorBg: ArchUpdaterService.selectedPackagesCount > 0 ? Color.mPrimary : Color.mSurfaceVariant colorBg: ArchUpdaterService.updateInProgress ? Color.mSurfaceVariant : (ArchUpdaterService.selectedPackagesCount
colorFg: ArchUpdaterService.selectedPackagesCount > 0 ? Color.mOnPrimary : Color.mOnSurfaceVariant > 0 ? Color.mPrimary : Color.mSurfaceVariant)
colorFg: ArchUpdaterService.updateInProgress ? Color.mOnSurfaceVariant : (ArchUpdaterService.selectedPackagesCount
> 0 ? Color.mOnPrimary : Color.mOnSurfaceVariant)
Layout.fillWidth: true Layout.fillWidth: true
} }
} }

View file

@ -4,27 +4,67 @@ import Quickshell.Wayland
import qs.Commons import qs.Commons
import qs.Services import qs.Services
Loader { Variants {
active: !Settings.data.wallpaper.swww.enabled id: backgroundVariants
model: Quickshell.screens
sourceComponent: Variants { delegate: Loader {
model: Quickshell.screens
delegate: PanelWindow { required property ShellScreen modelData
required property ShellScreen modelData
property string wallpaperSource: WallpaperService.currentWallpaper !== ""
&& !Settings.data.wallpaper.swww.enabled ? WallpaperService.currentWallpaper : ""
visible: wallpaperSource !== "" && !Settings.data.wallpaper.swww.enabled active: Settings.isLoaded && WallpaperService.getWallpaper(modelData.name)
// Force update when SWWW setting changes sourceComponent: PanelWindow {
onVisibleChanged: { id: root
if (visible) {
} else { // Internal state management
property bool firstWallpaper: true
property bool transitioning: false
property real transitionProgress: 0.0
// Wipe direction: 0=left, 1=right, 2=up, 3=down
property real wipeDirection: 0
property real wipeSmoothness: 0.05
// External state management
property string servicedWallpaper: WallpaperService.getWallpaper(modelData.name)
onServicedWallpaperChanged: {
if (servicedWallpaper && servicedWallpaper !== currentWallpaper.source) {
// Set wallpaper immediately on startup
if (firstWallpaper) {
firstWallpaper = false
setWallpaperImmediate(servicedWallpaper)
return
}
switch (Settings.data.wallpaper.transitionType) {
case "none":
setWallpaperImmediate(servicedWallpaper)
break
case "wipe_left":
wipeDirection = 0
setWallpaperWithTransition(servicedWallpaper)
break
case "wipe_right":
wipeDirection = 1
setWallpaperWithTransition(servicedWallpaper)
break
case "wipe_up":
wipeDirection = 2
setWallpaperWithTransition(servicedWallpaper)
break
case "wipe_down":
wipeDirection = 3
setWallpaperWithTransition(servicedWallpaper)
break
default:
setWallpaperWithTransition(servicedWallpaper)
break
}
} }
} }
color: Color.transparent color: Color.transparent
screen: modelData screen: modelData
WlrLayershell.layer: WlrLayer.Background WlrLayershell.layer: WlrLayer.Background
@ -38,18 +78,106 @@ Loader {
left: true left: true
} }
margins {
top: 0
}
Image { Image {
id: currentWallpaper
anchors.fill: parent anchors.fill: parent
fillMode: Image.PreserveAspectCrop fillMode: Image.PreserveAspectCrop
source: wallpaperSource source: ""
visible: wallpaperSource !== ""
cache: true cache: true
smooth: true smooth: true
mipmap: false mipmap: false
visible: false
}
Image {
id: nextWallpaper
anchors.fill: parent
fillMode: Image.PreserveAspectCrop
source: ""
cache: true
smooth: true
mipmap: false
visible: false
}
// Fade transition shader
ShaderEffect {
id: fadeShader
anchors.fill: parent
visible: Settings.data.wallpaper.transitionType === 'fade'
property variant source1: currentWallpaper
property variant source2: nextWallpaper
property real fade: transitionProgress
fragmentShader: Qt.resolvedUrl("../../Shaders/qsb/wp_fade.frag.qsb")
}
// Wipe transition shader
ShaderEffect {
id: wipeShader
anchors.fill: parent
visible: Settings.data.wallpaper.transitionType.startsWith('wipe_')
property variant source1: currentWallpaper
property variant source2: nextWallpaper
property real progress: transitionProgress
property real direction: wipeDirection
property real smoothness: wipeSmoothness
fragmentShader: Qt.resolvedUrl("../../Shaders/qsb/wp_wipe.frag.qsb")
}
// Animation for the transition progress
NumberAnimation {
id: transitionAnimation
target: root
property: "transitionProgress"
from: 0.0
to: 1.0
duration: Settings.data.wallpaper.transitionDuration ?? 1000
easing.type: {
const transitionType = Settings.data.wallpaper.transitionType ?? 'fade'
if (transitionType.startsWith('wipe_')) {
return Easing.InOutCubic
}
return Easing.InOutCubic
}
onFinished: {
// Swap images after transition completes
currentWallpaper.source = nextWallpaper.source
transitionProgress = 0.0
transitioning = false
}
}
function startTransition() {
if (!transitioning && nextWallpaper.source != currentWallpaper.source) {
transitioning = true
transitionAnimation.start()
}
}
function setWallpaperImmediate(source) {
currentWallpaper.source = source
nextWallpaper.source = source
transitionProgress = 0.0
transitioning = false
}
function setWallpaperWithTransition(source) {
if (source != currentWallpaper.source) {
if (transitioning) {
// We are interrupting a transition
currentWallpaper.source = nextWallpaper.source
transitionAnimation.stop()
transitionProgress = 0
transitioning = false
}
nextWallpaper.source = source
startTransition()
}
} }
} }
} }

View file

@ -6,24 +6,19 @@ import qs.Commons
import qs.Services import qs.Services
import qs.Widgets import qs.Widgets
Loader { Variants {
active: CompositorService.isNiri model: Quickshell.screens
Component.onCompleted: { delegate: Loader {
if (CompositorService.isNiri) { required property ShellScreen modelData
Logger.log("Overview", "Loading Overview component for Niri")
}
}
sourceComponent: Variants { active: Settings.isLoaded && CompositorService.isNiri
model: Quickshell.screens
delegate: PanelWindow { sourceComponent: PanelWindow {
required property ShellScreen modelData Component.onCompleted: {
property string wallpaperSource: WallpaperService.currentWallpaper !== "" Logger.log("Overview", "Loading Overview component for Niri on", modelData.name)
&& !Settings.data.wallpaper.swww.enabled ? WallpaperService.currentWallpaper : "" }
visible: wallpaperSource !== "" && !Settings.data.wallpaper.swww.enabled
color: Color.transparent color: Color.transparent
screen: modelData screen: modelData
WlrLayershell.layer: WlrLayer.Background WlrLayershell.layer: WlrLayer.Background
@ -39,19 +34,15 @@ Loader {
Image { Image {
id: bgImage id: bgImage
anchors.fill: parent anchors.fill: parent
fillMode: Image.PreserveAspectCrop fillMode: Image.PreserveAspectCrop
source: wallpaperSource source: WallpaperService.getWallpaper(modelData.name)
cache: true cache: true
smooth: true smooth: true
mipmap: false mipmap: false
visible: wallpaperSource !== ""
} }
MultiEffect { MultiEffect {
id: overviewBgBlur
anchors.fill: parent anchors.fill: parent
source: bgImage source: bgImage
blurEnabled: true blurEnabled: true

View file

@ -64,5 +64,6 @@ NIconButton {
PanelService.getPanel("archUpdaterPanel").toggle(screen, this) PanelService.getPanel("archUpdaterPanel").toggle(screen, this)
ArchUpdaterService.doPoll() ArchUpdaterService.doPoll()
ArchUpdaterService.doAurPoll()
} }
} }

View file

@ -93,7 +93,7 @@ Loader {
id: lockBgImage id: lockBgImage
anchors.fill: parent anchors.fill: parent
fillMode: Image.PreserveAspectCrop fillMode: Image.PreserveAspectCrop
source: WallpaperService.currentWallpaper !== "" ? WallpaperService.currentWallpaper : "" source: WallpaperService.getWallpaper(screen.name)
cache: true cache: true
smooth: true smooth: true
mipmap: false mipmap: false

View file

@ -188,7 +188,7 @@ ColumnLayout {
} }
NText { NText {
text: `${Math.round(ScalingService.scaleByName(modelData.name) * 100)}%` text: `${Math.round(ScalingService.getMonitorScale(modelData.name) * 100)}%`
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
Layout.minimumWidth: 50 * scaling Layout.minimumWidth: 50 * scaling
horizontalAlignment: Text.AlignRight horizontalAlignment: Text.AlignRight
@ -204,12 +204,8 @@ ColumnLayout {
from: 0.7 from: 0.7
to: 1.8 to: 1.8
stepSize: 0.01 stepSize: 0.01
value: ScalingService.scaleByName(modelData.name) value: ScalingService.getMonitorScale(modelData.name)
onPressedChanged: { onPressedChanged: ScalingService.setMonitorScale(modelData.name, value)
var data = Settings.data.monitorsScaling || {}
data[modelData.name] = value
Settings.data.monitorsScaling = data
}
Layout.fillWidth: true Layout.fillWidth: true
Layout.minimumWidth: 150 * scaling Layout.minimumWidth: 150 * scaling
} }
@ -217,11 +213,7 @@ ColumnLayout {
NIconButton { NIconButton {
icon: "refresh" icon: "refresh"
tooltipText: "Reset Scaling" tooltipText: "Reset Scaling"
onClicked: { onClicked: ScalingService.setMonitorScale(modelData.name, 1.0)
var data = Settings.data.monitorsScaling || {}
data[modelData.name] = 1.0
Settings.data.monitorsScaling = data
}
} }
} }
} }

View file

@ -23,13 +23,12 @@ ColumnLayout {
Layout.fillWidth: true Layout.fillWidth: true
Layout.preferredHeight: 140 * scaling Layout.preferredHeight: 140 * scaling
radius: Style.radiusM * scaling radius: Style.radiusM * scaling
color: Color.mPrimary color: Color.mSecondary
NImageRounded { NImageRounded {
id: currentWallpaperImage
anchors.fill: parent anchors.fill: parent
anchors.margins: Style.marginXS * scaling anchors.margins: Style.marginXS * scaling
imagePath: WallpaperService.currentWallpaper imagePath: WallpaperService.getWallpaper(screen.name)
fallbackIcon: "image" fallbackIcon: "image"
imageRadius: Style.radiusM * scaling imageRadius: Style.radiusM * scaling
} }
@ -62,41 +61,44 @@ ColumnLayout {
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
Layout.fillWidth: true Layout.fillWidth: true
} }
NText {
text: Settings.data.wallpaper.swww.enabled ? "Wallpapers will change with " + Settings.data.wallpaper.swww.transitionType
+ " transition." : "Wallpapers will change instantly."
color: Color.mOnSurface
font.pointSize: Style.fontSizeXS * scaling
visible: Settings.data.wallpaper.swww.enabled
}
} }
NIconButton { NIconButton {
icon: "refresh" icon: "refresh"
tooltipText: "Refresh wallpaper list" tooltipText: "Refresh wallpaper list"
onClicked: { onClicked: {
WallpaperService.listWallpapers() WallpaperService.refreshWallpapersList()
} }
Layout.alignment: Qt.AlignTop | Qt.AlignRight Layout.alignment: Qt.AlignVCenter | Qt.AlignRight
} }
} }
property list<string> wallpapersList: WallpaperService.getWallpapersList(screen.name)
NToggle {
label: "Assign selection to all monitors"
description: "Set selected wallpaper on all monitors at once."
checked: Settings.data.wallpaper.setWallpaperOnAllMonitors
onToggled: checked => Settings.data.wallpaper.setWallpaperOnAllMonitors = checked
visible: (wallpapersList.length > 0)
}
// Wallpaper grid container // Wallpaper grid container
Item { Item {
visible: !WallpaperService.scanning
Layout.fillWidth: true Layout.fillWidth: true
Layout.preferredHeight: { Layout.preferredHeight: {
return Math.ceil(WallpaperService.wallpaperList.length / wallpaperGridView.columns) * wallpaperGridView.cellHeight return Math.ceil(wallpapersList.length / wallpaperGridView.columns) * wallpaperGridView.cellHeight
} }
GridView { GridView {
id: wallpaperGridView id: wallpaperGridView
anchors.fill: parent anchors.fill: parent
clip: true clip: true
model: WallpaperService.wallpaperList model: wallpapersList
boundsBehavior: Flickable.StopAtBounds boundsBehavior: Flickable.StopAtBounds
flickableDirection: Flickable.AutoFlickDirection flickableDirection: Flickable.VerticalFlick
interactive: false interactive: false
property int columns: 5 property int columns: 5
@ -114,7 +116,7 @@ ColumnLayout {
id: wallpaperItem id: wallpaperItem
property string wallpaperPath: modelData property string wallpaperPath: modelData
property bool isSelected: wallpaperPath === WallpaperService.currentWallpaper property bool isSelected: wallpaperPath === WallpaperService.getWallpaper(screen.name)
width: wallpaperGridView.itemSize width: wallpaperGridView.itemSize
height: Math.floor(wallpaperGridView.itemSize * 0.67) height: Math.floor(wallpaperGridView.itemSize * 0.67)
@ -179,46 +181,65 @@ ColumnLayout {
acceptedButtons: Qt.LeftButton acceptedButtons: Qt.LeftButton
hoverEnabled: true hoverEnabled: true
onClicked: { onClicked: {
WallpaperService.changeWallpaper(wallpaperPath) if (Settings.data.wallpaper.setWallpaperOnAllMonitors) {
WallpaperService.changeWallpaper(undefined, wallpaperPath)
} else {
WallpaperService.changeWallpaper(screen.name, wallpaperPath)
}
} }
} }
} }
} }
}
// Empty state // Empty state
Rectangle { Rectangle {
color: Color.mSurface
radius: Style.radiusM * scaling
border.color: Color.mOutline
border.width: Math.max(1, Style.borderS * scaling)
visible: wallpapersList.length === 0 || WallpaperService.scanning
Layout.fillWidth: true
Layout.preferredHeight: 130 * scaling
ColumnLayout {
anchors.fill: parent anchors.fill: parent
color: Color.mSurface visible: WallpaperService.scanning
radius: Style.radiusM * scaling NBusyIndicator {
border.color: Color.mOutline Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
border.width: Math.max(1, Style.borderS * scaling) }
visible: WallpaperService.wallpaperList.length === 0 && !WallpaperService.scanning }
ColumnLayout { ColumnLayout {
anchors.centerIn: parent anchors.fill: parent
spacing: Style.marginM * scaling visible: wallpapersList.length === 0 && !WallpaperService.scanning
Item {
Layout.fillHeight: true
}
NIcon { NIcon {
text: "folder_open" text: "folder_open"
font.pointSize: Style.fontSizeL * scaling font.pointSize: Style.fontSizeXL * scaling
color: Color.mOnSurface color: Color.mOnSurface
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
} }
NText { NText {
text: "No wallpapers found" text: "No wallpaper found."
color: Color.mOnSurface color: Color.mOnSurface
font.weight: Style.fontWeightBold font.weight: Style.fontWeightBold
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
} }
NText { NText {
text: "Make sure your wallpaper directory is configured and contains image files." text: "Make sure your wallpaper directory is configured and contains image files."
color: Color.mOnSurface color: Color.mOnSurfaceVariant
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
horizontalAlignment: Text.AlignHCenter Layout.alignment: Qt.AlignHCenter
Layout.preferredWidth: Style.sliderWidth * 1.5 * scaling }
}
Item {
Layout.fillHeight: true
} }
} }
} }

View file

@ -1,6 +1,7 @@
import QtQuick import QtQuick
import QtQuick.Controls import QtQuick.Controls
import QtQuick.Layouts import QtQuick.Layouts
import Quickshell
import Quickshell.Io import Quickshell.Io
import qs.Commons import qs.Commons
import qs.Services import qs.Services
@ -9,36 +10,62 @@ import qs.Widgets
ColumnLayout { ColumnLayout {
id: root id: root
// Process to check if swww is installed ColumnLayout {
Process { spacing: Style.marginL * scaling
id: swwwCheck Layout.fillWidth: true
command: ["which", "swww"] NTextInput {
running: false label: "Wallpaper Directory"
description: "Path to your common wallpaper directory."
text: Settings.data.wallpaper.directory
onEditingFinished: {
Settings.data.wallpaper.directory = text
}
Layout.maximumWidth: 420 * scaling
}
onExited: function (exitCode) { // Monitor-specific directories
if (exitCode === 0) { NToggle {
// SWWW exists, enable it label: "Monitor-specific directories"
Settings.data.wallpaper.swww.enabled = true description: "Enable multi-monitor wallpaper directory management."
WallpaperService.startSWWWDaemon() checked: Settings.data.wallpaper.enableMultiMonitorDirectories
ToastService.showNotice("Swww", "Enabled") onToggled: checked => Settings.data.wallpaper.enableMultiMonitorDirectories = checked
} else { }
// SWWW not found
ToastService.showWarning("Swww", "Not installed") NBox {
visible: Settings.data.wallpaper.enableMultiMonitorDirectories
Layout.fillWidth: true
Layout.minimumWidth: 550 * scaling
radius: Style.radiusM * scaling
color: Color.mSurfaceVariant
border.color: Color.mOutline
border.width: Math.max(1, Style.borderS * scaling)
implicitHeight: contentCol.implicitHeight + Style.marginXL * 2 * scaling
ColumnLayout {
id: contentCol
anchors.fill: parent
anchors.margins: Style.marginXL * scaling
spacing: Style.marginM * scaling
Repeater {
model: Quickshell.screens || []
delegate: RowLayout {
NText {
text: (modelData.name || "Unknown")
color: Color.mSecondary
font.weight: Style.fontWeightBold
Layout.preferredWidth: 90 * scaling
}
NTextInput {
Layout.fillWidth: true
text: WallpaperService.getMonitorDirectory(modelData.name)
onEditingFinished: WallpaperService.setMonitorDirectory(modelData.name, text)
Layout.maximumWidth: 420 * scaling
}
}
}
} }
} }
stdout: StdioCollector {}
stderr: StdioCollector {}
}
NTextInput {
label: "Wallpaper Directory"
description: "Path to your wallpaper directory."
text: Settings.data.wallpaper.directory
onEditingFinished: {
Settings.data.wallpaper.directory = text
}
Layout.maximumWidth: 420 * scaling
} }
NDivider { NDivider {
@ -62,10 +89,42 @@ ColumnLayout {
NToggle { NToggle {
label: "Random Wallpaper" label: "Random Wallpaper"
description: "Automatically select random wallpapers from the folder." description: "Automatically select random wallpapers from the folder."
checked: Settings.data.wallpaper.isRandom checked: Settings.data.wallpaper.randomEnabled
onToggled: checked => { onToggled: checked => Settings.data.wallpaper.randomEnabled = checked
Settings.data.wallpaper.isRandom = checked }
}
// Transition Type
NComboBox {
label: "Transition Type"
description: "Animation type when switching between wallpapers."
model: WallpaperService.transitionsModel
currentKey: Settings.data.wallpaper.transitionType
onSelected: key => Settings.data.wallpaper.transitionType = key
}
// Transition Duration
ColumnLayout {
NLabel {
label: "Transition Duration"
description: "Duration of transition animations in seconds."
}
RowLayout {
spacing: Style.marginL * scaling
NSlider {
Layout.fillWidth: true
from: 100
to: 5000
stepSize: 100
value: Settings.data.wallpaper.transitionDuration
onMoved: Settings.data.wallpaper.transitionDuration = value
cutoutColor: Color.mSurface
}
NText {
text: (Settings.data.wallpaper.transitionDuration / 1000).toFixed(2) + "s"
Layout.alignment: Qt.AlignVCenter | Qt.AlignRight
}
}
} }
// Interval (slider + H:M inputs) // Interval (slider + H:M inputs)
@ -79,25 +138,28 @@ ColumnLayout {
NText { NText {
// Show friendly H:MM format from current settings // Show friendly H:MM format from current settings
text: Time.formatVagueHumanReadableDuration(Settings.data.wallpaper.randomInterval) text: Time.formatVagueHumanReadableDuration(Settings.data.wallpaper.randomIntervalSec)
Layout.alignment: Qt.AlignBottom | Qt.AlignRight Layout.alignment: Qt.AlignBottom | Qt.AlignRight
} }
} }
// Preset chips // Preset chips using Repeater
RowLayout { RowLayout {
id: presetRow id: presetRow
spacing: Style.marginS * scaling spacing: Style.marginS * scaling
// Preset seconds list // Factorized presets data
property var presets: [15 * 60, 30 * 60, 45 * 60, 60 * 60, 90 * 60, 120 * 60] property var intervalPresets: [5 * 60, 10 * 60, 15 * 60, 30 * 60, 45 * 60, 60 * 60, 90 * 60, 120 * 60]
// Whether current interval equals one of the presets // Whether current interval equals one of the presets
property bool isCurrentPreset: presets.indexOf(Settings.data.wallpaper.randomInterval) !== -1 property bool isCurrentPreset: {
return intervalPresets.some(seconds => seconds === Settings.data.wallpaper.randomIntervalSec)
}
// Allow user to force open the custom input; otherwise it's auto-open when not a preset // Allow user to force open the custom input; otherwise it's auto-open when not a preset
property bool customForcedVisible: false property bool customForcedVisible: false
function setIntervalSeconds(sec) { function setIntervalSeconds(sec) {
Settings.data.wallpaper.randomInterval = sec Settings.data.wallpaper.randomIntervalSec = sec
WallpaperService.restartRandomWallpaperTimer() WallpaperService.restartRandomWallpaperTimer()
// Hide custom when selecting a preset // Hide custom when selecting a preset
customForcedVisible = false customForcedVisible = false
@ -105,168 +167,25 @@ ColumnLayout {
// Helper to color selected chip // Helper to color selected chip
function isSelected(sec) { function isSelected(sec) {
return Settings.data.wallpaper.randomInterval === sec return Settings.data.wallpaper.randomIntervalSec === sec
} }
// 15m // Repeater for preset chips
Rectangle { Repeater {
radius: height * 0.5 model: presetRow.intervalPresets
color: presetRow.isSelected(15 * 60) ? Color.mPrimary : Color.mSurfaceVariant delegate: IntervalPresetChip {
implicitHeight: Math.max(Style.baseWidgetSize * 0.55 * scaling, 24 * scaling) seconds: modelData
implicitWidth: label15.implicitWidth + Style.marginM * 1.5 * scaling label: Time.formatVagueHumanReadableDuration(modelData)
border.width: 1 selected: presetRow.isSelected(modelData)
border.color: presetRow.isSelected(15 * 60) ? Color.transparent : Color.mOutline onClicked: presetRow.setIntervalSeconds(modelData)
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: presetRow.setIntervalSeconds(15 * 60)
}
NText {
id: label15
anchors.centerIn: parent
text: "15m"
font.pointSize: Style.fontSizeS * scaling
font.weight: Style.fontWeightMedium
color: presetRow.isSelected(15 * 60) ? Color.mOnPrimary : Color.mOnSurface
}
}
// 30m
Rectangle {
radius: height * 0.5
color: presetRow.isSelected(30 * 60) ? Color.mPrimary : Color.mSurfaceVariant
implicitHeight: Math.max(Style.baseWidgetSize * 0.55 * scaling, 24 * scaling)
implicitWidth: label30.implicitWidth + Style.marginM * 1.5 * scaling
border.width: 1
border.color: presetRow.isSelected(30 * 60) ? Color.transparent : Color.mOutline
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: presetRow.setIntervalSeconds(30 * 60)
}
NText {
id: label30
anchors.centerIn: parent
text: "30m"
font.pointSize: Style.fontSizeS * scaling
font.weight: Style.fontWeightMedium
color: presetRow.isSelected(30 * 60) ? Color.mOnPrimary : Color.mOnSurface
}
}
// 45m
Rectangle {
radius: height * 0.5
color: presetRow.isSelected(45 * 60) ? Color.mPrimary : Color.mSurfaceVariant
implicitHeight: Math.max(Style.baseWidgetSize * 0.55 * scaling, 24 * scaling)
implicitWidth: label45.implicitWidth + Style.marginM * 1.5 * scaling
border.width: 1
border.color: presetRow.isSelected(45 * 60) ? Color.transparent : Color.mOutline
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: presetRow.setIntervalSeconds(45 * 60)
}
NText {
id: label45
anchors.centerIn: parent
text: "45m"
font.pointSize: Style.fontSizeS * scaling
font.weight: Style.fontWeightMedium
color: presetRow.isSelected(45 * 60) ? Color.mOnPrimary : Color.mOnSurface
}
}
// 1h
Rectangle {
radius: height * 0.5
color: presetRow.isSelected(60 * 60) ? Color.mPrimary : Color.mSurfaceVariant
implicitHeight: Math.max(Style.baseWidgetSize * 0.55 * scaling, 24 * scaling)
implicitWidth: label1h.implicitWidth + Style.marginM * 1.5 * scaling
border.width: 1
border.color: presetRow.isSelected(60 * 60) ? Color.transparent : Color.mOutline
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: presetRow.setIntervalSeconds(60 * 60)
}
NText {
id: label1h
anchors.centerIn: parent
text: "1h"
font.pointSize: Style.fontSizeS * scaling
font.weight: Style.fontWeightMedium
color: presetRow.isSelected(60 * 60) ? Color.mOnPrimary : Color.mOnSurface
}
}
// 1h 30m
Rectangle {
radius: height * 0.5
color: presetRow.isSelected(90 * 60) ? Color.mPrimary : Color.mSurfaceVariant
implicitHeight: Math.max(Style.baseWidgetSize * 0.55 * scaling, 24 * scaling)
implicitWidth: label90.implicitWidth + Style.marginM * 1.5 * scaling
border.width: 1
border.color: presetRow.isSelected(90 * 60) ? Color.transparent : Color.mOutline
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: presetRow.setIntervalSeconds(90 * 60)
}
NText {
id: label90
anchors.centerIn: parent
text: "1h 30m"
font.pointSize: Style.fontSizeS * scaling
font.weight: Style.fontWeightMedium
color: presetRow.isSelected(90 * 60) ? Color.mOnPrimary : Color.mOnSurface
}
}
// 2h
Rectangle {
radius: height * 0.5
color: presetRow.isSelected(120 * 60) ? Color.mPrimary : Color.mSurfaceVariant
implicitHeight: Math.max(Style.baseWidgetSize * 0.55 * scaling, 24 * scaling)
implicitWidth: label2h.implicitWidth + Style.marginM * 1.5 * scaling
border.width: 1
border.color: presetRow.isSelected(120 * 60) ? Color.transparent : Color.mOutline
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: presetRow.setIntervalSeconds(120 * 60)
}
NText {
id: label2h
anchors.centerIn: parent
text: "2h"
font.pointSize: Style.fontSizeS * scaling
font.weight: Style.fontWeightMedium
color: presetRow.isSelected(120 * 60) ? Color.mOnPrimary : Color.mOnSurface
} }
} }
// Custom opens inline input // Custom opens inline input
Rectangle { IntervalPresetChip {
radius: height * 0.5 label: customRow.visible ? "Custom" : "Custom…"
color: customRow.visible ? Color.mPrimary : Color.mSurfaceVariant selected: customRow.visible
implicitHeight: Math.max(Style.baseWidgetSize * 0.55 * scaling, 24 * scaling) onClicked: presetRow.customForcedVisible = !presetRow.customForcedVisible
implicitWidth: labelCustom.implicitWidth + Style.marginM * 1.5 * scaling
border.width: 1
border.color: customRow.visible ? Color.transparent : Color.mOutline
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: presetRow.customForcedVisible = !presetRow.customForcedVisible
}
NText {
id: labelCustom
anchors.centerIn: parent
text: customRow.visible ? "Custom" : "Custom…"
font.pointSize: Style.fontSizeS * scaling
font.weight: Style.fontWeightMedium
color: customRow.visible ? Color.mOnPrimary : Color.mOnSurface
}
} }
} }
@ -282,12 +201,11 @@ ColumnLayout {
description: "Enter time as HH:MM (e.g., 01:30)." description: "Enter time as HH:MM (e.g., 01:30)."
inputMaxWidth: 100 * scaling inputMaxWidth: 100 * scaling
text: { text: {
const s = Settings.data.wallpaper.randomInterval const s = Settings.data.wallpaper.randomIntervalSec
const h = Math.floor(s / 3600) const h = Math.floor(s / 3600)
const m = Math.floor((s % 3600) / 60) const m = Math.floor((s % 3600) / 60)
return h + ":" + (m < 10 ? ("0" + m) : m) return h + ":" + (m < 10 ? ("0" + m) : m)
} }
onEditingFinished: { onEditingFinished: {
const m = text.trim().match(/^(\d{1,2}):(\d{2})$/) const m = text.trim().match(/^(\d{1,2}):(\d{2})$/)
if (m) { if (m) {
@ -297,7 +215,7 @@ ColumnLayout {
return return
h = Math.max(0, Math.min(24, h)) h = Math.max(0, Math.min(24, h))
min = Math.max(0, Math.min(59, min)) min = Math.max(0, Math.min(59, min))
Settings.data.wallpaper.randomInterval = (h * 3600) + (min * 60) Settings.data.wallpaper.randomIntervalSec = (h * 3600) + (min * 60)
WallpaperService.restartRandomWallpaperTimer() WallpaperService.restartRandomWallpaperTimer()
// Keep custom visible after manual entry // Keep custom visible after manual entry
presetRow.customForcedVisible = true presetRow.customForcedVisible = true
@ -308,193 +226,32 @@ ColumnLayout {
} }
} }
NDivider { // Reusable component for interval preset chips
Layout.fillWidth: true component IntervalPresetChip: Rectangle {
Layout.topMargin: Style.marginXL * scaling property int seconds: 0
Layout.bottomMargin: Style.marginXL * scaling property string label: ""
} property bool selected: false
signal clicked
// ------------------------------- radius: height * 0.5
// Swww color: selected ? Color.mPrimary : Color.mSurfaceVariant
ColumnLayout { implicitHeight: Math.max(Style.baseWidgetSize * 0.55 * scaling, 24 * scaling)
spacing: Style.marginL * scaling implicitWidth: chipLabel.implicitWidth + Style.marginM * 1.5 * scaling
Layout.fillWidth: true border.width: 1
border.color: selected ? Color.transparent : Color.mOutline
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: parent.clicked()
}
NText { NText {
text: "Swww" id: chipLabel
font.pointSize: Style.fontSizeXXL * scaling anchors.centerIn: parent
font.weight: Style.fontWeightBold text: parent.label
color: Color.mSecondary font.pointSize: Style.fontSizeS * scaling
} color: parent.selected ? Color.mOnPrimary : Color.mOnSurface
// Use SWWW
NToggle {
label: "Use Swww"
description: "Use Swww daemon for advanced wallpaper management."
checked: Settings.data.wallpaper.swww.enabled
onToggled: checked => {
if (checked) {
// Check if swww is installed
swwwCheck.running = true
} else {
Settings.data.wallpaper.swww.enabled = false
ToastService.showNotice("Swww", "Disabled")
}
}
}
// SWWW Settings (only visible when useSWWW is enabled)
ColumnLayout {
spacing: Style.marginS * scaling
Layout.fillWidth: true
Layout.topMargin: Style.marginS * scaling
visible: Settings.data.wallpaper.swww.enabled
// Resize Mode
NComboBox {
label: "Resize Mode"
description: "How Swww should resize wallpapers to fit the screen."
model: ListModel {
ListElement {
key: "no"
name: "No"
}
ListElement {
key: "crop"
name: "Crop"
}
ListElement {
key: "fit"
name: "Fit"
}
ListElement {
key: "stretch"
name: "Stretch"
}
}
currentKey: Settings.data.wallpaper.swww.resizeMethod
onSelected: key => {
Settings.data.wallpaper.swww.resizeMethod = key
}
}
// Transition Type
NComboBox {
label: "Transition Type"
description: "Animation type when switching between wallpapers."
model: ListModel {
ListElement {
key: "none"
name: "None"
}
ListElement {
key: "simple"
name: "Simple"
}
ListElement {
key: "fade"
name: "Fade"
}
ListElement {
key: "left"
name: "Left"
}
ListElement {
key: "right"
name: "Right"
}
ListElement {
key: "top"
name: "Top"
}
ListElement {
key: "bottom"
name: "Bottom"
}
ListElement {
key: "wipe"
name: "Wipe"
}
ListElement {
key: "wave"
name: "Wave"
}
ListElement {
key: "grow"
name: "Grow"
}
ListElement {
key: "center"
name: "Center"
}
ListElement {
key: "any"
name: "Any"
}
ListElement {
key: "outer"
name: "Outer"
}
ListElement {
key: "random"
name: "Random"
}
}
currentKey: Settings.data.wallpaper.swww.transitionType
onSelected: key => {
Settings.data.wallpaper.swww.transitionType = key
}
}
// Transition FPS
ColumnLayout {
NLabel {
label: "Transition FPS"
description: "Frames per second for transition animations."
}
RowLayout {
spacing: Style.marginL * scaling
NSlider {
Layout.fillWidth: true
from: 30
to: 500
stepSize: 5
value: Settings.data.wallpaper.swww.transitionFps
onMoved: Settings.data.wallpaper.swww.transitionFps = Math.round(value)
cutoutColor: Color.mSurface
}
NText {
text: Settings.data.wallpaper.swww.transitionFps + " FPS"
Layout.alignment: Qt.AlignVCenter | Qt.AlignRight
}
}
}
// Transition Duration
ColumnLayout {
NLabel {
label: "Transition Duration"
description: "Duration of transition animations in seconds."
}
RowLayout {
spacing: Style.marginL * scaling
NSlider {
Layout.fillWidth: true
from: 0.25
to: 10
stepSize: 0.05
value: Settings.data.wallpaper.swww.transitionDuration
onMoved: Settings.data.wallpaper.swww.transitionDuration = value
cutoutColor: Color.mSurface
}
NText {
text: Settings.data.wallpaper.swww.transitionDuration.toFixed(2) + "s"
Layout.alignment: Qt.AlignVCenter | Qt.AlignRight
}
}
}
} }
} }

View file

@ -27,11 +27,11 @@ Features a modern modular architecture with a status bar, notification system, c
## Preview ## Preview
![Launcher](https://assets.noctalia.dev/screenshots/launcher.png?v=2) ![Launcher](/Assets/Screenshots/launcher.png)
![SettingsPanel](https://assets.noctalia.dev/screenshots/settings-panel.png?v=2) ![SettingsPanel](/Assets/Screenshots/settings-panel.png?v=2)
![SidePanel](https://assets.noctalia.dev/screenshots/light-mode.png?v=2) ![SidePanel](/Assets/Screenshots/light-mode.png?v=2)
--- ---
@ -75,21 +75,14 @@ Features a modern modular architecture with a status bar, notification system, c
### Optional ### Optional
- `cliphist` - For clipboard history support - `cliphist` - For clipboard history support
- `swww` - Wallpaper animations and effects
- `matugen` - Material You color scheme generation - `matugen` - Material You color scheme generation
- `cava` - Audio visualizer component - `cava` - Audio visualizer component
- `wlsunset` - To be able to use NightLight - `wlsunset` - To be able to use NightLight
> There is one more optional dependency. > There are 2 more optional dependencies.
> Any `polkit agent` to be able to use the ArchUpdater widget.
> And also any `xdg-desktop-portal` to be able to use the "Portal" option from the screenRecorder. > And also any `xdg-desktop-portal` to be able to use the "Portal" option from the screenRecorder.
If you want to use the ArchUpdater please make sure to set your terminal with the `TERMINAL` environment variable.
```
sudo sed -i '/^TERMINAL=/d' /etc/environment && echo 'TERMINAL=/usr/bin/kitty' | sudo tee -a /etc/environment
```
In that command you **NEED** to replace kitty with whatever your terminal is (and perhaps edit the path depending on the distro).
--- ---
## Quick Start ## Quick Start
@ -276,14 +269,6 @@ The launcher supports special commands for enhanced functionality:
## Advanced Configuration ## Advanced Configuration
### Niri Configuration
Add this to your `layout` section for proper swww integration:
```
background-color "transparent"
```
### Recommended Compositor Settings ### Recommended Compositor Settings
For Niri: For Niri:
@ -294,11 +279,6 @@ window-rule {
clip-to-geometry true clip-to-geometry true
} }
layer-rule {
match namespace="^swww-daemon$"
place-within-backdrop true
}
layer-rule { layer-rule {
match namespace="^quickshell-wallpaper$" match namespace="^quickshell-wallpaper$"
} }

View file

@ -8,164 +8,28 @@ import qs.Commons
Singleton { Singleton {
id: updateService id: updateService
// ============================================================================ // Core properties
// CORE PROPERTIES readonly property bool busy: checkupdatesProcess.running
// ============================================================================ readonly property bool aurBusy: checkAurUpdatesProcess.running
readonly property int updates: repoPackages.length
// Package data readonly property int aurUpdates: aurPackages.length
readonly property int totalUpdates: updates + aurUpdates
property var repoPackages: [] property var repoPackages: []
property var aurPackages: [] property var aurPackages: []
property var selectedPackages: [] property var selectedPackages: []
property int selectedPackagesCount: 0 property int selectedPackagesCount: 0
// Update state
property bool updateInProgress: false property bool updateInProgress: false
property bool updateFailed: false
property string lastUpdateError: ""
// Computed properties
readonly property bool busy: checkupdatesProcess.running
readonly property bool aurBusy: checkParuUpdatesProcess.running
readonly property int updates: repoPackages.length
readonly property int aurUpdates: aurPackages.length
readonly property int totalUpdates: updates + aurUpdates
// ============================================================================
// TIMERS
// ============================================================================
// Refresh timer for post-update polling
Timer {
id: refreshTimer
interval: 5000 // Increased delay to ensure updates complete
repeat: false
onTriggered: {
console.log("ArchUpdater: Refreshing package lists after update...")
// Just refresh package lists without syncing database
doPoll()
}
}
// Timer to mark update as complete - with error handling
Timer {
id: updateCompleteTimer
interval: 30000 // Increased to 30 seconds to allow more time
repeat: false
onTriggered: {
console.log("ArchUpdater: Update timeout reached, checking for failures...")
checkForUpdateFailures()
}
}
// Timer to check if update processes are still running
Timer {
id: updateMonitorTimer
interval: 2000
repeat: true
running: updateInProgress
onTriggered: {
// Check if any update-related processes might still be running
checkUpdateStatus()
}
}
// ============================================================================
// MONITORING PROCESSES
// ============================================================================
// Process to monitor update completion
Process {
id: updateStatusProcess
command: ["pgrep", "-f", "(pacman|yay|paru).*(-S|-Syu)"]
onExited: function (exitCode) {
if (exitCode !== 0 && updateInProgress) {
// No update processes found, update likely completed
console.log("ArchUpdater: No update processes detected, marking update as complete")
updateInProgress = false
updateMonitorTimer.stop()
// Don't stop the complete timer - let it handle failures
// If the update actually failed, the timer will trigger and set updateFailed = true
// Refresh package lists after a short delay
Qt.callLater(() => {
doPoll()
}, 2000)
}
}
}
// Process to check for errors in log file (only when update is in progress)
Process {
id: errorCheckProcess
command: ["sh", "-c", "if [ -f /tmp/archupdater_output.log ]; then grep -i 'error\\|failed\\|failed to build\\|ERROR_DETECTED' /tmp/archupdater_output.log | tail -1; fi"]
onExited: function (exitCode) {
if (exitCode === 0 && updateInProgress) {
// Error found in log
console.log("ArchUpdater: Error detected in log file")
updateInProgress = false
updateFailed = true
updateCompleteTimer.stop()
updateMonitorTimer.stop()
lastUpdateError = "Build or update error detected"
// Refresh to check actual state
Qt.callLater(() => {
doPoll()
}, 1000)
}
}
}
// Timer to check for errors more frequently when update is in progress
Timer {
id: errorCheckTimer
interval: 5000 // Check every 5 seconds
repeat: true
running: updateInProgress
onTriggered: {
if (updateInProgress && !errorCheckProcess.running) {
errorCheckProcess.running = true
}
}
}
// ============================================================================
// MONITORING FUNCTIONS
// ============================================================================
function checkUpdateStatus() {
if (updateInProgress && !updateStatusProcess.running) {
updateStatusProcess.running = true
}
}
function checkForUpdateFailures() {
console.log("ArchUpdater: Checking for update failures...")
updateInProgress = false
updateFailed = true
updateCompleteTimer.stop()
updateMonitorTimer.stop()
// Refresh to check actual state after a delay
Qt.callLater(() => {
doPoll()
}, 2000)
}
// Initial check // Initial check
Component.onCompleted: { Component.onCompleted: {
getAurHelper()
doPoll() doPoll()
doAurPoll()
} }
// ============================================================================
// PACKAGE CHECKING PROCESSES
// ============================================================================
// Process for checking repo updates // Process for checking repo updates
Process { Process {
id: checkupdatesProcess id: checkupdatesProcess
command: ["checkupdates", "--nosync"] command: ["checkupdates"]
onExited: function (exitCode) { onExited: function (exitCode) {
if (exitCode !== 0 && exitCode !== 2) { if (exitCode !== 0 && exitCode !== 2) {
Logger.warn("ArchUpdater", "checkupdates failed (code:", exitCode, ")") Logger.warn("ArchUpdater", "checkupdates failed (code:", exitCode, ")")
@ -180,13 +44,13 @@ Singleton {
} }
} }
// Process for checking AUR updates with paru specifically // Process for checking AUR updates
Process { Process {
id: checkParuUpdatesProcess id: checkAurUpdatesProcess
command: ["paru", "-Qua"] command: ["sh", "-c", "command -v yay >/dev/null 2>&1 && yay -Qua || command -v paru >/dev/null 2>&1 && paru -Qua || echo ''"]
onExited: function (exitCode) { onExited: function (exitCode) {
if (exitCode !== 0) { if (exitCode !== 0) {
Logger.warn("ArchUpdater", "paru check failed (code:", exitCode, ")") Logger.warn("ArchUpdater", "AUR check failed (code:", exitCode, ")")
aurPackages = [] aurPackages = []
} }
} }
@ -198,12 +62,8 @@ Singleton {
} }
} }
// ============================================================================ // Parse checkupdates output
// PARSING FUNCTIONS function parseCheckupdatesOutput(output) {
// ============================================================================
// Generic package parsing function
function parsePackageOutput(output, source) {
const lines = output.trim().split('\n').filter(line => line.trim()) const lines = output.trim().split('\n').filter(line => line.trim())
const packages = [] const packages = []
@ -215,83 +75,65 @@ Singleton {
"oldVersion": m[2], "oldVersion": m[2],
"newVersion": m[3], "newVersion": m[3],
"description": `${m[1]} ${m[2]} -> ${m[3]}`, "description": `${m[1]} ${m[2]} -> ${m[3]}`,
"source": source "source": "repo"
}) })
} }
} }
// Only update if we have new data or if this is a fresh check repoPackages = packages
if (packages.length > 0 || output.trim() === "") {
if (source === "repo") {
repoPackages = packages
} else {
aurPackages = packages
}
}
}
// Parse checkupdates output
function parseCheckupdatesOutput(output) {
parsePackageOutput(output, "repo")
} }
// Parse AUR updates output // Parse AUR updates output
function parseAurUpdatesOutput(output) { function parseAurUpdatesOutput(output) {
parsePackageOutput(output, "aur") const lines = output.trim().split('\n').filter(line => line.trim())
const packages = []
for (const line of lines) {
const m = line.match(/^(\S+)\s+([^\s]+)\s+->\s+([^\s]+)$/)
if (m) {
packages.push({
"name": m[1],
"oldVersion": m[2],
"newVersion": m[3],
"description": `${m[1]} ${m[2]} -> ${m[3]}`,
"source": "aur"
})
}
}
aurPackages = packages
} }
// Check for updates
function doPoll() { function doPoll() {
// Start repo updates check if (busy)
if (!busy) { return
checkupdatesProcess.running = true checkupdatesProcess.running = true
}
// Start AUR updates check
if (!aurBusy) {
checkParuUpdatesProcess.running = true
}
} }
// ============================================================================ // Check for AUR updates
// UPDATE FUNCTIONS function doAurPoll() {
// ============================================================================ if (aurBusy)
return
// Helper function to generate update command with error detection checkAurUpdatesProcess.running = true
function generateUpdateCommand(baseCommand) {
return baseCommand + " 2>&1 | tee /tmp/archupdater_output.log; if [ $? -ne 0 ]; then echo 'ERROR_DETECTED'; fi; echo 'Update complete! Press Enter to close...'; read -p 'Press Enter to continue...'"
} }
// Update all packages (repo + AUR) // Update all packages (repo + AUR)
function runUpdate() { function runUpdate() {
if (totalUpdates === 0) { if (totalUpdates === 0) {
doPoll() doPoll()
doAurPoll()
return return
} }
// Reset any previous error states
updateFailed = false
lastUpdateError = ""
updateInProgress = true updateInProgress = true
console.log("ArchUpdater: Starting full system update...") // Update repos first, then AUR
Quickshell.execDetached(["pkexec", "pacman", "-Syu", "--noconfirm"])
Quickshell.execDetached(
["sh", "-c", "command -v yay >/dev/null 2>&1 && yay -Sua --noconfirm || command -v paru >/dev/null 2>&1 && paru -Sua --noconfirm || true"])
const terminal = Quickshell.env("TERMINAL") || "xterm" // Refresh after updates with multiple attempts
refreshAfterUpdate()
// Check if we have an AUR helper for full system update
const aurHelper = getAurHelper()
if (aurHelper && (aurUpdates > 0 || updates > 0)) {
// Use AUR helper for full system update (handles both repo and AUR)
const command = generateUpdateCommand(aurHelper + " -Syu")
Quickshell.execDetached([terminal, "-e", "bash", "-c", command])
} else if (updates > 0) {
// Fallback to pacman if no AUR helper or only repo updates
const command = generateUpdateCommand("sudo pacman -Syu")
Quickshell.execDetached([terminal, "-e", "bash", "-c", command])
}
// Start monitoring and timeout timers
refreshTimer.start()
updateCompleteTimer.start()
updateMonitorTimer.start()
} }
// Update selected packages // Update selected packages
@ -299,13 +141,7 @@ Singleton {
if (selectedPackages.length === 0) if (selectedPackages.length === 0)
return return
// Reset any previous error states
updateFailed = false
lastUpdateError = ""
updateInProgress = true updateInProgress = true
console.log("ArchUpdater: Starting selective update for", selectedPackages.length, "packages")
const terminal = Quickshell.env("TERMINAL") || "xterm"
// Split selected packages by source // Split selected packages by source
const repoPkgs = [] const repoPkgs = []
@ -323,140 +159,48 @@ Singleton {
} }
} }
// Update repo packages with sudo // Update repo packages
if (repoPkgs.length > 0) { if (repoPkgs.length > 0) {
const packageList = repoPkgs.join(" ") const repoCommand = ["pkexec", "pacman", "-S", "--noconfirm"].concat(repoPkgs)
const command = generateUpdateCommand("sudo pacman -S " + packageList) Logger.log("ArchUpdater", "Running repo command:", repoCommand.join(" "))
Quickshell.execDetached([terminal, "-e", "bash", "-c", command]) Quickshell.execDetached(repoCommand)
} }
// Update AUR packages with yay/paru // Update AUR packages
if (aurPkgs.length > 0) { if (aurPkgs.length > 0) {
const aurHelper = getAurHelper() const aurHelper = getAurHelper()
if (aurHelper) { if (aurHelper) {
const packageList = aurPkgs.join(" ") const aurCommand = [aurHelper, "-S", "--noconfirm"].concat(aurPkgs)
const command = generateUpdateCommand(aurHelper + " -S " + packageList) Logger.log("ArchUpdater", "Running AUR command:", aurCommand.join(" "))
Quickshell.execDetached([terminal, "-e", "bash", "-c", command]) Quickshell.execDetached(aurCommand)
} else { } else {
Logger.warn("ArchUpdater", "No AUR helper found for packages:", aurPkgs.join(", ")) Logger.warn("ArchUpdater", "No AUR helper found for packages:", aurPkgs.join(", "))
} }
} }
// Start monitoring and timeout timers // Clear selection and refresh
refreshTimer.start() selectedPackages = []
updateCompleteTimer.start() selectedPackagesCount = 0
updateMonitorTimer.start() refreshAfterUpdate()
} }
// Reset update state (useful for manual recovery)
function resetUpdateState() {
// If update is in progress, mark it as failed first
if (updateInProgress) {
updateFailed = true
}
updateInProgress = false
lastUpdateError = ""
updateCompleteTimer.stop()
updateMonitorTimer.stop()
refreshTimer.stop()
// Refresh to get current state
doPoll()
}
// Manual refresh function
function forceRefresh() {
// Prevent multiple simultaneous refreshes
if (busy || aurBusy) {
return
}
// Clear error states when refreshing
updateFailed = false
lastUpdateError = ""
// Just refresh the package lists without syncing databases
doPoll()
}
// ============================================================================
// UTILITY PROCESSES
// ============================================================================
// Process for checking yay availability
Process {
id: yayCheckProcess
command: ["which", "yay"]
onExited: function (exitCode) {
if (exitCode === 0) {
cachedAurHelper = "yay"
}
}
}
// Process for checking paru availability
Process {
id: paruCheckProcess
command: ["which", "paru"]
onExited: function (exitCode) {
if (exitCode === 0) {
if (cachedAurHelper === "") {
cachedAurHelper = "paru"
}
}
}
}
// Process for syncing package databases with sudo
Process {
id: syncDatabaseProcess
command: ["sudo", "pacman", "-Sy"]
onStarted: {
console.log("ArchUpdater: Starting database sync with sudo...")
}
onExited: function (exitCode) {
console.log("ArchUpdater: Database sync exited with code:", exitCode)
if (exitCode === 0) {
console.log("ArchUpdater: Database sync successful")
} else {
console.log("ArchUpdater: Database sync failed")
}
// After sync completes, wait a moment then refresh package lists
console.log("ArchUpdater: Database sync complete, waiting before refresh...")
Qt.callLater(() => {
console.log("ArchUpdater: Refreshing package lists after database sync...")
doPoll()
}, 2000)
}
}
// Cached AUR helper detection
property string cachedAurHelper: ""
// Helper function to detect AUR helper // Helper function to detect AUR helper
function getAurHelper() { function getAurHelper() {
// Return cached result if available // Check for yay first, then paru
if (cachedAurHelper !== "") { const yayCheck = Quickshell.exec("command -v yay", true)
return cachedAurHelper if (yayCheck.exitCode === 0 && yayCheck.stdout.trim()) {
return "yay"
} }
// Check for AUR helpers using Process objects const paruCheck = Quickshell.exec("command -v paru", true)
console.log("ArchUpdater: Detecting AUR helper...") if (paruCheck.exitCode === 0 && paruCheck.stdout.trim()) {
return "paru"
}
// Start the detection processes return null
yayCheckProcess.running = true
paruCheckProcess.running = true
// For now, return a default (will be updated by the processes)
// In a real implementation, you'd want to wait for the processes to complete
return "paru" // Default fallback
} }
// ============================================================================ // Package selection functions
// PACKAGE SELECTION FUNCTIONS
// ============================================================================
function togglePackageSelection(packageName) { function togglePackageSelection(packageName) {
const index = selectedPackages.indexOf(packageName) const index = selectedPackages.indexOf(packageName)
if (index > -1) { if (index > -1) {
@ -481,60 +225,47 @@ Singleton {
return selectedPackages.indexOf(packageName) > -1 return selectedPackages.indexOf(packageName) > -1
} }
// ============================================================================ // Robust refresh after updates
// REFRESH FUNCTIONS function refreshAfterUpdate() {
// ============================================================================ // First refresh attempt after 3 seconds
Qt.callLater(() => {
doPoll()
doAurPoll()
}, 3000)
// Function to manually sync package databases (separate from refresh) // Second refresh attempt after 8 seconds
function syncPackageDatabases() { Qt.callLater(() => {
console.log("ArchUpdater: Manual database sync requested...") doPoll()
const terminal = Quickshell.env("TERMINAL") || "xterm" doAurPoll()
const command = "sudo pacman -Sy && echo 'Database sync complete! Press Enter to close...' && read -p 'Press Enter to continue...'" }, 8000)
console.log("ArchUpdater: Executing sync command:", command)
console.log("ArchUpdater: Terminal:", terminal) // Third refresh attempt after 15 seconds
Quickshell.execDetached([terminal, "-e", "bash", "-c", command]) Qt.callLater(() => {
doPoll()
doAurPoll()
updateInProgress = false
}, 15000)
// Final refresh attempt after 30 seconds
Qt.callLater(() => {
doPoll()
doAurPoll()
}, 30000)
} }
// Function to force a complete refresh (sync + check)
function forceCompleteRefresh() {
console.log("ArchUpdater: Force complete refresh requested...")
// Start database sync process (will trigger refresh when complete)
console.log("ArchUpdater: Starting complete refresh process...")
syncDatabaseProcess.running = true
}
// Function to sync database and refresh package lists
function syncDatabaseAndRefresh() {
console.log("ArchUpdater: Syncing database and refreshing package lists...")
// Start database sync process (will trigger refresh when complete)
console.log("ArchUpdater: Starting database sync process...")
syncDatabaseProcess.running = true
}
// ============================================================================
// UTILITY FUNCTIONS
// ============================================================================
// Notification helper // Notification helper
function notify(title, body) { function notify(title, body) {
Quickshell.execDetached(["notify-send", "-a", "UpdateService", "-i", "system-software-update", title, body]) Quickshell.execDetached(["notify-send", "-a", "UpdateService", "-i", "system-software-update", title, body])
} }
// ============================================================================
// AUTO-POLL TIMER
// ============================================================================
// Auto-poll every 15 minutes // Auto-poll every 15 minutes
Timer { Timer {
interval: 15 * 60 * 1000 // 15 minutes interval: 15 * 60 * 1000 // 15 minutes
repeat: true repeat: true
running: true running: true
onTriggered: { onTriggered: {
if (!updateInProgress) { doPoll()
doPoll() doAurPoll()
}
} }
} }
} }

View file

@ -22,9 +22,11 @@ Singleton {
// Ensure cache dir exists // Ensure cache dir exists
Quickshell.execDetached(["mkdir", "-p", Settings.cacheDir]) Quickshell.execDetached(["mkdir", "-p", Settings.cacheDir])
Logger.log("Matugen", "Generating from wallpaper on screen:", Screen.name)
var wp = WallpaperService.getWallpaper(Screen.name).replace(/'/g, "'\\''")
var content = buildConfigToml() var content = buildConfigToml()
var mode = Settings.data.colorSchemes.darkMode ? "dark" : "light" var mode = Settings.data.colorSchemes.darkMode ? "dark" : "light"
var wp = WallpaperService.currentWallpaper.replace(/'/g, "'\\''")
var pathEsc = dynamicConfigPath.replace(/'/g, "'\\''") var pathEsc = dynamicConfigPath.replace(/'/g, "'\\''")
var extraRepo = (Quickshell.shellDir + "/Assets/Matugen/extra").replace(/'/g, "'\\''") var extraRepo = (Quickshell.shellDir + "/Assets/Matugen/extra").replace(/'/g, "'\\''")
var extraUser = (Settings.configDir + "matugen.d").replace(/'/g, "'\\''") var extraUser = (Settings.configDir + "matugen.d").replace(/'/g, "'\\''")

View file

@ -11,7 +11,7 @@ Singleton {
function scale(aScreen) { function scale(aScreen) {
try { try {
if (aScreen !== undefined && aScreen.name !== undefined) { if (aScreen !== undefined && aScreen.name !== undefined) {
return scaleByName(aScreen.name) return getMonitorScale(aScreen.name)
} }
} catch (e) { } catch (e) {
@ -20,21 +20,46 @@ Singleton {
return 1.0 return 1.0
} }
function scaleByName(aScreenName) { // -------------------------------------------
function getMonitorScale(aScreenName) {
try { try {
if (Settings.data.monitorsScaling !== undefined) { var monitors = Settings.data.ui.monitorsScaling
if (Settings.data.monitorsScaling[aScreenName] !== undefined) { if (monitors !== undefined) {
return Settings.data.monitorsScaling[aScreenName] for (var i = 0; i < monitors.length; i++) {
if (monitors[i].name !== undefined && monitors[i].name === aScreenName) {
return monitors[i].scale
}
} }
} }
} catch (e) { } catch (e) {
//Logger.warn(e) //Logger.warn(e)
} }
return 1.0 return 1.0
} }
// -------------------------------------------
function setMonitorScale(aScreenName, scale) {
try {
var monitors = Settings.data.ui.monitorsScaling
if (monitors !== undefined) {
for (var i = 0; i < monitors.length; i++) {
if (monitors[i].name !== undefined && monitors[i].name === aScreenName) {
monitors[i].scale = scale
return
}
}
}
monitors.push({
"name": aScreenName,
"scale": scale
})
} catch (e) {
//Logger.warn(e)
}
}
// ------------------------------------------- // -------------------------------------------
// Dynamic scaling based on resolution // Dynamic scaling based on resolution

View file

@ -10,84 +10,197 @@ Singleton {
id: root id: root
Component.onCompleted: { Component.onCompleted: {
Logger.log("Wallpapers", "Service started") Logger.log("Wallpaper", "Service started")
listWallpapers()
// Wallpaper is set when the settings are loaded.
// Don't start random wallpaper during initialization
} }
property var wallpaperList: [] // All available wallpaper transitions
property string currentWallpaper: Settings.data.wallpaper.current readonly property ListModel transitionsModel: ListModel {
property bool scanning: false ListElement {
key: "none"
// SWWW name: "None"
property string transitionType: Settings.data.wallpaper.swww.transitionType
property var randomChoices: ["simple", "fade", "left", "right", "top", "bottom", "wipe", "wave", "grow", "center", "any", "outer"]
function listWallpapers() {
Logger.log("Wallpapers", "Listing wallpapers")
scanning = true
wallpaperList = []
// Set the folder directly to avoid model reset issues
folderModel.folder = "file://" + (Settings.data.wallpaper.directory !== undefined ? Settings.data.wallpaper.directory : "")
}
function changeWallpaper(path) {
Logger.log("Wallpapers", "Changing to:", path)
setCurrentWallpaper(path, false)
}
function setCurrentWallpaper(path, isInitial) {
// Only regenerate colors if the wallpaper actually changed
var wallpaperChanged = currentWallpaper !== path
currentWallpaper = path
if (!isInitial) {
Settings.data.wallpaper.current = path
} }
if (Settings.data.wallpaper.swww.enabled) { ListElement {
if (Settings.data.wallpaper.swww.transitionType === "random") { key: "fade"
transitionType = randomChoices[Math.floor(Math.random() * randomChoices.length)] name: "Fade"
} else { }
transitionType = Settings.data.wallpaper.swww.transitionType ListElement {
key: "wipe_left"
name: "Wipe Left"
}
ListElement {
key: "wipe_right"
name: "Wipe Right"
}
ListElement {
key: "wipe_up"
name: "Wipe Up"
}
ListElement {
key: "wipe_down"
name: "Wipe Down"
}
}
property var wallpaperLists: ({})
property int scanningCount: 0
readonly property bool scanning: (scanningCount > 0)
Connections {
target: Settings.data.wallpaper
function onDirectoryChanged() {
root.refreshWallpapersList()
}
function onRandomEnabledChanged() {
root.toggleRandomWallpaper()
}
function onRandomIntervalSecChanged() {
root.restartRandomWallpaperTimer()
}
}
// -------------------------------------------------------------------
// Get specific monitor wallpaper data
function getMonitorConfig(screenName) {
var monitors = Settings.data.wallpaper.monitors
if (monitors !== undefined) {
for (var i = 0; i < monitors.length; i++) {
if (monitors[i].name !== undefined && monitors[i].name === screenName) {
return monitors[i]
}
} }
}
}
changeWallpaperProcess.running = true // -------------------------------------------------------------------
} else { // Get specific monitor directory
function getMonitorDirectory(screenName) {
// Fallback: update the settings directly for non-SWWW mode if (!Settings.data.wallpaper.enableMultiMonitorDirectories) {
//Logger.log("Wallpapers", "Not using Swww, setting wallpaper directly") return Settings.data.wallpaper.directory
} }
var monitor = getMonitorConfig(screenName)
if (monitor !== undefined && monitor.directory !== undefined) {
return monitor.directory
}
// Fall back to the main/single directory
return Settings.data.wallpaper.directory
}
// -------------------------------------------------------------------
// Set specific monitor directory
function setMonitorDirectory(screenName, directory) {
var monitor = getMonitorConfig(screenName)
if (monitor !== undefined) {
monitor.directory = directory
} else {
Settings.data.wallpaper.monitors.push({
"name": screenName,
"directory": directory,
"wallpaper": ""
})
}
}
// -------------------------------------------------------------------
// Get specific monitor wallpaper
function getWallpaper(screenName) {
var monitor = getMonitorConfig(screenName)
if ((monitor !== undefined) && (monitor["wallpaper"] !== undefined)) {
return monitor["wallpaper"]
}
return ""
}
// -------------------------------------------------------------------
function changeWallpaper(screenName, path) {
if (screenName !== undefined) {
setWallpaper(screenName, path)
} else {
// If no screenName specified change for all screens
for (var i = 0; i < Quickshell.screens.length; i++) {
setWallpaper(Quickshell.screens[i].name, path)
}
}
}
// -------------------------------------------------------------------
function setWallpaper(screenName, path) {
if (path === "" || path === undefined) {
return
}
if (screenName === undefined) {
Logger.warn("Wallpaper", "setWallpaper", "no screen specified")
return
}
Logger.log("Wallpaper", "setWallpaper on", screenName, ": ", path)
var wallpaperChanged = false
var monitor = getMonitorConfig(screenName)
if (monitor !== undefined) {
wallpaperChanged = (monitor["wallpaper"] !== path)
monitor["wallpaper"] = path
} else {
wallpaperChanged = true
Settings.data.wallpaper.monitors.push({
"name": screenName,
"directory": getMonitorDirectory(screenName),
"wallpaper": path
})
}
// Restart the random wallpaper timer
if (randomWallpaperTimer.running) { if (randomWallpaperTimer.running) {
randomWallpaperTimer.restart() randomWallpaperTimer.restart()
} }
// Only notify ColorScheme service if the wallpaper actually changed // Notify ColorScheme service if the wallpaper actually changed
if (wallpaperChanged) { if (wallpaperChanged) {
ColorSchemeService.changedWallpaper() ColorSchemeService.changedWallpaper()
} }
} }
// -------------------------------------------------------------------
function setRandomWallpaper() { function setRandomWallpaper() {
var randomIndex = Math.floor(Math.random() * wallpaperList.length) Logger.log("Wallpaper", "setRandomWallpaper")
var randomPath = wallpaperList[randomIndex]
if (!randomPath) { if (Settings.data.wallpaper.enableMultiMonitorDirectories) {
return // Pick a random wallpaper per screen
for (var i = 0; i < Quickshell.screens.length; i++) {
var screenName = Quickshell.screens[i].name
var wallpaperList = getWallpapersList(screenName)
if (wallpaperList.length > 0) {
var randomIndex = Math.floor(Math.random() * wallpaperList.length)
var randomPath = wallpaperList[randomIndex]
changeWallpaper(screenName, randomPath)
}
}
} else {
// Pick a random wallpaper common to all screens
// We can use any screenName here, so we just pick the primary one.
var wallpaperList = getWallpapersList(Screen.name)
if (wallpaperList.length > 0) {
var randomIndex = Math.floor(Math.random() * wallpaperList.length)
var randomPath = wallpaperList[randomIndex]
changeWallpaper(undefined, randomPath)
}
} }
setCurrentWallpaper(randomPath, false)
} }
// -------------------------------------------------------------------
function toggleRandomWallpaper() { function toggleRandomWallpaper() {
if (Settings.data.wallpaper.isRandom && !randomWallpaperTimer.running) { Logger.log("Wallpaper", "toggleRandomWallpaper")
randomWallpaperTimer.start() if (Settings.data.wallpaper.randomEnabled) {
randomWallpaperTimer.restart()
setRandomWallpaper() setRandomWallpaper()
} else if (!Settings.data.randomWallpaper && randomWallpaperTimer.running) {
randomWallpaperTimer.stop()
} }
} }
// -------------------------------------------------------------------
function restartRandomWallpaperTimer() { function restartRandomWallpaperTimer() {
if (Settings.data.wallpaper.isRandom) { if (Settings.data.wallpaper.isRandom) {
randomWallpaperTimer.stop() randomWallpaperTimer.stop()
@ -95,78 +208,81 @@ Singleton {
} }
} }
function startSWWWDaemon() { // -------------------------------------------------------------------
if (Settings.data.wallpaper.swww.enabled) { function getWallpapersList(screenName) {
Logger.log("Swww", "Requesting swww-daemon") if (screenName != undefined && wallpaperLists[screenName] != undefined) {
startDaemonProcess.running = true return wallpaperLists[screenName]
}
return []
}
// -------------------------------------------------------------------
function refreshWallpapersList() {
Logger.log("Wallpaper", "refreshWallpapersList")
scanningCount = 0
// Force refresh by toggling the folder property on each FolderListModel
for (var i = 0; i < wallpaperScanners.count; i++) {
var scanner = wallpaperScanners.objectAt(i)
if (scanner) {
var currentFolder = scanner.folder
scanner.folder = ""
scanner.folder = currentFolder
}
} }
} }
// -------------------------------------------------------------------
// -------------------------------------------------------------------
// -------------------------------------------------------------------
Timer { Timer {
id: randomWallpaperTimer id: randomWallpaperTimer
interval: Settings.data.wallpaper.randomInterval * 1000 interval: Settings.data.wallpaper.randomIntervalSec * 1000
running: false running: Settings.data.wallpaper.randomEnabled
repeat: true repeat: true
onTriggered: setRandomWallpaper() onTriggered: setRandomWallpaper()
triggeredOnStart: false triggeredOnStart: false
} }
FolderListModel { // Instantiator (not Repeater) to create FolderListModel for each monitor
id: folderModel Instantiator {
// Swww supports many images format but Quickshell only support a subset of those. id: wallpaperScanners
nameFilters: ["*.jpg", "*.jpeg", "*.png", "*.gif", "*.pnm", "*.bmp"] model: Quickshell.screens
showDirs: false delegate: FolderListModel {
sortField: FolderListModel.Name property string screenName: modelData.name
onStatusChanged: {
if (status === FolderListModel.Ready) { folder: "file://" + root.getMonitorDirectory(screenName)
var files = [] nameFilters: ["*.jpg", "*.jpeg", "*.png", "*.gif", "*.pnm", "*.bmp"]
for (var i = 0; i < count; i++) { showDirs: false
var directory = (Settings.data.wallpaper.directory !== undefined ? Settings.data.wallpaper.directory : "") sortField: FolderListModel.Name
var filepath = directory + "/" + get(i, "fileName") onStatusChanged: {
files.push(filepath) if (status === FolderListModel.Null) {
// Flush the list
var lists = root.wallpaperLists
lists[screenName] = []
root.wallpaperLists = lists
} else if (status === FolderListModel.Loading) {
// Flush the list
var lists = root.wallpaperLists
lists[screenName] = []
root.wallpaperLists = lists
scanningCount++
} else if (status === FolderListModel.Ready) {
var files = []
for (var i = 0; i < count; i++) {
var directory = root.getMonitorDirectory(screenName)
var filepath = directory + "/" + get(i, "fileName")
files.push(filepath)
}
var lists = root.wallpaperLists
lists[screenName] = files
root.wallpaperLists = lists
scanningCount--
Logger.log("Wallpaper", "List refreshed for", screenName, "count:", files.length)
} }
wallpaperList = files
scanning = false
Logger.log("Wallpapers", "List refreshed, count:", wallpaperList.length)
}
}
}
Process {
id: changeWallpaperProcess
command: ["swww", "img", "--resize", Settings.data.wallpaper.swww.resizeMethod, "--transition-fps", Settings.data.wallpaper.swww.transitionFps.toString(
), "--transition-type", transitionType, "--transition-duration", Settings.data.wallpaper.swww.transitionDuration.toString(
), currentWallpaper]
running: false
onStarted: {
}
onExited: function (exitCode, exitStatus) {
Logger.log("Swww", "Process finished with exit code:", exitCode, "status:", exitStatus)
if (exitCode !== 0) {
Logger.log("Swww", "Process failed. Make sure swww-daemon is running with: swww-daemon")
Logger.log("Swww", "You can start it with: swww-daemon --format xrgb")
}
}
}
Process {
id: startDaemonProcess
command: ["swww-daemon", "--format", "xrgb"]
running: false
onStarted: {
Logger.log("Swww", "Daemon start process initiated")
}
onExited: function (exitCode, exitStatus) {
Logger.log("Swww", "Daemon start process finished with exit code:", exitCode)
if (exitCode === 0) {
Logger.log("Swww", "Daemon started successfully")
} else {
Logger.log("Swww", "Failed to start daemon, may already be running")
} }
} }
} }

View file

@ -3,8 +3,12 @@ import QtQuick.Layouts
import qs.Commons import qs.Commons
ColumnLayout { ColumnLayout {
id: root
property string label: "" property string label: ""
property string description: "" property string description: ""
property color labelColor: Color.mOnSurface
property color descriptionColor: Color.mOnSurfaceVariant
spacing: Style.marginXXS * scaling spacing: Style.marginXXS * scaling
Layout.fillWidth: true Layout.fillWidth: true
@ -13,14 +17,14 @@ ColumnLayout {
text: label text: label
font.pointSize: Style.fontSizeL * scaling font.pointSize: Style.fontSizeL * scaling
font.weight: Style.fontWeightBold font.weight: Style.fontWeightBold
color: Color.mOnSurface color: labelColor
visible: label !== "" visible: label !== ""
} }
NText { NText {
text: description text: description
font.pointSize: Style.fontSizeS * scaling font.pointSize: Style.fontSizeS * scaling
color: Color.mOnSurfaceVariant color: descriptionColor
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
visible: description !== "" visible: description !== ""
Layout.fillWidth: true Layout.fillWidth: true

View file

@ -12,6 +12,8 @@ ColumnLayout {
property bool readOnly: false property bool readOnly: false
property bool enabled: true property bool enabled: true
property int inputMaxWidth: 420 * scaling property int inputMaxWidth: 420 * scaling
property color labelColor: Color.mOnSurface
property color descriptionColor: Color.mOnSurfaceVariant
property alias text: input.text property alias text: input.text
property alias placeholderText: input.placeholderText property alias placeholderText: input.placeholderText
@ -25,6 +27,8 @@ ColumnLayout {
NLabel { NLabel {
label: root.label label: root.label
description: root.description description: root.description
labelColor: root.labelColor
descriptionColor: root.descriptionColor
visible: root.label !== "" || root.description !== "" visible: root.label !== "" || root.description !== ""
} }

View file

@ -35,7 +35,7 @@ Item {
} }
} }
} }
Logger.log("NWidgetLoader", "Loaded", widgetName, "on screen", item.screen.name) //Logger.log("NWidgetLoader", "Loaded", widgetName, "on screen", item.screen.name)
} }
} }

View file

@ -47,7 +47,6 @@
libnotify libnotify
matugen matugen
networkmanager networkmanager
swww
wl-clipboard wl-clipboard
]; ];