mirror of
https://github.com/mue/mue.git
synced 2026-07-06 08:01:27 +02:00
refactor(background): use indexeddb for custom images
This commit is contained in:
@@ -45,6 +45,16 @@
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.images-row.fixed-width {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.image-container {
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
.images-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
handleBackgroundVisibility,
|
||||
handleBackgroundEffectEvent,
|
||||
} from './api/backgroundHelpers';
|
||||
import { getCustomImages } from 'utils/indexedDB';
|
||||
|
||||
const initialState = {
|
||||
blob: null,
|
||||
@@ -79,6 +80,18 @@ const Background = () => {
|
||||
}
|
||||
|
||||
const type = localStorage.getItem('backgroundType') || defaults.backgroundType;
|
||||
if (type === 'custom') {
|
||||
const customImages = await getCustomImages();
|
||||
if (customImages.length > 0) {
|
||||
const randomImage = customImages[Math.floor(Math.random() * customImages.length)];
|
||||
setBackgroundState({
|
||||
url: randomImage.url,
|
||||
type: 'custom',
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
await handleBackgroundType(type, offline, getAPIImageData, setBackgroundState);
|
||||
}, [getAPIImageData]);
|
||||
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import variables from 'config/variables';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { MdSource, MdOutlineKeyboardArrowRight, MdOutlineAutoAwesome } from 'react-icons/md';
|
||||
import { MdSource, MdOutlineAutoAwesome } from 'react-icons/md';
|
||||
|
||||
import { Header, PreferencesWrapper, Section } from 'components/Layout/Settings';
|
||||
import { Checkbox, Dropdown, Slider, Radio, Text, ChipSelect } from 'components/Form/Settings';
|
||||
import { Row, Content, Action } from 'components/Layout/Settings/Item';
|
||||
//import Text from 'components/Form/Settings/Text/Text';
|
||||
|
||||
import ColourSettings from './Colour';
|
||||
import CustomSettings from './Custom';
|
||||
@@ -35,6 +33,7 @@ function BackgroundOptions() {
|
||||
const [marketplaceEnabled, setMarketplaceEnabled] = useState(localStorage.getItem('photo_packs'));
|
||||
const [effects, setEffects] = useState(false);
|
||||
const [backgroundSettingsSection, setBackgroundSettingsSection] = useState(false);
|
||||
const [blurValue, setBlurValue] = useState(localStorage.getItem('blur') || 0);
|
||||
|
||||
const controller = new AbortController();
|
||||
useEffect(() => {
|
||||
@@ -43,6 +42,13 @@ function BackgroundOptions() {
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const backgroundImage = document.getElementById('backgroundImage');
|
||||
if (backgroundImage) {
|
||||
backgroundImage.style.filter = `blur(${blurValue}px)`;
|
||||
}
|
||||
}, [blurValue]);
|
||||
|
||||
async function getBackgroundCategories() {
|
||||
const data = await (
|
||||
await fetch(variables.constants.API_URL + '/images/categories', {
|
||||
@@ -189,6 +195,7 @@ function BackgroundOptions() {
|
||||
marks={values.background}
|
||||
category="backgroundeffect"
|
||||
element="#backgroundImage"
|
||||
onChange={(value) => setBlurValue(value)}
|
||||
/>
|
||||
<Slider
|
||||
title={variables.getMessage('settings:sections.background.effects.brightness')}
|
||||
@@ -299,7 +306,7 @@ function BackgroundOptions() {
|
||||
<Dropdown
|
||||
label={variables.getMessage('settings:sections.background.type.title')}
|
||||
name="backgroundType"
|
||||
onChange={(value) => this.setState({ backgroundType: value })}
|
||||
onChange={(value) => setBackgroundType(value)}
|
||||
category="background"
|
||||
items={getBackgroundOptionItems(marketplaceEnabled)}
|
||||
/>
|
||||
@@ -363,7 +370,6 @@ function BackgroundOptions() {
|
||||
/>
|
||||
</Action>
|
||||
</Row>
|
||||
{/* todo: ideally refactor all of this file, but we need interval to appear on marketplace too */}
|
||||
{backgroundSettings}
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -5,19 +5,15 @@ import {
|
||||
MdCancel,
|
||||
MdAddLink,
|
||||
MdAddPhotoAlternate,
|
||||
MdPersonalVideo,
|
||||
MdOutlineFileUpload,
|
||||
MdFolder,
|
||||
} from 'react-icons/md';
|
||||
import EventBus from 'utils/eventbus';
|
||||
import videoCheck from '../api/videoCheck';
|
||||
import { addCustomImage, getCustomImages, deleteCustomImage } from 'utils/indexedDB';
|
||||
|
||||
import { Checkbox, FileUpload } from 'components/Form/Settings';
|
||||
import { Tooltip, Button } from 'components/Elements';
|
||||
import Modal from 'react-modal';
|
||||
|
||||
import CustomURLModal from './CustomURLModal';
|
||||
import defaults from './default';
|
||||
|
||||
export default class CustomSettings extends PureComponent {
|
||||
getMessage = (text, obj) => variables.getMessage(text, obj || {});
|
||||
@@ -25,158 +21,80 @@ export default class CustomSettings extends PureComponent {
|
||||
constructor() {
|
||||
super();
|
||||
this.state = {
|
||||
customBackground: this.getCustom(),
|
||||
customBackground: [],
|
||||
customURLModal: false,
|
||||
urlError: '',
|
||||
isDragging: false,
|
||||
};
|
||||
this.customDnd = createRef(null);
|
||||
}
|
||||
|
||||
resetCustom = () => {
|
||||
localStorage.setItem('customBackground', '[]');
|
||||
this.setState({
|
||||
customBackground: [],
|
||||
});
|
||||
toast(variables.getMessage('toasts.reset'));
|
||||
EventBus.emit('refresh', 'background');
|
||||
};
|
||||
|
||||
customBackground(e, index) {
|
||||
const result = e.target.result;
|
||||
|
||||
const customBackground = this.state.customBackground;
|
||||
customBackground[index || customBackground.length] = result;
|
||||
|
||||
this.setState({
|
||||
customBackground,
|
||||
});
|
||||
|
||||
this.forceUpdate();
|
||||
|
||||
localStorage.setItem('customBackground', JSON.stringify(customBackground));
|
||||
document.querySelector('.reminder-info').style.display = 'flex';
|
||||
localStorage.setItem('showReminder', true);
|
||||
}
|
||||
|
||||
modifyCustomBackground(type, index) {
|
||||
const customBackground = this.state.customBackground;
|
||||
if (type === 'add') {
|
||||
customBackground.push('');
|
||||
} else {
|
||||
customBackground.splice(index, 1);
|
||||
}
|
||||
|
||||
this.setState({
|
||||
customBackground,
|
||||
});
|
||||
this.forceUpdate();
|
||||
|
||||
localStorage.setItem('customBackground', JSON.stringify(customBackground));
|
||||
document.querySelector('.reminder-info').style.display = 'flex';
|
||||
localStorage.setItem('showReminder', true);
|
||||
}
|
||||
|
||||
videoCustomSettings = () => {
|
||||
const hasVideo = this.state.customBackground.filter((bg) => videoCheck(bg));
|
||||
|
||||
if (hasVideo.length > 0) {
|
||||
return (
|
||||
<>
|
||||
<Checkbox
|
||||
name="backgroundVideoLoop"
|
||||
text={variables.getMessage('settings:sections.background.source.loop_video')}
|
||||
/>
|
||||
<Checkbox
|
||||
name="backgroundVideoMute"
|
||||
text={variables.getMessage('settings:sections.background.source.mute_video')}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
getCustom() {
|
||||
let data;
|
||||
try {
|
||||
data = JSON.parse(localStorage.getItem('customBackground'));
|
||||
} catch (e) {
|
||||
const custom = localStorage.getItem('customBackground');
|
||||
data = custom ? [custom] : defaults.customBackground;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
uploadCustomBackground() {
|
||||
document.getElementById('bg-input').setAttribute('index', this.state.customBackground.length);
|
||||
document.getElementById('bg-input').click();
|
||||
// to fix loadFunction
|
||||
this.setState({
|
||||
currentBackgroundIndex: this.state.customBackground.length,
|
||||
});
|
||||
}
|
||||
|
||||
addCustomURL(e) {
|
||||
// regex: https://ihateregex.io/expr/url/
|
||||
// eslint-disable-next-line no-useless-escape
|
||||
const urlRegex =
|
||||
/https?:\/\/(www\.)?[-a-zA-Z0-9@:%._~#=]{1,256}\.[a-zA-Z0-9()]{1,63}\b([-a-zA-Z0-9()!@:%_.~#?&=]*)/;
|
||||
if (urlRegex.test(e) === false) {
|
||||
return this.setState({
|
||||
urlError: variables.getMessage('widgets.quicklinks.url_error'),
|
||||
});
|
||||
}
|
||||
|
||||
this.setState({
|
||||
customURLModal: false,
|
||||
currentBackgroundIndex: this.state.customBackground.length,
|
||||
});
|
||||
this.customBackground({ target: { value: e } }, true, this.state.customBackground.length);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.loadCustomImages();
|
||||
|
||||
const dnd = this.customDnd.current;
|
||||
dnd.ondragover = dnd.ondragenter = (e) => {
|
||||
e.preventDefault();
|
||||
this.setState({ isDragging: true });
|
||||
};
|
||||
|
||||
dnd.ondragleave = (e) => {
|
||||
e.preventDefault();
|
||||
this.setState({ isDragging: false });
|
||||
};
|
||||
|
||||
// todo: make this get from FileUpload.jsx to prevent duplication
|
||||
dnd.ondrop = (e) => {
|
||||
e.preventDefault();
|
||||
const file = e.dataTransfer.files[0];
|
||||
const settings = {};
|
||||
|
||||
Object.keys(localStorage).forEach((key) => {
|
||||
settings[key] = localStorage.getItem(key);
|
||||
});
|
||||
|
||||
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'));
|
||||
}
|
||||
|
||||
return this.customBackground(file, this.state.currentBackgroundIndex);
|
||||
}
|
||||
|
||||
this.customBackground(
|
||||
{
|
||||
target: {
|
||||
result: file,
|
||||
},
|
||||
},
|
||||
this.state.currentBackgroundIndex,
|
||||
);
|
||||
e.preventDefault();
|
||||
this.setState({ isDragging: false });
|
||||
const files = Array.from(e.dataTransfer.files);
|
||||
this.handleFileUpload(files);
|
||||
};
|
||||
}
|
||||
|
||||
loadCustomImages = () => {
|
||||
getCustomImages().then((images) => {
|
||||
this.setState({ customBackground: images });
|
||||
});
|
||||
};
|
||||
|
||||
addCustomImage = (image) => {
|
||||
addCustomImage({ url: image }).then(() => {
|
||||
this.loadCustomImages();
|
||||
toast(variables.getMessage('toasts.upload_success'));
|
||||
});
|
||||
};
|
||||
|
||||
removeCustomImage = (id) => {
|
||||
deleteCustomImage(id).then(() => {
|
||||
this.loadCustomImages();
|
||||
toast(variables.getMessage('toasts.remove_success'));
|
||||
});
|
||||
};
|
||||
|
||||
uploadCustomBackground = () => {
|
||||
document.getElementById('bg-input').click();
|
||||
};
|
||||
|
||||
handleFileUpload = (files) => {
|
||||
const maxSize = 5 * 1024 * 1024; // 5MB size limit
|
||||
files.forEach((file) => {
|
||||
if (file.size > maxSize) {
|
||||
toast.error(variables.getMessage('toasts.upload_size_error'));
|
||||
return;
|
||||
}
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = (event) => {
|
||||
this.addCustomImage(event.target.result);
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<>
|
||||
<div className="dropzone" ref={this.customDnd}>
|
||||
<div ref={this.customDnd}>
|
||||
<div className="imagesTopBar">
|
||||
<div>
|
||||
<MdAddPhotoAlternate />
|
||||
@@ -192,7 +110,7 @@ export default class CustomSettings extends PureComponent {
|
||||
<div className="topbarbuttons">
|
||||
<Button
|
||||
type="settings"
|
||||
onClick={() => this.uploadCustomBackground()}
|
||||
onClick={this.uploadCustomBackground}
|
||||
icon={<MdOutlineFileUpload />}
|
||||
label={variables.getMessage('settings:sections.background.source.upload')}
|
||||
/>
|
||||
@@ -204,27 +122,21 @@ export default class CustomSettings extends PureComponent {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="dropzone-content">
|
||||
<div className={`dropzone dropzone-content ${this.state.isDragging ? 'dragging' : ''}`}>
|
||||
{this.state.customBackground.length > 0 ? (
|
||||
<div className="images-row">
|
||||
{this.state.customBackground.map((url, index) => (
|
||||
<div key={index}>
|
||||
<img
|
||||
alt={'Custom background ' + (index || 0)}
|
||||
src={`${!videoCheck(url) ? this.state.customBackground[index] : ''}`}
|
||||
/>
|
||||
{videoCheck(url) && <MdPersonalVideo className="customvideoicon" />}
|
||||
{this.state.customBackground.length > 0 && (
|
||||
<Tooltip
|
||||
title={variables.getMessage('settings:sections.background.source.remove')}
|
||||
>
|
||||
<Button
|
||||
type="settings"
|
||||
onClick={() => this.modifyCustomBackground('remove', index)}
|
||||
icon={<MdCancel />}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
<div className="images-row fixed-width">
|
||||
{this.state.customBackground.map((image, index) => (
|
||||
<div key={index} className="image-container">
|
||||
<img alt={'Custom background ' + index} src={image.url} />
|
||||
<Tooltip
|
||||
title={variables.getMessage('settings:sections.background.source.remove')}
|
||||
>
|
||||
<Button
|
||||
type="settings"
|
||||
onClick={() => this.removeCustomImage(image.id)}
|
||||
icon={<MdCancel />}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -242,7 +154,7 @@ export default class CustomSettings extends PureComponent {
|
||||
</span>
|
||||
<Button
|
||||
type="settings"
|
||||
onClick={() => this.uploadCustomBackground()}
|
||||
onClick={this.uploadCustomBackground}
|
||||
icon={<MdFolder />}
|
||||
label={variables.getMessage('settings:sections.background.source.select')}
|
||||
/>
|
||||
@@ -251,12 +163,17 @@ export default class CustomSettings extends PureComponent {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<FileUpload
|
||||
<input
|
||||
type="file"
|
||||
id="bg-input"
|
||||
accept="image/jpeg, image/png, image/webp, image/webm, image/gif, video/mp4, video/webm, video/ogg"
|
||||
loadFunction={(e) => this.customBackground(e, this.state.currentBackgroundIndex)}
|
||||
multiple
|
||||
style={{ display: 'none' }}
|
||||
onChange={(e) => {
|
||||
const files = Array.from(e.target.files);
|
||||
this.handleFileUpload(files);
|
||||
}}
|
||||
/>
|
||||
{this.videoCustomSettings()}
|
||||
<Modal
|
||||
closeTimeoutMS={100}
|
||||
onRequestClose={() => this.setState({ customURLModal: false })}
|
||||
@@ -266,7 +183,7 @@ export default class CustomSettings extends PureComponent {
|
||||
ariaHideApp={false}
|
||||
>
|
||||
<CustomURLModal
|
||||
modalClose={(e) => this.addCustomURL(e)}
|
||||
modalClose={(e) => this.addCustomImage(e)}
|
||||
urlError={this.state.urlError}
|
||||
modalCloseOnly={() => this.setState({ customURLModal: false })}
|
||||
/>
|
||||
|
||||
@@ -74,3 +74,8 @@
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.dropzone.dragging {
|
||||
border: 2px dashed #007bff;
|
||||
background-color: rgba(0, 123, 255, 0.1);
|
||||
}
|
||||
|
||||
@@ -17,14 +17,14 @@ function AppsOptions({ appsEnabled }) {
|
||||
const [appsModalInfo, setAppsModalInfo] = useState({
|
||||
newLink: false,
|
||||
edit: false,
|
||||
items: JSON.parse(localStorage.getItem('applinks')),
|
||||
items: JSON.parse(localStorage.getItem('applinks')) || [],
|
||||
urlError: '',
|
||||
iconError: '',
|
||||
editData: null,
|
||||
});
|
||||
|
||||
const addLink = async (name, url, icon) => {
|
||||
const data = JSON.parse(localStorage.getItem('applinks'));
|
||||
const data = JSON.parse(localStorage.getItem('applinks')) || [];
|
||||
|
||||
if (!url.startsWith('http://') && !url.startsWith('https://')) {
|
||||
url = 'https://' + url;
|
||||
@@ -73,7 +73,7 @@ function AppsOptions({ appsEnabled }) {
|
||||
};
|
||||
|
||||
const editLink = async (og, name, url, icon) => {
|
||||
const data = JSON.parse(localStorage.getItem('applinks'));
|
||||
const data = JSON.parse(localStorage.getItem('applinks')) || [];
|
||||
const dataobj = data.find((i) => i.key === og.key);
|
||||
dataobj.name = name || (await getTitleFromUrl(url));
|
||||
dataobj.url = url;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// indexedDB.js
|
||||
const DB_NAME = 'StatsDB';
|
||||
const DB_VERSION = 1;
|
||||
const DB_VERSION = 2; // Incremented version
|
||||
const STORE_NAME = 'eventLog';
|
||||
const CUSTOM_STORE_NAME = 'customImages';
|
||||
|
||||
export const openDB = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
@@ -12,6 +12,9 @@ export const openDB = () => {
|
||||
if (!db.objectStoreNames.contains(STORE_NAME)) {
|
||||
db.createObjectStore(STORE_NAME, { keyPath: 'id', autoIncrement: true });
|
||||
}
|
||||
if (!db.objectStoreNames.contains(CUSTOM_STORE_NAME)) {
|
||||
db.createObjectStore(CUSTOM_STORE_NAME, { keyPath: 'id', autoIncrement: true });
|
||||
}
|
||||
};
|
||||
|
||||
request.onsuccess = (event) => {
|
||||
@@ -59,3 +62,58 @@ export const getEvents = (query) => {
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// New methods for custom images
|
||||
export const addCustomImage = (image) => {
|
||||
return openDB().then((db) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = db.transaction([CUSTOM_STORE_NAME], 'readwrite');
|
||||
const store = transaction.objectStore(CUSTOM_STORE_NAME);
|
||||
const request = store.add(image);
|
||||
|
||||
request.onsuccess = () => {
|
||||
resolve();
|
||||
};
|
||||
|
||||
request.onerror = (event) => {
|
||||
reject(event.target.error);
|
||||
};
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export const getCustomImages = () => {
|
||||
return openDB().then((db) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = db.transaction([CUSTOM_STORE_NAME], 'readonly');
|
||||
const store = transaction.objectStore(CUSTOM_STORE_NAME);
|
||||
const request = store.getAll();
|
||||
|
||||
request.onsuccess = (event) => {
|
||||
resolve(event.target.result);
|
||||
};
|
||||
|
||||
request.onerror = (event) => {
|
||||
reject(event.target.error);
|
||||
};
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export const deleteCustomImage = (id) => {
|
||||
return openDB().then((db) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = db.transaction([CUSTOM_STORE_NAME], 'readwrite');
|
||||
const store = transaction.objectStore(CUSTOM_STORE_NAME);
|
||||
const request = store.delete(id);
|
||||
|
||||
request.onsuccess = () => {
|
||||
resolve();
|
||||
};
|
||||
|
||||
request.onerror = (event) => {
|
||||
reject(event.target.error);
|
||||
};
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user