refactor(background): use indexeddb for custom images

This commit is contained in:
alexsparkes
2024-11-27 19:42:26 +00:00
parent b01b26bf86
commit c15ce66bb8
7 changed files with 182 additions and 173 deletions

View File

@@ -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));

View File

@@ -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]);

View File

@@ -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}
</>
)}

View File

@@ -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 })}
/>

View File

@@ -74,3 +74,8 @@
opacity: 1;
}
}
.dropzone.dragging {
border: 2px dashed #007bff;
background-color: rgba(0, 123, 255, 0.1);
}

View File

@@ -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;

View File

@@ -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);
};
});
});
};