From 1eb924b809ae7cab21a277f859453becf275fa07 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Wed, 20 May 2026 18:22:17 +0000 Subject: [PATCH 01/10] Changes Co-authored-by: Reifonas <211114984+Reifonas@users.noreply.github.com> --- src/lib/recentFiles.ts | 122 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 122 insertions(+) create mode 100644 src/lib/recentFiles.ts diff --git a/src/lib/recentFiles.ts b/src/lib/recentFiles.ts new file mode 100644 index 0000000..3f1b92e --- /dev/null +++ b/src/lib/recentFiles.ts @@ -0,0 +1,122 @@ +// Armazena os arquivos GLB recentes (após conversão) para reuso rápido, +// sem necessidade de recarregar da nuvem ou do disco local. +// +// Estratégia: +// - Metadados (lista JSON ordenada) em localStorage → leitura síncrona / rápida +// - Blob real do GLB em IndexedDB → suporta arquivos grandes + +const META_KEY = 'tsxr_recent_files_v1'; +const DB_NAME = 'tsxr-recent'; +const STORE = 'glb'; +const MAX_RECENTS = 6; + +export interface RecentFileMeta { + id: string; // chave no IndexedDB + fileName: string; // nome do arquivo GLB + fileSize: number; // bytes + addedAt: number; // timestamp ms +} + +// ── IndexedDB helpers ───────────────────────────────────────────── +function openDB(): Promise { + return new Promise((resolve, reject) => { + const req = indexedDB.open(DB_NAME, 1); + req.onupgradeneeded = () => { + const db = req.result; + if (!db.objectStoreNames.contains(STORE)) db.createObjectStore(STORE); + }; + req.onsuccess = () => resolve(req.result); + req.onerror = () => reject(req.error); + }); +} + +async function idbPut(id: string, blob: Blob): Promise { + const db = await openDB(); + await new Promise((resolve, reject) => { + const tx = db.transaction(STORE, 'readwrite'); + tx.objectStore(STORE).put(blob, id); + tx.oncomplete = () => resolve(); + tx.onerror = () => reject(tx.error); + }); + db.close(); +} + +async function idbGet(id: string): Promise { + const db = await openDB(); + const blob = await new Promise((resolve, reject) => { + const tx = db.transaction(STORE, 'readonly'); + const req = tx.objectStore(STORE).get(id); + req.onsuccess = () => resolve(req.result as Blob | undefined); + req.onerror = () => reject(req.error); + }); + db.close(); + return blob; +} + +async function idbDelete(id: string): Promise { + const db = await openDB(); + await new Promise((resolve, reject) => { + const tx = db.transaction(STORE, 'readwrite'); + tx.objectStore(STORE).delete(id); + tx.oncomplete = () => resolve(); + tx.onerror = () => reject(tx.error); + }); + db.close(); +} + +// ── Metadata helpers ───────────────────────────────────────────── +export function listRecentFiles(): RecentFileMeta[] { + try { + const raw = localStorage.getItem(META_KEY); + if (!raw) return []; + const parsed = JSON.parse(raw) as RecentFileMeta[]; + return Array.isArray(parsed) ? parsed : []; + } catch { + return []; + } +} + +function saveMeta(list: RecentFileMeta[]) { + try { localStorage.setItem(META_KEY, JSON.stringify(list)); } catch {} +} + +/** Registra um GLB nos recentes (substitui se mesmo fileName) e mantém apenas MAX_RECENTS. */ +export async function addRecentFile(fileName: string, blob: Blob): Promise { + try { + const list = listRecentFiles(); + const existing = list.find(r => r.fileName === fileName); + const id = existing?.id ?? `rf_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 6)}`; + await idbPut(id, blob); + const filtered = list.filter(r => r.fileName !== fileName); + const next: RecentFileMeta[] = [ + { id, fileName, fileSize: blob.size, addedAt: Date.now() }, + ...filtered, + ].slice(0, MAX_RECENTS); + // Limpa blobs removidos + for (const r of filtered.slice(MAX_RECENTS - 1)) { + try { await idbDelete(r.id); } catch {} + } + saveMeta(next); + } catch (e) { + console.warn('[recentFiles] addRecent failed', e); + } +} + +/** Recupera o blob de um recente e devolve um object URL pronto para uso. */ +export async function loadRecentFile(id: string): Promise<{ url: string; blob: Blob } | null> { + const blob = await idbGet(id); + if (!blob) return null; + return { url: URL.createObjectURL(blob), blob }; +} + +export async function removeRecentFile(id: string): Promise { + const list = listRecentFiles().filter(r => r.id !== id); + saveMeta(list); + try { await idbDelete(id); } catch {} +} + +export async function clearRecentFiles(): Promise { + const list = listRecentFiles(); + for (const r of list) { try { await idbDelete(r.id); } catch {} } + saveMeta([]); +} From 97941de7bfc880218f9ddef7a15f76aeecf05f19 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Wed, 20 May 2026 18:22:39 +0000 Subject: [PATCH 02/10] Changes Co-authored-by: Reifonas <211114984+Reifonas@users.noreply.github.com> --- src/components/RecentFilesList.tsx | 131 +++++++++++++++++++++++++++++ 1 file changed, 131 insertions(+) create mode 100644 src/components/RecentFilesList.tsx diff --git a/src/components/RecentFilesList.tsx b/src/components/RecentFilesList.tsx new file mode 100644 index 0000000..62b18b3 --- /dev/null +++ b/src/components/RecentFilesList.tsx @@ -0,0 +1,131 @@ +import { useEffect, useState, useCallback } from 'react'; +import { Clock, Trash2, Loader2, FileBox } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { listRecentFiles, loadRecentFile, removeRecentFile, type RecentFileMeta } from '@/lib/recentFiles'; +import { useModelStore } from '@/stores/useModelStore'; +import { toast } from 'sonner'; + +function formatSize(bytes: number): string { + if (bytes < 1024) return `${bytes} B`; + if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; + return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; +} + +function formatWhen(ts: number): string { + const diff = Date.now() - ts; + const m = Math.floor(diff / 60000); + if (m < 1) return 'agora'; + if (m < 60) return `${m} min`; + const h = Math.floor(m / 60); + if (h < 24) return `${h} h`; + const d = Math.floor(h / 24); + return `${d} d`; +} + +export function RecentFilesList() { + const [items, setItems] = useState([]); + const [loadingId, setLoadingId] = useState(null); + const addModel = useModelStore((s) => s.addModel); + const models = useModelStore((s) => s.models); + const maxModels = useModelStore((s) => s.maxModels); + + const refresh = useCallback(() => setItems(listRecentFiles()), []); + + useEffect(() => { + refresh(); + const onStorage = (e: StorageEvent) => { + if (e.key === 'tsxr_recent_files_v1') refresh(); + }; + window.addEventListener('storage', onStorage); + // Polling leve para refletir adições no mesmo tab + const t = setInterval(refresh, 2000); + return () => { window.removeEventListener('storage', onStorage); clearInterval(t); }; + }, [refresh]); + + const handleLoad = useCallback(async (meta: RecentFileMeta) => { + if (models.length >= maxModels) { + toast.error(`Limite de ${maxModels} peças simultâneas atingido`); + return; + } + setLoadingId(meta.id); + try { + const res = await loadRecentFile(meta.id); + if (!res) { + toast.error('Arquivo recente não encontrado — pode ter sido limpo'); + await removeRecentFile(meta.id); + refresh(); + return; + } + addModel({ fileName: meta.fileName, fileSize: meta.fileSize, url: res.url }); + toast.success(`"${meta.fileName}" carregado dos recentes`); + } catch (e) { + console.error(e); + toast.error('Falha ao carregar arquivo recente'); + } finally { + setLoadingId(null); + } + }, [addModel, models.length, maxModels, refresh]); + + const handleRemove = useCallback(async (e: React.MouseEvent, meta: RecentFileMeta) => { + e.stopPropagation(); + await removeRecentFile(meta.id); + refresh(); + }, [refresh]); + + if (items.length === 0) { + return ( +
+ +

