Skip to content

Commit ef3137a

Browse files
feat(drive): image thumbnails in the mosaic/grid view (#4)
Image files now show a real preview in the grid instead of a generic icon, with a graceful fallback to the type icon if the image can't load. Works for server-encrypted and public files (Zero-Knowledge stays iconified). Thumbnails load via /files/:id/download?preview=1 — a new preview flag that skips the audit log, so a folder full of images doesn't flood it with one "download" entry per tile. Thumbnails are lazy-loaded.
1 parent 99f6a38 commit ef3137a

2 files changed

Lines changed: 29 additions & 4 deletions

File tree

apps/api/src/routes/files.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,9 +95,12 @@ export const fileRoutes: FastifyPluginAsync = async (app) => {
9595
}
9696
});
9797

98-
// GET /files/:id/download — decrypt and stream a SERVER-mode file.
98+
// GET /files/:id/download — decrypt and stream a SERVER-mode file. `?preview=1` is used for
99+
// inline previews / grid thumbnails and is NOT audited (so a folder of images doesn't flood the
100+
// audit log with one download entry per tile).
99101
app.get('/:id/download', async (req, reply) => {
100102
const { id } = req.params as { id: string };
103+
const { preview } = req.query as { preview?: string };
101104
const file = await prisma.fileObject.findFirst({
102105
where: { id, ownerId: req.user!.id, spaceId: null },
103106
});
@@ -106,7 +109,7 @@ export const fileRoutes: FastifyPluginAsync = async (app) => {
106109
return reply.code(400).send({ error: 'Zero-Knowledge files are fetched via the vault API' });
107110
}
108111

109-
await audit(req, 'file.download', { target: file.id });
112+
if (preview !== '1') await audit(req, 'file.download', { target: file.id });
110113
reply
111114
.header('Content-Type', file.mimeType)
112115
.header('Content-Length', Number(file.sizeBytes))

apps/web/src/app/(app)/drive/page.tsx

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ import {
6969
makeVerifier,
7070
randomSalt,
7171
} from '@/lib/zk';
72-
import { fileVisual } from '@/lib/fileType';
72+
import { fileVisual, previewKind } from '@/lib/fileType';
7373
import { signalDownload } from '@/lib/downloadFx';
7474
import { FileViewer, type ViewerSource } from '@/components/FileViewer';
7575
import { confirm, prompt, choose, toast } from '@/components/ui/overlays';
@@ -1743,11 +1743,33 @@ export default function EspacesPage() {
17431743

17441744
// ── Small presentational helpers ──────────────────────────────────────────────
17451745

1746+
/** Grid thumbnail: shows the actual image for image files (server-served, not ZK), with a
1747+
* graceful fallback to the type icon on error. `?preview=1` keeps it out of the audit log. */
1748+
function Thumb({ src, fallback }: { src: string; fallback: React.ReactNode }) {
1749+
const [err, setErr] = useState(false);
1750+
if (err) return <>{fallback}</>;
1751+
return (
1752+
// eslint-disable-next-line @next/next/no-img-element
1753+
<img
1754+
src={src}
1755+
alt=""
1756+
loading="lazy"
1757+
onError={() => setErr(true)}
1758+
className="h-16 w-16 rounded-lg object-cover ring-1 ring-white/10"
1759+
/>
1760+
);
1761+
}
1762+
17461763
function cardIcon(e: Entry, isZk: boolean) {
17471764
if (e.kind === 'folder') return isZk ? <FolderLock size={30} className="text-violet-300" /> : <Folder size={30} className="text-zinc-400" />;
17481765
if (e.kind === 'zk') return <Lock size={30} className="text-violet-300" />;
17491766
const v = fileVisual(e.file.name, e.file.mimeType);
1750-
return <v.Icon size={30} className={v.color} />;
1767+
const icon = <v.Icon size={30} className={v.color} />;
1768+
// Image files get a real thumbnail (server-decrypted / plaintext for public); others the icon.
1769+
if (previewKind(e.file.name, e.file.mimeType) === 'image') {
1770+
return <Thumb src={api.url(`/files/${e.file.id}/download?preview=1`)} fallback={icon} />;
1771+
}
1772+
return icon;
17511773
}
17521774
function rowIcon(e: Entry, isZk: boolean) {
17531775
if (e.kind === 'folder') return isZk ? <FolderLock size={18} className="text-violet-300" /> : <Folder size={18} className="text-zinc-400" />;

0 commit comments

Comments
 (0)