248 lines
7.5 KiB
TypeScript
248 lines
7.5 KiB
TypeScript
// RunningDownloads.tsx
|
||
'use client'
|
||
|
||
import { useMemo } from 'react'
|
||
import Table, { type Column } from './Table'
|
||
import Card from './Card'
|
||
import Button from './Button'
|
||
import ModelPreview from './ModelPreview'
|
||
import WaitingModelsTable, { type WaitingModelRow } from './WaitingModelsTable'
|
||
import type { RecordJob } from '../../types'
|
||
|
||
type PendingWatchedRoom = WaitingModelRow & {
|
||
currentShow: string // public / private / hidden / away / unknown
|
||
}
|
||
|
||
type Props = {
|
||
jobs: RecordJob[]
|
||
pending?: PendingWatchedRoom[]
|
||
onOpenPlayer: (job: RecordJob) => void
|
||
onStopJob: (id: string) => void
|
||
blurPreviews?: boolean
|
||
}
|
||
|
||
const baseName = (p: string) =>
|
||
(p || '').replaceAll('\\', '/').trim().split('/').pop() || ''
|
||
|
||
const modelNameFromOutput = (output?: string) => {
|
||
const file = baseName(output || '')
|
||
if (!file) return '—'
|
||
const stem = file.replace(/\.[^.]+$/, '')
|
||
|
||
// <model>_MM_DD_YYYY__HH-MM-SS
|
||
const m = stem.match(/^(.*?)_\d{1,2}_\d{1,2}_\d{4}__\d{1,2}-\d{2}-\d{2}$/)
|
||
if (m?.[1]) return m[1]
|
||
|
||
const i = stem.lastIndexOf('_')
|
||
return i > 0 ? stem.slice(0, i) : stem
|
||
}
|
||
|
||
const formatDuration = (ms: number): string => {
|
||
if (!Number.isFinite(ms) || ms <= 0) return '—'
|
||
const total = Math.floor(ms / 1000)
|
||
const h = Math.floor(total / 3600)
|
||
const m = Math.floor((total % 3600) / 60)
|
||
const s = total % 60
|
||
if (h > 0) return `${h}h ${m}m`
|
||
if (m > 0) return `${m}m ${s}s`
|
||
return `${s}s`
|
||
}
|
||
|
||
const runtimeOf = (j: RecordJob) => {
|
||
const start = Date.parse(String(j.startedAt || ''))
|
||
if (!Number.isFinite(start)) return '—'
|
||
const end = j.endedAt ? Date.parse(String(j.endedAt)) : Date.now()
|
||
if (!Number.isFinite(end)) return '—'
|
||
return formatDuration(end - start)
|
||
}
|
||
|
||
export default function RunningDownloads({ jobs, pending = [], onOpenPlayer, onStopJob, blurPreviews }: Props) {
|
||
const columns = useMemo<Column<RecordJob>[]>(() => {
|
||
return [
|
||
{
|
||
key: 'preview',
|
||
header: 'Vorschau',
|
||
cell: (j) => <ModelPreview jobId={j.id} blur={blurPreviews} />,
|
||
},
|
||
{
|
||
key: 'model',
|
||
header: 'Modelname',
|
||
cell: (j) => {
|
||
const name = modelNameFromOutput(j.output)
|
||
return (
|
||
<span className="truncate" title={name}>
|
||
{name}
|
||
</span>
|
||
)
|
||
},
|
||
},
|
||
{
|
||
key: 'sourceUrl',
|
||
header: 'Source',
|
||
cell: (j) => (
|
||
<a
|
||
href={j.sourceUrl}
|
||
target="_blank"
|
||
rel="noreferrer"
|
||
className="text-indigo-600 dark:text-indigo-400 hover:underline"
|
||
onClick={(e) => e.stopPropagation()}
|
||
>
|
||
{j.sourceUrl}
|
||
</a>
|
||
),
|
||
},
|
||
{
|
||
key: 'output',
|
||
header: 'Datei',
|
||
cell: (j) => baseName(j.output || ''),
|
||
},
|
||
{ key: 'status', header: 'Status' },
|
||
{
|
||
key: 'runtime',
|
||
header: 'Dauer',
|
||
cell: (j) => runtimeOf(j),
|
||
},
|
||
{
|
||
key: 'actions',
|
||
header: 'Aktion',
|
||
srOnlyHeader: true,
|
||
align: 'right',
|
||
cell: (j) => (
|
||
<Button
|
||
size="md"
|
||
variant="primary"
|
||
onClick={(e) => {
|
||
e.stopPropagation()
|
||
onStopJob(j.id)
|
||
}}
|
||
>
|
||
Stop
|
||
</Button>
|
||
),
|
||
},
|
||
]
|
||
}, [onStopJob])
|
||
|
||
if (jobs.length === 0 && pending.length === 0) {
|
||
return (
|
||
<Card grayBody>
|
||
<div className="text-sm text-gray-600 dark:text-gray-300">Keine laufenden Downloads.</div>
|
||
</Card>
|
||
)
|
||
}
|
||
|
||
return (
|
||
<>
|
||
{pending.length > 0 && (
|
||
<Card
|
||
grayBody
|
||
header={
|
||
<div className="flex items-center justify-between gap-3">
|
||
<div className="text-sm font-medium text-gray-900 dark:text-white">
|
||
Online (wartend – Download startet nur bei <span className="font-semibold">public</span>)
|
||
</div>
|
||
<div className="text-xs text-gray-600 dark:text-gray-300">{pending.length}</div>
|
||
</div>
|
||
}
|
||
>
|
||
<WaitingModelsTable models={pending} />
|
||
</Card>
|
||
)}
|
||
|
||
{jobs.length === 0 && pending.length > 0 && (
|
||
<Card grayBody>
|
||
<div className="text-sm text-gray-600 dark:text-gray-300">
|
||
Kein aktiver Download – wartet auf public.
|
||
</div>
|
||
</Card>
|
||
)}
|
||
|
||
{jobs.length > 0 && (
|
||
<>
|
||
{/* ✅ Mobile: Cards */}
|
||
<div className="sm:hidden space-y-3">
|
||
{jobs.map((j) => {
|
||
const model = modelNameFromOutput(j.output)
|
||
const file = baseName(j.output || '')
|
||
const dur = runtimeOf(j)
|
||
|
||
return (
|
||
<div
|
||
key={j.id}
|
||
role="button"
|
||
tabIndex={0}
|
||
className="cursor-pointer"
|
||
onClick={() => onOpenPlayer(j)}
|
||
onKeyDown={(e) => {
|
||
if (e.key === 'Enter' || e.key === ' ') onOpenPlayer(j)
|
||
}}
|
||
>
|
||
<Card
|
||
header={
|
||
<div className="flex items-start justify-between gap-3">
|
||
<div className="min-w-0">
|
||
<div className="truncate text-sm font-medium text-gray-900 dark:text-white">{model}</div>
|
||
<div className="truncate text-xs text-gray-600 dark:text-gray-300">{file || '—'}</div>
|
||
</div>
|
||
|
||
<Button
|
||
size="sm"
|
||
variant="primary"
|
||
onClick={(e) => {
|
||
e.stopPropagation()
|
||
onStopJob(j.id)
|
||
}}
|
||
>
|
||
Stop
|
||
</Button>
|
||
</div>
|
||
}
|
||
>
|
||
<div className="flex gap-3">
|
||
<div className="shrink-0" onClick={(e) => e.stopPropagation()}>
|
||
<ModelPreview jobId={j.id} blur={blurPreviews} />
|
||
</div>
|
||
|
||
<div className="min-w-0 flex-1">
|
||
<div className="text-xs text-gray-600 dark:text-gray-300">
|
||
Status: <span className="font-medium">{j.status}</span>
|
||
<span className="mx-2 opacity-60">•</span>
|
||
Dauer: <span className="font-medium">{dur}</span>
|
||
</div>
|
||
|
||
{j.sourceUrl ? (
|
||
<a
|
||
href={j.sourceUrl}
|
||
target="_blank"
|
||
rel="noreferrer"
|
||
className="mt-1 block truncate text-xs text-indigo-600 dark:text-indigo-400 hover:underline"
|
||
onClick={(e) => e.stopPropagation()}
|
||
>
|
||
{j.sourceUrl}
|
||
</a>
|
||
) : null}
|
||
</div>
|
||
</div>
|
||
</Card>
|
||
</div>
|
||
)
|
||
})}
|
||
</div>
|
||
|
||
{/* ✅ Desktop/Tablet: Tabelle */}
|
||
<div className="hidden sm:block">
|
||
<Table
|
||
rows={jobs}
|
||
columns={columns}
|
||
getRowKey={(r) => r.id}
|
||
striped
|
||
fullWidth
|
||
onRowClick={onOpenPlayer}
|
||
/>
|
||
</div>
|
||
</>
|
||
)}
|
||
</>
|
||
)
|
||
}
|