diff --git a/src/client.js b/src/client.js index 44f6f0ff..39b91358 100644 --- a/src/client.js +++ b/src/client.js @@ -21,6 +21,7 @@ import pixelTransferController from './ui/PixelTransferController'; import store from './store/store'; import renderApp from './components/App'; import { getRenderer } from './ui/rendererFactory'; +import templateLoader from './ui/templateLoader'; import socketClient from './socket/SocketClient'; import { GC_INTERVAL } from './core/constants'; @@ -31,6 +32,9 @@ persistStore(store, {}, () => { pixelTransferController.initialize(store, socketClient, getRenderer); + // TODO should be in middleware + templateLoader.initialize(store); + window.addEventListener('hashchange', () => { store.dispatch(urlChange()); }); diff --git a/src/components/AddTemplate.jsx b/src/components/AddTemplate.jsx new file mode 100644 index 00000000..3f91d157 --- /dev/null +++ b/src/components/AddTemplate.jsx @@ -0,0 +1,129 @@ +/* + * Settings of minimap / overlay + */ + +import React, { useState, useEffect, useRef } from 'react'; +import { useSelector, shallowEqual } from 'react-redux'; +import { t } from 'ttag'; + +import { coordsFromUrl } from '../core/utils'; +import templateLoader from '../ui/templateLoader'; + +const AddTemplate = ({ close, triggerClose: refClose }) => { + const [render, setRender] = useState(false); + const [coords, setCoords] = useState(null); + const [dimensions, setDimensions] = useState(null); + const [title, setTitle] = useState(''); + const [file, setFile] = useState(null); + const imgRef = useRef(); + const [ + canvasId, + canvases, + ] = useSelector((state) => [ + state.canvas.canvasId, + state.canvas.canvases, + ], shallowEqual); + const [selectedCanvas, selectCanvas] = useState(canvasId); + + useEffect(() => { + window.setTimeout(() => setRender(true), 10); + refClose.current = () => setRender(false); + }, [refClose]); + + useEffect(() => { + if (!file || !imgRef.current) { + return; + } + const fr = new FileReader(); + fr.onload = () => { imgRef.current.src = fr.result; }; + fr.readAsDataURL(file); + }, [file]); + + const canSubmit = (imgRef.current && file && coords && title && dimensions); + + return ( +
!render && close()} + > +
{ + event.preventDefault(); + if (canSubmit) { + await templateLoader.addFile( + file, title, selectedCanvas, ...coords, imgRef.current, + ); + setRender(false); + } + }} + > + preview setDimensions([ + evt.target.naturalWidth, + evt.target.naturalHeight, + ])} + onError={() => setDimensions(null)} + ref={imgRef} + /> + { + setDimensions(null); + setFile(evt.target.files?.[0]); + }} + /> + setTitle(evt.target.value)} + placeholder={t`Template Name`} + /> + + { + let co = evt.target.value.trim(); + co = coordsFromUrl(co) || co; + evt.target.value = co; + const newCoords = co.split('_').map((z) => parseInt(z, 10)); + setCoords((!newCoords.some(Number.isNaN) && newCoords.length === 2) + ? newCoords : null, + ); + }} + /> + +
+
+ ); +}; + +export default React.memo(AddTemplate); diff --git a/src/components/Converter.jsx b/src/components/Converter.jsx index 1963c51d..2cd5ba2a 100644 --- a/src/components/Converter.jsx +++ b/src/components/Converter.jsx @@ -10,11 +10,11 @@ import { jt, t } from 'ttag'; import { ColorDistanceCalculators, ImageQuantizerKernels, - readFileIntoCanvas, scaleImage, quantizeImage, addGrid, -} from '../utils/image'; +} from '../utils/imageFilters'; +import { fileToCanvas } from '../utils/imageFiles'; import printGIMPPalette from '../core/exportGPL'; import { copyCanvasToClipboard } from '../utils/clipboard'; @@ -247,7 +247,7 @@ function Converter() { const fileSel = evt.target; const file = (!fileSel.files || !fileSel.files[0]) ? null : fileSel.files[0]; - const imageData = await readFileIntoCanvas(file); + const imageData = await fileToCanvas(file); setInputImageCanvas(null); setScaleData({ enabled: false, diff --git a/src/components/TemplateItem.jsx b/src/components/TemplateItem.jsx new file mode 100644 index 00000000..7841012b --- /dev/null +++ b/src/components/TemplateItem.jsx @@ -0,0 +1,82 @@ +/** + * Item for list of Tamplates + */ + +import React, { useRef, useState, useEffect } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { t } from 'ttag'; + +import templateLoader from '../ui/templateLoader'; +import { changeTemplate } from '../store/actions/templates'; +import { selectCanvas, setViewCoordinates } from '../store/actions'; + +const TemplateItem = ({ + enabled, title, canvasId, x, y, width, height, imageId, startEditing, +}) => { + const imgRef = useRef(); + const [canvasTitle, setCanvasTitle] = useState('Earth'); + const canvases = useSelector((state) => state.canvas.canvases); + const dispatch = useDispatch(); + + useEffect(() => { + (async () => { + if (!imageId || !imgRef.current) { + return; + } + const previewImg = await templateLoader.getTemplate(imageId); + if (!previewImg) { + return; + } + imgRef.current.getContext('2d').drawImage(previewImg, 0, 0); + })(); + }, [imageId]); + + return ( +
dispatch(changeTemplate(title, { enabled: !enabled }))} + > +
+ +
+
+

{title}

+

+ {t`Canvas`}: {canvases[canvasId]?.title} +

+

+ {t`Coordinates`}: {`${x},${y}`} +

+

+ {t`Dimensions`}: {`${width} x ${height}`} +

+
+
+ + +
+
+ ); +}; + +export default React.memo(TemplateItem); diff --git a/src/components/TemplateItemEdit.jsx b/src/components/TemplateItemEdit.jsx new file mode 100644 index 00000000..01702951 --- /dev/null +++ b/src/components/TemplateItemEdit.jsx @@ -0,0 +1,145 @@ +/** + * Item for list of Tamplates + */ + +import React, { + useRef, useState, useEffect, useMemo, +} from 'react'; +import { useDispatch, useSelector, shallowEqual } from 'react-redux'; +import { t } from 'ttag'; + +import templateLoader from '../ui/templateLoader'; +import { changeTemplate } from '../store/actions/templates'; +import { selectCanvas, setViewCoordinates } from '../store/actions'; +import { coordsFromUrl } from '../core/utils'; + +const TemplateItem = ({ + title: initTitle, + canvasId: initCanvasId, + x: initX, y: initY, + width: initWidth, height: initHeight, + imageId: initImageId, + stopEditing, +}) => { + const [initCoords, initDimensions] = useMemo(() => [ + (Number.isNaN(parseInt(initX, 10)) + || Number.isNaN(parseInt(initY, 10))) ? null : [initX, initY], + (Number.isNaN(parseInt(initWidth, 10)) + || Number.isNaN(parseInt(initHeight, 10))) ? null : [initWidth, initHeight], + ], [initX, initY, initWidth, initHeight]); + + const [coords, setCoords] = useState(initCoords); + const [dimensions, setDimensions] = useState(initDimensions); + const [title, setTitle] = useState(initTitle); + const [file, setFile] = useState(null); + const [imageId, setImageId] = useState(initImageId); + const imgRef = useRef(); + const [ + storeCanvasId, + canvases, + ] = useSelector((state) => [ + state.canvas.canvasId, + state.canvas.canvases, + ], shallowEqual); + const [selectedCanvas, selectCanvas] = useState(initCanvasId ?? storeCanvasId); + const dispatch = useDispatch(); + + useEffect(() => { + (async () => { + if (!imageId || !imgRef.current) { + return; + } + const previewImg = await templateLoader.getTemplate(imageId); + if (!previewImg) { + return; + } + const canvas = imgRef.current; + canvas.width = previewImg.width; + canvas.height = previewImg.height; + canvas.getContext('2d').drawImage(previewImg, 0, 0); + })(); + }, [imageId]); + + const canSubmit = (imgRef.current && file && coords && title && dimensions); + + return ( +
+
+ +
{t`Select File`}
+
+
+

setTitle(evt.target.value)} + placeholder={t`Template Name`} + />

+

{t`Canvas`}:  + +

+

+ {t`Coordinates`}:  + { + let co = evt.target.value.trim(); + co = coordsFromUrl(co) || co; + evt.target.value = co; + const newCoords = co.split('_').map((z) => parseInt(z, 10)); + setCoords((!newCoords.some(Number.isNaN) && newCoords.length === 2) + ? newCoords : null, + ); + }} + /> +

+

+ {t`Dimensions`}: {(dimensions) ? dimensions.join(' x ') : 'N/A'} +

+
+
+ + + +
+
+ ); +}; + +export default React.memo(TemplateItem); diff --git a/src/components/TemplateSettings.jsx b/src/components/TemplateSettings.jsx new file mode 100644 index 00000000..981545cc --- /dev/null +++ b/src/components/TemplateSettings.jsx @@ -0,0 +1,82 @@ +/* + * Settings for minimap / overlay + */ + +import React, { useState, useCallback, useRef } from 'react'; +import { useSelector } from 'react-redux'; +import { t } from 'ttag'; + +import AddTemplate from './AddTemplate'; +import TemplateItem from './TemplateItem'; +import TemplateItemEdit from './TemplateItemEdit'; + +const TemplateSettings = () => { + const [showAdd, setShowAdd] = useState(false); + const list = useSelector((state) => state.templates.list); + const [editingIndices, setEditingIndices] = useState([]); + const close = useCallback(() => setShowAdd(false), []); + const refClose = useRef(); + + const toggleEditing = useCallback((title) => { + const index = list.findIndex((t) => t.title === title); + const ind = editingIndices.indexOf(index); + setEditingIndices((ind === -1) + ? [...editingIndices, index] + : editingIndices.toSpliced(ind, 1), + ); + }, [editingIndices]); + + console.log('editing', editingIndices); + + return ( +
+ {list.map(({ + enabled, imageId, canvasId, title, x, y, width, height, + }, index) => (editingIndices.includes(index) ? ( + + ) : ( + + )))} + {(showAdd) ? ( + refClose.current?.()} + > {t`Cancel adding Template`} + ) : ( + setShowAdd(true)} + > {t`Add Template`} + )} + {showAdd && } +
+ ); +}; + +export default TemplateSettings; diff --git a/src/components/windows/Settings.jsx b/src/components/windows/Settings.jsx index 64327e05..c0f82f23 100644 --- a/src/components/windows/Settings.jsx +++ b/src/components/windows/Settings.jsx @@ -8,6 +8,7 @@ import { c, t } from 'ttag'; import SettingsItem from '../SettingsItem'; import LanguageSelect from '../LanguageSelect'; +import TemplateSettings from '../TemplateSettings'; import { toggleGrid, togglePixelNotify, @@ -80,7 +81,6 @@ const Settings = () => { state.canvas.isHistoricalView, ], shallowEqual); const dispatch = useDispatch(); - const audioAvailable = window.AudioContext || window.webkitAudioContext; return ( @@ -192,6 +192,12 @@ const Settings = () => { )} +
+

