Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion PRIVACY.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ ZPLab is a label designer that runs on your device. It has no user accounts, no
ZPLab only connects to the following destinations:

- **Printers and data sources you configure.** Labels go to the printer you choose, over the network or USB, or through the system print spooler or the Zebra Browser Print agent on your computer. Variable data is read from the files, databases or servers you connect.
- **Labelary rendering.** Labelary is the default renderer for previews, **Print as Image** and PDF export. ZPLab asks for your consent before the first request to the public service. After that, each of these actions sends the label's ZPL to `api.labelary.com`, including values filled in from connected data sources. Labelary is an independent third-party service with its own [terms and privacy practices](https://labelary.com/service.html). If you set your own endpoint, requests go there instead. You can withdraw your consent or disable Labelary entirely under **File → Settings… → App → Preview**. In the desktop app, a connected printer can render instead.
- **Labelary rendering.** Labelary is the default renderer for previews, the **Other printer** way under **File → Output** and PDF export. ZPLab asks for your consent before the first request to the public service. After that, each of these actions sends the label's ZPL to `api.labelary.com`, including values filled in from connected data sources. Labelary is an independent third-party service with its own [terms and privacy practices](https://labelary.com/service.html). If you set your own endpoint, requests go there instead. You can withdraw your consent or disable Labelary entirely under **File → Settings… → App → Preview**. In the desktop app, a connected printer can render instead.
- **Updates.** A build downloaded from GitHub checks `github.com` for a new version at every start and downloads an update from GitHub's release hosts only when you choose to install it. The Store version and other packaged installs update through their package and skip this check.
- **MCP server, optional.** When you start it, the server listens on `127.0.0.1` only and requires a token that the app generates. An assistant you connect can read and edit the open label, so its content goes to whatever service that assistant uses.
- **The web version.** Your browser loads `app.zplab.org` from GitHub Pages, which is covered by [GitHub's privacy statement](https://docs.github.com/site-policy/privacy-policies/github-general-privacy-statement).
Expand Down
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,9 +81,9 @@ Select several objects with Shift-click or a lasso. Position and size changes ap
The **ZPL** panel at the bottom shows the generated ZPL, **Copy** puts it on the clipboard. On desktop the panel names the configured printer. A click shows its state and offers to read its configuration. The canvas is ZPLab's rendering of the label. A second rendering comes from the preview renderer under **File → Settings… → App → Preview**: the connected printer (desktop) or, with consent, the third-party service [Labelary](https://labelary.com/). **Preview** shows it for comparison.

- **File → Export ZPL:** saves a `.zpl` file, or `.prn` where the save dialog offers file types
- **File → Send to Zebra Printer:** opens the send dialog. It sends over the network, through the Zebra Browser Print agent in the browser, or through the system spooler or USB on desktop. On desktop, **Check printer** asks the printer for its state over the network or USB.
- **File → Export PDF:** saves one PDF page per label page, rendered by the preview renderer. With the renderer off, ZPLab captures the canvas, so the PDF holds the current page only. A batch PDF needs the renderer and holds one page per data row.
- **File → Print as Image (browser):** renders the current page with the same renderer, then opens the browser print dialog
- **File → Output:** opens the output dialog with two destinations.
- **Print** sends the code to a Zebra over the network, through the Zebra Browser Print agent in the browser, or through the system spooler or USB on desktop. Its **Other printer** way renders the label and hands it to the operating system's print dialog, which reaches any printer with a driver. On desktop, **Check printer** asks the printer for its state over the network or USB.
- **File** writes a **PDF** with one page per label page, rendered by the preview renderer. With the renderer off, ZPLab captures the canvas, so the PDF holds the current page only. The **Scope** option **One label per data row** needs the renderer and holds one page per row. **PNG** saves the canvas drawing of the current label.

### 5. Save the design

Expand Down Expand Up @@ -139,7 +139,7 @@ The **Fonts** tab uploads TrueType fonts and assigns their `^CW` alias. On the d

### Batch printing

**File → Import CSV data** (web) or **File → Connect data** (desktop) loads a CSV. Assign a column to each variable in the mapping dialog. **File → Export batch ZPL** or **File → Send to Zebra Printer** then produce one label per row. Export names the row count, the send item names the labels the printer will produce.
**File → Import CSV data** (web) or **File → Connect data** (desktop) loads a CSV. Assign a column to each variable in the mapping dialog. **File → Export batch ZPL** writes one label per row and names the row count. **File → Output** sends the same rows to the printer, and the dialog names the labels it will produce.

The batch stores the page as a format on the printer and recalls it per row. A page that names a stored format keeps that name when the name itself is at most 8 characters. Longer names fall back to `R:LBL.ZPL`.

Expand All @@ -151,11 +151,11 @@ On desktop, **Connect data** also reads an Excel worksheet. **File → Settings

- The **Objects** group selects the fonts and graphics the **Setup Script** uploads.
- It lists cached images and deletes the unused ones.
- On desktop it reads what the printer holds, so every row says where its file stands and a bar shows how full each drive is. Show a stored graphic or let the printer draw a font sample. Delete objects on the printer.
- On desktop it reads what the printer holds, so every row says where its file stands. A bar shows how full each drive is. Show a stored graphic or let the printer draw a font sample. Delete objects on the printer.
- Saved designs and label exports exclude **Setup Script** values such as printer name and locale.
- **Clear** resets the **Setup Script** values and keeps the uploads.

The **App → Printer** tab sets how labels are sent and to which printer. **Send to Zebra Printer** opens with that method selected. The **App → Preview** tab selects the renderer: the connected printer (desktop), Labelary, or off. It takes a premium Labelary endpoint and API key. The key is stored in the OS keychain on desktop and in browser storage on the web.
The **App → Printer** tab sets how labels are sent and to which printer. **File → Output** opens with that method selected. The **App → Preview** tab selects the renderer: the connected printer (desktop), Labelary, or off. It takes a premium Labelary endpoint and API key. The key is stored in the OS keychain on desktop and in browser storage on the web.

### MCP server

Expand Down
1 change: 1 addition & 0 deletions src-tauri/capabilities/default.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
],
"permissions": [
"core:default",
"core:webview:allow-print",
"window-state:default",
"updater:default",
"process:allow-restart",
Expand Down
61 changes: 24 additions & 37 deletions src/components/AppShell.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useState, useEffect, useRef, type ComponentType, type SVGProps } from "react";
import { useEffect, useRef, type ComponentType, type SVGProps } from "react";
import { DndContext, PointerSensor, useSensor, useSensors } from "@dnd-kit/core";
import { makePaletteCollision } from "../dnd/collision";
import { ObjectPalette } from "./Palette/ObjectPalette";
Expand All @@ -14,7 +14,7 @@ import { ZplImportModal } from "./Output/ZplImportModal";
import { VariableMappingModal } from "./Variables/VariableMappingModal";
import { ConnectDataWizard } from "./Variables/ConnectDataWizard";
import { CsvImportConfirmDialog } from "./Variables/CsvImportConfirmDialog";
import { PrintToZebraDialog } from "./Output/PrintToZebraDialog";
import { OutputDialog } from "./Output/OutputDialog";
import {
DropdownMenu,
DropdownItem,
Expand All @@ -34,24 +34,22 @@ import {
TableCellsIcon,
PrinterIcon,
Cog6ToothIcon,
PaperAirplaneIcon,
XMarkIcon,
SunIcon,
MoonIcon,
GlobeAltIcon,
} from "@heroicons/react/16/solid";
import { useLabelStore, useHistory, selectLabelaryNoticeRequired, selectEffectivePreviewProvider, selectEditorFrozen, selectSourceEditing, selectSourceEditDirty, selectDocumentEmits, selectBatchPrintCount } from "../store/labelStore";
import { useLabelStore, useHistory, selectEffectivePreviewProvider, selectEditorFrozen, selectSourceEditing, selectSourceEditDirty, selectDocumentEmits } from "../store/labelStore";
import { datasetTimestamp } from "@zplab/core/types/DataSource";
import { isCurrentDataContext, settleDatasetReplace } from "../store/datasetActions";
import { ConfirmDialog } from "./ui/ConfirmDialog";
import { formatTemplate } from "../lib/formatTemplate";
import { buildMenuModel, type MenuItemId } from "../lib/menuModel";
import { buildMenuModel, type MenuFlags, type MenuItemId } from "../lib/menuModel";
import { NativeMenuBridge } from "./NativeMenuBridge";
import type { MenuHandlers } from "../hooks/useNativeMenu";
import { isDesktopShell, isMacDesktop } from "../lib/platform";
import { acceptAttr, DESIGN_FILTER, CSV_FILTER } from "../lib/fileDialogs";
import { openExternal, REPO_URL } from "../lib/openExternal";
import { LabelaryNoticeModal } from "./Output/LabelaryNoticeModal";
import { PdfExportDialog } from "./Output/PdfExportDialog";
import { usePdfExport } from "../hooks/usePdfExport";
import { PrinterSettingsModal } from "./PrinterSettings/PrinterSettingsModal";
Expand Down Expand Up @@ -80,14 +78,11 @@ const MENU_ICONS: Partial<Record<MenuItemId, ComponentType<SVGProps<SVGSVGElemen
settings: Cog6ToothIcon,
exportZpl: ArrowDownTrayIcon,
exportBatch: ArrowDownTrayIcon,
exportPdf: ArrowDownTrayIcon,
exportBatchPdf: ArrowDownTrayIcon,
openDesign: FolderOpenIcon,
saveDesign: DocumentArrowDownIcon,
importCsv: TableCellsIcon,
connectData: TableCellsIcon,
print: PrinterIcon,
sendToZebra: PaperAirplaneIcon,
output: PrinterIcon,
undo: ArrowUturnLeftIcon,
redo: ArrowUturnRightIcon,
github: GitHubIcon,
Expand All @@ -96,7 +91,6 @@ const MENU_ICONS: Partial<Record<MenuItemId, ComponentType<SVGProps<SVGSVGElemen
export function AppShell() {
const t = useT();
const label = useLabelStore((s) => s.label);
const batchPrintCount = useLabelStore(selectBatchPrintCount);
const pages = useLabelStore((s) => s.pages);
const selectObject = useLabelStore((s) => s.selectObject);
const addPage = useLabelStore((s) => s.addPage);
Expand All @@ -111,12 +105,8 @@ export function AppShell() {
const restoreReplacedDesign = useLabelStore((s) => s.restoreReplacedDesign);
const dismissReplacedDesign = useLabelStore((s) => s.dismissReplacedDesign);
const sourceEditDirty = useLabelStore(selectSourceEditDirty);
const noticeRequired = useLabelStore(selectLabelaryNoticeRequired);
const renderer = useLabelStore(selectEffectivePreviewProvider);
const pageCount = useLabelStore((s) => s.pages.length);
// The Labelary notice gates every Labelary render, so the action waits here until the user continues.
const [afterNotice, setAfterNotice] = useState<(() => void) | null>(null);
const withNotice = (action: () => void) => (renderer === "labelary" && noticeRequired ? setAfterNotice(() => action) : action());
const appUpdate = useLabelStore((s) => s.appUpdate);
const checkForAppUpdate = useLabelStore((s) => s.checkForAppUpdate);
const installAppUpdate = useLabelStore((s) => s.installAppUpdate);
Expand Down Expand Up @@ -191,54 +181,52 @@ export function AppShell() {
showZplImport,
openZplImport,
closeZplImport,
showZebraPrint,
openZebraPrint,
closeZebraPrint,
outputSource,
openOutput,
closeOutput,
currentZpl,
handleDownload,
handleExportBatch,
canBatchExport,
batchRowCount,
handlePrint,
handleExportPng,
} = useZplImportExport(canvasRef);
// The panel may grow until the main row has no height left.
const mainRowRef = useRef<HTMLDivElement>(null);
const outputPanel = useOutputPanel(OUTPUT_DEFAULT_H, sourceEditing, () => mainRowRef.current?.getBoundingClientRect().top ?? 0);
const leftPanel = useCollapsiblePanel("zpl-panel-left");
const rightPanel = useCollapsiblePanel("zpl-panel-right");

// One menu model for both surfaces: the DOM dropdown (web header) and the
// native OS menu (desktop, where the header row is not rendered at all).
const menuModel = buildMenuModel(t, {
// One menu model for both surfaces: the DOM dropdown in the web header, and the native OS menu on
// the desktop, where the header row is not rendered at all.
const menuFlags: MenuFlags = {
hasObjects,
documentEmits,
sourceEditing,
canBatchExport,
batchRowCount,
batchPrintCount,
connectDataWizard: isDesktopShell,
canBatchPdf: renderer !== "none",
pdfCurrentPageOnly: renderer === "none" && pageCount > 1,
canUndo,
canRedo,
// On macOS quit lives in the app submenu (Cmd+Q), not the File section.
includeQuit: isDesktopShell && !isMacDesktop,
});
};
const menuModel = buildMenuModel(t, menuFlags);
const menuHandlers: MenuHandlers = {
new: handleNew,
addPage,
importZpl: openZplImport,
settings: () => setPrinterSettingsTab("appSettings"),
exportZpl: handleDownload,
exportBatch: handleExportBatch,
exportPdf: () => withNotice(exportPdf),
exportBatchPdf: () => withNotice(exportBatchPdf),
openDesign: handleOpen,
saveDesign: handleSave,
importCsv: openCsvPicker,
connectData: openConnectWizard,
print: () => withNotice(() => void handlePrint()),
sendToZebra: openZebraPrint,
output: openOutput,
undo: () => undo(),
redo: () => redo(),
github: () => openExternal(REPO_URL),
Expand Down Expand Up @@ -565,16 +553,15 @@ export function AppShell() {
onCancel={() => settleDatasetReplace(false)}
/>
)}
{showZebraPrint && (
<PrintToZebraDialog zpl={currentZpl()} onClose={closeZebraPrint} />
)}
{afterNotice && (
<LabelaryNoticeModal
onClose={() => setAfterNotice(null)}
onContinue={() => {
setAfterNotice(null);
afterNotice();
}}
{outputSource && (
<OutputDialog
zpl={currentZpl}
source={outputSource}
facts={menuFlags}
onClose={closeOutput}
onPrintImage={handlePrint}
onExportPdf={(scope) => (scope === "batch" ? exportBatchPdf() : exportPdf())}
onExportPng={handleExportPng}
/>
)}
{pdfProgress && <PdfExportDialog progress={pdfProgress} onCancel={cancelPdfExport} />}
Expand Down
25 changes: 12 additions & 13 deletions src/components/Canvas/LabelCanvas.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ import { zplForSelection } from "../../lib/zplForSelection";
import { finishZplExport } from "../../lib/exportZpl";
import { generateMultiPageZPL } from "@zplab/core/lib/zplGenerator";
import { captureLabelBlob, captureRegionBlob, copyPngToClipboard, CAPTURE_CHROME } from "../../lib/canvasImage";
import { saveFile, saveErrorMessage, PNG_FILTER } from "../../lib/fileDialogs";
import { saveLabelImage } from "../../lib/saveLabelImage";
import { printerPreviewLayout } from "../../lib/printerPreview";

/** Object types offered by the context menu's "Add object here". */
Expand Down Expand Up @@ -156,13 +156,20 @@ interface Props {
onViewRotationChange: (rotation: ViewRotation) => void;
}

/** Nothing drawn yet is nothing to capture, so the context menu and the output dialog get a click that
* does nothing. */
const labelCapture = (group: Konva.Group | null, paper: Konva.Rect | null) =>
group ? captureLabelBlob(group, paper) : Promise.resolve(null);

export interface LabelCanvasHandle {
alignSelection: (op: AlignOp, ref: AlignRef) => void;
distributeSelection: (axis: DistributeAxis) => void;
tidySelection: () => void;
convertObjectPositionType: (id: string, target: "FO" | "FT") => void;
/** The label paper as drawn, at exactly the given dots, so a raster of it lines up with a printer render. */
captureLabelDots: (dots: { width: number; height: number }) => Promise<Blob | null>;
/** The label as the canvas draws it, the capture behind the image export. */
captureLabel: () => Promise<Blob | null>;
}

export const LabelCanvas = forwardRef<LabelCanvasHandle, Props>(function LabelCanvas({
Expand Down Expand Up @@ -257,8 +264,6 @@ export const LabelCanvas = forwardRef<LabelCanvasHandle, Props>(function LabelCa
clipboard,
variables,
pages,
setUserError,
clearUserError,
} = useLabelStore();
// Everything on canvas is drawn in the current page's dot scale;
// designLabel stays the MODEL's design scope for the whole-document emit.
Expand Down Expand Up @@ -955,6 +960,7 @@ export const LabelCanvas = forwardRef<LabelCanvasHandle, Props>(function LabelCa
const group = rotationGroupRef.current;
return group ? captureLabelBlob(group, labelPaperRef.current, { pixelRatio: label.dpmm / scale, dots }) : Promise.resolve(null);
},
captureLabel: () => labelCapture(rotationGroupRef.current, labelPaperRef.current),
};
},
[updateObjects, updateObject, label.dpmm, scale],
Expand Down Expand Up @@ -1260,7 +1266,7 @@ export const LabelCanvas = forwardRef<LabelCanvasHandle, Props>(function LabelCa
const switchTypeLocked =
!!singleForSwitch &&
(!!singleForSwitch.locked || hasLockedAncestor(objects, singleForSwitch.id));
const captureLabel = () => (rotationGroupRef.current ? captureLabelBlob(rotationGroupRef.current, labelPaperRef.current) : Promise.resolve(null));
const captureLabel = () => labelCapture(rotationGroupRef.current, labelPaperRef.current);
// The region comes from the model like the selection frame: groups have no node, a sample barcode's
// node is chrome, and a line's node is its hit stroke. Frame px are stage px once the view rotation is 0.
const captureSelection = () => {
Expand All @@ -1279,13 +1285,6 @@ export const LabelCanvas = forwardRef<LabelCanvasHandle, Props>(function LabelCa
});
void copyPngToClipboard(blob).catch(() => undefined);
};
const exportImage = async (pending: Promise<Blob | null>, filename: string) => {
const blob = await pending;
if (!blob) return;
await saveFile(blob, { filename, filters: [PNG_FILTER] })
.then((wrote) => wrote && clearUserError())
.catch(() => setUserError(saveErrorMessage));
};
const dispatch = {
copy: copySelectedObjects,
cut: () => {
Expand All @@ -1307,9 +1306,9 @@ export const LabelCanvas = forwardRef<LabelCanvasHandle, Props>(function LabelCa
void copyText(finishZplExport(generateMultiPageZPL(designLabel, pages, variables)));
},
copyImageSelected: () => copyImage(captureSelection()),
exportImageSelected: () => exportImage(captureSelection(), `${(sel.length === 1 ? findObjectById(objects, sel[0] ?? "")?.type : undefined) ?? "selection"}.png`),
exportImageSelected: () => void saveLabelImage(captureSelection(), `${(sel.length === 1 ? findObjectById(objects, sel[0] ?? "")?.type : undefined) ?? "selection"}.png`),
copyImageLabel: () => copyImage(captureLabel()),
exportImageLabel: () => exportImage(captureLabel(), "label.png"),
exportImageLabel: () => void saveLabelImage(captureLabel(), "label.png"),
selectAll: () => selectObjects(objects.map((o) => o.id)),
switchType: (type: string) => {
if (singleForSwitch) {
Expand Down
Loading
Loading