diff --git a/__init__.py b/__init__.py index 1e7f373..bb44ec5 100644 --- a/__init__.py +++ b/__init__.py @@ -16,16 +16,23 @@ CUR_PATH = Path(__file__).parent # 读取配置文件 def load_config(): config_path = CUR_PATH.joinpath("config.json") + default_cfg = {"translation_enabled": True, "need_ui_component": True, "ui_position": None} if config_path.exists(): try: config_data = try_get_json(config_path) - return config_data.get("translation_enabled", True) + if not isinstance(config_data, dict): + return default_cfg + return { + "translation_enabled": config_data.get("translation_enabled", True), + "need_ui_component": config_data.get("need_ui_component", True), + "ui_position": config_data.get("ui_position", None), + } except Exception: - return True - return True + return default_cfg + return default_cfg # 全局配置变量 -TRANSLATION_ENABLED = load_config() +TRANSLATION_ENABLED = load_config().get("translation_enabled", True) def try_get_json(path: Path): @@ -108,8 +115,8 @@ async def get_translation(request: web.Request): headers = {} # 实时检查配置文件中的翻译开关 - current_enabled = load_config() - if not current_enabled: + current_cfg = load_config() + if not current_cfg.get("translation_enabled", True): return web.Response(status=200, body=json_data, headers=headers) try: @@ -126,8 +133,7 @@ async def get_translation(request: web.Request): @server.PromptServer.instance.routes.get("/agl/get_config") async def get_config(request: web.Request): # 实时读取配置文件 - current_enabled = load_config() - config_data = {"translation_enabled": current_enabled} + config_data = load_config() return web.Response(status=200, body=json.dumps(config_data), headers={"Content-Type": "application/json"}) @@ -135,21 +141,47 @@ async def get_config(request: web.Request): async def set_config(request: web.Request): try: post = await request.post() - enabled = post.get("translation_enabled", "true").lower() == "true" + enabled = post.get("translation_enabled", None) + ui_needed = post.get("need_ui_component", None) + ui_pos = post.get("ui_position", None) + + enabled_val = (str(enabled).lower() == "true") if enabled is not None else None + ui_needed_val = (str(ui_needed).lower() == "true") if ui_needed is not None else None + + # ui_position might be a json string or x,y string, keep as string or None + # But if it comes as "null" or empty, treat as None + ui_pos_val = ui_pos if ui_pos and str(ui_pos).lower() != "null" else None # 更新配置文件 config_path = CUR_PATH.joinpath("config.json") - config_data = {"translation_enabled": enabled} + current_cfg = load_config() + + if enabled_val is None: + enabled_val = current_cfg.get("translation_enabled", True) + if ui_needed_val is None: + ui_needed_val = current_cfg.get("need_ui_component", True) + if ui_pos_val is None and "ui_position" in current_cfg: + ui_pos_val = current_cfg.get("ui_position", None) + + config_data = { + "translation_enabled": enabled_val, + "need_ui_component": ui_needed_val, + "ui_position": ui_pos_val + } with open(config_path, 'w', encoding='utf-8') as f: json.dump(config_data, f, indent=2, ensure_ascii=False) # 更新全局变量 global TRANSLATION_ENABLED - TRANSLATION_ENABLED = enabled + TRANSLATION_ENABLED = enabled_val - return web.Response(status=200, body=json.dumps({"success": True, "translation_enabled": enabled}), - headers={"Content-Type": "application/json"}) + return web.Response(status=200, body=json.dumps({ + "success": True, + "translation_enabled": enabled_val, + "need_ui_component": ui_needed_val, + "ui_position": ui_pos_val + }), headers={"Content-Type": "application/json"}) except Exception as e: return web.Response(status=500, body=json.dumps({"success": False, "error": str(e)}), headers={"Content-Type": "application/json"}) diff --git a/config.json b/config.json index 889a8e5..bf50f39 100644 --- a/config.json +++ b/config.json @@ -1,3 +1,4 @@ { - "translation_enabled": true + "translation_enabled": true, + "need_ui_component": true } diff --git a/js/main.js b/js/main.js index 3d32a57..2bb167f 100644 --- a/js/main.js +++ b/js/main.js @@ -6,12 +6,15 @@ import { hasNativeTranslation, nativeTranslatedSettings, isTranslationEnabled, - toggleTranslation, + isNeedUIComponentEnabled, + setTranslationEnabled, + setNeedUIComponentEnabled, initConfig, error, isVueNodes2, applySuffixHeuristic } from "./utils.js"; +import { ensureTranslationModeToggleButton } from "./ui.js"; export class TUtils { static T = { @@ -603,19 +606,62 @@ const ext = { }, async setup(app) { try { + await initConfig(); const isComfyUIChineseNative = document.documentElement.lang === 'zh-CN'; + const translationSettingId = "🌐翻译设置.语言开关.Enable"; + const uiComponentSettingId = "🌐翻译设置.前端UI组件.Enable"; + let ignoreTranslationSettingChange = false; + let ignoreUIComponentSettingChange = false; + + try { + if (app?.ui?.settings?.setSettingValue) { + app.ui.settings.setSettingValue(translationSettingId, isTranslationEnabled()); + app.ui.settings.setSettingValue(uiComponentSettingId, isNeedUIComponentEnabled()); + } + } catch {} app.ui.settings.addSetting({ - id: "🌐翻译设置.语言开关.Enable", + id: translationSettingId, name: "是否开启附加翻译", type: "boolean", defaultValue: isTranslationEnabled(), onChange: async (value) => { + if (ignoreTranslationSettingChange) return; if (value !== isTranslationEnabled()) { - await toggleTranslation(); + await setTranslationEnabled(value); } }, }); + + app.ui.settings.addSetting({ + id: uiComponentSettingId, + name: "是否需要前端UI组件", + type: "boolean", + defaultValue: isNeedUIComponentEnabled(), + onChange: async (value) => { + if (ignoreUIComponentSettingChange) return; + if (value !== isNeedUIComponentEnabled()) { + await setNeedUIComponentEnabled(value); + } + }, + }); + + if (isNeedUIComponentEnabled()) { + ensureTranslationModeToggleButton(isTranslationEnabled(), async () => { + const newEnabled = !isTranslationEnabled(); + try { + if (app?.ui?.settings?.setSettingValue) { + ignoreTranslationSettingChange = true; + app.ui.settings.setSettingValue(translationSettingId, newEnabled); + } + } catch {} + try { + await setTranslationEnabled(newEnabled); + } finally { + ignoreTranslationSettingChange = false; + } + }); + } if (isTranslationEnabled()) { if (!isVueNodes2()) { diff --git a/js/ui.js b/js/ui.js new file mode 100644 index 0000000..748c6be --- /dev/null +++ b/js/ui.js @@ -0,0 +1,356 @@ +import { getUIPosition, setUIPosition } from "./utils.js"; + +export function ensureTranslationModeToggleButton(isEnabled, onToggle) { + const buttonId = "dd-translation-mode-toggle"; + let button = document.getElementById(buttonId); + + // 状态样式配置 + const styles = { + addon: { + bg: "#ffffff", + hover: "#f5f5f5", + text: "#333", + border: "1px solid #e5e7eb" + }, + official: { + bg: "#ffffff", + hover: "#f5f5f5", + text: "#333", + border: "1px solid #e5e7eb" + } + }; + const currentStyle = isEnabled ? styles.addon : styles.official; + const labelText = isEnabled ? "附加翻译已开启" : "官方翻译已开启"; + + // SVG 图标定义 + const svgBlue = ` + + + + + + `; + + const svgGreen = ` + + + + + + `; + + const iconSvg = isEnabled ? svgGreen : svgBlue; + + // 如果按钮已存在,更新样式和状态 + if (button) { + const textSpan = button.querySelector("span"); + if (textSpan) textSpan.textContent = labelText; + button.style.background = currentStyle.bg; + button.style.borderColor = currentStyle.border; + button.dataset.mode = isEnabled ? "addon" : "official"; + + // 更新图标 + const iconContainer = button.querySelector("div"); + if (iconContainer) { + iconContainer.innerHTML = iconSvg; + } + + return button; + } + + // 创建按钮 + button = document.createElement("button"); + button.id = buttonId; + button.type = "button"; + button.dataset.mode = isEnabled ? "addon" : "official"; + + button.innerHTML = ` +
${iconSvg}
+ ${labelText} + `; + + // 基础样式 + Object.assign(button.style, { + position: "fixed", + zIndex: "100", + padding: "0", // Reset padding to handle layout manually or via flex + width: "40px", // Initial circle size + height: "40px", + borderRadius: "20px", + border: currentStyle.border, + background: currentStyle.bg, + color: currentStyle.text, + fontSize: "13px", + fontWeight: "500", + cursor: "pointer", // 默认手型,提示可点击 + backdropFilter: "blur(8px)", + boxShadow: "0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)", + transition: "width 0.3s ease, background 0.2s", // Width transition for expansion + userSelect: "none", + fontFamily: "system-ui, -apple-system, sans-serif", + display: "flex", + alignItems: "center", + justifyContent: "center", // 改为居中,确保收起时图标居中 + overflow: "hidden", + paddingLeft: "0", // 移除默认的左内边距 + paddingRight: "0" + }); + + // 封装更新对齐方式的函数 + const updateButtonAlign = (left, top) => { + const winWidth = window.innerWidth; + // 只要不是紧贴左边缘(< 100px),就默认为右侧模式(向左展开),以避让右侧可能存在的UI组件 + const isRightSide = left > 100; + + if (isRightSide) { + // 右侧模式:使用 right 定位 + const rightDist = winWidth - left - 40; + button.style.left = "auto"; + button.style.right = `${rightDist}px`; + button.style.top = `${top}px`; + + button.style.flexDirection = "row-reverse"; + button.style.justifyContent = "flex-start"; // 展开时改为 flex-start + button.style.paddingLeft = "0"; + button.style.paddingRight = "8px"; + button.dataset.align = "right"; + } else { + // 左侧模式:使用 left 定位 + button.style.right = "auto"; + button.style.left = `${left}px`; + button.style.top = `${top}px`; + + button.style.flexDirection = "row"; + button.style.justifyContent = "flex-start"; // 展开时改为 flex-start + button.style.paddingLeft = "8px"; + button.style.paddingRight = "0"; + button.dataset.align = "left"; + } + + // 更新内部文字 margin + const span = button.querySelector("span"); + if (span) { + if (isRightSide) { + span.style.marginLeft = "0"; + span.style.marginRight = "8px"; + } else { + span.style.marginLeft = "8px"; + span.style.marginRight = "0"; + } + } + }; + + // 读取并应用保存的位置 + const savedPos = getUIPosition(); + let hasSetPos = false; + if (savedPos) { + try { + const { top, left, right, bottom } = JSON.parse(savedPos); + + // 尝试恢复并立即应用对齐逻辑 + let initialTop = "50%"; + let initialLeft = "12px"; + + if (top !== undefined) initialTop = top; + if (left !== undefined) initialLeft = left; + + // 处理 right/bottom 的情况,转换为 top/left 以便统一逻辑(如果需要的话) + // 简单起见,我们优先信任 left/top,如果只有 right/bottom 则需要计算 + // 但之前的保存逻辑只保存了 left/top + + button.style.top = initialTop; + button.style.left = initialLeft; + + // 立即计算对齐 + // 注意:此时 button 可能还未 append 到 body,getComputedStyle 可能拿不到像素值 + // 但 savedPos 里的值通常是 px 结尾的字符串 + const leftVal = parseInt(initialLeft) || 12; + const topVal = parseInt(initialTop) || (window.innerHeight / 2); + + updateButtonAlign(leftVal, topVal); + + hasSetPos = true; + } catch (e) { + // 解析失败 + } + } + + if (!hasSetPos) { + // 默认位置 + updateButtonAlign(12, window.innerHeight / 2); + button.style.transform = "translateY(-50%)"; + } + + // 拖拽逻辑 + let isDragging = false; + let hasMoved = false; + let startX, startY; + let initialLeft, initialTop; + + const onMouseDown = (e) => { + if (e.button !== 0) return; + isDragging = true; + hasMoved = false; + startX = e.clientX; + startY = e.clientY; + + const rect = button.getBoundingClientRect(); + initialLeft = rect.left; + initialTop = rect.top; + + // 拖动开始:强制收起、透明度降低、清除 right/bottom 定位 + button.style.right = "auto"; + button.style.bottom = "auto"; + button.style.left = `${initialLeft}px`; + button.style.top = `${initialTop}px`; + button.style.transform = "none"; + button.style.cursor = "grabbing"; + + // 强制保持圆形和收起状态 + button.style.width = "40px"; + button.style.borderRadius = "20px"; + button.style.justifyContent = "center"; // 拖动时强制居中 + button.style.paddingLeft = "0"; // 重置padding,确保图标居中 + button.style.paddingRight = "0"; + button.style.opacity = "0.8"; // 拖动时半透明 + // 禁用过渡,使跟随更跟手 + button.style.transition = "none"; + + const span = button.querySelector("span"); + if (span) { + span.style.opacity = "0"; + span.style.maxWidth = "0"; + span.style.margin = "0"; // 重置文字margin + } + + e.preventDefault(); + document.addEventListener("mousemove", onMouseMove); + document.addEventListener("mouseup", onMouseUp); + }; + + const onMouseMove = (e) => { + if (!isDragging) return; + const dx = e.clientX - startX; + const dy = e.clientY - startY; + + if (Math.abs(dx) > 2 || Math.abs(dy) > 2) { + hasMoved = true; + } + + button.style.left = `${initialLeft + dx}px`; + button.style.top = `${initialTop + dy}px`; + }; + + const onMouseUp = async (e) => { + if (!isDragging) return; + isDragging = false; + button.style.cursor = "pointer"; + button.style.opacity = "1"; + button.style.transition = "width 0.3s ease, background 0.2s"; // 恢复过渡 + + document.removeEventListener("mousemove", onMouseMove); + document.removeEventListener("mouseup", onMouseUp); + + // 吸附边界检查 + const rect = button.getBoundingClientRect(); + const winWidth = window.innerWidth; + const winHeight = window.innerHeight; + + let newLeft = rect.left; + let newTop = rect.top; + + // 吸附与边界限制 - 移除10px margin,允许完全贴边 + if (newLeft < 0) newLeft = 0; + if (newLeft + 40 > winWidth) newLeft = winWidth - 40; + if (newTop < 0) newTop = 0; + if (newTop + rect.height > winHeight) newTop = winHeight - rect.height; + + // 使用统一的对齐更新逻辑 + updateButtonAlign(newLeft, newTop); + + if (hasMoved) { + // 持久化保存坐标(依然保存 left/top 绝对值以便恢复时简单处理,加载时需复用判断逻辑) + // 或者保存 align 状态?不,简单起见只存坐标,加载时再次判断即可。 + // 但这里我们保存的是当前的视觉位置 + const posData = JSON.stringify({ top: `${newTop}px`, left: `${newLeft}px` }); + await setUIPosition(posData); + } + }; + + button.addEventListener("mousedown", onMouseDown); + + // Hover 展开效果 + button.addEventListener("mouseenter", () => { + // 拖拽中不响应 hover + if (isDragging) return; + + const mode = button.dataset.mode; + button.style.background = mode === "addon" ? styles.addon.hover : styles.official.hover; + + const isRightAlign = button.dataset.align === "right"; + + // 展开 + button.style.width = "150px"; // Approximate expanded width + + // 根据对齐方向设置 padding,确保图标位置不动,文字向另一侧展开 + button.style.justifyContent = "flex-start"; // 展开时改为 flex-start + if (isRightAlign) { + button.style.paddingLeft = "14px"; // 增加左侧内边距给文字 + button.style.paddingRight = "8px"; // 保持右侧图标边距 + } else { + button.style.paddingRight = "14px"; // 增加右侧内边距给文字 + button.style.paddingLeft = "8px"; // 保持左侧图标边距 + } + + const span = button.querySelector("span"); + if (span) { + span.style.opacity = "1"; + span.style.maxWidth = "200px"; + } + }); + + button.addEventListener("mouseleave", () => { + // 拖拽中不响应 hover leave + if (isDragging) return; + + const mode = button.dataset.mode; + button.style.background = mode === "addon" ? styles.addon.bg : styles.official.bg; + + const isRightAlign = button.dataset.align === "right"; + + // 收起 + button.style.width = "40px"; + button.style.justifyContent = "center"; // 收起时恢复居中 + if (isRightAlign) { + button.style.paddingLeft = "0"; + button.style.paddingRight = "8px"; + } else { + button.style.paddingRight = "0"; + button.style.paddingLeft = "8px"; + } + + const span = button.querySelector("span"); + if (span) { + span.style.opacity = "0"; + span.style.maxWidth = "0"; + } + }); + + // 点击事件 + button.addEventListener("click", async (e) => { + if (hasMoved || button.disabled) return; + + button.disabled = true; + button.style.opacity = "0.7"; + + try { + await onToggle(); + } finally { + button.disabled = false; + button.style.opacity = "1"; + } + }); + + document.body.appendChild(button); + return button; +} diff --git a/js/utils.js b/js/utils.js index dea67da..0bbde36 100644 --- a/js/utils.js +++ b/js/utils.js @@ -75,6 +75,8 @@ export const nativeTranslatedSettings = [ // 存储当前翻译状态 let currentTranslationEnabled = true; +let currentNeedUIComponent = true; +let currentUIPosition = null; /** * 从配置文件获取翻译状态 @@ -84,22 +86,30 @@ async function loadConfig() { const response = await fetch("./agl/get_config"); if (response.ok) { const config = await response.json(); - currentTranslationEnabled = config.translation_enabled; - return config.translation_enabled; + currentTranslationEnabled = config.translation_enabled ?? true; + currentNeedUIComponent = config.need_ui_component ?? true; + currentUIPosition = config.ui_position ?? null; + return config; } } catch (e) { error("获取配置失败:", e); } - return true; + return { translation_enabled: true, need_ui_component: true, ui_position: null }; } /** * 保存翻译状态到配置文件 */ -async function saveConfig(enabled) { +async function saveConfig(configPatch) { try { const formData = new FormData(); - formData.append('translation_enabled', enabled.toString()); + if (configPatch && typeof configPatch === "object") { + for (const key of Object.keys(configPatch)) { + const value = configPatch[key]; + if (value === undefined || value === null) continue; + formData.append(key, String(value)); + } + } const response = await fetch("./agl/set_config", { method: "POST", @@ -109,7 +119,15 @@ async function saveConfig(enabled) { if (response.ok) { const result = await response.json(); if (result.success) { - currentTranslationEnabled = enabled; + if (typeof result.translation_enabled === "boolean") { + currentTranslationEnabled = result.translation_enabled; + } + if (typeof result.need_ui_component === "boolean") { + currentNeedUIComponent = result.need_ui_component; + } + if (result.ui_position !== undefined) { + currentUIPosition = result.ui_position; + } return true; } } @@ -126,6 +144,21 @@ export function isTranslationEnabled() { return currentTranslationEnabled; } +export function isNeedUIComponentEnabled() { + return currentNeedUIComponent; +} + +export function getUIPosition() { + return currentUIPosition; +} + +export async function setUIPosition(pos) { + // pos string format "top,left" or similar + currentUIPosition = pos; + // Debounce save or save immediately? Save immediately for now as drag ends once. + await saveConfig({ ui_position: pos }); +} + /** * 初始化配置 */ @@ -133,16 +166,29 @@ export async function initConfig() { await loadConfig(); } +export async function setTranslationEnabled(enabled) { + const success = await saveConfig({ translation_enabled: enabled }); + if (success) { + setTimeout(() => location.reload(), 100); + } else { + error("设置翻译状态失败"); + } +} + /** * 切换翻译状态 */ export async function toggleTranslation() { const newEnabled = !currentTranslationEnabled; - const success = await saveConfig(newEnabled); + await setTranslationEnabled(newEnabled); +} + +export async function setNeedUIComponentEnabled(enabled) { + const success = await saveConfig({ need_ui_component: enabled }); if (success) { setTimeout(() => location.reload(), 100); } else { - error("切换翻译状态失败"); + error("切换前端UI组件失败"); } }