feat: implement autosave, direct saving, and topbar UI indicators
This commit is contained in:
+70
-8
@@ -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} />
|
||||
|
||||
Reference in New Issue
Block a user