mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-21 21:22:11 -03:00
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
This commit is contained in:
75
tests/frontend/versionDetection.test.js
Normal file
75
tests/frontend/versionDetection.test.js
Normal file
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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 `
|
||||
|
||||
Reference in New Issue
Block a user