mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-21 21:22:11 -03:00
Compare commits
2 Commits
ee765a6d22
...
4fcf641d57
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4fcf641d57 | ||
|
|
5c29e26c4e |
@@ -117,7 +117,10 @@ export class BulkContextMenu extends BaseContextMenu {
|
|||||||
countSkipStatus(skipState) {
|
countSkipStatus(skipState) {
|
||||||
let count = 0;
|
let count = 0;
|
||||||
for (const filePath of state.selectedModels) {
|
for (const filePath of state.selectedModels) {
|
||||||
const card = document.querySelector(`.model-card[data-filepath="${filePath}"]`);
|
const escapedPath = window.CSS && typeof window.CSS.escape === 'function'
|
||||||
|
? window.CSS.escape(filePath)
|
||||||
|
: filePath.replace(/["\\]/g, '\\$&');
|
||||||
|
const card = document.querySelector(`.model-card[data-filepath="${escapedPath}"]`);
|
||||||
if (card) {
|
if (card) {
|
||||||
const isSkipped = card.dataset.skip_metadata_refresh === 'true';
|
const isSkipped = card.dataset.skip_metadata_refresh === 'true';
|
||||||
if (isSkipped === skipState) {
|
if (isSkipped === skipState) {
|
||||||
|
|||||||
@@ -201,8 +201,9 @@ class RecipeCard {
|
|||||||
this.recipe.favorite = isFavorite;
|
this.recipe.favorite = isFavorite;
|
||||||
|
|
||||||
// Re-find star icon in case of re-render during fault
|
// Re-find star icon in case of re-render during fault
|
||||||
|
const filePathForXpath = this.recipe.file_path.replace(/"/g, '"');
|
||||||
const currentCard = card.ownerDocument.evaluate(
|
const currentCard = card.ownerDocument.evaluate(
|
||||||
`.//*[@data-filepath="${this.recipe.file_path}"]`,
|
`.//*[@data-filepath="${filePathForXpath}"]`,
|
||||||
card.ownerDocument, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null
|
card.ownerDocument, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null
|
||||||
).singleNodeValue || card;
|
).singleNodeValue || card;
|
||||||
|
|
||||||
|
|||||||
@@ -846,8 +846,14 @@ function setupLoraSpecificFields(filePath) {
|
|||||||
|
|
||||||
const currentPath = resolveFilePath();
|
const currentPath = resolveFilePath();
|
||||||
if (!currentPath) return;
|
if (!currentPath) return;
|
||||||
const loraCard = document.querySelector(`.model-card[data-filepath="${currentPath}"]`) ||
|
const escapedCurrentPath = window.CSS && typeof window.CSS.escape === 'function'
|
||||||
document.querySelector(`.model-card[data-filepath="${filePath}"]`);
|
? window.CSS.escape(currentPath)
|
||||||
|
: currentPath.replace(/["\\]/g, '\\$&');
|
||||||
|
const escapedFilePath = window.CSS && typeof window.CSS.escape === 'function'
|
||||||
|
? window.CSS.escape(filePath)
|
||||||
|
: filePath.replace(/["\\]/g, '\\$&');
|
||||||
|
const loraCard = document.querySelector(`.model-card[data-filepath="${escapedCurrentPath}"]`) ||
|
||||||
|
document.querySelector(`.model-card[data-filepath="${escapedFilePath}"]`);
|
||||||
const currentPresets = parsePresets(loraCard?.dataset.usage_tips);
|
const currentPresets = parsePresets(loraCard?.dataset.usage_tips);
|
||||||
|
|
||||||
if (key === 'strength_range') {
|
if (key === 'strength_range') {
|
||||||
|
|||||||
@@ -49,7 +49,10 @@ function formatPresetKey(key) {
|
|||||||
*/
|
*/
|
||||||
window.removePreset = async function(key) {
|
window.removePreset = async function(key) {
|
||||||
const filePath = document.querySelector('#modelModal .modal-content .file-path').dataset.filepath;
|
const filePath = document.querySelector('#modelModal .modal-content .file-path').dataset.filepath;
|
||||||
const loraCard = document.querySelector(`.model-card[data-filepath="${filePath}"]`);
|
const escapedPath = window.CSS && typeof window.CSS.escape === 'function'
|
||||||
|
? window.CSS.escape(filePath)
|
||||||
|
: filePath.replace(/["\\]/g, '\\$&');
|
||||||
|
const loraCard = document.querySelector(`.model-card[data-filepath="${escapedPath}"]`);
|
||||||
const currentPresets = parsePresets(loraCard.dataset.usage_tips);
|
const currentPresets = parsePresets(loraCard.dataset.usage_tips);
|
||||||
|
|
||||||
delete currentPresets[key];
|
delete currentPresets[key];
|
||||||
|
|||||||
@@ -568,7 +568,8 @@ export class BulkManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
deselectItem(filepath) {
|
deselectItem(filepath) {
|
||||||
const card = document.querySelector(`.model-card[data-filepath="${filepath}"]`);
|
const escapedPath = this.escapeAttributeValue(filepath);
|
||||||
|
const card = document.querySelector(`.model-card[data-filepath="${escapedPath}"]`);
|
||||||
if (card) {
|
if (card) {
|
||||||
card.classList.remove('selected');
|
card.classList.remove('selected');
|
||||||
}
|
}
|
||||||
@@ -632,7 +633,8 @@ export class BulkManager {
|
|||||||
for (const filepath of state.selectedModels) {
|
for (const filepath of state.selectedModels) {
|
||||||
const metadata = metadataCache.get(filepath);
|
const metadata = metadataCache.get(filepath);
|
||||||
if (metadata) {
|
if (metadata) {
|
||||||
const card = document.querySelector(`.model-card[data-filepath="${filepath}"]`);
|
const escapedPath = this.escapeAttributeValue(filepath);
|
||||||
|
const card = document.querySelector(`.model-card[data-filepath="${escapedPath}"]`);
|
||||||
if (card) {
|
if (card) {
|
||||||
this.updateMetadataCacheFromCard(filepath, card);
|
this.updateMetadataCacheFromCard(filepath, card);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,10 @@ let pendingExcludePath = null;
|
|||||||
export function showDeleteModal(filePath) {
|
export function showDeleteModal(filePath) {
|
||||||
pendingDeletePath = filePath;
|
pendingDeletePath = filePath;
|
||||||
|
|
||||||
const card = document.querySelector(`.model-card[data-filepath="${filePath}"]`);
|
const escapedPath = window.CSS && typeof window.CSS.escape === 'function'
|
||||||
|
? window.CSS.escape(filePath)
|
||||||
|
: filePath.replace(/["\\]/g, '\\$&');
|
||||||
|
const card = document.querySelector(`.model-card[data-filepath="${escapedPath}"]`);
|
||||||
const modelName = card ? card.dataset.name : filePath.split('/').pop();
|
const modelName = card ? card.dataset.name : filePath.split('/').pop();
|
||||||
const modal = modalManager.getModal('deleteModal').element;
|
const modal = modalManager.getModal('deleteModal').element;
|
||||||
const modelInfo = modal.querySelector('.delete-model-info');
|
const modelInfo = modal.querySelector('.delete-model-info');
|
||||||
@@ -47,7 +50,10 @@ export function closeDeleteModal() {
|
|||||||
export function showExcludeModal(filePath) {
|
export function showExcludeModal(filePath) {
|
||||||
pendingExcludePath = filePath;
|
pendingExcludePath = filePath;
|
||||||
|
|
||||||
const card = document.querySelector(`.model-card[data-filepath="${filePath}"]`);
|
const escapedPath = window.CSS && typeof window.CSS.escape === 'function'
|
||||||
|
? window.CSS.escape(filePath)
|
||||||
|
: filePath.replace(/["\\]/g, '\\$&');
|
||||||
|
const card = document.querySelector(`.model-card[data-filepath="${escapedPath}"]`);
|
||||||
const modelName = card ? card.dataset.name : filePath.split('/').pop();
|
const modelName = card ? card.dataset.name : filePath.split('/').pop();
|
||||||
const modal = modalManager.getModal('excludeModal').element;
|
const modal = modalManager.getModal('excludeModal').element;
|
||||||
const modelInfo = modal.querySelector('.exclude-model-info');
|
const modelInfo = modal.querySelector('.exclude-model-info');
|
||||||
|
|||||||
@@ -197,7 +197,10 @@ export function openCivitaiByMetadata(civitaiId, versionId, modelName = null) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function openCivitai(filePath) {
|
export function openCivitai(filePath) {
|
||||||
const loraCard = document.querySelector(`.model-card[data-filepath="${filePath}"]`);
|
const escapedPath = window.CSS && typeof window.CSS.escape === 'function'
|
||||||
|
? window.CSS.escape(filePath)
|
||||||
|
: filePath.replace(/["\\]/g, '\\$&');
|
||||||
|
const loraCard = document.querySelector(`.model-card[data-filepath="${escapedPath}"]`);
|
||||||
if (!loraCard) return;
|
if (!loraCard) return;
|
||||||
|
|
||||||
const metaData = JSON.parse(loraCard.dataset.meta);
|
const metaData = JSON.parse(loraCard.dataset.meta);
|
||||||
|
|||||||
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 { 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 BUTTON_TOOLTIP = "Launch LoRA Manager (Shift+Click opens in new window)";
|
||||||
const LORA_MANAGER_PATH = "/loras";
|
const LORA_MANAGER_PATH = "/loras";
|
||||||
const NEW_WINDOW_FEATURES = "width=1200,height=800,resizable=yes,scrollbars=yes,status=yes";
|
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 openLoraManager = (event) => {
|
||||||
const url = `${window.location.origin}${LORA_MANAGER_PATH}`;
|
const url = `${window.location.origin}${LORA_MANAGER_PATH}`;
|
||||||
@@ -15,6 +21,65 @@ const openLoraManager = (event) => {
|
|||||||
window.open(url, "_blank");
|
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 () => {
|
const fetchVersionInfo = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch("/api/lm/version-info");
|
const response = await fetch("/api/lm/version-info");
|
||||||
@@ -30,6 +95,51 @@ const fetchVersionInfo = async () => {
|
|||||||
return "";
|
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 createAboutBadge = (version) => {
|
||||||
const label = version ? `LoRA Manager v${version}` : "LoRA Manager";
|
const label = version ? `LoRA Manager v${version}` : "LoRA Manager";
|
||||||
|
|
||||||
@@ -40,18 +150,19 @@ const createAboutBadge = (version) => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
app.registerExtension({
|
const createExtensionObject = (useActionBar) => {
|
||||||
|
const extensionObj = {
|
||||||
name: "LoraManager.TopMenu",
|
name: "LoraManager.TopMenu",
|
||||||
actionBarButtons: [
|
|
||||||
{
|
|
||||||
icon: "icon-[mdi--alpha-l-box] size-4",
|
|
||||||
tooltip: BUTTON_TOOLTIP,
|
|
||||||
onClick: openLoraManager
|
|
||||||
}
|
|
||||||
],
|
|
||||||
aboutPageBadges: [createAboutBadge()],
|
|
||||||
async setup() {
|
async setup() {
|
||||||
const version = await fetchVersionInfo();
|
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)");
|
||||||
|
}
|
||||||
|
|
||||||
this.aboutPageBadges = [createAboutBadge(version)];
|
this.aboutPageBadges = [createAboutBadge(version)];
|
||||||
|
|
||||||
const injectStyles = () => {
|
const injectStyles = () => {
|
||||||
@@ -93,7 +204,26 @@ app.registerExtension({
|
|||||||
};
|
};
|
||||||
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 = () => {
|
const getLoraManagerIcon = () => {
|
||||||
return `
|
return `
|
||||||
|
|||||||
Reference in New Issue
Block a user