refactor(background): Converted to functional component + fixed background transition quirk

This commit is contained in:
alexsparkes
2024-11-26 21:17:33 +00:00
parent ac9eac2948
commit 89526d19b9
4 changed files with 388 additions and 243 deletions

View File

@@ -1,4 +1,4 @@
import React, { PureComponent } from 'react';
import React, { useState, useEffect, useCallback, useRef } from 'react';
import variables from 'config/variables';
import PhotoInformation from './components/PhotoInformation';
import EventBus from 'utils/eventbus';
@@ -13,85 +13,26 @@ import BackgroundImage from './components/BackgroundImage';
import BackgroundVideo from './components/BackgroundVideo';
import './scss/index.scss';
export default class Background extends PureComponent {
constructor() {
super();
this.state = {
blob: null,
style: '',
url: '',
currentAPI: '',
firstTime: false,
photoInfo: {
hidden: false,
offline: false,
photographerURL: '',
photoURL: '',
},
};
}
const Background = () => {
const [state, setState] = useState({
blob: null,
style: '',
url: '',
currentAPI: '',
firstTime: false,
photoInfo: {
hidden: false,
offline: false,
photographerURL: '',
photoURL: '',
},
type: '',
video: false,
});
async componentDidMount() {
const element = document.getElementById('backgroundImage');
EventBus.on('refresh', (data) => this.handleRefreshEvent(data));
EventBus.on('backgroundeffect', () => this.handleBackgroundEffectEvent());
if (localStorage.getItem('welcomeTab')) {
this.setState(JSON.parse(localStorage.getItem('welcomeImage')));
return;
}
this.getBackground();
}
const blobRef = useRef(null);
componentDidUpdate() {
if (this.state.video !== true) {
this.setBackground();
}
}
componentWillUnmount() {
EventBus.off('refresh');
EventBus.off('backgroundeffect');
}
async setBackground() {
if (this.blob) {
URL.revokeObjectURL(this.blob);
}
const backgroundImage = document.getElementById('backgroundImage');
if (this.state.url !== '') {
let url = this.state.url;
const photoInformation = document.querySelector('.photoInformation');
if (localStorage.getItem('bgtransition') === 'false') {
if (photoInformation) {
photoInformation.style.display = 'flex';
}
backgroundImage.style.background = `url(${url})`;
return;
}
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})`;
Stats.postEvent('feature', 'background-image', 'shown');
} else {
backgroundImage.setAttribute('style', this.state.style);
Stats.postEvent('background', 'colour', 'set');
}
}
async getAPIImageData(currentPun) {
const getAPIImageData = useCallback(async (currentPun) => {
let apiCategories;
try {
apiCategories = JSON.parse(localStorage.getItem('apiCategories'));
@@ -107,7 +48,8 @@ export default class Background extends PureComponent {
if (currentPun) {
backgroundExclude.push(currentPun);
}
let requestURL, data;
let requestURL;
switch (backgroundAPI) {
case 'unsplash':
case 'pexels':
@@ -121,58 +63,152 @@ export default class Background extends PureComponent {
requestURL = `${variables.constants.API_URL}/images/random?categories=${apiCategories || ''}&quality=${apiQuality}&excludes=${backgroundExclude}`;
break;
}
const accept = `application/json, ${supportsAVIF() ? 'image/avif' : 'image/webp'}`;
try {
data = await (await fetch(requestURL, { headers: { accept } })).json();
const response = await fetch(requestURL, { headers: { accept } });
const data = await response.json();
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,
},
};
} catch (e) {
this.setState(getOfflineImage('api'));
setState(getOfflineImage('api'));
Stats.postEvent('background', 'image', 'offline');
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,
},
};
}
}, []);
async getBackground() {
const setBackground = useCallback(async () => {
// Clean up previous blob URL
if (blobRef.current) {
URL.revokeObjectURL(blobRef.current);
blobRef.current = null;
}
const backgroundImage = document.getElementById('backgroundImage');
const blurhashOverlay = document.getElementById('blurhashOverlay');
const backgroundImageActual = document.getElementById('backgroundImageActual');
const photoInformation = document.querySelector('.photoInformation');
// Reset elements to default state
backgroundImageActual.style.opacity = 0;
blurhashOverlay.style.backgroundImage = '';
blurhashOverlay.style.backgroundColor = '';
// Handle different background types
try {
switch (state.type) {
case 'api':
// Handle API image with blurhash
if (state.url) {
if (localStorage.getItem('bgtransition') === 'false') {
if (photoInformation) {
photoInformation.style.display = 'flex';
}
backgroundImage.style.background = `url(${state.url})`;
return;
}
// Set blurhash overlay if available
if (state.photoInfo.blur_hash) {
blurhashOverlay.style.backgroundColor = state.photoInfo.colour;
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(state.photoInfo.blur_hash, 32, 32));
ctx.putImageData(imageData, 0, 0);
blurhashOverlay.style.backgroundImage = `url(${canvas.toDataURL()})`;
}
// Load actual image
const newBlob = URL.createObjectURL(await (await fetch(state.url)).blob());
blobRef.current = newBlob;
backgroundImageActual.src = newBlob;
}
break;
case 'colour':
case 'random_colour':
case 'random_gradient':
backgroundImage.style.background = state.style;
backgroundImageActual.src = '';
Stats.postEvent('background', 'colour', 'set');
break;
case 'custom':
case 'photo_pack':
if (state.url) {
backgroundImage.style.background = `url(${state.url})`;
if (photoInformation && !state.photoInfo.hidden) {
photoInformation.style.display = 'flex';
}
}
Stats.postEvent('background', state.type, 'set');
break;
default:
// Fallback to solid color
backgroundImage.style.background = state.style || 'rgb(0,0,0)';
Stats.postEvent('background', 'colour', 'set');
}
// Set up image load handler for all image types
if (backgroundImageActual.src) {
backgroundImageActual.onload = () => {
backgroundImageActual.style.opacity = 1;
blurhashOverlay.style.opacity = 0;
Stats.postEvent('feature', 'background-image', 'shown');
};
}
} catch (error) {
console.error('Error setting background:', error);
// Fallback to solid black background
backgroundImage.style.background = 'rgb(0,0,0)';
}
}, [state]);
const getBackground = useCallback(async () => {
let offline = localStorage.getItem('offlineMode') === 'true';
if (localStorage.getItem('showWelcome') !== 'false') {
offline = true;
}
const setFavourited = ({ type, url, credit, location, camera, pun, offline }) => {
if (type === 'random_colour' || type === 'random_gradient') {
this.setState({
setState({
type: 'colour',
style: `background:${url}`,
style: url,
});
} else {
this.setState({
setState({
url,
photoInfo: {
credit,
@@ -186,27 +222,29 @@ export default class Background extends PureComponent {
}
Stats.postEvent('background', 'favourite', 'set');
};
const favourited = JSON.parse(localStorage.getItem('favourite'));
if (favourited) {
setFavourited(favourited);
return;
}
const type = localStorage.getItem('backgroundType') || defaults.backgroundType;
switch (type) {
case 'api':
if (offline) {
this.setState(getOfflineImage('api'));
setState(getOfflineImage('api'));
Stats.postEvent('background', 'image', 'offline');
return;
}
let data = JSON.parse(localStorage.getItem('nextImage')) || (await this.getAPIImageData());
let data = JSON.parse(localStorage.getItem('nextImage')) || (await getAPIImageData());
localStorage.setItem('nextImage', null);
if (data) {
this.setState(data);
setState(data);
localStorage.setItem('currentBackground', JSON.stringify(data));
localStorage.setItem(
'nextImage',
JSON.stringify(await this.getAPIImageData(data.photoInfo.pun)),
JSON.stringify(await getAPIImageData(data.photoInfo.pun)),
);
Stats.postEvent('background', 'image', 'api');
}
@@ -222,15 +260,19 @@ export default class Background extends PureComponent {
customBackgroundColour = 'rgb(0,0,0)';
}
}
this.setState({
setState({
type: 'colour',
style: `background: ${customBackgroundColour || 'rgb(0,0,0)'}`,
style: customBackgroundColour || 'rgb(0,0,0)',
});
Stats.postEvent('background', 'colour', 'custom');
break;
case 'random_colour':
case 'random_gradient':
this.setState(randomColourStyleBuilder(type));
const randomStyle = randomColourStyleBuilder(type);
setState({
type: 'colour',
style: randomStyle,
});
Stats.postEvent('background', 'colour', 'random');
break;
case 'custom':
@@ -246,7 +288,7 @@ export default class Background extends PureComponent {
}
customBackground = customBackground[Math.floor(Math.random() * customBackground.length)];
if (offline && !customBackground.startsWith('data:')) {
this.setState(getOfflineImage('custom'));
setState(getOfflineImage('custom'));
Stats.postEvent('background', 'image', 'offline');
return;
}
@@ -263,14 +305,14 @@ export default class Background extends PureComponent {
hidden: true,
},
};
this.setState(object);
setState(object);
localStorage.setItem('currentBackground', JSON.stringify(object));
Stats.postEvent('background', 'image', 'custom');
}
break;
case 'photo_pack':
if (offline) {
this.setState(getOfflineImage('photo'));
setState(getOfflineImage('photo'));
Stats.postEvent('background', 'image', 'offline');
return;
}
@@ -283,12 +325,12 @@ export default class Background extends PureComponent {
}
});
if (photoPack.length === 0) {
this.setState(getOfflineImage('photo'));
setState(getOfflineImage('photo'));
Stats.postEvent('background', 'image', 'offline');
return;
}
const photo = photoPack[Math.floor(Math.random() * photoPack.length)];
this.setState({
setState({
url: photo.url.default,
type: 'photo_pack',
video: videoCheck(photo.url.default),
@@ -301,111 +343,132 @@ export default class Background extends PureComponent {
default:
break;
}
}
}, [getAPIImageData]);
handleRefreshEvent(data) {
const element = document.getElementById('backgroundImage');
const refresh = () => {
element.classList.remove('fade-in');
this.setState({
url: '',
style: '',
type: '',
video: false,
photoInfo: {
hidden: true,
},
});
this.getBackground();
};
if (data === 'welcomeLanguage') {
localStorage.setItem('welcomeImage', JSON.stringify(this.state));
}
if (data === 'background') {
if (localStorage.getItem('background') === 'false') {
if (this.state.photoInfo.hidden === false) {
document.querySelector('.photoInformation').style.display = 'none';
}
if (this.state.video === true) {
document.getElementById('backgroundVideo').style.display = 'none';
} else {
element.style.display = 'none';
}
return;
const handleRefreshEvent = useCallback(
(data) => {
const element = document.getElementById('backgroundImage');
const refresh = () => {
element.classList.remove('fade-in');
setState({
url: '',
style: '',
type: '',
video: false,
photoInfo: {
hidden: true,
},
});
getBackground();
};
if (data === 'welcomeLanguage') {
localStorage.setItem('welcomeImage', JSON.stringify(state));
}
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
if (data === 'background') {
if (localStorage.getItem('background') === 'false') {
if (state.photoInfo.hidden === false) {
document.querySelector('.photoInformation').style.display = 'none';
}
if (state.video === true) {
document.getElementById('backgroundVideo').style.display = 'none';
} else {
element.style.display = 'none';
}
return;
}
element.style.display = 'block';
}
const backgroundType = localStorage.getItem('backgroundType') || defaults.backgroundType;
if (this.state.photoInfo.offline !== true) {
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)
) {
if (state.video === true) {
document.getElementById('backgroundVideo').style.display = 'block';
} else {
if (state.photoInfo.hidden === false) {
try {
document.querySelector('.photoInformation').style.display = 'flex';
} catch (e) {
// Disregard exception
}
}
element.style.display = 'block';
}
const backgroundType = localStorage.getItem('backgroundType') || defaults.backgroundType;
if (state.photoInfo.offline !== true) {
if (
backgroundType !== state.type ||
(state.type === 'api' && localStorage.getItem('backgroundAPI') !== state.currentAPI) ||
(state.type === 'custom' && localStorage.getItem('customBackground') !== state.url) ||
JSON.parse(localStorage.getItem('backgroundExclude')).includes(state.photoInfo.pun)
) {
refresh();
return;
}
} else if (backgroundType !== state.type) {
refresh();
return;
}
} else if (backgroundType !== this.state.type) {
refresh();
return;
}
}
if (
data === 'marketplacebackgrounduninstall' ||
data === 'backgroundwelcome' ||
data === 'backgroundrefresh'
) {
refresh();
}
}
if (
data === 'marketplacebackgrounduninstall' ||
data === 'backgroundwelcome' ||
data === 'backgroundrefresh'
) {
refresh();
}
},
[state, getBackground],
);
handleBackgroundEffectEvent() {
const handleBackgroundEffectEvent = useCallback(() => {
const element = document.getElementById('backgroundImage');
const backgroundFilterSetting =
localStorage.getItem('backgroundFilter') || defaults.backgroundFilter;
const backgroundFilter = backgroundFilterSetting && backgroundFilterSetting !== 'none';
const filterValue = `blur(${localStorage.getItem('blur')}px) brightness(${localStorage.getItem(
'brightness',
)}%) ${
const filterValue = `blur(${localStorage.getItem('blur')}px) brightness(${localStorage.getItem('brightness')}%) ${
backgroundFilter
? backgroundFilterSetting + '(' + localStorage.getItem('backgroundFilterAmount') + '%)'
: ''
}`;
if (this.state.video === true) {
if (state.video === true) {
document.getElementById('backgroundVideo').style.filter = filterValue;
} else {
element.style.filter = filterValue;
}
}, [state.video]);
useEffect(() => {
if (localStorage.getItem('welcomeTab')) {
setState(JSON.parse(localStorage.getItem('welcomeImage')));
return;
}
getBackground();
}, [getBackground]);
useEffect(() => {
EventBus.on('refresh', handleRefreshEvent);
EventBus.on('backgroundeffect', handleBackgroundEffectEvent);
return () => {
EventBus.off('refresh', handleRefreshEvent);
EventBus.off('backgroundeffect', handleBackgroundEffectEvent);
};
}, [handleRefreshEvent, handleBackgroundEffectEvent]);
useEffect(() => {
if (state.video !== true) {
setBackground();
}
}, [state, setBackground]);
if (state.video === true) {
return <BackgroundVideo url={state.url} />;
}
render() {
if (this.state.video === true) {
return <BackgroundVideo url={this.state.url} />;
}
return (
<>
<BackgroundImage />
{this.state.photoInfo.credit !== '' && (
<PhotoInformation
info={this.state.photoInfo}
api={this.state.currentAPI}
url={this.state.url}
/>
)}
</>
);
}
}
return (
<>
<BackgroundImage />
{state.photoInfo && state.photoInfo.credit && (
<PhotoInformation info={state.photoInfo} api={state.currentAPI} url={state.url} />
)}
</>
);
};
export default Background;

