feat: full studio build -- light theme, canvas thumbnails, i18n (fa/en)
This commit is contained in:
@@ -0,0 +1,140 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
|
||||
import { useDebouncedValue } from "@/hooks/useDebouncedValue";
|
||||
import { fetchProject, patchProjectSceneData } from "@/lib/project-api";
|
||||
import { isDevProjectId } from "@/lib/project-ids";
|
||||
import { isImageSceneDataEmpty } from "@/lib/image-scene-data";
|
||||
import { useImageEditorStore } from "@/lib/image-editor-store";
|
||||
import {
|
||||
PROJECT_SAVE_DEBOUNCE_MS,
|
||||
PROJECT_SAVED_DISPLAY_MS,
|
||||
type ProjectSaveStatus,
|
||||
} from "@/lib/project-save-status";
|
||||
|
||||
export interface UseImageProjectPersistenceResult {
|
||||
projectName: string;
|
||||
setProjectName: (name: string) => void;
|
||||
saveStatus: ProjectSaveStatus;
|
||||
retrySave: () => void;
|
||||
}
|
||||
|
||||
export function useImageProjectPersistence(
|
||||
projectId: string | undefined
|
||||
): UseImageProjectPersistenceResult {
|
||||
const layers = useImageEditorStore((state) => state.layers);
|
||||
const canvasWidth = useImageEditorStore((state) => state.canvasWidth);
|
||||
const canvasHeight = useImageEditorStore((state) => state.canvasHeight);
|
||||
const adjustments = useImageEditorStore((state) => state.adjustments);
|
||||
const activeFilterPreset = useImageEditorStore(
|
||||
(state) => state.activeFilterPreset
|
||||
);
|
||||
const hydrateFromSceneData = useImageEditorStore(
|
||||
(state) => state.hydrateFromSceneData
|
||||
);
|
||||
const getSceneDataForSave = useImageEditorStore(
|
||||
(state) => state.getSceneDataForSave
|
||||
);
|
||||
|
||||
const [projectName, setProjectName] = useState("Untitled image");
|
||||
const [saveStatus, setSaveStatus] = useState<ProjectSaveStatus>("idle");
|
||||
|
||||
const skipSaveRef = useRef(true);
|
||||
const lastSavedPayloadRef = useRef<string | null>(null);
|
||||
const savedTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
|
||||
const persistPayload = useMemo(
|
||||
() => JSON.stringify(getSceneDataForSave()),
|
||||
[layers, canvasWidth, canvasHeight, adjustments, activeFilterPreset]
|
||||
);
|
||||
|
||||
const debouncedPayload = useDebouncedValue(
|
||||
persistPayload,
|
||||
PROJECT_SAVE_DEBOUNCE_MS
|
||||
);
|
||||
|
||||
const clearSavedTimer = useCallback(() => {
|
||||
if (savedTimerRef.current) {
|
||||
clearTimeout(savedTimerRef.current);
|
||||
savedTimerRef.current = null;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const performSave = useCallback(
|
||||
async (payloadJson: string) => {
|
||||
if (!projectId || isDevProjectId(projectId)) return;
|
||||
|
||||
setSaveStatus("saving");
|
||||
try {
|
||||
const scene_data = JSON.parse(payloadJson) as Record<string, unknown>;
|
||||
await patchProjectSceneData(projectId, scene_data);
|
||||
lastSavedPayloadRef.current = payloadJson;
|
||||
setSaveStatus("saved");
|
||||
clearSavedTimer();
|
||||
savedTimerRef.current = setTimeout(() => {
|
||||
setSaveStatus((current) => (current === "saved" ? "idle" : current));
|
||||
}, PROJECT_SAVED_DISPLAY_MS);
|
||||
} catch {
|
||||
setSaveStatus("error");
|
||||
}
|
||||
},
|
||||
[projectId, clearSavedTimer]
|
||||
);
|
||||
|
||||
const retrySave = useCallback(() => {
|
||||
void performSave(persistPayload);
|
||||
}, [performSave, persistPayload]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!projectId || isDevProjectId(projectId)) return;
|
||||
|
||||
let cancelled = false;
|
||||
skipSaveRef.current = true;
|
||||
|
||||
void (async () => {
|
||||
try {
|
||||
const { project } = await fetchProject(projectId);
|
||||
if (cancelled) return;
|
||||
|
||||
setProjectName(project.name);
|
||||
|
||||
if (!isImageSceneDataEmpty(project.scene_data)) {
|
||||
hydrateFromSceneData(project.scene_data);
|
||||
}
|
||||
|
||||
lastSavedPayloadRef.current = JSON.stringify(
|
||||
useImageEditorStore.getState().getSceneDataForSave()
|
||||
);
|
||||
} catch {
|
||||
if (!cancelled) {
|
||||
setSaveStatus("error");
|
||||
}
|
||||
} finally {
|
||||
if (!cancelled) {
|
||||
skipSaveRef.current = false;
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [projectId, hydrateFromSceneData]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!projectId || isDevProjectId(projectId) || skipSaveRef.current) return;
|
||||
if (persistPayload === lastSavedPayloadRef.current) return;
|
||||
setSaveStatus("pending");
|
||||
}, [projectId, persistPayload]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!projectId || isDevProjectId(projectId) || skipSaveRef.current) return;
|
||||
if (debouncedPayload === lastSavedPayloadRef.current) return;
|
||||
void performSave(debouncedPayload);
|
||||
}, [projectId, debouncedPayload, performSave]);
|
||||
|
||||
useEffect(() => clearSavedTimer, [clearSavedTimer]);
|
||||
|
||||
return { projectName, setProjectName, saveStatus, retrySave };
|
||||
}
|
||||
Reference in New Issue
Block a user