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 && (
+
+ )}
+

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) => {
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";
}}
>
-
))}
@@ -468,16 +468,9 @@ const Chapter = () => {
/* ------------------------------------------------------------------ */
<>
-
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 = () => {

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(", ");
+};