Download src/observability/web/settings.html from left0ver/WatcherRobot-com.left0ver.agentcade: direct link, hf CLI and curl.
- Browser
- Download file 24.3 kB
-
https://huggingface.co/spaces/left0ver/WatcherRobot-com.left0ver.agentcade/resolve/main/src/observability/web/settings.html
- Command line
-
hf download hf://spaces/left0ver/WatcherRobot-com.left0ver.agentcade/src/observability/web/settings.html
-
curl -L -o settings.html https://huggingface.co/spaces/left0ver/WatcherRobot-com.left0ver.agentcade/resolve/main/src/observability/web/settings.html
24.3 kB
| <html lang="zh-CN"> | |
| <head> | |
| <meta charset="utf-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1"> | |
| <title>设置 · Agentcade</title> | |
| <link rel="icon" href="assets/logo.svg"> | |
| <style> | |
| :root { | |
| color-scheme: light dark; | |
| --bg: #ffffff; --panel: #f7f7f8; --border: #e4e4e7; --text: #1c1c1f; --muted: #777783; | |
| --hover: #f1f1f3; --accent: #2f62e8; --error: #c9302c; --ok: #19874a; --game: #0c8585; | |
| --warn: #b26a00; | |
| --shadow: 0 6px 24px rgb(15 23 42 / .08); | |
| --mono: ui-monospace, "SF Mono", Menlo, Consolas, monospace; | |
| } | |
| @media (prefers-color-scheme: dark) { | |
| :root { | |
| --bg: #141416; --panel: #1a1a1d; --border: #2a2a2f; --text: #e6e6ea; --muted: #8b8b97; | |
| --hover: #202024; --accent: #7ea4ff; --error: #ff6f66; --ok: #4dc480; --game: #4fcfcf; | |
| --warn: #f0b04f; | |
| --shadow: 0 6px 24px rgb(0 0 0 / .4); | |
| } | |
| } | |
| * { box-sizing: border-box; } | |
| [hidden] { display: none ; } | |
| html, body { margin: 0; } | |
| body { | |
| background: var(--bg); color: var(--text); | |
| font: 14px/22px -apple-system, BlinkMacSystemFont, "PingFang SC", "Segoe UI", sans-serif; | |
| } | |
| button, input { font: inherit; color: inherit; } | |
| .top { | |
| position: sticky; top: 0; z-index: 2; display: flex; align-items: center; gap: 16px; height: 52px; | |
| padding: 0 18px; border-bottom: 1px solid var(--border); background: var(--bg); | |
| } | |
| .brand { display: flex; align-items: center; gap: 9px; font-weight: 650; color: inherit; text-decoration: none; } | |
| .logo { width: 26px; height: 26px; } | |
| .crumb { color: var(--muted); } | |
| .spacer { flex: 1; } | |
| .button { | |
| display: inline-flex; align-items: center; height: 28px; padding: 0 11px; border: 1px solid var(--border); | |
| border-radius: 7px; background: var(--bg); color: inherit; font-size: 13px; text-decoration: none; cursor: pointer; | |
| } | |
| .button:hover { background: var(--hover); } | |
| .page { max-width: 720px; margin: 0 auto; padding: 32px 24px 132px; } | |
| h1 { margin: 0 0 8px; font-size: 22px; line-height: 30px; font-weight: 650; } | |
| .where { margin: 16px 0 0; font: 12px/18px var(--mono); color: var(--muted); overflow-wrap: anywhere; } | |
| .banner { | |
| margin-top: 20px; padding: 12px 16px; border: 1px solid color-mix(in srgb, var(--accent) 35%, var(--border)); | |
| border-radius: 12px; background: color-mix(in srgb, var(--accent) 7%, transparent); | |
| } | |
| .card { margin-top: 20px; border: 1px solid var(--border); border-radius: 14px; overflow: hidden; } | |
| .card-head { | |
| display: flex; align-items: center; gap: 10px; padding: 14px 18px; border-bottom: 1px solid var(--border); | |
| background: var(--panel); | |
| } | |
| .card-head h2 { margin: 0; font-size: 15px; line-height: 22px; font-weight: 600; } | |
| .kind { | |
| padding: 0 7px; border-radius: 9px; font-size: 11px; line-height: 18px; color: var(--game); | |
| background: color-mix(in srgb, var(--game) 13%, transparent); | |
| } | |
| .kind.ok { color: var(--ok); background: color-mix(in srgb, var(--ok) 13%, transparent); } | |
| .kind.warn { color: var(--warn); background: color-mix(in srgb, var(--warn) 15%, transparent); } | |
| .kind.bad { color: var(--error); background: color-mix(in srgb, var(--error) 13%, transparent); } | |
| .state { margin-left: auto; font-size: 12px; line-height: 18px; color: var(--ok); text-align: right; } | |
| .state.problem { color: var(--error); } | |
| .card-body { padding: 6px 18px 16px; } | |
| .field { display: block; margin-top: 12px; } | |
| .label { display: flex; align-items: baseline; gap: 10px; } | |
| .label span { font-weight: 500; } | |
| .label code { margin-left: auto; font: 11px/18px var(--mono); color: var(--muted); white-space: nowrap; } | |
| .field input { | |
| width: 100%; height: 36px; margin-top: 5px; padding: 0 11px; border: 1px solid var(--border); | |
| border-radius: 9px; background: var(--bg); outline: none; | |
| } | |
| .field input:focus { border-color: var(--accent); box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 18%, transparent); } | |
| .field input::placeholder { color: var(--muted); } | |
| details { margin-top: 16px; } | |
| summary { color: var(--muted); font-size: 13px; cursor: pointer; } | |
| .bar { | |
| position: fixed; left: 0; right: 0; bottom: 0; display: flex; justify-content: center; padding: 14px 16px 18px; | |
| border-top: 1px solid var(--border); background: var(--bg); | |
| } | |
| .bar-inner { display: flex; align-items: center; gap: 14px; width: 100%; max-width: 672px; } | |
| .message { flex: 1; min-width: 0; color: var(--muted); } | |
| .message.error { color: var(--error); } | |
| .message.done { color: var(--ok); } | |
| .message a { color: var(--accent); } | |
| .save { | |
| height: 38px; padding: 0 22px; border: 0; border-radius: 10px; background: var(--accent); color: #fff; | |
| font-weight: 600; cursor: pointer; | |
| } | |
| .save:disabled { opacity: .55; cursor: default; } | |
| @media (prefers-color-scheme: dark) { .save { color: #0b1020; } } | |
| h2.section { margin: 36px 0 0; font-size: 17px; line-height: 26px; font-weight: 650; } | |
| .hint { margin: 4px 0 0; color: var(--muted); font-size: 13px; } | |
| .state.muted { color: var(--muted); } | |
| .games { padding: 4px 18px 16px; } | |
| .game { display: flex; gap: 12px; align-items: flex-start; padding: 12px 0; border-bottom: 1px solid var(--border); } | |
| .game:last-of-type { border-bottom: 0; } | |
| .game-main { flex: 1; min-width: 0; } | |
| .game-title { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; } | |
| .game-title b { font-weight: 600; } | |
| .game-text { color: var(--muted); font-size: 13px; line-height: 20px; overflow-wrap: anywhere; } | |
| .game-text.error { color: var(--error); } | |
| .game-text.warn { color: var(--warn); } | |
| .game-actions { display: flex; gap: 6px; flex-shrink: 0; } | |
| .button:disabled { opacity: .5; cursor: default; } | |
| .button.danger { color: var(--error); } | |
| .button.primary { border-color: var(--accent); background: var(--accent); color: #fff; } | |
| @media (prefers-color-scheme: dark) { .button.primary { color: #0b1020; } } | |
| .add { display: flex; gap: 8px; margin-top: 14px; } | |
| .add input { | |
| flex: 1; min-width: 0; height: 32px; padding: 0 11px; border: 1px solid var(--border); border-radius: 8px; | |
| background: var(--bg); outline: none; | |
| } | |
| .add input:focus { border-color: var(--accent); } | |
| .add .button { height: 32px; } | |
| .preview { margin-top: 12px; padding: 14px 16px; border: 1px solid var(--border); border-radius: 12px; background: var(--panel); } | |
| .preview h3 { margin: 0; font-size: 15px; line-height: 22px; } | |
| .preview ul { margin: 8px 0 0; padding: 0; list-style: none; } | |
| .preview li { display: flex; gap: 8px; align-items: center; margin-top: 6px; } | |
| .preview .actions { display: flex; gap: 8px; margin-top: 14px; } | |
| .notice { margin-top: 10px; padding: 8px 12px; border-radius: 9px; font-size: 13px; line-height: 20px; } | |
| .notice.ok { color: var(--ok); background: color-mix(in srgb, var(--ok) 9%, transparent); } | |
| .notice.warn { color: var(--warn); background: color-mix(in srgb, var(--warn) 11%, transparent); } | |
| .notice.bad { color: var(--error); background: color-mix(in srgb, var(--error) 9%, transparent); } | |
| .discover { margin-top: 6px; padding-top: 12px; border-top: 1px solid var(--border); } | |
| .discover-head { display: flex; align-items: center; gap: 10px; } | |
| .discover-head b { font-weight: 600; } | |
| .discover-head .game-text { flex: 1; min-width: 0; } | |
| .games-message { margin-top: 10px; font-size: 13px; color: var(--muted); } | |
| .games-message.error { color: var(--error); } | |
| @media (max-width: 640px) { | |
| .page { padding: 20px 14px 132px; } .label code { display: none; } | |
| .game { flex-direction: column; } .add { flex-direction: column; } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <header class="top"> | |
| <a class="brand" href="./"> | |
| <img class="logo" src="assets/logo.svg" alt=""> | |
| <b>Agentcade</b> | |
| </a> | |
| <span class="crumb">设置</span> | |
| <div class="spacer"></div> | |
| <a class="button" href="./">返回对话</a> | |
| </header> | |
| <div class="page"> | |
| <h1>设置</h1> | |
| <div class="banner" id="banner" hidden>填好并保存后,小懒会自动启动,不用重新打开应用。</div> | |
| <h2 class="section">游戏</h2> | |
| <p class="hint">改动马上生效,不用保存,也不用重启。远程游戏需要你确认它能让机器人做什么。</p> | |
| <section class="card"> | |
| <div class="games"> | |
| <div id="games"></div> | |
| <div class="discover"> | |
| <div class="discover-head"> | |
| <b>发现更多游戏</b> | |
| <span class="game-text" id="catalog-state"></span> | |
| <button class="button" id="catalog-refresh" type="button">刷新</button> | |
| </div> | |
| <div id="catalog"></div> | |
| </div> | |
| <div class="add"> | |
| <input id="game-url" type="url" placeholder="也可以粘贴没有收录的游戏地址,例如 http://1.2.3.4:7860" spellcheck="false" autocomplete="off"> | |
| <button class="button" id="game-preview" type="button">查看</button> | |
| </div> | |
| <div id="game-confirm"></div> | |
| <div class="games-message" id="games-message"></div> | |
| </div> | |
| </section> | |
| <h2 class="section">服务配置</h2> | |
| <div id="cards"></div> | |
| </div> | |
| <div class="bar"> | |
| <div class="bar-inner"> | |
| <div class="message" id="message"></div> | |
| <button class="save" id="save" type="button">保存</button> | |
| </div> | |
| </div> | |
| <script> | |
| const $ = (id) => document.getElementById(id); | |
| const forms = new Map(); | |
| let waiting = false; | |
| function el(tag, className, text) { | |
| const node = document.createElement(tag); | |
| if (className) node.className = className; | |
| if (text !== undefined) node.textContent = text; | |
| return node; | |
| } | |
| function fieldNode(field, inputs) { | |
| const node = el("label", "field"); | |
| const label = el("div", "label"); | |
| label.append(el("span", "", field.label), el("code", "", field.key)); | |
| const input = el("input"); | |
| input.type = field.secret ? "password" : "text"; | |
| input.autocomplete = "off"; | |
| input.spellcheck = false; | |
| input.value = field.value; | |
| // 密钥不回显:已经保存过的留空就继续用原来的。 | |
| input.placeholder = field.secret ? (field.saved ? "已保存,留空则不修改" : "请填写") : ""; | |
| inputs.set(field.key, input); | |
| node.append(label, input); | |
| return node; | |
| } | |
| function render(data) { | |
| waiting = data.waiting; | |
| $("banner").hidden = !data.waiting; | |
| forms.clear(); | |
| const cards = data.sections.map((section, index) => { | |
| const card = el("section", "card"); | |
| const head = el("div", "card-head"); | |
| head.append(el("h2", "", section.title)); | |
| if (index) head.append(el("span", "kind", "游戏")); | |
| if (section.disabled) head.append(el("span", "state muted", "游戏已停用,不影响启动")); | |
| else head.append(el("span", section.problem ? "state problem" : "state", section.problem || "已配置")); | |
| const body = el("div", "card-body"); | |
| card.append(head, body); | |
| const inputs = new Map(); | |
| forms.set(section.id, inputs); | |
| const advanced = el("details"); | |
| advanced.append(el("summary", "", "更多设置")); | |
| for (const field of section.fields) (field.advanced ? advanced : body).append(fieldNode(field, inputs)); | |
| if (advanced.childElementCount > 1) body.append(advanced); | |
| body.append(el("p", "where", `保存到:${section.file}`)); | |
| return card; | |
| }); | |
| $("cards").replaceChildren(...cards); | |
| } | |
| function say(text, kind) { | |
| $("message").className = `message ${kind || ""}`; | |
| $("message").textContent = text; | |
| } | |
| async function load() { | |
| const response = await fetch("settings/data", { cache: "no-store" }); | |
| const data = await response.json(); | |
| render(data); | |
| return data; | |
| } | |
| async function watch() { | |
| // 应用在等配置:保存之后它会自己继续启动,这里等到它启动完再提示。 | |
| for (let attempt = 0; attempt < 30; attempt += 1) { | |
| await new Promise((resolve) => setTimeout(resolve, 1000)); | |
| const data = await (await fetch("settings/data", { cache: "no-store" })).json(); | |
| if (!data.waiting) { | |
| $("banner").hidden = true; | |
| $("message").className = "message done"; | |
| $("message").replaceChildren("已保存,小懒已经启动。", Object.assign(el("a", "", "去对话页面"), { href: "./" })); | |
| return; | |
| } | |
| } | |
| } | |
| $("save").addEventListener("click", async () => { | |
| const body = {}; | |
| for (const [id, inputs] of forms) { | |
| body[id] = Object.fromEntries([...inputs].map(([key, input]) => [key, input.value])); | |
| } | |
| $("save").disabled = true; | |
| say("正在保存…"); | |
| try { | |
| const wasWaiting = waiting; | |
| const response = await fetch("settings/data", { | |
| method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), | |
| }); | |
| const data = await response.json(); | |
| if (!response.ok) { | |
| say(data.error || "保存失败", "error"); | |
| return; | |
| } | |
| render(data); | |
| const unfinished = data.sections.filter((section) => section.problem).map((section) => section.title); | |
| if (unfinished.length) say(`已保存,但还有没填好的:${unfinished.join("、")}`, "error"); | |
| else if (wasWaiting) { | |
| say("已保存,小懒正在启动…", "done"); | |
| watch(); | |
| } else say("已保存,重新启动应用后生效。", "done"); | |
| } catch { | |
| say("没有连上应用,请确认应用还在运行。", "error"); | |
| } finally { | |
| $("save").disabled = false; | |
| } | |
| }); | |
| // 游戏:列表、安装、停用、启用、卸载,每个操作马上生效。 | |
| const STATES = { connected: "已连接", disabled: "已停用", waiting: "配置填好后连接" }; | |
| // 远程游戏在游戏目录里的审核状态。 | |
| const REVIEWS = { | |
| reviewed: ["ok", "已审核"], changed: ["warn", "当前版本未经审核"], | |
| unreviewed: ["warn", "未审核"], removed: ["bad", "已下架"], | |
| }; | |
| function reviewBadge(review) { | |
| const [kind, text] = REVIEWS[review.state]; | |
| return el("span", `kind ${kind}`, text); | |
| } | |
| // 当前版本和审核时不一样在哪里。 | |
| function reviewChanges(review, version) { | |
| const parts = []; | |
| if (version && version !== review.version) parts.push(`现在是 ${version} 版,审核过的是 ${review.version} 版`); | |
| if (review.extra.length) parts.push(`审核时没有申请:${review.extra.map((item) => item.label).join(";")}`); | |
| return parts.join(";"); | |
| } | |
| function gamesSay(text, kind) { | |
| $("games-message").className = `games-message ${kind || ""}`; | |
| $("games-message").textContent = text || ""; | |
| } | |
| async function gameAction(body, done) { | |
| gamesSay("正在处理…"); | |
| try { | |
| const response = await fetch("settings/games", { | |
| method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), | |
| }); | |
| const data = await response.json(); | |
| if (!response.ok) { | |
| gamesSay(data.error || "操作失败", "error"); | |
| return null; | |
| } | |
| gamesSay(done || ""); | |
| if (data.games) renderGames(data); | |
| // 装上或卸载之后,“发现更多游戏”里的“已安装”跟着变。 | |
| if (["install", "uninstall"].includes(body.action)) loadCatalog(); | |
| return data; | |
| } catch { | |
| gamesSay("没有连上应用,请确认应用还在运行。", "error"); | |
| return null; | |
| } | |
| } | |
| function gameRow(game) { | |
| const row = el("div", "game"); | |
| const main = el("div", "game-main"); | |
| const title = el("div", "game-title"); | |
| title.append(el("b", "", game.name), el("span", "kind", game.source === "builtin" ? "内置" : game.host)); | |
| if (game.review) title.append(reviewBadge(game.review)); | |
| if (game.unencrypted) title.append(el("span", "kind warn", "未加密")); | |
| if (game.playing) title.append(el("span", "kind", "正在玩")); | |
| main.append(title); | |
| if (game.summary) main.append(el("div", "game-text", game.summary)); | |
| if (game.review?.state === "removed") { | |
| main.append(el("div", "game-text error", `已从游戏目录下架:${game.review.note}。建议卸载。`)); | |
| } else if (game.review?.state === "changed") { | |
| main.append(el("div", "game-text warn", reviewChanges(game.review, game.version))); | |
| } | |
| const state = game.state === "error" ? `连接失败:${game.error}` : STATES[game.state]; | |
| main.append(el("div", game.state === "error" ? "game-text error" : "game-text", state)); | |
| if (game.permissions.length) main.append(el("div", "game-text", `可以:${game.permissions.join(";")}`)); | |
| if (game.source === "remote") main.append(el("div", "game-text", `你在玩它时说的话会发送到 ${game.host}`)); | |
| const actions = el("div", "game-actions"); | |
| if (game.pending.length) { | |
| main.append(el("div", "game-text error", `还没允许:${game.pending.map((item) => item.label).join(";")}`)); | |
| const allow = el("button", "button", "允许"); | |
| allow.type = "button"; | |
| allow.onclick = () => gameAction( | |
| { action: "grant", id: game.id, capabilities: game.pending.map((item) => item.name) }, "已允许。"); | |
| actions.append(allow); | |
| } | |
| if (game.state === "error") { | |
| const retry = el("button", "button", "重新连接"); | |
| retry.type = "button"; | |
| retry.onclick = () => gameAction({ action: "enable", id: game.id }); | |
| actions.append(retry); | |
| } | |
| const toggle = el("button", "button", game.state === "disabled" ? "启用" : "停用"); | |
| toggle.type = "button"; | |
| toggle.disabled = game.playing; | |
| // 内置游戏停用后不再检查它的配置,服务配置那几栏要跟着刷新。 | |
| toggle.onclick = () => gameAction({ action: game.state === "disabled" ? "enable" : "disable", id: game.id }) | |
| .then(() => load()); | |
| actions.append(toggle); | |
| if (game.source === "remote") { | |
| const remove = el("button", "button danger", "卸载"); | |
| remove.type = "button"; | |
| remove.disabled = game.playing; | |
| remove.onclick = () => { | |
| if (confirm(`卸载“${game.name}”?`)) gameAction({ action: "uninstall", id: game.id }, "已卸载。"); | |
| }; | |
| actions.append(remove); | |
| } | |
| if (game.playing) toggle.title = "正在玩这个游戏,先说“结束游戏”"; | |
| row.append(main, actions); | |
| return row; | |
| } | |
| function renderGames(data) { | |
| $("games").replaceChildren(...data.games.map(gameRow)); | |
| } | |
| function renderPreview(preview) { | |
| const box = el("div", "preview"); | |
| const head = el("h3", "", preview.name); | |
| const meta = [preview.version && `版本 ${preview.version}`, preview.author && `作者 ${preview.author}`].filter(Boolean); | |
| box.append(head); | |
| if (meta.length) box.append(el("div", "game-text", meta.join(" · "))); | |
| const review = preview.review; | |
| const extra = new Set(review.extra?.map((item) => item.name) || []); | |
| if (review.state === "reviewed") { | |
| box.append(el("div", "notice ok", `已审核:收录在游戏目录里,${review.reviewed_at} 审核过这个版本。`)); | |
| } else if (review.state === "changed") { | |
| box.append(el("div", "notice warn", | |
| `收录在游戏目录里,但当前版本没有经过审核:${reviewChanges(review, preview.version)}。`)); | |
| } else if (review.state === "unreviewed") { | |
| box.append(el("div", "notice warn", "这个游戏没有收录在游戏目录里,没有经过审核。确认你信任它的作者再安装。")); | |
| } else { | |
| box.append(el("div", "notice bad", `这个游戏已经从游戏目录下架,不能安装:${review.note}`)); | |
| } | |
| if (preview.unencrypted) { | |
| box.append(el("div", "notice warn", | |
| "这个地址用的是 http,没有加密:你在玩这个游戏时说的话和游戏的回复都会明文传输,网络上的其他人可能看到或改动。")); | |
| } | |
| box.append(el("div", "", preview.summary)); | |
| if (preview.description && preview.description !== preview.summary) box.append(el("div", "game-text", preview.description)); | |
| box.append(el("div", "game-text", "安装后它可以:")); | |
| const list = el("ul"); | |
| const boxes = []; | |
| for (const capability of preview.capabilities) { | |
| const item = el("li"); | |
| const check = el("input"); | |
| check.type = "checkbox"; | |
| check.checked = capability.supported; | |
| check.disabled = !capability.supported; | |
| boxes.push([capability.name, check]); | |
| const notes = [!capability.supported && "机器人不支持", extra.has(capability.name) && "审核时没有这项"] | |
| .filter(Boolean); | |
| item.append(check, el("span", "", capability.label + (notes.length ? `(${notes.join(",")})` : ""))); | |
| list.append(item); | |
| } | |
| const data = el("li"); | |
| data.append(el("span", "", `你在玩这个游戏时说的话,会发送到 ${preview.host}`)); | |
| list.append(data); | |
| box.append(list); | |
| const actions = el("div", "actions"); | |
| const install = el("button", "button primary", "安装"); | |
| install.type = "button"; | |
| install.onclick = async () => { | |
| const granted = boxes.filter(([, check]) => check.checked).map(([name]) => name); | |
| const done = await gameAction({ action: "install", url: preview.url, granted }, `已安装“${preview.name}”。`); | |
| if (done) { | |
| $("game-confirm").replaceChildren(); | |
| $("game-url").value = ""; | |
| } | |
| }; | |
| const cancel = el("button", "button", "取消"); | |
| cancel.type = "button"; | |
| cancel.onclick = () => { $("game-confirm").replaceChildren(); gamesSay(""); }; | |
| if (review.state !== "removed") actions.append(install); | |
| actions.append(cancel); | |
| box.append(actions); | |
| $("game-confirm").replaceChildren(box); | |
| // 底部固定的保存栏可能挡住安装按钮,把确认内容滚到页面中间。 | |
| box.scrollIntoView({ block: "center", behavior: "smooth" }); | |
| } | |
| // 发现更多游戏:游戏目录里审核过的游戏,安装时和粘贴地址一样要确认权限。 | |
| function catalogRow(listing) { | |
| const row = el("div", "game"); | |
| const main = el("div", "game-main"); | |
| const title = el("div", "game-title"); | |
| title.append(el("b", "", listing.name), el("span", "kind", listing.host), el("span", "kind ok", "已审核")); | |
| if (listing.unencrypted) title.append(el("span", "kind warn", "未加密")); | |
| main.append(title, el("div", "game-text", listing.summary)); | |
| const meta = [`作者 ${listing.author}`, `版本 ${listing.version}`, `${listing.reviewed_at} 审核`]; | |
| main.append(el("div", "game-text", meta.join(" · "))); | |
| if (listing.permissions.length) main.append(el("div", "game-text", `需要:${listing.permissions.join(";")}`)); | |
| const actions = el("div", "game-actions"); | |
| const install = el("button", "button", listing.installed ? "已安装" : "安装"); | |
| install.type = "button"; | |
| install.disabled = listing.installed || listing.taken; | |
| if (listing.taken) install.title = `已经装了另一个 ID 为 ${listing.id} 的游戏,先卸载它`; | |
| install.onclick = async () => { | |
| $("game-confirm").replaceChildren(); | |
| const data = await gameAction({ action: "preview", url: listing.url }); | |
| if (data?.preview) renderPreview(data.preview); | |
| }; | |
| actions.append(install); | |
| row.append(main, actions); | |
| return row; | |
| } | |
| function renderCatalog(data) { | |
| const time = data.fetched_at && new Date(data.fetched_at * 1000).toLocaleString("zh-CN", { hour12: false }); | |
| const state = $("catalog-state"); | |
| state.className = data.error ? "game-text error" : "game-text"; | |
| if (data.error) state.textContent = data.fetched_at ? `${data.error},下面是 ${time} 读到的内容` : data.error; | |
| else state.textContent = data.fetched_at ? `更新于 ${time}` : ""; | |
| const rows = data.games.map(catalogRow); | |
| if (!rows.length && data.fetched_at) rows.push(el("div", "game-text", "目录里还没有游戏。")); | |
| $("catalog").replaceChildren(...rows); | |
| } | |
| async function loadCatalog(refresh) { | |
| $("catalog-refresh").disabled = true; | |
| if (refresh) $("catalog-state").textContent = "正在读取游戏目录…"; | |
| try { | |
| const response = await fetch(`settings/catalog${refresh ? "?refresh=1" : ""}`, { cache: "no-store" }); | |
| renderCatalog(await response.json()); | |
| } catch { | |
| $("catalog-state").className = "game-text error"; | |
| $("catalog-state").textContent = "没有连上应用,请确认应用还在运行。"; | |
| } finally { | |
| $("catalog-refresh").disabled = false; | |
| } | |
| } | |
| $("catalog-refresh").addEventListener("click", () => loadCatalog(true)); | |
| $("game-preview").addEventListener("click", async () => { | |
| const url = $("game-url").value.trim(); | |
| if (!url) return gamesSay("先粘贴游戏的地址。", "error"); | |
| $("game-confirm").replaceChildren(); | |
| const data = await gameAction({ action: "preview", url }); | |
| if (data?.preview) renderPreview(data.preview); | |
| }); | |
| async function loadGames() { | |
| const data = await (await fetch("settings/games", { cache: "no-store" })).json(); | |
| renderGames(data); | |
| } | |
| load().catch(() => say("没有连上应用,请确认应用还在运行。", "error")); | |
| loadGames().catch(() => gamesSay("没有连上应用,请确认应用还在运行。", "error")); | |
| loadCatalog(); | |
| </script> | |
| </body> | |
| </html> | |