From bcd76b7dcc1d52668aa680c3e7fdcb238dd4f29e Mon Sep 17 00:00:00 2001 From: Rodrigo Verdiani Date: Mon, 27 Apr 2026 09:28:47 -0300 Subject: [PATCH] feat: implement imgproxy integration with responsive image handling and loading states --- .../chapter/components/ChapterImage.tsx | 37 +++++++++++++++++++ src/features/home/components/MangaCard.tsx | 10 ++--- src/pages/Chapter.tsx | 23 ++++-------- src/pages/Manga.tsx | 11 +++--- src/utils/imgproxy.ts | 33 +++++++++++++++++ 5 files changed, 89 insertions(+), 25 deletions(-) create mode 100644 src/features/chapter/components/ChapterImage.tsx create mode 100644 src/utils/imgproxy.ts diff --git a/src/features/chapter/components/ChapterImage.tsx b/src/features/chapter/components/ChapterImage.tsx new file mode 100644 index 0000000..58d2b35 --- /dev/null +++ b/src/features/chapter/components/ChapterImage.tsx @@ -0,0 +1,37 @@ +import { buildImgproxyUrl, generateSrcSet } from "@/utils/imgproxy.ts"; +import { Skeleton } from "@/components/ui/skeleton"; +import { cn } from "@/lib/utils"; +import { useState } from "react"; + +interface ChapterImageProps { + imageKey: string; + pageNumber: number; + infiniteScroll?: boolean; +} + +export const ChapterImage = ({ imageKey, pageNumber, infiniteScroll }: ChapterImageProps) => { + const [isLoading, setIsLoading] = useState(true); + + return ( +
+ {isLoading && ( + + )} + {`Page setIsLoading(false)} + /> +
+ ); +}; \ No newline at end of file diff --git a/src/features/home/components/MangaCard.tsx b/src/features/home/components/MangaCard.tsx index 6b32389..48e6d6e 100644 --- a/src/features/home/components/MangaCard.tsx +++ b/src/features/home/components/MangaCard.tsx @@ -17,6 +17,8 @@ import { Skeleton } from "@/components/ui/skeleton.tsx"; import { useAuth } from "@/contexts/AuthContext.tsx"; import { cn } from "@/lib/utils.ts"; import { formatToTwoDigitsDateRange } from "@/utils/dateFormatter.ts"; +import { buildImgproxyUrl } from "@/utils/imgproxy.ts"; + interface MangaCardProps { manga: MangaListDTO; @@ -89,11 +91,9 @@ export const MangaCard = ({ manga, queryKey }: MangaCardProps) => { {manga.title} setIsImageLoading(false)} diff --git a/src/pages/Chapter.tsx b/src/pages/Chapter.tsx index f9abfa1..d58bd31 100644 --- a/src/pages/Chapter.tsx +++ b/src/pages/Chapter.tsx @@ -14,6 +14,7 @@ import { } from "@/components/ui/dialog"; import { useReadingProgressSync } from "@/features/chapter/hooks/useReadingProgressSync.ts"; import { MangaLoadingState } from "@/components/MangaLoadingState.tsx"; +import { ChapterImage } from "@/features/chapter/components/ChapterImage"; const Chapter = () => { const navigate = useNavigate(); @@ -420,11 +421,10 @@ const Chapter = () => { target.style.aspectRatio = "auto"; }} > - {`Page ))} @@ -468,16 +468,9 @@ const Chapter = () => { /* ------------------------------------------------------------------ */ <>
- {`Page
diff --git a/src/pages/Manga.tsx b/src/pages/Manga.tsx index 9fc876b..461d5b8 100644 --- a/src/pages/Manga.tsx +++ b/src/pages/Manga.tsx @@ -43,6 +43,8 @@ import { MangaChapter } from "@/features/manga/MangaChapter.tsx"; import { useScrollPersistence } from "@/hooks/useScrollPersistence.ts"; import { formatToTwoDigitsDateRange } from "@/utils/dateFormatter.ts"; import { Skeleton } from "@/components/ui/skeleton"; +import { buildImgproxyUrl } from "@/utils/imgproxy.ts"; + import { Spinner } from "@/components/ui/spinner"; import { useGetProgress } from "@/api/generated/reading-progress/reading-progress.ts"; @@ -265,12 +267,11 @@ const Manga = () => {
{mangaData.data?.title diff --git a/src/utils/imgproxy.ts b/src/utils/imgproxy.ts new file mode 100644 index 0000000..0cf0c35 --- /dev/null +++ b/src/utils/imgproxy.ts @@ -0,0 +1,33 @@ +export const buildImgproxyUrl = (sourceUrl: string, width?: number): string => { + const baseUrl = import.meta.env.VITE_IMGPROXY_BASE_URL; + + if (!baseUrl) { + // Fallback to original URL logic + const originalBase = import.meta.env.VITE_OMV_BASE_URL; + if (sourceUrl.startsWith("http")) { + return sourceUrl; + } + return `${originalBase}/${sourceUrl}`; + } + + // rs:fit:${width}:0:0 to resize preserving aspect ratio, capping width at `width` + // If width is provided, we resize, otherwise we keep original dimensions. + const parts = []; + if (width) { + parts.push(`rs:fit:${width}:0:0`); + } + parts.push("f:avif"); + + const options = parts.join("/"); + + return `${baseUrl}/insecure/${options}/plain/s3://${import.meta.env.VITE_S3_BUCKET}/${sourceUrl}`; +}; + +export const generateSrcSet = ( + sourceUrl: string, + widths: number[] = [400, 800, 1200, 1800], +): string => { + return widths + .map((width) => `${buildImgproxyUrl(sourceUrl, width)} ${width}w`) + .join(", "); +};