package de.tki.comfymodels; import de.tki.comfymodels.domain.ModelInfo; import de.tki.comfymodels.service.IDownloadManager; import de.tki.comfymodels.service.IModelAnalyzer; import de.tki.comfymodels.service.IWorkflowService; import de.tki.comfymodels.service.IModelValidator; import de.tki.comfymodels.service.IModelSearchService; import de.tki.comfymodels.service.impl.ConfigService; import de.tki.comfymodels.service.impl.GeminiAIService; import de.tki.comfymodels.service.impl.ModelListService; import de.tki.comfymodels.service.impl.ModelHashRegistry; import com.formdev.flatlaf.FlatDarkLaf; import com.formdev.flatlaf.FlatLightLaf; import com.formdev.flatlaf.FlatLaf; import com.formdev.flatlaf.extras.FlatAnimatedLafChange; import org.json.JSONArray; import org.json.JSONObject; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.stereotype.Component; import javax.swing.*; import javax.swing.table.DefaultTableModel; import javax.swing.event.TableModelListener; import javax.swing.event.TableModelEvent; import java.awt.*; import java.awt.datatransfer.DataFlavor; import java.awt.dnd.*; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.awt.event.ActionListener; import java.awt.image.BufferedImage; import java.io.*; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardCopyOption; import java.util.List; import java.util.ArrayList; import java.util.Map; import java.util.HashMap; import java.util.stream.Collectors; @Component public class Main extends JFrame { private final IModelAnalyzer analyzer; private final IDownloadManager downloadManager; private final IWorkflowService workflowService; private final IModelSearchService searchService; private final IModelValidator modelValidator; private final de.tki.comfymodels.service.impl.RestBridgeService restBridge; @Autowired private ConfigService configService; @Autowired private GeminiAIService geminiService; @Autowired private ModelListService modelListService; @Autowired private ModelHashRegistry hashRegistry; private JTextField modelsPathField; private JTextField comfyPathField; private JPasswordField geminiKeyField; private JPasswordField hfTokenField; private JCheckBox backgroundCheck; private JCheckBox shutdownCheck; private JCheckBox darkCheck; private JLabel activeAiModelLabel; private JTextArea jsonInputArea; private DefaultTableModel tableModel; private JLabel statusLabel; private JButton downloadButton, pauseButton, stopButton; private List modelsToDownload; private String currentFileName = "input.json"; public Main(IModelAnalyzer analyzer, IDownloadManager downloadManager, IWorkflowService workflowService, IModelSearchService searchService, IModelValidator modelValidator, de.tki.comfymodels.service.impl.RestBridgeService restBridge) { this.analyzer = analyzer; this.downloadManager = downloadManager; this.workflowService = workflowService; this.searchService = searchService; this.modelValidator = modelValidator; this.restBridge = restBridge; } public void launch(String[] args) { // Initialize REST Bridge consumer EARLY restBridge.setWorkflowConsumer(workflowJson -> { SwingUtilities.invokeLater(() -> { if (jsonInputArea != null) { jsonInputArea.setText(workflowJson); currentFileName = "remote_workflow.json"; analyzeJsonContent(); } setVisible(true); toFront(); requestFocus(); }); }); // Load settings to get/generate the API Token if (configService.isUnlocked()) { restBridge.setApiToken(configService.getApiToken()); } restBridge.startServer(); if (!promptForPassword()) { System.exit(0); } // Apply theme before UI initialization try { if (configService.isDarkMode()) { FlatDarkLaf.setup(); } else { FlatLightLaf.setup(); } } catch (Exception e) { System.err.println("Theme setup failed: " + e.getMessage()); } FlatLaf.updateUI(); // Modern UI tweaks UIManager.put("TitlePane.unifiedBackground", true); UIManager.put("Button.arc", 8); UIManager.put("Component.arc", 8); UIManager.put("TextComponent.arc", 8); UIManager.put("ProgressBar.arc", 8); SwingUtilities.invokeLater(() -> { try { initUI(); setupTrayIcon(); loadSettingsIntoUI(); updateAiModelDisplay(); // Add WindowListener to handle background mode addWindowListener(new java.awt.event.WindowAdapter() { @Override public void windowClosing(java.awt.event.WindowEvent e) { if (configService.isBackgroundModeEnabled()) { setVisible(false); } else { downloadManager.stop(); System.exit(0); } } }); setVisible(true); } catch (Exception e) { e.printStackTrace(); JOptionPane.showMessageDialog(null, "Critical UI Error: " + e.getMessage()); } }); } private void setupTrayIcon() { if (!SystemTray.isSupported()) return; SystemTray tray = SystemTray.getSystemTray(); BufferedImage image = new BufferedImage(16, 16, BufferedImage.TYPE_INT_RGB); Graphics2D g2 = image.createGraphics(); g2.setColor(new Color(255, 105, 180)); // Hot Pink g2.fillRect(0, 0, 16, 16); g2.dispose(); PopupMenu popup = new PopupMenu(); MenuItem showItem = new MenuItem("Show UI"); showItem.addActionListener(e -> setVisible(true)); MenuItem exitItem = new MenuItem("Exit"); exitItem.addActionListener(e -> { downloadManager.stop(); System.exit(0); }); popup.add(showItem); popup.addSeparator(); popup.add(exitItem); TrayIcon trayIcon = new TrayIcon(image, "ComfyUI Model Downloader", popup); trayIcon.setImageAutoSize(true); trayIcon.addActionListener(e -> setVisible(true)); try { tray.add(trayIcon); } catch (AWTException e) { System.err.println("TrayIcon could not be added."); } } private boolean promptForPassword() { while (true) { JPanel panel = new JPanel(new BorderLayout(5, 5)); panel.add(new JLabel("Enter Vault Password (to unlock API Keys):"), BorderLayout.NORTH); JPasswordField pf = new JPasswordField(); panel.add(pf, BorderLayout.CENTER); SwingUtilities.invokeLater(() -> pf.requestFocusInWindow()); int ok = JOptionPane.showConfirmDialog(null, panel, "Vault Unlock", JOptionPane.OK_CANCEL_OPTION, JOptionPane.QUESTION_MESSAGE); if (ok != JOptionPane.OK_OPTION) return false; String pass = new String(pf.getPassword()); if (pass.trim().isEmpty()) { JOptionPane.showMessageDialog(null, "Password cannot be empty.", "Validation", JOptionPane.WARNING_MESSAGE); continue; } try { configService.unlock(pass); restBridge.setApiToken(configService.getApiToken()); return true; } catch (Exception e) { JOptionPane.showMessageDialog(null, "Unlock Failed: " + e.getMessage(), "Error", JOptionPane.ERROR_MESSAGE); } } } private void loadSettingsIntoUI() { if (modelsPathField != null) modelsPathField.setText(configService.getModelsPath()); if (comfyPathField != null) comfyPathField.setText(configService.getComfyUIPath()); if (geminiKeyField != null) geminiKeyField.setText(configService.getGeminiApiKey()); if (hfTokenField != null) hfTokenField.setText(configService.getHfToken()); if (backgroundCheck != null) backgroundCheck.setSelected(configService.isBackgroundModeEnabled()); if (shutdownCheck != null) shutdownCheck.setSelected(configService.isShutdownAfterDownloadEnabled()); if (darkCheck != null) darkCheck.setSelected(configService.isDarkMode()); } private void updateAiModelDisplay() { new Thread(() -> { String model = geminiService.discoverBestModel(); SwingUtilities.invokeLater(() -> activeAiModelLabel.setText("Active AI: " + model)); }).start(); } private void initUI() { setTitle("ComfyUIModel-Downloader"); setSize(1450, 1000); setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE); getRootPane().putClientProperty("flatlaf.useWindowDecorations", true); JPanel mainContainer = new JPanel(new BorderLayout(10, 10)); mainContainer.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); setContentPane(mainContainer); JPanel settingsPanel = new JPanel(new GridLayout(5, 1, 5, 5)); // Row 1: Models Directory JPanel pathRow = new JPanel(new BorderLayout()); pathRow.setBorder(BorderFactory.createTitledBorder("Models Directory")); modelsPathField = new JTextField(); JButton browsePathBtn = new JButton("Set Folder..."); browsePathBtn.addActionListener(e -> { JFileChooser chooser = new JFileChooser(); chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); if (chooser.showOpenDialog(this) == JFileChooser.APPROVE_OPTION) { modelsPathField.setText(chooser.getSelectedFile().getAbsolutePath()); configService.setModelsPath(modelsPathField.getText()); } }); JButton savePathBtn = new JButton("Save Path"); savePathBtn.addActionListener(e -> { configService.setModelsPath(modelsPathField.getText()); statusLabel.setText("Models path saved."); }); JPanel pathButtons = new JPanel(new FlowLayout(FlowLayout.RIGHT, 0, 0)); pathButtons.add(savePathBtn); pathButtons.add(browsePathBtn); pathRow.add(modelsPathField, BorderLayout.CENTER); pathRow.add(pathButtons, BorderLayout.EAST); // Row 2: ComfyUI Installation (Simplified in Main) JPanel comfyRow = new JPanel(new BorderLayout()); comfyRow.setBorder(BorderFactory.createTitledBorder("ComfyUI Integration")); JButton openInstallDialogBtn = new JButton("🚀 Install/Update Bridge..."); openInstallDialogBtn.addActionListener(e -> showInstallationDialog()); comfyRow.add(openInstallDialogBtn, BorderLayout.CENTER); // Row 3: Gemini API Key JPanel apiRow = new JPanel(new BorderLayout()); apiRow.setBorder(BorderFactory.createTitledBorder("Gemini AI API Key (Optional)")); geminiKeyField = new JPasswordField(); JButton saveGeminiBtn = new JButton("Save & Discover"); saveGeminiBtn.addActionListener(e -> { configService.setGeminiApiKey(new String(geminiKeyField.getPassword())); updateAiModelDisplay(); }); apiRow.add(geminiKeyField, BorderLayout.CENTER); apiRow.add(saveGeminiBtn, BorderLayout.EAST); // Row 4: Hugging Face Token JPanel hfRow = new JPanel(new BorderLayout()); hfRow.setBorder(BorderFactory.createTitledBorder("Hugging Face Access Token (Optional)")); hfTokenField = new JPasswordField(); JButton saveHfBtn = new JButton("Save HF Token"); saveHfBtn.addActionListener(e -> { configService.setHfToken(new String(hfTokenField.getPassword())); statusLabel.setText("HF Token saved."); analyzeJsonContent(); }); hfRow.add(hfTokenField, BorderLayout.CENTER); hfRow.add(saveHfBtn, BorderLayout.EAST); // Row 5: Options & Info JPanel infoRow = new JPanel(new FlowLayout(FlowLayout.LEFT)); activeAiModelLabel = new JLabel("Active AI: Loading..."); JButton helpBtn = new JButton("ℹ Help"); helpBtn.addActionListener(e -> showHelpDialog()); JButton verifyBtn = new JButton("🔍 Fast Verify (Corruption)"); verifyBtn.addActionListener(e -> verifyLocalModels(false)); JButton optimizeBtn = new JButton("👯 Storage Optimizer (Duplicates)"); optimizeBtn.addActionListener(e -> { int confirm = JOptionPane.showConfirmDialog(this, "The Storage Optimizer calculates SHA-256 hashes for all local models.\n" + "This is EXTREMELY slow and resource-intensive for large libraries.\n\n" + "Do you want to proceed?", "Performance Warning", JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE); if (confirm == JOptionPane.YES_OPTION) { verifyLocalModels(true); } }); backgroundCheck = new JCheckBox("Stay in Background"); backgroundCheck.addActionListener(e -> configService.setBackgroundModeEnabled(backgroundCheck.isSelected())); shutdownCheck = new JCheckBox("Shutdown after Queue"); shutdownCheck.addActionListener(e -> configService.setShutdownAfterDownloadEnabled(shutdownCheck.isSelected())); darkCheck = new JCheckBox("Dark Mode"); darkCheck.addActionListener(e -> { configService.setDarkMode(darkCheck.isSelected()); FlatAnimatedLafChange.showSnapshot(); if (darkCheck.isSelected()) FlatDarkLaf.setup(); else FlatLightLaf.setup(); FlatLaf.updateUI(); FlatAnimatedLafChange.hideSnapshotWithAnimation(); }); infoRow.add(new JSeparator(JSeparator.VERTICAL)); infoRow.add(helpBtn); infoRow.add(verifyBtn); infoRow.add(optimizeBtn); infoRow.add(backgroundCheck); infoRow.add(shutdownCheck); infoRow.add(darkCheck); settingsPanel.add(pathRow); settingsPanel.add(comfyRow); settingsPanel.add(apiRow); settingsPanel.add(hfRow); settingsPanel.add(infoRow); JSplitPane splitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT); JPanel jsonPanel = new JPanel(new BorderLayout()); jsonPanel.setBorder(BorderFactory.createTitledBorder("Workflow (Drag & Drop JSON/PNG)")); jsonInputArea = new JTextArea(); setupDragAndDrop(jsonInputArea); JScrollPane jsonScroll = new JScrollPane(jsonInputArea); JPanel jsonButtons = new JPanel(new FlowLayout(FlowLayout.RIGHT)); JButton loadJsonBtn = new JButton("Load Workflow..."); loadJsonBtn.addActionListener(e -> { FileDialog fd = new FileDialog(this, "Select Workflow", FileDialog.LOAD); fd.setVisible(true); if (fd.getFile() != null) loadFile(new File(fd.getDirectory(), fd.getFile())); }); JButton importModelListBtn = new JButton("Import Model List..."); importModelListBtn.addActionListener(e -> { FileDialog fd = new FileDialog(this, "Select Model List JSON", FileDialog.LOAD); fd.setVisible(true); if (fd.getFile() != null) { try { modelListService.importJson(new File(fd.getDirectory(), fd.getFile())); JOptionPane.showMessageDialog(this, "Model list imported successfully! (" + modelListService.getModels().size() + " models)"); analyzeJsonContent(); } catch (Exception ex) { JOptionPane.showMessageDialog(this, "Error: " + ex.getMessage()); } } }); JButton analyzeBtn = new JButton("Deep Search"); analyzeBtn.addActionListener(e -> { analyzeJsonContent(); searchMissingOnline(); }); jsonButtons.add(loadJsonBtn); jsonButtons.add(importModelListBtn); jsonButtons.add(analyzeBtn); jsonPanel.add(jsonScroll, BorderLayout.CENTER); jsonPanel.add(jsonButtons, BorderLayout.SOUTH); String[] columnNames = {"Select", "Type", "Name", "Size", "AI Source", "Target Path", "URL", "Status"}; tableModel = new DefaultTableModel(columnNames, 0) { @Override public Class getColumnClass(int c) { return c == 0 ? Boolean.class : String.class; } @Override public boolean isCellEditable(int r, int c) { return c == 0; } }; tableModel.addTableModelListener(e -> { if (e.getType() == TableModelEvent.UPDATE && e.getColumn() == 0) updateDownloadManagerSelection(); }); JTable modelTable = new JTable(tableModel); modelTable.putClientProperty("terminateEditOnFocusLost", Boolean.TRUE); JScrollPane tableScroll = new JScrollPane(modelTable); JPanel tablePanel = new JPanel(new BorderLayout()); tablePanel.setBorder(BorderFactory.createTitledBorder("Identified Models")); tablePanel.add(tableScroll, BorderLayout.CENTER); splitPane.setTopComponent(jsonPanel); splitPane.setBottomComponent(tablePanel); splitPane.setDividerLocation(300); JPanel bottomPanel = new JPanel(new BorderLayout()); statusLabel = new JLabel("Ready"); JPanel progressPanel = new JPanel(new GridLayout(2, 1)); activeAiModelLabel.setFont(new Font("SansSerif", Font.ITALIC, 11)); activeAiModelLabel.setForeground(Color.GRAY); progressPanel.add(statusLabel); progressPanel.add(activeAiModelLabel); JPanel actionButtons = new JPanel(new FlowLayout(FlowLayout.RIGHT)); downloadButton = new JButton("Start Queue"); downloadButton.setEnabled(false); downloadButton.addActionListener(e -> startDownloadQueue()); pauseButton = new JButton("Pause"); pauseButton.addActionListener(e -> { downloadManager.togglePause(); pauseButton.setText(downloadManager.isPaused() ? "Resume" : "Pause"); }); stopButton = new JButton("Stop"); stopButton.addActionListener(e -> downloadManager.stop()); actionButtons.add(downloadButton); actionButtons.add(pauseButton); actionButtons.add(stopButton); bottomPanel.add(progressPanel, BorderLayout.CENTER); bottomPanel.add(actionButtons, BorderLayout.EAST); mainContainer.add(settingsPanel, BorderLayout.NORTH); mainContainer.add(splitPane, BorderLayout.CENTER); mainContainer.add(bottomPanel, BorderLayout.SOUTH); } private void startDownloadQueue() { int rowCount = tableModel.getRowCount(); if (rowCount == 0) return; boolean[] selected = new boolean[rowCount]; for (int i = 0; i < rowCount; i++) { selected[i] = (Boolean) tableModel.getValueAt(i, 0); String url = (String) tableModel.getValueAt(i, 6); if (url != null && !url.equals("MISSING")) modelsToDownload.get(i).setUrl(url); } downloadButton.setEnabled(false); downloadManager.startQueue(modelsToDownload, selected, modelsPathField.getText(), (idx, status) -> SwingUtilities.invokeLater(() -> tableModel.setValueAt(status, idx, 7)), () -> SwingUtilities.invokeLater(() -> { downloadButton.setEnabled(true); statusLabel.setText("Queue finished."); if (configService.isShutdownAfterDownloadEnabled()) performSystemShutdown(); }) ); } private void showInstallationDialog() { JDialog dialog = new JDialog(this, "ComfyUI Bridge Installation", true); dialog.setLayout(new BorderLayout()); dialog.setSize(650, 360); dialog.setLocationRelativeTo(this); JPanel content = new JPanel(new GridBagLayout()); content.setBorder(BorderFactory.createEmptyBorder(20, 20, 20, 20)); GridBagConstraints gbc = new GridBagConstraints(); gbc.fill = GridBagConstraints.HORIZONTAL; gbc.gridx = 0; gbc.gridy = 0; gbc.weightx = 1.0; gbc.insets = new Insets(0, 0, 15, 0); // Explanation text JTextArea infoArea = new JTextArea( "This installer will set up the ComfyUI-Model-Downloader bridge.\n\n" + "1. Select your ComfyUI main directory.\n" + "2. Old or conflicting bridge files will be cleaned up.\n" + "3. A link or copy of the UI extension will be created.\n\n" + "Important: Restart ComfyUI after the installation is finished." ); infoArea.setEditable(false); infoArea.setFocusable(false); infoArea.setBackground(content.getBackground()); infoArea.setLineWrap(true); infoArea.setWrapStyleWord(true); content.add(infoArea, gbc); // Path selection header gbc.gridy++; gbc.insets = new Insets(0, 0, 5, 0); content.add(new JLabel("ComfyUI Main Directory:"), gbc); // Path selection row gbc.gridy++; gbc.insets = new Insets(0, 0, 10, 0); JPanel pathRow = new JPanel(new BorderLayout(5, 0)); JTextField pathField = new JTextField(configService.getComfyUIPath()); JButton browseBtn = new JButton("Browse..."); browseBtn.addActionListener(e -> { JFileChooser chooser = new JFileChooser(); chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); if (chooser.showOpenDialog(dialog) == JFileChooser.APPROVE_OPTION) { pathField.setText(chooser.getSelectedFile().getAbsolutePath()); } }); pathRow.add(pathField, BorderLayout.CENTER); pathRow.add(browseBtn, BorderLayout.EAST); content.add(pathRow, gbc); // Spacer to push everything to the top gbc.gridy++; gbc.weighty = 1.0; content.add(new JPanel(), gbc); // Action buttons JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT)); JButton cancelBtn = new JButton("Cancel"); cancelBtn.addActionListener(e -> dialog.dispose()); JButton installBtn = new JButton("🚀 Start Installation"); installBtn.addActionListener(e -> { String selectedPath = pathField.getText().trim(); if (selectedPath.isEmpty()) { JOptionPane.showMessageDialog(dialog, "Please select a path first."); return; } installComfyUIBridge(selectedPath, dialog); }); buttonPanel.add(cancelBtn); buttonPanel.add(installBtn); dialog.add(content, BorderLayout.CENTER); dialog.add(buttonPanel, BorderLayout.SOUTH); dialog.setVisible(true); } private void installComfyUIBridge(String comfyPath, JDialog parentDialog) { File inputPath = new File(comfyPath); if (!inputPath.exists()) { JOptionPane.showMessageDialog(parentDialog, "The provided path does not exist: " + comfyPath, "Error", JOptionPane.ERROR_MESSAGE); return; } // Systematically search for custom_nodes File customNodesDir = null; // Candidate 0: Specific Desktop App nested structure (High Priority) File c0 = new File(inputPath, "resources" + File.separator + "ComfyUI" + File.separator + "custom_nodes"); // Candidate 1: inputPath/custom_nodes (Standard) File c1 = new File(inputPath, "custom_nodes"); // Candidate 2: inputPath/ComfyUI/custom_nodes (Portable Root) File c2 = new File(inputPath, "ComfyUI" + File.separator + "custom_nodes"); // Candidate 3: inputPath itself (User selected custom_nodes directly) File c3 = inputPath; if (c0.exists() && c0.isDirectory()) customNodesDir = c0; else if (c1.exists() && c1.isDirectory()) customNodesDir = c1; else if (c2.exists() && c2.isDirectory()) customNodesDir = c2; else if (c3.getName().equalsIgnoreCase("custom_nodes") && c3.isDirectory()) customNodesDir = c3; // --- IMPROVEMENT: DEEP SEARCH (if candidates fail) --- if (customNodesDir == null) { System.out.println("Standard candidates failed. Starting deep search in: " + inputPath.getAbsolutePath()); customNodesDir = findCustomNodesDeep(inputPath, 0); } if (customNodesDir == null) { String msg = "Could not find 'custom_nodes' folder in the selected directory.\n\n" + "Checked locations (among others):\n" + "- " + c0.getAbsolutePath() + "\n" + "- " + c1.getAbsolutePath() + "\n" + "- " + c2.getAbsolutePath() + "\n\n" + "Please ensure you select the folder that contains 'custom_nodes' or the main ComfyUI folder."; JOptionPane.showMessageDialog(parentDialog, msg, "Installation Error", JOptionPane.ERROR_MESSAGE); return; } // --- IMPROVEMENT 1: CLEANUP LEGACY/CONFLICTING FILES --- String[] conflictingFiles = {"comfyui_to_downloader.py", "comfyui-model-downloader.py"}; for (String conflict : conflictingFiles) { File conflictFile = new File(customNodesDir, conflict); if (conflictFile.exists()) { System.out.println("Cleaning up legacy file: " + conflictFile.getAbsolutePath()); conflictFile.delete(); } } configService.setComfyUIPath(comfyPath); File targetDir = new File(customNodesDir, "comfyui-model-downloader"); try { // Cleanup old directory if (targetDir.exists()) { deleteDirectory(targetDir); } targetDir.mkdirs(); // Extract from resources extractResource("/comfyui-bridge/__init__.py", new File(targetDir, "__init__.py")); File webDir = new File(targetDir, "web"); webDir.mkdirs(); extractResource("/comfyui-bridge/web/downloader.js", new File(webDir, "downloader.js")); writeExtensionConfig(targetDir); if (new File(targetDir, "__init__.py").exists()) { String msg = "🚀 ComfyUI Bridge installed successfully!\n\n" + "Location: " + targetDir.getAbsolutePath() + "\n\n" + "Please RESTART ComfyUI now to see the rocket icon."; JOptionPane.showMessageDialog(parentDialog, msg, "Success", JOptionPane.INFORMATION_MESSAGE); parentDialog.dispose(); } else { throw new Exception("Installation failed: __init__.py not found at target!"); } } catch (Exception e) { JOptionPane.showMessageDialog(parentDialog, "Critical failure during installation: " + e.getMessage(), "Error", JOptionPane.ERROR_MESSAGE); } } private void extractResource(String resourcePath, File destination) throws IOException { try (InputStream is = getClass().getResourceAsStream(resourcePath)) { if (is == null) throw new IOException("Resource not found: " + resourcePath); Files.copy(is, destination.toPath(), StandardCopyOption.REPLACE_EXISTING); } } private File findCustomNodesDeep(File dir, int depth) { if (depth > 2) return null; // Limit depth to avoid performance issues File[] files = dir.listFiles(); if (files == null) return null; // Check immediate children first for (File f : files) { if (f.isDirectory() && f.getName().equalsIgnoreCase("custom_nodes")) { return f; } } // Recurse for (File f : files) { if (f.isDirectory() && !f.getName().startsWith(".") && !f.getName().equalsIgnoreCase("node_modules")) { File found = findCustomNodesDeep(f, depth + 1); if (found != null) return found; } } return null; } private void writeExtensionConfig(File dir) { try { File webDir = new File(dir, "web"); if (!webDir.exists()) webDir.mkdirs(); JSONObject config = new JSONObject(); config.put("token", configService.getApiToken()); Files.writeString(new File(webDir, "config.json").toPath(), config.toString(4)); } catch (Exception e) { e.printStackTrace(); } } private void deleteDirectory(File dir) { File[] files = dir.listFiles(); if (files != null) for (File f : files) deleteDirectory(f); dir.delete(); } private void copyDirectory(File s, File d) throws IOException { if (s.isDirectory()) { if (!d.exists()) d.mkdirs(); String[] children = s.list(); if (children != null) for (String c : children) copyDirectory(new File(s, c), new File(d, c)); } else Files.copy(s.toPath(), d.toPath(), StandardCopyOption.REPLACE_EXISTING); } private void loadFile(File file) { try { currentFileName = file.getName(); jsonInputArea.setText(workflowService.extractWorkflow(file)); analyzeJsonContent(); } catch (IOException ex) { JOptionPane.showMessageDialog(this, "Error: " + ex.getMessage()); } } private void analyzeJsonContent() { String text = jsonInputArea.getText(); if (text == null || text.isEmpty()) return; modelsToDownload = analyzer.analyze(text, currentFileName); tableModel.setRowCount(0); String base = modelsPathField.getText(); for (int i = 0; i < modelsToDownload.size(); i++) { ModelInfo info = modelsToDownload.get(i); String type = info.getType() != null ? info.getType() : "checkpoints"; Path local = Paths.get(base, info.getSave_path() != null ? info.getSave_path() : type, info.getName()); boolean exists = Files.exists(local); // Populate size from local file if it exists and currently unknown if (exists && "Unknown".equals(info.getSize())) { try { long bytes = Files.size(local); info.setSize(searchService.formatSize(bytes)); } catch (Exception e) {} } String status = exists ? "✅ Already exists" : (info.getUrl().equals("MISSING") ? "Idle" : "✅ Known Good"); tableModel.addRow(new Object[]{!exists, info.getType(), info.getName(), info.getSize(), info.getPopularity(), "models/" + type, info.getUrl(), status}); } downloadButton.setEnabled(!modelsToDownload.isEmpty()); // NEW: Automatically fetch missing sizes for remote models in background fetchMissingRemoteSizes(); } private void fetchMissingRemoteSizes() { if (modelsToDownload == null) return; new Thread(() -> { for (int i = 0; i < modelsToDownload.size(); i++) { final int idx = i; ModelInfo info = modelsToDownload.get(idx); if (!info.getUrl().equals("MISSING") && "Unknown".equals(info.getSize())) { long size = searchService.getRemoteSize(info.getUrl()); if (size > 0) { String formatted = searchService.formatSize(size); info.setSize(formatted); SwingUtilities.invokeLater(() -> { if (idx < tableModel.getRowCount()) { tableModel.setValueAt(formatted, idx, 3); } }); } } } }).start(); } private void searchMissingOnline() { if (modelsToDownload == null) return; statusLabel.setText("Searching..."); boolean[] selected = new boolean[tableModel.getRowCount()]; for (int i = 0; i < selected.length; i++) selected[i] = (Boolean) tableModel.getValueAt(i, 0); searchService.searchOnline(modelsToDownload, selected, jsonInputArea.getText(), currentFileName, (idx, status) -> SwingUtilities.invokeLater(() -> tableModel.setValueAt(status, idx, 7)), (idx, info) -> SwingUtilities.invokeLater(() -> { tableModel.setValueAt(info.getSize(), idx, 3); tableModel.setValueAt(info.getUrl(), idx, 6); tableModel.setValueAt("✅ Found", idx, 7); }), () -> SwingUtilities.invokeLater(() -> statusLabel.setText("Search finished.")) ); } private void verifyLocalModels(boolean checkDuplicates) { String baseDir = modelsPathField.getText(); if (baseDir == null || baseDir.isEmpty()) { JOptionPane.showMessageDialog(this, "Please set a models directory first."); return; } File root = new File(baseDir); if (!root.exists() || !root.isDirectory()) { JOptionPane.showMessageDialog(this, "Invalid models directory."); return; } String taskName = checkDuplicates ? "Verifying models & checking for duplicates" : "Verifying models (Fast Check)"; statusLabel.setText(taskName + "... please wait."); new Thread(() -> { try { List errors = new ArrayList<>(); Map> hashToPaths = new HashMap<>(); List allFiles = Files.walk(root.toPath()) .filter(Files::isRegularFile) .filter(p -> { String n = p.getFileName().toString().toLowerCase(); return n.endsWith(".safetensors") || n.endsWith(".sft") || n.endsWith(".ckpt") || n.endsWith(".pth") || n.endsWith(".pt") || n.endsWith(".bin"); }) .collect(Collectors.toList()); int total = allFiles.size(); for (int i = 0; i < total; i++) { Path p = allFiles.get(i); final int current = i + 1; SwingUtilities.invokeLater(() -> statusLabel.setText("Checking (" + current + "/" + total + "): " + p.getFileName())); IModelValidator.ValidationResult res = modelValidator.validateFile(p.toFile()); if (!res.ok) { errors.add(res); } else if (checkDuplicates) { String hash = hashRegistry.getOrCalculateHash(p.toFile()); if (hash != null) { hashToPaths.computeIfAbsent(hash, k -> new ArrayList<>()).add(p); } } } Map> duplicates = checkDuplicates ? hashToPaths.entrySet().stream() .filter(e -> e.getValue().size() > 1) .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)) : new HashMap<>(); SwingUtilities.invokeLater(() -> { statusLabel.setText("Scan finished. Found " + errors.size() + " issues and " + duplicates.size() + " duplicate sets."); if (errors.isEmpty() && duplicates.isEmpty()) { JOptionPane.showMessageDialog(this, "All " + total + " models verified successfully!", "Verification Complete", JOptionPane.INFORMATION_MESSAGE); return; } if (!errors.isEmpty()) { StringBuilder sb = new StringBuilder("The following " + errors.size() + " files appear to be corrupted or invalid:\n\n"); for (IModelValidator.ValidationResult err : errors) { sb.append("- ").append(new File(err.filePath).getName()) .append(" (").append(err.message).append(")\n") .append(" Path: ").append(err.filePath).append("\n\n"); } JTextArea textArea = new JTextArea(sb.toString()); textArea.setEditable(false); JScrollPane scrollPane = new JScrollPane(textArea); scrollPane.setPreferredSize(new Dimension(800, 500)); Object[] options = {"OK", "Delete All Corrupted Files"}; int choice = JOptionPane.showOptionDialog(this, scrollPane, "Verification Results - Issues Found", JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE, null, options, options[0]); if (choice == 1) { // Delete All Corrupted Files int confirm = JOptionPane.showConfirmDialog(this, "Are you sure you want to delete these " + errors.size() + " files?\nThis action cannot be undone.", "Confirm Deletion", JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE); if (confirm == JOptionPane.YES_OPTION) { int deletedCount = 0; for (IModelValidator.ValidationResult err : errors) { File f = new File(err.filePath); if (f.exists() && f.delete()) { deletedCount++; } } JOptionPane.showMessageDialog(this, "Deleted " + deletedCount + " files."); statusLabel.setText("Cleanup finished. Deleted " + deletedCount + " files."); } } } if (!duplicates.isEmpty()) { showDuplicatesDialog(duplicates); } }); } catch (IOException e) { SwingUtilities.invokeLater(() -> JOptionPane.showMessageDialog(this, "Error during verification: " + e.getMessage())); } }).start(); } private void showDuplicatesDialog(Map> duplicates) { StringBuilder sb = new StringBuilder("Storage Optimizer - Duplicate Models Found:\n\n"); sb.append("The following files have identical content (SHA-256 match).\n"); sb.append("You might want to delete redundant copies to save space.\n\n"); for (Map.Entry> entry : duplicates.entrySet()) { sb.append("SHA-256: ").append(entry.getKey()).append("\n"); for (Path p : entry.getValue()) { sb.append(" -> ").append(p.toAbsolutePath()).append("\n"); } sb.append("\n"); } JTextArea textArea = new JTextArea(sb.toString()); textArea.setEditable(false); JScrollPane scrollPane = new JScrollPane(textArea); scrollPane.setPreferredSize(new Dimension(900, 600)); JOptionPane.showMessageDialog(this, scrollPane, "Duplicates Found", JOptionPane.INFORMATION_MESSAGE); } private void updateDownloadManagerSelection() { if (downloadManager == null || tableModel == null) return; boolean[] selected = new boolean[tableModel.getRowCount()]; for (int i = 0; i < selected.length; i++) selected[i] = (Boolean) tableModel.getValueAt(i, 0); downloadManager.updateSelection(selected); } private void setupDragAndDrop(JTextArea area) { new DropTarget(area, new DropTargetListener() { public void dragEnter(DropTargetDragEvent dtde) {} public void dragOver(DropTargetDragEvent dtde) {} public void dropActionChanged(DropTargetDragEvent dtde) {} public void dragExit(DropTargetEvent dte) {} public void drop(DropTargetDropEvent dtde) { try { dtde.acceptDrop(DnDConstants.ACTION_COPY); java.util.List files = (java.util.List) dtde.getTransferable().getTransferData(DataFlavor.javaFileListFlavor); if (!files.isEmpty()) loadFile(files.get(0)); } catch (Exception e) {} } }); } private void performSystemShutdown() { try { Runtime.getRuntime().exec("shutdown /s /t 60"); } catch (IOException ignored) {} } private void showHelpDialog() { JOptionPane.showMessageDialog(this, "ComfyUI Model Downloader\n\n1. Set your Models path\n2. Load or Drag&Drop a Workflow\n3. Click Start Queue", "Help", JOptionPane.INFORMATION_MESSAGE); } public static void main(String[] args) { FlatLaf.setUseNativeWindowDecorations(true); AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class); context.getBean(Main.class).launch(args); } @Configuration @ComponentScan("de.tki.comfymodels") public static class AppConfig {} }