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 [loadedModel, setLoadedModel] = useState<THREE.Object3D | null>(null);
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
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() {
<span className="hover:text-white cursor-pointer">Help</span>
</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">
<span>Startup</span>
<ChevronDown size={12} />