Merge pull request 'feat: implement imgproxy integration with responsive image handling and loading states' (#39) from feat/image-optimization into main
Reviewed-on: #39
This commit is contained in:
commit
30c893cc6f
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 { 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) => {
|
||||
<Link to={`/manga/${manga.id}`}>
|
||||
<img
|
||||
src={
|
||||
(manga.coverImageKey &&
|
||||
import.meta.env.VITE_OMV_BASE_URL +
|
||||
"/" +
|
||||
manga.coverImageKey) ||
|
||||
"/placeholder.svg"
|
||||
manga.coverImageKey
|
||||
? buildImgproxyUrl(manga.coverImageKey)
|
||||
: "/placeholder.svg"
|
||||
}
|
||||
alt={manga.title}
|
||||
onLoad={() => setIsImageLoading(false)}
|
||||
|
||||
@ -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";
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={`${import.meta.env.VITE_OMV_BASE_URL}/${key}`}
|
||||
className="w-full h-auto block m-0 p-0 border-none"
|
||||
alt={`Page ${idx + 1}`}
|
||||
loading="lazy"
|
||||
<ChapterImage
|
||||
imageKey={key}
|
||||
pageNumber={idx + 1}
|
||||
infiniteScroll
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
@ -468,16 +468,9 @@ const Chapter = () => {
|
||||
/* ------------------------------------------------------------------ */
|
||||
<>
|
||||
<div className="relative mx-auto mb-8 overflow-hidden rounded-lg border border-border bg-muted">
|
||||
<img
|
||||
src={
|
||||
import.meta.env.VITE_OMV_BASE_URL +
|
||||
"/" +
|
||||
images[currentPage - 1] || "/placeholder.svg"
|
||||
}
|
||||
alt={`Page ${currentPage}`}
|
||||
width={1000}
|
||||
height={1400}
|
||||
className="h-auto w-full"
|
||||
<ChapterImage
|
||||
imageKey={images[currentPage - 1]}
|
||||
pageNumber={currentPage}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@ -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 = () => {
|
||||
<div className="relative aspect-2/3 overflow-hidden rounded-lg border border-border bg-muted w-full">
|
||||
<img
|
||||
src={
|
||||
(mangaData.data?.coverImageKey &&
|
||||
import.meta.env.VITE_OMV_BASE_URL +
|
||||
"/" +
|
||||
mangaData.data?.coverImageKey) ||
|
||||
"/placeholder.svg"
|
||||
mangaData.data?.coverImageKey
|
||||
? buildImgproxyUrl(mangaData.data?.coverImageKey)
|
||||
: "/placeholder.svg"
|
||||
}
|
||||
|
||||
alt={mangaData.data?.title ?? ""}
|
||||
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