diff --git a/src-tauri/src/cmds.rs b/src-tauri/src/cmds.rs index 847e483..befe9f8 100644 --- a/src-tauri/src/cmds.rs +++ b/src-tauri/src/cmds.rs @@ -1,5 +1,5 @@ use anyhow::Result; -use std::path::PathBuf; +use std::{path::PathBuf, process::Command}; use crate::{ config::{Config, Group, ISettings, NVersion, Project}, @@ -157,6 +157,19 @@ pub async fn configration_import( wrap_err!(configration::configration_import(&app_handle, sync).await) } +/// open project with VsCode +#[tauri::command] +pub async fn open_with_vscode(path: String) -> CmdResult<()> { + #[cfg(windows)] + let cmd: &str = "code.cmd"; + + #[cfg(unix)] + let cmd = "code"; + + wrap_err!(Command::new(cmd).arg(&path).status())?; + Ok(()) +} + /// open data dir `.nvmd` #[tauri::command] pub fn open_data_dir() -> CmdResult<()> { diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 18fe446..8043c6d 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -61,6 +61,7 @@ fn main() -> tauri::Result<()> { cmds::sync_project_version, cmds::batch_update_project_version, cmds::open_dir, + cmds::open_with_vscode, // groups cmds::group_list, cmds::update_groups, diff --git a/src/components/vscode-logo.tsx b/src/components/vscode-logo.tsx new file mode 100644 index 0000000..5702898 --- /dev/null +++ b/src/components/vscode-logo.tsx @@ -0,0 +1,29 @@ +import { forwardRef } from 'react'; +import { cn } from '@/lib/utils'; + +export interface VsCodeLogoProps extends React.SVGAttributes {} + +export const VsCodeLogo = forwardRef( + ({ className, ...props }, ref) => { + return ( + + + + ); + } +); diff --git a/src/locales/en.json b/src/locales/en.json index 1114fe9..1ee67e7 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -134,5 +134,6 @@ "Invalid-project-path": "Invalid project path", "Error-500": "Sorry, something went wrong.", "Unzipping": "Unzipping", - "Set-as-default": "Set as the default version" + "Set-as-default": "Set as the default version", + "open-with-vscode": "Open with VsCode" } \ No newline at end of file diff --git a/src/locales/zh_CN.json b/src/locales/zh_CN.json index a5e7029..e734f18 100644 --- a/src/locales/zh_CN.json +++ b/src/locales/zh_CN.json @@ -134,5 +134,6 @@ "Invalid-project-path": "无效的项目路径", "Error-500": "抱歉,出了点问题。", "Unzipping": "解压中", - "Set-as-default": "设置为默认版本" + "Set-as-default": "设置为默认版本", + "open-with-vscode": "使用 VsCode 打开" } diff --git a/src/pages/projects/index.tsx b/src/pages/projects/index.tsx index 74c0a5f..797c07c 100644 --- a/src/pages/projects/index.tsx +++ b/src/pages/projects/index.tsx @@ -2,27 +2,32 @@ import { useMemo, useState, useEffect } from 'react'; import { useLoaderData } from 'react-router-dom'; import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, - AlertDialogTrigger, - Button, - DataDndTable, - DataTableToolbar, - LabelCopyable, - Select, - SelectContent, - SelectGroup, - SelectItem, - SelectLabel, - SelectTrigger, - SelectValue, + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, + Button, + DataDndTable, + DataTableToolbar, + LabelCopyable, + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectLabel, + SelectTrigger, + SelectValue, + Tooltip, + TooltipContent, + TooltipPortal, + TooltipTrigger, } from '@/components/ui'; +import { VsCodeLogo } from '@/components/vscode-logo'; import { DndProvider } from 'react-dnd'; import { HTML5Backend } from 'react-dnd-html5-backend'; import { FilePlusIcon, ReloadIcon, TrashIcon } from '@radix-ui/react-icons'; @@ -32,404 +37,419 @@ import { cn } from '@/lib/utils'; import { useAppContext } from '@/app-context'; import { useTranslation } from 'react-i18next'; import { - selectProjects, - groupList, - installedList, - projectList, - updateGroups, - updateProjects, - syncProjectVersion, - openDir, + selectProjects, + groupList, + installedList, + projectList, + updateGroups, + updateProjects, + syncProjectVersion, + openDir, + openWithVSCode, } from '@/services/cmds'; import { getCurrent } from '@/services/api'; import type { ColumnDef } from '@tanstack/react-table'; export async function loader() { - const versions = await Promise.all([ - projectList(), - groupList(), - installedList(), - ]); + const versions = await Promise.all([ + projectList(), + groupList(), + installedList(), + ]); - return versions; + return versions; } export const Component: React.FC = () => { - const [allProjects, allGroups, allInstalledVersions] = useLoaderData() as [ - Nvmd.Project[], - Nvmd.Group[], - Array - ]; + const [allProjects, allGroups, allInstalledVersions] = useLoaderData() as [ + Nvmd.Project[], + Nvmd.Group[], + Array + ]; - const [installedVersions, setInstalledVersions] = useState( - () => allInstalledVersions - ); - const [projects, setProjects] = useState(() => allProjects); - const [groups, setGroups] = useState(() => allGroups); - const [loading, setLoading] = useState(false); + const [installedVersions, setInstalledVersions] = useState( + () => allInstalledVersions + ); + const [projects, setProjects] = useState(() => allProjects); + const [groups, setGroups] = useState(() => allGroups); + const [loading, setLoading] = useState(false); - const { t } = useTranslation(); - const { settings } = useAppContext(); - const { directory } = settings; + const { t } = useTranslation(); + const { settings } = useAppContext(); + const { directory } = settings; - useEffect(() => { - const unlisted = getCurrent().listen( - 'call-projects-update', - async ({ payload }) => { - const [projects, groups] = await Promise.all([ - projectList(), - groupList(), - ]); - setProjects(projects); - setGroups(groups); - payload && - toast.success(t('Restart-Terminal', { version: `v${payload}` })); - } - ); + useEffect(() => { + const unlisted = getCurrent().listen( + 'call-projects-update', + async ({ payload }) => { + const [projects, groups] = await Promise.all([ + projectList(), + groupList(), + ]); + setProjects(projects); + setGroups(groups); + payload && + toast.success(t('Restart-Terminal', { version: `v${payload}` })); + } + ); - return () => { - unlisted.then((fn) => fn()); - }; - }, []); + return () => { + unlisted.then((fn) => fn()); + }; + }, []); - useEffect(() => { - const fetcher = async () => { - const iVersions = await installedList(false); - setInstalledVersions(iVersions); - }; + useEffect(() => { + const fetcher = async () => { + const iVersions = await installedList(false); + setInstalledVersions(iVersions); + }; - fetcher(); - }, [directory]); + fetcher(); + }, [directory]); - const columns: ColumnDef[] = useMemo( - () => [ - { - accessorKey: 'sort', - maxSize: 50, - enableHiding: false, - header: () => null, - }, - { - accessorKey: 'name', - header: t('Project-Name'), - maxSize: 240, - enableHiding: false, - }, - { - accessorKey: 'path', - header: t('Project-Path'), - enableHiding: false, - cell: ({ row }) => { - const path = row.original.path; - return ( - - { - if (!row.original.active) return; - try { - await openDir(path); - } catch { - toast.error(t('Invalid-project-path')); - } - }} - > - {path} - - - ); - }, - }, - { - accessorKey: 'version', - header: t('Version'), - meta: { - label: t('Version'), - }, - maxSize: 200, - cell: ({ row }) => { - const { version, path } = row.original; - return ( - { + // fromGroup: whether to switch from group, need to remove + // toGroup: whether to switch to group, need to add + const fromGroup = groups.find(({ name }) => name === version), + toGroup = groups.find(({ name }) => name === newVersion); + try { + const targetVersion = toGroup + ? toGroup.version + : newVersion || ''; + const code = await syncProjectVersion(path, targetVersion); - const updateProjectsPromise = async () => { - const newProjects = projects.map((project) => - project.path === path - ? { - ...project, - version: toGroup - ? toGroup.name - : newVersion - ? newVersion - : '', - active: code === 200 ? true : false, - updateAt: new Date().toISOString(), - } - : project - ); - await updateProjects(newProjects); + const updateProjectsPromise = async () => { + const newProjects = projects.map((project) => + project.path === path + ? { + ...project, + version: toGroup + ? toGroup.name + : newVersion + ? newVersion + : '', + active: code === 200 ? true : false, + updateAt: new Date().toISOString(), + } + : project + ); + await updateProjects(newProjects); - return newProjects; - }; + return newProjects; + }; - const updateGroupsPromise = async () => { - const newGroups = [...groups]; - let needUpdate: boolean = false; - newGroups.forEach((group) => { - const groupProjects = [...group.projects]; - if (fromGroup && group.name === version) { - needUpdate = true; - group.projects = groupProjects.filter( - (project) => project !== path - ); - } + const updateGroupsPromise = async () => { + const newGroups = [...groups]; + let needUpdate: boolean = false; + newGroups.forEach((group) => { + const groupProjects = [...group.projects]; + if (fromGroup && group.name === version) { + needUpdate = true; + group.projects = groupProjects.filter( + (project) => project !== path + ); + } - if (toGroup && group.name === newVersion) { - needUpdate = true; - group.projects = [path].concat(groupProjects); - } - }); + if (toGroup && group.name === newVersion) { + needUpdate = true; + group.projects = [path].concat(groupProjects); + } + }); - if (!needUpdate) return Promise.resolve(undefined); + if (!needUpdate) return Promise.resolve(undefined); - await updateGroups(newGroups); - return newGroups; - }; + await updateGroups(newGroups); + return newGroups; + }; - const [newProjects, newGroups] = await Promise.all([ - updateProjectsPromise(), - updateGroupsPromise(), - ]); + const [newProjects, newGroups] = await Promise.all([ + updateProjectsPromise(), + updateGroupsPromise(), + ]); - setProjects(newProjects); - newGroups && setGroups(newGroups); - code === 200 - ? toast.success( - t('Restart-Terminal', { version: `v${targetVersion}` }) - ) - : toast.error(`Project not found, please check it`); - } catch (err) { - toast.error('Something went wrong'); - } - }} - > - - - - - - - {t('Versions')} - - {installedVersions.map((version) => ( - - v{version} - - ))} - - - - {t('Groups')} - - {groups.map(({ name, desc }) => ( - - {name} - - ))} - - - - ); - }, - }, - { - header: t('Operation'), - maxSize: 120, - cell: ({ row }) => { - const { name, path, version } = row.original; - return ( - - - - - - - {name} - - {t('Project-Delete')} - - - - {t('Cancel')} - { - const [newProjects, newGroups] = await Promise.all([ - (async () => { - const newProjects = projects.filter( - ({ path: source }) => source !== path - ); - await updateProjects(newProjects, path); - return newProjects; - })(), - (async () => { - const newGroups = [...groups]; - let needUpdate: boolean = false; - newGroups.forEach((group) => { - if (group.name === version) { - needUpdate = true; - const projects = [...group.projects]; - group.projects = projects.filter( - (proPath) => proPath !== path - ); - } - }); - needUpdate && (await updateGroups(newGroups)); - return needUpdate ? newGroups : undefined; - })(), - ]); - setProjects(newProjects); - newGroups && setGroups(newGroups); - }} - > - {t('OK')} - - - - - ); - }, - }, - ], - [t, projects, installedVersions.length, groups.length] - ); + setProjects(newProjects); + newGroups && setGroups(newGroups); + code === 200 + ? toast.success( + t('Restart-Terminal', { version: `v${targetVersion}` }) + ) + : toast.error(`Project not found, please check it`); + } catch (err) { + toast.error('Something went wrong'); + } + }} + > + + + + + + + {t('Versions')} + + {installedVersions.map((version) => ( + + v{version} + + ))} + + + + {t('Groups')} + + {groups.map(({ name, desc }) => ( + + {name} + + ))} + + + + ); + }, + }, + { + header: t('Operation'), + maxSize: 120, + cell: ({ row }) => { + const { name, path, version } = row.original; + return ( + + + + + + + {name} + + {t('Project-Delete')} + + + + {t('Cancel')} + { + const [newProjects, newGroups] = await Promise.all([ + (async () => { + const newProjects = projects.filter( + ({ path: source }) => source !== path + ); + await updateProjects(newProjects, path); + return newProjects; + })(), + (async () => { + const newGroups = [...groups]; + let needUpdate: boolean = false; + newGroups.forEach((group) => { + if (group.name === version) { + needUpdate = true; + const projects = [...group.projects]; + group.projects = projects.filter( + (proPath) => proPath !== path + ); + } + }); + needUpdate && (await updateGroups(newGroups)); + return needUpdate ? newGroups : undefined; + })(), + ]); + setProjects(newProjects); + newGroups && setGroups(newGroups); + }} + > + {t('OK')} + + + + + ); + }, + }, + ], + [t, projects, installedVersions.length, groups.length] + ); - // add project (multiple) - const onAddProject = async () => { - const pInfo = await selectProjects(); - if (!pInfo) return; + // add project (multiple) + const onAddProject = async () => { + const pInfo = await selectProjects(); + if (!pInfo) return; - const addedProjects: Nvmd.Project[] = []; - pInfo.forEach(({ path, version }) => { - const name = path.split(OS_PLATFORM === 'win32' ? '\\' : '/').pop()!, - now = new Date().toISOString(); + const addedProjects: Nvmd.Project[] = []; + pInfo.forEach(({ path, version }) => { + const name = path.split(OS_PLATFORM === 'win32' ? '\\' : '/').pop()!, + now = new Date().toISOString(); - if (!projects.find(({ path: source }) => source === path)) { - addedProjects.push({ - name, - path, - version, - active: true, - createAt: now, - updateAt: now, - }); - } else { - toast.error(`The project "${name}" already exists`); - } - }); + if (!projects.find(({ path: source }) => source === path)) { + addedProjects.push({ + name, + path, + version, + active: true, + createAt: now, + updateAt: now, + }); + } else { + toast.error(`The project "${name}" already exists`); + } + }); - const newProjects = [...addedProjects, ...projects]; - setProjects(newProjects); - updateProjects(newProjects); - return; - }; + const newProjects = [...addedProjects, ...projects]; + setProjects(newProjects); + updateProjects(newProjects); + return; + }; - const reorderRow = (draggedRowIndex: number, targetRowIndex: number) => { - setProjects((previous) => { - previous.splice( - targetRowIndex, - 0, - previous.splice(draggedRowIndex, 1)[0] - ); + const reorderRow = (draggedRowIndex: number, targetRowIndex: number) => { + setProjects((previous) => { + previous.splice( + targetRowIndex, + 0, + previous.splice(draggedRowIndex, 1)[0] + ); - const newProject = [...previous]; - updateProjects(newProject); + const newProject = [...previous]; + updateProjects(newProject); - return newProject; - }); - }; + return newProject; + }); + }; - const onPageReload = async () => { - setLoading(true); - try { - const [allProjects, allGroups, installedVersions] = await Promise.all([ - projectList(true), - groupList(true), - installedList(), - ]); - setProjects(allProjects); - setGroups(allGroups); - setInstalledVersions(installedVersions); - toast.success(t('Refresh-successful')); - } finally { - setLoading(false); - } - }; + const onPageReload = async () => { + setLoading(true); + try { + const [allProjects, allGroups, installedVersions] = await Promise.all([ + projectList(true), + groupList(true), + installedList(), + ]); + setProjects(allProjects); + setGroups(allGroups); + setInstalledVersions(installedVersions); + toast.success(t('Refresh-successful')); + } finally { + setLoading(false); + } + }; - return ( - -
- ( -
- -
- - -
-
- )} - reorderRow={reorderRow} - /> -
-
- ); + return ( + +
+ ( +
+ +
+ + +
+
+ )} + reorderRow={reorderRow} + /> +
+
+ ); }; Component.displayName = 'Projects'; diff --git a/src/services/cmds.ts b/src/services/cmds.ts index bf04f2b..96057c5 100644 --- a/src/services/cmds.ts +++ b/src/services/cmds.ts @@ -179,6 +179,15 @@ export function configrationImport(sync: boolean = false) { }); } +/** + * @description: Open project with VsCode. + * @param {string} path + * @returns {Promise} + */ +export function openWithVSCode(path: string) { + return invoke('open_with_vscode', { path }); +} + /** * @description: Open dir with the File Explorer. * @param {string} dir