From 10d21f42c912e588864702d69289e258829c41ce Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Thu, 14 May 2026 11:58:09 +0000 Subject: [PATCH 1/7] Changes Co-authored-by: Reifonas <211114984+Reifonas@users.noreply.github.com> --- src/components/CloudLoader.tsx | 214 +++++++++++++++++++++++++++++++++ 1 file changed, 214 insertions(+) create mode 100644 src/components/CloudLoader.tsx diff --git a/src/components/CloudLoader.tsx b/src/components/CloudLoader.tsx new file mode 100644 index 0000000..f25c910 --- /dev/null +++ b/src/components/CloudLoader.tsx @@ -0,0 +1,214 @@ +import { useState, useCallback } from 'react'; +import { Cloud, Loader2, Link2 } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { + Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter, DialogTrigger, +} from '@/components/ui/dialog'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { useModelStore } from '@/stores/useModelStore'; +import { toast } from 'sonner'; +import { getSupportedExtension, convertToGLB } from '@/lib/convertToGLB'; +import { convertIFCtoGLB } from '@/lib/convertIFC'; + +const DEFAULT_URL = 'https://store.tracksteel.com.br/demo.ifc'; + +const PRESETS: { label: string; url: string }[] = [ + { label: 'Demo IFC (TrackSteel)', url: DEFAULT_URL }, +]; + +/** Normaliza URLs de Dropbox / OneDrive para apontar ao binário direto. */ +function normalizeCloudUrl(input: string): string { + try { + const u = new URL(input); + if (/(^|\.)dropbox\.com$/.test(u.hostname)) { + u.searchParams.set('dl', '1'); + return u.toString(); + } + if (/(^|\.)1drv\.ms$|onedrive\.live\.com$/.test(u.hostname)) { + u.searchParams.set('download', '1'); + return u.toString(); + } + return input; + } catch { + return input; + } +} + +/** Tenta obter um token JWT de uma instância FileBrowser NoAuth (POST /api/login vazio). */ +async function tryFileBrowserToken(origin: string): Promise { + try { + const r = await fetch(`${origin}/api/login`, { method: 'POST', mode: 'cors' }); + if (!r.ok) return null; + const txt = (await r.text()).trim(); + // FileBrowser devolve o JWT como texto puro + if (txt && txt.split('.').length === 3) return txt; + return null; + } catch { + return null; + } +} + +/** Faz fetch tentando vários endpoints típicos de hosts que servem SPA (FileBrowser etc.). */ +async function smartFetch(rawUrl: string): Promise { + const url = normalizeCloudUrl(rawUrl.trim()); + let res = await fetch(url, { mode: 'cors' }); + if (!res.ok) throw new Error(`HTTP ${res.status}`); + const ct = res.headers.get('content-type') || ''; + if (!ct.includes('text/html')) return res; + + // Servidor devolveu HTML — tentamos endpoints binários do FileBrowser + const u = new URL(url); + const path = u.pathname.startsWith('/api/raw/') ? u.pathname : `/api/raw${u.pathname}`; + + // 1ª tentativa: /api/raw/... cru + try { + const r = await fetch(`${u.origin}${path}${u.search}`, { mode: 'cors' }); + if (r.ok && !(r.headers.get('content-type') || '').includes('text/html')) return r; + if (r.status !== 401) { + // pode ser outro erro — segue para fluxo de token + } + } catch { /* continua */ } + + // 2ª tentativa: pega token JWT NoAuth e usa ?auth= + const token = await tryFileBrowserToken(u.origin); + if (token) { + const sep = u.search ? '&' : '?'; + const r = await fetch(`${u.origin}${path}${u.search}${sep}auth=${encodeURIComponent(token)}`, { mode: 'cors' }); + if (r.ok && !(r.headers.get('content-type') || '').includes('text/html')) return r; + } + + throw new Error( + 'O servidor devolveu HTML. Use um link DIRETO do arquivo (ex.: Dropbox ?dl=1, OneDrive ?download=1, ' + + 'ou habilite endpoint público no FileBrowser).' + ); +} + +interface CloudLoaderProps { + /** Renderiza como botão compacto (para HUD do AR) */ + compact?: boolean; + /** Variante visual do botão */ + variant?: 'default' | 'outline'; + className?: string; +} + +export function CloudLoader({ compact = false, variant = 'outline', className }: CloudLoaderProps) { + const setModel = useModelStore((s) => s.setModel); + const [open, setOpen] = useState(false); + const [url, setUrl] = useState(DEFAULT_URL); + const [loading, setLoading] = useState(false); + + const handleLoad = useCallback(async (rawUrl: string) => { + if (!rawUrl) return; + setLoading(true); + try { + const fileName = rawUrl.split('/').pop()?.split('?')[0] || 'modelo-cloud'; + const ext = getSupportedExtension(fileName); + if (!ext) { + toast.error('URL deve apontar para .GLB, .OBJ, .STL ou .IFC'); + return; + } + + const response = await smartFetch(rawUrl); + const buffer = await response.arrayBuffer(); + + let blob: Blob, outName = fileName, outSize = buffer.byteLength; + if (ext === 'glb') { + blob = new Blob([buffer], { type: 'model/gltf-binary' }); + } else if (ext === 'ifc') { + const r = await convertIFCtoGLB(buffer, fileName); + blob = r.blob; outName = r.fileName; outSize = r.fileSize; + } else { + const r = await convertToGLB(buffer, ext, fileName); + blob = r.blob; outName = r.fileName; outSize = r.fileSize; + } + const blobUrl = URL.createObjectURL(blob); + setModel({ fileName: outName, fileSize: outSize, url: blobUrl }); + toast.success(`"${outName}" carregado da nuvem`); + setOpen(false); + } catch (err) { + console.error(err); + toast.error(err instanceof Error ? err.message : 'Falha ao carregar da nuvem (verifique URL/CORS)'); + } finally { + setLoading(false); + } + }, [setModel]); + + return ( + + + {compact ? ( + + ) : ( + + )} + + + + + + Carregar modelo da nuvem + + + URL direta para .GLB, .OBJ, .STL ou .IFC. Dropbox use ?dl=1, OneDrive ?download=1. + FileBrowser é detectado automaticamente. + + + +
+
+ +
+ + setUrl(e.target.value)} + placeholder="https://exemplo.com/modelo.glb" + disabled={loading} + className="font-mono text-xs" + /> +
+
+ + {PRESETS.length > 0 && ( +
+ +
+ {PRESETS.map((p) => ( + + ))} +
+
+ )} +
+ + + + + +
+
+ ); +} From 566b42500286f1c392782e407893d376bf0b6fb2 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Thu, 14 May 2026 11:58:20 +0000 Subject: [PATCH 2/7] Changes Co-authored-by: Reifonas <211114984+Reifonas@users.noreply.github.com> --- src/pages/Index.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/pages/Index.tsx b/src/pages/Index.tsx index 43f17bd..95a4092 100644 --- a/src/pages/Index.tsx +++ b/src/pages/Index.tsx @@ -1,11 +1,12 @@ import { useEffect, useRef, useCallback, useState } from 'react'; import { useNavigate } from 'react-router-dom'; -import { Upload, Glasses, CheckCircle, XCircle, Loader2, Box, Package, Cloud } from 'lucide-react'; +import { Upload, Glasses, CheckCircle, XCircle, Loader2, Box, Package } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { useModelStore } from '@/stores/useModelStore'; import { toast } from 'sonner'; import { getSupportedExtension, convertToGLB, ACCEPTED_EXTENSIONS } from '@/lib/convertToGLB'; import { convertIFCtoGLB } from '@/lib/convertIFC'; +import { CloudLoader } from '@/components/CloudLoader'; /** * Normaliza URLs comuns para apontar ao binário direto. From 04f7b1639cf8de5e3ab0a323ddc2fe5caa877854 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Thu, 14 May 2026 11:58:27 +0000 Subject: [PATCH 3/7] Changes Co-authored-by: Reifonas <211114984+Reifonas@users.noreply.github.com> --- src/pages/Index.tsx | 22 ---------------------- 1 file changed, 22 deletions(-) diff --git a/src/pages/Index.tsx b/src/pages/Index.tsx index 95a4092..b1f2dd5 100644 --- a/src/pages/Index.tsx +++ b/src/pages/Index.tsx @@ -8,28 +8,6 @@ import { getSupportedExtension, convertToGLB, ACCEPTED_EXTENSIONS } from '@/lib/ import { convertIFCtoGLB } from '@/lib/convertIFC'; import { CloudLoader } from '@/components/CloudLoader'; -/** - * Normaliza URLs comuns para apontar ao binário direto. - * - Dropbox: força dl=1 - * - OneDrive (1drv.ms / onedrive.live.com): força download=1 - */ -function normalizeCloudUrl(input: string): string { - try { - const u = new URL(input); - if (/(^|\.)dropbox\.com$/.test(u.hostname)) { - u.searchParams.set('dl', '1'); - return u.toString(); - } - if (/(^|\.)1drv\.ms$|onedrive\.live\.com$/.test(u.hostname)) { - u.searchParams.set('download', '1'); - return u.toString(); - } - return input; - } catch { - return input; - } -} - const Index = () => { const navigate = useNavigate(); const fileInputRef = useRef(null); From 6ae5bed3192d419bd500cc99b7192a68059d1624 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Thu, 14 May 2026 11:58:38 +0000 Subject: [PATCH 4/7] Changes Co-authored-by: Reifonas <211114984+Reifonas@users.noreply.github.com> --- src/pages/Index.tsx | 67 +-------------------------------------------- 1 file changed, 1 insertion(+), 66 deletions(-) diff --git a/src/pages/Index.tsx b/src/pages/Index.tsx index b1f2dd5..ac1f499 100644 --- a/src/pages/Index.tsx +++ b/src/pages/Index.tsx @@ -63,72 +63,7 @@ const Index = () => { } }, [setModel]); - const handleLoadCloud = useCallback(async () => { - const DEFAULT_URL = 'https://store.tracksteel.com.br/demo.ifc'; - const raw = window.prompt( - 'URL do modelo (GLB · OBJ · STL · IFC).\nDica: Dropbox use ?dl=1 · OneDrive ?download=1 · FileBrowser /api/raw/', - DEFAULT_URL, - ); - if (!raw) return; - setLoadingCloud(true); - try { - // Normalizações automáticas para hosts comuns - const url = normalizeCloudUrl(raw.trim()); - const fileName = url.split('/').pop()?.split('?')[0] || 'modelo-cloud'; - const ext = getSupportedExtension(fileName); - if (!ext) { - toast.error('URL deve apontar para .GLB, .OBJ, .STL ou .IFC'); - return; - } - - let response = await fetch(url, { mode: 'cors' }); - if (!response.ok) throw new Error(`HTTP ${response.status}`); - - // Detecta servidores que retornam HTML em vez do binário (ex.: FileBrowser SPA) - let ct = response.headers.get('content-type') || ''; - if (ct.includes('text/html')) { - // Fallback: tenta endpoint /api/raw/ do FileBrowser - try { - const u = new URL(url); - if (!u.pathname.startsWith('/api/raw/')) { - const fbUrl = `${u.origin}/api/raw${u.pathname}${u.search}`; - const r2 = await fetch(fbUrl, { mode: 'cors' }); - if (r2.ok) { - response = r2; - ct = r2.headers.get('content-type') || ''; - } - } - } catch {} - } - if (ct.includes('text/html')) { - toast.error('O servidor devolveu uma página HTML, não o arquivo. Use o link DIRETO do arquivo (ex.: FileBrowser → /api/raw/arquivo.ifc).'); - return; - } - - const buffer = await response.arrayBuffer(); - - let blob: Blob; - let outName = fileName; - let outSize = buffer.byteLength; - if (ext === 'glb') { - blob = new Blob([buffer], { type: 'model/gltf-binary' }); - } else if (ext === 'ifc') { - const r = await convertIFCtoGLB(buffer, fileName); - blob = r.blob; outName = r.fileName; outSize = r.fileSize; - } else { - const r = await convertToGLB(buffer, ext, fileName); - blob = r.blob; outName = r.fileName; outSize = r.fileSize; - } - const blobUrl = URL.createObjectURL(blob); - setModel({ fileName: outName, fileSize: outSize, url: blobUrl }); - toast.success(`"${outName}" carregado da nuvem (RAM)`); - } catch (err) { - console.error(err); - toast.error('Falha ao carregar da nuvem (verifique URL/CORS)'); - } finally { - setLoadingCloud(false); - } - }, [setModel]); + // Carregar da Nuvem agora é encapsulado em const handleLoadDemoIFC = useCallback(async () => { setLoadingDemoIFC(true); From b87bc3b091f15960e9d7771aba67c253b5da920a Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Thu, 14 May 2026 11:58:47 +0000 Subject: [PATCH 5/7] Changes Co-authored-by: Reifonas <211114984+Reifonas@users.noreply.github.com> --- src/pages/Index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/Index.tsx b/src/pages/Index.tsx index ac1f499..6d92d64 100644 --- a/src/pages/Index.tsx +++ b/src/pages/Index.tsx @@ -13,7 +13,7 @@ const Index = () => { const fileInputRef = useRef(null); const [loadingDemoIFC, setLoadingDemoIFC] = useState(false); const [loadingDemoCotovelo, setLoadingDemoCotovelo] = useState(false); - const [loadingCloud, setLoadingCloud] = useState(false); + const [converting, setConverting] = useState(false); const { model, setModel, xrSupported, setXrSupported } = useModelStore(); From 3651d05222f16cc8bd2cbc8a2e3349dfbb58d531 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Thu, 14 May 2026 11:58:54 +0000 Subject: [PATCH 6/7] Changes Co-authored-by: Reifonas <211114984+Reifonas@users.noreply.github.com> --- src/pages/Index.tsx | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/pages/Index.tsx b/src/pages/Index.tsx index 6d92d64..3f5cff1 100644 --- a/src/pages/Index.tsx +++ b/src/pages/Index.tsx @@ -148,14 +148,7 @@ const Index = () => { {/* Demo buttons */}
- + +

Modo XR Imersivo