stash
This commit is contained in:
parent
ed04d98bf1
commit
aa94c92000
10 changed files with 873 additions and 296 deletions
|
|
@ -4,19 +4,51 @@
|
|||
<h5 class="card-title mb-0">Label layout</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="mb-3 btn-group" role="group">
|
||||
<input type="radio" class="btn-check" id="layout-filter-all"
|
||||
autocomplete="off" value="all" v-model="activeTag">
|
||||
<label class="btn btn-outline-secondary" for="layout-filter-all">All</label>
|
||||
<template v-for="tag in labelTags" :key="tag">
|
||||
<input type="radio" class="btn-check" :id="'layout-filter-' + tag"
|
||||
autocomplete="off" :value="tag" v-model="activeTag">
|
||||
<label class="btn btn-outline-secondary" :for="'layout-filter-' + tag">
|
||||
{{ tagLabel(tag) }}
|
||||
</label>
|
||||
</template>
|
||||
</div>
|
||||
<div class="template-grid d-flex flex-wrap align-items-start">
|
||||
<div v-for="t in labelTemplates" :key="t.id" class="template-option d-flex flex-column text-center"
|
||||
:class="{'template-option-disabled': !isSelectable(t)}"
|
||||
:title="unavailableReason(t)"
|
||||
role="button" @click="isSelectable(t) && $emit('input', t.id)">
|
||||
<canvas :ref="el => setTemplateCanvasRef(t.id, el)"
|
||||
class="img-thumbnail template-thumb-canvas"
|
||||
:class="{'border-primary': value === t.id}"></canvas>
|
||||
<div class="template-thumb-wrap">
|
||||
<canvas :ref="el => setTemplateCanvasRef(t.id, el)"
|
||||
class="img-thumbnail template-thumb-canvas"
|
||||
:class="{'border-primary': value === t.id}"></canvas>
|
||||
<span v-if="t.id in failed" class="template-thumb-warning" :title="failed[t.id].message">
|
||||
{{ failed[t.id].short }}
|
||||
</span>
|
||||
<span v-else-if="t.id in warned" class="template-thumb-warning template-thumb-warning-soft"
|
||||
:title="warned[t.id].map(w => w.message).join(' ')">
|
||||
{{ warned[t.id].map(w => w.short).join(", ") }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="small"
|
||||
:class="{'fw-bold text-primary': value === t.id}">
|
||||
{{ t.name }}
|
||||
<span v-for="tag in t.tags" :key="tag" class="badge bg-secondary">{{ tagLabel(tag) }}</span>
|
||||
</div>
|
||||
<div class="small text-muted">{{ t.description }}</div>
|
||||
<!-- Debugging aid: every QR-family/text leaf's raw size numbers, shown regardless
|
||||
of whether either tripped a warning above - see label.js's qrInfo/textInfo. -->
|
||||
<div v-if="t.id in qrInfo || t.id in textInfo" class="small text-secondary">
|
||||
<div v-for="(info, i) in qrInfo[t.id]" :key="'qr' + i">
|
||||
mm-per-mod: {{ info.moduleMm.toFixed(2) }}, px-per-mod: {{ info.scale }}
|
||||
</div>
|
||||
<div v-for="(info, i) in textInfo[t.id]" :key="'text' + i">
|
||||
text-mm: {{ info.fontMm.toFixed(2) }}, text-px: {{ info.fontPx.toFixed(1) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -38,6 +70,16 @@
|
|||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.template-option .badge {
|
||||
font-size: .65rem;
|
||||
vertical-align: middle;
|
||||
margin-left: .35rem;
|
||||
}
|
||||
|
||||
.template-thumb-wrap {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.template-thumb-canvas {
|
||||
display: block;
|
||||
width: 100%;
|
||||
|
|
@ -47,11 +89,44 @@
|
|||
object-fit: contain;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
/* Corner badge flagging *why* a layout that's otherwise available (fields filled in) still
|
||||
failed to render - e.g. label.js's "bigger code than the tape allows" or "doesn't fit on this
|
||||
tape" errors (see redraw()'s catch). Shows the short label directly (e.g. "too big") rather than
|
||||
just an icon, so the reason reads at a glance; the full sentence is still the title tooltip. Not
|
||||
shown for the more common "fields not filled in yet" case (see isSelectable/unavailableReason)
|
||||
since that's already conveyed by the greyed-out thumbnail. */
|
||||
.template-thumb-warning {
|
||||
position: absolute;
|
||||
top: .35rem;
|
||||
right: .35rem;
|
||||
max-width: calc(100% - .7rem);
|
||||
padding: .15rem .45rem;
|
||||
border-radius: 1rem;
|
||||
background: rgba(220, 53, 69, .9);
|
||||
color: #fff;
|
||||
font-size: .7rem;
|
||||
font-weight: 600;
|
||||
line-height: 1.2;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
box-shadow: 0 0 0 1px rgba(0, 0, 0, .15);
|
||||
cursor: help;
|
||||
}
|
||||
|
||||
/* Soft variant for a template that rendered fine but tripped one of label.js's scan-reliability
|
||||
thresholds (MIN_RECOMMENDED_QR_PX_PER_MODULE/MIN_RECOMMENDED_QR_MODULE_MM) - still selectable,
|
||||
just flagged, so amber rather than the hard-failure badge's red. */
|
||||
.template-thumb-warning-soft {
|
||||
background: rgba(255, 193, 7, .9);
|
||||
color: #000;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import {drawFallbackLabel, preloadQrEncoder} from "@/label.js";
|
||||
import {LABEL_TEMPLATES, templateIsAvailable, templateContent} from "@/label-layouts.js";
|
||||
import {drawLabel, drawFallbackLabel, preloadQrEncoder} from "@/label.js";
|
||||
import {LABEL_TEMPLATES, LABEL_TAGS, templateIsAvailable, templateContent} from "@/label-layouts.js";
|
||||
|
||||
export default {
|
||||
name: "LabelLayoutPreview",
|
||||
|
|
@ -73,6 +148,15 @@ export default {
|
|||
recentTemplateIds: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
// The connected printer's tape/resolution (see label.js's tapeFromStatus), or null when
|
||||
// no printer is connected. When present, redraw() renders every thumbnail with drawLabel
|
||||
// at this tape's real dpi/printArea/length instead of drawFallbackLabel's fixed reference
|
||||
// - so the grid's px/mm-per-module numbers and any length-limit failures reflect what
|
||||
// would actually come out of the connected printer.
|
||||
tape: {
|
||||
type: Object,
|
||||
default: null
|
||||
}
|
||||
},
|
||||
model: {
|
||||
|
|
@ -82,30 +166,67 @@ export default {
|
|||
emits: ["input"],
|
||||
data() {
|
||||
return {
|
||||
// Keyed by template id, holding the error message from its most recent redraw()
|
||||
// failure (e.g. text too long, or too many QR modules for the thumbnail's fixed
|
||||
// reference size - see label.js's snapQrToCrispSize). Absent, not just falsy, for a
|
||||
// template that last drew fine, so `t.id in failed` matches "has a message".
|
||||
// Keyed by template id, holding the {short, message} error from its most recent
|
||||
// redraw() failure (e.g. text too long, or too many QR modules for the thumbnail's
|
||||
// fixed reference size - see label.js's snapQrToCrispSize/encodeQr). Absent, not just
|
||||
// falsy, for a template that last drew fine, so `t.id in failed` matches "has one".
|
||||
failed: {},
|
||||
// Keyed by template id, holding the array of {short, message} scan-reliability
|
||||
// warnings (see label.js's drawFallbackLabel) from its most recent successful
|
||||
// redraw() - present only when that render tripped
|
||||
// MIN_RECOMMENDED_QR_PX_PER_MODULE/MIN_RECOMMENDED_QR_MODULE_MM.
|
||||
warned: {},
|
||||
// Keyed by template id, holding every QR-family leaf's raw {scale, moduleMm} from its
|
||||
// most recent successful redraw(), regardless of whether it warned - a debugging aid
|
||||
// shown unconditionally below each thumbnail.
|
||||
qrInfo: {},
|
||||
// Same idea as qrInfo, but every text leaf's raw {fontPx, fontMm} (see label.js's
|
||||
// checkTextSizes).
|
||||
textInfo: {},
|
||||
// Which LABEL_TAGS entry (or "all") the grid below is narrowed to.
|
||||
activeTag: "all",
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
labelTags() {
|
||||
return LABEL_TAGS;
|
||||
},
|
||||
// LABEL_TEMPLATES with recentTemplateIds' entries pulled to the front (most recent
|
||||
// first), everything else following in its original order.
|
||||
// first, everything else following in its original order), narrowed to activeTag, then
|
||||
// with anything currently in `failed` (not printable - see redraw()'s catch) stably
|
||||
// sorted to the back regardless of recency/tag order, so real errors don't crowd out
|
||||
// layouts that actually work.
|
||||
labelTemplates() {
|
||||
const recent = this.recentTemplateIds
|
||||
.map(id => LABEL_TEMPLATES.find(t => t.id === id))
|
||||
.filter(Boolean);
|
||||
const recentIds = new Set(recent.map(t => t.id));
|
||||
return [...recent, ...LABEL_TEMPLATES.filter(t => !recentIds.has(t.id))];
|
||||
const ordered = [...recent, ...LABEL_TEMPLATES.filter(t => !recentIds.has(t.id))];
|
||||
const filtered = this.activeTag === "all" ? ordered : ordered.filter(t => t.tags?.includes(this.activeTag));
|
||||
return [...filtered].sort((a, b) => (a.id in this.failed ? 1 : 0) - (b.id in this.failed ? 1 : 0));
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
fields() {
|
||||
this.redraw();
|
||||
},
|
||||
// Switching filters mounts fresh canvas elements for thumbnails hidden until now (see
|
||||
// labelTemplates); nextTick waits for those refs to land before drawing into them.
|
||||
activeTag() {
|
||||
this.$nextTick(() => this.redraw());
|
||||
},
|
||||
// Printer connect/disconnect/switch - every thumbnail's canvas already exists regardless
|
||||
// of tape (unlike Print.vue's own tape-fed preview), so no flush:'post' is needed here.
|
||||
tape() {
|
||||
this.redraw();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// "internal" -> "Internal", for both the filter buttons and each thumbnail's badge.
|
||||
tagLabel(tag) {
|
||||
return tag.charAt(0).toUpperCase() + tag.slice(1);
|
||||
},
|
||||
|
||||
setTemplateCanvasRef(id, el) {
|
||||
if (el) {
|
||||
this.templateCanvases[id] = el;
|
||||
|
|
@ -130,12 +251,25 @@ export default {
|
|||
if (!this.isAvailable(t)) {
|
||||
return "Not available - fill in the fields this layout needs above.";
|
||||
}
|
||||
return this.failed[t.id] ?? "";
|
||||
return this.failed[t.id]?.message ?? "";
|
||||
},
|
||||
|
||||
// Live per-template thumbnails. Always uses the content-fit fallback renderer (not the
|
||||
// tape-fed one) regardless of printer connection - illustrative previews via CSS
|
||||
// object-fit, not the to-be-printed-accurate canvas the main preview is.
|
||||
// .template-thumb-canvas's object-fit:contain scales the just-drawn bitmap into its fixed
|
||||
// CSS box (getBoundingClientRect, unlike canvas.width/height, reflects that box - object-fit
|
||||
// doesn't change it); when that's a magnification, switch to nearest-neighbor so the
|
||||
// print-accurate pixel edges stay crisp instead of blurring, same call Print.vue's own
|
||||
// fitZoom makes for its explicitly-sized preview.
|
||||
applyImageRendering(canvas) {
|
||||
const {width: boxWidth, height: boxHeight} = canvas.getBoundingClientRect();
|
||||
const scale = Math.min(boxWidth / canvas.width, boxHeight / canvas.height);
|
||||
canvas.style.imageRendering = scale >= 1 ? "pixelated" : "auto";
|
||||
},
|
||||
|
||||
// Live per-template thumbnails. Renders each with drawLabel at the connected printer's
|
||||
// real tape/resolution when one's connected (see the `tape` prop), so the numbers/errors
|
||||
// shown match what would actually print; falls back to drawFallbackLabel's fixed
|
||||
// reference size otherwise. Either way, still illustrative previews via CSS object-fit,
|
||||
// not the to-be-printed-accurate canvas Print.vue's own main preview is.
|
||||
redraw() {
|
||||
for (const t of LABEL_TEMPLATES) {
|
||||
const canvas = this.templateCanvases[t.id];
|
||||
|
|
@ -147,18 +281,42 @@ export default {
|
|||
canvas.width = 1;
|
||||
canvas.height = 1;
|
||||
delete this.failed[t.id];
|
||||
delete this.warned[t.id];
|
||||
delete this.qrInfo[t.id];
|
||||
delete this.textInfo[t.id];
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
drawFallbackLabel(canvas, content, "along");
|
||||
const {warnings, qrInfo, textInfo} = this.tape
|
||||
? drawLabel(canvas, this.tape, content, "along")
|
||||
: drawFallbackLabel(canvas, content, "along");
|
||||
this.applyImageRendering(canvas);
|
||||
delete this.failed[t.id];
|
||||
if (warnings.length) {
|
||||
this.warned[t.id] = warnings;
|
||||
} else {
|
||||
delete this.warned[t.id];
|
||||
}
|
||||
if (qrInfo.length) {
|
||||
this.qrInfo[t.id] = qrInfo;
|
||||
} else {
|
||||
delete this.qrInfo[t.id];
|
||||
}
|
||||
if (textInfo.length) {
|
||||
this.textInfo[t.id] = textInfo;
|
||||
} else {
|
||||
delete this.textInfo[t.id];
|
||||
}
|
||||
} catch (e) {
|
||||
// Grey the thumbnail out (see isSelectable/unavailableReason) rather than
|
||||
// leaving it blank - selecting a template that can't render here would only
|
||||
// hand Print.vue's own redraw the exact same failure.
|
||||
canvas.width = 1;
|
||||
canvas.height = 1;
|
||||
this.failed[t.id] = e.message;
|
||||
this.failed[t.id] = {short: e.short ?? "error", message: e.message};
|
||||
delete this.warned[t.id];
|
||||
delete this.qrInfo[t.id];
|
||||
delete this.textInfo[t.id];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -76,13 +76,23 @@ const QR_ONLY_TEMPLATES = [
|
|||
export const LABEL_TEMPLATES = [
|
||||
{
|
||||
id: "mqr-token", name: "MQR Token", description: "The code with the encoded text printed next to it.",
|
||||
required_vars: ["shortId"],
|
||||
required_vars: ["shortId"], tags: ["internal"],
|
||||
layout: [{type: "mqr", content: c => c.shortId}]
|
||||
},
|
||||
{
|
||||
id: "qr-url", name: "MQR Token", description: "The code with the encoded text printed next to it.",
|
||||
required_vars: ["shortUrl"],
|
||||
required_vars: ["shortUrl"], tags: ["external"],
|
||||
layout: [{type: "qr-h", content: c => c.shortUrl}]
|
||||
},
|
||||
{
|
||||
id: "rmqr-url", name: "rMQR Token", description: "The code with the encoded text printed next to it.",
|
||||
required_vars: ["shortUrl","itemId"], tags: ["external"],
|
||||
layout: [{type: "rmqr", content: c => c.shortUrl},GAP,{type: "text", content: c => c.itemId?.toString().padStart(4, "0")}]
|
||||
},
|
||||
{
|
||||
id: "mqr-tokem-id", name: "rMQR Token", description: "The code with the encoded text printed next to it.",
|
||||
required_vars: ["shortId","itemId"], tags: ["internal"],
|
||||
layout: [{type: "mqr", content: c => c.shortId},GAP,{type: "text", content: c => c.itemId?.toString().padStart(4, "0")}]
|
||||
},...QR_ONLY_TEMPLATES,
|
||||
|
||||
{
|
||||
|
|
@ -113,66 +123,66 @@ export const LABEL_TEMPLATES = [
|
|||
{
|
||||
id: "item-handle", name: "Item handle",
|
||||
description: "The compact owner@domain:id handle - meaningful in-app, not scannable on its own.",
|
||||
required_vars: ["itemHandle"],
|
||||
required_vars: ["itemHandle"], tags: ["internal"],
|
||||
layout: [{type: "text", content: c => c.itemHandle}]
|
||||
},
|
||||
{
|
||||
id: "item-url", name: "Item URL",
|
||||
description: "The full item URL as text, with no code - for copying rather than scanning.",
|
||||
required_vars: ["itemUrl"],
|
||||
required_vars: ["itemUrl"], tags: ["external"],
|
||||
layout: [{type: "text", content: c => c.itemUrl}]
|
||||
},
|
||||
{
|
||||
id: "owner-handle", name: "Owner handle", description: "Just the owning user's handle, as text only.",
|
||||
required_vars: ["userHandle"],
|
||||
required_vars: ["userHandle"], tags: ["internal"],
|
||||
layout: [{type: "text", content: c => c.userHandle}]
|
||||
},
|
||||
{
|
||||
id: "item-id", name: "Item ID", description: "Just the bare item id, as text only.",
|
||||
required_vars: ["itemId"],
|
||||
required_vars: ["itemId"], tags: ["internal"],
|
||||
layout: [{type: "text", content: c => c.itemId}]
|
||||
},
|
||||
{
|
||||
id: "owner-id-text", name: "Owner + item ID",
|
||||
description: "The owner's handle and the item id, as two lines of text - no code.",
|
||||
required_vars: ["userHandle", "itemId"],
|
||||
required_vars: ["userHandle", "itemId"], tags: ["internal"],
|
||||
layout: [{type: "text", content: c => [c.userHandle, c.itemId]}]
|
||||
},
|
||||
{
|
||||
id: "item-url-qr-handle", name: "Item URL + handle",
|
||||
description: "Scannable item URL, with the item's compact handle printed alongside.",
|
||||
required_vars: ["itemUrl", "itemHandle"],
|
||||
required_vars: ["itemUrl", "itemHandle"], tags: ["external"],
|
||||
layout: [{type: "qr", content: c => c.itemUrl}, GAP, {type: "text", content: c => c.itemHandle}]
|
||||
},
|
||||
{
|
||||
id: "item-url-qr-owner", name: "Item URL + owner",
|
||||
description: "Scannable item URL, with the owner's handle printed alongside.",
|
||||
required_vars: ["itemUrl", "userHandle"],
|
||||
required_vars: ["itemUrl", "userHandle"], tags: ["external"],
|
||||
layout: [{type: "qr", content: c => c.itemUrl}, GAP, {type: "text", content: c => c.userHandle}]
|
||||
},
|
||||
{
|
||||
id: "item-url-qr-id", name: "Item URL + item ID",
|
||||
description: "Scannable item URL, with the bare item id printed alongside.",
|
||||
required_vars: ["itemUrl", "itemId"],
|
||||
required_vars: ["itemUrl", "itemId"], tags: ["external"],
|
||||
layout: [{type: "qr", content: c => c.itemUrl}, GAP, {type: "text", content: c => c.itemId}]
|
||||
},
|
||||
{
|
||||
id: "item-url-qr-owner-id", name: "Item URL + owner + ID",
|
||||
description: "Scannable item URL, with the owner's handle and the item id on two lines alongside.",
|
||||
required_vars: ["itemUrl", "userHandle", "itemId"],
|
||||
required_vars: ["itemUrl", "userHandle", "itemId"], tags: ["external"],
|
||||
layout: [{type: "qr", content: c => c.itemUrl}, GAP, {type: "text", content: c => [c.userHandle, c.itemId]}]
|
||||
},
|
||||
{
|
||||
id: "short-url-qr", name: "Short link (QR code)",
|
||||
description: "Scannable short link for this item or storage location - more compact than "
|
||||
+ "the full URL. Available for any element with a resolvable short link, not just items.",
|
||||
required_vars: ["shortUrl"],
|
||||
required_vars: ["shortUrl"], tags: ["external"],
|
||||
layout: [{type: "qr", content: c => c.shortUrl}]
|
||||
},
|
||||
{
|
||||
id: "item-url-qr-owner-id2", name: "Item URL + owner + ID",
|
||||
description: "Scannable item URL, with the owner's handle and the item id on two lines alongside.",
|
||||
required_vars: ["itemUrl", "userHandle", "itemId"],
|
||||
required_vars: ["itemUrl", "userHandle", "itemId"], tags: ["external"],
|
||||
layout: [{type: "qr", content: c => c.itemUrl}, GAP, [{
|
||||
type: "text",
|
||||
content: c => c.userHandle
|
||||
|
|
@ -180,6 +190,10 @@ export const LABEL_TEMPLATES = [
|
|||
},
|
||||
];
|
||||
|
||||
// Every distinct tag value used across LABEL_TEMPLATES' tags, in first-seen order - drives
|
||||
// LabelLayoutPreview.vue's filter buttons without hand-listing "internal"/"external" there.
|
||||
export const LABEL_TAGS = [...new Set(LABEL_TEMPLATES.flatMap(t => t.tags ?? []))];
|
||||
|
||||
// Every field name any template's required_vars names, in first-seen order.
|
||||
export const KNOWN_VARS = [...new Set(LABEL_TEMPLATES.flatMap(t => t.required_vars))];
|
||||
|
||||
|
|
|
|||
|
|
@ -34,13 +34,24 @@ function isQrLeaf(node) {
|
|||
|
||||
function encodeQr(text, codeType, options) {
|
||||
if (!anyd) {
|
||||
throw new Error("The QR encoder is still loading — try again in a moment.");
|
||||
const err = new Error("The QR encoder is still loading — try again in a moment.");
|
||||
err.short = "loading…";
|
||||
throw err;
|
||||
}
|
||||
// BitMatrix-alike shim over anyd's row-major matrix, matching the old "qrcode" package's
|
||||
// modules.size/get() shape that drawQrLeaf/snapQrToCrispSize expect. See
|
||||
// docs/implementation.md#qr-module-matrix-shim.
|
||||
const {width, height, modules} = anyd.encode(codeType, new TextEncoder().encode(text), options).matrix;
|
||||
return {width, height, get: (row, col) => modules[row * width + col] !== 0};
|
||||
try {
|
||||
const {width, height, modules} = anyd.encode(codeType, new TextEncoder().encode(text), options).matrix;
|
||||
return {width, height, get: (row, col) => modules[row * width + col] !== 0};
|
||||
} catch (e) {
|
||||
// anyd's own errors (e.g. "capacity exceeded: …") have no `short` of their own - every
|
||||
// failure here comes down to the content not fitting the chosen QR variant's capacity.
|
||||
if (e && typeof e === "object" && !("short" in e)) {
|
||||
e.short = "too long";
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
const TRAILING_PADDING_PX = 3; /* blank columns after the cut, same idea as the leading margin */
|
||||
|
|
@ -80,8 +91,26 @@ function fontFamilyFor(fontPx) {
|
|||
return PIXEL_FONT_TIERS.find(t => fontPx < t.belowPx) ?? {family: "sans-serif"};
|
||||
}
|
||||
|
||||
// Tom Thumb reads clearly down to 5px per the same real-Chromium testing as PIXEL_FONT_TIERS; below this, drawTextLeaf blanks the field instead of rejecting the whole label.
|
||||
const MIN_READABLE_TEXT_PX = 5;
|
||||
// Below this font size (px) or physical height (mm) - Tom Thumb's real-Chromium-tested legibility
|
||||
// floor - text is a hard failure, the same two-metric shape as MIN_RECOMMENDED_QR_PX_PER_MODULE/
|
||||
// MIN_RECOMMENDED_QR_MODULE_MM below (see checkTextSizes). No longer just silently blanked.
|
||||
const MIN_TEXT_PX = 5;
|
||||
const MIN_TEXT_MM = 0.5;
|
||||
// Below this, still legible but a soft warning - flagged rather than blocking, same idea as
|
||||
// MIN_RECOMMENDED_QR_PX_PER_MODULE/MIN_RECOMMENDED_QR_MODULE_MM.
|
||||
const MIN_RECOMMENDED_TEXT_PX = 8;
|
||||
const MIN_RECOMMENDED_TEXT_MM = 1;
|
||||
|
||||
// Below this many pixels per module, a QR-family code still technically fits (see
|
||||
// snapQrToCrispSize's scale >= 1 hard requirement) but risks blurring together on a real thermal
|
||||
// printer's dot pitch - a soft warning rather than the outright rejection scale < 1 gets.
|
||||
const MIN_RECOMMENDED_QR_PX_PER_MODULE = 3;
|
||||
// Below this per-module physical size (in mm), a QR-family code is a rule-of-thumb risk for a
|
||||
// phone camera to resolve at normal scanning distance even when crisply printed at full
|
||||
// resolution - also a soft warning, independent of the pixels-per-module check above (a printer
|
||||
// can hit that check's px/module floor at any dpi, but only a high enough dpi keeps modules this
|
||||
// physically small still legible).
|
||||
const MIN_RECOMMENDED_QR_MODULE_MM = 0.5;
|
||||
|
||||
function isSplit(node) {
|
||||
return Array.isArray(node);
|
||||
|
|
@ -169,27 +198,102 @@ function positionTree(node, ownAxis, x, y) {
|
|||
}
|
||||
|
||||
// Pins each QR-family leaf's real crisp-pixel box.width/box.height so relation() above starts
|
||||
// treating it as fixed-size. See docs/implementation.md#crisp-qr-sizing.
|
||||
function snapQrToCrispSize(node) {
|
||||
// treating it as fixed-size. Appends a {short, message} entry to `warnings` for each of
|
||||
// MIN_RECOMMENDED_QR_PX_PER_MODULE/MIN_RECOMMENDED_QR_MODULE_MM the leaf falls short of (still
|
||||
// printable, just flagged as a scan-reliability risk), and unconditionally appends its raw
|
||||
// {scale, moduleMm} to `qrInfo` - callers needing to show those numbers regardless of whether
|
||||
// they tripped a threshold (see LabelLayoutPreview.vue's debug line) shouldn't have to re-derive
|
||||
// them. See docs/implementation.md#crisp-qr-sizing.
|
||||
function snapQrToCrispSize(node, pxPerMm, warnings, qrInfo) {
|
||||
if (isSplit(node)) {
|
||||
node.forEach(snapQrToCrispSize);
|
||||
node.forEach(child => snapQrToCrispSize(child, pxPerMm, warnings, qrInfo));
|
||||
return;
|
||||
}
|
||||
if (isQrLeaf(node)) {
|
||||
const {width: modulesW, height: modulesH} = node.qr;
|
||||
const scale = Math.floor(Math.min(node.box.width / modulesW, node.box.height / modulesH));
|
||||
if (!(scale >= 1)) {
|
||||
throw new Error("This text needs a bigger code than the tape allows — "
|
||||
const err = new Error("This text needs a bigger code than the tape allows — "
|
||||
+ "try a shorter value or a wider tape.");
|
||||
err.short = "too big";
|
||||
throw err;
|
||||
}
|
||||
node.crispWidth = modulesW * scale;
|
||||
node.crispHeight = modulesH * scale;
|
||||
|
||||
const moduleMm = scale / pxPerMm;
|
||||
qrInfo.push({scale, moduleMm});
|
||||
|
||||
if (scale < MIN_RECOMMENDED_QR_PX_PER_MODULE) {
|
||||
warnings.push({
|
||||
short: `${scale}px/mod`,
|
||||
message: `This code's modules are only ${scale}px wide - they may blur together `
|
||||
+ "when printed; consider a bigger label or shorter content.",
|
||||
});
|
||||
}
|
||||
if (moduleMm < MIN_RECOMMENDED_QR_MODULE_MM) {
|
||||
warnings.push({
|
||||
short: `${moduleMm.toFixed(2)}mm/mod`,
|
||||
message: `This code's modules are only ${moduleMm.toFixed(2)}mm across - it may `
|
||||
+ "be too small to scan reliably; consider a bigger label or shorter content.",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The rendered font size, floored to a whole pixel - same reasoning as snapQrToCrispSize's
|
||||
// integer module scale (rounding up could overflow the box a fractional size would have fit
|
||||
// exactly). checkTextSizes and drawTextLeaf both call this rather than each computing their own
|
||||
// raw fraction, so what gets measured/threshold-checked is exactly what gets drawn.
|
||||
function effectiveFontPx(node, referencePx) {
|
||||
return Math.floor(referencePx * (node.box.height / node.naturalHeight));
|
||||
}
|
||||
|
||||
// Same shape as snapQrToCrispSize but for "text" leaves: throws below MIN_TEXT_PX/MIN_TEXT_MM
|
||||
// (too small to read at all), appends a {short, message} warning to `warnings` for each of
|
||||
// MIN_RECOMMENDED_TEXT_PX/MIN_RECOMMENDED_TEXT_MM it falls short of, and unconditionally appends
|
||||
// its raw {fontPx, fontMm} to `textInfo`. Must run after the tree's *final* layoutTree pass (unlike
|
||||
// snapQrToCrispSize, which runs before solve() re-resolves the tree) - a text leaf's box.height
|
||||
// isn't stable until then, since (unlike a QR leaf's crispWidth/crispHeight) it doesn't feed back
|
||||
// into that re-resolve.
|
||||
function checkTextSizes(node, referencePx, pxPerMm, warnings, textInfo) {
|
||||
if (isSplit(node)) {
|
||||
node.forEach(child => checkTextSizes(child, referencePx, pxPerMm, warnings, textInfo));
|
||||
return;
|
||||
}
|
||||
if (node.type !== "text") {
|
||||
return;
|
||||
}
|
||||
const fontPx = effectiveFontPx(node, referencePx);
|
||||
const fontMm = fontPx / pxPerMm;
|
||||
if (fontPx < MIN_TEXT_PX || fontMm < MIN_TEXT_MM) {
|
||||
const err = new Error(`This text only fits at ${fontPx}px `
|
||||
+ `(${fontMm.toFixed(2)}mm) - too small to read; try a shorter value, a different `
|
||||
+ "layout, or a bigger label.");
|
||||
err.short = "too small";
|
||||
throw err;
|
||||
}
|
||||
textInfo.push({fontPx, fontMm});
|
||||
|
||||
if (fontPx < MIN_RECOMMENDED_TEXT_PX) {
|
||||
warnings.push({
|
||||
short: `${fontPx}px text`,
|
||||
message: `This text renders at only ${fontPx}px - it may be hard to read; `
|
||||
+ "consider a bigger label or shorter content.",
|
||||
});
|
||||
}
|
||||
if (fontMm < MIN_RECOMMENDED_TEXT_MM) {
|
||||
warnings.push({
|
||||
short: `${fontMm.toFixed(2)}mm text`,
|
||||
message: `This text renders at only ${fontMm.toFixed(2)}mm tall - it may be hard to `
|
||||
+ "read; consider a bigger label or shorter content.",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Always measures in sans-serif at the reference size, since the eventual font (see
|
||||
// PIXEL_FONT_TIERS) isn't known until layoutTree sizes the box this aspect ratio feeds into; the
|
||||
// resulting mismatch is invisible in practice, and MIN_READABLE_TEXT_PX still catches real failures.
|
||||
// resulting mismatch is invisible in practice, and checkTextSizes still catches real failures.
|
||||
function measureTextBlock(ctx, lines, referencePx) {
|
||||
ctx.font = `${referencePx}px sans-serif`;
|
||||
const width = Math.max(...lines.map(line => ctx.measureText(line).width));
|
||||
|
|
@ -232,25 +336,21 @@ function drawQrLeaf(ctx, node) {
|
|||
}
|
||||
}
|
||||
|
||||
// Returns the effective (un-normalized) font size drawn at, or that would have been if too small
|
||||
// to draw (see below); drawTree collects these into drawLabel/drawFallbackLabel's textSizesPx.
|
||||
// Returns the effective (un-normalized) font size drawn at; drawTree collects these into
|
||||
// drawLabel/drawFallbackLabel's textSizesPx. Always draws - checkTextSizes (run earlier, on the
|
||||
// same tree, before any of this) already rejected anything below MIN_TEXT_PX/MIN_TEXT_MM, so
|
||||
// there's no "too small to draw" case left to special-case here.
|
||||
function drawTextLeaf(ctx, node, referencePx) {
|
||||
const fontPx = referencePx * (node.box.height / node.naturalHeight);
|
||||
// Below the smallest legible size, leave this leaf blank rather than reject the whole label;
|
||||
// its box was already accounted for, so nothing else in the layout shifts.
|
||||
if (fontPx < MIN_READABLE_TEXT_PX) {
|
||||
return fontPx;
|
||||
}
|
||||
const fontPx = effectiveFontPx(node, referencePx);
|
||||
const {family, scale = 1} = fontFamilyFor(fontPx);
|
||||
const isPixelFont = family !== "sans-serif";
|
||||
// Pixel-font glyphs need whole-pixel size/position to stay grid-aligned, since node.box.x/y
|
||||
// are ordinary (fractional) layout math; sans-serif is left exact since anti-aliasing handles
|
||||
// fractional positions fine.
|
||||
// Position (not size - fontPx is already a whole pixel) still needs snapping for pixel fonts
|
||||
// to stay grid-aligned, since node.box.x/y are ordinary (fractional) layout math; sans-serif is
|
||||
// left exact since anti-aliasing handles fractional positions fine.
|
||||
const snap = isPixelFont ? Math.round : (v) => v;
|
||||
const drawFontPx = snap(fontPx);
|
||||
// `scale` (Tom Thumb only, see PIXEL_FONT_TIERS above) corrects the size handed to ctx.font
|
||||
// for its real ink; drawFontPx itself stays the logical size used for centering/stacking math.
|
||||
ctx.font = `${drawFontPx * scale}px "${family}"`;
|
||||
// for its real ink; fontPx itself stays the logical size used for centering/stacking math.
|
||||
ctx.font = `${fontPx * scale}px "${family}"`;
|
||||
// Canvas text silently falls back if drawn before a not-yet-loaded font resolves, unlike DOM
|
||||
// text. See docs/implementation.md#canvas-font-loading.
|
||||
if (isPixelFont) {
|
||||
|
|
@ -292,8 +392,8 @@ function drawDebugBorder(ctx, node) {
|
|||
ctx.restore();
|
||||
}
|
||||
|
||||
// Collects each text leaf's effective font size so callers (Print.vue) can spot a blank-rendered
|
||||
// field (see drawTextLeaf's MIN_READABLE_TEXT_PX check) as suspiciously small rather than silently missing.
|
||||
// Collects each text leaf's effective font size so callers (Print.vue) can spot a suspiciously
|
||||
// small field (checkTextSizes' warnings/textInfo cover the same numbers in more structured form).
|
||||
function drawTree(ctx, node, referencePx, textSizesPx) {
|
||||
if (isSplit(node)) {
|
||||
node.forEach(child => drawTree(ctx, child, referencePx, textSizesPx));
|
||||
|
|
@ -311,8 +411,8 @@ function drawTree(ctx, node, referencePx, textSizesPx) {
|
|||
}
|
||||
|
||||
// Builds, sizes and validates the tree for a fixed dimension plus pxPerMm; runs sizing twice so
|
||||
// QR-family leaves' real crisp size is known before the tree is finally resolved. See
|
||||
// docs/implementation.md#label-content-layout.
|
||||
// QR-family leaves' real crisp size is known before the tree is finally resolved, then checks
|
||||
// every text leaf's final font size. See docs/implementation.md#label-content-layout.
|
||||
function layoutContent(ctx, content, fixedSize, maxLength, referencePx, pxPerMm, orientation) {
|
||||
const tree = buildRenderTree(ctx, content, referencePx);
|
||||
const alongTape = orientation !== "across";
|
||||
|
|
@ -326,29 +426,38 @@ function layoutContent(ctx, content, fixedSize, maxLength, referencePx, pxPerMm,
|
|||
|
||||
let {width, height} = solve();
|
||||
layoutTree(tree, false, width, height, pxPerMm);
|
||||
snapQrToCrispSize(tree);
|
||||
const warnings = [];
|
||||
const qrInfo = [];
|
||||
snapQrToCrispSize(tree, pxPerMm, warnings, qrInfo);
|
||||
|
||||
({width, height} = solve());
|
||||
const length = alongTape ? width : height;
|
||||
if (maxLength !== Infinity && length > maxLength) {
|
||||
throw new Error("This doesn't fit on this tape — "
|
||||
const err = new Error("This doesn't fit on this tape — "
|
||||
+ "try a shorter value, a different layout, or a bigger label.");
|
||||
err.short = "too big";
|
||||
throw err;
|
||||
}
|
||||
layoutTree(tree, false, width, height, pxPerMm);
|
||||
return {tree, length};
|
||||
const textInfo = [];
|
||||
checkTextSizes(tree, referencePx, pxPerMm, warnings, textInfo);
|
||||
return {tree, length, warnings, qrInfo, textInfo};
|
||||
}
|
||||
|
||||
// The tape-fed layout: draws a fully resolved content tree (see templateContent) at the tape's
|
||||
// real pixel dimensions. See docs/implementation.md#tape-fed-label-drawing. Returns
|
||||
// {textSizesPx}: each "text" leaf's effective font size, in the tree's own left-to-right,
|
||||
// top-to-bottom order.
|
||||
// top-to-bottom order; {warnings}: {short, message} scan-reliability entries from
|
||||
// snapQrToCrispSize/checkTextSizes, if any; {qrInfo}: each QR-family leaf's raw
|
||||
// {scale, moduleMm}; {textInfo}: each text leaf's raw {fontPx, fontMm} - both regardless of
|
||||
// whether they tripped a warning.
|
||||
export function drawLabel(canvas, tape, content, orientation = "along") {
|
||||
const maxLength = tape.printLengthPx
|
||||
? tape.printLengthPx - tape.leadPx - TRAILING_PADDING_PX
|
||||
: Infinity;
|
||||
const measureCtx = canvas.getContext("2d");
|
||||
const pxPerMm = tape.dpi / 25.4;
|
||||
const {tree, length: contentLength} = layoutContent(
|
||||
const {tree, length: contentLength, warnings, qrInfo, textInfo} = layoutContent(
|
||||
measureCtx, content, tape.printAreaPx, maxLength, TEXT_REFERENCE_PX, pxPerMm, orientation);
|
||||
|
||||
const printedLength = tape.printLengthPx || Math.ceil(contentLength + tape.leadPx + TRAILING_PADDING_PX);
|
||||
|
|
@ -376,7 +485,7 @@ export function drawLabel(canvas, tape, content, orientation = "along") {
|
|||
positionTree(tree, false, origin, 0);
|
||||
drawTree(ctx, tree, TEXT_REFERENCE_PX, textSizesPx);
|
||||
}
|
||||
return {textSizesPx};
|
||||
return {textSizesPx, warnings, qrInfo, textInfo};
|
||||
}
|
||||
|
||||
const FALLBACK_LABEL_HEIGHT_PX = 200; /* reference height the no-webusb preview/PNG scales from */
|
||||
|
|
@ -384,11 +493,11 @@ const FALLBACK_DPI = 203; /* reference resolution for turning "empty" leaves' m
|
|||
|
||||
// The no-webusb preview/PNG: same layout tree/renderer as drawLabel, scaled from a fixed
|
||||
// reference height instead. See docs/implementation.md#fallback-label-preview. `orientation` and
|
||||
// the {textSizesPx} return, see drawLabel.
|
||||
// the {textSizesPx, warnings, qrInfo, textInfo} return, see drawLabel.
|
||||
export function drawFallbackLabel(canvas, content, orientation = "along") {
|
||||
const measureCtx = canvas.getContext("2d");
|
||||
const pxPerMm = FALLBACK_DPI / 25.4;
|
||||
const {tree, length: contentLength} = layoutContent(
|
||||
const {tree, length: contentLength, warnings, qrInfo, textInfo} = layoutContent(
|
||||
measureCtx, content, FALLBACK_LABEL_HEIGHT_PX, Infinity, TEXT_REFERENCE_PX, pxPerMm, orientation);
|
||||
|
||||
canvas.width = Math.ceil(contentLength);
|
||||
|
|
@ -411,7 +520,7 @@ export function drawFallbackLabel(canvas, content, orientation = "along") {
|
|||
positionTree(tree, false, 0, 0);
|
||||
drawTree(ctx, tree, TEXT_REFERENCE_PX, textSizesPx);
|
||||
}
|
||||
return {textSizesPx};
|
||||
return {textSizesPx, warnings, qrInfo, textInfo};
|
||||
}
|
||||
|
||||
// Turns a {kind, components} prefill (see Print.vue's `prefill` prop) into the literal string a
|
||||
|
|
|
|||
66
frontend/src/printerManager.js
Normal file
66
frontend/src/printerManager.js
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
// Shared printer device manager for WebUSB label printers (Print.vue) - centralizes device
|
||||
// enumeration and preferred-printer memory, mirroring cameraManager.js's approach for webcams.
|
||||
class PrinterManager {
|
||||
constructor() {
|
||||
this.availableDevices = [];
|
||||
}
|
||||
|
||||
// A USB device has no stable "deviceId" like MediaDeviceInfo - vendorId+productId+serialNumber
|
||||
// is the closest stable identity across plug/unplug and page reloads (empty serial number still
|
||||
// uniquely identifies the common single-printer-of-that-model setup).
|
||||
printerId(device) {
|
||||
return `${device.vendorId}:${device.productId}:${device.serialNumber || ''}`;
|
||||
}
|
||||
|
||||
async enumerateDevices() {
|
||||
try {
|
||||
this.availableDevices = await navigator.usb.getDevices();
|
||||
return this.availableDevices;
|
||||
} catch (err) {
|
||||
console.error('Error enumerating printers:', err);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
getAvailableDevices() {
|
||||
return this.availableDevices;
|
||||
}
|
||||
|
||||
getRecentPrinters() {
|
||||
try {
|
||||
const saved = localStorage.getItem('recentPrinterIds');
|
||||
return saved ? JSON.parse(saved) : [];
|
||||
} catch (err) {
|
||||
console.error('Error loading recent printers:', err);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
savePreferredPrinter(device) {
|
||||
const printerId = this.printerId(device);
|
||||
const recentPrinters = this.getRecentPrinters();
|
||||
const updated = [printerId, ...recentPrinters.filter((id) => id !== printerId)];
|
||||
|
||||
try {
|
||||
localStorage.setItem('recentPrinterIds', JSON.stringify(updated));
|
||||
} catch (err) {
|
||||
console.error('Error saving recent printers:', err);
|
||||
}
|
||||
}
|
||||
|
||||
// Most-recently-used device that's among `devices` (already-paired printers currently
|
||||
// visible), for auto-connecting on page load / when a printer is plugged back in - same
|
||||
// "recent list, first still-present match wins" logic as cameraManager's loadPreferredCamera.
|
||||
findPreferredDevice(devices) {
|
||||
const recentPrinters = this.getRecentPrinters();
|
||||
for (const printerId of recentPrinters) {
|
||||
const device = devices.find((d) => this.printerId(d) === printerId);
|
||||
if (device) {
|
||||
return device;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export default new PrinterManager();
|
||||
|
|
@ -6,6 +6,10 @@
|
|||
|
||||
<div v-if="error" class="alert alert-danger" role="alert">{{ error }}</div>
|
||||
|
||||
<div v-if="warnings.length" class="alert alert-warning" role="alert">
|
||||
<div v-for="(w, i) in warnings" :key="i">{{ w.message }}</div>
|
||||
</div>
|
||||
|
||||
<div v-if="!usbSupported" class="alert alert-warning">
|
||||
This browser can't talk to USB label printers directly. Open this page in Chrome, Edge or
|
||||
Opera over https:// (or http://localhost) to print straight from here, or use one of the
|
||||
|
|
@ -244,7 +248,7 @@
|
|||
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<label-layout-preview :fields="fields" :value="selectedTemplate"
|
||||
<label-layout-preview :fields="fields" :value="selectedTemplate" :tape="tape"
|
||||
:recent-template-ids="recentTemplateIds"
|
||||
@input="selectedTemplate = $event"></label-layout-preview>
|
||||
</div>
|
||||
|
|
@ -260,6 +264,7 @@ import {markRaw, nextTick} from "vue";
|
|||
import {mapActions, mapGetters} from "vuex";
|
||||
import BaseLayout from "@/components/BaseLayout.vue";
|
||||
import LabelLayoutPreview from "@/components/LabelLayoutPreview.vue";
|
||||
import printerManager from "@/printerManager.js";
|
||||
|
||||
import {MultiPrinterBlob, canvasToBitmap, bitmapToCanvas} from "../../vendor/weblabel.js";
|
||||
import {tapeFromStatus, drawLabel, drawFallbackLabel, buildLabelContent, buildLabelFields, preloadQrEncoder} from "@/label.js";
|
||||
|
|
@ -319,6 +324,8 @@ export default {
|
|||
printedWidthPx: 0,
|
||||
// Each "text" leaf's effective font size (see label.js's drawLabel), shown beside the tape width so a too-small-to-render field reads as a suspiciously tiny number rather than silently absent.
|
||||
textSizesPx: [],
|
||||
// Scan-reliability messages from label.js's drawLabel/drawFallbackLabel (too few px per QR module, or too small in mm) - the label still rendered/prints fine, just flagged as a risk.
|
||||
warnings: [],
|
||||
|
||||
// One input per BASE_VARS entry; derived vars (userHandle/itemUrl/itemHandle) are calculated-only (see the `fields` computed), never stored here. Prefilled from query params but left editable.
|
||||
varValues: {
|
||||
|
|
@ -554,7 +561,7 @@ export default {
|
|||
},
|
||||
|
||||
async refreshDevices() {
|
||||
this.devices = (await navigator.usb.getDevices()).map((d) => markRaw(d));
|
||||
this.devices = (await printerManager.enumerateDevices()).map((d) => markRaw(d));
|
||||
/* A printer unplugged while open drops from the list: its handle is gone, so close the card rather than keep it open on a dead connection. */
|
||||
if (this.connected !== null && !this.devices.includes(this.connected)) {
|
||||
this.connected = null;
|
||||
|
|
@ -584,18 +591,32 @@ export default {
|
|||
}
|
||||
},
|
||||
|
||||
// Only one open connection at a time: connecting a different printer disconnects the current one first.
|
||||
async connectToDevice(device) {
|
||||
await this.closeConnection();
|
||||
this.blob.setDevices([device]);
|
||||
await this.blob.open(device.vendorId, device.productId);
|
||||
this.connected = device;
|
||||
printerManager.savePreferredPrinter(device);
|
||||
const status = await this.blob.status();
|
||||
// Setting `tape` alone is enough to redraw - see the tape watcher above.
|
||||
this.tape = this.blob.can("print") ? tapeFromStatus(status) : null;
|
||||
},
|
||||
|
||||
connect(index) {
|
||||
this.guard(async () => {
|
||||
// Only one open connection at a time: connecting a different printer disconnects the current one first.
|
||||
await this.closeConnection();
|
||||
const device = this.devices[index];
|
||||
this.blob.setDevices([device]);
|
||||
await this.blob.open(device.vendorId, device.productId);
|
||||
this.connected = device;
|
||||
const status = await this.blob.status();
|
||||
// Setting `tape` alone is enough to redraw - see the tape watcher above.
|
||||
this.tape = this.blob.can("print") ? tapeFromStatus(status) : null;
|
||||
});
|
||||
this.guard(() => this.connectToDevice(this.devices[index]));
|
||||
},
|
||||
|
||||
// Auto-connects to the most-recently-used printer once it's paired and visible again (page
|
||||
// load, or plugged back in - see onUsbChange), mirroring cameraManager's preferred-camera
|
||||
// auto-select. Gated on "nothing connected yet" since a manual connect() to a different
|
||||
// paired printer shouldn't be silently overridden by a later device-list refresh.
|
||||
autoConnectPreferred() {
|
||||
if (this.connected) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
const preferred = printerManager.findPreferredDevice(this.devices);
|
||||
return preferred ? this.connectToDevice(preferred) : Promise.resolve();
|
||||
},
|
||||
|
||||
disconnect() {
|
||||
|
|
@ -613,15 +634,16 @@ export default {
|
|||
return;
|
||||
}
|
||||
this.resizeObserver.observe(canvas.parentElement);
|
||||
let textSizesPx;
|
||||
let textSizesPx, warnings;
|
||||
try {
|
||||
({textSizesPx} = drawLabel(canvas, this.tape, content, this.orientation));
|
||||
({textSizesPx, warnings} = drawLabel(canvas, this.tape, content, this.orientation));
|
||||
} catch (e) {
|
||||
this.error = e.message;
|
||||
return;
|
||||
}
|
||||
this.error = null;
|
||||
this.textSizesPx = textSizesPx;
|
||||
this.warnings = warnings;
|
||||
const bitmap = canvasToBitmap(canvas);
|
||||
bitmapToCanvas(canvas, bitmap);
|
||||
this.labelBitmap = bitmap;
|
||||
|
|
@ -640,13 +662,15 @@ export default {
|
|||
return;
|
||||
}
|
||||
this.resizeObserver.observe(canvas.parentElement);
|
||||
let warnings;
|
||||
try {
|
||||
drawFallbackLabel(canvas, content, this.orientation);
|
||||
({warnings} = drawFallbackLabel(canvas, content, this.orientation));
|
||||
} catch (e) {
|
||||
this.error = e.message;
|
||||
return;
|
||||
}
|
||||
this.error = null;
|
||||
this.warnings = warnings;
|
||||
this.fallbackReady = true;
|
||||
this.fitZoom(canvas);
|
||||
},
|
||||
|
|
@ -700,7 +724,10 @@ export default {
|
|||
},
|
||||
|
||||
onUsbChange() {
|
||||
this.guard(() => this.refreshDevices());
|
||||
this.guard(async () => {
|
||||
await this.refreshDevices();
|
||||
await this.autoConnectPreferred();
|
||||
});
|
||||
},
|
||||
|
||||
// Works around Vue's refInFor array-collecting behavior for :ref in v-for, same as LabelLayoutPreview.vue's setTemplateCanvasRef.
|
||||
|
|
@ -779,7 +806,10 @@ export default {
|
|||
navigator.usb.addEventListener("connect", this.onUsbChange);
|
||||
navigator.usb.addEventListener("disconnect", this.onUsbChange);
|
||||
await qrReady;
|
||||
await this.guard(() => this.refreshDevices());
|
||||
await this.guard(async () => {
|
||||
await this.refreshDevices();
|
||||
await this.autoConnectPreferred();
|
||||
});
|
||||
},
|
||||
beforeUnmount() {
|
||||
if ("usb" in navigator) {
|
||||
|
|
|
|||
|
|
@ -47,6 +47,10 @@
|
|||
<div class="invalid-feedback">{{ errors.email }}</div>
|
||||
</div>
|
||||
|
||||
<input type="text" class="visually-hidden" autocomplete="username"
|
||||
tabindex="-1" aria-hidden="true"
|
||||
v-model="fullHandle"/>
|
||||
|
||||
<div :class="errors.password?['mb-3','is-invalid']:['mb-3']">
|
||||
<label class="form-label">Password</label>
|
||||
<input class="form-control form-control-lg" type="password"
|
||||
|
|
@ -117,8 +121,39 @@ export default {
|
|||
domains: []
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
fullHandle: {
|
||||
get() {
|
||||
return this.form.domain ? `${this.form.username}@${this.form.domain}` : this.form.username;
|
||||
},
|
||||
set(value) {
|
||||
this.applyHandle(value);
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
// Catches the same "user@domain" shape when it's typed or pasted
|
||||
// directly into the visible username field instead.
|
||||
'form.username'(value) {
|
||||
if (value.includes('@')) {
|
||||
this.applyHandle(value);
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['lookupServer']),
|
||||
applyHandle(value) {
|
||||
const atIndex = value.indexOf('@');
|
||||
if (atIndex === -1) {
|
||||
this.form.username = value;
|
||||
return;
|
||||
}
|
||||
this.form.username = value.slice(0, atIndex);
|
||||
const domain = value.slice(atIndex + 1);
|
||||
if (this.domains.includes(domain)) {
|
||||
this.form.domain = domain;
|
||||
}
|
||||
},
|
||||
do_register() {
|
||||
console.log('do_register');
|
||||
console.log(this.form);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue