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, } from 'xstate' import { useCommandsContext } from 'hooks/useCommandsContext' import { fileMachine } from 'machines/fileMachine' import { mkdir, remove, rename, create } from '@tauri-apps/plugin-fs' import { isTauri } from 'lib/isTauri' import { join, sep } from '@tauri-apps/api/path' import { DEFAULT_FILE_NAME, FILE_EXT } from 'lib/constants' import { getProjectInfo } from 'lib/tauri' 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 } = 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 + sep() + event.data.name )}` ) } }, toastSuccess: (_, event) => event.data && toast.success((event.data || '') + ''), toastError: (_, event) => toast.error((event.data || '') + ''), }, services: { readFiles: async (context: ContextFrom) => { const newFiles = isTauri() ? (await getProjectInfo(context.project.name)).children : [] return { ...context.project, children: newFiles, } }, createFile: async (context, event) => { let name = event.data.name.trim() || DEFAULT_FILE_NAME if (event.data.makeDir) { await mkdir(await join(context.selectedDirectory.path, name)) } else { await create( context.selectedDirectory.path + sep() + name + (name.endsWith(FILE_EXT) ? '' : FILE_EXT) ) } return `Successfully created "${name}"` }, renameFile: async ( context: ContextFrom, event: EventFrom ) => { const { oldName, newName, isDir } = event.data let name = newName ? newName : DEFAULT_FILE_NAME await rename( await join(context.selectedDirectory.path, oldName), (await join(context.selectedDirectory.path, name)) + (name.endsWith(FILE_EXT) || isDir ? '' : FILE_EXT), {} ) return ( oldName !== name && `Successfully renamed "${oldName}" to "${name}"` ) }, deleteFile: async ( context: ContextFrom, event: EventFrom ) => { const isDir = !!event.data.children if (isDir) { await remove(event.data.path, { recursive: true, }).catch((e) => console.error('Error deleting directory', e)) } else { await remove(event.data.path).catch((e) => console.error('Error deleting file', e) ) } 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