nsfwapp/frontend/src/components/ui/DownloadsCardRow.tsx
2026-03-31 19:24:19 +02:00

850 lines
26 KiB
TypeScript

// frontend\src\components\ui\DownloadsCardRow.tsx
'use client'
import { useEffect, useRef, useState, type ReactNode } from 'react'
import Button from './Button'
import ModelPreview from './ModelPreview'
import ProgressBar from './ProgressBar'
import RecordJobActions from './RecordJobActions'
import type { RecordJob } from '../../types'
import LazyMountWhenVisible from './LazyMountWhenVisible'
export type PendingWatchedRoom = WaitingModelRow & {
currentShow: string
}
type WaitingModelRow = {
id: string
modelKey: string
url: string
imageUrl?: string
currentShow?: string
}
export type DownloadRow =
| { kind: 'job'; job: RecordJob }
| { kind: 'pending'; pending: PendingWatchedRoom }
type ModelFlags = {
favorite?: boolean
liked?: boolean | null
watching?: boolean
roomStatus?: string
}
type Props = {
r: DownloadRow
nowMs: number
blurPreviews?: boolean
modelsByKey: Record<string, ModelFlags>
roomStatusByModelKey: Record<string, string>
stopRequestedIds: Record<string, true>
growingByJobId: Record<string, boolean>
postworkInfoOf: (job: RecordJob) => { pos?: number; total?: number } | undefined
markStopRequested: (ids: string | string[]) => void
onOpenPlayer: (job: RecordJob) => void
onStopJob: (id: string) => void | Promise<void>
onRemoveQueuedPostworkJob?: (id: string) => void | Promise<void>
onToggleFavorite?: (job: RecordJob) => void | Promise<void>
onToggleLike?: (job: RecordJob) => void | Promise<void>
onToggleWatch?: (job: RecordJob) => void | Promise<void>
}
const pendingModelName = (p: PendingWatchedRoom) => {
const anyP = p as any
return (
anyP.modelName ??
anyP.model ??
anyP.modelKey ??
anyP.username ??
anyP.name ??
'—'
)
}
const pendingUrl = (p: PendingWatchedRoom) => {
const anyP = p as any
return anyP.sourceUrl ?? anyP.url ?? anyP.roomUrl ?? ''
}
const pendingImageUrl = (p: PendingWatchedRoom) => {
const anyP = p as any
return String(anyP.imageUrl ?? anyP.image_url ?? '').trim()
}
const normalizeRoomStatus = (v: unknown): string => {
const s = String(v ?? '').trim().toLowerCase()
switch (s) {
case 'public':
return 'Public'
case 'private':
return 'Private'
case 'hidden':
return 'Hidden'
case 'away':
return 'Away'
case 'offline':
return 'Offline'
default:
return 'Unknown'
}
}
const baseName = (p: string) =>
(p || '').replaceAll('\\', '/').trim().split('/').pop() || ''
const modelNameFromOutput = (output?: string) => {
const file = baseName(output || '')
if (!file) return '—'
const stem = file.replace(/\.[^.]+$/, '')
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 modelKeyFromJob = (job: RecordJob): string => {
const j = job as any
const rawUrl = String(j.sourceUrl ?? '').trim()
if (rawUrl) {
const m = rawUrl.match(/chaturbate\.com\/([^/?#]+)/i)
if (m?.[1]) {
try {
return decodeURIComponent(m[1]).trim().toLowerCase()
} catch {
return String(m[1]).trim().toLowerCase()
}
}
}
return String(modelNameFromOutput(j.output || '')).trim().toLowerCase()
}
const roomStatusOfJob = (
job: RecordJob,
roomStatusByModelKey?: Record<string, string>,
modelsByKey?: Record<string, { roomStatus?: string }>
): string => {
const j = job as any
const rawFromJob =
j.currentShow ??
j.roomStatus ??
j.modelStatus ??
j.show ??
j.statusText ??
j.model?.currentShow ??
j.model?.roomStatus ??
j.model?.status ??
j.room?.currentShow ??
j.room?.status
if (rawFromJob != null && String(rawFromJob).trim() !== '') {
return normalizeRoomStatus(rawFromJob)
}
const modelKey = modelKeyFromJob(job)
if (modelKey && roomStatusByModelKey?.[modelKey]) {
return normalizeRoomStatus(roomStatusByModelKey[modelKey])
}
if (modelKey && modelsByKey?.[modelKey]?.roomStatus) {
return normalizeRoomStatus(modelsByKey[modelKey].roomStatus)
}
return 'Unknown'
}
function effectiveRoomStatusOfJob(
job: RecordJob,
roomStatusByModelKey?: Record<string, string>,
modelsByKey?: Record<string, { roomStatus?: string }>,
growingByJobId?: Record<string, boolean>
): string {
const raw = roomStatusOfJob(job, roomStatusByModelKey, modelsByKey)
if (raw !== 'Unknown') return raw
const anyJ = job as any
const phaseLower = String(anyJ?.phase ?? '').trim().toLowerCase()
const statusLower = String(job.status ?? '').trim().toLowerCase()
const isActiveRecording =
!job.endedAt &&
statusLower === 'running' &&
(phaseLower === '' || phaseLower === 'recording')
if (isActiveRecording && growingByJobId?.[job.id]) {
return 'Public'
}
return raw
}
function previewRoomStatusOfJob(
job: RecordJob,
roomStatusByModelKey?: Record<string, string>,
modelsByKey?: Record<string, { roomStatus?: string }>,
growingByJobId?: Record<string, boolean>
): string {
const postworkState = getEffectivePostworkState(job)
if (postworkState === 'running' || postworkState === 'queued') {
return 'Public'
}
return effectiveRoomStatusOfJob(job, roomStatusByModelKey, modelsByKey, growingByJobId)
}
function previewAlignEndAtOfJob(job: RecordJob): string | null {
const postworkState = getEffectivePostworkState(job)
if (postworkState === 'running' || postworkState === 'queued') {
return null
}
return job.endedAt ?? null
}
const roomStatusTone = (status: string): string => {
switch (status) {
case 'Public':
return 'bg-emerald-500/15 text-emerald-900 ring-emerald-500/30 dark:bg-emerald-400/10 dark:text-emerald-200 dark:ring-emerald-400/25'
case 'Private':
return 'bg-fuchsia-500/15 text-fuchsia-900 ring-fuchsia-500/30 dark:bg-fuchsia-400/10 dark:text-fuchsia-200 dark:ring-fuchsia-400/25'
case 'Hidden':
return 'bg-slate-500/15 text-slate-900 ring-slate-500/30 dark:bg-slate-400/10 dark:text-slate-200 dark:ring-slate-400/25'
case 'Away':
return 'bg-amber-500/15 text-amber-900 ring-amber-500/30 dark:bg-amber-400/10 dark:text-amber-200 dark:ring-amber-400/25'
case 'Offline':
return 'bg-gray-900/5 text-gray-800 ring-gray-900/10 dark:bg-white/10 dark:text-gray-200 dark:ring-white/10'
case 'Unknown':
default:
return 'bg-gray-900/5 text-gray-800 ring-gray-900/10 dark:bg-white/10 dark:text-gray-200 dark:ring-white/10'
}
}
const toMs = (v: unknown): number => {
if (typeof v === 'number' && Number.isFinite(v)) {
return v < 1_000_000_000_000 ? v * 1000 : v
}
if (typeof v === 'string') {
const ms = Date.parse(v)
return Number.isFinite(ms) ? ms : 0
}
if (v instanceof Date) return v.getTime()
return 0
}
const absUrlMaybe = (u?: string | null): string => {
const s = String(u ?? '').trim()
if (!s) return ''
if (/^https?:\/\//i.test(s)) return s
if (s.startsWith('/')) return s
return `/${s}`
}
const jobThumbsJPGCandidates = (job: RecordJob): string[] => {
const j = job as any
const direct = [
j.thumbsJPGUrl,
j.thumbsUrl,
j.previewThumbsUrl,
j.thumbnailSheetUrl,
]
const base = [
j.previewBaseUrl ? `${String(j.previewBaseUrl).replace(/\/+$/, '')}/preview.jpg` : '',
j.assetBaseUrl ? `${String(j.assetBaseUrl).replace(/\/+$/, '')}/preview.jpg` : '',
j.thumbsBaseUrl ? `${String(j.thumbsBaseUrl).replace(/\/+$/, '')}/preview.jpg` : '',
]
return [...direct, ...base]
.map((x) => absUrlMaybe(String(x ?? '')))
.filter(Boolean)
}
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, nowMs: number) => {
const anyJ = j as any
const start =
toMs(anyJ.startedAtMs) ||
toMs(j.startedAt)
if (!Number.isFinite(start) || start <= 0) return '—'
const end =
j.endedAt
? (toMs(anyJ.endedAtMs) || toMs(j.endedAt))
: nowMs
if (!Number.isFinite(end) || end <= 0) return '—'
return formatDuration(end - start)
}
const sizeBytesOf = (job: RecordJob): number | null => {
const anyJob = job as any
const v = anyJob.sizeBytes ?? anyJob.fileSizeBytes ?? anyJob.bytes ?? anyJob.size ?? null
return typeof v === 'number' && Number.isFinite(v) && v > 0 ? v : null
}
const formatBytes = (bytes: number | null): string => {
if (!bytes || !Number.isFinite(bytes) || bytes <= 0) return '—'
const units = ['B', 'KB', 'MB', 'GB', 'TB']
let v = bytes
let i = 0
while (v >= 1024 && i < units.length - 1) {
v /= 1024
i++
}
const digits = i === 0 ? 0 : v >= 10 ? 1 : 2
const s = v.toFixed(digits).replace(/\.0+$/, '')
return `${s} ${units[i]}`
}
const phaseLabel = (p?: string) => {
switch ((p ?? '').toLowerCase()) {
case 'stopping':
return 'Stop wird angefordert…'
case 'probe':
return 'Analysiere Datei (Dauer/Streams)…'
case 'remuxing':
return 'Konvertiere Container zu MP4…'
case 'moving':
return 'Verschiebe nach Done…'
case 'assets':
return 'Erstelle Vorschau/Thumbnails…'
case 'analyze':
return 'AI analysiert Segmente…'
case 'postwork':
return 'Nacharbeiten laufen…'
default:
return ''
}
}
const isTerminalStatus = (status?: unknown) => {
const s = String(status ?? '').trim().toLowerCase()
return (
s === 'stopped' ||
s === 'finished' ||
s === 'failed' ||
s === 'done' ||
s === 'completed' ||
s === 'canceled' ||
s === 'cancelled'
)
}
const isPostworkJob = (job: RecordJob): boolean => {
const anyJ = job as any
const phase = String(anyJ.phase ?? '').trim()
const pw = anyJ.postWork
const pwKey = String(anyJ.postWorkKey ?? '').trim()
if (pwKey) return true
if (pw && (pw.state === 'queued' || pw.state === 'running')) return true
if (job.endedAt && phase) return true
if (phase === 'postwork') return true
return false
}
const getEffectivePostworkState = (job: RecordJob): 'running' | 'queued' | 'none' => {
const anyJ = job as any
const phase = String(anyJ.phase ?? '').trim().toLowerCase()
const pw = anyJ.postWork
const pwState = String(pw?.state ?? '').trim().toLowerCase()
const hasPwKey = String(anyJ.postWorkKey ?? '').trim() !== ''
if (!isPostworkJob(job)) return 'none'
if (isTerminalStatus(anyJ?.status)) return 'none'
if (pwState === 'queued') return 'queued'
if (pwState === 'running') return 'running'
if (
phase === 'postwork' ||
phase === 'probe' ||
phase === 'remuxing' ||
phase === 'moving' ||
phase === 'assets'
) {
return 'running'
}
if (
typeof pw?.position === 'number' &&
Number.isFinite(pw.position) &&
pw.position > 0
) {
return 'queued'
}
if (
typeof pw?.running === 'number' &&
Number.isFinite(pw.running) &&
pw.running > 0 &&
(!Number.isFinite(pw?.position) || pw.position <= 0)
) {
return 'running'
}
if (hasPwKey) {
return 'queued'
}
if (anyJ.endedAt || job.endedAt) return 'queued'
return 'none'
}
function postWorkLabel(
job: RecordJob,
override?: { pos?: number; total?: number }
): string {
const anyJ = job as any
const pw = anyJ.postWork
const effectiveState = getEffectivePostworkState(job)
if (effectiveState === 'running') {
const running = typeof pw?.running === 'number' && pw.running > 0 ? pw.running : 1
const maxP = typeof pw?.maxParallel === 'number' ? pw.maxParallel : 0
return maxP > 0
? `Nacharbeiten laufen… (${running}/${maxP} parallel)`
: 'Nacharbeiten laufen…'
}
if (effectiveState === 'queued') {
const posServer = typeof pw?.position === 'number' ? pw.position : 0
const waitingServer = typeof pw?.waiting === 'number' ? pw.waiting : 0
const totalServer = Math.max(waitingServer, posServer)
const pos =
typeof override?.pos === 'number' && Number.isFinite(override.pos) && override.pos > 0
? override.pos
: posServer
const total =
typeof override?.total === 'number' && Number.isFinite(override.total) && override.total > 0
? override.total
: totalServer
return pos > 0 && total > 0
? `Warte auf Nacharbeiten… ${pos} / ${total}`
: 'Warte auf Nacharbeiten…'
}
return 'Warte auf Nacharbeiten…'
}
export default function DownloadsCardRow({
r,
nowMs,
blurPreviews,
modelsByKey,
roomStatusByModelKey,
stopRequestedIds,
growingByJobId,
postworkInfoOf,
markStopRequested,
onOpenPlayer,
onStopJob,
onRemoveQueuedPostworkJob,
onToggleFavorite,
onToggleLike,
onToggleWatch,
}: Props) {
if (r.kind === 'pending') {
const p = r.pending
const name = pendingModelName(p)
const url = pendingUrl(p)
const show = normalizeRoomStatus(p.currentShow)
const img = pendingImageUrl(p)
return (
<div
className="
group relative isolate overflow-hidden rounded-2xl
border border-gray-200/90 bg-white shadow-sm
ring-1 ring-black/5
transition-all hover:-translate-y-0.5 hover:shadow-md active:translate-y-0
dark:border-white/10 dark:bg-gray-950
"
>
<div className="pointer-events-none absolute inset-0 bg-gradient-to-br from-gray-50/90 via-white to-gray-50/70 dark:from-white/10 dark:via-transparent dark:to-white/5" />
<div className="pointer-events-none absolute -inset-10 opacity-0 blur-2xl transition-opacity duration-300 group-hover:opacity-100 bg-white/25 dark:bg-white/5" />
<div className="relative p-3">
<div className="flex items-start gap-3">
<div
className="
relative
shrink-0 overflow-hidden rounded-lg
w-[112px] h-[64px]
bg-gray-100 ring-1 ring-black/5
dark:bg-white/10 dark:ring-white/10
"
>
{img ? (
<img
src={img}
alt={name}
className={[
'h-full w-full object-cover',
blurPreviews ? 'blur-md' : '',
].join(' ')}
loading="lazy"
referrerPolicy="no-referrer"
onError={(e) => {
;(e.currentTarget as HTMLImageElement).style.display = 'none'
}}
/>
) : (
<div className="grid h-full w-full place-items-center text-sm text-gray-500 dark:text-gray-300">
</div>
)}
</div>
<div className="min-w-0 flex-1">
<div className="flex items-start justify-between gap-2">
<div className="min-w-0">
<div className="flex items-center gap-2 min-w-0">
<div
className="truncate text-base font-semibold text-gray-900 dark:text-white"
title={name}
>
{name}
</div>
<span
className={[
'shrink-0 inline-flex items-center rounded-full px-2 py-0.5 text-[10px] font-semibold ring-1',
roomStatusTone(show),
].join(' ')}
title={show}
>
{show}
</span>
</div>
<div className="mt-0.5 truncate text-xs text-gray-600 dark:text-gray-300">
Wartend auf Autostart
</div>
</div>
<div className="shrink-0 flex flex-col items-end gap-1">
<span className="rounded-full bg-gray-900/5 px-2 py-0.5 text-[11px] font-medium text-gray-900 dark:bg-white/10 dark:text-gray-200">
</span>
</div>
</div>
{url ? (
<a
href={url}
target="_blank"
rel="noreferrer"
className="mt-1 block truncate text-xs text-indigo-600 hover:underline dark:text-indigo-400"
onClick={(e) => e.stopPropagation()}
title={url}
>
{url}
</a>
) : null}
</div>
</div>
<div className="mt-3">
<ProgressBar
label="Warte auf Online-Status…"
indeterminate
size="sm"
className="w-full"
/>
</div>
<div className="mt-3 flex items-center justify-between gap-2 border-t border-white/30 pt-3 dark:border-white/10">
<div className="flex items-center gap-1 opacity-60">
<span className="text-xs text-gray-500 dark:text-gray-400">
Wird automatisch gestartet, sobald das Model public ist.
</span>
</div>
<div className="hidden sm:block">
<Button
size="sm"
variant="primary"
color="red"
className="shrink-0"
disabled
>
Wartend
</Button>
</div>
</div>
</div>
</div>
)
}
const j = r.job
const name = modelNameFromOutput(j.output)
const file = baseName(j.output || '')
const phase = String((j as any).phase ?? '').trim()
const phaseLower = phase.toLowerCase()
const isRecording = phaseLower === 'recording'
const isStopRequested = Boolean(stopRequestedIds[j.id])
const rawStatus = String(j.status ?? '').toLowerCase()
const postworkState = getEffectivePostworkState(j)
const isQueuedPostwork = postworkState === 'queued'
const isBusyPhase = phaseLower !== '' && phaseLower !== 'recording'
const isStopping = isBusyPhase || rawStatus !== 'running' || isStopRequested
const showRemoveQueuedButton = isQueuedPostwork
const disableStopButton = showRemoveQueuedButton ? false : isStopping
const actionsBusy = showRemoveQueuedButton ? false : isStopping
const roomStatus = effectiveRoomStatusOfJob(
j,
roomStatusByModelKey,
modelsByKey,
growingByJobId
)
let phaseText = ''
if (phaseLower === 'recording') {
phaseText = 'Recording läuft…'
} else if (isPostworkJob(j)) {
const pwState = getEffectivePostworkState(j)
if (phaseLower === 'probe' || phaseLower === 'remuxing' || phaseLower === 'moving' || phaseLower === 'assets') {
phaseText = phaseLabel(phase) || phase
} else if (pwState === 'queued') {
phaseText = postWorkLabel(j, postworkInfoOf(j))
} else if (pwState === 'running') {
phaseText = phaseLabel(phase) || postWorkLabel(j, postworkInfoOf(j))
}
} else {
phaseText = phase ? (phaseLabel(phase) || phase) : ''
}
const progressLabel = phaseText || roomStatus
const progress = Number((j as any).progress ?? 0)
const showBar =
!isRecording &&
Number.isFinite(progress) &&
progress > 0 &&
progress < 100
const showIndeterminate =
!isRecording &&
!showBar &&
Boolean(phase) &&
(!Number.isFinite(progress) || progress <= 0 || progress >= 100)
const key = name && name !== '—' ? name.toLowerCase() : ''
const flags = key ? modelsByKey[key] : undefined
const isFav = Boolean(flags?.favorite)
const isLiked = flags?.liked === true
const isWatching = Boolean(flags?.watching)
return (
<div
className="
group relative isolate overflow-hidden rounded-2xl
border border-gray-200/90 bg-white shadow-sm
ring-1 ring-black/5
transition-all hover:-translate-y-0.5 hover:shadow-md active:translate-y-0
dark:border-white/10 dark:bg-gray-950
"
onClick={() => onOpenPlayer(j)}
role="button"
tabIndex={0}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') onOpenPlayer(j)
}}
>
<div className="pointer-events-none absolute inset-0 bg-gradient-to-br from-gray-50/90 via-white to-gray-50/70 dark:from-white/10 dark:via-transparent dark:to-white/5" />
<div className="pointer-events-none absolute -inset-10 opacity-0 blur-2xl transition-opacity duration-300 group-hover:opacity-100 bg-white/25 dark:bg-white/5" />
<div className="relative p-3">
<div className="flex items-start gap-3">
<div
className="
relative
shrink-0 overflow-hidden rounded-lg
w-[112px] h-[64px]
bg-gray-100 ring-1 ring-black/5
dark:bg-white/10 dark:ring-white/10
"
>
<LazyMountWhenVisible
className="w-full h-full"
placeholder={<div className="h-full w-full bg-gray-100 dark:bg-white/10" />}
>
<ModelPreview
jobId={j.id}
blur={blurPreviews}
roomStatus={previewRoomStatusOfJob(
j,
roomStatusByModelKey,
modelsByKey,
growingByJobId
)}
alignStartAt={j.startedAt}
alignEndAt={previewAlignEndAtOfJob(j)}
alignEveryMs={10_000}
fastRetryMs={1000}
fastRetryMax={25}
fastRetryWindowMs={60_000}
thumbsCandidates={jobThumbsJPGCandidates(j)}
className="w-full h-full"
/>
</LazyMountWhenVisible>
</div>
<div className="min-w-0 flex-1">
<div className="flex items-start justify-between gap-2">
<div className="min-w-0">
<div className="flex items-center gap-2 min-w-0">
<div className="truncate text-base font-semibold text-gray-900 dark:text-white" title={name}>
{name}
</div>
<span
className={[
'shrink-0 inline-flex items-center rounded-full px-2 py-0.5 text-[10px] font-semibold ring-1',
roomStatusTone(roomStatus),
].join(' ')}
title={roomStatus}
>
{roomStatus}
</span>
</div>
<div className="mt-0.5 truncate text-xs text-gray-600 dark:text-gray-300" title={j.output}>
{file || '—'}
</div>
</div>
<div className="shrink-0 flex flex-col items-end gap-1">
<span className="rounded-full bg-gray-900/5 px-2 py-0.5 text-[11px] font-medium text-gray-900 dark:bg-white/10 dark:text-gray-200">
{runtimeOf(j, nowMs)}
</span>
<span className="rounded-full bg-gray-900/5 px-2 py-0.5 text-[11px] font-medium text-gray-900 dark:bg-white/10 dark:text-gray-200">
{formatBytes(sizeBytesOf(j))}
</span>
</div>
</div>
{j.sourceUrl ? (
<a
href={j.sourceUrl}
target="_blank"
rel="noreferrer"
className="mt-1 block truncate text-xs text-indigo-600 hover:underline dark:text-indigo-400"
onClick={(e) => e.stopPropagation()}
title={j.sourceUrl}
>
{j.sourceUrl}
</a>
) : null}
</div>
</div>
{(showBar || showIndeterminate) ? (
<div className="mt-3">
{showBar ? (
<ProgressBar
label={progressLabel}
value={Math.max(0, Math.min(100, progress))}
showPercent
size="sm"
className="w-full"
/>
) : (
<ProgressBar
label={progressLabel}
indeterminate
size="sm"
className="w-full"
/>
)}
</div>
) : null}
<div className="mt-3 flex items-center justify-between gap-2 border-t border-white/30 pt-3 dark:border-white/10">
<div
className="flex items-center gap-1"
onClick={(e) => e.stopPropagation()}
onMouseDown={(e) => e.stopPropagation()}
>
<RecordJobActions
job={j}
variant="table"
busy={actionsBusy}
isFavorite={isFav}
isLiked={isLiked}
isWatching={isWatching}
onToggleFavorite={onToggleFavorite}
onToggleLike={onToggleLike}
onToggleWatch={onToggleWatch}
order={['watch', 'favorite', 'like', 'details']}
className="flex items-center gap-1"
/>
</div>
<div className="hidden sm:block">
<Button
size="sm"
variant="primary"
color={isQueuedPostwork ? 'red' : 'indigo'}
disabled={disableStopButton}
className="shrink-0"
onClick={async (e) => {
e.stopPropagation()
if (showRemoveQueuedButton) {
await onRemoveQueuedPostworkJob?.(j.id)
return
}
if (isStopping) return
markStopRequested(j.id)
await onStopJob(j.id)
}}
>
{showRemoveQueuedButton ? 'Entfernen' : (isStopping ? 'Stoppe…' : 'Stop')}
</Button>
</div>
</div>
</div>
</div>
)
}