Bug fixes: - Fix menu button not appearing: Use app.registerExtension() for proper ComfyUI initialization timing. The button now appears in the top bar next to the Manager button. - Fix source_hint incorrectly saved as source_url: Pattern-matched hints like "Lightricks/LTX-Video" are not valid URLs and should not be used for update checks. - Fix deprecated tempfile.mktemp(): Use secure mkstemp() instead to avoid race conditions. - Add os import for file descriptor handling in update_scheduler.py Test improvements: - Update Playwright tests to hover over UI to reveal ComfyUI menu - All 26 tests now passing (17 unit + 9 UI) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
708 lines
22 KiB
JavaScript
708 lines
22 KiB
JavaScript
/**
|
|
* ModelSync UI Panel for ComfyUI
|
|
*
|
|
* Provides a user interface for:
|
|
* - Viewing tracked models and their update status
|
|
* - Configuring auto-update settings
|
|
* - Managing sync targets
|
|
* - Triggering manual syncs
|
|
*/
|
|
|
|
import { app } from "../../scripts/app.js";
|
|
|
|
class ModelSyncPanel {
|
|
constructor() {
|
|
this.element = null;
|
|
this.models = [];
|
|
this.targets = [];
|
|
this.config = {};
|
|
this.currentTab = "models";
|
|
this.searchQuery = "";
|
|
this.filterStatus = "all"; // all, updates, auto
|
|
this.menuButtonCreated = false;
|
|
}
|
|
|
|
async init() {
|
|
// Load initial data
|
|
await this.refreshData();
|
|
}
|
|
|
|
async createMenuButton() {
|
|
if (this.menuButtonCreated) return;
|
|
|
|
const self = this;
|
|
|
|
// Try to add new-style button (ComfyUI with Vue frontend)
|
|
try {
|
|
const { ComfyButton } = await import("../../scripts/ui/components/button.js");
|
|
const { ComfyButtonGroup } = await import("../../scripts/ui/components/buttonGroup.js");
|
|
|
|
if (app.menu?.settingsGroup) {
|
|
const syncButton = new ComfyButton({
|
|
icon: "sync",
|
|
action: () => self.toggle(),
|
|
tooltip: "Model Sync - Track and sync model updates",
|
|
content: "Model Sync",
|
|
classList: "comfyui-button comfyui-menu-mobile-collapse"
|
|
});
|
|
|
|
const syncGroup = new ComfyButtonGroup(syncButton.element);
|
|
app.menu.settingsGroup.element.before(syncGroup.element);
|
|
this.menuButtonCreated = true;
|
|
console.log("[ModelSync] New-style menu button added");
|
|
return;
|
|
}
|
|
} catch (e) {
|
|
console.log("[ModelSync] New-style menu not available:", e.message);
|
|
}
|
|
|
|
// Fall back to old-style button
|
|
const tryCreateButton = () => {
|
|
const menu = document.querySelector(".comfy-menu") || document.querySelector(".comfyui-menu");
|
|
if (!menu) {
|
|
setTimeout(tryCreateButton, 500);
|
|
return;
|
|
}
|
|
|
|
// Check if button already exists (either new-style or legacy)
|
|
if (document.querySelector(".modelsync-menu-btn") ||
|
|
document.querySelector('[aria-label*="Model Sync"]')) {
|
|
return;
|
|
}
|
|
|
|
const button = document.createElement("button");
|
|
button.textContent = "Model Sync";
|
|
button.classList.add("modelsync-menu-btn");
|
|
button.style.cssText = "margin: 4px; padding: 4px 8px;";
|
|
button.addEventListener("click", () => this.toggle());
|
|
|
|
// Insert into menu
|
|
menu.appendChild(button);
|
|
this.menuButtonCreated = true;
|
|
console.log("[ModelSync] Legacy menu button added");
|
|
};
|
|
|
|
tryCreateButton();
|
|
}
|
|
|
|
async refreshData() {
|
|
try {
|
|
const [modelsRes, targetsRes, configRes] = await Promise.all([
|
|
fetch("/modelsync/models"),
|
|
fetch("/modelsync/targets"),
|
|
fetch("/modelsync/config"),
|
|
]);
|
|
|
|
if (modelsRes.ok) {
|
|
const data = await modelsRes.json();
|
|
this.models = data.models || [];
|
|
}
|
|
|
|
if (targetsRes.ok) {
|
|
const data = await targetsRes.json();
|
|
this.targets = data.targets || [];
|
|
}
|
|
|
|
if (configRes.ok) {
|
|
this.config = await configRes.json();
|
|
}
|
|
|
|
this.render();
|
|
} catch (error) {
|
|
console.error("[ModelSync] Error refreshing data:", error);
|
|
}
|
|
}
|
|
|
|
toggle() {
|
|
if (this.element) {
|
|
this.close();
|
|
} else {
|
|
this.open();
|
|
}
|
|
}
|
|
|
|
open() {
|
|
this.element = document.createElement("div");
|
|
this.element.classList.add("modelsync-panel");
|
|
this.element.innerHTML = this.getTemplate();
|
|
|
|
document.body.appendChild(this.element);
|
|
this.attachEventListeners();
|
|
this.refreshData();
|
|
}
|
|
|
|
close() {
|
|
if (this.element) {
|
|
this.element.remove();
|
|
this.element = null;
|
|
}
|
|
}
|
|
|
|
getTemplate() {
|
|
return `
|
|
<div class="modelsync-overlay" data-action="close"></div>
|
|
<div class="modelsync-content">
|
|
<div class="modelsync-header">
|
|
<h2>Model Sync</h2>
|
|
<button class="modelsync-close" data-action="close">×</button>
|
|
</div>
|
|
|
|
<div class="modelsync-tabs">
|
|
<button class="modelsync-tab active" data-tab="models">Models</button>
|
|
<button class="modelsync-tab" data-tab="targets">Sync Targets</button>
|
|
<button class="modelsync-tab" data-tab="settings">Settings</button>
|
|
</div>
|
|
|
|
<div class="modelsync-body">
|
|
<div class="modelsync-tab-content" id="modelsync-models">
|
|
<div class="modelsync-toolbar">
|
|
<input type="text" class="modelsync-search" placeholder="Search models..." />
|
|
<select class="modelsync-filter">
|
|
<option value="all">All Models</option>
|
|
<option value="updates">Updates Available</option>
|
|
<option value="auto">Auto-Update Enabled</option>
|
|
</select>
|
|
<button class="modelsync-btn" data-action="scan">Scan</button>
|
|
<button class="modelsync-btn primary" data-action="check">Check Updates</button>
|
|
</div>
|
|
<div class="modelsync-models-list"></div>
|
|
</div>
|
|
|
|
<div class="modelsync-tab-content hidden" id="modelsync-targets">
|
|
<div class="modelsync-toolbar">
|
|
<button class="modelsync-btn primary" data-action="add-target">Add Target</button>
|
|
<button class="modelsync-btn" data-action="sync-all">Sync All</button>
|
|
</div>
|
|
<div class="modelsync-targets-list"></div>
|
|
</div>
|
|
|
|
<div class="modelsync-tab-content hidden" id="modelsync-settings">
|
|
<div class="modelsync-settings-form">
|
|
<div class="modelsync-setting">
|
|
<label>
|
|
<input type="checkbox" id="auto-update-enabled" />
|
|
Enable Auto-Update
|
|
</label>
|
|
<p class="modelsync-hint">Automatically download updates for enabled models</p>
|
|
</div>
|
|
|
|
<div class="modelsync-setting">
|
|
<label for="check-interval">Check Interval (hours)</label>
|
|
<input type="number" id="check-interval" min="1" max="168" value="24" />
|
|
</div>
|
|
|
|
<div class="modelsync-setting">
|
|
<label>
|
|
<input type="checkbox" id="verify-downloads" checked />
|
|
Verify Downloads
|
|
</label>
|
|
<p class="modelsync-hint">Verify file hash after downloading</p>
|
|
</div>
|
|
|
|
<div class="modelsync-setting">
|
|
<label>
|
|
<input type="checkbox" id="backup-before-update" checked />
|
|
Backup Before Update
|
|
</label>
|
|
<p class="modelsync-hint">Create .bak file before replacing model</p>
|
|
</div>
|
|
|
|
<button class="modelsync-btn primary" data-action="save-settings">Save Settings</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="modelsync-footer">
|
|
<span class="modelsync-status"></span>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
attachEventListeners() {
|
|
if (!this.element) return;
|
|
|
|
// Close button and overlay
|
|
this.element.querySelectorAll("[data-action='close']").forEach((el) => {
|
|
el.addEventListener("click", () => this.close());
|
|
});
|
|
|
|
// Tabs
|
|
this.element.querySelectorAll(".modelsync-tab").forEach((tab) => {
|
|
tab.addEventListener("click", () => {
|
|
this.currentTab = tab.dataset.tab;
|
|
this.updateTabs();
|
|
});
|
|
});
|
|
|
|
// Search
|
|
const search = this.element.querySelector(".modelsync-search");
|
|
if (search) {
|
|
search.addEventListener("input", (e) => {
|
|
this.searchQuery = e.target.value.toLowerCase();
|
|
this.renderModelsList();
|
|
});
|
|
}
|
|
|
|
// Filter
|
|
const filter = this.element.querySelector(".modelsync-filter");
|
|
if (filter) {
|
|
filter.addEventListener("change", (e) => {
|
|
this.filterStatus = e.target.value;
|
|
this.renderModelsList();
|
|
});
|
|
}
|
|
|
|
// Action buttons
|
|
this.element.querySelectorAll("[data-action]").forEach((btn) => {
|
|
btn.addEventListener("click", () => this.handleAction(btn.dataset.action));
|
|
});
|
|
}
|
|
|
|
async handleAction(action) {
|
|
switch (action) {
|
|
case "scan":
|
|
await this.scanModels();
|
|
break;
|
|
case "check":
|
|
await this.checkUpdates();
|
|
break;
|
|
case "add-target":
|
|
this.showAddTargetDialog();
|
|
break;
|
|
case "sync-all":
|
|
await this.syncAll();
|
|
break;
|
|
case "save-settings":
|
|
await this.saveSettings();
|
|
break;
|
|
}
|
|
}
|
|
|
|
async scanModels() {
|
|
this.setStatus("Scanning models...");
|
|
try {
|
|
const res = await fetch("/modelsync/models/scan", { method: "POST" });
|
|
const data = await res.json();
|
|
this.setStatus(`Scanned ${data.scanned} models. New: ${data.new}, Updated: ${data.updated}`);
|
|
await this.refreshData();
|
|
} catch (error) {
|
|
this.setStatus("Scan failed: " + error.message);
|
|
}
|
|
}
|
|
|
|
async checkUpdates() {
|
|
this.setStatus("Checking updates (hashing, finding sources, checking versions)...");
|
|
|
|
// Disable button during check
|
|
const checkBtn = this.element?.querySelector('[data-action="check"]');
|
|
if (checkBtn) {
|
|
checkBtn.disabled = true;
|
|
checkBtn.textContent = "Checking...";
|
|
}
|
|
|
|
try {
|
|
const res = await fetch("/modelsync/check", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ concurrent: 5 }),
|
|
});
|
|
const data = await res.json();
|
|
|
|
const parts = [];
|
|
if (data.hashed > 0) parts.push(`${data.hashed} hashed`);
|
|
if (data.sources_found > 0) parts.push(`${data.sources_found} sources found`);
|
|
if (data.checked > 0) parts.push(`${data.checked} checked`);
|
|
if (data.updates_available > 0) parts.push(`${data.updates_available} updates!`);
|
|
if (data.errors > 0) parts.push(`${data.errors} errors`);
|
|
|
|
const summary = parts.length > 0 ? parts.join(", ") : "No models to process";
|
|
this.setStatus(`Done: ${summary}`);
|
|
await this.refreshData();
|
|
} catch (error) {
|
|
this.setStatus("Check failed: " + error.message);
|
|
} finally {
|
|
if (checkBtn) {
|
|
checkBtn.disabled = false;
|
|
checkBtn.textContent = "Check Updates";
|
|
}
|
|
}
|
|
}
|
|
|
|
async syncAll() {
|
|
this.setStatus("Syncing to all targets...");
|
|
try {
|
|
const res = await fetch("/modelsync/sync", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({}),
|
|
});
|
|
const data = await res.json();
|
|
const total = data.results.reduce((sum, r) => sum + r.models_synced, 0);
|
|
this.setStatus(`Sync complete. ${total} models synced.`);
|
|
} catch (error) {
|
|
this.setStatus("Sync failed: " + error.message);
|
|
}
|
|
}
|
|
|
|
async saveSettings() {
|
|
const settings = {
|
|
auto_update_enabled: this.element.querySelector("#auto-update-enabled").checked.toString(),
|
|
check_interval_hours: this.element.querySelector("#check-interval").value,
|
|
verify_downloads: this.element.querySelector("#verify-downloads").checked.toString(),
|
|
backup_before_update: this.element.querySelector("#backup-before-update").checked.toString(),
|
|
};
|
|
|
|
try {
|
|
await fetch("/modelsync/config", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(settings),
|
|
});
|
|
this.setStatus("Settings saved");
|
|
this.config = settings;
|
|
} catch (error) {
|
|
this.setStatus("Failed to save settings: " + error.message);
|
|
}
|
|
}
|
|
|
|
showAddTargetDialog() {
|
|
const name = prompt("Target name (e.g., 'frodo'):");
|
|
if (!name) return;
|
|
|
|
const sshHost = prompt("SSH host (e.g., 'user@host' or empty for local):");
|
|
const modelPath = prompt("Models path on target:");
|
|
if (!modelPath) return;
|
|
|
|
const maxVram = prompt("Max VRAM in MB (empty for no limit):");
|
|
|
|
this.addTarget({
|
|
name,
|
|
ssh_host: sshHost || null,
|
|
model_path: modelPath,
|
|
max_vram_mb: maxVram ? parseInt(maxVram) : null,
|
|
});
|
|
}
|
|
|
|
async addTarget(target) {
|
|
try {
|
|
await fetch("/modelsync/targets", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(target),
|
|
});
|
|
this.setStatus("Target added");
|
|
await this.refreshData();
|
|
} catch (error) {
|
|
this.setStatus("Failed to add target: " + error.message);
|
|
}
|
|
}
|
|
|
|
async deleteTarget(targetId) {
|
|
if (!confirm("Delete this sync target?")) return;
|
|
|
|
try {
|
|
await fetch(`/modelsync/targets/${targetId}`, { method: "DELETE" });
|
|
this.setStatus("Target deleted");
|
|
await this.refreshData();
|
|
} catch (error) {
|
|
this.setStatus("Failed to delete target: " + error.message);
|
|
}
|
|
}
|
|
|
|
async syncToTarget(targetId) {
|
|
this.setStatus("Syncing...");
|
|
try {
|
|
const res = await fetch(`/modelsync/sync/${targetId}`, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({}),
|
|
});
|
|
const data = await res.json();
|
|
this.setStatus(`Synced ${data.models_synced} models to ${data.target}`);
|
|
} catch (error) {
|
|
this.setStatus("Sync failed: " + error.message);
|
|
}
|
|
}
|
|
|
|
async toggleAutoUpdate(modelId, enabled) {
|
|
try {
|
|
await fetch(`/modelsync/models/${modelId}`, {
|
|
method: "PATCH",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ auto_update: enabled ? 1 : 0 }),
|
|
});
|
|
} catch (error) {
|
|
console.error("[ModelSync] Failed to update model:", error);
|
|
}
|
|
}
|
|
|
|
async downloadUpdate(modelId) {
|
|
this.setStatus("Downloading update...");
|
|
try {
|
|
const res = await fetch(`/modelsync/download/${modelId}`, { method: "POST" });
|
|
const data = await res.json();
|
|
this.setStatus(data.success ? "Download complete" : "Download failed");
|
|
await this.refreshData();
|
|
} catch (error) {
|
|
this.setStatus("Download failed: " + error.message);
|
|
}
|
|
}
|
|
|
|
updateTabs() {
|
|
if (!this.element) return;
|
|
|
|
this.element.querySelectorAll(".modelsync-tab").forEach((tab) => {
|
|
tab.classList.toggle("active", tab.dataset.tab === this.currentTab);
|
|
});
|
|
|
|
this.element.querySelectorAll(".modelsync-tab-content").forEach((content) => {
|
|
content.classList.toggle("hidden", !content.id.includes(this.currentTab));
|
|
});
|
|
}
|
|
|
|
render() {
|
|
if (!this.element) return;
|
|
|
|
this.renderModelsList();
|
|
this.renderTargetsList();
|
|
this.renderSettings();
|
|
}
|
|
|
|
renderModelsList() {
|
|
const container = this.element?.querySelector(".modelsync-models-list");
|
|
if (!container) return;
|
|
|
|
let filtered = this.models;
|
|
|
|
// Apply search filter
|
|
if (this.searchQuery) {
|
|
filtered = filtered.filter((m) =>
|
|
m.filename.toLowerCase().includes(this.searchQuery)
|
|
);
|
|
}
|
|
|
|
// Apply status filter
|
|
if (this.filterStatus === "updates") {
|
|
filtered = filtered.filter((m) => m.update_available);
|
|
} else if (this.filterStatus === "auto") {
|
|
filtered = filtered.filter((m) => m.auto_update);
|
|
}
|
|
|
|
if (filtered.length === 0) {
|
|
container.innerHTML = '<p class="modelsync-empty">No models found. Click "Scan" to discover models.</p>';
|
|
return;
|
|
}
|
|
|
|
container.innerHTML = `
|
|
<table class="modelsync-table">
|
|
<thead>
|
|
<tr>
|
|
<th>Name</th>
|
|
<th>Type</th>
|
|
<th>Status</th>
|
|
<th>Size</th>
|
|
<th>Auto</th>
|
|
<th>Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
${filtered.map((m) => this.renderModelRow(m)).join("")}
|
|
</tbody>
|
|
</table>
|
|
`;
|
|
|
|
// Attach row event listeners
|
|
container.querySelectorAll("[data-model-action]").forEach((el) => {
|
|
el.addEventListener("click", () => {
|
|
const action = el.dataset.modelAction;
|
|
const modelId = parseInt(el.dataset.modelId);
|
|
|
|
if (action === "download") {
|
|
this.downloadUpdate(modelId);
|
|
} else if (action === "toggle-auto") {
|
|
const model = this.models.find((m) => m.id === modelId);
|
|
this.toggleAutoUpdate(modelId, !model.auto_update);
|
|
model.auto_update = !model.auto_update;
|
|
this.renderModelsList();
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
renderModelRow(model) {
|
|
const sizeStr = model.file_size
|
|
? (model.file_size / (1024 * 1024 * 1024)).toFixed(2) + " GB"
|
|
: "Unknown";
|
|
|
|
// Determine status - check for truthy values (not null, not "null", not empty)
|
|
const hasBeenChecked = model.last_checked && model.last_checked !== "null" && model.last_checked !== "";
|
|
const hasUpdate = model.update_available === 1 || model.update_available === true;
|
|
|
|
const statusClass = hasUpdate
|
|
? "update"
|
|
: hasBeenChecked
|
|
? "current"
|
|
: "unknown";
|
|
|
|
const statusText = hasUpdate
|
|
? "Update Available"
|
|
: hasBeenChecked
|
|
? "Up to Date"
|
|
: "Not Checked";
|
|
|
|
return `
|
|
<tr>
|
|
<td title="${model.path}">${model.filename}</td>
|
|
<td>${model.model_type}</td>
|
|
<td><span class="modelsync-badge ${statusClass}">${statusText}</span></td>
|
|
<td>${sizeStr}</td>
|
|
<td>
|
|
<input type="checkbox" ${model.auto_update ? "checked" : ""}
|
|
data-model-action="toggle-auto" data-model-id="${model.id}" />
|
|
</td>
|
|
<td>
|
|
${
|
|
model.update_available
|
|
? `<button class="modelsync-btn small" data-model-action="download" data-model-id="${model.id}">Download</button>`
|
|
: ""
|
|
}
|
|
</td>
|
|
</tr>
|
|
`;
|
|
}
|
|
|
|
renderTargetsList() {
|
|
const container = this.element?.querySelector(".modelsync-targets-list");
|
|
if (!container) return;
|
|
|
|
if (this.targets.length === 0) {
|
|
container.innerHTML = '<p class="modelsync-empty">No sync targets configured.</p>';
|
|
return;
|
|
}
|
|
|
|
container.innerHTML = `
|
|
<div class="modelsync-targets">
|
|
${this.targets.map((t) => this.renderTargetCard(t)).join("")}
|
|
</div>
|
|
`;
|
|
|
|
// Attach target event listeners
|
|
container.querySelectorAll("[data-target-action]").forEach((el) => {
|
|
el.addEventListener("click", () => {
|
|
const action = el.dataset.targetAction;
|
|
const targetId = parseInt(el.dataset.targetId);
|
|
|
|
if (action === "sync") {
|
|
this.syncToTarget(targetId);
|
|
} else if (action === "delete") {
|
|
this.deleteTarget(targetId);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
renderTargetCard(target) {
|
|
return `
|
|
<div class="modelsync-target-card">
|
|
<div class="modelsync-target-header">
|
|
<strong>${target.name}</strong>
|
|
<span class="modelsync-badge ${target.enabled ? "current" : "unknown"}">
|
|
${target.enabled ? "Enabled" : "Disabled"}
|
|
</span>
|
|
</div>
|
|
<div class="modelsync-target-details">
|
|
<p><strong>Host:</strong> ${target.ssh_host || "Local"}</p>
|
|
<p><strong>Path:</strong> ${target.model_path}</p>
|
|
<p><strong>Max VRAM:</strong> ${target.max_vram_mb ? target.max_vram_mb + " MB" : "No limit"}</p>
|
|
<p><strong>Last Sync:</strong> ${target.last_sync || "Never"}</p>
|
|
</div>
|
|
<div class="modelsync-target-actions">
|
|
<button class="modelsync-btn small primary" data-target-action="sync" data-target-id="${target.id}">Sync Now</button>
|
|
<button class="modelsync-btn small" data-target-action="delete" data-target-id="${target.id}">Delete</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
renderSettings() {
|
|
if (!this.element) return;
|
|
|
|
const autoUpdate = this.element.querySelector("#auto-update-enabled");
|
|
const checkInterval = this.element.querySelector("#check-interval");
|
|
const verifyDownloads = this.element.querySelector("#verify-downloads");
|
|
const backupBefore = this.element.querySelector("#backup-before-update");
|
|
|
|
if (autoUpdate) {
|
|
autoUpdate.checked = this.config.auto_update_enabled === "true";
|
|
}
|
|
if (checkInterval) {
|
|
checkInterval.value = this.config.check_interval_hours || "24";
|
|
}
|
|
if (verifyDownloads) {
|
|
verifyDownloads.checked = this.config.verify_downloads !== "false";
|
|
}
|
|
if (backupBefore) {
|
|
backupBefore.checked = this.config.backup_before_update !== "false";
|
|
}
|
|
}
|
|
|
|
setStatus(message) {
|
|
const status = this.element?.querySelector(".modelsync-status");
|
|
if (status) {
|
|
status.textContent = message;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Create global instance
|
|
const modelSyncPanel = new ModelSyncPanel();
|
|
|
|
// Load CSS
|
|
function loadCSS() {
|
|
if (document.querySelector('link[href*="modelsync.css"]')) return;
|
|
|
|
const link = document.createElement("link");
|
|
link.rel = "stylesheet";
|
|
link.href = "/extensions/ComfyUI-ModelSync/modelsync.css";
|
|
document.head.appendChild(link);
|
|
}
|
|
|
|
// Register with ComfyUI's extension system
|
|
app.registerExtension({
|
|
name: "ModelSync",
|
|
|
|
async setup() {
|
|
loadCSS();
|
|
|
|
// Create menu button using ComfyUI's proper API
|
|
try {
|
|
const { ComfyButton } = await import("../../scripts/ui/components/button.js");
|
|
const { ComfyButtonGroup } = await import("../../scripts/ui/components/buttonGroup.js");
|
|
|
|
const syncButton = new ComfyButton({
|
|
icon: "sync",
|
|
action: () => modelSyncPanel.toggle(),
|
|
tooltip: "Model Sync - Track and sync model updates",
|
|
content: "Model Sync",
|
|
classList: "comfyui-button comfyui-menu-mobile-collapse"
|
|
});
|
|
|
|
const syncGroup = new ComfyButtonGroup(syncButton.element);
|
|
app.menu?.settingsGroup?.element?.before(syncGroup.element);
|
|
console.log("[ModelSync] Menu button added via extension setup");
|
|
} catch (e) {
|
|
console.log("[ModelSync] New-style button not available:", e.message);
|
|
// Fall back to legacy button
|
|
modelSyncPanel.createMenuButton();
|
|
}
|
|
|
|
// Initialize panel data
|
|
await modelSyncPanel.init();
|
|
console.log("[ModelSync] Extension initialized");
|
|
}
|
|
});
|
|
|
|
// Export for external access
|
|
window.ModelSyncPanel = modelSyncPanel;
|