import { Identifier } from 'deepslate' import { route } from 'preact-router' import { useCallback, useEffect, useMemo, useRef, useState } from 'preact/hooks' import config from '../../Config.js' import { DRAFT_PROJECT, getProjectRoot, useLocale, useProject, useVersion } from '../../contexts/index.js' import { useModal } from '../../contexts/Modal.jsx' import { useSpyglass } from '../../contexts/Spyglass.jsx' import { useFocus } from '../../hooks/useFocus.js' import { cleanUrl, writeZip } from '../../Utils.js' import { Btn } from '../Btn.js' import { BtnMenu } from '../BtnMenu.js' import { Octicon } from '../Octicon.jsx' import type { TreeViewGroupRenderer, TreeViewLeafRenderer } from '../TreeView.js' import { TreeView } from '../TreeView.js' import { FileRenaming } from './FileRenaming.jsx' import { ProjectCreation } from './ProjectCreation.jsx' import { ProjectDeletion } from './ProjectDeletion.jsx' export function ProjectPanel() { const { version } = useVersion() const { locale } = useLocale() const { showModal } = useModal() const { projects, project, projectUri, setProjectUri, changeProject } = useProject() const { client, service } = useSpyglass() const projectRoot = project ? getProjectRoot(project) : undefined const [entries, setEntries] = useState() useEffect(() => { setEntries(undefined) if (!projectRoot) { return } client.fs.readdir(projectRoot).then(entries => { setEntries(entries.flatMap(e => { return e.isFile() ? [e.name.slice(projectRoot.length)] : [] })) }) }, [projectRoot]) useEffect(() => { if (!service || !projectRoot) { return } service.watchTree(projectRoot, setEntries) return () => service.unwatchTree(projectRoot, setEntries) }, [service, projectRoot]) const download = useRef(null) const onDownload = async () => { if (!download.current || entries === undefined || !project) { return } const zipEntries = await Promise.all(entries.map(async e => { const data = await client.fs.readFile(projectRoot + e) return [e, data] as [string, Uint8Array] })) if (!zipEntries.some(e => e[0] === 'pack.mcmeta')) { const packFormat = config.versions.find(v => v.id === version)!.pack_format const packMcmeta = { pack: { description: project.name, pack_format: packFormat } } const data = new TextEncoder().encode(JSON.stringify(packMcmeta, null, 2)) zipEntries.push(['pack.mcmeta', data]) } const url = await writeZip(zipEntries) download.current.setAttribute('href', url) download.current.setAttribute('download', `${project.name.replaceAll(' ', '_')}.zip`) download.current.click() } const onDeleteProject = useCallback(() => { showModal(() => ) }, []) const onCreateProject = useCallback(() => { showModal(() => ) }, []) const actions = useMemo(() => [ { icon: 'pencil', label: locale('project.rename_file'), onAction: (uri: string) => { const res = service?.dissectUri(uri) if (res) { // This is pretty hacky, improve this in the future when spyglass has a "constructUri" function const oldSuffix = `${res.pack}/${res.namespace}/${res.path}/${res.identifier}${res.ext}` if (!uri.endsWith(oldSuffix)) { console.warn(`Expected ${uri} to end with ${oldSuffix}`) return } const onRename = (newId: string) => { const prefix = uri.substring(0, uri.length - oldSuffix.length) const { namespace, path } = Identifier.parse(newId) const newUri = prefix + `${res.pack}/${namespace}/${res.path}/${path}${res.ext}` service?.renameFile(uri, newUri).then(() => { setProjectUri(newUri) }) } showModal(() => ) } }, }, { icon: 'trashcan', label: locale('project.delete_file'), onAction: (uri: string) => { client.fs.unlink(uri).then(() => { setProjectUri(undefined) }) }, }, ], [client, service, projectRoot, showModal]) const FolderEntry: TreeViewGroupRenderer = useCallback(({ name, open, onClick }) => { return
{Octicon[!open ? 'chevron_right' : 'chevron_down']} {name}
}, []) const FileEntry: TreeViewLeafRenderer = useCallback(({ entry }) => { const [focused, setFocus] = useFocus() const uri = projectRoot + entry const onContextMenu = (evt: MouseEvent) => { evt.preventDefault() setFocus() } const onClick = () => { const category = uri.endsWith('/pack.mcmeta') ? 'pack_mcmeta' : service?.dissectUri(uri)?.category const gen = config.generators.find(g => g.id === category) if (!gen) { throw new Error(`Cannot find generator for uri ${uri}`) } route(cleanUrl(gen.url)) setProjectUri(uri) } return
{Octicon.file} {entry.split('/').at(-1)} {focused &&
{actions?.map(a =>
{ a.onAction(uri); e.stopPropagation(); setFocus(false) }}> {(Octicon as any)[a.icon]} {a.label}
)}
}
}, [service, actions, projectRoot, projectUri]) return
{projects.map(p => changeProject(p.name)} />)} {(project && project.name !== DRAFT_PROJECT.name) && }
{entries === undefined ?
: entries.length === 0 ? {locale('project.no_files')} : path.split('/')} group={FolderEntry} leaf={FileEntry} />}
}