feat(更新应用): 优化更新应用提示
build.yaml / build (push) Successful in 42s

This commit is contained in:
xiaoqidun committed 2026-10-17 18:47:24 +08:00
1 parent 2e5241733a
commit eaefd730ae
4 files changed
+138 -69

No files matched your search

+52 -50
View File
@@ -12,56 +12,59 @@
</head> </head>
<body> <body>
<div class="app"> <div class="app">
<header class="toolbar app-bar"> <header class="toolbar-frame">
<input id="ofdInput" class="file-input" type="file" accept=".ofd,.pdf"> <div class="toolbar app-bar">
<input id="fontInput" class="file-input" type="file" accept=".ttf,.otf,.ttc,font/*" multiple> <input id="ofdInput" class="file-input" type="file" accept=".ofd,.pdf">
<input id="fontDirectoryInput" class="file-input" type="file" webkitdirectory multiple> <input id="fontInput" class="file-input" type="file" accept=".ttf,.otf,.ttc,font/*" multiple>
<div class="tool-group"> <input id="fontDirectoryInput" class="file-input" type="file" webkitdirectory multiple>
<button id="ofdButton" class="icon-button" type="button" title="打开"><span class="open-icon"></span></button> <div class="tool-group">
<button id="newButton" class="icon-button" type="button" title="新建"><span class="new-icon"></span></button> <button id="ofdButton" class="icon-button" type="button" title="打开"><span class="open-icon"></span></button>
<button id="saveButton" class="icon-button" type="button" title="保存" disabled><span class="save-icon"></span></button> <button id="newButton" class="icon-button" type="button" title="新建"><span class="new-icon"></span></button>
<button id="editButton" class="icon-button" type="button" title="编辑" data-pressed="false" disabled><span class="edit-icon"></span></button> <button id="saveButton" class="icon-button" type="button" title="保存" disabled><span class="save-icon"></span></button>
<button id="convertButton" class="icon-button" type="button" title="转换"><span>&#8644;</span></button> <button id="editButton" class="icon-button" type="button" title="编辑" data-pressed="false" disabled><span class="edit-icon"></span></button>
<button id="togglePagesButton" class="icon-button" type="button" title="页面栏" data-pressed="true">&#9776;</button> <button id="convertButton" class="icon-button" type="button" title="转换"><span>&#8644;</span></button>
<button id="toggleMetaButton" class="icon-button" type="button" title="信息栏" data-pressed="true">&#9432;</button> <button id="togglePagesButton" class="icon-button" type="button" title="页面栏" data-pressed="true">&#9776;</button>
</div> <button id="toggleMetaButton" class="icon-button" type="button" title="信息栏" data-pressed="true">&#9432;</button>
<div class="toolbar-space"></div> </div>
<div class="tool-group"> <div class="toolbar-space"></div>
<button id="prevButton" class="icon-button arrow-prev" type="button" title="上一页" disabled></button> <div class="tool-group">
<label class="page-control"> <button id="prevButton" class="icon-button arrow-prev" type="button" title="上一页" disabled></button>
<input id="pageInput" type="number" min="1" value="0" disabled> <label class="page-control">
<span>/</span> <input id="pageInput" type="number" min="1" value="0" disabled>
<span id="pageTotal">0</span> <span>/</span>
</label> <span id="pageTotal">0</span>
<button id="nextButton" class="icon-button arrow-next" type="button" title="下一页" disabled></button> </label>
</div> <button id="nextButton" class="icon-button arrow-next" type="button" title="下一页" disabled></button>
<div class="tool-group"> </div>
<button id="zoomOutButton" class="icon-button" type="button" title="缩小" disabled>-</button> <div class="tool-group">
<span id="zoomLabel" class="zoom-label">100%</span> <button id="zoomOutButton" class="icon-button" type="button" title="缩小" disabled>-</button>
<button id="zoomInButton" class="icon-button" type="button" title="放大" disabled>+</button> <span id="zoomLabel" class="zoom-label">100%</span>
<button id="fitButton" class="button" type="button" data-short="宽" disabled>适宽</button> <button id="zoomInButton" class="icon-button" type="button" title="放大" disabled>+</button>
<button id="fitHeightButton" class="button" type="button" data-short="高" disabled>适高</button> <button id="fitButton" class="button" type="button" data-short="宽" disabled>适宽</button>
<button id="rotateButton" class="button" type="button" data-short="转" disabled>旋转</button> <button id="fitHeightButton" class="button" type="button" data-short="高" disabled>适高</button>
<button id="panButton" class="button" type="button" data-short="拖" data-pressed="false" disabled>拖动</button> <button id="rotateButton" class="button" type="button" data-short="转" disabled>旋转</button>
<button id="continuousButton" class="button" type="button" data-short="连" data-pressed="false" disabled>连续</button> <button id="panButton" class="button" type="button" data-short="拖" data-pressed="false" disabled>拖动</button>
<button id="annotationButton" class="button" type="button" data-short="注" data-pressed="true">注解</button> <button id="continuousButton" class="button" type="button" data-short="连" data-pressed="false" disabled>连续</button>
</div> <button id="annotationButton" class="button" type="button" data-short="注" data-pressed="true">注解</button>
<div class="tool-group"> </div>
<button id="renderVectorButton" class="button" type="button" data-short="矢" data-pressed="true" disabled>矢量</button> <div class="tool-group">
<button id="renderRasterButton" class="button" type="button" data-short="位" data-pressed="false" disabled>位图</button> <button id="renderVectorButton" class="button" type="button" data-short="矢" data-pressed="true" disabled>矢量</button>
</div> <button id="renderRasterButton" class="button" type="button" data-short="位" data-pressed="false" disabled>位图</button>
<div class="tool-group"> </div>
<label class="toolbar-select"><select id="exportFormat" class="format-select" title="导出格式" disabled></select><span id="formatValue"></span></label> <div class="tool-group">
<label class="toolbar-select"><select id="imageDPI" class="format-select" title="输出精度" disabled> <label class="toolbar-select"><select id="exportFormat" class="format-select" title="导出格式" disabled></select><span id="formatValue"></span></label>
<option value="96">96 dpi</option> <label class="toolbar-select"><select id="imageDPI" class="format-select" title="输出精度" disabled>
<option value="150">150 dpi</option> <option value="96">96 dpi</option>
<option value="300" selected>300 dpi</option> <option value="150">150 dpi</option>
<option value="600">600 dpi</option> <option value="300" selected>300 dpi</option>
</select><span id="dpiValue">300</span></label> <option value="600">600 dpi</option>
<label class="toolbar-select"><select id="outputCompression" class="format-select" title="压缩模式" disabled><option value="0">原始</option><option value="1">无损</option><option value="2">轻压</option><option value="3">均衡</option><option value="4">强压</option></select><span id="compressionValue" data-short="原">原始</span></label> </select><span id="dpiValue">300</span></label>
<button id="exportPageButton" class="button" type="button" data-short="单" disabled>单页</button> <label class="toolbar-select"><select id="outputCompression" class="format-select" title="压缩模式" disabled><option value="0">原始</option><option value="1">无损</option><option value="2">轻压</option><option value="3">均衡</option><option value="4">强压</option></select><span id="compressionValue" data-short="原">原始</span></label>
<button id="exportButton" class="button" type="button" data-short="多" disabled>文档</button> <button id="exportPageButton" class="button" type="button" data-short="单" disabled>单页</button>
<button id="exportButton" class="button" type="button" data-short="多" disabled>文档</button>
</div>
</div> </div>
<button id="refreshAppButton" class="button app-update" type="button" hidden>更新</button>
</header> </header>
<div id="editorTools" class="editor-toolbar" hidden> <div id="editorTools" class="editor-toolbar" hidden>
<div class="editor-group editor-actions"> <div class="editor-group editor-actions">
@@ -708,7 +711,6 @@
<div id="appPanelTitle" class="panel-title compact">应用</div> <div id="appPanelTitle" class="panel-title compact">应用</div>
<div class="panel-actions"> <div class="panel-actions">
<span id="offlineStatus" class="app-status">准备中</span> <span id="offlineStatus" class="app-status">准备中</span>
<button id="refreshAppButton" class="small-button app-refresh" type="button" title="刷新应用" disabled>&#8635;</button>
</div> </div>
</div> </div>
</section> </section>
+32 -8
View File
@@ -445,6 +445,38 @@ a:focus-visible {
box-shadow: 0 1px 0 rgba(255, 255, 255, 0.68) inset; box-shadow: 0 1px 0 rgba(255, 255, 255, 0.68) inset;
} }
.toolbar-frame {
display: flex;
min-width: 0;
background: var(--bar-bg);
border-bottom: 1px solid var(--line);
}
.toolbar-frame > .toolbar {
flex: 1 1 auto;
border-bottom: 0;
}
.app-update {
flex: 0 0 auto;
align-self: center;
width: 62px;
margin: 0 10px 0 0;
border-color: var(--accent);
background: #e4f3ef;
color: var(--accent-strong);
font-size: 14px;
line-height: 1;
}
.app-update:not(:disabled):hover {
background: #f2faf7;
}
.app-update[hidden] {
display: none;
}
.toolbar { .toolbar {
--tool-gap: 8px; --tool-gap: 8px;
position: relative; position: relative;
@@ -2281,14 +2313,6 @@ body[data-hide-meta] .meta-panel {
font-size: 12px; font-size: 12px;
} }
.app-refresh {
width: 26px;
padding: 0;
color: var(--accent-strong);
font-size: 18px;
line-height: 1;
}
.navigation-tabs { .navigation-tabs {
display: flex; display: flex;
flex-shrink: 0; flex-shrink: 0;
+52 -10
View File
@@ -4194,16 +4194,18 @@ async function prepareOffline() {
function watchApplicationUpdates(checksum) { function watchApplicationUpdates(checksum) {
let checking = false; let checking = false;
let checked = 0;
async function check() { async function check() {
if (checking || document.hidden || !navigator.onLine) { if (checking || document.hidden || !navigator.onLine || Date.now() - checked < 60000) {
return; return;
} }
checking = true; checking = true;
checked = Date.now();
try { try {
const response = await fetch("./", { method: "HEAD", cache: "no-store", signal: AbortSignal.timeout(10000) }); const response = await fetch("./", { method: "HEAD", cache: "no-store", signal: AbortSignal.timeout(10000) });
const latest = response.headers.get("X-OFDGo-Checksum"); const latest = response.headers.get("X-OFDGo-Checksum");
if (response.ok && !response.redirected && latest) { if (response.ok && !response.redirected && latest) {
el.offlineStatus.textContent = latest === checksum ? "可离线" : "可更新"; el.refreshAppButton.hidden = latest === checksum;
} }
} catch { } catch {
} finally { } finally {
@@ -4212,16 +4214,27 @@ function watchApplicationUpdates(checksum) {
} }
window.addEventListener("online", check); window.addEventListener("online", check);
document.addEventListener("visibilitychange", check); document.addEventListener("visibilitychange", check);
window.setInterval(check, 300000);
check(); check();
} }
function waitForWorker(worker, target) { function waitForWorker(worker, target) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
if (!worker) {
reject(new Error("离线服务未就绪"));
return;
}
const timer = setTimeout(() => {
worker.removeEventListener("statechange", changed);
reject(new Error("更新等待超时"));
}, 30000);
function changed() { function changed() {
if (worker.state === target || worker.state === "activated") { if (worker.state === target || worker.state === "activated") {
clearTimeout(timer);
worker.removeEventListener("statechange", changed); worker.removeEventListener("statechange", changed);
resolve(); resolve();
} else if (worker.state === "redundant") { } else if (worker.state === "redundant") {
clearTimeout(timer);
worker.removeEventListener("statechange", changed); worker.removeEventListener("statechange", changed);
reject(new Error("离线启动失败")); reject(new Error("离线启动失败"));
} }
@@ -4233,8 +4246,17 @@ function waitForWorker(worker, target) {
function requestOffline(worker, type) { function requestOffline(worker, type) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
if (!worker) {
reject(new Error("离线服务未就绪"));
return;
}
const channel = new MessageChannel(); const channel = new MessageChannel();
const timer = setTimeout(() => {
close();
reject(new Error("更新等待超时"));
}, 180000);
function close() { function close() {
clearTimeout(timer);
channel.port1.close(); channel.port1.close();
worker.removeEventListener("statechange", changed); worker.removeEventListener("statechange", changed);
} }
@@ -4253,13 +4275,23 @@ function requestOffline(worker, type) {
} }
}; };
worker.addEventListener("statechange", changed); worker.addEventListener("statechange", changed);
worker.postMessage({ type }, [channel.port2]); try {
worker.postMessage({ type }, [channel.port2]);
} catch (err) {
close();
channel.port2.close();
reject(err);
}
changed(); changed();
}); });
} }
async function refreshApplication() { async function refreshApplication() {
if (document.body.hasAttribute("data-busy")) { if (nativeHost || state.exporting || document.body.hasAttribute("data-busy")) {
return;
}
if (!navigator.onLine) {
setStatus("离线,联网后重试");
return; return;
} }
const temporaryFonts = fontManager.userFonts.some((font) => font.source === "upload"); const temporaryFonts = fontManager.userFonts.some((font) => font.source === "upload");
@@ -4267,21 +4299,29 @@ async function refreshApplication() {
if ((state.ofdBytes || state.doc || temporaryFonts) && !window.confirm(message)) { if ((state.ofdBytes || state.doc || temporaryFonts) && !window.confirm(message)) {
return; return;
} }
setBusy(true, "正在刷新应用", null, "正在刷新应用"); setBusy(true, "正在更新应用", null, "正在更新应用");
el.refreshAppButton.disabled = true; el.refreshAppButton.disabled = true;
const panels = document.querySelectorAll(".toolbar, .workspace"); el.refreshAppButton.textContent = "更新中";
const panels = document.querySelectorAll(".toolbar-frame, .workspace");
panels.forEach((panel) => { panel.inert = true; }); panels.forEach((panel) => { panel.inert = true; });
try { try {
if (navigator.storage?.persist) { if (navigator.storage?.persist) {
await navigator.storage.persist().catch(() => false); navigator.storage.persist().catch(() => false);
} }
const scope = new URL("./", location.href).href; const scope = new URL("./", location.href).href;
await navigator.locks.request(`ofdgo:${scope}:refresh`, { ifAvailable: true }, async (lock) => { await navigator.locks.request(`ofdgo:${scope}:refresh`, { ifAvailable: true }, async (lock) => {
if (!lock) { if (!lock) {
throw new Error("应用正在刷新"); throw new Error("其他页面正在更新");
} }
const registration = await registerOffline(); const registration = await registerOffline();
await registration.update(); let timer;
try {
await Promise.race([registration.update(), new Promise((resolve, reject) => {
timer = setTimeout(() => reject(new Error("更新等待超时")), 30000);
})]);
} finally {
clearTimeout(timer);
}
if (registration.installing) { if (registration.installing) {
await waitForWorker(registration.installing, "installed"); await waitForWorker(registration.installing, "installed");
} }
@@ -4292,10 +4332,11 @@ async function refreshApplication() {
location.reload(); location.reload();
}); });
} catch (err) { } catch (err) {
setStatus(err.message === "应用正在刷新" ? err.message : "应用刷新失败"); setStatus(err.message === "其他页面正在更新" ? err.message : "更新失败,请重试");
} finally { } finally {
panels.forEach((panel) => { panel.inert = false; }); panels.forEach((panel) => { panel.inert = false; });
el.refreshAppButton.disabled = false; el.refreshAppButton.disabled = false;
el.refreshAppButton.textContent = "更新";
setBusy(false); setBusy(false);
} }
} }
@@ -12019,6 +12060,7 @@ function showError(err, empty = !state.doc) {
function setBusy(busy, text = "", percent = 0, status = "") { function setBusy(busy, text = "", percent = 0, status = "") {
document.body.toggleAttribute("data-busy", busy); document.body.toggleAttribute("data-busy", busy);
el.refreshAppButton.disabled = busy;
updateStructureControls(); updateStructureControls();
updateDocumentControls(); updateDocumentControls();
renderSecurity(); renderSecurity();
+2 -1
View File
@@ -106,8 +106,9 @@ async function readResource(event, navigation) {
} }
async function updateBundle(meta) { async function updateBundle(meta) {
const signal = AbortSignal.timeout(120000);
const resources = await Promise.all(ASSETS.map(async (url) => { const resources = await Promise.all(ASSETS.map(async (url) => {
const response = await fetch(new Request(url, { cache: "no-store" })); const response = await fetch(new Request(url, { cache: "no-store", signal }));
if (!response.ok || response.redirected) { if (!response.ok || response.redirected) {
throw new Error("资源下载失败"); throw new Error("资源下载失败");
} }