feat: add project management features including open/save functionality and recovery support
This commit is contained in:
71
view/useProjectLifecycle.tsx
Normal file
71
view/useProjectLifecycle.tsx
Normal 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 };
|
||||
}
|
||||
Reference in New Issue
Block a user