diff --git a/prisma/schema.prisma b/prisma/schema.prisma
index 7d05ba2..779f84a 100644
--- a/prisma/schema.prisma
+++ b/prisma/schema.prisma
@@ -64,6 +64,7 @@ model Preferences {
displayMode Json
background Json
readerPrefetchCount Int @default(5)
+ anonymousMode Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index d4a3ca9..55c7f93 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -5,6 +5,7 @@ import { cn } from "@/lib/utils";
import ClientLayout from "@/components/layout/ClientLayout";
import { PreferencesService } from "@/lib/services/preferences.service";
import { PreferencesProvider } from "@/contexts/PreferencesContext";
+import { AnonymousProvider } from "@/contexts/AnonymousContext";
import { I18nProvider } from "@/components/providers/I18nProvider";
import { AuthProvider } from "@/components/providers/AuthProvider";
import { cookies, headers } from "next/headers";
@@ -313,13 +314,15 @@ export default async function RootLayout({ children }: { children: React.ReactNo
-
- {children}
-
+
+
+ {children}
+
+
diff --git a/src/app/page.tsx b/src/app/page.tsx
index 7e395ed..809b853 100644
--- a/src/app/page.tsx
+++ b/src/app/page.tsx
@@ -5,6 +5,7 @@ import { ErrorMessage } from "@/components/ui/ErrorMessage";
import { ERROR_CODES } from "@/constants/errorCodes";
import { AppError } from "@/utils/errors";
import { FavoritesService } from "@/lib/services/favorites.service";
+import { PreferencesService } from "@/lib/services/preferences.service";
import { redirect } from "next/navigation";
export default async function HomePage() {
@@ -12,16 +13,17 @@ export default async function HomePage() {
const provider = await getProvider();
if (!provider) redirect("/settings");
- const [homeData, favorites] = await Promise.all([
+ const [homeData, favorites, preferences] = await Promise.all([
provider.getHomeData(),
FavoritesService.getFavorites(),
+ PreferencesService.getPreferences().catch(() => null),
]);
const data = { ...homeData, favorites };
return (
-
+
);
} catch (error) {
diff --git a/src/components/home/HomeContent.tsx b/src/components/home/HomeContent.tsx
index 83565ca..6eaa8eb 100644
--- a/src/components/home/HomeContent.tsx
+++ b/src/components/home/HomeContent.tsx
@@ -3,9 +3,10 @@ import type { HomeData } from "@/types/home";
interface HomeContentProps {
data: HomeData;
+ isAnonymous?: boolean;
}
-export function HomeContent({ data }: HomeContentProps) {
+export function HomeContent({ data, isAnonymous = false }: HomeContentProps) {
// Merge onDeck (next unread per series) and ongoingBooks (currently reading),
// deduplicate by id, onDeck first
const continueReading = (() => {
@@ -20,7 +21,7 @@ export function HomeContent({ data }: HomeContentProps) {
return (
- {continueReading.length > 0 && (
+ {!isAnonymous && continueReading.length > 0 && (
)}
- {data.ongoing && data.ongoing.length > 0 && (
+ {!isAnonymous && data.ongoing && data.ongoing.length > 0 && (
{isSeriesItem ? (
<>
-
+
{title}
diff --git a/src/components/layout/Header.tsx b/src/components/layout/Header.tsx
index 7c035a3..ded170b 100644
--- a/src/components/layout/Header.tsx
+++ b/src/components/layout/Header.tsx
@@ -1,10 +1,11 @@
-import { Menu, Moon, Sun, RefreshCw, Search } from "lucide-react";
+import { Menu, Moon, Sun, RefreshCw, Search, EyeOff, Eye } from "lucide-react";
import { useTheme } from "next-themes";
import LanguageSelector from "@/components/LanguageSelector";
import { useTranslation } from "react-i18next";
import { IconButton } from "@/components/ui/icon-button";
import { useState } from "react";
import { GlobalSearch } from "@/components/layout/GlobalSearch";
+import { useAnonymous } from "@/contexts/AnonymousContext";
interface HeaderProps {
onToggleSidebar: () => void;
@@ -19,6 +20,7 @@ export function Header({
}: HeaderProps) {
const { theme, setTheme } = useTheme();
const { t } = useTranslation();
+ const { isAnonymous, toggleAnonymous } = useAnonymous();
const [isRefreshing, setIsRefreshing] = useState(false);
const [isMobileSearchOpen, setIsMobileSearchOpen] = useState(false);
@@ -87,6 +89,14 @@ export function Header({
className="h-9 w-9 rounded-full sm:hidden"
tooltip={t("header.search.placeholder")}
/>
+
@@ -153,14 +159,16 @@ function SeriesListItem({ series, isCompact = false }: SeriesListItemProps) {
{/* Badge de statut */}
-
- {statusInfo.label}
-
+ {statusInfo && (
+
+ {statusInfo.label}
+
+ )}
{/* Résumé */}
@@ -224,7 +232,7 @@ function SeriesListItem({ series, isCompact = false }: SeriesListItemProps) {
{/* Barre de progression */}
- {series.bookCount > 0 && !isCompleted && series.booksReadCount > 0 && (
+ {!isAnonymous && series.bookCount > 0 && !isCompleted && series.booksReadCount > 0 && (
diff --git a/src/components/reader/hooks/usePageNavigation.ts b/src/components/reader/hooks/usePageNavigation.ts
index 35161ba..2663414 100644
--- a/src/components/reader/hooks/usePageNavigation.ts
+++ b/src/components/reader/hooks/usePageNavigation.ts
@@ -4,6 +4,7 @@ import { ClientOfflineBookService } from "@/lib/services/client-offlinebook.serv
import type { NormalizedBook } from "@/lib/providers/types";
import logger from "@/lib/logger";
import { updateReadProgress } from "@/app/actions/read-progress";
+import { useAnonymous } from "@/contexts/AnonymousContext";
interface UsePageNavigationProps {
book: NormalizedBook;
@@ -23,6 +24,13 @@ export function usePageNavigation({
nextBook,
}: UsePageNavigationProps) {
const router = useRouter();
+ const { isAnonymous } = useAnonymous();
+ const isAnonymousRef = useRef(isAnonymous);
+
+ useEffect(() => {
+ isAnonymousRef.current = isAnonymous;
+ }, [isAnonymous]);
+
const [currentPage, setCurrentPage] = useState(() => {
const saved = ClientOfflineBookService.getCurrentPage(book);
return saved < 1 ? 1 : saved;
@@ -48,8 +56,10 @@ export function usePageNavigation({
async (page: number) => {
try {
ClientOfflineBookService.setCurrentPage(bookRef.current, page);
- const completed = page === pagesLengthRef.current;
- await updateReadProgress(bookRef.current.id, page, completed);
+ if (!isAnonymousRef.current) {
+ const completed = page === pagesLengthRef.current;
+ await updateReadProgress(bookRef.current.id, page, completed);
+ }
} catch (error) {
logger.error({ err: error }, "Sync error:");
}
diff --git a/src/components/series/BookList.tsx b/src/components/series/BookList.tsx
index da47bb9..de34b38 100644
--- a/src/components/series/BookList.tsx
+++ b/src/components/series/BookList.tsx
@@ -13,6 +13,7 @@ import { FileText } 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";
+import { useAnonymous } from "@/contexts/AnonymousContext";
interface BookListProps {
books: NormalizedBook[];
@@ -30,6 +31,7 @@ interface BookListItemProps {
function BookListItem({ book, onBookClick, onSuccess, isCompact = false }: BookListItemProps) {
const { t } = useTranslate();
+ const { isAnonymous } = useAnonymous();
const { isAccessible } = useBookOfflineStatus(book.id);
const handleClick = () => {
@@ -37,9 +39,9 @@ function BookListItem({ book, onBookClick, onSuccess, isCompact = false }: BookL
onBookClick(book);
};
- const isRead = book.readProgress?.completed || false;
- const hasReadProgress = book.readProgress !== null;
- const currentPage = ClientOfflineBookService.getCurrentPage(book);
+ const isRead = isAnonymous ? false : (book.readProgress?.completed || false);
+ const hasReadProgress = isAnonymous ? false : book.readProgress !== null;
+ const currentPage = isAnonymous ? 0 : ClientOfflineBookService.getCurrentPage(book);
const totalPages = book.pageCount;
const progressPercentage = totalPages > 0 ? (currentPage / totalPages) * 100 : 0;
@@ -118,14 +120,16 @@ function BookListItem({ book, onBookClick, onSuccess, isCompact = false }: BookL
>
{title}
-
- {statusInfo.label}
-
+ {!isAnonymous && (
+
+ {statusInfo.label}
+
+ )}
{/* Métadonnées minimales */}
@@ -191,14 +195,16 @@ function BookListItem({ book, onBookClick, onSuccess, isCompact = false }: BookL
{/* Badge de statut */}
-
- {statusInfo.label}
-
+ {!isAnonymous && (
+
+ {statusInfo.label}
+
+ )}
{/* Métadonnées */}
@@ -224,7 +230,7 @@ function BookListItem({ book, onBookClick, onSuccess, isCompact = false }: BookL
{/* Actions */}
- {!isRead && (
+ {!isAnonymous && !isRead && (
)}
- {hasReadProgress && (
+ {!isAnonymous && hasReadProgress && (
onSuccess(book, "unread")}
diff --git a/src/components/series/SeriesHeader.tsx b/src/components/series/SeriesHeader.tsx
index 6f8dd27..e761086 100644
--- a/src/components/series/SeriesHeader.tsx
+++ b/src/components/series/SeriesHeader.tsx
@@ -14,6 +14,7 @@ import { StatusBadge } from "@/components/ui/status-badge";
import { IconButton } from "@/components/ui/icon-button";
import logger from "@/lib/logger";
import { addToFavorites, removeFromFavorites } from "@/app/actions/favorites";
+import { useAnonymous } from "@/contexts/AnonymousContext";
interface SeriesHeaderProps {
series: NormalizedSeries;
@@ -23,6 +24,7 @@ interface SeriesHeaderProps {
export const SeriesHeader = ({ series, refreshSeries, initialIsFavorite }: SeriesHeaderProps) => {
const { toast } = useToast();
+ const { isAnonymous } = useAnonymous();
const [isFavorite, setIsFavorite] = useState(initialIsFavorite);
const [isDescriptionExpanded, setIsDescriptionExpanded] = useState(false);
const { t } = useTranslate();
@@ -100,7 +102,7 @@ export const SeriesHeader = ({ series, refreshSeries, initialIsFavorite }: Serie
};
};
- const statusInfo = getReadingStatusInfo();
+ const statusInfo = isAnonymous ? null : getReadingStatusInfo();
const authorsText = series.authors?.length
? series.authors.map((a) => a.name).join(", ")
: null;
@@ -152,9 +154,11 @@ export const SeriesHeader = ({ series, refreshSeries, initialIsFavorite }: Serie
)}
-
- {statusInfo.label}
-
+ {statusInfo && (
+
+ {statusInfo.label}
+
+ )}
{series.bookCount === 1
? t("series.header.books", { count: series.bookCount })
diff --git a/src/components/ui/book-cover.tsx b/src/components/ui/book-cover.tsx
index 64b0f87..911f896 100644
--- a/src/components/ui/book-cover.tsx
+++ b/src/components/ui/book-cover.tsx
@@ -10,6 +10,7 @@ import { useTranslate } from "@/hooks/useTranslate";
import { formatDate } from "@/lib/utils";
import { useBookOfflineStatus } from "@/hooks/useBookOfflineStatus";
import { WifiOff } from "lucide-react";
+import { useAnonymous } from "@/contexts/AnonymousContext";
// Fonction utilitaire pour obtenir les informations de statut de lecture
const getReadingStatusInfo = (
@@ -60,17 +61,18 @@ export function BookCover({
overlayVariant = "default",
}: BookCoverProps) {
const { t } = useTranslate();
+ const { isAnonymous } = useAnonymous();
const { isAccessible } = useBookOfflineStatus(book.id);
- const isCompleted = book.readProgress?.completed || false;
+ const isCompleted = isAnonymous ? false : (book.readProgress?.completed || false);
- const currentPage = ClientOfflineBookService.getCurrentPage(book);
+ const currentPage = isAnonymous ? 0 : ClientOfflineBookService.getCurrentPage(book);
const totalPages = book.pageCount;
- const showProgress = Boolean(showProgressUi && totalPages > 0 && currentPage > 0 && !isCompleted);
+ const showProgress = Boolean(!isAnonymous && showProgressUi && totalPages > 0 && currentPage > 0 && !isCompleted);
- const statusInfo = getReadingStatusInfo(book, t);
- const isRead = book.readProgress?.completed || false;
- const hasReadProgress = book.readProgress !== null || currentPage > 0;
+ const statusInfo = isAnonymous ? { label: "", className: "" } : getReadingStatusInfo(book, t);
+ const isRead = isAnonymous ? false : (book.readProgress?.completed || false);
+ const hasReadProgress = isAnonymous ? false : (book.readProgress !== null || currentPage > 0);
// Détermine si le livre doit être grisé (non accessible hors ligne)
const isUnavailable = !isAccessible;
@@ -115,7 +117,7 @@ export function BookCover({
{showControls && (
// Boutons en haut à droite avec un petit décalage
- {!isRead && (
+ {!isAnonymous && !isRead && (
)}
- {hasReadProgress && (
+ {!isAnonymous && hasReadProgress && (
handleMarkAsUnread()}
@@ -145,11 +147,13 @@ export function BookCover({
? t("navigation.volume", { number: book.number })
: "")}
-
-
- {statusInfo.label}
-
-
+ {!isAnonymous && (
+
+
+ {statusInfo.label}
+
+
+ )}
)}
@@ -162,12 +166,14 @@ export function BookCover({
? t("navigation.volume", { number: book.number })
: "")}
-
- {t("books.status.progress", {
- current: currentPage,
- total: book.pageCount,
- })}
-
+ {!isAnonymous && (
+
+ {t("books.status.progress", {
+ current: currentPage,
+ total: book.pageCount,
+ })}
+
+ )}
)}
>
diff --git a/src/components/ui/cover-utils.tsx b/src/components/ui/cover-utils.tsx
index 0f7f44a..3e4a1dd 100644
--- a/src/components/ui/cover-utils.tsx
+++ b/src/components/ui/cover-utils.tsx
@@ -18,4 +18,5 @@ export interface BookCoverProps extends BaseCoverProps {
export interface SeriesCoverProps extends BaseCoverProps {
series: NormalizedSeries;
+ isAnonymous?: boolean;
}
diff --git a/src/components/ui/series-cover.tsx b/src/components/ui/series-cover.tsx
index a606feb..8c9a428 100644
--- a/src/components/ui/series-cover.tsx
+++ b/src/components/ui/series-cover.tsx
@@ -7,12 +7,13 @@ export function SeriesCover({
alt = "Image de couverture",
className,
showProgressUi = true,
+ isAnonymous = false,
}: SeriesCoverProps) {
- const isCompleted = series.bookCount === series.booksReadCount;
+ const isCompleted = isAnonymous ? false : series.bookCount === series.booksReadCount;
- const readBooks = series.booksReadCount;
+ const readBooks = isAnonymous ? 0 : series.booksReadCount;
const totalBooks = series.bookCount;
- const showProgress = Boolean(showProgressUi && totalBooks > 0 && readBooks > 0 && !isCompleted);
+ const showProgress = Boolean(!isAnonymous && showProgressUi && totalBooks > 0 && readBooks > 0 && !isCompleted);
const missingCount = series.missingCount;
return (
diff --git a/src/contexts/AnonymousContext.tsx b/src/contexts/AnonymousContext.tsx
new file mode 100644
index 0000000..61e82aa
--- /dev/null
+++ b/src/contexts/AnonymousContext.tsx
@@ -0,0 +1,34 @@
+"use client";
+
+import React, { createContext, useContext, useMemo, useCallback } from "react";
+import { usePreferences } from "@/contexts/PreferencesContext";
+
+interface AnonymousContextType {
+ isAnonymous: boolean;
+ toggleAnonymous: () => void;
+}
+
+const AnonymousContext = createContext(undefined);
+
+export function AnonymousProvider({ children }: { children: React.ReactNode }) {
+ const { preferences, updatePreferences } = usePreferences();
+
+ const toggleAnonymous = useCallback(() => {
+ updatePreferences({ anonymousMode: !preferences.anonymousMode });
+ }, [preferences.anonymousMode, updatePreferences]);
+
+ const contextValue = useMemo(
+ () => ({ isAnonymous: preferences.anonymousMode, toggleAnonymous }),
+ [preferences.anonymousMode, toggleAnonymous]
+ );
+
+ return {children};
+}
+
+export function useAnonymous() {
+ const context = useContext(AnonymousContext);
+ if (context === undefined) {
+ throw new Error("useAnonymous must be used within an AnonymousProvider");
+ }
+ return context;
+}
diff --git a/src/i18n/messages/en/common.json b/src/i18n/messages/en/common.json
index 4ce593e..2a869af 100644
--- a/src/i18n/messages/en/common.json
+++ b/src/i18n/messages/en/common.json
@@ -452,6 +452,9 @@
"header": {
"toggleSidebar": "Toggle sidebar",
"toggleTheme": "Toggle theme",
+ "anonymousMode": "Anonymous mode",
+ "anonymousModeOn": "Anonymous mode enabled",
+ "anonymousModeOff": "Anonymous mode disabled",
"search": {
"placeholder": "Search series and books...",
"empty": "No results",
diff --git a/src/i18n/messages/fr/common.json b/src/i18n/messages/fr/common.json
index d5d75d3..f29a41e 100644
--- a/src/i18n/messages/fr/common.json
+++ b/src/i18n/messages/fr/common.json
@@ -450,6 +450,9 @@
"header": {
"toggleSidebar": "Afficher/masquer le menu latéral",
"toggleTheme": "Changer le thème",
+ "anonymousMode": "Mode anonyme",
+ "anonymousModeOn": "Mode anonyme activé",
+ "anonymousModeOff": "Mode anonyme désactivé",
"search": {
"placeholder": "Rechercher séries et tomes...",
"empty": "Aucun résultat",
diff --git a/src/lib/services/preferences.service.ts b/src/lib/services/preferences.service.ts
index a1387a4..88fa128 100644
--- a/src/lib/services/preferences.service.ts
+++ b/src/lib/services/preferences.service.ts
@@ -34,6 +34,7 @@ export class PreferencesService {
return {
showThumbnails: preferences.showThumbnails,
showOnlyUnread: preferences.showOnlyUnread,
+ anonymousMode: preferences.anonymousMode,
displayMode: {
...defaultPreferences.displayMode,
...displayMode,
@@ -72,6 +73,8 @@ export class PreferencesService {
}
if (preferences.readerPrefetchCount !== undefined)
updateData.readerPrefetchCount = preferences.readerPrefetchCount;
+ if (preferences.anonymousMode !== undefined)
+ updateData.anonymousMode = preferences.anonymousMode;
const updatedPreferences = await prisma.preferences.upsert({
where: { userId },
@@ -80,6 +83,7 @@ export class PreferencesService {
userId,
showThumbnails: preferences.showThumbnails ?? defaultPreferences.showThumbnails,
showOnlyUnread: preferences.showOnlyUnread ?? defaultPreferences.showOnlyUnread,
+ anonymousMode: preferences.anonymousMode ?? defaultPreferences.anonymousMode,
displayMode: preferences.displayMode ?? defaultPreferences.displayMode,
background: (preferences.background ??
defaultPreferences.background) as unknown as Prisma.InputJsonValue,
@@ -90,6 +94,7 @@ export class PreferencesService {
return {
showThumbnails: updatedPreferences.showThumbnails,
showOnlyUnread: updatedPreferences.showOnlyUnread,
+ anonymousMode: updatedPreferences.anonymousMode,
displayMode: updatedPreferences.displayMode as UserPreferences["displayMode"],
background: {
...defaultPreferences.background,
diff --git a/src/types/preferences.ts b/src/types/preferences.ts
index dde30d6..40bbb7b 100644
--- a/src/types/preferences.ts
+++ b/src/types/preferences.ts
@@ -12,6 +12,7 @@ export interface BackgroundPreferences {
export interface UserPreferences {
showThumbnails: boolean;
showOnlyUnread: boolean;
+ anonymousMode: boolean;
displayMode: {
compact: boolean;
itemsPerPage: number;
@@ -24,6 +25,7 @@ export interface UserPreferences {
export const defaultPreferences: UserPreferences = {
showThumbnails: true,
showOnlyUnread: false,
+ anonymousMode: false,
displayMode: {
compact: false,
itemsPerPage: 20,