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组件失败");
}
}