mirror of
https://github.com/MultiMote/niimblue
synced 2026-01-19 19:37:11 +03:00
New save/load menu (#39)
This commit is contained in:
@@ -50,3 +50,9 @@ export const OBJECT_DEFAULTS_TEXT = {
|
||||
originY: "center",
|
||||
lineHeight: 1,
|
||||
};
|
||||
|
||||
/** Scale image to this height when making a label thumbnail */
|
||||
export const THUMBNAIL_HEIGHT = 48;
|
||||
|
||||
/** Generate thumbnail in jpeg format with this quality */
|
||||
export const THUMBNAIL_QUALITY = 0.7;
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
import { iconCodepoints, type MaterialIcon } from "../mdi_icons";
|
||||
import { connectionState } from "../stores";
|
||||
import {
|
||||
ExportedLabelTemplateSchema,
|
||||
type ExportedLabelTemplate,
|
||||
type FabricJson,
|
||||
type LabelProps,
|
||||
@@ -31,9 +30,9 @@
|
||||
import QrCodeParamsPanel from "./QRCodeParamsControls.svelte";
|
||||
import TextParamsPanel from "./TextParamsControls.svelte";
|
||||
import VariableInsertControl from "./VariableInsertControl.svelte";
|
||||
import ZplImportButton from "./ZplImportButton.svelte";
|
||||
import { DEFAULT_LABEL_PROPS, GRID_SIZE } from "../defaults";
|
||||
import { ImageEditorUtils } from "../utils/image_editor_utils";
|
||||
import SavedLabelsMenu from "./SavedLabelsMenu.svelte";
|
||||
|
||||
let htmlCanvas: HTMLCanvasElement;
|
||||
let fabricCanvas: fabric.Canvas;
|
||||
@@ -149,59 +148,12 @@
|
||||
}
|
||||
};
|
||||
|
||||
const onSaveClicked = () => {
|
||||
if (confirm($tr("editor.warning.save"))) {
|
||||
try {
|
||||
LocalStoragePersistence.saveLabel(labelProps, fabricCanvas.toJSON());
|
||||
} catch (e) {
|
||||
Toasts.zodErrors(e, "Canvas save error:");
|
||||
}
|
||||
}
|
||||
const exportCurrentLabel = (): ExportedLabelTemplate => {
|
||||
return FileUtils.makeExportedLabel(fabricCanvas, labelProps);
|
||||
};
|
||||
|
||||
const onExportClicked = () => {
|
||||
try {
|
||||
FileUtils.saveLabelAsJson(fabricCanvas, labelProps);
|
||||
} catch (e) {
|
||||
Toasts.zodErrors(e, "Canvas save error:");
|
||||
}
|
||||
};
|
||||
|
||||
const onImportClicked = async () => {
|
||||
const contents = await FileUtils.pickAndReadTextFile("json");
|
||||
const rawData = JSON.parse(contents);
|
||||
|
||||
if (!confirm($tr("editor.warning.load"))) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const data = ExportedLabelTemplateSchema.parse(rawData);
|
||||
await loadLabelData(data);
|
||||
undo.push(fabricCanvas, labelProps);
|
||||
} catch (e) {
|
||||
Toasts.zodErrors(e, "Canvas load error:");
|
||||
}
|
||||
};
|
||||
|
||||
const onLoadClicked = async () => {
|
||||
if (!confirm($tr("editor.warning.load"))) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const labelData = LocalStoragePersistence.loadLabel();
|
||||
|
||||
if (labelData === null) {
|
||||
Toasts.error("No saved label data found, or data is corrupt");
|
||||
return;
|
||||
}
|
||||
|
||||
await loadLabelData(labelData);
|
||||
undo.push(fabricCanvas, labelProps);
|
||||
} catch (e) {
|
||||
Toasts.zodErrors(e, "Canvas load error:");
|
||||
}
|
||||
const onLoadRequested = (label: ExportedLabelTemplate) => {
|
||||
loadLabelData(label).then(() => undo.push(fabricCanvas, labelProps));
|
||||
};
|
||||
|
||||
const zplImageReady = (img: Blob) => {
|
||||
@@ -393,6 +345,8 @@
|
||||
<div class="toolbar d-flex flex-wrap gap-1 justify-content-center align-items-center">
|
||||
<LabelPropsEditor {labelProps} onChange={onUpdateLabelProps} />
|
||||
|
||||
<SavedLabelsMenu onRequestCurrentCanvas={exportCurrentLabel} {onLoadRequested} />
|
||||
|
||||
<button
|
||||
class="btn btn-sm btn-secondary"
|
||||
disabled={undoState.undoDisabled}
|
||||
@@ -415,45 +369,8 @@
|
||||
onUpdate={onCsvUpdate}
|
||||
onPlaceholderPicked={onCsvPlaceholderPicked} />
|
||||
|
||||
<div class="btn-group btn-group-sm" role="group">
|
||||
<button class="btn btn-secondary dropdown-toggle px-1" data-bs-toggle="dropdown">
|
||||
<MdIcon icon="save" />
|
||||
</button>
|
||||
<div class="dropdown-menu px-2">
|
||||
<div class="d-flex gap-1 flex-wrap">
|
||||
<button class="btn btn-secondary btn-sm" on:click={onSaveClicked}>
|
||||
<MdIcon icon="open_in_browser" />
|
||||
{$tr("editor.save.browser")}
|
||||
</button>
|
||||
<button class="btn btn-secondary btn-sm" on:click={onExportClicked}>
|
||||
<MdIcon icon="data_object" />
|
||||
{$tr("editor.save.json")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="btn-group btn-group-sm" role="group">
|
||||
<button class="btn btn-secondary dropdown-toggle px-1" data-bs-toggle="dropdown" data-bs-auto-close="outside">
|
||||
<MdIcon icon="folder" />
|
||||
</button>
|
||||
<div class="dropdown-menu px-2">
|
||||
<div class="d-flex gap-1 flex-wrap">
|
||||
<button class="btn btn-secondary btn-sm" on:click={onLoadClicked}>
|
||||
<MdIcon icon="open_in_browser" />
|
||||
{$tr("editor.load.browser")}
|
||||
</button>
|
||||
<button class="btn btn-secondary btn-sm" on:click={onImportClicked}>
|
||||
<MdIcon icon="data_object" />
|
||||
{$tr("editor.load.json")}
|
||||
</button>
|
||||
<ZplImportButton {labelProps} onImageReady={zplImageReady} text={$tr("editor.import.zpl")} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<IconPicker onSubmit={onIconPicked} />
|
||||
<ObjectPicker onSubmit={onObjectPicked} />
|
||||
<ObjectPicker onSubmit={onObjectPicked} labelProps={labelProps} zplImageReady={zplImageReady} />
|
||||
|
||||
<button class="btn btn-sm btn-primary ms-1" on:click={openPreview}>
|
||||
<MdIcon icon="visibility" />
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="preset-browser overflow-y-auto border d-flex gap-1 flex-wrap {$$props.class}">
|
||||
<div class="preset-browser overflow-y-auto border d-flex p-2 gap-1 flex-wrap {$$props.class}">
|
||||
{#each presets as item, idx}
|
||||
<button
|
||||
class="btn p-0 card-wrapper d-flex justify-content-center align-items-center"
|
||||
|
||||
@@ -167,9 +167,9 @@
|
||||
<MdIcon icon="settings" />
|
||||
</button>
|
||||
<div class="dropdown-menu">
|
||||
<h6 class="dropdown-header">{$tr("params.label.dialog_title")}</h6>
|
||||
<h6 class="dropdown-header">{$tr("params.label.menu_title")}</h6>
|
||||
|
||||
<div class="p-3">
|
||||
<div class="px-3">
|
||||
<div class="mb-3 {error ? 'cursor-help text-warning' : 'text-secondary'}" title={error}>
|
||||
{$tr("params.label.current")}
|
||||
{labelProps.size.width}x{labelProps.size.height}
|
||||
@@ -186,8 +186,7 @@
|
||||
class="mb-1"
|
||||
presets={labelPresets}
|
||||
onItemSelected={onLabelPresetSelected}
|
||||
onItemDelete={onLabelPresetDelete}
|
||||
onItemAdd={onLabelPresetAdd} />
|
||||
onItemDelete={onLabelPresetDelete} />
|
||||
|
||||
<div class="input-group flex-nowrap input-group-sm mb-3">
|
||||
<span class="input-group-text">{$tr("params.label.size")}</span>
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
<script lang="ts">
|
||||
import { type OjectType } from "../types";
|
||||
import { type LabelProps, type OjectType } from "../types";
|
||||
import { tr } from "../utils/i18n";
|
||||
import MdIcon from "./MdIcon.svelte";
|
||||
import ZplImportButton from "./ZplImportButton.svelte";
|
||||
|
||||
export let onSubmit: (i: OjectType) => void;
|
||||
export let labelProps: LabelProps;
|
||||
export let zplImageReady: (img: Blob) => void;
|
||||
</script>
|
||||
|
||||
<div class="dropdown">
|
||||
@@ -44,6 +47,8 @@
|
||||
<MdIcon icon="view_week" />
|
||||
{$tr("editor.objectpicker.barcode")}
|
||||
</button>
|
||||
|
||||
<ZplImportButton {labelProps} onImageReady={zplImageReady} text={$tr("editor.import.zpl")} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
133
src/lib/SavedLabelsBrowser.svelte
Normal file
133
src/lib/SavedLabelsBrowser.svelte
Normal file
@@ -0,0 +1,133 @@
|
||||
<script lang="ts">
|
||||
import type { ExportedLabelTemplate, LabelProps } from "../types";
|
||||
import { tr } from "../utils/i18n";
|
||||
import MdIcon from "./MdIcon.svelte";
|
||||
|
||||
export let onItemClicked: (index: number) => void;
|
||||
export let onItemDelete: (index: number) => void;
|
||||
export let onItemExport: (index: number) => void;
|
||||
export let labels: ExportedLabelTemplate[];
|
||||
export let selectedIndex: number = -1;
|
||||
|
||||
let deleteIndex: number = -1;
|
||||
|
||||
const scaleDimensions = (preset: LabelProps): { width: number; height: number } => {
|
||||
const scaleFactor = Math.min(100 / preset.size.width, 100 / preset.size.height);
|
||||
return {
|
||||
width: Math.round(preset.size.width * scaleFactor),
|
||||
height: Math.round(preset.size.height * scaleFactor),
|
||||
};
|
||||
};
|
||||
|
||||
const deleteConfirmed = (e: MouseEvent, idx: number) => {
|
||||
e.stopPropagation();
|
||||
deleteIndex = -1;
|
||||
onItemDelete(idx);
|
||||
};
|
||||
|
||||
const deleteRejected = (e: MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
deleteIndex = -1;
|
||||
};
|
||||
|
||||
const deleteRequested = (e: MouseEvent, idx: number) => {
|
||||
e.stopPropagation();
|
||||
deleteIndex = idx;
|
||||
};
|
||||
|
||||
const exportRequested = (e: MouseEvent, idx: number) => {
|
||||
e.stopPropagation();
|
||||
onItemExport(idx);
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="labels-browser overflow-y-auto border d-flex p-2 gap-1 flex-wrap {$$props.class}">
|
||||
{#each labels as item, idx}
|
||||
<button
|
||||
class="btn p-0 card-wrapper d-flex justify-content-center align-items-center {selectedIndex===idx ? 'border-primary' : ''}"
|
||||
on:click={() => onItemClicked(idx)}>
|
||||
<div
|
||||
class="card print-start-{item.label.printDirection} d-flex justify-content-center align-items-center"
|
||||
style="width: {scaleDimensions(item.label).width}%; height: {scaleDimensions(item.label).height}%;">
|
||||
<div class="buttons d-flex">
|
||||
<button class="btn text-primary-emphasis" on:click={(e) => exportRequested(e, idx)} title={$tr("params.saved_labels.save.json")}>
|
||||
<MdIcon icon="download" />
|
||||
</button>
|
||||
|
||||
{#if deleteIndex === idx}
|
||||
<button class="remove btn text-danger-emphasis" on:click={(e) => deleteConfirmed(e, idx)}>
|
||||
<MdIcon icon="delete" />
|
||||
</button>
|
||||
<button class="remove btn text-success" on:click={(e) => deleteRejected(e)}>
|
||||
<MdIcon icon="close" />
|
||||
</button>
|
||||
{:else}
|
||||
<button class="remove btn text-danger-emphasis" on:click={(e) => deleteRequested(e, idx)}>
|
||||
<MdIcon icon="delete" />
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if item.thumbnailBase64}
|
||||
<img class="thumbnail" src={item.thumbnailBase64} alt="thumbnail" />
|
||||
{/if}
|
||||
|
||||
{#if item.title}
|
||||
<span class="label p-1">
|
||||
{item.title}
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.labels-browser {
|
||||
max-height: 200px;
|
||||
max-width: 100%;
|
||||
min-height: 96px;
|
||||
}
|
||||
|
||||
.card-wrapper {
|
||||
width: 96px;
|
||||
height: 96px;
|
||||
}
|
||||
|
||||
.card {
|
||||
background-color: white;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.card > .buttons {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.card > .buttons > button {
|
||||
padding: 0;
|
||||
line-height: 100%;
|
||||
}
|
||||
|
||||
.card > .label {
|
||||
background-color: rgba(255, 255, 255, 0.8);
|
||||
color: black;
|
||||
border-radius: 8px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.card.print-start-left {
|
||||
border-left: 2px solid #ff4646;
|
||||
}
|
||||
.card.print-start-top {
|
||||
border-top: 2px solid #ff4646;
|
||||
}
|
||||
|
||||
.card .thumbnail {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
}
|
||||
</style>
|
||||
188
src/lib/SavedLabelsMenu.svelte
Normal file
188
src/lib/SavedLabelsMenu.svelte
Normal file
@@ -0,0 +1,188 @@
|
||||
<script lang="ts">
|
||||
import { tr } from "../utils/i18n";
|
||||
import { onMount } from "svelte";
|
||||
import MdIcon from "./MdIcon.svelte";
|
||||
import SavedLabelsBrowser from "./SavedLabelsBrowser.svelte";
|
||||
import { ExportedLabelTemplateSchema, type ExportedLabelTemplate } from "../types";
|
||||
import { LocalStoragePersistence } from "../utils/persistence";
|
||||
import { Toasts } from "../utils/toasts";
|
||||
import Dropdown from "bootstrap/js/dist/dropdown";
|
||||
import { FileUtils } from "../utils/file_utils";
|
||||
|
||||
export let onRequestCurrentCanvas: () => ExportedLabelTemplate;
|
||||
export let onLoadRequested: (label: ExportedLabelTemplate) => void;
|
||||
|
||||
let dropdownRef: Element;
|
||||
let savedLabels: ExportedLabelTemplate[] = [];
|
||||
let selectedIndex: number = -1;
|
||||
let title: string = "";
|
||||
|
||||
const onLabelSelected = (index: number) => {
|
||||
selectedIndex = index;
|
||||
title = savedLabels[index].title ?? "";
|
||||
};
|
||||
|
||||
const onLabelExport = (idx: number) => {
|
||||
try {
|
||||
FileUtils.saveLabelAsJson(savedLabels[idx]);
|
||||
} catch (e) {
|
||||
Toasts.zodErrors(e, "Canvas save error:");
|
||||
}
|
||||
};
|
||||
|
||||
const onLabelDelete = (idx: number) => {
|
||||
selectedIndex = -1;
|
||||
const result = [...savedLabels];
|
||||
result.splice(idx, 1);
|
||||
LocalStoragePersistence.saveLabels(result);
|
||||
|
||||
savedLabels = result;
|
||||
title = "";
|
||||
};
|
||||
|
||||
const onSaveReplaceClicked = () => {
|
||||
if (selectedIndex === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!confirm($tr("editor.warning.save"))) {
|
||||
return;
|
||||
}
|
||||
|
||||
const label = onRequestCurrentCanvas();
|
||||
label.title = title;
|
||||
|
||||
const result = [...savedLabels];
|
||||
result[selectedIndex] = label;
|
||||
|
||||
const errors = LocalStoragePersistence.saveLabels(result);
|
||||
errors.forEach((e) => Toasts.zodErrors(e, "Label save error"));
|
||||
|
||||
if (errors.length === 0) {
|
||||
savedLabels = result;
|
||||
}
|
||||
};
|
||||
|
||||
const onSaveClicked = () => {
|
||||
const label = onRequestCurrentCanvas();
|
||||
label.title = title;
|
||||
const result = [...savedLabels, label];
|
||||
console.log(result);
|
||||
|
||||
const errors = LocalStoragePersistence.saveLabels(result);
|
||||
errors.forEach((e) => Toasts.zodErrors(e, "Label save error"));
|
||||
|
||||
if (errors.length === 0) {
|
||||
savedLabels = result;
|
||||
}
|
||||
};
|
||||
|
||||
const onLoadClicked = () => {
|
||||
if (selectedIndex === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!confirm($tr("editor.warning.load"))) {
|
||||
return;
|
||||
}
|
||||
|
||||
onLoadRequested(savedLabels[selectedIndex]);
|
||||
new Dropdown(dropdownRef).hide();
|
||||
};
|
||||
|
||||
const onImportClicked = async () => {
|
||||
const contents = await FileUtils.pickAndReadTextFile("json");
|
||||
const rawData = JSON.parse(contents);
|
||||
|
||||
if (!confirm($tr("editor.warning.load"))) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
onLoadRequested(ExportedLabelTemplateSchema.parse(rawData));
|
||||
new Dropdown(dropdownRef).hide();
|
||||
} catch (e) {
|
||||
Toasts.zodErrors(e, "Canvas load error:");
|
||||
}
|
||||
};
|
||||
|
||||
const onExportClicked = () => {
|
||||
try {
|
||||
FileUtils.saveLabelAsJson(onRequestCurrentCanvas());
|
||||
} catch (e) {
|
||||
Toasts.zodErrors(e, "Canvas save error:");
|
||||
}
|
||||
};
|
||||
|
||||
const reload = () => {
|
||||
savedLabels = LocalStoragePersistence.loadLabels();
|
||||
};
|
||||
|
||||
onMount(() => {
|
||||
reload();
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-sm btn-secondary" data-bs-toggle="dropdown" data-bs-auto-close="outside">
|
||||
<MdIcon icon="sd_storage" />
|
||||
</button>
|
||||
<div class="dropdown-menu" bind:this={dropdownRef}>
|
||||
<h6 class="dropdown-header">{$tr("params.saved_labels.menu_title")}</h6>
|
||||
|
||||
<div class="px-3">
|
||||
<div class="p-1">
|
||||
<button class="btn btn-sm btn-outline-secondary" on:click={onImportClicked}>
|
||||
<MdIcon icon="data_object" />
|
||||
{$tr("params.saved_labels.load.json")}
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-secondary" on:click={onExportClicked}>
|
||||
<MdIcon icon="data_object" />
|
||||
{$tr("params.saved_labels.save.json")}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<SavedLabelsBrowser
|
||||
class="mb-1"
|
||||
{selectedIndex}
|
||||
labels={savedLabels}
|
||||
onItemClicked={onLabelSelected}
|
||||
onItemDelete={onLabelDelete}
|
||||
onItemExport={onLabelExport} />
|
||||
|
||||
<div class="input-group flex-nowrap input-group-sm mb-3">
|
||||
<span class="input-group-text">{$tr("params.saved_labels.label_title")}</span>
|
||||
<input
|
||||
class="form-control"
|
||||
type="text"
|
||||
placeholder={$tr("params.saved_labels.label_title.placeholder")}
|
||||
bind:value={title} />
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-1 flex-wrap justify-content-end">
|
||||
<button class="btn btn-sm btn-secondary" on:click={onSaveClicked}>
|
||||
<MdIcon icon="save" />
|
||||
{$tr("params.saved_labels.save.browser")}
|
||||
</button>
|
||||
|
||||
{#if selectedIndex !== -1}
|
||||
<button class="btn btn-sm btn-secondary" on:click={onSaveReplaceClicked}>
|
||||
<MdIcon icon="edit_note" />
|
||||
{$tr("params.saved_labels.save.browser.replace")}
|
||||
</button>
|
||||
|
||||
<button class="btn btn-sm btn-primary" on:click={onLoadClicked}>
|
||||
<MdIcon icon="folder" />
|
||||
{$tr("params.saved_labels.load.browser")}
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.dropdown-menu {
|
||||
min-width: 450px;
|
||||
}
|
||||
</style>
|
||||
@@ -45,7 +45,7 @@
|
||||
};
|
||||
</script>
|
||||
|
||||
<button class="btn btn-secondary btn-sm" on:click={onImportClicked}>
|
||||
<button class="btn btn-sm" on:click={onImportClicked}>
|
||||
<MdIcon icon="receipt_long" />
|
||||
{text}
|
||||
{#if state === "processing"}
|
||||
|
||||
@@ -10,14 +10,12 @@ export const translation_en = {
|
||||
"editor.redo": "Redo",
|
||||
"editor.default_text": "Text",
|
||||
"editor.delete": "Delete",
|
||||
"editor.save.json": "Save (JSON)",
|
||||
"editor.save.browser": "Save (browser)",
|
||||
"editor.load.json": "Load (JSON)",
|
||||
"editor.load.browser": "Load (browser)",
|
||||
"editor.import.zpl": "Import ZPL",
|
||||
|
||||
"editor.iconpicker.search": "Search",
|
||||
"editor.iconpicker.title": "Add icon",
|
||||
"editor.iconpicker.mdi_link_title": "See detailed list here",
|
||||
|
||||
"editor.objectpicker.barcode": "Barcode",
|
||||
"editor.objectpicker.circle": "Circle",
|
||||
"editor.objectpicker.image": "Image",
|
||||
@@ -26,27 +24,33 @@ export const translation_en = {
|
||||
"editor.objectpicker.rectangle": "Rectangle",
|
||||
"editor.objectpicker.text": "Text",
|
||||
"editor.objectpicker.title": "Add object",
|
||||
|
||||
"editor.preview": "Preview",
|
||||
"editor.print": "Print",
|
||||
"editor.warning.load": "Canvas will be replaced with saved data",
|
||||
"editor.warning.save": "Saved data will be overwritten. Save?",
|
||||
|
||||
"main.built": "built at",
|
||||
"main.code": "Code",
|
||||
|
||||
"params.barcode.content": "Content",
|
||||
"params.barcode.enable_caption": "Enable caption",
|
||||
"params.barcode.encoding": "Encoding",
|
||||
"params.barcode.font_size": "Font size",
|
||||
"params.barcode.scale": "Scale factor",
|
||||
|
||||
"params.csv.enabled": "Enabled",
|
||||
"params.csv.placeholders": "Variables:",
|
||||
"params.csv.rowsfound": "Data rows found:",
|
||||
"params.csv.tip": "First row is a header. It used as variable names. Commas are used as separators.",
|
||||
"params.csv.title": "Dynamic label data (CSV)",
|
||||
|
||||
"params.generic.center.horizontal": "Center horizontally",
|
||||
"params.generic.center.vertical": "Center vertically",
|
||||
|
||||
"params.label.apply": "Apply",
|
||||
"params.label.current": "Current parameters:",
|
||||
"params.label.dialog_title": "Label properties",
|
||||
"params.label.menu_title": "Label properties",
|
||||
"params.label.direction.left": "Left",
|
||||
"params.label.direction.top": "Top",
|
||||
"params.label.direction": "Print from",
|
||||
@@ -60,7 +64,9 @@ export const translation_en = {
|
||||
"params.label.size": "Size",
|
||||
"params.label.warning.direction": "Recommended direction for your printer:",
|
||||
"params.label.warning.width": "Label width is too big for your printer:",
|
||||
|
||||
"params.qrcode.ecl": "Error Correction Level",
|
||||
|
||||
"params.text.align.center": "Align text: Center",
|
||||
"params.text.align.left": "Align text: Left",
|
||||
"params.text.align.right": "Align text: Right",
|
||||
@@ -78,10 +84,21 @@ export const translation_en = {
|
||||
"params.text.vorigin": "Vertical Origin",
|
||||
"params.text.edit": "Edit in popup",
|
||||
"params.text.edit.title": "Editing text",
|
||||
|
||||
"params.variables.insert.date": "Date",
|
||||
"params.variables.insert.datetime": "Datetime",
|
||||
"params.variables.insert.time": "Time",
|
||||
"params.variables.insert": "Insert variable",
|
||||
|
||||
"params.saved_labels.menu_title": "Save/load (browser storage)",
|
||||
"params.saved_labels.save.json": "Export JSON",
|
||||
"params.saved_labels.save.browser": "Save",
|
||||
"params.saved_labels.save.browser.replace": "Save (replace)",
|
||||
"params.saved_labels.load.json": "Import JSON",
|
||||
"params.saved_labels.load.browser": "Load",
|
||||
"params.saved_labels.label_title": "Title",
|
||||
"params.saved_labels.label_title.placeholder": "(optional)",
|
||||
|
||||
"preview.close": "Close",
|
||||
"preview.copies": "Copies",
|
||||
"preview.density": "Density",
|
||||
|
||||
@@ -14,10 +14,6 @@ export const translation_ru: Record<TranslationKey, string> = {
|
||||
"connector.disconnect.heartbeat": "Отключено (принтер не отвечает)",
|
||||
/* ImageEditor */
|
||||
"editor.default_text": "Текст",
|
||||
"editor.save.json": "Сохранить (JSON)",
|
||||
"editor.save.browser": "Сохранить (браузер)",
|
||||
"editor.load.json": "Загрузить (JSON)",
|
||||
"editor.load.browser": "Загрузить (браузер)",
|
||||
"editor.import.zpl": "Импорт ZPL",
|
||||
"editor.preview": "Предпросмотр",
|
||||
"editor.print": "Печать",
|
||||
@@ -71,7 +67,7 @@ export const translation_ru: Record<TranslationKey, string> = {
|
||||
"params.csv.enabled": "Включить",
|
||||
"params.csv.placeholders": "Переменные:",
|
||||
/* LabelPropsEditor */
|
||||
"params.label.dialog_title": "Настройки этикетки",
|
||||
"params.label.menu_title": "Настройки этикетки",
|
||||
"params.label.label_title": "Своё название",
|
||||
"params.label.size": "Размер",
|
||||
"params.label.mm": "мм",
|
||||
@@ -114,10 +110,19 @@ export const translation_ru: Record<TranslationKey, string> = {
|
||||
"params.generic.center.horizontal": "Выровнять горизонтально",
|
||||
/* QRCodeParamsControls */
|
||||
"params.qrcode.ecl": "Уровень коррекции ошибок",
|
||||
/** BarcodeParamsControls */
|
||||
/* BarcodeParamsControls */
|
||||
"params.barcode.encoding": "Тип",
|
||||
"params.barcode.content": "Содержимое",
|
||||
"params.barcode.scale": "Масштаб",
|
||||
"params.barcode.font_size": "Размер шрифта",
|
||||
"params.barcode.enable_caption": "Показывать надпись",
|
||||
/* SavedLabelsMenu */
|
||||
"params.saved_labels.menu_title": "Сохранить/загрузить (хранилище браузера)",
|
||||
"params.saved_labels.save.json": "Экспорт JSON",
|
||||
"params.saved_labels.save.browser": "Сохранить",
|
||||
"params.saved_labels.save.browser.replace": "Сохранить (заменить)",
|
||||
"params.saved_labels.load.json": "Импорт JSON",
|
||||
"params.saved_labels.load.browser": "Открыть",
|
||||
"params.saved_labels.label_title": "Название",
|
||||
"params.saved_labels.label_title.placeholder": "(необязательно)",
|
||||
};
|
||||
|
||||
@@ -18,8 +18,7 @@ export const translation_zh_cn: Partial<Record<TranslationKey, string>> = {
|
||||
"editor.iconpicker.search": "搜索",
|
||||
"editor.iconpicker.title": "添加图标",
|
||||
"editor.import.zpl": "导入 ZPL 文件",
|
||||
"editor.load.browser": "从浏览器导入",
|
||||
"editor.load.json": "导入 JSON 文件",
|
||||
|
||||
"editor.objectpicker.barcode": "条码",
|
||||
"editor.objectpicker.circle": "圆形",
|
||||
"editor.objectpicker.image": "图片",
|
||||
@@ -30,8 +29,6 @@ export const translation_zh_cn: Partial<Record<TranslationKey, string>> = {
|
||||
"editor.objectpicker.title": "添加元素",
|
||||
"editor.preview": "预览",
|
||||
"editor.print": "打印",
|
||||
"editor.save.browser": "保存到浏览器",
|
||||
"editor.save.json": "导出 JSON 文件",
|
||||
"editor.warning.load": "画布将被替换为保存的数据,需要继续吗?",
|
||||
"editor.warning.save": "保存的数据将会被覆盖,需要继续吗?",
|
||||
/* Main page */
|
||||
@@ -55,7 +52,7 @@ export const translation_zh_cn: Partial<Record<TranslationKey, string>> = {
|
||||
/* LabelPropsEditor */
|
||||
"params.label.apply": "应用",
|
||||
"params.label.current": "当前设置:",
|
||||
"params.label.dialog_title": "标签设置",
|
||||
"params.label.menu_title": "标签设置",
|
||||
"params.label.direction.left": "向左",
|
||||
"params.label.direction.top": "向上",
|
||||
"params.label.direction": "出纸方向",
|
||||
@@ -92,6 +89,11 @@ export const translation_zh_cn: Partial<Record<TranslationKey, string>> = {
|
||||
"params.variables.insert.datetime": "日期时间",
|
||||
"params.variables.insert.time": "时间",
|
||||
"params.variables.insert": "插入变量",
|
||||
/* SavedLabelsMenu */
|
||||
"params.saved_labels.load.browser": "从浏览器导入",
|
||||
"params.saved_labels.load.json": "导入 JSON 文件",
|
||||
"params.saved_labels.save.browser": "保存到浏览器",
|
||||
"params.saved_labels.save.json": "导出 JSON 文件",
|
||||
/* PrintPreview */
|
||||
"preview.close": "关闭",
|
||||
"preview.copies": "份数",
|
||||
|
||||
@@ -39,6 +39,9 @@ export const FabricJsonSchema = z.object({
|
||||
export const ExportedLabelTemplateSchema = z.object({
|
||||
canvas: FabricJsonSchema,
|
||||
label: LabelPropsSchema,
|
||||
thumbnailBase64: z.string().optional(),
|
||||
title: z.string().optional(),
|
||||
timestamp: z.number().positive().optional()
|
||||
});
|
||||
|
||||
const [firstTask, ...otherTasks] = printTaskNames;
|
||||
|
||||
@@ -1,19 +1,36 @@
|
||||
import type { fabric } from "fabric";
|
||||
import { ExportedLabelTemplateSchema, type ExportedLabelTemplate, type FabricJson, type LabelProps } from "../types";
|
||||
import { OBJECT_DEFAULTS } from "../defaults";
|
||||
import { OBJECT_DEFAULTS, THUMBNAIL_HEIGHT, THUMBNAIL_QUALITY } from "../defaults";
|
||||
|
||||
export class FileUtils {
|
||||
/** Convert label template to JSON and download it */
|
||||
static saveLabelAsJson(canvas: fabric.Canvas, labelProps: LabelProps) {
|
||||
const timestamp = Math.floor(Date.now() / 1000);
|
||||
static timestamp(): number {
|
||||
return Math.floor(Date.now() / 1000);
|
||||
}
|
||||
|
||||
const labelRaw: ExportedLabelTemplate = {
|
||||
static makeExportedLabel (canvas: fabric.Canvas, labelProps: LabelProps): ExportedLabelTemplate {
|
||||
const thumbnailBase64: string = canvas.toDataURL({
|
||||
width: canvas.width,
|
||||
height: canvas.height,
|
||||
left: 0,
|
||||
top: 0,
|
||||
multiplier: THUMBNAIL_HEIGHT / (canvas.height || 1),
|
||||
quality: THUMBNAIL_QUALITY,
|
||||
format: "jpeg",
|
||||
});
|
||||
|
||||
return {
|
||||
canvas: canvas.toJSON(),
|
||||
label: labelProps,
|
||||
thumbnailBase64,
|
||||
timestamp: this.timestamp()
|
||||
};
|
||||
};
|
||||
|
||||
const label = ExportedLabelTemplateSchema.parse(labelRaw);
|
||||
const json: string = JSON.stringify(label);
|
||||
/** Convert label template to JSON and download it */
|
||||
static saveLabelAsJson(label: ExportedLabelTemplate) {
|
||||
const parsed = ExportedLabelTemplateSchema.parse(label);
|
||||
const timestamp = label.timestamp ?? this.timestamp();
|
||||
const json: string = JSON.stringify(parsed);
|
||||
const link = document.createElement("a");
|
||||
const file: Blob = new Blob([json], { type: "text/json" });
|
||||
link.href = URL.createObjectURL(file);
|
||||
|
||||
@@ -6,12 +6,12 @@ import {
|
||||
PreviewPropsSchema,
|
||||
type ConnectionType,
|
||||
type ExportedLabelTemplate,
|
||||
type FabricJson,
|
||||
type LabelPreset,
|
||||
type LabelProps,
|
||||
type PreviewProps,
|
||||
} from "../types";
|
||||
import { z } from "zod";
|
||||
import { FileUtils } from "./file_utils";
|
||||
|
||||
export class LocalStoragePersistence {
|
||||
static saveObject(key: string, data: any) {
|
||||
@@ -88,29 +88,71 @@ export class LocalStoragePersistence {
|
||||
this.validateAndSaveObject("last_label_props", labelData, LabelPropsSchema);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws {z.ZodError}
|
||||
*/
|
||||
static saveLabel(labelData: LabelProps, canvasData: FabricJson) {
|
||||
const obj = { label: labelData, canvas: canvasData };
|
||||
this.validateAndSaveObject("saved_label", obj, ExportedLabelTemplateSchema);
|
||||
static saveLabels(labels: ExportedLabelTemplate[]): z.ZodError[] {
|
||||
const errors: z.ZodError[] = [];
|
||||
Object.keys(localStorage).forEach((key) => {
|
||||
if (key.startsWith("saved_label")) {
|
||||
localStorage.removeItem(key);
|
||||
}
|
||||
});
|
||||
|
||||
labels.forEach((label) => {
|
||||
try {
|
||||
if (label.timestamp == undefined) {
|
||||
label.timestamp = FileUtils.timestamp();
|
||||
}
|
||||
|
||||
const basename = `saved_label_${label.timestamp}`;
|
||||
let counter = 0;
|
||||
|
||||
while (`${basename}_${counter}` in localStorage) {
|
||||
counter++;
|
||||
}
|
||||
|
||||
this.validateAndSaveObject(`${basename}_${counter}`, label, ExportedLabelTemplateSchema);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
if (e instanceof z.ZodError) {
|
||||
errors.push(e);
|
||||
}
|
||||
}
|
||||
});
|
||||
return errors;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws {z.ZodError}
|
||||
*/
|
||||
static loadLabel(): ExportedLabelTemplate | null {
|
||||
const label = this.loadAndValidateObject("saved_canvas_props", LabelPropsSchema);
|
||||
const canvas = this.loadAndValidateObject("saved_canvas_data", FabricJsonSchema);
|
||||
static loadLabels(): ExportedLabelTemplate[] {
|
||||
const legacyLabel = this.loadAndValidateObject("saved_canvas_props", LabelPropsSchema);
|
||||
const legacyCanvas = this.loadAndValidateObject("saved_canvas_data", FabricJsonSchema);
|
||||
const items: ExportedLabelTemplate[] = [];
|
||||
|
||||
if (label !== null && canvas !== null) {
|
||||
this.saveLabel(label, canvas);
|
||||
if (legacyLabel !== null && legacyCanvas !== null) {
|
||||
localStorage.removeItem("saved_canvas_props");
|
||||
localStorage.removeItem("saved_canvas_data");
|
||||
return { label, canvas };
|
||||
const item: ExportedLabelTemplate = {
|
||||
label: legacyLabel,
|
||||
canvas: legacyCanvas,
|
||||
timestamp: FileUtils.timestamp(),
|
||||
};
|
||||
this.validateAndSaveObject(`saved_label_${item.timestamp}`, item, ExportedLabelTemplateSchema);
|
||||
}
|
||||
|
||||
return this.loadAndValidateObject("saved_label", ExportedLabelTemplateSchema);
|
||||
Object.keys(localStorage).sort().forEach((key) => {
|
||||
if (key.startsWith("saved_label")) {
|
||||
try {
|
||||
const item = this.loadAndValidateObject(key, ExportedLabelTemplateSchema);
|
||||
if (item != null) {
|
||||
items.push(item);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user