diff --git a/src/app/Main.tsx b/src/app/Main.tsx index 28a0089f..f5b52c05 100644 --- a/src/app/Main.tsx +++ b/src/app/Main.tsx @@ -4,6 +4,7 @@ import '../styles/main.css' import '../styles/nodes.css' import { App } from './App.js' import { LocaleProvider, ProjectProvider, StoreProvider, ThemeProvider, TitleProvider, VersionProvider } from './contexts/index.js' +import { SpyglassProvider } from './contexts/Spyglass.jsx' function Main() { return ( @@ -12,9 +13,11 @@ function Main() { - - - + + + + + diff --git a/src/app/Store.ts b/src/app/Store.ts index 89ea0f85..d826acac 100644 --- a/src/app/Store.ts +++ b/src/app/Store.ts @@ -14,7 +14,6 @@ export namespace Store { export const ID_HIGHLIGHTING = 'output_highlighting' export const ID_SOUNDS_VERSION = 'minecraft_sounds_version' export const ID_PROJECTS = 'misode_projects' - export const ID_BACKUPS = 'misode_generator_backups' export const ID_PREVIEW_PANEL_OPEN = 'misode_preview_panel_open' export const ID_PROJECT_PANEL_OPEN = 'misode_project_panel_open' export const ID_OPEN_PROJECT = 'misode_open_project' @@ -71,11 +70,6 @@ export namespace Store { return [DRAFT_PROJECT] } - export function getBackup(id: string): object | undefined { - const backups = JSON.parse(localStorage.getItem(ID_BACKUPS) ?? '{}') - return backups[id] - } - export function getPreviewPanelOpen(): boolean | undefined { const open = localStorage.getItem(ID_PREVIEW_PANEL_OPEN) if (open === null) return undefined @@ -140,16 +134,6 @@ export namespace Store { if (projects) localStorage.setItem(ID_PROJECTS, JSON.stringify(projects)) } - export function setBackup(id: string, data: object | undefined) { - const backups = JSON.parse(localStorage.getItem(ID_BACKUPS) ?? '{}') - if (data === undefined) { - delete backups[id] - } else { - backups[id] = data - } - localStorage.setItem(ID_BACKUPS, JSON.stringify(backups)) - } - export function setPreviewPanelOpen(open: boolean | undefined) { if (open === undefined) { localStorage.removeItem(ID_PREVIEW_PANEL_OPEN) diff --git a/src/app/components/ErrorPanel.tsx b/src/app/components/ErrorPanel.tsx index c6bb8d03..c0582e3a 100644 --- a/src/app/components/ErrorPanel.tsx +++ b/src/app/components/ErrorPanel.tsx @@ -1,10 +1,10 @@ import type { ComponentChildren } from 'preact' import { getCurrentUrl } from 'preact-router' import { useEffect, useMemo, useState } from 'preact/hooks' -import { Store } from '../Store.js' -import { getGenerator } from '../Utils.js' +import { useSpyglass } from '../contexts/Spyglass.jsx' import { useVersion } from '../contexts/Version.jsx' import { latestVersion } from '../services/DataFetcher.js' +import { getGenerator } from '../Utils.js' import { Octicon } from './index.js' type ErrorPanelProps = { @@ -17,11 +17,12 @@ type ErrorPanelProps = { } export function ErrorPanel({ error, prefix, reportable, onDismiss, body: body_, children }: ErrorPanelProps) { const { version } = useVersion() + const { spyglass } = useSpyglass() const [stackVisible, setStackVisible] = useState(false) const [stack, setStack] = useState(undefined) const gen = getGenerator(getCurrentUrl()) - const source = gen ? Store.getBackup(gen.id) : undefined + const source = gen ? spyglass?.getFile(spyglass.getUnsavedFileUri(gen)).doc?.getText() : undefined const name = (prefix ?? '') + (error instanceof Error ? error.message : error) useEffect(() => { diff --git a/src/app/components/generator/PreviewPanel.tsx b/src/app/components/generator/PreviewPanel.tsx index f8222fcc..9077f8f3 100644 --- a/src/app/components/generator/PreviewPanel.tsx +++ b/src/app/components/generator/PreviewPanel.tsx @@ -1,4 +1,5 @@ import type { DocAndNode } from '@spyglassmc/core' +import { useDocAndNode } from '../../contexts/Spyglass.jsx' import { useVersion } from '../../contexts/Version.jsx' import { checkVersion } from '../../services/index.js' import { BiomeSourcePreview, BlockStatePreview, DecoratorPreview, DensityFunctionPreview, LootTablePreview, ModelPreview, NoisePreview, NoiseSettingsPreview, RecipePreview, StructureSetPreview } from '../previews/index.js' @@ -11,10 +12,12 @@ type PreviewPanelProps = { shown: boolean, onError: (message: string) => unknown, } -export function PreviewPanel({ docAndNode, id, shown }: PreviewPanelProps) { +export function PreviewPanel({ docAndNode: original, id, shown }: PreviewPanelProps) { const { version } = useVersion() - if (!docAndNode) return <> + if (!original) return <> + + const docAndNode = useDocAndNode(original) if (id === 'loot_table') { return diff --git a/src/app/components/generator/SchemaGenerator.tsx b/src/app/components/generator/SchemaGenerator.tsx index c017d42b..c6893751 100644 --- a/src/app/components/generator/SchemaGenerator.tsx +++ b/src/app/components/generator/SchemaGenerator.tsx @@ -1,13 +1,13 @@ import { route } from 'preact-router' -import { useCallback, useEffect, useErrorBoundary, useMemo, useState } from 'preact/hooks' +import { useCallback, useEffect, useErrorBoundary, useMemo, useRef, useState } from 'preact/hooks' import { Analytics } from '../../Analytics.js' import type { ConfigGenerator } from '../../Config.js' import config from '../../Config.js' import { DRAFT_PROJECT, useLocale, useProject, useVersion } from '../../contexts/index.js' +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, fetchPreset, fetchRegistries, getSnippet, shareSnippet } from '../../services/index.js' -import { Spyglass } from '../../services/Spyglass.js' import { Store } from '../../Store.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' @@ -21,7 +21,8 @@ interface Props { export function SchemaGenerator({ gen, allowedVersions }: Props) { const { locale } = useLocale() const { version, changeVersion, changeTargetVersion } = useVersion() - const { projects, project, file, updateProject, closeFile } = useProject() + const { spyglass, spyglassLoading } = useSpyglass() + const { projects, project, file, updateProject, updateFile, closeFile } = useProject() const [error, setError] = useState(null) const [errorBoundary, errorRetry] = useErrorBoundary() if (errorBoundary) { @@ -31,10 +32,6 @@ export function SchemaGenerator({ gen, allowedVersions }: Props) { useEffect(() => Store.visitGenerator(gen.id), [gen.id]) - const { value: spyglass, loading: spyglassLoading } = useAsync(() => { - return Spyglass.initialize(version) - }, [version]) - const uri = useMemo(() => { // TODO: return different uri when project file is open return spyglass?.getUnsavedFileUri(gen) @@ -42,13 +39,7 @@ export function SchemaGenerator({ gen, allowedVersions }: Props) { const [currentPreset, setCurrentPreset] = useSearchParam('preset') const [sharedSnippetId, setSharedSnippetId] = useSearchParam(SHARE_KEY) - const backup = useMemo(() => Store.getBackup(gen.id), [gen.id]) - - const loadBackup = () => { - if (backup !== undefined) { - // TODO: implement - } - } + const ignoreChange = useRef(false) const { value: docAndNode } = useAsync(async () => { if (spyglassLoading || !spyglass || !uri) { @@ -61,6 +52,7 @@ export function SchemaGenerator({ gen, allowedVersions }: Props) { } if (currentPreset) { data = await loadPreset(currentPreset) + ignoreChange.current = true } else if (sharedSnippetId) { const snippet = await getSnippet(sharedSnippetId) let cancel = false @@ -83,25 +75,36 @@ export function SchemaGenerator({ gen, allowedVersions }: Props) { setSourceShown(false) } Analytics.openSnippet(gen.id, sharedSnippetId, version) + ignoreChange.current = true data = snippet.data } else if (file) { if (project.version && project.version !== version) { changeVersion(project.version, false) return AsyncCancel } + ignoreChange.current = true data = file.data } - const docAndNode = await spyglass.setFileContents(uri, JSON.stringify(data ?? {})) + // TODO: if data is undefined, set to generator's default + const docAndNode = await spyglass.setFileContents(uri, data ? JSON.stringify(data) : undefined) Analytics.setGenerator(gen.id) return docAndNode }, [gen.id, version, sharedSnippetId, currentPreset, project.name, file?.id, spyglass, spyglassLoading]) - const { doc } = docAndNode ?? {} + const { doc } = docAndNode ?? {} - // TODO: when contents of file change: - // - remove preset and share id from url - // - update project - // - store backup + watchSpyglassUri(uri, ({ doc }) => { + if (!ignoreChange.current) { + setCurrentPreset(undefined, true) + setSharedSnippetId(undefined, true) + } + const data = JSON.parse(doc.getText()) + if (file) { + updateFile(gen.id, file.id, { id: file.id, data }) + } + ignoreChange.current = false + setError(null) + }, [updateFile]) const reset = () => { Analytics.resetGenerator(gen.id, 1, 'menu') @@ -304,7 +307,6 @@ export function SchemaGenerator({ gen, allowedVersions }: Props) { - {backup !== undefined && } diff --git a/src/app/components/generator/SourcePanel.tsx b/src/app/components/generator/SourcePanel.tsx index d22a162a..27b4065b 100644 --- a/src/app/components/generator/SourcePanel.tsx +++ b/src/app/components/generator/SourcePanel.tsx @@ -2,6 +2,7 @@ import type { DocAndNode } from '@spyglassmc/core' import { fileUtil } from '@spyglassmc/core' import { useCallback, useEffect, useRef, useState } from 'preact/hooks' import { useLocale } from '../../contexts/index.js' +import { useDocAndNode } from '../../contexts/Spyglass.jsx' import { useLocalStorage } from '../../hooks/index.js' import { getSourceFormats, getSourceIndent, getSourceIndents, parseSource, sortData, stringifySource } from '../../services/index.js' import type { Spyglass } from '../../services/Spyglass.js' @@ -47,7 +48,7 @@ export function SourcePanel({ spyglass, docAndNode, doCopy, doDownload, doImport return stringifySource(data, format, indent) }, [indent, format, sort]) - const text = docAndNode?.doc.getText() + const text = useDocAndNode(docAndNode)?.doc.getText() useEffect(() => { retransform.current = () => { diff --git a/src/app/contexts/Spyglass.tsx b/src/app/contexts/Spyglass.tsx new file mode 100644 index 00000000..bfea67d6 --- /dev/null +++ b/src/app/contexts/Spyglass.tsx @@ -0,0 +1,64 @@ +import type { DocAndNode } from '@spyglassmc/core' +import type { ComponentChildren } from 'preact' +import { createContext } from 'preact' +import type { Inputs } from 'preact/hooks' +import { useContext, useEffect, useState } from 'preact/hooks' +import { useAsync } from '../hooks/useAsync.js' +import { Spyglass } from '../services/Spyglass.js' +import { useVersion } from './Version.jsx' + +interface SpyglassContext { + spyglass?: Spyglass, + spyglassLoading: boolean, +} + +const SpyglassContext = createContext(undefined) + +export function useSpyglass(): SpyglassContext { + return useContext(SpyglassContext) ?? { spyglassLoading: true } +} + +export function watchSpyglassUri( + uri: string | undefined, + handler: (docAndNode: DocAndNode) => void, + inputs: Inputs = [], +) { + const { spyglass, spyglassLoading } = useSpyglass() + + useEffect(() => { + if (!uri || !spyglass || spyglassLoading) { + return + } + spyglass.watchFile(uri, handler) + return () => spyglass.unwatchFile(uri, handler) + }, [spyglass, uri, handler, ...inputs]) +} + +export function useDocAndNode(origina: DocAndNode, inputs?: Inputs): DocAndNode +export function useDocAndNode(origina: DocAndNode | undefined, inputs?: Inputs): DocAndNode | undefined +export function useDocAndNode(original: DocAndNode | undefined, inputs: Inputs = []) { + const [wrapped, setWrapped] = useState(original) + + watchSpyglassUri(original?.doc.uri, updated => { + setWrapped(updated) + }, [original?.doc.uri, setWrapped, ...inputs]) + + return wrapped +} + +export function SpyglassProvider({ children }: { children: ComponentChildren }) { + const { version } = useVersion() + + const { value: spyglass, loading: spyglassLoading } = useAsync(() => { + return Spyglass.initialize(version) + }, [version]) + + const value: SpyglassContext = { + spyglass, + spyglassLoading, + } + + return + {children} + +} diff --git a/src/app/services/Resources.ts b/src/app/services/Resources.ts index 8a7ea5fa..8e200814 100644 --- a/src/app/services/Resources.ts +++ b/src/app/services/Resources.ts @@ -44,7 +44,6 @@ export async function renderItem(version: VersionId, item: ItemStack) { throw new Error('Cannot get WebGL2 context') } const renderer = new ItemRenderer(gl, item, resources) - console.log('Rendering', item.toString()) renderer.drawItem() return canvas.toDataURL() })() diff --git a/src/app/services/Resources1204.ts b/src/app/services/Resources1204.ts index bdf889b2..a8aa7ceb 100644 --- a/src/app/services/Resources1204.ts +++ b/src/app/services/Resources1204.ts @@ -44,7 +44,6 @@ export async function renderItem(version: VersionId, item: ItemStack) { throw new Error('Cannot get WebGL2 context') } const renderer = new ItemRenderer(gl, item, resources) - console.log('Rendering', item.toString()) renderer.drawItem() return canvas.toDataURL() })() diff --git a/src/app/services/Spyglass.ts b/src/app/services/Spyglass.ts index a0aab5c2..f44efbbd 100644 --- a/src/app/services/Spyglass.ts +++ b/src/app/services/Spyglass.ts @@ -11,19 +11,34 @@ import * as nbt from '@spyglassmc/nbt' import * as zip from '@zip.js/zip.js' import type { ConfigGenerator, ConfigVersion } from '../Config.js' import siteConfig from '../Config.js' -import { genPath } from '../Utils.js' +import { computeIfAbsent, genPath } from '../Utils.js' import { fetchBlockStates, fetchRegistries, fetchVanillaMcdoc, getVersionChecksum } from './DataFetcher.js' import type { VersionId } from './Versions.js' export class Spyglass { private static readonly INSTANCES = new Map>() + private readonly watchers = new Map void)[]>() + private constructor( private readonly service: core.Service, private readonly version: ConfigVersion, - ) {} + ) { + this.service.project.on('documentUpdated', (e) => { + const uriWatchers = this.watchers.get(e.doc.uri) ?? [] + for (const handler of uriWatchers) { + handler(e) + } + }) + } - public async setFileContents(uri: string, contents: string) { + public async setFileContents(uri: string, contents?: string) { + if (contents !== undefined) { + await this.service.project.externals.fs.writeFile(uri, contents) + } else { + const buffer = await this.service.project.externals.fs.readFile(uri) + contents = new TextDecoder().decode(buffer) + } await this.service.project.onDidOpen(uri, 'json', 1, contents) const docAndNode = await this.service.project.ensureClientManagedChecked(uri) if (!docAndNode) { @@ -37,7 +52,18 @@ export class Spyglass { } public getUnsavedFileUri(gen: ConfigGenerator) { - return `file:project/data/draft/${genPath(gen, this.version.id)}/unsaved.json` + return `file:///project/data/draft/${genPath(gen, this.version.id)}/unsaved.json` + } + + public watchFile(uri: string, handler: (docAndNode: core.DocAndNode) => void) { + const uriWatchers = computeIfAbsent(this.watchers, uri, () => []) + uriWatchers.push(handler) + } + + public unwatchFile(uri: string, handler: (docAndNode: core.DocAndNode) => void) { + const uriWatchers = computeIfAbsent(this.watchers, uri, () => []) + const index = uriWatchers.findIndex(w => w === handler) + uriWatchers.splice(index, 1) } public static async initialize(versionId: VersionId) { @@ -54,8 +80,8 @@ export class Spyglass { 'project#ready', ]), project: { - cacheRoot: 'file:cache/', - projectRoots: ['file:project/'], + cacheRoot: 'file:///cache/', + projectRoots: ['file:///project/'], externals: { ...BrowserExternals, archive: {