1759 lines
83 KiB
Java
1759 lines
83 KiB
Java
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 de.tki.comfymodels.ui.DotGridPanel;
|
||
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;
|
||
private final de.tki.comfymodels.service.impl.ArchiveService archiveService;
|
||
|
||
@Autowired
|
||
private ConfigService configService;
|
||
|
||
@Autowired
|
||
private GeminiAIService geminiService;
|
||
|
||
@Autowired
|
||
private ModelListService modelListService;
|
||
|
||
@Autowired
|
||
private ModelHashRegistry hashRegistry;
|
||
|
||
@Autowired
|
||
private de.tki.comfymodels.service.impl.LocalModelScanner localScanner;
|
||
|
||
@Autowired
|
||
private de.tki.comfymodels.service.impl.PathResolver pathResolver;
|
||
|
||
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<ModelInfo> modelsToDownload;
|
||
private String currentFileName = "input.json";
|
||
private Image appIcon;
|
||
|
||
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) {
|
||
this.analyzer = analyzer;
|
||
this.downloadManager = downloadManager;
|
||
this.workflowService = workflowService;
|
||
this.searchService = searchService;
|
||
this.modelValidator = modelValidator;
|
||
this.restBridge = restBridge;
|
||
this.archiveService = archiveService;
|
||
}
|
||
|
||
public void launch(String[] args) {
|
||
if (de.tki.comfymodels.util.PlatformUtils.isMac()) {
|
||
System.setProperty("apple.laf.useScreenMenuBar", "true");
|
||
System.setProperty("apple.awt.application.name", "ComfyUI Model Downloader");
|
||
System.setProperty("apple.awt.application.appearance", "system");
|
||
}
|
||
|
||
// Initialize REST Bridge consumer EARLY
|
||
restBridge.setWorkflowConsumer(workflowJson -> {
|
||
SwingUtilities.invokeLater(() -> {
|
||
if (jsonInputArea != null) {
|
||
jsonInputArea.setText(workflowJson);
|
||
currentFileName = "remote_workflow.json";
|
||
analyzeJsonContent();
|
||
searchMissingOnline(); // NEW: Automatically perform Deep Search to verify sizes/links
|
||
}
|
||
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
|
||
setupTheme(configService.isDarkMode());
|
||
|
||
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 setupTheme(boolean darkMode) {
|
||
try {
|
||
// Global arcs for a modern feel
|
||
UIManager.put("Button.arc", 8);
|
||
UIManager.put("Component.arc", 8);
|
||
UIManager.put("TextComponent.arc", 8);
|
||
UIManager.put("ProgressBar.arc", 8);
|
||
UIManager.put("TitlePane.unifiedBackground", true);
|
||
|
||
if (darkMode) {
|
||
// ComfyUI Dark Palette - using ColorUIResource to play nice with L&F
|
||
Color nodeBg = new javax.swing.plaf.ColorUIResource(43, 43, 43);
|
||
Color comfySurface = new javax.swing.plaf.ColorUIResource(32, 32, 32);
|
||
Color comfyAccent = new javax.swing.plaf.ColorUIResource(255, 204, 0);
|
||
Color comfyText = new javax.swing.plaf.ColorUIResource(204, 204, 204);
|
||
Color comfyBorder = new javax.swing.plaf.ColorUIResource(55, 55, 55);
|
||
|
||
UIManager.put("DefaultBackgroundColor", comfySurface);
|
||
UIManager.put("Panel.background", nodeBg);
|
||
UIManager.put("Table.background", comfySurface);
|
||
UIManager.put("TextArea.background", comfySurface);
|
||
UIManager.put("TextField.background", new javax.swing.plaf.ColorUIResource(40, 40, 40));
|
||
UIManager.put("PasswordField.background", new javax.swing.plaf.ColorUIResource(40, 40, 40));
|
||
|
||
UIManager.put("Label.foreground", comfyText);
|
||
UIManager.put("Table.foreground", comfyText);
|
||
UIManager.put("TextArea.foreground", comfyText);
|
||
|
||
UIManager.put("Table.selectionBackground", new javax.swing.plaf.ColorUIResource(new Color(255, 204, 0, 60)));
|
||
UIManager.put("Table.selectionForeground", Color.WHITE);
|
||
UIManager.put("Component.focusedBorderColor", comfyAccent);
|
||
UIManager.put("Separator.foreground", comfyBorder);
|
||
|
||
UIManager.put("Button.background", new javax.swing.plaf.ColorUIResource(50, 50, 50));
|
||
UIManager.put("Button.foreground", comfyText);
|
||
UIManager.put("Button.focusedBackground", new javax.swing.plaf.ColorUIResource(58, 117, 196));
|
||
UIManager.put("Button.hoverBackground", new javax.swing.plaf.ColorUIResource(70, 130, 210));
|
||
UIManager.put("Button.pressedBackground", new javax.swing.plaf.ColorUIResource(30, 30, 30));
|
||
UIManager.put("Button.borderColor", comfyBorder);
|
||
|
||
UIManager.put("ScrollBar.track", comfySurface);
|
||
UIManager.put("ScrollBar.thumb", new javax.swing.plaf.ColorUIResource(70, 70, 70));
|
||
|
||
UIManager.put("TabbedPane.selectedBackground", comfyAccent);
|
||
UIManager.put("TabbedPane.selectedForeground", Color.BLACK);
|
||
|
||
UIManager.setLookAndFeel(new FlatDarkLaf());
|
||
} else {
|
||
// EXHAUSTIVE cleanup of custom overrides
|
||
String[] keysToClear = {
|
||
"DefaultBackgroundColor", "Panel.background", "Table.background", "TextArea.background",
|
||
"TextField.background", "PasswordField.background", "Label.foreground",
|
||
"Table.foreground", "TextArea.foreground", "Table.selectionBackground",
|
||
"Table.selectionForeground", "Component.focusedBorderColor", "Separator.foreground",
|
||
"Button.background", "Button.foreground", "Button.focusedBackground",
|
||
"Button.hoverBackground", "Button.pressedBackground", "Button.borderColor",
|
||
"ScrollBar.track", "ScrollBar.thumb", "TabbedPane.selectedBackground",
|
||
"TabbedPane.selectedForeground"
|
||
};
|
||
for (String key : keysToClear) {
|
||
UIManager.put(key, null);
|
||
}
|
||
UIManager.setLookAndFeel(new FlatLightLaf());
|
||
}
|
||
|
||
// Re-apply global arcs which might be cleared by setLookAndFeel
|
||
UIManager.put("Button.arc", 8);
|
||
UIManager.put("Component.arc", 8);
|
||
UIManager.put("TextComponent.arc", 8);
|
||
UIManager.put("ProgressBar.arc", 8);
|
||
|
||
FlatLaf.updateUI();
|
||
SwingUtilities.updateComponentTreeUI(this);
|
||
} catch (Exception e) {
|
||
System.err.println("Theme setup failed: " + e.getMessage());
|
||
}
|
||
}
|
||
|
||
private void loadIcon() {
|
||
try (InputStream is = getClass().getResourceAsStream("/icons/app_icon.jpg")) {
|
||
if (is != null) {
|
||
appIcon = javax.imageio.ImageIO.read(is);
|
||
if (appIcon != null) {
|
||
setIconImage(appIcon);
|
||
}
|
||
}
|
||
} catch (IOException e) {
|
||
System.err.println("Could not load app icon: " + e.getMessage());
|
||
}
|
||
}
|
||
|
||
private void setupTrayIcon() {
|
||
if (!de.tki.comfymodels.util.PlatformUtils.isSystemTraySupported()) {
|
||
System.err.println("[System-Tray] Not supported on this platform (e.g. Wayland). Background mode disabled.");
|
||
if (backgroundCheck != null) {
|
||
backgroundCheck.setSelected(false);
|
||
backgroundCheck.setEnabled(false);
|
||
backgroundCheck.setToolTipText("System Tray not supported on this OS.");
|
||
configService.setBackgroundModeEnabled(false);
|
||
}
|
||
return;
|
||
}
|
||
|
||
SystemTray tray = SystemTray.getSystemTray();
|
||
Image trayImage = appIcon;
|
||
|
||
// Fallback if appIcon failed to load
|
||
if (trayImage == null) {
|
||
BufferedImage fallback = new BufferedImage(16, 16, BufferedImage.TYPE_INT_RGB);
|
||
Graphics2D g2 = fallback.createGraphics();
|
||
g2.setColor(new Color(255, 204, 0));
|
||
g2.fillRect(0, 0, 16, 16);
|
||
g2.dispose();
|
||
trayImage = fallback;
|
||
}
|
||
|
||
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(trayImage, "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) {
|
||
boolean vaultExists = configService.hasVault();
|
||
String title = vaultExists ? "Vault Unlock" : "Vault Setup";
|
||
String prompt = vaultExists ? "Enter Vault Password (to unlock API Keys):" : "Set Vault Password (to protect API Keys):";
|
||
|
||
JPanel panel = new JPanel(new BorderLayout(5, 5));
|
||
panel.add(new JLabel(prompt), BorderLayout.NORTH);
|
||
JPasswordField pf = new JPasswordField();
|
||
panel.add(pf, BorderLayout.CENTER);
|
||
|
||
SwingUtilities.invokeLater(() -> pf.requestFocusInWindow());
|
||
|
||
Object[] options;
|
||
if (vaultExists) {
|
||
options = new Object[]{"Unlock", "Cancel", "Reset Vault (Forgot Password?)"};
|
||
} else {
|
||
options = new Object[]{"Set Password", "Cancel"};
|
||
}
|
||
|
||
int choice = JOptionPane.showOptionDialog(null, panel, title,
|
||
JOptionPane.YES_NO_CANCEL_OPTION, JOptionPane.QUESTION_MESSAGE,
|
||
null, options, options[0]);
|
||
|
||
if (choice == 1 || choice == JOptionPane.CLOSED_OPTION) return false; // Cancel or Closed
|
||
|
||
if (vaultExists && choice == 2) { // Reset Vault
|
||
int confirm = JOptionPane.showConfirmDialog(null,
|
||
"Warning: Resetting the vault will delete all your stored API keys.\n" +
|
||
"This action cannot be undone. Do you want to proceed?",
|
||
"Confirm Vault Reset", JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE);
|
||
|
||
if (confirm == JOptionPane.YES_OPTION) {
|
||
configService.resetVault();
|
||
continue; // Restart loop for fresh setup
|
||
} else {
|
||
continue; // Back to prompt
|
||
}
|
||
}
|
||
|
||
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());
|
||
syncBridgeToken();
|
||
|
||
// NEW: Auto-import model list on first start or vault reset
|
||
if (configService.isVaultFresh()) {
|
||
String url = "https://raw.githubusercontent.com/Comfy-Org/ComfyUI-Manager/main/model-list.json";
|
||
modelListService.importFromUrl(url);
|
||
}
|
||
|
||
return true;
|
||
} catch (Exception e) {
|
||
JOptionPane.showMessageDialog(null, "Unlock Failed: " + e.getMessage(), "Error", JOptionPane.ERROR_MESSAGE);
|
||
}
|
||
}
|
||
}
|
||
|
||
private void loadSettingsIntoUI() {
|
||
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() {
|
||
loadIcon();
|
||
setTitle("ComfyUIModel-Downloader");
|
||
setSize(1450, 900);
|
||
setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
|
||
|
||
getRootPane().putClientProperty("flatlaf.useWindowDecorations", true);
|
||
|
||
JPanel mainContainer = new DotGridPanel(new BorderLayout(10, 10));
|
||
mainContainer.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
|
||
setContentPane(mainContainer);
|
||
|
||
|
||
// --- NEW COMPACT TOOLBAR ---
|
||
JPanel toolbar = new JPanel(new FlowLayout(FlowLayout.LEFT, 10, 5));
|
||
toolbar.setBorder(BorderFactory.createEtchedBorder());
|
||
|
||
JButton settingsBtn = new JButton("⚙ Settings...");
|
||
settingsBtn.addActionListener(e -> showSettingsMenu(settingsBtn));
|
||
|
||
JButton verifyBtn = new JButton("🔍 Fast Verify");
|
||
verifyBtn.addActionListener(e -> verifyLocalModels(false));
|
||
|
||
JButton optimizeBtn = new JButton("👯 Storage Optimizer");
|
||
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);
|
||
}
|
||
});
|
||
|
||
JButton archiveBtn = new JButton("📦 Archive...");
|
||
archiveBtn.addActionListener(e -> showArchiveDialog());
|
||
|
||
JButton helpBtn = new JButton("ℹ Help");
|
||
helpBtn.addActionListener(e -> showHelpDialog());
|
||
|
||
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 -> {
|
||
boolean isDark = darkCheck.isSelected();
|
||
configService.setDarkMode(isDark);
|
||
FlatAnimatedLafChange.showSnapshot();
|
||
setupTheme(isDark);
|
||
revalidate();
|
||
repaint();
|
||
FlatAnimatedLafChange.hideSnapshotWithAnimation();
|
||
});
|
||
|
||
toolbar.add(settingsBtn);
|
||
toolbar.add(new JSeparator(JSeparator.VERTICAL));
|
||
toolbar.add(verifyBtn);
|
||
toolbar.add(optimizeBtn);
|
||
toolbar.add(archiveBtn);
|
||
toolbar.add(new JSeparator(JSeparator.VERTICAL));
|
||
toolbar.add(backgroundCheck);
|
||
toolbar.add(shutdownCheck);
|
||
toolbar.add(darkCheck);
|
||
|
||
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) {
|
||
if (c == 0) {
|
||
String status = (String) getValueAt(r, 7);
|
||
return status != null && !status.contains("Already exists");
|
||
}
|
||
return false;
|
||
}
|
||
};
|
||
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 = new JLabel("Active AI: Loading...");
|
||
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(toolbar, BorderLayout.NORTH);
|
||
mainContainer.add(splitPane, BorderLayout.CENTER);
|
||
mainContainer.add(bottomPanel, BorderLayout.SOUTH);
|
||
}
|
||
|
||
private void showSettingsMenu(JButton parent) {
|
||
JPopupMenu menu = new JPopupMenu();
|
||
|
||
JMenuItem pathsItem = new JMenuItem("📁 Directories...");
|
||
pathsItem.addActionListener(e -> showPathsDialog());
|
||
|
||
JMenuItem apiItem = new JMenuItem("🔑 AI & API Keys...");
|
||
apiItem.addActionListener(e -> showApiKeysDialog());
|
||
|
||
JMenuItem bridgeItem = new JMenuItem("🚀 ComfyUI Bridge...");
|
||
bridgeItem.addActionListener(e -> showInstallationDialog());
|
||
|
||
JMenuItem helpItem = new JMenuItem("ℹ Help...");
|
||
helpItem.addActionListener(e -> showHelpDialog());
|
||
|
||
JMenuItem exitItem = new JMenuItem("❌ Exit Application");
|
||
exitItem.addActionListener(e -> {
|
||
downloadManager.stop();
|
||
System.exit(0);
|
||
});
|
||
|
||
menu.add(pathsItem);
|
||
menu.add(apiItem);
|
||
menu.addSeparator();
|
||
menu.add(bridgeItem);
|
||
menu.addSeparator();
|
||
menu.add(helpItem);
|
||
menu.add(new JSeparator());
|
||
menu.add(exitItem);
|
||
|
||
menu.show(parent, 0, parent.getHeight());
|
||
}
|
||
|
||
private void showPathsDialog() {
|
||
JDialog dialog = new JDialog(this, "Directory Settings", true);
|
||
dialog.setLayout(new BorderLayout());
|
||
dialog.setSize(600, 300);
|
||
dialog.setLocationRelativeTo(this);
|
||
|
||
JPanel panel = new JPanel(new GridBagLayout());
|
||
panel.setBorder(BorderFactory.createEmptyBorder(15, 15, 15, 15));
|
||
GridBagConstraints gbc = new GridBagConstraints();
|
||
gbc.fill = GridBagConstraints.HORIZONTAL;
|
||
gbc.gridx = 0; gbc.gridy = 0; gbc.weightx = 1.0;
|
||
gbc.insets = new Insets(0, 0, 5, 0);
|
||
|
||
panel.add(new JLabel("ComfyUI Models Path:"), gbc);
|
||
|
||
gbc.gridy++;
|
||
JPanel row1 = new JPanel(new BorderLayout(5, 0));
|
||
JTextField field1 = new JTextField(configService.getModelsPath());
|
||
JButton browse1 = new JButton("Browse...");
|
||
browse1.addActionListener(e -> {
|
||
JFileChooser chooser = new JFileChooser();
|
||
chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
|
||
if (chooser.showOpenDialog(dialog) == JFileChooser.APPROVE_OPTION) {
|
||
field1.setText(chooser.getSelectedFile().getAbsolutePath());
|
||
}
|
||
});
|
||
row1.add(field1, BorderLayout.CENTER);
|
||
row1.add(browse1, BorderLayout.EAST);
|
||
panel.add(row1, gbc);
|
||
|
||
gbc.gridy++;
|
||
gbc.insets = new Insets(10, 0, 5, 0);
|
||
panel.add(new JLabel("Archive Path:"), gbc);
|
||
|
||
gbc.gridy++;
|
||
gbc.insets = new Insets(0, 0, 5, 0);
|
||
JPanel row2 = new JPanel(new BorderLayout(5, 0));
|
||
JTextField field2 = new JTextField(configService.getArchivePath());
|
||
JButton browse2 = new JButton("Browse...");
|
||
browse2.addActionListener(e -> {
|
||
JFileChooser chooser = new JFileChooser();
|
||
chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
|
||
if (chooser.showOpenDialog(dialog) == JFileChooser.APPROVE_OPTION) {
|
||
field2.setText(chooser.getSelectedFile().getAbsolutePath());
|
||
}
|
||
});
|
||
row2.add(field2, BorderLayout.CENTER);
|
||
row2.add(browse2, BorderLayout.EAST);
|
||
panel.add(row2, gbc);
|
||
|
||
gbc.gridy++;
|
||
gbc.weighty = 1.0;
|
||
panel.add(new JPanel(), gbc);
|
||
|
||
JPanel buttons = new JPanel(new FlowLayout(FlowLayout.RIGHT));
|
||
JButton cancel = new JButton("Cancel");
|
||
cancel.addActionListener(e -> dialog.dispose());
|
||
JButton save = new JButton("Save");
|
||
save.addActionListener(e -> {
|
||
configService.setModelsPath(field1.getText().trim());
|
||
configService.setArchivePath(field2.getText().trim());
|
||
statusLabel.setText("Settings updated.");
|
||
analyzeJsonContent();
|
||
dialog.dispose();
|
||
});
|
||
buttons.add(cancel);
|
||
buttons.add(save);
|
||
|
||
dialog.add(panel, BorderLayout.CENTER);
|
||
dialog.add(buttons, BorderLayout.SOUTH);
|
||
dialog.setVisible(true);
|
||
}
|
||
|
||
private void showApiKeysDialog() {
|
||
JDialog dialog = new JDialog(this, "AI & API Keys", true);
|
||
dialog.setLayout(new BorderLayout());
|
||
dialog.setSize(600, 320);
|
||
dialog.setLocationRelativeTo(this);
|
||
|
||
JPanel panel = new JPanel(new GridBagLayout());
|
||
panel.setBorder(BorderFactory.createEmptyBorder(15, 15, 15, 15));
|
||
GridBagConstraints gbc = new GridBagConstraints();
|
||
gbc.fill = GridBagConstraints.HORIZONTAL;
|
||
gbc.gridx = 0; gbc.gridy = 0; gbc.weightx = 1.0;
|
||
|
||
panel.add(new JLabel("Gemini AI API Key:"), gbc);
|
||
gbc.gridy++;
|
||
gbc.insets = new Insets(5, 0, 15, 0);
|
||
JPasswordField geminiField = new JPasswordField(configService.getGeminiApiKey());
|
||
panel.add(geminiField, gbc);
|
||
|
||
gbc.gridy++;
|
||
gbc.insets = new Insets(0, 0, 0, 0);
|
||
panel.add(new JLabel("Hugging Face Access Token:"), gbc);
|
||
gbc.gridy++;
|
||
gbc.insets = new Insets(5, 0, 5, 0);
|
||
JPasswordField hfField = new JPasswordField(configService.getHfToken());
|
||
panel.add(hfField, gbc);
|
||
|
||
gbc.gridy++;
|
||
gbc.weighty = 1.0;
|
||
panel.add(new JPanel(), gbc);
|
||
|
||
JPanel buttons = new JPanel(new FlowLayout(FlowLayout.RIGHT));
|
||
JButton cancel = new JButton("Cancel");
|
||
cancel.addActionListener(e -> dialog.dispose());
|
||
JButton save = new JButton("Save");
|
||
save.addActionListener(e -> {
|
||
configService.setGeminiApiKey(new String(geminiField.getPassword()).trim());
|
||
configService.setHfToken(new String(hfField.getPassword()).trim());
|
||
statusLabel.setText("API keys updated.");
|
||
updateAiModelDisplay();
|
||
analyzeJsonContent(); // Re-scout with new keys/tokens if needed
|
||
dialog.dispose();
|
||
});
|
||
buttons.add(cancel);
|
||
buttons.add(save);
|
||
|
||
dialog.add(panel, BorderLayout.CENTER);
|
||
dialog.add(buttons, BorderLayout.SOUTH);
|
||
dialog.setVisible(true);
|
||
}
|
||
|
||
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);
|
||
|
||
// Process archive restores FIRST as they are part of the visual queue
|
||
new Thread(() -> {
|
||
for (int i = 0; i < rowCount; i++) {
|
||
if (selected[i]) {
|
||
String currentStatus = (String) tableModel.getValueAt(i, 7);
|
||
if ("📦 Found in Archive".equals(currentStatus)) {
|
||
final int idx = i;
|
||
ModelInfo info = modelsToDownload.get(idx);
|
||
String folder = info.getSave_path() != null ? info.getSave_path() : (info.getType() != null ? info.getType() : de.tki.comfymodels.domain.ModelFolder.CHECKPOINTS.getDefaultFolderName());
|
||
String normalizedFolder = archiveService.normalizeFolder(folder);
|
||
|
||
SwingUtilities.invokeLater(() -> tableModel.setValueAt("📦 Restoring...", idx, 7));
|
||
|
||
long totalSize = info.getByteSize();
|
||
long[] currentBytes = {0};
|
||
long[] lastUpdate = {0};
|
||
|
||
boolean success = archiveService.restoreFromArchiveWithProgress(normalizedFolder, info.getName(), (delta) -> {
|
||
currentBytes[0] += delta;
|
||
long now = System.currentTimeMillis();
|
||
if (totalSize > 0 && (now - lastUpdate[0] > 200)) {
|
||
lastUpdate[0] = now;
|
||
int percent = (int) Math.min(100, (currentBytes[0] * 100) / totalSize);
|
||
SwingUtilities.invokeLater(() -> tableModel.setValueAt("📦 Restoring (" + percent + "%)...", idx, 7));
|
||
}
|
||
});
|
||
|
||
SwingUtilities.invokeLater(() -> {
|
||
if (success) {
|
||
tableModel.setValueAt("✅ Already exists", idx, 7);
|
||
tableModel.setValueAt(false, idx, 0); // Unselect after restoration
|
||
} else {
|
||
tableModel.setValueAt("❌ Restore Failed - Downloading...", idx, 7);
|
||
// Stay selected for download fallback
|
||
}
|
||
});
|
||
}
|
||
}
|
||
}
|
||
|
||
// After restores, start the actual download manager for the remaining models
|
||
SwingUtilities.invokeLater(() -> {
|
||
// Update selected array (some might have been unselected by restore)
|
||
for (int i = 0; i < rowCount; i++) {
|
||
selected[i] = (Boolean) tableModel.getValueAt(i, 0);
|
||
}
|
||
|
||
downloadManager.startQueue(modelsToDownload, selected, configService.getModelsPath(),
|
||
(idx, status) -> SwingUtilities.invokeLater(() -> {
|
||
String current = (String) tableModel.getValueAt(idx, 7);
|
||
if (status.startsWith("Skipped") && current != null && current.contains("✅")) {
|
||
return; // Don't overwrite success with skip
|
||
}
|
||
tableModel.setValueAt(status, idx, 7);
|
||
}),
|
||
() -> SwingUtilities.invokeLater(() -> {
|
||
downloadButton.setEnabled(true);
|
||
statusLabel.setText("Queue finished.");
|
||
if (configService.isShutdownAfterDownloadEnabled()) performSystemShutdown();
|
||
})
|
||
);
|
||
});
|
||
}).start();
|
||
}
|
||
|
||
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 syncBtn = new JButton("🔄 Repair / Sync Token");
|
||
syncBtn.setToolTipText("Only updates the API token in your existing ComfyUI extension.");
|
||
syncBtn.addActionListener(e -> {
|
||
String selectedPath = pathField.getText().trim();
|
||
if (selectedPath.isEmpty()) {
|
||
JOptionPane.showMessageDialog(dialog, "Please select a path first.");
|
||
return;
|
||
}
|
||
configService.setComfyUIPath(selectedPath);
|
||
syncBridgeToken();
|
||
JOptionPane.showMessageDialog(dialog, "API Token synchronized successfully.");
|
||
});
|
||
|
||
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(syncBtn);
|
||
buttonPanel.add(new JSeparator(JSeparator.VERTICAL));
|
||
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) {
|
||
Path inputPath = Paths.get(comfyPath);
|
||
if (!Files.exists(inputPath)) {
|
||
JOptionPane.showMessageDialog(parentDialog, "The provided path does not exist: " + comfyPath, "Error", JOptionPane.ERROR_MESSAGE);
|
||
return;
|
||
}
|
||
|
||
// Systematically search for custom_nodes using PathResolver
|
||
Path customNodesDir = pathResolver.findCustomNodes(inputPath);
|
||
|
||
if (customNodesDir == null) {
|
||
String msg = "Could not find 'custom_nodes' folder in the selected directory.\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: THOROUGH CLEANUP ---
|
||
// 1. Delete legacy single-file scripts that might conflict
|
||
String[] conflictingFiles = {"comfyui_to_downloader.py", "comfyui-model-downloader.py"};
|
||
for (String conflict : conflictingFiles) {
|
||
Path conflictFile = customNodesDir.resolve(conflict);
|
||
try {
|
||
if (Files.exists(conflictFile)) {
|
||
Files.delete(conflictFile);
|
||
System.out.println("Cleaned up legacy script: " + conflictFile);
|
||
}
|
||
} catch (IOException ignored) {}
|
||
}
|
||
|
||
// 2. Search for and remove ANY folder named 'comfyui-model-downloader' to ensure a clean slate
|
||
try (java.util.stream.Stream<Path> walk = Files.walk(customNodesDir, 2)) {
|
||
List<Path> oldDirs = walk
|
||
.filter(p -> Files.isDirectory(p) && p.getFileName().toString().equalsIgnoreCase("comfyui-model-downloader"))
|
||
.collect(Collectors.toList());
|
||
|
||
for (Path oldDir : oldDirs) {
|
||
System.out.println("Removing existing bridge directory: " + oldDir);
|
||
deleteDirectory(oldDir.toFile());
|
||
}
|
||
} catch (IOException e) {
|
||
System.err.println("Error during bridge cleanup: " + e.getMessage());
|
||
}
|
||
|
||
configService.setComfyUIPath(comfyPath);
|
||
|
||
Path targetDir = customNodesDir.resolve("comfyui-model-downloader");
|
||
|
||
try {
|
||
Files.createDirectories(targetDir);
|
||
|
||
// Extract from resources
|
||
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());
|
||
|
||
// Force immediate config write with current token
|
||
writeExtensionConfig(targetDir.toFile());
|
||
|
||
if (Files.exists(targetDir.resolve("__init__.py"))) {
|
||
String msg = "🚀 ComfyUI Bridge installed successfully!\n\n" +
|
||
"Location: " + targetDir.toAbsolutePath() + "\n\n" +
|
||
"Important:\n" +
|
||
"1. If ComfyUI is running, you MUST RESTART it.\n" +
|
||
"2. Check if a 'rocket' icon appears in the ComfyUI menu.";
|
||
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());
|
||
Path configFile = new File(webDir, "config.json").toPath();
|
||
Files.writeString(configFile, config.toString(4));
|
||
System.out.println("[Bridge-Sync] Successfully wrote config to: " + configFile.toAbsolutePath());
|
||
} catch (Exception e) {
|
||
System.err.println("[Bridge-Sync] Failed to write config: " + e.getMessage());
|
||
e.printStackTrace();
|
||
}
|
||
}
|
||
|
||
private void deleteDirectory(File dir) {
|
||
File[] files = dir.listFiles();
|
||
if (files != null) for (File f : files) deleteDirectory(f);
|
||
dir.delete();
|
||
}
|
||
|
||
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 = configService.getModelsPath();
|
||
String archive = configService.getArchivePath();
|
||
|
||
for (int i = 0; i < modelsToDownload.size(); i++) {
|
||
ModelInfo info = modelsToDownload.get(i);
|
||
String type = info.getType() != null ? info.getType() : de.tki.comfymodels.domain.ModelFolder.CHECKPOINTS.getDefaultFolderName();
|
||
String folder = info.getSave_path() != null ? info.getSave_path() : type;
|
||
|
||
String normalizedFolder = archiveService.normalizeFolder(folder);
|
||
|
||
Path local = "root".equals(normalizedFolder) ? Paths.get(base, info.getName()) : Paths.get(base, normalizedFolder, info.getName());
|
||
Path archivedPath = "root".equals(normalizedFolder) ? Paths.get(archive, info.getName()) : Paths.get(archive, normalizedFolder, info.getName());
|
||
|
||
boolean exists = Files.exists(local) && Files.isRegularFile(local);
|
||
boolean inArchive = Files.exists(archivedPath) && Files.isRegularFile(archivedPath);
|
||
boolean sizeMismatch = false;
|
||
|
||
// Initial size check for primary location
|
||
if (exists && info.getByteSize() > 0) {
|
||
try {
|
||
if (Files.size(local) != info.getByteSize()) {
|
||
sizeMismatch = true;
|
||
exists = false;
|
||
}
|
||
} catch (IOException e) { exists = false; }
|
||
}
|
||
|
||
// Fallback: Recursive search in ARCHIVE if not found at primary location
|
||
if (!inArchive && archive != null && !archive.isEmpty()) {
|
||
java.util.Optional<Path> foundInArchive = localScanner.findModelWithPrefSize(Paths.get(archive), info.getName(), info.getByteSize());
|
||
if (foundInArchive.isPresent()) {
|
||
archivedPath = foundInArchive.get();
|
||
inArchive = true;
|
||
}
|
||
}
|
||
|
||
// Fallback: Recursive search in LOCAL MODELS if not found OR size mismatch at primary location
|
||
if ((!exists || sizeMismatch) && base != null && !base.isEmpty()) {
|
||
java.util.Optional<Path> foundLocally = localScanner.findModelWithPrefSize(Paths.get(base), info.getName(), info.getByteSize());
|
||
if (foundLocally.isPresent()) {
|
||
Path potentialLocal = foundLocally.get();
|
||
try {
|
||
long potSize = Files.size(potentialLocal);
|
||
// Only accept if size matches OR we don't know the remote size yet
|
||
if (info.getByteSize() <= 0 || potSize == info.getByteSize()) {
|
||
local = potentialLocal;
|
||
exists = true;
|
||
sizeMismatch = false; // Found a correct one!
|
||
|
||
// Update normalizedFolder to the actual location found locally
|
||
Path root = Paths.get(base);
|
||
Path rel = root.relativize(local);
|
||
if (rel.getParent() != null) {
|
||
normalizedFolder = rel.getParent().toString().replace("\\", "/");
|
||
} else {
|
||
normalizedFolder = "root";
|
||
}
|
||
info.setSave_path(normalizedFolder);
|
||
}
|
||
} catch (Exception ignored) {}
|
||
}
|
||
}
|
||
|
||
// Safety check: if it exists at 'local' but 'local' is inside the archive folder, it's NOT locally existing in the right place
|
||
if (exists && archive != null && !archive.isEmpty() &&
|
||
local.toAbsolutePath().toString().toLowerCase().startsWith(new File(archive).getAbsolutePath().toLowerCase())) {
|
||
exists = false;
|
||
inArchive = true;
|
||
}
|
||
|
||
// Rigorous 'Already exists' check: must match byteSize if known (redundant but safe)
|
||
if (exists && info.getByteSize() > 0) {
|
||
try {
|
||
long localSize = Files.size(local);
|
||
if (localSize != info.getByteSize()) {
|
||
sizeMismatch = true;
|
||
exists = false;
|
||
}
|
||
} catch (IOException e) {
|
||
exists = false;
|
||
}
|
||
}
|
||
|
||
// Populate size from local or archive if it exists and currently unknown
|
||
if ((exists || inArchive || sizeMismatch) && ("Unknown".equals(info.getSize()) || info.getByteSize() <= 0)) {
|
||
try {
|
||
long bytes = Files.size(exists ? local : (sizeMismatch ? local : archivedPath));
|
||
if (bytes > 0) {
|
||
if (info.getByteSize() <= 0) info.setByteSize(bytes);
|
||
if ("Unknown".equals(info.getSize())) info.setSize(searchService.formatSize(bytes));
|
||
}
|
||
} catch (Exception e) {}
|
||
}
|
||
|
||
String status;
|
||
if (exists && inArchive) {
|
||
status = "✅ Already exists (Archived copy found)";
|
||
} else if (exists) {
|
||
status = "✅ Already exists";
|
||
} else if (inArchive) {
|
||
status = "📦 Found in Archive";
|
||
} else if (sizeMismatch) {
|
||
status = "🔄 Size Mismatch";
|
||
} else if (info.getUrl().equals("MISSING")) {
|
||
status = "Idle";
|
||
} else {
|
||
status = "✅ Known Good";
|
||
}
|
||
|
||
// Select for download/restore if NOT exists locally
|
||
boolean isSelected = !exists;
|
||
tableModel.addRow(new Object[]{isSelected, info.getType(), info.getName(), info.getSize(), info.getPopularity(), "models/" + normalizedFolder, 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);
|
||
|
||
String status = (idx < tableModel.getRowCount()) ? (String) tableModel.getValueAt(idx, 7) : "";
|
||
boolean needsCheck = "Unknown".equals(info.getSize()) || "🔄 Size Mismatch".equals(status);
|
||
|
||
if (!info.getUrl().equals("MISSING") && needsCheck) {
|
||
long size = searchService.getRemoteSize(info.getUrl());
|
||
if (size > 0) {
|
||
info.setByteSize(size);
|
||
String formatted = searchService.formatSize(size);
|
||
info.setSize(formatted);
|
||
SwingUtilities.invokeLater(() -> {
|
||
if (idx < tableModel.getRowCount()) {
|
||
tableModel.setValueAt(formatted, idx, 3);
|
||
|
||
// Re-verify status with new remote size info
|
||
String currentStatus = (String) tableModel.getValueAt(idx, 7);
|
||
if (currentStatus.contains("Already exists") || "🔄 Size Mismatch".equals(currentStatus) || "📦 Found in Archive".equals(currentStatus)) {
|
||
String type = info.getType() != null ? info.getType() : de.tki.comfymodels.domain.ModelFolder.CHECKPOINTS.getDefaultFolderName();
|
||
String folder = info.getSave_path() != null ? info.getSave_path() : type;
|
||
String base = configService.getModelsPath();
|
||
String archive = configService.getArchivePath();
|
||
|
||
// 1. Re-check local
|
||
Path local = Paths.get(base, archiveService.normalizeFolder(folder), info.getName());
|
||
if (!Files.exists(local)) {
|
||
local = Paths.get(base, type, info.getName());
|
||
}
|
||
|
||
// If still not found at "standard" locations, try recursive local
|
||
if (!Files.exists(local)) {
|
||
java.util.Optional<Path> recursiveLocal = localScanner.findModelWithPrefSize(Paths.get(base), info.getName(), size);
|
||
if (recursiveLocal.isPresent()) local = recursiveLocal.get();
|
||
}
|
||
|
||
boolean localExists = Files.exists(local);
|
||
boolean localSizeMatch = false;
|
||
if (localExists) {
|
||
try {
|
||
localSizeMatch = (Files.size(local) == size);
|
||
} catch (IOException ignored) {}
|
||
}
|
||
|
||
// 2. Re-check archive
|
||
boolean inArchive = false;
|
||
if (archive != null && !archive.isEmpty()) {
|
||
Path archived = Paths.get(archive, archiveService.normalizeFolder(folder), info.getName());
|
||
try {
|
||
if (Files.exists(archived) && Files.size(archived) == size) {
|
||
inArchive = true;
|
||
} else {
|
||
inArchive = localScanner.findModelWithPrefSize(Paths.get(archive), info.getName(), size).isPresent();
|
||
}
|
||
} catch (IOException ignored) {}
|
||
}
|
||
|
||
String newStatus;
|
||
boolean shouldSelect = false;
|
||
|
||
if (localSizeMatch) {
|
||
newStatus = inArchive ? "✅ Already exists (Archived copy found)" : "✅ Already exists";
|
||
shouldSelect = false;
|
||
} else if (localExists) {
|
||
newStatus = "🔄 Size Mismatch";
|
||
shouldSelect = true;
|
||
} else if (inArchive) {
|
||
newStatus = "📦 Found in Archive";
|
||
shouldSelect = true;
|
||
} else {
|
||
newStatus = "✅ Known Good";
|
||
shouldSelect = true;
|
||
}
|
||
|
||
final String finalStatus = newStatus;
|
||
final boolean finalSelect = shouldSelect;
|
||
SwingUtilities.invokeLater(() -> {
|
||
tableModel.setValueAt(finalStatus, idx, 7);
|
||
tableModel.setValueAt(finalSelect, idx, 0);
|
||
});
|
||
}
|
||
}
|
||
});
|
||
}
|
||
}
|
||
}
|
||
}).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(() -> {
|
||
String current = (String) tableModel.getValueAt(idx, 7);
|
||
// Protection: Don't overwrite successful detection (local or archive) with search progress/failure
|
||
if (current != null && (current.contains("✅") || current.contains("📦"))) {
|
||
if (status.contains("No trusted match") || status.startsWith("🔍") || status.startsWith("✨")) {
|
||
return;
|
||
}
|
||
}
|
||
tableModel.setValueAt(status, idx, 7);
|
||
}),
|
||
(idx, info) -> SwingUtilities.invokeLater(() -> {
|
||
tableModel.setValueAt(info.getSize(), idx, 3);
|
||
tableModel.setValueAt(info.getUrl(), idx, 6);
|
||
|
||
String base = configService.getModelsPath();
|
||
String archive = configService.getArchivePath();
|
||
long size = info.getByteSize();
|
||
|
||
// Re-verify local existence with potential new size info
|
||
boolean exists = false;
|
||
boolean inArchive = false;
|
||
|
||
if (base != null && !base.isEmpty()) {
|
||
java.util.Optional<Path> foundLocally = localScanner.findModelWithPrefSize(Paths.get(base), info.getName(), size);
|
||
if (foundLocally.isPresent()) {
|
||
exists = true;
|
||
// Update target path display
|
||
try {
|
||
Path rel = Paths.get(base).relativize(foundLocally.get());
|
||
tableModel.setValueAt("models/" + rel.getParent().toString().replace("\\", "/"), idx, 5);
|
||
} catch (Exception ignored) {}
|
||
}
|
||
}
|
||
|
||
if (!exists && archive != null && !archive.isEmpty()) {
|
||
inArchive = localScanner.findModelWithPrefSize(Paths.get(archive), info.getName(), size).isPresent();
|
||
}
|
||
|
||
String newStatus;
|
||
if (exists) {
|
||
newStatus = "✅ Already exists";
|
||
} else if (inArchive) {
|
||
newStatus = "📦 Found in Archive";
|
||
} else {
|
||
newStatus = "✅ Known Good";
|
||
}
|
||
|
||
tableModel.setValueAt(newStatus, idx, 7);
|
||
tableModel.setValueAt(!exists, idx, 0); // Keep selected for download/restore if NOT local
|
||
}),
|
||
() -> SwingUtilities.invokeLater(() -> statusLabel.setText("Search finished."))
|
||
);
|
||
}
|
||
|
||
private void verifyLocalModels(boolean checkDuplicates) {
|
||
String baseDir = configService.getModelsPath();
|
||
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<IModelValidator.ValidationResult> errors = new ArrayList<>();
|
||
Map<String, List<Path>> hashToPaths = new HashMap<>();
|
||
|
||
List<Path> allFiles = Files.walk(root.toPath())
|
||
.filter(Files::isRegularFile)
|
||
.filter(p -> {
|
||
String relPath = root.toPath().relativize(p).toString().toLowerCase();
|
||
return !relPath.contains(".venv") && !relPath.contains("archive");
|
||
})
|
||
.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<String, List<Path>> 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<String, List<Path>> 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<String, List<Path>> 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 showArchiveDialog() {
|
||
JDialog dialog = new JDialog(this, "Archive Manager", true);
|
||
dialog.setLayout(new BorderLayout());
|
||
dialog.setSize(1000, 700);
|
||
dialog.setLocationRelativeTo(this);
|
||
|
||
JTabbedPane tabs = new JTabbedPane();
|
||
|
||
// --- TAB 1: ARCHIVE (Models -> Archive) ---
|
||
JPanel archivePanel = new JPanel(new BorderLayout(10, 10));
|
||
archivePanel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
|
||
|
||
Map<String, List<ModelInfo>> groupedToArchive = archiveService.getModelsGroupedByFolder();
|
||
String[] columns = {"Select", "Folder", "Name", "Size"};
|
||
DefaultTableModel archiveTableModel = new DefaultTableModel(columns, 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; }
|
||
};
|
||
|
||
for (Map.Entry<String, List<ModelInfo>> entry : groupedToArchive.entrySet()) {
|
||
for (ModelInfo model : entry.getValue()) {
|
||
archiveTableModel.addRow(new Object[]{false, entry.getKey(), model.getName(), model.getSize()});
|
||
}
|
||
}
|
||
|
||
Map<String, List<ModelInfo>> groupedToRestore = archiveService.getArchivedModelsGroupedByFolder();
|
||
DefaultTableModel restoreTableModel = new DefaultTableModel(columns, 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; }
|
||
};
|
||
|
||
for (Map.Entry<String, List<ModelInfo>> entry : groupedToRestore.entrySet()) {
|
||
for (ModelInfo model : entry.getValue()) {
|
||
restoreTableModel.addRow(new Object[]{false, entry.getKey(), model.getName(), model.getSize()});
|
||
}
|
||
}
|
||
|
||
JTable archiveTable = new JTable(archiveTableModel);
|
||
archiveTable.setRowHeight(25);
|
||
archiveTable.getColumnModel().getColumn(0).setMaxWidth(50);
|
||
JScrollPane archiveScroll = new JScrollPane(archiveTable);
|
||
archivePanel.add(archiveScroll, BorderLayout.CENTER);
|
||
|
||
JPanel archiveBottom = new JPanel(new BorderLayout());
|
||
JLabel archiveCountLabel = new JLabel("Selected: 0 models");
|
||
archiveTableModel.addTableModelListener(e -> {
|
||
int selected = 0;
|
||
for (int i = 0; i < archiveTableModel.getRowCount(); i++) {
|
||
if ((Boolean) archiveTableModel.getValueAt(i, 0)) selected++;
|
||
}
|
||
archiveCountLabel.setText("Selected: " + selected + " models");
|
||
});
|
||
|
||
JButton archiveNowBtn = new JButton("📦 Move to Archive");
|
||
archiveNowBtn.addActionListener(e -> {
|
||
List<Integer> selectedRows = new ArrayList<>();
|
||
for (int i = 0; i < archiveTableModel.getRowCount(); i++) if ((Boolean) archiveTableModel.getValueAt(i, 0)) selectedRows.add(i);
|
||
if (selectedRows.isEmpty()) return;
|
||
|
||
showModalProgressDialog("Archive", "Archiving", selectedRows, archiveTableModel, restoreTableModel, (folder, name, update) -> {
|
||
try {
|
||
archiveService.moveToArchiveWithProgress(folder, name, update);
|
||
return true;
|
||
} catch (IOException ex) {
|
||
SwingUtilities.invokeLater(() -> {
|
||
JOptionPane.showMessageDialog(dialog,
|
||
"Failed to archive '" + name + "':\n" + ex.getMessage(),
|
||
"Archive Error", JOptionPane.ERROR_MESSAGE);
|
||
});
|
||
return false;
|
||
}
|
||
});
|
||
});
|
||
|
||
JPanel archiveActionPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT));
|
||
archiveActionPanel.add(archiveNowBtn);
|
||
archiveBottom.add(archiveCountLabel, BorderLayout.WEST);
|
||
archiveBottom.add(archiveActionPanel, BorderLayout.EAST);
|
||
archivePanel.add(archiveBottom, BorderLayout.SOUTH);
|
||
|
||
// --- TAB 2: RESTORE (Archive -> Models) ---
|
||
JPanel restorePanel = new JPanel(new BorderLayout(10, 10));
|
||
restorePanel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
|
||
|
||
JTable restoreTable = new JTable(restoreTableModel);
|
||
restoreTable.setRowHeight(25);
|
||
restoreTable.getColumnModel().getColumn(0).setMaxWidth(50);
|
||
JScrollPane restoreScroll = new JScrollPane(restoreTable);
|
||
restorePanel.add(restoreScroll, BorderLayout.CENTER);
|
||
|
||
JPanel restoreBottom = new JPanel(new BorderLayout());
|
||
JLabel restoreCountLabel = new JLabel("Selected: 0 models");
|
||
restoreTableModel.addTableModelListener(e -> {
|
||
int selected = 0;
|
||
for (int i = 0; i < restoreTableModel.getRowCount(); i++) {
|
||
if ((Boolean) restoreTableModel.getValueAt(i, 0)) selected++;
|
||
}
|
||
restoreCountLabel.setText("Selected: " + selected + " models");
|
||
});
|
||
|
||
JButton restoreNowBtn = new JButton("🚀 Restore from Archive");
|
||
restoreNowBtn.addActionListener(e -> {
|
||
List<Integer> selectedRows = new ArrayList<>();
|
||
for (int i = 0; i < restoreTableModel.getRowCount(); i++) if ((Boolean) restoreTableModel.getValueAt(i, 0)) selectedRows.add(i);
|
||
if (selectedRows.isEmpty()) return;
|
||
|
||
showModalProgressDialog("Restore", "Restoring", selectedRows, restoreTableModel, archiveTableModel, (folder, name, update) -> {
|
||
return archiveService.restoreFromArchiveWithProgress(folder, name, update);
|
||
});
|
||
});
|
||
|
||
JPanel restoreActionPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT));
|
||
restoreActionPanel.add(restoreNowBtn);
|
||
restoreBottom.add(restoreCountLabel, BorderLayout.WEST);
|
||
restoreBottom.add(restoreActionPanel, BorderLayout.EAST);
|
||
restorePanel.add(restoreBottom, BorderLayout.SOUTH);
|
||
|
||
tabs.addTab("📦 Archive Models", archivePanel);
|
||
tabs.addTab("🚀 Restore Models", restorePanel);
|
||
|
||
dialog.add(tabs, BorderLayout.CENTER);
|
||
|
||
JPanel dialogButtons = new JPanel(new FlowLayout(FlowLayout.RIGHT));
|
||
JButton closeBtn = new JButton("Close");
|
||
closeBtn.addActionListener(e -> dialog.dispose());
|
||
dialogButtons.add(closeBtn);
|
||
dialog.add(dialogButtons, BorderLayout.SOUTH);
|
||
|
||
dialog.setVisible(true);
|
||
}
|
||
|
||
private static class ArchiveWorkItem {
|
||
int index;
|
||
String folder, name, size;
|
||
ArchiveWorkItem(int i, String f, String n, String s) { index = i; folder = f; name = n; size = s; }
|
||
}
|
||
|
||
private void showModalProgressDialog(String title, String statusPrefix, List<Integer> selectedRows,
|
||
DefaultTableModel sourceModel, DefaultTableModel targetModel, TaskExecutor executor) {
|
||
JDialog progressDialog = new JDialog(this, title, true);
|
||
progressDialog.setLayout(new BorderLayout(10, 10));
|
||
progressDialog.setSize(500, 180);
|
||
progressDialog.setLocationRelativeTo(this);
|
||
progressDialog.setDefaultCloseOperation(JDialog.DO_NOTHING_ON_CLOSE);
|
||
|
||
JPanel panel = new JPanel(new BorderLayout(10, 10));
|
||
panel.setBorder(BorderFactory.createEmptyBorder(20, 20, 20, 20));
|
||
|
||
JLabel label = new JLabel(statusPrefix + "...");
|
||
label.setFont(new Font("SansSerif", Font.BOLD, 12));
|
||
JProgressBar progressBar = new JProgressBar(0, 100);
|
||
progressBar.setStringPainted(true);
|
||
progressBar.setPreferredSize(new Dimension(400, 30));
|
||
|
||
panel.add(label, BorderLayout.NORTH);
|
||
panel.add(progressBar, BorderLayout.CENTER);
|
||
progressDialog.add(panel, BorderLayout.CENTER);
|
||
|
||
// Pre-collect data on EDT
|
||
List<ArchiveWorkItem> workItems = new ArrayList<>();
|
||
long totalBytes = 0;
|
||
for (int row : selectedRows) {
|
||
String sizeStr = (String) sourceModel.getValueAt(row, 3);
|
||
workItems.add(new ArchiveWorkItem(row, (String) sourceModel.getValueAt(row, 1), (String) sourceModel.getValueAt(row, 2), sizeStr));
|
||
totalBytes += parseSizeToBytes(sizeStr);
|
||
}
|
||
final long finalTotalBytes = totalBytes > 0 ? totalBytes : 1;
|
||
|
||
new Thread(() -> {
|
||
int successCount = 0;
|
||
List<Object[]> rowsToMove = new ArrayList<>();
|
||
List<Integer> processedRowIndices = new ArrayList<>();
|
||
long[] bytesProcessedTotal = {0};
|
||
|
||
for (int i = 0; i < workItems.size(); i++) {
|
||
ArchiveWorkItem item = workItems.get(i);
|
||
final int fileIndex = i + 1;
|
||
SwingUtilities.invokeLater(() -> label.setText(statusPrefix + " (" + fileIndex + "/" + workItems.size() + "): " + item.name));
|
||
|
||
if (executor.execute(item.folder, item.name, (bytesDelta) -> {
|
||
bytesProcessedTotal[0] += bytesDelta;
|
||
final long current = bytesProcessedTotal[0];
|
||
final int percent = (int) Math.min(100, (current * 100) / finalTotalBytes);
|
||
SwingUtilities.invokeLater(() -> {
|
||
progressBar.setValue(percent);
|
||
progressBar.setString(percent + "% (" + searchService.formatSize(current) + " / " + searchService.formatSize(finalTotalBytes) + ")");
|
||
});
|
||
})) {
|
||
successCount++;
|
||
processedRowIndices.add(item.index);
|
||
rowsToMove.add(new Object[]{false, item.folder, item.name, item.size});
|
||
}
|
||
}
|
||
|
||
final int finalSuccess = successCount;
|
||
SwingUtilities.invokeLater(() -> {
|
||
progressDialog.dispose();
|
||
processedRowIndices.sort((a, b) -> b.compareTo(a));
|
||
for (int r : processedRowIndices) sourceModel.removeRow(r);
|
||
for (Object[] rowData : rowsToMove) targetModel.addRow(rowData);
|
||
|
||
JOptionPane.showMessageDialog(this, finalSuccess + " models successfully " + title.toLowerCase() + "ed.",
|
||
"Operation Complete", JOptionPane.INFORMATION_MESSAGE);
|
||
analyzeJsonContent();
|
||
});
|
||
}).start();
|
||
|
||
progressDialog.setVisible(true);
|
||
}
|
||
|
||
private long parseSizeToBytes(String sizeStr) {
|
||
if (sizeStr == null || sizeStr.equals("Unknown") || sizeStr.isEmpty()) return 0;
|
||
try {
|
||
String[] parts = sizeStr.trim().split("\\s+");
|
||
double val = Double.parseDouble(parts[0].replace(",", "."));
|
||
if (parts.length < 2) return (long) val;
|
||
String unit = parts[1].toUpperCase();
|
||
if (unit.contains("GB")) return (long) (val * 1024 * 1024 * 1024);
|
||
if (unit.contains("MB")) return (long) (val * 1024 * 1024);
|
||
if (unit.contains("KB")) return (long) (val * 1024);
|
||
return (long) val;
|
||
} catch (Exception e) { return 0; }
|
||
}
|
||
|
||
@FunctionalInterface
|
||
interface TaskExecutor {
|
||
boolean execute(String folder, String name, java.util.function.LongConsumer progressUpdate);
|
||
}
|
||
|
||
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<File> files = (java.util.List<File>) dtde.getTransferable().getTransferData(DataFlavor.javaFileListFlavor);
|
||
if (!files.isEmpty()) loadFile(files.get(0));
|
||
} catch (Exception e) {}
|
||
}
|
||
});
|
||
}
|
||
|
||
private void performSystemShutdown() {
|
||
de.tki.comfymodels.util.PlatformUtils.shutdownSystem();
|
||
}
|
||
|
||
private void showHelpDialog() {
|
||
JDialog helpDialog = new JDialog(this, "User Guide & Documentation", true);
|
||
helpDialog.setLayout(new BorderLayout());
|
||
helpDialog.setSize(950, 750);
|
||
helpDialog.setLocationRelativeTo(this);
|
||
|
||
JEditorPane editorPane = new JEditorPane();
|
||
editorPane.setEditable(false);
|
||
editorPane.setContentType("text/html");
|
||
|
||
boolean darkMode = configService.isDarkMode();
|
||
String bgColor = darkMode ? "#161616" : "#f8f9fa";
|
||
String textColor = darkMode ? "#e6e6e6" : "#212529";
|
||
String accentColor = darkMode ? "#ffff00" : "#0056b3";
|
||
String boxBg = darkMode ? "#202020" : "#ffffff";
|
||
String boxBorder = darkMode ? "#333" : "#dee2e6";
|
||
String tipBg = darkMode ? "#333" : "#e9ecef";
|
||
|
||
StringBuilder sb = new StringBuilder();
|
||
sb.append("<html><body style='padding: 25px; font-family: sans-serif; background-color: ").append(bgColor).append("; color: ").append(textColor).append(";'>");
|
||
sb.append("<h1 style='color: ").append(accentColor).append("; text-align: center; margin-bottom: 25px;'>🚀 ComfyUI Model Downloader</h1>");
|
||
|
||
sb.append("<div style='background-color: ").append(boxBg).append("; padding: 15px; border-radius: 8px; margin-bottom: 20px; border: 1px solid ").append(boxBorder).append(";'>");
|
||
sb.append("<h2 style='color: ").append(accentColor).append("; margin-top: 0;'>🛠️ Getting Started (Setup)</h2>");
|
||
sb.append("<ol>");
|
||
sb.append("<li><b>Set Directories:</b> Go to <i>Settings -> Directories</i> and set your ComfyUI 'models' path. " +
|
||
"Additionally, choose an 'Archive' path (ideally on a large HDD) to offload models you aren't currently using.</li>");
|
||
sb.append("<li><b>Unlock Vault:</b> On the first start, you set a master password. This securely encrypts your API keys on your disk.</li>");
|
||
sb.append("<li><b>Store API Keys:</b> Go to <i>Settings -> AI & API Keys</i>. A <b>Gemini API Key</b> is highly recommended, " +
|
||
"as the AI reads workflows like a human and intelligently finds missing download links.</li>");
|
||
sb.append("<li><b>Install Bridge:</b> Use <i>Settings -> ComfyUI Bridge</i> to add the 🚀 button directly into your ComfyUI interface.</li>");
|
||
sb.append("</ol></div>");
|
||
|
||
sb.append("<h2 style='color: ").append(accentColor).append(";'>✨ Core Features</h2>");
|
||
|
||
sb.append("<p><b>📦 Archive Manager:</b> Models you don't need right now can be moved to the archive via <i>Archive...</i>. " +
|
||
"If a workflow needs them later, the Downloader detects them in the archive and offers a <b>one-click restoration</b>.</p>");
|
||
|
||
sb.append("<p><b>👯 Storage Optimizer:</b> Scans your library for duplicates. " +
|
||
"Using SHA-256 hash comparison, it identifies identical files even if they have different names.</p>");
|
||
|
||
sb.append("<p><b>🔍 Fast Verify:</b> Quickly checks if your model files are valid or have corrupted headers.</p>");
|
||
|
||
sb.append("<p><b>🧠 AI Search:</b> When loading a workflow, the AI automatically tries to identify models. " +
|
||
"Use <i>Deep Search</i> to have the AI specifically search for download sources.</p>");
|
||
|
||
sb.append("<div style='background-color: ").append(tipBg).append("; padding: 10px; border-radius: 5px; margin-top: 20px;'>");
|
||
sb.append("<b>💡 Pro Tips:</b><br>");
|
||
sb.append("• Use <b>Drag & Drop</b> for images (.png) to directly read their embedded workflow.<br>");
|
||
sb.append("• Enable <b>Shutdown after Queue</b> for overnight downloads.<br>");
|
||
sb.append("• The app minimizes to the <b>System Tray</b> (bottom right) if 'Stay in Background' is active.");
|
||
sb.append("</div>");
|
||
|
||
sb.append("<p style='margin-top: 30px; color: #888; font-style: italic; text-align: center;'>Version 1.0.5 - developed by TKI</p>");
|
||
sb.append("</body></html>");
|
||
|
||
editorPane.setText(sb.toString());
|
||
editorPane.setCaretPosition(0);
|
||
|
||
JScrollPane scrollPane = new JScrollPane(editorPane);
|
||
scrollPane.setBorder(null);
|
||
helpDialog.add(scrollPane, BorderLayout.CENTER);
|
||
|
||
JButton closeBtn = new JButton("Close");
|
||
closeBtn.addActionListener(e -> helpDialog.dispose());
|
||
JPanel btnPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT));
|
||
btnPanel.add(closeBtn);
|
||
helpDialog.add(btnPanel, BorderLayout.SOUTH);
|
||
|
||
helpDialog.setVisible(true);
|
||
}
|
||
|
||
private void syncBridgeToken() {
|
||
String comfyPath = configService.getComfyUIPath();
|
||
if (comfyPath == null || comfyPath.isEmpty()) return;
|
||
|
||
Path comfyDir = Paths.get(comfyPath);
|
||
if (!Files.exists(comfyDir)) return;
|
||
|
||
Path customNodesDir = pathResolver.findCustomNodes(comfyDir);
|
||
if (customNodesDir == null) return;
|
||
|
||
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);
|
||
}
|
||
}
|
||
|
||
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 {}
|
||
}
|