81 lines
4.6 KiB
TypeScript
81 lines
4.6 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 = { 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(() => { 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 = working ? 'Working on it' : choosing ? 'Choose a download' : operation.status === 'complete' ? 'Done' : 'Needs your attention'
|
|
const currentStep = 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 = 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() }}>Dismiss activity</button></footer>}
|
|
</div>
|
|
</dialog>
|
|
</div>
|
|
)
|
|
}
|