fix: supprime le layout shift des boutons qBittorrent au chargement

La config qBittorrent est maintenant récupérée côté serveur et passée
en prop au QbittorrentProvider, évitant le fetch client qui causait
l'apparition tardive des boutons de téléchargement.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-27 08:38:57 +01:00
parent 59f9a657a4
commit f5ddeb461b
5 changed files with 29 additions and 11 deletions

View File

@@ -65,11 +65,12 @@ function formatEta(seconds: number): string {
interface DownloadsPageProps { interface DownloadsPageProps {
initialDownloads: TorrentDownloadDto[]; initialDownloads: TorrentDownloadDto[];
initialLatestFound: LatestFoundPerLibraryDto[]; initialLatestFound: LatestFoundPerLibraryDto[];
qbConfigured?: boolean;
} }
const PAGE_SIZE = 10; const PAGE_SIZE = 10;
export function DownloadsPage({ initialDownloads, initialLatestFound }: DownloadsPageProps) { export function DownloadsPage({ initialDownloads, initialLatestFound, qbConfigured }: DownloadsPageProps) {
const { t } = useTranslation(); const { t } = useTranslation();
const [downloads, setDownloads] = useState<TorrentDownloadDto[]>(initialDownloads); const [downloads, setDownloads] = useState<TorrentDownloadDto[]>(initialDownloads);
const [filter, setFilter] = useState<string>("all"); const [filter, setFilter] = useState<string>("all");
@@ -184,7 +185,7 @@ export function DownloadsPage({ initialDownloads, initialLatestFound }: Download
{/* Available downloads from latest detection */} {/* Available downloads from latest detection */}
{initialLatestFound.length > 0 && ( {initialLatestFound.length > 0 && (
<QbittorrentProvider> <QbittorrentProvider initialConfigured={qbConfigured}>
<div className="mt-10"> <div className="mt-10">
<h2 className="text-xl font-bold text-foreground mb-4 flex items-center gap-2"> <h2 className="text-xl font-bold text-foreground mb-4 flex items-center gap-2">
<Icon name="search" size="lg" /> <Icon name="search" size="lg" />

View File

@@ -3,10 +3,20 @@ import { DownloadsPage } from "./DownloadsPage";
export const dynamic = "force-dynamic"; export const dynamic = "force-dynamic";
async function isQbConfigured(): Promise<boolean> {
try {
const data = await apiFetch<{ url?: string; username?: string }>("/settings/qbittorrent");
return !!(data && data.url?.trim() && data.username?.trim());
} catch {
return false;
}
}
export default async function Page() { export default async function Page() {
const [downloads, latestFound] = await Promise.all([ const [downloads, latestFound, qbConfigured] = await Promise.all([
fetchTorrentDownloads().catch(() => [] as TorrentDownloadDto[]), fetchTorrentDownloads().catch(() => [] as TorrentDownloadDto[]),
apiFetch<LatestFoundPerLibraryDto[]>("/download-detection/latest-found").catch(() => [] as LatestFoundPerLibraryDto[]), apiFetch<LatestFoundPerLibraryDto[]>("/download-detection/latest-found").catch(() => [] as LatestFoundPerLibraryDto[]),
isQbConfigured(),
]); ]);
return <DownloadsPage initialDownloads={downloads} initialLatestFound={latestFound} />; return <DownloadsPage initialDownloads={downloads} initialLatestFound={latestFound} qbConfigured={qbConfigured} />;
} }

View File

@@ -48,15 +48,16 @@ export function DownloadDetectionErrorsCard({ results, t }: {
); );
} }
export function DownloadDetectionResultsCard({ results, libraryId, t }: { export function DownloadDetectionResultsCard({ results, libraryId, qbConfigured, t }: {
results: DownloadDetectionResultDto[]; results: DownloadDetectionResultDto[];
libraryId: string | null; libraryId: string | null;
qbConfigured?: boolean;
t: TranslateFunction; t: TranslateFunction;
}) { }) {
if (results.length === 0) return null; if (results.length === 0) return null;
return ( return (
<QbittorrentProvider> <QbittorrentProvider initialConfigured={qbConfigured}>
<Card className="lg:col-span-2"> <Card className="lg:col-span-2">
<CardHeader> <CardHeader>
<CardTitle>{t("jobDetail.downloadAvailableReleases")}</CardTitle> <CardTitle>{t("jobDetail.downloadAvailableReleases")}</CardTitle>

View File

@@ -149,11 +149,15 @@ export default async function JobDetailPage({ params }: JobDetailPageProps) {
let downloadDetectionReport: DownloadDetectionReportDto | null = null; let downloadDetectionReport: DownloadDetectionReportDto | null = null;
let downloadDetectionResults: DownloadDetectionResultDto[] = []; let downloadDetectionResults: DownloadDetectionResultDto[] = [];
let downloadDetectionErrors: DownloadDetectionResultDto[] = []; let downloadDetectionErrors: DownloadDetectionResultDto[] = [];
let qbConfigured = false;
if (isDownloadDetection) { if (isDownloadDetection) {
[downloadDetectionReport, downloadDetectionResults, downloadDetectionErrors] = await Promise.all([ [downloadDetectionReport, downloadDetectionResults, downloadDetectionErrors, qbConfigured] = await Promise.all([
getDownloadDetectionReport(id).catch(() => null), getDownloadDetectionReport(id).catch(() => null),
getDownloadDetectionResults(id, "found").catch(() => []), getDownloadDetectionResults(id, "found").catch(() => []),
getDownloadDetectionResults(id, "error").catch(() => []), getDownloadDetectionResults(id, "error").catch(() => []),
apiFetch<{ url?: string; username?: string }>("/settings/qbittorrent")
.then(d => !!(d?.url?.trim() && d?.username?.trim()))
.catch(() => false),
]); ]);
} }
@@ -273,7 +277,7 @@ export default async function JobDetailPage({ params }: JobDetailPageProps) {
{/* Download detection */} {/* Download detection */}
{isDownloadDetection && downloadDetectionReport && <DownloadDetectionReportCard report={downloadDetectionReport} t={t} />} {isDownloadDetection && downloadDetectionReport && <DownloadDetectionReportCard report={downloadDetectionReport} t={t} />}
{isDownloadDetection && <DownloadDetectionErrorsCard results={downloadDetectionErrors} t={t} />} {isDownloadDetection && <DownloadDetectionErrorsCard results={downloadDetectionErrors} t={t} />}
{isDownloadDetection && <DownloadDetectionResultsCard results={downloadDetectionResults} libraryId={job.library_id} t={t} />} {isDownloadDetection && <DownloadDetectionResultsCard results={downloadDetectionResults} libraryId={job.library_id} qbConfigured={qbConfigured} t={t} />}
{/* Metadata batch results */} {/* Metadata batch results */}
{isMetadataBatch && <MetadataBatchResultsCard results={batchResults} libraryId={job.library_id} t={t} />} {isMetadataBatch && <MetadataBatchResultsCard results={batchResults} libraryId={job.library_id} t={t} />}

View File

@@ -6,17 +6,19 @@ import { useTranslation } from "@/lib/i18n/context";
const QbConfigContext = createContext(false); const QbConfigContext = createContext(false);
export function QbittorrentProvider({ children }: { children: ReactNode }) { export function QbittorrentProvider({ children, initialConfigured }: { children: ReactNode; initialConfigured?: boolean }) {
const [configured, setConfigured] = useState(false); const [configured, setConfigured] = useState(initialConfigured ?? false);
useEffect(() => { useEffect(() => {
// Skip client fetch if server already told us
if (initialConfigured !== undefined) return;
fetch("/api/settings/qbittorrent") fetch("/api/settings/qbittorrent")
.then((r) => (r.ok ? r.json() : null)) .then((r) => (r.ok ? r.json() : null))
.then((data) => { .then((data) => {
setConfigured(!!(data && data.url && data.url.trim() && data.username && data.username.trim())); setConfigured(!!(data && data.url && data.url.trim() && data.username && data.username.trim()));
}) })
.catch(() => setConfigured(false)); .catch(() => setConfigured(false));
}, []); }, [initialConfigured]);
return <QbConfigContext.Provider value={configured}>{children}</QbConfigContext.Provider>; return <QbConfigContext.Provider value={configured}>{children}</QbConfigContext.Provider>;
} }