:template editing, adding, export and import
This commit is contained in:
parent
372b4c60a9
commit
ddd692bcbd
|
@ -14,7 +14,6 @@ const TemplateItem = ({
|
||||||
enabled, title, canvasId, x, y, width, height, imageId, startEditing,
|
enabled, title, canvasId, x, y, width, height, imageId, startEditing,
|
||||||
}) => {
|
}) => {
|
||||||
const imgRef = useRef();
|
const imgRef = useRef();
|
||||||
const [canvasTitle, setCanvasTitle] = useState('Earth');
|
|
||||||
const canvases = useSelector((state) => state.canvas.canvases);
|
const canvases = useSelector((state) => state.canvas.canvases);
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
@ -27,19 +26,25 @@ const TemplateItem = ({
|
||||||
if (!previewImg) {
|
if (!previewImg) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
imgRef.current.getContext('2d').drawImage(previewImg, 0, 0);
|
console.log('rerendering image', imageId, previewImg);
|
||||||
|
const bitmap = await createImageBitmap(previewImg);
|
||||||
|
imgRef.current.getContext('bitmaprenderer')
|
||||||
|
.transferFromImageBitmap(bitmap);
|
||||||
|
bitmap.close();
|
||||||
})();
|
})();
|
||||||
}, [imageId]);
|
}, [imageId]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={(enabled) ? 'tmpitm' : 'tmpitm disabled'}
|
className={(enabled) ? 'tmpitm' : 'tmpitm disabled'}
|
||||||
|
style={{ cursor: 'pointer' }}
|
||||||
onClick={() => dispatch(changeTemplate(title, { enabled: !enabled }))}
|
onClick={() => dispatch(changeTemplate(title, { enabled: !enabled }))}
|
||||||
>
|
>
|
||||||
<div className="tmpitm-preview">
|
<div className="tmpitm-preview">
|
||||||
<canvas
|
<canvas
|
||||||
className="tmpitm-img"
|
className="tmpitm-img"
|
||||||
ref={imgRef}
|
ref={imgRef}
|
||||||
|
key="showimg"
|
||||||
width={width}
|
width={width}
|
||||||
height={height}
|
height={height}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -5,44 +5,41 @@
|
||||||
import React, {
|
import React, {
|
||||||
useRef, useState, useEffect, useMemo,
|
useRef, useState, useEffect, useMemo,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import { useDispatch, useSelector, shallowEqual } from 'react-redux';
|
import { useSelector, shallowEqual } from 'react-redux';
|
||||||
import { t } from 'ttag';
|
import { t } from 'ttag';
|
||||||
|
|
||||||
import templateLoader from '../ui/templateLoader';
|
import templateLoader from '../ui/templateLoader';
|
||||||
import { changeTemplate } from '../store/actions/templates';
|
|
||||||
import { selectCanvas, setViewCoordinates } from '../store/actions';
|
|
||||||
import { coordsFromUrl } from '../core/utils';
|
import { coordsFromUrl } from '../core/utils';
|
||||||
|
|
||||||
const TemplateItem = ({
|
const TemplateItemEdit = ({
|
||||||
title: initTitle,
|
title: initTitle,
|
||||||
canvasId: initCanvasId,
|
canvasId: initCanvasId,
|
||||||
x: initX, y: initY,
|
x: initX, y: initY,
|
||||||
width: initWidth, height: initHeight,
|
imageId,
|
||||||
imageId: initImageId,
|
|
||||||
stopEditing,
|
stopEditing,
|
||||||
}) => {
|
}) => {
|
||||||
const [initCoords, initDimensions] = useMemo(() => [
|
const [initCoords, initDimensions] = useMemo(() => [
|
||||||
(Number.isNaN(parseInt(initX, 10))
|
(Number.isNaN(parseInt(initX, 10))
|
||||||
|| Number.isNaN(parseInt(initY, 10))) ? null : [initX, initY],
|
|| Number.isNaN(parseInt(initY, 10))) ? null : [initX, initY],
|
||||||
(Number.isNaN(parseInt(initWidth, 10))
|
], [initX, initY]);
|
||||||
|| Number.isNaN(parseInt(initHeight, 10))) ? null : [initWidth, initHeight],
|
|
||||||
], [initX, initY, initWidth, initHeight]);
|
|
||||||
|
|
||||||
const [coords, setCoords] = useState(initCoords);
|
const [coords, setCoords] = useState(initCoords);
|
||||||
const [dimensions, setDimensions] = useState(initDimensions);
|
const [dimensions, setDimensions] = useState(initDimensions);
|
||||||
const [title, setTitle] = useState(initTitle);
|
const [title, setTitle] = useState(initTitle || '');
|
||||||
const [file, setFile] = useState(null);
|
const [file, setFile] = useState(null);
|
||||||
const [imageId, setImageId] = useState(initImageId);
|
const [titleUnique, setTitleUnique] = useState(true);
|
||||||
const imgRef = useRef();
|
const imgRef = useRef();
|
||||||
|
const fileRef = useRef();
|
||||||
const [
|
const [
|
||||||
storeCanvasId,
|
storeCanvasId,
|
||||||
canvases,
|
canvases,
|
||||||
|
templateList,
|
||||||
] = useSelector((state) => [
|
] = useSelector((state) => [
|
||||||
state.canvas.canvasId,
|
state.canvas.canvasId,
|
||||||
state.canvas.canvases,
|
state.canvas.canvases,
|
||||||
|
state.templates.list,
|
||||||
], shallowEqual);
|
], shallowEqual);
|
||||||
const [selectedCanvas, selectCanvas] = useState(initCanvasId ?? storeCanvasId);
|
const [canvasId, selectCanvas] = useState(initCanvasId ?? storeCanvasId);
|
||||||
const dispatch = useDispatch();
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(async () => {
|
(async () => {
|
||||||
|
@ -53,36 +50,78 @@ const TemplateItem = ({
|
||||||
if (!previewImg) {
|
if (!previewImg) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const bitmap = await createImageBitmap(previewImg);
|
||||||
const canvas = imgRef.current;
|
const canvas = imgRef.current;
|
||||||
canvas.width = previewImg.width;
|
const { width, height } = bitmap;
|
||||||
canvas.height = previewImg.height;
|
canvas.width = width;
|
||||||
canvas.getContext('2d').drawImage(previewImg, 0, 0);
|
canvas.height = height;
|
||||||
|
canvas.getContext('bitmaprenderer').transferFromImageBitmap(bitmap);
|
||||||
|
setDimensions([width, height]);
|
||||||
|
bitmap.close();
|
||||||
})();
|
})();
|
||||||
}, [imageId]);
|
}, [imageId]);
|
||||||
|
|
||||||
const canSubmit = (imgRef.current && file && coords && title && dimensions);
|
useEffect(() => {
|
||||||
|
if (!file || !imgRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
(async () => {
|
||||||
|
const bitmap = await createImageBitmap(file);
|
||||||
|
const canvas = imgRef.current;
|
||||||
|
const { width, height } = bitmap;
|
||||||
|
canvas.width = width;
|
||||||
|
canvas.height = height;
|
||||||
|
canvas.getContext('bitmaprenderer').transferFromImageBitmap(bitmap);
|
||||||
|
setDimensions([width, height]);
|
||||||
|
bitmap.close();
|
||||||
|
})();
|
||||||
|
}, [file]);
|
||||||
|
|
||||||
|
const canSubmit = (imgRef.current && (file || imageId)
|
||||||
|
&& titleUnique && coords && title && dimensions);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="tmpitm">
|
<div className="tmpitm">
|
||||||
<div className="tmpitm-preview">
|
<div className="tmpitm-preview">
|
||||||
<canvas
|
<div style={{ width: '100%', height: '100%' }}>
|
||||||
className="tmpitm-img"
|
<canvas
|
||||||
ref={imgRef}
|
className="tmpitm-img"
|
||||||
style={{ opacity: 0.4 }}
|
ref={imgRef}
|
||||||
|
key="editimg"
|
||||||
|
style={{ opacity: 0.4 }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="centered-on-img modallink"
|
||||||
|
onClick={(evt) => fileRef.current?.click()}
|
||||||
|
>{t`Select File`}</div>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
key="hinpt"
|
||||||
|
accept="image/*"
|
||||||
|
ref={fileRef}
|
||||||
|
style={{ display: 'none' }}
|
||||||
|
onChange={(evt) => {
|
||||||
|
setDimensions(null);
|
||||||
|
setFile(evt.target.files?.[0]);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<div className="centered-on-img modallink">{t`Select File`}</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="tmpitm-desc">
|
<div className="tmpitm-desc">
|
||||||
<h4><input
|
<h4><input
|
||||||
value={title}
|
value={title}
|
||||||
style={{ width: '10em' }}
|
style={{ width: '10em' }}
|
||||||
type="text"
|
type="text"
|
||||||
onChange={(evt) => setTitle(evt.target.value)}
|
onChange={(evt) => {
|
||||||
|
const newTitle = evt.target.value;
|
||||||
|
setTitleUnique(!templateList.some((t) => t.title === newTitle));
|
||||||
|
setTitle(evt.target.value);
|
||||||
|
}}
|
||||||
placeholder={t`Template Name`}
|
placeholder={t`Template Name`}
|
||||||
/></h4>
|
/></h4>
|
||||||
<p>{t`Canvas`}:
|
<p>{t`Canvas`}:
|
||||||
<span><select
|
<span><select
|
||||||
value={selectedCanvas}
|
value={canvasId}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const sel = e.target;
|
const sel = e.target;
|
||||||
selectCanvas(sel.options[sel.selectedIndex].value);
|
selectCanvas(sel.options[sel.selectedIndex].value);
|
||||||
|
@ -117,23 +156,54 @@ const TemplateItem = ({
|
||||||
/></span>
|
/></span>
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
{t`Dimensions`}: <span>{(dimensions) ? dimensions.join(' x ') : 'N/A'}</span>
|
{t`Dimensions`}: <span>
|
||||||
|
{(dimensions) ? dimensions.join(' x ') : 'N/A'}
|
||||||
|
</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="tmpitm-actions">
|
<div className="tmpitm-actions">
|
||||||
<button>
|
{(initTitle) && (
|
||||||
{t`Delete`}
|
<button
|
||||||
</button>
|
onClick={() => {
|
||||||
|
stopEditing(initTitle);
|
||||||
|
templateLoader.deleteTemplate(initTitle);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t`Delete`}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
<button
|
<button
|
||||||
onClick={(evt) => {
|
onClick={(evt) => stopEditing(title)}
|
||||||
evt.stopPropagation();
|
|
||||||
stopEditing(title);
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{t`Cancel`}
|
{t`Cancel`}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
disabled={!canSubmit}
|
disabled={!canSubmit}
|
||||||
|
onClick={async (evt) => {
|
||||||
|
if (!canSubmit) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const [x, y] = coords;
|
||||||
|
if (!initTitle) {
|
||||||
|
console.log('Create new template');
|
||||||
|
await templateLoader.addFile(file, title, canvasId, x, y);
|
||||||
|
} else {
|
||||||
|
if (file && imageId) {
|
||||||
|
console.log('file changed for id', imageId);
|
||||||
|
await templateLoader.updateFile(imageId, file);
|
||||||
|
}
|
||||||
|
if (initTitle
|
||||||
|
&& (initTitle !== title || initX !== x
|
||||||
|
|| initY !== y || initCanvasId !== canvasId
|
||||||
|
)) {
|
||||||
|
console.log(`template ${title} changed`);
|
||||||
|
templateLoader.changeTemplate(initTitle, {
|
||||||
|
title, canvasId, x, y,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stopEditing(initTitle);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{t`Save`}
|
{t`Save`}
|
||||||
</button>
|
</button>
|
||||||
|
@ -142,4 +212,4 @@ const TemplateItem = ({
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default React.memo(TemplateItem);
|
export default React.memo(TemplateItemEdit);
|
||||||
|
|
|
@ -4,18 +4,21 @@
|
||||||
|
|
||||||
import React, { useState, useCallback, useRef } from 'react';
|
import React, { useState, useCallback, useRef } from 'react';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
|
import fileDownload from 'js-file-download';
|
||||||
import { t } from 'ttag';
|
import { t } from 'ttag';
|
||||||
|
|
||||||
import AddTemplate from './AddTemplate';
|
import AddTemplate from './AddTemplate';
|
||||||
import TemplateItem from './TemplateItem';
|
import TemplateItem from './TemplateItem';
|
||||||
import TemplateItemEdit from './TemplateItemEdit';
|
import TemplateItemEdit from './TemplateItemEdit';
|
||||||
|
import templateLoader from '../ui/templateLoader';
|
||||||
|
|
||||||
|
|
||||||
const TemplateSettings = () => {
|
const TemplateSettings = () => {
|
||||||
const [showAdd, setShowAdd] = useState(false);
|
const [showAdd, setShowAdd] = useState(false);
|
||||||
const list = useSelector((state) => state.templates.list);
|
const list = useSelector((state) => state.templates.list);
|
||||||
const [editingIndices, setEditingIndices] = useState([]);
|
const [editingIndices, setEditingIndices] = useState([]);
|
||||||
const close = useCallback(() => setShowAdd(false), []);
|
const close = useCallback(() => setShowAdd(false), []);
|
||||||
const refClose = useRef();
|
const importRef = useRef();
|
||||||
|
|
||||||
const toggleEditing = useCallback((title) => {
|
const toggleEditing = useCallback((title) => {
|
||||||
const index = list.findIndex((t) => t.title === title);
|
const index = list.findIndex((t) => t.title === title);
|
||||||
|
@ -24,58 +27,95 @@ const TemplateSettings = () => {
|
||||||
? [...editingIndices, index]
|
? [...editingIndices, index]
|
||||||
: editingIndices.toSpliced(ind, 1),
|
: editingIndices.toSpliced(ind, 1),
|
||||||
);
|
);
|
||||||
}, [editingIndices]);
|
}, [list, editingIndices]);
|
||||||
|
|
||||||
console.log('editing', editingIndices);
|
console.log('list', list);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="content">
|
<>
|
||||||
{list.map(({
|
<h3>{t`Templates`}</h3>
|
||||||
enabled, imageId, canvasId, title, x, y, width, height,
|
<p>
|
||||||
}, index) => (editingIndices.includes(index) ? (
|
{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.`}
|
||||||
<TemplateItemEdit
|
</p>
|
||||||
enabled={enabled}
|
<div className="content">
|
||||||
key={index}
|
{list.map(({
|
||||||
title={title}
|
enabled, imageId, canvasId, title, x, y, width, height,
|
||||||
imageId={imageId}
|
}, index) => (editingIndices.includes(index) ? (
|
||||||
canvasId={canvasId}
|
<TemplateItemEdit
|
||||||
x={x}
|
enabled={enabled}
|
||||||
y={y}
|
key={index}
|
||||||
width={width}
|
title={title}
|
||||||
height={height}
|
imageId={imageId}
|
||||||
stopEditing={toggleEditing}
|
canvasId={canvasId}
|
||||||
/>
|
x={x}
|
||||||
) : (
|
y={y}
|
||||||
<TemplateItem
|
stopEditing={toggleEditing}
|
||||||
enabled={enabled}
|
/>
|
||||||
key={index}
|
) : (
|
||||||
title={title}
|
<TemplateItem
|
||||||
imageId={imageId}
|
enabled={enabled}
|
||||||
canvasId={canvasId}
|
key={index}
|
||||||
x={x}
|
title={title}
|
||||||
y={y}
|
imageId={imageId}
|
||||||
width={width}
|
canvasId={canvasId}
|
||||||
height={height}
|
x={x}
|
||||||
startEditing={toggleEditing}
|
y={y}
|
||||||
/>
|
width={width}
|
||||||
)))}
|
height={height}
|
||||||
{(showAdd) ? (
|
startEditing={toggleEditing}
|
||||||
|
/>
|
||||||
|
)))}
|
||||||
|
{showAdd && <TemplateItemEdit stopEditing={close} />}
|
||||||
|
{(showAdd) ? (
|
||||||
|
<span
|
||||||
|
role="button"
|
||||||
|
tabIndex={-1}
|
||||||
|
className="modallink"
|
||||||
|
onClick={() => close()}
|
||||||
|
> {t`Cancel adding Template`}</span>
|
||||||
|
) : (
|
||||||
|
<span
|
||||||
|
role="button"
|
||||||
|
tabIndex={-1}
|
||||||
|
className="modallink"
|
||||||
|
onClick={() => setShowAdd(true)}
|
||||||
|
> {t`Add Template`}</span>
|
||||||
|
)}
|
||||||
|
{(list.some((z) => z.enabled)) && (
|
||||||
|
<React.Fragment key="exps">
|
||||||
|
|
|
||||||
|
<span
|
||||||
|
role="button"
|
||||||
|
tabIndex={-1}
|
||||||
|
className="modallink"
|
||||||
|
onClick={async () => {
|
||||||
|
const data = await templateLoader.exportEnabledTemplates();
|
||||||
|
fileDownload(
|
||||||
|
JSON.stringify(data), 'PixelplanetTemplates.json',
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>{t`Export enabled templates`}</span>
|
||||||
|
</React.Fragment>
|
||||||
|
)}
|
||||||
|
|
|
||||||
<span
|
<span
|
||||||
role="button"
|
role="button"
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
className="modallink"
|
className="modallink"
|
||||||
onClick={() => refClose.current?.()}
|
onClick={async () => importRef.current?.click()}
|
||||||
> {t`Cancel adding Template`}</span>
|
>{t`Import templates`}</span>
|
||||||
) : (
|
<input
|
||||||
<span
|
type="file"
|
||||||
role="button"
|
key="impin"
|
||||||
tabIndex={-1}
|
accept="image/*"
|
||||||
className="modallink"
|
ref={importRef}
|
||||||
onClick={() => setShowAdd(true)}
|
style={{ display: 'none' }}
|
||||||
> {t`Add Template`}</span>
|
onChange={(evt) => {
|
||||||
)}
|
templateLoader.importTemplates(evt.target.files?.[0]);
|
||||||
{showAdd && <AddTemplate close={close} triggerClose={refClose} />}
|
}}
|
||||||
</div>
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -67,6 +67,7 @@ const Settings = () => {
|
||||||
isMuted,
|
isMuted,
|
||||||
chatNotify,
|
chatNotify,
|
||||||
isHistoricalView,
|
isHistoricalView,
|
||||||
|
templatesAvailable,
|
||||||
] = useSelector((state) => [
|
] = useSelector((state) => [
|
||||||
state.gui.showGrid,
|
state.gui.showGrid,
|
||||||
state.gui.showPixelNotify,
|
state.gui.showPixelNotify,
|
||||||
|
@ -79,6 +80,7 @@ const Settings = () => {
|
||||||
state.gui.mute,
|
state.gui.mute,
|
||||||
state.gui.chatNotify,
|
state.gui.chatNotify,
|
||||||
state.canvas.isHistoricalView,
|
state.canvas.isHistoricalView,
|
||||||
|
state.templates.available,
|
||||||
], shallowEqual);
|
], shallowEqual);
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const audioAvailable = window.AudioContext || window.webkitAudioContext;
|
const audioAvailable = window.AudioContext || window.webkitAudioContext;
|
||||||
|
@ -193,11 +195,7 @@ const Settings = () => {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="modaldivider" />
|
<div className="modaldivider" />
|
||||||
<h3>{t`Templates`}</h3>
|
{(templatesAvailable) && <TemplateSettings />}
|
||||||
<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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -688,3 +688,25 @@ export function parentExists() {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function bufferToBase64(array) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const blob = new Blob([array]);
|
||||||
|
const reader = new FileReader();
|
||||||
|
|
||||||
|
reader.onload = (event) => {
|
||||||
|
const dataUrl = event.target.result;
|
||||||
|
const [_, base64] = dataUrl.split(',');
|
||||||
|
|
||||||
|
resolve(base64);
|
||||||
|
};
|
||||||
|
|
||||||
|
reader.readAsDataURL(blob);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function base64ToBuffer(base64) {
|
||||||
|
const dataUrl = "data:application/octet-binary;base64," + base64;
|
||||||
|
const res = await fetch(dataUrl);
|
||||||
|
return res.arrayBuffer();
|
||||||
|
}
|
||||||
|
|
|
@ -15,6 +15,12 @@ export function listTemplate(imageId, title, canvasId, x, y, width, height) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function templatesReady(title) {
|
||||||
|
return {
|
||||||
|
type: 'TEMPLATES_READY',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function removeTemplate(title) {
|
export function removeTemplate(title) {
|
||||||
return {
|
return {
|
||||||
type: 's/REM_TEMPLATE',
|
type: 's/REM_TEMPLATE',
|
||||||
|
@ -29,3 +35,13 @@ export function changeTemplate(title, props) {
|
||||||
props,
|
props,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function updatedTemplateImage(imageId, width, height) {
|
||||||
|
console.log('update', width, height, 'store');
|
||||||
|
return {
|
||||||
|
type: 's/UPD_TEMPLATE_IMG',
|
||||||
|
imageId,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
|
@ -71,6 +71,47 @@ export default function templates(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case 's/REM_TEMPLATE': {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
list: state.list.filter((t) => t.title !== action.title),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
case 's/UPD_TEMPLATE_IMG': {
|
||||||
|
const { imageId, width, height } = action;
|
||||||
|
const { list } = state;
|
||||||
|
const index = list.findIndex((t) => t.imageId === imageId);
|
||||||
|
if (index === -1) {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
list: [
|
||||||
|
...list.slice(0, index),
|
||||||
|
{
|
||||||
|
...list[index],
|
||||||
|
imageId,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
},
|
||||||
|
...list.slice(index + 1),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'TEMPLATES_READY':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
available: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
case 'persist/REHYDRATE':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
available: false,
|
||||||
|
};
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
|
@ -316,7 +316,6 @@ tr:nth-child(even) {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
cursor: pointer;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.tmpitm.disabled {
|
.tmpitm.disabled {
|
||||||
|
@ -356,9 +355,9 @@ tr:nth-child(even) {
|
||||||
|
|
||||||
.centered-on-img {
|
.centered-on-img {
|
||||||
position: relative;
|
position: relative;
|
||||||
transform: translateY(-99px);
|
transform: translateY(-96px);
|
||||||
line-height: 96px;
|
line-height: 96px;
|
||||||
background: #e6e6e6d1;
|
background: #e6e6e6a6;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
max-width: 96px;
|
max-width: 96px;
|
||||||
height: 96px;
|
height: 96px;
|
||||||
|
|
|
@ -6,7 +6,11 @@ import FileStorage from '../utils/FileStorage';
|
||||||
import {
|
import {
|
||||||
removeTemplate,
|
removeTemplate,
|
||||||
listTemplate,
|
listTemplate,
|
||||||
|
updatedTemplateImage,
|
||||||
|
changeTemplate,
|
||||||
|
templatesReady,
|
||||||
} from '../store/actions/templates';
|
} from '../store/actions/templates';
|
||||||
|
import { bufferToBase64, base64ToBuffer } from '../core/utils';
|
||||||
import Template from './Template';
|
import Template from './Template';
|
||||||
|
|
||||||
const STORE_NAME = 'templates';
|
const STORE_NAME = 'templates';
|
||||||
|
@ -20,13 +24,21 @@ class TemplateLoader {
|
||||||
ready = false;
|
ready = false;
|
||||||
|
|
||||||
async initialize(store) {
|
async initialize(store) {
|
||||||
this.#store = store;
|
try {
|
||||||
await this.#fileStorage.initialize();
|
this.#store = store;
|
||||||
await this.syncDB();
|
await this.#fileStorage.initialize();
|
||||||
this.ready = true;
|
this.ready = true;
|
||||||
|
this.#store.dispatch(templatesReady());
|
||||||
|
await this.syncDB();
|
||||||
|
} catch (err) {
|
||||||
|
console.warn(`Couldn't initialize Templates: ${err.message}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getTemplate(id) {
|
async getTemplate(id) {
|
||||||
|
if (!this.ready) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
let template = this.#templates.get(id);
|
let template = this.#templates.get(id);
|
||||||
if (template) {
|
if (template) {
|
||||||
return template.image;
|
return template.image;
|
||||||
|
@ -88,6 +100,7 @@ class TemplateLoader {
|
||||||
throw new Error('File does not exist in indexedDB');
|
throw new Error('File does not exist in indexedDB');
|
||||||
}
|
}
|
||||||
const { mimetype, buffer } = fileData;
|
const { mimetype, buffer } = fileData;
|
||||||
|
console.log('mime', mimetype, 'buffer', buffer);
|
||||||
const template = new Template(imageId);
|
const template = new Template(imageId);
|
||||||
await template.fromBuffer(buffer, mimetype);
|
await template.fromBuffer(buffer, mimetype);
|
||||||
this.#templates.set(imageId, template);
|
this.#templates.set(imageId, template);
|
||||||
|
@ -105,16 +118,11 @@ class TemplateLoader {
|
||||||
* @param element optional image or canvas element where file is already loaded,
|
* @param element optional image or canvas element where file is already loaded,
|
||||||
* can be used to avoid having to load it multiple times
|
* can be used to avoid having to load it multiple times
|
||||||
*/
|
*/
|
||||||
async addFile(file, title, canvasId, x, y, element = null) {
|
async addFile(file, title, canvasId, x, y) {
|
||||||
try {
|
try {
|
||||||
const imageId = await this.#fileStorage.saveFile(file);
|
const imageId = await this.#fileStorage.saveFile(file);
|
||||||
const template = new Template(imageId);
|
const template = new Template(imageId);
|
||||||
let dimensions;
|
const dimensions = await template.fromFile(file);
|
||||||
if (element) {
|
|
||||||
dimensions = await template.fromImage(element);
|
|
||||||
} else {
|
|
||||||
dimensions = await template.fromFile(file);
|
|
||||||
}
|
|
||||||
this.#templates.set(imageId, template);
|
this.#templates.set(imageId, template);
|
||||||
this.#store.dispatch(listTemplate(
|
this.#store.dispatch(listTemplate(
|
||||||
imageId, title, canvasId, x, y, ...dimensions,
|
imageId, title, canvasId, x, y, ...dimensions,
|
||||||
|
@ -124,6 +132,89 @@ class TemplateLoader {
|
||||||
console.error(`Error saving template ${title}: ${err.message}`);
|
console.error(`Error saving template ${title}: ${err.message}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async updateFile(imageId, file) {
|
||||||
|
try {
|
||||||
|
await this.#fileStorage.updateFile(imageId, file);
|
||||||
|
const template = new Template(imageId);
|
||||||
|
const dimensions = await template.fromFile(file);
|
||||||
|
this.#templates.set(imageId, template);
|
||||||
|
this.#store.dispatch(updatedTemplateImage(imageId, ...dimensions));
|
||||||
|
} catch (err) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.error(`Error updating file ${imageId}: ${err.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
changeTemplate(title, props) {
|
||||||
|
this.#store.dispatch(changeTemplate(title, props));
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteTemplate(title) {
|
||||||
|
const { list } = this.#store.getState().templates;
|
||||||
|
const tData = list.find((z) => z.title === title)
|
||||||
|
if (!tData) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { imageId } = tData;
|
||||||
|
this.#store.dispatch(removeTemplate(title));
|
||||||
|
this.#fileStorage.deleteFile(imageId);
|
||||||
|
this.#templates.delete(imageId);
|
||||||
|
}
|
||||||
|
|
||||||
|
async exportEnabledTemplates() {
|
||||||
|
const { list } = this.#store.getState().templates;
|
||||||
|
const tDataList = list.filter((z) => z.enabled);
|
||||||
|
const temps = await this.#fileStorage.loadFile(
|
||||||
|
tDataList.map((z) => z.imageId),
|
||||||
|
);
|
||||||
|
const serilizableObj = [];
|
||||||
|
for (let i = 0; i < tDataList.length; i += 1) {
|
||||||
|
const { buffer, mimetype } = temps[i];
|
||||||
|
console.log('mimetype', mimetype);
|
||||||
|
serilizableObj.push({
|
||||||
|
...tDataList[i],
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
buffer: await bufferToBase64(buffer),
|
||||||
|
mimetype,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return serilizableObj;
|
||||||
|
}
|
||||||
|
|
||||||
|
async importTemplates(file) {
|
||||||
|
const tDataList = JSON.parse(await file.text());
|
||||||
|
const bufferList = await Promise.all(
|
||||||
|
tDataList.map((z) => base64ToBuffer(z.buffer)),
|
||||||
|
);
|
||||||
|
const fileList = [];
|
||||||
|
for (let i = 0; i < tDataList.length; i += 1) {
|
||||||
|
const { mimetype } = tDataList[i];
|
||||||
|
console.log('mimetype', mimetype, 'buffer', bufferList[i]);
|
||||||
|
fileList.push(new Blob([bufferList[i]], { type: mimetype }));
|
||||||
|
}
|
||||||
|
const { list } = this.#store.getState().templates;
|
||||||
|
const imageIdList = await this.#fileStorage.saveFile(fileList);
|
||||||
|
const idsToDelete = [];
|
||||||
|
for (let i = 0; i < tDataList.length; i += 1) {
|
||||||
|
const { x, y, width, height, canvasId, title } = tDataList[i];
|
||||||
|
const imageId = imageIdList[i];
|
||||||
|
const existing = list.find((z) => z.title === title);
|
||||||
|
if (existing) {
|
||||||
|
idsToDelete.push(existing.imageId);
|
||||||
|
this.changeTemplate(title, {
|
||||||
|
imageId, title, canvasId, x, y, width, height,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.#store.dispatch(listTemplate(
|
||||||
|
imageId, title, canvasId, x, y, width, height,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (idsToDelete.length) {
|
||||||
|
this.#fileStorage.deleteFile(idsToDelete);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const templateLoader = new TemplateLoader();
|
const templateLoader = new TemplateLoader();
|
||||||
|
|
|
@ -49,7 +49,7 @@ class FileStorage {
|
||||||
|
|
||||||
request.onerror = () => {
|
request.onerror = () => {
|
||||||
console.error('Error on opening indexedDB:', request.error);
|
console.error('Error on opening indexedDB:', request.error);
|
||||||
reject();
|
reject(request.error);
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -74,7 +74,7 @@ class FileStorage {
|
||||||
transaction.onabort = (event) => {
|
transaction.onabort = (event) => {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
console.log('Saving files to indexedDB aborted:', event, result);
|
console.log('Saving files to indexedDB aborted:', event, result);
|
||||||
reject();
|
reject(event.target.error);
|
||||||
};
|
};
|
||||||
|
|
||||||
const os = transaction.objectStore('files');
|
const os = transaction.objectStore('files');
|
||||||
|
@ -92,6 +92,29 @@ class FileStorage {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async updateFile(id, file) {
|
||||||
|
const { db } = FileStorage;
|
||||||
|
if (!db) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const buffer = await file.arrayBuffer();
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const transaction = db.transaction('files', 'readwrite');
|
||||||
|
|
||||||
|
transaction.onabort = (event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
console.log('Saving files to indexedDB aborted:', event);
|
||||||
|
reject(event.target.error);
|
||||||
|
};
|
||||||
|
|
||||||
|
transaction.objectStore('files').put({
|
||||||
|
type: this.type,
|
||||||
|
mimetpe: file.type,
|
||||||
|
buffer,
|
||||||
|
}, id).onsuccess = (event) => resolve(event.target.result);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line class-methods-use-this
|
// eslint-disable-next-line class-methods-use-this
|
||||||
loadFile(ids) {
|
loadFile(ids) {
|
||||||
const { db } = FileStorage;
|
const { db } = FileStorage;
|
||||||
|
@ -111,7 +134,7 @@ class FileStorage {
|
||||||
transaction.onabort = (event) => {
|
transaction.onabort = (event) => {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
console.log('Loading file from indexedDB aborted:', event.target.error);
|
console.log('Loading file from indexedDB aborted:', event.target.error);
|
||||||
reject();
|
reject(event.target.error);
|
||||||
};
|
};
|
||||||
|
|
||||||
const os = transaction.objectStore('files');
|
const os = transaction.objectStore('files');
|
||||||
|
@ -143,7 +166,7 @@ class FileStorage {
|
||||||
transaction.onabort = (event) => {
|
transaction.onabort = (event) => {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
console.log('Saving files to indexedDB aborted:', event);
|
console.log('Saving files to indexedDB aborted:', event);
|
||||||
reject();
|
reject(event.target.error);
|
||||||
};
|
};
|
||||||
|
|
||||||
const os = transaction.objectStore('files');
|
const os = transaction.objectStore('files');
|
||||||
|
|
Loading…
Reference in New Issue
Block a user