feat(frontend): add image preview and prev/next navigation

This commit is contained in:
2026-06-24 19:22:48 +03:00
parent a40c1f7396
commit 1c98dae3d2
3 changed files with 134 additions and 15 deletions

View File

@@ -2,16 +2,45 @@
import type { FileRecord } from '$lib/types';
import { fade } from 'svelte/transition';
import VideoPreview from './VideoPreview.svelte';
import ImagePreview from './ImagePreview.svelte';
let {
file,
onclose,
onprev,
onnext,
hasPrev = false,
hasNext = false
}: {
file: FileRecord;
onclose: () => void;
onprev?: () => void;
onnext?: () => void;
hasPrev?: boolean;
hasNext?: boolean;
} = $props();
let { file, onclose }: { file: FileRecord; onclose: () => void } = $props();
const isVideo = $derived(file.fileType.includes('video'));
</script>
const isImage = $derived(file.fileType.includes('image'));
<svelte:window
onkeydown={(e) => {
if (e.key === 'Escape') onclose();
}}
/>
$effect(() => {
function onKeydown(e: KeyboardEvent) {
if (e.key === 'Escape') {
onclose();
} else if (e.key === 'ArrowLeft' && hasPrev) {
e.preventDefault();
e.stopPropagation();
onprev?.();
} else if (e.key === 'ArrowRight' && hasNext) {
e.preventDefault();
e.stopPropagation();
onnext?.();
}
}
window.addEventListener('keydown', onKeydown, { capture: true });
return () => window.removeEventListener('keydown', onKeydown, { capture: true });
});
</script>
<!-- svelte-ignore a11y_click_events_have_key_events -->
<!-- svelte-ignore a11y_no_static_element_interactions -->
@@ -20,12 +49,73 @@
onclick={onclose}
transition:fade={{ duration: 150 }}
>
{#if hasPrev}
<button
class="absolute top-1/2 left-4 z-10 -translate-y-1/2 cursor-pointer rounded-full bg-black/40 p-2 text-white/70 transition-colors hover:bg-black/60 hover:text-white"
onclick={(e) => {
e.stopPropagation();
onprev?.();
}}
aria-label="Previous"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-5 w-5"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<polyline points="15 18 9 12 15 6" />
</svg>
</button>
{/if}
{#if hasNext}
<button
class="absolute top-1/2 right-4 z-10 -translate-y-1/2 cursor-pointer rounded-full bg-black/40 p-2 text-white/70 transition-colors hover:bg-black/60 hover:text-white"
onclick={(e) => {
e.stopPropagation();
onnext?.();
}}
aria-label="Next"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-5 w-5"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<polyline points="9 18 15 12 9 6" />
</svg>
</button>
{/if}
<div
class="relative aspect-video h-[85vh] max-w-[95vw] overflow-hidden rounded-xl border border-sky-200/20 bg-[#0f1117]"
class="relative overflow-hidden rounded-xl border border-sky-200/20 bg-[#0f1117] {isImage
? 'max-h-[85vh] max-w-[95vw]'
: 'aspect-video h-[85vh] max-w-[95vw]'}"
onclick={(e) => e.stopPropagation()}
>
{#if isVideo}
<VideoPreview src={`/api/files/${file.id}/stream_part`} />
{#key file.id}
{#if isVideo}
<VideoPreview src={`/api/files/${file.id}/stream_part`} />
{:else if isImage}
<ImagePreview src={`/api/files/${file.id}/download`} alt={file.name} />
{:else}
<div class="flex h-full w-full items-center justify-center text-sm text-white/40">
No preview available
</div>
{/if}
{/key}
{#if isVideo || isImage}
<div
class="pointer-events-none absolute inset-x-0 top-0 z-10 flex items-center justify-between bg-linear-to-b from-black/70 to-transparent px-4 py-3"
>
@@ -52,10 +142,6 @@
</svg>
</button>
</div>
{:else}
<div class="flex h-full w-full items-center justify-center text-sm text-white/40">
No preview available
</div>
{/if}
</div>
</div>

View File

@@ -0,0 +1,5 @@
<script lang="ts">
let { src, alt }: { src: string; alt: string } = $props();
</script>
<img {src} {alt} class="block max-h-[85vh] max-w-[95vw] object-contain" />

View File

@@ -29,6 +29,27 @@
fileRecords.filter((f) => f.name.toLowerCase().includes(search.toLowerCase()))
);
let previewIndex = $derived.by(() => {
if (!previewFile) {
return -1;
}
const id = previewFile.id;
return filteredFileRecords.findIndex((f) => f.id === id);
});
let hasPrev = $derived(previewIndex > 0);
let hasNext = $derived(previewIndex >= 0 && previewIndex < filteredFileRecords.length - 1);
function goPrev() {
if (hasPrev) {
previewFile = filteredFileRecords[previewIndex - 1];
}
}
function goNext() {
if (hasNext) {
previewFile = filteredFileRecords[previewIndex + 1];
}
}
let fileInput: HTMLInputElement;
async function handleUpload(event: Event) {
@@ -214,5 +235,12 @@
</div>
{#if previewFile}
<FilePreview file={previewFile} onclose={() => (previewFile = null)} />
<FilePreview
file={previewFile}
onclose={() => (previewFile = null)}
onprev={goPrev}
onnext={goNext}
{hasPrev}
{hasNext}
/>
{/if}