Migration DB (0070 + 0071): - Backup automatique de book_reading_progress avant migration - Crée table series (fusion de series_metadata) avec UUID PK - Ajoute series_id FK à books, external_metadata_links, anilist_series_links, available_downloads, download_detection_results - Supprime les colonnes TEXT legacy et la table series_metadata Backend API + Indexer: - Toutes les queries SQL migrées vers series_id FK + JOIN series - Routes /series/:name → /series/:series_id (UUID) - Nouvel endpoint GET /series/by-name/:name pour lookup par nom - match_title_volumes() factorisé entre prowlarr.rs et download_detection.rs - Fix scheduler.rs: settings → app_settings - OpenAPI mis à jour avec les nouveaux endpoints Frontend: - Routes /libraries/[id]/series/[name] → /series/[seriesId] - Tous les composants (Edit, Delete, MarkRead, Prowlarr, Metadata, ReadingStatus) utilisent seriesId - compressVolumes() pour afficher T1→3 au lieu de T1 T2 T3 - Titre release en entier (plus de truncate) dans available downloads Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
78 lines
2.7 KiB
TypeScript
78 lines
2.7 KiB
TypeScript
"use client";
|
|
|
|
import { useState } from "react";
|
|
import { useRouter } from "next/navigation";
|
|
import { useTranslation } from "../../lib/i18n/context";
|
|
|
|
interface MarkSeriesReadButtonProps {
|
|
seriesId: string;
|
|
seriesName: string;
|
|
bookCount: number;
|
|
booksReadCount: number;
|
|
}
|
|
|
|
export function MarkSeriesReadButton({ seriesId, seriesName, bookCount, booksReadCount }: MarkSeriesReadButtonProps) {
|
|
const { t } = useTranslation();
|
|
const [loading, setLoading] = useState(false);
|
|
const router = useRouter();
|
|
|
|
const allRead = booksReadCount >= bookCount;
|
|
const targetStatus = allRead ? "unread" : "read";
|
|
const label = allRead ? t("markRead.markUnread") : t("markRead.markAllRead");
|
|
|
|
const handleClick = async (e: React.MouseEvent) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
setLoading(true);
|
|
try {
|
|
const res = await fetch("/api/series/mark-read", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ series: seriesId, status: targetStatus }),
|
|
});
|
|
if (!res.ok) {
|
|
const body = await res.json().catch(() => ({ error: res.statusText }));
|
|
console.error("Failed to mark series:", body.error);
|
|
}
|
|
router.refresh();
|
|
} catch (err) {
|
|
console.error("Failed to mark series:", err);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<button
|
|
onClick={handleClick}
|
|
disabled={loading}
|
|
className={`inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg border text-sm font-medium transition-colors disabled:opacity-50 ${
|
|
allRead
|
|
? "border-green-500/30 bg-green-500/10 text-green-600 hover:bg-green-500/20"
|
|
: "border-border bg-card text-muted-foreground hover:text-foreground hover:border-primary"
|
|
}`}
|
|
>
|
|
{loading ? (
|
|
<svg className="w-4 h-4 animate-spin" fill="none" viewBox="0 0 24 24">
|
|
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
|
|
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
|
|
</svg>
|
|
) : allRead ? (
|
|
<>
|
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" strokeWidth={2}>
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M9 15 3 9m0 0 6-6M3 9h12a6 6 0 0 1 0 12h-3" />
|
|
</svg>
|
|
{label}
|
|
</>
|
|
) : (
|
|
<>
|
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" strokeWidth={2}>
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M9 12.75 11.25 15 15 9.75M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0z" />
|
|
</svg>
|
|
{label}
|
|
</>
|
|
)}
|
|
</button>
|
|
);
|
|
}
|