"use strict"; // Offline custom-library rename: only user-owned cards are editable. // The File System Access picker writes the user-selected library.json; no remote upload. (() => { const cards = [...document.querySelectorAll(".char-card")].filter(card => card.querySelector(".custom-badge")); if (!cards.length) return; const categories = { "panel-characters": "characters", "panel-props": "props", "panel-scenes": "scenes" }; const storageKey = "handraw-custom-asset-renames-v1"; const baseNames = new Map(); function key(category, id) { return category + ":" + id; } function english() { return document.documentElement.lang === "en"; } function label(zh, en) { return english() ? en : zh; } function readOverrides() { try { const data = JSON.parse(localStorage.getItem(storageKey) || "{}"); return data && typeof data === "object" && !Array.isArray(data) ? data : {}; } catch { return {}; } } function writeOverrides(data) { try { localStorage.setItem(storageKey, JSON.stringify(data)); } catch { /* file: storage may be disabled */ } } function assetFor(category, id) { const items = window.CUSTOM_ASSETS_DATA && window.CUSTOM_ASSETS_DATA[category]; return Array.isArray(items) ? items.find(item => item && item.id === id) : undefined; } function displayName(card, name, nameEn) { card.dataset.nameZh = name; card.dataset.nameEn = nameEn; const title = card.querySelector(".char-name"); if (title) { title.dataset.zh = name; title.dataset.en = nameEn; title.textContent = english() ? nameEn : name; } const thumb = card.querySelector(".char-thumb-wrap"); if (thumb) thumb.setAttribute("aria-label", label("查看 " + name + " 基准图详情", "Preview " + nameEn)); const img = card.querySelector(".char-thumb-wrap img"); if (img) img.alt = name; } const overrides = readOverrides(); for (const card of cards) { const category = categories[card.closest(".asset-panel")?.id]; const id = card.dataset.id; if (!category || !id) continue; const asset = assetFor(category, id); if (!asset) continue; const assetKey = key(category, id); baseNames.set(assetKey, String(asset.name || id)); const override = overrides[assetKey]; if (override && asset.name === override.previous && typeof override.name === "string") { displayName(card, override.name, override.nameEn || override.name); asset.name = override.name; asset.name_en = override.nameEn || override.name; } else if (override) { delete overrides[assetKey]; // The regenerated index (or another editor) has won. } } writeOverrides(overrides); const style = document.createElement("style"); style.textContent = ` .char-card-actions { flex-wrap: wrap; } .char-card-actions .rename-asset-btn { flex: 0 0 auto; } #asset-rename-dialog { width: min(92vw, 440px); max-width: 440px; padding: 24px; border: 0; border-radius: 14px; background: #fff; color: #24211e; box-shadow: 0 22px 75px #0004; } #asset-rename-dialog::backdrop { background: #0008; } #asset-rename-dialog h2 { font-size: 19px; margin: 0 0 8px; } #asset-rename-dialog p { font-size: 13px; color: #665f57; line-height: 1.6; } #asset-rename-dialog label { display: block; font-size: 13px; font-weight: 700; margin: 15px 0 5px; } #asset-rename-dialog input { box-sizing: border-box; width: 100%; border: 1px solid #c9c1b6; border-radius: 8px; padding: 10px; font: inherit; } #asset-rename-dialog input:focus { outline: 2px solid #b74227; outline-offset: 2px; } #asset-rename-dialog .rename-actions { display: flex; flex-wrap: wrap; gap: 9px; margin-top: 16px; } #asset-rename-dialog button { cursor: pointer; border-radius: 7px; padding: 9px 12px; border: 1px solid #c9c1b6; background: #fff; color: #403a34; font: 700 13px system-ui; } #asset-rename-dialog .rename-save { background: #b74227; border-color: #b74227; color: #fff; } #asset-rename-dialog button:disabled { opacity: .5; cursor: wait; } #asset-rename-status { min-height: 20px; white-space: pre-wrap; overflow-wrap: anywhere; } `; document.head.appendChild(style); const dialog = document.createElement("dialog"); dialog.id = "asset-rename-dialog"; dialog.setAttribute("aria-labelledby", "asset-rename-title"); dialog.innerHTML = `

修改图片名称

仅修改显示名称;图片文件、编号和引用保持不变。保存时请选择当前自建图库目录下的 library.json 文件。

