diff --git a/src/main/java/de/tki/comfymodels/Main.java b/src/main/java/de/tki/comfymodels/Main.java index 86501f8..204420a 100644 --- a/src/main/java/de/tki/comfymodels/Main.java +++ b/src/main/java/de/tki/comfymodels/Main.java @@ -55,7 +55,6 @@ public class Main extends JFrame { private final IModelValidator modelValidator; private final de.tki.comfymodels.service.impl.RestBridgeService restBridge; private final de.tki.comfymodels.service.impl.ArchiveService archiveService; - private final de.tki.comfymodels.service.impl.ComfyUIBridgeClient bridgeClient; @Autowired private ConfigService configService; @@ -90,8 +89,7 @@ public class Main extends JFrame { public Main(IModelAnalyzer analyzer, IDownloadManager downloadManager, IWorkflowService workflowService, IModelSearchService searchService, IModelValidator modelValidator, de.tki.comfymodels.service.impl.RestBridgeService restBridge, - de.tki.comfymodels.service.impl.ArchiveService archiveService, - de.tki.comfymodels.service.impl.ComfyUIBridgeClient bridgeClient) { + de.tki.comfymodels.service.impl.ArchiveService archiveService) { this.analyzer = analyzer; this.downloadManager = downloadManager; this.workflowService = workflowService; @@ -99,7 +97,6 @@ public class Main extends JFrame { this.modelValidator = modelValidator; this.restBridge = restBridge; this.archiveService = archiveService; - this.bridgeClient = bridgeClient; } public void launch(String[] args) { @@ -354,7 +351,7 @@ public class Main extends JFrame { try { configService.unlock(pass); restBridge.setApiToken(configService.getApiToken()); - syncBridgeToken(); + syncBridgeFiles(); // NEW: Automatically sync code & token on every unlock // NEW: Auto-import model list on first start or vault reset if (configService.isVaultFresh()) { @@ -769,7 +766,6 @@ public class Main extends JFrame { () -> SwingUtilities.invokeLater(() -> { downloadButton.setEnabled(true); statusLabel.setText("Queue finished."); - bridgeClient.notifyModelDownloaded(); if (configService.isShutdownAfterDownloadEnabled()) performSystemShutdown(); }) ); @@ -843,7 +839,7 @@ public class Main extends JFrame { return; } configService.setComfyUIPath(selectedPath); - syncBridgeToken(); + syncBridgeFiles(); JOptionPane.showMessageDialog(dialog, "API Token synchronized successfully."); }); @@ -1447,6 +1443,36 @@ public class Main extends JFrame { JTable archiveTable = new JTable(archiveTableModel); archiveTable.setRowHeight(25); archiveTable.getColumnModel().getColumn(0).setMaxWidth(50); + + // Enable sorting with custom comparator for human-readable sizes + javax.swing.table.TableRowSorter archiveSorter = new javax.swing.table.TableRowSorter<>(archiveTableModel); + archiveSorter.setComparator(3, (s1, s2) -> Long.compare(parseSizeToBytes((String)s1), parseSizeToBytes((String)s2))); + archiveSorter.setSortable(0, false); // Disable sorting for checkbox column to allow "Select All" + archiveTable.setRowSorter(archiveSorter); + + // Add "Select All" functionality to header + archiveTable.getTableHeader().addMouseListener(new MouseAdapter() { + @Override + public void mouseClicked(MouseEvent e) { + int viewIdx = archiveTable.columnAtPoint(e.getPoint()); + if (viewIdx == -1) return; + int modelIdx = archiveTable.convertColumnIndexToModel(viewIdx); + if (modelIdx == 0) { + boolean allSelected = true; + for (int i = 0; i < archiveTableModel.getRowCount(); i++) { + if (!(Boolean) archiveTableModel.getValueAt(i, 0)) { + allSelected = false; + break; + } + } + boolean newValue = !allSelected; + for (int i = 0; i < archiveTableModel.getRowCount(); i++) { + archiveTableModel.setValueAt(newValue, i, 0); + } + } + } + }); + JScrollPane archiveScroll = new JScrollPane(archiveTable); archivePanel.add(archiveScroll, BorderLayout.CENTER); @@ -1494,6 +1520,36 @@ public class Main extends JFrame { JTable restoreTable = new JTable(restoreTableModel); restoreTable.setRowHeight(25); restoreTable.getColumnModel().getColumn(0).setMaxWidth(50); + + // Enable sorting with custom comparator for human-readable sizes + javax.swing.table.TableRowSorter restoreSorter = new javax.swing.table.TableRowSorter<>(restoreTableModel); + restoreSorter.setComparator(3, (s1, s2) -> Long.compare(parseSizeToBytes((String)s1), parseSizeToBytes((String)s2))); + restoreSorter.setSortable(0, false); // Disable sorting for checkbox column to allow "Select All" + restoreTable.setRowSorter(restoreSorter); + + // Add "Select All" functionality to header + restoreTable.getTableHeader().addMouseListener(new MouseAdapter() { + @Override + public void mouseClicked(MouseEvent e) { + int viewIdx = restoreTable.columnAtPoint(e.getPoint()); + if (viewIdx == -1) return; + int modelIdx = restoreTable.convertColumnIndexToModel(viewIdx); + if (modelIdx == 0) { + boolean allSelected = true; + for (int i = 0; i < restoreTableModel.getRowCount(); i++) { + if (!(Boolean) restoreTableModel.getValueAt(i, 0)) { + allSelected = false; + break; + } + } + boolean newValue = !allSelected; + for (int i = 0; i < restoreTableModel.getRowCount(); i++) { + restoreTableModel.setValueAt(newValue, i, 0); + } + } + } + }); + JScrollPane restoreScroll = new JScrollPane(restoreTable); restorePanel.add(restoreScroll, BorderLayout.CENTER); @@ -1611,6 +1667,7 @@ public class Main extends JFrame { JOptionPane.showMessageDialog(this, finalSuccess + " models successfully " + title.toLowerCase() + "ed.", "Operation Complete", JOptionPane.INFORMATION_MESSAGE); analyzeJsonContent(); + downloadManager.notifyComfyUI(); }); }).start(); @@ -1735,7 +1792,7 @@ public class Main extends JFrame { helpDialog.setVisible(true); } - private void syncBridgeToken() { + private void syncBridgeFiles() { String comfyPath = configService.getComfyUIPath(); if (comfyPath == null || comfyPath.isEmpty()) return; @@ -1747,8 +1804,19 @@ public class Main extends JFrame { Path targetDir = customNodesDir.resolve("comfyui-model-downloader"); if (Files.exists(targetDir) && Files.isDirectory(targetDir)) { - writeExtensionConfig(targetDir.toFile()); - System.out.println("Synchronized API token with ComfyUI bridge at: " + targetDir); + try { + // Always sync latest code files from resources to target + extractResource("/comfyui-bridge/__init__.py", targetDir.resolve("__init__.py").toFile()); + Path webDir = targetDir.resolve("web"); + Files.createDirectories(webDir); + extractResource("/comfyui-bridge/web/downloader.js", webDir.resolve("downloader.js").toFile()); + + // Also update config + writeExtensionConfig(targetDir.toFile()); + System.out.println("[Bridge-Sync] Successfully synchronized latest bridge code and token."); + } catch (IOException e) { + System.err.println("[Bridge-Sync] Failed to sync code: " + e.getMessage()); + } } } diff --git a/src/main/java/de/tki/comfymodels/service/IDownloadManager.java b/src/main/java/de/tki/comfymodels/service/IDownloadManager.java index c49ee3d..975f669 100644 --- a/src/main/java/de/tki/comfymodels/service/IDownloadManager.java +++ b/src/main/java/de/tki/comfymodels/service/IDownloadManager.java @@ -11,6 +11,11 @@ public interface IDownloadManager { void stop(); boolean isPaused(); + /** + * Triggers a refresh signal to the ComfyUI bridge. + */ + void notifyComfyUI(); + /** * Returns a map of current statuses for each model in the queue. */ diff --git a/src/main/java/de/tki/comfymodels/service/impl/ComfyUIBridgeClient.java b/src/main/java/de/tki/comfymodels/service/impl/ComfyUIBridgeClient.java deleted file mode 100644 index bfa526a..0000000 --- a/src/main/java/de/tki/comfymodels/service/impl/ComfyUIBridgeClient.java +++ /dev/null @@ -1,52 +0,0 @@ -package de.tki.comfymodels.service.impl; - -import org.springframework.stereotype.Service; - -import java.net.URI; -import java.net.http.HttpClient; -import java.net.http.HttpRequest; -import java.net.http.HttpResponse; -import java.time.Duration; - -@Service -public class ComfyUIBridgeClient { - - private final HttpClient httpClient = HttpClient.newBuilder() - .connectTimeout(Duration.ofSeconds(2)) - .build(); - - private String comfyUrl = "http://127.0.0.1:8188"; - - public void setComfyUrl(String url) { - if (url != null && !url.isEmpty()) { - this.comfyUrl = url.endsWith("/") ? url.substring(0, url.length() - 1) : url; - } - } - - public void notifyModelDownloaded() { - try { - HttpRequest request = HttpRequest.newBuilder() - .uri(URI.create(comfyUrl + "/kippster/model-downloaded")) - .POST(HttpRequest.BodyPublishers.noBody()) - .timeout(Duration.ofSeconds(5)) - .build(); - - System.out.println("Notifying ComfyUI at: " + request.uri()); - - httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofString()) - .thenAccept(response -> { - if (response.statusCode() >= 200 && response.statusCode() < 300) { - System.out.println("ComfyUI notified successfully: " + response.body()); - } else { - System.err.println("Failed to notify ComfyUI. Status: " + response.statusCode()); - } - }) - .exceptionally(ex -> { - System.err.println("Could not reach ComfyUI for refresh: " + ex.getMessage()); - return null; - }); - } catch (Exception e) { - System.err.println("Error creating notification request: " + e.getMessage()); - } - } -} diff --git a/src/main/java/de/tki/comfymodels/service/impl/ConfigService.java b/src/main/java/de/tki/comfymodels/service/impl/ConfigService.java index 45985ce..cab5712 100644 --- a/src/main/java/de/tki/comfymodels/service/impl/ConfigService.java +++ b/src/main/java/de/tki/comfymodels/service/impl/ConfigService.java @@ -154,6 +154,15 @@ public class ConfigService { save(); } + public String getComfyUIUrl() { return settings.optString("comfyui_url", "http://127.0.0.1:8188"); } + public void setComfyUIUrl(String url) { + if (url != null && !url.isEmpty()) { + String sanitized = url.endsWith("/") ? url.substring(0, url.length() - 1) : url; + settings.put("comfyui_url", sanitized); + save(); + } + } + public String getApiToken() { String token = settings.optString("api_token", ""); if (token.isEmpty() && isUnlocked()) { 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 36b3c88..097c720 100644 --- a/src/main/java/de/tki/comfymodels/service/impl/DefaultDownloadManager.java +++ b/src/main/java/de/tki/comfymodels/service/impl/DefaultDownloadManager.java @@ -121,20 +121,68 @@ public class DefaultDownloadManager implements IDownloadManager { } // NEU: Benachrichtigt ComfyUI - private void notifyComfyUI() { + @Override + public void notifyComfyUI() { + String comfyUrl = configService.getComfyUIUrl(); + if (sendRefreshPing(comfyUrl)) { + return; + } + + // Auto-Discovery: If configured URL fails, search for ComfyUI on common ports + System.out.println("🔍 [Model-Downloader] ComfyUI unter " + comfyUrl + " nicht erreicht. Suche automatisch..."); + String discoveredUrl = discoverComfyUrl(); + if (discoveredUrl != null) { + System.out.println("✨ [Model-Downloader] ComfyUI automatisch gefunden unter: " + discoveredUrl); + configService.setComfyUIUrl(discoveredUrl); + sendRefreshPing(discoveredUrl); + } else { + System.err.println("❌ [Model-Downloader] ComfyUI konnte nicht automatisch gefunden werden. Bitte stelle sicher, dass es läuft."); + } + } + + private boolean sendRefreshPing(String url) { try { - System.out.println("🚀 [Model-Downloader] Sende Refresh-Ping an ComfyUI (Port 8188)..."); HttpRequest request = HttpRequest.newBuilder() - .uri(URI.create("http://127.0.0.1:8188/kippster/model-downloaded")) + .uri(URI.create(url + "/kippster/model-downloaded")) + .timeout(java.time.Duration.ofSeconds(2)) .POST(HttpRequest.BodyPublishers.noBody()) .build(); - // Synchron senden, damit wir sehen, was passiert HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); - System.out.println("✅ [Model-Downloader] ComfyUI hat geantwortet mit Status: " + response.statusCode()); - } catch (Exception e) { - System.err.println("❌ [Model-Downloader] Konnte ComfyUI nicht erreichen. Läuft es auf Port 8188? Fehler: " + e.getMessage()); + if (response.statusCode() == 200) { + System.out.println("✅ [Model-Downloader] ComfyUI (" + url + ") erfolgreich benachrichtigt."); + return true; + } + } catch (Exception ignored) {} + return false; + } + + private String discoverComfyUrl() { + // Scan typical ComfyUI ports, including common alternatives like 8000 + int[] commonPorts = {8188, 8000, 8189, 8190, 3000, 8080}; + for (int port : commonPorts) { + if (checkPort(port)) return "http://127.0.0.1:" + port; } + + // Expanded range scan + for (int port = 8191; port <= 8199; port++) { + if (checkPort(port)) return "http://127.0.0.1:" + port; + } + return null; + } + + private boolean checkPort(int port) { + String url = "http://127.0.0.1:" + port; + try { + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(url + "/system_stats")) + .timeout(java.time.Duration.ofMillis(300)) + .GET() + .build(); + HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + return response.statusCode() == 200 && response.body().contains("system"); + } catch (Exception ignored) {} + return false; } private boolean isSelected(int index) { diff --git a/src/main/resources/comfyui-bridge/__init__.py b/src/main/resources/comfyui-bridge/__init__.py index 0333e83..758cb82 100644 --- a/src/main/resources/comfyui-bridge/__init__.py +++ b/src/main/resources/comfyui-bridge/__init__.py @@ -1,16 +1,45 @@ # ComfyUI Model Downloader Bridge import server +import folder_paths +import os +import asyncio from aiohttp import web print("\033[95m[Model-Downloader] UI Extension Bridge active\033[0m") @server.PromptServer.instance.routes.post("/kippster/model-downloaded") async def model_downloaded(request): - # Dieser Text MUSS in der ComfyUI-Konsole auftauchen, wenn Java fertig ist! - print("\033[92m🔥 [Model-Downloader] PING ERHALTEN! Sende Signal ans Frontend...\033[0m") + print("\033[92m🔥 [Model-Downloader] PING ERHALTEN! Erzwinge harten Backend-Reset...\033[0m") + try: + # 1. Dateisystem-Cache löschen (Windows braucht hier Zeit) + await asyncio.sleep(0.5) + if hasattr(folder_paths, "base_path_layers"): + for layer in folder_paths.base_path_layers.values(): + layer.folder_names.clear() + if hasattr(folder_paths, "folder_names"): + folder_paths.folder_names.clear() + + # 2. Den Definitions-Cache von ComfyUI löschen + # Das zwingt ComfyUI, die Dropdown-Listen für den Browser komplett neu zu generieren + server.PromptServer.instance.object_info = None + + # 3. Aktiven Scan triggern + for folder in ["checkpoints", "loras", "vae", "controlnet", "diffusion_models", "upscale_models", "clip"]: + try: + folder_paths.get_filename_list(folder) + except: pass + + print("[Model-Downloader] Backend-Reset und Scan abgeschlossen.") + + except Exception as e: + print(f"[Model-Downloader] Fehler beim Reset: {e}") + + # 4. Signale an alle Browser (jetzt mit frischen Daten) + server.PromptServer.instance.send_sync("refresh", {}) server.PromptServer.instance.send_sync("kippster-refresh-ui", {"message": "ready"}) - return web.json_response({"status": "success", "message": "UI refreshed"}) + + return web.json_response({"status": "success", "message": "Backend reset and rescan complete"}) 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 9174f55..1bab8bb 100644 --- a/src/main/resources/comfyui-bridge/web/downloader.js +++ b/src/main/resources/comfyui-bridge/web/downloader.js @@ -1,370 +1,104 @@ -// Try different ways to get the ComfyUI app and api objects +// ComfyUI Model Downloader - Bridge Extension let app = null; let api = null; -const tryImports = async () => { - const paths = [ - "../../scripts/app.js", - "/scripts/app.js", - "../../../scripts/app.js" - ]; - const apiPaths = [ - "../../scripts/api.js", - "/scripts/api.js", - "../../../scripts/api.js" - ]; +const initializeExtension = async () => { + const paths = ["../../scripts/app.js", "/scripts/app.js", "../../../scripts/app.js"]; + const apiPaths = ["../../scripts/api.js", "/scripts/api.js", "../../../scripts/api.js"]; + for (const path of paths) { try { const mod = await import(path); if (mod.app) { app = mod.app; break; } } catch (e) {} } + for (const path of apiPaths) { try { const mod = await import(path); if (mod.api) { api = mod.api; break; } } catch (e) {} } - for (const path of paths) { + if (!app || !api) { setTimeout(initializeExtension, 2000); return; } + + api.addEventListener("kippster-refresh-ui", async (event) => { + console.log("%c🔥 [Model-Downloader] SIGNAL EMPFANGEN! Starte Deep-Node-Update...", "background: #ff0000; color: #fff; font-size: 14px;"); + try { - const mod = await import(path); - if (mod.app) { - app = mod.app; - console.log(`[Model-Downloader] Successfully imported app from ${path}`); - break; - } - } catch (e) {} - } + // 1. Backend Zeit zum Scannen geben + await new Promise(r => setTimeout(r, 800)); - 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(); - -// NEU: Dynamischer Import für den WebSocket-Listener -const setupListener = async () => { - const apiPaths = [ - "../../scripts/api.js", - "/scripts/api.js", - "../../../scripts/api.js" - ]; - - for (const path of apiPaths) { - try { - const mod = await import(path); - if (mod.api) { - console.log("%c[Model-Downloader] WebSocket Listener erfolgreich verknüpft!", "color: #00ff00; font-weight: bold;"); - - mod.api.addEventListener("kippster-refresh-ui", (event) => { - console.log("%c🔥 [Model-Downloader] SIGNAL EMPFANGEN! Lade UI neu...", "background: #ff0000; color: #fff; font-size: 14px;"); - - if (app && typeof app.refresh === 'function') { - app.refresh(); // Aktualisiert die Model-Dropdowns! - - // Kleines UI-Popup zur Bestätigung - if (app.ui?.dialog?.show) { - app.ui.dialog.show("✅ Downloads abgeschlossen! Modelle sind jetzt im Dropdown."); - } else { - alert("✅ Downloads abgeschlossen! Modelle aktualisiert."); + // 2. Definitionen neu laden (Zwingt den Browser die neuesten Modell-Listen zu kennen) + const nodeDefs = await api.getNodeDefs(); + + // 3. Bestehende Nodes auf dem Canvas aktualisieren + if (app.graph && app.graph._nodes) { + console.log("[Model-Downloader] Aktualisiere Dropdowns in " + app.graph._nodes.length + " Nodes..."); + for (const node of app.graph._nodes) { + const def = nodeDefs[node.type]; + if (def && def.input && def.input.required) { + for (const widgetName in def.input.required) { + const inputDef = def.input.required[widgetName]; + // Falls es ein Dropdown (Combo) ist + if (Array.isArray(inputDef[0])) { + const newValues = inputDef[0]; + const widget = node.widgets?.find(w => w.name === widgetName); + if (widget && widget.type === "combo") { + widget.options.values = newValues; + + // Falls das gewählte Modell jetzt da ist, "heilen" wir den Node + if (newValues.includes(widget.value)) { + node.color = ""; + node.bgcolor = ""; + } + } + } } } - }); - break; // Erfolgreich gefunden, Schleife abbrechen + if (node.onRefresh) node.onRefresh(); + } } - } catch (e) {} - } -}; -await setupListener(); + if (app.refresh) await app.refresh(); + app.graph.setDirtyCanvas(true, true); + + if (app.ui?.dialog?.show) { + app.ui.dialog.show("✅ Sync abgeschlossen! Neue Modelle wurden in die Dropdowns geladen."); + } + + } catch (e) { + console.error("[Model-Downloader] Fehler beim Deep-Node-Update:", e); + } + }); -// Define setup logic that can be called by extension or as fallback -const initializeExtension = async () => { let apiToken = null; - - // We removed the old inline listener here to avoid duplicates - const loadConfig = async () => { try { const baseUrl = new URL(".", import.meta.url).href; const configUrl = `${baseUrl}config.json?v=${Date.now()}`; - 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) { - 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.error(`[Model-Downloader] Could not load config.json (Status: ${response.status})`); - } - } catch (e) { - console.error("[Model-Downloader] Failed to load config.json:", e); + if (response.ok) { const config = await response.json(); apiToken = config.token; } + } catch (e) {} + }; + await loadConfig(); + + const addUI = () => { + if (!document.getElementById("tki-downloader-fab")) { + const fab = document.createElement("div"); + fab.id = "tki-downloader-fab"; + fab.innerHTML = "🚀"; + 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;"; + fab.onclick = async () => { + if (!app?.graph) return; + try { + await fetch("http://127.0.0.1:12345/import", { + method: "POST", mode: "cors", + headers: { "Content-Type": "application/json", "Authorization": `Bearer ${apiToken}` }, + body: JSON.stringify(app.graph.serialize()) + }); + } catch (e) {} + }; + document.body.appendChild(fab); } }; - - loadConfig(); - - 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:

-
    -
  • Die Java-App gestartet wurde.
  • -
  • Port 12345 nicht blockiert wird.
  • -
-

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); - }; - }; - - 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 { - const response = await fetch("http://127.0.0.1:12345/import", { - method: "POST", - mode: "cors", - headers: { - "Content-Type": "application/json", - "Authorization": `Bearer ${apiToken}` - }, - body: JSON.stringify(workflow) - }); - if (response.ok) notify("🚀 Workflow erfolgreich gesendet!"); - else notify("❌ Fehler: Downloader abgelehnt (Status: " + response.status + ")"); - } catch (e) { - showConnectionError(); - } - }; - - 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; - const fab = document.createElement("div"); - fab.id = "tki-downloader-fab"; - fab.innerHTML = "🚀"; - 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); - }; - - const addV2Action = () => { - if (app?.ui?.menu?.addPrimaryAction) { - 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 }); - } - }; - - const addClassic = () => { - const menu = document.querySelector(".comfy-menu"); - if (menu) { - 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); - } - } - }; - - addFAB(); addClassic(); addV2Action(); + addUI(); }; -// 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); +const register = () => { + if (window.app && window.app.registerExtension) { + window.app.registerExtension({ name: "TKI.ModelDownloader", async setup() { await initializeExtension(); } }); + } else { + if (document.readyState === "complete") initializeExtension(); + else window.addEventListener("load", initializeExtension); } }; - -setupListener(); - -if (app) { - app.registerExtension({ name: "TKI.ModelDownloader", async setup() { await initializeExtension(); } }); -} else if (document.readyState === "complete") { - initializeExtension(); -} else { - window.addEventListener("load", initializeExtension); -} +register(); diff --git a/src/test/java/de/tki/comfymodels/service/ComprehensiveStatusFlowTest.java b/src/test/java/de/tki/comfymodels/service/ComprehensiveStatusFlowTest.java new file mode 100644 index 0000000..150b08a --- /dev/null +++ b/src/test/java/de/tki/comfymodels/service/ComprehensiveStatusFlowTest.java @@ -0,0 +1,113 @@ +package de.tki.comfymodels.service; + +import de.tki.comfymodels.domain.ModelInfo; +import de.tki.comfymodels.service.impl.*; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.springframework.test.util.ReflectionTestUtils; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +public class ComprehensiveStatusFlowTest { + + private ComfyModelAnalyzer analyzer; + private ConfigService configService; + private ArchiveService archiveService; + private PathResolver pathResolver; + private LocalModelScanner localScanner; + + @TempDir + Path tempBase; + + Path modelsDir; + Path archiveDir; + + @BeforeEach + public void setup() throws IOException { + modelsDir = tempBase.resolve("models"); + archiveDir = tempBase.resolve("archive"); + Files.createDirectories(modelsDir); + Files.createDirectories(archiveDir); + + pathResolver = new PathResolver(); + configService = new ConfigService(new EncryptionUtils(), pathResolver); + configService.setModelsPath(modelsDir.toString()); + configService.setArchivePath(archiveDir.toString()); + + localScanner = new LocalModelScanner(configService); + archiveService = new ArchiveService(configService, pathResolver); + analyzer = new ComfyModelAnalyzer(); + + ReflectionTestUtils.setField(analyzer, "modelListService", new ModelListService()); + // ComfyModelAnalyzer doesn't have pathResolver/configService/archiveService fields anymore, + // it uses modelListService and AI services. + } + + @Test + public void testFullStatusLifecycle() throws IOException { + String modelName = "test_model.safetensors"; + String workflow = "{\"nodes\":[{\"type\":\"CheckpointLoaderSimple\",\"widgets_values\":[\"" + modelName + "\"]}]}"; + + // 1. Initial State: Missing (Idle) + List results = analyzer.analyze(workflow, "test.json"); + assertEquals(1, results.size()); + assertEquals("Idle", getStatus(results.get(0), false, false, 0)); + + // 2. State: Known Good (URL found) + results.get(0).setUrl("http://example.com/model.safetensors"); + assertEquals("✅ Known Good", getStatus(results.get(0), false, false, 0)); + + // 3. State: Size Mismatch (Local file exists but size unknown/different) + Path modelPath = modelsDir.resolve("checkpoints").resolve(modelName); + Files.createDirectories(modelPath.getParent()); + Files.write(modelPath, new byte[1000]); // 1KB local file + + results.get(0).setByteSize(2000); // 2KB remote size + assertEquals("🔄 Size Mismatch", getStatus(results.get(0), true, false, 1000)); + + // 4. State: Already Exists (Size match) + Files.write(modelPath, new byte[2000]); // Update to 2KB + assertEquals("✅ Already exists", getStatus(results.get(0), true, false, 2000)); + + // 5. State: Found in Archive (Removed locally, moved to archive) + Files.delete(modelPath); + Path archivePath = archiveDir.resolve("checkpoints").resolve(modelName); + Files.createDirectories(archivePath.getParent()); + Files.write(archivePath, new byte[2000]); + + assertEquals("📦 Found in Archive", getStatus(results.get(0), false, true, 0)); + + // 6. State: Already exists (Archived copy found) - Exists in both + Files.write(modelPath, new byte[2000]); + assertEquals("✅ Already exists (Archived copy found)", getStatus(results.get(0), true, true, 2000)); + } + + /** + * Helper to simulate the status logic from Main.java analyzeJsonContent() + */ + private String getStatus(ModelInfo info, boolean existsLocally, boolean existsInArchive, long localSize) { + boolean sizeMismatch = false; + if (existsLocally && info.getByteSize() > 0) { + if (localSize != info.getByteSize()) { + sizeMismatch = true; + existsLocally = false; + } + } + + if (existsLocally && existsInArchive) return "✅ Already exists (Archived copy found)"; + if (existsLocally) return "✅ Already exists"; + if (existsInArchive) return "📦 Found in Archive"; + if (sizeMismatch) return "🔄 Size Mismatch"; + + if (info.getUrl().equals("MISSING")) return "Idle"; + + return "✅ Known Good"; + } +} diff --git a/web/downloader.js b/web/downloader.js index beecc6d..e27c7f0 100644 --- a/web/downloader.js +++ b/web/downloader.js @@ -1,108 +1,62 @@ -// Try different ways to get the ComfyUI app and api objects +// ComfyUI Model Downloader - Bridge Extension let app = null; let api = null; -const tryImports = async () => { - const paths = [ - "../../scripts/app.js", - "/scripts/app.js", - "../../../scripts/app.js" - ]; - const apiPaths = [ - "../../scripts/api.js", - "/scripts/api.js", - "../../../scripts/api.js" - ]; +const initializeExtension = async () => { + console.log("[Model-Downloader] Initializing..."); + + // 1. Try to find ComfyUI app and api objects + const paths = ["../../scripts/app.js", "/scripts/app.js", "../../../scripts/app.js"]; + const apiPaths = ["../../scripts/api.js", "/scripts/api.js", "../../../scripts/api.js"]; for (const path of paths) { try { const mod = await import(path); - if (mod.app) { - app = mod.app; - console.log(`[Model-Downloader] Successfully imported app from ${path}`); - break; - } + if (mod.app) { app = mod.app; break; } } 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; - } + if (mod.api) { api = mod.api; break; } } catch (e) {} } -}; -await tryImports(); - -// NEU: Dynamischer Import für den WebSocket-Listener -const setupListener = async () => { - const apiPaths = [ - "../../scripts/api.js", - "/scripts/api.js", - "../../../scripts/api.js" - ]; - - for (const path of apiPaths) { - try { - const mod = await import(path); - if (mod.api) { - console.log("%c[Model-Downloader] WebSocket Listener erfolgreich verknüpft!", "color: #00ff00; font-weight: bold;"); - - mod.api.addEventListener("kippster-refresh-ui", (event) => { - console.log("%c🔥 [Model-Downloader] SIGNAL EMPFANGEN! Lade UI neu...", "background: #ff0000; color: #fff; font-size: 14px;"); - - if (app && typeof app.refresh === 'function') { - app.refresh(); // Aktualisiert die Model-Dropdowns! - - // Kleines UI-Popup zur Bestätigung - if (app.ui?.dialog?.show) { - app.ui.dialog.show("✅ Downloads abgeschlossen! Modelle sind jetzt im Dropdown."); - } else { - alert("✅ Downloads abgeschlossen! Modelle aktualisiert."); - } - } - }); - break; // Erfolgreich gefunden, Schleife abbrechen - } - } catch (e) {} + if (!app) { + console.warn("[Model-Downloader] Could not find ComfyUI app object. Retrying in 2s..."); + setTimeout(initializeExtension, 2000); + return; } -}; -await setupListener(); + // 2. Setup WebSocket Listener for Auto-Refresh + if (api) { + console.log("%c[Model-Downloader] WebSocket Listener verknüpft!", "color: #00ff00; font-weight: bold;"); + api.addEventListener("kippster-refresh-ui", (event) => { + console.log("%c🔥 [Model-Downloader] SIGNAL EMPFANGEN! Lade UI neu...", "background: #ff0000; color: #fff; font-size: 14px;"); + if (app && typeof app.refresh === 'function') { + app.refresh(); + const msg = "✅ Downloads abgeschlossen! Modelle sind jetzt verfügbar."; + if (app.ui?.dialog?.show) app.ui.dialog.show(msg); + else alert(msg); + } + }); + } -// Define setup logic that can be called by extension or as fallback -const initializeExtension = async () => { + // 3. Load Token and setup UI components let apiToken = null; - const loadConfig = async () => { try { const baseUrl = new URL(".", import.meta.url).href; const configUrl = `${baseUrl}config.json?v=${Date.now()}`; - 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) { - 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.error(`[Model-Downloader] Could not load config.json (Status: ${response.status})`); } - } catch (e) { - console.error("[Model-Downloader] Failed to load config.json:", e); - } + } catch (e) { console.error("[Model-Downloader] Config load error:", e); } }; - - loadConfig(); + await loadConfig(); const notify = (msg) => { if (app?.ui?.dialog?.show) app.ui.dialog.show(msg); @@ -117,225 +71,68 @@ const initializeExtension = async () => {
🚀❌

