diff --git a/_locales/vi/translations.json b/_locales/vi/translations.json index 86db54e1..d33e6e65 100644 --- a/_locales/vi/translations.json +++ b/_locales/vi/translations.json @@ -18,6 +18,7 @@ "Default group": "Default group", "Folder": "Thư mục", "Photo by ": "Ảnh tạo bởi ", + "Photo from Wallhaven": "Ảnh từ Wallhaven", "Editing": "Đang sửa", "No selection": "Không chọn", "Grid position": "Vị trí", @@ -81,6 +82,9 @@ "Unsplash collection": "Bộ sưu tập Unsplash", "Tips on Unsplash collections": "Mẹo về bộ sưu tập Unsplash", "Type the collection ID from its URL, add more by separating them with a comma.": "Nhập ID bộ sưu tập từ URL của nó, thêm nhiều ID bằng cách phân tách chúng bằng dấu phẩy.", + "Wallhaven parameters": "Tham số Wallhaven", + "Tips on Wallhaven parameters": "Mẹo về tham số Wallhaven", + "Type the full search URL, or keywords.": "Nhập URL tìm kiếm, hoặc từ khóa.", "Blur intensity": "Làm mờ", "Brightness": "Độ sáng", "Quick Links": "Liên kết nhanh", diff --git a/src/scripts/defaults.ts b/src/scripts/defaults.ts index 3c82727f..96dbd35c 100644 --- a/src/scripts/defaults.ts +++ b/src/scripts/defaults.ts @@ -135,6 +135,13 @@ export const SYNC_DEFAULT: Sync.Storage = { pausedImage: undefined, time: undefined, }, + wallhaven: { + every: 'hour', + parameters: '', + lastParams: 'day', + pausedImage: undefined, + time: undefined, + }, weather: { ccode: undefined, city: undefined, @@ -193,4 +200,11 @@ export const LOCAL_DEFAULT: Local.Storage = { night: [], user: [], }, + wallhavenCache: { + noon: [], + day: [], + evening: [], + night: [], + user: [], + }, } diff --git a/src/scripts/features/backgrounds/index.ts b/src/scripts/features/backgrounds/index.ts index 40d23575..46b9efde 100644 --- a/src/scripts/features/backgrounds/index.ts +++ b/src/scripts/features/backgrounds/index.ts @@ -1,4 +1,5 @@ import unsplashBackgrounds from './unsplash' +import wallhavenBackgrounds from './wallhaven' import localBackgrounds from './local' import { rgbToHex } from '../../utils' import { eventDebounce } from '../../utils/debounce' @@ -30,7 +31,13 @@ export default function initBackground(data: Sync.Storage, local: Local.Storage) bgsecond.style.transform = 'scale(1.1) translateX(0px) translate3d(0, 0, 0)' } - type === 'local' ? localBackgrounds() : unsplashBackgrounds({ unsplash: data.unsplash, cache: local.unsplashCache }) + if (type === 'local') { + localBackgrounds() + } else if (type === 'unsplash') { + unsplashBackgrounds({ unsplash: data.unsplash, cache: local.unsplashCache }) + } else if (type === 'wallhaven') { + wallhavenBackgrounds({ wallhaven: data.wallhaven, cache: local.wallhavenCache }) + } } // @@ -86,14 +93,26 @@ export function backgroundFilter({ blur, brightness, isEvent }: FilterOptions) { export function updateBackgroundOption({ freq, refresh }: UpdateOptions) { const i_type = document.getElementById('i_type') as HTMLInputElement - const isLocal = i_type.value === 'local' + const type = i_type.value || 'unsplash' if (freq !== undefined) { - isLocal ? localBackgrounds({ freq }) : unsplashBackgrounds(undefined, { every: freq }) + if (type === 'local') { + localBackgrounds({ freq }) + } else if (type === 'unsplash') { + unsplashBackgrounds(undefined, { every: freq }) + } else if (type === 'wallhaven') { + wallhavenBackgrounds(undefined, { every: freq }) + } } if (refresh) { - isLocal ? localBackgrounds({ refresh }) : unsplashBackgrounds(undefined, { refresh }) + if (type === 'local') { + localBackgrounds({ refresh }) + } else if (type === 'unsplash') { + unsplashBackgrounds(undefined, { refresh }) + } else if (type === 'wallhaven') { + wallhavenBackgrounds(undefined, { refresh }) + } } } diff --git a/src/scripts/features/backgrounds/wallhaven.ts b/src/scripts/features/backgrounds/wallhaven.ts new file mode 100644 index 00000000..6f179fa6 --- /dev/null +++ b/src/scripts/features/backgrounds/wallhaven.ts @@ -0,0 +1,346 @@ +import { periodOfDay, turnRefreshButton, apiFetch, freqControl, isEvery } from '../../utils' +import { LOCAL_DEFAULT, SYNC_DEFAULT } from '../../defaults' +import { imgBackground } from '.' +import { tradThis } from '../../utils/translations' +import errorMessage from '../../utils/errormessage' +import networkForm from '../../utils/networkform' +import storage from '../../storage' + +type wallhavenInit = { + wallhaven: Wallhaven.Sync + cache: Wallhaven.Local +} + +type WallhavenUpdate = { + refresh?: HTMLElement + parameter?: string + every?: string +} + +const wallhavenForm = networkForm('f_parameters') + +const search_settings = 'categories=110&purity=100&sorting=random&order=desc&ai_art_filter=1' + +export const bonjourrParams = { + noon: `sunrise`, + day: `sunlight`, + evening: `sunset`, + night: `night`, +} + +export default function wallhavenBackgrounds(init?: WallhavenInit, event?: WallhavenUpdate) { + if (event) { + updateWallhaven(event) + } + + if (init) { + try { + if (init.wallhaven.time === undefined) { + initWallhavenBackgrounds(init.wallhaven, init.cache) + } else { + cacheControl(init.wallhaven, init.cache) + } + } catch (e) { + errorMessage(e) + } + } +} + +async function updateWallhaven({ refresh, every, parameters }: WallhavenUpdate) { + const { wallhaven } = await storage.sync.get('wallhaven') + const wallhavenCache = await getCache() + + if (!wallhaven) { + return + } + + if (refresh) { + if (sessionStorage.waitingForPreload) { + turnRefreshButton(refresh, false) + return + } + + wallhaven.time = 0 + storage.sync.set({ wallhaven }) + turnRefreshButton(refresh, true) + + setTimeout(() => cacheControl(wallhaven), 400) + } + + if (isEvery(every)) { + const currentImage = wallhavenCache[wallhaven.lastParams][0] + wallhaven.pausedImage = every === 'pause' ? currentImage : undefined + wallhaven.every = every + wallhaven.time = freqControl.set() + storage.sync.set({ wallhaven }) + } + + if (parameters === '') { + wallhavenCache.user = [] + wallhaven.parameters = '' + wallhaven.lastParams = 'day' + + wallhavenBackgrounds({ wallhaven, cache: wallhavenCache }) + wallhavenForm.accept('i_parameters', bonjourrParams[wallhaven.lastParams]) + } + + if (parameters !== undefined && parameters.length > 0) { + if (!navigator.onLine) { + return wallhavenForm.warn(tradThis('No internet connection')) + } + + // add new parameters + wallhaven.parameters = parameters + wallhaven.lastParams = 'user' + wallhaven.time = freqControl.set() + + wallhavenForm.load() + + const list = await requestNewList(parameters) + + if (!list || list.length === 0) { + wallhavenForm.warn(`Cannot get "${parameters}"`) + return + } + + wallhavenCache['user'] = list + + await preloadImage(wallhavenCache['user'][0].path) + preloadImage(wallhavenCache['user'][1].path) + loadBackground(wallhavenCache['user'][0]) + + wallhavenForm.accept('i_parameters', parameters) + } + + storage.sync.set({ wallhaven }) + storage.local.set({ wallhavenCache }) +} + +async function cacheControl(wallhaven: Wallhaven.Sync, cache?: Wallhaven.Local) { + wallhaven = { ...SYNC_DEFAULT.wallhaven, ...wallhaven } + cache = cache ?? (await getCache()) + + let { lastParams } = wallhaven + const { every, time, parameters, pausedImage } = wallhaven + + const needNewImage = freqControl.get(every, time ?? Date.now()) + const needNewParams = !every.match(/day|pause/) && periodOfDay() !== lastParams + + if (needNewParams && lastParams !== 'user') { + lastParams = periodOfDay() + } + + let params = lastParams === 'user' ? parameters : bonjourrParams[lastParams] + let list = cache[lastParams] + + if (list.length === 0) { + const newlist = await requestNewList(params) + + if (!newlist) { + return + } + + list = newlist + await preloadImage(list[0].path) + + cache[lastParams] = list + storage.local.set({ wallhavenCache: cache }) + sessionStorage.setItem('waitingForPreload', 'true') + } + + if (sessionStorage.waitingForPreload === 'true') { + loadBackground(list[0]) + await preloadImage(list[1].path) + return + } + + if (!needNewImage) { + const hasPausedImage = every === 'pause' && pausedImage + loadBackground(hasPausedImage ? pausedImage : list[0]) + return + } + + // Needs new image, Update time + wallhaven.lastParams = lastParams + wallhaven.time = freqControl.set() + + if (list.length > 1) { + list.shift() + } + + loadBackground(list[0]) + + if (every === 'pause') { + wallhaven.pausedImage = list[0] + } + + // If end of cache, get & save new list + if (list.length === 1 && navigator.onLine) { + const newList = await requestNewList(params) + + if (newList) { + cache[wallhaven.lastParams] = list.concat(newList) + await preloadImage(newList[0].path) + } + } + + // Or preload next + else if (list.length > 1) { + await preloadImage(list[1].path) + } + + storage.sync.set({ wallhaven }) + storage.local.set({ wallhavenCache: cache }) +} + +async function initWallhavenBackgrounds(wallhaven: Wallhaven.Sync, cache: Wallhaven.Local) { + wallhaven = { ...SYNC_DEFAULT.wallhaven, ...wallhaven } + cache = cache ?? (await getCache()) + + const lastParams = periodOfDay() + let list = await requestNewList(bonjourrParams[lastParams]) + + if (!list) { + return + } + + cache[lastParams] = list + wallhaven.lastParams = lastParams + wallhaven.time = new Date().getTime() + preloadImage(list[0].path) + + // With weather loaded and different suntime + // maybe use another collection ? + + await new Promise((sleep) => setTimeout(sleep, 200)) + + const lastParamsAgain = periodOfDay() + + if (lastParams !== lastParamsAgain) { + list = (await requestNewList(bonjourrParams[lastParamsAgain])) ?? [] + wallhaven.lastParams = lastParamsAgain + cache[lastParamsAgain] = list + } + + storage.sync.set({ wallhaven }) + storage.local.set({ wallhavenCache: cache }) + sessionStorage.setItem('waitingForPreload', 'true') + + loadBackground(list[0]) + await preloadImage(list[1].path) +} + +async function requestNewList(parameters: string): Promise { + let json: Wallhaven.API[] + + const fullUrlParams = parameters.match(/(?<=wallhaven\.cc\/search\?).+/) + const url = 'https://wallhaven.cc/api/v1/search?' + + (fullUrlParams ?? `${search_settings}&q=${parameters}`) + + const resp = await apiFetch(url) + + if (resp?.status === 404) { + return null + } + + json = await resp?.json() + + if (json.length === 1) { + return null + } + + const filteredList: Wallhaven.Image[] = [] + + for (const img of json.data) { + filteredList.push({ + id: img.id, + url: img.url, + path: img.path, + colors: img.colors[0], + }) + } + + return filteredList +} + +function imgCredits(image: Wallhaven.Image) { + const domcontainer = document.getElementById('credit-container') + const domcredit = document.getElementById('credit') + + if (!domcontainer || !domcredit) return + + const domurl = document.createElement('a') + domurl.textContent = tradThis('Photo from Wallhaven') + domurl.href = image.url + + domcredit.textContent = '' + domcredit.appendChild(domurl) + appendSaveLink(domcredit, image) + + domcontainer.classList.toggle('shown', true) +} + +async function getCache(): Promise { + const cache = (await storage.local.get('wallhavenCache'))?.wallhavenCache ?? { ...LOCAL_DEFAULT.wallhavenCache } + return cache +} + +function loadBackground(props: Wallhaven.Image) { + imgBackground(props.path, props.colors) + imgCredits(props) +} + +async function preloadImage(src: string) { + const img = new Image() + img.referrerPolicy = 'no-referrer' + + sessionStorage.setItem('waitingForPreload', 'true') + + try { + img.src = src + await img.decode() + img.remove() + sessionStorage.removeItem('waitingForPreload') + } catch (_) { + console.warn('Could not decode image: ', src) + } + +} + +function appendSaveLink(domcredit: HTMLElement, image: Wallhaven.Image) { + const domsave = document.createElement('a') + domsave.className = 'save' + domsave.title = 'Download the current background to your computer' + domsave.onclick = () => saveImage(domsave, image) + + domcredit.appendChild(domsave) +} + +async function saveImage(domsave: HTMLAnchorElement, image: Wallhaven.Image) { + domsave.classList.add('loading') + try { + const imageResponse = await fetch(image.path) + + if (!imageResponse.ok) return + + const blob = await imageResponse.blob() + + domsave.onclick = null + domsave.href = URL.createObjectURL(blob) + domsave.download = image.path + + domsave.click() + } finally { + domsave.classList.remove('loading') + } +} + +async function apiFetch(url: string): Promise { + url = `https://api.bonjourr.fr/proxy?query=${url}`; + try { + return await fetch(url) + } catch (error) { + console.warn(error) + await new Promise((r) => setTimeout(() => r(true), 200)) + } +} diff --git a/src/scripts/settings.ts b/src/scripts/settings.ts index b218c3cf..7d0a1dc1 100644 --- a/src/scripts/settings.ts +++ b/src/scripts/settings.ts @@ -10,6 +10,7 @@ import moveElements from './features/move' import interfacePopup from './features/popup' import localBackgrounds from './features/backgrounds/local' import unsplashBackgrounds, { bonjourrCollections } from './features/backgrounds/unsplash' +import wallhavenBackgrounds, { bonjourrParams } from './features/backgrounds/wallhaven' import storage, { getSyncDefaults } from './storage' import customFont, { fontIsAvailableInSubset } from './features/fonts' import { backgroundFilter, updateBackgroundOption } from './features/backgrounds' @@ -113,7 +114,7 @@ function initOptionsValues(data: Sync.Storage) { setInput('i_row', data.linksrow || 8) setInput('i_linkstyle', data.linkstyle || 'default') setInput('i_type', data.background_type || 'unsplash') - setInput('i_freq', data.unsplash?.every) + setInput('i_freq', data[data.background_type]?.every) setInput('i_dark', data.dark || 'system') setInput('i_favicon', data.favicon ?? '') setInput('i_tabtitle', data.tabtitle ?? '') @@ -236,11 +237,16 @@ function initOptionsValues(data: Sync.Storage) { // Backgrounds options init paramId('local_options')?.classList.toggle('shown', data.background_type === 'local') paramId('unsplash_options')?.classList.toggle('shown', data.background_type === 'unsplash') + paramId('wallhaven_options')?.classList.toggle('shown', data.background_type === 'wallhaven') // Unsplash collection paramId('i_collection')?.setAttribute('value', data?.unsplash?.collection ?? '') paramId('i_collection')?.setAttribute('placeholder', data?.unsplash?.collection || bonjourrCollections[data?.unsplash?.lastCollec ?? 'day']) + // Wallhaven parameters + paramId('i_parameters')?.setAttribute('value', data?.wallhaven?.parameters ?? '') + paramId('i_parameters')?.setAttribute('placeholder', data?.wallhaven?.parameters || bonjourrParams[data?.wallhaven?.lastParams ?? 'day']) + // Quotes option display paramId('quotes_options')?.classList.toggle('shown', data.quotes?.on) paramId('quotes_userlist')?.classList.toggle('shown', data.quotes?.type === 'user') @@ -393,6 +399,14 @@ function initOptionsEvents() { }) }) + paramId('f_parameters').addEventListener('submit', function (this, event) { + event.preventDefault() + wallhavenBackgrounds(undefined, { + parameters: stringMaxSize(paramId('i_parameters').value, 256), + }) + }) + + // Custom backgrounds paramId('b_background-upload').onclickdown(function (this: HTMLInputElement) { @@ -859,6 +873,7 @@ function showall(val: boolean, event: boolean) { async function selectBackgroundType(cat: string) { document.getElementById('local_options')?.classList.toggle('shown', cat === 'local') document.getElementById('unsplash_options')?.classList.toggle('shown', cat === 'unsplash') + document.getElementById('wallhaven_options')?.classList.toggle('shown', cat === 'wallhaven') if (cat === 'local') { localBackgrounds({ settings: document.getElementById('settings') as HTMLElement }) @@ -883,6 +898,24 @@ async function selectBackgroundType(cat: string) { ) } + if (cat === 'wallhaven') { + const data = await storage.sync.get() + const local = await storage.local.get('wallhavenCache') + + if (!data.wallhaven) return + + document.querySelector('#i_freq')!.value = data.wallhaven.every || 'hour' + document.getElementById('credit-container')?.classList.toggle('shown', true) + setTimeout( + () => + wallhavenBackgrounds({ + wallhaven: data.wallhaven, + cache: local.wallhavenCache, + }), + 100 + ) + } + storage.sync.set({ background_type: cat }) } diff --git a/src/settings.html b/src/settings.html index dfec8180..ff8a8e27 100644 --- a/src/settings.html +++ b/src/settings.html @@ -96,6 +96,7 @@

Background

@@ -179,6 +180,55 @@

Background

+ +