Changes
Co-authored-by: Reifonas <211114984+Reifonas@users.noreply.github.com>
This commit is contained in:
@@ -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<RecentFileMeta[]>([]);
|
||||
const [loadingId, setLoadingId] = useState<string | null>(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 (
|
||||
<div className="rounded-lg border border-dashed border-muted-foreground/30 bg-muted/10 p-3 text-center">
|
||||
<Clock className="mx-auto mb-1 h-4 w-4 text-muted-foreground" />
|
||||
<p className="font-mono text-[10px] text-muted-foreground">
|
||||
Nenhum arquivo recente — os modelos carregados aparecerão aqui
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Clock className="h-3 w-3 text-primary" />
|
||||
<span className="font-mono text-[11px] font-semibold uppercase tracking-wider text-muted-foreground">
|
||||
Recentes
|
||||
</span>
|
||||
<span className="ml-auto font-mono text-[11px] text-muted-foreground">
|
||||
{items.length}/6
|
||||
</span>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
{items.map((r) => {
|
||||
const isLoading = loadingId === r.id;
|
||||
return (
|
||||
<div
|
||||
key={r.id}
|
||||
className="flex items-center gap-2 rounded-md border border-border bg-muted/30 p-2 transition-colors cursor-pointer hover:bg-muted/50 hover:border-primary/50"
|
||||
onClick={() => !isLoading && handleLoad(r)}
|
||||
title="Clique para carregar"
|
||||
>
|
||||
{isLoading
|
||||
? <Loader2 className="h-3 w-3 shrink-0 animate-spin text-primary" />
|
||||
: <FileBox className="h-3 w-3 shrink-0 text-muted-foreground" />}
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="truncate font-mono text-[11px] text-foreground">{r.fileName}</p>
|
||||
<p className="font-mono text-[9px] text-muted-foreground">
|
||||
{formatSize(r.fileSize)} · {formatWhen(r.addedAt)} atrás
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-5 w-5 shrink-0"
|
||||
onClick={(e) => handleRemove(e, r)}
|
||||
title="Remover dos recentes"
|
||||
>
|
||||
<Trash2 className="h-3 w-3 text-destructive" />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user