View File

@@ -1,34 +1,89 @@
/**
* It returns a random colour or random gradient as a style object
* @param type - The type of the style. This is used to determine which style builder to use.
* @returns An object with two properties: type and style.
* Converts HSL values to Hex color code
* @param {number} h - Hue (0-360)
* @param {number} s - Saturation (0-100)
* @param {number} l - Lightness (0-100)
* @returns {string} Hex color code
*/
const hslToHex = (h, s, l) => {
l /= 100;
const a = (s * Math.min(l, 1 - l)) / 100;
const f = (n) => {
const k = (n + h / 30) % 12;
const color = l - a * Math.max(Math.min(k - 3, 9 - k, 1), -1);
return Math.round(255 * color)
.toString(16)
.padStart(2, '0');
};
return `#${f(0)}${f(8)}${f(4)}`;
};
/**
* Generates a random number within a range
* @param {number} min - Minimum value
* @param {number} max - Maximum value
* @returns {number}
*/
const randomRange = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min;
/**
* Aesthetic color palettes
*/
const PALETTES = {
nature: [
{ h: [85, 150], s: [40, 70], l: [35, 60] }, // Forest greens
{ h: [200, 240], s: [40, 70], l: [40, 65] }, // Ocean blues
{ h: [35, 45], s: [40, 80], l: [35, 60] }, // Earth tones
{ h: [270, 310], s: [30, 60], l: [40, 65] }, // Lavender
],
sunset: [
{ h: [0, 40], s: [60, 90], l: [45, 65] }, // Warm oranges
{ h: [280, 320], s: [30, 60], l: [40, 65] }, // Soft purples
{ h: [20, 40], s: [70, 90], l: [50, 70] }, // Golden hour
],
};
/**
* Generates an aesthetically pleasing random color
* @returns {string} Hex color code
*/
const generateRandomColor = () => {
const palette = PALETTES.nature.concat(PALETTES.sunset);
const colors = palette[Math.floor(Math.random() * palette.length)];
const h = randomRange(colors.h[0], colors.h[1]);
const s = randomRange(colors.s[0], colors.s[1]);
const l = randomRange(colors.l[0], colors.l[1]);
return hslToHex(h, s, l);
};
/**
* Generates a complementary color based on the input color
* @param {string} baseColor - Base color in hex
* @returns {string} Complementary color in hex
*/
const getComplementaryColor = (baseColor) => {
// Convert hex to HSL, shift hue by 180 degrees
const h = (parseInt(baseColor.slice(1), 16) + 180) % 360;
const s = randomRange(40, 70);
const l = randomRange(40, 65);
return hslToHex(h, s, l);
};
/**
* Generates random background style based on type
* @param {string} type - Either 'random_colour' or 'random_gradient'
* @returns {string} CSS background style string
*/
export function randomColourStyleBuilder(type) {
// randomColour based on https://stackoverflow.com/a/5092872
const randomColour = () =>
'#000000'.replace(/0/g, () => {
return (~~(Math.random() * 16)).toString(16);
});
let style = `background:${randomColour()};`;
if (type === 'random_gradient') {
const directions = [
'to right',
'to left',
'to bottom',
'to top',
'to bottom right',
'to bottom left',
'to top right',
'to top left',
];
style = `background:linear-gradient(${
directions[Math.floor(Math.random() * directions.length)]
}, ${randomColour()}, ${randomColour()});`;
const directions = ['to right', 'to bottom right', 'to bottom', 'to bottom left', 'to left'];
const direction = directions[Math.floor(Math.random() * directions.length)];
const color1 = generateRandomColor();
const color2 = getComplementaryColor(color1);
return `linear-gradient(${direction}, ${color1}, ${color2})`;
}
return {
type: 'colour',
style,
};
return generateRandomColor();
}

View File

@@ -16,7 +16,11 @@ const BackgroundImage = () => {
}`,
}}
id="backgroundImage"
/>
className="backgroundImage"
>
<div id="blurhashOverlay" className="blurhashOverlay" />
<img id="backgroundImageActual" className="backgroundImageActual" alt="background" />
</div>
);
};

View File

@@ -7,13 +7,13 @@
background-repeat: no-repeat !important;
background-position: center !important;
background-attachment: fixed !important;
zoom: 100% !important;
transform: scale(1) !important;
position: relative;
}
.backgroundTransform {
@include animation('fadein2 2s');
transition: background 2s;
transition: opacity 2s;
}
@include keyframes(fadein2) {
@@ -26,6 +26,29 @@
}
}
.blurhashOverlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-size: cover;
background-repeat: no-repeat;
background-position: center;
transition: opacity 2s;
}
.backgroundImageActual {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover;
opacity: 0;
transition: opacity 2s;
}
.backgroundPreload {
opacity: 0;
}