From 6fe2f490be8305f9e0d9b73c74108703e7863af3 Mon Sep 17 00:00:00 2001 From: AI Bot Date: Fri, 7 Aug 2026 19:35:40 +0530 Subject: [PATCH] feat: implement autosave, direct saving, and topbar UI indicators --- src/pages/Editor.tsx | 78 +++++++++++++++++++++++++++++++++++++++----- 1 file changed, 70 insertions(+), 8 deletions(-) diff --git a/src/pages/Editor.tsx b/src/pages/Editor.tsx index d41915b..f4a0b24 100644 --- a/src/pages/Editor.tsx +++ b/src/pages/Editor.tsx @@ -272,6 +272,20 @@ export default function Editor() { const [theme, setTheme] = useState<"light" | "dark">("dark"); const [loadedModel, setLoadedModel] = useState(null); const [blobURLs, setBlobURLs] = useState([]); + const [projectName, setProjectName] = useState(''); + const [saveStatus, setSaveStatus] = useState<'saved' | 'saving' | 'unsaved' | 'offline'>('saved'); + const [isOffline, setIsOffline] = useState(!navigator.onLine); + + useEffect(() => { + const handleOnline = () => setIsOffline(false); + const handleOffline = () => setIsOffline(true); + window.addEventListener('online', handleOnline); + window.addEventListener('offline', handleOffline); + return () => { + window.removeEventListener('online', handleOnline); + window.removeEventListener('offline', handleOffline); + }; + }, []); // Cleanup blob URLs on unmount useEffect(() => { @@ -314,8 +328,10 @@ export default function Editor() { setIsLoading(true); try { const data = await api.getProject(projectId); - if (data.project && data.project.scene_data) { - let sd = data.project.scene_data; + if (data.project) { + if (data.project.name) setProjectName(data.project.name); + if (data.project.scene_data) { + let sd = data.project.scene_data; if (sd.titansceneUrl) { try { @@ -680,7 +696,13 @@ export default function Editor() { if (sceneInputRef.current) sceneInputRef.current.value = ''; }; - const saveScene = async (name?: string) => { + const saveScene = async (name: string) => { + if (!name) { + alert("Please provide a valid file name."); + return; + } + setSaveStatus('saving'); + const sceneData = { sceneObjects, lightProps, groundProps, cameraProps, envPreset, envRotation, backgroundColor, backgroundType, @@ -688,7 +710,7 @@ export default function Editor() { modelData: modelData ? { ...modelData } : null }; - const projectName = name || exportDialogName || 'Untitled Scene'; + const projectNameStr = name || exportDialogName || projectName || 'Untitled Scene'; try { setIsRendering(true); @@ -728,25 +750,48 @@ export default function Editor() { } // Upload to Cloud - const file = new File([blob], `${projectName.replace(/[^a-z0-9]/gi, '_').toLowerCase()}.titanscene`, { type: 'application/octet-stream' }); + const file = new File([blob], `${projectNameStr.replace(/[^a-z0-9]/gi, '_').toLowerCase()}.titanscene`, { type: 'application/octet-stream' }); const fileUrl = await api.uploadAsset(file); const result = await api.saveProject({ id: projectId && projectId !== 'new' ? projectId : undefined, - name: projectName, + name: projectNameStr, scene_data: { titansceneUrl: fileUrl } }); if (projectId === 'new' && result.id) { navigate(`/editor/${result.id}`, { replace: true }); } + setSaveStatus('saved'); } catch (error) { console.error("Failed to save project to cloud", error); + setSaveStatus('unsaved'); } finally { setIsRendering(false); } setExportDialog(null); }; + const saveSceneRef = useRef(saveScene); + saveSceneRef.current = saveScene; + + // Track changes to mark as unsaved + useEffect(() => { + if (saveStatus !== 'unsaved' && saveStatus !== 'saving') { + setSaveStatus('unsaved'); + } + }, [sceneObjects, lightProps, groundProps, cameraProps, envPreset, envRotation, backgroundColor, backgroundType, customHdri, backplateImage, additionalLights, modelBounds, cameraPresets, modelData]); + + // Autosave loop + useEffect(() => { + if (saveStatus !== 'unsaved' || isOffline || !projectId || projectId === 'new' || !projectName) return; + + const interval = setInterval(() => { + saveSceneRef.current(projectName); + }, 120000); // 2 minutes + + return () => clearInterval(interval); + }, [saveStatus, isOffline, projectId, projectName]); + const exportTitanScene = async (name: string) => { const sceneData = { sceneObjects, lightProps, groundProps, cameraProps, @@ -802,7 +847,12 @@ export default function Editor() { }; const openSaveDialog = () => { - const defaultName = modelData?.name ? modelData.name.replace(/\.[^.]+$/, '') : 'scene'; + if (projectId && projectId !== 'new' && projectName) { + saveScene(projectName); + setOpenMenu(null); + return; + } + const defaultName = projectName || (modelData?.name ? modelData.name.replace(/\.[^.]+$/, '') : 'scene'); setExportDialogName(defaultName); setExportDialog({ mode: 'scene', defaultName }); setOpenMenu(null); @@ -2078,7 +2128,19 @@ export default function Editor() { Help -
+
+ {isOffline ? ( +
+ + Offline +
+ ) : projectId && projectId !== 'new' ? ( +
+ {saveStatus === 'saving' && } + {saveStatus === 'saved' && } + {saveStatus === 'saved' ? 'Saved' : saveStatus === 'saving' ? 'Saving...' : 'Unsaved changes'} +
+ ) : null}
Startup