feat: show upload progress for project saving and model imports
Build and Deploy / build-and-push (push) Failing after 28s

This commit is contained in:
AI Bot
2026-09-02 09:59:32 +05:30
parent 324b93d898
commit 35c688ba47
3 changed files with 18 additions and 8 deletions
+6 -2
View File
@@ -111,13 +111,14 @@ export const api = {
}, },
// Storage // Storage
uploadAsset: async (fileBlob) => { uploadAsset: async (fileBlob: Blob | File, onProgress?: (progress: number) => void) => {
const chunkSize = 512 * 1024; // 512KB chunks to bypass 1MB Nginx limits const chunkSize = 512 * 1024; // 512KB chunks to bypass 1MB Nginx limits
const token = localStorage.getItem('auth_token'); const token = localStorage.getItem('auth_token');
const authHeaders = token ? { Authorization: `Bearer ${token}` } : {}; const authHeaders = token ? { Authorization: `Bearer ${token}` } : {};
if (fileBlob.size <= chunkSize) { if (fileBlob.size <= chunkSize) {
// Standard upload for small files // Standard upload for small files
if (onProgress) onProgress(0);
const formData = new FormData(); const formData = new FormData();
formData.append('file', fileBlob); formData.append('file', fileBlob);
@@ -128,9 +129,11 @@ export const api = {
}); });
const data = await res.json(); const data = await res.json();
if (!res.ok) throw new Error(data.error); if (!res.ok) throw new Error(data.error);
if (onProgress) onProgress(100);
return data.url; return data.url;
} else { } else {
// Chunked upload for large files // Chunked upload for large files
if (onProgress) onProgress(0);
const uploadId = Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15); const uploadId = Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
const totalChunks = Math.ceil(fileBlob.size / chunkSize); const totalChunks = Math.ceil(fileBlob.size / chunkSize);
@@ -148,6 +151,7 @@ export const api = {
body: formData body: formData
}); });
if (!res.ok) throw new Error(`Failed to upload chunk ${i}`); if (!res.ok) throw new Error(`Failed to upload chunk ${i}`);
if (onProgress) onProgress(Math.round(((i + 1) / totalChunks) * 100));
} }
const completeHeaders = { const completeHeaders = {
@@ -160,7 +164,7 @@ export const api = {
headers: completeHeaders, headers: completeHeaders,
body: JSON.stringify({ body: JSON.stringify({
uploadId, uploadId,
fileName: fileBlob.name || 'uploaded_file', fileName: (fileBlob as File).name || 'uploaded_file',
totalChunks totalChunks
}) })
}); });
+2 -2
View File
@@ -1,8 +1,8 @@
import { api } from './api'; import { api } from './api';
export const uploadAssetToStorage = async (file: Blob | File, bucket: string, path: string): Promise<string> => { export const uploadAssetToStorage = async (file: Blob | File, bucket: string, path: string, onProgress?: (progress: number) => void): Promise<string> => {
try { try {
const url = await api.uploadAsset(file); const url = await api.uploadAsset(file, onProgress);
return url; return url;
} catch (err) { } catch (err) {
console.warn("Storage upload exception.", err); console.warn("Storage upload exception.", err);
+10 -4
View File
@@ -274,6 +274,7 @@ export default function Editor() {
const [blobURLs, setBlobURLs] = useState<string[]>([]); const [blobURLs, setBlobURLs] = useState<string[]>([]);
const [projectName, setProjectName] = useState<string>(''); const [projectName, setProjectName] = useState<string>('');
const [saveStatus, setSaveStatus] = useState<'saved' | 'saving' | 'unsaved' | 'offline'>('saved'); const [saveStatus, setSaveStatus] = useState<'saved' | 'saving' | 'unsaved' | 'offline'>('saved');
const [saveProgress, setSaveProgress] = useState(0);
const [isOffline, setIsOffline] = useState(!navigator.onLine); const [isOffline, setIsOffline] = useState(!navigator.onLine);
useEffect(() => { useEffect(() => {
@@ -738,8 +739,11 @@ export default function Editor() {
} }
// Upload to Cloud // Upload to Cloud
setSaveProgress(0);
const file = new File([blob], `${projectNameStr.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, (progress) => {
setSaveProgress(progress);
});
const result = await api.saveProject({ const result = await api.saveProject({
id: projectId && projectId !== 'new' ? projectId : undefined, id: projectId && projectId !== 'new' ? projectId : undefined,
@@ -1492,8 +1496,10 @@ export default function Editor() {
if (object) { if (object) {
// Save model data for scene persistence // Save model data for scene persistence
const modelBlob = await fetch(rootFile.url).then(r => r.blob()); const modelBlob = await fetch(rootFile.url).then(r => r.blob());
setImportStatus("Uploading to cloud storage..."); setImportStatus("Uploading to cloud storage... 0%");
const cloudUrl = await uploadAssetToStorage(modelBlob, 'assets', `models/${user?.id || 'guest'}_${Date.now()}_${rootFile.name}`); const cloudUrl = await uploadAssetToStorage(modelBlob, 'assets', `models/${user?.id || 'guest'}_${Date.now()}_${rootFile.name}`, (prog) => {
setImportStatus(`Uploading to cloud storage... ${prog}%`);
});
setModelData({ name: rootFile.name, data: cloudUrl, extension: extension || '' }); setModelData({ name: rootFile.name, data: cloudUrl, extension: extension || '' });
setImportStatus("Optimizing geometry..."); setImportStatus("Optimizing geometry...");
@@ -2126,7 +2132,7 @@ export default function Editor() {
<div className={`flex items-center gap-1 transition-colors ${saveStatus === 'saved' ? 'text-green-500' : saveStatus === 'saving' ? 'text-blue-400' : 'text-zinc-500'}`}> <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 === 'saving' && <Loader2 size={10} className="animate-spin" />}
{saveStatus === 'saved' && <Save size={10} />} {saveStatus === 'saved' && <Save size={10} />}
<span>{saveStatus === 'saved' ? 'Saved' : saveStatus === 'saving' ? 'Saving...' : 'Unsaved changes'}</span> <span>{saveStatus === 'saved' ? 'Saved' : saveStatus === 'saving' ? (saveProgress > 0 && saveProgress < 100 ? `Saving... ${saveProgress}%` : 'Saving...') : 'Unsaved changes'}</span>
</div> </div>
) : null} ) : 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">