feat: show upload progress for project saving and model imports
Build and Deploy / build-and-push (push) Failing after 28s
Build and Deploy / build-and-push (push) Failing after 28s
This commit is contained in:
+6
-2
@@ -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
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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
@@ -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">
|
||||||
|
|||||||
Reference in New Issue
Block a user