pragma Singleton import QtQuick import Quickshell import qs.Commons Singleton { id: root // State property bool fetching: false property bool initialSearchScheduled: false property var currentResults: [] property var currentMeta: ({}) property string lastError: "" property string currentQuery: "" property int currentPage: 1 property int lastPage: 1 // Search parameters property string categories: "111" // general,anime,people (all enabled by default) property string purity: "100" // sfw property string sorting: "date_added" // date_added, relevance, random, views, favorites, toplist property string order: "desc" // desc, asc property string topRange: "1M" // 1d, 3d, 1w, 1M, 3M, 6M, 1y property string seed: "" // For random sorting property string minResolution: "" // e.g., "1920x1080" property string resolutions: "" // e.g., "1920x1080,1920x1200" property string ratios: "" // e.g., "16x9,16x10" property string colors: "" // Color hex codes // Signals signal searchCompleted(var results, var meta) signal searchFailed(string error) signal wallpaperDownloaded(string wallpaperId, string localPath) // Base API URL readonly property string apiBaseUrl: "https://wallhaven.cc/api/v1" // ------------------------------------------------- function search(query, page) { if (fetching) { return } // Reset initial search flag once we start a search if (initialSearchScheduled) { initialSearchScheduled = false } fetching = true lastError = "" currentQuery = query || "" currentPage = page || 1 var url = apiBaseUrl + "/search" var params = [] if (currentQuery) { params.push("q=" + encodeURIComponent(currentQuery)) } params.push("categories=" + categories) params.push("purity=" + purity) params.push("sorting=" + sorting) params.push("order=" + order) if (sorting === "toplist") { params.push("topRange=" + topRange) } if (sorting === "random" && seed) { params.push("seed=" + seed) } if (minResolution) { params.push("atleast=" + minResolution) } if (resolutions) { params.push("resolutions=" + resolutions) } if (ratios) { params.push("ratios=" + ratios) } if (colors) { params.push("colors=" + colors) } params.push("page=" + currentPage) url += "?" + params.join("&") Logger.d("Wallhaven", "Searching:", url) var xhr = new XMLHttpRequest() xhr.onreadystatechange = function () { if (xhr.readyState === XMLHttpRequest.DONE) { fetching = false if (xhr.status === 200) { try { var response = JSON.parse(xhr.responseText) if (response.data && Array.isArray(response.data)) { currentResults = response.data currentMeta = response.meta || {} lastPage = currentMeta.last_page || 1 // Store seed for random sorting if (currentMeta.seed) { seed = currentMeta.seed } Logger.i("Wallhaven", "Search completed:", currentResults.length, "results, page", currentPage, "of", lastPage) searchCompleted(currentResults, currentMeta) } else { var errorMsg = "Invalid API response" lastError = errorMsg Logger.e("Wallhaven", errorMsg) searchFailed(errorMsg) } } catch (e) { var errorMsg = "Failed to parse API response: " + e.toString() lastError = errorMsg Logger.e("Wallhaven", errorMsg) searchFailed(errorMsg) } } else if (xhr.status === 429) { var errorMsg = "Rate limit exceeded (45 requests/minute)" lastError = errorMsg Logger.w("Wallhaven", errorMsg) searchFailed(errorMsg) } else { var errorMsg = "API error: " + xhr.status lastError = errorMsg Logger.e("Wallhaven", "Search failed:", errorMsg) searchFailed(errorMsg) } } } xhr.open("GET", url) xhr.send() } // ------------------------------------------------- function getWallpaperUrl(wallpaper) { // Use the 'path' field which contains the full resolution image URL if (wallpaper.path) { return wallpaper.path } // Fallback to constructing URL from ID if (wallpaper.id) { var idPrefix = wallpaper.id.substring(0, 2) return "https://w.wallhaven.cc/full/" + idPrefix + "/wallhaven-" + wallpaper.id + ".jpg" } return "" } // ------------------------------------------------- function getThumbnailUrl(wallpaper, size) { // size: "small", "large", "original" if (wallpaper.thumbs && wallpaper.thumbs[size]) { return wallpaper.thumbs[size] } // Fallback if (wallpaper.id) { var idPrefix = wallpaper.id.substring(0, 2) var sizeMap = { "small": "small", "large": "lg", "original": "orig" } var sizePath = sizeMap[size] || "lg" return "https://th.wallhaven.cc/" + sizePath + "/" + idPrefix + "/" + wallpaper.id + ".jpg" } return "" } // ------------------------------------------------- function downloadWallpaper(wallpaper, callback) { var url = getWallpaperUrl(wallpaper) if (!url) { Logger.e("Wallhaven", "No URL available for wallpaper", wallpaper.id) if (callback) callback(false, "") return } var wallpaperId = wallpaper.id // Get the user's wallpaper directory var wallpaperDir = Settings.preprocessPath(Settings.data.wallpaper.directory) if (!wallpaperDir || wallpaperDir === "") { wallpaperDir = Settings.defaultWallpapersDirectory } // Ensure directory ends with / if (!wallpaperDir.endsWith("/")) { wallpaperDir += "/" } var localPath = wallpaperDir + "wallhaven_" + wallpaperId + ".jpg" Logger.d("Wallhaven", "Downloading wallpaper", wallpaperId, "to", localPath) // Use curl or wget to download the file, ensuring directory exists first var downloadProcess = Qt.createQmlObject(` import QtQuick import Quickshell.Io Process { id: downloadProcess command: ["sh", "-c", "mkdir -p '` + wallpaperDir + `' && (curl -L -s -o '` + localPath + `' '` + url + `' || wget -q -O '` + localPath + `' '` + url + `')"] } `, root, "DownloadProcess_" + wallpaperId) downloadProcess.exited.connect(function (exitCode) { if (exitCode === 0) { Logger.i("Wallhaven", "Wallpaper downloaded:", localPath) wallpaperDownloaded(wallpaperId, localPath) if (callback) callback(true, localPath) } else { Logger.e("Wallhaven", "Failed to download wallpaper, exit code:", exitCode) if (callback) callback(false, "") } downloadProcess.destroy() }) downloadProcess.running = true } // ------------------------------------------------- function reset() { currentResults = [] currentMeta = {} currentQuery = "" currentPage = 1 lastPage = 1 seed = "" lastError = "" } // ------------------------------------------------- function nextPage() { if (currentPage < lastPage && !fetching) { search(currentQuery, currentPage + 1) } } // ------------------------------------------------- function previousPage() { if (currentPage > 1 && !fetching) { search(currentQuery, currentPage - 1) } } }