2026-03-31 01:29:35 +02:00
|
|
|
import { createEmptySceneDocument, type SceneDocument } from "../document/scene-document";
|
|
|
|
|
|
|
|
|
|
import { parseSceneDocumentJson, serializeSceneDocument } from "./scene-document-json";
|
|
|
|
|
|
|
|
|
|
export interface KeyValueStorage {
|
|
|
|
|
getItem(key: string): string | null;
|
|
|
|
|
setItem(key: string, value: string): void;
|
|
|
|
|
removeItem(key: string): void;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-31 01:50:29 +02:00
|
|
|
export interface BrowserStorageAccessResult {
|
|
|
|
|
storage: KeyValueStorage | null;
|
|
|
|
|
diagnostic: string | null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export type SaveSceneDocumentDraftResult =
|
|
|
|
|
| { status: "saved"; message: string }
|
|
|
|
|
| { status: "error"; message: string };
|
|
|
|
|
|
|
|
|
|
export type LoadSceneDocumentDraftResult =
|
|
|
|
|
| { status: "loaded"; document: SceneDocument; message: string }
|
|
|
|
|
| { status: "missing"; message: string }
|
|
|
|
|
| { status: "error"; message: string };
|
|
|
|
|
|
|
|
|
|
export interface LoadOrCreateSceneDocumentResult {
|
|
|
|
|
document: SceneDocument;
|
|
|
|
|
diagnostic: string | null;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-31 01:29:35 +02:00
|
|
|
export const DEFAULT_SCENE_DRAFT_STORAGE_KEY = "webeditor3d.scene-document-draft";
|
|
|
|
|
|
2026-03-31 01:50:29 +02:00
|
|
|
function getErrorDetail(error: unknown): string {
|
|
|
|
|
if (error instanceof Error && error.message.trim().length > 0) {
|
|
|
|
|
return error.message.trim();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return "Unknown error.";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function formatStorageDiagnostic(prefix: string, error: unknown): string {
|
|
|
|
|
return `${prefix} ${getErrorDetail(error)}`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function getBrowserStorageAccess(): BrowserStorageAccessResult {
|
2026-03-31 01:29:35 +02:00
|
|
|
if (typeof window === "undefined") {
|
2026-03-31 01:50:29 +02:00
|
|
|
return {
|
|
|
|
|
storage: null,
|
|
|
|
|
diagnostic: null
|
|
|
|
|
};
|
2026-03-31 01:29:35 +02:00
|
|
|
}
|
|
|
|
|
|
2026-03-31 01:50:29 +02:00
|
|
|
try {
|
|
|
|
|
return {
|
|
|
|
|
storage: window.localStorage,
|
|
|
|
|
diagnostic: null
|
|
|
|
|
};
|
|
|
|
|
} catch (error) {
|
|
|
|
|
return {
|
|
|
|
|
storage: null,
|
|
|
|
|
diagnostic: formatStorageDiagnostic("Browser local storage is unavailable.", error)
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function getBrowserStorage(): KeyValueStorage | null {
|
|
|
|
|
return getBrowserStorageAccess().storage;
|
2026-03-31 01:29:35 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function saveSceneDocumentDraft(
|
|
|
|
|
storage: KeyValueStorage,
|
|
|
|
|
document: SceneDocument,
|
|
|
|
|
key = DEFAULT_SCENE_DRAFT_STORAGE_KEY
|
2026-03-31 01:50:29 +02:00
|
|
|
): SaveSceneDocumentDraftResult {
|
|
|
|
|
try {
|
|
|
|
|
storage.setItem(key, serializeSceneDocument(document));
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
status: "saved",
|
|
|
|
|
message: "Local draft saved."
|
|
|
|
|
};
|
|
|
|
|
} catch (error) {
|
|
|
|
|
return {
|
|
|
|
|
status: "error",
|
|
|
|
|
message: formatStorageDiagnostic("Local draft could not be saved.", error)
|
|
|
|
|
};
|
|
|
|
|
}
|
2026-03-31 01:29:35 +02:00
|
|
|
}
|
|
|
|
|
|
2026-03-31 01:50:29 +02:00
|
|
|
export function loadSceneDocumentDraft(
|
|
|
|
|
storage: KeyValueStorage,
|
|
|
|
|
key = DEFAULT_SCENE_DRAFT_STORAGE_KEY
|
|
|
|
|
): LoadSceneDocumentDraftResult {
|
|
|
|
|
try {
|
|
|
|
|
const rawDocument = storage.getItem(key);
|
2026-03-31 01:29:35 +02:00
|
|
|
|
2026-03-31 01:50:29 +02:00
|
|
|
if (rawDocument === null) {
|
|
|
|
|
return {
|
|
|
|
|
status: "missing",
|
|
|
|
|
message: "No local draft was found."
|
|
|
|
|
};
|
|
|
|
|
}
|
2026-03-31 01:29:35 +02:00
|
|
|
|
2026-03-31 01:50:29 +02:00
|
|
|
return {
|
|
|
|
|
status: "loaded",
|
|
|
|
|
document: parseSceneDocumentJson(rawDocument),
|
|
|
|
|
message: "Local draft loaded."
|
|
|
|
|
};
|
|
|
|
|
} catch (error) {
|
|
|
|
|
return {
|
|
|
|
|
status: "error",
|
|
|
|
|
message: formatStorageDiagnostic("Stored local draft could not be loaded.", error)
|
|
|
|
|
};
|
|
|
|
|
}
|
2026-03-31 01:29:35 +02:00
|
|
|
}
|
|
|
|
|
|
2026-03-31 01:50:29 +02:00
|
|
|
export function loadOrCreateSceneDocument(
|
|
|
|
|
storage: KeyValueStorage | null,
|
|
|
|
|
key = DEFAULT_SCENE_DRAFT_STORAGE_KEY
|
|
|
|
|
): LoadOrCreateSceneDocumentResult {
|
2026-03-31 01:29:35 +02:00
|
|
|
if (storage === null) {
|
2026-03-31 01:50:29 +02:00
|
|
|
return {
|
|
|
|
|
document: createEmptySceneDocument(),
|
|
|
|
|
diagnostic: null
|
|
|
|
|
};
|
2026-03-31 01:29:35 +02:00
|
|
|
}
|
|
|
|
|
|
2026-03-31 01:50:29 +02:00
|
|
|
const draftResult = loadSceneDocumentDraft(storage, key);
|
|
|
|
|
|
|
|
|
|
switch (draftResult.status) {
|
|
|
|
|
case "loaded":
|
|
|
|
|
return {
|
|
|
|
|
document: draftResult.document,
|
|
|
|
|
diagnostic: null
|
|
|
|
|
};
|
|
|
|
|
case "missing":
|
|
|
|
|
return {
|
|
|
|
|
document: createEmptySceneDocument(),
|
|
|
|
|
diagnostic: null
|
|
|
|
|
};
|
|
|
|
|
case "error":
|
|
|
|
|
return {
|
|
|
|
|
document: createEmptySceneDocument(),
|
|
|
|
|
diagnostic: `${draftResult.message} Starting with a fresh empty document.`
|
|
|
|
|
};
|
|
|
|
|
}
|
2026-03-31 01:29:35 +02:00
|
|
|
}
|