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: {