diff --git a/src/main/main.ts b/src/main/main.ts index c32fdb3..ad9746f 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -24,7 +24,7 @@ import { gt } from "semver"; import loadLocale from "./locale"; import { Closer, Themes } from "../types"; -import type { MenuItemConstructorOptions } from "electron"; +import type { MenuItemConstructorOptions, OpenDialogOptions } from "electron"; let mainWindow: BrowserWindow | null = null, updater: AppUpdater | null = null, @@ -105,7 +105,7 @@ const createWindow = async (code?: number) => { ? "#000000" : "#ffffff", webPreferences: { - preload: app.isPackaged + preload: app.isPackaged ? join(__dirname, "../preload/preload.js") : join(__dirname, "../../out/preload/preload.js") } @@ -462,20 +462,28 @@ Promise.resolve().then(() => { ipcMain.handle( "open-folder-selecter", - async (_event, { title, project }: { title: string; project?: boolean }) => { + async ( + _event, + { title, multiple, project }: { title: string; multiple?: boolean; project?: boolean } + ) => { + const properties: OpenDialogOptions["properties"] = [ + "openDirectory", + "createDirectory", + "showHiddenFiles" + ]; + multiple && properties.push("multiSelections"); const { canceled, filePaths } = await dialog.showOpenDialog(mainWindow!, { title, - properties: ["openDirectory", "createDirectory", "showHiddenFiles"] + properties: properties }); if (canceled) return { canceled, filePaths }; - const [path] = filePaths; - if (!project) return { canceled, filePaths }; - const version = await getVersion(path); - return { canceled, filePaths, version }; + const versions = await Promise.all(filePaths.map((path) => getVersion(path))); + + return { canceled, filePaths, versions }; } ); diff --git a/src/preload/index.ts b/src/preload/index.ts index 8b7e5d2..40a8752 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -1,17 +1,17 @@ // Disable no-unused-vars, broken for spread args /* eslint no-unused-vars: off */ -import { contextBridge, ipcRenderer } from 'electron' +import { contextBridge, ipcRenderer } from "electron"; -import type { OpenDialogReturnValue } from 'electron' -import type { ProgressInfo, UpdateInfo } from 'electron-updater' +import type { OpenDialogReturnValue } from "electron"; +import type { ProgressInfo, UpdateInfo } from "electron-updater"; -type OnCheckUpdateResultCallback = (info: UpdateInfo | 'update-not-available') => void -type OnUpdateProgressCallback = (progress: ProgressInfo) => void -type OnProgressCallback = (id: string, data: Nvmd.ProgressData) => void -type OnThemeChangedCallback = (theme: string) => void -type OnCurVersionChange = (version: string) => void -type OnProjectUpdate = (projects: Nvmd.Project[], version: string) => void -type OnMigrationError = () => void +type OnCheckUpdateResultCallback = (info: UpdateInfo | "update-not-available") => void; +type OnUpdateProgressCallback = (progress: ProgressInfo) => void; +type OnProgressCallback = (id: string, data: Nvmd.ProgressData) => void; +type OnThemeChangedCallback = (theme: string) => void; +type OnCurVersionChange = (version: string) => void; +type OnProjectUpdate = (projects: Nvmd.Project[], version: string) => void; +type OnMigrationError = () => void; let onCheckUpdateResult: OnCheckUpdateResultCallback | null = null, onUpdateProgress: OnUpdateProgressCallback | null = null, @@ -19,122 +19,130 @@ let onCheckUpdateResult: OnCheckUpdateResultCallback | null = null, onThemeChanged: OnThemeChangedCallback | null = null, onCurVersionChange: OnCurVersionChange | null = null, onProjectUpdate: OnProjectUpdate | null = null, - onMigrationError: OnMigrationError | null = null + onMigrationError: OnMigrationError | null = null; -ipcRenderer.on('update-available', (_event, info: UpdateInfo) => { - onCheckUpdateResult?.(info) -}) +ipcRenderer.on("update-available", (_event, info: UpdateInfo) => { + onCheckUpdateResult?.(info); +}); -ipcRenderer.on('update-not-available', (_event, info: 'update-not-available') => { - onCheckUpdateResult?.(info) -}) +ipcRenderer.on("update-not-available", (_event, info: "update-not-available") => { + onCheckUpdateResult?.(info); +}); -ipcRenderer.on('download-progress', (_event, progress: ProgressInfo) => { - onUpdateProgress?.(progress) -}) +ipcRenderer.on("download-progress", (_event, progress: ProgressInfo) => { + onUpdateProgress?.(progress); +}); -ipcRenderer.on('update-error', (_event, err) => { - console.log(err) -}) +ipcRenderer.on("update-error", (_event, err) => { + console.log(err); +}); -ipcRenderer.on('get-node:progress', (_event, id: string, progress: Nvmd.ProgressData) => { - onProgress?.(id, progress) -}) +ipcRenderer.on("get-node:progress", (_event, id: string, progress: Nvmd.ProgressData) => { + onProgress?.(id, progress); +}); -ipcRenderer.on('native-theme:changed', (_event, theme: string) => { - onThemeChanged?.(theme) -}) +ipcRenderer.on("native-theme:changed", (_event, theme: string) => { + onThemeChanged?.(theme); +}); -ipcRenderer.on('current-version-update', (_evnet, version: string) => { - onCurVersionChange?.(version) -}) +ipcRenderer.on("current-version-update", (_evnet, version: string) => { + onCurVersionChange?.(version); +}); -ipcRenderer.on('call-projects-update', (_evnet, projects: Nvmd.Project[], version: string) => { - onProjectUpdate?.(projects, version) -}) +ipcRenderer.on("call-projects-update", (_evnet, projects: Nvmd.Project[], version: string) => { + onProjectUpdate?.(projects, version); +}); -ipcRenderer.on('migration-error', (_evnet) => { - onMigrationError?.() -}) +ipcRenderer.on("migration-error", (_evnet) => { + onMigrationError?.(); +}); const electronHandler = { platform: process.platform, arch: process.arch, - version: ipcRenderer.sendSync('get-app-version') as string, + version: ipcRenderer.sendSync("get-app-version") as string, windowClose: () => { - ipcRenderer.send('window:close') + ipcRenderer.send("window:close"); }, windowMinimize: () => { - ipcRenderer.send('window:minimize') + ipcRenderer.send("window:minimize"); }, - checkForUpdates: () => ipcRenderer.invoke('check-for-updates') as Promise, - comfirmUpdate: () => ipcRenderer.invoke('confirm-update') as Promise, + checkForUpdates: () => ipcRenderer.invoke("check-for-updates") as Promise, + comfirmUpdate: () => ipcRenderer.invoke("confirm-update") as Promise, makeUpdateNow() { - ipcRenderer.send('make-update-now') + ipcRenderer.send("make-update-now"); }, onCheckUpdateResultCallback(callback: OnCheckUpdateResultCallback) { - onCheckUpdateResult = callback + onCheckUpdateResult = callback; }, onRegistUpdateProgress(callback: OnUpdateProgressCallback) { - onUpdateProgress = callback + onUpdateProgress = callback; }, getSettingData: () => - ipcRenderer.sendSync('setting-data-get') as Nvmd.Setting & { - localeMessages: I18n.Message + ipcRenderer.sendSync("setting-data-get") as Nvmd.Setting & { + localeMessages: I18n.Message; }, updateSettingData: (setting: Nvmd.Setting) => - ipcRenderer.invoke('setting-data-set', setting) as Promise, - getLocaleData: () => ipcRenderer.sendSync('locale-data') as I18n.Message, + ipcRenderer.invoke("setting-data-set", setting) as Promise, + getLocaleData: () => ipcRenderer.sendSync("locale-data") as I18n.Message, getAllNodeVersions: async (arg?: { id?: string; fetch?: boolean }) => - ipcRenderer.invoke('all-node-versions', arg) as Promise, + ipcRenderer.invoke("all-node-versions", arg) as Promise, getInstalledNodeVersions: async (refresh: boolean = false): Promise => - ipcRenderer.invoke('installed-node-versions', refresh), + ipcRenderer.invoke("installed-node-versions", refresh), - getNode: async (args: { id: string; version: string }) => ipcRenderer.invoke('get-node', args), - controllerAbort: (id: string) => ipcRenderer.invoke('controller:abort', id), + getNode: async (args: { id: string; version: string }) => ipcRenderer.invoke("get-node", args), + controllerAbort: (id: string) => ipcRenderer.invoke("controller:abort", id), - useNodeVersion: (version: string) => ipcRenderer.invoke('use-version', version), - getCurrentVersion: (fetch: boolean = false) => ipcRenderer.invoke('current-version', fetch), + useNodeVersion: (version: string) => ipcRenderer.invoke("use-version", version), + getCurrentVersion: (fetch: boolean = false) => ipcRenderer.invoke("current-version", fetch), onRegistCurVersionChange: (callback: OnCurVersionChange) => { - onCurVersionChange = callback + onCurVersionChange = callback; }, onRegistProgress: (onProgressSource: OnProgressCallback) => { - onProgress = onProgressSource + onProgress = onProgressSource; }, uninstallVersion: (version: string, current: boolean = false) => - ipcRenderer.invoke('uninstall-node-version', version, current), + ipcRenderer.invoke("uninstall-node-version", version, current), - getSystemTheme: () => ipcRenderer.sendSync('get-system-theme') as string, + getSystemTheme: () => ipcRenderer.sendSync("get-system-theme") as string, onRegistThemeCallback: (callback: OnThemeChangedCallback) => { - onThemeChanged = callback + onThemeChanged = callback; }, - openFolderSelecter: ({ title, project = false }: { title: string; project?: boolean }) => - ipcRenderer.invoke('open-folder-selecter', { title, project }) as Promise< - OpenDialogReturnValue & { version?: string } + openFolderSelecter: ({ + title, + multiple = false, + project = false + }: { + title: string; + multiple?: boolean; + project?: boolean; + }) => + ipcRenderer.invoke("open-folder-selecter", { title, multiple, project }) as Promise< + OpenDialogReturnValue & { versions?: string[] } >, getProjects: (load: boolean = false) => - ipcRenderer.invoke('get-projects', load) as Promise, + ipcRenderer.invoke("get-projects", load) as Promise, updateProjects: (projects: Nvmd.Project[], path?: string) => - ipcRenderer.invoke('update-projects', projects, path) as Promise, + ipcRenderer.invoke("update-projects", projects, path) as Promise, syncProjectVersion: (path: string, version: string) => - ipcRenderer.invoke('sync-project-version', path, version) as Promise<404 | 200>, + ipcRenderer.invoke("sync-project-version", path, version) as Promise<404 | 200>, onRegistProjectUpdate: (callback: OnProjectUpdate | null) => { - onProjectUpdate = callback + onProjectUpdate = callback; }, onRegistMigrationError: (callback: OnMigrationError) => { - onMigrationError = callback + onMigrationError = callback; } -} +}; -contextBridge.exposeInMainWorld('Context', electronHandler) +contextBridge.exposeInMainWorld("Context", electronHandler); -export type ElectronHandler = typeof electronHandler +export type ElectronHandler = typeof electronHandler; diff --git a/src/renderer/src/pages/projects/index.tsx b/src/renderer/src/pages/projects/index.tsx index e66ed58..dc9e817 100644 --- a/src/renderer/src/pages/projects/index.tsx +++ b/src/renderer/src/pages/projects/index.tsx @@ -188,28 +188,39 @@ export const Component: React.FC = () => { ); const onAddProject = async () => { - const { canceled, filePaths, version } = await window.Context.openFolderSelecter({ + const { + canceled, + filePaths, + versions = [] + } = await window.Context.openFolderSelecter({ title: i18n("Project-Select"), + multiple: true, project: true }); if (canceled) return; - const [path] = filePaths; - if (projects.find(({ path: source }) => source === path)) { - return toast.error("The project already exists"); - } + const addProjects: Nvmd.Project[] = []; - const pathArr = path.split(window.Context.platform === "win32" ? "\\" : "/"); - const now = new Date().toISOString(); - const project: Nvmd.Project = { - name: pathArr[pathArr.length - 1], - path, - version, - active: true, - createAt: now, - updateAt: now - }; - const newProjects = [project, ...projects]; + filePaths.forEach((path, index) => { + const pathArr = path.split(window.Context.platform === "win32" ? "\\" : "/"), + name = pathArr[pathArr.length - 1], + now = new Date().toISOString(); + + if (!projects.find(({ path: source }) => source === path)) { + addProjects.push({ + name, + path, + version: versions[index], + active: true, + createAt: now, + updateAt: now + }); + } else { + toast.error(`The project "${name}" already exists`); + } + }); + + const newProjects = [...addProjects, ...projects]; setProjects(newProjects); window.Context.updateProjects(newProjects); return;