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 37a99ef0..00000000 Binary files a/Shaders/qsb/circled_image.frag.qsb and /dev/null differ diff --git a/Shaders/qsb/rounded_image.frag.qsb b/Shaders/qsb/rounded_image.frag.qsb deleted file mode 100644 index c404fc76..00000000 Binary files a/Shaders/qsb/rounded_image.frag.qsb and /dev/null differ diff --git a/Shaders/qsb/wp_iris_bloom.frag.qsb b/Shaders/qsb/wp_iris_bloom.frag.qsb new file mode 100644 index 00000000..2244e2e7 Binary files /dev/null and b/Shaders/qsb/wp_iris_bloom.frag.qsb differ diff --git a/Shaders/qsb/wp_pixelate.frag.qsb b/Shaders/qsb/wp_pixelate.frag.qsb new file mode 100644 index 00000000..c784cc52 Binary files /dev/null and b/Shaders/qsb/wp_pixelate.frag.qsb differ diff --git a/Shaders/qsb/wp_portal.frag.qsb b/Shaders/qsb/wp_portal.frag.qsb new file mode 100644 index 00000000..731f1916 Binary files /dev/null and b/Shaders/qsb/wp_portal.frag.qsb differ 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 {}