mirror of
https://github.com/mue/mue.git
synced 2026-07-15 04:53:48 +02:00
feat(background): implement IndexedDB for custom background storage and enhance background management features
This commit is contained in:
@@ -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 () => {
|
||||
|
||||
@@ -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'}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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} />
|
||||
)}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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 && (
|
||||
<>
|
||||
|
||||
@@ -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 = () => {
|
||||
|
||||
218
src/utils/customBackgroundDB.js
Normal file
218
src/utils/customBackgroundDB.js
Normal 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user