mirror of
https://github.com/misode/misode.github.io.git
synced 2026-04-23 23:27:09 +00:00
Refactor projects to use indexeddb
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import type { ComponentChildren } from 'preact'
|
||||
import { getCurrentUrl } from 'preact-router'
|
||||
import { useEffect, useMemo, useState } from 'preact/hooks'
|
||||
import { useProject } from '../contexts/Project.jsx'
|
||||
import { useSpyglass } from '../contexts/Spyglass.jsx'
|
||||
import { useVersion } from '../contexts/Version.jsx'
|
||||
import { useAsync } from '../hooks/useAsync.js'
|
||||
@@ -19,6 +20,7 @@ type ErrorPanelProps = {
|
||||
export function ErrorPanel({ error, prefix, reportable, onDismiss, body: body_, children }: ErrorPanelProps) {
|
||||
const { version } = useVersion()
|
||||
const { service } = useSpyglass()
|
||||
const { projectUri } = useProject()
|
||||
const [stackVisible, setStackVisible] = useState(false)
|
||||
const [stack, setStack] = useState<string | undefined>(undefined)
|
||||
|
||||
@@ -28,13 +30,12 @@ export function ErrorPanel({ error, prefix, reportable, onDismiss, body: body_,
|
||||
if (!service || !gen) {
|
||||
return undefined
|
||||
}
|
||||
// TODO: read project file if open
|
||||
const uri = service.getUnsavedFileUri(gen)
|
||||
const uri = projectUri ?? service.getUnsavedFileUri(gen)
|
||||
if (!uri) {
|
||||
return undefined
|
||||
}
|
||||
return await service.readFile(uri)
|
||||
}, [service, version, gen])
|
||||
}, [service, version, projectUri, gen])
|
||||
|
||||
useEffect(() => {
|
||||
if (error instanceof Error) {
|
||||
|
||||
@@ -1,46 +1,56 @@
|
||||
import type { DocAndNode } from '@spyglassmc/core'
|
||||
import { useState } from 'preact/hooks'
|
||||
import { Identifier } from 'deepslate'
|
||||
import { useCallback, useState } from 'preact/hooks'
|
||||
import type { Method } from '../../Analytics.js'
|
||||
import { Analytics } from '../../Analytics.js'
|
||||
import { useLocale, useProject } from '../../contexts/index.js'
|
||||
import { safeJsonParse } from '../../Utils.js'
|
||||
import type { ConfigGenerator } from '../../Config.js'
|
||||
import { getProjectRoot, useLocale, useProject, useVersion } from '../../contexts/index.js'
|
||||
import { useSpyglass } from '../../contexts/Spyglass.jsx'
|
||||
import { genPath, message } from '../../Utils.js'
|
||||
import { Btn } from '../Btn.js'
|
||||
import { TextInput } from '../forms/index.js'
|
||||
import { Modal } from '../Modal.js'
|
||||
|
||||
interface Props {
|
||||
docAndNode: DocAndNode,
|
||||
id: string,
|
||||
method: string,
|
||||
gen: ConfigGenerator,
|
||||
method: Method,
|
||||
onCreate: (uri: string) => void,
|
||||
onClose: () => void,
|
||||
}
|
||||
export function FileCreation({ docAndNode, id, method, onClose }: Props) {
|
||||
export function FileCreation({ docAndNode, gen, method, onCreate, onClose }: Props) {
|
||||
const { locale } = useLocale()
|
||||
const { projects, project, updateFile } = useProject()
|
||||
const [fileId, setFileId] = useState(id === 'pack_mcmeta' ? 'pack' : '')
|
||||
const [error, setError] = useState<string>()
|
||||
const { version } = useVersion()
|
||||
const { project } = useProject()
|
||||
const { client } = useSpyglass()
|
||||
|
||||
const [fileId, setFileId] = useState(gen.id === 'pack_mcmeta' ? 'pack' : '')
|
||||
const [error, setError] = useState<string>()
|
||||
|
||||
const changeFileId = (str: string) => {
|
||||
setError(undefined)
|
||||
setFileId(str)
|
||||
}
|
||||
|
||||
const doSave = () => {
|
||||
const doSave = useCallback(() => {
|
||||
if (!fileId.match(/^([a-z0-9_.-]+:)?[a-z0-9/_.-]+$/)) {
|
||||
setError('Invalid resource location')
|
||||
return
|
||||
}
|
||||
Analytics.saveProjectFile(id, projects.length, project.files.length, method as any)
|
||||
const id = Identifier.parse(fileId.includes(':') || project.namespace === undefined ? fileId : `${project.namespace}:${fileId}`)
|
||||
const uri = `${getProjectRoot(project)}data/${id.namespace}/${genPath(gen, version)}/${id.path}.json`
|
||||
Analytics.saveProjectFile(method)
|
||||
const text = docAndNode.doc.getText()
|
||||
const data = safeJsonParse(text)
|
||||
if (data !== undefined) {
|
||||
updateFile(id, undefined, { type: id, id: fileId, data })
|
||||
}
|
||||
onClose()
|
||||
}
|
||||
client.fs.writeFile(uri, text).then(() => {
|
||||
onCreate(uri)
|
||||
}).catch((e) => {
|
||||
setError(message(e))
|
||||
})
|
||||
}, [version, project, client, fileId ])
|
||||
|
||||
return <Modal class="file-modal" onDismiss={onClose}>
|
||||
<p>{locale('project.save_current_file')}</p>
|
||||
<TextInput autofocus={id !== 'pack_mcmeta'} class="btn btn-input" value={fileId} onChange={changeFileId} onEnter={doSave} onCancel={onClose} placeholder={locale('resource_location')} spellcheck={false} readOnly={id === 'pack_mcmeta'} />
|
||||
<TextInput autofocus={gen.id !== 'pack_mcmeta'} class="btn btn-input" value={fileId} onChange={changeFileId} onEnter={doSave} onCancel={onClose} placeholder={locale('resource_location')} spellcheck={false} readOnly={gen.id === 'pack_mcmeta'} />
|
||||
{error !== undefined && <span class="invalid">{error}</span>}
|
||||
<Btn icon="file" label={locale('project.save')} onClick={doSave} />
|
||||
</Modal>
|
||||
|
||||
@@ -1,19 +1,17 @@
|
||||
import { useState } from 'preact/hooks'
|
||||
import { Analytics } from '../../Analytics.js'
|
||||
import { useLocale, useProject } from '../../contexts/index.js'
|
||||
import { useLocale } from '../../contexts/index.js'
|
||||
import { Btn } from '../Btn.js'
|
||||
import { TextInput } from '../forms/index.js'
|
||||
import { Modal } from '../Modal.js'
|
||||
|
||||
interface Props {
|
||||
id: string,
|
||||
name: string,
|
||||
uri: string,
|
||||
onClose: () => void,
|
||||
}
|
||||
export function FileRenaming({ id, name, onClose }: Props) {
|
||||
export function FileRenaming({ uri, onClose }: Props) {
|
||||
const { locale } = useLocale()
|
||||
const { projects, project, updateFile } = useProject()
|
||||
const [fileId, setFileId] = useState(name)
|
||||
const [fileId, setFileId] = useState(uri) // TODO: get original file id
|
||||
const [error, setError] = useState<string>()
|
||||
|
||||
const changeFileId = (str: string) => {
|
||||
@@ -26,8 +24,8 @@ export function FileRenaming({ id, name, onClose }: Props) {
|
||||
setError('Invalid resource location')
|
||||
return
|
||||
}
|
||||
Analytics.renameProjectFile(id, projects.length, project.files.length, 'menu')
|
||||
updateFile(id, name, { type: id, id: fileId })
|
||||
Analytics.renameProjectFile('menu')
|
||||
// TODO: rename file
|
||||
onClose()
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { useEffect, useMemo, useRef, useState } from 'preact/hooks'
|
||||
import { useCallback, useMemo, useState } from 'preact/hooks'
|
||||
import config from '../../Config.js'
|
||||
import type { Project } from '../../contexts/index.js'
|
||||
import { disectFilePath, useLocale, useProject } from '../../contexts/index.js'
|
||||
import { useLocale, useProject } from '../../contexts/index.js'
|
||||
import { useSpyglass } from '../../contexts/Spyglass.jsx'
|
||||
import type { VersionId } from '../../services/index.js'
|
||||
import { DEFAULT_VERSION, parseSource } from '../../services/index.js'
|
||||
import { message, readZip } from '../../Utils.js'
|
||||
import { DEFAULT_VERSION } from '../../services/index.js'
|
||||
import { PROJECTS_URI } from '../../services/Spyglass.js'
|
||||
import { hexId, readZip } from '../../Utils.js'
|
||||
import { Btn, BtnMenu, FileUpload, Octicon, TextInput } from '../index.js'
|
||||
import { Modal } from '../Modal.js'
|
||||
|
||||
@@ -13,7 +14,8 @@ interface Props {
|
||||
}
|
||||
export function ProjectCreation({ onClose }: Props) {
|
||||
const { locale } = useLocale()
|
||||
const { projects, createProject, changeProject, updateProject } = useProject()
|
||||
const { projects, createProject, changeProject } = useProject()
|
||||
const { client } = useSpyglass()
|
||||
|
||||
const [name, setName] = useState('')
|
||||
const [namespace, setNamespace] = useState('')
|
||||
@@ -32,36 +34,17 @@ export function ProjectCreation({ onClose }: Props) {
|
||||
}
|
||||
}
|
||||
|
||||
const projectUpdater = useRef(updateProject)
|
||||
useEffect(() => {
|
||||
projectUpdater.current = updateProject
|
||||
}, [updateProject])
|
||||
|
||||
const onCreate = () => {
|
||||
const onCreate = useCallback(() => {
|
||||
setCreating(true)
|
||||
createProject(name, namespace || undefined, version)
|
||||
const rootUri = `${PROJECTS_URI}${hexId()}/`
|
||||
createProject({ name, namespace, version, storage: { type: 'indexeddb', rootUri } })
|
||||
changeProject(name)
|
||||
if (file) {
|
||||
readZip(file).then(async (entries) => {
|
||||
const project: Partial<Project> = { files: [] }
|
||||
await Promise.all(entries.map(async (entry) => {
|
||||
const file = disectFilePath(entry[0], version)
|
||||
if (file) {
|
||||
try {
|
||||
const text = await parseSource(entry[1], 'json')
|
||||
const data = JSON.parse(text)
|
||||
project.files!.push({ ...file, data })
|
||||
return
|
||||
} catch (e) {
|
||||
console.warn(`Failed parsing ${file.type} ${file.id}: ${message(e)}`)
|
||||
}
|
||||
}
|
||||
if (project.unknownFiles === undefined) {
|
||||
project.unknownFiles = []
|
||||
}
|
||||
project.unknownFiles.push({ path: entry[0], data: entry[1] })
|
||||
await Promise.all(entries.map((entry) => {
|
||||
const path = entry[0].startsWith('/') ? entry[0].slice(1) : entry[0]
|
||||
return client.fs.writeFile(rootUri + path, entry[1])
|
||||
}))
|
||||
projectUpdater.current(project)
|
||||
onClose()
|
||||
}).catch(() => {
|
||||
onClose()
|
||||
@@ -69,7 +52,7 @@ export function ProjectCreation({ onClose }: Props) {
|
||||
} else {
|
||||
onClose()
|
||||
}
|
||||
}
|
||||
}, [createProject, changeProject, client, version, name, namespace, file])
|
||||
|
||||
const invalidName = useMemo(() => {
|
||||
return projects.map(p => p.name.trim().toLowerCase()).includes(name.trim().toLowerCase())
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useCallback } from 'preact/hooks'
|
||||
import { Analytics } from '../../Analytics.js'
|
||||
import { useLocale, useProject } from '../../contexts/index.js'
|
||||
import { Btn } from '../Btn.js'
|
||||
@@ -8,13 +9,13 @@ interface Props {
|
||||
}
|
||||
export function ProjectDeletion({ onClose }: Props) {
|
||||
const { locale } = useLocale()
|
||||
const { projects, project, deleteProject } = useProject()
|
||||
const { project, deleteProject } = useProject()
|
||||
|
||||
const doSave = () => {
|
||||
Analytics.deleteProject(projects.length, project.files.length, 'menu')
|
||||
const doSave = useCallback(() => {
|
||||
Analytics.deleteProject('menu')
|
||||
deleteProject(project.name)
|
||||
onClose()
|
||||
}
|
||||
}, [onClose, deleteProject])
|
||||
|
||||
return <Modal class="file-modal" onDismiss={onClose}>
|
||||
<p>{locale('project.delete_confirm.1', project.name)}</p>
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { useCallback, useMemo, useRef, useState } from 'preact/hooks'
|
||||
import { Analytics } from '../../Analytics.js'
|
||||
import { route } from 'preact-router'
|
||||
import { useCallback, useMemo, useRef } from 'preact/hooks'
|
||||
import config from '../../Config.js'
|
||||
import { disectFilePath, DRAFT_PROJECT, getFilePath, useLocale, useProject, useVersion } from '../../contexts/index.js'
|
||||
import { DRAFT_PROJECT, getProjectRoot, useLocale, useProject } from '../../contexts/index.js'
|
||||
import { useSpyglass } from '../../contexts/Spyglass.jsx'
|
||||
import { useAsync } from '../../hooks/useAsync.js'
|
||||
import { useFocus } from '../../hooks/useFocus.js'
|
||||
import { stringifySource } from '../../services/index.js'
|
||||
import { Store } from '../../Store.js'
|
||||
import { writeZip } from '../../Utils.js'
|
||||
import { cleanUrl, writeZip } from '../../Utils.js'
|
||||
import { Btn } from '../Btn.js'
|
||||
import { BtnMenu } from '../BtnMenu.js'
|
||||
import { Octicon } from '../Octicon.jsx'
|
||||
@@ -13,73 +13,35 @@ import type { TreeViewGroupRenderer, TreeViewLeafRenderer } from '../TreeView.js
|
||||
import { TreeView } from '../TreeView.js'
|
||||
|
||||
interface Props {
|
||||
onError: (message: string) => unknown,
|
||||
onRename: (file: { type: string, id: string }) => unknown,
|
||||
onCreate: () => unknown,
|
||||
onDeleteProject: () => unknown,
|
||||
onError: (message: string) => void,
|
||||
onRename: (uri: string) => void,
|
||||
onCreateProject: () => void,
|
||||
onDeleteProject: () => void,
|
||||
}
|
||||
export function ProjectPanel({ onRename, onCreate, onDeleteProject }: Props) {
|
||||
export function ProjectPanel({ onRename, onCreateProject, onDeleteProject}: Props) {
|
||||
const { locale } = useLocale()
|
||||
const { version } = useVersion()
|
||||
const { projects, project, changeProject, file, openFile, updateFile } = useProject()
|
||||
const { projects, project, projectUri, setProjectUri, changeProject } = useProject()
|
||||
const { client, service } = useSpyglass()
|
||||
|
||||
const [treeViewMode, setTreeViewMode] = useState(Store.getTreeViewMode())
|
||||
const projectRoot = getProjectRoot(project)
|
||||
|
||||
const changeTreeViewMode = useCallback((mode: string) => {
|
||||
Store.setTreeViewMode(mode)
|
||||
Analytics.setTreeViewMode(mode)
|
||||
setTreeViewMode(mode)
|
||||
}, [])
|
||||
|
||||
const disectEntry = useCallback((entry: string) => {
|
||||
if (treeViewMode === 'resources' && entry !== 'pack.mcmeta') {
|
||||
const [type, id] = entry.split('/')
|
||||
return {
|
||||
type: type.replaceAll('\u2215', '/'),
|
||||
id: id.replaceAll('\u2215', '/'),
|
||||
}
|
||||
}
|
||||
return disectFilePath(entry, version)
|
||||
}, [treeViewMode, version])
|
||||
|
||||
const entries = useMemo(() => project.files.flatMap(f => {
|
||||
const path = getFilePath(f, version)
|
||||
if (!path) return []
|
||||
if (f.type === 'pack_mcmeta') return 'pack.mcmeta'
|
||||
if (treeViewMode === 'resources') {
|
||||
return [`${f.type.replaceAll('/', '\u2215')}/${f.id.replaceAll('/', '\u2215')}`]
|
||||
}
|
||||
return [path]
|
||||
}), [treeViewMode, version, ...project.files])
|
||||
|
||||
const selected = useMemo(() => file && getFilePath(file, version), [file, version])
|
||||
|
||||
const selectFile = useCallback((entry: string) => {
|
||||
const file = disectEntry(entry)
|
||||
if (file) {
|
||||
openFile(file.type, file.id)
|
||||
}
|
||||
}, [disectEntry])
|
||||
const { value: entries } = useAsync(async () => {
|
||||
const entries = await client.fs.readdir(projectRoot)
|
||||
return entries.flatMap(e => {
|
||||
return e.name.startsWith(projectRoot) ? [e.name.slice(projectRoot.length)] : []
|
||||
})
|
||||
}, [project])
|
||||
|
||||
const download = useRef<HTMLAnchorElement>(null)
|
||||
|
||||
const onDownload = async () => {
|
||||
if (!download.current) return
|
||||
let hasPack = false
|
||||
const entries = project.files.flatMap(file => {
|
||||
const path = getFilePath(file, version)
|
||||
if (path === undefined) return []
|
||||
if (path === 'pack.mcmeta') hasPack = true
|
||||
return [[path, stringifySource(JSON.stringify(file.data))]] as [string, string][]
|
||||
})
|
||||
project.unknownFiles?.forEach(({ path, data }) => {
|
||||
entries.push([path, data])
|
||||
})
|
||||
if (!hasPack) {
|
||||
const pack_format = config.versions.find(v => v.id === version)!.pack_format
|
||||
entries.push(['pack.mcmeta', stringifySource(JSON.stringify({ pack: { pack_format, description: '' } }, null, 2))])
|
||||
}
|
||||
const url = await writeZip(entries)
|
||||
if (!download.current || entries === undefined) return
|
||||
const zipEntries = await Promise.all(entries.map(async e => {
|
||||
const data = await client.fs.readFile(projectRoot + e)
|
||||
const text = new TextDecoder().decode(data)
|
||||
return [e, text] as [string, string]
|
||||
}))
|
||||
const url = await writeZip(zipEntries)
|
||||
download.current.setAttribute('href', url)
|
||||
download.current.setAttribute('download', `${project.name.replaceAll(' ', '_')}.zip`)
|
||||
download.current.click()
|
||||
@@ -89,25 +51,20 @@ export function ProjectPanel({ onRename, onCreate, onDeleteProject }: Props) {
|
||||
{
|
||||
icon: 'pencil',
|
||||
label: locale('project.rename_file'),
|
||||
onAction: (entry: string) => {
|
||||
const file = disectEntry(entry)
|
||||
if (file) {
|
||||
onRename(file)
|
||||
}
|
||||
onAction: (uri: string) => {
|
||||
onRename(uri)
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: 'trashcan',
|
||||
label: locale('project.delete_file'),
|
||||
onAction: (entry: string) => {
|
||||
const file = disectEntry(entry)
|
||||
if (file) {
|
||||
Analytics.deleteProjectFile(file.type, projects.length, project.files.length, 'menu')
|
||||
updateFile(file.type, file.id, {})
|
||||
}
|
||||
onAction: (uri: string) => {
|
||||
client.fs.unlink(uri).then(() => {
|
||||
setProjectUri(undefined)
|
||||
})
|
||||
},
|
||||
},
|
||||
], [disectEntry, updateFile, onRename])
|
||||
], [client, onRename, projectRoot])
|
||||
|
||||
const FolderEntry: TreeViewGroupRenderer = useCallback(({ name, open, onClick }) => {
|
||||
return <div class="entry" onClick={onClick} >
|
||||
@@ -118,23 +75,34 @@ export function ProjectPanel({ onRename, onCreate, onDeleteProject }: Props) {
|
||||
|
||||
const FileEntry: TreeViewLeafRenderer<string> = useCallback(({ entry }) => {
|
||||
const [focused, setFocus] = useFocus()
|
||||
const uri = projectRoot + entry
|
||||
const onContextMenu = (evt: MouseEvent) => {
|
||||
evt.preventDefault()
|
||||
setFocus()
|
||||
}
|
||||
const file = disectEntry(entry)
|
||||
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 <div class={`entry ${file && getFilePath(file, version) === selected ? 'active' : ''} ${focused ? 'focused' : ''}`} onClick={() => selectFile(entry)} onContextMenu={onContextMenu} >
|
||||
return <div class={`entry ${uri === projectUri ? 'active' : ''} ${focused ? 'focused' : ''}`} onClick={onClick} onContextMenu={onContextMenu} >
|
||||
{Octicon.file}
|
||||
<span>{entry.split('/').at(-1)}</span>
|
||||
{focused && <div class="entry-menu">
|
||||
{actions?.map(a => <div class="action [&>svg]:inline" onClick={e => { a.onAction(entry); e.stopPropagation(); setFocus(false) }}>
|
||||
{actions?.map(a => <div class="action [&>svg]:inline" onClick={e => { a.onAction(uri); e.stopPropagation(); setFocus(false) }}>
|
||||
{(Octicon as any)[a.icon]}
|
||||
<span>{a.label}</span>
|
||||
</div>)}
|
||||
</div>}
|
||||
</div>
|
||||
}, [actions, disectEntry])
|
||||
}, [service, actions, projectRoot, projectUri])
|
||||
|
||||
return <>
|
||||
<div class="project-controls">
|
||||
@@ -143,15 +111,16 @@ export function ProjectPanel({ onRename, onCreate, onDeleteProject }: Props) {
|
||||
</BtnMenu>
|
||||
<BtnMenu icon="kebab_horizontal" >
|
||||
<Btn icon="file_zip" label={locale('project.download')} onClick={onDownload} />
|
||||
<Btn icon="plus_circle" label={locale('project.new')} onClick={onCreate} />
|
||||
<Btn icon={treeViewMode === 'resources' ? 'three_bars' : 'rows'} label={locale(treeViewMode === 'resources' ? 'project.show_file_paths' : 'project.show_resources')} onClick={() => changeTreeViewMode(treeViewMode === 'resources' ? 'files' : 'resources')} />
|
||||
<Btn icon="plus_circle" label={locale('project.new')} onClick={onCreateProject} />
|
||||
{project.name !== DRAFT_PROJECT.name && <Btn icon="trashcan" label={locale('project.delete')} onClick={onDeleteProject} />}
|
||||
</BtnMenu>
|
||||
</div>
|
||||
<div class="file-view">
|
||||
{entries.length === 0
|
||||
? <span>{locale('project.no_files')}</span>
|
||||
: <TreeView entries={entries} split={path => path.split('/')} group={FolderEntry} leaf={FileEntry} />}
|
||||
{entries === undefined
|
||||
? <></>
|
||||
: entries.length === 0
|
||||
? <span>{locale('project.no_files')}</span>
|
||||
: <TreeView entries={entries} split={path => path.split('/')} group={FolderEntry} leaf={FileEntry} />}
|
||||
</div>
|
||||
<a ref={download} style="display: none;"></a>
|
||||
</>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { route } from 'preact-router'
|
||||
import { useCallback, useEffect, useErrorBoundary, useMemo, useRef, useState } from 'preact/hooks'
|
||||
import type { Method } from '../../Analytics.js'
|
||||
import { Analytics } from '../../Analytics.js'
|
||||
import type { ConfigGenerator } from '../../Config.js'
|
||||
import config from '../../Config.js'
|
||||
@@ -8,8 +9,9 @@ import { useSpyglass, watchSpyglassUri } from '../../contexts/Spyglass.jsx'
|
||||
import { AsyncCancel, useActiveTimeout, useAsync, useSearchParam } from '../../hooks/index.js'
|
||||
import type { VersionId } from '../../services/index.js'
|
||||
import { checkVersion, fetchDependencyMcdoc, fetchPreset, fetchRegistries, getSnippet, shareSnippet } from '../../services/index.js'
|
||||
import { DEPENDENCY_URI } from '../../services/Spyglass.js'
|
||||
import { Store } from '../../Store.js'
|
||||
import { cleanUrl, genPath, safeJsonParse } from '../../Utils.js'
|
||||
import { cleanUrl, genPath } from '../../Utils.js'
|
||||
import { Ad, Btn, BtnMenu, ErrorPanel, FileCreation, FileRenaming, Footer, HasPreview, Octicon, PreviewPanel, ProjectCreation, ProjectDeletion, ProjectPanel, SearchList, SourcePanel, TextInput, Tree, VersionSwitcher } from '../index.js'
|
||||
import { getRootDefault } from './McdocHelpers.js'
|
||||
|
||||
@@ -23,7 +25,7 @@ export function SchemaGenerator({ gen, allowedVersions }: Props) {
|
||||
const { locale } = useLocale()
|
||||
const { version, changeVersion, changeTargetVersion } = useVersion()
|
||||
const { service } = useSpyglass()
|
||||
const { projects, project, file, updateProject, updateFile, closeFile } = useProject()
|
||||
const { project, projectUri, setProjectUri, updateProject } = useProject()
|
||||
const [error, setError] = useState<Error | string | null>(null)
|
||||
const [errorBoundary, errorRetry] = useErrorBoundary()
|
||||
if (errorBoundary) {
|
||||
@@ -34,9 +36,21 @@ export function SchemaGenerator({ gen, allowedVersions }: Props) {
|
||||
useEffect(() => Store.visitGenerator(gen.id), [gen.id])
|
||||
|
||||
const uri = useMemo(() => {
|
||||
// TODO: return different uri when project file is open
|
||||
return service?.getUnsavedFileUri(gen)
|
||||
}, [service, version, gen])
|
||||
if (!service) {
|
||||
return undefined
|
||||
}
|
||||
if (projectUri) {
|
||||
const category = projectUri.endsWith('/pack.mcmeta')
|
||||
? 'pack_mcmeta'
|
||||
: service.dissectUri(projectUri)?.category
|
||||
if (category === gen.id) {
|
||||
return projectUri
|
||||
} else {
|
||||
setProjectUri(undefined)
|
||||
}
|
||||
}
|
||||
return service.getUnsavedFileUri(gen)
|
||||
}, [service, version, gen, projectUri])
|
||||
|
||||
const [currentPreset, setCurrentPreset] = useSearchParam('preset')
|
||||
const [sharedSnippetId, setSharedSnippetId] = useSearchParam(SHARE_KEY)
|
||||
@@ -75,20 +89,13 @@ export function SchemaGenerator({ gen, allowedVersions }: Props) {
|
||||
Analytics.openSnippet(gen.id, sharedSnippetId, version)
|
||||
ignoreChange.current = true
|
||||
text = snippet.text
|
||||
} else if (file) {
|
||||
if (project.version && project.version !== version) {
|
||||
changeVersion(project.version, false)
|
||||
return AsyncCancel
|
||||
}
|
||||
ignoreChange.current = true
|
||||
text = JSON.stringify(file.data, null, 2)
|
||||
}
|
||||
if (!service || !uri) {
|
||||
return AsyncCancel
|
||||
}
|
||||
if (gen.dependency) {
|
||||
const dependency = await fetchDependencyMcdoc(gen.dependency)
|
||||
const dependencyUri = `file:///project/mcdoc/${gen.dependency}.mcdoc`
|
||||
const dependencyUri = `${DEPENDENCY_URI}${gen.dependency}.mcdoc`
|
||||
await service.writeFile(dependencyUri, dependency)
|
||||
}
|
||||
if (text !== undefined) {
|
||||
@@ -104,24 +111,18 @@ export function SchemaGenerator({ gen, allowedVersions }: Props) {
|
||||
const docAndNode = await service.openFile(uri)
|
||||
Analytics.setGenerator(gen.id)
|
||||
return docAndNode
|
||||
}, [gen.id, version, sharedSnippetId, currentPreset, project.name, file?.id, service])
|
||||
}, [gen.id, version, sharedSnippetId, currentPreset, project.name, service, uri])
|
||||
|
||||
const { doc } = docAndNode ?? {}
|
||||
|
||||
watchSpyglassUri(uri, ({ doc }) => {
|
||||
watchSpyglassUri(uri, () => {
|
||||
if (!ignoreChange.current) {
|
||||
setCurrentPreset(undefined, true)
|
||||
setSharedSnippetId(undefined, true)
|
||||
}
|
||||
if (file) {
|
||||
const data = safeJsonParse(doc.getText())
|
||||
if (data !== undefined) {
|
||||
updateFile(gen.id, file.id, { id: file.id, data })
|
||||
}
|
||||
}
|
||||
ignoreChange.current = false
|
||||
setError(null)
|
||||
}, [updateFile])
|
||||
}, [])
|
||||
|
||||
const reset = async () => {
|
||||
if (!service || !uri) {
|
||||
@@ -304,9 +305,9 @@ export function SchemaGenerator({ gen, allowedVersions }: Props) {
|
||||
const [projectShown, setProjectShown] = useState(Store.getProjectPanelOpen() ?? window.innerWidth > 1000)
|
||||
const toggleProjectShown = useCallback(() => {
|
||||
if (projectShown) {
|
||||
Analytics.hideProject(gen.id, projects.length, project.files.length, 'menu')
|
||||
Analytics.hideProject('menu')
|
||||
} else {
|
||||
Analytics.showProject(gen.id, projects.length, project.files.length, 'menu')
|
||||
Analytics.showProject('menu')
|
||||
}
|
||||
Store.setProjectPanelOpen(!projectShown)
|
||||
setProjectShown(!projectShown)
|
||||
@@ -314,13 +315,13 @@ export function SchemaGenerator({ gen, allowedVersions }: Props) {
|
||||
|
||||
const [projectCreating, setProjectCreating] = useState(false)
|
||||
const [projectDeleting, setprojectDeleting] = useState(false)
|
||||
const [fileSaving, setFileSaving] = useState<string | undefined>(undefined)
|
||||
const [fileRenaming, setFileRenaming] = useState<{ type: string, id: string } | undefined>(undefined)
|
||||
const [fileSaving, setFileSaving] = useState<Method | undefined>(undefined)
|
||||
const [fileRenaming, setFileRenaming] = useState<string | undefined>(undefined)
|
||||
|
||||
const onNewFile = useCallback(() => {
|
||||
closeFile()
|
||||
setProjectUri(undefined)
|
||||
// TODO: create new file with default contents
|
||||
}, [closeFile])
|
||||
}, [setProjectUri])
|
||||
|
||||
return <>
|
||||
<main class={`${previewShown ? 'has-preview' : ''} ${projectShown ? 'has-project' : ''}`}>
|
||||
@@ -379,11 +380,11 @@ export function SchemaGenerator({ gen, allowedVersions }: Props) {
|
||||
</div>
|
||||
</div>
|
||||
<div class={`popup-project${projectShown ? ' shown' : ''}`}>
|
||||
<ProjectPanel onError={setError} onDeleteProject={() => setprojectDeleting(true)} onRename={setFileRenaming} onCreate={() => setProjectCreating(true)} />
|
||||
<ProjectPanel onError={setError} onRename={setFileRenaming} onDeleteProject={() => setprojectDeleting(true)} onCreateProject={() => setProjectCreating(true)} />
|
||||
</div>
|
||||
{projectCreating && <ProjectCreation onClose={() => setProjectCreating(false)} />}
|
||||
{projectDeleting && <ProjectDeletion onClose={() => setprojectDeleting(false)} />}
|
||||
{docAndNode && fileSaving && <FileCreation id={gen.id} docAndNode={docAndNode} method={fileSaving} onClose={() => setFileSaving(undefined)} />}
|
||||
{fileRenaming && <FileRenaming id={fileRenaming.type } name={fileRenaming.id} onClose={() => setFileRenaming(undefined)} />}
|
||||
{docAndNode && fileSaving && <FileCreation gen={gen} docAndNode={docAndNode} method={fileSaving} onCreate={(uri) => {setFileSaving(undefined); setProjectUri(uri)}} onClose={() => setFileSaving(undefined)} />}
|
||||
{fileRenaming && <FileRenaming uri={fileRenaming} onClose={() => setFileRenaming(undefined)} />}
|
||||
</>
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import type { DocAndNode, Range } from '@spyglassmc/core'
|
||||
import { dissectUri } from '@spyglassmc/java-edition/lib/binder/index.js'
|
||||
import type { JsonNode } from '@spyglassmc/json'
|
||||
import { JsonFileNode } from '@spyglassmc/json'
|
||||
import { useCallback, useErrorBoundary, useMemo } from 'preact/hooks'
|
||||
import { disectFilePath, useLocale, useVersion } from '../../contexts/index.js'
|
||||
import { useLocale } from '../../contexts/index.js'
|
||||
import { useDocAndNode, useSpyglass } from '../../contexts/Spyglass.jsx'
|
||||
import { getRootType, simplifyType } from './McdocHelpers.js'
|
||||
import type { McdocContext } from './McdocRenderer.jsx'
|
||||
@@ -14,7 +15,6 @@ type TreePanelProps = {
|
||||
}
|
||||
export function Tree({ docAndNode: original, onError }: TreePanelProps) {
|
||||
const { lang } = useLocale()
|
||||
const { version } = useVersion()
|
||||
const { service } = useSpyglass()
|
||||
|
||||
if (lang === 'none') return <></>
|
||||
@@ -61,19 +61,23 @@ export function Tree({ docAndNode: original, onError }: TreePanelProps) {
|
||||
}, [docAndNode, service])
|
||||
|
||||
const resourceType = useMemo(() => {
|
||||
const path = original.doc.uri
|
||||
.replace(/^file:\/\/\/project\//, '')
|
||||
.replace(/\.json$/, '')
|
||||
const res = disectFilePath(path, version)
|
||||
return res?.type
|
||||
}, [original, version])
|
||||
if (original.doc.uri.endsWith('/pack.mcmeta')) {
|
||||
return 'pack_mcmeta'
|
||||
}
|
||||
if (ctx === undefined) {
|
||||
return undefined
|
||||
}
|
||||
const res = dissectUri(original.doc.uri, ctx)
|
||||
return res?.category
|
||||
}, [original, ctx])
|
||||
|
||||
const mcdocType = useMemo(() => {
|
||||
if (!ctx || !resourceType) {
|
||||
return undefined
|
||||
}
|
||||
const rootType = getRootType(resourceType)
|
||||
return simplifyType(rootType, ctx)
|
||||
const type = simplifyType(rootType, ctx)
|
||||
return type
|
||||
}, [resourceType, ctx])
|
||||
|
||||
return <div class="tree node-root" data-cy="tree" data-category={getCategory(resourceType)}>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { clampedMap } from 'deepslate'
|
||||
import { mat3 } from 'gl-matrix'
|
||||
import { useCallback, useRef, useState } from 'preact/hooks'
|
||||
import { getProjectData, useLocale, useProject, useStore, useVersion } from '../../contexts/index.js'
|
||||
import { useLocale, useProject, useStore, useVersion } from '../../contexts/index.js'
|
||||
import { useAsync } from '../../hooks/index.js'
|
||||
import { checkVersion } from '../../services/Versions.js'
|
||||
import { Store } from '../../Store.js'
|
||||
@@ -37,7 +37,7 @@ export const BiomeSourcePreview = ({ docAndNode, shown }: PreviewProps) => {
|
||||
const hasRandomness = type === 'multi_noise' || type === 'the_end'
|
||||
|
||||
const { value } = useAsync(async function loadBiomeSource() {
|
||||
await DEEPSLATE.loadVersion(version, getProjectData(project))
|
||||
await DEEPSLATE.loadVersion(version, {}) // TODO: get project data
|
||||
await DEEPSLATE.loadChunkGenerator(data?.generator?.settings, data?.generator?.biome_source, seed)
|
||||
return {
|
||||
biomeSource: { loaded: true },
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { Voxel } from 'deepslate/render'
|
||||
import { clampedMap, VoxelRenderer } from 'deepslate/render'
|
||||
import type { mat3, mat4 } from 'gl-matrix'
|
||||
import { useCallback, useEffect, useRef, useState } from 'preact/hooks'
|
||||
import { getProjectData, useLocale, useProject, useVersion } from '../../contexts/index.js'
|
||||
import { useLocale, useProject, useVersion } from '../../contexts/index.js'
|
||||
import { useAsync } from '../../hooks/useAsync.js'
|
||||
import { useLocalStorage } from '../../hooks/useLocalStorage.js'
|
||||
import { Store } from '../../Store.js'
|
||||
@@ -32,7 +32,7 @@ export const DensityFunctionPreview = ({ docAndNode, shown }: PreviewProps) => {
|
||||
const text = docAndNode.doc.getText()
|
||||
|
||||
const { value: df } = useAsync(async () => {
|
||||
await DEEPSLATE.loadVersion(version, getProjectData(project))
|
||||
await DEEPSLATE.loadVersion(version, {}) // TODO: get project data
|
||||
const df = DEEPSLATE.loadDensityFunction(safeJsonParse(text) ?? {}, minY, height, seed)
|
||||
return df
|
||||
}, [version, project, minY, height, seed, text])
|
||||
|
||||
@@ -2,7 +2,7 @@ import { clampedMap } from 'deepslate'
|
||||
import type { mat3 } from 'gl-matrix'
|
||||
import { vec2 } from 'gl-matrix'
|
||||
import { useCallback, useRef, useState } from 'preact/hooks'
|
||||
import { getProjectData, useLocale, useProject, useVersion } from '../../contexts/index.js'
|
||||
import { useLocale, useProject, useVersion } from '../../contexts/index.js'
|
||||
import { useAsync } from '../../hooks/index.js'
|
||||
import { fetchRegistries } from '../../services/index.js'
|
||||
import { Store } from '../../Store.js'
|
||||
@@ -27,7 +27,7 @@ export const NoiseSettingsPreview = ({ docAndNode, shown }: PreviewProps) => {
|
||||
|
||||
const { value, error } = useAsync(async () => {
|
||||
const data = safeJsonParse(text) ?? {}
|
||||
await DEEPSLATE.loadVersion(version, getProjectData(project))
|
||||
await DEEPSLATE.loadVersion(version, {}) // TODO: get project data
|
||||
const biomeSource = { type: 'fixed', biome }
|
||||
await DEEPSLATE.loadChunkGenerator(data, biomeSource, seed)
|
||||
const noiseSettings = DEEPSLATE.getNoiseSettings()
|
||||
|
||||
Reference in New Issue
Block a user