From 712b50553fd79a0355f8a5a64c996e949e6975d8 Mon Sep 17 00:00:00 2001 From: Jedrzej Kosinski Date: Thu, 26 Mar 2026 03:30:36 -0700 Subject: [PATCH] feat: add model browser and downloads drawer - Add Model Browser tab to ModelsView with folder sidebar, searchable file list (name, size, date), and show-in-folder action - Add global Downloads Drawer accessible from sidebar, replacing the inline DownloadsPanel in ModelsView - Backend: add getModelFolders/getModelFiles IPC handlers that scan shared model directories - Add ModelFileInfo type, preload bindings, and i18n keys Co-authored-by: Amp Amp-Thread-ID: https://ampcode.com/threads/T-019d29a1-effe-7670-be7c-424140001724 --- locales/en.json | 13 +- src/main/lib/ipc.ts | 18 +- src/main/lib/models.ts | 51 ++++ src/preload/index.ts | 2 + src/renderer/src/App.vue | 20 +- src/renderer/src/assets/main.css | 3 +- src/renderer/src/components/ModelBrowser.vue | 281 +++++++++++++++++++ src/renderer/src/types/ipc.ts | 1 + src/renderer/src/views/DownloadsView.vue | 245 ++++++++++++++++ src/renderer/src/views/ModelsView.vue | 85 ++++-- src/types/ipc.ts | 9 + 11 files changed, 691 insertions(+), 37 deletions(-) create mode 100644 src/renderer/src/components/ModelBrowser.vue create mode 100644 src/renderer/src/views/DownloadsView.vue diff --git a/locales/en.json b/locales/en.json index 420b1c88..a0c5fa1f 100644 --- a/locales/en.json +++ b/locales/en.json @@ -45,7 +45,7 @@ }, "downloads": { - "title": "Model Downloads", + "title": "Downloads", "empty": "No downloads in progress.", "pause": "Pause", "resume": "Resume", @@ -58,7 +58,8 @@ "completed": "Complete", "cancelled": "Cancelled", "error": "Failed", - "sizeProgress": "{received} / {total}" + "sizeProgress": "{received} / {total}", + "clearCompleted": "Clear Completed" }, "media": { @@ -76,7 +77,13 @@ "removeDir": "Remove", "primary": "primary", "default": "default", - "makePrimary": "Make Primary" + "makePrimary": "Make Primary", + "browse": "Browse Models", + "directoriesTab": "Directories", + "folderTypes": "Folder Types", + "searchPlaceholder": "Search model files…", + "noFiles": "No model files found", + "openFolder": "Show in Folder" }, "common": { diff --git a/src/main/lib/ipc.ts b/src/main/lib/ipc.ts index f79fcdee..8e604b11 100644 --- a/src/main/lib/ipc.ts +++ b/src/main/lib/ipc.ts @@ -36,7 +36,7 @@ import { formatTime } from './util' import { getActiveDownloads } from './comfyDownloadManager' import * as releaseCache from './release-cache' import * as i18n from './i18n' -import { syncCustomModelFolders, discoverExtraModelFolders } from './models' +import { syncCustomModelFolders, discoverExtraModelFolders, listModelFolders, listModelFiles } from './models' import { copyDirWithProgress } from './copy' import { fetchJSON } from './fetch' import { fetchLatestRelease } from './comfyui-releases' @@ -1522,6 +1522,22 @@ export function register(callbacks: RegisterCallbacks = {}): void { } }) + ipcMain.handle('get-model-folders', () => { + const s = settings.getAll() + const modelsDirs = s.modelsDirs as string[] | undefined + if (!modelsDirs || modelsDirs.length === 0) return [] + return listModelFolders(modelsDirs) + }) + + ipcMain.handle('get-model-files', async (_event, folder: string) => { + const s = settings.getAll() + const modelsDirs = s.modelsDirs as string[] | undefined + if (!modelsDirs || modelsDirs.length === 0) return [] + const validFolders = listModelFolders(modelsDirs) + if (!validFolders.includes(folder)) return [] + return listModelFiles(modelsDirs, folder) + }) + ipcMain.handle('get-media-sections', () => { const s = settings.getAll() return [ diff --git a/src/main/lib/models.ts b/src/main/lib/models.ts index 381a7a25..15801722 100644 --- a/src/main/lib/models.ts +++ b/src/main/lib/models.ts @@ -2,6 +2,7 @@ import path from 'path' import fs from 'fs' import { dataDir } from './paths' import { writeFileSafe } from './safe-file' +import type { ModelFileInfo } from '../../types/ipc' // Canonical ComfyUI model folder types from folder_paths.py. // When ComfyUI adds `all_model_folders` support in extra_model_paths.yaml, @@ -216,3 +217,53 @@ export function syncCustomModelFolders( return { newFolders, config } } + +/** + * Returns the deduplicated, sorted list of model folder names present across + * all shared model directories. Includes both canonical and extra folders. + */ +export function listModelFolders(modelsDirs: string[]): string[] { + const seen = new Set() + for (const dir of modelsDirs) { + for (const name of allFoldersIn(dir)) { + if (name.startsWith('.')) continue + seen.add(name) + } + } + return [...seen].sort() +} + +/** + * Lists model files in a given folder name across all shared model directories. + * Returns deduplicated entries sorted by name. Uses async IO to avoid blocking + * the main thread on large model libraries. + */ +export async function listModelFiles(modelsDirs: string[], folder: string): Promise { + const seen = new Map() + for (const dir of modelsDirs) { + const folderPath = path.join(dir, folder) + let entries: fs.Dirent[] + try { + entries = await fs.promises.readdir(folderPath, { withFileTypes: true }) + } catch { + continue + } + for (const entry of entries) { + if (!entry.isFile()) continue + if (seen.has(entry.name)) continue + const entryPath = path.join(folderPath, entry.name) + try { + const stat = await fs.promises.stat(entryPath) + seen.set(entry.name, { + name: entry.name, + fullPath: entryPath, + sizeBytes: stat.size, + modifiedAt: stat.mtimeMs, + }) + } catch { + // skip files we can't stat + } + } + } + return [...seen.values()].sort((a, b) => a.name.localeCompare(b.name)) +} diff --git a/src/preload/index.ts b/src/preload/index.ts index 491e557b..c0bfa034 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -85,6 +85,8 @@ const api: ElectronApi = { // Settings getSettingsSections: () => ipcRenderer.invoke('get-settings-sections'), getModelsSections: () => ipcRenderer.invoke('get-models-sections'), + getModelFolders: () => ipcRenderer.invoke('get-model-folders'), + getModelFiles: (folder) => ipcRenderer.invoke('get-model-files', folder), getUniqueName: (baseName: string) => ipcRenderer.invoke('get-unique-name', baseName), getMediaSections: () => ipcRenderer.invoke('get-media-sections'), setSetting: (key, value) => ipcRenderer.invoke('set-setting', key, value), diff --git a/src/renderer/src/App.vue b/src/renderer/src/App.vue index b53df5e1..5a4a77f6 100644 --- a/src/renderer/src/App.vue +++ b/src/renderer/src/App.vue @@ -28,9 +28,10 @@ import NewInstallModal from './views/NewInstallModal.vue' import QuickInstallModal from './views/QuickInstallModal.vue' import TrackModal from './views/TrackModal.vue' import LoadSnapshotModal from './views/LoadSnapshotModal.vue' +import DownloadsView from './views/DownloadsView.vue' // Lucide icons -import { LayoutDashboard, Box, Play, FolderOpen, Image, Settings, MessageSquarePlus } from 'lucide-vue-next' +import { LayoutDashboard, Box, Play, FolderOpen, Image, Settings, MessageSquarePlus, Download } from 'lucide-vue-next' import { buildSupportUrl } from './lib/supportUrl' const { t, setLocaleMessage, locale } = useI18n() @@ -43,7 +44,7 @@ const launcherPrefs = useLauncherPrefs() useTheme() // --- View state --- -type TabView = 'dashboard' | 'list' | 'running' | 'models' | 'media' | 'settings' +type TabView = 'dashboard' | 'list' | 'running' | 'models' | 'media' | 'downloads' | 'settings' const activeView = ref('dashboard') const appVersion = ref('') @@ -76,6 +77,7 @@ const sidebarItems = computed(() => [ { key: 'running' as const, icon: Play, labelKey: 'sidebar.running' }, { key: 'models' as const, icon: FolderOpen, labelKey: 'models.title' }, { key: 'media' as const, icon: Image, labelKey: 'media.title' }, + { key: 'downloads' as const, icon: Download, labelKey: 'downloads.title' }, { key: 'settings' as const, icon: Settings, labelKey: 'settings.title' }, ]) @@ -272,12 +274,16 @@ onMounted(async () => {