Downloader nicht erreichbar

-

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

-
    -
  • Die Java-App gestartet wurde.
  • -
  • Port 12345 nicht blockiert wird.
  • -
-

Noch nicht installiert? Lade die App hier herunter:

+

Die Model Downloader App antwortet nicht. Bitte stelle sicher, dass sie läuft.

📥 Download via SourceForge
`; document.body.appendChild(dialog); dialog.showModal(); - dialog.querySelector("#tki-error-close").onclick = () => { - dialog.close(); - document.body.removeChild(dialog); - }; + dialog.querySelector("#tki-error-close").onclick = () => { dialog.close(); document.body.removeChild(dialog); }; }; const sendWorkflow = async () => { - if (!app || !app.graph) return notify("❌ Fehler: ComfyUI Graph nicht bereit."); + if (!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 { const response = await fetch("http://127.0.0.1:12345/import", { - method: "POST", - mode: "cors", - headers: { - "Content-Type": "application/json", - "Authorization": `Bearer ${apiToken}` - }, + method: "POST", mode: "cors", + headers: { "Content-Type": "application/json", "Authorization": `Bearer ${apiToken}` }, body: JSON.stringify(workflow) }); if (response.ok) notify("🚀 Workflow erfolgreich gesendet!"); else notify("❌ Fehler: Downloader abgelehnt (Status: " + response.status + ")"); - } catch (e) { - showConnectionError(); + } catch (e) { showConnectionError(); } + }; + + // UI Buttons + const addUI = () => { + if (!document.getElementById("tki-downloader-fab")) { + const fab = document.createElement("div"); + fab.id = "tki-downloader-fab"; + fab.innerHTML = "🚀"; + 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(0,0,0,0.5); border: 2px solid white;"; + fab.onclick = sendWorkflow; + document.body.appendChild(fab); + } + const menu = document.querySelector(".comfy-menu"); + if (menu && !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); } - }; - - 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; - const fab = document.createElement("div"); - fab.id = "tki-downloader-fab"; - fab.innerHTML = "🚀"; - 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); - }; - - const addV2Action = () => { if (app?.ui?.menu?.addPrimaryAction) { 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 }); } }; - const addClassic = () => { - const menu = document.querySelector(".comfy-menu"); - if (menu) { - 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); - } - } - }; - - addFAB(); addClassic(); addV2Action(); + addUI(); }; -if (app) { - app.registerExtension({ name: "TKI.ModelDownloader", async setup() { await initializeExtension(); } }); -} else if (document.readyState === "complete") { - initializeExtension(); -} else { - window.addEventListener("load", initializeExtension); -} +// Register the extension +const register = () => { + // We try multiple ways to hook into ComfyUI + if (window.app && window.app.registerExtension) { + window.app.registerExtension({ name: "TKI.ModelDownloader", async setup() { await initializeExtension(); } }); + } else { + // Fallback for manual load + if (document.readyState === "complete") initializeExtension(); + else window.addEventListener("load", initializeExtension); + } +}; + +register();