mirror of
https://github.com/PR0M3TH3AN/Artstr-Studio.git
synced 2026-10-27 08:40:39 +00:00
book(E2): PDF export modal with bleed / crop marks / range / split
Replaces the one-click "Export PDF…" with a dedicated modal that
exposes the options the pipeline now supports.
Options:
- Page range — All / 1-based from–to. Editing either input flips the
radio so the user doesn't have to click it first.
- Include cover spread (default = on when the book has a cover).
- Include bleed margin — pages export at trim + 2×bleed, with the
trim canvas centered inside; the surrounding strip is the page bg.
- Crop marks — _drawCropMarks emits L-shaped marks outside each
trim corner via pdf-lib's drawLine (printer guidance for cutting).
- Output: single PDF (interior + cover) or split (interior + cover
as two files).
- Filename pattern: <name>.pdf, or <name>-interior.pdf +
<name>-cover.pdf for split mode. Default = title-slug.
Pipeline:
- exportBookToPdf now accepts {dpi, includeCover, includeBleed,
cropMarks, range:{from,to}, split}. Range filters
enumerateBookPhysicalPages. Bleed-inclusive pages are sized
trimW+2×bleed and place the trim canvas at the bleed offset, so
artwork extends past the trim edge only when bleed-aware layers
on the page actually do — for v1 the strip stays the page bg.
- Cover spread honours includeBleed too: when off, the bleed-
inclusive canvas draws at a negative offset so only the trim
area lands on the (smaller) page. Cover crop marks position at
the spread's trim corners.
- Split output returns { split: true, interior, cover, warnings };
single output keeps the existing { blob, warnings } shape.
Modal lifecycle:
- openBookExportModal seeds totals + sensible defaults.
- runBookExportFromModal reads inputs, calls exportBookToPdf, and
triggers the download(s). Auto-closes on success after 800 ms;
surfaces warnings inline.
- Click-outside + ESC close behave like other modals.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
This commit is contained in:
1 parent
f4243887cc
commit
ef73d73152
1 file changed
+278
-54
+278
-54
@@ -5314,6 +5314,69 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal" id="bookExportPdfModal" aria-hidden="true" style="display:none">
|
||||
<div class="modalInner" style="max-width:520px;position:relative">
|
||||
<button type="button" id="bookExportPdfCloseBtn" class="welcomeCloseBtn" aria-label="Close">×</button>
|
||||
<h2 style="margin:0 0 4px;font-size:1.05rem;font-family:inherit">Export PDF</h2>
|
||||
<p class="hint" style="margin:0 0 14px">Choose what to include and how to package the output. Files render at 200 DPI; raster-only in v1 (real PDF text for embeddable fonts lands in a follow-up).</p>
|
||||
|
||||
<fieldset style="margin-top:8px;padding:8px 10px;border:1px solid var(--line);border-radius:6px">
|
||||
<legend style="font-size:.82rem;padding:0 4px">Pages</legend>
|
||||
<label class="toggle" style="display:flex;align-items:center;gap:6px;margin:2px 0">
|
||||
<input type="radio" name="bookExportRange" value="all" id="bookExportRangeAll" checked />
|
||||
<span>All physical pages</span>
|
||||
</label>
|
||||
<label class="toggle" style="display:flex;align-items:center;gap:6px;margin:2px 0">
|
||||
<input type="radio" name="bookExportRange" value="range" id="bookExportRangeCustom" />
|
||||
<span>Page range:</span>
|
||||
<input type="number" id="bookExportRangeFrom" min="1" step="1" style="width:60px" />
|
||||
<span>to</span>
|
||||
<input type="number" id="bookExportRangeTo" min="1" step="1" style="width:60px" />
|
||||
</label>
|
||||
<label class="toggle" style="display:flex;align-items:center;gap:6px;margin:2px 0">
|
||||
<input type="checkbox" id="bookExportIncludeCover" checked />
|
||||
<span>Include cover spread</span>
|
||||
</label>
|
||||
</fieldset>
|
||||
|
||||
<fieldset style="margin-top:8px;padding:8px 10px;border:1px solid var(--line);border-radius:6px">
|
||||
<legend style="font-size:.82rem;padding:0 4px">Print marks</legend>
|
||||
<label class="toggle" style="display:flex;align-items:center;gap:6px;margin:2px 0">
|
||||
<input type="checkbox" id="bookExportIncludeBleed" />
|
||||
<span>Include bleed margin on each page</span>
|
||||
</label>
|
||||
<label class="toggle" style="display:flex;align-items:center;gap:6px;margin:2px 0">
|
||||
<input type="checkbox" id="bookExportCropMarks" />
|
||||
<span>Draw crop marks at trim corners</span>
|
||||
</label>
|
||||
<p class="hint" style="margin:6px 0 0;font-size:.72rem">Bleed extends the page beyond the trim by the spec's bleed value; crop marks tell the printer where to cut.</p>
|
||||
</fieldset>
|
||||
|
||||
<fieldset style="margin-top:8px;padding:8px 10px;border:1px solid var(--line);border-radius:6px">
|
||||
<legend style="font-size:.82rem;padding:0 4px">Output</legend>
|
||||
<label class="toggle" style="display:flex;align-items:center;gap:6px;margin:2px 0">
|
||||
<input type="radio" name="bookExportSplit" value="single" id="bookExportSplitSingle" checked />
|
||||
<span>Single PDF (interior + cover)</span>
|
||||
</label>
|
||||
<label class="toggle" style="display:flex;align-items:center;gap:6px;margin:2px 0">
|
||||
<input type="radio" name="bookExportSplit" value="split" id="bookExportSplitSplit" />
|
||||
<span>Interior PDF + Cover PDF (two files)</span>
|
||||
</label>
|
||||
</fieldset>
|
||||
|
||||
<label for="bookExportFilename" style="margin-top:10px;display:block;font-size:.82rem">Filename pattern</label>
|
||||
<input type="text" id="bookExportFilename" placeholder="book" style="width:100%;padding:4px 8px;border:1px solid var(--line)" />
|
||||
<p class="hint" style="margin:2px 0 0;font-size:.72rem">Single-file: <code><name>.pdf</code>. Split: <code><name>-interior.pdf</code> + <code><name>-cover.pdf</code>.</p>
|
||||
|
||||
<p class="hint" id="bookExportPdfMsg" style="margin:10px 0 0;font-size:.78rem"></p>
|
||||
|
||||
<div class="buttonRow" style="margin-top:14px;justify-content:flex-end;gap:8px">
|
||||
<button type="button" id="bookExportPdfCancelBtn">Cancel</button>
|
||||
<button type="button" id="bookExportPdfRunBtn" class="primary">Export</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal" id="embedCodeModal" aria-hidden="true" style="display:none">
|
||||
<div class="modalInner" style="max-width:680px;position:relative">
|
||||
<button type="button" id="embedCloseBtn" class="welcomeCloseBtn" aria-label="Close">×</button>
|
||||
@@ -25213,26 +25276,73 @@
|
||||
}, 'image/png');
|
||||
});
|
||||
}
|
||||
// Phase E1 orchestrator. Walks the physical-page sequence, renders
|
||||
// each to canvas, embeds as PDF pages. Returns { blob, warnings }.
|
||||
// Draw L-shaped crop marks at each trim corner of a pdf-lib
|
||||
// page. Each mark is a short pair of lines outside the trim,
|
||||
// offset by a small gap so they don't intrude on the artwork.
|
||||
function _drawCropMarks(pdfPage, opts) {
|
||||
const { trimX, trimY, trimW, trimH, gap = 4.5, length = 12, thickness = 0.5 } = opts;
|
||||
const x0 = trimX, y0 = trimY;
|
||||
const x1 = trimX + trimW, y1 = trimY + trimH;
|
||||
const cfg = { thickness, color: window.PDFLib.rgb(0, 0, 0) };
|
||||
// Top-left
|
||||
pdfPage.drawLine({ start: { x: x0, y: y1 + gap }, end: { x: x0, y: y1 + gap + length }, ...cfg });
|
||||
pdfPage.drawLine({ start: { x: x0 - gap - length, y: y1 }, end: { x: x0 - gap, y: y1 }, ...cfg });
|
||||
// Top-right
|
||||
pdfPage.drawLine({ start: { x: x1, y: y1 + gap }, end: { x: x1, y: y1 + gap + length }, ...cfg });
|
||||
pdfPage.drawLine({ start: { x: x1 + gap, y: y1 }, end: { x: x1 + gap + length, y: y1 }, ...cfg });
|
||||
// Bottom-left
|
||||
pdfPage.drawLine({ start: { x: x0, y: y0 - gap }, end: { x: x0, y: y0 - gap - length }, ...cfg });
|
||||
pdfPage.drawLine({ start: { x: x0 - gap - length, y: y0 }, end: { x: x0 - gap, y: y0 }, ...cfg });
|
||||
// Bottom-right
|
||||
pdfPage.drawLine({ start: { x: x1, y: y0 - gap }, end: { x: x1, y: y0 - gap - length }, ...cfg });
|
||||
pdfPage.drawLine({ start: { x: x1 + gap, y: y0 }, end: { x: x1 + gap + length, y: y0 }, ...cfg });
|
||||
}
|
||||
// Phase E1/E2 orchestrator. Walks the physical-page sequence, renders
|
||||
// each to canvas, embeds as PDF pages. Options:
|
||||
// dpi — render resolution (default 200)
|
||||
// includeCover — bool, default true
|
||||
// includeBleed — bool; pages export at trim+bleed when true
|
||||
// cropMarks — bool; L-marks outside the trim
|
||||
// range — { from, to } 1-based; defaults to all
|
||||
// split — bool; when true returns { interior, cover, warnings }
|
||||
async function exportBookToPdf(opts = {}) {
|
||||
const PDFLib = await loadPdfLibOnce();
|
||||
const book = state.book;
|
||||
if (!book) throw new Error('No book to export.');
|
||||
const dpi = Math.max(72, Math.min(450, Number(opts.dpi) || 200));
|
||||
const includeCover = opts.includeCover !== false;
|
||||
const seq = enumerateBookPhysicalPages(book);
|
||||
const includeBleed = !!opts.includeBleed;
|
||||
const cropMarks = !!opts.cropMarks;
|
||||
const split = !!opts.split;
|
||||
const range = opts.range || null;
|
||||
const seq0 = enumerateBookPhysicalPages(book);
|
||||
// Range filter — physical-page index, 1-based.
|
||||
let seq = seq0;
|
||||
if (range && Number.isFinite(range.from) && Number.isFinite(range.to)) {
|
||||
const from = Math.max(1, Math.min(seq0.length, range.from | 0));
|
||||
const to = Math.max(from, Math.min(seq0.length, range.to | 0));
|
||||
seq = seq0.slice(from - 1, to);
|
||||
}
|
||||
if (!seq.length && !(includeCover && book.cover)) {
|
||||
throw new Error('Nothing to export — add at least one page or chapter first.');
|
||||
throw new Error('Nothing to export — the selection is empty.');
|
||||
}
|
||||
const pdfDoc = await PDFLib.PDFDocument.create();
|
||||
const warnings = [];
|
||||
_exportFailedImages.clear();
|
||||
// Per physical page.
|
||||
const reflowCache = new Map(); // entryId → paginateChapter result
|
||||
const spec = book.spec || {};
|
||||
const trimWpt = (Number(spec.trimW) || 6) * 72;
|
||||
const trimHpt = (Number(spec.trimH) || 9) * 72;
|
||||
const trimW = Number(spec.trimW) || 6;
|
||||
const trimH = Number(spec.trimH) || 9;
|
||||
const bleed = Number(spec.bleed) || 0;
|
||||
const pageWIn = includeBleed ? (trimW + bleed * 2) : trimW;
|
||||
const pageHIn = includeBleed ? (trimH + bleed * 2) : trimH;
|
||||
const trimOffsetIn = includeBleed ? bleed : 0;
|
||||
const trimWpt = trimW * 72;
|
||||
const trimHpt = trimH * 72;
|
||||
const pageWpt = pageWIn * 72;
|
||||
const pageHpt = pageHIn * 72;
|
||||
const trimOffsetPt = trimOffsetIn * 72;
|
||||
// Build interior PDF.
|
||||
const interiorDoc = await PDFLib.PDFDocument.create();
|
||||
const reflowCache = new Map();
|
||||
for (const slot of seq) {
|
||||
const entry = book.pages.find(p => p.id === slot.entryId);
|
||||
if (!entry) continue;
|
||||
@@ -25250,28 +25360,57 @@
|
||||
warnings.push(`Page ${(slot.pageNumber != null) ? slot.pageNumber : '?'}: ${err.message || err}`);
|
||||
continue;
|
||||
}
|
||||
// Reframe the trim-sized canvas onto a bleed-inclusive page if
|
||||
// requested — the canvas covers the trim area, and the
|
||||
// surrounding strip stays the page's background (white).
|
||||
const png = await _canvasToPngBytes(canvas);
|
||||
const embedded = await pdfDoc.embedPng(png);
|
||||
const page = pdfDoc.addPage([trimWpt, trimHpt]);
|
||||
page.drawImage(embedded, { x: 0, y: 0, width: trimWpt, height: trimHpt });
|
||||
const embedded = await interiorDoc.embedPng(png);
|
||||
const pdfPage = interiorDoc.addPage([pageWpt, pageHpt]);
|
||||
pdfPage.drawImage(embedded, {
|
||||
x: trimOffsetPt, y: trimOffsetPt,
|
||||
width: trimWpt, height: trimHpt,
|
||||
});
|
||||
if (cropMarks) {
|
||||
_drawCropMarks(pdfPage, {
|
||||
trimX: trimOffsetPt, trimY: trimOffsetPt,
|
||||
trimW: trimWpt, trimH: trimHpt,
|
||||
});
|
||||
}
|
||||
}
|
||||
// Cover spread on its own landscape page at the end.
|
||||
// Cover lives in its own doc when split, otherwise appended to interior.
|
||||
let coverDoc = null;
|
||||
if (includeCover && book.cover) {
|
||||
try {
|
||||
const coverCanvas = await renderBookCoverToCanvas(dpi);
|
||||
const png = await _canvasToPngBytes(coverCanvas);
|
||||
const embedded = await pdfDoc.embedPng(png);
|
||||
const spec2 = book.spec || {};
|
||||
const trimW = Number(spec2.trimW) || 6;
|
||||
const trimH = Number(spec2.trimH) || 9;
|
||||
const bleed = Number(spec2.bleed) || 0;
|
||||
const hasBack = !!spec2.coverHasBack;
|
||||
const hasSpine = !!spec2.coverHasSpine;
|
||||
const target = split ? await PDFLib.PDFDocument.create() : interiorDoc;
|
||||
const embedded = await target.embedPng(png);
|
||||
const hasBack = !!spec.coverHasBack;
|
||||
const hasSpine = !!spec.coverHasSpine;
|
||||
const spineW = hasSpine ? bookSpineWidthInches() : 0;
|
||||
const spreadW = (hasBack ? trimW : 0) + spineW + trimW + bleed * 2;
|
||||
const spreadH = trimH + bleed * 2;
|
||||
const page = pdfDoc.addPage([spreadW * 72, spreadH * 72]);
|
||||
page.drawImage(embedded, { x: 0, y: 0, width: spreadW * 72, height: spreadH * 72 });
|
||||
const spreadTrimW = (hasBack ? trimW : 0) + spineW + trimW;
|
||||
const spreadTrimH = trimH;
|
||||
const spreadPageW = includeBleed ? (spreadTrimW + bleed * 2) : spreadTrimW;
|
||||
const spreadPageH = includeBleed ? (spreadTrimH + bleed * 2) : spreadTrimH;
|
||||
// The cover canvas already includes the bleed strip — if
|
||||
// includeBleed is false, crop to trim by drawing the
|
||||
// bleed-inclusive image at a negative offset so only the
|
||||
// trim area lands on the page.
|
||||
const pdfPage = target.addPage([spreadPageW * 72, spreadPageH * 72]);
|
||||
const drawW = (spreadTrimW + bleed * 2) * 72;
|
||||
const drawH = (spreadTrimH + bleed * 2) * 72;
|
||||
const offX = includeBleed ? 0 : -bleed * 72;
|
||||
const offY = includeBleed ? 0 : -bleed * 72;
|
||||
pdfPage.drawImage(embedded, { x: offX, y: offY, width: drawW, height: drawH });
|
||||
if (cropMarks) {
|
||||
_drawCropMarks(pdfPage, {
|
||||
trimX: includeBleed ? bleed * 72 : 0,
|
||||
trimY: includeBleed ? bleed * 72 : 0,
|
||||
trimW: spreadTrimW * 72,
|
||||
trimH: spreadTrimH * 72,
|
||||
});
|
||||
}
|
||||
if (split) coverDoc = target;
|
||||
} catch (err) {
|
||||
warnings.push(`Cover: ${err.message || err}`);
|
||||
}
|
||||
@@ -25279,45 +25418,114 @@
|
||||
if (_exportFailedImages.size) {
|
||||
warnings.push(`${_exportFailedImages.size} image${_exportFailedImages.size === 1 ? '' : 's'} could not be embedded (CORS / missing).`);
|
||||
}
|
||||
const bytes = await pdfDoc.save();
|
||||
const blob = new Blob([bytes], { type: 'application/pdf' });
|
||||
return { blob, warnings };
|
||||
const interiorBytes = await interiorDoc.save();
|
||||
const interior = new Blob([interiorBytes], { type: 'application/pdf' });
|
||||
if (split && coverDoc) {
|
||||
const coverBytes = await coverDoc.save();
|
||||
const cover = new Blob([coverBytes], { type: 'application/pdf' });
|
||||
return { split: true, interior, cover, warnings };
|
||||
}
|
||||
return { blob: interior, warnings };
|
||||
}
|
||||
async function downloadBookPdf() {
|
||||
function _bookExportDefaultSlug() {
|
||||
return (state.meta?.title || 'book')
|
||||
.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 64) || 'book';
|
||||
}
|
||||
function openBookExportModal() {
|
||||
const modal = document.getElementById('bookExportPdfModal');
|
||||
if (!modal) return;
|
||||
const book = state.book;
|
||||
const total = book ? enumerateBookPhysicalPages(book).length : 0;
|
||||
const fromEl = $('bookExportRangeFrom');
|
||||
const toEl = $('bookExportRangeTo');
|
||||
if (fromEl) { fromEl.max = String(total || 1); fromEl.value = '1'; }
|
||||
if (toEl) { toEl.max = String(total || 1); toEl.value = String(total || 1); }
|
||||
const allRadio = $('bookExportRangeAll');
|
||||
if (allRadio) allRadio.checked = true;
|
||||
const cover = $('bookExportIncludeCover');
|
||||
if (cover) cover.checked = !!book?.cover;
|
||||
const bleed = $('bookExportIncludeBleed');
|
||||
if (bleed) bleed.checked = false;
|
||||
const crop = $('bookExportCropMarks');
|
||||
if (crop) crop.checked = false;
|
||||
const single = $('bookExportSplitSingle');
|
||||
if (single) single.checked = true;
|
||||
const fnEl = $('bookExportFilename');
|
||||
if (fnEl) fnEl.value = _bookExportDefaultSlug();
|
||||
const msg = $('bookExportPdfMsg');
|
||||
if (msg) { msg.textContent = ''; msg.style.color = 'var(--muted)'; }
|
||||
modal.style.display = 'flex';
|
||||
modal.setAttribute('aria-hidden', 'false');
|
||||
if (typeof bringModalToFront === 'function') bringModalToFront(modal);
|
||||
}
|
||||
function closeBookExportModal() {
|
||||
const modal = document.getElementById('bookExportPdfModal');
|
||||
if (!modal) return;
|
||||
modal.style.display = 'none';
|
||||
modal.setAttribute('aria-hidden', 'true');
|
||||
}
|
||||
async function runBookExportFromModal() {
|
||||
const book = state.book;
|
||||
if (!book) return;
|
||||
const btn = document.getElementById('bookExportPdfBtn');
|
||||
const status = document.getElementById('bookExportPdfStatus');
|
||||
const setStatus = (text, kind) => {
|
||||
if (!status) return;
|
||||
status.textContent = text || '';
|
||||
status.style.color = kind === 'error' ? '#b91c1c'
|
||||
: kind === 'ok' ? '#15803d' : 'var(--muted)';
|
||||
const rangeMode = (document.querySelector('input[name="bookExportRange"]:checked')?.value) || 'all';
|
||||
const splitMode = (document.querySelector('input[name="bookExportSplit"]:checked')?.value) || 'single';
|
||||
const opts = {
|
||||
includeCover: $('bookExportIncludeCover')?.checked !== false,
|
||||
includeBleed: !!$('bookExportIncludeBleed')?.checked,
|
||||
cropMarks: !!$('bookExportCropMarks')?.checked,
|
||||
split: splitMode === 'split',
|
||||
};
|
||||
if (btn) btn.disabled = true;
|
||||
setStatus('Rendering PDF…');
|
||||
if (rangeMode === 'range') {
|
||||
const from = Number($('bookExportRangeFrom')?.value);
|
||||
const to = Number($('bookExportRangeTo')?.value);
|
||||
if (Number.isFinite(from) && Number.isFinite(to)) opts.range = { from, to };
|
||||
}
|
||||
const slug = ($('bookExportFilename')?.value || '').trim() || _bookExportDefaultSlug();
|
||||
const runBtn = $('bookExportPdfRunBtn');
|
||||
const cancelBtn = $('bookExportPdfCancelBtn');
|
||||
const msg = $('bookExportPdfMsg');
|
||||
const setMsg = (text, kind) => {
|
||||
if (!msg) return;
|
||||
msg.textContent = text || '';
|
||||
msg.style.color = kind === 'error' ? '#b91c1c' : kind === 'ok' ? '#15803d' : 'var(--muted)';
|
||||
};
|
||||
if (runBtn) runBtn.disabled = true;
|
||||
if (cancelBtn) cancelBtn.disabled = true;
|
||||
setMsg('Rendering PDF…');
|
||||
try {
|
||||
const { blob, warnings } = await exportBookToPdf();
|
||||
const a = document.createElement('a');
|
||||
const url = URL.createObjectURL(blob);
|
||||
const slug = (state.meta?.title || 'book')
|
||||
.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 64);
|
||||
a.href = url;
|
||||
a.download = `${slug || 'book'}.pdf`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
setTimeout(() => URL.revokeObjectURL(url), 4000);
|
||||
if (warnings.length) {
|
||||
setStatus(`Exported with ${warnings.length} warning${warnings.length === 1 ? '' : 's'}: ${warnings[0]}`, 'error');
|
||||
const result = await exportBookToPdf(opts);
|
||||
const triggerDownload = (blob, name) => {
|
||||
const a = document.createElement('a');
|
||||
const url = URL.createObjectURL(blob);
|
||||
a.href = url;
|
||||
a.download = name;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
setTimeout(() => URL.revokeObjectURL(url), 4000);
|
||||
};
|
||||
let totalKB;
|
||||
if (result.split) {
|
||||
triggerDownload(result.interior, `${slug}-interior.pdf`);
|
||||
triggerDownload(result.cover, `${slug}-cover.pdf`);
|
||||
totalKB = ((result.interior.size + result.cover.size) / 1024).toFixed(0);
|
||||
} else {
|
||||
setStatus(`Exported ${(blob.size / 1024).toFixed(0)} KB.`, 'ok');
|
||||
triggerDownload(result.blob, `${slug}.pdf`);
|
||||
totalKB = (result.blob.size / 1024).toFixed(0);
|
||||
}
|
||||
const w = result.warnings || [];
|
||||
if (w.length) {
|
||||
setMsg(`Exported ${totalKB} KB with ${w.length} warning${w.length === 1 ? '' : 's'}: ${w[0]}`, 'error');
|
||||
} else {
|
||||
setMsg(`Exported ${totalKB} KB.`, 'ok');
|
||||
setTimeout(closeBookExportModal, 800);
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('PDF export failed', err);
|
||||
setStatus('Export failed: ' + (err?.message || 'unknown'), 'error');
|
||||
setMsg('Export failed: ' + (err?.message || 'unknown'), 'error');
|
||||
} finally {
|
||||
if (btn) btn.disabled = false;
|
||||
if (runBtn) runBtn.disabled = false;
|
||||
if (cancelBtn) cancelBtn.disabled = false;
|
||||
}
|
||||
}
|
||||
// ---- end Book PDF export --------------------------------------------
|
||||
@@ -34736,7 +34944,23 @@
|
||||
on('bookViewTogglePages', 'click', () => setBookSubView('pages'));
|
||||
on('bookViewToggleCover', 'click', () => setBookSubView('cover'));
|
||||
on('bookCoverEditBtn', 'click', openBookCover);
|
||||
on('bookExportPdfBtn', 'click', downloadBookPdf);
|
||||
on('bookExportPdfBtn', 'click', openBookExportModal);
|
||||
on('bookExportPdfCloseBtn', 'click', closeBookExportModal);
|
||||
on('bookExportPdfCancelBtn', 'click', closeBookExportModal);
|
||||
on('bookExportPdfRunBtn', 'click', runBookExportFromModal);
|
||||
// Flip the page-range radio to "range" when the user types in the
|
||||
// from/to fields, so they don't have to click the radio first.
|
||||
['bookExportRangeFrom', 'bookExportRangeTo'].forEach(id => {
|
||||
on(id, 'input', () => {
|
||||
const r = document.getElementById('bookExportRangeCustom');
|
||||
if (r) r.checked = true;
|
||||
});
|
||||
});
|
||||
// Click-outside to close.
|
||||
const _bookExportPdfModal = document.getElementById('bookExportPdfModal');
|
||||
if (_bookExportPdfModal) _bookExportPdfModal.addEventListener('click', (e) => {
|
||||
if (e.target === _bookExportPdfModal) closeBookExportModal();
|
||||
});
|
||||
on('bookPageEditBack', 'click', () => {
|
||||
// Same back-button serves per-page, per-master, and cover modes.
|
||||
if (state.book?.editingMasterId) closeBookMaster();
|
||||
|
||||
Reference in new issue
Block a user