[FA-misc] Gallery
All checks were successful
CI / build-backend (pull_request) Successful in 1m13s
CI / build-frontend (pull_request) Successful in 38s

This commit is contained in:
gamer147
2025-12-10 12:28:50 -05:00
parent 7185b95c65
commit f830773af5
3 changed files with 178 additions and 18 deletions

View File

@@ -51,6 +51,9 @@
import ChevronDown from '@lucide/svelte/icons/chevron-down';
import ChevronUp from '@lucide/svelte/icons/chevron-up';
import RefreshCw from '@lucide/svelte/icons/refresh-cw';
import X from '@lucide/svelte/icons/x';
import ChevronLeft from '@lucide/svelte/icons/chevron-left';
import ChevronRight from '@lucide/svelte/icons/chevron-right';
interface Props {
novelId?: string;
@@ -66,10 +69,22 @@
let refreshError: string | null = $state(null);
let refreshSuccess = $state(false);
// Image viewer state
type GalleryImage = {
src: string;
alt: string;
chapterId?: number;
chapterOrder?: number;
chapterName?: string;
isCover: boolean;
};
let viewerOpen = $state(false);
let viewerIndex = $state(0);
const DESCRIPTION_PREVIEW_LENGTH = 300;
// Derived values
const coverSrc = $derived(novel?.coverImage?.newPath ?? novel?.coverImage?.originalPath);
const coverSrc = $derived(novel?.coverImage?.newPath);
const status = $derived(novel?.rawStatus ?? 'UNKNOWN');
const statusColor = $derived(statusColors[status]);
const statusLabel = $derived(statusLabels[status]);
@@ -102,6 +117,65 @@
return lastUpdated.getTime() < sixHoursAgo;
});
// Gallery images - cover + chapter images
const galleryImages = $derived.by(() => {
const images: GalleryImage[] = [];
// Add cover image first
if (coverSrc && novel) {
images.push({ src: coverSrc, alt: `${novel.name} cover`, isCover: true });
}
// Add chapter images
for (const chapter of sortedChapters) {
for (const img of chapter.images ?? []) {
if (img.newPath) {
images.push({
src: img.newPath,
alt: `Image from ${chapter.name}`,
chapterId: chapter.id,
chapterOrder: chapter.order,
chapterName: chapter.name,
isCover: false
});
}
}
}
return images;
});
const currentImage = $derived(galleryImages[viewerIndex]);
// Image viewer functions
function openImageViewer(index: number) {
viewerIndex = index;
viewerOpen = true;
}
function closeViewer() {
viewerOpen = false;
}
function nextImage() {
if (galleryImages.length > 0) {
viewerIndex = (viewerIndex + 1) % galleryImages.length;
}
}
function prevImage() {
if (galleryImages.length > 0) {
viewerIndex = (viewerIndex - 1 + galleryImages.length) % galleryImages.length;
}
}
function handleViewerKeydown(e: KeyboardEvent) {
if (!viewerOpen) return;
if (e.key === 'Escape') closeViewer();
if (e.key === 'ArrowRight') nextImage();
if (e.key === 'ArrowLeft') prevImage();
}
async function fetchNovel() {
if (!novelId) {
error = 'No novel ID provided';
@@ -371,18 +445,17 @@
Chapters
</TabsTrigger>
<TabsTrigger
value="comments"
disabled
class="rounded-md data-[state=active]:bg-background data-[state=active]:shadow-sm px-3 py-1.5 text-sm font-medium transition-all disabled:opacity-50 disabled:cursor-not-allowed"
value="gallery"
class="rounded-md data-[state=active]:bg-background data-[state=active]:shadow-sm px-3 py-1.5 text-sm font-medium transition-all"
>
Comments
Gallery
</TabsTrigger>
<TabsTrigger
value="recommendations"
value="bookmarks"
disabled
class="rounded-md data-[state=active]:bg-background data-[state=active]:shadow-sm px-3 py-1.5 text-sm font-medium transition-all disabled:opacity-50 disabled:cursor-not-allowed"
>
Recommendations
Bookmarks
</TabsTrigger>
</TabsList>
</CardHeader>
@@ -420,15 +493,32 @@
{/if}
</TabsContent>
<TabsContent value="comments" class="mt-0">
<p class="text-muted-foreground text-sm py-8 text-center">
Comments coming soon.
</p>
<TabsContent value="gallery" class="mt-0">
{#if galleryImages.length === 0}
<p class="text-muted-foreground text-sm py-4 text-center">
No images available.
</p>
{:else}
<div class="grid grid-cols-3 sm:grid-cols-4 md:grid-cols-5 gap-2">
{#each galleryImages as image, index (image.src)}
<button
type="button"
onclick={() => openImageViewer(index)}
class="relative aspect-square overflow-hidden rounded-md bg-muted/50 hover:ring-2 ring-primary transition-all"
>
<img src={image.src} alt={image.alt} class="h-full w-full object-cover" />
{#if image.isCover}
<Badge class="absolute top-1 left-1 text-xs">Cover</Badge>
{/if}
</button>
{/each}
</div>
{/if}
</TabsContent>
<TabsContent value="recommendations" class="mt-0">
<TabsContent value="bookmarks" class="mt-0">
<p class="text-muted-foreground text-sm py-8 text-center">
Recommendations coming soon.
Bookmarks coming soon.
</p>
</TabsContent>
</CardContent>
@@ -436,3 +526,72 @@
</Card>
{/if}
</div>
<!-- Image Viewer Modal -->
{#if viewerOpen && currentImage}
<div
class="fixed inset-0 z-50 flex items-center justify-center bg-black/90 backdrop-blur-sm"
onclick={closeViewer}
onkeydown={handleViewerKeydown}
role="dialog"
aria-modal="true"
tabindex="-1"
>
<!-- Close button -->
<Button
variant="ghost"
size="icon"
class="absolute top-4 right-4 text-white hover:bg-white/10"
onclick={closeViewer}
>
<X class="h-6 w-6" />
</Button>
<!-- Navigation arrows -->
{#if galleryImages.length > 1}
<Button
variant="ghost"
size="icon"
class="absolute left-4 text-white hover:bg-white/10 h-12 w-12"
onclick={(e: MouseEvent) => { e.stopPropagation(); prevImage(); }}
>
<ChevronLeft class="h-8 w-8" />
</Button>
<Button
variant="ghost"
size="icon"
class="absolute right-4 top-1/2 -translate-y-1/2 text-white hover:bg-white/10 h-12 w-12"
onclick={(e: MouseEvent) => { e.stopPropagation(); nextImage(); }}
>
<ChevronRight class="h-8 w-8" />
</Button>
{/if}
<!-- Image container -->
<!-- svelte-ignore a11y_click_events_have_key_events -->
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div class="flex flex-col items-center max-w-[90vw] max-h-[90vh]" onclick={(e: MouseEvent) => e.stopPropagation()}>
<img
src={currentImage.src}
alt={currentImage.alt}
class="max-w-full max-h-[80vh] object-contain rounded-lg"
/>
<!-- Chapter link (if not cover) -->
{#if !currentImage.isCover && currentImage.chapterOrder}
<a
href="/novels/{novelId}/chapters/{currentImage.chapterOrder}"
class="text-white/80 hover:text-white text-sm inline-flex items-center gap-1 mt-3"
>
From: Ch. {currentImage.chapterOrder} - {currentImage.chapterName}
<ExternalLink class="h-3 w-3" />
</a>
{/if}
<!-- Image counter -->
<div class="text-white/60 text-sm mt-2">
{viewerIndex + 1} / {galleryImages.length}
</div>
</div>
</div>
{/if}