This commit is contained in:
j3d1 2026-08-23 00:56:03 +02:00
parent 5c3b7fc252
commit bbe52e4a78
9 changed files with 817 additions and 287 deletions

View file

@ -102,21 +102,6 @@
</div>
</div>
<div class="mb-3">
<label class="form-label">Code type</label>
<div class="btn-group d-block" role="group">
<template v-for="opt in codeTypeOptions" :key="opt.id">
<input type="radio" class="btn-check"
:id="'code-type-' + opt.id + '-fallback'"
autocomplete="off" :value="opt.id" v-model="codeType">
<label class="btn btn-outline-secondary"
:for="'code-type-' + opt.id + '-fallback'">
{{ opt.label }}
</label>
</template>
</div>
</div>
<button class="btn btn-primary" :disabled="!fallbackReady" @click="downloadPng">
<b-icon-download class="me-1"></b-icon-download>
Download label as PNG
@ -207,19 +192,6 @@
</label>
</div>
</div>
<div class="col-auto">
<label class="form-label">Code type</label>
<div class="btn-group" role="group">
<template v-for="opt in codeTypeOptions" :key="opt.id">
<input type="radio" class="btn-check"
:id="'code-type-' + opt.id" autocomplete="off"
:value="opt.id" v-model="codeType">
<label class="btn btn-outline-secondary" :for="'code-type-' + opt.id">
{{ opt.label }}
</label>
</template>
</div>
</div>
<div class="col-auto">
<label class="form-label">Copies</label>
<input type="number" class="form-control copies-input"
@ -275,7 +247,7 @@
<div class="row">
<div class="col-12">
<label-layout-preview :fields="fields" :value="selectedTemplate" :code-type="codeType"
<label-layout-preview :fields="fields" :value="selectedTemplate"
@input="selectedTemplate = $event"></label-layout-preview>
</div>
</div>
@ -287,12 +259,27 @@
<script>
import * as BIcons from "bootstrap-icons-vue";
import {markRaw, nextTick} from "vue";
import {mapActions, mapGetters} from "vuex";
import BaseLayout from "@/components/BaseLayout.vue";
import LabelLayoutPreview from "@/components/LabelLayoutPreview.vue";
import {MultiPrinterBlob, canvasToBitmap, bitmapToCanvas} from "../../vendor/weblabel.js";
import {tapeFromStatus, drawLabel, drawFallbackLabel, buildLabelContent, buildLabelFields, preloadQrEncoder, QR_CODE_TYPES} from "@/label.js";
import {tapeFromStatus, drawLabel, drawFallbackLabel, buildLabelContent, buildLabelFields, preloadQrEncoder} from "@/label.js";
import {LABEL_TEMPLATES, BASE_VARS, DERIVED_VARS, withDerivedVars, templateContent} from "@/label-layouts.js";
import {shortenedRoute} from "@/router";
// The extra "Calculated" fields (and label-layouts.js "Short link (QR code)" template input) this
// view adds on top of label-layouts.js's own DERIVED_VARS - resolving either needs the current
// identityIdByHandle map (see store.js's fetchIdMap) to turn a handle into the numeric
// owner_identity_id short-id.js's 'item'/'storage_location' kinds encode, so neither can be a pure
// fields->value calc like the others and both live here instead of in label-layouts.js. Excluded
// from baseVars below since, unlike every other entry KNOWN_VARS picks up from a template's
// required_vars, neither is ever typed directly.
const SHORT_URL_VAR = "shortUrl";
// The bare short-id.js token itself (e.g. "~AbCd12"), with no domain or leading "/" - what
// shortUrl's own path is built from (see fields()/the shortId method below), for a label that
// wants just the compact code rather than a full scannable URL.
const SHORT_ID_VAR = "shortId";
// Served verbatim from public/vendor/ rather than bundled: libweblabel.js's
// own emscripten glue resolves its .wasm sibling relative to *its own*
@ -314,15 +301,6 @@ const RULER_TIERS = [
{aboveMm: 500, tickMm: 5, majorEveryMm: 25},
];
// Display labels for label.js's QR_CODE_TYPES - the code-type button group below reads this
// (via codeTypeOptions) rather than hard-coding its own list, so a new symbology added to
// anyd-qr.js/QR_CODE_TYPES only needs a label here, not a whole new radio group.
const CODE_TYPE_LABELS = {
"qr": "QR",
"micro-qr": "Micro QR",
"rmqr": "rMQR",
};
export default {
name: "Print",
components: {
@ -386,10 +364,6 @@ export default {
// instead, so it reads across the tape rather than along it. See label.js's
// drawLabel/drawFallbackLabel for how that turn is actually drawn.
orientation: "along",
// Which of label.js's QR_CODE_TYPES every "qrcode" leaf in the current template
// renders as - one global choice for the whole label, same as `orientation` above,
// since a template only ever has one scannable code in practice.
codeType: "qr",
fallbackReady: false,
// TODO: replace with the real commands for our printers.
@ -417,19 +391,20 @@ export default {
};
},
computed: {
...mapGetters(["identityIdByHandle"]),
// The base variables the "Label content" form renders an input for, and the derived ones
// it instead calculates and lists read-only beside that form - plain passthroughs, but
// keep the template from importing label-layouts.js just for these.
// keep the template from importing label-layouts.js just for these. SHORT_URL_VAR is
// excluded here even though the "Short link (QR code)" template's required_vars puts it in
// BASE_VARS (it isn't a label-layouts.js DERIVED_VARS entry) - see fields() below for why
// it's calculated, not typed. SHORT_ID_VAR isn't referenced by any template's
// required_vars (so isn't actually in BASE_VARS today), filtered out too in case one ever
// is.
baseVars() {
return BASE_VARS;
},
// {id, label} pairs for the code-type button group (see CODE_TYPE_LABELS above), in
// label.js's QR_CODE_TYPES order.
codeTypeOptions() {
return QR_CODE_TYPES.map(id => ({id, label: CODE_TYPE_LABELS[id]}));
return BASE_VARS.filter(v => v !== SHORT_URL_VAR && v !== SHORT_ID_VAR);
},
derivedVars() {
return Object.keys(DERIVED_VARS);
return [...Object.keys(DERIVED_VARS), SHORT_ID_VAR, SHORT_URL_VAR];
},
// Named content fields the templates draw from: the form's own base vars, plus every
// DERIVED_VARS format string calculated live from those - so typing a userHandle and
@ -445,7 +420,15 @@ export default {
base[v] = this.varValues[v];
}
}
return withDerivedVars(base);
const derived = withDerivedVars(base);
const shortId = this.shortId(derived);
if (shortId) {
derived[SHORT_ID_VAR] = shortId;
if (derived.webdomain) {
derived[SHORT_URL_VAR] = derived.webdomain + "/" + shortId;
}
}
return derived;
},
currentTemplate() {
return LABEL_TEMPLATES.find(t => t.id === this.selectedTemplate) || LABEL_TEMPLATES[0];
@ -546,13 +529,6 @@ export default {
this.redrawFallback();
}
},
codeType() {
if (this.usbSupported) {
this.redraw();
} else {
this.redrawFallback();
}
},
// Covers connecting/disconnecting/switching printers - anything that changes the
// tape dimensions redraw() sizes the canvas from. flush: 'post' because the canvas
// itself only exists once `tape` is truthy (see the v-if/v-else in the template), so
@ -565,6 +541,8 @@ export default {
},
},
methods: {
...mapActions(["fetchIdMap"]),
// Turns a camelCase variable name (see label-layouts.js's KNOWN_VARS) into a form label,
// e.g. "itemHandle" -> "Item Handle" - so adding a new template variable doesn't also
// require hand-writing a label for it here.
@ -572,6 +550,36 @@ export default {
return v.replace(/([a-z])([A-Z])/g, "$1 $2").replace(/^./, c => c.toUpperCase());
},
// The same shortened link Inventory.vue/StorageLocation.vue's own shortIdLink builds for
// one of their rows, but as the bare token (see short-id.js's encodeShortId) rather than a
// router target or full URL - shortenedRoute's own leading "/" is stripped since this is a
// plain display field/potential label content, not something this view itself navigates
// to; fields() below reattaches a domain and slash to build shortUrl from this same value,
// so the two can never disagree. Which short-id.js kind applies depends on which id field
// the current prefill's LABEL_FIELD_BUILDERS populated (see label.js) - itemId for an
// item, locationId for a storage location - rather than trusting the prefill's own `kind`
// directly, so hand-typing userHandle+itemId with no prefill at all still resolves this
// the same way. Falls back to no value - same as an unresolved DERIVED_VARS entry - until
// identityIdByHandle has loaded (see mounted's fetchIdMap) or if the handle isn't in it.
shortId(f) {
if (!f.userHandle) {
return null;
}
const owner_identity_id = this.identityIdByHandle[f.userHandle];
if (owner_identity_id === undefined) {
return null;
}
if (f.itemId) {
return shortenedRoute({kind: "item", owner_identity_id, item_local_id: f.itemId}).slice(1);
}
if (f.locationId) {
return shortenedRoute({
kind: "storage_location", owner_identity_id, storage_location_id: f.locationId
}).slice(1);
}
return null;
},
// Ticks from 0 up to totalMm, each positioned in on-screen pixels via tapePxPerMm - shared
// by the horizontal/vertical ruler computeds above. Both the plain tick spacing and the
// labeled/major one come from the shared rulerTier (see above), not from this totalMm, so
@ -663,7 +671,7 @@ export default {
this.resizeObserver.observe(canvas.parentElement);
let textSizesPx;
try {
({textSizesPx} = drawLabel(canvas, this.tape, content, this.orientation, this.codeType));
({textSizesPx} = drawLabel(canvas, this.tape, content, this.orientation));
} catch (e) {
this.error = e.message;
return;
@ -689,7 +697,7 @@ export default {
}
this.resizeObserver.observe(canvas.parentElement);
try {
drawFallbackLabel(canvas, content, this.orientation, this.codeType);
drawFallbackLabel(canvas, content, this.orientation);
} catch (e) {
this.error = e.message;
return;
@ -823,10 +831,16 @@ export default {
},
async mounted() {
this.drawCandidateFontTests();
// Not awaited: itemShortUrl just reads whatever identityIdByHandle currently holds (see
// the fields computed), so this resolving after first render only means it shows ""
// briefly rather than blocking the rest of mounted's (unrelated) printer/wasm setup.
this.fetchIdMap().catch(e => {
this.error = e.message;
});
this.resizeObserver = new ResizeObserver(this.handleContainerResize);
// Kicked off here rather than awaited immediately, so it loads concurrently with
// MultiPrinterBlob below instead of serializing two independent wasm fetches - every
// redraw()/redrawFallback() call below still waits on it first, since a "qrcode" leaf
// redraw()/redrawFallback() call below still waits on it first, since a qr/mqr/rmqr leaf
// throws (see label.js's encodeQr) until it resolves.
const qrReady = preloadQrEncoder();
if (!("usb" in navigator)) {