feat: implement imgproxy integration with responsive image handling and loading states
This commit is contained in:
parent
4613f71f41
commit
bcd76b7dcc
37
src/features/chapter/components/ChapterImage.tsx
Normal file
37
src/features/chapter/components/ChapterImage.tsx
Normal file
@ -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 (
|
||||||
|
<div className={cn(
|
||||||
|
"relative w-full overflow-hidden",
|
||||||
|
isLoading && (infiniteScroll ? "min-h-[600px] aspect-[2/3]" : "aspect-[2/3]")
|
||||||
|
)}>
|
||||||
|
{isLoading && (
|
||||||
|
<Skeleton className="absolute inset-0 z-10 h-full w-full" />
|
||||||
|
)}
|
||||||
|
<img
|
||||||
|
src={buildImgproxyUrl(imageKey, 1200)}
|
||||||
|
srcSet={generateSrcSet(imageKey)}
|
||||||
|
sizes="(max-width: 896px) 100vw, 896px"
|
||||||
|
className={cn(
|
||||||
|
"w-full h-auto block m-0 p-0 border-none transition-opacity duration-300",
|
||||||
|
isLoading ? "opacity-0" : "opacity-100"
|
||||||
|
)}
|
||||||
|
alt={`Page ${pageNumber}`}
|
||||||
|
loading="lazy"
|
||||||
|
onLoad={() => setIsLoading(false)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -17,6 +17,8 @@ import { Skeleton } from "@/components/ui/skeleton.tsx";
|
|||||||
import { useAuth } from "@/contexts/AuthContext.tsx";
|
import { useAuth } from "@/contexts/AuthContext.tsx";
|
||||||
import { cn } from "@/lib/utils.ts";
|
import { cn } from "@/lib/utils.ts";
|
||||||
import { formatToTwoDigitsDateRange } from "@/utils/dateFormatter.ts";
|
import { formatToTwoDigitsDateRange } from "@/utils/dateFormatter.ts";
|
||||||
|
import { buildImgproxyUrl } from "@/utils/imgproxy.ts";
|
||||||
|
|
||||||
|
|
||||||
interface MangaCardProps {
|
interface MangaCardProps {
|
||||||
manga: MangaListDTO;
|
manga: MangaListDTO;
|
||||||
@ -89,11 +91,9 @@ export const MangaCard = ({ manga, queryKey }: MangaCardProps) => {
|
|||||||
<Link to={`/manga/${manga.id}`}>
|
<Link to={`/manga/${manga.id}`}>
|
||||||
<img
|
<img
|
||||||
src={
|
src={
|
||||||
(manga.coverImageKey &&
|
manga.coverImageKey
|
||||||
import.meta.env.VITE_OMV_BASE_URL +
|
? buildImgproxyUrl(manga.coverImageKey)
|
||||||
"/" +
|
: "/placeholder.svg"
|
||||||
manga.coverImageKey) ||
|
|
||||||
"/placeholder.svg"
|
|
||||||
}
|
}
|
||||||
alt={manga.title}
|
alt={manga.title}
|
||||||
onLoad={() => setIsImageLoading(false)}
|
onLoad={() => setIsImageLoading(false)}
|
||||||
|
|||||||
@ -14,6 +14,7 @@ import {
|
|||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
import { useReadingProgressSync } from "@/features/chapter/hooks/useReadingProgressSync.ts";
|
import { useReadingProgressSync } from "@/features/chapter/hooks/useReadingProgressSync.ts";
|
||||||
import { MangaLoadingState } from "@/components/MangaLoadingState.tsx";
|
import { MangaLoadingState } from "@/components/MangaLoadingState.tsx";
|
||||||
|
import { ChapterImage } from "@/features/chapter/components/ChapterImage";
|
||||||
|
|
||||||
const Chapter = () => {
|
const Chapter = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@ -420,11 +421,10 @@ const Chapter = () => {
|
|||||||
target.style.aspectRatio = "auto";
|
target.style.aspectRatio = "auto";
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<img
|
<ChapterImage
|
||||||
src={`${import.meta.env.VITE_OMV_BASE_URL}/${key}`}
|
imageKey={key}
|
||||||
className="w-full h-auto block m-0 p-0 border-none"
|
pageNumber={idx + 1}
|
||||||
alt={`Page ${idx + 1}`}
|
infiniteScroll
|
||||||
loading="lazy"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
@ -468,16 +468,9 @@ const Chapter = () => {
|
|||||||
/* ------------------------------------------------------------------ */
|
/* ------------------------------------------------------------------ */
|
||||||
<>
|
<>
|
||||||
<div className="relative mx-auto mb-8 overflow-hidden rounded-lg border border-border bg-muted">
|
<div className="relative mx-auto mb-8 overflow-hidden rounded-lg border border-border bg-muted">
|
||||||
<img
|
<ChapterImage
|
||||||
src={
|
imageKey={images[currentPage - 1]}
|
||||||
import.meta.env.VITE_OMV_BASE_URL +
|
pageNumber={currentPage}
|
||||||
"/" +
|
|
||||||
images[currentPage - 1] || "/placeholder.svg"
|
|
||||||
}
|
|
||||||
alt={`Page ${currentPage}`}
|
|
||||||
width={1000}
|
|
||||||
height={1400}
|
|
||||||
className="h-auto w-full"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -43,6 +43,8 @@ import { MangaChapter } from "@/features/manga/MangaChapter.tsx";
|
|||||||
import { useScrollPersistence } from "@/hooks/useScrollPersistence.ts";
|
import { useScrollPersistence } from "@/hooks/useScrollPersistence.ts";
|
||||||
import { formatToTwoDigitsDateRange } from "@/utils/dateFormatter.ts";
|
import { formatToTwoDigitsDateRange } from "@/utils/dateFormatter.ts";
|
||||||
import { Skeleton } from "@/components/ui/skeleton";
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
|
import { buildImgproxyUrl } from "@/utils/imgproxy.ts";
|
||||||
|
|
||||||
import { Spinner } from "@/components/ui/spinner";
|
import { Spinner } from "@/components/ui/spinner";
|
||||||
|
|
||||||
import { useGetProgress } from "@/api/generated/reading-progress/reading-progress.ts";
|
import { useGetProgress } from "@/api/generated/reading-progress/reading-progress.ts";
|
||||||
@ -265,12 +267,11 @@ const Manga = () => {
|
|||||||
<div className="relative aspect-2/3 overflow-hidden rounded-lg border border-border bg-muted w-full">
|
<div className="relative aspect-2/3 overflow-hidden rounded-lg border border-border bg-muted w-full">
|
||||||
<img
|
<img
|
||||||
src={
|
src={
|
||||||
(mangaData.data?.coverImageKey &&
|
mangaData.data?.coverImageKey
|
||||||
import.meta.env.VITE_OMV_BASE_URL +
|
? buildImgproxyUrl(mangaData.data?.coverImageKey)
|
||||||
"/" +
|
: "/placeholder.svg"
|
||||||
mangaData.data?.coverImageKey) ||
|
|
||||||
"/placeholder.svg"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
alt={mangaData.data?.title ?? ""}
|
alt={mangaData.data?.title ?? ""}
|
||||||
className="absolute inset-0 w-full h-full object-cover"
|
className="absolute inset-0 w-full h-full object-cover"
|
||||||
/>
|
/>
|
||||||
|
|||||||
33
src/utils/imgproxy.ts
Normal file
33
src/utils/imgproxy.ts
Normal file
@ -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(", ");
|
||||||
|
};
|
||||||
Loading…
x
Reference in New Issue
Block a user