diff --git a/src/App.ts b/src/App.ts index 391642dd5..5a5e35242 100644 --- a/src/App.ts +++ b/src/App.ts @@ -82,7 +82,16 @@ export async function setup() { if (fileSystem instanceof TauriFileSystem) await setupTauriFileSystem() - if (fileSystem instanceof LocalFileSystem) fileSystem.setRootName('fileSystemPolyfill') + if (fileSystem instanceof LocalFileSystem) { + fileSystem.setRootName('fileSystemPolyfill') + + // On browsers without the File System Access API (e.g. Safari, Firefox, Android Chrome) the + // entire project lives in IndexedDB. Request persistent storage so the browser doesn't evict + // it, which otherwise causes saved files to silently disappear. + if (navigator.storage?.persist) { + navigator.storage.persist().catch(() => {}) + } + } setupTypescript() setupLang() diff --git a/src/components/Windows/Presets/Presets.vue b/src/components/Windows/Presets/Presets.vue index a7b380826..773fde00d 100644 --- a/src/components/Windows/Presets/Presets.vue +++ b/src/components/Windows/Presets/Presets.vue @@ -76,6 +76,32 @@ const validationError: ComputedRef = computed(() => { return null }) +function openFileInput(fieldId: string) { + const input = document.getElementById(`preset-file-input-${fieldId}`) + + if (input instanceof HTMLInputElement) input.click() +} + +async function onFileInputChange(event: Event, fieldId: string) { + const input = event.target + + const file = input.files?.[0] + + if (!file) return + + const content = new Uint8Array(await file.arrayBuffer()) + + // Store a fake file handle matching the shape that preset scripts (and loadPresetFile) expect: + // `name` + `content` (binary, read by createFile) + `text()` (read by e.g. the Block Model script). + createPresetOptions.value[fieldId] = { + name: file.name, + content, + async text() { + return new TextDecoder().decode(content) + }, + } +} + async function create() { if (validationError.value !== null) return @@ -214,15 +240,22 @@ watch(filteredCategories, () => { class="mb-6 flex bg-background" v-slot="{ focus, blur }" > - + diff --git a/src/libs/compiler/DashService.ts b/src/libs/compiler/DashService.ts index 7113c5dc8..3916ea81c 100644 --- a/src/libs/compiler/DashService.ts +++ b/src/libs/compiler/DashService.ts @@ -71,7 +71,10 @@ export class DashService implements AsyncDisposable { await sendAndWait( { action: 'setup', - config: this.project.config, + // Pass a plain, structured-clone-safe copy of the config. Safari's structured clone + // is stricter than Chromium's and throws DataCloneError on non-plain objects, which + // broke .mcaddon exports (the only export path that hands data to the Dash worker). + config: this.project.config ? JSON.parse(JSON.stringify(this.project.config)) : this.project.config, mode, configPath: join(this.project.path, 'config.json'), compilerConfigPath, diff --git a/src/libs/fileSystem/LocalFileSystem.ts b/src/libs/fileSystem/LocalFileSystem.ts index 7972c7ce5..1cd8f5f0a 100644 --- a/src/libs/fileSystem/LocalFileSystem.ts +++ b/src/libs/fileSystem/LocalFileSystem.ts @@ -173,7 +173,14 @@ export class LocalFileSystem extends BaseFileSystem { path = resolve('/', path) - await del(`localFileSystem/${this.rootName}${path}`) + // Remove the directory entry itself as well as every file and subdirectory nested inside of it. + // idb-keyval has no concept of folders, so deleting only the directory key would orphan its children. + const directoryKey = `localFileSystem/${this.rootName}${path}` + const childPrefix = `${directoryKey}/` + + const childKeys = (await keys()).filter((key) => key.toString().startsWith(childPrefix)) + + await Promise.all([del(directoryKey), ...childKeys.map((key) => del(key))]) if ( this.pathsToWatch.find((watchPath) => path.startsWith(watchPath)) !== undefined &&