refactor: optimize background loading and rendering logic

This commit is contained in:
alexsparkes
2025-10-29 10:06:09 +00:00
parent 293cc93500
commit ac3924aaf7
6 changed files with 140 additions and 56 deletions

View File

@@ -1,5 +1,3 @@
import { useEffect } from 'react';
import BackgroundImage from './components/BackgroundImage';
import BackgroundVideo from './components/BackgroundVideo';

View File

@@ -115,24 +115,49 @@ function getColourBackground() {
}
/**
* Gets API background with caching
* Gets API background with caching and prefetching
*/
async function getAPIBackground(isOffline) {
if (isOffline) return getOfflineImage('api');
// Use cached next image if available
const cached = parseJSON('nextImage');
const data = cached || (await fetchAPIImageData());
const cachedQueue = parseJSON('imageQueue', []);
let data;
if (cachedQueue.length > 0) {
data = cachedQueue.shift();
localStorage.setItem('imageQueue', JSON.stringify(cachedQueue));
} else {
data = 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));
});
// Pre-fetch next 3 images in the background
const targetQueueSize = 3;
const currentQueueSize = cachedQueue.length;
if (currentQueueSize < targetQueueSize) {
const excludedPuns = [
data.photoInfo.pun,
...cachedQueue.map((img) => img.photoInfo?.pun).filter(Boolean),
];
// Prefetch remaining images asynchronously
Promise.all(
Array.from({ length: targetQueueSize - currentQueueSize }, (_, i) =>
fetchAPIImageData(excludedPuns[i] || data.photoInfo.pun),
),
).then((newImages) => {
const validImages = newImages.filter(Boolean);
if (validImages.length > 0) {
const updatedQueue = [...cachedQueue, ...validImages];
localStorage.setItem('imageQueue', JSON.stringify(updatedQueue));
}
});
}
return data;
}
@@ -141,7 +166,7 @@ async function getAPIBackground(isOffline) {
* Gets custom background
*/
function getCustomBackground(isOffline) {
let backgrounds = parseJSON('customBackground');
const backgrounds = parseJSON('customBackground');
if (backgrounds.length === 0) return null;

View File

@@ -5,11 +5,19 @@
*/
export async function createBlobUrl(url) {
try {
const response = await fetch(url);
const response = await fetch(url, {
mode: 'cors',
credentials: 'omit',
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const blob = await response.blob();
return URL.createObjectURL(blob);
} catch (error) {
console.error('Failed to create blob URL:', error);
} catch {
// Silently fail - we'll use direct URL as fallback
return null;
}
}

View File

@@ -13,14 +13,19 @@ export function useBackgroundLoader(updateBackground, resetBackground) {
try {
// Check for welcome tab first
const welcomeImage = localStorage.getItem('welcomeImage');
if (localStorage.getItem('welcomeTab') && welcomeImage) {
updateBackground(JSON.parse(welcomeImage));
return;
const welcomeTab = localStorage.getItem('welcomeTab');
if (welcomeTab) {
const welcomeImage = localStorage.getItem('welcomeImage');
if (welcomeImage) {
updateBackground(JSON.parse(welcomeImage));
return;
}
}
const data = await getBackgroundData();
if (data) updateBackground(data);
if (data) {
updateBackground(data);
}
} catch (error) {
console.error('Failed to load background:', error);
} finally {
@@ -36,12 +41,14 @@ export function useBackgroundLoader(updateBackground, resetBackground) {
// Initial load - only run once on mount
useEffect(() => {
const changeMode = localStorage.getItem('backgroundchange');
if (changeMode === 'refresh' || !changeMode) {
const hasStartTime = localStorage.getItem('backgroundStartTime');
if (!hasStartTime || changeMode === 'refresh') {
localStorage.setItem('backgroundStartTime', Date.now());
loadBackground();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
loadBackground();
}, [loadBackground]);
return { loadBackground, refreshBackground };
}

View File

@@ -15,12 +15,6 @@ export function useBackgroundRenderer(backgroundData) {
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';
@@ -31,26 +25,77 @@ export function useBackgroundRenderer(backgroundData) {
return;
}
element.style.background = null;
// Create or get overlay element for smooth transitions
let overlay = document.getElementById('backgroundOverlay');
if (!overlay) {
overlay = document.createElement('div');
overlay.id = 'backgroundOverlay';
// Insert right after the background element
element.parentNode.insertBefore(overlay, element.nextSibling);
}
// Apply blur hash placeholder
if (backgroundData.photoInfo?.blur_hash && backgroundData.photoInfo?.colour) {
// Set background color
if (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
// Generate and show blur hash immediately on main element
if (backgroundData.photoInfo?.blur_hash) {
const blurHashUrl = generateBlurHashDataUrl(backgroundData.photoInfo.blur_hash, 64, 64);
if (blurHashUrl) {
element.style.backgroundImage = `url(${blurHashUrl})`;
}
}
// Load the full image
const blobUrl = await createBlobUrl(backgroundData.url);
const finalUrl = blobUrl || backgroundData.url;
if (blobUrl) {
if (blobRef.current) {
URL.revokeObjectURL(blobRef.current);
}
blobRef.current = blobUrl;
element.classList.add('backgroundTransform');
element.style.backgroundImage = `url(${blobUrl})`;
} else {
element.style.backgroundImage = `url(${backgroundData.url})`;
}
// Preload the image
const img = new Image();
img.src = finalUrl;
await new Promise((resolve) => {
img.onload = resolve;
img.onerror = resolve;
});
// CRITICAL: Reset overlay to invisible FIRST
overlay.style.transition = 'none';
overlay.style.opacity = '0';
overlay.style.backgroundImage = '';
// Force a reflow to ensure opacity is actually 0
void overlay.offsetHeight;
// Now set the new image
overlay.style.backgroundImage = `url(${finalUrl})`;
// Force another reflow
void overlay.offsetHeight;
// Re-enable transition
overlay.style.transition = 'opacity 1.2s ease-in-out';
// Force another reflow before changing opacity
void overlay.offsetHeight;
// Now fade it in
overlay.style.opacity = '1';
// After fade completes, swap to main element and reset overlay
setTimeout(() => {
element.style.backgroundImage = `url(${finalUrl})`;
overlay.style.opacity = '0';
overlay.style.backgroundImage = '';
}, 1300);
} else if (backgroundData.style) {
element.setAttribute('style', backgroundData.style);
}

View File

@@ -8,22 +8,23 @@
background-position: center !important;
background-attachment: fixed !important;
zoom: 100% !important;
position: relative;
z-index: -2;
}
.backgroundTransform {
@include animation('fadein2 2s');
transition: background 2s;
}
@include keyframes(fadein2) {
from {
opacity: 0;
}
to {
opacity: 1;
}
#backgroundOverlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100vh;
background-size: cover !important;
background-repeat: no-repeat !important;
background-position: center !important;
background-attachment: fixed !important;
pointer-events: none;
z-index: -1;
transition: opacity 1.2s ease-in-out;
}
.backgroundPreload {
@@ -39,7 +40,7 @@
}
.fade-in {
@include animation('fadein 1s');
@include animation('fadein 0.8s ease-in-out');
}
@include keyframes(fadein) {