Add autosave functionality and update project package handling
This commit is contained in:
@@ -201,7 +201,13 @@ import type { FirstPersonTelemetry } from "../runtime-three/navigation-controlle
|
||||
import type { RuntimeInteractionPrompt } from "../runtime-three/runtime-interaction-system";
|
||||
import { buildRuntimeSceneFromDocument, type RuntimeNavigationMode, type RuntimeSceneDefinition } from "../runtime-three/runtime-scene-build";
|
||||
import { validateRuntimeSceneBuild } from "../runtime-three/runtime-scene-validation";
|
||||
import { EditorAutosaveController } from "../serialization/editor-autosave";
|
||||
import { Panel } from "../shared-ui/Panel";
|
||||
import {
|
||||
loadProjectPackage,
|
||||
PROJECT_PACKAGE_FILE_EXTENSION,
|
||||
saveProjectPackage
|
||||
} from "../serialization/project-package";
|
||||
import { HierarchicalMenu, type HierarchicalMenuItem, type HierarchicalMenuPosition } from "../shared-ui/HierarchicalMenu";
|
||||
import { createWorldBackgroundStyle } from "../shared-ui/world-background-style";
|
||||
import { ViewportPanel } from "../viewport-three/ViewportPanel";
|
||||
@@ -942,6 +948,16 @@ function formatBoxVolumeRenderPathLabel(path: BoxVolumeRenderPath): string {
|
||||
}
|
||||
}
|
||||
|
||||
function createProjectDownloadName(documentName: string): string {
|
||||
const slug = documentName
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/gu, "-")
|
||||
.replace(/^-+|-+$/gu, "");
|
||||
|
||||
return `${slug.length > 0 ? slug : "scene"}${PROJECT_PACKAGE_FILE_EXTENSION}`;
|
||||
}
|
||||
|
||||
export function App({ store, initialStatusMessage }: AppProps) {
|
||||
const editorState = useEditorStoreState(store);
|
||||
const brushList = Object.values(editorState.document.brushes);
|
||||
@@ -1127,7 +1143,7 @@ export function App({ store, initialStatusMessage }: AppProps) {
|
||||
kind: "none"
|
||||
}
|
||||
});
|
||||
const importInputRef = useRef<HTMLInputElement | null>(null);
|
||||
const importProjectInputRef = useRef<HTMLInputElement | null>(null);
|
||||
const importModelInputRef = useRef<HTMLInputElement | null>(null);
|
||||
const importBackgroundImageInputRef = useRef<HTMLInputElement | null>(null);
|
||||
const importAudioInputRef = useRef<HTMLInputElement | null>(null);
|
||||
@@ -1135,6 +1151,8 @@ export function App({ store, initialStatusMessage }: AppProps) {
|
||||
const loadedModelAssetsRef = useRef<Record<string, LoadedModelAsset>>({});
|
||||
const loadedImageAssetsRef = useRef<Record<string, LoadedImageAsset>>({});
|
||||
const loadedAudioAssetsRef = useRef<Record<string, LoadedAudioAsset>>({});
|
||||
const autosaveControllerRef = useRef<EditorAutosaveController | null>(null);
|
||||
const lastAutosaveErrorRef = useRef<string | null>(null);
|
||||
const viewportQuadSplitRef = useRef(editorState.viewportQuadSplit);
|
||||
const lastPointerPositionRef = useRef<HierarchicalMenuPosition>({
|
||||
x: Math.round(window.innerWidth * 0.5),
|
||||
@@ -1394,6 +1412,68 @@ export function App({ store, initialStatusMessage }: AppProps) {
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
autosaveControllerRef.current = new EditorAutosaveController({
|
||||
saveDraft: () => store.saveDraft(),
|
||||
onComplete: (result) => {
|
||||
if (result.status === "error") {
|
||||
if (lastAutosaveErrorRef.current !== result.message) {
|
||||
lastAutosaveErrorRef.current = result.message;
|
||||
setStatusMessage(result.message);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
lastAutosaveErrorRef.current = null;
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
autosaveControllerRef.current?.dispose();
|
||||
autosaveControllerRef.current = null;
|
||||
};
|
||||
}, [store]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!editorState.storageAvailable) {
|
||||
return;
|
||||
}
|
||||
|
||||
autosaveControllerRef.current?.schedule();
|
||||
}, [
|
||||
editorState.activeViewportPanelId,
|
||||
editorState.document,
|
||||
editorState.storageAvailable,
|
||||
editorState.viewportLayoutMode,
|
||||
editorState.viewportPanels,
|
||||
editorState.viewportQuadSplit
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!editorState.storageAvailable) {
|
||||
return;
|
||||
}
|
||||
|
||||
const flushAutosave = () => {
|
||||
autosaveControllerRef.current?.flush();
|
||||
};
|
||||
const handleVisibilityChange = () => {
|
||||
if (document.visibilityState === "hidden") {
|
||||
flushAutosave();
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("beforeunload", flushAutosave);
|
||||
window.addEventListener("pagehide", flushAutosave);
|
||||
document.addEventListener("visibilitychange", handleVisibilityChange);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("beforeunload", flushAutosave);
|
||||
window.removeEventListener("pagehide", flushAutosave);
|
||||
document.removeEventListener("visibilitychange", handleVisibilityChange);
|
||||
};
|
||||
}, [editorState.storageAvailable]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!projectAssetStorageReady) {
|
||||
return;
|
||||
|
||||
@@ -206,12 +206,12 @@ export function saveSceneDocumentDraft(
|
||||
|
||||
return {
|
||||
status: "saved",
|
||||
message: "Local draft saved."
|
||||
message: "Autosave updated."
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
status: "error",
|
||||
message: formatStorageDiagnostic("Local draft could not be saved.", error)
|
||||
message: formatStorageDiagnostic("Autosave could not be saved.", error)
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -226,7 +226,7 @@ export function loadSceneDocumentDraft(
|
||||
if (rawDocument === null) {
|
||||
return {
|
||||
status: "missing",
|
||||
message: "No local draft was found."
|
||||
message: "No autosave was found."
|
||||
};
|
||||
}
|
||||
|
||||
@@ -237,7 +237,7 @@ export function loadSceneDocumentDraft(
|
||||
status: "loaded",
|
||||
document: parseSceneDocumentJson(JSON.stringify(parsedDraft.document)),
|
||||
viewportLayoutState: parseViewportLayoutState(parsedDraft.viewportLayoutState ?? null),
|
||||
message: "Local draft loaded."
|
||||
message: "Recovered latest autosave."
|
||||
};
|
||||
}
|
||||
|
||||
@@ -245,12 +245,12 @@ export function loadSceneDocumentDraft(
|
||||
status: "loaded",
|
||||
document: parseSceneDocumentJson(rawDocument),
|
||||
viewportLayoutState: null,
|
||||
message: "Local draft loaded."
|
||||
message: "Recovered latest autosave."
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
status: "error",
|
||||
message: formatStorageDiagnostic("Stored local draft could not be loaded.", error)
|
||||
message: formatStorageDiagnostic("Stored autosave could not be loaded.", error)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user