feat: add multi-provider support (Komga + Stripstream Librarian)
Some checks failed
Deploy with Docker Compose / deploy (push) Has been cancelled
Some checks failed
Deploy with Docker Compose / deploy (push) Has been cancelled
- Introduce provider abstraction layer (IMediaProvider, KomgaProvider, StripstreamProvider) - Add Stripstream Librarian as second media provider with full feature parity - Migrate all pages and components from direct Komga services to provider factory - Remove dead service code (BaseApiService, HomeService, LibraryService, SearchService, TestService) - Fix library/series page-based pagination for both providers (Komga 0-indexed, Stripstream 1-indexed) - Fix unread filter and search on library page for both providers - Fix read progress display for Stripstream (reading_status mapping) - Fix series read status (books_read_count) for Stripstream - Add global search with series results for Stripstream (series_hits from Meilisearch) - Fix thumbnail proxy to return 404 gracefully instead of JSON on upstream error - Replace duration-based cache debug detection with x-nextjs-cache header Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useRouter } from "next/navigation";
|
||||
import type { KomgaBook, KomgaSeries } from "@/types/komga";
|
||||
import type { NormalizedBook, NormalizedSeries } from "@/lib/providers/types";
|
||||
import { BookCover } from "../ui/book-cover";
|
||||
import { SeriesCover } from "../ui/series-cover";
|
||||
import { useTranslate } from "@/hooks/useTranslate";
|
||||
@@ -12,34 +12,9 @@ import { Card } from "@/components/ui/card";
|
||||
import { useBookOfflineStatus } from "@/hooks/useBookOfflineStatus";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface BaseItem {
|
||||
id: string;
|
||||
metadata: {
|
||||
title: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface OptimizedSeries extends BaseItem {
|
||||
booksCount: number;
|
||||
booksReadCount: number;
|
||||
}
|
||||
|
||||
interface OptimizedBook extends BaseItem {
|
||||
readProgress: {
|
||||
page: number;
|
||||
};
|
||||
media: {
|
||||
pagesCount: number;
|
||||
};
|
||||
metadata: {
|
||||
title: string;
|
||||
number?: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface MediaRowProps {
|
||||
titleKey: string;
|
||||
items: (OptimizedSeries | OptimizedBook)[];
|
||||
items: (NormalizedSeries | NormalizedBook)[];
|
||||
iconName?: string;
|
||||
featuredHeader?: boolean;
|
||||
}
|
||||
@@ -52,13 +27,17 @@ const iconMap = {
|
||||
History,
|
||||
};
|
||||
|
||||
function isSeries(item: NormalizedSeries | NormalizedBook): item is NormalizedSeries {
|
||||
return "bookCount" in item;
|
||||
}
|
||||
|
||||
export function MediaRow({ titleKey, items, iconName, featuredHeader = false }: MediaRowProps) {
|
||||
const router = useRouter();
|
||||
const { t } = useTranslate();
|
||||
const icon = iconName ? iconMap[iconName as keyof typeof iconMap] : undefined;
|
||||
|
||||
const onItemClick = (item: OptimizedSeries | OptimizedBook) => {
|
||||
const path = "booksCount" in item ? `/series/${item.id}` : `/books/${item.id}`;
|
||||
const onItemClick = (item: NormalizedSeries | NormalizedBook) => {
|
||||
const path = isSeries(item) ? `/series/${item.id}` : `/books/${item.id}`;
|
||||
router.push(path);
|
||||
};
|
||||
|
||||
@@ -92,24 +71,24 @@ export function MediaRow({ titleKey, items, iconName, featuredHeader = false }:
|
||||
}
|
||||
|
||||
interface MediaCardProps {
|
||||
item: OptimizedSeries | OptimizedBook;
|
||||
item: NormalizedSeries | NormalizedBook;
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
function MediaCard({ item, onClick }: MediaCardProps) {
|
||||
const { t } = useTranslate();
|
||||
const isSeries = "booksCount" in item;
|
||||
const { isAccessible } = useBookOfflineStatus(isSeries ? "" : item.id);
|
||||
const isSeriesItem = isSeries(item);
|
||||
const { isAccessible } = useBookOfflineStatus(isSeriesItem ? "" : item.id);
|
||||
|
||||
const title = isSeries
|
||||
? item.metadata.title
|
||||
: item.metadata.title ||
|
||||
(item.metadata.number ? t("navigation.volume", { number: item.metadata.number }) : "");
|
||||
const title = isSeriesItem
|
||||
? item.name
|
||||
: item.title ||
|
||||
(item.number ? t("navigation.volume", { number: item.number }) : "");
|
||||
|
||||
const handleClick = () => {
|
||||
// Pour les séries, toujours autoriser le clic
|
||||
// Pour les livres, vérifier si accessible
|
||||
if (isSeries || isAccessible) {
|
||||
if (isSeriesItem || isAccessible) {
|
||||
onClick?.();
|
||||
}
|
||||
};
|
||||
@@ -119,24 +98,24 @@ function MediaCard({ item, onClick }: MediaCardProps) {
|
||||
onClick={handleClick}
|
||||
className={cn(
|
||||
"relative flex w-[188px] flex-shrink-0 flex-col overflow-hidden rounded-xl border border-border/60 bg-card/85 shadow-sm transition-all duration-200 hover:-translate-y-0.5 hover:bg-card hover:shadow-md sm:w-[200px]",
|
||||
!isSeries && !isAccessible ? "cursor-not-allowed" : "cursor-pointer"
|
||||
!isSeriesItem && !isAccessible ? "cursor-not-allowed" : "cursor-pointer"
|
||||
)}
|
||||
>
|
||||
<div className="relative aspect-[2/3] bg-muted">
|
||||
{isSeries ? (
|
||||
{isSeriesItem ? (
|
||||
<>
|
||||
<SeriesCover series={item as KomgaSeries} alt={`Couverture de ${title}`} />
|
||||
<div className="absolute inset-0 flex flex-col justify-end bg-gradient-to-t from-black/75 via-black/30 to-transparent p-3 opacity-0 transition-opacity duration-200 hover:opacity-100">
|
||||
<h3 className="font-medium text-sm text-white line-clamp-2">{title}</h3>
|
||||
<p className="text-xs text-white/80 mt-1">
|
||||
{t("series.books", { count: item.booksCount })}
|
||||
</p>
|
||||
<SeriesCover series={item} alt={`Couverture de ${title}`} />
|
||||
<div className="absolute inset-0 flex flex-col justify-end bg-gradient-to-t from-black/75 via-black/30 to-transparent p-3 opacity-0 transition-opacity duration-200 hover:opacity-100">
|
||||
<h3 className="font-medium text-sm text-white line-clamp-2">{title}</h3>
|
||||
<p className="text-xs text-white/80 mt-1">
|
||||
{t("series.books", { count: item.bookCount })}
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<BookCover
|
||||
book={item as KomgaBook}
|
||||
book={item}
|
||||
alt={`Couverture de ${title}`}
|
||||
showControls={false}
|
||||
overlayVariant="home"
|
||||
|
||||
Reference in New Issue
Block a user