From 1a1e35823872c87682da8a0aacdb519261768c53 Mon Sep 17 00:00:00 2001 From: 49lf Date: Tue, 30 Jul 2024 15:58:27 -0400 Subject: [PATCH] Most FS functions work now --- package.json | 1 + src/components/FileMachineProvider.tsx | 24 ++-- src/components/FileTree.tsx | 4 +- src/components/HelpMenu.tsx | 2 +- src/components/ProjectCard/ProjectCard.tsx | 14 +-- .../ProjectCard/ProjectCardRenameForm.tsx | 2 +- src/components/ProjectSidebarMenu.tsx | 4 +- src/components/Settings/AllSettingsFields.tsx | 6 +- src/components/SettingsAuthProvider.tsx | 2 +- src/components/Stream.tsx | 1 + src/lib/desktop.ts | 111 ++++++++++-------- src/lib/{tauriFS.ts => desktopFS.ts} | 7 +- src/lib/electron.ts | 12 ++ src/lib/paths.ts | 9 +- src/lib/routeLoaders.ts | 2 +- src/lib/settings/initialSettings.tsx | 31 +++-- src/lib/sorting.ts | 6 +- src/lib/tauriFS.test.ts | 2 +- src/main.ts | 11 +- src/routes/Home.tsx | 15 ++- src/wasm-lib/kcl/src/settings/types/file.rs | 10 +- src/wasm-lib/kcl/src/settings/types/mod.rs | 28 ++--- vite.config.ts | 69 +++++++++++ 23 files changed, 235 insertions(+), 138 deletions(-) rename src/lib/{tauriFS.ts => desktopFS.ts} (94%) create mode 100644 vite.config.ts diff --git a/package.json b/package.json index 33e2b35c1..ded59f11e 100644 --- a/package.json +++ b/package.json @@ -65,6 +65,7 @@ "xstate": "^4.38.2" }, "scripts": { + "start": "vite", "start:prod": "vite preview --port=3000", "serve": "vite serve --port=3000", "build": "curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && source \"$HOME/.cargo/env\" && curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh -s -- -y && yarn build:wasm && vite build", diff --git a/src/components/FileMachineProvider.tsx b/src/components/FileMachineProvider.tsx index bcbb49eaf..56e115ef5 100644 --- a/src/components/FileMachineProvider.tsx +++ b/src/components/FileMachineProvider.tsx @@ -15,7 +15,6 @@ import { } from 'xstate' import { useCommandsContext } from 'hooks/useCommandsContext' import { fileMachine } from 'machines/fileMachine' -import { mkdir, remove, rename, create } from '@tauri-apps/plugin-fs' import { isDesktop } from 'lib/isDesktop' import { join, sep } from '@tauri-apps/api/path' import { DEFAULT_FILE_NAME, FILE_EXT } from 'lib/constants' @@ -87,7 +86,7 @@ export const FileMachineProvider = ({ services: { readFiles: async (context: ContextFrom) => { const newFiles = isDesktop() - ? (await getProjectInfo(context.project.file.path)).children + ? (await getProjectInfo(context.project.path)).children : [] return { ...context.project, @@ -99,15 +98,15 @@ export const FileMachineProvider = ({ let createdPath: string if (event.data.makeDir) { - createdPath = await join(context.selectedDirectory.path, createdName) - await mkdir(createdPath) + createdPath = window.electron.path.join(context.selectedDirectory.path, createdName) + await window.electron.mkdir(createdPath) } else { createdPath = context.selectedDirectory.path + window.electron.path.sep + createdName + (createdName.endsWith(FILE_EXT) ? '' : FILE_EXT) - await create(createdPath) + await window.electron.writeFile(createdPath, '') } return { @@ -121,14 +120,15 @@ export const FileMachineProvider = ({ ) => { const { oldName, newName, isDir } = event.data const name = newName ? newName : DEFAULT_FILE_NAME - const oldPath = await join(context.selectedDirectory.path, oldName) - const newDirPath = await join(context.selectedDirectory.path, name) + const oldPath = window.electron.path.join(context.selectedDirectory.path, oldName) + const newDirPath = window.electron.path.join(context.selectedDirectory.path, name) const newPath = newDirPath + (name.endsWith(FILE_EXT) || isDir ? '' : FILE_EXT) - await rename(oldPath, newPath, {}) + await window.electron.rename(oldPath, newPath, {}) - if (oldPath === file?.path && project?.path) { + const currentFilePath = window.electron.path.join(file.path, file.name) + if (oldPath === currentFilePath && project?.path) { // If we just renamed the current file, navigate to the new path navigate(paths.FILE + '/' + encodeURIComponent(newPath)) } else if (file?.path.includes(oldPath)) { @@ -153,11 +153,11 @@ export const FileMachineProvider = ({ const isDir = !!event.data.children if (isDir) { - await remove(event.data.path, { + await window.electron.rm(event.data.path, { recursive: true, }).catch((e) => console.error('Error deleting directory', e)) } else { - await remove(event.data.path).catch((e) => + await window.electron.rm(event.data.path).catch((e) => console.error('Error deleting file', e) ) } @@ -169,7 +169,7 @@ export const FileMachineProvider = ({ file?.path.includes(event.data.path)) && project?.path ) { - navigate(paths.FILE + '/' + encodeURIComponent(project.file.path)) + navigate(paths.FILE + '/' + encodeURIComponent(project.path)) } return `Successfully deleted ${isDir ? 'folder' : 'file'} "${ diff --git a/src/components/FileTree.tsx b/src/components/FileTree.tsx index e522f1a48..3f66115c5 100644 --- a/src/components/FileTree.tsx +++ b/src/components/FileTree.tsx @@ -9,7 +9,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { faChevronRight } from '@fortawesome/free-solid-svg-icons' import { useFileContext } from 'hooks/useFileContext' import styles from './FileTree.module.css' -import { sortProject } from 'lib/tauriFS' +import { sortProject } from 'lib/desktopFS' import { FILE_EXT } from 'lib/constants' import { CustomIcon } from './CustomIcon' import { codeManager, kclManager } from 'lib/singletons' @@ -171,7 +171,7 @@ const FileTreeItem = ({ // Import non-kcl files // We want to update both the state and editor here. codeManager.updateCodeStateEditor( - `import("${fileOrDir.path.replace(project.file.path, '.')}")\n` + + `import("${fileOrDir.path.replace(project.path, '.')}")\n` + codeManager.code ) codeManager.writeToFile() diff --git a/src/components/HelpMenu.tsx b/src/components/HelpMenu.tsx index ebd780dbc..906c0a7d7 100644 --- a/src/components/HelpMenu.tsx +++ b/src/components/HelpMenu.tsx @@ -3,7 +3,7 @@ import Tooltip from './Tooltip' import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext' import { CustomIcon } from './CustomIcon' import { useLocation, useNavigate } from 'react-router-dom' -import { createAndOpenNewProject } from 'lib/tauriFS' +import { createAndOpenNewProject } from 'lib/desktopFS' import { paths } from 'lib/paths' import { useAbsoluteFilePath } from 'hooks/useAbsoluteFilePath' import { useLspContext } from './LspProvider' diff --git a/src/components/ProjectCard/ProjectCard.tsx b/src/components/ProjectCard/ProjectCard.tsx index 8332e8401..a5eaebf37 100644 --- a/src/components/ProjectCard/ProjectCard.tsx +++ b/src/components/ProjectCard/ProjectCard.tsx @@ -36,8 +36,8 @@ function ProjectCard({ void handleRenameProject(e, project).then(() => setIsEditing(false)) } - function getDisplayedTime(dateStr: string) { - const date = new Date(dateStr) + function getDisplayedTime(dateTimeMs: number) { + const date = new Date(dateTimeMs) const startOfToday = new Date() startOfToday.setHours(0, 0, 0, 0) return date.getTime() < startOfToday.getTime() @@ -103,7 +103,7 @@ function ProjectCard({ /> ) : (

- {project.file.name?.replace(FILE_EXT, '')} + {project.name?.replace(FILE_EXT, '')}

)} @@ -113,8 +113,8 @@ function ProjectCard({ Edited{' '} - {project.metadata && project.metadata?.modified - ? getDisplayedTime(project.metadata.modified) + {project.metadata && project.metadata.mtimeMs + ? getDisplayedTime(project.metadata.mtimeMs) : 'never'} @@ -169,11 +169,11 @@ function ProjectCard({ onDismiss={() => setIsConfirmingDelete(false)} >

- This will permanently delete "{project.file.name || 'this file'} + This will permanently delete "{project.name || 'this file'} ".

- Are you sure you want to delete "{project.file.name || 'this file'} + Are you sure you want to delete "{project.name || 'this file'} "? This action cannot be undone.

diff --git a/src/components/ProjectCard/ProjectCardRenameForm.tsx b/src/components/ProjectCard/ProjectCardRenameForm.tsx index c5d7ce4a6..837b05db1 100644 --- a/src/components/ProjectCard/ProjectCardRenameForm.tsx +++ b/src/components/ProjectCard/ProjectCardRenameForm.tsx @@ -24,7 +24,7 @@ export const ProjectCardRenameForm = forwardRef( required autoCorrect="off" autoCapitalize="off" - defaultValue={project.file.name} + defaultValue={project.name} ref={ref} onKeyDown={(e) => { if (e.key === 'Escape') { diff --git a/src/components/ProjectSidebarMenu.tsx b/src/components/ProjectSidebarMenu.tsx index e0c50d591..7b5ec0ec9 100644 --- a/src/components/ProjectSidebarMenu.tsx +++ b/src/components/ProjectSidebarMenu.tsx @@ -35,7 +35,7 @@ const ProjectSidebarMenu = ({ className="hidden select-none cursor-default text-sm text-chalkboard-110 dark:text-chalkboard-20 whitespace-nowrap lg:block" data-testid="project-name" > - {project?.name ? project.file.name : APP_NAME} + {project?.name ? project.name : APP_NAME} )} @@ -212,7 +212,7 @@ function ProjectMenuPopover({ {isDesktop() && project?.name && ( - {project.file.name} + {project.name} )} diff --git a/src/components/Settings/AllSettingsFields.tsx b/src/components/Settings/AllSettingsFields.tsx index 621290c50..0086e6d54 100644 --- a/src/components/Settings/AllSettingsFields.tsx +++ b/src/components/Settings/AllSettingsFields.tsx @@ -12,10 +12,10 @@ import { useLocation, useNavigate } from 'react-router-dom' import { isDesktop } from 'lib/isDesktop' import { ActionButton } from 'components/ActionButton' import { SettingsFieldInput } from './SettingsFieldInput' -import { getInitialDefaultDir, showInFolder } from 'lib/desktop' +import { getInitialDefaultDir } from 'lib/desktop' import toast from 'react-hot-toast' import { APP_VERSION } from 'routes/Settings' -import { createAndOpenNewProject, getSettingsFolderPaths } from 'lib/tauriFS' +import { createAndOpenNewProject, getSettingsFolderPaths } from 'lib/desktopFS' import { paths } from 'lib/paths' import { useDotDotSlash } from 'hooks/useDotDotSlash' import { sep } from '@tauri-apps/api/path' @@ -190,7 +190,7 @@ export const AllSettingsFields = forwardRef( const paths = await getSettingsFolderPaths( projectPath ? decodeURIComponent(projectPath) : undefined ) - showInFolder(paths[searchParamTab]) + window.electron.showInFolder(paths[searchParamTab]) }} iconStart={{ icon: 'folder', diff --git a/src/components/SettingsAuthProvider.tsx b/src/components/SettingsAuthProvider.tsx index 6d6dc57c6..b6762965b 100644 --- a/src/components/SettingsAuthProvider.tsx +++ b/src/components/SettingsAuthProvider.tsx @@ -209,7 +209,7 @@ export const SettingsAuthProviderBase = ({ }, services: { 'Persist settings': (context) => - saveSettings(context, loadedProject?.project?.path), + saveSettings(context, loadedProject?.path), }, } ) diff --git a/src/components/Stream.tsx b/src/components/Stream.tsx index f3f84aed1..7cfc2a068 100644 --- a/src/components/Stream.tsx +++ b/src/components/Stream.tsx @@ -206,6 +206,7 @@ export const Stream = () => { if (!videoRef.current) return if (!mediaStream) return + // The browser complains if we try to load a new stream without pausing first. // Do not immediately play the stream! try { videoRef.current.srcObject = mediaStream diff --git a/src/lib/desktop.ts b/src/lib/desktop.ts index 907c0368c..001ec4864 100644 --- a/src/lib/desktop.ts +++ b/src/lib/desktop.ts @@ -1,12 +1,11 @@ import { Models } from '@kittycad/lib' -import { Configuration } from 'wasm-lib/kcl/bindings/Configuration' import { ProjectConfiguration } from 'wasm-lib/kcl/bindings/ProjectConfiguration' import { Project } from 'wasm-lib/kcl/bindings/Project' -import { FileEntry } from 'wasm-lib/kcl/bindings/FileEntry' import { ProjectState } from 'wasm-lib/kcl/bindings/ProjectState' import { ProjectRoute } from 'wasm-lib/kcl/bindings/ProjectRoute' import { components } from './machine-api' import { isDesktop } from './isDesktop' +import { SaveSettingsPayload } from 'lib/settings/settingsUtils' import { defaultAppSettings, @@ -17,7 +16,6 @@ import { export { parseProjectRoute, } from 'lang/wasm' -import { SaveSettingsPayload } from 'lib/settings/settingsUtils' const DEFAULT_HOST = 'https://api.zoo.dev' const SETTINGS_FILE_NAME = 'settings.toml' @@ -25,7 +23,6 @@ const PROJECT_SETTINGS_FILE_NAME = 'project.toml' const PROJECT_FOLDER = 'zoo-modeling-app-projects' const DEFAULT_PROJECT_KCL_FILE = "main.kcl" - // List machines on the local network. export async function listMachines(): Promise<{ [key: string]: components['schemas']['Machine'] @@ -43,15 +40,32 @@ export async function renameProjectDirectory( projectPath: string, newName: string ): Promise { - debugger -} - -export async function showInFolder(path: string | undefined): Promise { - if (!path) { - console.error('path is undefined cannot call desktop showInFolder') - return + if (!newName) { + return Promise.reject(new Error(`New name for project cannot be empty`)) } - debugger + + try { await window.electron.stat(projectPath) } + catch (e) { + if (e === 'ENOENT') { + return Promise.reject(new Error(`Path ${projectPath} is not a directory`)) + } + } + + // Make sure the new name does not exist. + const newPath = window.electron.path.join(projectPath.split('/').slice(0, -1).join('/'), newName) + try { + await window.electron.stat(newPath) + // If we get here it means the stat succeeded and there's a file already + // with the same name... + return Promise.reject(new Error(`Path ${newPath} already exists, cannot rename to an existing path`)) + } catch (e) { + // Otherwise if it failed and the failure is "it doesnt exist" then rename it! + if (e === 'ENOENT') { + await window.electron.rename(projectPath, newPath) + return newPath + } + } + return Promise.reject(new Error('Unreachable')) } export async function ensureProjectDirectoryExists( @@ -62,7 +76,7 @@ export async function ensureProjectDirectoryExists( await window.electron.stat(projectDir) } catch (e) { if (e === 'ENOENT') { - window.electron.mkdir(projectDir, { recursive: true }, (e) => { + await window.electron.mkdir(projectDir, { recursive: true }, (e) => { console.log(e) }) } @@ -92,7 +106,7 @@ export async function createNewProjectDirectory( await window.electron.stat(projectDir) } catch (e) { if (e === 'ENOENT') { - window.electron.mkdir(projectDir, { recursive: true }) + await window.electron.mkdir(projectDir, { recursive: true }) } } @@ -101,17 +115,16 @@ export async function createNewProjectDirectory( DEFAULT_PROJECT_KCL_FILE ) await window.electron.writeFile(projectFile, initialCode ?? '') + const metadata = await window.electron.stat(projectFile) return { - file: { - path: projectDir, - name: projectName, - // We don't need to recursively get all files in the project directory. - // Because we just created it and it's empty. - children: undefined, - }, + path: projectDir, + name: projectName, + // We don't need to recursively get all files in the project directory. + // Because we just created it and it's empty. + children: undefined, default_file: projectFile, - metadata: undefined /* TODO */, + metadata, kcl_file_count: 1, directory_count: 0, } @@ -120,6 +133,9 @@ export async function createNewProjectDirectory( export async function listProjects( configuration?: Partial ): Promise { + if (configuration === undefined) { + configuration = await readAppSettingsFile() + } const projectDir = await ensureProjectDirectoryExists(configuration) const projects = [] const entries = await window.electron.readdir(projectDir) @@ -140,7 +156,7 @@ export async function listProjects( const IMPORT_FILE_EXTENSIONS = [ // TODO Use ImportFormat enum "stp", "glb", "fbxb", "kcl" -]; +] const isRelevantFile = (filename: string): boolean => IMPORT_FILE_EXTENSIONS.some( (ext) => filename.endsWith('.' + ext)) @@ -157,13 +173,13 @@ const collectAllFilesRecursiveFrom = async (path: string) => { // Make sure the path is a directory. const isPathDir = await window.electron.statIsDirectory(path) if (!isPathDir) { - return Promise.reject(new Error(`Path ${path} is not a directory`)); + return Promise.reject(new Error(`Path ${path} is not a directory`)) } const pathParts = path.split('/') let entry = /* FileEntry */ { name: pathParts.slice(-1)[0], - path: pathParts.slice(0, -1).join('/'), + path, children: [], } @@ -185,9 +201,9 @@ const collectAllFilesRecursiveFrom = async (path: string) => { if (!isRelevantFile(ePath)) { continue } children.push(/* FileEntry */ { name: e, - path: ePath.split('/').slice(0, -1).join('/'), + path: ePath, children: undefined, - }); + }) } } @@ -199,13 +215,13 @@ const collectAllFilesRecursiveFrom = async (path: string) => { const getDefaultKclFileForDir = async (projectDir, file) => { // Make sure the dir is a directory. - const isFileEntryDir = await window.electron.statIsDirectory(file.path) + const isFileEntryDir = await window.electron.statIsDirectory(projectDir) if (!isFileEntryDir) { - return Promise.reject(new Error(`Path ${file.path} is not a directory`)) + return Promise.reject(new Error(`Path ${projectDir} is not a directory`)) } - let defaultFilePath = window.electron.path.join(file.path, DEFAULT_PROJECT_KCL_FILE) - try { await window.eletron.stat(defaultFilePath) } + let defaultFilePath = window.electron.path.join(projectDir, DEFAULT_PROJECT_KCL_FILE) + try { await window.electron.stat(defaultFilePath) } catch (e) { if (e === 'ENOENT') { // Find a kcl file in the directory. @@ -213,7 +229,7 @@ const getDefaultKclFileForDir = async (projectDir, file) => { for (let entry of file.children) { if (entry.name.endsWith(".kcl")) { return window.electron.path.join(projectDir, entry.name) - } else if (entry.children.is_some()) { + } else if (entry.children.length > 0) { // Recursively find a kcl file in the directory. return getDefaultKclFileForDir(entry.path, entry) } @@ -228,7 +244,7 @@ const getDefaultKclFileForDir = async (projectDir, file) => { } const kclFileCount = (file /* fileEntry */) => { - let count = 0; + let count = 0 if (file.children) { for (let entry of file.children) { if (entry.name.endsWith(".kcl")) { @@ -263,40 +279,37 @@ export async function getProjectInfo( try { await window.electron.stat(projectPath) } catch (e) { if (e === 'ENOENT') { - return Promise.reject(new Error(`Project directory does not exist: ${project_path}`)); + return Promise.reject(new Error(`Project directory does not exist: ${project_path}`)) } } // Make sure it is a directory. const projectPathIsDir = await window.electron.statIsDirectory(projectPath) if (!projectPathIsDir) { - return Promise.reject(new Error(`Project path is not a directory: ${project_path}`)); + return Promise.reject(new Error(`Project path is not a directory: ${project_path}`)) } let walked = await collectAllFilesRecursiveFrom(projectPath) let default_file = await getDefaultKclFileForDir(projectPath, walked) + const metadata = await window.electron.stat(projectPath) let project = /* FileEntry */ { - file: walked, - metadata: undefined, + ...walked, + metadata, kcl_file_count: 0, directory_count: 0, default_file, - }; + } // Populate the number of KCL files in the project. - project.kcl_file_count = kclFileCount(project.file) + project.kcl_file_count = kclFileCount(project) //Populate the number of directories in the project. - project.directory_count = directoryCount(project.file) + project.directory_count = directoryCount(project) return project } -export async function readDirRecursive(path: string): Promise { - debugger -} - // Write project settings file. export async function writeProjectSettingsFile({ projectPath, @@ -320,8 +333,8 @@ const getAppSettingsFilePath = async () => { await window.electron.stat(fullPath) } catch (e) { // File/path doesn't exist - if (e.code === 'ENOENT') { - window.electron.mkdir(fullPath, { recursive: true }) + if (e === 'ENOENT') { + await window.electron.mkdir(fullPath, { recursive: true }) } } return window.electron.path.join(fullPath, SETTINGS_FILE_NAME) @@ -331,8 +344,8 @@ const getProjectSettingsFilePath = async (projectPath: string) => { try { await window.electron.stat(projectPath) } catch (e) { - if (e.code === 'ENOENT') { - window.electron.mkdir(projectPath, { recursive: true }) + if (e === 'ENOENT') { + await window.electron.mkdir(projectPath, { recursive: true }) } } return window.electron.path.join(projectPath, PROJECT_SETTINGS_FILE_NAME) @@ -419,7 +432,7 @@ export const getUser = async ( } // Use kittycad library to fetch the user info from /user/me - if (baseurl != DEFAULT_HOST) { + if (baseurl !== DEFAULT_HOST) { // The TypeScript generated library uses environment variables for this // because it was intended for NodeJS. window.electron.process.env.BASE_URL(baseurl) diff --git a/src/lib/tauriFS.ts b/src/lib/desktopFS.ts similarity index 94% rename from src/lib/tauriFS.ts rename to src/lib/desktopFS.ts index e502b2b56..d9ea2f212 100644 --- a/src/lib/tauriFS.ts +++ b/src/lib/desktopFS.ts @@ -1,4 +1,3 @@ -import { appConfigDir } from '@tauri-apps/api/path' import { isDesktop } from './isDesktop' import type { FileEntry } from 'lib/types' import { @@ -64,9 +63,9 @@ function interpolateProjectName(projectName: string) { } // Returns the next available index for a project name -export function getNextProjectIndex(projectName: string, files: FileEntry[]) { +export function getNextProjectIndex(projectName: string, projects: FileEntry[]) { const regex = interpolateProjectName(projectName) - const matches = files.map((file) => file.name?.match(regex)) + const matches = projects.map((project) => project.name?.match(regex)) const indices = matches .filter(Boolean) .map((match) => match![1]) @@ -108,7 +107,7 @@ function getPaddedIdentifierRegExp() { } export async function getSettingsFolderPaths(projectPath?: string) { - const user = isDesktop() ? await appConfigDir() : '/' + const user = isDesktop() ? await window.electron.getPath('appData') : '/' const project = projectPath !== undefined ? projectPath : undefined return { diff --git a/src/lib/electron.ts b/src/lib/electron.ts index adca5d455..3ddd1032d 100644 --- a/src/lib/electron.ts +++ b/src/lib/electron.ts @@ -3,7 +3,11 @@ import path from 'path' import fs from 'node:fs/promises' import packageJson from '../../package.json' +const open = (args: any) => ipcRenderer.invoke('dialog', args) +const showInFolder = (path: string) => ipcRenderer.invoke('shell.showItemInFolder', path) + const readFile = (path: string) => fs.readFile(path, 'utf-8') +const rename = (prev: string, next: string) => fs.rename(prev, next) const writeFile = (path: string, data: string) => fs.writeFile(path, data, 'utf-8') const readdir = (path: string) => fs.readdir(path, 'utf-8') @@ -27,13 +31,21 @@ const exposeProcessEnv = (varName: string) => { import('@kittycad/lib').then((kittycad) => { contextBridge.exposeInMainWorld('electron', { + // Passing fs directly is not recommended since it gives a lot of power + // to the browser side / potential malicious code. We restrict what is + // exported. readFile, writeFile, readdir, + rename, + rm: fs.rm, path, stat, statIsDirectory, mkdir: fs.mkdir, + // opens a dialog + open, + showInFolder, getPath, packageJson, platform: process.platform, diff --git a/src/lib/paths.ts b/src/lib/paths.ts index d6152aafb..3e3fad8cf 100644 --- a/src/lib/paths.ts +++ b/src/lib/paths.ts @@ -4,7 +4,6 @@ import { isDesktop } from './isDesktop' import { Configuration } from 'wasm-lib/kcl/bindings/Configuration' import { ProjectRoute } from 'wasm-lib/kcl/bindings/ProjectRoute' import { parseProjectRoute, readAppSettingsFile } from './desktop' -import { parseProjectRoute as parseProjectRouteWasm } from 'lang/wasm' import { readLocalStorageAppSettingsFile } from './settings/settingsUtils' import { err } from 'lib/trap' @@ -38,19 +37,17 @@ export async function getProjectMetaByRouteId( ): Promise { if (!id) return undefined - const inTauri = isDesktop() + const onDesktop = isDesktop() if (configuration === undefined) { - configuration = inTauri + configuration = onDesktop ? await readAppSettingsFile() : readLocalStorageAppSettingsFile() } if (err(configuration)) return Promise.reject(configuration) - const route = inTauri - ? await parseProjectRoute(configuration, id) - : parseProjectRouteWasm(configuration, id) + const route = parseProjectRoute(configuration, id) if (err(route)) return Promise.reject(route) diff --git a/src/lib/routeLoaders.ts b/src/lib/routeLoaders.ts index 52400a497..513dd6381 100644 --- a/src/lib/routeLoaders.ts +++ b/src/lib/routeLoaders.ts @@ -126,7 +126,7 @@ export const fileLoader: LoaderFunction = async ({ }, file: { name: current_file_name, - path: current_file_path, + path: current_file_path.split('/').slice(0, -1).join('/'), children: [], }, } diff --git a/src/lib/settings/initialSettings.tsx b/src/lib/settings/initialSettings.tsx index 7c8fc812b..d5665427b 100644 --- a/src/lib/settings/initialSettings.tsx +++ b/src/lib/settings/initialSettings.tsx @@ -14,7 +14,6 @@ import { } from 'lib/cameraControls' import { isDesktop } from 'lib/isDesktop' import { useRef } from 'react' -import { open } from '@tauri-apps/plugin-dialog' import { CustomIcon } from 'components/CustomIcon' import Tooltip from 'components/Tooltip' @@ -209,22 +208,20 @@ export function createSettings() { onClick={async () => { // In Tauri end-to-end tests we can't control the file picker, // so we seed the new directory value in the element's dataset - const newValue = - inputRef.current && inputRef.current.dataset.testValue - ? inputRef.current.dataset.testValue - : await open({ - directory: true, - recursive: true, - defaultPath: value, - title: 'Choose a new project directory', - }) - if ( - newValue && - newValue !== null && - newValue !== value && - !Array.isArray(newValue) - ) { - updateValue(newValue) + const inputRefVal = inputRef.current?.dataset.testValue + if (inputRef.current && inputRefVal && !Array.isArray(inputRefVal)) { + updateValue(inputRefVal) + } else { + const newPath = await window.electron.open({ + properties: [ + 'openDirectory', + 'createDirectory', + ], + defaultPath: value, + title: 'Choose a new project directory', + }) + if (newPath.canceled) return + updateValue(newPath.filePaths[0]) } }} className="p-0 m-0 border-none hover:bg-primary/10 focus:bg-primary/10 dark:hover:bg-primary/20 dark:focus::bg-primary/20" diff --git a/src/lib/sorting.ts b/src/lib/sorting.ts index 64c52b264..a988caaa0 100644 --- a/src/lib/sorting.ts +++ b/src/lib/sorting.ts @@ -36,9 +36,9 @@ export function getSortFunction(sortBy: string) { } const sortByModified = (a: Project, b: Project) => { - if (a.metadata?.modified && b.metadata?.modified) { - const aDate = new Date(a.metadata.modified) - const bDate = new Date(b.metadata.modified) + if (a.metadata?.mtimeMs && b.metadata?.mtimeMs) { + const aDate = new Date(a.metadata.mtimeMs) + const bDate = new Date(b.metadata.mtimeMs) return !sortBy || sortBy.includes('desc') ? bDate.getTime() - aDate.getTime() : aDate.getTime() - bDate.getTime() diff --git a/src/lib/tauriFS.test.ts b/src/lib/tauriFS.test.ts index 04370a77f..6c9095361 100644 --- a/src/lib/tauriFS.test.ts +++ b/src/lib/tauriFS.test.ts @@ -1,4 +1,4 @@ -import { getNextProjectIndex, interpolateProjectNameWithIndex } from './tauriFS' +import { getNextProjectIndex, interpolateProjectNameWithIndex } from './desktopFS' import { MAX_PADDING } from './constants' describe('Test project name utility functions', () => { diff --git a/src/main.ts b/src/main.ts index 50d0d5689..ac3d7a841 100644 --- a/src/main.ts +++ b/src/main.ts @@ -2,7 +2,7 @@ // template that ElectronJS provides. import { Configuration } from 'wasm-lib/kcl/bindings/Configuration' -import { app, BrowserWindow, ipcMain } from 'electron' +import { app, BrowserWindow, ipcMain, dialog, shell } from 'electron' import path from 'path' // Handle creating/removing shortcuts on Windows when installing/uninstalling. @@ -12,6 +12,7 @@ if (require('electron-squirrel-startup')) { const createWindow = () => { let mainWindow = new BrowserWindow({ + autoHideMenuBar: true, width: 800, height: 600, webPreferences: { @@ -52,3 +53,11 @@ app.on('ready', createWindow) ipcMain.handle('app.getPath', (event, data) => { return app.getPath(data) }) + +ipcMain.handle('dialog', (event, data) => { + return dialog.showOpenDialog(data) +}) + +ipcMain.handle('shell.showItemInFolder', (event, data) => { + return shell.showItemInFolder(data) +}) diff --git a/src/routes/Home.tsx b/src/routes/Home.tsx index e0c23562c..42160fb6c 100644 --- a/src/routes/Home.tsx +++ b/src/routes/Home.tsx @@ -1,10 +1,9 @@ import { FormEvent, useEffect, useRef } from 'react' -import { remove } from '@tauri-apps/plugin-fs' import { getNextProjectIndex, interpolateProjectNameWithIndex, doesProjectNameNeedInterpolated, -} from 'lib/tauriFS' +} from 'lib/desktopFS' import { ActionButton } from 'components/ActionButton' import { toast } from 'react-hot-toast' import { AppHeader } from 'components/AppHeader' @@ -131,7 +130,7 @@ const Home = () => { } await renameProjectDirectory( - await join(context.defaultDirectory, oldName), + window.electron.path.join(context.defaultDirectory, oldName), name ) return `Successfully renamed "${oldName}" to "${name}"` @@ -140,7 +139,7 @@ const Home = () => { context: ContextFrom, event: EventFrom ) => { - await remove(await join(context.defaultDirectory, event.data.name), { + await window.electron.rm(window.electron.path.join(context.defaultDirectory, event.data.name), { recursive: true, }) return `Successfully deleted "${event.data.name}"` @@ -192,15 +191,15 @@ const Home = () => { new FormData(e.target as HTMLFormElement) ) - if (newProjectName !== project.file.name) { + if (newProjectName !== project.name) { send('Rename project', { - data: { oldName: project.file.name, newName: newProjectName }, + data: { oldName: project.name, newName: newProjectName }, }) } } async function handleDeleteProject(project: Project) { - send('Delete project', { data: { name: project.file.name || '' } }) + send('Delete project', { data: { name: project.name || '' } }) } return ( @@ -296,7 +295,7 @@ const Home = () => {
    {searchResults.sort(getSortFunction(sort)).map((project) => ( [viteTsconfigPaths()], + }, +}) + +export default config