diff --git a/config/model_paths_linux.yaml b/config/model_paths_linux.yaml deleted file mode 100644 index 8b6d7e26..00000000 --- a/config/model_paths_linux.yaml +++ /dev/null @@ -1,23 +0,0 @@ -# ComfyUI extra_model_paths.yaml for Linux -comfyui: - base_path: ~/.config/ComfyUI - is_default: true - checkpoints: models/checkpoints/ - classifiers: models/classifiers/ - clip: models/clip/ - clip_vision: models/clip_vision/ - configs: models/configs/ - controlnet: models/controlnet/ - diffusers: models/diffusers/ - diffusion_models: models/diffusion_models/ - embeddings: models/embeddings/ - gligen: models/gligen/ - hypernetworks: models/hypernetworks/ - loras: models/loras/ - photomaker: models/photomaker/ - style_models: models/style_models/ - unet: models/unet/ - upscale_models: models/upscale_models/ - vae: models/vae/ - vae_approx: models/vae_approx/ - custom_nodes: custom_nodes/ diff --git a/config/model_paths_mac.yaml b/config/model_paths_mac.yaml deleted file mode 100644 index 1c17a8e8..00000000 --- a/config/model_paths_mac.yaml +++ /dev/null @@ -1,23 +0,0 @@ -# ComfyUI extra_model_paths.yaml for macOS -comfyui: - base_path: ~/Library/Application Support/ComfyUI - is_default: true - checkpoints: models/checkpoints/ - classifiers: models/classifiers/ - clip: models/clip/ - clip_vision: models/clip_vision/ - configs: models/configs/ - controlnet: models/controlnet/ - diffusers: models/diffusers/ - diffusion_models: models/diffusion_models/ - embeddings: models/embeddings/ - gligen: models/gligen/ - hypernetworks: models/hypernetworks/ - loras: models/loras/ - photomaker: models/photomaker/ - style_models: models/style_models/ - unet: models/unet/ - upscale_models: models/upscale_models/ - vae: models/vae/ - vae_approx: models/vae_approx/ - custom_nodes: custom_nodes/ diff --git a/config/model_paths_windows.yaml b/config/model_paths_windows.yaml deleted file mode 100644 index cf951773..00000000 --- a/config/model_paths_windows.yaml +++ /dev/null @@ -1,23 +0,0 @@ -# ComfyUI extra_model_paths.yaml for Windows -comfyui: - base_path: '%USERPROFILE%/comfyui-electron' - is_default: true - checkpoints: models/checkpoints/ - classifiers: models/classifiers/ - clip: models/clip/ - clip_vision: models/clip_vision/ - configs: models/configs/ - controlnet: models/controlnet/ - diffusers: models/diffusers/ - diffusion_models: models/diffusion_models/ - embeddings: models/embeddings/ - gligen: models/gligen/ - hypernetworks: models/hypernetworks/ - loras: models/loras/ - photomaker: models/photomaker/ - style_models: models/style_models/ - unet: models/unet/ - upscale_models: models/upscale_models/ - vae: models/vae/ - vae_approx: models/vae_approx/ - custom_nodes: custom_nodes/ diff --git a/forge.config.ts b/forge.config.ts index 1d480012..e0c35bf0 100644 --- a/forge.config.ts +++ b/forge.config.ts @@ -45,32 +45,12 @@ const config: ForgeConfig = { rebuildConfig: {}, hooks: { prePackage: async () => { - const configDir = path.join(__dirname, 'config'); const assetDir = path.join(__dirname, 'assets', 'ComfyUI'); // Ensure the asset directory exists if (!fs.existsSync(assetDir)) { fs.mkdirSync(assetDir, { recursive: true }); } - - let sourceFile; - if (process.platform === 'darwin') { - sourceFile = path.join(configDir, 'model_paths_mac.yaml'); - } else if (process.platform === 'win32') { - sourceFile = path.join(configDir, 'model_paths_windows.yaml'); - } else { - sourceFile = path.join(configDir, 'model_paths_linux.yaml'); - } - - const destFile = path.join(assetDir, 'extra_model_paths.yaml'); - - try { - fs.copyFileSync(sourceFile, destFile); - console.log(`Copied ${sourceFile} to ${destFile}`); - } catch (err) { - console.error(`Failed to copy config file: ${err}`); - throw err; // This will stop the packaging process if the copy fails - } }, postPackage: async (forgeConfig, packageResult) => { console.log('Post-package hook started'); diff --git a/index.html b/index.html index 577ed180..7e8cd1f3 100644 --- a/index.html +++ b/index.html @@ -6,6 +6,6 @@
- + diff --git a/package.json b/package.json index ed1fe6cc..86c39768 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "productName": "ComfyUI", "repository": "github:comfy-org/electron", "copyright": "Copyright © 2024 Comfy Org", - "version": "0.1.25", + "version": "0.1.26", "description": "The best modular GUI to run AI diffusion models.", "main": ".vite/build/main.js", "packageManager": "yarn@4.5.0", @@ -87,6 +87,7 @@ "react-dom": "^18.3.1", "systeminformation": "^5.23.5", "tar": "^7.4.3", - "update-electron-app": "^3.0.0" + "update-electron-app": "^3.0.0", + "yaml": "^2.6.0" } } diff --git a/src/config/extra_model_config.ts b/src/config/extra_model_config.ts new file mode 100644 index 00000000..237f7ec0 --- /dev/null +++ b/src/config/extra_model_config.ts @@ -0,0 +1,87 @@ +import * as fsPromises from 'node:fs/promises'; +import path from 'path'; +import log from 'electron-log/main'; +import { stringify } from 'yaml'; + +interface ModelPaths { + comfyui: { + base_path: string; + is_default: boolean; + [key: string]: string | boolean; + }; +} + +const commonPaths = { + is_default: true, + checkpoints: 'models/checkpoints/', + classifiers: 'models/classifiers/', + clip: 'models/clip/', + clip_vision: 'models/clip_vision/', + configs: 'models/configs/', + controlnet: 'models/controlnet/', + diffusers: 'models/diffusers/', + diffusion_models: 'models/diffusion_models/', + embeddings: 'models/embeddings/', + gligen: 'models/gligen/', + hypernetworks: 'models/hypernetworks/', + loras: 'models/loras/', + photomaker: 'models/photomaker/', + style_models: 'models/style_models/', + unet: 'models/unet/', + upscale_models: 'models/upscale_models/', + vae: 'models/vae/', + vae_approx: 'models/vae_approx/', + custom_nodes: 'custom_nodes/', +}; + +const configTemplates: Record = { + win32: { + comfyui: { + base_path: '%USERPROFILE%/comfyui-electron', + ...commonPaths, + }, + }, + darwin: { + comfyui: { + base_path: '~/Library/Application Support/ComfyUI', + ...commonPaths, + }, + }, + linux: { + comfyui: { + base_path: '~/.config/ComfyUI', + ...commonPaths, + }, + }, +}; + +export async function createModelConfigFiles(extraModelConfigPath: string, customBasePath?: string): Promise { + log.info(`Creating model config files in ${extraModelConfigPath} with base path ${customBasePath}`); + try { + for (const [platform, config] of Object.entries(configTemplates)) { + if (platform !== process.platform) { + continue; + } + + log.info(`Creating model config files for ${platform}`); + + // If a custom base path is provided, use it + if (customBasePath) { + config.comfyui.base_path = customBasePath; + } + + const yamlContent = stringify(config, { lineWidth: -1 }); + + // Add a comment at the top of the file + const fileContent = `# ComfyUI extra_model_paths.yaml for ${platform}\n${yamlContent}`; + await fsPromises.writeFile(extraModelConfigPath, fileContent, 'utf8'); + log.info(`Created extra_model_paths.yaml at ${extraModelConfigPath}`); + return true; + } + log.info(`No model config files created for platform ${process.platform}`); + return false; + } catch (error) { + log.error('Error creating model config files:', error); + return false; + } +} diff --git a/src/constants.ts b/src/constants.ts index c0109b45..c335c7ce 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -3,7 +3,13 @@ export const IPC_CHANNELS = { RENDERER_READY: 'renderer-ready', RESTART_APP: 'restart-app', LOG_MESSAGE: 'log-message', -}; + SHOW_SELECT_DIRECTORY: 'show-select-directory', + SELECTED_DIRECTORY: 'selected-directory', + OPEN_DIALOG: 'open-dialog', + FIRST_TIME_SETUP_COMPLETE: 'first-time-setup-complete', +} as const; + +export type IPCChannel = (typeof IPC_CHANNELS)[keyof typeof IPC_CHANNELS]; export const ELECTRON_BRIDGE_API = 'electronAPI'; diff --git a/src/main.ts b/src/main.ts index 43c4e531..5622b942 100644 --- a/src/main.ts +++ b/src/main.ts @@ -4,9 +4,9 @@ import fs from 'fs'; import axios from 'axios'; import path from 'node:path'; import { SetupTray } from './tray'; -import { IPC_CHANNELS, SENTRY_URL_ENDPOINT } from './constants'; +import { IPC_CHANNELS, IPCChannel, SENTRY_URL_ENDPOINT } from './constants'; import dotenv from 'dotenv'; -import { app, BrowserWindow, screen, ipcMain, Menu, MenuItem } from 'electron'; +import { app, BrowserWindow, dialog, screen, ipcMain, Menu, MenuItem } from 'electron'; import tar from 'tar'; import log from 'electron-log/main'; import * as Sentry from '@sentry/electron/main'; @@ -14,6 +14,13 @@ import Store from 'electron-store'; import { updateElectronApp, UpdateSourceType } from 'update-electron-app'; import * as net from 'net'; import { graphics } from 'systeminformation'; +import { createModelConfigFiles } from './config/extra_model_config'; + +let pythonProcess: ChildProcess | null = null; +const host = '127.0.0.1'; +let port = 8188; +let mainWindow: BrowserWindow | null; +const messageQueue: Array = []; // Stores mesaages before renderer is ready. import { StoreType } from './store'; log.initialize(); @@ -33,8 +40,6 @@ updateElectronApp({ }); const gotTheLock = app.requestSingleInstanceLock(); -const windowsLocalAppData = path.join(app.getPath('home'), 'comfyui-electron'); -log.info('Windows Local App Data directory: ', windowsLocalAppData); if (!gotTheLock) { app.quit(); @@ -103,29 +108,17 @@ if (!gotTheLock) { app.on('ready', async () => { log.info('App ready'); - app.on('activate', () => { + app.on('activate', async () => { // On OS X it's common to re-create a window in the app when the // dock icon is clicked and there are no other windows open. if (BrowserWindow.getAllWindows().length === 0) { - const { userResourcesPath } = getResourcesPaths(); + const { userResourcesPath } = await determineResourcesPaths(); createWindow(userResourcesPath); } }); - const { userResourcesPath, appResourcesPath } = getResourcesPaths(); - log.info(`userResourcesPath: ${userResourcesPath}`); - log.info(`appResourcesPath: ${appResourcesPath}`); - try { - dotenv.config({ path: path.join(appResourcesPath, 'ComfyUI', '.env') }); - } catch { - // if no .env file, skip it - } - - createDirIfNotExists(userResourcesPath); - - try { - await createWindow(userResourcesPath); + await createWindow(); mainWindow.on('close', () => { mainWindow = null; app.quit(); @@ -139,21 +132,26 @@ if (!gotTheLock) { mainWindow.webContents.send(message.channel, message.data); } }); + ipcMain.handle(IPC_CHANNELS.OPEN_DIALOG, (event, options: Electron.OpenDialogOptions) => { + log.info('Open dialog'); + return dialog.showOpenDialogSync({ + ...options, + defaultPath: getDefaultUserResourcesPath(), + }); + }); + await handleFirstTimeSetup(); + const { userResourcesPath, appResourcesPath, pythonInstallPath, modelConfigPath } = + await determineResourcesPaths(); + SetupTray(mainWindow, userResourcesPath); port = await findAvailablePort(8000, 9999).catch((err) => { log.error(`ERROR: Failed to find available port: ${err}`); throw err; }); - sendProgressUpdate('Setting up comfy environment...'); - createComfyDirectories(userResourcesPath); - const pythonRootPath = path.join(userResourcesPath, 'python'); - const pythonInterpreterPath = - process.platform === 'win32' - ? path.join(pythonRootPath, 'python.exe') - : path.join(pythonRootPath, 'bin', 'python'); + sendProgressUpdate('Setting up Python Environment...'); - await setupPythonEnvironment(pythonInterpreterPath, appResourcesPath, userResourcesPath); + const pythonInterpreterPath = await setupPythonEnvironment(appResourcesPath, pythonInstallPath); sendProgressUpdate('Starting Comfy Server...'); - await launchPythonServer(pythonInterpreterPath, appResourcesPath, userResourcesPath); + await launchPythonServer(pythonInterpreterPath, appResourcesPath, userResourcesPath, modelConfigPath); } catch (error) { log.error(error); sendProgressUpdate( @@ -222,6 +220,8 @@ async function loadRendererIntoMainWindow(): Promise { if (MAIN_WINDOW_VITE_DEV_SERVER_URL) { log.info('Loading Vite Dev Server'); await mainWindow.loadURL(MAIN_WINDOW_VITE_DEV_SERVER_URL); + log.info('Opened Vite Dev Server'); + mainWindow.webContents.openDevTools(); } else { mainWindow.loadFile(path.join(__dirname, `../renderer/${MAIN_WINDOW_VITE_NAME}/index.html`)); } @@ -233,12 +233,6 @@ function restartApp() { app.quit(); } -let pythonProcess: ChildProcess | null = null; -const host = '127.0.0.1'; -let port = 8188; -let mainWindow: BrowserWindow | null; -const messageQueue: Array = []; // Stores mesaages before renderer is ready. - function buildMenu(): Menu { const isMac = process.platform === 'darwin'; @@ -280,7 +274,7 @@ function buildMenu(): Menu { * @param userResourcesPath The path to the user's resources. * @returns The main window. */ -export const createWindow = async (userResourcesPath: string): Promise => { +export const createWindow = async (userResourcesPath?: string): Promise => { const primaryDisplay = screen.getPrimaryDisplay(); const { width, height } = primaryDisplay.workAreaSize; @@ -307,12 +301,15 @@ export const createWindow = async (userResourcesPath: string): Promise { const { width, height, x, y } = mainWindow.getBounds(); @@ -374,7 +371,8 @@ let spawnServerTimeout: NodeJS.Timeout = null; const launchPythonServer = async ( pythonInterpreterPath: string, appResourcesPath: string, - userResourcesPath: string + userResourcesPath: string, + modelConfigPath: string ) => { const isServerRunning = await isComfyServerReady(host, port); if (isServerRunning) { @@ -401,6 +399,8 @@ const launchPythonServer = async ( ...(process.env.COMFYUI_CPU_ONLY === 'true' ? ['--cpu'] : []), '--front-end-version', 'Comfy-Org/ComfyUI_frontend@latest', + '--extra-model-paths-config', + modelConfigPath, '--port', port.toString(), ]; @@ -440,54 +440,38 @@ const launchPythonServer = async ( }); }; -function getResourcesPaths() { - const { userResourcesPath, appResourcesPath } = app.isPackaged - ? { - // production: install python to per-user application data dir - userResourcesPath: process.platform === 'win32' ? windowsLocalAppData : app.getPath('userData'), - appResourcesPath: process.resourcesPath, - } - : { - // development: install python to in-tree assets dir - userResourcesPath: path.join(app.getAppPath(), 'assets'), - appResourcesPath: path.join(app.getAppPath(), 'assets'), - }; - - return { userResourcesPath, appResourcesPath }; -} - /** Interval to send progress updates to the renderer. */ let progressInterval: NodeJS.Timeout | null = null; function sendProgressUpdate(status: string): void { if (mainWindow) { log.info('Sending progress update to renderer ' + status); + sendRendererMessage(IPC_CHANNELS.LOADING_PROGRESS, { + status, + }); + } +} - const sendUpdate = (status: string) => { - const newMessage = { - channel: IPC_CHANNELS.LOADING_PROGRESS, - data: { - status, - }, - }; - if (!mainWindow.webContents || mainWindow.webContents.isLoading()) { - log.info('Queueing message since renderer is not ready yet.'); - messageQueue.push(newMessage); - return; - } +const sendRendererMessage = (channel: IPCChannel, data: any) => { + const newMessage = { + channel: channel, + data: data, + }; + if (!mainWindow.webContents || mainWindow.webContents.isLoading()) { + log.info('Queueing message since renderer is not ready yet.'); + messageQueue.push(newMessage); + return; + } - if (messageQueue.length > 0) { - while (messageQueue.length > 0) { - const message = messageQueue.shift(); - log.info('Sending queued message ', message.channel, message.data); - mainWindow.webContents.send(message.channel, message.data); - } - } - mainWindow.webContents.send(newMessage.channel, newMessage.data); - }; - sendUpdate(status); + if (messageQueue.length > 0) { + while (messageQueue.length > 0) { + const message = messageQueue.shift(); + log.info('Sending queued message ', message.channel, message.data); + mainWindow.webContents.send(message.channel, message.data); + } } -} + mainWindow.webContents.send(newMessage.channel, newMessage.data); +}; const killPythonServer = async (): Promise => { if (pythonProcess) { @@ -613,18 +597,19 @@ const spawnPythonAsync = ( }); }; -async function setupPythonEnvironment( - pythonInterpreterPath: string, - appResourcesPath: string, - userResourcesPath: string -) { - const pythonRootPath = path.join(userResourcesPath, 'python'); - const pythonRecordPath = path.join(pythonRootPath, 'INSTALLER'); +async function setupPythonEnvironment(appResourcesPath: string, pythonResourcesPath: string) { + const pythonRootPath = path.join(pythonResourcesPath, 'python'); + const pythonInterpreterPath = + process.platform === 'win32' ? path.join(pythonRootPath, 'python.exe') : path.join(pythonRootPath, 'bin', 'python'); + const pythonRecordPath = path.join(pythonInterpreterPath, 'INSTALLER'); try { // check for existence of both interpreter and INSTALLER record to ensure a correctly installed python env + log.info( + `Checking for existence of python interpreter at ${pythonInterpreterPath} and INSTALLER record at ${pythonRecordPath}` + ); await Promise.all([fsPromises.access(pythonInterpreterPath), fsPromises.access(pythonRecordPath)]); } catch { - log.info('Running one-time python installation on first startup...'); + log.info(`Running one-time python installation on first startup at ${pythonResourcesPath} and ${pythonRootPath}`); try { // clean up any possible existing non-functional python env @@ -634,9 +619,10 @@ async function setupPythonEnvironment( } const pythonTarPath = path.join(appResourcesPath, 'python.tgz'); + log.info(`Extracting python bundle from ${pythonTarPath} to ${pythonResourcesPath}`); await tar.extract({ file: pythonTarPath, - cwd: userResourcesPath, + cwd: pythonResourcesPath, strict: true, }); @@ -708,12 +694,14 @@ async function setupPythonEnvironment( throw new Error('Python rehydration failed'); } } + return pythonInterpreterPath; } type DirectoryStructure = (string | [string, string[]])[]; // Create directories needed by ComfyUI in the user's data directory. function createComfyDirectories(localComfyDirectory: string): void { + log.info(`Creating ComfyUI directories in ${localComfyDirectory}`); const directories: DirectoryStructure = [ 'custom_nodes', 'input', @@ -822,3 +810,85 @@ function findAvailablePort(startPort: number, endPort: number): Promise tryPort(startPort); }); } +/** + * Check if the user has completed the first time setup wizard. + * This means the extra_models_config.yaml file exists in the user's data directory. + */ +function isFirstTimeSetup(): boolean { + const userDataPath = app.getPath('userData'); + const extraModelsConfigPath = path.join(userDataPath, 'extra_models_config.yaml'); + return !fs.existsSync(extraModelsConfigPath); +} + +async function selectedInstallDirectory(): Promise { + return new Promise((resolve, reject) => { + ipcMain.on(IPC_CHANNELS.SELECTED_DIRECTORY, (_event, value) => { + log.info('Directory selected:', value); + resolve(value); + }); + }); +} + +async function handleFirstTimeSetup() { + const firstTimeSetup = isFirstTimeSetup(); + log.info('First time setup:', firstTimeSetup); + if (firstTimeSetup) { + sendRendererMessage(IPC_CHANNELS.SHOW_SELECT_DIRECTORY, null); + const selectedDirectory = await selectedInstallDirectory(); + createComfyDirectories(selectedDirectory); + + const { modelConfigPath } = await determineResourcesPaths(); + createModelConfigFiles(modelConfigPath, selectedDirectory); + } else { + sendRendererMessage(IPC_CHANNELS.FIRST_TIME_SETUP_COMPLETE, null); + } +} + +async function determineResourcesPaths(): Promise<{ + userResourcesPath: string; + pythonInstallPath: string; + appResourcesPath: string; + modelConfigPath: string; +}> { + const modelConfigPath = path.join(app.getPath('userData'), 'extra_models_config.yaml'); + if (!app.isPackaged) { + return { + // development: install python to in-tree assets dir + userResourcesPath: path.join(app.getAppPath(), 'assets'), + pythonInstallPath: path.join(app.getAppPath(), 'assets'), + appResourcesPath: path.join(app.getAppPath(), 'assets'), + modelConfigPath: modelConfigPath, + }; + } + + const defaultUserResourcesPath = getDefaultUserResourcesPath(); + const defaultPythonInstallPath = + process.platform === 'win32' + ? path.join(path.dirname(path.dirname(app.getPath('userData'))), 'Local', 'comfyui_electron') + : app.getPath('userData'); + + const appResourcePath = process.resourcesPath; + + // TODO(robinhuang): Look for extra models yaml file and use that as the userResourcesPath if it exists. + return { + userResourcesPath: defaultUserResourcesPath, + pythonInstallPath: defaultPythonInstallPath, + appResourcesPath: appResourcePath, + modelConfigPath: modelConfigPath, + }; +} + +function getDefaultUserResourcesPath(): string { + return process.platform === 'win32' ? path.join(app.getPath('home'), 'comfyui-electron') : app.getPath('userData'); +} + +function dev_getDefaultModelConfigPath(): string { + switch (process.platform) { + case 'win32': + return path.join(app.getAppPath(), 'config', 'model_paths_windows.yaml'); + case 'darwin': + return path.join(app.getAppPath(), 'config', 'model_paths_mac.yaml'); + default: + return path.join(app.getAppPath(), 'config', 'model_paths_linux.yaml'); + } +} diff --git a/src/preload.ts b/src/preload.ts index 2083fcc5..d97045fb 100644 --- a/src/preload.ts +++ b/src/preload.ts @@ -5,11 +5,24 @@ import { IPC_CHANNELS, ELECTRON_BRIDGE_API } from './constants'; import log from 'electron-log/main'; export interface ElectronAPI { + /** + * Callback for progress updates from the main process for starting ComfyUI. + * @param callback + * @returns + */ onProgressUpdate: (callback: (update: { status: string }) => void) => void; + /** + * Callback for when the user clicks the "Select Directory" button in the setup wizard. + * @param callback + */ + selectSetupDirectory: (directory: string) => void; + onShowSelectDirectory: (callback: () => void) => void; onLogMessage: (callback: (message: string) => void) => void; + onFirstTimeSetupComplete: (callback: () => void) => void; sendReady: () => void; restartApp: () => void; isPackaged: boolean; + openDialog: (options: Electron.OpenDialogOptions) => Promise; } const electronAPI: ElectronAPI = { @@ -34,6 +47,18 @@ const electronAPI: ElectronAPI = { log.info('Sending restarting app message to main process'); ipcRenderer.send(IPC_CHANNELS.RESTART_APP); }, + onShowSelectDirectory: (callback: () => void) => { + ipcRenderer.on(IPC_CHANNELS.SHOW_SELECT_DIRECTORY, () => callback()); + }, + selectSetupDirectory: (directory: string) => { + ipcRenderer.send(IPC_CHANNELS.SELECTED_DIRECTORY, directory); + }, + openDialog: (options: Electron.OpenDialogOptions) => { + return ipcRenderer.invoke(IPC_CHANNELS.OPEN_DIALOG, options); + }, + onFirstTimeSetupComplete: (callback: () => void) => { + ipcRenderer.on(IPC_CHANNELS.FIRST_TIME_SETUP_COMPLETE, () => callback()); + }, }; contextBridge.exposeInMainWorld(ELECTRON_BRIDGE_API, electronAPI); diff --git a/src/renderer.ts b/src/renderer.tsx similarity index 96% rename from src/renderer.ts rename to src/renderer.tsx index 928acd57..1d328d3a 100644 --- a/src/renderer.ts +++ b/src/renderer.tsx @@ -28,6 +28,7 @@ import './index.css'; import ReactDOM from 'react-dom/client'; +import * as React from 'react'; import Home from './renderer/index'; import * as Sentry from '@sentry/electron/renderer'; import { ELECTRON_BRIDGE_API, SENTRY_URL_ENDPOINT } from './constants'; @@ -42,4 +43,4 @@ if (ELECTRON_BRIDGE_API in window) { } // Generate the the app then render the root -ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(Home()); +ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(); diff --git a/src/renderer/index.tsx b/src/renderer/index.tsx index 158c4674..bba22847 100644 --- a/src/renderer/index.tsx +++ b/src/renderer/index.tsx @@ -1,6 +1,9 @@ -import React from 'react'; +import React, { useEffect, useState, useCallback } from 'react'; import ProgressOverlay from './screens/ProgressOverlay'; import log from 'electron-log/renderer'; +import FirstTimeSetup from './screens/FirstTimeSetup'; +import { ElectronAPI } from 'src/preload'; +import { ELECTRON_BRIDGE_API } from 'src/constants'; const bodyStyle: React.CSSProperties = { fontFamily: 'Arial, sans-serif', @@ -16,12 +19,39 @@ const bodyStyle: React.CSSProperties = { // Main entry point for the front end renderer. // Currently this serves as the overlay to show progress as the comfy backend is coming online. // after coming online the main.ts will replace the renderer with comfy's internal index.html -function Home(): React.ReactElement { +const Home: React.FC = () => { + const [showSetup, setShowSetup] = useState(null); + + useEffect(() => { + const electronAPI: ElectronAPI = (window as any)[ELECTRON_BRIDGE_API]; + + log.info(`Sending ready event from renderer`); + electronAPI.sendReady(); + + electronAPI.onShowSelectDirectory(() => { + log.info('Showing select directory'); + setShowSetup(true); + }); + + electronAPI.onFirstTimeSetupComplete(() => { + log.info('First time setup complete'); + setShowSetup(false); + }); + }, []); + + if (showSetup === null) { + return <> Loading ....; + } + + if (showSetup) { + return setShowSetup(false)} />; + } + return (
); -} +}; export default Home; diff --git a/src/renderer/screens/FirstTimeSetup.tsx b/src/renderer/screens/FirstTimeSetup.tsx new file mode 100644 index 00000000..97661695 --- /dev/null +++ b/src/renderer/screens/FirstTimeSetup.tsx @@ -0,0 +1,107 @@ +import React, { useState } from 'react'; +import { ElectronAPI } from 'src/preload'; +import log from 'electron-log/renderer'; + +interface FirstTimeSetupProps { + onComplete: (selectedDirectory: string) => void; +} + +const FirstTimeSetup: React.FC = ({ onComplete }) => { + const [selectedPath, setSelectedPath] = useState(''); + const electronAPI: ElectronAPI = (window as any).electronAPI; + + const handleDirectorySelect = async () => { + const options: Electron.OpenDialogOptions = { + title: 'Select a directory', + properties: ['openDirectory', 'createDirectory'], + }; + const directory = await electronAPI.openDialog(options); + if (directory && directory.length > 0) { + log.info('Selected directory', directory[0]); + setSelectedPath(directory[0]); + } else { + log.error('No directory selected'); + } + }; + + const handleInstall = () => { + if (selectedPath) { + log.info('Installing to directory', selectedPath); + electronAPI.selectSetupDirectory(selectedPath); + onComplete(selectedPath); + } else { + log.error('No directory selected for installation'); + alert('Please select a directory before installing.'); + } + }; + + return ( +
+

Install ComfyUI

+

+ Please select a directory for where ComfyUI will store models, outputs, etc. If you already have a ComfyUI + setup, you can select that to reuse the model files. +

+ + {selectedPath && ( +
+

Selected path: {selectedPath}

+
+ )} + +
+ ); +}; + +const styles = { + container: { + display: 'flex', + flexDirection: 'column' as const, + alignItems: 'center', + padding: '20px', + maxWidth: '600px', + margin: '0 auto', + }, + title: { + fontSize: '24px', + marginBottom: '20px', + }, + description: { + textAlign: 'center' as const, + marginBottom: '20px', + }, + button: { + padding: '10px 20px', + fontSize: '16px', + cursor: 'pointer', + marginBottom: '10px', + }, + pathDisplay: { + marginTop: '10px', + marginBottom: '20px', + padding: '10px', + backgroundColor: '#f0f0f0', + borderRadius: '5px', + width: '100%', + }, + installButton: { + backgroundColor: '#4CAF50', + color: 'white', + border: 'none', + }, + disabledButton: { + backgroundColor: '#cccccc', + color: '#666666', + cursor: 'not-allowed', + }, +}; + +export default FirstTimeSetup; diff --git a/yarn.lock b/yarn.lock index af36f92d..6b3cf29c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5890,6 +5890,7 @@ __metadata: typescript: "npm:~5.5.4" update-electron-app: "npm:^3.0.0" vite: "npm:^5.0.12" + yaml: "npm:^2.6.0" languageName: unknown linkType: soft @@ -13436,6 +13437,15 @@ __metadata: languageName: node linkType: hard +"yaml@npm:^2.6.0": + version: 2.6.0 + resolution: "yaml@npm:2.6.0" + bin: + yaml: bin.mjs + checksum: 10c0/9e74cdb91cc35512a1c41f5ce509b0e93cc1d00eff0901e4ba831ee75a71ddf0845702adcd6f4ee6c811319eb9b59653248462ab94fa021ab855543a75396ceb + languageName: node + linkType: hard + "yargs-parser@npm:^20.2.2": version: 20.2.9 resolution: "yargs-parser@npm:20.2.9"