Add bundled bus fonts and shared font favorites
This commit is contained in:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user