From cf5dec733d4a51188fa3acb01522479ae438b51b Mon Sep 17 00:00:00 2001 From: bbedward Date: Mon, 22 Sep 2025 17:51:59 -0400 Subject: [PATCH] More wallpaper effects + per-monitor auto cycling --- Common/SessionData.qml | 61 +++++- Modules/Settings/PersonalizationTab.qml | 164 +++++++++++++--- Modules/WallpaperBackground.qml | 72 ++++++++ Services/WallpaperCyclingService.qml | 236 ++++++++++++++++++++++-- Shaders/frag/circled_image.frag | 30 --- Shaders/frag/rounded_image.frag | 56 ------ Shaders/frag/wp_iris_bloom.frag | 100 ++++++++++ Shaders/frag/wp_pixelate.frag | 99 ++++++++++ Shaders/frag/wp_portal.frag | 103 +++++++++++ Shaders/qsb/circled_image.frag.qsb | Bin 1717 -> 0 bytes Shaders/qsb/rounded_image.frag.qsb | Bin 2767 -> 0 bytes Shaders/qsb/wp_iris_bloom.frag.qsb | Bin 0 -> 5502 bytes Shaders/qsb/wp_pixelate.frag.qsb | Bin 0 -> 5792 bytes Shaders/qsb/wp_portal.frag.qsb | Bin 0 -> 5555 bytes Widgets/DankAlbumArt.qml | 4 +- Widgets/DankCircularImage.qml | 124 ++++++------- shell.qml | 2 + 17 files changed, 850 insertions(+), 201 deletions(-) delete mode 100644 Shaders/frag/circled_image.frag delete mode 100644 Shaders/frag/rounded_image.frag create mode 100644 Shaders/frag/wp_iris_bloom.frag create mode 100644 Shaders/frag/wp_pixelate.frag create mode 100644 Shaders/frag/wp_portal.frag delete mode 100644 Shaders/qsb/circled_image.frag.qsb delete mode 100644 Shaders/qsb/rounded_image.frag.qsb create mode 100644 Shaders/qsb/wp_iris_bloom.frag.qsb create mode 100644 Shaders/qsb/wp_pixelate.frag.qsb create mode 100644 Shaders/qsb/wp_portal.frag.qsb diff --git a/Common/SessionData.qml b/Common/SessionData.qml index d5c12e7a..25093019 100644 --- a/Common/SessionData.qml +++ b/Common/SessionData.qml @@ -43,6 +43,7 @@ Singleton { property string wallpaperCyclingMode: "interval" // "interval" or "time" property int wallpaperCyclingInterval: 300 // seconds (5 minutes) property string wallpaperCyclingTime: "06:00" // HH:mm format + property var monitorCyclingSettings: ({}) property string lastBrightnessDevice: "" property string launchPrefix: "" property string wallpaperTransition: "fade" @@ -113,6 +114,7 @@ Singleton { wallpaperCyclingMode = settings.wallpaperCyclingMode !== undefined ? settings.wallpaperCyclingMode : "interval" wallpaperCyclingInterval = settings.wallpaperCyclingInterval !== undefined ? settings.wallpaperCyclingInterval : 300 wallpaperCyclingTime = settings.wallpaperCyclingTime !== undefined ? settings.wallpaperCyclingTime : "06:00" + monitorCyclingSettings = settings.monitorCyclingSettings !== undefined ? settings.monitorCyclingSettings : {} lastBrightnessDevice = settings.lastBrightnessDevice !== undefined ? settings.lastBrightnessDevice : "" launchPrefix = settings.launchPrefix !== undefined ? settings.launchPrefix : "" wallpaperTransition = settings.wallpaperTransition !== undefined ? settings.wallpaperTransition : "fade" @@ -166,6 +168,7 @@ Singleton { "wallpaperCyclingMode": wallpaperCyclingMode, "wallpaperCyclingInterval": wallpaperCyclingInterval, "wallpaperCyclingTime": wallpaperCyclingTime, + "monitorCyclingSettings": monitorCyclingSettings, "lastBrightnessDevice": lastBrightnessDevice, "launchPrefix": launchPrefix, "wallpaperTransition": wallpaperTransition, @@ -367,18 +370,64 @@ Singleton { saveSettings() } + function getMonitorCyclingSettings(screenName) { + return monitorCyclingSettings[screenName] || { + enabled: false, + mode: "interval", + interval: 300, + time: "06:00" + } + } + + function setMonitorCyclingEnabled(screenName, enabled) { + var newSettings = Object.assign({}, monitorCyclingSettings) + if (!newSettings[screenName]) { + newSettings[screenName] = { enabled: false, mode: "interval", interval: 300, time: "06:00" } + } + newSettings[screenName].enabled = enabled + monitorCyclingSettings = newSettings + saveSettings() + } + + function setMonitorCyclingMode(screenName, mode) { + var newSettings = Object.assign({}, monitorCyclingSettings) + if (!newSettings[screenName]) { + newSettings[screenName] = { enabled: false, mode: "interval", interval: 300, time: "06:00" } + } + newSettings[screenName].mode = mode + monitorCyclingSettings = newSettings + saveSettings() + } + + function setMonitorCyclingInterval(screenName, interval) { + var newSettings = Object.assign({}, monitorCyclingSettings) + if (!newSettings[screenName]) { + newSettings[screenName] = { enabled: false, mode: "interval", interval: 300, time: "06:00" } + } + newSettings[screenName].interval = interval + monitorCyclingSettings = newSettings + saveSettings() + } + + function setMonitorCyclingTime(screenName, time) { + var newSettings = Object.assign({}, monitorCyclingSettings) + if (!newSettings[screenName]) { + newSettings[screenName] = { enabled: false, mode: "interval", interval: 300, time: "06:00" } + } + newSettings[screenName].time = time + monitorCyclingSettings = newSettings + saveSettings() + } + function setPerMonitorWallpaper(enabled) { perMonitorWallpaper = enabled - - // Disable automatic cycling when per-monitor mode is enabled - if (enabled && wallpaperCyclingEnabled) { - wallpaperCyclingEnabled = false - } - saveSettings() // Refresh dynamic theming when per-monitor mode changes if (typeof Theme !== "undefined") { + if (Theme.currentTheme === Theme.dynamic) { + Theme.extractColors() + } Theme.generateSystemThemesFromCurrentTheme() } } diff --git a/Modules/Settings/PersonalizationTab.qml b/Modules/Settings/PersonalizationTab.qml index 51d7202e..11dd8ce4 100644 --- a/Modules/Settings/PersonalizationTab.qml +++ b/Modules/Settings/PersonalizationTab.qml @@ -511,13 +511,13 @@ Item { height: 1 color: Theme.outline opacity: 0.2 - visible: SessionData.wallpaperPath !== "" && !SessionData.perMonitorWallpaper + visible: SessionData.wallpaperPath !== "" || SessionData.perMonitorWallpaper } Column { width: parent.width spacing: Theme.spacingM - visible: SessionData.wallpaperPath !== "" && !SessionData.perMonitorWallpaper + visible: SessionData.wallpaperPath !== "" || SessionData.perMonitorWallpaper Row { width: parent.width @@ -554,11 +554,23 @@ Item { id: cyclingToggle anchors.verticalCenter: parent.verticalCenter - checked: SessionData.wallpaperCyclingEnabled - enabled: !SessionData.perMonitorWallpaper + checked: SessionData.perMonitorWallpaper ? SessionData.getMonitorCyclingSettings(selectedMonitorName).enabled : SessionData.wallpaperCyclingEnabled onToggled: toggled => { - return SessionData.setWallpaperCyclingEnabled(toggled) + if (SessionData.perMonitorWallpaper) { + return SessionData.setMonitorCyclingEnabled(selectedMonitorName, toggled) + } else { + return SessionData.setWallpaperCyclingEnabled(toggled) + } } + + Connections { + target: personalizationTab + function onSelectedMonitorNameChanged() { + cyclingToggle.checked = Qt.binding(() => { + return SessionData.perMonitorWallpaper ? SessionData.getMonitorCyclingSettings(selectedMonitorName).enabled : SessionData.wallpaperCyclingEnabled + }) + } + } } } @@ -566,7 +578,7 @@ Item { Column { width: parent.width spacing: Theme.spacingS - visible: SessionData.wallpaperCyclingEnabled + visible: SessionData.perMonitorWallpaper ? SessionData.getMonitorCyclingSettings(selectedMonitorName).enabled : SessionData.wallpaperCyclingEnabled leftPadding: Theme.iconSize + Theme.spacingM Row { @@ -596,40 +608,104 @@ Item { "text": "Time", "icon": "access_time" }] - currentIndex: SessionData.wallpaperCyclingMode === "time" ? 1 : 0 + currentIndex: { + if (SessionData.perMonitorWallpaper) { + return SessionData.getMonitorCyclingSettings(selectedMonitorName).mode === "time" ? 1 : 0 + } else { + return SessionData.wallpaperCyclingMode === "time" ? 1 : 0 + } + } onTabClicked: index => { - SessionData.setWallpaperCyclingMode(index === 1 ? "time" : "interval") + if (SessionData.perMonitorWallpaper) { + SessionData.setMonitorCyclingMode(selectedMonitorName, index === 1 ? "time" : "interval") + } else { + SessionData.setWallpaperCyclingMode(index === 1 ? "time" : "interval") + } } + + Connections { + target: personalizationTab + function onSelectedMonitorNameChanged() { + modeTabBar.currentIndex = Qt.binding(() => { + if (SessionData.perMonitorWallpaper) { + return SessionData.getMonitorCyclingSettings(selectedMonitorName).mode === "time" ? 1 : 0 + } else { + return SessionData.wallpaperCyclingMode === "time" ? 1 : 0 + } + }) + Qt.callLater(modeTabBar.updateIndicator) + } + } } } } // Interval settings DankDropdown { + id: intervalDropdown property var intervalOptions: ["1 minute", "5 minutes", "15 minutes", "30 minutes", "1 hour", "1.5 hours", "2 hours", "3 hours", "4 hours", "6 hours", "8 hours", "12 hours"] property var intervalValues: [60, 300, 900, 1800, 3600, 5400, 7200, 10800, 14400, 21600, 28800, 43200] width: parent.width - parent.leftPadding - visible: SessionData.wallpaperCyclingMode === "interval" + visible: { + if (SessionData.perMonitorWallpaper) { + return SessionData.getMonitorCyclingSettings(selectedMonitorName).mode === "interval" + } else { + return SessionData.wallpaperCyclingMode === "interval" + } + } text: "Interval" description: "How often to change wallpaper" options: intervalOptions currentValue: { - const currentSeconds = SessionData.wallpaperCyclingInterval + var currentSeconds + if (SessionData.perMonitorWallpaper) { + currentSeconds = SessionData.getMonitorCyclingSettings(selectedMonitorName).interval + } else { + currentSeconds = SessionData.wallpaperCyclingInterval + } const index = intervalValues.indexOf(currentSeconds) return index >= 0 ? intervalOptions[index] : "5 minutes" } onValueChanged: value => { const index = intervalOptions.indexOf(value) - if (index >= 0) - SessionData.setWallpaperCyclingInterval(intervalValues[index]) + if (index >= 0) { + if (SessionData.perMonitorWallpaper) { + SessionData.setMonitorCyclingInterval(selectedMonitorName, intervalValues[index]) + } else { + SessionData.setWallpaperCyclingInterval(intervalValues[index]) + } + } } + + Connections { + target: personalizationTab + function onSelectedMonitorNameChanged() { + // Force dropdown to refresh its currentValue + Qt.callLater(() => { + var currentSeconds + if (SessionData.perMonitorWallpaper) { + currentSeconds = SessionData.getMonitorCyclingSettings(selectedMonitorName).interval + } else { + currentSeconds = SessionData.wallpaperCyclingInterval + } + const index = intervalDropdown.intervalValues.indexOf(currentSeconds) + intervalDropdown.currentValue = index >= 0 ? intervalDropdown.intervalOptions[index] : "5 minutes" + }) + } + } } // Time settings Row { spacing: Theme.spacingM - visible: SessionData.wallpaperCyclingMode === "time" + visible: { + if (SessionData.perMonitorWallpaper) { + return SessionData.getMonitorCyclingSettings(selectedMonitorName).mode === "time" + } else { + return SessionData.wallpaperCyclingMode === "time" + } + } width: parent.width - parent.leftPadding StyledText { @@ -640,32 +716,71 @@ Item { } DankTextField { + id: timeTextField width: 100 height: 40 - text: SessionData.wallpaperCyclingTime + text: { + if (SessionData.perMonitorWallpaper) { + return SessionData.getMonitorCyclingSettings(selectedMonitorName).time + } else { + return SessionData.wallpaperCyclingTime + } + } placeholderText: "00:00" maximumLength: 5 topPadding: Theme.spacingS bottomPadding: Theme.spacingS onAccepted: { var isValid = /^([0-1][0-9]|2[0-3]):[0-5][0-9]$/.test(text) - if (isValid) - SessionData.setWallpaperCyclingTime(text) - else - text = SessionData.wallpaperCyclingTime + if (isValid) { + if (SessionData.perMonitorWallpaper) { + SessionData.setMonitorCyclingTime(selectedMonitorName, text) + } else { + SessionData.setWallpaperCyclingTime(text) + } + } else { + if (SessionData.perMonitorWallpaper) { + text = SessionData.getMonitorCyclingSettings(selectedMonitorName).time + } else { + text = SessionData.wallpaperCyclingTime + } + } } onEditingFinished: { var isValid = /^([0-1][0-9]|2[0-3]):[0-5][0-9]$/.test(text) - if (isValid) - SessionData.setWallpaperCyclingTime(text) - else - text = SessionData.wallpaperCyclingTime + if (isValid) { + if (SessionData.perMonitorWallpaper) { + SessionData.setMonitorCyclingTime(selectedMonitorName, text) + } else { + SessionData.setWallpaperCyclingTime(text) + } + } else { + if (SessionData.perMonitorWallpaper) { + text = SessionData.getMonitorCyclingSettings(selectedMonitorName).time + } else { + text = SessionData.wallpaperCyclingTime + } + } } anchors.verticalCenter: parent.verticalCenter validator: RegularExpressionValidator { regularExpression: /^([0-1][0-9]|2[0-3]):[0-5][0-9]$/ } + + Connections { + target: personalizationTab + function onSelectedMonitorNameChanged() { + // Force text field to refresh its value + Qt.callLater(() => { + if (SessionData.perMonitorWallpaper) { + timeTextField.text = SessionData.getMonitorCyclingSettings(selectedMonitorName).time + } else { + timeTextField.text = SessionData.wallpaperCyclingTime + } + }) + } + } } StyledText { @@ -695,10 +810,13 @@ Item { case "wipe": return "Wipe" case "disc": return "Disc" case "stripes": return "Stripes" + case "iris bloom": return "Iris Bloom" + case "pixelate": return "Pixelate" + case "portal": return "Portal" default: return "Fade" } } - options: ["Fade", "Wipe", "Disc", "Stripes"] + options: ["Fade", "Wipe", "Disc", "Stripes", "Iris Bloom", "Pixelate", "Portal"] onValueChanged: value => { var transition = value.toLowerCase() SessionData.setWallpaperTransition(transition) diff --git a/Modules/WallpaperBackground.qml b/Modules/WallpaperBackground.qml index 03425061..eb921ea0 100644 --- a/Modules/WallpaperBackground.qml +++ b/Modules/WallpaperBackground.qml @@ -282,6 +282,78 @@ LazyLoader { fragmentShader: Qt.resolvedUrl("../Shaders/qsb/wp_stripes.frag.qsb") } + ShaderEffect { + id: irisBloomShader + anchors.fill: parent + visible: (root.transitionType === "iris bloom" || root.transitionType === "none") && (root.hasCurrent || root.booting) + + property variant source1: root.hasCurrent ? currentWallpaper : transparentSource + property variant source2: nextWallpaper + property real progress: root.transitionProgress + property real smoothness: root.edgeSmoothness + property real centerX: root.discCenterX + property real centerY: root.discCenterY + property real aspectRatio: root.width / root.height + property real fillMode: root.fillMode + property vector4d fillColor: root.fillColor + property real imageWidth1: Math.max(1, root.hasCurrent ? source1.sourceSize.width : width) + property real imageHeight1: Math.max(1, root.hasCurrent ? source1.sourceSize.height : height) + property real imageWidth2: Math.max(1, source2.sourceSize.width) + property real imageHeight2: Math.max(1, source2.sourceSize.height) + property real screenWidth: width + property real screenHeight: height + + fragmentShader: Qt.resolvedUrl("../Shaders/qsb/wp_iris_bloom.frag.qsb") + } + + ShaderEffect { + id: pixelateShader + anchors.fill: parent + visible: root.transitionType === "pixelate" && (root.hasCurrent || root.booting) + + property variant source1: root.hasCurrent ? currentWallpaper : transparentSource + property variant source2: nextWallpaper + property real progress: root.transitionProgress + property real smoothness: root.edgeSmoothness // controls starting block size + property real fillMode: root.fillMode + property vector4d fillColor: root.fillColor + property real imageWidth1: Math.max(1, root.hasCurrent ? source1.sourceSize.width : width) + property real imageHeight1: Math.max(1, root.hasCurrent ? source1.sourceSize.height : height) + property real imageWidth2: Math.max(1, source2.sourceSize.width) + property real imageHeight2: Math.max(1, source2.sourceSize.height) + property real screenWidth: width + property real screenHeight: height + property real centerX: root.discCenterX + property real centerY: root.discCenterY + property real aspectRatio: root.width / root.height + + fragmentShader: Qt.resolvedUrl("../Shaders/qsb/wp_pixelate.frag.qsb") + } + + ShaderEffect { + id: portalShader + anchors.fill: parent + visible: root.transitionType === "portal" && (root.hasCurrent || root.booting) + + property variant source1: root.hasCurrent ? currentWallpaper : transparentSource + property variant source2: nextWallpaper + property real progress: root.transitionProgress + property real smoothness: root.edgeSmoothness + property real aspectRatio: root.width / root.height + property real centerX: root.discCenterX + property real centerY: root.discCenterY + property real fillMode: root.fillMode + property vector4d fillColor: root.fillColor + property real imageWidth1: Math.max(1, root.hasCurrent ? source1.sourceSize.width : width) + property real imageHeight1: Math.max(1, root.hasCurrent ? source1.sourceSize.height : height) + property real imageWidth2: Math.max(1, source2.sourceSize.width) + property real imageHeight2: Math.max(1, source2.sourceSize.height) + property real screenWidth: width + property real screenHeight: height + + fragmentShader: Qt.resolvedUrl("../Shaders/qsb/wp_portal.frag.qsb") + } + NumberAnimation { id: transitionAnimation target: root diff --git a/Services/WallpaperCyclingService.qml b/Services/WallpaperCyclingService.qml index 5d7b072d..7a49ef7c 100644 --- a/Services/WallpaperCyclingService.qml +++ b/Services/WallpaperCyclingService.qml @@ -13,11 +13,63 @@ Singleton { property string cachedCyclingTime: SessionData.wallpaperCyclingTime property int cachedCyclingInterval: SessionData.wallpaperCyclingInterval property string lastTimeCheck: "" - + property var monitorTimers: ({}) + property var monitorLastTimeChecks: ({}) + property var monitorProcesses: ({}) Component.onCompleted: { updateCyclingState() } + Component { + id: monitorTimerComponent + Timer { + property string targetScreen: "" + running: false + repeat: true + onTriggered: { + if (typeof WallpaperCyclingService !== "undefined" && targetScreen !== "") { + WallpaperCyclingService.cycleNextForMonitor(targetScreen) + } + } + } + } + + Component { + id: monitorProcessComponent + Process { + property string targetScreenName: "" + property string currentWallpaper: "" + property bool goToPrevious: false + running: false + stdout: StdioCollector { + onStreamFinished: { + if (text && text.trim()) { + const files = text.trim().split('\n').filter(file => file.length > 0) + if (files.length <= 1) return + const wallpaperList = files.sort() + const currentPath = currentWallpaper + let currentIndex = wallpaperList.findIndex(path => path === currentPath) + if (currentIndex === -1) currentIndex = 0 + let targetIndex + if (goToPrevious) { + targetIndex = currentIndex === 0 ? wallpaperList.length - 1 : currentIndex - 1 + } else { + targetIndex = (currentIndex + 1) % wallpaperList.length + } + const targetWallpaper = wallpaperList[targetIndex] + if (targetWallpaper && targetWallpaper !== currentPath) { + if (targetScreenName) { + SessionData.setMonitorWallpaper(targetScreenName, targetWallpaper) + } else { + SessionData.setWallpaper(targetWallpaper) + } + } + } + } + } + } + } + Connections { target: SessionData @@ -46,13 +98,46 @@ Singleton { function onPerMonitorWallpaperChanged() { updateCyclingState() } + + function onMonitorCyclingSettingsChanged() { + updateCyclingState() + } } function updateCyclingState() { - if (SessionData.wallpaperCyclingEnabled && SessionData.wallpaperPath && !SessionData.perMonitorWallpaper) { + if (SessionData.perMonitorWallpaper) { + stopCycling() + updatePerMonitorCycling() + } else if (SessionData.wallpaperCyclingEnabled && SessionData.wallpaperPath) { startCycling() + stopAllMonitorCycling() } else { stopCycling() + stopAllMonitorCycling() + } + } + + function updatePerMonitorCycling() { + if (typeof Quickshell === "undefined") return + + var screens = Quickshell.screens + for (var i = 0; i < screens.length; i++) { + var screenName = screens[i].name + var settings = SessionData.getMonitorCyclingSettings(screenName) + var wallpaper = SessionData.getMonitorWallpaper(screenName) + + if (settings.enabled && wallpaper && !wallpaper.startsWith("#") && !wallpaper.startsWith("we:")) { + startMonitorCycling(screenName, settings) + } else { + stopMonitorCycling(screenName) + } + } + } + + function stopAllMonitorCycling() { + var screenNames = Object.keys(monitorTimers) + for (var i = 0; i < screenNames.length; i++) { + stopMonitorCycling(screenNames[i]) } } @@ -72,15 +157,80 @@ Singleton { cyclingActive = false } + function startMonitorCycling(screenName, settings) { + if (settings.mode === "interval") { + var timer = monitorTimers[screenName] + if (!timer && monitorTimerComponent && monitorTimerComponent.status === Component.Ready) { + var newTimers = Object.assign({}, monitorTimers) + newTimers[screenName] = monitorTimerComponent.createObject(root) + newTimers[screenName].targetScreen = screenName + monitorTimers = newTimers + timer = monitorTimers[screenName] + } + if (timer) { + timer.interval = settings.interval * 1000 + timer.start() + } + } else if (settings.mode === "time") { + var newChecks = Object.assign({}, monitorLastTimeChecks) + newChecks[screenName] = "" + monitorLastTimeChecks = newChecks + } + } + + function stopMonitorCycling(screenName) { + var timer = monitorTimers[screenName] + if (timer) { + timer.stop() + timer.destroy() + var newTimers = Object.assign({}, monitorTimers) + delete newTimers[screenName] + monitorTimers = newTimers + } + + var process = monitorProcesses[screenName] + if (process) { + process.destroy() + var newProcesses = Object.assign({}, monitorProcesses) + delete newProcesses[screenName] + monitorProcesses = newProcesses + } + + var newChecks = Object.assign({}, monitorLastTimeChecks) + delete newChecks[screenName] + monitorLastTimeChecks = newChecks + } + function cycleToNextWallpaper(screenName, wallpaperPath) { const currentWallpaper = wallpaperPath || SessionData.wallpaperPath if (!currentWallpaper) return const wallpaperDir = currentWallpaper.substring(0, currentWallpaper.lastIndexOf('/')) - cyclingProcess.command = ["sh", "-c", `ls -1 "${wallpaperDir}"/*.jpg "${wallpaperDir}"/*.jpeg "${wallpaperDir}"/*.png "${wallpaperDir}"/*.bmp "${wallpaperDir}"/*.gif "${wallpaperDir}"/*.webp 2>/dev/null | sort`] - cyclingProcess.targetScreenName = screenName || "" - cyclingProcess.currentWallpaper = currentWallpaper - cyclingProcess.running = true + + if (screenName && monitorProcessComponent && monitorProcessComponent.status === Component.Ready) { + // Use per-monitor process + var process = monitorProcesses[screenName] + if (!process) { + var newProcesses = Object.assign({}, monitorProcesses) + newProcesses[screenName] = monitorProcessComponent.createObject(root) + monitorProcesses = newProcesses + process = monitorProcesses[screenName] + } + + if (process) { + process.command = ["sh", "-c", `ls -1 "${wallpaperDir}"/*.jpg "${wallpaperDir}"/*.jpeg "${wallpaperDir}"/*.png "${wallpaperDir}"/*.bmp "${wallpaperDir}"/*.gif "${wallpaperDir}"/*.webp 2>/dev/null | sort`] + process.targetScreenName = screenName + process.currentWallpaper = currentWallpaper + process.goToPrevious = false + process.running = true + } + } else { + // Use global process for fallback + cyclingProcess.command = ["sh", "-c", `ls -1 "${wallpaperDir}"/*.jpg "${wallpaperDir}"/*.jpeg "${wallpaperDir}"/*.png "${wallpaperDir}"/*.bmp "${wallpaperDir}"/*.gif "${wallpaperDir}"/*.webp 2>/dev/null | sort`] + cyclingProcess.targetScreenName = screenName || "" + cyclingProcess.currentWallpaper = currentWallpaper + cyclingProcess.running = true + } } function cycleToPrevWallpaper(screenName, wallpaperPath) { @@ -88,10 +238,31 @@ Singleton { if (!currentWallpaper) return const wallpaperDir = currentWallpaper.substring(0, currentWallpaper.lastIndexOf('/')) - prevCyclingProcess.command = ["sh", "-c", `ls -1 "${wallpaperDir}"/*.jpg "${wallpaperDir}"/*.jpeg "${wallpaperDir}"/*.png "${wallpaperDir}"/*.bmp "${wallpaperDir}"/*.gif "${wallpaperDir}"/*.webp 2>/dev/null | sort`] - prevCyclingProcess.targetScreenName = screenName || "" - prevCyclingProcess.currentWallpaper = currentWallpaper - prevCyclingProcess.running = true + + if (screenName && monitorProcessComponent && monitorProcessComponent.status === Component.Ready) { + // Use per-monitor process (same as next, but with prev flag) + var process = monitorProcesses[screenName] + if (!process) { + var newProcesses = Object.assign({}, monitorProcesses) + newProcesses[screenName] = monitorProcessComponent.createObject(root) + monitorProcesses = newProcesses + process = monitorProcesses[screenName] + } + + if (process) { + process.command = ["sh", "-c", `ls -1 "${wallpaperDir}"/*.jpg "${wallpaperDir}"/*.jpeg "${wallpaperDir}"/*.png "${wallpaperDir}"/*.bmp "${wallpaperDir}"/*.gif "${wallpaperDir}"/*.webp 2>/dev/null | sort`] + process.targetScreenName = screenName + process.currentWallpaper = currentWallpaper + process.goToPrevious = true + process.running = true + } + } else { + // Use global process for fallback + prevCyclingProcess.command = ["sh", "-c", `ls -1 "${wallpaperDir}"/*.jpg "${wallpaperDir}"/*.jpeg "${wallpaperDir}"/*.png "${wallpaperDir}"/*.bmp "${wallpaperDir}"/*.gif "${wallpaperDir}"/*.webp 2>/dev/null | sort`] + prevCyclingProcess.targetScreenName = screenName || "" + prevCyclingProcess.currentWallpaper = currentWallpaper + prevCyclingProcess.running = true + } } function cycleNextManually() { @@ -122,7 +293,7 @@ Singleton { function cycleNextForMonitor(screenName) { if (!screenName) return - + var currentWallpaper = SessionData.getMonitorWallpaper(screenName) if (currentWallpaper) { cycleToNextWallpaper(screenName, currentWallpaper) @@ -141,12 +312,41 @@ Singleton { function checkTimeBasedCycling() { const currentTime = Qt.formatTime(systemClock.date, "hh:mm") - if (currentTime === cachedCyclingTime - && currentTime !== lastTimeCheck) { - lastTimeCheck = currentTime - cycleToNextWallpaper() - } else if (currentTime !== cachedCyclingTime) { - lastTimeCheck = "" + if (!SessionData.perMonitorWallpaper) { + if (currentTime === cachedCyclingTime && currentTime !== lastTimeCheck) { + lastTimeCheck = currentTime + cycleToNextWallpaper() + } else if (currentTime !== cachedCyclingTime) { + lastTimeCheck = "" + } + } else { + checkPerMonitorTimeBasedCycling(currentTime) + } + } + + function checkPerMonitorTimeBasedCycling(currentTime) { + if (typeof Quickshell === "undefined") return + + var screens = Quickshell.screens + for (var i = 0; i < screens.length; i++) { + var screenName = screens[i].name + var settings = SessionData.getMonitorCyclingSettings(screenName) + var wallpaper = SessionData.getMonitorWallpaper(screenName) + + if (settings.enabled && settings.mode === "time" && wallpaper && !wallpaper.startsWith("#") && !wallpaper.startsWith("we:")) { + var lastCheck = monitorLastTimeChecks[screenName] || "" + + if (currentTime === settings.time && currentTime !== lastCheck) { + var newChecks = Object.assign({}, monitorLastTimeChecks) + newChecks[screenName] = currentTime + monitorLastTimeChecks = newChecks + cycleNextForMonitor(screenName) + } else if (currentTime !== settings.time) { + var newChecks = Object.assign({}, monitorLastTimeChecks) + newChecks[screenName] = "" + monitorLastTimeChecks = newChecks + } + } } } @@ -162,7 +362,7 @@ Singleton { id: systemClock precision: SystemClock.Minutes onDateChanged: { - if (SessionData.wallpaperCyclingMode === "time" && cyclingActive) { + if ((SessionData.wallpaperCyclingMode === "time" && cyclingActive) || SessionData.perMonitorWallpaper) { checkTimeBasedCycling() } } diff --git a/Shaders/frag/circled_image.frag b/Shaders/frag/circled_image.frag deleted file mode 100644 index 308a9c5b..00000000 --- a/Shaders/frag/circled_image.frag +++ /dev/null @@ -1,30 +0,0 @@ -#version 450 - -layout(location = 0) in vec2 qt_TexCoord0; -layout(location = 0) out vec4 fragColor; - -layout(binding = 1) uniform sampler2D source; - -layout(std140, binding = 0) uniform buf { - mat4 qt_Matrix; - float qt_Opacity; - float imageOpacity; -} ubuf; - -void main() { - // Center coordinates around (0, 0) - vec2 uv = qt_TexCoord0 - 0.5; - - // Calculate distance from center - float distance = length(uv); - - // Create circular mask - anything beyond radius 0.5 is transparent - float mask = 1.0 - smoothstep(0.48, 0.52, distance); - - // Sample the texture - vec4 color = texture(source, qt_TexCoord0); - - // Apply the circular mask and opacity - float finalAlpha = color.a * mask * ubuf.imageOpacity * ubuf.qt_Opacity; - fragColor = vec4(color.rgb * finalAlpha, finalAlpha); -} \ No newline at end of file diff --git a/Shaders/frag/rounded_image.frag b/Shaders/frag/rounded_image.frag deleted file mode 100644 index 9d493b21..00000000 --- a/Shaders/frag/rounded_image.frag +++ /dev/null @@ -1,56 +0,0 @@ -#version 450 - -layout(location = 0) in vec2 qt_TexCoord0; -layout(location = 0) out vec4 fragColor; - -layout(binding = 1) uniform sampler2D source; - -layout(std140, binding = 0) uniform buf { - mat4 qt_Matrix; - float qt_Opacity; - // Custom properties with non-conflicting names - float itemWidth; - float itemHeight; - float cornerRadius; - float imageOpacity; -} ubuf; - -// Function to calculate the signed distance from a point to a rounded box -float roundedBoxSDF(vec2 centerPos, vec2 boxSize, float radius) { - vec2 d = abs(centerPos) - boxSize + radius; - return length(max(d, 0.0)) + min(max(d.x, d.y), 0.0) - radius; -} - -void main() { - // Get size from uniforms - vec2 itemSize = vec2(ubuf.itemWidth, ubuf.itemHeight); - float cornerRadius = ubuf.cornerRadius; - float itemOpacity = ubuf.imageOpacity; - - // Normalize coordinates to [-0.5, 0.5] range - vec2 uv = qt_TexCoord0 - 0.5; - - // Scale by aspect ratio to maintain uniform rounding - vec2 aspectRatio = itemSize / max(itemSize.x, itemSize.y); - uv *= aspectRatio; - - // Calculate half size in normalized space - vec2 halfSize = 0.5 * aspectRatio; - - // Normalize the corner radius - float normalizedRadius = cornerRadius / max(itemSize.x, itemSize.y); - - // Calculate distance to rounded rectangle - float distance = roundedBoxSDF(uv, halfSize, normalizedRadius); - - // Create smooth alpha mask - float smoothedAlpha = 1.0 - smoothstep(0.0, fwidth(distance), distance); - - // Sample the texture - vec4 color = texture(source, qt_TexCoord0); - - // Apply the rounded mask and opacity - // Make sure areas outside the rounded rect are completely transparent - float finalAlpha = color.a * smoothedAlpha * itemOpacity * ubuf.qt_Opacity; - fragColor = vec4(color.rgb * finalAlpha, finalAlpha); -} \ No newline at end of file diff --git a/Shaders/frag/wp_iris_bloom.frag b/Shaders/frag/wp_iris_bloom.frag new file mode 100644 index 00000000..e06e44b5 --- /dev/null +++ b/Shaders/frag/wp_iris_bloom.frag @@ -0,0 +1,100 @@ +// ===== wp_iris_bloom.frag ===== +#version 450 + +layout(location = 0) in vec2 qt_TexCoord0; +layout(location = 0) out vec4 fragColor; + +layout(binding = 1) uniform sampler2D source1; // Current wallpaper +layout(binding = 2) uniform sampler2D source2; // Next wallpaper + +layout(std140, binding = 0) uniform buf { + mat4 qt_Matrix; + float qt_Opacity; + float progress; // 0.0 -> 1.0 + float centerX; // 0..1 + float centerY; // 0..1 + float smoothness; // 0..1 (edge softness) + float aspectRatio; // width / height + + // Fill mode parameters + float fillMode; // 0=no(center), 1=crop(fill), 2=fit(contain), 3=stretch + float imageWidth1; + float imageHeight1; + float imageWidth2; + float imageHeight2; + float screenWidth; + float screenHeight; + vec4 fillColor; +} ubuf; + +vec2 calculateUV(vec2 uv, float imgWidth, float imgHeight) { + vec2 transformedUV = uv; + + if (ubuf.fillMode < 0.5) { + vec2 screenPixel = uv * vec2(ubuf.screenWidth, ubuf.screenHeight); + vec2 imageOffset = (vec2(ubuf.screenWidth, ubuf.screenHeight) - vec2(imgWidth, imgHeight)) * 0.5; + vec2 imagePixel = screenPixel - imageOffset; + transformedUV = imagePixel / vec2(imgWidth, imgHeight); + } + else if (ubuf.fillMode < 1.5) { + float scale = max(ubuf.screenWidth / imgWidth, ubuf.screenHeight / imgHeight); + vec2 scaledImageSize = vec2(imgWidth, imgHeight) * scale; + vec2 offset = (scaledImageSize - vec2(ubuf.screenWidth, ubuf.screenHeight)) / scaledImageSize; + transformedUV = uv * (vec2(1.0) - offset) + offset * 0.5; + } + else if (ubuf.fillMode < 2.5) { + float scale = min(ubuf.screenWidth / imgWidth, ubuf.screenHeight / imgHeight); + vec2 scaledImageSize = vec2(imgWidth, imgHeight) * scale; + vec2 offset = (vec2(ubuf.screenWidth, ubuf.screenHeight) - vec2(scaledImageSize)) * 0.5; + vec2 screenPixel = uv * vec2(ubuf.screenWidth, ubuf.screenHeight); + vec2 imagePixel = (screenPixel - offset) / scale; + transformedUV = imagePixel / vec2(imgWidth, imgHeight); + } + // else stretch + + return transformedUV; +} + +vec4 sampleWithFillMode(sampler2D tex, vec2 uv, float imgWidth, float imgHeight) { + vec2 tuv = calculateUV(uv, imgWidth, imgHeight); + if (tuv.x < 0.0 || tuv.x > 1.0 || tuv.y < 0.0 || tuv.y > 1.0) { + return ubuf.fillColor; + } + return texture(tex, tuv); +} + +void main() { + vec2 uv = qt_TexCoord0; + + vec4 color1 = sampleWithFillMode(source1, uv, ubuf.imageWidth1, ubuf.imageHeight1); + vec4 color2 = sampleWithFillMode(source2, uv, ubuf.imageWidth2, ubuf.imageHeight2); + + // Edge softness mapping + float edgeSoft = mix(0.001, 0.45, ubuf.smoothness * ubuf.smoothness); + + // Aspect-corrected coordinates so the iris stays circular + vec2 center = vec2(ubuf.centerX, ubuf.centerY); + vec2 acUv = vec2(uv.x * ubuf.aspectRatio, uv.y); + vec2 acCenter = vec2(center.x * ubuf.aspectRatio, center.y); + float dist = length(acUv - acCenter); + + // Max radius needed to cover the screen from the chosen center + float maxDistX = max(center.x * ubuf.aspectRatio, (1.0 - center.x) * ubuf.aspectRatio); + float maxDistY = max(center.y, 1.0 - center.y); + float maxDist = length(vec2(maxDistX, maxDistY)); + + float p = ubuf.progress; + p = p * p * (3.0 - 2.0 * p); + + float radius = p * maxDist - edgeSoft; + + // Soft circular edge: inside -> color2 (new), outside -> color1 (old) + float t = smoothstep(radius - edgeSoft, radius + edgeSoft, dist); + vec4 col = mix(color2, color1, t); + + // Exact snaps at ends + if (ubuf.progress <= 0.0) col = color1; + if (ubuf.progress >= 1.0) col = color2; + + fragColor = col * ubuf.qt_Opacity; +} diff --git a/Shaders/frag/wp_pixelate.frag b/Shaders/frag/wp_pixelate.frag new file mode 100644 index 00000000..d5f5e030 --- /dev/null +++ b/Shaders/frag/wp_pixelate.frag @@ -0,0 +1,99 @@ +// ===== wp_pixelate.frag ===== +#version 450 + +layout(location = 0) in vec2 qt_TexCoord0; +layout(location = 0) out vec4 fragColor; + +layout(binding = 1) uniform sampler2D source1; // Current wallpaper (underlay) +layout(binding = 2) uniform sampler2D source2; // Next wallpaper (pixelated overlay → sharp) + +layout(std140, binding = 0) uniform buf { + mat4 qt_Matrix; + float qt_Opacity; + float progress; // 0..1 + float centerX; // (unused, API compat) + float centerY; // (unused) + float smoothness; // controls starting block size (0..1) + float aspectRatio; // (unused) + + // Fill mode parameters + float fillMode; // 0=no(center), 1=crop, 2=fit, 3=stretch + float imageWidth1; + float imageHeight1; + float imageWidth2; + float imageHeight2; + float screenWidth; + float screenHeight; + vec4 fillColor; +} ubuf; + +vec2 calculateUV(vec2 uv, float imgWidth, float imgHeight) { + vec2 transformedUV = uv; + if (ubuf.fillMode < 0.5) { + vec2 screenPixel = uv * vec2(ubuf.screenWidth, ubuf.screenHeight); + vec2 imageOffset = (vec2(ubuf.screenWidth, ubuf.screenHeight) - vec2(imgWidth, imgHeight)) * 0.5; + vec2 imagePixel = screenPixel - imageOffset; + transformedUV = imagePixel / vec2(imgWidth, imgHeight); + } else if (ubuf.fillMode < 1.5) { + float scale = max(ubuf.screenWidth / imgWidth, ubuf.screenHeight / imgHeight); + vec2 scaledImageSize = vec2(imgWidth, imgHeight) * scale; + vec2 offset = (scaledImageSize - vec2(ubuf.screenWidth, ubuf.screenHeight)) / scaledImageSize; + transformedUV = uv * (vec2(1.0) - offset) + offset * 0.5; + } else if (ubuf.fillMode < 2.5) { + float scale = min(ubuf.screenWidth / imgWidth, ubuf.screenHeight / imgHeight); + vec2 scaledImageSize = vec2(imgWidth, imgHeight) * scale; + vec2 offset = (vec2(ubuf.screenWidth, ubuf.screenHeight) - scaledImageSize) * 0.5; + vec2 screenPixel = uv * vec2(ubuf.screenWidth, ubuf.screenHeight); + vec2 imagePixel = (screenPixel - offset) / scale; + transformedUV = imagePixel / vec2(imgWidth, imgHeight); + } + return transformedUV; +} + +vec4 sampleWithFillMode(sampler2D tex, vec2 uv, float w, float h) { + vec2 tuv = calculateUV(uv, w, h); + if (tuv.x < 0.0 || tuv.x > 1.0 || tuv.y < 0.0 || tuv.y > 1.0) return ubuf.fillColor; + return texture(tex, tuv); +} + +vec2 quantizeUV(vec2 uv, float cellPx) { + vec2 screenSize = vec2(max(1.0, ubuf.screenWidth), max(1.0, ubuf.screenHeight)); + float cell = max(1.0, ceil(cellPx)); // integer pixel cells + vec2 grid = floor(uv * screenSize / cell) * cell + 0.5 * cell; + return grid / screenSize; +} + +void main() { + vec2 uv = qt_TexCoord0; + + vec4 oldCol = sampleWithFillMode(source1, uv, ubuf.imageWidth1, ubuf.imageHeight1); + + float p = clamp(ubuf.progress, 0.0, 1.0); + float pe = p * p * (3.0 - 2.0 * p); // smootherstep for opacity + + // Screen-relative starting cell size: + // smoothness=0 → ~10% of min(screen), smoothness=1 → ~80% of min(screen) + float s = clamp(ubuf.smoothness, 0.0, 1.0); + float minSide = min(max(1.0, ubuf.screenWidth), max(1.0, ubuf.screenHeight)); + float startPx = mix(minSide * 0.10, minSide * 0.80, s); // big and obvious even on small screens + + // Cell size shrinks continuously from startPx → 1 as p grows + float cellPx = mix(startPx, 1.0, p); + + // Sample next as pixelated overlay + vec2 uvq = quantizeUV(uv, cellPx); + vec4 newPix = sampleWithFillMode(source2, uvq, ubuf.imageWidth2, ubuf.imageHeight2); + + // As we approach the end, sharpen the next from pixelated → full-res + float sharpen = smoothstep(0.75, 1.0, p); // only near the end + vec4 newFull = sampleWithFillMode(source2, uv, ubuf.imageWidth2, ubuf.imageHeight2); + vec4 newCol = mix(newPix, newFull, sharpen); + + vec4 outColor = mix(oldCol, newCol, pe); + + // Snaps + if (p <= 0.0) outColor = oldCol; + if (p >= 1.0) outColor = newFull; + + fragColor = outColor * ubuf.qt_Opacity; +} diff --git a/Shaders/frag/wp_portal.frag b/Shaders/frag/wp_portal.frag new file mode 100644 index 00000000..0a2cc427 --- /dev/null +++ b/Shaders/frag/wp_portal.frag @@ -0,0 +1,103 @@ +// ===== wp_portal.frag ===== +#version 450 + +layout(location = 0) in vec2 qt_TexCoord0; +layout(location = 0) out vec4 fragColor; + +layout(binding = 1) uniform sampler2D source1; // Current wallpaper (shrinks away) +layout(binding = 2) uniform sampler2D source2; // Next wallpaper (underneath) + +layout(std140, binding = 0) uniform buf { + mat4 qt_Matrix; + float qt_Opacity; + float progress; // 0..1 + float centerX; // 0..1 + float centerY; // 0..1 + float smoothness; // 0..1 (edge softness) + float aspectRatio; // width / height + + // Fill mode parameters + float fillMode; // 0=no(center), 1=crop(fill), 2=fit(contain), 3=stretch + float imageWidth1; + float imageHeight1; + float imageWidth2; + float imageHeight2; + float screenWidth; + float screenHeight; + vec4 fillColor; +} ubuf; + +vec2 calculateUV(vec2 uv, float imgWidth, float imgHeight) { + vec2 transformedUV = uv; + + if (ubuf.fillMode < 0.5) { + vec2 screenPixel = uv * vec2(ubuf.screenWidth, ubuf.screenHeight); + vec2 imageOffset = (vec2(ubuf.screenWidth, ubuf.screenHeight) - vec2(imgWidth, imgHeight)) * 0.5; + vec2 imagePixel = screenPixel - imageOffset; + transformedUV = imagePixel / vec2(imgWidth, imgHeight); + } + else if (ubuf.fillMode < 1.5) { + float scale = max(ubuf.screenWidth / imgWidth, ubuf.screenHeight / imgHeight); + vec2 scaledImageSize = vec2(imgWidth, imgHeight) * scale; + vec2 offset = (scaledImageSize - vec2(ubuf.screenWidth, ubuf.screenHeight)) / scaledImageSize; + transformedUV = uv * (vec2(1.0) - offset) + offset * 0.5; + } + else if (ubuf.fillMode < 2.5) { + float scale = min(ubuf.screenWidth / imgWidth, ubuf.screenHeight / imgHeight); + vec2 scaledImageSize = vec2(imgWidth, imgHeight) * scale; + vec2 offset = (vec2(ubuf.screenWidth, ubuf.screenHeight) - scaledImageSize) * 0.5; + vec2 screenPixel = uv * vec2(ubuf.screenWidth, ubuf.screenHeight); + vec2 imagePixel = (screenPixel - offset) / scale; + transformedUV = imagePixel / vec2(imgWidth, imgHeight); + } + // else: stretch + + return transformedUV; +} + +vec4 sampleWithFillMode(sampler2D tex, vec2 uv, float w, float h) { + vec2 tuv = calculateUV(uv, w, h); + if (tuv.x < 0.0 || tuv.x > 1.0 || tuv.y < 0.0 || tuv.y > 1.0) return ubuf.fillColor; + return texture(tex, tuv); +} + +void main() { + vec2 uv = qt_TexCoord0; + + vec4 oldCol = sampleWithFillMode(source1, uv, ubuf.imageWidth1, ubuf.imageHeight1); + vec4 newCol = sampleWithFillMode(source2, uv, ubuf.imageWidth2, ubuf.imageHeight2); + + // Edge softness + float edgeSoft = mix(0.001, 0.45, ubuf.smoothness * ubuf.smoothness); + + // Aspect-corrected distance from center (keep circle round) + vec2 center = vec2(ubuf.centerX, ubuf.centerY); + vec2 acUv = vec2(uv.x * ubuf.aspectRatio, uv.y); + vec2 acCenter = vec2(center.x * ubuf.aspectRatio, center.y); + float dist = length(acUv - acCenter); + + // Max radius from center to cover screen + float maxDistX = max(center.x * ubuf.aspectRatio, (1.0 - center.x) * ubuf.aspectRatio); + float maxDistY = max(center.y, 1.0 - center.y); + float maxDist = length(vec2(maxDistX, maxDistY)); + + // Smooth easing for a friendly feel + float p = ubuf.progress; + p = p * p * (3.0 - 2.0 * p); + + // Portal radius shrinks from full to zero (bias by edgeSoft so it vanishes cleanly) + float radius = (1.0 - p) * (maxDist + edgeSoft) - edgeSoft; + + // Inside circle = old wallpaper; outside = new wallpaper + float t = smoothstep(radius - edgeSoft, radius + edgeSoft, dist); + // When radius is large: t ~ 0 inside (old), ~1 outside (new) + // As radius shrinks, old area collapses to center. + + vec4 col = mix(oldCol, newCol, t); + + // Snaps + if (ubuf.progress <= 0.0) col = oldCol; // full old at start + if (ubuf.progress >= 1.0) col = newCol; // full new at end + + fragColor = col * ubuf.qt_Opacity; +} diff --git a/Shaders/qsb/circled_image.frag.qsb b/Shaders/qsb/circled_image.frag.qsb deleted file mode 100644 index 37a99ef08dbbc9febf0363349dbb46c1cdf5b81a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1717 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 V)?6pu6>I*2BCn&be*?wwtCWWGn>qjh diff --git a/Shaders/qsb/wp_iris_bloom.frag.qsb b/Shaders/qsb/wp_iris_bloom.frag.qsb new file mode 100644 index 0000000000000000000000000000000000000000..2244e2e7d65b49cef5233670767c7746be8066c7 GIT binary patch literal 5502 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 A)Bpeg literal 0 HcmV?d00001 diff --git a/Shaders/qsb/wp_pixelate.frag.qsb b/Shaders/qsb/wp_pixelate.frag.qsb new file mode 100644 index 0000000000000000000000000000000000000000..c784cc526cf4a3146d282591ee9230a52959a746 GIT binary patch literal 5792 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 BhOz(v literal 0 HcmV?d00001 diff --git a/Widgets/DankAlbumArt.qml b/Widgets/DankAlbumArt.qml index 74f36da4..d62658b4 100644 --- a/Widgets/DankAlbumArt.qml +++ b/Widgets/DankAlbumArt.qml @@ -159,8 +159,8 @@ Item { imageSource: artUrl || lastValidArtUrl || "" fallbackIcon: "album" - borderColor: Theme.primary - borderWidth: 2 + border.color: Theme.primary + border.width: 2 onImageSourceChanged: { if (imageSource && imageStatus !== Image.Error) { diff --git a/Widgets/DankCircularImage.qml b/Widgets/DankCircularImage.qml index e8a30c92..b53feb75 100644 --- a/Widgets/DankCircularImage.qml +++ b/Widgets/DankCircularImage.qml @@ -1,92 +1,84 @@ import QtQuick +import QtQuick.Effects import Quickshell import qs.Common import qs.Widgets -Item { +Rectangle { id: root property string imageSource: "" property string fallbackIcon: "notifications" property string fallbackText: "" property bool hasImage: imageSource !== "" - property alias imageStatus: sourceImage.status - property color borderColor: "transparent" - property real borderWidth: 0 - property real imageOpacity: 1.0 + property alias imageStatus: internalImage.status - width: 64 - height: 64 + radius: width / 2 + color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1) + border.color: "transparent" + border.width: 0 - Rectangle { - id: background + Image { + id: internalImage anchors.fill: parent - color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1) - radius: width * 0.5 + anchors.margins: 2 + asynchronous: true + fillMode: Image.PreserveAspectCrop + smooth: true + mipmap: true + cache: true + visible: false + source: root.imageSource - Image { - id: sourceImage - anchors.fill: parent - anchors.margins: 2 - source: root.imageSource - visible: false - fillMode: Image.PreserveAspectCrop - smooth: true - mipmap: true - asynchronous: true - antialiasing: true - cache: true - - Component.onCompleted: { - sourceSize.width = 128 - sourceSize.height = 128 - } + Component.onCompleted: { + sourceSize.width = 128 + sourceSize.height = 128 } + } - ShaderEffect { - anchors.fill: parent - anchors.margins: 2 - visible: sourceImage.status === Image.Ready && root.imageSource !== "" + MultiEffect { + anchors.fill: parent + anchors.margins: 2 + source: internalImage + maskEnabled: true + maskSource: circularMask + visible: internalImage.status === Image.Ready && root.imageSource !== "" + maskThresholdMin: 0.5 + maskSpreadAtMin: 1 + } - property var source: ShaderEffectSource { - sourceItem: sourceImage - hideSource: true - live: true - recursive: false - format: ShaderEffectSource.RGBA - } - - property real imageOpacity: root.imageOpacity - - fragmentShader: Qt.resolvedUrl("../Shaders/qsb/circled_image.frag.qsb") - supportsAtlasTextures: false - blending: true - } - - DankIcon { - anchors.centerIn: parent - name: root.fallbackIcon - size: parent.width * 0.5 - color: Theme.surfaceVariantText - visible: sourceImage.status !== Image.Ready && root.imageSource === "" && root.fallbackIcon !== "" - } - - StyledText { - anchors.centerIn: parent - visible: root.imageSource === "" && root.fallbackIcon === "" && root.fallbackText !== "" - text: root.fallbackText - font.pixelSize: Math.max(12, parent.width * 0.36) - font.weight: Font.Bold - color: Theme.primaryText - } + Item { + id: circularMask + width: parent.width - 4 + height: parent.height - 4 + anchors.centerIn: parent + layer.enabled: true + layer.smooth: true + visible: false Rectangle { anchors.fill: parent radius: width / 2 - color: "transparent" - border.color: root.borderColor !== "transparent" ? root.borderColor : Theme.popupBackground() - border.width: root.hasImage && sourceImage.status === Image.Ready ? (root.borderWidth > 0 ? root.borderWidth : 3) : 0 + color: "black" antialiasing: true } } + + DankIcon { + anchors.centerIn: parent + name: root.fallbackIcon + size: parent.width * 0.5 + color: Theme.surfaceVariantText + visible: internalImage.status !== Image.Ready && root.imageSource === "" && root.fallbackIcon !== "" + } + + + StyledText { + anchors.centerIn: parent + visible: root.imageSource === "" && root.fallbackIcon === "" && root.fallbackText !== "" + text: root.fallbackText + font.pixelSize: Math.max(12, parent.width * 0.36) + font.weight: Font.Bold + color: Theme.primaryText + } } \ No newline at end of file diff --git a/shell.qml b/shell.qml index 5fc8135a..d5cedcad 100644 --- a/shell.qml +++ b/shell.qml @@ -34,6 +34,8 @@ ShellRoot { PortalService.init() // Initialize DisplayService night mode functionality DisplayService.nightModeEnabled + // Initialize WallpaperCyclingService + WallpaperCyclingService.cyclingActive } WallpaperBackground {}