Add bundled bus fonts and shared font favorites

This commit is contained in:
2026-10-12 15:00:40 +08:00
parent c9dd03c23b
commit 4be74a34f9
181 changed files with 2078575 additions and 76 deletions
@@ -1,3 +1,5 @@
import { renderUiCopy, unregisterUiCopySubtree } from "./ui-copy-runtime.js";
const SOURCE_ORDER = Object.freeze(["automatic", "imported", "system"]);
const SOURCE_LABELS = Object.freeze({
automatic: "自动字体",
@@ -33,23 +35,29 @@ export function filterFontItems(items, query = "") {
));
}
export function groupFontItems(items) {
return SOURCE_ORDER
export function groupFontItems(items, favoriteIds = []) {
const favorites = new Set(favoriteIds);
return [{ source: "favorites", label: "收藏字体", items: items.filter(item => favorites.has(item.id)) }, ...SOURCE_ORDER
.map((source) => ({
source,
label: SOURCE_LABELS[source],
items: items.filter((item) => item.source === source),
}))
.filter((group) => group.items.length);
.filter((group) => group.items.length)];
}
export function createFontPicker({ input, listbox, onCommit }) {
export function createFontPicker({ input, listbox, onCommit, onFavorite, onOpen }) {
let items = [];
let selectedId = "default";
let visibleItems = [];
let activeIndex = -1;
let enabled = false;
let closeTimer = null;
let favoriteIds = [];
let queryText = "";
let favoriteBusy = false;
let dialogOpen = false;
let restoringFocus = false;
function selectedItem() {
return items.find((item) => item.id === selectedId) || null;
@@ -101,17 +109,31 @@ export function createFontPicker({ input, listbox, onCommit }) {
}
function render(query = "") {
visibleItems = filterFontItems(items, query);
const groups = groupFontItems(visibleItems);
const bounds = input.getBoundingClientRect();
listbox.style.width = `${Math.max(bounds.width, Math.min(440, window.innerWidth - bounds.left - 16))}px`;
queryText = query;
const filtered = filterFontItems(items, query);
const groups = groupFontItems(filtered, favoriteIds);
visibleItems = groups.flatMap(group => group.items);
const nodes = [];
let optionIndex = 0;
for (const group of groups) {
const heading = document.createElement("div");
heading.className = "font-option-group";
heading.textContent = group.label;
if (group.source === "favorites") renderUiCopy("copy.dynamic.font.favorites", heading, "收藏字体");
else heading.textContent = group.label;
heading.setAttribute("role", "presentation");
nodes.push(heading);
if (group.source === "favorites" && !group.items.length) {
const empty = document.createElement("div");
empty.className = "font-options-empty";
renderUiCopy(query.trim() ? "copy.dynamic.font.no_matches" : "copy.dynamic.font.empty", empty, query.trim() ? "没有匹配的收藏字体" : "暂无收藏字体");
nodes.push(empty);
}
for (const item of group.items) {
const row = document.createElement("div");
row.className = "font-option-row";
row.setAttribute("role", "presentation");
const option = document.createElement("div");
option.id = optionId(optionIndex);
option.className = "font-option";
@@ -124,34 +146,96 @@ export function createFontPicker({ input, listbox, onCommit }) {
const source = document.createElement("small");
source.textContent = item.style;
source.setAttribute("aria-hidden", "true");
option.append(label, source);
const star = document.createElement("button");
star.type = "button";
star.className = "font-favorite-button";
const favorite = favoriteIds.includes(item.id);
star.textContent = favorite ? "★" : "☆";
star.setAttribute("aria-label", `${favorite ? "取消收藏" : "收藏"}字体“${item.label}”`);
star.setAttribute("aria-pressed", String(favorite));
star.disabled = favoriteBusy;
star.addEventListener("pointerdown", event => event.stopPropagation());
star.addEventListener("click", event => {
event.stopPropagation();
toggleFavorite(item, favorite);
});
option.append(label);
row.append(option, star, source);
option.addEventListener("pointerdown", (event) => event.preventDefault());
option.addEventListener("click", () => commit(item));
nodes.push(option);
row.addEventListener("pointerdown", event => { if (event.target !== star) event.preventDefault(); });
row.addEventListener("click", () => commit(item));
nodes.push(row);
optionIndex += 1;
}
}
if (!visibleItems.length) {
if (!filtered.length) {
const empty = document.createElement("div");
empty.className = "font-options-empty";
empty.textContent = "没有匹配的字体";
nodes.push(empty);
}
unregisterUiCopySubtree(listbox);
listbox.replaceChildren(...nodes);
activeIndex = visibleItems.findIndex((item) => item.id === selectedId);
if (activeIndex < 0 && visibleItems.length) activeIndex = 0;
setActive(activeIndex);
}
async function toggleFavorite(item, favorite) {
if (favoriteBusy || dialogOpen) return;
if (favorite) {
dialogOpen = true;
const dialog = document.createElement("dialog");
dialog.className = "font-favorite-dialog";
dialog.setAttribute("aria-label", "确认取消收藏");
const message = document.createElement("p");
renderUiCopy("copy.dynamic.font.confirm", message, "确认取消收藏字体“{name}/{style}”?", { name: item.family, style: item.style });
dialog.append(message);
for (const [value, title] of [["cancel", "取消"], ["confirm", "取消收藏"]]) {
const button = document.createElement("button");
button.type = "button";
renderUiCopy(value === "cancel" ? "copy.dynamic.font.cancel" : "copy.dynamic.font.remove", button, title);
button.addEventListener("click", () => dialog.close(value));
dialog.append(button);
}
document.body.append(dialog);
const result = new Promise(resolve => dialog.addEventListener("close", () => resolve(dialog.returnValue === "confirm"), { once: true }));
dialog.showModal();
const confirmed = await result;
unregisterUiCopySubtree(dialog);
dialog.remove();
dialogOpen = false;
if (!confirmed) { restoreInputFocus(); return; }
}
favoriteBusy = true;
render(queryText);
try {
const result = await onFavorite?.(item, !favorite);
if (Array.isArray(result)) favoriteIds = [...result];
} finally {
favoriteBusy = false;
render(queryText);
restoreInputFocus();
}
}
function restoreInputFocus() {
restoringFocus = true;
input.focus({ preventScroll: true });
restoringFocus = false;
}
function open(query = "") {
if (!enabled) return;
const wasClosed = listbox.hidden;
render(query);
listbox.hidden = false;
input.setAttribute("aria-expanded", "true");
if (wasClosed) onOpen?.();
}
input.addEventListener("focus", () => {
if (!enabled) return;
if (!enabled || restoringFocus) return;
input.select();
open();
});
@@ -179,7 +263,15 @@ export function createFontPicker({ input, listbox, onCommit }) {
}
});
input.addEventListener("blur", () => {
closeTimer = window.setTimeout(() => close(), 0);
closeTimer = window.setTimeout(() => {
if (!dialogOpen && !favoriteBusy && !listbox.contains(document.activeElement)) close();
}, 0);
});
listbox.addEventListener("focusout", () => {
closeTimer = window.setTimeout(() => {
if (!dialogOpen && !favoriteBusy && document.activeElement !== input && !listbox.contains(document.activeElement)) close();
}, 0);
});
restoreLabel();
@@ -187,7 +279,11 @@ export function createFontPicker({ input, listbox, onCommit }) {
setItems(nextItems) {
items = normalizeFontItems(nextItems);
restoreLabel();
if (!listbox.hidden) render();
if (!listbox.hidden) render(queryText);
},
setFavorites(ids) {
favoriteIds = Array.isArray(ids) ? [...ids] : [];
if (!listbox.hidden && !favoriteBusy && !dialogOpen) render(queryText);
},
setValue(identifier) {
selectedId = typeof identifier === "string" && identifier ? identifier : "default";
@@ -3158,3 +3158,17 @@ noscript {
#actual-screen-frame[hidden] { display:none; }
#actual-screen-frame span { position:absolute; top:2px; left:3px; font-size:11px; color:#FFCC66; background:#000b; padding:2px; white-space:nowrap; }
.board-expanded #actual-screen-frame { inset:33.333333%; }
.font-option-row { display:flex; align-items:center; gap:6px; border-radius:8px; padding-right:10px; }
.font-option-row .font-option { flex:1; min-width:0; }
.font-option-row small { flex:0 0 auto; max-width:30%; overflow:hidden; text-overflow:ellipsis; color:var(--muted); }
.font-option-row:has(.font-option.active), .font-option-row:hover { background:rgb(53 194 160 / 16%); }
.font-favorite-button { flex:0 0 44px; width:44px; min-height:44px; padding:0; background:transparent; border:0; font-size:24px; color:var(--muted); }
.font-favorite-button[aria-pressed="true"] { color:#ffd166; }
.font-favorite-button:focus-visible { outline:2px solid var(--accent); outline-offset:-2px; }
.font-favorite-dialog { background:var(--surface, #111923); color:var(--text); border:1px solid var(--muted); border-radius:12px; max-width:calc(100vw - 32px); }
.font-favorite-dialog p { overflow-wrap:anywhere; }
.font-favorite-dialog button { margin:4px; }
.font-favorite-dialog::backdrop { background:rgb(0 0 0 / 60%); }
.font-options { right:auto; box-sizing:border-box; }
@@ -234,46 +234,264 @@
"scope": "text",
"source": "views/text.js::font_catalog_failed"
},
"copy.dynamic.media.state_uploading": {"text": "上传中", "scope": "media-import", "source": "views/media-import.js::state_uploading"},
"copy.dynamic.media.state_analyzing": {"text": "分析中", "scope": "media-import", "source": "views/media-import.js::state_analyzing"},
"copy.dynamic.media.state_awaiting": {"text": "等待设置", "scope": "media-import", "source": "views/media-import.js::state_awaiting"},
"copy.dynamic.media.state_queued": {"text": "排队中", "scope": "media-import", "source": "views/media-import.js::state_queued"},
"copy.dynamic.media.state_converting": {"text": "转换中", "scope": "media-import", "source": "views/media-import.js::state_converting"},
"copy.dynamic.media.state_failed": {"text": "失败", "scope": "media-import", "source": "views/media-import.js::state_failed"},
"copy.dynamic.media.queue_position": {"text": "{state} · 队列第 {position} 位", "scope": "media-import", "source": "views/media-import.js::queue_position"},
"copy.dynamic.media.preview": {"text": "代表帧 {index}", "scope": "media-import", "source": "views/media-import.js::preview"},
"copy.dynamic.media.output_name": {"text": "保存名称", "scope": "media-import", "source": "views/media-import.js::output_name"},
"copy.dynamic.media.fit_mode": {"text": "1:1 适配", "scope": "media-import", "source": "views/media-import.js::fit_mode"},
"copy.dynamic.media.mode_crop": {"text": "自由取景", "scope": "media-import", "source": "views/media-import.js::mode_crop"},
"copy.dynamic.media.mode_contain": {"text": "完整包含", "scope": "media-import", "source": "views/media-import.js::mode_contain"},
"copy.dynamic.media.mode_stretch": {"text": "拉伸", "scope": "media-import", "source": "views/media-import.js::mode_stretch"},
"copy.dynamic.media.crop_zoom": {"text": "取景缩放", "scope": "media-import", "source": "views/media-import.js::crop_zoom"},
"copy.dynamic.media.transparency": {"text": "透明像素填充色", "scope": "media-import", "source": "views/media-import.js::transparency"},
"copy.dynamic.media.padding": {"text": "留白颜色", "scope": "media-import", "source": "views/media-import.js::padding"},
"copy.dynamic.media.convert": {"text": "保存设置并开始转换", "scope": "media-import", "source": "views/media-import.js::convert"},
"copy.dynamic.media.cancel_source": {"text": "取消并删除源文件", "scope": "media-import", "source": "views/media-import.js::cancel_source"},
"copy.dynamic.media.retry": {"text": "重试", "scope": "media-import", "source": "views/media-import.js::retry"},
"copy.dynamic.media.cancel": {"text": "取消并删除", "scope": "media-import", "source": "views/media-import.js::cancel"},
"copy.dynamic.media.deleted": {"text": "任务和源文件已删除。", "scope": "media-import", "source": "views/media-import.js::deleted"},
"copy.dynamic.media.delete_failed": {"text": "删除任务失败:{error}", "scope": "media-import", "source": "views/media-import.js::delete_failed"},
"copy.dynamic.media.queued": {"text": "任务已加入转换队列。", "scope": "media-import", "source": "views/media-import.js::queued"},
"copy.dynamic.media.name_conflict": {"text": "保存名称已存在,请更换名称。", "scope": "media-import", "source": "views/media-import.js::name_conflict"},
"copy.dynamic.media.convert_failed": {"text": "无法开始转换:{error}", "scope": "media-import", "source": "views/media-import.js::convert_failed"},
"copy.dynamic.media.retry_failed": {"text": "重试失败:{error}", "scope": "media-import", "source": "views/media-import.js::retry_failed"},
"copy.dynamic.media.completed": {"text": "“{name}”转换完成,已加入内容库。", "scope": "media-import", "source": "views/media-import.js::completed"},
"copy.dynamic.media.empty": {"text": "当前没有等待或转换中的任务。", "scope": "media-import", "source": "views/media-import.js::empty"},
"copy.dynamic.media.list_failed": {"text": "任务列表读取失败:{error}", "scope": "media-import", "source": "views/media-import.js::list_failed"},
"copy.dynamic.media.uploaded": {"text": "上传完成,设备正在分析媒体。", "scope": "media-import", "source": "views/media-import.js::uploaded"},
"copy.dynamic.media.upload_failed": {"text": "上传失败:{error}", "scope": "media-import", "source": "views/media-import.js::upload_failed"},
"copy.dynamic.current_display.loading": {"text": "正在读取当前画面…", "scope": "dialog:current-display-dialog", "source": "views/device.js::current_display_loading"},
"copy.dynamic.current_display.load_failed": {"text": "当前画面读取失败:{reason}", "scope": "dialog:current-display-dialog", "source": "views/device.js::current_display_load_failed"},
"copy.dynamic.current_display.pause": {"text": "暂停", "scope": "dialog:current-display-dialog", "source": "views/device.js::current_display_pause"},
"copy.dynamic.current_display.resume": {"text": "继续播放", "scope": "dialog:current-display-dialog", "source": "views/device.js::current_display_resume"},
"copy.dynamic.current_display.pausing": {"text": "正在暂停…", "scope": "dialog:current-display-dialog", "source": "views/device.js::current_display_pausing"},
"copy.dynamic.current_display.resuming": {"text": "正在继续播放…", "scope": "dialog:current-display-dialog", "source": "views/device.js::current_display_resuming"},
"copy.dynamic.current_display.paused": {"text": "动图已暂停", "scope": "dialog:current-display-dialog", "source": "views/device.js::current_display_paused"},
"copy.dynamic.current_display.resumed": {"text": "动图继续播放", "scope": "dialog:current-display-dialog", "source": "views/device.js::current_display_resumed"},
"copy.dynamic.current_display.speed_changing": {"text": "正在切换到 {speed}×…", "scope": "dialog:current-display-dialog", "source": "views/device.js::current_display_speed_changing"},
"copy.dynamic.current_display.speed_changed": {"text": "已切换到 {speed}×", "scope": "dialog:current-display-dialog", "source": "views/device.js::current_display_speed_changed"},
"copy.dynamic.current_display.control_failed": {"text": "动图播放控制失败:{reason}", "scope": "dialog:current-display-dialog", "source": "views/device.js::current_display_control_failed"}
"copy.dynamic.media.state_uploading": {
"text": "上传中",
"scope": "media-import",
"source": "views/media-import.js::state_uploading"
},
"copy.dynamic.media.state_analyzing": {
"text": "分析中",
"scope": "media-import",
"source": "views/media-import.js::state_analyzing"
},
"copy.dynamic.media.state_awaiting": {
"text": "等待设置",
"scope": "media-import",
"source": "views/media-import.js::state_awaiting"
},
"copy.dynamic.media.state_queued": {
"text": "排队中",
"scope": "media-import",
"source": "views/media-import.js::state_queued"
},
"copy.dynamic.media.state_converting": {
"text": "转换中",
"scope": "media-import",
"source": "views/media-import.js::state_converting"
},
"copy.dynamic.media.state_failed": {
"text": "失败",
"scope": "media-import",
"source": "views/media-import.js::state_failed"
},
"copy.dynamic.media.queue_position": {
"text": "{state} · 队列第 {position} 位",
"scope": "media-import",
"source": "views/media-import.js::queue_position"
},
"copy.dynamic.media.preview": {
"text": "代表帧 {index}",
"scope": "media-import",
"source": "views/media-import.js::preview"
},
"copy.dynamic.media.output_name": {
"text": "保存名称",
"scope": "media-import",
"source": "views/media-import.js::output_name"
},
"copy.dynamic.media.fit_mode": {
"text": "1:1 适配",
"scope": "media-import",
"source": "views/media-import.js::fit_mode"
},
"copy.dynamic.media.mode_crop": {
"text": "自由取景",
"scope": "media-import",
"source": "views/media-import.js::mode_crop"
},
"copy.dynamic.media.mode_contain": {
"text": "完整包含",
"scope": "media-import",
"source": "views/media-import.js::mode_contain"
},
"copy.dynamic.media.mode_stretch": {
"text": "拉伸",
"scope": "media-import",
"source": "views/media-import.js::mode_stretch"
},
"copy.dynamic.media.crop_zoom": {
"text": "取景缩放",
"scope": "media-import",
"source": "views/media-import.js::crop_zoom"
},
"copy.dynamic.media.transparency": {
"text": "透明像素填充色",
"scope": "media-import",
"source": "views/media-import.js::transparency"
},
"copy.dynamic.media.padding": {
"text": "留白颜色",
"scope": "media-import",
"source": "views/media-import.js::padding"
},
"copy.dynamic.media.convert": {
"text": "保存设置并开始转换",
"scope": "media-import",
"source": "views/media-import.js::convert"
},
"copy.dynamic.media.cancel_source": {
"text": "取消并删除源文件",
"scope": "media-import",
"source": "views/media-import.js::cancel_source"
},
"copy.dynamic.media.retry": {
"text": "重试",
"scope": "media-import",
"source": "views/media-import.js::retry"
},
"copy.dynamic.media.cancel": {
"text": "取消并删除",
"scope": "media-import",
"source": "views/media-import.js::cancel"
},
"copy.dynamic.media.deleted": {
"text": "任务和源文件已删除。",
"scope": "media-import",
"source": "views/media-import.js::deleted"
},
"copy.dynamic.media.delete_failed": {
"text": "删除任务失败:{error}",
"scope": "media-import",
"source": "views/media-import.js::delete_failed"
},
"copy.dynamic.media.queued": {
"text": "任务已加入转换队列。",
"scope": "media-import",
"source": "views/media-import.js::queued"
},
"copy.dynamic.media.name_conflict": {
"text": "保存名称已存在,请更换名称。",
"scope": "media-import",
"source": "views/media-import.js::name_conflict"
},
"copy.dynamic.media.convert_failed": {
"text": "无法开始转换:{error}",
"scope": "media-import",
"source": "views/media-import.js::convert_failed"
},
"copy.dynamic.media.retry_failed": {
"text": "重试失败:{error}",
"scope": "media-import",
"source": "views/media-import.js::retry_failed"
},
"copy.dynamic.media.completed": {
"text": "“{name}”转换完成,已加入内容库。",
"scope": "media-import",
"source": "views/media-import.js::completed"
},
"copy.dynamic.media.empty": {
"text": "当前没有等待或转换中的任务。",
"scope": "media-import",
"source": "views/media-import.js::empty"
},
"copy.dynamic.media.list_failed": {
"text": "任务列表读取失败:{error}",
"scope": "media-import",
"source": "views/media-import.js::list_failed"
},
"copy.dynamic.media.uploaded": {
"text": "上传完成,设备正在分析媒体。",
"scope": "media-import",
"source": "views/media-import.js::uploaded"
},
"copy.dynamic.media.upload_failed": {
"text": "上传失败:{error}",
"scope": "media-import",
"source": "views/media-import.js::upload_failed"
},
"copy.dynamic.current_display.loading": {
"text": "正在读取当前画面…",
"scope": "dialog:current-display-dialog",
"source": "views/device.js::current_display_loading"
},
"copy.dynamic.current_display.load_failed": {
"text": "当前画面读取失败:{reason}",
"scope": "dialog:current-display-dialog",
"source": "views/device.js::current_display_load_failed"
},
"copy.dynamic.current_display.pause": {
"text": "暂停",
"scope": "dialog:current-display-dialog",
"source": "views/device.js::current_display_pause"
},
"copy.dynamic.current_display.resume": {
"text": "继续播放",
"scope": "dialog:current-display-dialog",
"source": "views/device.js::current_display_resume"
},
"copy.dynamic.current_display.pausing": {
"text": "正在暂停…",
"scope": "dialog:current-display-dialog",
"source": "views/device.js::current_display_pausing"
},
"copy.dynamic.current_display.resuming": {
"text": "正在继续播放…",
"scope": "dialog:current-display-dialog",
"source": "views/device.js::current_display_resuming"
},
"copy.dynamic.current_display.paused": {
"text": "动图已暂停",
"scope": "dialog:current-display-dialog",
"source": "views/device.js::current_display_paused"
},
"copy.dynamic.current_display.resumed": {
"text": "动图继续播放",
"scope": "dialog:current-display-dialog",
"source": "views/device.js::current_display_resumed"
},
"copy.dynamic.current_display.speed_changing": {
"text": "正在切换到 {speed}×…",
"scope": "dialog:current-display-dialog",
"source": "views/device.js::current_display_speed_changing"
},
"copy.dynamic.current_display.speed_changed": {
"text": "已切换到 {speed}×",
"scope": "dialog:current-display-dialog",
"source": "views/device.js::current_display_speed_changed"
},
"copy.dynamic.current_display.control_failed": {
"text": "动图播放控制失败:{reason}",
"scope": "dialog:current-display-dialog",
"source": "views/device.js::current_display_control_failed"
},
"copy.dynamic.font.favorites": {
"text": "收藏字体",
"scope": "text",
"source": "font-picker.js / views/text.js::favorites"
},
"copy.dynamic.font.empty": {
"text": "暂无收藏字体",
"scope": "text",
"source": "font-picker.js / views/text.js::favorites"
},
"copy.dynamic.font.no_matches": {
"text": "没有匹配的收藏字体",
"scope": "text",
"source": "font-picker.js / views/text.js::favorites"
},
"copy.dynamic.font.confirm": {
"text": "确认取消收藏字体“{name}/{style}”?",
"scope": "text",
"source": "font-picker.js / views/text.js::favorites"
},
"copy.dynamic.font.cancel": {
"text": "取消",
"scope": "text",
"source": "font-picker.js / views/text.js::favorites"
},
"copy.dynamic.font.remove": {
"text": "取消收藏",
"scope": "text",
"source": "font-picker.js / views/text.js::favorites"
},
"copy.dynamic.font.saved": {
"text": "字体已收藏",
"scope": "text",
"source": "font-picker.js / views/text.js::favorites"
},
"copy.dynamic.font.removed": {
"text": "已取消收藏字体",
"scope": "text",
"source": "font-picker.js / views/text.js::favorites"
},
"copy.dynamic.font.read_error": {
"text": "收藏状态读取失败,请重新打开字体列表",
"scope": "text",
"source": "font-picker.js / views/text.js::favorites"
},
"copy.dynamic.font.save_error": {
"text": "收藏保存失败:{error}",
"scope": "text",
"source": "font-picker.js / views/text.js::favorites"
}
}
@@ -5206,6 +5206,111 @@
"source": "views/device.js::voltage_ok",
"text": "屏幕输入电压 {volts} V"
},
"copy.dynamic.font.cancel": {
"kind": "dynamic_template",
"placeholders": [],
"render": {
"type": "dynamic_template"
},
"scope": "text",
"source": "font-picker.js / views/text.js::favorites",
"text": "取消"
},
"copy.dynamic.font.confirm": {
"kind": "dynamic_template",
"placeholders": [
"name",
"style"
],
"render": {
"type": "dynamic_template"
},
"scope": "text",
"source": "font-picker.js / views/text.js::favorites",
"text": "确认取消收藏字体“{name}/{style}”?"
},
"copy.dynamic.font.empty": {
"kind": "dynamic_template",
"placeholders": [],
"render": {
"type": "dynamic_template"
},
"scope": "text",
"source": "font-picker.js / views/text.js::favorites",
"text": "暂无收藏字体"
},
"copy.dynamic.font.favorites": {
"kind": "dynamic_template",
"placeholders": [],
"render": {
"type": "dynamic_template"
},
"scope": "text",
"source": "font-picker.js / views/text.js::favorites",
"text": "收藏字体"
},
"copy.dynamic.font.no_matches": {
"kind": "dynamic_template",
"placeholders": [],
"render": {
"type": "dynamic_template"
},
"scope": "text",
"source": "font-picker.js / views/text.js::favorites",
"text": "没有匹配的收藏字体"
},
"copy.dynamic.font.read_error": {
"kind": "dynamic_template",
"placeholders": [],
"render": {
"type": "dynamic_template"
},
"scope": "text",
"source": "font-picker.js / views/text.js::favorites",
"text": "收藏状态读取失败,请重新打开字体列表"
},
"copy.dynamic.font.remove": {
"kind": "dynamic_template",
"placeholders": [],
"render": {
"type": "dynamic_template"
},
"scope": "text",
"source": "font-picker.js / views/text.js::favorites",
"text": "取消收藏"
},
"copy.dynamic.font.removed": {
"kind": "dynamic_template",
"placeholders": [],
"render": {
"type": "dynamic_template"
},
"scope": "text",
"source": "font-picker.js / views/text.js::favorites",
"text": "已取消收藏字体"
},
"copy.dynamic.font.save_error": {
"kind": "dynamic_template",
"placeholders": [
"error"
],
"render": {
"type": "dynamic_template"
},
"scope": "text",
"source": "font-picker.js / views/text.js::favorites",
"text": "收藏保存失败:{error}"
},
"copy.dynamic.font.saved": {
"kind": "dynamic_template",
"placeholders": [],
"render": {
"type": "dynamic_template"
},
"scope": "text",
"source": "font-picker.js / views/text.js::favorites",
"text": "字体已收藏"
},
"copy.dynamic.media.cancel": {
"kind": "dynamic_template",
"placeholders": [],
@@ -32,6 +32,7 @@ const fontOptions = document.getElementById("text-font-options");
const importFontButton = document.getElementById("import-font");
const fontFileInput = document.getElementById("font-file");
const fontFeedback = document.getElementById("font-feedback");
let favoriteEpoch = 0;
let initialized = false;
let suppressForm = false;
let renderedSelectionId = null;
@@ -43,6 +44,25 @@ let acceptedFontExtensions = [".ttf", ".otf", ".ttc", ".otc"];
const fontPicker = createFontPicker({
input: fontInput,
listbox: fontOptions,
onOpen() {
const epoch = ++favoriteEpoch;
apiJson("/api/fonts").then(catalog => { if (epoch === favoriteEpoch) fontPicker.setFavorites(catalog.favorite_ids); }).catch(() => {
setFontFeedbackTemplate("copy.dynamic.font.read_error", "收藏状态读取失败,请重新打开字体列表", {}, true);
});
},
async onFavorite(item, favorite) {
favoriteEpoch += 1;
try {
const result = await apiJson("/api/fonts/favorite", {
method: "PUT", body: JSON.stringify({ id: item.id, favorite }),
});
setFontFeedbackTemplate(favorite ? "copy.dynamic.font.saved" : "copy.dynamic.font.removed", favorite ? "字体已收藏" : "已取消收藏字体");
return result.favorite_ids;
} catch (error) {
setFontFeedbackTemplate("copy.dynamic.font.save_error", "收藏保存失败:{error}", { error: error.message }, true);
return undefined;
}
},
onCommit(item) {
const selected = getSelectedTextElement();
if (!selected) return;
@@ -220,6 +240,7 @@ subscribeScene(renderSelection);
async function loadFontCatalog() {
const catalog = await apiJson("/api/fonts");
fontPicker.setItems(catalog.items);
fontPicker.setFavorites(catalog.favorite_ids);
acceptedFontExtensions = Array.isArray(catalog.accepted_extensions)
? catalog.accepted_extensions.map((value) => String(value).toLowerCase())
: acceptedFontExtensions;