mirror of
https://github.com/mue/mue.git
synced 2026-07-16 21:44:22 +02:00
refactor: restructure background feature
This commit is contained in:
@@ -1,526 +1,35 @@
|
||||
// todo: rewrite this mess
|
||||
import variables from 'config/variables';
|
||||
import { PureComponent } from 'react';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import PhotoInformation from './components/PhotoInformation';
|
||||
import BackgroundImage from './components/BackgroundImage';
|
||||
import BackgroundVideo from './components/BackgroundVideo';
|
||||
|
||||
import EventBus from 'utils/eventbus';
|
||||
|
||||
import { supportsAVIF } from './api/avif';
|
||||
import { getOfflineImage } from './api/getOfflineImage';
|
||||
import videoCheck from './api/videoCheck';
|
||||
import { randomColourStyleBuilder } from './api/randomColour';
|
||||
import { useBackgroundState } from './hooks/useBackgroundState';
|
||||
import { useBackgroundLoader } from './hooks/useBackgroundLoader';
|
||||
import { useBackgroundRenderer, useBackgroundFilters } from './hooks/useBackgroundRenderer';
|
||||
import { useBackgroundEvents } from './hooks/useBackgroundEvents';
|
||||
|
||||
import './scss/index.scss';
|
||||
import { decodeBlurHash } from 'fast-blurhash';
|
||||
|
||||
export default class Background extends PureComponent {
|
||||
constructor() {
|
||||
super();
|
||||
this.state = {
|
||||
blob: null,
|
||||
style: '',
|
||||
url: '',
|
||||
currentAPI: '',
|
||||
firstTime: false,
|
||||
photoInfo: { hidden: false, offline: false, photographerURL: '', photoURL: '' },
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Background component - Manages and displays backgrounds
|
||||
* Supports: API images, custom images, colors, gradients, videos, and photo packs
|
||||
*/
|
||||
export default function Background() {
|
||||
const { backgroundData, updateBackground, resetBackground } = useBackgroundState();
|
||||
const { refreshBackground } = useBackgroundLoader(updateBackground, resetBackground);
|
||||
const filterStyle = useBackgroundFilters();
|
||||
|
||||
async setBackground() {
|
||||
// clean up the previous image to prevent a memory leak
|
||||
if (this.blob) {
|
||||
URL.revokeObjectURL(this.blob);
|
||||
}
|
||||
useBackgroundRenderer(backgroundData);
|
||||
useBackgroundEvents(backgroundData, refreshBackground);
|
||||
|
||||
const backgroundImage = document.getElementById('backgroundImage');
|
||||
|
||||
if (this.state.url !== '') {
|
||||
const url = this.state.url;
|
||||
const photoInformation = document.querySelector('.photoInformation');
|
||||
|
||||
// just set the background
|
||||
if (localStorage.getItem('bgtransition') === 'false') {
|
||||
photoInformation?.[(photoInformation.style.display = 'flex')];
|
||||
return (backgroundImage.style.background = `url(${url})`);
|
||||
}
|
||||
|
||||
backgroundImage.style.background = null;
|
||||
|
||||
if (this.state.photoInfo.blur_hash) {
|
||||
backgroundImage.style.backgroundColor = this.state.photoInfo.colour;
|
||||
backgroundImage.classList.add('fade-in');
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = 32;
|
||||
canvas.height = 32;
|
||||
const ctx = canvas.getContext('2d');
|
||||
const imageData = ctx.createImageData(32, 32);
|
||||
imageData.data.set(decodeBlurHash(this.state.photoInfo.blur_hash, 32, 32));
|
||||
ctx.putImageData(imageData, 0, 0);
|
||||
backgroundImage.style.backgroundImage = `url(${canvas.toDataURL()})`;
|
||||
}
|
||||
|
||||
this.blob = URL.createObjectURL(await (await fetch(url)).blob());
|
||||
backgroundImage.classList.add('backgroundTransform');
|
||||
backgroundImage.style.backgroundImage = `url(${this.blob})`;
|
||||
} else {
|
||||
// custom colour
|
||||
backgroundImage.setAttribute('style', this.state.style);
|
||||
}
|
||||
}
|
||||
|
||||
async getAPIImageData(currentPun) {
|
||||
let apiCategories;
|
||||
|
||||
try {
|
||||
apiCategories = JSON.parse(localStorage.getItem('apiCategories'));
|
||||
} catch (error) {
|
||||
apiCategories = localStorage.getItem('apiCategories');
|
||||
}
|
||||
|
||||
const backgroundAPI = localStorage.getItem('backgroundAPI');
|
||||
const apiQuality = localStorage.getItem('apiQuality');
|
||||
let backgroundExclude = JSON.parse(localStorage.getItem('backgroundExclude'));
|
||||
if (!Array.isArray(backgroundExclude)) {
|
||||
backgroundExclude = [];
|
||||
}
|
||||
if (currentPun) {
|
||||
backgroundExclude.push(currentPun);
|
||||
}
|
||||
|
||||
let requestURL, data;
|
||||
|
||||
switch (backgroundAPI) {
|
||||
case 'unsplash':
|
||||
case 'pexels': {
|
||||
const collection = localStorage.getItem('unsplashCollections');
|
||||
if (collection) {
|
||||
requestURL = `${variables.constants.API_URL}/images/unsplash?collections=${collection}&quality=${apiQuality}`;
|
||||
} else {
|
||||
requestURL = `${variables.constants.API_URL}/images/unsplash?categories=${apiCategories || ''}&quality=${apiQuality}`;
|
||||
}
|
||||
break;
|
||||
}
|
||||
// Defaults to Mue
|
||||
default:
|
||||
requestURL = `${variables.constants.API_URL}/images/random?categories=${apiCategories || ''}&quality=${apiQuality}&excludes=${backgroundExclude}`;
|
||||
break;
|
||||
}
|
||||
|
||||
const accept = 'application/json, ' + ((await supportsAVIF()) ? 'image/avif' : 'image/webp');
|
||||
try {
|
||||
data = await (await fetch(requestURL, { headers: { accept } })).json();
|
||||
} catch {
|
||||
// if requesting to the API fails, we get an offline image
|
||||
this.setState(getOfflineImage('api'));
|
||||
return null;
|
||||
}
|
||||
|
||||
let photoURL, photographerURL;
|
||||
|
||||
if (backgroundAPI === 'unsplash') {
|
||||
photoURL = data.photo_page;
|
||||
photographerURL = data.photographer_page;
|
||||
}
|
||||
|
||||
return {
|
||||
url: data.file,
|
||||
type: 'api',
|
||||
currentAPI: backgroundAPI,
|
||||
photoInfo: {
|
||||
hidden: false,
|
||||
category: data.category,
|
||||
credit: data.photographer,
|
||||
location: data.location.name,
|
||||
camera: data.camera,
|
||||
url: data.file,
|
||||
photographerURL,
|
||||
photoURL,
|
||||
latitude: data.location.latitude || null,
|
||||
longitude: data.location.longitude || null,
|
||||
views: data.views || null,
|
||||
downloads: data.downloads || null,
|
||||
likes: data.likes || null,
|
||||
description: data.description || null,
|
||||
colour: data.colour,
|
||||
blur_hash: data.blur_hash,
|
||||
pun: data.pun || null,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// Main background getting function
|
||||
async getBackground() {
|
||||
let offline = localStorage.getItem('offlineMode') === 'true';
|
||||
if (localStorage.getItem('showWelcome') === 'true') {
|
||||
offline = true;
|
||||
}
|
||||
|
||||
const setFavourited = ({ type, url, credit, location, camera, pun, offline }) => {
|
||||
if (type === 'random_colour' || type === 'random_gradient') {
|
||||
return this.setState({ type: 'colour', style: `background:${url}` });
|
||||
}
|
||||
this.setState({ url, photoInfo: { credit, location, camera, pun, offline, url } });
|
||||
};
|
||||
|
||||
const favourited = JSON.parse(localStorage.getItem('favourite'));
|
||||
if (favourited) {
|
||||
return setFavourited(favourited);
|
||||
}
|
||||
|
||||
const type = localStorage.getItem('backgroundType');
|
||||
switch (type) {
|
||||
case 'api': {
|
||||
if (offline) {
|
||||
return this.setState(getOfflineImage('api'));
|
||||
}
|
||||
|
||||
// API background
|
||||
const data =
|
||||
JSON.parse(localStorage.getItem('nextImage')) || (await this.getAPIImageData());
|
||||
localStorage.setItem('nextImage', null);
|
||||
if (data) {
|
||||
this.setState(data);
|
||||
localStorage.setItem('currentBackground', JSON.stringify(data));
|
||||
localStorage.setItem(
|
||||
'nextImage',
|
||||
JSON.stringify(await this.getAPIImageData(data.photoInfo.pun)),
|
||||
); // pre-fetch data about the next image
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'colour': {
|
||||
let customBackgroundColour = localStorage.getItem('customBackgroundColour');
|
||||
// check if its a json object
|
||||
if (customBackgroundColour && customBackgroundColour.startsWith('{')) {
|
||||
const customBackground = JSON.parse(customBackgroundColour);
|
||||
// move to new format
|
||||
try {
|
||||
localStorage.setItem('customBackgroundColour', customBackground.gradient[0].colour);
|
||||
customBackgroundColour = customBackground.gradient.colour;
|
||||
} catch {
|
||||
// give up
|
||||
customBackgroundColour = 'rgb(0,0,0)';
|
||||
}
|
||||
}
|
||||
this.setState({
|
||||
type: 'colour',
|
||||
style: `background: ${customBackgroundColour || 'rgb(0,0,0)'}`,
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case 'random_colour':
|
||||
case 'random_gradient':
|
||||
this.setState(randomColourStyleBuilder(type));
|
||||
break;
|
||||
case 'custom': {
|
||||
let customBackground = [];
|
||||
const customSaved = localStorage.getItem('customBackground');
|
||||
try {
|
||||
customBackground = JSON.parse(customSaved);
|
||||
} catch {
|
||||
if (customSaved !== '') {
|
||||
// move to new format
|
||||
customBackground = [customSaved];
|
||||
}
|
||||
localStorage.setItem('customBackground', JSON.stringify(customBackground));
|
||||
}
|
||||
|
||||
// pick random
|
||||
customBackground = customBackground[Math.floor(Math.random() * customBackground.length)];
|
||||
|
||||
// allow users to use offline images
|
||||
if (offline && !customBackground.startsWith('data:')) {
|
||||
return this.setState(getOfflineImage('custom'));
|
||||
}
|
||||
|
||||
if (
|
||||
customBackground !== '' &&
|
||||
customBackground !== 'undefined' &&
|
||||
customBackground !== undefined
|
||||
) {
|
||||
const object = {
|
||||
url: customBackground,
|
||||
type: 'custom',
|
||||
video: videoCheck(customBackground),
|
||||
photoInfo: { hidden: true },
|
||||
};
|
||||
|
||||
this.setState(object);
|
||||
|
||||
localStorage.setItem('currentBackground', JSON.stringify(object));
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'photo_pack': {
|
||||
if (offline) {
|
||||
return this.setState(getOfflineImage('photo_pack'));
|
||||
}
|
||||
|
||||
const photofavourited = JSON.parse(localStorage.getItem('favourite'));
|
||||
if (photofavourited) {
|
||||
return setFavourited(photofavourited);
|
||||
}
|
||||
|
||||
const photoPack = [];
|
||||
const installed = JSON.parse(localStorage.getItem('installed'));
|
||||
installed.forEach((item) => {
|
||||
if (item.type === 'photos') {
|
||||
photoPack.push(...item.photos);
|
||||
}
|
||||
});
|
||||
if (photoPack) {
|
||||
const randomNumber = Math.floor(Math.random() * photoPack.length);
|
||||
const randomPhoto = photoPack[randomNumber];
|
||||
if (
|
||||
(localStorage.getItem('backgroundchange') === 'refresh' &&
|
||||
this.state.firstTime === true) ||
|
||||
(localStorage.getItem('backgroundchange') === null && this.state.firstTime === true)
|
||||
) {
|
||||
if (this.state.firstTime !== true) {
|
||||
localStorage.setItem('marketplaceNumber', randomNumber);
|
||||
this.setState({
|
||||
firstTime: false,
|
||||
url: randomPhoto.url.default,
|
||||
type: 'photo_pack',
|
||||
photoInfo: {
|
||||
hidden: false,
|
||||
credit: randomPhoto.photographer,
|
||||
location: randomPhoto.location,
|
||||
},
|
||||
});
|
||||
}
|
||||
} else {
|
||||
if (
|
||||
Number(
|
||||
Number(localStorage.getItem('backgroundStartTime')) +
|
||||
Number(localStorage.getItem('backgroundchange')) >=
|
||||
Number(Date.now()),
|
||||
)
|
||||
) {
|
||||
const randomPhoto = photoPack[localStorage.getItem('marketplaceNumber')];
|
||||
if (this.state.firstTime !== true) {
|
||||
this.setState({
|
||||
url: randomPhoto.url.default,
|
||||
type: 'photo_pack',
|
||||
photoInfo: {
|
||||
hidden: false,
|
||||
credit: randomPhoto.photographer,
|
||||
location: randomPhoto.location,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
this.setState({ firstTime: true });
|
||||
}
|
||||
this.setState({ firstTime: true });
|
||||
} else {
|
||||
localStorage.setItem('marketplaceNumber', randomNumber);
|
||||
return this.setState({
|
||||
url: randomPhoto.url.default,
|
||||
type: 'photo_pack',
|
||||
photoInfo: {
|
||||
hidden: false,
|
||||
credit: randomPhoto.photographer,
|
||||
location: randomPhoto.location,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const element = document.getElementById('backgroundImage');
|
||||
|
||||
// this resets it so the fade in and getting background all works properly
|
||||
const refresh = () => {
|
||||
element.classList.remove('fade-in');
|
||||
this.setState({ url: '', style: '', type: '', video: false, photoInfo: { hidden: true } });
|
||||
this.getBackground();
|
||||
};
|
||||
|
||||
EventBus.on('refresh', (data) => {
|
||||
if (data === 'welcomeLanguage') {
|
||||
localStorage.setItem('welcomeImage', JSON.stringify(this.state));
|
||||
}
|
||||
|
||||
if (data === 'background') {
|
||||
if (localStorage.getItem('background') === 'false') {
|
||||
// user is using custom colour or image
|
||||
if (this.state.photoInfo.hidden === false) {
|
||||
document.querySelector('.photoInformation').style.display = 'none';
|
||||
}
|
||||
|
||||
// video backgrounds
|
||||
if (this.state.video === true) {
|
||||
return (document.getElementById('backgroundVideo').style.display = 'none');
|
||||
} else {
|
||||
return (element.style.display = 'none');
|
||||
}
|
||||
}
|
||||
|
||||
// video backgrounds
|
||||
if (this.state.video === true) {
|
||||
document.getElementById('backgroundVideo').style.display = 'block';
|
||||
} else {
|
||||
if (this.state.photoInfo.hidden === false) {
|
||||
try {
|
||||
document.querySelector('.photoInformation').style.display = 'flex';
|
||||
} catch (e) {
|
||||
// Disregard exception
|
||||
}
|
||||
}
|
||||
|
||||
element.style.display = 'block';
|
||||
}
|
||||
|
||||
const backgroundType = localStorage.getItem('backgroundType');
|
||||
|
||||
if (this.state.photoInfo.offline !== true) {
|
||||
// basically check to make sure something has changed before we try getting another background
|
||||
if (
|
||||
backgroundType !== this.state.type ||
|
||||
(this.state.type === 'api' &&
|
||||
localStorage.getItem('backgroundAPI') !== this.state.currentAPI) ||
|
||||
(this.state.type === 'custom' &&
|
||||
localStorage.getItem('customBackground') !== this.state.url) ||
|
||||
JSON.parse(localStorage.getItem('backgroundExclude')).includes(this.state.photoInfo.pun)
|
||||
) {
|
||||
return refresh();
|
||||
}
|
||||
} else if (backgroundType !== this.state.type) {
|
||||
return refresh();
|
||||
}
|
||||
}
|
||||
|
||||
// uninstall photo pack reverts your background to what you had previously
|
||||
if (
|
||||
data === 'marketplacebackgrounduninstall' ||
|
||||
data === 'backgroundwelcome' ||
|
||||
data === 'backgroundrefresh'
|
||||
) {
|
||||
refresh();
|
||||
}
|
||||
|
||||
if (data === 'backgroundeffect') {
|
||||
// background effects so we don't get another image again
|
||||
const backgroundFilterSetting = localStorage.getItem('backgroundFilter');
|
||||
const backgroundFilter = backgroundFilterSetting && backgroundFilterSetting !== 'none';
|
||||
|
||||
if (this.state.video === true) {
|
||||
document.getElementById('backgroundVideo').style.webkitFilter =
|
||||
`blur(${localStorage.getItem('blur')}px) brightness(${localStorage.getItem(
|
||||
'brightness',
|
||||
)}%) ${
|
||||
backgroundFilter
|
||||
? backgroundFilterSetting +
|
||||
'(' +
|
||||
localStorage.getItem('backgroundFilterAmount') +
|
||||
'%)'
|
||||
: ''
|
||||
}`;
|
||||
} else {
|
||||
element.style.webkitFilter = `blur(${localStorage.getItem(
|
||||
'blur',
|
||||
)}px) brightness(${localStorage.getItem('brightness')}%) ${
|
||||
backgroundFilter
|
||||
? backgroundFilterSetting +
|
||||
'(' +
|
||||
localStorage.getItem('backgroundFilterAmount') +
|
||||
'%)'
|
||||
: ''
|
||||
}`;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (localStorage.getItem('welcomeTab')) {
|
||||
return this.setState(JSON.parse(localStorage.getItem('welcomeImage')));
|
||||
}
|
||||
|
||||
if (
|
||||
localStorage.getItem('backgroundchange') === 'refresh' ||
|
||||
localStorage.getItem('backgroundchange') === null
|
||||
) {
|
||||
try {
|
||||
document.getElementById('backgroundImage').classList.remove('fade-in');
|
||||
document.getElementsByClassName('photoInformation')[0].classList.remove('fade-in');
|
||||
} catch (e) {
|
||||
// Disregard exception
|
||||
}
|
||||
this.getBackground();
|
||||
localStorage.setItem('backgroundStartTime', Date.now());
|
||||
}
|
||||
}
|
||||
|
||||
// only set once we've got the info
|
||||
componentDidUpdate() {
|
||||
if (this.state.video === true) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setBackground();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
EventBus.off('refresh');
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.video === true) {
|
||||
const enabled = (setting) => {
|
||||
return localStorage.getItem(setting) === 'true';
|
||||
};
|
||||
|
||||
return (
|
||||
<video
|
||||
autoPlay
|
||||
muted={enabled('backgroundVideoMute')}
|
||||
loop={enabled('backgroundVideoLoop')}
|
||||
style={{
|
||||
WebkitFilter: `blur(${localStorage.getItem(
|
||||
'blur',
|
||||
)}px) brightness(${localStorage.getItem('brightness')}%)`,
|
||||
}}
|
||||
id="backgroundVideo"
|
||||
>
|
||||
<source src={this.state.url} />
|
||||
</video>
|
||||
);
|
||||
}
|
||||
|
||||
const backgroundFilter = localStorage.getItem('backgroundFilter');
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
style={{
|
||||
WebkitFilter: `blur(${localStorage.getItem(
|
||||
'blur',
|
||||
)}px) brightness(${localStorage.getItem('brightness')}%) ${
|
||||
backgroundFilter && backgroundFilter !== 'none'
|
||||
? backgroundFilter + '(' + localStorage.getItem('backgroundFilterAmount') + '%)'
|
||||
: ''
|
||||
}`,
|
||||
}}
|
||||
id="backgroundImage"
|
||||
/>
|
||||
{this.state.photoInfo.credit !== '' && (
|
||||
<PhotoInformation
|
||||
info={this.state.photoInfo}
|
||||
api={this.state.currentAPI}
|
||||
url={this.state.url}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
return backgroundData.video ? (
|
||||
<BackgroundVideo url={backgroundData.url} filterStyle={filterStyle} />
|
||||
) : (
|
||||
<BackgroundImage
|
||||
filterStyle={filterStyle}
|
||||
photoInfo={backgroundData.photoInfo}
|
||||
currentAPI={backgroundData.currentAPI}
|
||||
url={backgroundData.url}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,14 +2,14 @@ const testImage =
|
||||
'AAAAIGZ0eXBhdmlmAAAAAGF2aWZtaWYxbWlhZk1BMUIAAAFCbWV0YQAAAAAAAAAoaGRscgAAAAAAAAAAcGljdAAAAAAAAAAAAAAAAFBETmF2aWYAAAAADnBpdG0AAAAAAAEAAAAsaWxvYwAAAABEAAACAAEAAAABAAACRgAAABgAAgAAAAEAAAFqAAAA3AAAAEFpaW5mAAAAAAACAAAAGmluZmUCAAAAAAEAAGF2MDFDb2xvcgAAAAAZaW5mZQIAAAEAAgAARXhpZkV4aWYAAAAAGmlyZWYAAAAAAAAADmNkc2MAAgABAAEAAAB5aXBycAAAAFlpcGNvAAAAFGlzcGUAAAAAAAAAAQAAAAEAAAAQcGFzcAAAAAEAAAABAAAADGF2MUOBABwAAAAADnBpeGkAAAAAAQgAAAATY29scm5jbHgAAQANAAGAAAAAGGlwbWEAAAAAAAAAAQABBQECg4SFAAAA/G1kYXQAAAAASUkqAAgAAAAGABIBAwABAAAAAQAAABoBBQABAAAAVgAAABsBBQABAAAAXgAAACgBAwABAAAAAgAAADEBAgARAAAAZgAAAGmHBAABAAAAeAAAAAAAAAABAAAAAQAAAAEAAAABAAAAcGFpbnQubmV0IDQuMy4xMgAABQAAkAcABAAAADAyMzABoAMAAQAAAAEAAAACoAQAAQAAAAEAAAADoAQAAQAAAAEAAAAFoAQAAQAAALoAAAAAAAAAAgABAAIABAAAAFI5OAACAAcABAAAADAxMDAAAAAAEgAKBxgABpgIaA0yCxJABBEAEADG1FkX';
|
||||
|
||||
/**
|
||||
* It creates a new image element, sets the source to a base64 encoded AVIF image, and then resolves
|
||||
* true if the image loads successfully, or false if it doesn't
|
||||
* Checks if the browser supports AVIF image format
|
||||
* @returns {Promise<boolean>} True if AVIF is supported, false otherwise
|
||||
*/
|
||||
export const supportsAVIF = () => {
|
||||
new Promise((resolve) => {
|
||||
export function supportsAVIF() {
|
||||
return new Promise((resolve) => {
|
||||
const image = new Image();
|
||||
image.src = `data:image/avif;base64,${testImage}`;
|
||||
image.onload = () => resolve(true);
|
||||
image.onerror = () => resolve(false);
|
||||
});
|
||||
};
|
||||
}
|
||||
18
src/features/background/api/backgroundFilters.js
Normal file
18
src/features/background/api/backgroundFilters.js
Normal file
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* Generates the filter style string for backgrounds
|
||||
* @returns {string} The filter CSS string
|
||||
*/
|
||||
export function getBackgroundFilterStyle() {
|
||||
const blur = localStorage.getItem('blur') || '0';
|
||||
const brightness = localStorage.getItem('brightness') || '100';
|
||||
const backgroundFilter = localStorage.getItem('backgroundFilter');
|
||||
const backgroundFilterAmount = localStorage.getItem('backgroundFilterAmount') || '100';
|
||||
|
||||
let filterString = `blur(${blur}px) brightness(${brightness}%)`;
|
||||
|
||||
if (backgroundFilter && backgroundFilter !== 'none') {
|
||||
filterString += ` ${backgroundFilter}(${backgroundFilterAmount}%)`;
|
||||
}
|
||||
|
||||
return filterString;
|
||||
}
|
||||
211
src/features/background/api/backgroundLoader.js
Normal file
211
src/features/background/api/backgroundLoader.js
Normal file
@@ -0,0 +1,211 @@
|
||||
import variables from 'config/variables';
|
||||
import { supportsAVIF } from './avifSupport';
|
||||
import { getOfflineImage } from './offlineImage';
|
||||
import { randomColourStyleBuilder } from './randomColour';
|
||||
import videoCheck from './videoCheck';
|
||||
|
||||
const parseJSON = (key, fallback = null) => {
|
||||
try {
|
||||
return JSON.parse(localStorage.getItem(key)) || fallback;
|
||||
} catch {
|
||||
return fallback;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetches image data from the configured API
|
||||
*/
|
||||
export async function fetchAPIImageData(excludedPun = null) {
|
||||
const api = localStorage.getItem('backgroundAPI') || 'mue';
|
||||
const quality = localStorage.getItem('apiQuality') || 'high';
|
||||
const categories = parseJSON('apiCategories', localStorage.getItem('apiCategories'));
|
||||
const excludes = [...parseJSON('backgroundExclude', []), ...(excludedPun ? [excludedPun] : [])];
|
||||
|
||||
const baseURL = `${variables.constants.API_URL}/images`;
|
||||
const collection = localStorage.getItem('unsplashCollections');
|
||||
|
||||
const url = (api === 'unsplash' || api === 'pexels')
|
||||
? `${baseURL}/unsplash?${collection ? `collections=${collection}` : `categories=${categories || ''}`}&quality=${quality}`
|
||||
: `${baseURL}/random?categories=${categories || ''}&quality=${quality}&excludes=${excludes}`;
|
||||
|
||||
try {
|
||||
const accept = `application/json, ${await supportsAVIF() ? 'image/avif' : 'image/webp'}`;
|
||||
const data = await (await fetch(url, { headers: { accept } })).json();
|
||||
|
||||
return {
|
||||
url: data.file,
|
||||
type: 'api',
|
||||
currentAPI: api,
|
||||
photoInfo: {
|
||||
hidden: false,
|
||||
category: data.category,
|
||||
credit: data.photographer,
|
||||
location: data.location.name,
|
||||
camera: data.camera,
|
||||
url: data.file,
|
||||
photographerURL: api === 'unsplash' ? data.photographer_page : undefined,
|
||||
photoURL: api === 'unsplash' ? data.photo_page : undefined,
|
||||
latitude: data.location.latitude,
|
||||
longitude: data.location.longitude,
|
||||
views: data.views,
|
||||
downloads: data.downloads,
|
||||
likes: data.likes,
|
||||
description: data.description,
|
||||
colour: data.colour,
|
||||
blur_hash: data.blur_hash,
|
||||
pun: data.pun,
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch API image:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets background data based on current configuration
|
||||
*/
|
||||
export async function getBackgroundData() {
|
||||
const isOffline = localStorage.getItem('offlineMode') === 'true' || localStorage.getItem('showWelcome') === 'true';
|
||||
|
||||
// Handle favourited background
|
||||
const fav = parseJSON('favourite');
|
||||
if (fav) {
|
||||
if (fav.type === 'random_colour' || fav.type === 'random_gradient') {
|
||||
return { type: 'colour', style: `background:${fav.url}` };
|
||||
}
|
||||
return { url: fav.url, photoInfo: { ...fav, url: fav.url } };
|
||||
}
|
||||
|
||||
const type = localStorage.getItem('backgroundType');
|
||||
|
||||
switch (type) {
|
||||
case 'api':
|
||||
return getAPIBackground(isOffline);
|
||||
|
||||
case 'colour':
|
||||
return getColourBackground();
|
||||
|
||||
case 'random_colour':
|
||||
case 'random_gradient':
|
||||
return randomColourStyleBuilder(type);
|
||||
|
||||
case 'custom':
|
||||
return getCustomBackground(isOffline);
|
||||
|
||||
case 'photo_pack':
|
||||
return getPhotoPackBackground(isOffline);
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets solid colour background
|
||||
*/
|
||||
function getColourBackground() {
|
||||
let colour = localStorage.getItem('customBackgroundColour');
|
||||
|
||||
// Migrate legacy format
|
||||
if (colour?.startsWith('{')) {
|
||||
try {
|
||||
colour = JSON.parse(colour).gradient[0].colour;
|
||||
localStorage.setItem('customBackgroundColour', colour);
|
||||
} catch {
|
||||
colour = 'rgb(0,0,0)';
|
||||
}
|
||||
}
|
||||
|
||||
return { type: 'colour', style: `background: ${colour || 'rgb(0,0,0)'}` };
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets API background with caching
|
||||
*/
|
||||
async function getAPIBackground(isOffline) {
|
||||
if (isOffline) return getOfflineImage('api');
|
||||
|
||||
// Use cached next image if available
|
||||
const cached = parseJSON('nextImage');
|
||||
const data = cached || await fetchAPIImageData();
|
||||
|
||||
if (!data) return getOfflineImage('api');
|
||||
|
||||
localStorage.setItem('currentBackground', JSON.stringify(data));
|
||||
localStorage.setItem('nextImage', null);
|
||||
|
||||
// Pre-fetch next image
|
||||
fetchAPIImageData(data.photoInfo.pun).then((next) => {
|
||||
if (next) localStorage.setItem('nextImage', JSON.stringify(next));
|
||||
});
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets custom background
|
||||
*/
|
||||
function getCustomBackground(isOffline) {
|
||||
let backgrounds = parseJSON('customBackground');
|
||||
|
||||
// Migrate legacy format
|
||||
if (!Array.isArray(backgrounds)) {
|
||||
const saved = localStorage.getItem('customBackground');
|
||||
backgrounds = saved ? [saved] : [];
|
||||
localStorage.setItem('customBackground', JSON.stringify(backgrounds));
|
||||
}
|
||||
|
||||
if (backgrounds.length === 0) return null;
|
||||
|
||||
const selected = backgrounds[Math.floor(Math.random() * backgrounds.length)];
|
||||
|
||||
if (isOffline && !selected.startsWith('data:')) return getOfflineImage('custom');
|
||||
|
||||
const data = {
|
||||
url: selected,
|
||||
type: 'custom',
|
||||
video: videoCheck(selected),
|
||||
photoInfo: { hidden: true },
|
||||
};
|
||||
|
||||
localStorage.setItem('currentBackground', JSON.stringify(data));
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets photo pack background
|
||||
*/
|
||||
function getPhotoPackBackground(isOffline) {
|
||||
if (isOffline) return getOfflineImage('photo_pack');
|
||||
|
||||
const photos = parseJSON('installed', []).flatMap((item) =>
|
||||
item.type === 'photos' && item.photos ? item.photos : []
|
||||
);
|
||||
|
||||
if (photos.length === 0) return null;
|
||||
|
||||
const interval = localStorage.getItem('backgroundchange');
|
||||
const startTime = Number(localStorage.getItem('backgroundStartTime'));
|
||||
const shouldRefresh = !interval || interval === 'refresh' || startTime + Number(interval) < Date.now();
|
||||
|
||||
let index;
|
||||
if (shouldRefresh) {
|
||||
index = Math.floor(Math.random() * photos.length);
|
||||
localStorage.setItem('marketplaceNumber', index);
|
||||
} else {
|
||||
index = Number(localStorage.getItem('marketplaceNumber')) || 0;
|
||||
}
|
||||
|
||||
const photo = photos[index];
|
||||
|
||||
return photo ? {
|
||||
url: photo.url.default,
|
||||
type: 'photo_pack',
|
||||
photoInfo: {
|
||||
hidden: false,
|
||||
credit: photo.photographer,
|
||||
location: photo.location,
|
||||
},
|
||||
} : null;
|
||||
}
|
||||
15
src/features/background/api/blobUrl.js
Normal file
15
src/features/background/api/blobUrl.js
Normal file
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* Creates a blob URL from a remote URL
|
||||
* @param {string} url - The remote URL to fetch
|
||||
* @returns {Promise<string|null>} The blob URL or null if failed
|
||||
*/
|
||||
export async function createBlobUrl(url) {
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
const blob = await response.blob();
|
||||
return URL.createObjectURL(blob);
|
||||
} catch (error) {
|
||||
console.error('Failed to create blob URL:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
24
src/features/background/api/blurHash.js
Normal file
24
src/features/background/api/blurHash.js
Normal file
@@ -0,0 +1,24 @@
|
||||
import { decodeBlurHash } from 'fast-blurhash';
|
||||
|
||||
/**
|
||||
* Generates a blur hash placeholder image as a data URL
|
||||
* @param {string} blurHash - The blur hash string
|
||||
* @param {number} width - Canvas width (default: 32)
|
||||
* @param {number} height - Canvas height (default: 32)
|
||||
* @returns {string|null} - Data URL of the blur hash image or null if failed
|
||||
*/
|
||||
export function generateBlurHashDataUrl(blurHash, width = 32, height = 32) {
|
||||
try {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
const ctx = canvas.getContext('2d');
|
||||
const imageData = ctx.createImageData(width, height);
|
||||
imageData.data.set(decodeBlurHash(blurHash, width, height));
|
||||
ctx.putImageData(imageData, 0, 0);
|
||||
return canvas.toDataURL();
|
||||
} catch (error) {
|
||||
console.error('Failed to generate blur hash:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,9 @@
|
||||
import offlineImages from '../offline_images.json';
|
||||
|
||||
/**
|
||||
* It gets a random photographer from the offlineImages.json file, then gets a random image from that
|
||||
* photographer, and returns an object with the image's URL, type, and photoInfo.
|
||||
* </code>
|
||||
* @param type - 'background' or 'thumbnail'
|
||||
* @returns An object with the following properties:
|
||||
* url: A string that is the path to the image.
|
||||
* type: A string that is the type of image.
|
||||
* photoInfo: An object with the following properties:
|
||||
* offline: A boolean that is true.
|
||||
* credit: A string that is the name of the photographer.
|
||||
* Gets a random offline image from the bundled collection
|
||||
* @param {string} type - The background type (for storage)
|
||||
* @returns {object} Background data object with offline image
|
||||
*/
|
||||
export function getOfflineImage(type) {
|
||||
const photographers = Object.keys(offlineImages);
|
||||
18
src/features/background/components/BackgroundImage.jsx
Normal file
18
src/features/background/components/BackgroundImage.jsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import { memo } from 'react';
|
||||
import PhotoInformation from './PhotoInformation';
|
||||
|
||||
/**
|
||||
* BackgroundImage component for rendering image backgrounds
|
||||
*/
|
||||
function BackgroundImage({ filterStyle, photoInfo, currentAPI, url }) {
|
||||
return (
|
||||
<>
|
||||
<div style={filterStyle} id="backgroundImage" />
|
||||
{photoInfo?.credit && (
|
||||
<PhotoInformation info={photoInfo} api={currentAPI} url={url} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(BackgroundImage);
|
||||
23
src/features/background/components/BackgroundVideo.jsx
Normal file
23
src/features/background/components/BackgroundVideo.jsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import { memo } from 'react';
|
||||
|
||||
/**
|
||||
* BackgroundVideo component for rendering video backgrounds
|
||||
*/
|
||||
function BackgroundVideo({ url, filterStyle }) {
|
||||
const isMuted = localStorage.getItem('backgroundVideoMute') === 'true';
|
||||
const shouldLoop = localStorage.getItem('backgroundVideoLoop') === 'true';
|
||||
|
||||
return (
|
||||
<video
|
||||
autoPlay
|
||||
muted={isMuted}
|
||||
loop={shouldLoop}
|
||||
style={filterStyle}
|
||||
id="backgroundVideo"
|
||||
>
|
||||
<source src={url} />
|
||||
</video>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(BackgroundVideo);
|
||||
56
src/features/background/hooks/useBackgroundEvents.js
Normal file
56
src/features/background/hooks/useBackgroundEvents.js
Normal file
@@ -0,0 +1,56 @@
|
||||
import { useEffect } from 'react';
|
||||
import EventBus from 'utils/eventbus';
|
||||
import { getBackgroundFilterStyle } from '../api/backgroundFilters';
|
||||
|
||||
/**
|
||||
* Hook for handling EventBus background events
|
||||
*/
|
||||
export function useBackgroundEvents(backgroundData, refreshBackground) {
|
||||
useEffect(() => {
|
||||
const handleEvent = (event) => {
|
||||
if (event === 'welcomeLanguage') {
|
||||
localStorage.setItem('welcomeImage', JSON.stringify(backgroundData));
|
||||
} else if (event === 'background') {
|
||||
handleVisibilityToggle();
|
||||
} else if (['marketplacebackgrounduninstall', 'backgroundwelcome', 'backgroundrefresh'].includes(event)) {
|
||||
refreshBackground();
|
||||
} else if (event === 'backgroundeffect') {
|
||||
applyFilters();
|
||||
}
|
||||
};
|
||||
|
||||
const handleVisibilityToggle = () => {
|
||||
const element = document.getElementById(backgroundData.video ? 'backgroundVideo' : 'backgroundImage');
|
||||
const photoInfo = document.querySelector('.photoInformation');
|
||||
const isEnabled = localStorage.getItem('background') !== 'false';
|
||||
|
||||
if (!isEnabled) {
|
||||
element?.style.setProperty('display', 'none');
|
||||
if (!backgroundData.photoInfo?.hidden) photoInfo?.style.setProperty('display', 'none');
|
||||
return;
|
||||
}
|
||||
|
||||
element?.style.setProperty('display', 'block');
|
||||
if (!backgroundData.photoInfo?.hidden) photoInfo?.style.setProperty('display', 'flex');
|
||||
|
||||
// Check if refresh needed
|
||||
const type = localStorage.getItem('backgroundType');
|
||||
const needsRefresh =
|
||||
(type !== backgroundData.type && !(backgroundData.photoInfo?.offline && type === backgroundData.type)) ||
|
||||
(backgroundData.type === 'api' && localStorage.getItem('backgroundAPI') !== backgroundData.currentAPI) ||
|
||||
(backgroundData.type === 'custom' && localStorage.getItem('customBackground') !== backgroundData.url) ||
|
||||
(backgroundData.photoInfo?.pun && JSON.parse(localStorage.getItem('backgroundExclude') || '[]').includes(backgroundData.photoInfo.pun));
|
||||
|
||||
if (needsRefresh) refreshBackground();
|
||||
};
|
||||
|
||||
const applyFilters = () => {
|
||||
const filter = getBackgroundFilterStyle();
|
||||
const element = document.getElementById(backgroundData.video ? 'backgroundVideo' : 'backgroundImage');
|
||||
if (element) element.style.webkitFilter = filter;
|
||||
};
|
||||
|
||||
EventBus.on('refresh', handleEvent);
|
||||
return () => EventBus.off('refresh', handleEvent);
|
||||
}, [backgroundData, refreshBackground]);
|
||||
}
|
||||
47
src/features/background/hooks/useBackgroundLoader.js
Normal file
47
src/features/background/hooks/useBackgroundLoader.js
Normal file
@@ -0,0 +1,47 @@
|
||||
import { useCallback, useEffect, useRef } from 'react';
|
||||
import { getBackgroundData } from '../api/backgroundLoader';
|
||||
|
||||
/**
|
||||
* Hook for loading and refreshing background data
|
||||
*/
|
||||
export function useBackgroundLoader(updateBackground, resetBackground) {
|
||||
const isLoadingRef = useRef(false);
|
||||
|
||||
const loadBackground = useCallback(async () => {
|
||||
if (isLoadingRef.current) return;
|
||||
isLoadingRef.current = true;
|
||||
|
||||
try {
|
||||
// Check for welcome tab first
|
||||
const welcomeImage = localStorage.getItem('welcomeImage');
|
||||
if (localStorage.getItem('welcomeTab') && welcomeImage) {
|
||||
updateBackground(JSON.parse(welcomeImage));
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await getBackgroundData();
|
||||
if (data) updateBackground(data);
|
||||
} catch (error) {
|
||||
console.error('Failed to load background:', error);
|
||||
} finally {
|
||||
isLoadingRef.current = false;
|
||||
}
|
||||
}, [updateBackground]);
|
||||
|
||||
const refreshBackground = useCallback(() => {
|
||||
resetBackground();
|
||||
loadBackground();
|
||||
}, [loadBackground, resetBackground]);
|
||||
|
||||
// Initial load - only run once on mount
|
||||
useEffect(() => {
|
||||
const changeMode = localStorage.getItem('backgroundchange');
|
||||
if (changeMode === 'refresh' || !changeMode) {
|
||||
localStorage.setItem('backgroundStartTime', Date.now());
|
||||
loadBackground();
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
return { loadBackground, refreshBackground };
|
||||
}
|
||||
72
src/features/background/hooks/useBackgroundRenderer.js
Normal file
72
src/features/background/hooks/useBackgroundRenderer.js
Normal file
@@ -0,0 +1,72 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { createBlobUrl } from '../api/blobUrl';
|
||||
import { generateBlurHashDataUrl } from '../api/blurHash';
|
||||
import { getBackgroundFilterStyle } from '../api/backgroundFilters';
|
||||
|
||||
/**
|
||||
* Hook for rendering backgrounds to the DOM
|
||||
*/
|
||||
export function useBackgroundRenderer(backgroundData) {
|
||||
const blobRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (backgroundData.video) return;
|
||||
|
||||
const element = document.getElementById('backgroundImage');
|
||||
if (!element) return;
|
||||
|
||||
// Cleanup previous blob
|
||||
if (blobRef.current) {
|
||||
URL.revokeObjectURL(blobRef.current);
|
||||
blobRef.current = null;
|
||||
}
|
||||
|
||||
const applyBackground = async () => {
|
||||
if (backgroundData.url) {
|
||||
const hasTransition = localStorage.getItem('bgtransition') !== 'false';
|
||||
|
||||
if (!hasTransition) {
|
||||
element.style.background = `url(${backgroundData.url})`;
|
||||
document.querySelector('.photoInformation')?.style.setProperty('display', 'flex');
|
||||
return;
|
||||
}
|
||||
|
||||
element.style.background = null;
|
||||
|
||||
// Apply blur hash placeholder
|
||||
if (backgroundData.photoInfo?.blur_hash && backgroundData.photoInfo?.colour) {
|
||||
element.style.backgroundColor = backgroundData.photoInfo.colour;
|
||||
element.classList.add('fade-in');
|
||||
|
||||
const blurHash = generateBlurHashDataUrl(backgroundData.photoInfo.blur_hash);
|
||||
if (blurHash) element.style.backgroundImage = `url(${blurHash})`;
|
||||
}
|
||||
|
||||
// Load full image
|
||||
const blobUrl = await createBlobUrl(backgroundData.url);
|
||||
if (blobUrl) {
|
||||
blobRef.current = blobUrl;
|
||||
element.classList.add('backgroundTransform');
|
||||
element.style.backgroundImage = `url(${blobUrl})`;
|
||||
} else {
|
||||
element.style.backgroundImage = `url(${backgroundData.url})`;
|
||||
}
|
||||
} else if (backgroundData.style) {
|
||||
element.setAttribute('style', backgroundData.style);
|
||||
}
|
||||
};
|
||||
|
||||
applyBackground();
|
||||
|
||||
return () => {
|
||||
if (blobRef.current) URL.revokeObjectURL(blobRef.current);
|
||||
};
|
||||
}, [backgroundData.url, backgroundData.style, backgroundData.video, backgroundData.photoInfo]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to get computed filter styles
|
||||
*/
|
||||
export function useBackgroundFilters() {
|
||||
return { WebkitFilter: getBackgroundFilterStyle() };
|
||||
}
|
||||
48
src/features/background/hooks/useBackgroundState.js
Normal file
48
src/features/background/hooks/useBackgroundState.js
Normal file
@@ -0,0 +1,48 @@
|
||||
import { useState, useCallback } from 'react';
|
||||
|
||||
/**
|
||||
* Custom hook for managing background state
|
||||
*/
|
||||
export function useBackgroundState() {
|
||||
const [backgroundData, setBackgroundData] = useState({
|
||||
url: '',
|
||||
style: '',
|
||||
type: '',
|
||||
currentAPI: '',
|
||||
video: false,
|
||||
photoInfo: {
|
||||
hidden: false,
|
||||
offline: false,
|
||||
photographerURL: '',
|
||||
photoURL: '',
|
||||
},
|
||||
});
|
||||
|
||||
const updateBackground = useCallback((newData) => {
|
||||
setBackgroundData((prev) => ({
|
||||
...prev,
|
||||
...newData,
|
||||
photoInfo: {
|
||||
...prev.photoInfo,
|
||||
...(newData.photoInfo || {}),
|
||||
},
|
||||
}));
|
||||
}, []);
|
||||
|
||||
const resetBackground = useCallback(() => {
|
||||
setBackgroundData({
|
||||
url: '',
|
||||
style: '',
|
||||
type: '',
|
||||
currentAPI: '',
|
||||
video: false,
|
||||
photoInfo: { hidden: true },
|
||||
});
|
||||
}, []);
|
||||
|
||||
return {
|
||||
backgroundData,
|
||||
updateBackground,
|
||||
resetBackground,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user