Merge branch 'main' into feat/debugmode
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffect, useState, useCallback } from "react";
|
||||
import { KomgaBook } from "@/types/komga";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { Card } from "@/components/ui/card";
|
||||
@@ -31,12 +31,11 @@ export function DownloadManager() {
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const { toast } = useToast();
|
||||
|
||||
const getStorageKey = (bookId: string) => `book-status-${bookId}`;
|
||||
const getStorageKey = useCallback((bookId: string) => `book-status-${bookId}`, []);
|
||||
|
||||
const loadDownloadedBooks = async () => {
|
||||
const loadDownloadedBooks = useCallback(async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
// Récupère tous les livres du localStorage
|
||||
const books: DownloadedBook[] = [];
|
||||
for (let i = 0; i < localStorage.length; i++) {
|
||||
const key = localStorage.key(i);
|
||||
@@ -70,9 +69,9 @@ export function DownloadManager() {
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
}, [toast]);
|
||||
|
||||
const updateBookStatuses = () => {
|
||||
const updateBookStatuses = useCallback(() => {
|
||||
setDownloadedBooks((prevBooks) => {
|
||||
return prevBooks.map((downloadedBook) => {
|
||||
const status = JSON.parse(
|
||||
@@ -87,29 +86,25 @@ export function DownloadManager() {
|
||||
};
|
||||
});
|
||||
});
|
||||
};
|
||||
}, [getStorageKey]);
|
||||
|
||||
useEffect(() => {
|
||||
loadDownloadedBooks();
|
||||
|
||||
// Écoute les changements de statut des livres
|
||||
const handleStorageChange = (e: StorageEvent) => {
|
||||
if (e.key?.startsWith("book-status-")) {
|
||||
updateBookStatuses();
|
||||
}
|
||||
};
|
||||
|
||||
// Écoute les changements dans d'autres onglets
|
||||
window.addEventListener("storage", handleStorageChange);
|
||||
|
||||
// Écoute les changements dans l'onglet courant
|
||||
const interval = setInterval(updateBookStatuses, 1000);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("storage", handleStorageChange);
|
||||
clearInterval(interval);
|
||||
};
|
||||
}, []);
|
||||
}, [loadDownloadedBooks, updateBookStatuses]);
|
||||
|
||||
const handleDeleteBook = async (book: KomgaBook) => {
|
||||
try {
|
||||
|
||||
@@ -2,11 +2,13 @@ import { HeroSection } from "./HeroSection";
|
||||
import { MediaRow } from "./MediaRow";
|
||||
import { KomgaBook, KomgaSeries } from "@/types/komga";
|
||||
import { RefreshButton } from "@/components/library/RefreshButton";
|
||||
import { BookOpenCheck, History, Sparkles, Clock } from "lucide-react";
|
||||
|
||||
interface HomeData {
|
||||
ongoing: KomgaSeries[];
|
||||
recentlyRead: KomgaBook[];
|
||||
onDeck: KomgaBook[];
|
||||
latestSeries: KomgaSeries[];
|
||||
}
|
||||
|
||||
interface HomeContentProps {
|
||||
@@ -61,15 +63,35 @@ export function HomeContent({ data, refreshHome }: HomeContentProps) {
|
||||
{/* Sections de contenu */}
|
||||
<div className="space-y-12">
|
||||
{data.ongoing && data.ongoing.length > 0 && (
|
||||
<MediaRow title="Continuer la lecture" items={optimizeSeriesData(data.ongoing)} />
|
||||
<MediaRow
|
||||
title="Continuer la lecture"
|
||||
items={optimizeSeriesData(data.ongoing)}
|
||||
icon={<BookOpenCheck className="w-6 h-6" />}
|
||||
/>
|
||||
)}
|
||||
|
||||
{data.onDeck && data.onDeck.length > 0 && (
|
||||
<MediaRow title="À suivre" items={optimizeBookData(data.onDeck)} />
|
||||
<MediaRow
|
||||
title="À suivre"
|
||||
items={optimizeBookData(data.onDeck)}
|
||||
icon={<Clock className="w-6 h-6" />}
|
||||
/>
|
||||
)}
|
||||
|
||||
{data.latestSeries && data.latestSeries.length > 0 && (
|
||||
<MediaRow
|
||||
title="Dernières séries"
|
||||
items={optimizeSeriesData(data.latestSeries)}
|
||||
icon={<Sparkles className="w-6 h-6" />}
|
||||
/>
|
||||
)}
|
||||
|
||||
{data.recentlyRead && data.recentlyRead.length > 0 && (
|
||||
<MediaRow title="Ajouts récents" items={optimizeBookData(data.recentlyRead)} />
|
||||
<MediaRow
|
||||
title="Ajouts récents"
|
||||
items={optimizeBookData(data.recentlyRead)}
|
||||
icon={<History className="w-6 h-6" />}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</main>
|
||||
|
||||
@@ -26,9 +26,10 @@ interface OptimizedBook extends BaseItem {
|
||||
interface MediaRowProps {
|
||||
title: string;
|
||||
items: (OptimizedSeries | OptimizedBook)[];
|
||||
icon?: React.ReactNode;
|
||||
}
|
||||
|
||||
export function MediaRow({ title, items }: MediaRowProps) {
|
||||
export function MediaRow({ title, items, icon }: MediaRowProps) {
|
||||
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
||||
const [showLeftArrow, setShowLeftArrow] = useState(false);
|
||||
const [showRightArrow, setShowRightArrow] = useState(true);
|
||||
@@ -58,7 +59,10 @@ export function MediaRow({ title, items }: MediaRowProps) {
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<h2 className="text-2xl font-bold tracking-tight">{title}</h2>
|
||||
<div className="flex items-center gap-2">
|
||||
{icon}
|
||||
<h2 className="text-2xl font-bold tracking-tight">{title}</h2>
|
||||
</div>
|
||||
<div className="relative">
|
||||
{/* Bouton de défilement gauche */}
|
||||
{showLeftArrow && (
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { Download, Check, Loader2 } from "lucide-react";
|
||||
import { Button } from "./button";
|
||||
import { useToast } from "./use-toast";
|
||||
@@ -27,156 +27,141 @@ export function BookOfflineButton({ book, className }: BookOfflineButtonProps) {
|
||||
const [downloadProgress, setDownloadProgress] = useState(0);
|
||||
const { toast } = useToast();
|
||||
|
||||
const getStorageKey = (bookId: string) => `book-status-${bookId}`;
|
||||
const getStorageKey = useCallback((bookId: string) => `book-status-${bookId}`, []);
|
||||
|
||||
const getBookStatus = (bookId: string): BookDownloadStatus => {
|
||||
try {
|
||||
const status = localStorage.getItem(getStorageKey(bookId));
|
||||
return status ? JSON.parse(status) : { status: "idle", progress: 0, timestamp: 0 };
|
||||
} catch {
|
||||
return { status: "idle", progress: 0, timestamp: 0 };
|
||||
}
|
||||
};
|
||||
|
||||
const setBookStatus = (bookId: string, status: BookDownloadStatus) => {
|
||||
localStorage.setItem(getStorageKey(bookId), JSON.stringify(status));
|
||||
};
|
||||
|
||||
const downloadBook = async (startFromPage: number = 1) => {
|
||||
try {
|
||||
const cache = await caches.open("stripstream-books");
|
||||
|
||||
// Marque le début du téléchargement
|
||||
setBookStatus(book.id, {
|
||||
status: "downloading",
|
||||
progress: ((startFromPage - 1) / book.media.pagesCount) * 100,
|
||||
timestamp: Date.now(),
|
||||
lastDownloadedPage: startFromPage - 1,
|
||||
});
|
||||
|
||||
// Ajoute le livre au cache si on commence depuis le début
|
||||
if (startFromPage === 1) {
|
||||
const pagesResponse = await fetch(`/api/komga/images/books/${book.id}/pages/1`);
|
||||
if (!pagesResponse.ok) throw new Error("Erreur lors de la récupération des pages");
|
||||
await cache.put(`/api/komga/images/books/${book.id}/pages`, pagesResponse.clone());
|
||||
const getBookStatus = useCallback(
|
||||
(bookId: string): BookDownloadStatus => {
|
||||
try {
|
||||
const status = localStorage.getItem(getStorageKey(bookId));
|
||||
return status ? JSON.parse(status) : { status: "idle", progress: 0, timestamp: 0 };
|
||||
} catch {
|
||||
return { status: "idle", progress: 0, timestamp: 0 };
|
||||
}
|
||||
},
|
||||
[getStorageKey]
|
||||
);
|
||||
|
||||
// Cache chaque page avec retry
|
||||
let failedPages = 0;
|
||||
for (let i = startFromPage; i <= book.media.pagesCount; i++) {
|
||||
let retryCount = 0;
|
||||
const maxRetries = 3;
|
||||
const setBookStatus = useCallback(
|
||||
(bookId: string, status: BookDownloadStatus) => {
|
||||
localStorage.setItem(getStorageKey(bookId), JSON.stringify(status));
|
||||
},
|
||||
[getStorageKey]
|
||||
);
|
||||
|
||||
while (retryCount < maxRetries) {
|
||||
try {
|
||||
const pageResponse = await fetch(`/api/komga/images/books/${book.id}/pages/${i}`);
|
||||
if (!pageResponse.ok) {
|
||||
const downloadBook = useCallback(
|
||||
async (startFromPage: number = 1) => {
|
||||
try {
|
||||
const cache = await caches.open("stripstream-books");
|
||||
|
||||
// Marque le début du téléchargement
|
||||
setBookStatus(book.id, {
|
||||
status: "downloading",
|
||||
progress: ((startFromPage - 1) / book.media.pagesCount) * 100,
|
||||
timestamp: Date.now(),
|
||||
lastDownloadedPage: startFromPage - 1,
|
||||
});
|
||||
|
||||
// Ajoute le livre au cache si on commence depuis le début
|
||||
if (startFromPage === 1) {
|
||||
const pagesResponse = await fetch(`/api/komga/images/books/${book.id}/pages/1`);
|
||||
if (!pagesResponse.ok) throw new Error("Erreur lors de la récupération des pages");
|
||||
await cache.put(`/api/komga/images/books/${book.id}/pages`, pagesResponse.clone());
|
||||
}
|
||||
|
||||
// Cache chaque page avec retry
|
||||
let failedPages = 0;
|
||||
for (let i = startFromPage; i <= book.media.pagesCount; i++) {
|
||||
let retryCount = 0;
|
||||
const maxRetries = 3;
|
||||
|
||||
while (retryCount < maxRetries) {
|
||||
try {
|
||||
const pageResponse = await fetch(`/api/komga/images/books/${book.id}/pages/${i}`);
|
||||
if (!pageResponse.ok) {
|
||||
retryCount++;
|
||||
if (retryCount === maxRetries) {
|
||||
failedPages++;
|
||||
console.error(
|
||||
`Échec du téléchargement de la page ${i} après ${maxRetries} tentatives`
|
||||
);
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000)); // Attendre 1s avant de réessayer
|
||||
continue;
|
||||
}
|
||||
await cache.put(
|
||||
`/api/komga/images/books/${book.id}/pages/${i}`,
|
||||
pageResponse.clone()
|
||||
);
|
||||
break; // Sortir de la boucle si réussi
|
||||
} catch (error) {
|
||||
retryCount++;
|
||||
if (retryCount === maxRetries) {
|
||||
failedPages++;
|
||||
console.error(
|
||||
`Échec du téléchargement de la page ${i} après ${maxRetries} tentatives`
|
||||
);
|
||||
console.error(`Erreur lors du téléchargement de la page ${i}:`, error);
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000)); // Attendre 1s avant de réessayer
|
||||
continue;
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
}
|
||||
await cache.put(`/api/komga/images/books/${book.id}/pages/${i}`, pageResponse.clone());
|
||||
break; // Sortir de la boucle si réussi
|
||||
} catch (error) {
|
||||
retryCount++;
|
||||
if (retryCount === maxRetries) {
|
||||
failedPages++;
|
||||
console.error(`Erreur lors du téléchargement de la page ${i}:`, error);
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
}
|
||||
|
||||
// Mise à jour du statut
|
||||
const progress = (i / book.media.pagesCount) * 100;
|
||||
setDownloadProgress(progress);
|
||||
setBookStatus(book.id, {
|
||||
status: "downloading",
|
||||
progress,
|
||||
timestamp: Date.now(),
|
||||
lastDownloadedPage: i,
|
||||
});
|
||||
|
||||
// Vérifier si le statut a changé pendant le téléchargement
|
||||
const currentStatus = getBookStatus(book.id);
|
||||
if (currentStatus.status === "idle") {
|
||||
// Le téléchargement a été annulé
|
||||
throw new Error("Téléchargement annulé");
|
||||
}
|
||||
}
|
||||
|
||||
// Mise à jour du statut
|
||||
const progress = (i / book.media.pagesCount) * 100;
|
||||
setDownloadProgress(progress);
|
||||
setBookStatus(book.id, {
|
||||
status: "downloading",
|
||||
progress,
|
||||
timestamp: Date.now(),
|
||||
lastDownloadedPage: i,
|
||||
});
|
||||
|
||||
// Vérifier si le statut a changé pendant le téléchargement
|
||||
const currentStatus = getBookStatus(book.id);
|
||||
if (currentStatus.status === "idle") {
|
||||
// Le téléchargement a été annulé
|
||||
throw new Error("Téléchargement annulé");
|
||||
}
|
||||
}
|
||||
|
||||
if (failedPages > 0) {
|
||||
// Si des pages ont échoué, on supprime tout le cache pour ce livre
|
||||
await cache.delete(`/api/komga/images/books/${book.id}/pages`);
|
||||
for (let i = 1; i <= book.media.pagesCount; i++) {
|
||||
await cache.delete(`/api/komga/images/books/${book.id}/pages/${i}`);
|
||||
}
|
||||
setIsAvailableOffline(false);
|
||||
setBookStatus(book.id, { status: "error", progress: 0, timestamp: Date.now() });
|
||||
toast({
|
||||
title: "Erreur",
|
||||
description: `${failedPages} page(s) n'ont pas pu être téléchargées. Le livre ne sera pas disponible hors ligne.`,
|
||||
variant: "destructive",
|
||||
});
|
||||
} else {
|
||||
setIsAvailableOffline(true);
|
||||
setBookStatus(book.id, { status: "available", progress: 100, timestamp: Date.now() });
|
||||
toast({
|
||||
title: "Livre téléchargé",
|
||||
description: "Le livre est maintenant disponible hors ligne",
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Erreur lors du téléchargement:", error);
|
||||
// Ne pas changer le statut si le téléchargement a été volontairement annulé
|
||||
if ((error as Error)?.message !== "Téléchargement annulé") {
|
||||
setBookStatus(book.id, { status: "error", progress: 0, timestamp: Date.now() });
|
||||
toast({
|
||||
title: "Erreur",
|
||||
description: "Une erreur est survenue lors du téléchargement",
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
setDownloadProgress(0);
|
||||
}
|
||||
};
|
||||
|
||||
// Vérifie si le livre est déjà disponible hors ligne
|
||||
useEffect(() => {
|
||||
const checkStatus = async () => {
|
||||
const storedStatus = getBookStatus(book.id);
|
||||
|
||||
// Si le livre est marqué comme en cours de téléchargement
|
||||
if (storedStatus.status === "downloading") {
|
||||
// Si le téléchargement a commencé il y a plus de 5 minutes, on considère qu'il a échoué
|
||||
if (Date.now() - storedStatus.timestamp > 5 * 60 * 1000) {
|
||||
if (failedPages > 0) {
|
||||
// Si des pages ont échoué, on supprime tout le cache pour ce livre
|
||||
await cache.delete(`/api/komga/images/books/${book.id}/pages`);
|
||||
for (let i = 1; i <= book.media.pagesCount; i++) {
|
||||
await cache.delete(`/api/komga/images/books/${book.id}/pages/${i}`);
|
||||
}
|
||||
setIsAvailableOffline(false);
|
||||
setBookStatus(book.id, { status: "error", progress: 0, timestamp: Date.now() });
|
||||
setIsLoading(false);
|
||||
setDownloadProgress(0);
|
||||
toast({
|
||||
title: "Erreur",
|
||||
description: `${failedPages} page(s) n'ont pas pu être téléchargées. Le livre ne sera pas disponible hors ligne.`,
|
||||
variant: "destructive",
|
||||
});
|
||||
} else {
|
||||
// On reprend le téléchargement là où il s'était arrêté
|
||||
setIsLoading(true);
|
||||
setDownloadProgress(storedStatus.progress);
|
||||
const startFromPage = (storedStatus.lastDownloadedPage || 0) + 1;
|
||||
downloadBook(startFromPage);
|
||||
setIsAvailableOffline(true);
|
||||
setBookStatus(book.id, { status: "available", progress: 100, timestamp: Date.now() });
|
||||
toast({
|
||||
title: "Livre téléchargé",
|
||||
description: "Le livre est maintenant disponible hors ligne",
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Erreur lors du téléchargement:", error);
|
||||
// Ne pas changer le statut si le téléchargement a été volontairement annulé
|
||||
if ((error as Error)?.message !== "Téléchargement annulé") {
|
||||
setBookStatus(book.id, { status: "error", progress: 0, timestamp: Date.now() });
|
||||
toast({
|
||||
title: "Erreur",
|
||||
description: "Une erreur est survenue lors du téléchargement",
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
setDownloadProgress(0);
|
||||
}
|
||||
},
|
||||
[book.id, book.media.pagesCount, getBookStatus, setBookStatus, toast]
|
||||
);
|
||||
|
||||
await checkOfflineAvailability();
|
||||
};
|
||||
|
||||
checkStatus();
|
||||
}, [book.id]);
|
||||
|
||||
const checkOfflineAvailability = async () => {
|
||||
const checkOfflineAvailability = useCallback(async () => {
|
||||
if (!("caches" in window)) return;
|
||||
|
||||
try {
|
||||
@@ -209,7 +194,30 @@ export function BookOfflineButton({ book, className }: BookOfflineButtonProps) {
|
||||
console.error("Erreur lors de la vérification du cache:", error);
|
||||
setBookStatus(book.id, { status: "error", progress: 0, timestamp: Date.now() });
|
||||
}
|
||||
};
|
||||
}, [book.id, book.media.pagesCount, setBookStatus]);
|
||||
|
||||
useEffect(() => {
|
||||
const checkStatus = async () => {
|
||||
const storedStatus = getBookStatus(book.id);
|
||||
|
||||
if (storedStatus.status === "downloading") {
|
||||
if (Date.now() - storedStatus.timestamp > 5 * 60 * 1000) {
|
||||
setBookStatus(book.id, { status: "error", progress: 0, timestamp: Date.now() });
|
||||
setIsLoading(false);
|
||||
setDownloadProgress(0);
|
||||
} else {
|
||||
setIsLoading(true);
|
||||
setDownloadProgress(storedStatus.progress);
|
||||
const startFromPage = (storedStatus.lastDownloadedPage || 0) + 1;
|
||||
downloadBook(startFromPage);
|
||||
}
|
||||
}
|
||||
|
||||
await checkOfflineAvailability();
|
||||
};
|
||||
|
||||
checkStatus();
|
||||
}, [book.id, checkOfflineAvailability, downloadBook, getBookStatus, setBookStatus]);
|
||||
|
||||
const handleToggleOffline = async () => {
|
||||
if (!("caches" in window)) {
|
||||
|
||||
Reference in New Issue
Block a user