pragma Singleton pragma ComponentBehavior: Bound import Quickshell import Quickshell.Io import QtQuick import ZShell.Models import ZShell.Wallpaper import ZShell.Config import qs.Components import qs.Services import qs.Helpers import qs.Paths Searcher { id: root property string actualCurrent: WallpaperPath.currentWallpaperPath readonly property string current: showPreview ? previewPath : actualCurrent readonly property string currentThumbnail: thumbnailFor(current) readonly property bool currentIsVideo: isVideoPath(current) property bool pendingPreviewClear property bool previewColorLock property string previewPath property bool recentlyChanged property bool showPreview: false property int cropVersion: 0 function getCategoryFor(w: WallpaperEntry): string { let category = w.parentDir.slice(Paths.wallsdir.length + 1); if (category.includes("/")) category = category.slice(0, category.indexOf("/")); return category; } readonly property var videoExts: ["mp4", "mkv", "webm", "avi", "mov", "wmv", "flv", "m4v", "mpg", "mpeg", "3gp", "ogv", "ts", "vob"] function isVideoPath(path: string): bool { if (!path) return false; const dot = path.lastIndexOf("."); if (dot < 0) return false; return videoExts.includes(path.slice(dot + 1).toLowerCase()); } function thumbnailFor(path: string): string { if (!path) return ""; for (const entry of root.list) { if (entry.path === path) return entry.thumbnailPath || path; } return path; } function getCrop(screen: string): var { return wallpapers.crop(root.current, screen); } function preview(path: string): void { previewPath = path; showPreview = true; if (Config.colors.schemeGen) previewColorsProc.running = true; } function setCrop(screen: string, rect: rect, zoom: real): void { if (zoom <= 0) zoom = 1.0; else if (zoom > 5.0) zoom = 5.0; wallpapers.setCrop(root.current, screen, { x: rect.x, y: rect.y, width: rect.width, height: rect.height, zoom: zoom }); } function setWallpaper(path: string): void { actualCurrent = path; WallpaperPath.currentWallpaperPath = path; Quickshell.execDetached(["zshell-cli", "wallpaper", "lockscreen", "--input-image", `${root.actualCurrent}`, "--output-path", `${Paths.state}/lockscreen_bg.png`, "--blur-amount", `${Config.lock.blurAmount}`]); if (Config.colors.schemeGen) Quickshell.execDetached(["zshell-cli", "scheme", "generate", "--image-path", `${root.actualCurrent}`, "--scheme", `${Config.colors.schemeType}`, "--mode", `${Config.colors.mode}`]); } function stopPreview(): void { showPreview = false; if (previewColorLock) pendingPreviewClear = true; else Colors.showPreview = false; } extraOpts: useFuzzy ? ({}) : ({ forward: false }) key: "relativePath" list: wallpapers.entries useFuzzy: true onPreviewColorLockChanged: { if (!previewColorLock && pendingPreviewClear) Colors.showPreview = false; } IpcHandler { function set(path: string): void { root.setWallpaper(path); } target: "wallpaper" } WallpaperDb { id: wallpapers dbPath: `${Paths.state}/wallpapers.sqlite` cacheSize: wallpaper.largestBufferSize requireHardwareCodec: true path: Config.general.wallpaperPath } Process { id: previewColorsProc command: ["zshell-cli", "scheme", "generate", "-p", "-i", root.previewPath] stdout: StdioCollector { onStreamFinished: { Colors.load(text, true); Colors.showPreview = true; } } } WallpaperService { id: wallpaper dbPath: wallpapers.dbPath path: root.current } Connections { target: wallpapers function onCacheStored(path: string): void { if (path === root.current) wallpaper.invalidateCache(); } function onCropsChanged(path: string): void { if (path === root.current) root.cropVersion++; } } Variants { id: screenVariants model: Quickshell.screens WallpaperObject { id: obj required property var modelData // Crop animation only for edits, never for wallpaper swaps. property bool animateCrop: false screen: modelData.name service: wallpaper // Apply the wallpaper's own crop. Driven off the service's pathChanged // (which fires after applyPath has snapshotted the outgoing frame with // the outgoing crop) and off crop edits, never as a reactive binding on // the wallpaper path — so a swap applies the incoming crop atomically // after the base is captured. function seedCrop(): void { // Load the current wallpaper's crop without pushing: the service // already read it from the database. This only seeds the values so // a later edit animates from the right starting point. obj.syncCrop(root.getCrop(modelData.name)); } // Apply an edited crop: assign via the setters so the Behaviors animate // the numbers and push each intermediate value to the service. function editCrop(): void { const c = root.getCrop(modelData.name); obj.cropX = c?.x ?? 0.0; obj.cropY = c?.y ?? 0.0; obj.cropWidth = c?.width ?? 1.0; obj.cropHeight = c?.height ?? 1.0; obj.zoom = c?.zoom ?? 1.0; } Component.onCompleted: seedCrop() onScreenChanged: seedCrop() Behavior on cropHeight { enabled: obj.animateCrop Anim {} } Behavior on cropWidth { enabled: obj.animateCrop Anim {} } Behavior on cropX { enabled: obj.animateCrop Anim {} } Behavior on cropY { enabled: obj.animateCrop Anim {} } } } Connections { target: wallpaper function onPathChanged(): void { for (const obj of screenVariants.instances) { obj.animateCrop = false; obj.seedCrop(); } } } Connections { target: root function onCropVersionChanged(): void { for (const obj of screenVariants.instances) { obj.animateCrop = true; obj.editCrop(); } } } }