stash
This commit is contained in:
parent
7276750c66
commit
341967a611
1 changed files with 216 additions and 213 deletions
|
|
@ -4,124 +4,37 @@
|
||||||
<div class="container-fluid p-0">
|
<div class="container-fluid p-0">
|
||||||
<h1 class="h3 mb-3">Print a label</h1>
|
<h1 class="h3 mb-3">Print a label</h1>
|
||||||
|
|
||||||
<div v-if="error" class="alert alert-danger" role="alert">{{ error }}</div>
|
<!-- Left: what to print. Right: how it looks and how it prints, kept in view via .print-sticky. -->
|
||||||
|
<div class="row">
|
||||||
|
|
||||||
<div v-if="warnings.length" class="alert alert-warning" role="alert">
|
<div class="col-lg-7 mb-4">
|
||||||
<div v-for="(w, i) in warnings" :key="i">{{ w.message }}</div>
|
<div class="print-sticky">
|
||||||
</div>
|
<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="!usbSupported" class="alert alert-warning">
|
<div v-if="error" class="alert alert-danger" role="alert">{{ error }}</div>
|
||||||
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.
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row mb-3">
|
<div v-if="warnings.length" class="alert alert-warning" role="alert">
|
||||||
<div class="col-lg-8">
|
<div v-for="(w, i) in warnings" :key="i">{{ w.message }}</div>
|
||||||
<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>
|
|
||||||
<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 v-if="!usbSupported" class="alert alert-warning">
|
||||||
<div class="col-lg-7">
|
This browser can't pair with USB label printers directly. Open this page in
|
||||||
<div class="card">
|
Chrome, Edge or
|
||||||
<div class="card-header">
|
Opera over https:// for that - you can still build and download labels
|
||||||
<h5 class="card-title mb-0">Label preview</h5>
|
as PNGs below.
|
||||||
</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>
|
||||||
</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="preview-row mb-3" v-show="labelBitmap">
|
||||||
<div class="ruler-v">
|
<div class="ruler-v">
|
||||||
<div class="ruler-v-corner"></div>
|
<div class="ruler-v-corner"></div>
|
||||||
<div class="ruler ruler-v-ticks"
|
<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"
|
<span v-for="t in verticalRulerTicks" :key="'tick-' + t.mm"
|
||||||
class="tick" :class="{'tick-major': t.major}"
|
class="tick" :class="{'tick-major': t.major}"
|
||||||
:style="{top: t.pos + 'px'}"></span>
|
:style="{top: t.pos + 'px'}"></span>
|
||||||
|
|
@ -142,7 +55,7 @@
|
||||||
</div>
|
</div>
|
||||||
<!-- Tape's full physical width; canvas is narrower/centered, margin is real tape. See docs/implementation.md#tape-full-print-margin. -->
|
<!-- 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"
|
<div class="tape-full"
|
||||||
:style="{height: (tape.mediaWidthMm * tapePxPerMm) + 'px'}">
|
:style="{height: (previewTape.mediaWidthMm * tapePxPerMm) + 'px'}">
|
||||||
<div class="label-preview">
|
<div class="label-preview">
|
||||||
<canvas ref="labelCanvas"></canvas>
|
<canvas ref="labelCanvas"></canvas>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -169,60 +82,97 @@
|
||||||
<div class="col-auto">
|
<div class="col-auto">
|
||||||
<label class="form-label">Copies</label>
|
<label class="form-label">Copies</label>
|
||||||
<input type="number" class="form-control copies-input"
|
<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>
|
||||||
<div class="col-auto ms-auto">
|
<div class="col-auto ms-auto">
|
||||||
<button class="btn btn-primary" :disabled="!canPrint || busy"
|
<button class="btn btn-primary" :disabled="!canAct"
|
||||||
@click="print">
|
@click="connected ? print() : downloadPng()">
|
||||||
<b-icon-printer class="me-1"></b-icon-printer>
|
<b-icon-printer v-if="connected" class="me-1"></b-icon-printer>
|
||||||
Print
|
<b-icon-download v-else class="me-1"></b-icon-download>
|
||||||
|
{{ connected ? "Print" : "Download PNG" }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
|
||||||
|
<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>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-lg-5 mb-4">
|
<div class="col-lg-5 mb-4">
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<h5 class="card-title mb-0">Printer</h5>
|
<h5 class="card-title mb-0">Content</h5>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<p v-if="!devices.length" class="text-muted">
|
<div class="row g-3">
|
||||||
No label printer paired yet.
|
<div :class="v === 'text' ? 'col-12' : 'col-sm-6'" v-for="v in baseVars" :key="v">
|
||||||
</p>
|
<label class="form-label">{{ varLabel(v) }}</label>
|
||||||
<ul v-else class="list-group mb-3">
|
<textarea v-if="v === 'text'" class="form-control" rows="3"
|
||||||
<li v-for="d in deviceRows" :key="d.index"
|
v-model="varValues[v]"
|
||||||
class="list-group-item d-flex justify-content-between align-items-center">
|
placeholder="https://example.com/…"></textarea>
|
||||||
<div>
|
<input v-else type="text" class="form-control" v-model="varValues[v]">
|
||||||
<div>{{ d.name }}</div>
|
</div>
|
||||||
<small v-if="d.isConnected" class="text-success">connected</small>
|
</div>
|
||||||
<small v-else-if="!d.supported" class="text-muted">{{ d.reason }}</small>
|
|
||||||
|
<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>
|
||||||
<button v-if="d.isConnected" class="btn btn-sm btn-outline-secondary"
|
</div>
|
||||||
:disabled="busy" @click="disconnect()">
|
</div>
|
||||||
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">
|
|
||||||
<b-icon-plug class="me-1"></b-icon-plug>
|
|
||||||
Pair new printer
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12">
|
<div class="col-lg-12 mb-4">
|
||||||
<label-layout-preview :fields="fields" :value="selectedTemplate" :tape="tape"
|
<label-layout-preview :fields="fields" :value="selectedTemplate" :tape="previewTape"
|
||||||
:recent-template-ids="recentTemplateIds"
|
:recent-template-ids="recentTemplateIds" class="mb-3"
|
||||||
@input="selectedTemplate = $event"></label-layout-preview>
|
@input="selectedTemplate = $event"></label-layout-preview>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -243,7 +193,6 @@ import {MultiPrinterBlob, canvasToBitmap, bitmapToCanvas} from "../../vendor/web
|
||||||
import {
|
import {
|
||||||
tapeFromStatus,
|
tapeFromStatus,
|
||||||
drawLabel,
|
drawLabel,
|
||||||
drawFallbackLabel,
|
|
||||||
buildLabelContent,
|
buildLabelContent,
|
||||||
buildLabelFields,
|
buildLabelFields,
|
||||||
preloadQrEncoder,
|
preloadQrEncoder,
|
||||||
|
|
@ -268,6 +217,14 @@ const MAX_RECENT_TEMPLATES = 4;
|
||||||
|
|
||||||
const MAX_ZOOM = 4; /* never magnify the preview more than this */
|
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 */
|
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.
|
// Tick spacing tiers, coarser the longer the ruler runs. See docs/implementation.md#ruler-tier-selection.
|
||||||
const RULER_TIERS = [
|
const RULER_TIERS = [
|
||||||
{aboveMm: 0, tickMm: 1, majorEveryMm: 5},
|
{aboveMm: 0, tickMm: 1, majorEveryMm: 5},
|
||||||
|
|
@ -297,7 +254,8 @@ export default {
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
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,
|
blobReady: false,
|
||||||
busy: false,
|
busy: false,
|
||||||
error: null,
|
error: null,
|
||||||
|
|
@ -311,7 +269,7 @@ export default {
|
||||||
printedWidthPx: 0,
|
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.
|
// 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: [],
|
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: [],
|
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.
|
// 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]])),
|
...Object.fromEntries(BASE_VARS.filter(v => v in this.queryFields).map(v => [v, this.queryFields[v]])),
|
||||||
},
|
},
|
||||||
copies: 1,
|
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,
|
selectedTemplate: LABEL_TEMPLATES[0].id,
|
||||||
// Ids of the last MAX_RECENT_TEMPLATES distinct templates printed/downloaded, most recent first. See rememberPrintedTemplate/loadRecentTemplateIds.
|
// Ids of the last MAX_RECENT_TEMPLATES distinct templates printed/downloaded, most recent first. See rememberPrintedTemplate/loadRecentTemplateIds.
|
||||||
recentTemplateIds: [],
|
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",
|
orientation: "along",
|
||||||
|
|
||||||
fallbackReady: false,
|
|
||||||
// TODO: replace with the real commands for our printers.
|
// TODO: replace with the real commands for our printers.
|
||||||
brotherCliExample: "brother_ql --model QL-000 --printer usb://0000:0000 print --label 00 label.png",
|
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",
|
niimbotCliExample: "niimprint --model b00 --conn usb print --density 3 --image label.png",
|
||||||
|
|
@ -371,11 +332,22 @@ export default {
|
||||||
currentTemplate() {
|
currentTemplate() {
|
||||||
return LABEL_TEMPLATES.find(t => t.id === this.selectedTemplate) || LABEL_TEMPLATES[0];
|
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() {
|
deviceRows() {
|
||||||
|
const pngRow = {
|
||||||
|
index: PNG_EXPORT_INDEX,
|
||||||
|
name: "PNG Export",
|
||||||
|
supported: true,
|
||||||
|
reason: "",
|
||||||
|
isConnected: !this.connected,
|
||||||
|
isPngExport: true,
|
||||||
|
};
|
||||||
if (!this.blob) {
|
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);
|
const info = this.blob.findDevice(d.vendorId, d.productId);
|
||||||
return {
|
return {
|
||||||
index,
|
index,
|
||||||
|
|
@ -385,20 +357,41 @@ export default {
|
||||||
isConnected: d === this.connected,
|
isConnected: d === this.connected,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
return [pngRow, ...real];
|
||||||
},
|
},
|
||||||
canPrint() {
|
// Print/Download button and Copies input are only meaningful with a real printer connected; PNG Export can always export once a bitmap's rendered.
|
||||||
return Boolean(this.tape && this.labelBitmap && !this.busy);
|
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).
|
// 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() {
|
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.
|
// Physical mm length each ruler axis must cover; see horizontal/verticalRulerTicks for what each measures.
|
||||||
horizontalTotalMm() {
|
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() {
|
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.
|
// Shared tier keyed off whichever axis is longer. See docs/implementation.md#ruler-tier-selection.
|
||||||
rulerTier() {
|
rulerTier() {
|
||||||
|
|
@ -407,14 +400,14 @@ export default {
|
||||||
},
|
},
|
||||||
// Ticks along the tape's printed length, feed margins included (still real tape).
|
// Ticks along the tape's printed length, feed margins included (still real tape).
|
||||||
horizontalRulerTicks() {
|
horizontalRulerTicks() {
|
||||||
if (!this.tape || !this.printedWidthPx) {
|
if (!this.printedWidthPx) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
return this.rulerTicks(this.horizontalTotalMm);
|
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.
|
// Ticks across the tape's full width, not just the printable area. See docs/implementation.md#tape-full-print-margin.
|
||||||
verticalRulerTicks() {
|
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.
|
// e.g. "(5px, 23px)", or "" so it appends cleanly onto the tape-width <small> with nothing shown.
|
||||||
textSizesSummary() {
|
textSizesSummary() {
|
||||||
|
|
@ -427,29 +420,20 @@ export default {
|
||||||
watch: {
|
watch: {
|
||||||
varValues: {
|
varValues: {
|
||||||
handler() {
|
handler() {
|
||||||
if (this.usbSupported) {
|
this.redraw();
|
||||||
this.redraw();
|
|
||||||
} else {
|
|
||||||
this.redrawFallback();
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
deep: true,
|
deep: true,
|
||||||
},
|
},
|
||||||
selectedTemplate() {
|
selectedTemplate() {
|
||||||
if (this.usbSupported) {
|
this.redraw();
|
||||||
this.redraw();
|
|
||||||
} else {
|
|
||||||
this.redrawFallback();
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
orientation() {
|
orientation() {
|
||||||
if (this.usbSupported) {
|
this.redraw();
|
||||||
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: {
|
tape: {
|
||||||
handler() {
|
handler() {
|
||||||
this.redraw();
|
this.redraw();
|
||||||
|
|
@ -517,7 +501,7 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
rememberPrintedTemplate(id) {
|
rememberPrintedTemplate(id) {
|
||||||
const updated = [id, ...this.recentTemplateIds.filter(t => t !== id)].slice(0, MAX_RECENT_TEMPLATES);
|
const updated = [id, ...this.recentTemplateIds.filter(t => t !== id)].slice(0, MAX_RECENT_TEMPLATES);
|
||||||
this.recentTemplateIds = updated;
|
this.recentTemplateIds = updated;
|
||||||
try {
|
try {
|
||||||
|
|
@ -583,6 +567,10 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
connect(index) {
|
connect(index) {
|
||||||
|
if (index === PNG_EXPORT_INDEX) {
|
||||||
|
this.guard(() => this.closeConnection());
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.guard(() => this.connectToDevice(this.devices[index]));
|
this.guard(() => this.connectToDevice(this.devices[index]));
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -604,9 +592,6 @@ export default {
|
||||||
|
|
||||||
redraw() {
|
redraw() {
|
||||||
this.labelBitmap = null;
|
this.labelBitmap = null;
|
||||||
if (!this.tape) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const canvas = this.$refs.labelCanvas;
|
const canvas = this.$refs.labelCanvas;
|
||||||
if (!canvas) {
|
if (!canvas) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -615,7 +600,7 @@ export default {
|
||||||
let textSizesPx, warnings;
|
let textSizesPx, warnings;
|
||||||
try {
|
try {
|
||||||
const content = templateContent(this.currentTemplate, this.fields);
|
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) {
|
} catch (e) {
|
||||||
this.error = e.message;
|
this.error = e.message;
|
||||||
return;
|
return;
|
||||||
|
|
@ -630,29 +615,8 @@ export default {
|
||||||
this.zoom = this.fitZoom(canvas);
|
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() {
|
downloadPng() {
|
||||||
const canvas = this.$refs.fallbackCanvas;
|
const canvas = this.$refs.labelCanvas;
|
||||||
if (!canvas) {
|
if (!canvas) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -681,7 +645,7 @@ export default {
|
||||||
print() {
|
print() {
|
||||||
this.guard(async () => {
|
this.guard(async () => {
|
||||||
const copies = Math.max(1, Math.min(20, Number(this.copies) || 1));
|
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);
|
this.rememberPrintedTemplate(this.selectedTemplate);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
@ -723,22 +687,23 @@ export default {
|
||||||
// Same reasoning - drawTextLeaf's bitmap-font tiers (see label.js's fontTierFor) need this
|
// Same reasoning - drawTextLeaf's bitmap-font tiers (see label.js's fontTierFor) need this
|
||||||
// loaded before the first real draw, same as qrReady below.
|
// loaded before the first real draw, same as qrReady below.
|
||||||
const fontsReady = Promise.all([qrReady, preloadPixelFontRenderer()]);
|
const fontsReady = Promise.all([qrReady, preloadPixelFontRenderer()]);
|
||||||
if (!("usb" in navigator)) {
|
const blobLoad = this.usbSupported
|
||||||
this.usbSupported = false;
|
? MultiPrinterBlob.load(BLOB_URL).catch(e => {
|
||||||
await Promise.all([nextTick(), fontsReady]);
|
this.error = `Could not load the printer driver: ${e.message}`;
|
||||||
this.redrawFallback();
|
return null;
|
||||||
return;
|
})
|
||||||
}
|
: Promise.resolve(null);
|
||||||
try {
|
await fontsReady;
|
||||||
this.blob = await MultiPrinterBlob.load(BLOB_URL);
|
// 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.
|
||||||
} catch (e) {
|
await nextTick();
|
||||||
this.error = `Could not load the printer driver: ${e.message}`;
|
this.redraw();
|
||||||
|
this.blob = await blobLoad;
|
||||||
|
if (!this.blob) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.blobReady = true;
|
this.blobReady = true;
|
||||||
navigator.usb.addEventListener("connect", this.onUsbChange);
|
navigator.usb.addEventListener("connect", this.onUsbChange);
|
||||||
navigator.usb.addEventListener("disconnect", this.onUsbChange);
|
navigator.usb.addEventListener("disconnect", this.onUsbChange);
|
||||||
await fontsReady;
|
|
||||||
await this.guard(async () => {
|
await this.guard(async () => {
|
||||||
await this.refreshDevices();
|
await this.refreshDevices();
|
||||||
await this.autoConnectPreferred();
|
await this.autoConnectPreferred();
|
||||||
|
|
@ -756,6 +721,44 @@ export default {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<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 {
|
.label-preview {
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
padding: .75rem;
|
padding: .75rem;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue