diff --git a/src/main/java/de/tki/comfymodels/service/impl/DefaultDownloadManager.java b/src/main/java/de/tki/comfymodels/service/impl/DefaultDownloadManager.java index d52359d..659006e 100644 --- a/src/main/java/de/tki/comfymodels/service/impl/DefaultDownloadManager.java +++ b/src/main/java/de/tki/comfymodels/service/impl/DefaultDownloadManager.java @@ -113,12 +113,27 @@ public class DefaultDownloadManager implements IDownloadManager { } finally { if (finishedCalled.compareAndSet(false, true)) { + notifyComfyUI(); // NEU: Ping abfeuern onFinished.run(); } } }).start(); } + // NEU: Benachrichtigt ComfyUI + private void notifyComfyUI() { + try { + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create("http://127.0.0.1:8188/kippster/model-downloaded")) + .POST(HttpRequest.BodyPublishers.noBody()) + .build(); + // Fire and forget + httpClient.sendAsync(request, HttpResponse.BodyHandlers.discarding()); + } catch (Exception e) { + // Wenn ComfyUI nicht läuft, ignorieren wir das leise + } + } + private boolean isSelected(int index) { return currentSelection == null || (index < currentSelection.length && currentSelection[index]); } diff --git a/src/main/resources/comfyui-bridge/__init__.py b/src/main/resources/comfyui-bridge/__init__.py index f16fd69..65e4d33 100644 --- a/src/main/resources/comfyui-bridge/__init__.py +++ b/src/main/resources/comfyui-bridge/__init__.py @@ -4,14 +4,13 @@ from aiohttp import web print("\033[95m[Model-Downloader] UI Extension Bridge active\033[0m") +# NEU: Der API-Endpunkt für den Java-Ping @server.PromptServer.instance.routes.post("/kippster/model-downloaded") async def model_downloaded(request): - try: - # Broadcast event to all connected clients - server.PromptServer.instance.send_sync("kippster-refresh-ui", {"message": "New models available!"}) - return web.json_response({"status": "success", "message": "UI refresh triggered"}) - except Exception as e: - return web.json_response({"status": "error", "message": str(e)}, status=500) + print("[Model-Downloader] Download finished trigger received from Java!") + # Sendet das Signal ans Frontend + server.PromptServer.instance.send_sync("kippster-refresh-ui", {"message": "New models available!"}) + return web.json_response({"status": "success", "message": "UI refresh triggered"}) WEB_DIRECTORY = "web" NODE_CLASS_MAPPINGS = {} diff --git a/src/main/resources/comfyui-bridge/web/downloader.js b/src/main/resources/comfyui-bridge/web/downloader.js index c2b5754..b93b778 100644 --- a/src/main/resources/comfyui-bridge/web/downloader.js +++ b/src/main/resources/comfyui-bridge/web/downloader.js @@ -310,6 +310,33 @@ const initializeExtension = async () => { addFAB(); addClassic(); addV2Action(); }; +// NEU: Wir laden die API +const setupListener = async () => { + try { + const { api } = await import("../../scripts/api.js"); + api.addEventListener("kippster-refresh-ui", (event) => { + console.log("%c[Model-Downloader] Triggering UI Refresh!", "background: green; color: white;"); + + if (app && app.refresh) { + app.refresh(); // Magischer Reload-Befehl + + // Kleines Popup für den User + const notify = (msg) => { + if (app.ui?.dialog?.show) app.ui.dialog.show(msg); + else if (window.comfyAPI?.dialog?.show) window.comfyAPI.dialog.show(msg); + else alert(msg); + }; + notify("✅ Downloads abgeschlossen! Modelle sind jetzt in ComfyUI verfügbar."); + } + }); + console.log("[Model-Downloader] WebSocket Listener aktiv."); + } catch (e) { + console.error("[Model-Downloader] Konnte API nicht für Listener laden.", e); + } +}; + +setupListener(); + if (app) { app.registerExtension({ name: "TKI.ModelDownloader", async setup() { await initializeExtension(); } }); } else if (document.readyState === "complete") { diff --git a/web/downloader.js b/web/downloader.js index d13570c..d6c2eba 100644 --- a/web/downloader.js +++ b/web/downloader.js @@ -1,5 +1,6 @@ -// Try different ways to get the ComfyUI app object +// Try different ways to get the ComfyUI app and api objects let app = null; +let api = null; const tryImports = async () => { const paths = [ @@ -7,6 +8,11 @@ const tryImports = async () => { "/scripts/app.js", "../../../scripts/app.js" ]; + const apiPaths = [ + "../../scripts/api.js", + "/scripts/api.js", + "../../../scripts/api.js" + ]; for (const path of paths) { try { @@ -18,72 +24,87 @@ const tryImports = async () => { } } catch (e) {} } + + for (const path of apiPaths) { + try { + const mod = await import(path); + if (mod.api) { + api = mod.api; + console.log(`[Model-Downloader] Successfully imported api from ${path}`); + break; + } + } catch (e) {} + } }; await tryImports(); -console.log("%c[Model-Downloader] DEBUG: JS File Loaded", "background: #222; color: #ffcc00; font-size: 20px;"); - // Define setup logic that can be called by extension or as fallback const initializeExtension = async () => { - console.log("%c[Model-Downloader] DEBUG: Initializing Extension Logic", "background: #222; color: #ffcc00; font-size: 16px;"); - let apiToken = null; - // Load token from local config.json (generated during installation) const loadConfig = async () => { try { - // Use import.meta.url to get the correct relative path for the extension const baseUrl = new URL(".", import.meta.url).href; const configUrl = `${baseUrl}config.json?v=${Date.now()}`; - - console.log("[Model-Downloader] Fetching config from:", configUrl); + console.log(`[Model-Downloader] Loading config from: ${configUrl}`); const response = await fetch(configUrl); if (response.ok) { const config = await response.json(); apiToken = config.token; if (apiToken) { - console.log("[Model-Downloader] Pre-shared Token loaded."); + const masked = apiToken.substring(0, 4) + "..." + apiToken.substring(apiToken.length - 4); + console.log(`[Model-Downloader] Token loaded successfully: ${masked}`); } else { console.warn("[Model-Downloader] Token in config.json is empty!"); } } else { - console.warn("[Model-Downloader] config.json not found (Status: " + response.status + "). Did you run the installer in the Java App?"); + console.error(`[Model-Downloader] Could not load config.json (Status: ${response.status})`); } } catch (e) { console.error("[Model-Downloader] Failed to load config.json:", e); } }; - // Trigger config load in background loadConfig(); - const sendWorkflow = async () => { - if (!app || !app.graph) { - console.error("[Model-Downloader] App or Graph not ready."); - const notify = (msg) => { - if (window.comfyAPI?.dialog?.show) window.comfyAPI.dialog.show(msg); - else alert(msg); - }; - notify("❌ Fehler: ComfyUI Graph nicht bereit."); - return; - } - const workflow = app.graph.serialize(); - console.log("[Model-Downloader] Workflow:", workflow); - - const notify = (msg) => { - if (app.ui?.dialog?.show) app.ui.dialog.show(msg); - else if (window.comfyAPI?.dialog?.show) window.comfyAPI.dialog.show(msg); - else alert(msg); + const notify = (msg) => { + if (app?.ui?.dialog?.show) app.ui.dialog.show(msg); + else if (window.comfyAPI?.dialog?.show) window.comfyAPI.dialog.show(msg); + else alert(msg); + }; + + const showConnectionError = () => { + const dialog = document.createElement("dialog"); + dialog.style = "background:#222; color:white; border:2px solid #ffcc00; border-radius:10px; padding:20px; width:450px; font-family:sans-serif; box-shadow:0 0 30px rgba(0,0,0,0.5); z-index:20002;"; + dialog.innerHTML = ` +
+
🚀❌
+

Downloader nicht erreichbar

+

Die Model Downloader App antwortet nicht. Bitte stelle sicher, dass:

+ +

Noch nicht installiert? Lade die App hier herunter:

+ 📥 Download via SourceForge + +
+ `; + document.body.appendChild(dialog); + dialog.showModal(); + dialog.querySelector("#tki-error-close").onclick = () => { + dialog.close(); + document.body.removeChild(dialog); }; + }; - if (!apiToken) { - notify("❌ Sicherheits-Fehler: Kein API-Token gefunden. Bitte führe den Installer in der Java-App aus."); - return; - } + const sendWorkflow = async () => { + if (!app || !app.graph) return notify("❌ Fehler: ComfyUI Graph nicht bereit."); + if (!apiToken) return notify("❌ Sicherheits-Fehler: Kein API-Token gefunden."); + const workflow = app.graph.serialize(); try { - console.log("[Model-Downloader] Sending request to 127.0.0.1:12345..."); const response = await fetch("http://127.0.0.1:12345/import", { method: "POST", mode: "cors", @@ -93,98 +114,219 @@ const initializeExtension = async () => { }, body: JSON.stringify(workflow) }); - - if (response.ok) { - notify("🚀 Workflow erfolgreich gesendet!"); - } else if (response.status === 401) { - notify("❌ Sicherheits-Fehler: Token ungültig. Bitte Downloader neu starten."); - apiToken = null; - } else { - notify("❌ Fehler: Downloader abgelehnt (Status: " + response.status + ")"); - } + if (response.ok) notify("🚀 Workflow erfolgreich gesendet!"); + else notify("❌ Fehler: Downloader abgelehnt (Status: " + response.status + ")"); } catch (e) { - notify("❌ Keine Verbindung zum Downloader-Tool! Läuft die Java-App?"); + showConnectionError(); } }; - // Strategy 1: The Modern FAB (Always works if DOM is ready) + const openCleanupDialog = async () => { + if (!apiToken) return notify("❌ Sicherheits-Fehler: Kein API-Token gefunden."); + + const dialog = document.createElement("dialog"); + dialog.id = "tki-cleanup-dialog"; + dialog.style = "background:#222; color:white; border:2px solid #ffcc00; border-radius:10px; padding:20px; width:800px; max-height:80vh; overflow:hidden; font-family:sans-serif; box-shadow:0 0 30px rgba(0,0,0,0.5); z-index:20001;"; + + dialog.innerHTML = ` +
+

🧹 Lokale Modelle aufräumen

+
+
+ Lade lokale Modelle... +
+ +
+ + `; + + document.body.appendChild(dialog); + dialog.showModal(); + + const refreshData = async () => { + const loading = dialog.querySelector("#tki-cleanup-loading"); + const container = dialog.querySelector("#tki-cleanup-table-container"); + loading.style.display = "block"; + container.style.display = "none"; + + try { + const response = await fetch("http://127.0.0.1:12345/api/models/local", { + headers: { "Authorization": `Bearer ${apiToken}` } + }); + const models = await response.json(); + + const tbody = dialog.querySelector("#tki-cleanup-tbody"); + tbody.innerHTML = models.map(m => ` + + + ${m.name} + ${m.type} + ${m.size} + + `).join(""); + + loading.style.display = "none"; + container.style.display = "block"; + + // Wire up select all + const selectAll = dialog.querySelector("#tki-select-all"); + const checkboxes = dialog.querySelectorAll(".tki-model-select"); + selectAll.checked = false; + selectAll.onchange = () => checkboxes.forEach(cb => cb.checked = selectAll.checked); + + } catch (e) { + dialog.close(); + document.body.removeChild(dialog); + showConnectionError(); + } + }; + + refreshData(); + + dialog.querySelector("#tki-cleanup-cancel").onclick = () => { + dialog.close(); + document.body.removeChild(dialog); + }; + + dialog.querySelector("#tki-cleanup-archive").onclick = async () => { + const checkboxes = dialog.querySelectorAll(".tki-model-select"); + const selectedPaths = Array.from(checkboxes) + .filter(cb => cb.checked) + .map(cb => cb.getAttribute("data-path")); + + if (selectedPaths.length === 0) return alert("Bitte wähle mindestens ein Modell aus."); + + const status = dialog.querySelector("#tki-cleanup-status"); + const btn = dialog.querySelector("#tki-cleanup-archive"); + btn.disabled = true; + status.textContent = `Archiviere ${selectedPaths.length} Dateien...`; + + try { + const res = await fetch("http://127.0.0.1:12345/api/models/archive", { + method: "POST", + headers: { "Content-Type": "application/json", "Authorization": `Bearer ${apiToken}` }, + body: JSON.stringify(selectedPaths) + }); + if (res.ok) { + const data = await res.json(); + alert(`Erfolgreich ${data.archived} Modelle archiviert!`); + status.textContent = ""; + btn.disabled = false; + refreshData(); // Reload list + } else { + throw new Error("API Fehler (Status: " + res.status + ")"); + } + } catch (e) { + console.error("[Model-Downloader] Archive error:", e); + btn.disabled = false; + status.textContent = ""; + if (e.message.includes("fetch") || e.name === "TypeError") { + dialog.close(); + document.body.removeChild(dialog); + showConnectionError(); + } else { + alert("Fehler: " + e.message); + } + } + }; + }; + const addFAB = () => { if (document.getElementById("tki-downloader-fab")) return; - console.log("[Model-Downloader] Adding FAB..."); const fab = document.createElement("div"); fab.id = "tki-downloader-fab"; fab.innerHTML = "🚀"; - fab.title = "Send to Model Downloader"; - fab.style = "position:fixed; bottom:30px; right:30px; z-index:10000; cursor:pointer; font-size:30px; background:#ffcc00; border-radius:50%; width:60px; height:60px; display:flex; align-items:center; justify-content:center; box-shadow:0 0 20px rgba(255,105,180,0.5); border: 2px solid white; transition: transform 0.2s;"; - fab.onmouseover = () => fab.style.transform = "scale(1.1)"; - fab.onmouseout = () => fab.style.transform = "scale(1.0)"; + fab.style = "position:fixed; bottom:30px; right:30px; z-index:10000; cursor:pointer; font-size:30px; background:#ffcc00; border-radius:50%; width:60px; height:60px; display:flex; align-items:center; justify-content:center; box-shadow:0 0 20px rgba(255,204,0,0.5); border: 2px solid white; transition: transform 0.2s;"; fab.onclick = sendWorkflow; document.body.appendChild(fab); }; - // Strategy 2: Primary Action (V2 UI) const addV2Action = () => { if (app?.ui?.menu?.addPrimaryAction) { - console.log("[Model-Downloader] Adding to V2 Menu..."); - app.ui.menu.addPrimaryAction({ - id: "tki.model-downloader.send", - icon: "pi pi-download", - label: "Send to Downloader", - callback: sendWorkflow - }); - } - if (app?.ui?.toolbar?.addPrimaryAction) { - console.log("[Model-Downloader] Adding to V2 Toolbar..."); - app.ui.toolbar.addPrimaryAction({ - id: "tki.model-downloader.send-toolbar", - icon: "pi pi-download", - label: "Send to Downloader", - callback: sendWorkflow - }); + app.ui.menu.addPrimaryAction({ id: "tki.model-downloader.send", icon: "pi pi-download", label: "Send to Downloader", callback: sendWorkflow }); + app.ui.menu.addPrimaryAction({ id: "tki.model-downloader.cleanup", icon: "pi pi-trash", label: "Aufräumen / Archivieren", callback: openCleanupDialog }); } }; - // Strategy 3: Classic Menu const addClassic = () => { const menu = document.querySelector(".comfy-menu"); if (menu) { - if (document.getElementById("tki-downloader-classic")) return; - console.log("[Model-Downloader] Adding to Classic Menu..."); - const btn = document.createElement("button"); - btn.id = "tki-downloader-classic"; - btn.textContent = "🚀 Downloader"; - btn.style.background = "#ffcc00"; - btn.style.color = "white"; - btn.onclick = sendWorkflow; - menu.appendChild(btn); + if (!document.getElementById("tki-downloader-classic")) { + const btn = document.createElement("button"); + btn.id = "tki-downloader-classic"; + btn.textContent = "🚀 Downloader"; + btn.style.background = "#ffcc00"; btn.style.color = "white"; + btn.onclick = sendWorkflow; + menu.appendChild(btn); + } + if (!document.getElementById("tki-cleanup-classic")) { + const cleanBtn = document.createElement("button"); + cleanBtn.id = "tki-cleanup-classic"; + cleanBtn.textContent = "🧹 Aufräumen"; + cleanBtn.style.background = "#444"; cleanBtn.style.color = "white"; cleanBtn.style.marginLeft = "5px"; + cleanBtn.onclick = openCleanupDialog; + menu.appendChild(cleanBtn); + } } }; - // Run immediately - addFAB(); - addClassic(); - addV2Action(); - - // Retry with delays to ensure UI is ready - setTimeout(addFAB, 1000); - setTimeout(addFAB, 5000); - setTimeout(addClassic, 2000); - setTimeout(addV2Action, 3000); + addFAB(); addClassic(); addV2Action(); }; -if (app) { - app.registerExtension({ - name: "TKI.ModelDownloader", - async setup() { - await initializeExtension(); - } - }); -} else { - console.warn("[Model-Downloader] App object not found. Falling back to manual initialization."); - // Fallback if app is not found or registration fails - if (document.readyState === "complete") { - initializeExtension(); - } else { - window.addEventListener("load", initializeExtension); +// NEU: Wir laden die API +const setupListener = async () => { + try { + const { api } = await import("../../scripts/api.js"); + api.addEventListener("kippster-refresh-ui", (event) => { + console.log("%c[Model-Downloader] Triggering UI Refresh!", "background: green; color: white;"); + + if (app && app.refresh) { + app.refresh(); // Magischer Reload-Befehl + + // Kleines Popup für den User + const notify = (msg) => { + if (app.ui?.dialog?.show) app.ui.dialog.show(msg); + else if (window.comfyAPI?.dialog?.show) window.comfyAPI.dialog.show(msg); + else alert(msg); + }; + notify("✅ Downloads abgeschlossen! Modelle sind jetzt in ComfyUI verfügbar."); + } + }); + console.log("[Model-Downloader] WebSocket Listener aktiv."); + } catch (e) { + console.error("[Model-Downloader] Konnte API nicht für Listener laden.", e); } +}; + +setupListener(); + +if (app) { + app.registerExtension({ name: "TKI.ModelDownloader", async setup() { await initializeExtension(); } }); +} else if (document.readyState === "complete") { + initializeExtension(); +} else { + window.addEventListener("load", initializeExtension); }