/* New NAI Frontend App JS */ (() => { const $ = (sel, root = document) => root.querySelector(sel); const $$ = (sel, root = document) => [...root.querySelectorAll(sel)]; const byId = (id) => document.getElementById(id); // 提示音状态(运行时),默认关闭,URL 指向 /ring/ring.mp3 let soundEnabled = false; let soundUrl = "/ring/ring.mp3"; // 检测是否为移动端设备 function isMobileDevice() { return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) || (window.innerWidth <= 768); } // ===== 移动端后台恢复处理 ===== // 修复手机切屏后回来无法输入的问题 function handlePageResume() { // 重新激活所有输入框 const inputs = document.querySelectorAll('input, textarea'); inputs.forEach(input => { if (input.disabled) return; // 移除可能的只读属性 input.removeAttribute('readonly'); // 确保输入框可交互 input.style.pointerEvents = 'auto'; }); } // 监听页面可见性变化(切后台/切回来) document.addEventListener('visibilitychange', () => { if (!document.hidden) { // 页面变为可见时恢复 handlePageResume(); } }); // 监听页面显示事件(处理 iOS Safari 的 bfcache) window.addEventListener('pageshow', (event) => { if (event.persisted) { // 从 bfcache 恢复时重新激活 handlePageResume(); } }); // 监听窗口获得焦点 window.addEventListener('focus', () => { handlePageResume(); }); // 页面加载完成后初始化 window.addEventListener('load', () => { handlePageResume(); }); // 主题:深色/浅色 切换 const rootEl = document.documentElement; function applyTheme(theme) { rootEl.setAttribute('data-theme', theme); localStorage.setItem('theme', theme); const text = '主题:' + (theme === 'dark' ? '深色' : '浅色'); const btn = byId('theme-toggle'); const btnMobile = byId('theme-toggle-mobile'); if (btn) btn.textContent = text; if (btnMobile) btnMobile.textContent = text; } const initialTheme = localStorage.getItem('theme') || 'dark'; applyTheme(initialTheme); function setupThemeToggle() { const themeBtn = byId('theme-toggle'); const themeBtnMobile = byId('theme-toggle-mobile'); const toggleTheme = () => { const next = (rootEl.getAttribute('data-theme') === 'dark') ? 'light' : 'dark'; applyTheme(next); }; if (themeBtn) themeBtn.addEventListener('click', toggleTheme); if (themeBtnMobile) themeBtnMobile.addEventListener('click', toggleTheme); } setupThemeToggle(); // 提示音开关(在主题按钮旁) const soundPlayer = byId('sound-player'); function updateSoundToggle() { const text = '提示音:' + (soundEnabled ? '开' : '关'); const pressed = soundEnabled ? 'true' : 'false'; const soundBtn = byId('sound-toggle'); const soundBtnMobile = byId('sound-toggle-mobile'); if (soundBtn) { soundBtn.setAttribute('aria-pressed', pressed); soundBtn.textContent = text; } if (soundBtnMobile) { soundBtnMobile.setAttribute('aria-pressed', pressed); soundBtnMobile.textContent = text; } } function setupSoundToggle() { const soundBtn = byId('sound-toggle'); const soundBtnMobile = byId('sound-toggle-mobile'); const toggle = () => { soundEnabled = !soundEnabled; updateSoundToggle(); }; if (soundBtn) soundBtn.addEventListener('click', toggle); if (soundBtnMobile) soundBtnMobile.addEventListener('click', toggle); } setupSoundToggle(); // 侧边栏控制 function setupSidebar() { const menuToggle = byId('menu-toggle'); const sidebar = byId('sidebar'); const sidebarClose = byId('sidebar-close'); const sidebarOverlay = byId('sidebar-overlay'); function openSidebar() { if (sidebar) sidebar.classList.add('active'); if (sidebarOverlay) sidebarOverlay.classList.add('active'); } function closeSidebar() { if (sidebar) sidebar.classList.remove('active'); if (sidebarOverlay) sidebarOverlay.classList.remove('active'); } if (menuToggle) menuToggle.addEventListener('click', openSidebar); if (sidebarClose) sidebarClose.addEventListener('click', closeSidebar); if (sidebarOverlay) sidebarOverlay.addEventListener('click', closeSidebar); // 侧边栏中的Tab按钮也要同步 const sidebarBtns = sidebar ? sidebar.querySelectorAll('.tab-btn[data-tab]') : []; sidebarBtns.forEach(btn => { btn.addEventListener('click', () => { closeSidebar(); }); }); } setupSidebar(); // Tabs(桌面端和侧边栏都要同步) $$(".tab-btn[data-tab]").forEach((btn) => { btn.addEventListener("click", () => { const tabId = btn.getAttribute("data-tab"); // 同步所有tab按钮状态 $$(".tab-btn[data-tab]").forEach((b) => b.classList.remove("active")); $$(".tab-btn[data-tab='" + tabId + "']").forEach((b) => b.classList.add("active")); // 切换tab内容 $$(".tab").forEach((tab) => tab.classList.remove("active")); const targetTab = byId(tabId); if (targetTab) targetTab.classList.add("active"); }); }); // UI helpers // Toast 提示(3秒后自动消失) let toastTimer = null; const toast = (msg, type = "info") => { const el = byId("toast"); el.textContent = msg || ""; el.className = `toast ${type}`; // 清除之前的定时器 if (toastTimer) { clearTimeout(toastTimer); } // 3秒后自动消失 toastTimer = setTimeout(() => { el.textContent = ""; el.className = "toast"; }, 3000); // 成功提示时若开启提示音则播放 try { if (type === "success" && soundEnabled) { const p = byId("sound-player"); if (p) { p.currentTime = 0; // 忽略浏览器自动播放限制的异常 p.play().catch(() => {}); } } } catch {} }; const loading = { show() { byId("loading").classList.remove("hidden"); }, hide() { byId("loading").classList.add("hidden"); }, }; // ===== 自定义背景功能 ===== const STORAGE_KEY_BG = 'nai_custom_background'; function applyCustomBackground(imageDataUrl) { if (imageDataUrl) { document.body.style.backgroundImage = `url('${imageDataUrl}')`; localStorage.setItem(STORAGE_KEY_BG, imageDataUrl); } } function resetBackground() { document.body.style.backgroundImage = ''; localStorage.removeItem(STORAGE_KEY_BG); // 让CSS的默认背景生效 toast("已重置为默认背景", "success"); } function loadCustomBackground() { try { const saved = localStorage.getItem(STORAGE_KEY_BG); if (saved) { document.body.style.backgroundImage = `url('${saved}')`; } } catch {} } function setupCustomBackground() { const bgInput = byId("cfg-custom-bg"); const resetBtn = byId("btn-reset-bg"); if (bgInput) { bgInput.addEventListener("change", async (e) => { const file = e.target.files?.[0]; if (!file) return; // 检查文件大小(限制5MB) if (file.size > 5 * 1024 * 1024) { toast("图片文件过大,请选择小于5MB的图片", "error"); return; } try { const reader = new FileReader(); reader.onload = (ev) => { const dataUrl = ev.target?.result; if (dataUrl) { applyCustomBackground(dataUrl); toast("背景已更新", "success"); } }; reader.onerror = () => { toast("读取图片失败", "error"); }; reader.readAsDataURL(file); } catch (err) { toast("设置背景失败", "error"); } }); } if (resetBtn) { resetBtn.addEventListener("click", resetBackground); } } // Config async function loadConfig() { try { const res = await fetch("/api/config"); if (!res.ok) throw new Error(await res.text()); const cfg = await res.json(); byId("cfg-key").value = cfg.key ?? ""; byId("cfg-model").value = cfg.model ?? "nai-diffusion-3"; byId("cfg-sampler").value = cfg.sampler ?? "k_euler"; byId("cfg-steps").value = cfg.steps ?? ""; byId("cfg-scale").value = cfg.scale ?? ""; byId("cfg-cfg-rescale").value = cfg.cfg_rescale ?? ""; byId("cfg-noise-schedule").value = cfg.noise_schedule ?? ""; byId("cfg-uc-preset").value = cfg.uc_preset ?? ""; byId("cfg-quality-toggle").checked = !!cfg.quality_toggle; byId("cfg-legacy-uc").checked = !!cfg.legacy_uc; byId("cfg-save-output").checked = !!cfg.save_output; if (byId("cfg-output-dir")) byId("cfg-output-dir").value = cfg.output_dir ?? ""; // 提示音配置 soundEnabled = !!cfg.sound_enabled; soundUrl = cfg.sound_url || "/ring/ring.mp3"; if (byId("sound-player")) byId("sound-player").src = soundUrl; (typeof updateSoundToggle === "function") && updateSoundToggle(); // 隐藏下方"配置已读取"提示 byId("cfg-message").textContent = ""; } catch (err) { byId("cfg-message").textContent = "读取失败:" + err.message; toast("读取配置失败", "error"); } } function nullIfEmpty(v) { if (v === undefined || v === null) return null; if (typeof v === "string" && v.trim() === "") return null; return v; } // HF版本:配置自动保存到服务器(无需手动点击保存按钮) async function autoSaveConfig() { const payload = { key: nullIfEmpty(byId("cfg-key").value), model: nullIfEmpty(byId("cfg-model").value), sampler: nullIfEmpty(byId("cfg-sampler").value), steps: byId("cfg-steps").value ? Number(byId("cfg-steps").value) : null, scale: byId("cfg-scale").value ? Number(byId("cfg-scale").value) : null, cfg_rescale: byId("cfg-cfg-rescale").value ? Number(byId("cfg-cfg-rescale").value) : null, noise_schedule: nullIfEmpty(byId("cfg-noise-schedule").value), uc_preset: byId("cfg-uc-preset").value ? Number(byId("cfg-uc-preset").value) : null, quality_toggle: byId("cfg-quality-toggle").checked, legacy_uc: byId("cfg-legacy-uc").checked, save_output: byId("cfg-save-output").checked, output_dir: nullIfEmpty(byId("cfg-output-dir")?.value), sound_enabled: !!soundEnabled, sound_url: nullIfEmpty(soundUrl), }; try { const res = await fetch("/api/config", { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }); // 静默保存,不显示提示 } catch (err) { // 忽略错误 } } // 配置字段变化时自动保存(防抖) let configSaveTimer = null; function setupAutoSaveConfig() { const configFields = [ "cfg-key", "cfg-model", "cfg-sampler", "cfg-steps", "cfg-scale", "cfg-cfg-rescale", "cfg-noise-schedule", "cfg-uc-preset", "cfg-quality-toggle", "cfg-legacy-uc", "cfg-save-output", "cfg-output-dir" ]; configFields.forEach(id => { const el = byId(id); if (el) { el.addEventListener("change", () => { if (configSaveTimer) clearTimeout(configSaveTimer); configSaveTimer = setTimeout(autoSaveConfig, 500); }); // 对于文本输入,也监听input事件 if (el.tagName === "INPUT" && (el.type === "text" || el.type === "password" || el.type === "number")) { el.addEventListener("input", () => { if (configSaveTimer) clearTimeout(configSaveTimer); configSaveTimer = setTimeout(autoSaveConfig, 1000); }); } } }); } // File -> Base64 (strip data URL prefix) function fileToBase64(file) { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = () => { const result = reader.result || ""; const idx = String(result).indexOf(","); resolve(idx >= 0 ? String(result).slice(idx + 1) : String(result)); }; reader.onerror = reject; reader.readAsDataURL(file); }); } // Ensure x64 function ensureX64(n) { const val = Number(n || 0); if (!val || val <= 64) return 64; if (val % 64 === 0) return val; const fract = (val / 64) % 1; return fract >= 0.5 ? (Math.floor(val / 64) + 1) * 64 : Math.floor(val / 64) * 64; } // ===== 批量/并发与渲染辅助 ===== function ensureCountField(tabId, inputId) { if (byId(inputId)) return; const form = byId(tabId)?.querySelector(".form-grid"); if (!form) return; const label = document.createElement("label"); const span = document.createElement("span"); span.textContent = "数量"; const input = document.createElement("input"); input.type = "number"; input.min = "1"; input.max = "8"; input.step = "1"; input.value = "1"; input.id = inputId; label.appendChild(span); label.appendChild(input); form.appendChild(label); } function ensureGrid(tabId, gridId, singleImgId) { const section = byId(tabId); if (!section) return null; const res = section.querySelector(".result"); if (!res) return null; let grid = byId(gridId); if (!grid) { grid = document.createElement("div"); grid.id = gridId; res.prepend(grid); } grid.style.display = "grid"; grid.style.gridTemplateColumns = "repeat(auto-fill, minmax(160px, 1fr))"; grid.style.gap = "10px"; const single = byId(singleImgId); if (single) single.style.display = "none"; return grid; } function setBusy(btn, busy, busyText = "生成中...") { if (!btn) return; if (busy) { btn.setAttribute("data-text", btn.textContent || ""); btn.textContent = busyText; btn.disabled = true; } else { const t = btn.getAttribute("data-text"); if (t !== null) btn.textContent = t; btn.disabled = false; } } function getCount(inputId, def = 1) { const n = Number(byId(inputId)?.value || def); return Math.max(1, Math.min(8, isNaN(n) ? def : n)); } // 展开/收起图片信息 function setupImageInfoToggle() { document.addEventListener('click', (e) => { if (e.target.classList.contains('image-info-toggle')) { const targetId = e.target.getAttribute('data-target'); const content = document.getElementById(targetId); if (content) { const isExpanded = content.classList.contains('expanded'); if (isExpanded) { content.classList.remove('expanded'); e.target.textContent = '展开 ▼'; } else { content.classList.add('expanded'); e.target.textContent = '收起 ▲'; } } } }); } // 显示图片信息 function displayImageInfo(tabPrefix, data, prompt, negative) { const infoEl = document.getElementById(`${tabPrefix}-info`); const modelEl = document.getElementById(`${tabPrefix}-model`); const resolutionEl = document.getElementById(`${tabPrefix}-resolution`); const promptEl = document.getElementById(`${tabPrefix}-prompt-text`); const negativeEl = document.getElementById(`${tabPrefix}-negative-text`); if (infoEl && modelEl && resolutionEl && promptEl && negativeEl) { // 获取配置中的模型 const model = byId('cfg-model')?.value || 'nai-diffusion-3'; // 显示信息 infoEl.style.display = 'block'; modelEl.textContent = model; resolutionEl.textContent = `${data.width || 768} × ${data.height || 768}`; promptEl.textContent = prompt || '无'; negativeEl.textContent = negative || '无'; } // 显示保存按钮 const actionsEl = document.getElementById(`${tabPrefix}-actions`); if (actionsEl) { actionsEl.style.display = 'block'; } } // ===== 图片下载功能 ===== // 下载图片(兼容手机和电脑) function downloadImage(imgElement, filename) { if (!imgElement || !imgElement.src) { toast("没有可下载的图片", "error"); return; } const src = imgElement.src; // 如果是 data URI if (src.startsWith('data:')) { const link = document.createElement('a'); link.href = src; link.download = filename || `nai_${Date.now()}.png`; document.body.appendChild(link); link.click(); document.body.removeChild(link); toast("图片已开始下载", "success"); } else { // 如果是普通 URL,先 fetch 转换为 blob fetch(src) .then(res => res.blob()) .then(blob => { const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = filename || `nai_${Date.now()}.png`; document.body.appendChild(link); link.click(); document.body.removeChild(link); URL.revokeObjectURL(url); toast("图片已开始下载", "success"); }) .catch(err => { toast("下载失败: " + err.message, "error"); }); } } // 设置保存图片按钮 function setupSaveButtons() { // T2I 保存按钮 const btnSaveT2I = byId("btn-save-t2i"); if (btnSaveT2I) { btnSaveT2I.addEventListener("click", () => { const img = byId("t2i-img"); // 尝试从 grid 获取第一张图片 const grid = byId("t2i-grid"); const firstGridImg = grid?.querySelector("img"); const targetImg = (firstGridImg && firstGridImg.src) ? firstGridImg : img; downloadImage(targetImg, `nai_t2i_${Date.now()}.png`); }); } // I2I 保存按钮 const btnSaveI2I = byId("btn-save-i2i"); if (btnSaveI2I) { btnSaveI2I.addEventListener("click", () => { const img = byId("i2i-img"); const grid = byId("i2i-grid"); const firstGridImg = grid?.querySelector("img"); const targetImg = (firstGridImg && firstGridImg.src) ? firstGridImg : img; downloadImage(targetImg, `nai_i2i_${Date.now()}.png`); }); } // Inpaint 保存按钮 const btnSaveInpaint = byId("btn-save-inpaint"); if (btnSaveInpaint) { btnSaveInpaint.addEventListener("click", () => { const img = byId("inpaint-img"); const grid = byId("inpaint-grid"); const firstGridImg = grid?.querySelector("img"); const targetImg = (firstGridImg && firstGridImg.src) ? firstGridImg : img; downloadImage(targetImg, `nai_inpaint_${Date.now()}.png`); }); } } // ===== localStorage 生成参数保存/恢复 ===== const STORAGE_KEY_T2I = 'nai_t2i_params'; const STORAGE_KEY_I2I = 'nai_i2i_params'; const STORAGE_KEY_INPAINT = 'nai_inpaint_params'; function saveT2IParams() { const params = { prompt: byId("t2i-prompt").value || "", negative: byId("t2i-negative").value || "", width: byId("t2i-width").value || "768", height: byId("t2i-height").value || "768", steps: byId("t2i-steps").value || "", scale: byId("t2i-scale").value || "", sampler: byId("t2i-sampler").value || "", noise_schedule: byId("t2i-noise-schedule").value || "", seed: byId("t2i-seed").value || "-1", variety: byId("t2i-variety").checked, decrisp: byId("t2i-decrisp").checked, cfg_rescale: byId("t2i-cfg-rescale").value || "", count: byId("t2i-count")?.value || "1" }; try { localStorage.setItem(STORAGE_KEY_T2I, JSON.stringify(params)); } catch {} } function restoreT2IParams() { try { const saved = localStorage.getItem(STORAGE_KEY_T2I); if (!saved) return; const params = JSON.parse(saved); if (params.prompt !== undefined) byId("t2i-prompt").value = params.prompt; if (params.negative !== undefined) byId("t2i-negative").value = params.negative; if (params.width) byId("t2i-width").value = params.width; if (params.height) byId("t2i-height").value = params.height; if (params.steps) byId("t2i-steps").value = params.steps; if (params.scale) byId("t2i-scale").value = params.scale; if (params.sampler !== undefined) byId("t2i-sampler").value = params.sampler; if (params.noise_schedule !== undefined) byId("t2i-noise-schedule").value = params.noise_schedule; if (params.seed !== undefined) byId("t2i-seed").value = params.seed; if (params.variety !== undefined) byId("t2i-variety").checked = params.variety; if (params.decrisp !== undefined) byId("t2i-decrisp").checked = params.decrisp; if (params.cfg_rescale) byId("t2i-cfg-rescale").value = params.cfg_rescale; if (params.count && byId("t2i-count")) byId("t2i-count").value = params.count; } catch {} } function saveI2IParams() { const params = { positive: byId("i2i-positive").value || "", negative: byId("i2i-negative").value || "", width: byId("i2i-width").value || "", height: byId("i2i-height").value || "", steps: byId("i2i-steps").value || "", scale: byId("i2i-scale").value || "", sampler: byId("i2i-sampler").value || "", noise_schedule: byId("i2i-noise-schedule").value || "", seed: byId("i2i-seed").value || "-1", strength: byId("i2i-strength").value || "0.5", noise: byId("i2i-noise").value || "0.0", variety: byId("i2i-variety").checked, decrisp: byId("i2i-decrisp").checked, cfg_rescale: byId("i2i-cfg-rescale").value || "", count: byId("i2i-count")?.value || "1" }; try { localStorage.setItem(STORAGE_KEY_I2I, JSON.stringify(params)); } catch {} } function restoreI2IParams() { try { const saved = localStorage.getItem(STORAGE_KEY_I2I); if (!saved) return; const params = JSON.parse(saved); if (params.positive !== undefined) byId("i2i-positive").value = params.positive; if (params.negative !== undefined) byId("i2i-negative").value = params.negative; if (params.width) byId("i2i-width").value = params.width; if (params.height) byId("i2i-height").value = params.height; if (params.steps) byId("i2i-steps").value = params.steps; if (params.scale) byId("i2i-scale").value = params.scale; if (params.sampler !== undefined) byId("i2i-sampler").value = params.sampler; if (params.noise_schedule !== undefined) byId("i2i-noise-schedule").value = params.noise_schedule; if (params.seed !== undefined) byId("i2i-seed").value = params.seed; if (params.strength !== undefined) byId("i2i-strength").value = params.strength; if (params.noise !== undefined) byId("i2i-noise").value = params.noise; if (params.variety !== undefined) byId("i2i-variety").checked = params.variety; if (params.decrisp !== undefined) byId("i2i-decrisp").checked = params.decrisp; if (params.cfg_rescale) byId("i2i-cfg-rescale").value = params.cfg_rescale; if (params.count && byId("i2i-count")) byId("i2i-count").value = params.count; } catch {} } function saveInpaintParams() { const params = { positive: byId("inpaint-positive").value || "", negative: byId("inpaint-negative").value || "", add_original: byId("inpaint-add-original").checked, width: byId("inpaint-width").value || "", height: byId("inpaint-height").value || "", steps: byId("inpaint-steps").value || "", scale: byId("inpaint-scale").value || "", sampler: byId("inpaint-sampler").value || "", noise_schedule: byId("inpaint-noise-schedule").value || "", seed: byId("inpaint-seed").value || "-1", strength: byId("inpaint-strength").value || "0.5", noise: byId("inpaint-noise-val").value || "0.0", variety: byId("inpaint-variety").checked, decrisp: byId("inpaint-decrisp").checked, cfg_rescale: byId("inpaint-cfg-rescale").value || "", count: byId("inpaint-count")?.value || "1" }; try { localStorage.setItem(STORAGE_KEY_INPAINT, JSON.stringify(params)); } catch {} } function restoreInpaintParams() { try { const saved = localStorage.getItem(STORAGE_KEY_INPAINT); if (!saved) return; const params = JSON.parse(saved); if (params.positive !== undefined) byId("inpaint-positive").value = params.positive; if (params.negative !== undefined) byId("inpaint-negative").value = params.negative; if (params.add_original !== undefined) byId("inpaint-add-original").checked = params.add_original; if (params.width) byId("inpaint-width").value = params.width; if (params.height) byId("inpaint-height").value = params.height; if (params.steps) byId("inpaint-steps").value = params.steps; if (params.scale) byId("inpaint-scale").value = params.scale; if (params.sampler !== undefined) byId("inpaint-sampler").value = params.sampler; if (params.noise_schedule !== undefined) byId("inpaint-noise-schedule").value = params.noise_schedule; if (params.seed !== undefined) byId("inpaint-seed").value = params.seed; if (params.strength !== undefined) byId("inpaint-strength").value = params.strength; if (params.noise !== undefined) byId("inpaint-noise-val").value = params.noise; if (params.variety !== undefined) byId("inpaint-variety").checked = params.variety; if (params.decrisp !== undefined) byId("inpaint-decrisp").checked = params.decrisp; if (params.cfg_rescale) byId("inpaint-cfg-rescale").value = params.cfg_rescale; if (params.count && byId("inpaint-count")) byId("inpaint-count").value = params.count; } catch {} } // T2I async function handleT2I() { // 保存当前参数到 localStorage saveT2IParams(); const payloadBase = { prompt: byId("t2i-prompt").value || "", negative: byId("t2i-negative").value || "", width: ensureX64(byId("t2i-width").value || 768), height: ensureX64(byId("t2i-height").value || 768), steps: byId("t2i-steps").value ? Number(byId("t2i-steps").value) : null, scale: byId("t2i-scale").value ? Number(byId("t2i-scale").value) : null, sampler: nullIfEmpty(byId("t2i-sampler").value), noise_schedule: nullIfEmpty(byId("t2i-noise-schedule").value), seed: byId("t2i-seed").value ? Number(byId("t2i-seed").value) : -1, variety: byId("t2i-variety").checked, decrisp: byId("t2i-decrisp").checked, cfg_rescale: byId("t2i-cfg-rescale").value ? Number(byId("t2i-cfg-rescale").value) : null, }; if (!payloadBase.prompt.trim()) { toast("请填写正面提示词", "error"); return; } const btn = byId("btn-t2i"); setBusy(btn, true); const count = getCount("t2i-count", 1); try { const tasks = Array.from({ length: count }, async () => { const res = await fetch("/api/generate/t2i", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payloadBase), }); const txt = await res.text(); let body; try { body = JSON.parse(txt); } catch { throw new Error(txt); } if (!res.ok) throw new Error(body?.detail || "生成失败"); return body; }); const settled = await Promise.allSettled(tasks); const oks = settled.filter(s => s.status === "fulfilled").map(s => s.value); if (!oks.length) throw settled[0]?.reason || new Error("生成失败"); const grid = ensureGrid("tab-t2i", "t2i-grid", "t2i-img"); if (grid) { grid.innerHTML = ""; oks.forEach(o => { const im = document.createElement("img"); im.src = o.image_base64; im.alt = "生成结果"; im.style.width = "100%"; im.style.borderRadius = "12px"; im.style.border = "1px solid var(--border)"; grid.appendChild(im); }); } else { const imgEl = byId("t2i-img"); if (imgEl) imgEl.src = oks[0].image_base64; } const paths = oks.map(o => o.saved_path || "").filter(Boolean).join(" | "); if (byId("t2i-saved")) byId("t2i-saved").value = paths; // 显示第一张图片的信息 if (oks.length > 0) { displayImageInfo('t2i', { width: payloadBase.width, height: payloadBase.height }, payloadBase.prompt, payloadBase.negative); } toast(`生成成功 ${oks.length} 张`, "success"); } catch (err) { toast(String(err.message || err), "error"); } finally { setBusy(btn, false); } } // I2I async function handleI2I() { // 保存当前参数到 localStorage saveI2IParams(); const file = byId("i2i-image").files?.[0]; if (!file) { toast("请上传输入图片", "error"); return; } const image_base64 = await fileToBase64(file); const payloadBase = { positive: byId("i2i-positive").value || "", negative: byId("i2i-negative").value || "", image_base64, width: byId("i2i-width").value ? ensureX64(byId("i2i-width").value) : null, height: byId("i2i-height").value ? ensureX64(byId("i2i-height").value) : null, steps: byId("i2i-steps").value ? Number(byId("i2i-steps").value) : null, scale: byId("i2i-scale").value ? Number(byId("i2i-scale").value) : null, sampler: nullIfEmpty(byId("i2i-sampler").value), noise_schedule: nullIfEmpty(byId("i2i-noise-schedule").value), seed: byId("i2i-seed").value ? Number(byId("i2i-seed").value) : -1, strength: byId("i2i-strength").value ? Number(byId("i2i-strength").value) : 0.5, noise: byId("i2i-noise").value ? Number(byId("i2i-noise").value) : 0.0, variety: byId("i2i-variety").checked, decrisp: byId("i2i-decrisp").checked, cfg_rescale: byId("i2i-cfg-rescale").value ? Number(byId("i2i-cfg-rescale").value) : null, }; if (!payloadBase.positive.trim()) { toast("请填写正面提示词", "error"); return; } const btn = byId("btn-i2i"); setBusy(btn, true); const count = getCount("i2i-count", 1); try { const tasks = Array.from({ length: count }, async () => { const res = await fetch("/api/generate/i2i", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payloadBase), }); const txt = await res.text(); let body; try { body = JSON.parse(txt); } catch { throw new Error(txt); } if (!res.ok) throw new Error(body?.detail || "生成失败"); return body; }); const settled = await Promise.allSettled(tasks); const oks = settled.filter(s => s.status === "fulfilled").map(s => s.value); if (!oks.length) throw settled[0]?.reason || new Error("生成失败"); const grid = ensureGrid("tab-i2i", "i2i-grid", "i2i-img"); if (grid) { grid.innerHTML = ""; oks.forEach(o => { const im = document.createElement("img"); im.src = o.image_base64; im.alt = "生成结果"; im.style.width = "100%"; im.style.borderRadius = "12px"; im.style.border = "1px solid var(--border)"; grid.appendChild(im); }); } else { const imgEl = byId("i2i-img"); if (imgEl) imgEl.src = oks[0].image_base64; } const paths = oks.map(o => o.saved_path || "").filter(Boolean).join(" | "); if (byId("i2i-saved")) byId("i2i-saved").value = paths; // 显示第一张图片的信息 if (oks.length > 0) { displayImageInfo('i2i', { width: payloadBase.width || 768, height: payloadBase.height || 768 }, payloadBase.positive, payloadBase.negative); } toast(`生成成功 ${oks.length} 张`, "success"); } catch (err) { toast(String(err.message || err), "error"); } finally { setBusy(btn, false); } } // Inpaint async function handleInpaint() { // 保存当前参数到 localStorage saveInpaintParams(); const imgFile = byId("inpaint-image").files?.[0]; const maskFile = byId("inpaint-mask").files?.[0]; if (!imgFile || !maskFile) { toast("请上传底图与遮罩", "error"); return; } const image_base64 = await fileToBase64(imgFile); const mask_base64 = await fileToBase64(maskFile); const payloadBase = { positive: byId("inpaint-positive").value || "", negative: byId("inpaint-negative").value || "", image_base64, mask_base64, add_original_image: byId("inpaint-add-original").checked, width: byId("inpaint-width").value ? ensureX64(byId("inpaint-width").value) : null, height: byId("inpaint-height").value ? ensureX64(byId("inpaint-height").value) : null, steps: byId("inpaint-steps").value ? Number(byId("inpaint-steps").value) : null, scale: byId("inpaint-scale").value ? Number(byId("inpaint-scale").value) : null, sampler: nullIfEmpty(byId("inpaint-sampler").value), noise_schedule: nullIfEmpty(byId("inpaint-noise-schedule").value), seed: byId("inpaint-seed").value ? Number(byId("inpaint-seed").value) : -1, strength: byId("inpaint-strength").value ? Number(byId("inpaint-strength").value) : 0.5, noise: byId("inpaint-noise-val").value ? Number(byId("inpaint-noise-val").value) : 0.0, variety: byId("inpaint-variety").checked, decrisp: byId("inpaint-decrisp").checked, cfg_rescale: byId("inpaint-cfg-rescale").value ? Number(byId("inpaint-cfg-rescale").value) : null, }; if (!payloadBase.positive.trim()) { toast("请填写 Positive", "error"); return; } const btn = byId("btn-inpaint"); setBusy(btn, true); const count = getCount("inpaint-count", 1); try { const tasks = Array.from({ length: count }, async () => { const res = await fetch("/api/generate/inpaint", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payloadBase), }); const txt = await res.text(); let body; try { body = JSON.parse(txt); } catch { throw new Error(txt); } if (!res.ok) throw new Error(body?.detail || "生成失败"); return body; }); const settled = await Promise.allSettled(tasks); const oks = settled.filter(s => s.status === "fulfilled").map(s => s.value); if (!oks.length) throw settled[0]?.reason || new Error("生成失败"); const grid = ensureGrid("tab-inpaint", "inpaint-grid", "inpaint-img"); if (grid) { grid.innerHTML = ""; oks.forEach(o => { const im = document.createElement("img"); im.src = o.image_base64; im.alt = "生成结果"; im.style.width = "100%"; im.style.borderRadius = "12px"; im.style.border = "1px solid var(--border)"; grid.appendChild(im); }); } else { const imgEl = byId("inpaint-img"); if (imgEl) imgEl.src = oks[0].image_base64; } const paths = oks.map(o => o.saved_path || "").filter(Boolean).join(" | "); if (byId("inpaint-saved")) byId("inpaint-saved").value = paths; // 显示第一张图片的信息 if (oks.length > 0) { displayImageInfo('inpaint', { width: payloadBase.width || 768, height: payloadBase.height || 768 }, payloadBase.positive, payloadBase.negative); } toast(`生成成功 ${oks.length} 张`, "success"); } catch (err) { toast(String(err.message || err), "error"); } finally { setBusy(btn, false); } } // Bindings // 为每个 Tab 注入"数量"输入(默认 1,可调至 8) ensureCountField("tab-t2i", "t2i-count"); ensureCountField("tab-i2i", "i2i-count"); ensureCountField("tab-inpaint", "inpaint-count"); // ===== 选择保存目录 ===== // 使用 File System Access API(支持的浏览器)或手动输入 async function selectOutputDirectory() { // 尝试使用浏览器原生目录选择器 if ('showDirectoryPicker' in window) { try { const dirHandle = await window.showDirectoryPicker({ mode: 'readwrite' }); // 获取目录名称作为路径提示 const input = byId("cfg-output-dir"); if (input) { input.value = dirHandle.name; autoSaveConfig(); toast("已选择目录: " + dirHandle.name + "(注意:这是本地目录名,服务器需手动填写完整路径)", "info"); } } catch (e) { // 用户取消或不支持 if (e.name !== 'AbortError') { toast("请在输入框中直接填写服务器保存路径", "info"); } } } else { // 不支持目录选择API,提示用户手动输入 toast("请在输入框中直接填写保存目录路径", "info"); byId("cfg-output-dir")?.focus(); } } // 选择保存目录按钮 const selOutBtn = byId("btn-select-output-dir"); if (selOutBtn) { selOutBtn.addEventListener("click", selectOutputDirectory); } // 打开保存目录按钮 const openOutBtn = byId("btn-open-output-dir"); if (openOutBtn) { openOutBtn.addEventListener("click", async () => { try { loading.show(); const p = byId("cfg-output-dir")?.value || ""; if (!p) { toast("请先填写保存目录路径", "error"); return; } const res = await fetch("/api/open-dir", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ path: p }), }); if (!res.ok) { const body = await res.json().catch(() => ({})); throw new Error(body?.detail || "打开目录失败"); } toast("已打开保存目录", "success"); } catch (e) { toast(String(e?.message || e), "error"); } finally { loading.hide(); } }); } byId("btn-t2i").addEventListener("click", handleT2I); byId("btn-i2i").addEventListener("click", handleI2I); byId("btn-inpaint").addEventListener("click", handleInpaint); // 设置图片信息展开/收起功能 setupImageInfoToggle(); // 设置保存图片按钮 setupSaveButtons(); // 设置自定义背景功能 setupCustomBackground(); // 设置配置自动保存 setupAutoSaveConfig(); // Init - 加载配置、恢复上次的生成参数、加载自定义背景 loadConfig(); loadCustomBackground(); restoreT2IParams(); restoreI2IParams(); restoreInpaintParams(); })();