mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 13:56:32 +08:00
This commit is contained in:
1 parent
7ee1052c8b
commit
080985caec
7 files changed
+348
-93
No files matched your search
+18
-2
@@ -14,9 +14,25 @@
|
||||
|
||||
package webuiassets
|
||||
|
||||
import "embed"
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"embed"
|
||||
"encoding/hex"
|
||||
)
|
||||
|
||||
// FS WebUI嵌入静态文件
|
||||
//
|
||||
//go:embed index.html ofdgo.js ofdgo.css wasm_exec.js ofdgo.wasm
|
||||
//go:embed index.html ofdgo.css ofdgo.js ofdgo.sw.js ofdgo.wasm wasm_exec.js
|
||||
var FS embed.FS
|
||||
|
||||
// Checksum 计算WebUI嵌入资源校验值
|
||||
// 返回: string SHA-256十六进制校验值
|
||||
func Checksum() string {
|
||||
hash := sha256.New()
|
||||
entries, _ := FS.ReadDir(".")
|
||||
for _, entry := range entries {
|
||||
data, _ := FS.ReadFile(entry.Name())
|
||||
hash.Write(data)
|
||||
}
|
||||
return hex.EncodeToString(hash.Sum(nil))
|
||||
}
|
||||
+10
-1
@@ -13,7 +13,7 @@
|
||||
<input id="fontInput" class="file-input" type="file" accept=".ttf,.otf,.ttc,font/*" multiple>
|
||||
<input id="fontDirectoryInput" class="file-input" type="file" webkitdirectory multiple>
|
||||
<div class="tool-group">
|
||||
<button id="ofdButton" class="button primary" type="button" data-short="打开" aria-label="打开">打开</button>
|
||||
<button id="ofdButton" class="icon-button" type="button" title="打开" aria-label="打开">🗁</button>
|
||||
<button id="togglePagesButton" class="icon-button" type="button" title="页面栏" aria-label="页面栏" aria-pressed="true">☰</button>
|
||||
<button id="toggleMetaButton" class="icon-button" type="button" title="信息栏" aria-label="信息栏" aria-pressed="true">ⓘ</button>
|
||||
</div>
|
||||
@@ -70,6 +70,15 @@
|
||||
</section>
|
||||
|
||||
<aside class="meta-panel">
|
||||
<section id="appPanel" class="app-panel" aria-labelledby="appPanelTitle" hidden>
|
||||
<div class="panel-row">
|
||||
<div id="appPanelTitle" class="panel-title compact">应用</div>
|
||||
<div class="panel-actions">
|
||||
<span id="offlineStatus" class="app-status" role="status">准备中</span>
|
||||
<button id="refreshAppButton" class="small-button app-refresh" type="button" title="刷新应用" aria-label="刷新应用" disabled>↻</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<div class="panel-title">文档</div>
|
||||
<dl class="meta-list">
|
||||
<div>
|
||||
|
||||
+26
-57
@@ -103,7 +103,6 @@ a:focus-visible {
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
}
|
||||
|
||||
.toolbar::-webkit-scrollbar {
|
||||
@@ -132,16 +131,6 @@ a:focus-visible {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.button.primary {
|
||||
border-color: var(--accent);
|
||||
background: var(--accent);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.button.primary:hover {
|
||||
background: var(--accent-strong);
|
||||
}
|
||||
|
||||
button:not(:disabled):hover {
|
||||
border-color: var(--accent);
|
||||
background: #f2faf7;
|
||||
@@ -172,11 +161,6 @@ button:not(:disabled):hover {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.tool-group + .tool-group {
|
||||
margin-left: 0;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.tool-group + .tool-group::before {
|
||||
flex: 0 0 1px;
|
||||
width: 1px;
|
||||
@@ -332,6 +316,30 @@ body[data-hide-meta] .meta-panel {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.app-panel {
|
||||
margin-bottom: 12px;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.app-panel .panel-row {
|
||||
margin-bottom: 0;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
.app-status {
|
||||
color: var(--text-muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.app-refresh {
|
||||
width: 26px;
|
||||
padding: 0;
|
||||
color: var(--accent-strong);
|
||||
font-size: 18px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.page-list {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
@@ -760,7 +768,8 @@ body[data-hide-meta] .meta-panel {
|
||||
}
|
||||
|
||||
.font-badge.embedded,
|
||||
.font-badge.matched {
|
||||
.font-badge.matched,
|
||||
.font-badge.valid {
|
||||
background: #e4f3ef;
|
||||
color: var(--accent-strong);
|
||||
}
|
||||
@@ -776,11 +785,6 @@ body[data-hide-meta] .meta-panel {
|
||||
color: #803f38;
|
||||
}
|
||||
|
||||
.font-badge.valid {
|
||||
background: #e4f3ef;
|
||||
color: var(--accent-strong);
|
||||
}
|
||||
|
||||
.signature-line {
|
||||
display: grid;
|
||||
grid-template-columns: 30px minmax(0, 1fr);
|
||||
@@ -932,9 +936,6 @@ body[data-hide-meta] .meta-panel {
|
||||
flex: 0 0 var(--sidebar-width);
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
font-weight: 400;
|
||||
color: var(--accent-strong);
|
||||
text-decoration: none;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
@@ -977,8 +978,6 @@ body[data-hide-meta] .meta-panel {
|
||||
|
||||
.toolbar {
|
||||
--tool-gap: 6px;
|
||||
flex-wrap: nowrap;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.toolbar > * {
|
||||
@@ -989,19 +988,11 @@ body[data-hide-meta] .meta-panel {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.app-footer {
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
.footer-brand,
|
||||
.footer-links {
|
||||
flex: 0 1 auto;
|
||||
}
|
||||
|
||||
.status-text {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.workspace {
|
||||
position: relative;
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
@@ -1059,20 +1050,6 @@ body[data-hide-meta] .meta-panel {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
height: var(--bar-height);
|
||||
min-height: var(--bar-height);
|
||||
flex-wrap: nowrap;
|
||||
align-content: center;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
row-gap: 0;
|
||||
}
|
||||
|
||||
.toolbar-space {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tool-group + .tool-group::before {
|
||||
display: none;
|
||||
}
|
||||
@@ -1092,10 +1069,6 @@ body[data-hide-meta] .meta-panel {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.button.primary[data-short] {
|
||||
min-width: 42px;
|
||||
}
|
||||
|
||||
.icon-button {
|
||||
font-size: 13px;
|
||||
}
|
||||
@@ -1118,8 +1091,4 @@ body[data-hide-meta] .meta-panel {
|
||||
.dpi-select {
|
||||
width: 48px;
|
||||
}
|
||||
|
||||
.footer-links {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
+134
-24
@@ -115,6 +115,9 @@ const el = {
|
||||
pageListPanel: document.querySelector(".page-list-panel"),
|
||||
pageList: document.querySelector("#pageList"),
|
||||
metaPanel: document.querySelector(".meta-panel"),
|
||||
appPanel: document.querySelector("#appPanel"),
|
||||
offlineStatus: document.querySelector("#offlineStatus"),
|
||||
refreshAppButton: document.querySelector("#refreshAppButton"),
|
||||
metaTitle: document.querySelector("#metaTitle"),
|
||||
metaAuthor: document.querySelector("#metaAuthor"),
|
||||
metaVersion: document.querySelector("#metaVersion"),
|
||||
@@ -152,6 +155,7 @@ el.annotationButton.addEventListener("click", toggleAnnotations);
|
||||
el.pageExportFormat.addEventListener("change", () => updateDPIControl());
|
||||
el.exportPageButton.addEventListener("click", exportCurrentPage);
|
||||
el.exportButton.addEventListener("click", exportPDF);
|
||||
el.refreshAppButton.addEventListener("click", refreshApplication);
|
||||
el.pageInput.addEventListener("change", () => {
|
||||
const page = Number.parseInt(el.pageInput.value, 10);
|
||||
if (Number.isFinite(page)) {
|
||||
@@ -228,6 +232,10 @@ function resizeViewer() {
|
||||
async function boot() {
|
||||
setBusy(true, "正在准备引擎", 8, STATUS.engine);
|
||||
try {
|
||||
if (!await prepareOffline()) {
|
||||
return;
|
||||
}
|
||||
setProgress("正在准备引擎", 8, STATUS.engine);
|
||||
await ensureWASM();
|
||||
setProgress("引擎准备完成", 70);
|
||||
loadExportFormats();
|
||||
@@ -247,6 +255,120 @@ async function boot() {
|
||||
}
|
||||
}
|
||||
|
||||
async function registerOffline() {
|
||||
const scope = new URL("./", location.href).href;
|
||||
let registration = await navigator.serviceWorker.getRegistration(scope);
|
||||
if (!registration || registration.scope !== scope) {
|
||||
registration = await navigator.serviceWorker.register("./ofdgo.sw.js", { updateViaCache: "none" });
|
||||
}
|
||||
const worker = registration.active || registration.installing || registration.waiting;
|
||||
await waitForWorker(worker, "activated");
|
||||
return registration;
|
||||
}
|
||||
|
||||
async function prepareOffline() {
|
||||
if (!window.isSecureContext || !("serviceWorker" in navigator) || !("caches" in window) || !("locks" in navigator)) {
|
||||
return true;
|
||||
}
|
||||
el.appPanel.hidden = false;
|
||||
setProgress("正在准备离线", null, "正在准备离线");
|
||||
try {
|
||||
const registration = await registerOffline();
|
||||
const result = await requestOffline(registration.active, "prepare");
|
||||
if (result.reload) {
|
||||
location.reload();
|
||||
return false;
|
||||
}
|
||||
el.offlineStatus.textContent = "可离线";
|
||||
} catch {
|
||||
el.offlineStatus.textContent = "未就绪";
|
||||
}
|
||||
el.refreshAppButton.disabled = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
function waitForWorker(worker, target) {
|
||||
return new Promise((resolve, reject) => {
|
||||
function changed() {
|
||||
if (worker.state === target || worker.state === "activated") {
|
||||
worker.removeEventListener("statechange", changed);
|
||||
resolve();
|
||||
} else if (worker.state === "redundant") {
|
||||
worker.removeEventListener("statechange", changed);
|
||||
reject(new Error("离线服务启动失败"));
|
||||
}
|
||||
}
|
||||
worker.addEventListener("statechange", changed);
|
||||
changed();
|
||||
});
|
||||
}
|
||||
|
||||
function requestOffline(worker, type) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const channel = new MessageChannel();
|
||||
function close() {
|
||||
channel.port1.close();
|
||||
worker.removeEventListener("statechange", changed);
|
||||
}
|
||||
function changed() {
|
||||
if (worker.state === "redundant") {
|
||||
close();
|
||||
reject(new Error("离线服务已更新"));
|
||||
}
|
||||
}
|
||||
channel.port1.onmessage = ({ data }) => {
|
||||
close();
|
||||
if (data.ok) {
|
||||
resolve(data);
|
||||
} else {
|
||||
reject(new Error(data.error));
|
||||
}
|
||||
};
|
||||
worker.addEventListener("statechange", changed);
|
||||
worker.postMessage({ type }, [channel.port2]);
|
||||
changed();
|
||||
});
|
||||
}
|
||||
|
||||
async function refreshApplication() {
|
||||
if (document.body.hasAttribute("aria-busy")) {
|
||||
return;
|
||||
}
|
||||
if ((state.ofdBytes || state.userFonts.length) && !window.confirm("刷新后需重新打开文件和添加字体,是否继续?")) {
|
||||
return;
|
||||
}
|
||||
setBusy(true, "正在刷新应用", null, "正在刷新应用");
|
||||
el.refreshAppButton.disabled = true;
|
||||
const panels = document.querySelectorAll(".toolbar, .workspace");
|
||||
panels.forEach((panel) => { panel.inert = true; });
|
||||
try {
|
||||
if (navigator.storage?.persist) {
|
||||
await navigator.storage.persist().catch(() => false);
|
||||
}
|
||||
const scope = new URL("./", location.href).href;
|
||||
await navigator.locks.request(`ofdgo:${scope}:refresh`, { ifAvailable: true }, async (lock) => {
|
||||
if (!lock) {
|
||||
throw new Error("应用正在刷新");
|
||||
}
|
||||
const registration = await registerOffline();
|
||||
await registration.update();
|
||||
if (registration.installing) {
|
||||
await waitForWorker(registration.installing, "installed");
|
||||
}
|
||||
const worker = registration.waiting || registration.active;
|
||||
await requestOffline(worker, "refresh");
|
||||
await waitForWorker(worker, "activated");
|
||||
location.reload();
|
||||
});
|
||||
} catch (err) {
|
||||
setStatus(err.message === "应用正在刷新" ? err.message : "应用刷新失败");
|
||||
} finally {
|
||||
panels.forEach((panel) => { panel.inert = false; });
|
||||
el.refreshAppButton.disabled = false;
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
function loadExportFormats() {
|
||||
const formats = callWASM("ofdgoExportFormats") || [];
|
||||
state.exportFormats = formats;
|
||||
@@ -532,7 +654,7 @@ async function loadDocumentLocalFonts(available, openSeq = state.openSeq) {
|
||||
renderFontList();
|
||||
return false;
|
||||
}
|
||||
if (docFonts.length && !docNames.length) {
|
||||
if (!docNames.length) {
|
||||
state.localFonts = [];
|
||||
setStatus("OFD 字体均为内嵌");
|
||||
updateFontSummary();
|
||||
@@ -540,7 +662,7 @@ async function loadDocumentLocalFonts(available, openSeq = state.openSeq) {
|
||||
return false;
|
||||
}
|
||||
const docWanted = fontNameKeys(docNames);
|
||||
const docLoadLimit = docNames.length ? Math.min(LOCAL_FONT_LOAD_LIMIT, Math.max(4, docNames.length * 3)) : 6;
|
||||
const docLoadLimit = Math.min(LOCAL_FONT_LOAD_LIMIT, Math.max(4, docNames.length * 3));
|
||||
let selected = selectLocalFonts(available, docWanted, docLoadLimit);
|
||||
if (!selected.length) {
|
||||
const fallbackWanted = fontNameKeys(COMMON_FONT_NAMES);
|
||||
@@ -549,7 +671,7 @@ async function loadDocumentLocalFonts(available, openSeq = state.openSeq) {
|
||||
const emptyStatus = available.length === 0 ? "未读取到系统字体" : "未匹配到文档所需字体";
|
||||
const fonts = [];
|
||||
for (let i = 0; i < selected.length; i += 1) {
|
||||
setProgress(`正在读取字体 ${i + 1}/${selected.length}`, 20 + Math.round(i / Math.max(1, selected.length) * 60));
|
||||
setProgress(`正在读取字体 ${i + 1}/${selected.length}`, 20 + Math.round(i / selected.length * 60));
|
||||
const item = selected[i];
|
||||
const blob = await item.blob();
|
||||
if (openSeq !== state.openSeq) {
|
||||
@@ -563,7 +685,7 @@ async function loadDocumentLocalFonts(available, openSeq = state.openSeq) {
|
||||
if (openSeq !== state.openSeq) {
|
||||
return false;
|
||||
}
|
||||
state.localFonts = mergeFontRecords([], fonts);
|
||||
state.localFonts = fonts;
|
||||
setStatus(fonts.length ? `已加载 ${fonts.length} 个系统字体` : emptyStatus);
|
||||
updateFontSummary();
|
||||
renderFontList();
|
||||
@@ -624,8 +746,9 @@ function fontData(fonts) {
|
||||
}
|
||||
|
||||
function updateFontSummary() {
|
||||
const total = fontRecords().length;
|
||||
const enabled = fontRecords().filter((font) => font.enabled).length;
|
||||
const fonts = fontRecords();
|
||||
const total = fonts.length;
|
||||
const enabled = fonts.filter((font) => font.enabled).length;
|
||||
el.availableFontSummary.textContent = total ? `${enabled}/${total}` : "0";
|
||||
}
|
||||
|
||||
@@ -640,20 +763,6 @@ function createFontRecord(name, data, source) {
|
||||
};
|
||||
}
|
||||
|
||||
function mergeFontRecords(oldFonts, newFonts) {
|
||||
const seen = new Set(oldFonts.map((font) => normalizeFontName(font.name)));
|
||||
const merged = [...oldFonts];
|
||||
for (const font of newFonts) {
|
||||
const key = normalizeFontName(font.name);
|
||||
if (!key || seen.has(key)) {
|
||||
continue;
|
||||
}
|
||||
seen.add(key);
|
||||
merged.push(font);
|
||||
}
|
||||
return merged;
|
||||
}
|
||||
|
||||
function fontRecords() {
|
||||
return [...state.localFonts, ...state.userFonts];
|
||||
}
|
||||
@@ -2347,10 +2456,7 @@ function setBusy(busy, text = "", percent = 0, status = "") {
|
||||
return;
|
||||
}
|
||||
el.progressPanel.hidden = false;
|
||||
if (status) {
|
||||
setStatus(status);
|
||||
}
|
||||
setProgress(text, percent);
|
||||
setProgress(text, percent, status);
|
||||
}
|
||||
|
||||
function setProgress(text = "", percent = 0, status = "") {
|
||||
@@ -2360,6 +2466,10 @@ function setProgress(text = "", percent = 0, status = "") {
|
||||
if (status) {
|
||||
setStatus(status);
|
||||
}
|
||||
el.progressBar.parentElement.hidden = percent === null;
|
||||
if (percent === null) {
|
||||
return;
|
||||
}
|
||||
const value = Math.max(0, Math.min(100, percent));
|
||||
el.progressBar.style.width = `${value}%`;
|
||||
el.progressBar.setAttribute("aria-valuenow", String(value));
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
const CACHE_PREFIX = `ofdgo:${self.registration.scope}:app:`;
|
||||
const META_CACHE = `${CACHE_PREFIX}meta`;
|
||||
const ASSETS = ["./", "ofdgo.css", "ofdgo.js", "wasm_exec.js", "ofdgo.wasm", "ofdgo.sw.js"]
|
||||
.map((path) => new URL(path, self.registration.scope).href);
|
||||
const CLIENT_PREFIX = new URL("__client/", self.registration.scope).href;
|
||||
|
||||
self.addEventListener("install", (event) => {
|
||||
event.waitUntil(caches.open(META_CACHE));
|
||||
});
|
||||
|
||||
self.addEventListener("fetch", (event) => {
|
||||
const url = resourceURL(event.request.url);
|
||||
const navigation = event.request.mode === "navigate";
|
||||
if (event.request.method !== "GET" || !url.startsWith(self.registration.scope) || (navigation && url !== self.registration.scope)) {
|
||||
return;
|
||||
}
|
||||
event.respondWith(navigator.locks.request(CACHE_PREFIX, { mode: navigation ? "exclusive" : "shared" }, () => readResource(event, navigation)));
|
||||
});
|
||||
|
||||
self.addEventListener("message", (event) => {
|
||||
const type = event.data?.type;
|
||||
const port = event.ports[0];
|
||||
if (!port || !event.source || (type !== "prepare" && type !== "refresh")) {
|
||||
return;
|
||||
}
|
||||
event.waitUntil((async () => {
|
||||
try {
|
||||
const reload = await navigator.locks.request(CACHE_PREFIX, async () => {
|
||||
const meta = await caches.open(META_CACHE);
|
||||
await cleanBundles(meta);
|
||||
if (type === "refresh") {
|
||||
await updateBundle(meta);
|
||||
return true;
|
||||
}
|
||||
const bundle = await readBundle(meta, CLIENT_PREFIX + event.source.id);
|
||||
if (await isBundleComplete(bundle)) {
|
||||
return false;
|
||||
}
|
||||
await currentBundle(meta);
|
||||
return true;
|
||||
});
|
||||
if (type === "refresh") {
|
||||
await self.skipWaiting();
|
||||
}
|
||||
port.postMessage({ ok: true, reload });
|
||||
} catch (err) {
|
||||
port.postMessage({ error: err.message });
|
||||
} finally {
|
||||
port.close();
|
||||
}
|
||||
})());
|
||||
});
|
||||
|
||||
function resourceURL(value) {
|
||||
const url = new URL(value);
|
||||
url.search = "";
|
||||
url.hash = "";
|
||||
if (url.href === new URL("index.html", self.registration.scope).href) {
|
||||
return self.registration.scope;
|
||||
}
|
||||
return url.href;
|
||||
}
|
||||
|
||||
async function readBundle(meta, key) {
|
||||
const response = await meta.match(key);
|
||||
return response ? response.json() : null;
|
||||
}
|
||||
|
||||
async function isBundleComplete(bundle) {
|
||||
if (!bundle || !await caches.has(bundle.name)) {
|
||||
return false;
|
||||
}
|
||||
const cache = await caches.open(bundle.name);
|
||||
const keys = await cache.keys();
|
||||
return bundle.assets.every((url) => keys.some((key) => key.url === url));
|
||||
}
|
||||
|
||||
async function currentBundle(meta) {
|
||||
const bundle = await readBundle(meta, self.registration.scope);
|
||||
return await isBundleComplete(bundle) ? bundle : updateBundle(meta);
|
||||
}
|
||||
|
||||
async function readResource(event, navigation) {
|
||||
const meta = await caches.open(META_CACHE);
|
||||
let bundle;
|
||||
if (navigation) {
|
||||
bundle = await currentBundle(meta);
|
||||
await meta.put(CLIENT_PREFIX + event.resultingClientId, Response.json(bundle));
|
||||
} else {
|
||||
bundle = await readBundle(meta, CLIENT_PREFIX + event.clientId);
|
||||
}
|
||||
if (!bundle || !await caches.has(bundle.name)) {
|
||||
return Response.error();
|
||||
}
|
||||
const url = resourceURL(event.request.url);
|
||||
if (!bundle.assets.includes(url)) {
|
||||
return fetch(event.request);
|
||||
}
|
||||
const cache = await caches.open(bundle.name);
|
||||
return await cache.match(url) || Response.error();
|
||||
}
|
||||
|
||||
async function updateBundle(meta) {
|
||||
const resources = await Promise.all(ASSETS.map(async (url) => {
|
||||
const response = await fetch(new Request(url, { cache: "no-store" }));
|
||||
if (!response.ok || response.redirected) {
|
||||
throw new Error("应用资源下载失败");
|
||||
}
|
||||
return { url, response };
|
||||
}));
|
||||
if (resources.some(({ response }) => response.headers.get("X-OFDGo-Checksum") !== BUNDLE_CHECKSUM)) {
|
||||
throw new Error("应用资源版本不一致");
|
||||
}
|
||||
const bundle = { name: CACHE_PREFIX + crypto.randomUUID(), assets: ASSETS };
|
||||
try {
|
||||
const cache = await caches.open(bundle.name);
|
||||
for (const { url, response } of resources) {
|
||||
await cache.put(url, response);
|
||||
}
|
||||
await meta.put(self.registration.scope, Response.json(bundle));
|
||||
} catch (err) {
|
||||
await caches.delete(bundle.name);
|
||||
throw err;
|
||||
}
|
||||
return bundle;
|
||||
}
|
||||
|
||||
async function cleanBundles(meta) {
|
||||
const current = await readBundle(meta, self.registration.scope);
|
||||
const keep = new Set([META_CACHE, current?.name]);
|
||||
for (const key of await meta.keys()) {
|
||||
if (!key.url.startsWith(CLIENT_PREFIX)) {
|
||||
continue;
|
||||
}
|
||||
const client = await self.clients.get(key.url.slice(CLIENT_PREFIX.length));
|
||||
if (client) {
|
||||
keep.add((await readBundle(meta, key)).name);
|
||||
} else {
|
||||
await meta.delete(key);
|
||||
}
|
||||
}
|
||||
for (const name of await caches.keys()) {
|
||||
if (name.startsWith(CACHE_PREFIX) && !keep.has(name)) {
|
||||
await caches.delete(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
+13
-5
@@ -17,12 +17,14 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"flag"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
webuiassets "github.com/xiaoqidun/ofdgo/assets/webui"
|
||||
"github.com/xiaoqidun/ofdgo/assets/webui"
|
||||
)
|
||||
|
||||
func main() {
|
||||
@@ -36,10 +38,16 @@ func main() {
|
||||
// serveWebUI 创建WebUI静态文件处理器
|
||||
// 返回: http.Handler HTTP处理器
|
||||
func serveWebUI() http.Handler {
|
||||
files := http.FileServer(http.FS(webuiassets.FS))
|
||||
files := http.FileServerFS(webuiassets.FS)
|
||||
checksum := webuiassets.Checksum()
|
||||
serviceWorker, _ := webuiassets.FS.ReadFile("ofdgo.sw.js")
|
||||
serviceWorker = append([]byte(fmt.Sprintf("const BUNDLE_CHECKSUM = %q;\n", checksum)), serviceWorker...)
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if strings.HasSuffix(r.URL.Path, ".wasm") {
|
||||
w.Header().Set("Content-Type", "application/wasm")
|
||||
w.Header().Set("Cache-Control", "no-store")
|
||||
w.Header().Set("X-OFDGo-Checksum", checksum)
|
||||
if r.URL.Path == "/ofdgo.sw.js" {
|
||||
http.ServeContent(w, r, "ofdgo.sw.js", time.Time{}, bytes.NewReader(serviceWorker))
|
||||
return
|
||||
}
|
||||
files.ServeHTTP(w, r)
|
||||
})
|
||||
|
||||
@@ -27,9 +27,6 @@ import (
|
||||
// currentSession 当前WebUI文档会话
|
||||
var currentSession *Session
|
||||
|
||||
// callbacks 浏览器回调函数引用
|
||||
var callbacks []js.Func
|
||||
|
||||
// apiResult 浏览器接口返回结果
|
||||
type apiResult struct {
|
||||
OK bool `json:"ok"`
|
||||
@@ -64,7 +61,6 @@ func registerCallback(name string, fn func([]js.Value) (any, error)) {
|
||||
return encodeResult(apiResult{OK: true, Data: data})
|
||||
})
|
||||
js.Global().Set(name, cb)
|
||||
callbacks = append(callbacks, cb)
|
||||
}
|
||||
|
||||
// safeCall 调用浏览器回调并转换异常
|
||||
|
||||
Reference in new issue
Block a user