From 5c29e26c4ecb2feac2110175ba3325dd10189e6b Mon Sep 17 00:00:00 2001 From: Will Miao Date: Thu, 12 Mar 2026 07:41:29 +0800 Subject: [PATCH] fix(top-menu): add backward compatibility for actionBarButtons API (#853) - Implement version detection using __COMFYUI_FRONTEND_VERSION__ and /system_stats API - Add version parsing and comparison utilities - Dynamically register extension based on frontend version - Use actionBarButtons API for frontend >= 1.33.9 - Fallback to legacy ComfyButton approach for older versions - Add comprehensive version detection tests --- tests/frontend/versionDetection.test.js | 75 ++++++++ web/comfyui/top_menu_extension.js | 236 ++++++++++++++++++------ 2 files changed, 258 insertions(+), 53 deletions(-) create mode 100644 tests/frontend/versionDetection.test.js diff --git a/tests/frontend/versionDetection.test.js b/tests/frontend/versionDetection.test.js new file mode 100644 index 00000000..e43b2213 --- /dev/null +++ b/tests/frontend/versionDetection.test.js @@ -0,0 +1,75 @@ +import { describe, it, expect } from 'vitest'; + +describe('Version Detection Logic', () => { + const parseVersion = (versionStr) => { + if (!versionStr || typeof versionStr !== 'string') { + return [0, 0, 0]; + } + + const cleanVersion = versionStr.replace(/^[vV]/, '').split('-')[0]; + const parts = cleanVersion.split('.').map(part => parseInt(part, 10) || 0); + + while (parts.length < 3) { + parts.push(0); + } + + return parts; + }; + + const compareVersions = (version1, version2) => { + const v1 = typeof version1 === 'string' ? parseVersion(version1) : version1; + const v2 = typeof version2 === 'string' ? parseVersion(version2) : version2; + + for (let i = 0; i < 3; i++) { + if (v1[i] > v2[i]) return 1; + if (v1[i] < v2[i]) return -1; + } + + return 0; + }; + + const MIN_VERSION_FOR_ACTION_BAR = [1, 33, 9]; + + const supportsActionBarButtons = (version) => { + return compareVersions(version, MIN_VERSION_FOR_ACTION_BAR) >= 0; + }; + + it('should parse version strings correctly', () => { + expect(parseVersion('1.33.9')).toEqual([1, 33, 9]); + expect(parseVersion('v1.33.9')).toEqual([1, 33, 9]); + expect(parseVersion('1.33.9-beta')).toEqual([1, 33, 9]); + expect(parseVersion('1.33')).toEqual([1, 33, 0]); + expect(parseVersion('1')).toEqual([1, 0, 0]); + expect(parseVersion('')).toEqual([0, 0, 0]); + expect(parseVersion(null)).toEqual([0, 0, 0]); + }); + + it('should compare versions correctly', () => { + expect(compareVersions('1.33.9', '1.33.9')).toBe(0); + expect(compareVersions('1.33.10', '1.33.9')).toBe(1); + expect(compareVersions('1.34.0', '1.33.9')).toBe(1); + expect(compareVersions('2.0.0', '1.33.9')).toBe(1); + expect(compareVersions('1.33.8', '1.33.9')).toBe(-1); + expect(compareVersions('1.32.0', '1.33.9')).toBe(-1); + expect(compareVersions('0.9.9', '1.33.9')).toBe(-1); + }); + + it('should return false for versions below 1.33.9', () => { + expect(supportsActionBarButtons('1.33.8')).toBe(false); + expect(supportsActionBarButtons('1.32.0')).toBe(false); + expect(supportsActionBarButtons('0.9.9')).toBe(false); + }); + + it('should return true for versions 1.33.9 and above', () => { + expect(supportsActionBarButtons('1.33.9')).toBe(true); + expect(supportsActionBarButtons('1.33.10')).toBe(true); + expect(supportsActionBarButtons('1.34.0')).toBe(true); + expect(supportsActionBarButtons('2.0.0')).toBe(true); + }); + + it('should handle edge cases in version parsing', () => { + expect(supportsActionBarButtons('v1.33.9')).toBe(true); + expect(supportsActionBarButtons('1.33.9-rc.1')).toBe(true); + expect(supportsActionBarButtons('1.33.9-beta')).toBe(true); + }); +}); diff --git a/web/comfyui/top_menu_extension.js b/web/comfyui/top_menu_extension.js index ec3e8855..b5e5446a 100644 --- a/web/comfyui/top_menu_extension.js +++ b/web/comfyui/top_menu_extension.js @@ -1,8 +1,14 @@ import { app } from "../../scripts/app.js"; +import { ComfyButtonGroup } from "../../scripts/ui/components/buttonGroup.js"; +import { ComfyButton } from "../../scripts/ui/components/button.js"; const BUTTON_TOOLTIP = "Launch LoRA Manager (Shift+Click opens in new window)"; const LORA_MANAGER_PATH = "/loras"; const NEW_WINDOW_FEATURES = "width=1200,height=800,resizable=yes,scrollbars=yes,status=yes"; +const MAX_ATTACH_ATTEMPTS = 120; +const BUTTON_GROUP_CLASS = "lora-manager-top-menu-group"; + +const MIN_VERSION_FOR_ACTION_BAR = [1, 33, 9]; const openLoraManager = (event) => { const url = `${window.location.origin}${LORA_MANAGER_PATH}`; @@ -15,6 +21,65 @@ const openLoraManager = (event) => { window.open(url, "_blank"); }; +const getComfyUIFrontendVersion = async () => { + try { + if (window['__COMFYUI_FRONTEND_VERSION__']) { + return window['__COMFYUI_FRONTEND_VERSION__']; + } + } catch (error) { + console.warn("LoRA Manager: unable to read __COMFYUI_FRONTEND_VERSION__:", error); + } + + try { + const response = await fetch("/system_stats"); + const data = await response.json(); + + if (data?.system?.comfyui_frontend_version) { + return data.system.comfyui_frontend_version; + } + + if (data?.system?.required_frontend_version) { + return data.system.required_frontend_version; + } + } catch (error) { + console.warn("LoRA Manager: unable to fetch system_stats:", error); + } + + return "0.0.0"; +}; + +const parseVersion = (versionStr) => { + if (!versionStr || typeof versionStr !== 'string') { + return [0, 0, 0]; + } + + const cleanVersion = versionStr.replace(/^[vV]/, '').split('-')[0]; + const parts = cleanVersion.split('.').map(part => parseInt(part, 10) || 0); + + while (parts.length < 3) { + parts.push(0); + } + + return parts; +}; + +const compareVersions = (version1, version2) => { + const v1 = typeof version1 === 'string' ? parseVersion(version1) : version1; + const v2 = typeof version2 === 'string' ? parseVersion(version2) : version2; + + for (let i = 0; i < 3; i++) { + if (v1[i] > v2[i]) return 1; + if (v1[i] < v2[i]) return -1; + } + + return 0; +}; + +const supportsActionBarButtons = async () => { + const version = await getComfyUIFrontendVersion(); + return compareVersions(version, MIN_VERSION_FOR_ACTION_BAR) >= 0; +}; + const fetchVersionInfo = async () => { try { const response = await fetch("/api/lm/version-info"); @@ -30,6 +95,51 @@ const fetchVersionInfo = async () => { return ""; }; +const createTopMenuButton = () => { + const button = new ComfyButton({ + icon: "loramanager", + tooltip: BUTTON_TOOLTIP, + app, + enabled: true, + classList: "comfyui-button comfyui-menu-mobile-collapse primary", + }); + + button.element.setAttribute("aria-label", BUTTON_TOOLTIP); + button.element.title = BUTTON_TOOLTIP; + + if (button.iconElement) { + button.iconElement.innerHTML = getLoraManagerIcon(); + button.iconElement.style.width = "1.2rem"; + button.iconElement.style.height = "1.2rem"; + } + + button.element.addEventListener("click", openLoraManager); + return button; +}; + +const attachTopMenuButton = (attempt = 0) => { + if (document.querySelector(`.${BUTTON_GROUP_CLASS}`)) { + return; + } + + const settingsGroup = app.menu?.settingsGroup; + if (!settingsGroup?.element?.parentElement) { + if (attempt >= MAX_ATTACH_ATTEMPTS) { + console.warn("LoRA Manager: unable to locate the ComfyUI settings button group."); + return; + } + + requestAnimationFrame(() => attachTopMenuButton(attempt + 1)); + return; + } + + const loraManagerButton = createTopMenuButton(); + const buttonGroup = new ComfyButtonGroup(loraManagerButton); + buttonGroup.element.classList.add(BUTTON_GROUP_CLASS); + + settingsGroup.element.before(buttonGroup.element); +}; + const createAboutBadge = (version) => { const label = version ? `LoRA Manager v${version}` : "LoRA Manager"; @@ -40,60 +150,80 @@ const createAboutBadge = (version) => { }; }; -app.registerExtension({ - name: "LoraManager.TopMenu", - actionBarButtons: [ - { - icon: "icon-[mdi--alpha-l-box] size-4", - tooltip: BUTTON_TOOLTIP, - onClick: openLoraManager - } - ], - aboutPageBadges: [createAboutBadge()], - async setup() { - const version = await fetchVersionInfo(); - this.aboutPageBadges = [createAboutBadge(version)]; - - const injectStyles = () => { - const styleId = 'lm-top-menu-button-styles'; - if (document.getElementById(styleId)) return; - - const style = document.createElement('style'); - style.id = styleId; - style.textContent = ` - button[aria-label="Launch LoRA Manager (Shift+Click opens in new window)"].lm-top-menu-button { - transition: all 0.2s ease; - border: 1px solid transparent; - } - button[aria-label="Launch LoRA Manager (Shift+Click opens in new window)"].lm-top-menu-button:hover { - background-color: var(--primary-hover-bg) !important; - } - `; - document.head.appendChild(style); - }; - injectStyles(); - - const replaceButtonIcon = () => { - const buttons = document.querySelectorAll('button[aria-label="Launch LoRA Manager (Shift+Click opens in new window)"]'); - buttons.forEach(button => { - button.classList.add('lm-top-menu-button'); - button.innerHTML = getLoraManagerIcon(); - button.style.borderRadius = '4px'; - button.style.padding = '6px'; - button.style.backgroundColor = 'var(--primary-bg)'; - const svg = button.querySelector('svg'); - if (svg) { - svg.style.width = '20px'; - svg.style.height = '20px'; - } - }); - if (buttons.length === 0) { - requestAnimationFrame(replaceButtonIcon); +const createExtensionObject = (useActionBar) => { + const extensionObj = { + name: "LoraManager.TopMenu", + async setup() { + const version = await fetchVersionInfo(); + + if (!useActionBar) { + console.log("LoRA Manager: using legacy button attachment (frontend version < 1.33.9)"); + attachTopMenuButton(); + } else { + console.log("LoRA Manager: using actionBarButtons API (frontend version >= 1.33.9)"); } - }; - requestAnimationFrame(replaceButtonIcon); - }, -}); + + this.aboutPageBadges = [createAboutBadge(version)]; + + const injectStyles = () => { + const styleId = 'lm-top-menu-button-styles'; + if (document.getElementById(styleId)) return; + + const style = document.createElement('style'); + style.id = styleId; + style.textContent = ` + button[aria-label="Launch LoRA Manager (Shift+Click opens in new window)"].lm-top-menu-button { + transition: all 0.2s ease; + border: 1px solid transparent; + } + button[aria-label="Launch LoRA Manager (Shift+Click opens in new window)"].lm-top-menu-button:hover { + background-color: var(--primary-hover-bg) !important; + } + `; + document.head.appendChild(style); + }; + injectStyles(); + + const replaceButtonIcon = () => { + const buttons = document.querySelectorAll('button[aria-label="Launch LoRA Manager (Shift+Click opens in new window)"]'); + buttons.forEach(button => { + button.classList.add('lm-top-menu-button'); + button.innerHTML = getLoraManagerIcon(); + button.style.borderRadius = '4px'; + button.style.padding = '6px'; + button.style.backgroundColor = 'var(--primary-bg)'; + const svg = button.querySelector('svg'); + if (svg) { + svg.style.width = '20px'; + svg.style.height = '20px'; + } + }); + if (buttons.length === 0) { + requestAnimationFrame(replaceButtonIcon); + } + }; + requestAnimationFrame(replaceButtonIcon); + }, + }; + + if (useActionBar) { + extensionObj.actionBarButtons = [ + { + icon: "icon-[mdi--alpha-l-box] size-4", + tooltip: BUTTON_TOOLTIP, + onClick: openLoraManager + } + ]; + } + + return extensionObj; +}; + +(async () => { + const useActionBar = await supportsActionBarButtons(); + const extensionObj = createExtensionObject(useActionBar); + app.registerExtension(extensionObj); +})(); const getLoraManagerIcon = () => { return `