import { useMachine } from '@xstate/react' import { useNavigate, useRouteLoaderData } from 'react-router-dom' import { type IndexLoaderData } from 'lib/types' import { PATHS } from 'lib/paths' import React, { createContext } from 'react' import { toast } from 'react-hot-toast' import { AnyStateMachine, ContextFrom, EventFrom, InterpreterFrom, Prop, StateFrom, assign, } from 'xstate' import { useCommandsContext } from 'hooks/useCommandsContext' import { fileMachine } from 'machines/fileMachine' import { isDesktop } from 'lib/isDesktop' import { DEFAULT_FILE_NAME, FILE_EXT } from 'lib/constants' import { getProjectInfo } from 'lib/desktop' type MachineContext = { state: StateFrom context: ContextFrom send: Prop, 'send'> } export const FileContext = createContext( {} as MachineContext ) export const FileMachineProvider = ({ children, }: { children: React.ReactNode }) => { const navigate = useNavigate() const { commandBarSend } = useCommandsContext() const { project, file } = useRouteLoaderData(PATHS.FILE) as IndexLoaderData const [state, send] = useMachine(fileMachine, { context: { project, selectedDirectory: project, }, actions: { navigateToFile: (context, event) => { if (event.data && 'name' in event.data) { commandBarSend({ type: 'Close' }) navigate( `${PATHS.FILE}/${encodeURIComponent( context.selectedDirectory + window.electron.path.sep + event.data.name )}` ) } else if ( event.data && 'path' in event.data && event.data.path.endsWith(FILE_EXT) ) { // Don't navigate to newly created directories navigate(`${PATHS.FILE}/${encodeURIComponent(event.data.path)}`) } }, addFileToRenamingQueue: assign({ itemsBeingRenamed: (context, event) => [ ...context.itemsBeingRenamed, event.data.path, ], }), removeFileFromRenamingQueue: assign({ itemsBeingRenamed: ( context, event: EventFrom ) => context.itemsBeingRenamed.filter( (path) => path !== event.data.oldPath ), }), renameToastSuccess: (_, event) => toast.success(event.data.message), createToastSuccess: (_, event) => toast.success(event.data.message), toastSuccess: (_, event) => event.data && toast.success((event.data || '') + ''), toastError: (_, event) => toast.error((event.data || '') + ''), }, services: { readFiles: async (context: ContextFrom) => { const newFiles = isDesktop() ? (await getProjectInfo(context.project.path)).children : [] return { ...context.project, children: newFiles, } }, createFile: async (context, event) => { let createdName = event.data.name.trim() || DEFAULT_FILE_NAME let createdPath: string if (event.data.makeDir) { 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 window.electron.writeFile(createdPath, '') } return { message: `Successfully created "${createdName}"`, path: createdPath, } }, renameFile: async ( context: ContextFrom, event: EventFrom ) => { const { oldName, newName, isDir } = event.data const name = newName ? newName : DEFAULT_FILE_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 window.electron.rename(oldPath, newPath) if (!file) { return Promise.reject(new Error('file is not defined')) } 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)) { // If we just renamed a directory that the current file is in, navigate to the new path navigate( PATHS.FILE + '/' + encodeURIComponent(file.path.replace(oldPath, newDirPath)) ) } return { message: `Successfully renamed "${oldName}" to "${name}"`, newPath, oldPath, } }, deleteFile: async ( context: ContextFrom, event: EventFrom ) => { const isDir = !!event.data.children if (isDir) { await window.electron .rm(event.data.path, { recursive: true, }) .catch((e) => console.error('Error deleting directory', e)) } else { await window.electron .rm(event.data.path) .catch((e) => console.error('Error deleting file', e)) } // If we just deleted the current file or one of its parent directories, // navigate to the project root if ( (event.data.path === file?.path || file?.path.includes(event.data.path)) && project?.path ) { navigate(PATHS.FILE + '/' + encodeURIComponent(project.path)) } return `Successfully deleted ${isDir ? 'folder' : 'file'} "${ event.data.name }"` }, }, guards: { 'Has at least 1 file': (_, event: EventFrom) => { if (event.type !== 'done.invoke.read-files') return false return !!event?.data?.children && event.data.children.length > 0 }, }, }) return ( {children} ) } export default FileMachineProvider