feat(前端界面): 精简字体管理代码,优化打开图标
build.yaml / build (push) Successful in 38s

This commit is contained in:
xiaoqidun committed 2026-09-08 20:55:47 +08:00
1 parent c3bf0b3a9a
commit 3a0bd802d3
3 files changed
+261 -28

No files matched your search

+1 -1
View File
@@ -13,7 +13,7 @@
<input id="fontInput" class="file-input" type="file" accept=".ttf,.otf,.ttc,font/*" multiple> <input id="fontInput" class="file-input" type="file" accept=".ttf,.otf,.ttc,font/*" multiple>
<input id="fontDirectoryInput" class="file-input" type="file" webkitdirectory multiple> <input id="fontDirectoryInput" class="file-input" type="file" webkitdirectory multiple>
<div class="tool-group"> <div class="tool-group">
<button id="ofdButton" class="icon-button" type="button" title="打开" aria-label="打开">&#128449;</button> <button id="ofdButton" class="icon-button" type="button" title="打开" aria-label="打开"><span class="open-icon" aria-hidden="true"></span></button>
<button id="togglePagesButton" class="icon-button" type="button" title="页面栏" aria-label="页面栏" aria-pressed="true">&#9776;</button> <button id="togglePagesButton" class="icon-button" type="button" title="页面栏" aria-label="页面栏" aria-pressed="true">&#9776;</button>
<button id="toggleMetaButton" class="icon-button" type="button" title="信息栏" aria-label="信息栏" aria-pressed="true">&#9432;</button> <button id="toggleMetaButton" class="icon-button" type="button" title="信息栏" aria-label="信息栏" aria-pressed="true">&#9432;</button>
</div> </div>
+24
View File
@@ -149,6 +149,30 @@ button:not(:disabled):hover {
font-size: 14px; font-size: 14px;
} }
.open-icon {
position: relative;
width: 14px;
height: 12px;
}
.open-icon::before {
position: absolute;
top: 0;
left: 0;
width: 6px;
height: 4px;
border: 1px solid currentColor;
border-bottom: 0;
content: "";
}
.open-icon::after {
position: absolute;
inset: 3px 0 0;
border: 1px solid currentColor;
content: "";
}
.toolbar-space { .toolbar-space {
flex: 1 1 auto; flex: 1 1 auto;
} }
+236 -27
View File
@@ -17,6 +17,8 @@ const COMMON_FONT_NAMES = [
"Times New Roman", "Times New Roman",
]; ];
const LOCAL_FONT_LOAD_LIMIT = 16; const LOCAL_FONT_LOAD_LIMIT = 16;
const FONT_DATABASE = "ofdgo-fonts";
const fontChannel = typeof BroadcastChannel === "function" ? new BroadcastChannel(FONT_DATABASE) : null;
const COMPACT_LAYOUT = window.matchMedia("(max-width: 900px)"); const COMPACT_LAYOUT = window.matchMedia("(max-width: 900px)");
const DEFAULT_IMAGE_DPI = 300; const DEFAULT_IMAGE_DPI = 300;
const STATUS = { const STATUS = {
@@ -42,6 +44,7 @@ const WASM_CALLBACKS = [
let wasmPromise = null; let wasmPromise = null;
let wasmModule = null; let wasmModule = null;
let wasmRecoveryTimer = 0; let wasmRecoveryTimer = 0;
let fontDatabase = null;
const state = { const state = {
ready: false, ready: false,
@@ -55,6 +58,8 @@ const state = {
fontSeq: 0, fontSeq: 0,
localFonts: [], localFonts: [],
userFonts: [], userFonts: [],
fontSyncPending: false,
fontSyncing: false,
systemFontCatalog: [], systemFontCatalog: [],
systemFontCatalogLoaded: false, systemFontCatalogLoaded: false,
systemFontPermission: "prompt", systemFontPermission: "prompt",
@@ -168,6 +173,19 @@ el.viewerPanel.addEventListener("scroll", () => {
schedulePageSync(); schedulePageSync();
}); });
el.viewerPanel.addEventListener("dblclick", openOFDFromViewer); el.viewerPanel.addEventListener("dblclick", openOFDFromViewer);
if (fontChannel) {
fontChannel.onmessage = scheduleFontSync;
}
el.fontList.addEventListener("focusout", () => {
if (state.fontSyncPending) {
window.setTimeout(syncUserFonts, 0);
}
});
document.addEventListener("visibilitychange", () => {
if (!document.hidden) {
scheduleFontSync();
}
});
el.fontDirectoryButton.disabled = !("webkitdirectory" in el.fontDirectoryInput); el.fontDirectoryButton.disabled = !("webkitdirectory" in el.fontDirectoryInput);
updateSidebarState(); updateSidebarState();
@@ -190,6 +208,9 @@ async function openOFDFromViewer() {
} }
function openFontFile(input) { function openFontFile(input) {
if (document.body.hasAttribute("aria-busy")) {
return;
}
input.value = ""; input.value = "";
input.click(); input.click();
} }
@@ -237,6 +258,12 @@ async function boot() {
} }
setProgress("正在准备引擎", 8, STATUS.engine); setProgress("正在准备引擎", 8, STATUS.engine);
await ensureWASM(); await ensureWASM();
let fontsRestored = true;
try {
await restoreUserFonts();
} catch {
fontsRestored = false;
}
setProgress("引擎准备完成", 70); setProgress("引擎准备完成", 70);
loadExportFormats(); loadExportFormats();
setEmpty("选择 OFD 文件"); setEmpty("选择 OFD 文件");
@@ -245,7 +272,7 @@ async function boot() {
updateControls(); updateControls();
updateLocalFontButton(); updateLocalFontButton();
await refreshLocalFontPermission(); await refreshLocalFontPermission();
setStatus(STATUS.ready); setStatus(fontsRestored ? STATUS.ready : "字体读取失败");
setBusy(false); setBusy(false);
} catch (err) { } catch (err) {
setStatus("渲染引擎加载失败"); setStatus("渲染引擎加载失败");
@@ -334,7 +361,9 @@ async function refreshApplication() {
if (document.body.hasAttribute("aria-busy")) { if (document.body.hasAttribute("aria-busy")) {
return; return;
} }
if ((state.ofdBytes || state.userFonts.length) && !window.confirm("刷新后需重新打开文件和添加字体,是否继续?")) { const temporaryFonts = state.userFonts.some((font) => font.source === "upload");
const message = temporaryFonts ? "刷新后需重新打开文件和添加未保存字体,是否继续?" : "刷新后需重新打开文件,是否继续?";
if ((state.ofdBytes || temporaryFonts) && !window.confirm(message)) {
return; return;
} }
setBusy(true, "正在刷新应用", null, "正在刷新应用"); setBusy(true, "正在刷新应用", null, "正在刷新应用");
@@ -530,14 +559,103 @@ function isOFDFile(file) {
return /\.ofd$/i.test(file.name || ""); return /\.ofd$/i.test(file.name || "");
} }
function openFontDatabase() {
if (!fontDatabase) {
fontDatabase = new Promise((resolve, reject) => {
const request = indexedDB.open(FONT_DATABASE, 1);
request.onupgradeneeded = () => {
const store = request.result.createObjectStore("fonts", { keyPath: "id", autoIncrement: true });
store.createIndex("checksum", "checksum", { unique: true });
};
request.onsuccess = () => {
const db = request.result;
db.onversionchange = () => {
db.close();
fontDatabase = null;
};
resolve(db);
};
request.onerror = () => reject(request.error);
}).catch((err) => {
fontDatabase = null;
throw err;
});
}
return fontDatabase;
}
async function fontTransaction(mode, action) {
const db = await openFontDatabase();
return new Promise((resolve, reject) => {
const tx = db.transaction("fonts", mode);
let request;
tx.oncomplete = () => {
if (mode === "readwrite") {
fontChannel?.postMessage(null);
}
resolve(request?.result);
};
tx.onabort = () => reject(tx.error || new Error("字体保存失败"));
try {
request = action(tx.objectStore("fonts"));
} catch (err) {
tx.abort();
reject(err);
}
});
}
function listStoredFonts() {
return fontTransaction("readonly", (store) => store.getAll());
}
async function addStoredFonts(fonts) {
const records = new Map();
for (const font of fonts) {
const digest = await crypto.subtle.digest("SHA-256", font.data);
const checksum = Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("");
if (!records.has(checksum)) {
records.set(checksum, { checksum, name: font.name, enabled: font.enabled, data: new Blob([font.data]) });
}
}
await fontTransaction("readwrite", (store) => {
for (const font of records.values()) {
const request = store.index("checksum").getKey(font.checksum);
request.onsuccess = () => {
if (request.result === undefined) {
store.add(font);
}
};
}
});
}
function updateStoredFont(id, changes) {
return fontTransaction("readwrite", (store) => {
const request = store.get(id);
request.onsuccess = () => {
if (request.result) {
store.put({ ...request.result, ...changes });
}
};
});
}
function deleteStoredFont(id) {
return fontTransaction("readwrite", (store) => store.delete(id));
}
async function openSelectedFonts(event) { async function openSelectedFonts(event) {
if (document.body.hasAttribute("aria-busy")) {
return;
}
const input = event.currentTarget; const input = event.currentTarget;
const files = Array.from(input.files || []).filter((file) => ( const files = Array.from(input.files || []).filter((file) => (
!input.webkitdirectory || /\.(ttf|otf|ttc)$/i.test(file.name) !input.webkitdirectory || /\.(ttf|otf|ttc)$/i.test(file.name)
)); ));
input.value = ""; input.value = "";
if (!files.length) { if (!files.length) {
setStatus("未发现字体文件"); setStatus("暂无字体文件");
return; return;
} }
setBusy(true, "正在读取字体", 10, STATUS.fonts); setBusy(true, "正在读取字体", 10, STATUS.fonts);
@@ -548,11 +666,21 @@ async function openSelectedFonts(event) {
const file = files[i]; const file = files[i];
fonts.push(createFontRecord(file.name, new Uint8Array(await file.arrayBuffer()), "upload")); fonts.push(createFontRecord(file.name, new Uint8Array(await file.arrayBuffer()), "upload"));
} }
state.userFonts.push(...fonts); let saved = true;
await applyFontChange(); try {
if (!state.doc) { await addStoredFonts(fonts);
setStatus(`已添加 ${fonts.length} 个字体`); } catch {
saved = false;
state.userFonts.push(...fonts);
} }
const changed = saved ? await restoreUserFonts() : true;
if (saved && navigator.storage?.persist) {
navigator.storage.persist().catch(() => false);
}
if (changed) {
await applyFontChange();
}
setStatus(saved ? "字体保存完成" : "字体仅限本次");
} catch (err) { } catch (err) {
showError(err, false); showError(err, false);
} finally { } finally {
@@ -561,6 +689,9 @@ async function openSelectedFonts(event) {
} }
async function loadLocalFonts() { async function loadLocalFonts() {
if (document.body.hasAttribute("aria-busy")) {
return;
}
if (!canReadLocalFonts()) { if (!canReadLocalFonts()) {
setStatus("当前浏览器不支持读取系统字体"); setStatus("当前浏览器不支持读取系统字体");
return; return;
@@ -728,21 +859,64 @@ function selectLocalFonts(fonts, wanted, limit) {
return uniqueLocalFonts(ranked.map((item) => item.font)).slice(0, limit); return uniqueLocalFonts(ranked.map((item) => item.font)).slice(0, limit);
} }
function allFonts() { async function fontData(fonts) {
return fontData(fontRecords()); const result = [];
for (const font of fonts) {
if (!font.enabled) {
continue;
}
if (font.data instanceof Blob) {
font.data = new Uint8Array(await font.data.arrayBuffer());
}
result.push({ name: font.name, data: font.data });
}
return result;
} }
function uploadedFonts() { async function restoreUserFonts() {
return fontData(state.userFonts); const stored = await listStoredFonts();
const previous = new Map(state.userFonts.map((font) => [font.id, font]));
const fonts = stored.map((font) => ({
...font,
data: previous.get(font.id)?.checksum === font.checksum ? previous.get(font.id).data : font.data,
source: "stored",
}));
fonts.push(...state.userFonts.filter((font) => font.source === "upload"));
if (fonts.length === state.userFonts.length && fonts.every((font, index) => {
const old = state.userFonts[index];
return font.id === old.id && font.checksum === old.checksum && font.name === old.name && font.enabled === old.enabled;
})) {
return false;
}
state.userFonts = fonts;
return true;
} }
function fontData(fonts) { function scheduleFontSync() {
return fonts state.fontSyncPending = true;
.filter((font) => font.enabled) syncUserFonts();
.map((font) => ({ }
name: font.name,
data: font.data, async function syncUserFonts() {
})); if (!state.ready || !state.fontSyncPending || state.fontSyncing || document.hidden || document.body.hasAttribute("aria-busy") || document.activeElement?.classList.contains("font-name-input")) {
return;
}
state.fontSyncPending = false;
state.fontSyncing = true;
setBusy(true, "正在同步字体", null);
try {
if (await restoreUserFonts()) {
await applyFontChange();
}
if (!state.doc) {
setStatus(STATUS.ready);
}
} catch {
setStatus("字体读取失败");
} finally {
state.fontSyncing = false;
setBusy(false);
}
} }
function updateFontSummary() { function updateFontSummary() {
@@ -865,6 +1039,7 @@ async function applyFontChange(options = {}) {
await openDocument({ await openDocument({
pageIndex: state.pageIndex, pageIndex: state.pageIndex,
fitMode: state.fitMode, fitMode: state.fitMode,
scale: state.scale,
skipAutoFonts: options.skipAutoFonts !== false, skipAutoFonts: options.skipAutoFonts !== false,
reuseSession: true, reuseSession: true,
fontsChanged: true, fontsChanged: true,
@@ -890,6 +1065,36 @@ function removeFont(id) {
state.userFonts = state.userFonts.filter((font) => font.id !== id); state.userFonts = state.userFonts.filter((font) => font.id !== id);
} }
async function changeFont(font, changes) {
if (document.body.hasAttribute("aria-busy")) {
return;
}
setBusy(true, "正在更新字体", null, STATUS.fonts);
try {
if (font.source === "stored") {
if (changes) {
await updateStoredFont(font.id, changes);
} else {
await deleteStoredFont(font.id);
}
await restoreUserFonts();
} else if (changes) {
Object.assign(font, changes);
} else {
removeFont(font.id);
}
await applyFontChange();
if (!state.doc) {
setStatus(STATUS.ready);
}
} catch {
renderFontList();
setStatus("字体更新失败");
} finally {
setBusy(false);
}
}
function renderFontList() { function renderFontList() {
el.fontList.replaceChildren(); el.fontList.replaceChildren();
const fonts = fontRecords(); const fonts = fontRecords();
@@ -918,9 +1123,7 @@ function renderFontList() {
const enabledText = document.createElement("span"); const enabledText = document.createElement("span");
enabledText.textContent = font.enabled ? "启用" : "停用"; enabledText.textContent = font.enabled ? "启用" : "停用";
enabled.addEventListener("change", async () => { enabled.addEventListener("change", async () => {
font.enabled = enabled.checked; await changeFont(font, { enabled: enabled.checked });
enabledText.textContent = font.enabled ? "启用" : "停用";
await applyFontChange();
}); });
const name = document.createElement("input"); const name = document.createElement("input");
@@ -933,8 +1136,7 @@ function renderFontList() {
name.value = font.name; name.value = font.name;
return; return;
} }
font.name = next; await changeFont(font, { name: next });
await applyFontChange();
}); });
const main = document.createElement("div"); const main = document.createElement("div");
@@ -951,8 +1153,7 @@ function renderFontList() {
remove.title = "删除字体"; remove.title = "删除字体";
remove.textContent = "×"; remove.textContent = "×";
remove.addEventListener("click", async () => { remove.addEventListener("click", async () => {
removeFont(font.id); await changeFont(font);
await applyFontChange();
}); });
const actions = document.createElement("div"); const actions = document.createElement("div");
@@ -1028,9 +1229,13 @@ async function openDocument(options = {}) {
if (openSeq !== state.openSeq) { if (openSeq !== state.openSeq) {
return; return;
} }
const fonts = resetLocalFonts ? uploadedFonts() : allFonts(); const fonts = !options.reuseSession || options.fontsChanged
? await fontData(resetLocalFonts ? state.userFonts : fontRecords()) : null;
if (openSeq !== state.openSeq) {
return;
}
const doc = options.reuseSession const doc = options.reuseSession
? callWASM("ofdgoConfigure", options.fontsChanged ? fonts : null, state.renderAnnotations) ? callWASM("ofdgoConfigure", fonts, state.renderAnnotations)
: callWASM("ofdgoOpen", state.ofdBytes, fonts, state.renderAnnotations); : callWASM("ofdgoOpen", state.ofdBytes, fonts, state.renderAnnotations);
if (openSeq !== state.openSeq) { if (openSeq !== state.openSeq) {
return; return;
@@ -2451,8 +2656,12 @@ function showError(err, empty = !state.doc) {
function setBusy(busy, text = "", percent = 0, status = "") { function setBusy(busy, text = "", percent = 0, status = "") {
document.body.toggleAttribute("aria-busy", busy); document.body.toggleAttribute("aria-busy", busy);
el.fontList.inert = busy;
if (!busy) { if (!busy) {
el.progressPanel.hidden = true; el.progressPanel.hidden = true;
if (state.fontSyncPending) {
window.setTimeout(syncUserFonts, 0);
}
return; return;
} }
el.progressPanel.hidden = false; el.progressPanel.hidden = false;