247 lines
7.1 KiB
Svelte
247 lines
7.1 KiB
Svelte
|
|
<script lang="ts">
|
||
|
|
import { X, Download, ZoomIn, ZoomOut, RotateCw, Maximize2, FileText, Image, File } from 'lucide-svelte';
|
||
|
|
import { Button } from '$lib/components/ui/button';
|
||
|
|
|
||
|
|
interface Document {
|
||
|
|
id: string;
|
||
|
|
title: string;
|
||
|
|
file_name: string;
|
||
|
|
file_path: string;
|
||
|
|
mime_type: string;
|
||
|
|
file_size: number;
|
||
|
|
}
|
||
|
|
|
||
|
|
let { document, previewUrl, onClose }: {
|
||
|
|
document: Document;
|
||
|
|
previewUrl: string;
|
||
|
|
onClose: () => void;
|
||
|
|
} = $props();
|
||
|
|
|
||
|
|
let zoom = $state(100);
|
||
|
|
let rotation = $state(0);
|
||
|
|
let isLoading = $state(true);
|
||
|
|
let loadError = $state(false);
|
||
|
|
let textContent = $state<string | null>(null);
|
||
|
|
|
||
|
|
const isPdf = $derived(document.mime_type === 'application/pdf');
|
||
|
|
const isImage = $derived(document.mime_type.startsWith('image/'));
|
||
|
|
const isText = $derived(
|
||
|
|
document.mime_type.startsWith('text/') ||
|
||
|
|
['application/json', 'application/javascript', 'text/csv'].includes(document.mime_type)
|
||
|
|
);
|
||
|
|
const isOffice = $derived(
|
||
|
|
document.mime_type.includes('word') ||
|
||
|
|
document.mime_type.includes('excel') ||
|
||
|
|
document.mime_type.includes('spreadsheet') ||
|
||
|
|
document.mime_type.includes('powerpoint') ||
|
||
|
|
document.mime_type.includes('presentation')
|
||
|
|
);
|
||
|
|
|
||
|
|
function handleKeydown(e: KeyboardEvent) {
|
||
|
|
if (e.key === 'Escape') {
|
||
|
|
onClose();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function handleZoomIn() {
|
||
|
|
zoom = Math.min(zoom + 25, 300);
|
||
|
|
}
|
||
|
|
|
||
|
|
function handleZoomOut() {
|
||
|
|
zoom = Math.max(zoom - 25, 25);
|
||
|
|
}
|
||
|
|
|
||
|
|
function handleRotate() {
|
||
|
|
rotation = (rotation + 90) % 360;
|
||
|
|
}
|
||
|
|
|
||
|
|
function handleDownload() {
|
||
|
|
const link = document.createElement('a');
|
||
|
|
link.href = previewUrl;
|
||
|
|
link.download = document.file_name;
|
||
|
|
link.target = '_blank';
|
||
|
|
document.body.appendChild(link);
|
||
|
|
link.click();
|
||
|
|
document.body.removeChild(link);
|
||
|
|
}
|
||
|
|
|
||
|
|
function formatFileSize(bytes: number): string {
|
||
|
|
if (bytes === 0) return '0 Bytes';
|
||
|
|
const k = 1024;
|
||
|
|
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
||
|
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||
|
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||
|
|
}
|
||
|
|
|
||
|
|
// Load text content for text files
|
||
|
|
$effect(() => {
|
||
|
|
if (isText && previewUrl) {
|
||
|
|
fetch(previewUrl)
|
||
|
|
.then(res => res.text())
|
||
|
|
.then(text => {
|
||
|
|
textContent = text;
|
||
|
|
isLoading = false;
|
||
|
|
})
|
||
|
|
.catch(() => {
|
||
|
|
loadError = true;
|
||
|
|
isLoading = false;
|
||
|
|
});
|
||
|
|
}
|
||
|
|
});
|
||
|
|
</script>
|
||
|
|
|
||
|
|
<svelte:window onkeydown={handleKeydown} />
|
||
|
|
|
||
|
|
<!-- Modal Backdrop -->
|
||
|
|
<div
|
||
|
|
class="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-sm p-4"
|
||
|
|
role="dialog"
|
||
|
|
aria-modal="true"
|
||
|
|
aria-labelledby="preview-title"
|
||
|
|
>
|
||
|
|
<!-- Modal Container -->
|
||
|
|
<div class="relative flex h-full w-full max-w-6xl flex-col rounded-2xl bg-slate-900/95 shadow-2xl">
|
||
|
|
<!-- Header -->
|
||
|
|
<div class="flex items-center justify-between border-b border-slate-700/50 px-6 py-4">
|
||
|
|
<div class="flex items-center gap-3">
|
||
|
|
{#if isPdf}
|
||
|
|
<FileText class="h-6 w-6 text-red-400" />
|
||
|
|
{:else if isImage}
|
||
|
|
<Image class="h-6 w-6 text-blue-400" />
|
||
|
|
{:else}
|
||
|
|
<File class="h-6 w-6 text-slate-400" />
|
||
|
|
{/if}
|
||
|
|
<div>
|
||
|
|
<h2 id="preview-title" class="text-lg font-semibold text-white">{document.title}</h2>
|
||
|
|
<p class="text-sm text-slate-400">{document.file_name} · {formatFileSize(document.file_size)}</p>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="flex items-center gap-2">
|
||
|
|
<!-- Zoom Controls (for images) -->
|
||
|
|
{#if isImage}
|
||
|
|
<div class="flex items-center gap-1 rounded-lg bg-slate-800 px-2 py-1">
|
||
|
|
<button
|
||
|
|
onclick={handleZoomOut}
|
||
|
|
class="rounded p-1 text-slate-400 hover:bg-slate-700 hover:text-white"
|
||
|
|
aria-label="Zoom out"
|
||
|
|
>
|
||
|
|
<ZoomOut class="h-4 w-4" />
|
||
|
|
</button>
|
||
|
|
<span class="min-w-[3rem] text-center text-sm text-slate-300">{zoom}%</span>
|
||
|
|
<button
|
||
|
|
onclick={handleZoomIn}
|
||
|
|
class="rounded p-1 text-slate-400 hover:bg-slate-700 hover:text-white"
|
||
|
|
aria-label="Zoom in"
|
||
|
|
>
|
||
|
|
<ZoomIn class="h-4 w-4" />
|
||
|
|
</button>
|
||
|
|
<button
|
||
|
|
onclick={handleRotate}
|
||
|
|
class="rounded p-1 text-slate-400 hover:bg-slate-700 hover:text-white"
|
||
|
|
aria-label="Rotate"
|
||
|
|
>
|
||
|
|
<RotateCw class="h-4 w-4" />
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
{/if}
|
||
|
|
|
||
|
|
<!-- Download Button -->
|
||
|
|
<Button
|
||
|
|
variant="outline"
|
||
|
|
size="sm"
|
||
|
|
onclick={handleDownload}
|
||
|
|
class="border-slate-600 bg-slate-800 text-white hover:bg-slate-700"
|
||
|
|
>
|
||
|
|
<Download class="mr-2 h-4 w-4" />
|
||
|
|
Download
|
||
|
|
</Button>
|
||
|
|
|
||
|
|
<!-- Close Button -->
|
||
|
|
<button
|
||
|
|
onclick={onClose}
|
||
|
|
class="rounded-lg p-2 text-slate-400 hover:bg-slate-800 hover:text-white"
|
||
|
|
aria-label="Close preview"
|
||
|
|
>
|
||
|
|
<X class="h-6 w-6" />
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- Content Area -->
|
||
|
|
<div class="relative flex-1 overflow-auto p-4">
|
||
|
|
{#if isLoading && !isImage && !isPdf}
|
||
|
|
<div class="flex h-full items-center justify-center">
|
||
|
|
<div class="h-8 w-8 animate-spin rounded-full border-2 border-monaco-600 border-t-transparent"></div>
|
||
|
|
</div>
|
||
|
|
{:else if loadError}
|
||
|
|
<div class="flex h-full flex-col items-center justify-center gap-4 text-slate-400">
|
||
|
|
<File class="h-16 w-16" />
|
||
|
|
<p>Unable to load preview</p>
|
||
|
|
<Button
|
||
|
|
variant="outline"
|
||
|
|
onclick={handleDownload}
|
||
|
|
class="border-slate-600 bg-slate-800 text-white hover:bg-slate-700"
|
||
|
|
>
|
||
|
|
<Download class="mr-2 h-4 w-4" />
|
||
|
|
Download File
|
||
|
|
</Button>
|
||
|
|
</div>
|
||
|
|
{:else if isPdf}
|
||
|
|
<!-- PDF Preview -->
|
||
|
|
<iframe
|
||
|
|
src={previewUrl}
|
||
|
|
class="h-full w-full rounded-lg bg-white"
|
||
|
|
title={document.title}
|
||
|
|
onload={() => isLoading = false}
|
||
|
|
onerror={() => { loadError = true; isLoading = false; }}
|
||
|
|
></iframe>
|
||
|
|
{:else if isImage}
|
||
|
|
<!-- Image Preview -->
|
||
|
|
<div class="flex h-full items-center justify-center overflow-auto">
|
||
|
|
<img
|
||
|
|
src={previewUrl}
|
||
|
|
alt={document.title}
|
||
|
|
class="max-h-full max-w-full object-contain transition-transform duration-200"
|
||
|
|
style="transform: scale({zoom / 100}) rotate({rotation}deg);"
|
||
|
|
onload={() => isLoading = false}
|
||
|
|
onerror={() => { loadError = true; isLoading = false; }}
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
{:else if isText && textContent !== null}
|
||
|
|
<!-- Text Preview -->
|
||
|
|
<div class="h-full overflow-auto rounded-lg bg-slate-950 p-4">
|
||
|
|
<pre class="whitespace-pre-wrap font-mono text-sm text-slate-300">{textContent}</pre>
|
||
|
|
</div>
|
||
|
|
{:else if isOffice}
|
||
|
|
<!-- Office Documents - Offer Download -->
|
||
|
|
<div class="flex h-full flex-col items-center justify-center gap-4 text-slate-400">
|
||
|
|
<File class="h-16 w-16" />
|
||
|
|
<p class="text-lg">Office documents cannot be previewed directly</p>
|
||
|
|
<p class="text-sm">Download the file to view it in Microsoft Office or compatible application</p>
|
||
|
|
<Button
|
||
|
|
variant="monaco"
|
||
|
|
onclick={handleDownload}
|
||
|
|
>
|
||
|
|
<Download class="mr-2 h-4 w-4" />
|
||
|
|
Download {document.file_name}
|
||
|
|
</Button>
|
||
|
|
</div>
|
||
|
|
{:else}
|
||
|
|
<!-- Unsupported file type -->
|
||
|
|
<div class="flex h-full flex-col items-center justify-center gap-4 text-slate-400">
|
||
|
|
<File class="h-16 w-16" />
|
||
|
|
<p class="text-lg">Preview not available for this file type</p>
|
||
|
|
<Button
|
||
|
|
variant="monaco"
|
||
|
|
onclick={handleDownload}
|
||
|
|
>
|
||
|
|
<Download class="mr-2 h-4 w-4" />
|
||
|
|
Download File
|
||
|
|
</Button>
|
||
|
|
</div>
|
||
|
|
{/if}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|