diff --git a/src/lib/api.ts b/src/lib/api.ts index 654de77..b779fd1 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -111,13 +111,14 @@ export const api = { }, // 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 token = localStorage.getItem('auth_token'); const authHeaders = token ? { Authorization: `Bearer ${token}` } : {}; if (fileBlob.size <= chunkSize) { // Standard upload for small files + if (onProgress) onProgress(0); const formData = new FormData(); formData.append('file', fileBlob); @@ -128,9 +129,11 @@ export const api = { }); const data = await res.json(); if (!res.ok) throw new Error(data.error); + if (onProgress) onProgress(100); return data.url; } else { // 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 totalChunks = Math.ceil(fileBlob.size / chunkSize); @@ -148,6 +151,7 @@ export const api = { body: formData }); if (!res.ok) throw new Error(`Failed to upload chunk ${i}`); + if (onProgress) onProgress(Math.round(((i + 1) / totalChunks) * 100)); } const completeHeaders = { @@ -160,7 +164,7 @@ export const api = { headers: completeHeaders, body: JSON.stringify({ uploadId, - fileName: fileBlob.name || 'uploaded_file', + fileName: (fileBlob as File).name || 'uploaded_file', totalChunks }) }); diff --git a/src/lib/storageUtils.ts b/src/lib/storageUtils.ts index e03219d..053990f 100644 --- a/src/lib/storageUtils.ts +++ b/src/lib/storageUtils.ts @@ -1,8 +1,8 @@ import { api } from './api'; -export const uploadAssetToStorage = async (file: Blob | File, bucket: string, path: string): Promise => { +export const uploadAssetToStorage = async (file: Blob | File, bucket: string, path: string, onProgress?: (progress: number) => void): Promise => { try { - const url = await api.uploadAsset(file); + const url = await api.uploadAsset(file, onProgress); return url; } catch (err) { console.warn("Storage upload exception.", err); diff --git a/src/pages/Editor.tsx b/src/pages/Editor.tsx index 0b7ca7e..09f7115 100644 --- a/src/pages/Editor.tsx +++ b/src/pages/Editor.tsx @@ -274,6 +274,7 @@ export default function Editor() { const [blobURLs, setBlobURLs] = useState([]); const [projectName, setProjectName] = useState(''); const [saveStatus, setSaveStatus] = useState<'saved' | 'saving' | 'unsaved' | 'offline'>('saved'); + const [saveProgress, setSaveProgress] = useState(0); const [isOffline, setIsOffline] = useState(!navigator.onLine); useEffect(() => { @@ -738,8 +739,11 @@ export default function Editor() { } // Upload to Cloud + setSaveProgress(0); 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({ id: projectId && projectId !== 'new' ? projectId : undefined, @@ -1492,8 +1496,10 @@ export default function Editor() { if (object) { // Save model data for scene persistence const modelBlob = await fetch(rootFile.url).then(r => r.blob()); - setImportStatus("Uploading to cloud storage..."); - const cloudUrl = await uploadAssetToStorage(modelBlob, 'assets', `models/${user?.id || 'guest'}_${Date.now()}_${rootFile.name}`); + setImportStatus("Uploading to cloud storage... 0%"); + 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 || '' }); setImportStatus("Optimizing geometry..."); @@ -2126,7 +2132,7 @@ export default function Editor() {
{saveStatus === 'saving' && } {saveStatus === 'saved' && } - {saveStatus === 'saved' ? 'Saved' : saveStatus === 'saving' ? 'Saving...' : 'Unsaved changes'} + {saveStatus === 'saved' ? 'Saved' : saveStatus === 'saving' ? (saveProgress > 0 && saveProgress < 100 ? `Saving... ${saveProgress}%` : 'Saving...') : 'Unsaved changes'}
) : null}