mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 16:44:23 +08:00
feat(导出文档): 支持取消导出
This commit is contained in:
1 parent
c7ab15baa5
commit
b55e8468ab
7 files changed
+125
-38
No files matched your search
@@ -104,6 +104,7 @@
|
||||
<div id="progressPanel" class="progress-panel" role="status" aria-live="polite" hidden>
|
||||
<div class="progress-spinner"></div>
|
||||
<div id="progressLabel" class="progress-label">加载中</div>
|
||||
<button id="cancelExportButton" class="small-button" type="button" title="取消导出" aria-label="取消导出" hidden>取消</button>
|
||||
<div class="progress-track">
|
||||
<div id="progressBar" class="progress-bar" role="progressbar" aria-valuemin="0" aria-valuemax="100" aria-valuenow="0"></div>
|
||||
</div>
|
||||
|
||||
@@ -803,6 +803,7 @@ button.outline-link:hover {
|
||||
.form-row[hidden],
|
||||
.page-list[hidden],
|
||||
.page-frame[hidden],
|
||||
.progress-panel > [hidden],
|
||||
.progress-panel[hidden] {
|
||||
display: none;
|
||||
}
|
||||
@@ -825,6 +826,10 @@ button.outline-link:hover {
|
||||
backdrop-filter: blur(8px);
|
||||
}
|
||||
|
||||
.progress-panel:has(> button:not([hidden])) {
|
||||
grid-template-columns: auto minmax(0, 1fr) auto;
|
||||
}
|
||||
|
||||
.progress-spinner {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
|
||||
+31
-5
@@ -28,6 +28,7 @@ const state = {
|
||||
wasmRecovering: false,
|
||||
wasmRecoveries: 0,
|
||||
exporting: false,
|
||||
exportRequestID: 0,
|
||||
ofdBytes: null,
|
||||
fileName: "ofdgo.ofd",
|
||||
openSeq: 0,
|
||||
@@ -111,6 +112,7 @@ const el = {
|
||||
emptyState: document.querySelector("#emptyState"),
|
||||
progressPanel: document.querySelector("#progressPanel"),
|
||||
progressLabel: document.querySelector("#progressLabel"),
|
||||
cancelExportButton: document.querySelector("#cancelExportButton"),
|
||||
progressBar: document.querySelector("#progressBar"),
|
||||
pageFrame: document.querySelector("#pageFrame"),
|
||||
viewerPanel: document.querySelector(".viewer-panel"),
|
||||
@@ -214,6 +216,7 @@ el.exportFormat.addEventListener("change", () => updateDPIControl());
|
||||
el.exportPageButton.addEventListener("click", () => exportFile(false));
|
||||
el.exportButton.addEventListener("click", openExportPanel);
|
||||
el.exportCancel.addEventListener("click", () => el.exportPanel.close());
|
||||
el.cancelExportButton.addEventListener("click", cancelExport);
|
||||
el.exportPanel.addEventListener("pointerdown", (event) => {
|
||||
state.exportBackdrop = event.target === el.exportPanel;
|
||||
});
|
||||
@@ -673,11 +676,14 @@ async function loadWASM() {
|
||||
} else if (data.type === "export") {
|
||||
if (wasmRequests.get(data.id)?.openSeq === state.openSeq && state.exporting) {
|
||||
if (data.stage === "save") {
|
||||
el.cancelExportButton.disabled = true;
|
||||
setProgress("正在保存", null);
|
||||
} else if (data.completed === data.total) {
|
||||
setProgress("正在封装", null);
|
||||
} else {
|
||||
setProgress(`正在导出 ${data.completed} / ${data.total} 页`, data.completed / data.total * 100);
|
||||
} else if (!el.cancelExportButton.disabled) {
|
||||
if (data.completed === data.total) {
|
||||
setProgress("正在封装", null);
|
||||
} else {
|
||||
setProgress(`正在导出 ${data.completed} / ${data.total} 页`, data.completed / data.total * 100);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (data.type === "ready") {
|
||||
@@ -691,7 +697,11 @@ async function loadWASM() {
|
||||
if (data.ok) {
|
||||
request.resolve(data.data);
|
||||
} else {
|
||||
request.reject(new Error(data.error));
|
||||
const err = new Error(data.error);
|
||||
if (data.canceled) {
|
||||
err.name = "AbortError";
|
||||
}
|
||||
request.reject(err);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1613,6 +1623,8 @@ async function exportFile(whole, indices = null) {
|
||||
}
|
||||
} finally {
|
||||
state.exporting = false;
|
||||
state.exportRequestID = 0;
|
||||
el.cancelExportButton.hidden = true;
|
||||
if (openSeq === state.openSeq) {
|
||||
setBusy(false);
|
||||
updateControls();
|
||||
@@ -1620,6 +1632,15 @@ async function exportFile(whole, indices = null) {
|
||||
}
|
||||
}
|
||||
|
||||
function cancelExport() {
|
||||
if (!state.exportRequestID || el.cancelExportButton.disabled) {
|
||||
return;
|
||||
}
|
||||
el.cancelExportButton.disabled = true;
|
||||
setProgress("正在取消", null);
|
||||
wasmWorker.postMessage({ type: "cancel", id: state.exportRequestID });
|
||||
}
|
||||
|
||||
function renderPageFlow() {
|
||||
el.svgHost.replaceChildren();
|
||||
el.viewerPanel.classList.remove("single-page-fits-height");
|
||||
@@ -3312,6 +3333,11 @@ async function callWASM(name, ...args) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const id = ++wasmRequestID;
|
||||
wasmRequests.set(id, { resolve, reject, openSeq: state.openSeq });
|
||||
if (name === "ofdgoExportPage" || name === "ofdgoExportDocument") {
|
||||
state.exportRequestID = id;
|
||||
el.cancelExportButton.hidden = false;
|
||||
el.cancelExportButton.disabled = false;
|
||||
}
|
||||
try {
|
||||
wasmWorker.postMessage({ id, name, args });
|
||||
} catch (err) {
|
||||
|
||||
@@ -1,38 +1,61 @@
|
||||
importScripts("./wasm_exec.js");
|
||||
|
||||
let pending = Promise.resolve();
|
||||
const exports = new Map();
|
||||
self.onmessage = ({ data }) => {
|
||||
if (data.type === "cancel") {
|
||||
exports.get(data.id)?.abort();
|
||||
return;
|
||||
}
|
||||
if (data.name === "ofdgoExportPage" || data.name === "ofdgoExportDocument") {
|
||||
exports.set(data.id, new AbortController());
|
||||
}
|
||||
pending = pending.then(() => handleMessage(data));
|
||||
};
|
||||
async function handleMessage({ id, name, args }) {
|
||||
let output;
|
||||
let channel;
|
||||
const signal = exports.get(id)?.signal;
|
||||
try {
|
||||
signal?.throwIfAborted();
|
||||
const chunks = [];
|
||||
let size = 0;
|
||||
const exporting = name === "ofdgoExportPage" || name === "ofdgoExportDocument";
|
||||
if (exporting) {
|
||||
if (signal) {
|
||||
const file = args.pop();
|
||||
if (file) {
|
||||
output = await file.createWritable();
|
||||
}
|
||||
signal.throwIfAborted();
|
||||
channel = new MessageChannel();
|
||||
const finish = (done, err) => {
|
||||
channel.port1.onmessage = () => done(err?.message || "", signal.aborted);
|
||||
channel.port2.postMessage(null);
|
||||
};
|
||||
args.push((bytes, done) => {
|
||||
if (signal.aborted) {
|
||||
finish(done);
|
||||
return;
|
||||
}
|
||||
size += bytes.length;
|
||||
if (output) {
|
||||
output.write(bytes).then(() => done(), (err) => done(err.message));
|
||||
output.write(bytes).then(() => finish(done), (err) => finish(done, err));
|
||||
} else {
|
||||
chunks.push(new Blob([bytes]));
|
||||
setTimeout(done, 0);
|
||||
finish(done);
|
||||
}
|
||||
});
|
||||
if (name === "ofdgoExportDocument") {
|
||||
args.push((completed, total) => {
|
||||
args.push((completed, total, done) => {
|
||||
self.postMessage({ id, type: "export", stage: "pages", completed, total });
|
||||
finish(done);
|
||||
});
|
||||
}
|
||||
}
|
||||
const payload = await globalThis[name](...args);
|
||||
signal?.throwIfAborted();
|
||||
const result = typeof payload === "string" ? JSON.parse(payload) : payload;
|
||||
if (exporting && result.ok) {
|
||||
if (signal && result.ok) {
|
||||
exports.delete(id);
|
||||
result.data.size = size;
|
||||
self.postMessage({ id, type: "export", stage: "save" });
|
||||
if (output) {
|
||||
@@ -47,7 +70,16 @@ async function handleMessage({ id, name, args }) {
|
||||
self.postMessage({ id, ...result }, result.data?.bytes ? [result.data.bytes.buffer] : []);
|
||||
} catch (err) {
|
||||
await output?.abort().catch(() => {});
|
||||
self.postMessage({ id, ok: false, error: err.message });
|
||||
const result = { id, ok: false, error: err.message };
|
||||
if (signal?.aborted) {
|
||||
result.error = "导出已取消";
|
||||
result.canceled = true;
|
||||
}
|
||||
self.postMessage(result);
|
||||
} finally {
|
||||
channel?.port1.close();
|
||||
channel?.port2.close();
|
||||
exports.delete(id);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user