diff --git a/Commons/Logger.qml b/Commons/Logger.qml index 22a4726..72b62e4 100644 --- a/Commons/Logger.qml +++ b/Commons/Logger.qml @@ -17,6 +17,14 @@ Singleton { } } + function _getStackTrace() { + try { + throw new Error("Stack trace") + } catch (e) { + return e.stack + } + } + function log(...args) { var msg = _formatMessage(...args) console.log(msg) @@ -31,4 +39,20 @@ Singleton { var msg = _formatMessage(...args) 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", "--------------------------") + } } diff --git a/Commons/Settings.qml b/Commons/Settings.qml index 17187be..b2759fb 100644 --- a/Commons/Settings.qml +++ b/Commons/Settings.qml @@ -89,23 +89,16 @@ Singleton { reload() } onLoaded: function () { - Qt.callLater(function () { - // Some stuff like wallpaper setup and settings validation should just be executed once on startup - // And not on every reload - if (!isLoaded) { - 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) - } + if (!isLoaded) { + Logger.log("Settings", "----------------------------") + Logger.log("Settings", "Settings loaded successfully") + isLoaded = true - // Validate monitor configurations, only once - // if none of the configured monitors exist, clear the lists + Qt.callLater(function () { + // Some stuff like settings validation should just be executed once on startup and not on every reload validateMonitorConfigurations() - - isLoaded = true - } - }) + }) + } } onLoadFailed: function (error) { if (error.toString().includes("No such file") || error === 2) @@ -171,22 +164,13 @@ Singleton { // wallpaper property JsonObject wallpaper: JsonObject { property string directory: "/usr/share/wallpapers" - property string current: "" - property bool isRandom: false - property int randomInterval: 300 - property JsonObject swww - - onDirectoryChanged: WallpaperService.listWallpapers() - onIsRandomChanged: WallpaperService.toggleRandomWallpaper() - 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 - } + property bool enableMultiMonitorDirectories: false + property bool setWallpaperOnAllMonitors: true + property bool randomEnabled: false + property int randomIntervalSec: 300 // 5 min + property int transitionDuration: 1500 // 1500 ms + property string transitionType: "fade" + property list monitors: [] } // applauncher @@ -234,19 +218,10 @@ Singleton { property string fontDefault: "Roboto" // Default font for all text property string fontFixed: "DejaVu Sans Mono" // Fixed width font for terminal property string fontBillboard: "Inter" // Large bold font for clocks and prominent displays - - // Legacy compatibility - property string fontFamily: fontDefault // Keep for backward compatibility - - // Idle inhibitor state + property list monitorsScaling: [] property bool idleInhibitorEnabled: false } - // Scaling (not stored inside JsonObject, or it crashes) - property var monitorsScaling: { - - } - // brightness property JsonObject brightness: JsonObject { property int brightnessStep: 5 diff --git a/Modules/ArchUpdaterPanel/ArchUpdaterPanel.qml b/Modules/ArchUpdaterPanel/ArchUpdaterPanel.qml index d1e2474..a4759ca 100644 --- a/Modules/ArchUpdaterPanel/ArchUpdaterPanel.qml +++ b/Modules/ArchUpdaterPanel/ArchUpdaterPanel.qml @@ -15,9 +15,8 @@ NPanel { // When the panel opens onOpened: { - console.log("ArchUpdaterPanel: Panel opened, refreshing package lists...") - // Always refresh when panel opens to ensure we have the latest data - ArchUpdaterService.forceRefresh() + ArchUpdaterService.doPoll() + ArchUpdaterService.doAurPoll() } panelContent: Rectangle { @@ -48,19 +47,6 @@ NPanel { 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 { icon: "close" tooltipText: "Close" @@ -73,10 +59,8 @@ NPanel { Layout.fillWidth: true } - // Update summary (only show when packages are available) + // Update summary NText { - visible: !ArchUpdaterService.updateInProgress && !ArchUpdaterService.updateFailed && !ArchUpdaterService.busy - && !ArchUpdaterService.aurBusy && ArchUpdaterService.totalUpdates > 0 text: ArchUpdaterService.totalUpdates + " package" + (ArchUpdaterService.totalUpdates !== 1 ? "s" : "") + " can be updated" font.pointSize: Style.fontSizeL * scaling font.weight: Style.fontWeightMedium @@ -84,184 +68,16 @@ NPanel { Layout.fillWidth: true } - // Package selection info (only show when not updating and have packages) + // Package selection info NText { - visible: !ArchUpdaterService.updateInProgress && !ArchUpdaterService.updateFailed && !ArchUpdaterService.busy - && !ArchUpdaterService.aurBusy && ArchUpdaterService.totalUpdates > 0 text: ArchUpdaterService.selectedPackagesCount + " of " + ArchUpdaterService.totalUpdates + " packages selected" font.pointSize: Style.fontSizeS * scaling color: Color.mOnSurfaceVariant Layout.fillWidth: true } - // Update in progress state - 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) + // Unified list NBox { - visible: !ArchUpdaterService.updateInProgress && !ArchUpdaterService.updateFailed && !ArchUpdaterService.busy - && !ArchUpdaterService.aurBusy && ArchUpdaterService.totalUpdates > 0 Layout.fillWidth: true Layout.fillHeight: true @@ -348,49 +164,50 @@ NPanel { } } - // Action buttons (only show when not updating) + // Action buttons RowLayout { - visible: !ArchUpdaterService.updateInProgress && !ArchUpdaterService.updateFailed Layout.fillWidth: true spacing: Style.marginL * scaling NIconButton { icon: "refresh" - tooltipText: "Refresh package lists" + tooltipText: "Check for updates" onClicked: { - ArchUpdaterService.forceRefresh() + ArchUpdaterService.doPoll() + ArchUpdaterService.doAurPoll() } colorBg: Color.mSurfaceVariant colorFg: Color.mOnSurface Layout.fillWidth: true - enabled: !ArchUpdaterService.busy && !ArchUpdaterService.aurBusy } NIconButton { - icon: "system_update_alt" - tooltipText: "Update all packages" - enabled: ArchUpdaterService.totalUpdates > 0 + icon: ArchUpdaterService.updateInProgress ? "hourglass_empty" : "system_update_alt" + tooltipText: ArchUpdaterService.updateInProgress ? "Update in progress..." : "Update all packages" + enabled: !ArchUpdaterService.updateInProgress onClicked: { ArchUpdaterService.runUpdate() root.close() } - colorBg: ArchUpdaterService.totalUpdates > 0 ? Color.mPrimary : Color.mSurfaceVariant - colorFg: ArchUpdaterService.totalUpdates > 0 ? Color.mOnPrimary : Color.mOnSurfaceVariant + colorBg: ArchUpdaterService.updateInProgress ? Color.mSurfaceVariant : Color.mPrimary + colorFg: ArchUpdaterService.updateInProgress ? Color.mOnSurfaceVariant : Color.mOnPrimary Layout.fillWidth: true } NIconButton { - icon: "check_box" - tooltipText: "Update selected packages" - enabled: ArchUpdaterService.selectedPackagesCount > 0 + icon: ArchUpdaterService.updateInProgress ? "hourglass_empty" : "check_box" + tooltipText: ArchUpdaterService.updateInProgress ? "Update in progress..." : "Update selected packages" + enabled: !ArchUpdaterService.updateInProgress && ArchUpdaterService.selectedPackagesCount > 0 onClicked: { if (ArchUpdaterService.selectedPackagesCount > 0) { ArchUpdaterService.runSelectiveUpdate() root.close() } } - colorBg: ArchUpdaterService.selectedPackagesCount > 0 ? Color.mPrimary : Color.mSurfaceVariant - colorFg: ArchUpdaterService.selectedPackagesCount > 0 ? Color.mOnPrimary : Color.mOnSurfaceVariant + colorBg: ArchUpdaterService.updateInProgress ? Color.mSurfaceVariant : (ArchUpdaterService.selectedPackagesCount + > 0 ? Color.mPrimary : Color.mSurfaceVariant) + colorFg: ArchUpdaterService.updateInProgress ? Color.mOnSurfaceVariant : (ArchUpdaterService.selectedPackagesCount + > 0 ? Color.mOnPrimary : Color.mOnSurfaceVariant) Layout.fillWidth: true } } diff --git a/Modules/Background/Background.qml b/Modules/Background/Background.qml index 54a7878..9d8a5f9 100644 --- a/Modules/Background/Background.qml +++ b/Modules/Background/Background.qml @@ -4,27 +4,67 @@ import Quickshell.Wayland import qs.Commons import qs.Services -Loader { - active: !Settings.data.wallpaper.swww.enabled +Variants { + id: backgroundVariants + model: Quickshell.screens - sourceComponent: Variants { - model: Quickshell.screens + delegate: Loader { - delegate: PanelWindow { - required property ShellScreen modelData - property string wallpaperSource: WallpaperService.currentWallpaper !== "" - && !Settings.data.wallpaper.swww.enabled ? WallpaperService.currentWallpaper : "" + required property ShellScreen modelData - visible: wallpaperSource !== "" && !Settings.data.wallpaper.swww.enabled + active: Settings.isLoaded && WallpaperService.getWallpaper(modelData.name) - // Force update when SWWW setting changes - onVisibleChanged: { - if (visible) { + sourceComponent: PanelWindow { + id: root - } 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 screen: modelData WlrLayershell.layer: WlrLayer.Background @@ -38,18 +78,106 @@ Loader { left: true } - margins { - top: 0 - } - Image { + id: currentWallpaper anchors.fill: parent fillMode: Image.PreserveAspectCrop - source: wallpaperSource - visible: wallpaperSource !== "" + source: "" cache: true smooth: true 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() + } } } } diff --git a/Modules/Background/Overview.qml b/Modules/Background/Overview.qml index e673663..32237d1 100644 --- a/Modules/Background/Overview.qml +++ b/Modules/Background/Overview.qml @@ -6,24 +6,19 @@ import qs.Commons import qs.Services import qs.Widgets -Loader { - active: CompositorService.isNiri +Variants { + model: Quickshell.screens - Component.onCompleted: { - if (CompositorService.isNiri) { - Logger.log("Overview", "Loading Overview component for Niri") - } - } + delegate: Loader { + required property ShellScreen modelData - sourceComponent: Variants { - model: Quickshell.screens + active: Settings.isLoaded && CompositorService.isNiri - delegate: PanelWindow { - required property ShellScreen modelData - property string wallpaperSource: WallpaperService.currentWallpaper !== "" - && !Settings.data.wallpaper.swww.enabled ? WallpaperService.currentWallpaper : "" + sourceComponent: PanelWindow { + Component.onCompleted: { + Logger.log("Overview", "Loading Overview component for Niri on", modelData.name) + } - visible: wallpaperSource !== "" && !Settings.data.wallpaper.swww.enabled color: Color.transparent screen: modelData WlrLayershell.layer: WlrLayer.Background @@ -39,19 +34,15 @@ Loader { Image { id: bgImage - anchors.fill: parent fillMode: Image.PreserveAspectCrop - source: wallpaperSource + source: WallpaperService.getWallpaper(modelData.name) cache: true smooth: true mipmap: false - visible: wallpaperSource !== "" } MultiEffect { - id: overviewBgBlur - anchors.fill: parent source: bgImage blurEnabled: true diff --git a/Modules/Bar/Widgets/ArchUpdater.qml b/Modules/Bar/Widgets/ArchUpdater.qml index 05598e1..106b167 100644 --- a/Modules/Bar/Widgets/ArchUpdater.qml +++ b/Modules/Bar/Widgets/ArchUpdater.qml @@ -64,5 +64,6 @@ NIconButton { PanelService.getPanel("archUpdaterPanel").toggle(screen, this) ArchUpdaterService.doPoll() + ArchUpdaterService.doAurPoll() } } diff --git a/Modules/LockScreen/LockScreen.qml b/Modules/LockScreen/LockScreen.qml index f155919..131f406 100644 --- a/Modules/LockScreen/LockScreen.qml +++ b/Modules/LockScreen/LockScreen.qml @@ -93,7 +93,7 @@ Loader { id: lockBgImage anchors.fill: parent fillMode: Image.PreserveAspectCrop - source: WallpaperService.currentWallpaper !== "" ? WallpaperService.currentWallpaper : "" + source: WallpaperService.getWallpaper(screen.name) cache: true smooth: true mipmap: false diff --git a/Modules/SettingsPanel/Tabs/DisplayTab.qml b/Modules/SettingsPanel/Tabs/DisplayTab.qml index 5dfcbfe..a81a361 100644 --- a/Modules/SettingsPanel/Tabs/DisplayTab.qml +++ b/Modules/SettingsPanel/Tabs/DisplayTab.qml @@ -188,7 +188,7 @@ ColumnLayout { } NText { - text: `${Math.round(ScalingService.scaleByName(modelData.name) * 100)}%` + text: `${Math.round(ScalingService.getMonitorScale(modelData.name) * 100)}%` Layout.alignment: Qt.AlignVCenter Layout.minimumWidth: 50 * scaling horizontalAlignment: Text.AlignRight @@ -204,12 +204,8 @@ ColumnLayout { from: 0.7 to: 1.8 stepSize: 0.01 - value: ScalingService.scaleByName(modelData.name) - onPressedChanged: { - var data = Settings.data.monitorsScaling || {} - data[modelData.name] = value - Settings.data.monitorsScaling = data - } + value: ScalingService.getMonitorScale(modelData.name) + onPressedChanged: ScalingService.setMonitorScale(modelData.name, value) Layout.fillWidth: true Layout.minimumWidth: 150 * scaling } @@ -217,11 +213,7 @@ ColumnLayout { NIconButton { icon: "refresh" tooltipText: "Reset Scaling" - onClicked: { - var data = Settings.data.monitorsScaling || {} - data[modelData.name] = 1.0 - Settings.data.monitorsScaling = data - } + onClicked: ScalingService.setMonitorScale(modelData.name, 1.0) } } } diff --git a/Modules/SettingsPanel/Tabs/WallpaperSelectorTab.qml b/Modules/SettingsPanel/Tabs/WallpaperSelectorTab.qml index 0fa0338..b708f9d 100644 --- a/Modules/SettingsPanel/Tabs/WallpaperSelectorTab.qml +++ b/Modules/SettingsPanel/Tabs/WallpaperSelectorTab.qml @@ -23,13 +23,12 @@ ColumnLayout { Layout.fillWidth: true Layout.preferredHeight: 140 * scaling radius: Style.radiusM * scaling - color: Color.mPrimary + color: Color.mSecondary NImageRounded { - id: currentWallpaperImage anchors.fill: parent anchors.margins: Style.marginXS * scaling - imagePath: WallpaperService.currentWallpaper + imagePath: WallpaperService.getWallpaper(screen.name) fallbackIcon: "image" imageRadius: Style.radiusM * scaling } @@ -62,41 +61,44 @@ ColumnLayout { wrapMode: Text.WordWrap 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 { icon: "refresh" tooltipText: "Refresh wallpaper list" onClicked: { - WallpaperService.listWallpapers() + WallpaperService.refreshWallpapersList() } - Layout.alignment: Qt.AlignTop | Qt.AlignRight + Layout.alignment: Qt.AlignVCenter | Qt.AlignRight } } + property list 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 Item { + visible: !WallpaperService.scanning Layout.fillWidth: true Layout.preferredHeight: { - return Math.ceil(WallpaperService.wallpaperList.length / wallpaperGridView.columns) * wallpaperGridView.cellHeight + return Math.ceil(wallpapersList.length / wallpaperGridView.columns) * wallpaperGridView.cellHeight } GridView { id: wallpaperGridView anchors.fill: parent clip: true - model: WallpaperService.wallpaperList + model: wallpapersList boundsBehavior: Flickable.StopAtBounds - flickableDirection: Flickable.AutoFlickDirection + flickableDirection: Flickable.VerticalFlick interactive: false property int columns: 5 @@ -114,7 +116,7 @@ ColumnLayout { id: wallpaperItem property string wallpaperPath: modelData - property bool isSelected: wallpaperPath === WallpaperService.currentWallpaper + property bool isSelected: wallpaperPath === WallpaperService.getWallpaper(screen.name) width: wallpaperGridView.itemSize height: Math.floor(wallpaperGridView.itemSize * 0.67) @@ -179,46 +181,65 @@ ColumnLayout { acceptedButtons: Qt.LeftButton hoverEnabled: true onClicked: { - WallpaperService.changeWallpaper(wallpaperPath) + if (Settings.data.wallpaper.setWallpaperOnAllMonitors) { + WallpaperService.changeWallpaper(undefined, wallpaperPath) + } else { + WallpaperService.changeWallpaper(screen.name, wallpaperPath) + } } } } } + } - // Empty state - Rectangle { + // Empty state + 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 - color: Color.mSurface - radius: Style.radiusM * scaling - border.color: Color.mOutline - border.width: Math.max(1, Style.borderS * scaling) - visible: WallpaperService.wallpaperList.length === 0 && !WallpaperService.scanning + visible: WallpaperService.scanning + NBusyIndicator { + Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter + } + } - ColumnLayout { - anchors.centerIn: parent - spacing: Style.marginM * scaling + ColumnLayout { + anchors.fill: parent + visible: wallpapersList.length === 0 && !WallpaperService.scanning + Item { + Layout.fillHeight: true + } - NIcon { - text: "folder_open" - font.pointSize: Style.fontSizeL * scaling - color: Color.mOnSurface - Layout.alignment: Qt.AlignHCenter - } + NIcon { + text: "folder_open" + font.pointSize: Style.fontSizeXL * scaling + color: Color.mOnSurface + Layout.alignment: Qt.AlignHCenter + } - NText { - text: "No wallpapers found" - color: Color.mOnSurface - font.weight: Style.fontWeightBold - Layout.alignment: Qt.AlignHCenter - } + NText { + text: "No wallpaper found." + color: Color.mOnSurface + font.weight: Style.fontWeightBold + Layout.alignment: Qt.AlignHCenter + } - NText { - text: "Make sure your wallpaper directory is configured and contains image files." - color: Color.mOnSurface - wrapMode: Text.WordWrap - horizontalAlignment: Text.AlignHCenter - Layout.preferredWidth: Style.sliderWidth * 1.5 * scaling - } + NText { + text: "Make sure your wallpaper directory is configured and contains image files." + color: Color.mOnSurfaceVariant + wrapMode: Text.WordWrap + Layout.alignment: Qt.AlignHCenter + } + + Item { + Layout.fillHeight: true } } } diff --git a/Modules/SettingsPanel/Tabs/WallpaperTab.qml b/Modules/SettingsPanel/Tabs/WallpaperTab.qml index e96b5ef..546a4ca 100644 --- a/Modules/SettingsPanel/Tabs/WallpaperTab.qml +++ b/Modules/SettingsPanel/Tabs/WallpaperTab.qml @@ -1,6 +1,7 @@ import QtQuick import QtQuick.Controls import QtQuick.Layouts +import Quickshell import Quickshell.Io import qs.Commons import qs.Services @@ -9,36 +10,62 @@ import qs.Widgets ColumnLayout { id: root - // Process to check if swww is installed - Process { - id: swwwCheck - command: ["which", "swww"] - running: false + ColumnLayout { + spacing: Style.marginL * scaling + Layout.fillWidth: true + NTextInput { + 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) { - if (exitCode === 0) { - // SWWW exists, enable it - Settings.data.wallpaper.swww.enabled = true - WallpaperService.startSWWWDaemon() - ToastService.showNotice("Swww", "Enabled") - } else { - // SWWW not found - ToastService.showWarning("Swww", "Not installed") + // Monitor-specific directories + NToggle { + label: "Monitor-specific directories" + description: "Enable multi-monitor wallpaper directory management." + checked: Settings.data.wallpaper.enableMultiMonitorDirectories + onToggled: checked => Settings.data.wallpaper.enableMultiMonitorDirectories = checked + } + + 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 { @@ -62,10 +89,42 @@ ColumnLayout { NToggle { label: "Random Wallpaper" description: "Automatically select random wallpapers from the folder." - checked: Settings.data.wallpaper.isRandom - onToggled: checked => { - Settings.data.wallpaper.isRandom = checked - } + checked: Settings.data.wallpaper.randomEnabled + onToggled: checked => Settings.data.wallpaper.randomEnabled = 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) @@ -79,25 +138,28 @@ ColumnLayout { NText { // 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 } } - // Preset chips + // Preset chips using Repeater RowLayout { id: presetRow spacing: Style.marginS * scaling - // Preset seconds list - property var presets: [15 * 60, 30 * 60, 45 * 60, 60 * 60, 90 * 60, 120 * 60] + // Factorized presets data + 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 - 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 property bool customForcedVisible: false function setIntervalSeconds(sec) { - Settings.data.wallpaper.randomInterval = sec + Settings.data.wallpaper.randomIntervalSec = sec WallpaperService.restartRandomWallpaperTimer() // Hide custom when selecting a preset customForcedVisible = false @@ -105,168 +167,25 @@ ColumnLayout { // Helper to color selected chip function isSelected(sec) { - return Settings.data.wallpaper.randomInterval === sec + return Settings.data.wallpaper.randomIntervalSec === sec } - // 15m - Rectangle { - radius: height * 0.5 - color: presetRow.isSelected(15 * 60) ? Color.mPrimary : Color.mSurfaceVariant - implicitHeight: Math.max(Style.baseWidgetSize * 0.55 * scaling, 24 * scaling) - implicitWidth: label15.implicitWidth + Style.marginM * 1.5 * scaling - border.width: 1 - border.color: presetRow.isSelected(15 * 60) ? Color.transparent : Color.mOutline - 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 + // Repeater for preset chips + Repeater { + model: presetRow.intervalPresets + delegate: IntervalPresetChip { + seconds: modelData + label: Time.formatVagueHumanReadableDuration(modelData) + selected: presetRow.isSelected(modelData) + onClicked: presetRow.setIntervalSeconds(modelData) } } // Custom… opens inline input - Rectangle { - radius: height * 0.5 - color: customRow.visible ? Color.mPrimary : Color.mSurfaceVariant - implicitHeight: Math.max(Style.baseWidgetSize * 0.55 * scaling, 24 * scaling) - 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 - } + IntervalPresetChip { + label: customRow.visible ? "Custom" : "Custom…" + selected: customRow.visible + onClicked: presetRow.customForcedVisible = !presetRow.customForcedVisible } } @@ -282,12 +201,11 @@ ColumnLayout { description: "Enter time as HH:MM (e.g., 01:30)." inputMaxWidth: 100 * scaling text: { - const s = Settings.data.wallpaper.randomInterval + const s = Settings.data.wallpaper.randomIntervalSec const h = Math.floor(s / 3600) const m = Math.floor((s % 3600) / 60) return h + ":" + (m < 10 ? ("0" + m) : m) } - onEditingFinished: { const m = text.trim().match(/^(\d{1,2}):(\d{2})$/) if (m) { @@ -297,7 +215,7 @@ ColumnLayout { return h = Math.max(0, Math.min(24, h)) 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() // Keep custom visible after manual entry presetRow.customForcedVisible = true @@ -308,193 +226,32 @@ ColumnLayout { } } - NDivider { - Layout.fillWidth: true - Layout.topMargin: Style.marginXL * scaling - Layout.bottomMargin: Style.marginXL * scaling - } + // Reusable component for interval preset chips + component IntervalPresetChip: Rectangle { + property int seconds: 0 + property string label: "" + property bool selected: false + signal clicked - // ------------------------------- - // Swww - ColumnLayout { - spacing: Style.marginL * scaling - Layout.fillWidth: true + radius: height * 0.5 + color: selected ? Color.mPrimary : Color.mSurfaceVariant + implicitHeight: Math.max(Style.baseWidgetSize * 0.55 * scaling, 24 * scaling) + implicitWidth: chipLabel.implicitWidth + Style.marginM * 1.5 * scaling + border.width: 1 + border.color: selected ? Color.transparent : Color.mOutline + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: parent.clicked() + } NText { - text: "Swww" - font.pointSize: Style.fontSizeXXL * scaling - font.weight: Style.fontWeightBold - color: Color.mSecondary - } - - // 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 - } - } - } + id: chipLabel + anchors.centerIn: parent + text: parent.label + font.pointSize: Style.fontSizeS * scaling + color: parent.selected ? Color.mOnPrimary : Color.mOnSurface } } diff --git a/README.md b/README.md index 9429f2d..b701e0e 100644 --- a/README.md +++ b/README.md @@ -27,11 +27,11 @@ Features a modern modular architecture with a status bar, notification system, c ## 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 - `cliphist` - For clipboard history support -- `swww` - Wallpaper animations and effects - `matugen` - Material You color scheme generation - `cava` - Audio visualizer component - `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. -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 @@ -276,14 +269,6 @@ The launcher supports special commands for enhanced functionality: ## Advanced Configuration -### Niri Configuration - -Add this to your `layout` section for proper swww integration: - -``` -background-color "transparent" -``` - ### Recommended Compositor Settings For Niri: @@ -294,11 +279,6 @@ window-rule { clip-to-geometry true } -layer-rule { - match namespace="^swww-daemon$" - place-within-backdrop true -} - layer-rule { match namespace="^quickshell-wallpaper$" } diff --git a/Services/ArchUpdaterService.qml b/Services/ArchUpdaterService.qml index cd40a1d..de3b3af 100644 --- a/Services/ArchUpdaterService.qml +++ b/Services/ArchUpdaterService.qml @@ -8,164 +8,28 @@ import qs.Commons Singleton { id: updateService - // ============================================================================ - // CORE PROPERTIES - // ============================================================================ - - // Package data + // Core properties + readonly property bool busy: checkupdatesProcess.running + readonly property bool aurBusy: checkAurUpdatesProcess.running + readonly property int updates: repoPackages.length + readonly property int aurUpdates: aurPackages.length + readonly property int totalUpdates: updates + aurUpdates property var repoPackages: [] property var aurPackages: [] property var selectedPackages: [] property int selectedPackagesCount: 0 - - // Update state 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 Component.onCompleted: { - getAurHelper() doPoll() + doAurPoll() } - // ============================================================================ - // PACKAGE CHECKING PROCESSES - // ============================================================================ - // Process for checking repo updates Process { id: checkupdatesProcess - command: ["checkupdates", "--nosync"] + command: ["checkupdates"] onExited: function (exitCode) { if (exitCode !== 0 && exitCode !== 2) { 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 { - id: checkParuUpdatesProcess - command: ["paru", "-Qua"] + id: checkAurUpdatesProcess + 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) { if (exitCode !== 0) { - Logger.warn("ArchUpdater", "paru check failed (code:", exitCode, ")") + Logger.warn("ArchUpdater", "AUR check failed (code:", exitCode, ")") aurPackages = [] } } @@ -198,12 +62,8 @@ Singleton { } } - // ============================================================================ - // PARSING FUNCTIONS - // ============================================================================ - - // Generic package parsing function - function parsePackageOutput(output, source) { + // Parse checkupdates output + function parseCheckupdatesOutput(output) { const lines = output.trim().split('\n').filter(line => line.trim()) const packages = [] @@ -215,83 +75,65 @@ Singleton { "oldVersion": m[2], "newVersion": 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 - if (packages.length > 0 || output.trim() === "") { - if (source === "repo") { - repoPackages = packages - } else { - aurPackages = packages - } - } - } - - // Parse checkupdates output - function parseCheckupdatesOutput(output) { - parsePackageOutput(output, "repo") + repoPackages = packages } // Parse AUR updates 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() { - // Start repo updates check - if (!busy) { - checkupdatesProcess.running = true - } - - // Start AUR updates check - if (!aurBusy) { - checkParuUpdatesProcess.running = true - } + if (busy) + return + checkupdatesProcess.running = true } - // ============================================================================ - // UPDATE FUNCTIONS - // ============================================================================ - - // Helper function to generate update command with error detection - 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...'" + // Check for AUR updates + function doAurPoll() { + if (aurBusy) + return + checkAurUpdatesProcess.running = true } // Update all packages (repo + AUR) function runUpdate() { if (totalUpdates === 0) { doPoll() + doAurPoll() return } - // Reset any previous error states - updateFailed = false - lastUpdateError = "" 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" - - // 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() + // Refresh after updates with multiple attempts + refreshAfterUpdate() } // Update selected packages @@ -299,13 +141,7 @@ Singleton { if (selectedPackages.length === 0) return - // Reset any previous error states - updateFailed = false - lastUpdateError = "" updateInProgress = true - console.log("ArchUpdater: Starting selective update for", selectedPackages.length, "packages") - - const terminal = Quickshell.env("TERMINAL") || "xterm" // Split selected packages by source const repoPkgs = [] @@ -323,140 +159,48 @@ Singleton { } } - // Update repo packages with sudo + // Update repo packages if (repoPkgs.length > 0) { - const packageList = repoPkgs.join(" ") - const command = generateUpdateCommand("sudo pacman -S " + packageList) - Quickshell.execDetached([terminal, "-e", "bash", "-c", command]) + const repoCommand = ["pkexec", "pacman", "-S", "--noconfirm"].concat(repoPkgs) + Logger.log("ArchUpdater", "Running repo command:", repoCommand.join(" ")) + Quickshell.execDetached(repoCommand) } - // Update AUR packages with yay/paru + // Update AUR packages if (aurPkgs.length > 0) { const aurHelper = getAurHelper() if (aurHelper) { - const packageList = aurPkgs.join(" ") - const command = generateUpdateCommand(aurHelper + " -S " + packageList) - Quickshell.execDetached([terminal, "-e", "bash", "-c", command]) + const aurCommand = [aurHelper, "-S", "--noconfirm"].concat(aurPkgs) + Logger.log("ArchUpdater", "Running AUR command:", aurCommand.join(" ")) + Quickshell.execDetached(aurCommand) } else { Logger.warn("ArchUpdater", "No AUR helper found for packages:", aurPkgs.join(", ")) } } - // Start monitoring and timeout timers - refreshTimer.start() - updateCompleteTimer.start() - updateMonitorTimer.start() + // Clear selection and refresh + selectedPackages = [] + selectedPackagesCount = 0 + 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 function getAurHelper() { - // Return cached result if available - if (cachedAurHelper !== "") { - return cachedAurHelper + // Check for yay first, then paru + const yayCheck = Quickshell.exec("command -v yay", true) + if (yayCheck.exitCode === 0 && yayCheck.stdout.trim()) { + return "yay" } - // Check for AUR helpers using Process objects - console.log("ArchUpdater: Detecting AUR helper...") + const paruCheck = Quickshell.exec("command -v paru", true) + if (paruCheck.exitCode === 0 && paruCheck.stdout.trim()) { + return "paru" + } - // Start the detection processes - 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 + return null } - // ============================================================================ - // PACKAGE SELECTION FUNCTIONS - // ============================================================================ + // Package selection functions function togglePackageSelection(packageName) { const index = selectedPackages.indexOf(packageName) if (index > -1) { @@ -481,60 +225,47 @@ Singleton { return selectedPackages.indexOf(packageName) > -1 } - // ============================================================================ - // REFRESH FUNCTIONS - // ============================================================================ + // Robust refresh after updates + function refreshAfterUpdate() { + // First refresh attempt after 3 seconds + Qt.callLater(() => { + doPoll() + doAurPoll() + }, 3000) - // Function to manually sync package databases (separate from refresh) - function syncPackageDatabases() { - console.log("ArchUpdater: Manual database sync requested...") - const terminal = Quickshell.env("TERMINAL") || "xterm" - const command = "sudo pacman -Sy && echo 'Database sync complete! Press Enter to close...' && read -p 'Press Enter to continue...'" - console.log("ArchUpdater: Executing sync command:", command) - console.log("ArchUpdater: Terminal:", terminal) - Quickshell.execDetached([terminal, "-e", "bash", "-c", command]) + // Second refresh attempt after 8 seconds + Qt.callLater(() => { + doPoll() + doAurPoll() + }, 8000) + + // Third refresh attempt after 15 seconds + 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 function notify(title, body) { Quickshell.execDetached(["notify-send", "-a", "UpdateService", "-i", "system-software-update", title, body]) } - // ============================================================================ - // AUTO-POLL TIMER - // ============================================================================ - // Auto-poll every 15 minutes Timer { interval: 15 * 60 * 1000 // 15 minutes repeat: true running: true onTriggered: { - if (!updateInProgress) { - doPoll() - } + doPoll() + doAurPoll() } } } diff --git a/Services/MatugenService.qml b/Services/MatugenService.qml index 1749410..9c40aaf 100644 --- a/Services/MatugenService.qml +++ b/Services/MatugenService.qml @@ -22,9 +22,11 @@ Singleton { // Ensure cache dir exists 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 mode = Settings.data.colorSchemes.darkMode ? "dark" : "light" - var wp = WallpaperService.currentWallpaper.replace(/'/g, "'\\''") var pathEsc = dynamicConfigPath.replace(/'/g, "'\\''") var extraRepo = (Quickshell.shellDir + "/Assets/Matugen/extra").replace(/'/g, "'\\''") var extraUser = (Settings.configDir + "matugen.d").replace(/'/g, "'\\''") diff --git a/Services/ScalingService.qml b/Services/ScalingService.qml index 2b6cc58..8aa32bb 100644 --- a/Services/ScalingService.qml +++ b/Services/ScalingService.qml @@ -11,7 +11,7 @@ Singleton { function scale(aScreen) { try { if (aScreen !== undefined && aScreen.name !== undefined) { - return scaleByName(aScreen.name) + return getMonitorScale(aScreen.name) } } catch (e) { @@ -20,21 +20,46 @@ Singleton { return 1.0 } - function scaleByName(aScreenName) { + // ------------------------------------------- + function getMonitorScale(aScreenName) { try { - if (Settings.data.monitorsScaling !== undefined) { - if (Settings.data.monitorsScaling[aScreenName] !== undefined) { - return Settings.data.monitorsScaling[aScreenName] + 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) { + return monitors[i].scale + } } } } catch (e) { //Logger.warn(e) } - 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 diff --git a/Services/WallpaperService.qml b/Services/WallpaperService.qml index 0b43142..15742c3 100644 --- a/Services/WallpaperService.qml +++ b/Services/WallpaperService.qml @@ -10,84 +10,197 @@ Singleton { id: root Component.onCompleted: { - Logger.log("Wallpapers", "Service started") - listWallpapers() - - // Wallpaper is set when the settings are loaded. - // Don't start random wallpaper during initialization + Logger.log("Wallpaper", "Service started") } - property var wallpaperList: [] - property string currentWallpaper: Settings.data.wallpaper.current - property bool scanning: false - - // SWWW - 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 + // All available wallpaper transitions + readonly property ListModel transitionsModel: ListModel { + ListElement { + key: "none" + name: "None" } - if (Settings.data.wallpaper.swww.enabled) { - if (Settings.data.wallpaper.swww.transitionType === "random") { - transitionType = randomChoices[Math.floor(Math.random() * randomChoices.length)] - } else { - transitionType = Settings.data.wallpaper.swww.transitionType + ListElement { + key: "fade" + name: "Fade" + } + 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 { - - // Fallback: update the settings directly for non-SWWW mode - //Logger.log("Wallpapers", "Not using Swww, setting wallpaper directly") + // ------------------------------------------------------------------- + // Get specific monitor directory + function getMonitorDirectory(screenName) { + if (!Settings.data.wallpaper.enableMultiMonitorDirectories) { + 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) { randomWallpaperTimer.restart() } - // Only notify ColorScheme service if the wallpaper actually changed + // Notify ColorScheme service if the wallpaper actually changed if (wallpaperChanged) { ColorSchemeService.changedWallpaper() } } + // ------------------------------------------------------------------- function setRandomWallpaper() { - var randomIndex = Math.floor(Math.random() * wallpaperList.length) - var randomPath = wallpaperList[randomIndex] - if (!randomPath) { - return + Logger.log("Wallpaper", "setRandomWallpaper") + + if (Settings.data.wallpaper.enableMultiMonitorDirectories) { + // 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() { - if (Settings.data.wallpaper.isRandom && !randomWallpaperTimer.running) { - randomWallpaperTimer.start() + Logger.log("Wallpaper", "toggleRandomWallpaper") + if (Settings.data.wallpaper.randomEnabled) { + randomWallpaperTimer.restart() setRandomWallpaper() - } else if (!Settings.data.randomWallpaper && randomWallpaperTimer.running) { - randomWallpaperTimer.stop() } } + // ------------------------------------------------------------------- function restartRandomWallpaperTimer() { if (Settings.data.wallpaper.isRandom) { randomWallpaperTimer.stop() @@ -95,78 +208,81 @@ Singleton { } } - function startSWWWDaemon() { - if (Settings.data.wallpaper.swww.enabled) { - Logger.log("Swww", "Requesting swww-daemon") - startDaemonProcess.running = true + // ------------------------------------------------------------------- + function getWallpapersList(screenName) { + if (screenName != undefined && wallpaperLists[screenName] != undefined) { + 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 { id: randomWallpaperTimer - interval: Settings.data.wallpaper.randomInterval * 1000 - running: false + interval: Settings.data.wallpaper.randomIntervalSec * 1000 + running: Settings.data.wallpaper.randomEnabled repeat: true onTriggered: setRandomWallpaper() triggeredOnStart: false } - FolderListModel { - id: folderModel - // Swww supports many images format but Quickshell only support a subset of those. - nameFilters: ["*.jpg", "*.jpeg", "*.png", "*.gif", "*.pnm", "*.bmp"] - showDirs: false - sortField: FolderListModel.Name - onStatusChanged: { - if (status === FolderListModel.Ready) { - var files = [] - for (var i = 0; i < count; i++) { - var directory = (Settings.data.wallpaper.directory !== undefined ? Settings.data.wallpaper.directory : "") - var filepath = directory + "/" + get(i, "fileName") - files.push(filepath) + // Instantiator (not Repeater) to create FolderListModel for each monitor + Instantiator { + id: wallpaperScanners + model: Quickshell.screens + delegate: FolderListModel { + property string screenName: modelData.name + + folder: "file://" + root.getMonitorDirectory(screenName) + nameFilters: ["*.jpg", "*.jpeg", "*.png", "*.gif", "*.pnm", "*.bmp"] + showDirs: false + sortField: FolderListModel.Name + onStatusChanged: { + 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") } } } diff --git a/Widgets/NLabel.qml b/Widgets/NLabel.qml index cf05b8f..e05604c 100644 --- a/Widgets/NLabel.qml +++ b/Widgets/NLabel.qml @@ -3,8 +3,12 @@ import QtQuick.Layouts import qs.Commons ColumnLayout { + id: root + property string label: "" property string description: "" + property color labelColor: Color.mOnSurface + property color descriptionColor: Color.mOnSurfaceVariant spacing: Style.marginXXS * scaling Layout.fillWidth: true @@ -13,14 +17,14 @@ ColumnLayout { text: label font.pointSize: Style.fontSizeL * scaling font.weight: Style.fontWeightBold - color: Color.mOnSurface + color: labelColor visible: label !== "" } NText { text: description font.pointSize: Style.fontSizeS * scaling - color: Color.mOnSurfaceVariant + color: descriptionColor wrapMode: Text.WordWrap visible: description !== "" Layout.fillWidth: true diff --git a/Widgets/NTextInput.qml b/Widgets/NTextInput.qml index 6533af9..cad5077 100644 --- a/Widgets/NTextInput.qml +++ b/Widgets/NTextInput.qml @@ -12,6 +12,8 @@ ColumnLayout { property bool readOnly: false property bool enabled: true property int inputMaxWidth: 420 * scaling + property color labelColor: Color.mOnSurface + property color descriptionColor: Color.mOnSurfaceVariant property alias text: input.text property alias placeholderText: input.placeholderText @@ -25,6 +27,8 @@ ColumnLayout { NLabel { label: root.label description: root.description + labelColor: root.labelColor + descriptionColor: root.descriptionColor visible: root.label !== "" || root.description !== "" } diff --git a/Widgets/NWidgetLoader.qml b/Widgets/NWidgetLoader.qml index a9e7ac5..c0a623e 100644 --- a/Widgets/NWidgetLoader.qml +++ b/Widgets/NWidgetLoader.qml @@ -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) } } diff --git a/flake.nix b/flake.nix index 67aec51..b61342f 100644 --- a/flake.nix +++ b/flake.nix @@ -47,7 +47,6 @@ libnotify matugen networkmanager - swww wl-clipboard ];