feat: implement autosave, direct saving, and topbar UI indicators

This commit is contained in:
AI Bot
2026-08-07 19:35:40 +05:30
parent 491884a28e
commit 6fe2f490be
+70 -8
View File
@@ -272,6 +272,20 @@ export default function Editor() {
const [theme, setTheme] = useState<"light" | "dark">("dark"); const [theme, setTheme] = useState<"light" | "dark">("dark");
const [loadedModel, setLoadedModel] = useState<THREE.Object3D | null>(null); const [loadedModel, setLoadedModel] = useState<THREE.Object3D | null>(null);
const [blobURLs, setBlobURLs] = useState<string[]>([]); const [blobURLs, setBlobURLs] = useState<string[]>([]);
const [projectName, setProjectName] = useState<string>('');
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 // Cleanup blob URLs on unmount
useEffect(() => { useEffect(() => {
@@ -314,8 +328,10 @@ export default function Editor() {
setIsLoading(true); setIsLoading(true);
try { try {
const data = await api.getProject(projectId); const data = await api.getProject(projectId);
if (data.project && data.project.scene_data) { if (data.project) {
let sd = data.project.scene_data; if (data.project.name) setProjectName(data.project.name);
if (data.project.scene_data) {
let sd = data.project.scene_data;
if (sd.titansceneUrl) { if (sd.titansceneUrl) {
try { try {
@@ -680,7 +696,13 @@ export default function Editor() {
if (sceneInputRef.current) sceneInputRef.current.value = ''; 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 = { const sceneData = {
sceneObjects, lightProps, groundProps, cameraProps, sceneObjects, lightProps, groundProps, cameraProps,
envPreset, envRotation, backgroundColor, backgroundType, envPreset, envRotation, backgroundColor, backgroundType,
@@ -688,7 +710,7 @@ export default function Editor() {
modelData: modelData ? { ...modelData } : null modelData: modelData ? { ...modelData } : null
}; };
const projectName = name || exportDialogName || 'Untitled Scene'; const projectNameStr = name || exportDialogName || projectName || 'Untitled Scene';
try { try {
setIsRendering(true); setIsRendering(true);
@@ -728,25 +750,48 @@ export default function Editor() {
} }
// Upload to Cloud // 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 fileUrl = await api.uploadAsset(file);
const result = await api.saveProject({ const result = await api.saveProject({
id: projectId && projectId !== 'new' ? projectId : undefined, id: projectId && projectId !== 'new' ? projectId : undefined,
name: projectName, name: projectNameStr,
scene_data: { titansceneUrl: fileUrl } scene_data: { titansceneUrl: fileUrl }
}); });
if (projectId === 'new' && result.id) { if (projectId === 'new' && result.id) {
navigate(`/editor/${result.id}`, { replace: true }); navigate(`/editor/${result.id}`, { replace: true });
} }
setSaveStatus('saved');
} catch (error) { } catch (error) {
console.error("Failed to save project to cloud", error); console.error("Failed to save project to cloud", error);
setSaveStatus('unsaved');
} finally { } finally {
setIsRendering(false); setIsRendering(false);
} }
setExportDialog(null); 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 exportTitanScene = async (name: string) => {
const sceneData = { const sceneData = {
sceneObjects, lightProps, groundProps, cameraProps, sceneObjects, lightProps, groundProps, cameraProps,
@@ -802,7 +847,12 @@ export default function Editor() {
}; };
const openSaveDialog = () => { 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); setExportDialogName(defaultName);
setExportDialog({ mode: 'scene', defaultName }); setExportDialog({ mode: 'scene', defaultName });
setOpenMenu(null); setOpenMenu(null);
@@ -2078,7 +2128,19 @@ export default function Editor() {
<span className="hover:text-white cursor-pointer">Help</span> <span className="hover:text-white cursor-pointer">Help</span>
</div> </div>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-4">
{isOffline ? (
<div className="flex items-center gap-1 text-red-500 font-medium">
<span className="w-1.5 h-1.5 rounded-full bg-red-500 animate-pulse" />
Offline
</div>
) : projectId && projectId !== 'new' ? (
<div className={`flex items-center gap-1 transition-colors ${saveStatus === 'saved' ? 'text-green-500' : saveStatus === 'saving' ? 'text-blue-400' : 'text-zinc-500'}`}>
{saveStatus === 'saving' && <Loader2 size={10} className="animate-spin" />}
{saveStatus === 'saved' && <Save size={10} />}
<span>{saveStatus === 'saved' ? 'Saved' : saveStatus === 'saving' ? 'Saving...' : 'Unsaved changes'}</span>
</div>
) : null}
<div className="flex items-center gap-1 bg-white dark:bg-zinc-800 px-2 py-0.5 rounded"> <div className="flex items-center gap-1 bg-white dark:bg-zinc-800 px-2 py-0.5 rounded">
<span>Startup</span> <span>Startup</span>
<ChevronDown size={12} /> <ChevronDown size={12} />