WallpaperPanel: add wallhaven support

This commit is contained in:
Ly-sec
2025-11-15 18:03:26 +01:00
parent 2b5010f477
commit 9576daf70a
13 changed files with 1363 additions and 57 deletions
+25 -1
View File
@@ -580,6 +580,7 @@
"search-icons": "z.B. noctalia, niri, battery, cloud",
"search-launcher": "Einträge suchen... oder > für Befehle verwenden",
"search-wallpapers": "Zum Filtern von Hintergrundbildern eingeben...",
"search-wallhaven": "Wallhaven durchsuchen...",
"select": "Auswählen",
"test": "Test"
},
@@ -2010,6 +2011,7 @@
"previous-month": "Vorheriger Monat",
"refresh": "Aktualisieren",
"refresh-devices": "Geräte aktualisieren",
"refresh-wallhaven": "Wallhaven-Ergebnisse aktualisieren",
"refresh-wallpaper-list": "Hintergrundbild-Liste aktualisieren",
"remove-widget": "Widget entfernen",
"screen-recorder-not-installed": "Bildschirmrekorder ist nicht installiert",
@@ -2039,7 +2041,24 @@
"description": "Ausgewähltes Hintergrundbild auf alle Monitore gleichzeitig anwenden.",
"label": "Auf alle Monitore anwenden"
},
"categories": {
"anime": "Anime",
"general": "Allgemein",
"label": "Kategorien",
"people": "Personen"
},
"purity": {
"all": "Alle",
"label": "Inhaltsfilter",
"sfw": "SFW",
"sketchy": "Sketchy"
},
"search": "Suchen:",
"source": {
"label": "Quelle",
"local": "Lokal",
"wallhaven": "Wallhaven"
},
"title": "Hintergrundbild-Auswahl"
},
"transitions": {
@@ -2050,7 +2069,12 @@
"stripes": "Streifen",
"wipe": "Wischen"
},
"try-different-search": "Versuchen Sie eine andere Suchanfrage."
"try-different-search": "Versuchen Sie eine andere Suchanfrage.",
"wallhaven": {
"loading": "Hintergrundbilder werden geladen...",
"no-results": "Keine Hintergrundbilder gefunden. Versuchen Sie eine andere Suchanfrage.",
"page": "Seite {current} von {total}"
}
},
"weather": {
"clear-sky": "Klarer Himmel",
+29 -4
View File
@@ -580,6 +580,7 @@
"search-icons": "e.g., noctalia, niri, battery, cloud",
"search-launcher": "Search entries... or use > for commands",
"search-wallpapers": "Type to filter wallpapers...",
"search-wallhaven": "Search Wallhaven...",
"select": "Select",
"test": "Test"
},
@@ -2010,6 +2011,7 @@
"previous-month": "Previous month",
"refresh": "Refresh",
"refresh-devices": "Refresh devices",
"refresh-wallhaven": "Refresh Wallhaven results",
"refresh-wallpaper-list": "Refresh wallpaper list",
"remove-widget": "Remove widget",
"screen-recorder-not-installed": "Screen recorder is not installed",
@@ -2036,11 +2038,28 @@
"no-wallpaper": "No wallpaper found.",
"panel": {
"apply-all-monitors": {
"description": "Apply selected wallpaper to all monitors at once.",
"description": "Apply the selected wallpaper to all monitors.",
"label": "Apply to all monitors"
},
"search": "Search:",
"title": "Wallpaper selector"
"categories": {
"anime": "Anime",
"general": "General",
"label": "Categories",
"people": "People"
},
"purity": {
"all": "All",
"label": "Content filter",
"sfw": "SFW",
"sketchy": "Sketchy"
},
"search": "Search",
"source": {
"label": "Source",
"local": "Local",
"wallhaven": "Wallhaven"
},
"title": "Wallpaper Selector"
},
"transitions": {
"disc": "Disc",
@@ -2050,7 +2069,13 @@
"stripes": "Stripes",
"wipe": "Wipe"
},
"try-different-search": "Try a different search query."
"try-different-search": "Try a different search query.",
"unknown": "Unknown",
"wallhaven": {
"loading": "Loading wallpapers...",
"no-results": "No wallpapers found. Try a different search query.",
"page": "Page {current} of {total}"
}
},
"weather": {
"clear-sky": "Clear sky",
+25 -1
View File
@@ -580,6 +580,7 @@
"search-icons": "ej., noctalia, niri, battery, cloud",
"search-launcher": "Buscar entradas... o usa > para comandos",
"search-wallpapers": "Escribe para filtrar fondos de pantalla...",
"search-wallhaven": "Buscar en Wallhaven...",
"select": "Seleccionar",
"test": "Probar"
},
@@ -2010,6 +2011,7 @@
"previous-month": "Mes anterior",
"refresh": "Actualizar",
"refresh-devices": "Actualizar dispositivos",
"refresh-wallhaven": "Actualizar resultados de Wallhaven",
"refresh-wallpaper-list": "Actualizar lista de fondos de pantalla",
"remove-widget": "Eliminar widget",
"screen-recorder-not-installed": "La grabadora de pantalla no está instalada",
@@ -2039,7 +2041,24 @@
"description": "Aplica el fondo de pantalla seleccionado a todos los monitores a la vez.",
"label": "Aplicar a todos los monitores"
},
"categories": {
"anime": "Anime",
"general": "General",
"label": "Categorías",
"people": "Personas"
},
"purity": {
"all": "Todo",
"label": "Filtro de contenido",
"sfw": "SFW",
"sketchy": "Sketchy"
},
"search": "Buscar:",
"source": {
"label": "Fuente",
"local": "Local",
"wallhaven": "Wallhaven"
},
"title": "Selector de fondo de pantalla"
},
"transitions": {
@@ -2050,7 +2069,12 @@
"stripes": "Rayas",
"wipe": "Barrido"
},
"try-different-search": "Intenta con una búsqueda diferente."
"try-different-search": "Intenta con una búsqueda diferente.",
"wallhaven": {
"loading": "Cargando fondos de pantalla...",
"no-results": "No se encontraron fondos de pantalla. Intenta con una búsqueda diferente.",
"page": "Página {current} de {total}"
}
},
"weather": {
"clear-sky": "Cielo despejado",
+25 -1
View File
@@ -580,6 +580,7 @@
"search-icons": "ex: noctalia, niri, batterie, nuage",
"search-launcher": "Rechercher des entrées... ou utilisez > pour les commandes",
"search-wallpapers": "Tapez pour filtrer les fonds d'écran...",
"search-wallhaven": "Rechercher sur Wallhaven...",
"select": "Sélectionner",
"test": "Tester"
},
@@ -2010,6 +2011,7 @@
"previous-month": "Mois précédent",
"refresh": "Actualiser",
"refresh-devices": "Actualiser les appareils",
"refresh-wallhaven": "Actualiser les résultats de Wallhaven",
"refresh-wallpaper-list": "Actualiser la liste des fonds d'écran",
"remove-widget": "Supprimer le widget",
"screen-recorder-not-installed": "L'enregistreur d'écran n'est pas installé",
@@ -2039,7 +2041,24 @@
"description": "Appliquer le fond d'écran sélectionné à tous les moniteurs en même temps.",
"label": "Appliquer à tous les moniteurs"
},
"categories": {
"anime": "Anime",
"general": "Général",
"label": "Catégories",
"people": "Personnes"
},
"purity": {
"all": "Tout",
"label": "Filtre de contenu",
"sfw": "SFW",
"sketchy": "Sketchy"
},
"search": "Rechercher :",
"source": {
"label": "Source",
"local": "Local",
"wallhaven": "Wallhaven"
},
"title": "Sélecteur de fond d'écran"
},
"transitions": {
@@ -2050,7 +2069,12 @@
"stripes": "Rayures",
"wipe": "Balayage"
},
"try-different-search": "Essayez une autre requête de recherche."
"try-different-search": "Essayez une autre requête de recherche.",
"wallhaven": {
"loading": "Chargement des fonds d'écran...",
"no-results": "Aucun fond d'écran trouvé. Essayez une autre requête de recherche.",
"page": "Page {current} sur {total}"
}
},
"weather": {
"clear-sky": "Ciel dégagé",
+25 -1
View File
@@ -580,6 +580,7 @@
"search-icons": "bijv. noctalia, niri, battery, cloud",
"search-launcher": "Items zoeken... of gebruik > voor commando's",
"search-wallpapers": "Typ om achtergronden te filteren...",
"search-wallhaven": "Zoek in Wallhaven...",
"select": "Selecteren",
"test": "Test"
},
@@ -2010,6 +2011,7 @@
"previous-month": "Vorige maand",
"refresh": "Verversen",
"refresh-devices": "Apparaten verversen",
"refresh-wallhaven": "Wallhaven-resultaten verversen",
"refresh-wallpaper-list": "Achtergrondlijst verversen",
"remove-widget": "Widget verwijderen",
"screen-recorder-not-installed": "Schermrecorder is niet geïnstalleerd",
@@ -2039,7 +2041,24 @@
"description": "Pas de geselecteerde achtergrond in één keer op alle monitoren toe.",
"label": "Toepassen op alle monitoren"
},
"categories": {
"anime": "Anime",
"general": "Algemeen",
"label": "Categorieën",
"people": "Mensen"
},
"purity": {
"all": "Alles",
"label": "Inhoudsfilter",
"sfw": "SFW",
"sketchy": "Sketchy"
},
"search": "Zoeken:",
"source": {
"label": "Bron",
"local": "Lokaal",
"wallhaven": "Wallhaven"
},
"title": "Achtergrondkiezer"
},
"transitions": {
@@ -2050,7 +2069,12 @@
"stripes": "Strepen",
"wipe": "Vegen"
},
"try-different-search": "Probeer een andere zoekopdracht."
"try-different-search": "Probeer een andere zoekopdracht.",
"wallhaven": {
"loading": "Achtergronden laden...",
"no-results": "Geen achtergronden gevonden. Probeer een andere zoekopdracht.",
"page": "Pagina {current} van {total}"
}
},
"weather": {
"clear-sky": "Onbewolkt",
+25 -1
View File
@@ -580,6 +580,7 @@
"search-icons": "ex., noctalia, niri, battery, cloud",
"search-launcher": "Pesquisar entradas... ou use > para comandos",
"search-wallpapers": "Digite para filtrar papéis de parede...",
"search-wallhaven": "Pesquisar no Wallhaven...",
"select": "Selecionar",
"test": "Testar"
},
@@ -2010,6 +2011,7 @@
"previous-month": "Mês anterior",
"refresh": "Atualizar",
"refresh-devices": "Atualizar dispositivos",
"refresh-wallhaven": "Atualizar resultados do Wallhaven",
"refresh-wallpaper-list": "Atualizar lista de papéis de parede",
"remove-widget": "Remover widget",
"screen-recorder-not-installed": "O gravador de tela não está instalado",
@@ -2039,7 +2041,24 @@
"description": "Aplica o papel de parede selecionado a todos os monitores de uma só vez.",
"label": "Aplicar a todos os monitores"
},
"categories": {
"anime": "Anime",
"general": "Geral",
"label": "Categorias",
"people": "Pessoas"
},
"purity": {
"all": "Tudo",
"label": "Filtro de conteúdo",
"sfw": "SFW",
"sketchy": "Sketchy"
},
"search": "Pesquisar:",
"source": {
"label": "Fonte",
"local": "Local",
"wallhaven": "Wallhaven"
},
"title": "Seletor de papel de parede"
},
"transitions": {
@@ -2050,7 +2069,12 @@
"stripes": "Listras",
"wipe": "Limpar"
},
"try-different-search": "Tente uma busca diferente."
"try-different-search": "Tente uma busca diferente.",
"wallhaven": {
"loading": "Carregando papéis de parede...",
"no-results": "Nenhum papel de parede encontrado. Tente uma busca diferente.",
"page": "Página {current} de {total}"
}
},
"weather": {
"clear-sky": "Céu limpo",
+25 -1
View File
@@ -580,6 +580,7 @@
"search-icons": "например, noctalia, niri, battery, cloud",
"search-launcher": "Поиск записей... или используйте > для команд",
"search-wallpapers": "Введите для фильтрации обоев...",
"search-wallhaven": "Поиск в Wallhaven...",
"select": "Выбрать",
"test": "Тест"
},
@@ -2010,6 +2011,7 @@
"previous-month": "Предыдущий месяц",
"refresh": "Обновить",
"refresh-devices": "Обновить устройства",
"refresh-wallhaven": "Обновить результаты Wallhaven",
"refresh-wallpaper-list": "Обновить список обоев",
"remove-widget": "Удалить виджет",
"screen-recorder-not-installed": "Запись экрана не установлена",
@@ -2039,7 +2041,24 @@
"description": "Применить выбранные обои ко всем мониторам одновременно.",
"label": "Применить ко всем мониторам"
},
"categories": {
"anime": "Аниме",
"general": "Общее",
"label": "Категории",
"people": "Люди"
},
"purity": {
"all": "Все",
"label": "Фильтр контента",
"sfw": "SFW",
"sketchy": "Sketchy"
},
"search": "Поиск:",
"source": {
"label": "Источник",
"local": "Локальный",
"wallhaven": "Wallhaven"
},
"title": "Выбор обоев"
},
"transitions": {
@@ -2050,7 +2069,12 @@
"stripes": "Полосы",
"wipe": "Стирание"
},
"try-different-search": "Попробуйте другой поисковый запрос."
"try-different-search": "Попробуйте другой поисковый запрос.",
"wallhaven": {
"loading": "Загрузка обоев...",
"no-results": "Обои не найдены. Попробуйте другой поисковый запрос.",
"page": "Страница {current} из {total}"
}
},
"weather": {
"clear-sky": "Ясное небо",
+25 -1
View File
@@ -580,6 +580,7 @@
"search-icons": "örn., noctalia, niri, battery, cloud",
"search-launcher": "Girişleri arayın... veya > komutları için kullanın",
"search-wallpapers": "Duvar kağıtlarını filtrelemek için yazın...",
"search-wallhaven": "Wallhaven'da ara...",
"select": "Seç",
"test": "Test"
},
@@ -2010,6 +2011,7 @@
"previous-month": "Önceki ay",
"refresh": "Yenile",
"refresh-devices": "Cihazları yenile",
"refresh-wallhaven": "Wallhaven sonuçlarını yenile",
"refresh-wallpaper-list": "Duvar kağıdı listesini yenile",
"remove-widget": "Widget kaldır",
"screen-recorder-not-installed": "Ekran kaydedici yüklü değil",
@@ -2039,7 +2041,24 @@
"description": "Seçilen duvar kağıdını tüm monitörlere aynı anda uygulayın.",
"label": "Tüm monitörler için uygula"
},
"categories": {
"anime": "Anime",
"general": "Genel",
"label": "Kategoriler",
"people": "İnsanlar"
},
"purity": {
"all": "Tümü",
"label": "İçerik filtresi",
"sfw": "SFW",
"sketchy": "Sketchy"
},
"search": "Ara:",
"source": {
"label": "Kaynak",
"local": "Yerel",
"wallhaven": "Wallhaven"
},
"title": "Duvar kağıdı seçici"
},
"transitions": {
@@ -2050,7 +2069,12 @@
"stripes": "Çizgiler",
"wipe": "Silme"
},
"try-different-search": "Farklı bir arama sorgusu deneyin."
"try-different-search": "Farklı bir arama sorgusu deneyin.",
"wallhaven": {
"loading": "Duvar kağıtları yükleniyor...",
"no-results": "Duvar kağıdı bulunamadı. Farklı bir arama sorgusu deneyin.",
"page": "Sayfa {current} / {total}"
}
},
"weather": {
"clear-sky": "Açık hava",
+25 -1
View File
@@ -580,6 +580,7 @@
"search-icons": "напр., noctalia, niri, battery, cloud",
"search-launcher": "Пошук записів... або використовуйте > для команд",
"search-wallpapers": "Введіть для фільтрації шпалер...",
"search-wallhaven": "Пошук у Wallhaven...",
"select": "Вибрати",
"test": "Тест"
},
@@ -2010,6 +2011,7 @@
"previous-month": "Попередній місяць",
"refresh": "Оновити",
"refresh-devices": "Оновити пристрої",
"refresh-wallhaven": "Оновити результати Wallhaven",
"refresh-wallpaper-list": "Оновити список шпалер",
"remove-widget": "Видалити віджет",
"screen-recorder-not-installed": "Запис екрана не встановлено",
@@ -2039,7 +2041,24 @@
"description": "Застосувати вибрані шпалери до всіх моніторів одночасно.",
"label": "Застосувати до всіх моніторів"
},
"categories": {
"anime": "Аніме",
"general": "Загальне",
"label": "Категорії",
"people": "Люди"
},
"purity": {
"all": "Всі",
"label": "Фільтр контенту",
"sfw": "SFW",
"sketchy": "Sketchy"
},
"search": "Пошук:",
"source": {
"label": "Джерело",
"local": "Локальний",
"wallhaven": "Wallhaven"
},
"title": "Вибір шпалер"
},
"transitions": {
@@ -2050,7 +2069,12 @@
"stripes": "Смуги",
"wipe": "Змітання"
},
"try-different-search": "Спробуйте інший пошуковий запит."
"try-different-search": "Спробуйте інший пошуковий запит.",
"wallhaven": {
"loading": "Завантаження шпалер...",
"no-results": "Шпалери не знайдено. Спробуйте інший пошуковий запит.",
"page": "Сторінка {current} з {total}"
}
},
"weather": {
"clear-sky": "Ясне небо",
+25 -1
View File
@@ -580,6 +580,7 @@
"search-icons": "例如:noctalia, niri, battery, cloud",
"search-launcher": "搜索条目...或使用 > 执行命令",
"search-wallpapers": "输入以筛选壁纸...",
"search-wallhaven": "搜索 Wallhaven...",
"select": "选择",
"test": "测试"
},
@@ -2010,6 +2011,7 @@
"previous-month": "上个月",
"refresh": "刷新",
"refresh-devices": "刷新设备",
"refresh-wallhaven": "刷新 Wallhaven 结果",
"refresh-wallpaper-list": "刷新壁纸列表",
"remove-widget": "移除小部件",
"screen-recorder-not-installed": "屏幕录制器未安装",
@@ -2039,7 +2041,24 @@
"description": "一次性将选定的壁纸应用到所有显示器。",
"label": "应用到所有显示器"
},
"categories": {
"anime": "动漫",
"general": "通用",
"label": "分类",
"people": "人物"
},
"purity": {
"all": "全部",
"label": "内容过滤器",
"sfw": "SFW",
"sketchy": "Sketchy"
},
"search": "搜索:",
"source": {
"label": "来源",
"local": "本地",
"wallhaven": "Wallhaven"
},
"title": "壁纸选择器"
},
"transitions": {
@@ -2050,7 +2069,12 @@
"stripes": "条纹",
"wipe": "擦除"
},
"try-different-search": "尝试不同的搜索查询。"
"try-different-search": "尝试不同的搜索查询。",
"wallhaven": {
"loading": "正在加载壁纸...",
"no-results": "未找到壁纸。请尝试不同的搜索查询。",
"page": "第 {current} 页,共 {total} 页"
}
},
"weather": {
"clear-sky": "晴朗",
+7
View File
@@ -263,6 +263,13 @@ Singleton {
property list<var> monitors: []
property string panelPosition: "follow_bar"
property bool hideWallpaperFilenames: false
// Wallhaven settings
property bool useWallhaven: false
property string wallhavenQuery: ""
property string wallhavenSorting: "date_added"
property string wallhavenOrder: "desc"
property string wallhavenCategories: "111" // general,anime,people
property string wallhavenPurity: "100" // sfw only
}
// applauncher
+845 -44
View File
@@ -193,9 +193,17 @@ SmartPanel {
NIconButton {
icon: "refresh"
tooltipText: I18n.tr("tooltips.refresh-wallpaper-list")
tooltipText: Settings.data.wallpaper.useWallhaven ? I18n.tr("tooltips.refresh-wallhaven") : I18n.tr("tooltips.refresh-wallpaper-list")
baseSize: Style.baseWidgetSize * 0.8
onClicked: WallpaperService.refreshWallpapersList()
onClicked: {
if (Settings.data.wallpaper.useWallhaven) {
if (typeof WallhavenService !== "undefined") {
WallhavenService.search(Settings.data.wallpaper.wallhavenQuery, 1)
}
} else {
WallpaperService.refreshWallpapersList()
}
}
}
NIconButton {
@@ -210,6 +218,425 @@ SmartPanel {
Layout.fillWidth: true
}
// Unified search input
RowLayout {
Layout.fillWidth: true
spacing: Style.marginM
NText {
text: I18n.tr("wallpaper.panel.search")
color: Color.mOnSurface
pointSize: Style.fontSizeM
Layout.preferredWidth: implicitWidth
}
NTextInput {
id: searchInput
placeholderText: Settings.data.wallpaper.useWallhaven ? I18n.tr("placeholders.search-wallhaven") : I18n.tr("placeholders.search-wallpapers")
Layout.fillWidth: true
property bool initializing: true
Component.onCompleted: {
// Initialize text based on current mode
if (Settings.data.wallpaper.useWallhaven) {
searchInput.text = Settings.data.wallpaper.wallhavenQuery || ""
} else {
searchInput.text = wallpaperPanel.filterText || ""
}
// Give focus to search input
if (searchInput.inputItem && searchInput.inputItem.visible) {
searchInput.inputItem.forceActiveFocus()
}
// Mark initialization as complete after a short delay
Qt.callLater(function() {
searchInput.initializing = false
})
}
Connections {
target: Settings.data.wallpaper
function onUseWallhavenChanged() {
// Update text when mode changes
if (Settings.data.wallpaper.useWallhaven) {
searchInput.text = Settings.data.wallpaper.wallhavenQuery || ""
} else {
searchInput.text = wallpaperPanel.filterText || ""
}
}
}
onTextChanged: {
// Don't trigger search during initialization - Component.onCompleted will handle initial search
if (initializing) {
return
}
if (Settings.data.wallpaper.useWallhaven) {
wallhavenSearchDebounceTimer.restart()
} else {
searchDebounceTimer.restart()
}
}
onEditingFinished: {
if (Settings.data.wallpaper.useWallhaven) {
wallhavenSearchDebounceTimer.stop()
Settings.data.wallpaper.wallhavenQuery = text
if (typeof WallhavenService !== "undefined") {
wallhavenView.loading = true
WallhavenService.search(text, 1)
}
}
}
Keys.onDownPressed: {
if (Settings.data.wallpaper.useWallhaven) {
if (wallhavenView && wallhavenView.gridView) {
wallhavenView.gridView.forceActiveFocus()
}
} else {
let currentView = screenRepeater.itemAt(currentScreenIndex)
if (currentView && currentView.gridView) {
currentView.gridView.forceActiveFocus()
}
}
}
}
}
// Debounce timer for Wallhaven search
Timer {
id: wallhavenSearchDebounceTimer
interval: 500
onTriggered: {
Settings.data.wallpaper.wallhavenQuery = searchInput.text
if (typeof WallhavenService !== "undefined") {
wallhavenView.loading = true
WallhavenService.search(searchInput.text, 1)
}
}
}
// Source selector
RowLayout {
Layout.fillWidth: true
spacing: Style.marginM
NText {
text: I18n.tr("wallpaper.panel.source.label")
color: Color.mOnSurface
pointSize: Style.fontSizeM
Layout.preferredWidth: implicitWidth
}
NComboBox {
id: sourceComboBox
Layout.fillWidth: true
model: [
{ "key": "local", "name": I18n.tr("wallpaper.panel.source.local") },
{ "key": "wallhaven", "name": I18n.tr("wallpaper.panel.source.wallhaven") }
]
currentKey: Settings.data.wallpaper.useWallhaven ? "wallhaven" : "local"
property bool skipNextSelected: false
Component.onCompleted: {
// Skip the first onSelected if it fires during initialization
skipNextSelected = true
Qt.callLater(function() {
skipNextSelected = false
})
}
onSelected: key => {
if (skipNextSelected) {
return
}
var useWallhaven = (key === "wallhaven")
Settings.data.wallpaper.useWallhaven = useWallhaven
// Update search input text based on mode
if (useWallhaven) {
searchInput.text = Settings.data.wallpaper.wallhavenQuery || ""
} else {
searchInput.text = wallpaperPanel.filterText || ""
}
if (useWallhaven && typeof WallhavenService !== "undefined") {
// Update service properties when switching to Wallhaven
// Don't search here - Component.onCompleted will handle it when the component is created
// This prevents duplicate searches
WallhavenService.categories = Settings.data.wallpaper.wallhavenCategories
WallhavenService.purity = Settings.data.wallpaper.wallhavenPurity
WallhavenService.sorting = Settings.data.wallpaper.wallhavenSorting
WallhavenService.order = Settings.data.wallpaper.wallhavenOrder
// If the view is already initialized, trigger a new search when switching to it
if (wallhavenView && wallhavenView.initialized && !WallhavenService.fetching) {
wallhavenView.loading = true
WallhavenService.search(Settings.data.wallpaper.wallhavenQuery || "", 1)
}
}
}
}
}
// Wallhaven category toggles
RowLayout {
Layout.fillWidth: true
spacing: Style.marginM
visible: Settings.data.wallpaper.useWallhaven
NText {
text: I18n.tr("wallpaper.panel.categories.label")
color: Color.mOnSurface
pointSize: Style.fontSizeM
Layout.preferredWidth: implicitWidth
}
Item {
Layout.fillWidth: true
}
RowLayout {
id: categoriesRow
spacing: Style.marginL
function getCategoryValue(index) {
var cats = Settings.data.wallpaper.wallhavenCategories || "111"
return cats.length > index && cats.charAt(index) === "1"
}
function updateCategories(general, anime, people) {
var categories = (general ? "1" : "0") + (anime ? "1" : "0") + (people ? "1" : "0")
Settings.data.wallpaper.wallhavenCategories = categories
// Update checkboxes immediately
generalToggle.checked = general
animeToggle.checked = anime
peopleToggle.checked = people
if (typeof WallhavenService !== "undefined") {
WallhavenService.categories = categories
WallhavenService.search(Settings.data.wallpaper.wallhavenQuery, 1)
}
}
Connections {
target: Settings.data.wallpaper
function onWallhavenCategoriesChanged() {
generalToggle.checked = categoriesRow.getCategoryValue(0)
animeToggle.checked = categoriesRow.getCategoryValue(1)
peopleToggle.checked = categoriesRow.getCategoryValue(2)
}
}
Component.onCompleted: {
generalToggle.checked = categoriesRow.getCategoryValue(0)
animeToggle.checked = categoriesRow.getCategoryValue(1)
peopleToggle.checked = categoriesRow.getCategoryValue(2)
}
// General checkbox
Item {
Layout.preferredWidth: generalCheckboxRow.implicitWidth
Layout.preferredHeight: generalCheckboxRow.implicitHeight
RowLayout {
id: generalCheckboxRow
anchors.fill: parent
spacing: Style.marginS
NText {
text: I18n.tr("wallpaper.panel.categories.general")
color: Color.mOnSurface
pointSize: Style.fontSizeM
}
Rectangle {
id: generalBox
implicitWidth: Math.round(Style.baseWidgetSize * 0.7)
implicitHeight: Math.round(Style.baseWidgetSize * 0.7)
radius: Style.radiusXS
color: generalToggle.checked ? Color.mPrimary : Color.mSurface
border.color: Color.mOutline
border.width: Style.borderS
Behavior on color {
ColorAnimation {
duration: Style.animationFast
}
}
NIcon {
visible: generalToggle.checked
anchors.centerIn: parent
anchors.horizontalCenterOffset: -1
icon: "check"
color: Color.mOnPrimary
pointSize: Math.max(Style.fontSizeXS, generalBox.width * 0.5)
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: generalToggle.toggled(!generalToggle.checked)
}
}
}
}
// Anime checkbox
Item {
Layout.preferredWidth: animeCheckboxRow.implicitWidth
Layout.preferredHeight: animeCheckboxRow.implicitHeight
RowLayout {
id: animeCheckboxRow
anchors.fill: parent
spacing: Style.marginS
NText {
text: I18n.tr("wallpaper.panel.categories.anime")
color: Color.mOnSurface
pointSize: Style.fontSizeM
}
Rectangle {
id: animeBox
implicitWidth: Math.round(Style.baseWidgetSize * 0.7)
implicitHeight: Math.round(Style.baseWidgetSize * 0.7)
radius: Style.radiusXS
color: animeToggle.checked ? Color.mPrimary : Color.mSurface
border.color: Color.mOutline
border.width: Style.borderS
Behavior on color {
ColorAnimation {
duration: Style.animationFast
}
}
NIcon {
visible: animeToggle.checked
anchors.centerIn: parent
anchors.horizontalCenterOffset: -1
icon: "check"
color: Color.mOnPrimary
pointSize: Math.max(Style.fontSizeXS, animeBox.width * 0.5)
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: animeToggle.toggled(!animeToggle.checked)
}
}
}
}
// People checkbox
Item {
Layout.preferredWidth: peopleCheckboxRow.implicitWidth
Layout.preferredHeight: peopleCheckboxRow.implicitHeight
RowLayout {
id: peopleCheckboxRow
anchors.fill: parent
spacing: Style.marginS
NText {
text: I18n.tr("wallpaper.panel.categories.people")
color: Color.mOnSurface
pointSize: Style.fontSizeM
}
Rectangle {
id: peopleBox
implicitWidth: Math.round(Style.baseWidgetSize * 0.7)
implicitHeight: Math.round(Style.baseWidgetSize * 0.7)
radius: Style.radiusXS
color: peopleToggle.checked ? Color.mPrimary : Color.mSurface
border.color: Color.mOutline
border.width: Style.borderS
Behavior on color {
ColorAnimation {
duration: Style.animationFast
}
}
NIcon {
visible: peopleToggle.checked
anchors.centerIn: parent
anchors.horizontalCenterOffset: -1
icon: "check"
color: Color.mOnPrimary
pointSize: Math.max(Style.fontSizeXS, peopleBox.width * 0.5)
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: peopleToggle.toggled(!peopleToggle.checked)
}
}
}
}
// Invisible checkboxes to maintain the signal handlers
QtObject {
id: generalToggle
property bool checked: false
signal toggled(bool checked)
onToggled: checked => {
categoriesRow.updateCategories(checked, categoriesRow.getCategoryValue(1), categoriesRow.getCategoryValue(2))
}
}
QtObject {
id: animeToggle
property bool checked: false
signal toggled(bool checked)
onToggled: checked => {
categoriesRow.updateCategories(categoriesRow.getCategoryValue(0), checked, categoriesRow.getCategoryValue(2))
}
}
QtObject {
id: peopleToggle
property bool checked: false
signal toggled(bool checked)
onToggled: checked => {
categoriesRow.updateCategories(categoriesRow.getCategoryValue(0), categoriesRow.getCategoryValue(1), checked)
}
}
}
}
// Wallhaven purity selector
RowLayout {
Layout.fillWidth: true
spacing: Style.marginM
visible: Settings.data.wallpaper.useWallhaven
NText {
text: I18n.tr("wallpaper.panel.purity.label")
color: Color.mOnSurface
pointSize: Style.fontSizeM
Layout.preferredWidth: implicitWidth
}
NComboBox {
id: purityComboBox
Layout.fillWidth: true
model: [
{ "key": "111", "name": I18n.tr("wallpaper.panel.purity.all") },
{ "key": "100", "name": I18n.tr("wallpaper.panel.purity.sfw") },
{ "key": "010", "name": I18n.tr("wallpaper.panel.purity.sketchy") }
]
currentKey: Settings.data.wallpaper.wallhavenPurity
onSelected: key => {
Settings.data.wallpaper.wallhavenPurity = key
if (typeof WallhavenService !== "undefined") {
WallhavenService.purity = key
WallhavenService.search(Settings.data.wallpaper.wallhavenQuery, 1)
}
}
}
}
NToggle {
label: I18n.tr("wallpaper.panel.apply-all-monitors.label")
description: I18n.tr("wallpaper.panel.apply-all-monitors.description")
@@ -221,7 +648,7 @@ SmartPanel {
// Monitor tabs
NTabBar {
id: screenTabBar
visible: !Settings.data.wallpaper.setWallpaperOnAllMonitors || Settings.data.wallpaper.enableMultiMonitorDirectories
visible: (!Settings.data.wallpaper.setWallpaperOnAllMonitors || Settings.data.wallpaper.enableMultiMonitorDirectories) && !Settings.data.wallpaper.useWallhaven
Layout.fillWidth: true
currentIndex: currentScreenIndex
onCurrentIndexChanged: currentScreenIndex = currentIndex
@@ -238,55 +665,30 @@ SmartPanel {
}
}
}
// StackLayout for each screen's wallpaper content
// Content stack: Wallhaven or Local
StackLayout {
id: screenStack
id: contentStack
Layout.fillWidth: true
Layout.fillHeight: true
currentIndex: currentScreenIndex
currentIndex: Settings.data.wallpaper.useWallhaven ? 1 : 0
Repeater {
id: screenRepeater
model: Quickshell.screens
delegate: WallpaperScreenView {
targetScreen: modelData
}
}
}
// Local wallpapers
StackLayout {
id: screenStack
currentIndex: currentScreenIndex
// Filter input
RowLayout {
Layout.fillWidth: true
spacing: Style.marginM
NText {
text: I18n.tr("wallpaper.panel.search")
color: Color.mOnSurface
pointSize: Style.fontSizeM
Layout.preferredWidth: implicitWidth
}
NTextInput {
id: searchInput
placeholderText: I18n.tr("placeholders.search-wallpapers")
Layout.fillWidth: true
onTextChanged: {
searchDebounceTimer.restart()
}
Keys.onDownPressed: {
let currentView = screenRepeater.itemAt(currentScreenIndex)
if (currentView && currentView.gridView) {
currentView.gridView.forceActiveFocus()
Repeater {
id: screenRepeater
model: Quickshell.screens
delegate: WallpaperScreenView {
targetScreen: modelData
}
}
}
Component.onCompleted: {
if (searchInput.inputItem && searchInput.inputItem.visible) {
searchInput.inputItem.forceActiveFocus()
}
}
// Wallhaven wallpapers
WallhavenView {
id: wallhavenView
}
}
}
@@ -622,4 +1024,403 @@ SmartPanel {
}
}
}
// Component for Wallhaven wallpapers view
component WallhavenView: Item {
id: wallhavenViewRoot
property alias gridView: wallhavenGridView
property var wallpapers: []
property bool loading: false
property string errorMessage: ""
property bool initialized: false
property bool searchScheduled: false
Connections {
target: typeof WallhavenService !== "undefined" ? WallhavenService : null
function onSearchCompleted(results, meta) {
wallhavenViewRoot.wallpapers = results || []
wallhavenViewRoot.loading = false
wallhavenViewRoot.errorMessage = ""
wallhavenViewRoot.searchScheduled = false
}
function onSearchFailed(error) {
wallhavenViewRoot.loading = false
wallhavenViewRoot.errorMessage = error || ""
wallhavenViewRoot.searchScheduled = false
}
}
Component.onCompleted: {
// Initialize service properties and perform initial search if Wallhaven is active
if (typeof WallhavenService !== "undefined" && Settings.data.wallpaper.useWallhaven && !initialized) {
// Set flags immediately to prevent race conditions
if (WallhavenService.initialSearchScheduled) {
// Another instance already scheduled the search, just initialize properties
initialized = true
return
}
// We're the first one - claim the search
initialized = true
WallhavenService.initialSearchScheduled = true
WallhavenService.categories = Settings.data.wallpaper.wallhavenCategories
WallhavenService.purity = Settings.data.wallpaper.wallhavenPurity
WallhavenService.sorting = Settings.data.wallpaper.wallhavenSorting
WallhavenService.order = Settings.data.wallpaper.wallhavenOrder
// Now check if we can actually search (fetching check is in WallhavenService.search)
loading = true
WallhavenService.search(Settings.data.wallpaper.wallhavenQuery || "", 1)
}
}
ColumnLayout {
anchors.fill: parent
spacing: Style.marginM
Item {
Layout.fillWidth: true
Layout.fillHeight: true
GridView {
id: wallhavenGridView
anchors.fill: parent
visible: !loading && errorMessage === "" && (wallpapers && wallpapers.length > 0)
interactive: true
clip: true
focus: true
keyNavigationEnabled: true
keyNavigationWraps: false
model: wallpapers || []
property int columns: (screen.width > 1920) ? 5 : 4
property int itemSize: cellWidth
cellWidth: Math.floor((width - leftMargin - rightMargin) / columns)
cellHeight: Math.floor(itemSize * 0.7) + Style.marginXS + Style.fontSizeXS + Style.marginM
leftMargin: Style.marginS
rightMargin: Style.marginS
topMargin: Style.marginS
bottomMargin: Style.marginS
onCurrentIndexChanged: {
if (currentIndex >= 0) {
let row = Math.floor(currentIndex / columns)
let itemY = row * cellHeight
let viewportTop = contentY
let viewportBottom = viewportTop + height
if (itemY < viewportTop) {
contentY = Math.max(0, itemY - cellHeight)
} else if (itemY + cellHeight > viewportBottom) {
contentY = itemY + cellHeight - height + cellHeight
}
}
}
Keys.onPressed: event => {
if (event.key === Qt.Key_Return || event.key === Qt.Key_Space) {
if (currentIndex >= 0 && currentIndex < wallpapers.length) {
let wallpaper = wallpapers[currentIndex]
if (typeof WallhavenService !== "undefined") {
WallhavenService.downloadWallpaper(wallpaper, function (success, localPath) {
if (success) {
if (Settings.data.wallpaper.setWallpaperOnAllMonitors) {
WallpaperService.changeWallpaper(localPath, undefined)
} else {
WallpaperService.changeWallpaper(localPath, screen.name)
}
}
})
}
}
event.accepted = true
}
}
ScrollBar.vertical: ScrollBar {
policy: ScrollBar.AsNeeded
parent: wallhavenGridView
x: wallhavenGridView.mirrored ? 0 : wallhavenGridView.width - width
y: 0
height: wallhavenGridView.height
property color handleColor: Qt.alpha(Color.mHover, 0.8)
property color handleHoverColor: handleColor
property color handlePressedColor: handleColor
property real handleWidth: 6
property real handleRadius: Style.radiusM
contentItem: Rectangle {
implicitWidth: parent.handleWidth
implicitHeight: 100
radius: parent.handleRadius
color: parent.pressed ? parent.handlePressedColor : parent.hovered ? parent.handleHoverColor : parent.handleColor
opacity: parent.policy === ScrollBar.AlwaysOn || parent.active ? 1.0 : 0.0
Behavior on opacity {
NumberAnimation {
duration: Style.animationFast
}
}
Behavior on color {
ColorAnimation {
duration: Style.animationFast
}
}
}
background: Rectangle {
implicitWidth: parent.handleWidth
implicitHeight: 100
color: Color.transparent
opacity: parent.policy === ScrollBar.AlwaysOn || parent.active ? 0.3 : 0.0
radius: parent.handleRadius / 2
Behavior on opacity {
NumberAnimation {
duration: Style.animationFast
}
}
}
}
delegate: ColumnLayout {
id: wallhavenItem
required property var modelData
required property int index
property string thumbnailUrl: (modelData && typeof WallhavenService !== "undefined") ? WallhavenService.getThumbnailUrl(modelData, "large") : ""
property string wallpaperId: (modelData && modelData.id) ? modelData.id : ""
width: wallhavenGridView.itemSize
spacing: Style.marginXS
Rectangle {
id: imageContainer
Layout.fillWidth: true
Layout.preferredHeight: Math.round(wallhavenGridView.itemSize * 0.67)
color: Color.transparent
Image {
id: img
source: thumbnailUrl
anchors.fill: parent
fillMode: Image.PreserveAspectCrop
asynchronous: true
cache: true
smooth: true
sourceSize.width: Math.round(wallhavenGridView.itemSize * 0.67)
sourceSize.height: Math.round(wallhavenGridView.itemSize * 0.67)
}
Rectangle {
anchors.fill: parent
color: Color.transparent
border.color: wallhavenGridView.currentIndex === index ? Color.mHover : Color.mSurface
border.width: Math.max(1, Style.borderL * 1.5)
}
Rectangle {
anchors.fill: parent
color: Color.mSurface
opacity: hoverHandler.hovered || wallhavenGridView.currentIndex === index ? 0 : 0.3
Behavior on opacity {
NumberAnimation {
duration: Style.animationFast
}
}
}
HoverHandler {
id: hoverHandler
}
TapHandler {
onTapped: {
wallhavenGridView.currentIndex = index
if (typeof WallhavenService !== "undefined") {
WallhavenService.downloadWallpaper(modelData, function (success, localPath) {
if (success) {
if (Settings.data.wallpaper.setWallpaperOnAllMonitors) {
WallpaperService.changeWallpaper(localPath, undefined)
} else {
WallpaperService.changeWallpaper(localPath, screen.name)
}
}
})
}
}
}
}
NText {
text: wallpaperId || I18n.tr("wallpaper.unknown")
color: hoverHandler.hovered || wallhavenGridView.currentIndex === index ? Color.mOnSurface : Color.mOnSurfaceVariant
pointSize: Style.fontSizeXS
Layout.fillWidth: true
Layout.leftMargin: Style.marginS
Layout.rightMargin: Style.marginS
Layout.alignment: Qt.AlignHCenter
horizontalAlignment: Text.AlignHCenter
elide: Text.ElideRight
}
}
}
// Loading overlay - fills same space as GridView to prevent jumping
Rectangle {
anchors.fill: parent
color: Color.mSurface
radius: Style.radiusM
border.color: Color.mOutline
border.width: Style.borderS
visible: loading
z: 10
ColumnLayout {
anchors.fill: parent
anchors.margins: Style.marginL
spacing: Style.marginM
Item {
Layout.fillHeight: true
}
NBusyIndicator {
size: Style.baseWidgetSize * 1.5
color: Color.mPrimary
Layout.alignment: Qt.AlignHCenter
}
NText {
text: I18n.tr("wallpaper.wallhaven.loading")
color: Color.mOnSurfaceVariant
pointSize: Style.fontSizeM
Layout.alignment: Qt.AlignHCenter
}
Item {
Layout.fillHeight: true
}
}
}
// Error overlay
Rectangle {
anchors.fill: parent
color: Color.mSurface
radius: Style.radiusM
border.color: Color.mOutline
border.width: Style.borderS
visible: errorMessage !== "" && !loading
z: 10
ColumnLayout {
anchors.fill: parent
anchors.margins: Style.marginL
spacing: Style.marginM
Item {
Layout.fillHeight: true
}
NIcon {
icon: "alert-circle"
pointSize: Style.fontSizeXXL
color: Color.mError
Layout.alignment: Qt.AlignHCenter
}
NText {
text: errorMessage
color: Color.mOnSurface
wrapMode: Text.WordWrap
Layout.alignment: Qt.AlignHCenter
Layout.fillWidth: true
horizontalAlignment: Text.AlignHCenter
}
Item {
Layout.fillHeight: true
}
}
}
// Empty state overlay
Rectangle {
anchors.fill: parent
color: Color.mSurface
radius: Style.radiusM
border.color: Color.mOutline
border.width: Style.borderS
visible: (!wallpapers || wallpapers.length === 0) && !loading && errorMessage === ""
z: 10
ColumnLayout {
anchors.fill: parent
anchors.margins: Style.marginL
spacing: Style.marginM
Item {
Layout.fillHeight: true
}
NIcon {
icon: "image"
pointSize: Style.fontSizeXXL
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
}
NText {
text: I18n.tr("wallpaper.wallhaven.no-results")
color: Color.mOnSurface
wrapMode: Text.WordWrap
Layout.alignment: Qt.AlignHCenter
Layout.fillWidth: true
horizontalAlignment: Text.AlignHCenter
}
Item {
Layout.fillHeight: true
}
}
}
}
// Pagination
RowLayout {
Layout.fillWidth: true
visible: !loading && errorMessage === "" && typeof WallhavenService !== "undefined"
spacing: Style.marginM
NIconButton {
icon: "chevron-left"
enabled: WallhavenService.currentPage > 1 && !WallhavenService.fetching
onClicked: WallhavenService.previousPage()
}
NText {
text: I18n.tr("wallpaper.wallhaven.page").replace("{current}", WallhavenService.currentPage).replace("{total}", WallhavenService.lastPage)
color: Color.mOnSurface
Layout.fillWidth: true
horizontalAlignment: Text.AlignHCenter
}
NIconButton {
icon: "chevron-right"
enabled: WallhavenService.currentPage < WallhavenService.lastPage && !WallhavenService.fetching
onClicked: WallhavenService.nextPage()
}
}
}
}
}
+257
View File
@@ -0,0 +1,257 @@
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.e("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)
}
}
}