mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-31 22:04:06 +08:00
1 parent
aa18f986ab
commit
5fce4f0a71
3 files changed
+394
-15
No files matched your search
@@ -346,7 +346,7 @@
|
|||||||
<div class="form-actions"><div id="objectBoundsStatus" class="form-status"></div><button id="objectBoundsCancel" class="button" type="button">取消</button><button class="button" type="submit">确定</button></div>
|
<div class="form-actions"><div id="objectBoundsStatus" class="form-status"></div><button id="objectBoundsCancel" class="button" type="button">取消</button><button class="button" type="submit">确定</button></div>
|
||||||
</form>
|
</form>
|
||||||
</dialog>
|
</dialog>
|
||||||
<dialog id="createPanel" class="form-dialog">
|
<dialog id="createPanel" class="form-dialog create-dialog">
|
||||||
<form id="createForm">
|
<form id="createForm">
|
||||||
<h2 id="createTitle" class="form-title">新建文档</h2>
|
<h2 id="createTitle" class="form-title">新建文档</h2>
|
||||||
<div id="createModeRow" class="form-row"><label for="createMode">来源</label><select id="createMode"><option value="blank">空白</option><option value="images">图片</option></select></div>
|
<div id="createModeRow" class="form-row"><label for="createMode">来源</label><select id="createMode"><option value="blank">空白</option><option value="images">图片</option></select></div>
|
||||||
@@ -354,8 +354,14 @@
|
|||||||
<label for="createName">名称</label>
|
<label for="createName">名称</label>
|
||||||
<input id="createName" type="text" value="未命名" required autofocus>
|
<input id="createName" type="text" value="未命名" required autofocus>
|
||||||
</div>
|
</div>
|
||||||
<div id="createImagesRow" class="form-row" hidden><label for="createImages">图片</label><input id="createImages" type="file" accept=".png,.jpg,.jpeg,.bmp,.tif,.tiff,.jb2,.jbig2" multiple disabled></div>
|
<div id="createImagesRow" class="form-row" hidden><span>图片</span><div class="create-image-actions"><button id="createImageAdd" class="button" type="button">添加</button><span id="createImageCount">0</span><input id="createImages" type="file" accept=".png,.jpg,.jpeg,.bmp,.tif,.tiff,.jb2,.jbig2" multiple hidden disabled></div></div>
|
||||||
<div id="createSizeRow" class="form-row" hidden><label for="createSize">尺寸</label><select id="createSize" disabled><option value="image">随图</option><option value="custom">自定</option></select></div>
|
<div id="createImageQueue" hidden>
|
||||||
|
<div class="form-row"><label for="createImageOrder">排序</label><select id="createImageOrder"><option value="name-asc">名称升序</option><option value="name-desc">名称降序</option><option value="time-asc">修改时间升序</option><option value="time-desc">修改时间降序</option><option value="added">添加顺序</option><option value="custom">自定顺序</option></select></div>
|
||||||
|
<div id="createImageList" class="create-image-list"></div>
|
||||||
|
<div class="create-image-actions"><button id="createImageUp" class="button" type="button">上移</button><button id="createImageDown" class="button" type="button">下移</button><button id="createImageRemove" class="button" type="button">移除</button></div>
|
||||||
|
</div>
|
||||||
|
<div id="createSizeRow" class="form-row"><label for="createSize">纸张</label><select id="createSize"><option value="image" hidden disabled>随图</option><option value="a4" selected>A4</option><option value="a3">A3</option><option value="a5">A5</option><option value="custom">自定</option></select></div>
|
||||||
|
<div id="createDirectionRow" class="form-row"><span>方向</span><div class="form-options"><label><input id="createPortrait" type="radio" name="direction" value="portrait" checked><span>竖向</span></label><label><input id="createLandscape" type="radio" name="direction" value="landscape"><span>横向</span></label></div></div>
|
||||||
<div id="createDimensions" class="form-pair">
|
<div id="createDimensions" class="form-pair">
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<label for="createWidth">宽度</label>
|
<label for="createWidth">宽度</label>
|
||||||
|
|||||||
@@ -1657,6 +1657,110 @@ button:not(:disabled):hover {
|
|||||||
gap: 12px;
|
gap: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.create-dialog {
|
||||||
|
width: 360px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-dialog.create-images {
|
||||||
|
width: 600px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-image-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#createImageCount {
|
||||||
|
margin-left: auto;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
#createImageQueue {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-image-list {
|
||||||
|
max-height: min(24dvh, 216px);
|
||||||
|
overflow: auto;
|
||||||
|
overscroll-behavior: contain;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: var(--radius-control);
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-image-entry {
|
||||||
|
position: relative;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 3ch 48px minmax(0, 1fr);
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
width: 100%;
|
||||||
|
padding: 6px 8px;
|
||||||
|
border: 0;
|
||||||
|
border-bottom: 1px solid var(--line);
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-image-entry:last-child {
|
||||||
|
border-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-image-entry[data-selected="true"] {
|
||||||
|
background: var(--selected-fill);
|
||||||
|
color: var(--accent-strong);
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-image-entry:focus-visible {
|
||||||
|
outline-offset: -2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-image-entry[data-drop]::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
inset-inline: 0;
|
||||||
|
height: 2px;
|
||||||
|
background: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-image-entry[data-drop="before"]::after { top: 0; }
|
||||||
|
.create-image-entry[data-drop="after"]::after { bottom: 0; }
|
||||||
|
|
||||||
|
.create-image-preview {
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
overflow: hidden;
|
||||||
|
background: var(--well-fill);
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-image-preview img {
|
||||||
|
display: block;
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-image-details {
|
||||||
|
display: grid;
|
||||||
|
min-width: 0;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-image-details > span {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-image-details > time {
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
.encryption-current {
|
.encryption-current {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
+281
-12
@@ -92,6 +92,7 @@ function createDocumentState() {
|
|||||||
createInsert: false,
|
createInsert: false,
|
||||||
createPackage: false,
|
createPackage: false,
|
||||||
createDocumentAt: null,
|
createDocumentAt: null,
|
||||||
|
createImageQueue: null,
|
||||||
documentManagement: null,
|
documentManagement: null,
|
||||||
versionManagement: null,
|
versionManagement: null,
|
||||||
infoDocumentIndex: null,
|
infoDocumentIndex: null,
|
||||||
@@ -190,6 +191,8 @@ function createDocumentState() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let state = createDocumentState();
|
let state = createDocumentState();
|
||||||
|
let createImagePicker = null;
|
||||||
|
const createPaperSizes = { a4: [210, 297], a3: [297, 420], a5: [148, 210] };
|
||||||
|
|
||||||
const el = {
|
const el = {
|
||||||
appearanceButton: document.querySelector("#appearanceButton"),
|
appearanceButton: document.querySelector("#appearanceButton"),
|
||||||
@@ -502,6 +505,16 @@ const el = {
|
|||||||
createTitle: document.querySelector("#createTitle"),
|
createTitle: document.querySelector("#createTitle"),
|
||||||
createMode: document.querySelector("#createMode"),
|
createMode: document.querySelector("#createMode"),
|
||||||
createImages: document.querySelector("#createImages"),
|
createImages: document.querySelector("#createImages"),
|
||||||
|
createImageQueue: document.querySelector("#createImageQueue"),
|
||||||
|
createImageList: document.querySelector("#createImageList"),
|
||||||
|
createImageAdd: document.querySelector("#createImageAdd"),
|
||||||
|
createImageCount: document.querySelector("#createImageCount"),
|
||||||
|
createImageOrder: document.querySelector("#createImageOrder"),
|
||||||
|
createImageUp: document.querySelector("#createImageUp"),
|
||||||
|
createImageDown: document.querySelector("#createImageDown"),
|
||||||
|
createImageRemove: document.querySelector("#createImageRemove"),
|
||||||
|
createPortrait: document.querySelector("#createPortrait"),
|
||||||
|
createLandscape: document.querySelector("#createLandscape"),
|
||||||
createSize: document.querySelector("#createSize"),
|
createSize: document.querySelector("#createSize"),
|
||||||
createMargin: document.querySelector("#createMargin"),
|
createMargin: document.querySelector("#createMargin"),
|
||||||
createPosition: document.querySelector("#createPosition"),
|
createPosition: document.querySelector("#createPosition"),
|
||||||
@@ -1664,9 +1677,82 @@ el.createCancel.addEventListener("click", () => state.imageImporting ? cancelExp
|
|||||||
el.createPanel.addEventListener("cancel", event => {
|
el.createPanel.addEventListener("cancel", event => {
|
||||||
if (state.imageImporting) { event.preventDefault(); cancelExport(); }
|
if (state.imageImporting) { event.preventDefault(); cancelExport(); }
|
||||||
});
|
});
|
||||||
el.createPanel.addEventListener("close", () => { el.createImages.value = ""; });
|
el.createPanel.addEventListener("close", () => { releaseCreateImages(documentTab()); el.createImages.value = ""; el.createImageList.replaceChildren(); });
|
||||||
el.createMode.addEventListener("change", updateCreateOptions);
|
el.createMode.addEventListener("change", () => {
|
||||||
el.createSize.addEventListener("change", updateCreateOptions);
|
el.createSize.value = el.createMode.value === "images" ? "image" : "a4";
|
||||||
|
applyCreatePaper();
|
||||||
|
});
|
||||||
|
el.createSize.addEventListener("change", applyCreatePaper);
|
||||||
|
for (const input of [el.createWidth, el.createHeight]) input.addEventListener("input", () => updateCreateDirection(true));
|
||||||
|
for (const radio of [el.createPortrait, el.createLandscape]) radio.addEventListener("change", () => {
|
||||||
|
[el.createWidth.value, el.createHeight.value] = [el.createHeight.value, el.createWidth.value];
|
||||||
|
updateCreateDirection();
|
||||||
|
});
|
||||||
|
el.createImageAdd.addEventListener("click", () => {
|
||||||
|
createImagePicker = { tab: documentTab(), queue: state.createImageQueue };
|
||||||
|
el.createImages.click();
|
||||||
|
});
|
||||||
|
el.createImages.addEventListener("change", addCreateImages);
|
||||||
|
el.createImages.addEventListener("cancel", () => { createImagePicker = null; });
|
||||||
|
el.createImageOrder.addEventListener("change", () => { state.createImageQueue.order = el.createImageOrder.value; sortCreateImages(); });
|
||||||
|
el.createImageUp.addEventListener("click", () => moveCreateImage(-1));
|
||||||
|
el.createImageDown.addEventListener("click", () => moveCreateImage(1));
|
||||||
|
el.createImageRemove.addEventListener("click", removeCreateImage);
|
||||||
|
el.createImageList.addEventListener("click", event => {
|
||||||
|
const row = event.target.closest("[data-image-id]");
|
||||||
|
if (row) { state.createImageQueue.selected = Number(row.dataset.imageId); updateCreateImageSelection(); }
|
||||||
|
});
|
||||||
|
el.createImageList.addEventListener("keydown", event => {
|
||||||
|
const row = event.target.closest("[data-image-id]"), queue = state.createImageQueue;
|
||||||
|
if (!row || event.ctrlKey || event.metaKey || event.shiftKey) return;
|
||||||
|
const index = queue.entries.findIndex(entry => entry.id === Number(row.dataset.imageId));
|
||||||
|
if (["ArrowUp", "ArrowDown", "Home", "End"].includes(event.key)) {
|
||||||
|
event.preventDefault();
|
||||||
|
queue.selected = queue.entries[index].id;
|
||||||
|
if (event.altKey && event.key.startsWith("Arrow")) moveCreateImage(event.key === "ArrowUp" ? -1 : 1);
|
||||||
|
else {
|
||||||
|
const next = event.key === "Home" ? 0 : event.key === "End" ? queue.entries.length - 1 : Math.max(0, Math.min(queue.entries.length - 1, index + (event.key === "ArrowUp" ? -1 : 1)));
|
||||||
|
queue.selected = queue.entries[next].id;
|
||||||
|
updateCreateImageSelection();
|
||||||
|
}
|
||||||
|
focusCreateImage();
|
||||||
|
} else if (event.key === "Delete" && !event.altKey) {
|
||||||
|
event.preventDefault();
|
||||||
|
queue.selected = queue.entries[index].id;
|
||||||
|
removeCreateImage();
|
||||||
|
focusCreateImage();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
el.createImageList.addEventListener("dragstart", event => {
|
||||||
|
const row = event.target.closest("[data-image-id]");
|
||||||
|
if (!row || state.imageImporting) { event.preventDefault(); return; }
|
||||||
|
state.createImageQueue.drag = Number(row.dataset.imageId);
|
||||||
|
event.dataTransfer.effectAllowed = "move";
|
||||||
|
event.dataTransfer.setData("application/x-ofdgo-image", row.dataset.imageId);
|
||||||
|
});
|
||||||
|
el.createImageList.addEventListener("dragover", event => {
|
||||||
|
if (state.createImageQueue?.drag == null || !event.dataTransfer.types.includes("application/x-ofdgo-image")) return;
|
||||||
|
event.preventDefault();
|
||||||
|
event.dataTransfer.dropEffect = "move";
|
||||||
|
const row = event.target.closest("[data-image-id]");
|
||||||
|
for (const item of el.createImageList.children) delete item.dataset.drop;
|
||||||
|
if (row) row.dataset.drop = event.clientY < row.getBoundingClientRect().top + row.offsetHeight / 2 ? "before" : "after";
|
||||||
|
});
|
||||||
|
el.createImageList.addEventListener("drop", event => {
|
||||||
|
const queue = state.createImageQueue, row = event.target.closest("[data-image-id]");
|
||||||
|
if (queue?.drag == null || !event.dataTransfer.types.includes("application/x-ofdgo-image")) return;
|
||||||
|
event.preventDefault();
|
||||||
|
const from = queue.entries.findIndex(entry => entry.id === queue.drag);
|
||||||
|
let to = row ? queue.entries.findIndex(entry => entry.id === Number(row.dataset.imageId)) + (row.dataset.drop === "after" ? 1 : 0) : queue.entries.length;
|
||||||
|
if (to > from) to--;
|
||||||
|
queue.selected = queue.drag;
|
||||||
|
queue.drag = null;
|
||||||
|
reorderCreateImage(from, to);
|
||||||
|
});
|
||||||
|
el.createImageList.addEventListener("dragend", () => {
|
||||||
|
if (state.createImageQueue) state.createImageQueue.drag = null;
|
||||||
|
for (const row of el.createImageList.children) delete row.dataset.drop;
|
||||||
|
});
|
||||||
el.createForm.addEventListener("submit", createDocument);
|
el.createForm.addEventListener("submit", createDocument);
|
||||||
editorClick(el.insertTextButton, toggleTextTool);
|
editorClick(el.insertTextButton, toggleTextTool);
|
||||||
editorClick(el.insertImageButton, () => openInsertPanel());
|
editorClick(el.insertImageButton, () => openInsertPanel());
|
||||||
@@ -2860,6 +2946,10 @@ function detachDocumentView() {
|
|||||||
state.searchRunning = false;
|
state.searchRunning = false;
|
||||||
}
|
}
|
||||||
tab.localFonts = fontManager.localFonts;
|
tab.localFonts = fontManager.localFonts;
|
||||||
|
if (state.createImageQueue) {
|
||||||
|
state.createImageQueue.observer?.disconnect();
|
||||||
|
state.createImageQueue.drag = null;
|
||||||
|
}
|
||||||
tab.status = el.statusText.textContent;
|
tab.status = el.statusText.textContent;
|
||||||
advanceDocumentSequence();
|
advanceDocumentSequence();
|
||||||
state.pageSeq++;
|
state.pageSeq++;
|
||||||
@@ -2932,6 +3022,7 @@ function attachDocumentTab(tab) {
|
|||||||
el.ofdInput.value = "";
|
el.ofdInput.value = "";
|
||||||
canvasEditor.pendingSelection = tab.view?.selection || null;
|
canvasEditor.pendingSelection = tab.view?.selection || null;
|
||||||
documentDialogs?.setOwner(tab);
|
documentDialogs?.setOwner(tab);
|
||||||
|
if (el.createPanel.open && state.createImageQueue) renderCreateImages();
|
||||||
workspace.resourceView = resourceElements.Panel.open ? resourceManager : null;
|
workspace.resourceView = resourceElements.Panel.open ? resourceManager : null;
|
||||||
pageView.setContinuous(state.continuous);
|
pageView.setContinuous(state.continuous);
|
||||||
pageView.updatePanControl();
|
pageView.updatePanControl();
|
||||||
@@ -3053,6 +3144,7 @@ async function mountDocumentTab(tab) {
|
|||||||
|
|
||||||
async function releaseDocumentTab(tab) {
|
async function releaseDocumentTab(tab) {
|
||||||
if (tab.closed) return;
|
if (tab.closed) return;
|
||||||
|
releaseCreateImages(tab);
|
||||||
const clipboard = workspace.objectClipboard;
|
const clipboard = workspace.objectClipboard;
|
||||||
if (clipboard?.tab === tab) {
|
if (clipboard?.tab === tab) {
|
||||||
const prepared = await prepareObjectClipboard(clipboard, tab);
|
const prepared = await prepareObjectClipboard(clipboard, tab);
|
||||||
@@ -3734,14 +3826,18 @@ function openCreatePanel(insert = false, packageDocument = false) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
el.createForm.reset();
|
el.createForm.reset();
|
||||||
|
releaseCreateImages(documentTab());
|
||||||
|
state.createImageQueue = { entries: [], selected: null, next: 0, order: "name-asc", drag: null, observer: null, task: Promise.resolve() };
|
||||||
state.createInsert = insert === true;
|
state.createInsert = insert === true;
|
||||||
state.createPackage = packageDocument === true;
|
state.createPackage = packageDocument === true;
|
||||||
el.createTitle.textContent = state.createInsert ? "插入页面" : state.createPackage ? "新增文档" : "新建文档";
|
el.createTitle.textContent = state.createInsert ? "插入页面" : state.createPackage ? "新增文档" : "新建文档";
|
||||||
state.createDocumentAt = state.createPackage ? managedDocumentIndex() + 1 : null;
|
state.createDocumentAt = state.createPackage ? managedDocumentIndex() + 1 : null;
|
||||||
el.createMode.value = state.createInsert ? "images" : "blank";
|
el.createMode.value = state.createInsert ? "images" : "blank";
|
||||||
|
el.createSize.value = state.createInsert ? "image" : "a4";
|
||||||
el.createSubmit.textContent = state.createInsert ? "插入" : state.createPackage ? "新增" : "新建";
|
el.createSubmit.textContent = state.createInsert ? "插入" : state.createPackage ? "新增" : "新建";
|
||||||
el.createCancel.disabled = false;
|
el.createCancel.disabled = false;
|
||||||
updateCreateOptions();
|
updateCreateOptions();
|
||||||
|
renderCreateImages();
|
||||||
el.createStatus.textContent = "";
|
el.createStatus.textContent = "";
|
||||||
el.createPanel.showModal();
|
el.createPanel.showModal();
|
||||||
}
|
}
|
||||||
@@ -3753,21 +3849,187 @@ function updateCreateOptions() {
|
|||||||
el.createName.closest(".form-row").hidden = Boolean(state.createInsert);
|
el.createName.closest(".form-row").hidden = Boolean(state.createInsert);
|
||||||
el.createName.disabled = busy || Boolean(state.createInsert);
|
el.createName.disabled = busy || Boolean(state.createInsert);
|
||||||
el.createMode.disabled = busy;
|
el.createMode.disabled = busy;
|
||||||
for (const [id, input] of [["createImagesRow", el.createImages], ["createSizeRow", el.createSize], ["createMarginRow", el.createMargin]]) {
|
for (const [id, input] of [["createImagesRow", el.createImages], ["createMarginRow", el.createMargin]]) {
|
||||||
toggle(id, images);
|
toggle(id, images);
|
||||||
input.disabled = busy || !images;
|
input.disabled = busy || !images;
|
||||||
}
|
}
|
||||||
el.createImages.required = images;
|
el.createSize.disabled = busy;
|
||||||
const custom = !images || el.createSize.value === "custom";
|
el.createSize.querySelector('[value="image"]').hidden = !images;
|
||||||
toggle("createDimensions", custom);
|
el.createSize.querySelector('[value="image"]').disabled = !images;
|
||||||
el.createWidth.disabled = el.createHeight.disabled = busy || !custom;
|
el.createImageQueue.hidden = !images || !state.createImageQueue?.entries.length;
|
||||||
|
el.createPanel.classList.toggle("create-images", images);
|
||||||
|
el.createImageAdd.disabled = el.createImageOrder.disabled = busy;
|
||||||
|
const dimensions = !images || el.createSize.value !== "image";
|
||||||
|
toggle("createDimensions", dimensions);
|
||||||
|
toggle("createDirectionRow", dimensions);
|
||||||
|
el.createWidth.disabled = el.createHeight.disabled = busy || !dimensions;
|
||||||
toggle("createPositionRow", Boolean(state.createInsert));
|
toggle("createPositionRow", Boolean(state.createInsert));
|
||||||
el.createPosition.disabled = busy || !state.createInsert;
|
el.createPosition.disabled = busy || !state.createInsert;
|
||||||
el.createSubmit.disabled = busy;
|
el.createSubmit.disabled = busy || images && !state.createImageQueue?.entries.length;
|
||||||
|
updateCreateDirection();
|
||||||
|
updateCreateImageSelection();
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyCreatePaper() {
|
||||||
|
const paper = createPaperSizes[el.createSize.value];
|
||||||
|
if (paper) {
|
||||||
|
const [width, height] = el.createLandscape.checked ? [...paper].reverse() : paper;
|
||||||
|
el.createWidth.value = width;
|
||||||
|
el.createHeight.value = height;
|
||||||
|
}
|
||||||
|
updateCreateOptions();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateCreateDirection(matchPaper = false) {
|
||||||
|
const width = Number(el.createWidth.value), height = Number(el.createHeight.value);
|
||||||
|
const valid = width > 0 && height > 0;
|
||||||
|
el.createPortrait.checked = valid && width < height;
|
||||||
|
el.createLandscape.checked = valid && width > height;
|
||||||
|
el.createPortrait.disabled = el.createLandscape.disabled = !valid || width === height || el.createWidth.disabled;
|
||||||
|
if (matchPaper) el.createSize.value = Object.entries(createPaperSizes).find(([, [w, h]]) => Math.min(width, height) === w && Math.max(width, height) === h)?.[0] || "custom";
|
||||||
|
}
|
||||||
|
|
||||||
|
function releaseCreateImages(tab) {
|
||||||
|
const queue = tab.state.createImageQueue;
|
||||||
|
if (!queue) return;
|
||||||
|
queue.observer?.disconnect();
|
||||||
|
for (const entry of queue.entries) if (entry.url) URL.revokeObjectURL(entry.url);
|
||||||
|
queue.entries.length = 0;
|
||||||
|
tab.state.createImageQueue = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function addCreateImages() {
|
||||||
|
const { tab, queue } = createImagePicker || { tab: documentTab(), queue: state.createImageQueue };
|
||||||
|
createImagePicker = null;
|
||||||
|
const files = [...el.createImages.files];
|
||||||
|
el.createImages.value = "";
|
||||||
|
if (tab.closed || !queue || tab.state.createImageQueue !== queue) return;
|
||||||
|
for (const file of files) queue.entries.push({ id: queue.next++, file, url: null, previewed: false });
|
||||||
|
sortCreateImages(queue);
|
||||||
|
if (isForegroundDocument(tab)) updateCreateOptions();
|
||||||
|
}
|
||||||
|
|
||||||
|
function sortCreateImages(queue = state.createImageQueue) {
|
||||||
|
const order = queue.order;
|
||||||
|
if (order !== "custom") queue.entries.sort((a, b) => {
|
||||||
|
const value = order.startsWith("name") ? a.file.name.localeCompare(b.file.name, undefined, { numeric: true }) : order.startsWith("time") ? a.file.lastModified - b.file.lastModified : a.id - b.id;
|
||||||
|
return (order.endsWith("desc") ? -value : value) || a.id - b.id;
|
||||||
|
});
|
||||||
|
if (queue === state.createImageQueue) renderCreateImages();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateCreateImageSelection() {
|
||||||
|
const queue = state.createImageQueue;
|
||||||
|
const index = queue?.entries.findIndex(entry => entry.id === queue.selected) ?? -1;
|
||||||
|
el.createImageUp.disabled = state.imageImporting || index <= 0;
|
||||||
|
el.createImageDown.disabled = state.imageImporting || index < 0 || index === queue.entries.length - 1;
|
||||||
|
el.createImageRemove.disabled = state.imageImporting || index < 0;
|
||||||
|
for (const row of el.createImageList.children) {
|
||||||
|
row.dataset.selected = String(Number(row.dataset.imageId) === queue?.selected);
|
||||||
|
row.tabIndex = row.dataset.selected === "true" ? 0 : -1;
|
||||||
|
row.disabled = state.imageImporting;
|
||||||
|
row.draggable = !state.imageImporting;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function focusCreateImage() {
|
||||||
|
(el.createImageList.querySelector('[data-selected="true"]') || el.createImageAdd).focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
function reorderCreateImage(from, to) {
|
||||||
|
const queue = state.createImageQueue;
|
||||||
|
if (from < 0 || to < 0 || to >= queue.entries.length) return;
|
||||||
|
if (from !== to) {
|
||||||
|
queue.entries.splice(to, 0, queue.entries.splice(from, 1)[0]);
|
||||||
|
queue.order = "custom";
|
||||||
|
}
|
||||||
|
renderCreateImages();
|
||||||
|
}
|
||||||
|
|
||||||
|
function moveCreateImage(direction) {
|
||||||
|
const queue = state.createImageQueue, index = queue.entries.findIndex(entry => entry.id === queue.selected);
|
||||||
|
reorderCreateImage(index, index + direction);
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeCreateImage() {
|
||||||
|
const queue = state.createImageQueue, index = queue.entries.findIndex(entry => entry.id === queue.selected);
|
||||||
|
if (index < 0) return;
|
||||||
|
const [entry] = queue.entries.splice(index, 1);
|
||||||
|
if (entry.url) URL.revokeObjectURL(entry.url);
|
||||||
|
queue.selected = queue.entries[Math.min(index, queue.entries.length - 1)]?.id ?? null;
|
||||||
|
renderCreateImages();
|
||||||
|
updateCreateOptions();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderCreateImages() {
|
||||||
|
const tab = documentTab(), queue = state.createImageQueue;
|
||||||
|
queue.observer?.disconnect();
|
||||||
|
const top = el.createImageList.scrollTop;
|
||||||
|
const focused = el.createImageList.contains(document.activeElement);
|
||||||
|
el.createImageList.replaceChildren();
|
||||||
|
el.createImageOrder.value = queue.order;
|
||||||
|
el.createImageCount.textContent = String(queue.entries.length);
|
||||||
|
if (!queue.entries.some(entry => entry.id === queue.selected)) queue.selected = queue.entries[0]?.id ?? null;
|
||||||
|
queue.observer = new IntersectionObserver(items => {
|
||||||
|
for (const item of items) if (item.isIntersecting && isForegroundDocument(tab)) {
|
||||||
|
queue.observer.unobserve(item.target);
|
||||||
|
const entry = queue.entries.find(entry => entry.id === Number(item.target.dataset.imageId));
|
||||||
|
queue.task = queue.task.then(() => loadCreateImagePreview(tab, queue, entry));
|
||||||
|
}
|
||||||
|
}, { root: el.createImageList, rootMargin: "64px" });
|
||||||
|
for (const [index, entry] of queue.entries.entries()) {
|
||||||
|
const row = document.createElement("button"), number = document.createElement("span"), preview = document.createElement("span"), details = document.createElement("span");
|
||||||
|
row.type = "button";
|
||||||
|
row.className = "create-image-entry";
|
||||||
|
row.dataset.imageId = entry.id;
|
||||||
|
row.title = entry.file.name;
|
||||||
|
number.textContent = String(index + 1);
|
||||||
|
preview.className = "create-image-preview";
|
||||||
|
preview.textContent = entry.file.name.split(".").at(-1).toUpperCase();
|
||||||
|
details.className = "create-image-details";
|
||||||
|
const name = document.createElement("span"), time = document.createElement("time");
|
||||||
|
name.textContent = entry.file.name;
|
||||||
|
time.textContent = new Date(entry.file.lastModified).toLocaleString();
|
||||||
|
details.append(name, time);
|
||||||
|
row.append(number, preview, details);
|
||||||
|
el.createImageList.append(row);
|
||||||
|
if (entry.url) showCreateImagePreview(entry, row);
|
||||||
|
else if (!entry.previewed) queue.observer.observe(row);
|
||||||
|
}
|
||||||
|
el.createImageList.scrollTop = top;
|
||||||
|
updateCreateImageSelection();
|
||||||
|
if (focused) focusCreateImage();
|
||||||
|
}
|
||||||
|
|
||||||
|
function showCreateImagePreview(entry, row) {
|
||||||
|
const image = document.createElement("img");
|
||||||
|
image.src = entry.url;
|
||||||
|
image.draggable = false;
|
||||||
|
row.querySelector(".create-image-preview").replaceChildren(image);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadCreateImagePreview(tab, queue, entry) {
|
||||||
|
if (!entry || entry.previewed || tab.state.createImageQueue !== queue || !queue.entries.includes(entry)) return;
|
||||||
|
entry.previewed = true;
|
||||||
|
let bitmap;
|
||||||
|
try {
|
||||||
|
bitmap = await createImageBitmap(entry.file, { resizeWidth: 64 });
|
||||||
|
const canvas = document.createElement("canvas"), scale = Math.min(64 / bitmap.width, 64 / bitmap.height, 1);
|
||||||
|
canvas.width = Math.max(1, Math.round(bitmap.width * scale));
|
||||||
|
canvas.height = Math.max(1, Math.round(bitmap.height * scale));
|
||||||
|
canvas.getContext("2d").drawImage(bitmap, 0, 0, canvas.width, canvas.height);
|
||||||
|
const blob = await new Promise(resolve => canvas.toBlob(resolve));
|
||||||
|
if (tab.state.createImageQueue !== queue || !queue.entries.includes(entry)) return;
|
||||||
|
entry.url = URL.createObjectURL(blob);
|
||||||
|
if (isForegroundDocument(tab)) {
|
||||||
|
const current = el.createImageList.querySelector(`[data-image-id="${entry.id}"]`);
|
||||||
|
if (current) showCreateImagePreview(entry, current);
|
||||||
|
}
|
||||||
|
} catch {} finally { bitmap?.close(); }
|
||||||
}
|
}
|
||||||
|
|
||||||
function imagePageRequest() {
|
function imagePageRequest() {
|
||||||
const files = [...el.createImages.files].sort((a, b) => a.name.localeCompare(b.name, undefined, { numeric: true }));
|
const files = state.createImageQueue.entries.map(entry => entry.file);
|
||||||
const size = el.createSize.value;
|
const size = el.createSize.value;
|
||||||
return [files, {
|
return [files, {
|
||||||
Width: size === "image" ? 0 : Number(el.createWidth.value),
|
Width: size === "image" ? 0 : Number(el.createWidth.value),
|
||||||
@@ -3778,14 +4040,21 @@ function imagePageRequest() {
|
|||||||
|
|
||||||
async function createDocument(event) {
|
async function createDocument(event) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
const owner = documentTab();
|
||||||
if (state.createPackage) {
|
if (state.createPackage) {
|
||||||
if (await changeDocument("ofdgoChangeDocument", null, "add", el.createName.value.trim() || "未命名",
|
if (await changeDocument("ofdgoChangeDocument", null, "add", el.createName.value.trim() || "未命名",
|
||||||
Number(el.createWidth.value), Number(el.createHeight.value), state.createDocumentAt)) el.createPanel.close();
|
Number(el.createWidth.value), Number(el.createHeight.value), state.createDocumentAt)) {
|
||||||
|
releaseCreateImages(owner);
|
||||||
|
documentDialogs.closeFor(owner, el.createPanel);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (state.createInsert) {
|
if (state.createInsert) {
|
||||||
const at = { before: state.pageIndex, after: state.pageIndex + 1, first: 0, last: state.doc.pageCount }[el.createPosition.value];
|
const at = { before: state.pageIndex, after: state.pageIndex + 1, first: 0, last: state.doc.pageCount }[el.createPosition.value];
|
||||||
if (await changeDocument("ofdgoImportImages", null, ...imagePageRequest(), at, "", state.renderAnnotations)) el.createPanel.close();
|
if (await changeDocument("ofdgoImportImages", null, ...imagePageRequest(), at, "", state.renderAnnotations)) {
|
||||||
|
releaseCreateImages(owner);
|
||||||
|
documentDialogs.closeFor(owner, el.createPanel);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (workspace.closing) {
|
if (workspace.closing) {
|
||||||
|
|||||||
Reference in new issue
Block a user