{t`Templates`}

+

+ {t`Tired of always spaming one single color? Want to create art instead, but you have to count pixels from some other image? Templates can help you with that! Templates can show as overlay and you can draw over them. One pixel on the template, should be one pixel on the canvas.`} +

+
); }; diff --git a/src/store/actions/templates.js b/src/store/actions/templates.js new file mode 100644 index 00000000..4eb6c510 --- /dev/null +++ b/src/store/actions/templates.js @@ -0,0 +1,31 @@ +/* + * actions for templates + */ + +export function listTemplate(imageId, title, canvasId, x, y, width, height) { + return { + type: 's/LIST_TEMPLATE', + imageId, + title, + canvasId, + x, + y, + width, + height, + }; +} + +export function removeTemplate(title) { + return { + type: 's/REM_TEMPLATE', + title, + }; +} + +export function changeTemplate(title, props) { + return { + type: 's/CHG_TEMPLATE', + title, + props, + }; +} diff --git a/src/store/middleware/rendererHook.js b/src/store/middleware/rendererHook.js index b97fe019..1386aa06 100644 --- a/src/store/middleware/rendererHook.js +++ b/src/store/middleware/rendererHook.js @@ -64,6 +64,7 @@ export default (store) => (next) => (action) => { initRenderer(store, is3D); } + // TODO this looks shade, i.e. when a nwe Renderer appears if (state.canvas.isHistoricalView) { const { historicalDate, diff --git a/src/store/reducers/templates.js b/src/store/reducers/templates.js new file mode 100644 index 00000000..710408c5 --- /dev/null +++ b/src/store/reducers/templates.js @@ -0,0 +1,77 @@ +/* + * store for minimap / overlay templates + * + */ + +const initialState = { + // prefix o: overlay, m: minimap + ovEnabled: false, + mEnabled: false, + oOpacity: 1.0, + oSmallPxls: false, + /* + * [{ + * enabled, + * title, + * canvasId, + * x, + * y, + * imageId, + * width, + * height, + * },... ] + */ + list: [], +}; + +export default function templates( + state = initialState, + action, +) { + switch (action.type) { + case 's/LIST_TEMPLATE': { + const { + imageId, title, canvasId, x, y, width, height, + } = action; + return { + ...state, + list: [ + ...state.list, + { + enabled: true, + title, + canvasId, + x, + y, + imageId, + width, + height, + }, + ], + }; + } + + case 's/CHG_TEMPLATE': { + const { title, props } = action; + const { list } = state; + const index = list.findIndex((t) => t.title === title); + if (index === -1) { + return state; + } + return { + ...state, + list: [ + ...list.slice(0, index), + { + ...list[index], + ...props, + }, + ...list.slice(index + 1), + ], + }; + } + + default: + return state; + } +} diff --git a/src/store/sharedReducers.js b/src/store/sharedReducers.js index 42852f42..095bbd9d 100644 --- a/src/store/sharedReducers.js +++ b/src/store/sharedReducers.js @@ -12,6 +12,7 @@ import ranks from './reducers/ranks'; import chatRead from './reducers/chatRead'; import user from './reducers/user'; import canvas from './reducers/canvas'; +import templates from './reducers/templates'; import chat from './reducers/chat'; import fetching from './reducers/fetching'; @@ -48,10 +49,18 @@ const chatReadPersist = persistReducer({ migrate, }, chatRead); +const templatesPersist = persistReducer({ + key: 'tem', + storage, + version: CURRENT_VERSION, + migrate, +}, templates); + export default { gui: guiPersist, ranks: ranksPersist, chatRead: chatReadPersist, + templates: templatesPersist, user, canvas, chat, diff --git a/src/styles/default.css b/src/styles/default.css index 2ddcf217..44bd2c9f 100644 --- a/src/styles/default.css +++ b/src/styles/default.css @@ -186,7 +186,6 @@ h3 { margin-right: 10px; overflow: hidden; word-wrap: break-word; - margin-bottom: 0; } h4 { @@ -309,6 +308,63 @@ tr:nth-child(even) { color: #212121; } +.tmpitm { + margin-top: 8px; + margin-bottom: 8px; + border: #c5c5c5; + border-style: solid; + display: flex; + flex-direction: row; + align-items: center; + cursor: pointer; +} + +.tmpitm.disabled { + border-style: dotted; + opacity: 0.6; +} + +.tmpitm-preview { + width: 96px; + height: 96px; + margin: 5px; +} + +.tmpitm-img { + max-width: 100%; + max-height: 100%; +} + +.tmpitm-actions { + margin-right: 10px; +} + +.tmpitm.disabled .tmpitm-img { + opacity: 0.6; +} + +.tmpitm-desc { + margin-top: 3px; + margin-bottom: 3px; + flex-grow: 1; +} + +.tmpitm-desc p { + margin-top: 3px; + margin-bottom: 3px; +} + +.centered-on-img { + position: relative; + transform: translateY(-99px); + line-height: 96px; + background: #e6e6e6d1; + white-space: nowrap; + max-width: 96px; + height: 96px; + overflow: clip; +} + .cvbtn { margin-top: 8px; margin-bottom: 8px; @@ -576,7 +632,7 @@ tr:nth-child(even) { font-size: 16px; } -.Alert p,.window p { +.Alert p { margin-top: 12px; margin-bottom: 12px; } @@ -594,7 +650,6 @@ tr:nth-child(even) { font-size: 14px; line-height: 20px; font-weight: 500; - margin-top: 4px; } .modaldivider { @@ -612,7 +667,7 @@ tr:nth-child(even) { height: 100%; } -.modalinfo { +.modalinfo, .tmpitm-desc span { color: #4f545c; font-size: 15px; font-weight: 500; @@ -643,6 +698,7 @@ tr:nth-child(even) { .setitem h3 { margin-top: 0; + margin-bottom: 1px; } .setitem { diff --git a/src/ui/ChunkLoader.js b/src/ui/ChunkLoader.js index 53559707..79c47b27 100644 --- a/src/ui/ChunkLoader.js +++ b/src/ui/ChunkLoader.js @@ -20,12 +20,11 @@ import { class ChunkLoader { #store; // Map of chunkId: chunkRGB - #chunks; + #chunks = new Map(); #canvasId; constructor(store, canvasId) { this.#store = store; - this.#chunks = new Map(); this.#canvasId = canvasId; } diff --git a/src/ui/Renderer3D.js b/src/ui/Renderer3D.js index 97f83a09..d6285716 100644 --- a/src/ui/Renderer3D.js +++ b/src/ui/Renderer3D.js @@ -491,9 +491,8 @@ class Renderer3D extends Renderer { ); } - onDocumentTouchMove(event) { + onDocumentTouchMove() { if (this.rollOverMesh.position.y !== -10) { - console.log('unset hover'); this.store.dispatch(unsetHover()); this.rollOverMesh.position.y = -10; } diff --git a/src/ui/Template.js b/src/ui/Template.js new file mode 100644 index 00000000..a1a390e6 --- /dev/null +++ b/src/ui/Template.js @@ -0,0 +1,55 @@ +/* + * Template for the minimap / overlay + */ + +import { + fileToCanvas, + imageToCanvas, + bufferToCanvas, + canvasToBuffer, +} from '../utils/imageFiles'; + +class Template { + // HTMLCanvasElement of image + image; + // id to hold in indexedDB, in store as imageId + id; + // dimensions + width; + height; + // if image is loaded + ready = false; + + constructor(imageId) { + this.id = imageId; + } + + async arrayBuffer() { + return canvasToBuffer(this.image); + } + + setDimensionFromCanvas() { + const { width, height } = this.image; + this.width = width; + this.height = height; + this.read = true; + return [width, height]; + } + + fromImage(img) { + this.image = imageToCanvas(img); + return this.setDimensionFromCanvas(); + } + + async fromFile(file) { + this.image = await fileToCanvas(file); + return this.setDimensionFromCanvas(); + } + + async fromBuffer(buffer, type = 'image/png') { + this.image = await bufferToCanvas(buffer, type); + return this.setDimensionFromCanvas(); + } +} + +export default Template; diff --git a/src/ui/templateLoader.js b/src/ui/templateLoader.js new file mode 100644 index 00000000..44295b61 --- /dev/null +++ b/src/ui/templateLoader.js @@ -0,0 +1,131 @@ +/* + * class for storing templates for minimap / overlay + */ + +import FileStorage from '../utils/FileStorage'; +import { + removeTemplate, + listTemplate, +} from '../store/actions/templates'; +import Template from './Template'; + +const STORE_NAME = 'templates'; + +class TemplateLoader { + #store; + #fileStorage = new FileStorage(STORE_NAME); + // Map of templates + #templates = new Map(); + // if loader is ready + ready = false; + + async initialize(store) { + this.#store = store; + await this.#fileStorage.initialize(); + await this.syncDB(); + this.ready = true; + } + + async getTemplate(id) { + let template = this.#templates.get(id); + if (template) { + return template.image; + } + template = await this.loadExistingTemplate(id); + if (template) { + return template.image; + } + return null; + } + + /* + getTemplatesInView() { + this.#store.templates + } + */ + + /* + * sync database to store, + * remove templates with images that aren't present in both + */ + async syncDB() { + try { + const { list } = this.#store.getState().templates; + const ids = list.map((t) => t.imageId); + const deadIds = await this.#fileStorage.sync(ids); + list.filter((t) => deadIds.includes(t.imageId)).forEach((t) => { + this.#store.dispatch(removeTemplate(t.title)); + }); + } catch (err) { + // eslint-disable-next-line no-console + console.error( + `Error on syncing templates store and indexedDB: ${err.message}`, + ); + } + } + + /* + * load all missing template from store + */ + async loadAllMissing() { + const { templates } = this.#store.getState(); + const ids = templates.list.map((t) => t.imageId); + const toLoad = ids.filter((i) => !this.#templates.has(i)); + for (const id of toLoad) { + // eslint-disable-next-line no-await-in-loop + await this.loadExistingTemplate(id); + } + } + + /* + * load a template from db + * @param imageId + */ + async loadExistingTemplate(imageId) { + try { + const fileData = await this.#fileStorage.loadFile(imageId); + if (!fileData) { + throw new Error('File does not exist in indexedDB'); + } + const { mimetype, buffer } = fileData; + const template = new Template(imageId); + await template.fromBuffer(buffer, mimetype); + this.#templates.set(imageId, template); + return template; + } catch (err) { + // eslint-disable-next-line no-console + console.error(`Error loading template ${imageId}: ${err.message}`); + return null; + } + } + + /* + * add template from File or Blob + * @param file, title, canvasId, x, y self explenatory + * @param element optional image or canvas element where file is already loaded, + * can be used to avoid having to load it multiple times + */ + async addFile(file, title, canvasId, x, y, element = null) { + try { + const imageId = await this.#fileStorage.saveFile(file); + const template = new Template(imageId); + let dimensions; + if (element) { + dimensions = await template.fromImage(element); + } else { + dimensions = await template.fromFile(file); + } + this.#templates.set(imageId, template); + this.#store.dispatch(listTemplate( + imageId, title, canvasId, x, y, ...dimensions, + )); + } catch (err) { + // eslint-disable-next-line no-console + console.error(`Error saving template ${title}: ${err.message}`); + } + } +} + +const templateLoader = new TemplateLoader(); + +export default templateLoader; diff --git a/src/utils/FileStorage.js b/src/utils/FileStorage.js new file mode 100644 index 00000000..7477b2e1 --- /dev/null +++ b/src/utils/FileStorage.js @@ -0,0 +1,199 @@ +/* + * store files in indexedDB on incrementing indices + */ + +const CURRENT_VERSION = 1; +const DB_NAME = 'ppfun_files'; + +// TODO make sure we get sane errors on reject() + +class FileStorage { + type; + static db; + + constructor(type) { + this.type = type; + } + + async initialize() { + await FileStorage.openDB(); + return this; + } + + static openDB() { + if (FileStorage.db) { + return FileStorage.db; + } + return new Promise((resolve, reject) => { + const request = window.indexedDB.open(DB_NAME, CURRENT_VERSION); + + request.onsuccess = (event) => { + console.log('Successfully opened indexedDB'); + const db = event.target.result; + + db.onerror = (evt) => { + console.error('indexedDB error:', evt.target.error); + }; + + FileStorage.db = db; + resolve(db); + }; + + request.onupgradeneeded = (event) => { + const os = event.target.result.createObjectStore('files', { + autoIncrement: true, + }); + os.createIndex('type', 'type', { unique: false }); + os.createIndex('mimetype', 'mimetype', { unique: false }); + }; + + request.onerror = () => { + console.error('Error on opening indexedDB:', request.error); + reject(); + }; + }); + } + + async saveFile(files) { + const { db } = FileStorage; + if (!db) { + return null; + } + const fileArray = Array.isArray(files) ? files : [files]; + const buffers = await Promise.all(fileArray.map((f) => f.arrayBuffer())); + console.log('buffers', buffers); + + return new Promise((resolve, reject) => { + const result = []; + const transaction = db.transaction('files', 'readwrite'); + + transaction.oncomplete = () => { + console.log('Success on saving files to indexedDB', result); + resolve(Array.isArray(files) ? result : result[0]); + }; + transaction.onabort = (event) => { + event.stopPropagation(); + console.log('Saving files to indexedDB aborted:', event, result); + reject(); + }; + + const os = transaction.objectStore('files'); + fileArray.forEach((file, index) => { + console.log('type', this.type, 'mime', file.type, 'buffer', buffers[index], 'file', file); + result.push(null); + os.add({ + type: this.type, + mimetype: file.type, + buffer: buffers[index], + }).onsuccess = (event) => { + result[index] = event.target.result; + }; + }); + }); + } + + // eslint-disable-next-line class-methods-use-this + loadFile(ids) { + const { db } = FileStorage; + if (!db) { + return null; + } + const indicesArray = Array.isArray(ids) ? ids : [ids]; + + return new Promise((resolve, reject) => { + const result = []; + const transaction = db.transaction('files', 'readonly'); + + transaction.oncomplete = () => { + console.log('Success on loading file', result); + resolve(Array.isArray(ids) ? result : result[0]); + }; + transaction.onabort = (event) => { + event.stopPropagation(); + console.log('Loading file from indexedDB aborted:', event.target.error); + reject(); + }; + + const os = transaction.objectStore('files'); + indicesArray.forEach((id, index) => { + result.push(null); + os.get(id).onsuccess = (event) => { + result[index] = event.target.result; + }; + }); + }); + } + + deleteFile(ids) { + const { db } = FileStorage; + if (!db) { + return null; + } + const indicesArray = Array.isArray(ids) ? ids : [ids]; + + return new Promise((resolve, reject) => { + const transaction = db.transaction('files', 'readwrite'); + + transaction.oncomplete = (event) => { + console.log( + `Successfully deleted ${indicesArray.length} files from indexedDB`, + ); + resolve(); + }; + transaction.onabort = (event) => { + event.stopPropagation(); + console.log('Saving files to indexedDB aborted:', event); + reject(); + }; + + const os = transaction.objectStore('files'); + indicesArray.forEach((id) => { + os.delete(id); + }); + }); + } + + getAllKeys() { + const { db } = FileStorage; + if (!db) { + return null; + } + return new Promise((resolve, reject) => { + const transaction = db.transaction('files', 'readonly'); + const request = transaction.objectStore('files') + .index('type') + .getAllKeys(this.type); + + request.onsuccess = (event) => { + console.log('got all keys', event.target.result); + resolve(event.target.result); + }; + transaction.onabort = (event) => { + event.stopPropagation(); + console.log('GetAllKeys aborted:', event.target); + reject(event.target.error); + }; + }); + } + + /* + * deletes keys that are in storage but are not in given array + * @param ids array of ids + * @return array of ids that are given but not in db + */ + async sync(ids) { + const allKeys = await this.getAllKeys(); + const toDelete = allKeys.filter((i) => !ids.includes(i)); + if (toDelete.length) { + console.log('Templaes: Keys in db but not in store', toDelete); + await this.deleteFile(toDelete); + } + const deadIds = ids.filter((i) => !allKeys.includes(i)); + if (deadIds.length) { + console.log('Templates: Keys in store but not in db', deadIds); + } + return deadIds; + } +} + +export default FileStorage; diff --git a/src/utils/imageFiles.js b/src/utils/imageFiles.js new file mode 100644 index 00000000..79d6dc08 --- /dev/null +++ b/src/utils/imageFiles.js @@ -0,0 +1,71 @@ +/** + * function relating image and files + */ + +/* + * read image element into canvas + * @param img image element + * @return canvas + */ +export function imageToCanvas(img) { + const canvas = document.createElement('canvas'); + canvas.width = img.naturalWidth; + canvas.height = img.naturalHeight; + const context = canvas.getContext('2d'); + context.drawImage(img, 0, 0); + return canvas; +} + +/* + * read File object into canvas + * @param file + * @return HTMLCanvas + */ +export function fileToCanvas(file) { + return new Promise((resolve, reject) => { + const fr = new FileReader(); + fr.onload = () => { + const img = new Image(); + img.onload = () => { + resolve(imageToCanvas(img)); + }; + img.onerror = (error) => reject(error); + img.src = fr.result; + }; + fr.onerror = (error) => reject(error); + fr.readAsDataURL(file); + }); +} + +/* + * read fimage file from arraybuffer into canvas + * @param Buffer + * @param type mimetype + * @return HTMLCanvas + */ +export function bufferToCanvas(buffer, type) { + const blob = new Blob([buffer], { type }); + return fileToCanvas(blob); +} + +/* + * read canvas into arraybuffer + * @param canvas + * @param type optional, defaults to image/png + * @return buffer + */ +export function canvasToBuffer(canvas, type) { + return new Promise((resolve, reject) => { + canvas.toBlob((blob) => { + blob.arrayBuffer().then(resolve).catch(reject); + }, type); + }); +} + +/* + * convert canvas into base64 encoded png + * @param canvas + * @return base64 + */ +export function canvasToBas64PNG() { +} diff --git a/src/utils/image.js b/src/utils/imageFilters.js similarity index 92% rename from src/utils/image.js rename to src/utils/imageFilters.js index d34e25b3..a058f934 100644 --- a/src/utils/image.js +++ b/src/utils/imageFilters.js @@ -101,32 +101,6 @@ export function scaleImage(imgCanvas, width, height, doAA) { return can; } -/* - * read File object into canvas - * @param file - * @return HTMLCanvas - */ -export function readFileIntoCanvas(file) { - return new Promise((resolve, reject) => { - const fr = new FileReader(); - fr.onload = () => { - const img = new Image(); - img.onload = () => { - const cani = document.createElement('canvas'); - cani.width = img.width; - cani.height = img.height; - const ctxi = cani.getContext('2d'); - ctxi.drawImage(img, 0, 0); - resolve(cani); - }; - img.onerror = (error) => reject(error); - img.src = fr.result; - }; - fr.onerror = (error) => reject(error); - fr.readAsDataURL(file); - }); -} - /* * converts pointContainer to HTMLCanvas * @param pointContainer