feat: add project management features including open/save functionality and recovery support

This commit is contained in:
syntaxbullet
2026-07-10 23:46:01 +02:00
parent 6d652c3264
commit 9697075d29
15 changed files with 613 additions and 2 deletions

View File

@@ -0,0 +1,71 @@
import { useCallback, useEffect, useRef } from "react";
import type { AppStore } from "@editor/store";
import { createProjectLifecycle } from "@operations/project/lifecycle";
import { projectFileName } from "@operations/project/format";
import { browserRecoveryStorage, downloadProjectFile, readProjectFile } from "@platform/browser/projectFiles";
export function useProjectLifecycle(store: AppStore) {
const inputRef = useRef<HTMLInputElement | null>(null);
const lifecycleRef = useRef<ReturnType<typeof createProjectLifecycle> | null>(null);
useEffect(() => {
const lifecycle = createProjectLifecycle({ store, storage: browserRecoveryStorage });
lifecycleRef.current = lifecycle;
void lifecycle.recover();
return () => {
lifecycle.dispose();
lifecycleRef.current = null;
};
}, [store]);
const save = useCallback(() => {
const lifecycle = lifecycleRef.current;
if (!lifecycle) return;
const document = store.getState().document;
downloadProjectFile(lifecycle.snapshot(), projectFileName(document.name));
void lifecycle.markSaved();
}, [store]);
const openFilePicker = useCallback(() => inputRef.current?.click(), []);
const openFile = useCallback(async (file: File) => {
const lifecycle = lifecycleRef.current;
if (!lifecycle) return;
try {
await lifecycle.open(await readProjectFile(file));
} catch (error) {
window.alert(error instanceof Error ? error.message : "The project could not be opened.");
}
}, []);
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (event.defaultPrevented || (!event.metaKey && !event.ctrlKey)) return;
if (event.key.toLowerCase() === "s") {
event.preventDefault();
save();
} else if (event.shiftKey && event.key.toLowerCase() === "o") {
event.preventDefault();
openFilePicker();
}
};
window.addEventListener("keydown", handleKeyDown);
return () => window.removeEventListener("keydown", handleKeyDown);
}, [openFilePicker, save]);
const input = (
<input
ref={inputRef}
type="file"
accept=".json,.image-studio.json,application/json"
className="hidden"
onChange={(event) => {
const file = event.currentTarget.files?.[0];
event.currentTarget.value = "";
if (file) void openFile(file);
}}
/>
);
return { input, save, openFilePicker };
}