174 lines
5.7 KiB
TypeScript
174 lines
5.7 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useRef, useState } from "react";
|
|
import "./latest-activity.css";
|
|
import { lockBodyScroll } from "../../lib/scrollLock";
|
|
|
|
type Event = { id: string; service: string; state: string; message: string; started_at?: string; finished_at?: string };
|
|
type Operation = {
|
|
summary?: { title: string; message: string; next: string; action?: string };
|
|
id: string;
|
|
label: string;
|
|
status: string;
|
|
events: Event[];
|
|
};
|
|
|
|
export default function LatestActivity({
|
|
operation,
|
|
besideDownload,
|
|
onDismiss,
|
|
}: {
|
|
operation: Operation;
|
|
besideDownload: boolean;
|
|
onDismiss: () => void;
|
|
}) {
|
|
const dialog = useRef<HTMLDialogElement>(null);
|
|
const trigger = useRef<HTMLButtonElement>(null);
|
|
const [open, setOpen] = useState(true);
|
|
useEffect(() => {
|
|
void operation.id;
|
|
setOpen(true);
|
|
}, [operation.id]);
|
|
// Events are appended in order. Client result events have no timestamp.
|
|
const latest = operation.events.at(-1);
|
|
const working = operation.status === "running" || operation.status === "searching";
|
|
const message = latest?.message ?? "";
|
|
const choosing = /[1-9]\d* releases? (found|shown)/i.test(message);
|
|
const sent =
|
|
operation.status === "complete" &&
|
|
/sent|accepted.*release/i.test(message) &&
|
|
/download|release|Sonarr|Radarr/i.test(message);
|
|
const interrupted = latest?.id === "connection-error";
|
|
const status =
|
|
operation.summary?.title ??
|
|
(working
|
|
? "Working on it"
|
|
: choosing
|
|
? "Choose a download"
|
|
: operation.status === "complete"
|
|
? "Done"
|
|
: "Needs your attention");
|
|
const currentStep =
|
|
operation.summary?.message ??
|
|
(working
|
|
? /send release/i.test(operation.label)
|
|
? "Sending your download..."
|
|
: /search/i.test(operation.label)
|
|
? "Looking for a download..."
|
|
: latest?.service === "Jellyfin"
|
|
? "Checking if it is ready to watch..."
|
|
: latest?.service === "qBittorrent"
|
|
? "Checking your download..."
|
|
: "Checking your request..."
|
|
: interrupted
|
|
? "The connection was lost."
|
|
: choosing
|
|
? "The search is finished. Choose a version to download."
|
|
: sent
|
|
? "Your download has been sent."
|
|
: operation.status === "error"
|
|
? "We could not finish this step."
|
|
: "This check is finished.");
|
|
const nextStep =
|
|
operation.summary?.next ??
|
|
(working
|
|
? "Please wait. You can close this box while we work."
|
|
: interrupted
|
|
? "Close this box and check the request before trying again."
|
|
: choosing
|
|
? "Close this box to see the available downloads."
|
|
: sent
|
|
? "You can close this box. The request will update when the download starts."
|
|
: operation.status === "error"
|
|
? "Close this box to review the request and its available options."
|
|
: "Close this box to see the updated request status.");
|
|
const progress = (
|
|
<div
|
|
className={`activity-process is-${working ? "working" : operation.status}`}
|
|
role="progressbar"
|
|
aria-label={currentStep}
|
|
aria-valuemin={0}
|
|
aria-valuemax={100}
|
|
aria-valuenow={working || operation.status === "error" ? undefined : 100}
|
|
>
|
|
<span />
|
|
</div>
|
|
);
|
|
|
|
useEffect(() => {
|
|
if (!open) return;
|
|
const element = dialog.current;
|
|
element?.showModal();
|
|
const unlock = lockBodyScroll();
|
|
return () => {
|
|
element?.close();
|
|
unlock();
|
|
trigger.current?.focus();
|
|
};
|
|
}, [open]);
|
|
|
|
return (
|
|
<div className={`request-overview-block latest-activity ${besideDownload ? "beside-download" : "full-row"}`}>
|
|
<button
|
|
ref={trigger}
|
|
type="button"
|
|
className="latest-activity-trigger"
|
|
onClick={() => setOpen(true)}
|
|
aria-haspopup="dialog"
|
|
aria-expanded={open}
|
|
>
|
|
<span className="latest-activity-heading">
|
|
<span className="request-overview-label">Latest activity</span>
|
|
<span className={`latest-activity-badge is-${operation.status}`}>{status}</span>
|
|
</span>
|
|
<span className="latest-activity-message" role="status">
|
|
{working && <span className="activity-spinner" aria-hidden="true" />}
|
|
{currentStep}
|
|
</span>
|
|
{progress}
|
|
<span className="latest-activity-more">View progress</span>
|
|
</button>
|
|
<dialog
|
|
ref={dialog}
|
|
className="activity-dialog"
|
|
aria-labelledby="activity-dialog-title"
|
|
onCancel={() => setOpen(false)}
|
|
onClose={() => setOpen(false)}
|
|
>
|
|
<div className="activity-dialog-content">
|
|
<header>
|
|
<div>
|
|
<span className="request-overview-label">Request progress</span>
|
|
<h2 id="activity-dialog-title">{status}</h2>
|
|
</div>
|
|
<button type="button" onClick={() => setOpen(false)}>
|
|
Close
|
|
</button>
|
|
</header>
|
|
<div className="activity-current" role="status" aria-live="polite" aria-atomic="true">
|
|
<p className="activity-current-step">
|
|
{working && <span className="activity-spinner" aria-hidden="true" />}
|
|
{currentStep}
|
|
</p>
|
|
{progress}
|
|
<p className="activity-next-step">{nextStep}</p>
|
|
</div>
|
|
{!working && (
|
|
<footer>
|
|
<button
|
|
type="button"
|
|
onClick={() => {
|
|
setOpen(false);
|
|
onDismiss();
|
|
}}
|
|
>
|
|
{operation.summary?.action ?? "Dismiss activity"}
|
|
</button>
|
|
</footer>
|
|
)}
|
|
</div>
|
|
</dialog>
|
|
</div>
|
|
);
|
|
}
|