mirror of
https://github.com/misode/misode.github.io.git
synced 2026-04-23 07:10:41 +00:00
Refactor projects to use indexeddb
This commit is contained in:
130
package-lock.json
generated
130
package-lock.json
generated
@@ -616,136 +616,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-1.4.2.tgz",
|
||||
"integrity": "sha512-VMOxsWh/QDwrxPsgkSQnuZ+8mfNy1OTjzzUdLBvvZtpahwPTHTeVZ51RZRqO4xfKVrR+btIPA8D01IL3xeG66w=="
|
||||
},
|
||||
"node_modules/@mcschema/core": {
|
||||
"version": "0.13.0",
|
||||
"resolved": "https://registry.npmjs.org/@mcschema/core/-/core-0.13.0.tgz",
|
||||
"integrity": "sha512-nJRDvdEI2Z7Yw7eWKcbkuLFKtPIyeCc1d04E7EeUOpwxfHffgL5a0VBD2PX692z3igfTHGNXgPkzrleV91Q/ww=="
|
||||
},
|
||||
"node_modules/@mcschema/java-1.15": {
|
||||
"version": "0.2.13",
|
||||
"resolved": "https://registry.npmjs.org/@mcschema/java-1.15/-/java-1.15-0.2.13.tgz",
|
||||
"integrity": "sha512-f3NanEsd48svxZfqquSylXrHZDd5vjvi8D6r15blrCi2J2C42032Ir0dMPD38hLxu9ey0eIrgucDApn/GXO1uA==",
|
||||
"dependencies": {
|
||||
"@mcschema/core": "^0.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@mcschema/java-1.16": {
|
||||
"version": "0.6.20",
|
||||
"resolved": "https://registry.npmjs.org/@mcschema/java-1.16/-/java-1.16-0.6.20.tgz",
|
||||
"integrity": "sha512-pJihtCYPloVAk6Tq2JVvfk666rPLaNmXkHhD6/a1kk/AUaP2wk/3YuYdXyRqz7/lmxkBpJK/BvhAO4ZtsQfNtg==",
|
||||
"dependencies": {
|
||||
"@mcschema/core": "^0.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@mcschema/java-1.17": {
|
||||
"version": "0.2.40",
|
||||
"resolved": "https://registry.npmjs.org/@mcschema/java-1.17/-/java-1.17-0.2.40.tgz",
|
||||
"integrity": "sha512-aFlH9APa4JEMsz0y3BKjYGILXn6QQcQDOCHA+qgdXDjc0xsrHffO1l/TVVDi5GQCCqEgGA15guqfLVV+7Vtb2A==",
|
||||
"dependencies": {
|
||||
"@mcschema/core": "^0.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@mcschema/java-1.18": {
|
||||
"version": "0.3.16",
|
||||
"resolved": "https://registry.npmjs.org/@mcschema/java-1.18/-/java-1.18-0.3.16.tgz",
|
||||
"integrity": "sha512-crxXl0OW6BYlD+Cjaa8cJEsoehnBa6whrE6JBG37nZ1nQBl0e9vun4fbaZi0buzFjeJGZ0st7Lm0LsFJRe7FEw==",
|
||||
"dependencies": {
|
||||
"@mcschema/core": "^0.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@mcschema/java-1.18.2": {
|
||||
"version": "0.1.26",
|
||||
"resolved": "https://registry.npmjs.org/@mcschema/java-1.18.2/-/java-1.18.2-0.1.26.tgz",
|
||||
"integrity": "sha512-GN6gCyEyTTJTiv0vn4cTVMYVgG2BJpDXJh2KPrg5cT/7gCpHUrt4PJAyzEaYLkDZoW17p2gWuY12gIM/yfuiDA==",
|
||||
"dependencies": {
|
||||
"@mcschema/core": "^0.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@mcschema/java-1.19": {
|
||||
"version": "0.1.54",
|
||||
"resolved": "https://registry.npmjs.org/@mcschema/java-1.19/-/java-1.19-0.1.54.tgz",
|
||||
"integrity": "sha512-eh8kTBs3EAGlWzTbmGzoIRtePd2dUkttooGsmXmjadvwf/yT19Ew+P+U4Y6LLFLnNoWJxruCVhznS1KxzoT0wA==",
|
||||
"dependencies": {
|
||||
"@mcschema/core": "^0.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@mcschema/java-1.19.3": {
|
||||
"version": "0.0.17",
|
||||
"resolved": "https://registry.npmjs.org/@mcschema/java-1.19.3/-/java-1.19.3-0.0.17.tgz",
|
||||
"integrity": "sha512-8V3UpdmVPb2JZJ5A3Mi6J5Lf5dicmY9w11dq8VQm9jSEPwY7IXSnE4l9J3AmFkIgAhiaNTQ+/lbmliR7LNUAlQ==",
|
||||
"dependencies": {
|
||||
"@mcschema/core": "^0.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@mcschema/java-1.19.4": {
|
||||
"version": "0.1.21",
|
||||
"resolved": "https://registry.npmjs.org/@mcschema/java-1.19.4/-/java-1.19.4-0.1.21.tgz",
|
||||
"integrity": "sha512-QZhe7L6t7/Uf6vK0pXWSJYoGA0D+VAv10wrMrwG6jTHoCI1iPkFhet9JqUnOwOv0Ql+jwh7zDNQjTHJIkHwfMQ==",
|
||||
"dependencies": {
|
||||
"@mcschema/core": "^0.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@mcschema/java-1.20": {
|
||||
"version": "0.0.24",
|
||||
"resolved": "https://registry.npmjs.org/@mcschema/java-1.20/-/java-1.20-0.0.24.tgz",
|
||||
"integrity": "sha512-Bcm4n7YTXQ/6mGmj7l9KcRXl8ta9nokNZDyrgEeexsrE9jzj1ef6TsNra2bI9kCka9l4s4XzZ1VDuM/Ag/Ud0g==",
|
||||
"dependencies": {
|
||||
"@mcschema/core": "^0.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@mcschema/java-1.20.2": {
|
||||
"version": "0.0.15",
|
||||
"resolved": "https://registry.npmjs.org/@mcschema/java-1.20.2/-/java-1.20.2-0.0.15.tgz",
|
||||
"integrity": "sha512-crGOWodFnWT5XGjp2b8kZeXKsHpdQHDaES09NYf/VNWUIdGNYD7hXlIVvQEsqbZt1ABuisIKWvNYZJwH7Fhdew==",
|
||||
"dependencies": {
|
||||
"@mcschema/core": "^0.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@mcschema/java-1.20.3": {
|
||||
"version": "0.0.16",
|
||||
"resolved": "https://registry.npmjs.org/@mcschema/java-1.20.3/-/java-1.20.3-0.0.16.tgz",
|
||||
"integrity": "sha512-XNX02G7RHB8u/ibwU0GSo+lsz/5rcduHnWKk/BJcGH6Q2NswLKJDiyt8Ow5KrcmtCBxprCLjg5pJaj/Ql6aKoQ==",
|
||||
"dependencies": {
|
||||
"@mcschema/core": "^0.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@mcschema/java-1.20.5": {
|
||||
"version": "0.0.42",
|
||||
"resolved": "https://registry.npmjs.org/@mcschema/java-1.20.5/-/java-1.20.5-0.0.42.tgz",
|
||||
"integrity": "sha512-LgVeCvHQPMQUMPPiJ5tkm8RG7EKowvw6eHxu8RPakeX/Del5OYkQI252hkP2RC3AA46NHE+iKqtsk8A+lvCuow==",
|
||||
"dependencies": {
|
||||
"@mcschema/core": "^0.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@mcschema/java-1.21": {
|
||||
"version": "0.0.27",
|
||||
"resolved": "https://registry.npmjs.org/@mcschema/java-1.21/-/java-1.21-0.0.27.tgz",
|
||||
"integrity": "sha512-X2o8VJouEv5ZixLrpngyq0srwYB8Bp0lmIBKRJvJ+7/5RB6Mrf9I/Z4y7BkPjpKjw8TME1r5Tw/g7sjrYXvEUQ==",
|
||||
"dependencies": {
|
||||
"@mcschema/core": "^0.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@mcschema/java-1.21.2": {
|
||||
"version": "0.0.16",
|
||||
"resolved": "https://registry.npmjs.org/@mcschema/java-1.21.2/-/java-1.21.2-0.0.16.tgz",
|
||||
"integrity": "sha512-LExJ3pvpkVE1wfKr//KemkX6PFrMS4MxrOoyxEi7NvWzxNVeXDCsLsW1MkkoJ2QZ3hAjsuT50g1kivEBbd2jEQ==",
|
||||
"dependencies": {
|
||||
"@mcschema/core": "^0.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@mcschema/java-1.21.4": {
|
||||
"version": "0.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@mcschema/java-1.21.4/-/java-1.21.4-0.0.3.tgz",
|
||||
"integrity": "sha512-1KPHjCDkyMk0DtyrpFiTmpXMG/9XCBHCjMkE1ZvdBMYDLqbHFNPJwGnwDljpNgNJCOfKkfeVpmTBZv0ur9jpIg==",
|
||||
"dependencies": {
|
||||
"@mcschema/core": "^0.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@mcschema/locales": {
|
||||
"version": "0.1.104",
|
||||
"resolved": "https://registry.npmjs.org/@mcschema/locales/-/locales-0.1.104.tgz",
|
||||
"integrity": "sha512-fZ9zzb4OvMnxRFtVYpPuFDugd2LGYMfGRsYzl+RG6wFZAUuB6pe+igNmK5o8VfX1ldO2W5FWkddgh/MjtemFOA=="
|
||||
},
|
||||
"node_modules/@nodelib/fs.scandir": {
|
||||
"version": "2.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { ColormapType } from './components/previews/Colormap.js'
|
||||
import type { VersionId } from './services/index.js'
|
||||
|
||||
type Method = 'menu' | 'hotkey'
|
||||
export type Method = 'menu' | 'hotkey'
|
||||
|
||||
export namespace Analytics {
|
||||
|
||||
@@ -226,61 +226,44 @@ export namespace Analytics {
|
||||
})
|
||||
}
|
||||
|
||||
export function showProject(file_type: string, projects_count: number, project_size: number, method: Method) {
|
||||
export function showProject(method: Method) {
|
||||
event(ID_GENERATOR, 'show-project', legacyMethod(method))
|
||||
gtag('event', 'show_project', {
|
||||
file_type,
|
||||
projects_count,
|
||||
project_size,
|
||||
method,
|
||||
})
|
||||
}
|
||||
|
||||
export function hideProject(file_type: string, projects_count: number, project_size: number, method: Method) {
|
||||
export function hideProject(method: Method) {
|
||||
event(ID_GENERATOR, 'hide-project', legacyMethod(method))
|
||||
gtag('event', 'hide_project', {
|
||||
file_type,
|
||||
projects_count,
|
||||
project_size,
|
||||
method,
|
||||
})
|
||||
}
|
||||
|
||||
export function saveProjectFile(file_type: string, projects_count: number, project_size: number, method: Method) {
|
||||
export function saveProjectFile(method: Method) {
|
||||
event(ID_GENERATOR, 'save-project-file', legacyMethod(method))
|
||||
gtag('event', 'save_project_file', {
|
||||
file_type,
|
||||
projects_count,
|
||||
project_size,
|
||||
method,
|
||||
})
|
||||
}
|
||||
|
||||
export function deleteProjectFile(file_type: string, projects_count: number, project_size: number, method: Method) {
|
||||
export function deleteProjectFile(method: Method) {
|
||||
event(ID_GENERATOR, 'delete-project-file', legacyMethod(method))
|
||||
gtag('event', 'delete_project_file', {
|
||||
file_type,
|
||||
projects_count,
|
||||
project_size,
|
||||
method,
|
||||
})
|
||||
}
|
||||
|
||||
export function renameProjectFile(file_type: string, projects_count: number, project_size: number, method: Method) {
|
||||
export function renameProjectFile(method: Method) {
|
||||
event(ID_GENERATOR, 'rename-project-file', legacyMethod(method))
|
||||
gtag('event', 'rename_project_file', {
|
||||
file_type,
|
||||
projects_count,
|
||||
project_size,
|
||||
method,
|
||||
})
|
||||
}
|
||||
|
||||
export function deleteProject(projects_count: number, project_size: number, method: Method) {
|
||||
export function deleteProject(method: Method) {
|
||||
event(ID_GENERATOR, 'delete-project', legacyMethod(method))
|
||||
gtag('event', 'delete_project', {
|
||||
projects_count,
|
||||
project_size,
|
||||
method,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { ColormapType } from './components/previews/Colormap.js'
|
||||
import { ColormapTypes } from './components/previews/Colormap.js'
|
||||
import type { Project } from './contexts/index.js'
|
||||
import type { ProjectMeta } from './contexts/index.js'
|
||||
import { DRAFT_PROJECT } from './contexts/index.js'
|
||||
import type { VersionId } from './services/index.js'
|
||||
import { DEFAULT_VERSION, VersionIds } from './services/index.js'
|
||||
@@ -63,7 +63,7 @@ export namespace Store {
|
||||
return localStorage.getItem(ID_SOUNDS_VERSION) ?? 'latest'
|
||||
}
|
||||
|
||||
export function getProjects(): Project[] {
|
||||
export function getProjects(): ProjectMeta[] {
|
||||
const projects = localStorage.getItem(ID_PROJECTS)
|
||||
if (projects) {
|
||||
return safeJsonParse(projects) ?? []
|
||||
@@ -132,7 +132,7 @@ export namespace Store {
|
||||
if (version) localStorage.setItem(ID_SOUNDS_VERSION, version)
|
||||
}
|
||||
|
||||
export function setProjects(projects: Project[] | undefined) {
|
||||
export function setProjects(projects: ProjectMeta[] | undefined) {
|
||||
if (projects) localStorage.setItem(ID_PROJECTS, JSON.stringify(projects))
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -1,97 +1,96 @@
|
||||
import type { ComponentChildren } from 'preact'
|
||||
import { createContext } from 'preact'
|
||||
import { route } from 'preact-router'
|
||||
import { useCallback, useContext, useMemo, useState } from 'preact/hooks'
|
||||
import config from '../Config.js'
|
||||
import { useCallback, useContext, useEffect, useMemo, useState } from 'preact/hooks'
|
||||
import type { VersionId } from '../services/index.js'
|
||||
import { ROOT_URI } from '../services/Spyglass.js'
|
||||
import { Store } from '../Store.js'
|
||||
import { cleanUrl, genPath } from '../Utils.js'
|
||||
import { useVersion } from './Version.jsx'
|
||||
|
||||
export type Project = {
|
||||
export type ProjectMeta = {
|
||||
name: string,
|
||||
namespace?: string,
|
||||
version?: VersionId,
|
||||
files: ProjectFile[],
|
||||
storage?: ProjectStorage,
|
||||
/** @deprecated */
|
||||
files?: ProjectFile[],
|
||||
/** @deprecated */
|
||||
unknownFiles?: UnknownFile[],
|
||||
}
|
||||
export const DRAFT_PROJECT: Project = {
|
||||
name: 'Drafts',
|
||||
namespace: 'draft',
|
||||
files: [],
|
||||
|
||||
export type ProjectStorage = {
|
||||
type: 'indexeddb',
|
||||
rootUri: string,
|
||||
}
|
||||
|
||||
export type ProjectFile = {
|
||||
type ProjectFile = {
|
||||
type: string,
|
||||
id: string,
|
||||
data: any,
|
||||
}
|
||||
|
||||
export type UnknownFile = {
|
||||
type UnknownFile = {
|
||||
path: string,
|
||||
data: string,
|
||||
}
|
||||
|
||||
export const FilePatterns = [
|
||||
'immersive_weathering/[a-z_]+',
|
||||
'neoforge/[a-z_]+',
|
||||
'ohthetreesyoullgrow/[a-z_]+',
|
||||
'worldgen/[a-z_]+',
|
||||
'tags/worldgen/[a-z_]+',
|
||||
'tags/[a-z_]+',
|
||||
'[a-z_]+',
|
||||
].map(e => RegExp(`^data/([a-z0-9._-]+)/(${e})/([a-z0-9/._-]+)$`))
|
||||
export const DRAFT_PROJECT: ProjectMeta = {
|
||||
name: 'Drafts',
|
||||
namespace: 'draft',
|
||||
storage: {
|
||||
type: 'indexeddb',
|
||||
rootUri: `${ROOT_URI}drafts/`,
|
||||
},
|
||||
}
|
||||
|
||||
interface ProjectContext {
|
||||
projects: Project[],
|
||||
project: Project,
|
||||
file?: ProjectFile,
|
||||
createProject: (name: string, namespace?: string, version?: VersionId) => unknown,
|
||||
deleteProject: (name: string) => unknown,
|
||||
changeProject: (name: string) => unknown,
|
||||
updateProject: (project: Partial<Project>) => unknown,
|
||||
updateFile: (type: string, id: string | undefined, file: Partial<ProjectFile>) => boolean,
|
||||
openFile: (type: string, id: string) => unknown,
|
||||
closeFile: () => unknown,
|
||||
projects: ProjectMeta[],
|
||||
project: ProjectMeta,
|
||||
projectUri: string | undefined,
|
||||
setProjectUri: (uri: string | undefined) => void,
|
||||
createProject: (project: ProjectMeta) => void,
|
||||
deleteProject: (name: string) => void,
|
||||
changeProject: (name: string) => void,
|
||||
updateProject: (project: Partial<ProjectMeta>) => void,
|
||||
}
|
||||
const Project = createContext<ProjectContext>({
|
||||
projects: [DRAFT_PROJECT],
|
||||
project: DRAFT_PROJECT,
|
||||
createProject: () => {},
|
||||
deleteProject: () => {},
|
||||
changeProject: () => {},
|
||||
updateProject: () => {},
|
||||
updateFile: () => false,
|
||||
openFile: () => {},
|
||||
closeFile: () => {},
|
||||
})
|
||||
const Project = createContext<ProjectContext | undefined>(undefined)
|
||||
|
||||
export function useProject() {
|
||||
return useContext(Project)
|
||||
const context = useContext(Project)
|
||||
if (context === undefined) {
|
||||
throw new Error('Cannot use project outside of provider')
|
||||
}
|
||||
return context
|
||||
}
|
||||
|
||||
export function ProjectProvider({ children }: { children: ComponentChildren }) {
|
||||
const [projects, setProjects] = useState<Project[]>(Store.getProjects())
|
||||
const { version } = useVersion()
|
||||
const [projects, setProjects] = useState<ProjectMeta[]>(Store.getProjects())
|
||||
const [openProject, setProjectName] = useState<string>()
|
||||
|
||||
useEffect(() => {
|
||||
const initialProject = Store.getOpenProject()
|
||||
const project = projects.find(p => p.name === initialProject)
|
||||
if (!project) {
|
||||
return
|
||||
}
|
||||
if (project.storage !== undefined) {
|
||||
setProjectName(initialProject)
|
||||
return
|
||||
}
|
||||
// TODO: Import legacy projects
|
||||
}, [])
|
||||
|
||||
const [projectName, setProjectName] = useState<string>(Store.getOpenProject())
|
||||
const project = useMemo(() => {
|
||||
return projects.find(p => p.name === projectName) ?? DRAFT_PROJECT
|
||||
}, [projects, projectName])
|
||||
return projects.find(p => p.name === openProject) ?? DRAFT_PROJECT
|
||||
}, [projects, openProject])
|
||||
|
||||
const [fileId, setFileId] = useState<[string, string] | undefined>(undefined)
|
||||
const file = useMemo(() => {
|
||||
if (!fileId) return undefined
|
||||
return project.files.find(f => f.type === fileId[0] && f.id === fileId[1])
|
||||
}, [project, fileId])
|
||||
const [projectUri, setProjectUri] = useState<string>()
|
||||
|
||||
const changeProjects = useCallback((projects: Project[]) => {
|
||||
const changeProjects = useCallback((projects: ProjectMeta[]) => {
|
||||
Store.setProjects(projects)
|
||||
setProjects(projects)
|
||||
}, [])
|
||||
|
||||
const createProject = useCallback((name: string, namespace?: string, version?: VersionId) => {
|
||||
changeProjects([...projects, { name, namespace, version, files: [] }])
|
||||
const createProject = useCallback((project: ProjectMeta) => {
|
||||
changeProjects([...projects, project])
|
||||
}, [projects])
|
||||
|
||||
const deleteProject = useCallback((name: string) => {
|
||||
@@ -104,55 +103,19 @@ export function ProjectProvider({ children }: { children: ComponentChildren }) {
|
||||
setProjectName(name)
|
||||
}, [])
|
||||
|
||||
const updateProject = useCallback((edits: Partial<Project>) => {
|
||||
changeProjects(projects.map(p => p.name === projectName ? { ...p, ...edits } : p))
|
||||
}, [projects, projectName])
|
||||
|
||||
const updateFile = useCallback((type: string, id: string | undefined, edits: Partial<ProjectFile>) => {
|
||||
if (!edits.id) { // remove
|
||||
updateProject({ files: project.files.filter(f => f.type !== type || f.id !== id) })
|
||||
} else {
|
||||
const newId = type === 'pack_mcmeta' ? 'pack' : edits.id.includes(':') ? edits.id : `${project.namespace ?? 'minecraft'}:${edits.id}`
|
||||
const exists = project.files.some(f => f.type === type && f.id === newId)
|
||||
if (!id) { // create
|
||||
if (exists) return false
|
||||
updateProject({ files: [...project.files, { type, id: newId, data: edits.data ?? {} } ]})
|
||||
setFileId([type, newId])
|
||||
} else { // rename or update data
|
||||
if (file?.id === id && id !== newId && exists) {
|
||||
return false
|
||||
}
|
||||
updateProject({ files: project.files.map(f => f.type === type && f.id === id ? { ...f, ...edits, id: newId } : f)})
|
||||
if (file?.id === id) setFileId([type, newId])
|
||||
}
|
||||
}
|
||||
return true
|
||||
}, [updateProject, project, file])
|
||||
|
||||
const openFile = useCallback((type: string, id: string) => {
|
||||
const gen = config.generators.find(g => g.id === type || genPath(g, version) === type)
|
||||
if (!gen) {
|
||||
throw new Error(`Cannot find generator of type ${type}`)
|
||||
}
|
||||
setFileId([gen.id, id])
|
||||
route(cleanUrl(gen.url))
|
||||
}, [version])
|
||||
|
||||
const closeFile = useCallback(() => {
|
||||
setFileId(undefined)
|
||||
}, [])
|
||||
const updateProject = useCallback((edits: Partial<ProjectMeta>) => {
|
||||
changeProjects(projects.map(p => p.name === openProject ? { ...p, ...edits } : p))
|
||||
}, [projects, openProject])
|
||||
|
||||
const value: ProjectContext = {
|
||||
projects,
|
||||
project,
|
||||
file,
|
||||
projectUri,
|
||||
setProjectUri,
|
||||
createProject,
|
||||
changeProject,
|
||||
deleteProject,
|
||||
updateProject,
|
||||
updateFile,
|
||||
openFile,
|
||||
closeFile,
|
||||
}
|
||||
|
||||
return <Project.Provider value={value}>
|
||||
@@ -160,44 +123,9 @@ export function ProjectProvider({ children }: { children: ComponentChildren }) {
|
||||
</Project.Provider>
|
||||
}
|
||||
|
||||
export function getFilePath(file: { id: string, type: string }, version: VersionId) {
|
||||
const [namespace, id] = file.id.includes(':') ? file.id.split(':') : ['minecraft', file.id]
|
||||
if (file.type === 'pack_mcmeta') {
|
||||
if (file.id === 'pack') return 'pack.mcmeta'
|
||||
return undefined
|
||||
export function getProjectRoot(project: ProjectMeta) {
|
||||
if (project.storage?.type === 'indexeddb') {
|
||||
return project.storage.rootUri
|
||||
}
|
||||
const gen = config.generators.find(g => g.id === file.type)
|
||||
if (!gen) {
|
||||
return undefined
|
||||
}
|
||||
return `data/${namespace}/${genPath(gen, version)}/${id}.json`
|
||||
}
|
||||
|
||||
export function disectFilePath(path: string, version: VersionId) {
|
||||
if (path === 'pack.mcmeta') {
|
||||
return { type: 'pack_mcmeta', id: 'pack' }
|
||||
}
|
||||
for (const p of FilePatterns) {
|
||||
const match = path.match(p)
|
||||
if (!match) continue
|
||||
const gen = config.generators.find(g => (genPath(g, version) ?? g.id) === match[2])
|
||||
if (!gen) continue
|
||||
const namespace = match[1]
|
||||
const name = match[3].replace(/\.[a-z]+$/, '')
|
||||
return {
|
||||
type: gen.id,
|
||||
id: `${namespace}:${name}`,
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
export function getProjectData(project: Project) {
|
||||
return Object.fromEntries(['worldgen/noise_settings', 'worldgen/noise', 'worldgen/density_function'].map(type => {
|
||||
const resources = Object.fromEntries(
|
||||
project.files.filter(file => file.type === type)
|
||||
.map<[string, unknown]>(file => [file.id, file.data])
|
||||
)
|
||||
return [type, resources]
|
||||
}))
|
||||
throw new Error(`Unsupported project storage ${project.storage?.type}`)
|
||||
}
|
||||
|
||||
@@ -52,8 +52,13 @@ export class MixedFileSystem implements core.ExternalFileSystem {
|
||||
}
|
||||
|
||||
async setOverlay(prefix: string, fs: core.ExternalFileSystem) {
|
||||
this.overlays.push({ prefix, fs })
|
||||
this.overlays.sort((a, b) => b.prefix.length - a.prefix.length)
|
||||
const index = this.overlays.findIndex(overlay => overlay.prefix === prefix)
|
||||
if (index !== -1) {
|
||||
this.overlays.splice(index, 1, { prefix, fs })
|
||||
} else {
|
||||
this.overlays.push({ prefix, fs })
|
||||
this.overlays.sort((a, b) => b.prefix.length - a.prefix.length)
|
||||
}
|
||||
if (this.watcher) {
|
||||
await this.watcher.withOverlay(prefix, fs)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import * as core from '@spyglassmc/core'
|
||||
import { FormatterContext } from '@spyglassmc/core'
|
||||
import { BrowserExternals } from '@spyglassmc/core/lib/browser.js'
|
||||
import { dissectUri } from '@spyglassmc/java-edition/lib/binder/index.js'
|
||||
import type { McmetaSummary } from '@spyglassmc/java-edition/lib/dependency/index.js'
|
||||
import { Fluids, ReleaseVersion, symbolRegistrar } from '@spyglassmc/java-edition/lib/dependency/index.js'
|
||||
import * as jeJson from '@spyglassmc/java-edition/lib/json/index.js'
|
||||
@@ -17,9 +18,15 @@ import siteConfig from '../Config.js'
|
||||
import { computeIfAbsent, genPath, message } from '../Utils.js'
|
||||
import type { VersionMeta } from './DataFetcher.js'
|
||||
import { fetchBlockStates, fetchRegistries, fetchVanillaMcdoc, fetchVersions, getVersionChecksum } from './DataFetcher.js'
|
||||
import { IndexedDbFileSystem, MemoryFileSystem, MixedFileSystem } from './FileSystem.js'
|
||||
import { IndexedDbFileSystem, MixedFileSystem } from './FileSystem.js'
|
||||
import type { VersionId } from './Versions.js'
|
||||
|
||||
export const CACHE_URI = 'file:///cache/'
|
||||
export const ROOT_URI = 'file:///root/'
|
||||
export const DEPENDENCY_URI = `${ROOT_URI}dependency/`
|
||||
export const UNSAVED_URI = `${ROOT_URI}unsaved/`
|
||||
export const PROJECTS_URI = `${ROOT_URI}projects/`
|
||||
|
||||
const builtinMcdoc = `
|
||||
use ::java::server::util::text::Text
|
||||
use ::java::data::worldgen::dimension::Dimension
|
||||
@@ -47,7 +54,7 @@ interface ClientDocument {
|
||||
|
||||
export class SpyglassClient {
|
||||
public readonly fs = new MixedFileSystem(new IndexedDbFileSystem(), [
|
||||
{ prefix: 'file:///project/mcdoc/', fs: new MemoryFileSystem() },
|
||||
// { prefix: DEPENDENCY_URI, fs: new MemoryFileSystem() },
|
||||
])
|
||||
public readonly externals: core.Externals = {
|
||||
...BrowserExternals,
|
||||
@@ -83,7 +90,7 @@ export class SpyglassService {
|
||||
|
||||
public getCheckerContext(doc?: TextDocument, errors?: core.LanguageError[]) {
|
||||
if (!doc) {
|
||||
doc = TextDocument.create('file:///temp.json', 'json', 1, '')
|
||||
doc = TextDocument.create('file:///unknown.json', 'json', 1, '')
|
||||
}
|
||||
const err = new core.ErrorReporter()
|
||||
if (errors) {
|
||||
@@ -92,6 +99,10 @@ export class SpyglassService {
|
||||
return core.CheckerContext.create(this.service.project, { doc, err })
|
||||
}
|
||||
|
||||
public dissectUri(uri: string) {
|
||||
return dissectUri(uri, this.getCheckerContext(TextDocument.create(uri, 'json', 1, '')))
|
||||
}
|
||||
|
||||
public async openFile(uri: string) {
|
||||
const lang = core.fileUtil.extname(uri)?.slice(1) ?? 'txt'
|
||||
const content = await this.readFile(uri)
|
||||
@@ -193,9 +204,9 @@ export class SpyglassService {
|
||||
|
||||
public getUnsavedFileUri(gen: ConfigGenerator) {
|
||||
if (gen.id === 'pack_mcmeta') {
|
||||
return 'file:///project/pack.mcmeta'
|
||||
return `${UNSAVED_URI}pack.mcmeta`
|
||||
}
|
||||
return `file:///project/data/draft/${genPath(gen, this.version)}/unsaved.json`
|
||||
return `${UNSAVED_URI}data/draft/${genPath(gen, this.version)}/draft.json`
|
||||
}
|
||||
|
||||
public watchFile(uri: string, handler: (docAndNode: core.DocAndNode) => void) {
|
||||
@@ -222,8 +233,8 @@ export class SpyglassService {
|
||||
'project#ready#bind',
|
||||
]),
|
||||
project: {
|
||||
cacheRoot: 'file:///cache/',
|
||||
projectRoots: ['file:///project/'],
|
||||
cacheRoot: CACHE_URI,
|
||||
projectRoots: [ROOT_URI],
|
||||
externals: client.externals,
|
||||
defaultConfig: core.ConfigService.merge(core.VanillaConfig, {
|
||||
env: {
|
||||
@@ -251,7 +262,7 @@ export class SpyglassService {
|
||||
},
|
||||
// Partner resources
|
||||
...Object.fromEntries(siteConfig.generators.filter(gen => gen.dependency).map(gen =>
|
||||
[gen.path, {
|
||||
[gen.path ?? gen.id, {
|
||||
category: gen.id,
|
||||
}]
|
||||
)),
|
||||
|
||||
Reference in New Issue
Block a user