Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 10 additions & 3 deletions locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
},

"downloads": {
"title": "Model Downloads",
"title": "Downloads",
"empty": "No downloads in progress.",
"pause": "Pause",
"resume": "Resume",
Expand All @@ -62,7 +62,8 @@
"completed": "Complete",
"cancelled": "Cancelled",
"error": "Failed",
"sizeProgress": "{received} / {total}"
"sizeProgress": "{received} / {total}",
"clearCompleted": "Clear Completed"
},

"media": {
Expand All @@ -80,7 +81,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": {
Expand Down
18 changes: 17 additions & 1 deletion src/main/lib/ipc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -1573,6 +1573,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 [
Expand Down
51 changes: 51 additions & 0 deletions src/main/lib/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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<string>()
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<ModelFileInfo[]> {
const seen = new Map<string, ModelFileInfo>()
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))
}
2 changes: 2 additions & 0 deletions src/preload/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,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),
Expand Down
20 changes: 15 additions & 5 deletions src/renderer/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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<TabView>('dashboard')
const appVersion = ref('')

Expand Down Expand Up @@ -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' },
])

Expand Down Expand Up @@ -272,12 +274,16 @@ onMounted(async () => {
<nav class="sidebar">
<div class="sidebar-brand">Desktop 2.0</div>
<div class="sidebar-nav">
<button
<div
v-for="item in sidebarItems"
:key="item.key"
class="sidebar-item"
:class="{ active: activeView === item.key }"
role="button"
tabindex="0"
@click="switchView(item.key)"
@keydown.enter="switchView(item.key)"
@keydown.space.prevent="switchView(item.key)"
>
<component :is="item.icon" :size="18" />
<span>{{ $t(item.labelKey) }}</span>
Expand All @@ -291,13 +297,13 @@ onMounted(async () => {
class="sidebar-count"
>{{ sessionStore.runningTabCount }}</span>
</template>
<template v-if="item.key === 'models'">
<template v-if="item.key === 'downloads'">
<span
v-if="downloadStore.activeDownloads.length > 0"
class="sidebar-count"
>{{ downloadStore.activeDownloads.length }}</span>
</template>
</button>
</div>
</div>
<button class="sidebar-item sidebar-feedback" @click="openFeedback">
<MessageSquarePlus :size="18" />
Expand Down Expand Up @@ -349,6 +355,10 @@ onMounted(async () => {
ref="mediaRef"
/>

<DownloadsView
v-show="activeView === 'downloads'"
/>

<SettingsView
v-show="activeView === 'settings'"
ref="settingsRef"
Expand Down
3 changes: 2 additions & 1 deletion src/renderer/src/assets/main.css
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@
* ================================================================ */

* { margin: 0; padding: 0; box-sizing: border-box; }
:root { --sidebar-width: 200px; }

::-webkit-scrollbar { width: 8px; }
::-webkit-scrollbar-track { background: transparent; }
Expand All @@ -181,7 +182,7 @@ body {
.app-layout { display: flex; height: 100vh; }

.sidebar {
width: 200px;
width: var(--sidebar-width);
flex-shrink: 0;
background: var(--surface);
border-right: 1px solid var(--border);
Expand Down
Loading
Loading