+ Nenhum arquivo recente — os modelos carregados aparecerão aqui +

+
+ ); + } + + return ( +
+
+ + + Recentes + + + {items.length}/6 + +
+
+ {items.map((r) => { + const isLoading = loadingId === r.id; + return ( +
!isLoading && handleLoad(r)} + title="Clique para carregar" + > + {isLoading + ? + : } +
+

{r.fileName}

+

+ {formatSize(r.fileSize)} · {formatWhen(r.addedAt)} atrás +

+
+ +
+ ); + })} +
+
+ ); +} From 2ddbf9045bd413c1ecc82851983203f1449dca36 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Wed, 20 May 2026 18:22:45 +0000 Subject: [PATCH 03/10] Changes Co-authored-by: Reifonas <211114984+Reifonas@users.noreply.github.com> --- src/pages/Index.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/pages/Index.tsx b/src/pages/Index.tsx index e8018b7..35e8216 100644 --- a/src/pages/Index.tsx +++ b/src/pages/Index.tsx @@ -9,6 +9,8 @@ import { convertIFCtoGLB } from '@/lib/convertIFC'; import { validateModelFile } from '@/lib/validateModelFile'; import { CloudLoader } from '@/components/CloudLoader'; import { SceneModelList } from '@/components/SceneModelList'; +import { RecentFilesList } from '@/components/RecentFilesList'; +import { addRecentFile } from '@/lib/recentFiles'; const Index = () => { const navigate = useNavigate(); From b591037d3f938affeb01deacf27944efaf06109d Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Wed, 20 May 2026 18:22:58 +0000 Subject: [PATCH 04/10] Changes Co-authored-by: Reifonas <211114984+Reifonas@users.noreply.github.com> --- src/components/CloudLoader.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/CloudLoader.tsx b/src/components/CloudLoader.tsx index e3e399f..ea6bfad 100644 --- a/src/components/CloudLoader.tsx +++ b/src/components/CloudLoader.tsx @@ -11,6 +11,7 @@ import { toast } from 'sonner'; import { getSupportedExtension, convertToGLB } from '@/lib/convertToGLB'; import { convertIFCtoGLB } from '@/lib/convertIFC'; import { validateModelFile } from '@/lib/validateModelFile'; +import { addRecentFile } from '@/lib/recentFiles'; const DEFAULT_URL = 'https://store.tracksteel.com.br/demo.ifc'; From 06e5adb97d16b855ffc5cdfe3ab118bc87bd84ed Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Wed, 20 May 2026 18:23:05 +0000 Subject: [PATCH 05/10] Changes Co-authored-by: Reifonas <211114984+Reifonas@users.noreply.github.com> --- src/components/CloudLoader.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/CloudLoader.tsx b/src/components/CloudLoader.tsx index ea6bfad..30fbe11 100644 --- a/src/components/CloudLoader.tsx +++ b/src/components/CloudLoader.tsx @@ -140,6 +140,7 @@ export function CloudLoader({ compact = false, variant = 'outline', className }: } const blobUrl = URL.createObjectURL(blob); addModel({ fileName: outName, fileSize: outSize, url: blobUrl }); + addRecentFile(outName, blob).catch(() => {}); toast.success(`"${outName}" adicionado à cena`); setOpen(false); } catch (err) { From e82666d014672a70ef23fc102a7be9a7bcf3a6f0 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Wed, 20 May 2026 18:23:12 +0000 Subject: [PATCH 06/10] Changes Co-authored-by: Reifonas <211114984+Reifonas@users.noreply.github.com> --- src/pages/Index.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pages/Index.tsx b/src/pages/Index.tsx index 35e8216..4b8e652 100644 --- a/src/pages/Index.tsx +++ b/src/pages/Index.tsx @@ -61,6 +61,7 @@ const Index = () => { } const url = URL.createObjectURL(file); addModel({ fileName: file.name, fileSize: file.size, url }); + addRecentFile(file.name, file).catch(() => {}); toast.success(`Modelo "${file.name}" carregado com sucesso!`); return; } From 5555c9f672c1aa9a16422c27423646c80b95818f Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Wed, 20 May 2026 18:23:19 +0000 Subject: [PATCH 07/10] Changes Co-authored-by: Reifonas <211114984+Reifonas@users.noreply.github.com> --- src/pages/Index.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pages/Index.tsx b/src/pages/Index.tsx index 4b8e652..3c904c6 100644 --- a/src/pages/Index.tsx +++ b/src/pages/Index.tsx @@ -84,6 +84,7 @@ const Index = () => { } const url = URL.createObjectURL(result.blob); addModel({ fileName: result.fileName, fileSize: result.fileSize, url }); + addRecentFile(result.fileName, result.blob).catch(() => {}); toast.success(`"${file.name}" convertido para GLB e carregado!`); } catch (err) { console.error('[upload] conversion failed', err); From 7083a36d6b609d9654de57bd9ce7f5f95946fa4d Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Wed, 20 May 2026 18:23:26 +0000 Subject: [PATCH 08/10] Changes Co-authored-by: Reifonas <211114984+Reifonas@users.noreply.github.com> --- src/pages/Index.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pages/Index.tsx b/src/pages/Index.tsx index 3c904c6..a712461 100644 --- a/src/pages/Index.tsx +++ b/src/pages/Index.tsx @@ -105,6 +105,7 @@ const Index = () => { const result = await convertIFCtoGLB(buffer, 'demo250hp.ifc'); const url = URL.createObjectURL(result.blob); addModel({ fileName: result.fileName, fileSize: result.fileSize, url }); + addRecentFile(result.fileName, result.blob).catch(() => {}); toast.success('Modelo demo "250 HP" (IFC) carregado!'); } catch (err) { console.error(err); From 3646b2d60f17bc53ecd46e06ef81acc6b691eaa1 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Wed, 20 May 2026 18:23:32 +0000 Subject: [PATCH 09/10] Changes Co-authored-by: Reifonas <211114984+Reifonas@users.noreply.github.com> --- src/pages/Index.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pages/Index.tsx b/src/pages/Index.tsx index a712461..07185b1 100644 --- a/src/pages/Index.tsx +++ b/src/pages/Index.tsx @@ -123,6 +123,7 @@ const Index = () => { const result = await convertIFCtoGLB(buffer, 'B126-COTOVELO.ifc'); const url = URL.createObjectURL(result.blob); addModel({ fileName: result.fileName, fileSize: result.fileSize, url }); + addRecentFile(result.fileName, result.blob).catch(() => {}); toast.success('Modelo demo "B126 Cotovelo" (IFC) carregado!'); } catch (err) { console.error(err); From 4f6a11c269af16d7c75755697aef0f1b1b49d9e3 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Wed, 20 May 2026 18:23:42 +0000 Subject: [PATCH 10/10] Changes Co-authored-by: Reifonas <211114984+Reifonas@users.noreply.github.com> --- src/pages/Index.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/pages/Index.tsx b/src/pages/Index.tsx index 07185b1..f15d7a3 100644 --- a/src/pages/Index.tsx +++ b/src/pages/Index.tsx @@ -215,6 +215,12 @@ const Index = () => { )} + {/* Recent files card — últimos 6 modelos abertos (cache local) */} +
+ +
+ + {/* Enter viewer */}