forked from ppfun/pixelplanet
add indexedDB based file storage and start with template ui
This commit is contained in:
parent
1768dd88bf
commit
372b4c60a9
|
@ -21,6 +21,7 @@ import pixelTransferController from './ui/PixelTransferController';
|
||||||
import store from './store/store';
|
import store from './store/store';
|
||||||
import renderApp from './components/App';
|
import renderApp from './components/App';
|
||||||
import { getRenderer } from './ui/rendererFactory';
|
import { getRenderer } from './ui/rendererFactory';
|
||||||
|
import templateLoader from './ui/templateLoader';
|
||||||
import socketClient from './socket/SocketClient';
|
import socketClient from './socket/SocketClient';
|
||||||
import { GC_INTERVAL } from './core/constants';
|
import { GC_INTERVAL } from './core/constants';
|
||||||
|
|
||||||
|
@ -31,6 +32,9 @@ persistStore(store, {}, () => {
|
||||||
|
|
||||||
pixelTransferController.initialize(store, socketClient, getRenderer);
|
pixelTransferController.initialize(store, socketClient, getRenderer);
|
||||||
|
|
||||||
|
// TODO should be in middleware
|
||||||
|
templateLoader.initialize(store);
|
||||||
|
|
||||||
window.addEventListener('hashchange', () => {
|
window.addEventListener('hashchange', () => {
|
||||||
store.dispatch(urlChange());
|
store.dispatch(urlChange());
|
||||||
});
|
});
|
||||||
|
|
129
src/components/AddTemplate.jsx
Normal file
129
src/components/AddTemplate.jsx
Normal 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);
|
|
@ -10,11 +10,11 @@ import { jt, t } from 'ttag';
|
||||||
import {
|
import {
|
||||||
ColorDistanceCalculators,
|
ColorDistanceCalculators,
|
||||||
ImageQuantizerKernels,
|
ImageQuantizerKernels,
|
||||||
readFileIntoCanvas,
|
|
||||||
scaleImage,
|
scaleImage,
|
||||||
quantizeImage,
|
quantizeImage,
|
||||||
addGrid,
|
addGrid,
|
||||||
} from '../utils/image';
|
} from '../utils/imageFilters';
|
||||||
|
import { fileToCanvas } from '../utils/imageFiles';
|
||||||
import printGIMPPalette from '../core/exportGPL';
|
import printGIMPPalette from '../core/exportGPL';
|
||||||
import { copyCanvasToClipboard } from '../utils/clipboard';
|
import { copyCanvasToClipboard } from '../utils/clipboard';
|
||||||
|
|
||||||
|
@ -247,7 +247,7 @@ function Converter() {
|
||||||
const fileSel = evt.target;
|
const fileSel = evt.target;
|
||||||
const file = (!fileSel.files || !fileSel.files[0])
|
const file = (!fileSel.files || !fileSel.files[0])
|
||||||
? null : fileSel.files[0];
|
? null : fileSel.files[0];
|
||||||
const imageData = await readFileIntoCanvas(file);
|
const imageData = await fileToCanvas(file);
|
||||||
setInputImageCanvas(null);
|
setInputImageCanvas(null);
|
||||||
setScaleData({
|
setScaleData({
|
||||||
enabled: false,
|
enabled: false,
|
||||||
|
|
82
src/components/TemplateItem.jsx
Normal file
82
src/components/TemplateItem.jsx
Normal 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`}: <span>{canvases[canvasId]?.title}</span>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
{t`Coordinates`}: <span>{`${x},${y}`}</span>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
{t`Dimensions`}: <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);
|
145
src/components/TemplateItemEdit.jsx
Normal file
145
src/components/TemplateItemEdit.jsx
Normal 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`}:
|
||||||
|
<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`}:
|
||||||
|
<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`}: <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);
|
82
src/components/TemplateSettings.jsx
Normal file
82
src/components/TemplateSettings.jsx
Normal 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;
|
|
@ -8,6 +8,7 @@ import { c, t } from 'ttag';
|
||||||
|
|
||||||
import SettingsItem from '../SettingsItem';
|
import SettingsItem from '../SettingsItem';
|
||||||
import LanguageSelect from '../LanguageSelect';
|
import LanguageSelect from '../LanguageSelect';
|
||||||
|
import TemplateSettings from '../TemplateSettings';
|
||||||
import {
|
import {
|
||||||
toggleGrid,
|
toggleGrid,
|
||||||
togglePixelNotify,
|
togglePixelNotify,
|
||||||
|
@ -80,7 +81,6 @@ const Settings = () => {
|
||||||
state.canvas.isHistoricalView,
|
state.canvas.isHistoricalView,
|
||||||
], shallowEqual);
|
], shallowEqual);
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
const audioAvailable = window.AudioContext || window.webkitAudioContext;
|
const audioAvailable = window.AudioContext || window.webkitAudioContext;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -192,6 +192,12 @@ const Settings = () => {
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
31
src/store/actions/templates.js
Normal file
31
src/store/actions/templates.js
Normal 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,
|
||||||
|
};
|
||||||
|
}
|
|
@ -64,6 +64,7 @@ export default (store) => (next) => (action) => {
|
||||||
initRenderer(store, is3D);
|
initRenderer(store, is3D);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO this looks shade, i.e. when a nwe Renderer appears
|
||||||
if (state.canvas.isHistoricalView) {
|
if (state.canvas.isHistoricalView) {
|
||||||
const {
|
const {
|
||||||
historicalDate,
|
historicalDate,
|
||||||
|
|
77
src/store/reducers/templates.js
Normal file
77
src/store/reducers/templates.js
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -12,6 +12,7 @@ import ranks from './reducers/ranks';
|
||||||
import chatRead from './reducers/chatRead';
|
import chatRead from './reducers/chatRead';
|
||||||
import user from './reducers/user';
|
import user from './reducers/user';
|
||||||
import canvas from './reducers/canvas';
|
import canvas from './reducers/canvas';
|
||||||
|
import templates from './reducers/templates';
|
||||||
import chat from './reducers/chat';
|
import chat from './reducers/chat';
|
||||||
import fetching from './reducers/fetching';
|
import fetching from './reducers/fetching';
|
||||||
|
|
||||||
|
@ -48,10 +49,18 @@ const chatReadPersist = persistReducer({
|
||||||
migrate,
|
migrate,
|
||||||
}, chatRead);
|
}, chatRead);
|
||||||
|
|
||||||
|
const templatesPersist = persistReducer({
|
||||||
|
key: 'tem',
|
||||||
|
storage,
|
||||||
|
version: CURRENT_VERSION,
|
||||||
|
migrate,
|
||||||
|
}, templates);
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
gui: guiPersist,
|
gui: guiPersist,
|
||||||
ranks: ranksPersist,
|
ranks: ranksPersist,
|
||||||
chatRead: chatReadPersist,
|
chatRead: chatReadPersist,
|
||||||
|
templates: templatesPersist,
|
||||||
user,
|
user,
|
||||||
canvas,
|
canvas,
|
||||||
chat,
|
chat,
|
||||||
|
|
|
@ -186,7 +186,6 @@ h3 {
|
||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
h4 {
|
h4 {
|
||||||
|
@ -309,6 +308,63 @@ tr:nth-child(even) {
|
||||||
color: #212121;
|
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 {
|
.cvbtn {
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
|
@ -576,7 +632,7 @@ tr:nth-child(even) {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.Alert p,.window p {
|
.Alert p {
|
||||||
margin-top: 12px;
|
margin-top: 12px;
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
}
|
}
|
||||||
|
@ -594,7 +650,6 @@ tr:nth-child(even) {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
line-height: 20px;
|
line-height: 20px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
margin-top: 4px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.modaldivider {
|
.modaldivider {
|
||||||
|
@ -612,7 +667,7 @@ tr:nth-child(even) {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modalinfo {
|
.modalinfo, .tmpitm-desc span {
|
||||||
color: #4f545c;
|
color: #4f545c;
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
|
@ -643,6 +698,7 @@ tr:nth-child(even) {
|
||||||
|
|
||||||
.setitem h3 {
|
.setitem h3 {
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
|
margin-bottom: 1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.setitem {
|
.setitem {
|
||||||
|
|
|
@ -20,12 +20,11 @@ import {
|
||||||
class ChunkLoader {
|
class ChunkLoader {
|
||||||
#store;
|
#store;
|
||||||
// Map of chunkId: chunkRGB
|
// Map of chunkId: chunkRGB
|
||||||
#chunks;
|
#chunks = new Map();
|
||||||
#canvasId;
|
#canvasId;
|
||||||
|
|
||||||
constructor(store, canvasId) {
|
constructor(store, canvasId) {
|
||||||
this.#store = store;
|
this.#store = store;
|
||||||
this.#chunks = new Map();
|
|
||||||
this.#canvasId = canvasId;
|
this.#canvasId = canvasId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -491,9 +491,8 @@ class Renderer3D extends Renderer {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
onDocumentTouchMove(event) {
|
onDocumentTouchMove() {
|
||||||
if (this.rollOverMesh.position.y !== -10) {
|
if (this.rollOverMesh.position.y !== -10) {
|
||||||
console.log('unset hover');
|
|
||||||
this.store.dispatch(unsetHover());
|
this.store.dispatch(unsetHover());
|
||||||
this.rollOverMesh.position.y = -10;
|
this.rollOverMesh.position.y = -10;
|
||||||
}
|
}
|
||||||
|
|
55
src/ui/Template.js
Normal file
55
src/ui/Template.js
Normal 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
131
src/ui/templateLoader.js
Normal 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
199
src/utils/FileStorage.js
Normal 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
71
src/utils/imageFiles.js
Normal 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() {
|
||||||
|
}
|
|
@ -101,32 +101,6 @@ export function scaleImage(imgCanvas, width, height, doAA) {
|
||||||
return can;
|
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
|
* converts pointContainer to HTMLCanvas
|
||||||
* @param pointContainer
|
* @param pointContainer
|
Loading…
Reference in New Issue
Block a user