v0.2.96
This commit is contained in:
1 parent
b15a6c54ce
commit
d36a4b2451
22 files changed
+926
-114
No files matched your search
+185
-48
@@ -128,6 +128,57 @@ function isBlankPrintLine(node) {
|
||||
return node.children.length === 1 && node.firstElementChild?.tagName === "BR";
|
||||
}
|
||||
|
||||
function trimTrailingBlankPrintLines(root) {
|
||||
let node = root.lastElementChild;
|
||||
while (isBlankPrintLine(node)) {
|
||||
const remove = node;
|
||||
node = node.previousElementSibling;
|
||||
remove.remove();
|
||||
}
|
||||
}
|
||||
|
||||
function makePdfLayoutMarker(doc, directive) {
|
||||
const marker = doc.createElement("div");
|
||||
marker.className = "pdf-layout-marker";
|
||||
marker.dataset.pdfDirective = directive;
|
||||
marker.setAttribute("aria-hidden", "true");
|
||||
return marker;
|
||||
}
|
||||
|
||||
function extractPdfLayout(root) {
|
||||
const layout = { headerHtml: "", footerHtml: "", verticalAlign: null };
|
||||
const headers = [];
|
||||
const footers = [];
|
||||
|
||||
[...root.querySelectorAll(".pdf-layout-marker[data-pdf-directive]")].forEach(marker => {
|
||||
const directive = String(marker.dataset.pdfDirective || "").toLowerCase();
|
||||
if (directive === "align_top" || directive === "align_center") {
|
||||
layout.verticalAlign = directive === "align_center" ? "center" : "top";
|
||||
marker.remove();
|
||||
return;
|
||||
}
|
||||
|
||||
let block = marker.nextElementSibling;
|
||||
while (isBlankPrintLine(block)) {
|
||||
const remove = block;
|
||||
block = block.nextElementSibling;
|
||||
remove.remove();
|
||||
}
|
||||
marker.remove();
|
||||
if (!block || block.classList?.contains("pdf-page-break")) return;
|
||||
|
||||
block.remove();
|
||||
if (directive === "header") headers.push(block.outerHTML);
|
||||
if (directive === "footer") footers.push(block.outerHTML);
|
||||
});
|
||||
|
||||
root.querySelectorAll(".pdf-layout-marker").forEach(marker => marker.remove());
|
||||
trimTrailingBlankPrintLines(root);
|
||||
layout.headerHtml = headers.join("");
|
||||
layout.footerHtml = footers.join("");
|
||||
return layout;
|
||||
}
|
||||
|
||||
function collapseHeadingGaps(root) {
|
||||
root.querySelectorAll("h1, h2, h3, h4, h5, h6").forEach(heading => {
|
||||
let next = heading.nextElementSibling;
|
||||
@@ -274,34 +325,47 @@ function preparePrintPagination(root) {
|
||||
}
|
||||
|
||||
async function ensurePrintableMermaid(root, darkMode = false) {
|
||||
const nodes = [...root.querySelectorAll(".mermaid[data-mermaid-source]")];
|
||||
const nodes = [...root.querySelectorAll(".mermaid")];
|
||||
if (!nodes.length) return;
|
||||
|
||||
root.querySelectorAll(".mermaid-error").forEach(error => error.remove());
|
||||
|
||||
const originals = nodes.map(node => ({
|
||||
node,
|
||||
html: node.innerHTML,
|
||||
processed: node.getAttribute("data-processed"),
|
||||
source: node.dataset.mermaidSource || node.textContent || "",
|
||||
hadSvg: Boolean(node.querySelector("svg")),
|
||||
}));
|
||||
const renderable = originals.filter(item => item.node.dataset.mermaidSource || !item.hadSvg);
|
||||
if (!renderable.length) return;
|
||||
|
||||
const staging = document.createElement("div");
|
||||
staging.setAttribute("aria-hidden", "true");
|
||||
staging.style.cssText = "position:fixed;left:-200vw;top:0;width:1200px;visibility:hidden;pointer-events:none;z-index:-1";
|
||||
|
||||
try {
|
||||
const mermaid = await loadMermaid();
|
||||
nodes.forEach(node => {
|
||||
renderable.forEach(({ node, source }) => {
|
||||
node.removeAttribute("data-processed");
|
||||
node.textContent = node.dataset.mermaidSource || "";
|
||||
node.textContent = source;
|
||||
});
|
||||
|
||||
staging.appendChild(root);
|
||||
document.body.appendChild(staging);
|
||||
mermaid.initialize({ startOnLoad: false, theme: darkMode ? "dark" : "default", securityLevel: "strict" });
|
||||
await mermaid.run({ nodes });
|
||||
await mermaid.run({ nodes: renderable.map(item => item.node) });
|
||||
root.querySelectorAll(".mermaid-error").forEach(error => error.remove());
|
||||
} catch {
|
||||
originals.forEach(({ node, html, processed }) => {
|
||||
originals.forEach(({ node, html, processed, hadSvg }) => {
|
||||
node.innerHTML = html;
|
||||
if (processed === null) node.removeAttribute("data-processed");
|
||||
else node.setAttribute("data-processed", processed);
|
||||
if (hadSvg) return;
|
||||
const message = document.createElement("p");
|
||||
message.className = "error mermaid-error";
|
||||
message.textContent = t("public.mermaidFailed", {}, "Failed to load Mermaid.");
|
||||
node.parentNode?.insertBefore(message, node);
|
||||
});
|
||||
} finally {
|
||||
if (root.parentElement === staging) staging.removeChild(root);
|
||||
@@ -334,8 +398,10 @@ function measureLastPageFill(doc, documentRoot, pageContentHeight) {
|
||||
"z-index:-1",
|
||||
].join(";");
|
||||
|
||||
const title = documentRoot.querySelector(":scope > .pdf-document-title");
|
||||
if (title) probe.appendChild(title.cloneNode(true));
|
||||
for (const child of documentRoot.children) {
|
||||
if (child === content) break;
|
||||
if (child.matches?.(".pdf-document-title, .pdf-custom-header")) probe.appendChild(child.cloneNode(true));
|
||||
}
|
||||
|
||||
const flow = content.cloneNode(true);
|
||||
flow.querySelectorAll(".pdf-page-break").forEach(pageBreak => {
|
||||
@@ -367,40 +433,82 @@ function fallbackLastPageFill(documentRoot, spacer, pageContentHeight) {
|
||||
function alignFooterToLastPage(doc, pageHeightMm, marginMm) {
|
||||
const documentRoot = doc.querySelector(".pdf-document");
|
||||
const spacer = doc.querySelector(".pdf-footer-spacer");
|
||||
const footer = doc.querySelector(".pdf-generated-footer");
|
||||
const footer = doc.querySelector(".pdf-document-footer");
|
||||
if (!documentRoot || !spacer || !footer) return;
|
||||
|
||||
spacer.style.height = "0px";
|
||||
footer.style.removeProperty("margin-top");
|
||||
footer.style.marginTop = "0px";
|
||||
|
||||
const pxPerMm = mmToPx(doc);
|
||||
const pageContentHeight = (Number(pageHeightMm) - (2 * Number(marginMm))) * pxPerMm;
|
||||
const footerHeight = footer.getBoundingClientRect().height;
|
||||
const footerMarginTop = Number.parseFloat(doc.defaultView?.getComputedStyle(footer).marginTop || "0") || 0;
|
||||
const bottomGuard = 0.75 * pxPerMm;
|
||||
const bottomGuard = Math.max(2, 1.5 * pxPerMm);
|
||||
const minGap = 4 * pxPerMm;
|
||||
if (!Number.isFinite(pageContentHeight) || pageContentHeight <= footerHeight || footerHeight <= 0) return;
|
||||
|
||||
const measuredFill = measureLastPageFill(doc, documentRoot, pageContentHeight);
|
||||
const positionOnPage = measuredFill ?? fallbackLastPageFill(documentRoot, spacer, pageContentHeight);
|
||||
const available = Math.max(0, pageContentHeight - positionOnPage);
|
||||
|
||||
let actualMarginTop = footerMarginTop;
|
||||
let spacerHeight;
|
||||
const spacerHeight = available >= footerHeight + minGap + bottomGuard
|
||||
? available - footerHeight - bottomGuard
|
||||
: available + pageContentHeight - footerHeight - bottomGuard;
|
||||
|
||||
if (available >= footerHeight + bottomGuard) {
|
||||
actualMarginTop = Math.min(
|
||||
footerMarginTop,
|
||||
Math.max(0, available - footerHeight - bottomGuard),
|
||||
);
|
||||
spacerHeight = available - footerHeight - actualMarginTop - bottomGuard;
|
||||
} else {
|
||||
spacerHeight = available + pageContentHeight - footerHeight - actualMarginTop - bottomGuard;
|
||||
}
|
||||
|
||||
footer.style.marginTop = `${Math.max(0, actualMarginTop)}px`;
|
||||
spacer.style.height = `${Math.max(0, spacerHeight)}px`;
|
||||
}
|
||||
|
||||
function fitOversizedKeepUnits(doc, pageHeightMm, marginMm) {
|
||||
const root = doc.querySelector(".pdf-content");
|
||||
if (!root) return;
|
||||
const pxPerMm = mmToPx(doc);
|
||||
const pageContentHeight = (Number(pageHeightMm) - (2 * Number(marginMm))) * pxPerMm;
|
||||
const maxHeight = Math.max(40, pageContentHeight - Math.max(2, pxPerMm));
|
||||
|
||||
root.querySelectorAll(".pdf-keep-unit").forEach(unit => {
|
||||
unit.classList.remove("pdf-keep-unit--oversized");
|
||||
let rect = unit.getBoundingClientRect();
|
||||
if (rect.height <= maxHeight) return;
|
||||
|
||||
const scalable = unit.querySelector(".mermaid svg, img, .rustpad-media, video, canvas");
|
||||
if (scalable) {
|
||||
const scalableRect = scalable.getBoundingClientRect();
|
||||
const overhead = Math.max(0, rect.height - scalableRect.height);
|
||||
const targetHeight = Math.floor(maxHeight - overhead - 2);
|
||||
if (targetHeight >= 48 && targetHeight < scalableRect.height) {
|
||||
scalable.style.setProperty("max-height", `${targetHeight}px`, "important");
|
||||
scalable.style.setProperty("height", "auto", "important");
|
||||
}
|
||||
rect = unit.getBoundingClientRect();
|
||||
}
|
||||
|
||||
if (rect.height > maxHeight) unit.classList.add("pdf-keep-unit--oversized");
|
||||
});
|
||||
}
|
||||
|
||||
function alignContentVertically(doc, pageHeightMm, marginMm, mode) {
|
||||
const documentRoot = doc.querySelector(".pdf-document");
|
||||
const content = doc.querySelector(".pdf-content");
|
||||
const footer = doc.querySelector(".pdf-document-footer");
|
||||
if (!documentRoot || !content || !footer) return;
|
||||
|
||||
content.style.marginTop = "0px";
|
||||
if (mode !== "center" || content.querySelector(".pdf-page-break")) return;
|
||||
|
||||
const pxPerMm = mmToPx(doc);
|
||||
const pageContentHeight = (Number(pageHeightMm) - (2 * Number(marginMm))) * pxPerMm;
|
||||
const rootRect = documentRoot.getBoundingClientRect();
|
||||
const contentRect = content.getBoundingClientRect();
|
||||
const footerHeight = footer.getBoundingClientRect().height;
|
||||
const headerHeight = Math.max(0, contentRect.top - rootRect.top);
|
||||
const bottomGuard = Math.max(2, 1.5 * pxPerMm);
|
||||
const minGap = 4 * pxPerMm;
|
||||
const used = headerHeight + contentRect.height + footerHeight + minGap + bottomGuard;
|
||||
if (!Number.isFinite(pageContentHeight) || used >= pageContentHeight) return;
|
||||
|
||||
const freeSpace = pageContentHeight - headerHeight - footerHeight - bottomGuard - contentRect.height;
|
||||
content.style.marginTop = `${Math.max(0, freeSpace / 2)}px`;
|
||||
}
|
||||
|
||||
function preparePreview(preview) {
|
||||
const clone = preview.cloneNode(true);
|
||||
clone.removeAttribute("id");
|
||||
@@ -426,11 +534,16 @@ function preparePreview(preview) {
|
||||
|
||||
if (clone.classList.contains("preview--raw")) {
|
||||
clone.querySelectorAll(".preview-source-line").forEach(line => {
|
||||
if (!/^\[PAGEBREAK\]$/i.test(line.textContent.trim())) return;
|
||||
const pageBreak = document.createElement("div");
|
||||
pageBreak.className = "pdf-page-break";
|
||||
pageBreak.setAttribute("aria-hidden", "true");
|
||||
line.replaceWith(pageBreak);
|
||||
const text = line.textContent.trim();
|
||||
if (/^\[(?:PDF_PAGEBREAK|PAGEBREAK)\]$/i.test(text)) {
|
||||
const pageBreak = document.createElement("div");
|
||||
pageBreak.className = "pdf-page-break";
|
||||
pageBreak.setAttribute("aria-hidden", "true");
|
||||
line.replaceWith(pageBreak);
|
||||
return;
|
||||
}
|
||||
const directive = text.match(/^\[PDF_(HEADER|FOOTER|ALIGN_TOP|ALIGN_CENTER)\]$/i);
|
||||
if (directive) line.replaceWith(makePdfLayoutMarker(document, directive[1].toLowerCase()));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -441,7 +554,7 @@ function preparePreview(preview) {
|
||||
marker.replaceWith(pageBreak);
|
||||
});
|
||||
|
||||
preparePrintPagination(clone);
|
||||
trimTrailingBlankPrintLines(clone);
|
||||
return clone;
|
||||
}
|
||||
|
||||
@@ -483,8 +596,17 @@ function printStyles(cssSize, pageWidthMm, pageHeightMm, marginMm, darkMode) {
|
||||
background: ${pageBackground} !important;
|
||||
color: ${textColor} !important;
|
||||
}
|
||||
.pdf-document-title { margin: 0 0 1.2em; padding: 0 0 .45em; border-bottom: 1px solid ${borderColor}; }
|
||||
.pdf-document-title,
|
||||
.pdf-custom-header {
|
||||
margin: 0 0 1.2em;
|
||||
padding: 0 0 .45em;
|
||||
border-bottom: 1px solid ${borderColor};
|
||||
break-inside: avoid-page;
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
.pdf-document-title h1 { margin: 0; font: 700 1.7rem/1.18 Inter, ui-sans-serif, system-ui, sans-serif; color: ${headingColor}; }
|
||||
.pdf-custom-header > :first-child { margin-top: 0 !important; }
|
||||
.pdf-custom-header > :last-child { margin-bottom: 0 !important; }
|
||||
.pdf-footer-spacer {
|
||||
display: block;
|
||||
width: 100%;
|
||||
@@ -493,9 +615,9 @@ function printStyles(cssSize, pageWidthMm, pageHeightMm, marginMm, darkMode) {
|
||||
padding: 0;
|
||||
border: 0;
|
||||
}
|
||||
.pdf-generated-footer {
|
||||
.pdf-document-footer {
|
||||
box-sizing: border-box;
|
||||
margin-top: 8mm;
|
||||
margin-top: 0;
|
||||
padding-top: 1.5mm;
|
||||
border-top: .2mm solid ${borderColor};
|
||||
color: ${footerColor};
|
||||
@@ -505,6 +627,8 @@ function printStyles(cssSize, pageWidthMm, pageHeightMm, marginMm, darkMode) {
|
||||
break-inside: avoid-page;
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
.pdf-custom-footer > :first-child { margin-top: 0 !important; }
|
||||
.pdf-custom-footer > :last-child { margin-bottom: 0 !important; }
|
||||
.pdf-content {
|
||||
width: 100%;
|
||||
min-height: 0;
|
||||
@@ -521,20 +645,11 @@ function printStyles(cssSize, pageWidthMm, pageHeightMm, marginMm, darkMode) {
|
||||
.pdf-content a { color: inherit !important; text-decoration: underline; }
|
||||
|
||||
/* Book-like pagination: keep small semantic units together, not whole sections. */
|
||||
/*
|
||||
* Do not rely on break-after: avoid for the main Markdown headings.
|
||||
* Chromium can first paint the heading at the end of the old page and
|
||||
* then move it to the next page, leaving a clipped strip of glyphs behind.
|
||||
* A small invisible reserve inside the heading keeps one following line
|
||||
* with it without triggering that fragmentation bug.
|
||||
*/
|
||||
.pdf-content h1,
|
||||
.pdf-content h2,
|
||||
.pdf-content h3 {
|
||||
break-after: auto;
|
||||
page-break-after: auto;
|
||||
padding-bottom: 1.15em;
|
||||
margin-bottom: calc(.18em - 1.15em);
|
||||
}
|
||||
.pdf-content h4,
|
||||
.pdf-content h5,
|
||||
@@ -637,16 +752,18 @@ function printStyles(cssSize, pageWidthMm, pageHeightMm, marginMm, darkMode) {
|
||||
}
|
||||
.pdf-content .pdf-keep-unit > :first-child { margin-top: 0 !important; }
|
||||
.pdf-content .pdf-keep-unit > :last-child { margin-bottom: 0 !important; }
|
||||
.pdf-content .pdf-keep-unit.pdf-keep-unit--oversized {
|
||||
display: block;
|
||||
break-inside: auto;
|
||||
page-break-inside: auto;
|
||||
}
|
||||
.pdf-content .pdf-keep-short {
|
||||
break-inside: avoid-page !important;
|
||||
page-break-inside: avoid !important;
|
||||
}
|
||||
.pdf-content .pdf-heading-lead > h1,
|
||||
.pdf-content .pdf-heading-lead > h2,
|
||||
.pdf-content .pdf-heading-lead > h3 {
|
||||
padding-bottom: 0;
|
||||
margin-bottom: .18em;
|
||||
}
|
||||
.pdf-content .pdf-heading-lead > h3 { margin-bottom: .18em; }
|
||||
.pdf-content .pdf-atomic-visual {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
@@ -819,7 +936,7 @@ function printStyles(cssSize, pageWidthMm, pageHeightMm, marginMm, darkMode) {
|
||||
`;
|
||||
}
|
||||
|
||||
function openPrintWindow({ preview, title, formatKey, marginMm, includeTitle, darkMode }) {
|
||||
function openPrintWindow({ preview, title, formatKey, marginMm, includeTitle, darkMode, verticalAlign }) {
|
||||
const format = PAGE_FORMATS[formatKey] || PAGE_FORMATS["a4-portrait"];
|
||||
const safeMargin = [0, 8, 12, 20].includes(Number(marginMm)) ? Number(marginMm) : 12;
|
||||
const printWindow = window.open("", "_blank");
|
||||
@@ -827,26 +944,39 @@ function openPrintWindow({ preview, title, formatKey, marginMm, includeTitle, da
|
||||
try { printWindow.opener = null; } catch { /* same-origin hardening is best effort */ }
|
||||
|
||||
const content = preparePreview(preview);
|
||||
const pdfLayout = extractPdfLayout(content);
|
||||
const resolvedVerticalAlign = pdfLayout.verticalAlign || (verticalAlign === "center" ? "center" : "top");
|
||||
const titleBlock = includeTitle && title
|
||||
? `<header class="pdf-document-title"><h1>${escapeHtml(title)}</h1></header>`
|
||||
: "";
|
||||
const customHeader = pdfLayout.headerHtml
|
||||
? `<header class="pdf-custom-header">${pdfLayout.headerHtml}</header>`
|
||||
: "";
|
||||
const lang = escapeHtml(document.documentElement.lang || "en");
|
||||
const base = escapeHtml(`${location.origin}/`);
|
||||
const css = printStyles(format.cssSize, format.widthMm, format.heightMm, safeMargin, darkMode);
|
||||
const footerText = escapeHtml(t("editor.pdfGeneratedFooter", {}, "Generated by RustPad"));
|
||||
const footerBlock = pdfLayout.footerHtml
|
||||
? `<footer class="pdf-document-footer pdf-custom-footer">${pdfLayout.footerHtml}</footer>`
|
||||
: `<footer class="pdf-document-footer pdf-generated-footer" aria-hidden="true">${footerText}</footer>`;
|
||||
|
||||
return (async () => {
|
||||
await ensurePrintableMermaid(content, darkMode);
|
||||
await ensureSyntaxHighlight(content);
|
||||
preparePrintPagination(content);
|
||||
|
||||
printWindow.document.open();
|
||||
printWindow.document.write(`<!doctype html><html lang="${lang}" data-theme="${darkMode ? "dark" : "light"}"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><base href="${base}"><title>${escapeHtml(title || "RustPad")}</title>${stylesheetLinks()}<style>${css}</style></head><body><main class="pdf-document">${titleBlock}${content.outerHTML}<div class="pdf-footer-spacer" aria-hidden="true"></div><footer class="pdf-generated-footer" aria-hidden="true">${footerText}</footer></main></body></html>`);
|
||||
printWindow.document.write(`<!doctype html><html lang="${lang}" data-theme="${darkMode ? "dark" : "light"}"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><base href="${base}"><title>${escapeHtml(title || "RustPad")}</title>${stylesheetLinks()}<style>${css}</style></head><body><main class="pdf-document">${customHeader}${titleBlock}${content.outerHTML}<div class="pdf-footer-spacer" aria-hidden="true"></div>${footerBlock}</main></body></html>`);
|
||||
printWindow.document.close();
|
||||
|
||||
const runPrint = async () => {
|
||||
await waitForPrintableLayout(printWindow.document);
|
||||
fitOversizedKeepUnits(printWindow.document, format.heightMm, safeMargin);
|
||||
alignContentVertically(printWindow.document, format.heightMm, safeMargin, resolvedVerticalAlign);
|
||||
alignFooterToLastPage(printWindow.document, format.heightMm, safeMargin);
|
||||
await nextFrame(printWindow);
|
||||
fitOversizedKeepUnits(printWindow.document, format.heightMm, safeMargin);
|
||||
alignContentVertically(printWindow.document, format.heightMm, safeMargin, resolvedVerticalAlign);
|
||||
alignFooterToLastPage(printWindow.document, format.heightMm, safeMargin);
|
||||
await nextFrame(printWindow);
|
||||
printWindow.focus();
|
||||
@@ -868,6 +998,7 @@ export function bindPdfExport({ button, dialog, form, preview, title }) {
|
||||
if (!button || !dialog || !form || !preview) return;
|
||||
const formatSelect = form.querySelector("#pdf-page-format");
|
||||
const marginSelect = form.querySelector("#pdf-page-margin");
|
||||
const verticalAlignSelect = form.querySelector("#pdf-vertical-align");
|
||||
const darkMode = form.querySelector("#pdf-dark-mode");
|
||||
const includeTitle = form.querySelector("#pdf-include-title");
|
||||
const cancelButtons = form.querySelectorAll("[data-pdf-cancel]");
|
||||
@@ -878,6 +1009,10 @@ export function bindPdfExport({ button, dialog, form, preview, title }) {
|
||||
const savedMargin = saved("margin", "12");
|
||||
marginSelect.value = ["0", "8", "12", "20"].includes(savedMargin) ? savedMargin : "12";
|
||||
}
|
||||
if (verticalAlignSelect) {
|
||||
const savedVerticalAlign = saved("vertical-align", "top");
|
||||
verticalAlignSelect.value = ["top", "center"].includes(savedVerticalAlign) ? savedVerticalAlign : "top";
|
||||
}
|
||||
if (darkMode) darkMode.checked = saved("dark-mode", "on") !== "off";
|
||||
includeTitle.checked = saved("include-title", "off") === "on";
|
||||
|
||||
@@ -891,6 +1026,7 @@ export function bindPdfExport({ button, dialog, form, preview, title }) {
|
||||
const marginMm = Number(marginSelect?.value ?? 12);
|
||||
save("format", formatKey);
|
||||
save("margin", marginMm);
|
||||
save("vertical-align", verticalAlignSelect?.value === "center" ? "center" : "top");
|
||||
save("dark-mode", darkMode?.checked === false ? "off" : "on");
|
||||
save("include-title", includeTitle.checked ? "on" : "off");
|
||||
try {
|
||||
@@ -901,6 +1037,7 @@ export function bindPdfExport({ button, dialog, form, preview, title }) {
|
||||
marginMm,
|
||||
includeTitle: includeTitle.checked,
|
||||
darkMode: darkMode?.checked !== false,
|
||||
verticalAlign: verticalAlignSelect?.value === "center" ? "center" : "top",
|
||||
});
|
||||
dialog.close("confirm");
|
||||
void printJob.catch(error => {
|
||||
|
||||
Reference in new issue
Block a user