Files
noctalia-shell/Services/UI/WallhavenService.qml
T
2025-11-16 16:31:49 +01:00

260 lines
7.7 KiB
QML

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: "relevance" // 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.d("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)
}
}
}