stash
This commit is contained in:
parent
7276750c66
commit
341967a611
1 changed files with 216 additions and 213 deletions
|
|
@ -4,6 +4,20 @@
|
|||
<div class="container-fluid p-0">
|
||||
<h1 class="h3 mb-3">Print a label</h1>
|
||||
|
||||
<!-- Left: what to print. Right: how it looks and how it prints, kept in view via .print-sticky. -->
|
||||
<div class="row">
|
||||
|
||||
<div class="col-lg-7 mb-4">
|
||||
<div class="print-sticky">
|
||||
<div class="card">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h5 class="card-title mb-0">Preview</h5>
|
||||
<small class="text-muted">{{ previewTape.mediaWidthMm }} mm tape{{
|
||||
textSizesSummary
|
||||
}}<span v-if="!tape"> (example)</span></small>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
|
||||
<div v-if="error" class="alert alert-danger" role="alert">{{ error }}</div>
|
||||
|
||||
<div v-if="warnings.length" class="alert alert-warning" role="alert">
|
||||
|
|
@ -11,117 +25,16 @@
|
|||
</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
|
||||
alternatives below.
|
||||
This browser can't pair with USB label printers directly. Open this page in
|
||||
Chrome, Edge or
|
||||
Opera over https:// for that - you can still build and download labels
|
||||
as PNGs below.
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-lg-8">
|
||||
<div class="card h-100">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">Label content</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div :class="v === 'text' ? 'col-12' : 'col-md-6'" v-for="v in baseVars" :key="v">
|
||||
<label class="form-label">{{ varLabel(v) }}</label>
|
||||
<textarea v-if="v === 'text'" class="form-control" rows="3"
|
||||
v-model="varValues[v]"
|
||||
placeholder="https://example.com/…"></textarea>
|
||||
<input v-else type="text" class="form-control" v-model="varValues[v]">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-4">
|
||||
<div class="card h-100">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">Calculated</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div v-for="v in derivedVars" :key="v" class="mb-2">
|
||||
<div class="small text-muted">{{ varLabel(v) }}</div>
|
||||
<div class="text-break">{{ fields[v] || "—" }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="!usbSupported" class="row">
|
||||
<div class="col-lg-7">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">Label preview</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="label-preview mb-3" v-show="fallbackReady">
|
||||
<canvas ref="fallbackCanvas"></canvas>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Orientation</label>
|
||||
<div class="btn-group d-block" role="group">
|
||||
<input type="radio" class="btn-check" id="orientation-along-fallback"
|
||||
autocomplete="off" value="along" v-model="orientation">
|
||||
<label class="btn btn-outline-secondary" for="orientation-along-fallback">
|
||||
Along tape
|
||||
</label>
|
||||
<input type="radio" class="btn-check" id="orientation-across-fallback"
|
||||
autocomplete="off" value="across" v-model="orientation">
|
||||
<label class="btn btn-outline-secondary" for="orientation-across-fallback">
|
||||
Across tape
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-primary" :disabled="!fallbackReady" @click="downloadPng">
|
||||
<b-icon-download class="me-1"></b-icon-download>
|
||||
Download label as PNG
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-5 mb-4">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">Print from the command line</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="text-muted">
|
||||
Download the PNG above, then send it to a label printer with its CLI tool.
|
||||
</p>
|
||||
<p class="mb-1"><strong>Brother QL-series</strong></p>
|
||||
<pre class="code-block"><code>{{ brotherCliExample }}</code></pre>
|
||||
<p class="mb-1"><strong>Niimbot</strong></p>
|
||||
<pre class="code-block"><code>{{ niimbotCliExample }}</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="row">
|
||||
|
||||
<div class="col-lg-7">
|
||||
<div class="card">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h5 class="card-title mb-0">Label</h5>
|
||||
<small v-if="tape" class="text-muted">{{ tape.mediaWidthMm }} mm tape{{
|
||||
textSizesSummary
|
||||
}}</small>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p v-if="!tape" class="text-muted">
|
||||
Connect a printer to preview and print a label.
|
||||
</p>
|
||||
<template v-else>
|
||||
<div class="preview-row mb-3" v-show="labelBitmap">
|
||||
<div class="ruler-v">
|
||||
<div class="ruler-v-corner"></div>
|
||||
<div class="ruler ruler-v-ticks"
|
||||
:style="{height: (tape.mediaWidthMm * tapePxPerMm) + 'px'}">
|
||||
:style="{height: (previewTape.mediaWidthMm * tapePxPerMm) + 'px'}">
|
||||
<span v-for="t in verticalRulerTicks" :key="'tick-' + t.mm"
|
||||
class="tick" :class="{'tick-major': t.major}"
|
||||
:style="{top: t.pos + 'px'}"></span>
|
||||
|
|
@ -142,7 +55,7 @@
|
|||
</div>
|
||||
<!-- Tape's full physical width; canvas is narrower/centered, margin is real tape. See docs/implementation.md#tape-full-print-margin. -->
|
||||
<div class="tape-full"
|
||||
:style="{height: (tape.mediaWidthMm * tapePxPerMm) + 'px'}">
|
||||
:style="{height: (previewTape.mediaWidthMm * tapePxPerMm) + 'px'}">
|
||||
<div class="label-preview">
|
||||
<canvas ref="labelCanvas"></canvas>
|
||||
</div>
|
||||
|
|
@ -169,60 +82,97 @@
|
|||
<div class="col-auto">
|
||||
<label class="form-label">Copies</label>
|
||||
<input type="number" class="form-control copies-input"
|
||||
v-model.number="copies" min="1" max="20">
|
||||
v-model.number="copies" min="1" max="20" :disabled="!connected">
|
||||
</div>
|
||||
<div class="col-auto" v-if="chainSupported">
|
||||
<label class="form-label d-block" for="chain-mode">Chain mode</label>
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" role="switch"
|
||||
id="chain-mode" v-model="chainMode">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-auto" v-if="!connected">
|
||||
<label class="form-label">Tape height (px)</label>
|
||||
<input type="number" class="form-control copies-input"
|
||||
v-model.number="simulatedHeightPx" min="8" max="1000">
|
||||
</div>
|
||||
<div class="col-auto ms-auto">
|
||||
<button class="btn btn-primary" :disabled="!canPrint || busy"
|
||||
@click="print">
|
||||
<b-icon-printer class="me-1"></b-icon-printer>
|
||||
Print
|
||||
<button class="btn btn-primary" :disabled="!canAct"
|
||||
@click="connected ? print() : downloadPng()">
|
||||
<b-icon-printer v-if="connected" class="me-1"></b-icon-printer>
|
||||
<b-icon-download v-else class="me-1"></b-icon-download>
|
||||
{{ connected ? "Print" : "Download PNG" }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-5 mb-4">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">Printer</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p v-if="!devices.length" class="text-muted">
|
||||
No label printer paired yet.
|
||||
</p>
|
||||
<ul v-else class="list-group mb-3">
|
||||
<li v-for="d in deviceRows" :key="d.index"
|
||||
class="list-group-item d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<div>{{ d.name }}</div>
|
||||
<small v-if="d.isConnected" class="text-success">connected</small>
|
||||
<small v-else-if="!d.supported" class="text-muted">{{ d.reason }}</small>
|
||||
</div>
|
||||
<button v-if="d.isConnected" class="btn btn-sm btn-outline-secondary"
|
||||
:disabled="busy" @click="disconnect()">
|
||||
Disconnect
|
||||
</button>
|
||||
<button v-else-if="d.supported" class="btn btn-sm btn-outline-primary"
|
||||
:disabled="busy" @click="connect(d.index)">
|
||||
Connect
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
<button class="btn btn-primary" :disabled="busy || !blobReady" @click="requestPrinter">
|
||||
|
||||
<div
|
||||
class="printer-bar d-flex flex-wrap align-items-center chip-row mt-3 pt-3 border-top">
|
||||
<span v-for="d in deviceRows" :key="d.index" class="printer-chip"
|
||||
:class="{'printer-chip-connected': d.isConnected, 'printer-chip-disabled': !d.supported}"
|
||||
:title="!d.supported ? d.reason : ''">
|
||||
{{ d.name }}
|
||||
<button v-if="d.isConnected && !d.isPngExport"
|
||||
class="btn btn-link btn-sm p-0 ms-1"
|
||||
:disabled="busy" @click="disconnect()">disconnect</button>
|
||||
<button v-else-if="!d.isConnected && d.supported"
|
||||
class="btn btn-link btn-sm p-0 ms-1"
|
||||
:disabled="busy" @click="connect(d.index)">connect</button>
|
||||
</span>
|
||||
<button v-if="usbSupported" class="btn btn-sm btn-outline-primary ms-auto"
|
||||
:disabled="busy || !blobReady" @click="requestPrinter">
|
||||
<b-icon-plug class="me-1"></b-icon-plug>
|
||||
Pair new printer
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<details class="cli-details mt-3 pt-3 border-top">
|
||||
<summary class="small text-muted">Print from the command line instead</summary>
|
||||
<div class="mt-2">
|
||||
<p class="mb-1"><strong>Brother QL-series</strong></p>
|
||||
<pre class="code-block"><code>{{ brotherCliExample }}</code></pre>
|
||||
<p class="mb-1"><strong>Niimbot</strong></p>
|
||||
<pre class="code-block"><code>{{ niimbotCliExample }}</code></pre>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-5 mb-4">
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">Content</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div :class="v === 'text' ? 'col-12' : 'col-sm-6'" v-for="v in baseVars" :key="v">
|
||||
<label class="form-label">{{ varLabel(v) }}</label>
|
||||
<textarea v-if="v === 'text'" class="form-control" rows="3"
|
||||
v-model="varValues[v]"
|
||||
placeholder="https://example.com/…"></textarea>
|
||||
<input v-else type="text" class="form-control" v-model="varValues[v]">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="calculated-fields mt-3 pt-3 border-top">
|
||||
<div class="small text-uppercase text-muted mb-2">Calculated</div>
|
||||
<div class="d-flex flex-wrap chip-row">
|
||||
<div v-for="v in derivedVars" :key="v" class="calculated-field">
|
||||
<div class="small text-muted">{{ varLabel(v) }}</div>
|
||||
<div class="text-break">{{ fields[v] || "—" }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<label-layout-preview :fields="fields" :value="selectedTemplate" :tape="tape"
|
||||
:recent-template-ids="recentTemplateIds"
|
||||
<div class="col-lg-12 mb-4">
|
||||
<label-layout-preview :fields="fields" :value="selectedTemplate" :tape="previewTape"
|
||||
:recent-template-ids="recentTemplateIds" class="mb-3"
|
||||
@input="selectedTemplate = $event"></label-layout-preview>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -243,7 +193,6 @@ import {MultiPrinterBlob, canvasToBitmap, bitmapToCanvas} from "../../vendor/web
|
|||
import {
|
||||
tapeFromStatus,
|
||||
drawLabel,
|
||||
drawFallbackLabel,
|
||||
buildLabelContent,
|
||||
buildLabelFields,
|
||||
preloadQrEncoder,
|
||||
|
|
@ -268,6 +217,14 @@ const MAX_RECENT_TEMPLATES = 4;
|
|||
|
||||
const MAX_ZOOM = 4; /* never magnify the preview more than this */
|
||||
const MAX_PREVIEW_HEIGHT_PX = 300; /* never let the on-screen preview grow taller than this */
|
||||
// A plausible printer profile for the preview/ruler when the PNG Export "printer" (see deviceRows) is the active one; its height is user-adjustable (see simulatedHeightPx), everything else about it isn't.
|
||||
const SIMULATED_DPI = 180;
|
||||
const SIMULATED_DEFAULT_HEIGHT_PX = 72; /* ~9mm at SIMULATED_DPI */
|
||||
const SIMULATED_MIN_HEIGHT_PX = 8;
|
||||
const SIMULATED_MAX_HEIGHT_PX = 1000;
|
||||
const SIMULATED_LEAD_PX = Math.round(2 * SIMULATED_DPI / 25.4); /* blank runway before content, same idea as tapeFromStatus's marginsMm */
|
||||
// deviceRows' sentinel index for the always-present PNG Export row - never a real index into `devices`.
|
||||
const PNG_EXPORT_INDEX = -1;
|
||||
// Tick spacing tiers, coarser the longer the ruler runs. See docs/implementation.md#ruler-tier-selection.
|
||||
const RULER_TIERS = [
|
||||
{aboveMm: 0, tickMm: 1, majorEveryMm: 5},
|
||||
|
|
@ -297,7 +254,8 @@ export default {
|
|||
},
|
||||
data() {
|
||||
return {
|
||||
usbSupported: true,
|
||||
// Whether this browser can pair with real USB printers at all; when false, deviceRows below only ever has its PNG Export row.
|
||||
usbSupported: "usb" in navigator,
|
||||
blobReady: false,
|
||||
busy: false,
|
||||
error: null,
|
||||
|
|
@ -311,7 +269,7 @@ 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.
|
||||
// Scan-reliability messages from label.js's drawLabel (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. queryFields is applied last so an explicit ?text=... etc. always wins over a `kind` builder's computed value.
|
||||
|
|
@ -324,13 +282,16 @@ export default {
|
|||
...Object.fromEntries(BASE_VARS.filter(v => v in this.queryFields).map(v => [v, this.queryFields[v]])),
|
||||
},
|
||||
copies: 1,
|
||||
// Only sent to a real printer whose driver reports the "chain" capability (see chainSupported) - harmless to leave set when switching to a driver that ignores it.
|
||||
chainMode: false,
|
||||
// PNG Export's stand-in for a real tape's mediaWidthMm - there's no physical tape to measure, so this is the one thing about previewTape's simulated profile the user can adjust.
|
||||
simulatedHeightPx: SIMULATED_DEFAULT_HEIGHT_PX,
|
||||
selectedTemplate: LABEL_TEMPLATES[0].id,
|
||||
// Ids of the last MAX_RECENT_TEMPLATES distinct templates printed/downloaded, most recent first. See rememberPrintedTemplate/loadRecentTemplateIds.
|
||||
recentTemplateIds: [],
|
||||
// "along" reads along the tape's feed direction (usual case, width-constrained); "across" turns 90deg on that same width instead. See label.js's drawLabel/drawFallbackLabel.
|
||||
// "along" reads along the tape's feed direction (usual case, width-constrained); "across" turns 90deg on that same width instead. See label.js's drawLabel.
|
||||
orientation: "along",
|
||||
|
||||
fallbackReady: false,
|
||||
// TODO: replace with the real commands for our printers.
|
||||
brotherCliExample: "brother_ql --model QL-000 --printer usb://0000:0000 print --label 00 label.png",
|
||||
niimbotCliExample: "niimprint --model b00 --conn usb print --density 3 --image label.png",
|
||||
|
|
@ -371,11 +332,22 @@ export default {
|
|||
currentTemplate() {
|
||||
return LABEL_TEMPLATES.find(t => t.id === this.selectedTemplate) || LABEL_TEMPLATES[0];
|
||||
},
|
||||
// Always includes the virtual PNG Export row, so the printer list (and the preview built from
|
||||
// whichever row is active) looks the same whether or not this browser/session has any real
|
||||
// USB printer paired - PNG Export is simply the active row whenever no real device is.
|
||||
deviceRows() {
|
||||
const pngRow = {
|
||||
index: PNG_EXPORT_INDEX,
|
||||
name: "PNG Export",
|
||||
supported: true,
|
||||
reason: "",
|
||||
isConnected: !this.connected,
|
||||
isPngExport: true,
|
||||
};
|
||||
if (!this.blob) {
|
||||
return [];
|
||||
return [pngRow];
|
||||
}
|
||||
return this.devices.map((d, index) => {
|
||||
const real = this.devices.map((d, index) => {
|
||||
const info = this.blob.findDevice(d.vendorId, d.productId);
|
||||
return {
|
||||
index,
|
||||
|
|
@ -385,20 +357,41 @@ export default {
|
|||
isConnected: d === this.connected,
|
||||
};
|
||||
});
|
||||
return [pngRow, ...real];
|
||||
},
|
||||
canPrint() {
|
||||
return Boolean(this.tape && this.labelBitmap && !this.busy);
|
||||
// Print/Download button and Copies input are only meaningful with a real printer connected; PNG Export can always export once a bitmap's rendered.
|
||||
canAct() {
|
||||
return Boolean(this.labelBitmap && !this.busy);
|
||||
},
|
||||
// Whether the connected printer's driver honors chain mode - some (e.g. Brother's QL line) don't. See vendor/weblabel.js's MultiPrinterBlob.can.
|
||||
chainSupported() {
|
||||
return Boolean(this.connected && this.blob && this.blob.can("chain", this.connected));
|
||||
},
|
||||
// The connected printer's tape, or PNG Export's stand-in profile (its height user-adjustable via simulatedHeightPx) so the ruler/preview always have something realistic to show.
|
||||
previewTape() {
|
||||
if (this.tape) {
|
||||
return this.tape;
|
||||
}
|
||||
const printAreaPx = Math.min(SIMULATED_MAX_HEIGHT_PX,
|
||||
Math.max(SIMULATED_MIN_HEIGHT_PX, Number(this.simulatedHeightPx) || SIMULATED_DEFAULT_HEIGHT_PX));
|
||||
return {
|
||||
dpi: SIMULATED_DPI,
|
||||
mediaWidthMm: Math.round(printAreaPx / SIMULATED_DPI * 25.4 * 10) / 10,
|
||||
printAreaPx,
|
||||
printLengthPx: 0,
|
||||
leadPx: SIMULATED_LEAD_PX,
|
||||
};
|
||||
},
|
||||
// px per real mm at current zoom; meaningful only for the tape-fed preview (the no-webusb fallback has no real tape/dpi, so no ruler).
|
||||
tapePxPerMm() {
|
||||
return this.tape ? (this.tape.dpi / 25.4) * this.zoom : 0;
|
||||
return (this.previewTape.dpi / 25.4) * this.zoom;
|
||||
},
|
||||
// Physical mm length each ruler axis must cover; see horizontal/verticalRulerTicks for what each measures.
|
||||
horizontalTotalMm() {
|
||||
return (this.tape && this.printedWidthPx) ? this.printedWidthPx / (this.tape.dpi / 25.4) : 0;
|
||||
return this.printedWidthPx ? this.printedWidthPx / (this.previewTape.dpi / 25.4) : 0;
|
||||
},
|
||||
verticalTotalMm() {
|
||||
return this.tape ? this.tape.mediaWidthMm : 0;
|
||||
return this.previewTape.mediaWidthMm;
|
||||
},
|
||||
// Shared tier keyed off whichever axis is longer. See docs/implementation.md#ruler-tier-selection.
|
||||
rulerTier() {
|
||||
|
|
@ -407,14 +400,14 @@ export default {
|
|||
},
|
||||
// Ticks along the tape's printed length, feed margins included (still real tape).
|
||||
horizontalRulerTicks() {
|
||||
if (!this.tape || !this.printedWidthPx) {
|
||||
if (!this.printedWidthPx) {
|
||||
return [];
|
||||
}
|
||||
return this.rulerTicks(this.horizontalTotalMm);
|
||||
},
|
||||
// Ticks across the tape's full width, not just the printable area. See docs/implementation.md#tape-full-print-margin.
|
||||
verticalRulerTicks() {
|
||||
return this.tape ? this.rulerTicks(this.verticalTotalMm) : [];
|
||||
return this.rulerTicks(this.verticalTotalMm);
|
||||
},
|
||||
// e.g. "(5px, 23px)", or "" so it appends cleanly onto the tape-width <small> with nothing shown.
|
||||
textSizesSummary() {
|
||||
|
|
@ -427,29 +420,20 @@ export default {
|
|||
watch: {
|
||||
varValues: {
|
||||
handler() {
|
||||
if (this.usbSupported) {
|
||||
this.redraw();
|
||||
} else {
|
||||
this.redrawFallback();
|
||||
}
|
||||
},
|
||||
deep: true,
|
||||
},
|
||||
selectedTemplate() {
|
||||
if (this.usbSupported) {
|
||||
this.redraw();
|
||||
} else {
|
||||
this.redrawFallback();
|
||||
}
|
||||
},
|
||||
orientation() {
|
||||
if (this.usbSupported) {
|
||||
this.redraw();
|
||||
} else {
|
||||
this.redrawFallback();
|
||||
}
|
||||
},
|
||||
// Catches printer connect/disconnect/switch; flush:'post' since the canvas only exists once `tape` is truthy (template's v-if).
|
||||
simulatedHeightPx() {
|
||||
this.redraw();
|
||||
},
|
||||
// Catches printer connect/disconnect/switch, redrawing with the real tape or falling back to previewTape's simulated one.
|
||||
tape: {
|
||||
handler() {
|
||||
this.redraw();
|
||||
|
|
@ -583,6 +567,10 @@ export default {
|
|||
},
|
||||
|
||||
connect(index) {
|
||||
if (index === PNG_EXPORT_INDEX) {
|
||||
this.guard(() => this.closeConnection());
|
||||
return;
|
||||
}
|
||||
this.guard(() => this.connectToDevice(this.devices[index]));
|
||||
},
|
||||
|
||||
|
|
@ -604,9 +592,6 @@ export default {
|
|||
|
||||
redraw() {
|
||||
this.labelBitmap = null;
|
||||
if (!this.tape) {
|
||||
return;
|
||||
}
|
||||
const canvas = this.$refs.labelCanvas;
|
||||
if (!canvas) {
|
||||
return;
|
||||
|
|
@ -615,7 +600,7 @@ export default {
|
|||
let textSizesPx, warnings;
|
||||
try {
|
||||
const content = templateContent(this.currentTemplate, this.fields);
|
||||
({textSizesPx, warnings} = drawLabel(canvas, this.tape, content, this.orientation));
|
||||
({textSizesPx, warnings} = drawLabel(canvas, this.previewTape, content, this.orientation));
|
||||
} catch (e) {
|
||||
this.error = e.message;
|
||||
return;
|
||||
|
|
@ -630,29 +615,8 @@ export default {
|
|||
this.zoom = this.fitZoom(canvas);
|
||||
},
|
||||
|
||||
redrawFallback() {
|
||||
this.fallbackReady = false;
|
||||
const canvas = this.$refs.fallbackCanvas;
|
||||
if (!canvas) {
|
||||
return;
|
||||
}
|
||||
this.resizeObserver.observe(canvas.parentElement);
|
||||
let warnings;
|
||||
try {
|
||||
const content = templateContent(this.currentTemplate, this.fields);
|
||||
({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);
|
||||
},
|
||||
|
||||
downloadPng() {
|
||||
const canvas = this.$refs.fallbackCanvas;
|
||||
const canvas = this.$refs.labelCanvas;
|
||||
if (!canvas) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -681,7 +645,7 @@ export default {
|
|||
print() {
|
||||
this.guard(async () => {
|
||||
const copies = Math.max(1, Math.min(20, Number(this.copies) || 1));
|
||||
await this.blob.printBitmap(this.labelBitmap, {copies});
|
||||
await this.blob.printBitmap(this.labelBitmap, {copies, chain: this.chainMode});
|
||||
this.rememberPrintedTemplate(this.selectedTemplate);
|
||||
});
|
||||
},
|
||||
|
|
@ -723,22 +687,23 @@ export default {
|
|||
// Same reasoning - drawTextLeaf's bitmap-font tiers (see label.js's fontTierFor) need this
|
||||
// loaded before the first real draw, same as qrReady below.
|
||||
const fontsReady = Promise.all([qrReady, preloadPixelFontRenderer()]);
|
||||
if (!("usb" in navigator)) {
|
||||
this.usbSupported = false;
|
||||
await Promise.all([nextTick(), fontsReady]);
|
||||
this.redrawFallback();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
this.blob = await MultiPrinterBlob.load(BLOB_URL);
|
||||
} catch (e) {
|
||||
const blobLoad = this.usbSupported
|
||||
? MultiPrinterBlob.load(BLOB_URL).catch(e => {
|
||||
this.error = `Could not load the printer driver: ${e.message}`;
|
||||
return null;
|
||||
})
|
||||
: Promise.resolve(null);
|
||||
await fontsReady;
|
||||
// Draws PNG Export's simulated tape right away rather than waiting on the driver/device enumeration below; the tape watcher redraws again with a real one if/once autoConnectPreferred finds it.
|
||||
await nextTick();
|
||||
this.redraw();
|
||||
this.blob = await blobLoad;
|
||||
if (!this.blob) {
|
||||
return;
|
||||
}
|
||||
this.blobReady = true;
|
||||
navigator.usb.addEventListener("connect", this.onUsbChange);
|
||||
navigator.usb.addEventListener("disconnect", this.onUsbChange);
|
||||
await fontsReady;
|
||||
await this.guard(async () => {
|
||||
await this.refreshDevices();
|
||||
await this.autoConnectPreferred();
|
||||
|
|
@ -756,6 +721,44 @@ export default {
|
|||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* Sticks the preview/print card in view while the left column scrolls; only above `lg`, where the columns are actually side by side. */
|
||||
@media (min-width: 992px) {
|
||||
.print-sticky {
|
||||
position: sticky;
|
||||
top: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.chip-row {
|
||||
gap: .5rem;
|
||||
}
|
||||
|
||||
.calculated-field {
|
||||
min-width: 6rem;
|
||||
}
|
||||
|
||||
.printer-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
border: 1px solid var(--bs-gray-300);
|
||||
border-radius: 1rem;
|
||||
padding: .15rem .35rem .15rem .7rem;
|
||||
font-size: .85rem;
|
||||
}
|
||||
|
||||
.printer-chip-connected {
|
||||
border-color: var(--bs-success);
|
||||
color: var(--bs-success);
|
||||
}
|
||||
|
||||
.printer-chip-disabled {
|
||||
color: var(--bs-gray-600);
|
||||
}
|
||||
|
||||
.cli-details summary {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.label-preview {
|
||||
overflow-x: auto;
|
||||
padding: .75rem;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue