diff --git a/src/components/common/ViewModeButton.tsx b/src/components/common/ViewModeButton.tsx new file mode 100644 index 0000000..81ce0f9 --- /dev/null +++ b/src/components/common/ViewModeButton.tsx @@ -0,0 +1,36 @@ +import { useDisplayPreferences } from "@/hooks/useDisplayPreferences"; +import { useTranslate } from "@/hooks/useTranslate"; +import { LayoutGrid, List } from "lucide-react"; +import { Button } from "@/components/ui/button"; + +interface ViewModeButtonProps { + onToggle?: (viewMode: "grid" | "list") => void; +} + +export function ViewModeButton({ onToggle }: ViewModeButtonProps) { + const { viewMode, handleViewModeToggle } = useDisplayPreferences(); + const { t } = useTranslate(); + + const handleClick = async () => { + const newViewMode = viewMode === "grid" ? "list" : "grid"; + await handleViewModeToggle(newViewMode); + onToggle?.(newViewMode); + }; + + const Icon = viewMode === "grid" ? List : LayoutGrid; + const label = viewMode === "grid" ? t("books.display.list") : t("books.display.grid"); + + return ( + + ); +} + diff --git a/src/components/library/PaginatedSeriesGrid.tsx b/src/components/library/PaginatedSeriesGrid.tsx index 39078e0..e0af2c4 100644 --- a/src/components/library/PaginatedSeriesGrid.tsx +++ b/src/components/library/PaginatedSeriesGrid.tsx @@ -87,12 +87,6 @@ export function PaginatedSeriesGrid({ }); }; - const handleCompactToggle = async (newCompactState: boolean) => { - await updateUrlParams({ - page: "1", - compact: newCompactState.toString(), - }); - }; const handlePageSizeChange = async (size: number) => { await updateUrlParams({ @@ -125,7 +119,7 @@ export function PaginatedSeriesGrid({
- +
diff --git a/src/components/series/BookList.tsx b/src/components/series/BookList.tsx new file mode 100644 index 0000000..ce99ea3 --- /dev/null +++ b/src/components/series/BookList.tsx @@ -0,0 +1,275 @@ +"use client"; + +import type { KomgaBook } from "@/types/komga"; +import { BookCover } from "@/components/ui/book-cover"; +import { useState, useEffect } from "react"; +import { useTranslate } from "@/hooks/useTranslate"; +import { cn } from "@/lib/utils"; +import { useBookOfflineStatus } from "@/hooks/useBookOfflineStatus"; +import { formatDate } from "@/lib/utils"; +import { ClientOfflineBookService } from "@/lib/services/client-offlinebook.service"; +import { Progress } from "@/components/ui/progress"; +import { Calendar, FileText, User, Tag } from "lucide-react"; +import { MarkAsReadButton } from "@/components/ui/mark-as-read-button"; +import { MarkAsUnreadButton } from "@/components/ui/mark-as-unread-button"; +import { BookOfflineButton } from "@/components/ui/book-offline-button"; + +interface BookListProps { + books: KomgaBook[]; + onBookClick: (book: KomgaBook) => void; +} + +interface BookListItemProps { + book: KomgaBook; + onBookClick: (book: KomgaBook) => void; + onSuccess: (book: KomgaBook, action: "read" | "unread") => void; +} + +function BookListItem({ book, onBookClick, onSuccess }: BookListItemProps) { + const { t } = useTranslate(); + const { isAccessible } = useBookOfflineStatus(book.id); + + const handleClick = () => { + if (!isAccessible) return; + onBookClick(book); + }; + + const isRead = book.readProgress?.completed || false; + const hasReadProgress = book.readProgress !== null; + const currentPage = ClientOfflineBookService.getCurrentPage(book); + const totalPages = book.media.pagesCount; + const progressPercentage = totalPages > 0 ? (currentPage / totalPages) * 100 : 0; + + const getStatusInfo = () => { + if (!book.readProgress) { + return { + label: t("books.status.unread"), + className: "bg-yellow-500/10 text-yellow-500", + }; + } + + if (book.readProgress.completed) { + const readDate = book.readProgress.readDate ? formatDate(book.readProgress.readDate) : null; + return { + label: readDate ? t("books.status.readDate", { date: readDate }) : t("books.status.read"), + className: "bg-green-500/10 text-green-500", + }; + } + + if (currentPage > 0) { + return { + label: t("books.status.progress", { + current: currentPage, + total: totalPages, + }), + className: "bg-blue-500/10 text-blue-500", + }; + } + + return { + label: t("books.status.unread"), + className: "bg-yellow-500/10 text-yellow-500", + }; + }; + + const statusInfo = getStatusInfo(); + const title = book.metadata.title || + (book.metadata.number + ? t("navigation.volume", { number: book.metadata.number }) + : book.name); + + return ( +
+ {/* Couverture */} +
+ +
+ + {/* Contenu */} +
+ {/* Titre et numéro */} +
+
+

+ {title} +

+ {book.metadata.number && ( +

+ {t("navigation.volume", { number: book.metadata.number })} +

+ )} +
+ + {/* Badge de statut */} + + {statusInfo.label} + +
+ + {/* Résumé */} + {book.metadata.summary && ( +

+ {book.metadata.summary} +

+ )} + + {/* Métadonnées */} +
+ {/* Pages */} +
+ + + {totalPages} {totalPages > 1 ? t("books.pages_plural") : t("books.pages")} + +
+ + {/* Auteurs */} + {book.metadata.authors && book.metadata.authors.length > 0 && ( +
+ + + {book.metadata.authors.map(a => a.name).join(", ")} + +
+ )} + + {/* Date de sortie */} + {book.metadata.releaseDate && ( +
+ + {formatDate(book.metadata.releaseDate)} +
+ )} + + {/* Tags */} + {book.metadata.tags && book.metadata.tags.length > 0 && ( +
+ + + {book.metadata.tags.slice(0, 3).join(", ")} + {book.metadata.tags.length > 3 && ` +${book.metadata.tags.length - 3}`} + +
+ )} +
+ + {/* Barre de progression */} + {hasReadProgress && !isRead && currentPage > 0 && ( +
+ +

+ {Math.round(progressPercentage)}% {t("books.completed")} +

+
+ )} + + {/* Actions */} +
+ {!isRead && ( + onSuccess(book, "read")} + className="text-xs" + /> + )} + {hasReadProgress && ( + onSuccess(book, "unread")} + className="text-xs" + /> + )} + +
+
+
+ ); +} + +export function BookList({ books, onBookClick }: BookListProps) { + const [localBooks, setLocalBooks] = useState(books); + const { t } = useTranslate(); + + useEffect(() => { + setLocalBooks(books); + }, [books]); + + if (!localBooks.length) { + return ( +
+

{t("books.empty")}

+
+ ); + } + + const handleOnSuccess = (book: KomgaBook, action: "read" | "unread") => { + if (action === "read") { + setLocalBooks( + localBooks.map((previousBook) => + previousBook.id === book.id + ? { + ...previousBook, + readProgress: { + completed: true, + page: previousBook.media.pagesCount, + readDate: new Date().toISOString(), + created: new Date().toISOString(), + lastModified: new Date().toISOString(), + }, + } + : previousBook + ) + ); + } else if (action === "unread") { + setLocalBooks( + localBooks.map((previousBook) => + previousBook.id === book.id + ? { + ...previousBook, + readProgress: null, + } + : previousBook + ) + ); + } + }; + + return ( +
+ {localBooks.map((book) => ( + + ))} +
+ ); +} + diff --git a/src/components/series/PaginatedBookGrid.tsx b/src/components/series/PaginatedBookGrid.tsx index da2679b..2031161 100644 --- a/src/components/series/PaginatedBookGrid.tsx +++ b/src/components/series/PaginatedBookGrid.tsx @@ -1,6 +1,7 @@ "use client"; import { BookGrid } from "./BookGrid"; +import { BookList } from "./BookList"; import { Pagination } from "@/components/ui/Pagination"; import { useRouter, usePathname, useSearchParams } from "next/navigation"; import { useState, useEffect, useCallback } from "react"; @@ -9,6 +10,7 @@ import { useTranslate } from "@/hooks/useTranslate"; import { useDisplayPreferences } from "@/hooks/useDisplayPreferences"; import { PageSizeSelect } from "@/components/common/PageSizeSelect"; import { CompactModeButton } from "@/components/common/CompactModeButton"; +import { ViewModeButton } from "@/components/common/ViewModeButton"; import { UnreadFilterButton } from "@/components/common/UnreadFilterButton"; interface PaginatedBookGridProps { @@ -32,7 +34,7 @@ export function PaginatedBookGrid({ const pathname = usePathname(); const searchParams = useSearchParams(); const [showOnlyUnread, setShowOnlyUnread] = useState(initialShowOnlyUnread); - const { isCompact, itemsPerPage } = useDisplayPreferences(); + const { isCompact, itemsPerPage, viewMode } = useDisplayPreferences(); const { t } = useTranslate(); const updateUrlParams = useCallback(async ( @@ -81,13 +83,6 @@ export function PaginatedBookGrid({ }); }; - const handleCompactToggle = async (newCompactState: boolean) => { - await updateUrlParams({ - page: "1", - compact: newCompactState.toString(), - }); - }; - const handlePageSizeChange = async (size: number) => { await updateUrlParams({ page: "1", @@ -119,12 +114,17 @@ export function PaginatedBookGrid({

{getShowingText()}

- + + {viewMode === "grid" && }
- + {viewMode === "grid" ? ( + + ) : ( + + )}

diff --git a/src/contexts/PreferencesContext.tsx b/src/contexts/PreferencesContext.tsx index 73daa2a..c929c47 100644 --- a/src/contexts/PreferencesContext.tsx +++ b/src/contexts/PreferencesContext.tsx @@ -39,6 +39,11 @@ export function PreferencesProvider({ setPreferences({ ...defaultPreferences, ...data, + displayMode: { + ...defaultPreferences.displayMode, + ...(data.displayMode || {}), + viewMode: data.displayMode?.viewMode || defaultPreferences.displayMode.viewMode, + }, }); } catch (error) { logger.error({ err: error }, "Erreur lors de la récupération des préférences"); diff --git a/src/hooks/useDisplayPreferences.ts b/src/hooks/useDisplayPreferences.ts index baf8780..e1ad160 100644 --- a/src/hooks/useDisplayPreferences.ts +++ b/src/hooks/useDisplayPreferences.ts @@ -30,10 +30,25 @@ export function useDisplayPreferences() { } }; + const handleViewModeToggle = async (viewMode: "grid" | "list") => { + try { + await updatePreferences({ + displayMode: { + ...preferences.displayMode, + viewMode, + }, + }); + } catch (error) { + logger.error({ err: error }, "Erreur lors de la mise à jour du mode d'affichage"); + } + }; + return { isCompact: preferences.displayMode.compact, itemsPerPage: preferences.displayMode.itemsPerPage, + viewMode: preferences.displayMode.viewMode || "grid", handleCompactToggle, handlePageSizeChange, + handleViewModeToggle, }; } diff --git a/src/i18n/messages/en/common.json b/src/i18n/messages/en/common.json index 0d6f89a..1ce634d 100644 --- a/src/i18n/messages/en/common.json +++ b/src/i18n/messages/en/common.json @@ -325,9 +325,14 @@ "progress": "Page {{current}}/{{total}}", "offline": "Unavailable offline" }, + "pages": "page", + "pages_plural": "pages", + "completed": "completed", "display": { "showing": "Showing books {start} to {end} of {total}", - "page": "Page {current} of {total}" + "page": "Page {current} of {total}", + "grid": "Grid view", + "list": "List view" }, "filters": { "showAll": "Show all", diff --git a/src/i18n/messages/fr/common.json b/src/i18n/messages/fr/common.json index 7ba289b..8c1931b 100644 --- a/src/i18n/messages/fr/common.json +++ b/src/i18n/messages/fr/common.json @@ -323,9 +323,14 @@ "progress": "Page {{current}}/{{total}}", "offline": "Indisponible hors ligne" }, + "pages": "page", + "pages_plural": "pages", + "completed": "complété", "display": { "showing": "Affichage des tomes {start} à {end} sur {total}", - "page": "Page {current} sur {total}" + "page": "Page {current} sur {total}", + "grid": "Vue grille", + "list": "Vue liste" }, "filters": { "showAll": "Afficher tout", diff --git a/src/lib/services/preferences.service.ts b/src/lib/services/preferences.service.ts index 844b475..89e0771 100644 --- a/src/lib/services/preferences.service.ts +++ b/src/lib/services/preferences.service.ts @@ -29,11 +29,17 @@ export class PreferencesService { return { ...defaultPreferences }; } + const displayMode = preferences.displayMode as UserPreferences["displayMode"]; + return { showThumbnails: preferences.showThumbnails, cacheMode: preferences.cacheMode as "memory" | "file", showOnlyUnread: preferences.showOnlyUnread, - displayMode: preferences.displayMode as UserPreferences["displayMode"], + displayMode: { + ...defaultPreferences.displayMode, + ...displayMode, + viewMode: displayMode?.viewMode || defaultPreferences.displayMode.viewMode, + }, background: preferences.background as unknown as BackgroundPreferences, komgaMaxConcurrentRequests: preferences.komgaMaxConcurrentRequests, readerPrefetchCount: preferences.readerPrefetchCount, diff --git a/src/types/preferences.ts b/src/types/preferences.ts index e2f56c8..e271aad 100644 --- a/src/types/preferences.ts +++ b/src/types/preferences.ts @@ -22,6 +22,7 @@ export interface UserPreferences { displayMode: { compact: boolean; itemsPerPage: number; + viewMode: "grid" | "list"; }; background: BackgroundPreferences; komgaMaxConcurrentRequests: number; @@ -36,6 +37,7 @@ export const defaultPreferences: UserPreferences = { displayMode: { compact: false, itemsPerPage: 20, + viewMode: "grid", }, background: { type: "default",