mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 15:06:46 +08:00
1 parent
d3904521b9
commit
3e87048a30
8 files changed
+336
-32
No files matched your search
+22
-2
@@ -50,9 +50,29 @@
|
||||
<option value="600">600</option>
|
||||
</select>
|
||||
<button id="exportPageButton" class="button" type="button" data-short="单" title="导出单页" aria-label="导出单页" disabled>单页</button>
|
||||
<button id="exportButton" class="button" type="button" data-short="全" title="导出文档" aria-label="导出文档" disabled>文档</button>
|
||||
<button id="exportButton" class="button" type="button" data-short="档" title="导出文档" aria-label="导出文档" aria-haspopup="dialog" popovertarget="exportPanel" disabled>文档</button>
|
||||
</div>
|
||||
</header>
|
||||
<div id="exportPanel" class="export-panel" popover role="dialog" aria-label="导出文档">
|
||||
<form id="exportForm">
|
||||
<div class="export-row">
|
||||
<span id="exportScopeLabel">范围</span>
|
||||
<div class="export-options" role="radiogroup" aria-labelledby="exportScopeLabel">
|
||||
<label><input id="exportAll" type="radio" name="scope" value="all" checked autofocus><span>全部</span></label>
|
||||
<label><input id="exportSpecified" type="radio" name="scope" value="specified"><span>指定</span></label>
|
||||
</div>
|
||||
</div>
|
||||
<div id="exportRangeRow" class="export-row" hidden>
|
||||
<label for="exportRange">页码</label>
|
||||
<input id="exportRange" type="text" aria-describedby="exportRangeStatus" autocomplete="off" spellcheck="false">
|
||||
</div>
|
||||
<div class="export-actions">
|
||||
<div id="exportRangeStatus" class="export-range-status" role="status" aria-live="polite"></div>
|
||||
<button class="button" type="button" popovertarget="exportPanel" popovertargetaction="hide">取消</button>
|
||||
<button id="exportSubmit" class="button" type="submit">导出</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<main class="workspace">
|
||||
<aside class="page-list-panel">
|
||||
@@ -70,7 +90,7 @@
|
||||
<button type="submit" class="button">搜索</button>
|
||||
</form>
|
||||
<div class="search-tools">
|
||||
<span id="searchStatus" role="status" aria-live="polite">输入文字</span>
|
||||
<span id="searchStatus" role="status" aria-live="polite"></span>
|
||||
<span id="searchCount">0 / 0</span>
|
||||
<button id="searchPrev" type="button" class="icon-button arrow-prev" title="上一处" aria-label="上一处" aria-keyshortcuts="Shift+F3" disabled></button>
|
||||
<button id="searchNext" type="button" class="icon-button arrow-next" title="下一处" aria-label="下一处" aria-keyshortcuts="F3" disabled></button>
|
||||
|
||||
@@ -256,6 +256,109 @@ button:not(:disabled):hover {
|
||||
width: 54px;
|
||||
}
|
||||
|
||||
.export-panel {
|
||||
top: calc(var(--bar-height) + 8px);
|
||||
right: 0;
|
||||
bottom: auto;
|
||||
left: auto;
|
||||
width: var(--sidebar-width);
|
||||
max-height: calc(100dvh - var(--bar-height) - 18px);
|
||||
margin: 0;
|
||||
padding: 12px;
|
||||
border: 1px solid var(--line);
|
||||
background: var(--panel);
|
||||
color: #1f2428;
|
||||
box-shadow: var(--shadow-soft);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.export-row {
|
||||
display: grid;
|
||||
grid-template-columns: 2em minmax(0, 1fr);
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.export-options {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.export-options label {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.export-options input {
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.export-options span {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: var(--control-height);
|
||||
border: 1px solid var(--line);
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.export-options label + label span {
|
||||
border-left: 0;
|
||||
}
|
||||
|
||||
.export-options input:checked + span {
|
||||
background: #e4f3ef;
|
||||
color: var(--accent-strong);
|
||||
box-shadow: 0 0 0 1px var(--accent) inset;
|
||||
}
|
||||
|
||||
.export-options input:focus-visible + span {
|
||||
outline: 2px solid var(--focus);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.export-row input[type="text"] {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
height: var(--control-height);
|
||||
padding: 0 6px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 0;
|
||||
background: #ffffff;
|
||||
color: #1f2428;
|
||||
}
|
||||
|
||||
.export-row input[aria-invalid="true"] {
|
||||
border-color: #b14343;
|
||||
}
|
||||
|
||||
.export-range-status {
|
||||
min-width: 0;
|
||||
color: var(--text-muted);
|
||||
font-size: 12px;
|
||||
font-variant-numeric: tabular-nums;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.export-actions {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto auto;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
#exportSubmit {
|
||||
border-color: var(--accent);
|
||||
background: var(--accent);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
#exportSubmit:not(:disabled):hover {
|
||||
background: var(--accent-strong);
|
||||
}
|
||||
|
||||
.format-select:focus,
|
||||
.format-select:focus-visible {
|
||||
border-color: var(--accent);
|
||||
@@ -691,6 +794,7 @@ button.outline-link:hover {
|
||||
.empty-state[hidden],
|
||||
.meta-list > [hidden],
|
||||
.navigation-tabs[hidden],
|
||||
.export-row[hidden],
|
||||
.page-list[hidden],
|
||||
.page-frame[hidden],
|
||||
.progress-panel[hidden] {
|
||||
@@ -1326,6 +1430,11 @@ button.outline-link:hover {
|
||||
--sidebar-width: min(78vw, 292px);
|
||||
}
|
||||
|
||||
.export-panel {
|
||||
right: 10px;
|
||||
width: min(300px, calc(100% - 20px));
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
--tool-gap: 6px;
|
||||
}
|
||||
|
||||
+71
-6
@@ -66,6 +66,7 @@ const state = {
|
||||
thumbnailObserver: null,
|
||||
visibleThumbnails: new Set(),
|
||||
exportFormats: [],
|
||||
exportPages: null,
|
||||
showPages: !COMPACT_LAYOUT.matches,
|
||||
showMeta: !COMPACT_LAYOUT.matches,
|
||||
};
|
||||
@@ -97,6 +98,14 @@ const el = {
|
||||
exportFormat: document.querySelector("#exportFormat"),
|
||||
exportPageButton: document.querySelector("#exportPageButton"),
|
||||
exportButton: document.querySelector("#exportButton"),
|
||||
exportPanel: document.querySelector("#exportPanel"),
|
||||
exportForm: document.querySelector("#exportForm"),
|
||||
exportAll: document.querySelector("#exportAll"),
|
||||
exportSpecified: document.querySelector("#exportSpecified"),
|
||||
exportRangeRow: document.querySelector("#exportRangeRow"),
|
||||
exportRange: document.querySelector("#exportRange"),
|
||||
exportRangeStatus: document.querySelector("#exportRangeStatus"),
|
||||
exportSubmit: document.querySelector("#exportSubmit"),
|
||||
emptyState: document.querySelector("#emptyState"),
|
||||
progressPanel: document.querySelector("#progressPanel"),
|
||||
progressLabel: document.querySelector("#progressLabel"),
|
||||
@@ -201,7 +210,19 @@ document.addEventListener("selectionchange", () => {
|
||||
});
|
||||
el.exportFormat.addEventListener("change", () => updateDPIControl());
|
||||
el.exportPageButton.addEventListener("click", () => exportFile(false));
|
||||
el.exportButton.addEventListener("click", () => exportFile(true));
|
||||
el.exportPanel.addEventListener("beforetoggle", prepareExportPanel);
|
||||
el.exportAll.addEventListener("change", updateExportRange);
|
||||
el.exportSpecified.addEventListener("change", () => {
|
||||
updateExportRange();
|
||||
el.exportRange.focus();
|
||||
});
|
||||
el.exportRange.addEventListener("input", updateExportRange);
|
||||
el.exportForm.addEventListener("submit", (event) => {
|
||||
event.preventDefault();
|
||||
if (!el.exportSubmit.disabled) {
|
||||
return exportFile(true, state.exportPages);
|
||||
}
|
||||
});
|
||||
el.refreshAppButton.addEventListener("click", refreshApplication);
|
||||
el.pageInput.addEventListener("change", () => {
|
||||
const page = Number.parseInt(el.pageInput.value, 10);
|
||||
@@ -254,7 +275,7 @@ updateSidebarState();
|
||||
boot();
|
||||
|
||||
function handleKeyDown(event) {
|
||||
if (event.defaultPrevented || event.isComposing || event.altKey || document.body.hasAttribute("aria-busy")) {
|
||||
if (event.defaultPrevented || event.isComposing || event.altKey || document.body.hasAttribute("aria-busy") || el.exportPanel.matches(":popover-open")) {
|
||||
return;
|
||||
}
|
||||
const key = event.key;
|
||||
@@ -1487,7 +1508,48 @@ async function downloadAttachment(attachment) {
|
||||
}
|
||||
}
|
||||
|
||||
async function exportFile(whole) {
|
||||
function prepareExportPanel(event) {
|
||||
if (event.newState !== "open") {
|
||||
return;
|
||||
}
|
||||
if (!state.doc || document.body.hasAttribute("aria-busy")) {
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
el.exportForm.reset();
|
||||
updateExportRange();
|
||||
}
|
||||
|
||||
async function updateExportRange() {
|
||||
const specified = el.exportSpecified.checked;
|
||||
const value = el.exportRange.value.trim();
|
||||
const openSeq = state.openSeq;
|
||||
state.exportPages = null;
|
||||
el.exportRangeRow.hidden = !specified;
|
||||
el.exportRange.removeAttribute("aria-invalid");
|
||||
el.exportSubmit.disabled = specified;
|
||||
el.exportRangeStatus.textContent = specified && value ? "正在校验" : "";
|
||||
if (!specified || !value) {
|
||||
return;
|
||||
}
|
||||
const current = () => openSeq === state.openSeq && el.exportPanel.matches(":popover-open")
|
||||
&& el.exportSpecified.checked && value === el.exportRange.value.trim();
|
||||
try {
|
||||
const indices = await callWASM("ofdgoParsePageRange", value);
|
||||
if (current()) {
|
||||
state.exportPages = indices;
|
||||
el.exportSubmit.disabled = false;
|
||||
el.exportRangeStatus.textContent = `共 ${indices.length} 页`;
|
||||
}
|
||||
} catch {
|
||||
if (current()) {
|
||||
el.exportRange.setAttribute("aria-invalid", "true");
|
||||
el.exportRangeStatus.textContent = "页码无效";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function exportFile(whole, indices = null) {
|
||||
if (!state.doc || document.body.hasAttribute("aria-busy")) {
|
||||
return;
|
||||
}
|
||||
@@ -1514,7 +1576,7 @@ async function exportFile(whole) {
|
||||
return;
|
||||
}
|
||||
const result = whole
|
||||
? await callWASM("ofdgoExportDocument", format.value, dpi, file)
|
||||
? await callWASM("ofdgoExportDocument", format.value, dpi, indices, file)
|
||||
: await callWASM("ofdgoExportPage", pageIndex, format.value, dpi, file);
|
||||
if (openSeq !== state.openSeq) {
|
||||
return;
|
||||
@@ -2124,7 +2186,7 @@ function resetSearch(clearInput = true) {
|
||||
if (clearInput) {
|
||||
el.searchInput.value = "";
|
||||
}
|
||||
el.searchStatus.textContent = "输入文字";
|
||||
el.searchStatus.textContent = "";
|
||||
el.searchResults.replaceChildren();
|
||||
updateSearchCount();
|
||||
clearSearchHighlights();
|
||||
@@ -3260,6 +3322,9 @@ function setBusy(busy, text = "", percent = 0, status = "") {
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (el.exportPanel.matches(":popover-open")) {
|
||||
el.exportPanel.hidePopover();
|
||||
}
|
||||
endPan();
|
||||
el.progressPanel.hidden = false;
|
||||
setProgress(text, percent, status);
|
||||
@@ -3317,7 +3382,7 @@ function baseFileName() {
|
||||
|
||||
function pageFileName(extension) {
|
||||
const page = String(state.pageIndex + 1).padStart(String(state.doc?.pageCount || 1).length, "0");
|
||||
return `${baseFileName()}_p${page}.${extension}`;
|
||||
return `${baseFileName()}_${page}.${extension}`;
|
||||
}
|
||||
|
||||
function exportFormatInfo(value) {
|
||||
|
||||
Reference in new issue
Block a user