feat: comprehensive model management enhancements and platform fixes

- Block 1: Refactored path logic into a centralized PathResolver for OS-agnostic and relative path handling.
- Block 2: Improved platform compatibility (shutdown, macOS menu, System Tray fallback).
- Block 3: Added Power-User features (Model Updater, Smart Cleanup, Thumbnails, Node Sync, Remote Status).
- Fixes: Recursive subfolder scanning, archive detection logic, and UI progress throttling.
This commit is contained in:
Thomas Kippster
2026-05-02 19:53:49 +02:00
parent acad50c989
commit ff9f98fe3f
54 changed files with 4828 additions and 531 deletions
@@ -24,11 +24,16 @@ public class DefaultDownloadManager implements IDownloadManager {
private volatile boolean isStopped = false;
private volatile boolean[] currentSelection;
private final java.util.Set<Integer> completedIndices = java.util.concurrent.ConcurrentHashMap.newKeySet();
private final java.util.Map<Integer, String> statusMap = new java.util.concurrent.ConcurrentHashMap<>();
@Autowired
private ConfigService configService;
@Autowired
private PathResolver pathResolver;
private void safeUpdateStatus(int index, String status, BiConsumer<Integer, String> statusUpdater) {
statusMap.put(index, status);
// Only mark as completed if it's a final state
boolean isFinal = status.contains("✅ Finished") ||
status.contains("Stopped") ||
@@ -42,12 +47,18 @@ public class DefaultDownloadManager implements IDownloadManager {
statusUpdater.accept(index, status);
}
@Override
public java.util.Map<Integer, String> getQueueStatus() {
return new java.util.HashMap<>(statusMap);
}
@Override
public void startQueue(List<ModelInfo> models, boolean[] selectedIndices, String baseDir, BiConsumer<Integer, String> statusUpdater, Runnable onFinished) {
isStopped = false;
isPaused = false;
this.currentSelection = selectedIndices;
completedIndices.clear();
statusMap.clear();
java.util.concurrent.atomic.AtomicBoolean finishedCalled = new java.util.concurrent.atomic.AtomicBoolean(false);
@@ -65,7 +76,6 @@ public class DefaultDownloadManager implements IDownloadManager {
}
if (!isSelected(index)) {
safeUpdateStatus(index, "Skipped (Not Selected)", statusUpdater);
futures[index] = java.util.concurrent.CompletableFuture.completedFuture(null);
continue;
}
@@ -77,11 +87,13 @@ public class DefaultDownloadManager implements IDownloadManager {
try {
String subPath = info.getSave_path();
if (subPath == null || subPath.isEmpty() || "default".equalsIgnoreCase(subPath)) {
subPath = info.getType() != null ? info.getType() : "checkpoints";
subPath = de.tki.comfymodels.domain.ModelFolder.fromString(info.getType()).getDefaultFolderName();
}
String finalBaseDir = baseDir != null ? baseDir : ".";
Path targetDir = Paths.get(finalBaseDir, subPath);
Path modelsBase = (baseDir != null && !baseDir.isEmpty()) ?
pathResolver.resolve(baseDir) :
pathResolver.resolve(configService.getModelsPath());
Path targetDir = modelsBase.resolve(subPath);
Files.createDirectories(targetDir);
downloadWithResume(info, targetDir.resolve(info.getName()), index, statusUpdater);
} catch (Exception e) {
@@ -124,6 +136,10 @@ public class DefaultDownloadManager implements IDownloadManager {
if (waitForPauseAndCheckSelection(index, statusUpdater)) return;
File file = targetFile.toFile();
String archivePath = configService.getArchivePath();
boolean isActuallyInArchive = archivePath != null && !archivePath.isEmpty() &&
file.getAbsolutePath().toLowerCase().startsWith(new File(archivePath).getAbsolutePath().toLowerCase());
String hfToken = configService.getHfToken();
// Check disk space before starting
@@ -134,11 +150,12 @@ public class DefaultDownloadManager implements IDownloadManager {
}
} catch (Exception ignored) {}
if (file.exists() && file.length() < 10240) {
file.delete();
if (file.exists() && (file.length() < 10240 || isActuallyInArchive)) {
// If it's in the archive, we don't treat it as "local existing model" for the download manager.
if (!isActuallyInArchive) file.delete();
}
long existingFileSize = file.exists() ? file.length() : 0;
long existingFileSize = (file.exists() && !isActuallyInArchive) ? file.length() : 0;
if (waitForPauseAndCheckSelection(index, statusUpdater)) return;