feat(i18n): download page
This commit is contained in:
@@ -1,11 +1,9 @@
|
|||||||
import { PageHeader } from "@/components/layout/PageHeader";
|
|
||||||
import { DownloadManager } from "@/components/downloads/DownloadManager";
|
import { DownloadManager } from "@/components/downloads/DownloadManager";
|
||||||
import { withPageTiming } from "@/lib/hoc/withPageTiming";
|
import { withPageTiming } from "@/lib/hoc/withPageTiming";
|
||||||
|
|
||||||
function DownloadsPage() {
|
function DownloadsPage() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PageHeader title="Téléchargements" description="Gérez vos livres disponibles hors ligne" />
|
|
||||||
<DownloadManager />
|
<DownloadManager />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { Progress } from "@/components/ui/progress";
|
|||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { BookOfflineButton } from "@/components/ui/book-offline-button";
|
import { BookOfflineButton } from "@/components/ui/book-offline-button";
|
||||||
|
import { useTranslate } from "@/hooks/useTranslate";
|
||||||
|
|
||||||
type BookStatus = "idle" | "downloading" | "available" | "error";
|
type BookStatus = "idle" | "downloading" | "available" | "error";
|
||||||
|
|
||||||
@@ -30,6 +31,7 @@ export function DownloadManager() {
|
|||||||
const [downloadedBooks, setDownloadedBooks] = useState<DownloadedBook[]>([]);
|
const [downloadedBooks, setDownloadedBooks] = useState<DownloadedBook[]>([]);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
const { t } = useTranslate();
|
||||||
|
|
||||||
const getStorageKey = useCallback((bookId: string) => `book-status-${bookId}`, []);
|
const getStorageKey = useCallback((bookId: string) => `book-status-${bookId}`, []);
|
||||||
|
|
||||||
@@ -116,26 +118,25 @@ export function DownloadManager() {
|
|||||||
localStorage.removeItem(getStorageKey(book.id));
|
localStorage.removeItem(getStorageKey(book.id));
|
||||||
setDownloadedBooks((prev) => prev.filter((b) => b.book.id !== book.id));
|
setDownloadedBooks((prev) => prev.filter((b) => b.book.id !== book.id));
|
||||||
toast({
|
toast({
|
||||||
title: "Livre supprimé",
|
title: t("downloads.toast.deleted"),
|
||||||
description: "Le livre n'est plus disponible hors ligne",
|
description: t("downloads.toast.deletedDesc"),
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Erreur lors de la suppression du livre:", error);
|
console.error("Erreur lors de la suppression du livre:", error);
|
||||||
toast({
|
toast({
|
||||||
title: "Erreur",
|
title: t("downloads.toast.error"),
|
||||||
description: "Une erreur est survenue lors de la suppression",
|
description: t("downloads.toast.errorDesc"),
|
||||||
variant: "destructive",
|
variant: "destructive",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRetryDownload = async (book: KomgaBook) => {
|
const handleRetryDownload = async (book: KomgaBook) => {
|
||||||
// Réinitialise le statut et laisse le composant BookOfflineButton gérer le téléchargement
|
|
||||||
localStorage.removeItem(getStorageKey(book.id));
|
localStorage.removeItem(getStorageKey(book.id));
|
||||||
setDownloadedBooks((prev) => prev.filter((b) => b.book.id !== book.id));
|
setDownloadedBooks((prev) => prev.filter((b) => b.book.id !== book.id));
|
||||||
toast({
|
toast({
|
||||||
title: "Téléchargement relancé",
|
title: t("downloads.toast.retry"),
|
||||||
description: "Le téléchargement va reprendre depuis le début",
|
description: t("downloads.toast.retryDesc"),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -148,18 +149,33 @@ export function DownloadManager() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
|
<div className="flex flex-col gap-1 mb-8">
|
||||||
|
<h1 className="text-3xl font-bold tracking-tight">{t("downloads.page.title")}</h1>
|
||||||
|
{t("downloads.page.description") && (
|
||||||
|
<p className="text-lg text-muted-foreground">{t("downloads.page.description")}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
<Tabs defaultValue="all" className="space-y-4">
|
<Tabs defaultValue="all" className="space-y-4">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<TabsList>
|
<TabsList>
|
||||||
<TabsTrigger value="all">Tous ({downloadedBooks.length})</TabsTrigger>
|
<TabsTrigger value="all">
|
||||||
|
{t("downloads.tabs.all", { count: downloadedBooks.length })}
|
||||||
|
</TabsTrigger>
|
||||||
<TabsTrigger value="downloading">
|
<TabsTrigger value="downloading">
|
||||||
En cours ({downloadedBooks.filter((b) => b.status.status === "downloading").length})
|
{t("downloads.tabs.downloading", {
|
||||||
|
count: downloadedBooks.filter((b) => b.status.status === "downloading").length,
|
||||||
|
})}
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
<TabsTrigger value="available">
|
<TabsTrigger value="available">
|
||||||
Disponibles ({downloadedBooks.filter((b) => b.status.status === "available").length})
|
{t("downloads.tabs.available", {
|
||||||
|
count: downloadedBooks.filter((b) => b.status.status === "available").length,
|
||||||
|
})}
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
<TabsTrigger value="error">
|
<TabsTrigger value="error">
|
||||||
Erreurs ({downloadedBooks.filter((b) => b.status.status === "error").length})
|
{t("downloads.tabs.error", {
|
||||||
|
count: downloadedBooks.filter((b) => b.status.status === "error").length,
|
||||||
|
})}
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
{downloadedBooks.some((b) => b.status.status === "error") && (
|
{downloadedBooks.some((b) => b.status.status === "error") && (
|
||||||
@@ -170,14 +186,14 @@ export function DownloadManager() {
|
|||||||
const errorBooks = downloadedBooks.filter((b) => b.status.status === "error");
|
const errorBooks = downloadedBooks.filter((b) => b.status.status === "error");
|
||||||
errorBooks.forEach((book) => handleRetryDownload(book.book));
|
errorBooks.forEach((book) => handleRetryDownload(book.book));
|
||||||
toast({
|
toast({
|
||||||
title: "Relance des téléchargements",
|
title: t("downloads.toast.retryAll"),
|
||||||
description: `${errorBooks.length} téléchargement(s) relancé(s)`,
|
description: t("downloads.toast.retryAllDesc", { count: errorBooks.length }),
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
className="gap-2"
|
className="gap-2"
|
||||||
>
|
>
|
||||||
<Download className="h-4 w-4" />
|
<Download className="h-4 w-4" />
|
||||||
Tout relancer
|
{t("downloads.actions.retryAll")}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -193,7 +209,7 @@ export function DownloadManager() {
|
|||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
{downloadedBooks.length === 0 && (
|
{downloadedBooks.length === 0 && (
|
||||||
<p className="text-center text-muted-foreground p-8">Aucun livre téléchargé</p>
|
<p className="text-center text-muted-foreground p-8">{t("downloads.empty.all")}</p>
|
||||||
)}
|
)}
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
@@ -210,7 +226,9 @@ export function DownloadManager() {
|
|||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
{downloadedBooks.filter((b) => b.status.status === "downloading").length === 0 && (
|
{downloadedBooks.filter((b) => b.status.status === "downloading").length === 0 && (
|
||||||
<p className="text-center text-muted-foreground p-8">Aucun téléchargement en cours</p>
|
<p className="text-center text-muted-foreground p-8">
|
||||||
|
{t("downloads.empty.downloading")}
|
||||||
|
</p>
|
||||||
)}
|
)}
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
@@ -227,7 +245,9 @@ export function DownloadManager() {
|
|||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
{downloadedBooks.filter((b) => b.status.status === "available").length === 0 && (
|
{downloadedBooks.filter((b) => b.status.status === "available").length === 0 && (
|
||||||
<p className="text-center text-muted-foreground p-8">Aucun livre disponible hors ligne</p>
|
<p className="text-center text-muted-foreground p-8">
|
||||||
|
{t("downloads.empty.available")}
|
||||||
|
</p>
|
||||||
)}
|
)}
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
@@ -244,10 +264,11 @@ export function DownloadManager() {
|
|||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
{downloadedBooks.filter((b) => b.status.status === "error").length === 0 && (
|
{downloadedBooks.filter((b) => b.status.status === "error").length === 0 && (
|
||||||
<p className="text-center text-muted-foreground p-8">Aucune erreur</p>
|
<p className="text-center text-muted-foreground p-8">{t("downloads.empty.error")}</p>
|
||||||
)}
|
)}
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -259,9 +280,11 @@ interface BookDownloadCardProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function BookDownloadCard({ book, status, onDelete, onRetry }: BookDownloadCardProps) {
|
function BookDownloadCard({ book, status, onDelete, onRetry }: BookDownloadCardProps) {
|
||||||
|
const { t } = useTranslate();
|
||||||
|
|
||||||
const formatSize = (bytes: number) => {
|
const formatSize = (bytes: number) => {
|
||||||
const mb = bytes / (1024 * 1024);
|
const mb = bytes / (1024 * 1024);
|
||||||
return `${mb.toFixed(1)} Mo`;
|
return t("downloads.info.size", { size: mb.toFixed(1) });
|
||||||
};
|
};
|
||||||
|
|
||||||
const getStatusIcon = (status: BookStatus) => {
|
const getStatusIcon = (status: BookStatus) => {
|
||||||
@@ -278,16 +301,7 @@ function BookDownloadCard({ book, status, onDelete, onRetry }: BookDownloadCardP
|
|||||||
};
|
};
|
||||||
|
|
||||||
const getStatusText = (status: BookStatus) => {
|
const getStatusText = (status: BookStatus) => {
|
||||||
switch (status) {
|
return t(`downloads.status.${status}`);
|
||||||
case "downloading":
|
|
||||||
return "En cours de téléchargement";
|
|
||||||
case "available":
|
|
||||||
return "Disponible hors ligne";
|
|
||||||
case "error":
|
|
||||||
return "Erreur de téléchargement";
|
|
||||||
default:
|
|
||||||
return "Non téléchargé";
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -296,7 +310,7 @@ function BookDownloadCard({ book, status, onDelete, onRetry }: BookDownloadCardP
|
|||||||
<div className="relative w-12 aspect-[2/3] bg-muted rounded overflow-hidden">
|
<div className="relative w-12 aspect-[2/3] bg-muted rounded overflow-hidden">
|
||||||
<Image
|
<Image
|
||||||
src={`/api/komga/images/books/${book.id}/thumbnail`}
|
src={`/api/komga/images/books/${book.id}/thumbnail`}
|
||||||
alt={`Couverture de ${book.metadata?.title}`}
|
alt={t("books.coverAlt", { title: book.metadata?.title })}
|
||||||
className="object-cover"
|
className="object-cover"
|
||||||
fill
|
fill
|
||||||
sizes="48px"
|
sizes="48px"
|
||||||
@@ -309,7 +323,7 @@ function BookDownloadCard({ book, status, onDelete, onRetry }: BookDownloadCardP
|
|||||||
className="hover:underline hover:text-primary transition-colors"
|
className="hover:underline hover:text-primary transition-colors"
|
||||||
>
|
>
|
||||||
<h3 className="font-medium truncate">
|
<h3 className="font-medium truncate">
|
||||||
{book.metadata?.title || `Tome ${book.metadata?.number}`}
|
{book.metadata?.title || t("books.title", { number: book.metadata?.number })}
|
||||||
</h3>
|
</h3>
|
||||||
</Link>
|
</Link>
|
||||||
<div className="flex items-center gap-2 text-xs text-muted-foreground">
|
<div className="flex items-center gap-2 text-xs text-muted-foreground">
|
||||||
@@ -317,10 +331,11 @@ function BookDownloadCard({ book, status, onDelete, onRetry }: BookDownloadCardP
|
|||||||
<span>•</span>
|
<span>•</span>
|
||||||
<span>
|
<span>
|
||||||
{status.status === "downloading"
|
{status.status === "downloading"
|
||||||
? `${Math.floor((status.progress * book.media.pagesCount) / 100)}/${
|
? t("downloads.info.pages", {
|
||||||
book.media.pagesCount
|
current: Math.floor((status.progress * book.media.pagesCount) / 100),
|
||||||
} pages`
|
total: book.media.pagesCount,
|
||||||
: `${book.media.pagesCount} pages`}
|
})
|
||||||
|
: t("downloads.info.totalPages", { count: book.media.pagesCount })}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||||
@@ -342,7 +357,7 @@ function BookDownloadCard({ book, status, onDelete, onRetry }: BookDownloadCardP
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
onClick={onRetry}
|
onClick={onRetry}
|
||||||
title="Réessayer"
|
title={t("downloads.actions.retry")}
|
||||||
className="h-8 w-8 p-0 rounded-br-lg rounded-tl-lg"
|
className="h-8 w-8 p-0 rounded-br-lg rounded-tl-lg"
|
||||||
>
|
>
|
||||||
<Download className="h-5 w-5" />
|
<Download className="h-5 w-5" />
|
||||||
@@ -354,7 +369,7 @@ function BookDownloadCard({ book, status, onDelete, onRetry }: BookDownloadCardP
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
onClick={onDelete}
|
onClick={onDelete}
|
||||||
title="Supprimer"
|
title={t("downloads.actions.delete")}
|
||||||
className="h-8 w-8 p-0 rounded-br-lg rounded-tl-lg"
|
className="h-8 w-8 p-0 rounded-br-lg rounded-tl-lg"
|
||||||
>
|
>
|
||||||
<Trash2 className="h-5 w-5" />
|
<Trash2 className="h-5 w-5" />
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
interface PageHeaderProps {
|
|
||||||
title: string;
|
|
||||||
description?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function PageHeader({ title, description }: PageHeaderProps) {
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col gap-1 mb-8">
|
|
||||||
<h1 className="text-3xl font-bold tracking-tight">{title}</h1>
|
|
||||||
{description && <p className="text-lg text-muted-foreground">{description}</p>}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -174,5 +174,49 @@
|
|||||||
"unread": "Unread"
|
"unread": "Unread"
|
||||||
},
|
},
|
||||||
"loading": "Loading..."
|
"loading": "Loading..."
|
||||||
|
},
|
||||||
|
"downloads": {
|
||||||
|
"page": {
|
||||||
|
"title": "Downloads",
|
||||||
|
"description": "Manage your offline available books"
|
||||||
|
},
|
||||||
|
"tabs": {
|
||||||
|
"all": "All ({count})",
|
||||||
|
"downloading": "Downloading ({count})",
|
||||||
|
"available": "Available ({count})",
|
||||||
|
"error": "Errors ({count})"
|
||||||
|
},
|
||||||
|
"empty": {
|
||||||
|
"all": "No downloaded books",
|
||||||
|
"downloading": "No downloads in progress",
|
||||||
|
"available": "No books available offline",
|
||||||
|
"error": "No errors"
|
||||||
|
},
|
||||||
|
"actions": {
|
||||||
|
"retryAll": "Retry all",
|
||||||
|
"retry": "Retry",
|
||||||
|
"delete": "Delete"
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"downloading": "Downloading",
|
||||||
|
"available": "Available offline",
|
||||||
|
"error": "Download error",
|
||||||
|
"idle": "Not downloaded"
|
||||||
|
},
|
||||||
|
"toast": {
|
||||||
|
"deleted": "Book deleted",
|
||||||
|
"deletedDesc": "The book is no longer available offline",
|
||||||
|
"error": "Error",
|
||||||
|
"errorDesc": "An error occurred while deleting",
|
||||||
|
"retryAll": "Retrying downloads",
|
||||||
|
"retryAllDesc": "{count} download(s) restarted",
|
||||||
|
"retryDesc": "Download will start from the beginning",
|
||||||
|
"retry": "Download restarted"
|
||||||
|
},
|
||||||
|
"info": {
|
||||||
|
"size": "{size} MB",
|
||||||
|
"pages": "{current}/{total} pages",
|
||||||
|
"totalPages": "{count} pages"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -174,5 +174,49 @@
|
|||||||
"unread": "À lire"
|
"unread": "À lire"
|
||||||
},
|
},
|
||||||
"loading": "Chargement..."
|
"loading": "Chargement..."
|
||||||
|
},
|
||||||
|
"downloads": {
|
||||||
|
"page": {
|
||||||
|
"title": "Téléchargements",
|
||||||
|
"description": "Gérez vos livres disponibles hors ligne"
|
||||||
|
},
|
||||||
|
"tabs": {
|
||||||
|
"all": "Tous ({count})",
|
||||||
|
"downloading": "En cours ({count})",
|
||||||
|
"available": "Disponibles ({count})",
|
||||||
|
"error": "Erreurs ({count})"
|
||||||
|
},
|
||||||
|
"empty": {
|
||||||
|
"all": "Aucun livre téléchargé",
|
||||||
|
"downloading": "Aucun téléchargement en cours",
|
||||||
|
"available": "Aucun livre disponible hors ligne",
|
||||||
|
"error": "Aucune erreur"
|
||||||
|
},
|
||||||
|
"actions": {
|
||||||
|
"retryAll": "Tout relancer",
|
||||||
|
"retry": "Réessayer",
|
||||||
|
"delete": "Supprimer"
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"downloading": "En cours de téléchargement",
|
||||||
|
"available": "Disponible hors ligne",
|
||||||
|
"error": "Erreur de téléchargement",
|
||||||
|
"idle": "Non téléchargé"
|
||||||
|
},
|
||||||
|
"toast": {
|
||||||
|
"deleted": "Livre supprimé",
|
||||||
|
"deletedDesc": "Le livre n'est plus disponible hors ligne",
|
||||||
|
"error": "Erreur",
|
||||||
|
"errorDesc": "Une erreur est survenue lors de la suppression",
|
||||||
|
"retryAll": "Relance des téléchargements",
|
||||||
|
"retryAllDesc": "{count} téléchargement(s) relancé(s)",
|
||||||
|
"retryDesc": "Le téléchargement va reprendre depuis le début",
|
||||||
|
"retry": "Téléchargement relancé"
|
||||||
|
},
|
||||||
|
"info": {
|
||||||
|
"size": "{size} Mo",
|
||||||
|
"pages": "{current}/{total} pages",
|
||||||
|
"totalPages": "{count} pages"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user