feat(background): implement IndexedDB for custom background storage and enhance background management features

This commit is contained in:
alexsparkes
2026-01-24 18:55:27 +00:00
parent d84b09801c
commit 76c9aa4df9
9 changed files with 552 additions and 106 deletions

View File

@@ -35,6 +35,29 @@ function MainModal({ modalClose, deepLinkData }) {
setProductView(null);
}, [currentTab]);
// Handle deep link updates (when modal opens via EventBus with new deep link)
useEffect(() => {
if (deepLinkData) {
// Update tab if different
if (deepLinkData.tab && deepLinkData.tab !== currentTab) {
setCurrentTab(deepLinkData.tab);
}
// Handle settings section navigation with subsection
if (deepLinkData.tab === TAB_TYPES.SETTINGS && deepLinkData.section) {
setNavigationTrigger({
type: 'settings-section',
data: deepLinkData.section,
timestamp: Date.now(),
});
// Set sub-section if present
if (deepLinkData.subSection) {
setCurrentSubSection(deepLinkData.subSection);
}
}
}
}, [deepLinkData]);
// Clear hash when modal closes
useEffect(() => {
return () => {

View File

@@ -10,16 +10,17 @@ const FileUpload = memo(({ id, type, accept, loadFunction }) => {
if (!fileInput) return;
const handleChange = (e) => {
const reader = new FileReader();
const file = e.target.files[0];
const files = Array.from(e.target.files);
if (type === 'settings') {
const reader = new FileReader();
const file = files[0];
reader.readAsText(file, 'UTF-8');
reader.onload = (e) => {
return loadFunction(e.target.result);
};
} else {
// background upload
// background upload - handle multiple files
const settings = {};
Object.keys(localStorage).forEach((key) => {
@@ -27,26 +28,30 @@ const FileUpload = memo(({ id, type, accept, loadFunction }) => {
});
const settingsSize = new TextEncoder().encode(JSON.stringify(settings)).length;
if (videoCheck(file.type) === true) {
if (settingsSize + file.size > 4850000) {
return toast(variables.getMessage('toasts.no_storage'));
// Process each file
files.forEach((file, index) => {
if (videoCheck(file.type) === true) {
if (settingsSize + file.size > 4850000) {
return toast(variables.getMessage('toasts.no_storage'));
}
return loadFunction(file, index);
}
return loadFunction(file);
}
compressAccurately(file, {
size: 450,
accuracy: 0.9,
}).then(async (res) => {
if (settingsSize + res.size > 4850000) {
return toast(variables.getMessage('toasts.no_storage'));
}
compressAccurately(file, {
size: 450,
accuracy: 0.9,
}).then(async (res) => {
if (settingsSize + res.size > 4850000) {
return toast(variables.getMessage('toasts.no_storage'));
}
loadFunction({
target: {
result: await filetoDataURL(res),
},
loadFunction({
target: {
result: await filetoDataURL(res),
},
}, index);
});
});
}
@@ -67,6 +72,7 @@ const FileUpload = memo(({ id, type, accept, loadFunction }) => {
type="file"
style={{ display: 'none' }}
accept={accept}
multiple={type !== 'settings'}
/>
);
});

View File

@@ -3,10 +3,16 @@ import { supportsAVIF } from './avifSupport';
import { getOfflineImage } from './offlineImage';
import { randomColourStyleBuilder } from './randomColour';
import videoCheck from './videoCheck';
import { getAllBackgrounds } from 'utils/customBackgroundDB';
const parseJSON = (key, fallback = null) => {
const item = localStorage.getItem(key);
if (item === null || item === 'null') {
return fallback;
}
try {
return JSON.parse(localStorage.getItem(key)) || fallback;
const parsed = JSON.parse(item);
return parsed !== null ? parsed : fallback;
} catch {
return fallback;
}
@@ -94,7 +100,7 @@ export async function getBackgroundData() {
return randomColourStyleBuilder(type);
case 'custom':
return getCustomBackground(isOffline);
return await getCustomBackground(isOffline);
case 'photo_pack':
return getPhotoPackBackground(isOffline);
@@ -165,13 +171,22 @@ async function getAPIBackground(isOffline) {
/**
* Gets custom background
*/
function getCustomBackground(isOffline) {
const backgrounds = parseJSON('customBackground');
async function getCustomBackground(isOffline) {
// Try to get from IndexedDB first
let backgrounds = await getAllBackgrounds();
if (backgrounds.length === 0) return null;
// Fallback to localStorage if IndexedDB is empty
if (!backgrounds || backgrounds.length === 0) {
backgrounds = parseJSON('customBackground', []);
}
if (!backgrounds || backgrounds.length === 0) return null;
const selected = backgrounds[Math.floor(Math.random() * backgrounds.length)];
// Check if selected is valid before using it
if (!selected) return null;
if (isOffline && !selected.startsWith('data:')) return getOfflineImage('custom');
const data = {
@@ -181,7 +196,19 @@ function getCustomBackground(isOffline) {
photoInfo: { hidden: true },
};
localStorage.setItem('currentBackground', JSON.stringify(data));
// Don't store full image data in localStorage to avoid quota errors
// Just store metadata
try {
localStorage.setItem('currentBackground', JSON.stringify({
type: 'custom',
video: data.video,
photoInfo: data.photoInfo,
}));
} catch (e) {
// Ignore quota errors for currentBackground
console.warn('Could not save currentBackground to localStorage:', e);
}
return data;
}

View File

@@ -1,13 +1,73 @@
import { memo } from 'react';
import PhotoInformation from './PhotoInformation';
import variables from 'config/variables';
import { updateHash } from 'utils/deepLinking';
import EventBus from 'utils/eventbus';
/**
* BackgroundImage component for rendering image backgrounds
*/
function BackgroundImage({ photoInfo, currentAPI, url }) {
const isCustomType = localStorage.getItem('backgroundType') === 'custom';
const customBackgrounds = (() => {
try {
const stored = localStorage.getItem('customBackground');
return stored && stored !== 'null' ? JSON.parse(stored) : [];
} catch {
return [];
}
})();
const hasNoCustomImages = isCustomType && (!customBackgrounds || customBackgrounds.length === 0);
const handleOpenSettings = () => {
updateHash('#settings/background/source');
EventBus.emit('modal', 'openMainModal');
};
return (
<>
<div id="backgroundImage" />
{hasNoCustomImages && (
<div style={{
position: 'absolute',
bottom: '20px',
left: '20px',
color: 'white',
background: 'rgba(0, 0, 0, 0.6)',
padding: '20px 30px',
borderRadius: '10px',
zIndex: 1,
}}>
<h2 style={{ margin: '0 0 10px 0', fontSize: '20px' }}>
{variables.getMessage('widgets.background.no_images_title') || 'No Custom Images'}
</h2>
<p style={{ margin: '0 0 15px 0', fontSize: '14px', opacity: 0.9 }}>
{variables.getMessage('widgets.background.no_images_description') || 'Please add custom images in the Background settings'}
</p>
<button
onClick={handleOpenSettings}
style={{
background: 'rgba(255, 255, 255, 0.2)',
border: '1px solid rgba(255, 255, 255, 0.3)',
color: 'white',
padding: '10px 20px',
borderRadius: '5px',
cursor: 'pointer',
fontSize: '14px',
fontWeight: '500',
transition: 'all 0.2s',
}}
onMouseOver={(e) => {
e.target.style.background = 'rgba(255, 255, 255, 0.3)';
}}
onMouseOut={(e) => {
e.target.style.background = 'rgba(255, 255, 255, 0.2)';
}}
>
{variables.getMessage('widgets.background.add_images_button') || 'Add Images'}
</button>
</div>
)}
{photoInfo?.credit && (
<PhotoInformation info={photoInfo} api={currentAPI} url={url} />
)}

View File

@@ -38,7 +38,7 @@ export function useBackgroundEvents(backgroundData, refreshBackground) {
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) ||
(type === 'custom' && backgroundData.type !== 'custom') ||
(backgroundData.photoInfo?.pun && JSON.parse(localStorage.getItem('backgroundExclude') || '[]').includes(backgroundData.photoInfo.pun));
if (needsRefresh) refreshBackground();

View File

@@ -1,6 +1,7 @@
import variables from 'config/variables';
import { memo, useState, useEffect, useCallback, useRef } from 'react';
import { MdSource, MdOutlineAutoAwesome } from 'react-icons/md';
import EventBus from 'utils/eventbus';
import { Header } from 'components/Layout/Settings';
import { Dropdown } from 'components/Form/Settings';
@@ -191,6 +192,10 @@ const BackgroundOptions = memo(({ currentSubSection, onSubSectionChange, section
// Clear prefetch queue when changing background type
localStorage.removeItem('imageQueue');
setBackgroundType(value);
// Automatically refresh background when switching to custom images
if (value === 'custom') {
EventBus.emit('refresh', 'background');
}
}}
category="background"
items={getBackgroundOptionItems(marketplaceEnabled)}

View File

@@ -12,6 +12,14 @@ import {
import EventBus from 'utils/eventbus';
import { compressAccurately, filetoDataURL } from 'image-conversion';
import videoCheck from '../api/videoCheck';
import {
getAllBackgrounds,
addBackground,
updateBackground,
deleteBackground,
clearAllBackgrounds,
migrateFromLocalStorage,
} from 'utils/customBackgroundDB';
import { Checkbox, FileUpload } from 'components/Form/Settings';
import { Tooltip, Button } from 'components/Elements';
@@ -19,56 +27,118 @@ import Modal from 'react-modal';
import CustomURLModal from './CustomURLModal';
const getCustom = () => {
let data;
try {
data = JSON.parse(localStorage.getItem('customBackground'));
} catch (e) {
data = [localStorage.getItem('customBackground')];
}
return data;
};
const CustomSettings = memo(() => {
const [customBackground, setCustomBackground] = useState(getCustom());
const [customBackground, setCustomBackground] = useState([]);
const [customURLModal, setCustomURLModal] = useState(false);
const [urlError, setUrlError] = useState('');
const [currentBackgroundIndex, setCurrentBackgroundIndex] = useState(0);
const [isLoading, setIsLoading] = useState(true);
const customDnd = useRef(null);
const resetCustom = useCallback(() => {
localStorage.setItem('customBackground', '[]');
setCustomBackground([]);
toast(variables.getMessage('toasts.reset'));
EventBus.emit('refresh', 'background');
}, []);
// Load backgrounds from IndexedDB on mount
useEffect(() => {
const loadBackgrounds = async () => {
try {
// Try migration first
await migrateFromLocalStorage();
const handleCustomBackground = useCallback((e, index) => {
const result = e.target.result;
setCustomBackground((prev) => {
const updated = [...prev];
updated[index || updated.length] = result;
localStorage.setItem('customBackground', JSON.stringify(updated));
document.querySelector('.reminder-info').style.display = 'flex';
localStorage.setItem('showReminder', true);
return updated;
});
}, []);
const modifyCustomBackground = useCallback((type, index) => {
setCustomBackground((prev) => {
const updated = [...prev];
if (type === 'add') {
updated.push('');
} else {
updated.splice(index, 1);
// Load from IndexedDB
const backgrounds = await getAllBackgrounds();
setCustomBackground(backgrounds);
} catch (error) {
console.error('Error loading backgrounds:', error);
toast(variables.getMessage('toasts.error'));
} finally {
setIsLoading(false);
}
};
loadBackgrounds();
}, []);
const resetCustom = useCallback(async () => {
try {
await clearAllBackgrounds();
setCustomBackground([]);
// Keep localStorage in sync
localStorage.setItem('customBackground', '[]');
toast(variables.getMessage('toasts.reset'));
EventBus.emit('refresh', 'background');
} catch (error) {
console.error('Error resetting backgrounds:', error);
toast(variables.getMessage('toasts.error'));
}
}, []);
const handleCustomBackground = useCallback(
async (e, index) => {
const result = e.target.result;
try {
// Update or add to IndexedDB
if (index < customBackground.length) {
await updateBackground(index, result);
} else {
await addBackground(result);
}
// Reload from IndexedDB to get the latest state
const backgrounds = await getAllBackgrounds();
setCustomBackground(backgrounds);
// Store count in localStorage for backward compatibility
try {
localStorage.setItem('customBackground', JSON.stringify(backgrounds));
} catch (quotaError) {
// If quota exceeded, just store the count
console.warn('localStorage quota exceeded, storing count only');
localStorage.setItem('customBackgroundCount', backgrounds.length.toString());
}
const reminderInfo = document.querySelector('.reminder-info');
if (reminderInfo) {
reminderInfo.style.display = 'flex';
}
localStorage.setItem('showReminder', true);
EventBus.emit('refresh', 'background');
} catch (error) {
console.error('Error saving background:', error);
toast(variables.getMessage('toasts.error'));
}
},
[customBackground.length],
);
const modifyCustomBackground = useCallback(async (type, index) => {
try {
if (type === 'add') {
await addBackground('');
} else {
await deleteBackground(index);
}
// Reload from IndexedDB to get the latest state
const backgrounds = await getAllBackgrounds();
setCustomBackground(backgrounds);
// Store in localStorage with quota handling
try {
localStorage.setItem('customBackground', JSON.stringify(backgrounds));
} catch (quotaError) {
console.warn('localStorage quota exceeded, storing count only');
localStorage.setItem('customBackgroundCount', backgrounds.length.toString());
}
const reminderInfo = document.querySelector('.reminder-info');
if (reminderInfo) {
reminderInfo.style.display = 'flex';
}
localStorage.setItem('customBackground', JSON.stringify(updated));
document.querySelector('.reminder-info').style.display = 'flex';
localStorage.setItem('showReminder', true);
return updated;
});
EventBus.emit('refresh', 'background');
} catch (error) {
console.error('Error modifying background:', error);
toast(variables.getMessage('toasts.error'));
}
}, []);
const uploadCustomBackground = useCallback(() => {
@@ -78,7 +148,7 @@ const CustomSettings = memo(() => {
setCurrentBackgroundIndex(newIndex);
}, [customBackground.length]);
const addCustomURL = useCallback((e) => {
const addCustomURL = useCallback(async (e) => {
// regex: https://ihateregex.io/expr/url/
const urlRegex =
/https?:\/\/(www\.)?[-a-zA-Z0-9@:%._~#=]{1,256}\.[a-zA-Z0-9()]{1,63}\b([-a-zA-Z0-9()!@:%_.~#?&=]*)/;
@@ -89,7 +159,7 @@ const CustomSettings = memo(() => {
const newIndex = customBackground.length;
setCustomURLModal(false);
setCurrentBackgroundIndex(newIndex);
handleCustomBackground({ target: { result: e } }, newIndex);
await handleCustomBackground({ target: { result: e } }, newIndex);
}, [customBackground.length, handleCustomBackground]);
useEffect(() => {
@@ -102,7 +172,7 @@ const CustomSettings = memo(() => {
const handleDrop = (e) => {
e.preventDefault();
const file = e.dataTransfer.files[0];
const files = Array.from(e.dataTransfer.files);
const settings = {};
Object.keys(localStorage).forEach((key) => {
@@ -110,36 +180,41 @@ const CustomSettings = memo(() => {
});
const settingsSize = new TextEncoder().encode(JSON.stringify(settings)).length;
if (videoCheck(file.type) === true) {
if (settingsSize + file.size > 4850000) {
return toast(variables.getMessage('toasts.no_storage'));
// Process each dropped file
files.forEach((file, index) => {
const fileIndex = customBackground.length + index;
if (videoCheck(file.type) === true) {
if (settingsSize + file.size > 4850000) {
return toast(variables.getMessage('toasts.no_storage'));
}
const reader = new FileReader();
reader.onloadend = () => {
handleCustomBackground({ target: { result: reader.result } }, fileIndex);
};
reader.readAsDataURL(file);
return;
}
const reader = new FileReader();
reader.onloadend = () => {
handleCustomBackground({ target: { result: reader.result } }, currentBackgroundIndex);
};
reader.readAsDataURL(file);
return;
}
compressAccurately(file, {
size: 450,
accuracy: 0.9,
}).then(async (res) => {
if (settingsSize + res.size > 4850000) {
return toast(variables.getMessage('toasts.no_storage'));
}
compressAccurately(file, {
size: 450,
accuracy: 0.9,
}).then(async (res) => {
if (settingsSize + res.size > 4850000) {
return toast(variables.getMessage('toasts.no_storage'));
}
handleCustomBackground(
{
target: {
result: await filetoDataURL(res),
handleCustomBackground(
{
target: {
result: await filetoDataURL(res),
},
},
},
currentBackgroundIndex,
);
fileIndex,
);
});
});
};
@@ -156,7 +231,15 @@ const CustomSettings = memo(() => {
};
}, [currentBackgroundIndex, handleCustomBackground]);
const hasVideo = customBackground.filter((bg) => videoCheck(bg)).length > 0;
const hasVideo = customBackground.filter((bg) => bg && videoCheck(bg)).length > 0;
if (isLoading) {
return (
<div style={{ padding: '20px', textAlign: 'center' }}>
<span>{variables.getMessage('modals.main.loading')}</span>
</div>
);
}
return (
<>
@@ -201,11 +284,14 @@ const CustomSettings = memo(() => {
<div className="images-row">
{customBackground.map((url, index) => (
<div key={index}>
<img
alt={'Custom background ' + (index || 0)}
src={`${!videoCheck(url) ? customBackground[index] : ''}`}
/>
{videoCheck(url) && <MdPersonalVideo className="customvideoicon" />}
{url && !videoCheck(url) ? (
<img
alt={'Custom background ' + (index || 0)}
src={customBackground[index]}
/>
) : url && videoCheck(url) ? (
<MdPersonalVideo className="customvideoicon" />
) : null}
{customBackground.length > 0 && (
<Tooltip
title={variables.getMessage(
@@ -255,7 +341,10 @@ const CustomSettings = memo(() => {
<FileUpload
id="bg-input"
accept="image/jpeg, image/png, image/webp, image/webm, image/gif, video/mp4, video/webm, video/ogg"
loadFunction={(e) => handleCustomBackground(e, currentBackgroundIndex)}
loadFunction={(e, fileIndex) => {
const index = currentBackgroundIndex + fileIndex;
handleCustomBackground(e, index);
}}
/>
{hasVideo && (
<>

View File

@@ -54,8 +54,26 @@ const Modals = () => {
}
}
// hide refresh reminder once the user has refreshed the page
localStorage.setItem('showReminder', false);
// Only hide refresh reminder if user navigated naturally (not via deep link or forced intro skip)
// This ensures the reminder shows after user refreshes when they've made changes
if (!shouldAutoOpenModal() && window.location.search !== '?nointro=true') {
localStorage.setItem('showReminder', false);
}
// Listen for EventBus modal open requests
const handleModalOpen = (data) => {
if (data === 'openMainModal') {
const linkData = parseDeepLink();
setDeepLinkData(linkData);
setMainModal(true);
}
};
EventBus.on('modal', handleModalOpen);
return () => {
EventBus.off('modal', handleModalOpen);
};
}, []);
const closeWelcome = () => {

View File

@@ -0,0 +1,218 @@
/**
* IndexedDB utility for storing custom background images
* Uses IndexedDB to overcome localStorage quota limitations
*/
const DB_NAME = 'MueCustomBackgrounds';
const DB_VERSION = 1;
const STORE_NAME = 'backgrounds';
/**
* Open or create the IndexedDB database
* @returns {Promise<IDBDatabase>}
*/
function openDB() {
return new Promise((resolve, reject) => {
const request = indexedDB.open(DB_NAME, DB_VERSION);
request.onerror = () => reject(request.error);
request.onsuccess = () => resolve(request.result);
request.onupgradeneeded = (event) => {
const db = event.target.result;
// Create object store if it doesn't exist
if (!db.objectStoreNames.contains(STORE_NAME)) {
const objectStore = db.createObjectStore(STORE_NAME, { keyPath: 'id', autoIncrement: true });
objectStore.createIndex('url', 'url', { unique: false });
}
};
});
}
/**
* Get all custom backgrounds
* @returns {Promise<Array<string>>} Array of background URLs
*/
export async function getAllBackgrounds() {
try {
const db = await openDB();
const transaction = db.transaction(STORE_NAME, 'readonly');
const store = transaction.objectStore(STORE_NAME);
const request = store.getAll();
return new Promise((resolve, reject) => {
request.onsuccess = () => {
const results = request.result;
// Return array of URLs in order
resolve(results.map(item => item.url));
};
request.onerror = () => reject(request.error);
});
} catch (error) {
console.error('Error getting backgrounds from IndexedDB:', error);
return [];
}
}
/**
* Add a new background
* @param {string} url - The background URL (data URL or remote URL)
* @returns {Promise<number>} The ID of the added background
*/
export async function addBackground(url) {
const db = await openDB();
const transaction = db.transaction(STORE_NAME, 'readwrite');
const store = transaction.objectStore(STORE_NAME);
const request = store.add({ url, createdAt: Date.now() });
return new Promise((resolve, reject) => {
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
/**
* Update a background at a specific index
* @param {number} index - The index to update (0-based)
* @param {string} url - The new URL
* @returns {Promise<void>}
*/
export async function updateBackground(index, url) {
const db = await openDB();
const transaction = db.transaction(STORE_NAME, 'readwrite');
const store = transaction.objectStore(STORE_NAME);
// Get all items to find the one at the specified index
const getAllRequest = store.getAll();
return new Promise((resolve, reject) => {
getAllRequest.onsuccess = () => {
const items = getAllRequest.result;
if (items[index]) {
const item = items[index];
item.url = url;
item.updatedAt = Date.now();
const updateRequest = store.put(item);
updateRequest.onsuccess = () => resolve();
updateRequest.onerror = () => reject(updateRequest.error);
} else {
// If index doesn't exist, add it
addBackground(url).then(resolve).catch(reject);
}
};
getAllRequest.onerror = () => reject(getAllRequest.error);
});
}
/**
* Delete a background at a specific index
* @param {number} index - The index to delete (0-based)
* @returns {Promise<void>}
*/
export async function deleteBackground(index) {
const db = await openDB();
const transaction = db.transaction(STORE_NAME, 'readwrite');
const store = transaction.objectStore(STORE_NAME);
// Get all items to find the one at the specified index
const getAllRequest = store.getAll();
return new Promise((resolve, reject) => {
getAllRequest.onsuccess = () => {
const items = getAllRequest.result;
if (items[index]) {
const deleteRequest = store.delete(items[index].id);
deleteRequest.onsuccess = () => resolve();
deleteRequest.onerror = () => reject(deleteRequest.error);
} else {
resolve(); // Index doesn't exist, nothing to delete
}
};
getAllRequest.onerror = () => reject(getAllRequest.error);
});
}
/**
* Clear all custom backgrounds
* @returns {Promise<void>}
*/
export async function clearAllBackgrounds() {
const db = await openDB();
const transaction = db.transaction(STORE_NAME, 'readwrite');
const store = transaction.objectStore(STORE_NAME);
const request = store.clear();
return new Promise((resolve, reject) => {
request.onsuccess = () => resolve();
request.onerror = () => reject(request.error);
});
}
/**
* Get count of stored backgrounds
* @returns {Promise<number>}
*/
export async function getBackgroundCount() {
const db = await openDB();
const transaction = db.transaction(STORE_NAME, 'readonly');
const store = transaction.objectStore(STORE_NAME);
const request = store.count();
return new Promise((resolve, reject) => {
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
/**
* Migrate backgrounds from localStorage to IndexedDB
* @returns {Promise<boolean>} True if migration occurred
*/
export async function migrateFromLocalStorage() {
try {
const stored = localStorage.getItem('customBackground');
if (!stored || stored === 'null' || stored === '[]') {
return false;
}
let backgrounds = [];
try {
backgrounds = JSON.parse(stored);
if (!Array.isArray(backgrounds)) {
backgrounds = [stored];
}
} catch (e) {
backgrounds = [stored];
}
// Filter out null/empty values
backgrounds = backgrounds.filter(bg => bg && bg.trim() !== '');
if (backgrounds.length === 0) {
return false;
}
// Check if we already have data in IndexedDB
const count = await getBackgroundCount();
if (count > 0) {
return false; // Already migrated
}
// Migrate each background
for (const bg of backgrounds) {
await addBackground(bg);
}
console.log(`Migrated ${backgrounds.length} backgrounds from localStorage to IndexedDB`);
// Keep localStorage as backup for now, but mark as migrated
localStorage.setItem('customBackgroundMigrated', 'true');
return true;
} catch (error) {
console.error('Error migrating backgrounds:', error);
return false;
}
}