add indexedDB based file storage and start with template ui

This commit is contained in:
HF 2024-01-29 02:51:50 +01:00
parent 1768dd88bf
commit 372b4c60a9
19 changed files with 1088 additions and 38 deletions

View File

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

View File

@ -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 (
<div
className="inarea"
style={{
opacity: render ? 1 : 0,
transition: 'opacity 200ms',
}}
onTransitionEnd={() => !render && close()}
>
<form
onSubmit={async (event) => {
event.preventDefault();
if (canSubmit) {
await templateLoader.addFile(
file, title, selectedCanvas, ...coords, imgRef.current,
);
setRender(false);
}
}}
>
<img
src="./logo.svg"
alt="preview"
key="logo"
style={{
maxWidth: 96,
maxHeight: 96,
}}
onLoad={(evt) => setDimensions([
evt.target.naturalWidth,
evt.target.naturalHeight,
])}
onError={() => setDimensions(null)}
ref={imgRef}
/>
<input
type="file"
onChange={(evt) => {
setDimensions(null);
setFile(evt.target.files?.[0]);
}}
/>
<input
value={title}
type="text"
onChange={(evt) => setTitle(evt.target.value)}
placeholder={t`Template Name`}
/>
<select
value={selectedCanvas}
onChange={(e) => {
const sel = e.target;
selectCanvas(sel.options[sel.selectedIndex].value);
}}
>
{Object.keys(canvases).filter((c) => !canvases[c].v).map((canvas) => (
<option key={canvas} value={canvas}>
{canvases[canvas].title}
</option>
))}
</select>
<input
type="text"
style={{
display: 'inline-block',
width: '100%',
maxWidth: '15em',
}}
placeholder="X_Y or URL"
onChange={(evt) => {
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,
);
}}
/>
<button type="submit" disabled={!canSubmit}>{t`Save`}</button>
</form>
</div>
);
};
export default React.memo(AddTemplate);

View File

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

View File

@ -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 (
<div
className={(enabled) ? 'tmpitm' : 'tmpitm disabled'}
onClick={() => dispatch(changeTemplate(title, { enabled: !enabled }))}
>
<div className="tmpitm-preview">
<canvas
className="tmpitm-img"
ref={imgRef}
width={width}
height={height}
/>
</div>
<div className="tmpitm-desc">
<h4>{title}</h4>
<p>
{t`Canvas`}:&nbsp;<span>{canvases[canvasId]?.title}</span>
</p>
<p>
{t`Coordinates`}:&nbsp;<span>{`${x},${y}`}</span>
</p>
<p>
{t`Dimensions`}:&nbsp;<span>{`${width} x ${height}`}</span>
</p>
</div>
<div className="tmpitm-actions">
<button
onClick={(evt) => {
evt.stopPropagation();
startEditing(title);
}}
>
{t`Edit`}
</button>
<button
onClick={(evt) => {
evt.stopPropagation();
dispatch(selectCanvas(canvasId));
dispatch(setViewCoordinates([x + width / 2, y + height / 2]));
}}
>
{t`Go to`}
</button>
</div>
</div>
);
};
export default React.memo(TemplateItem);

View File

@ -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 (
<div className="tmpitm">
<div className="tmpitm-preview">
<canvas
className="tmpitm-img"
ref={imgRef}
style={{ opacity: 0.4 }}
/>
<div className="centered-on-img modallink">{t`Select File`}</div>
</div>
<div className="tmpitm-desc">
<h4><input
value={title}
style={{ width: '10em' }}
type="text"
onChange={(evt) => setTitle(evt.target.value)}
placeholder={t`Template Name`}
/></h4>
<p>{t`Canvas`}:&nbsp;
<span><select
value={selectedCanvas}
onChange={(e) => {
const sel = e.target;
selectCanvas(sel.options[sel.selectedIndex].value);
}}
>
{Object.keys(canvases).filter((c) => !canvases[c].v).map((canvas) => (
<option key={canvas} value={canvas}>
{canvases[canvas].title}
</option>
))}
</select></span>
</p>
<p>
{t`Coordinates`}:&nbsp;
<span><input
type="text"
defaultValue={coords && coords.join('_')}
style={{
display: 'inline-block',
maxWidth: '8em',
}}
placeholder="X_Y or URL"
onChange={(evt) => {
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,
);
}}
/></span>
</p>
<p>
{t`Dimensions`}:&nbsp;<span>{(dimensions) ? dimensions.join(' x ') : 'N/A'}</span>
</p>
</div>
<div className="tmpitm-actions">
<button>
{t`Delete`}
</button>
<button
onClick={(evt) => {
evt.stopPropagation();
stopEditing(title);
}}
>
{t`Cancel`}
</button>
<button
disabled={!canSubmit}
>
{t`Save`}
</button>
</div>
</div>
);
};
export default React.memo(TemplateItem);

View File

@ -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 (
<div className="content">
{list.map(({
enabled, imageId, canvasId, title, x, y, width, height,
}, index) => (editingIndices.includes(index) ? (
<TemplateItemEdit
enabled={enabled}
key={index}
title={title}
imageId={imageId}
canvasId={canvasId}
x={x}
y={y}
width={width}
height={height}
stopEditing={toggleEditing}
/>
) : (
<TemplateItem
enabled={enabled}
key={index}
title={title}
imageId={imageId}
canvasId={canvasId}
x={x}
y={y}
width={width}
height={height}
startEditing={toggleEditing}
/>
)))}
{(showAdd) ? (
<span
role="button"
tabIndex={-1}
className="modallink"
onClick={() => refClose.current?.()}
> {t`Cancel adding Template`}</span>
) : (
<span
role="button"
tabIndex={-1}
className="modallink"
onClick={() => setShowAdd(true)}
> {t`Add Template`}</span>
)}
{showAdd && <AddTemplate close={close} triggerClose={refClose} />}
</div>
);
};
export default TemplateSettings;

View File

@ -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 = () => {
</div>
</div>
)}
<div className="modaldivider" />
<h3>{t`Templates`}</h3>
<p>
{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.`}
</p>
<TemplateSettings />
</div>
);
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

55
src/ui/Template.js Normal file
View File

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

131
src/ui/templateLoader.js Normal file
View File

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

199
src/utils/FileStorage.js Normal file
View File

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

71
src/utils/imageFiles.js Normal file
View File

@ -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() {
}

View File

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