`; document.body.appendChild(dialog); const title = dialog.querySelector("#asset-rename-title"); const help = dialog.querySelector("#asset-rename-help"); const input = dialog.querySelector("#asset-rename-input"); const status = dialog.querySelector("#asset-rename-status"); const save = dialog.querySelector("#asset-rename-save"); const copy = dialog.querySelector("#asset-rename-copy"); const cancel = dialog.querySelector("#asset-rename-cancel"); let selected = null; function statusText(zh, en) { status.textContent = label(zh, en); } function refreshLabels() { cards.forEach(card => { const button = card.querySelector(".rename-asset-btn"); if (button) button.textContent = label("修改名称", "Rename"); }); title.textContent = label("修改图片名称", "Rename custom asset"); help.textContent = label("仅修改显示名称;编号、图片文件和引用不变。保存时选择图库目录中的 library.json。", "Changes the display name only. Select library.json in your library folder to save."); dialog.querySelector('label[for="asset-rename-input"]').textContent = label("图片名称", "Display name"); save.textContent = label("选择 library.json 并保存", "Choose library.json and save"); copy.textContent = label("复制 AI 修改指令", "Copy AI command"); cancel.textContent = label("关闭", "Close"); } cards.forEach(card => { const category = categories[card.closest(".asset-panel")?.id]; const id = card.dataset.id; const actions = card.querySelector(".char-card-actions"); if (!category || !id || !actions || !assetFor(category, id)) return; const button = document.createElement("button"); button.type = "button"; button.className = "btn-action rename-asset-btn"; button.addEventListener("click", () => { selected = { card, category, id }; input.value = card.dataset.nameZh || ""; status.textContent = ""; refreshLabels(); dialog.showModal(); input.focus(); input.select(); }); actions.insertBefore(button, actions.lastElementChild); }); refreshLabels(); document.getElementById("lang-btn")?.addEventListener("click", refreshLabels); cancel.addEventListener("click", () => dialog.close()); dialog.addEventListener("click", event => { if (event.target === dialog) dialog.close(); }); input.addEventListener("keydown", event => { if (event.key === "Enter") { event.preventDefault(); save.click(); } }); function validatedName() { const name = input.value.trim(); if (!name || name.length > 80 || /[\x00-\x1f\x7f]/.test(name)) { statusText("请输入 1–80 个字符的有效名称。", "Enter a valid name (1–80 characters)."); input.focus(); return null; } return name; } copy.addEventListener("click", async () => { const name = validatedName(); if (!name || !selected) return; const command = `把自建图库中的 ${selected.id} 名称修改为:${name}`; try { await navigator.clipboard.writeText(command); statusText("已复制。可粘贴到 Codex / AI 客户端,由 custom-asset-manager 保存并刷新图库。", "Copied. Paste into your AI coding assistant to save and refresh the gallery."); } catch { status.textContent = command; } }); save.addEventListener("click", async () => { if (!selected) return; const name = validatedName(); if (!name) return; if (typeof window.showOpenFilePicker !== "function") { statusText("此浏览器不支持直接写入本地文件。请使用“复制 AI 修改指令”,或运行 custom_library_manager.py rename。", "File editing is unavailable in this browser. Use Copy AI command or the Python rename CLI."); return; } save.disabled = true; try { const [handle] = await window.showOpenFilePicker({ multiple: false, types: [{ description: "HandRaw library.json", accept: { "application/json": [".json"] } }] }); if (!handle || handle.name !== "library.json") { throw new Error(label("请选择自建图库根目录中的 library.json。", "Choose your library's library.json.")); } const manifest = JSON.parse(await (await handle.getFile()).text()); const entries = manifest?.[selected.category]; if (!Array.isArray(entries)) throw new Error(label("所选文件不是有效的图库清单。", "Invalid library manifest.")); const entry = entries.find(item => item && String(item.id).toUpperCase() === selected.id.toUpperCase()); if (!entry) throw new Error(label("所选图库未包含该资产,请检查目录。", "Asset not found in the selected library.")); if (entry.name !== selected.card.dataset.nameZh) { throw new Error(label("图库名称已被其他操作更新,请刷新图库索引后再编辑。", "This name was changed elsewhere. Refresh the gallery index first.")); } const old = entry.name; entry.name = name; if (!entry.name_en || entry.name_en === old) entry.name_en = name; entry.updated_at = new Date().toISOString().slice(0, 10); const writer = await handle.createWritable(); try { await writer.write(JSON.stringify(manifest, null, 2) + "\n"); await writer.close(); } catch (error) { try { await writer.abort(); } catch { /* ignored */ } throw error; } const assetKey = key(selected.category, selected.id); const asset = assetFor(selected.category, selected.id); if (asset) { asset.name = name; asset.name_en = entry.name_en; } displayName(selected.card, name, entry.name_en); const cache = readOverrides(); cache[assetKey] = { previous: baseNames.get(assetKey), name, nameEn: entry.name_en }; writeOverrides(cache); input.value = name; statusText("已保存到 library.json,当前卡片已更新。下次 Skill 刷新图库索引后会永久同步到离线页面。", "Saved to library.json and updated this card. The next Skill gallery sync will refresh the offline index."); } catch (error) { if (error?.name !== "AbortError") { status.textContent = label("保存失败:", "Save failed: ") + (error?.message || String(error)); } } finally { save.disabled = false; } }); })();