From c15ce66bb8ec53bf5a1be78e6e81d936a7609ec8 Mon Sep 17 00:00:00 2001 From: alexsparkes Date: Wed, 27 Nov 2024 19:42:26 +0000 Subject: [PATCH] refactor(background): use indexeddb for custom images --- .../scss/settings/modules/tabs/_order.scss | 10 + src/features/background/Background.jsx | 13 + .../background/options/BackgroundOptions.jsx | 16 +- src/features/background/options/Custom.jsx | 243 ++++++------------ src/features/background/scss/index.scss | 5 + src/features/navbar/options/AppsOptions.jsx | 6 +- src/utils/indexedDB.js | 62 ++++- 7 files changed, 182 insertions(+), 173 deletions(-) diff --git a/src/components/Elements/MainModal/scss/settings/modules/tabs/_order.scss b/src/components/Elements/MainModal/scss/settings/modules/tabs/_order.scss index 6481c846..f56b0c81 100644 --- a/src/components/Elements/MainModal/scss/settings/modules/tabs/_order.scss +++ b/src/components/Elements/MainModal/scss/settings/modules/tabs/_order.scss @@ -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)); diff --git a/src/features/background/Background.jsx b/src/features/background/Background.jsx index 0c2c0c5c..95695091 100644 --- a/src/features/background/Background.jsx +++ b/src/features/background/Background.jsx @@ -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]); diff --git a/src/features/background/options/BackgroundOptions.jsx b/src/features/background/options/BackgroundOptions.jsx index 5a89d8c6..f46c3a5a 100644 --- a/src/features/background/options/BackgroundOptions.jsx +++ b/src/features/background/options/BackgroundOptions.jsx @@ -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)} /> this.setState({ backgroundType: value })} + onChange={(value) => setBackgroundType(value)} category="background" items={getBackgroundOptionItems(marketplaceEnabled)} /> @@ -363,7 +370,6 @@ function BackgroundOptions() { /> - {/* todo: ideally refactor all of this file, but we need interval to appear on marketplace too */} {backgroundSettings} )} diff --git a/src/features/background/options/Custom.jsx b/src/features/background/options/Custom.jsx index 27caf79c..1950c792 100644 --- a/src/features/background/options/Custom.jsx +++ b/src/features/background/options/Custom.jsx @@ -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 ( - <> - - - - ); - } 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 ( <> -
+
@@ -192,7 +110,7 @@ export default class CustomSettings extends PureComponent {
-
+
{this.state.customBackground.length > 0 ? ( -
- {this.state.customBackground.map((url, index) => ( -
- {'Custom - {videoCheck(url) && } - {this.state.customBackground.length > 0 && ( - -
))}
@@ -242,7 +154,7 @@ export default class CustomSettings extends PureComponent {
- this.customBackground(e, this.state.currentBackgroundIndex)} + multiple + style={{ display: 'none' }} + onChange={(e) => { + const files = Array.from(e.target.files); + this.handleFileUpload(files); + }} /> - {this.videoCustomSettings()} this.setState({ customURLModal: false })} @@ -266,7 +183,7 @@ export default class CustomSettings extends PureComponent { ariaHideApp={false} > this.addCustomURL(e)} + modalClose={(e) => this.addCustomImage(e)} urlError={this.state.urlError} modalCloseOnly={() => this.setState({ customURLModal: false })} /> diff --git a/src/features/background/scss/index.scss b/src/features/background/scss/index.scss index 529cf794..c09143e5 100644 --- a/src/features/background/scss/index.scss +++ b/src/features/background/scss/index.scss @@ -74,3 +74,8 @@ opacity: 1; } } + +.dropzone.dragging { + border: 2px dashed #007bff; + background-color: rgba(0, 123, 255, 0.1); +} diff --git a/src/features/navbar/options/AppsOptions.jsx b/src/features/navbar/options/AppsOptions.jsx index e50fe6e5..1051a69b 100644 --- a/src/features/navbar/options/AppsOptions.jsx +++ b/src/features/navbar/options/AppsOptions.jsx @@ -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; diff --git a/src/utils/indexedDB.js b/src/utils/indexedDB.js index 0eaea186..5866fb86 100644 --- a/src/utils/indexedDB.js +++ b/src/utils/indexedDB.js @@ -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); + }; + }); + }); +};