// frontend\src\components\ui\FinishedDownloadsDesktopCard.tsx 'use client' import { memo, useCallback, type ReactNode } from 'react' import Card from './Card' import Checkbox from './Checkbox' import CountryFlag, { resolveFinishedCountry } from './CountryFlag' import FinishedVideoPreview from './FinishedVideoPreview' import ModelGenderIcon from './ModelGenderIcon' import RecordJobActions from './RecordJobActions' import TagOverflowRow from './TagOverflowRow' import { parseFinishedTags, resolveFinishedModelFlags } from './finishedModelFlags' import { isHotName, stripHotPrefix } from './hotName' import { type FinishedSelectionStore, useSelectionChecked, } from './finishedSelectionStore' import type { FinishedPostworkSummary, GalleryModelFlags, InlinePlayState, RecordJob, } from '../../types' import type { FinishedDownloadsTeaserState } from './teaserPlayback' import { shouldAnimateTeaser, shouldEnableTeaserAudio, } from './teaserPlayback' import { parseJobMeta } from './previewSprite' import PreviewRatingOverlay from './PreviewRatingOverlay' import PreviewMetaOverlay from './PreviewMetaOverlay' export type FinishedDownloadsDesktopCardProps = { job: RecordJob selectionStore: FinishedSelectionStore isDeleting: boolean isKeeping: boolean isRemoving: boolean postworkBadge?: FinishedPostworkSummary blurPreviews?: boolean durationSeconds?: number teaserState: FinishedDownloadsTeaserState inlinePlay: InlinePlayState assetNonce?: number forcePreviewMuted?: boolean keyFor: (j: RecordJob) => string baseName: (p: string) => string modelNameFromOutput: (output?: string) => string runtimeOf: (job: RecordJob) => string sizeBytesOf: (job: RecordJob) => number | null formatBytes: (bytes?: number | null) => string lower: (s: string) => string jobForDetails: (job: RecordJob) => RecordJob handleDuration: (job: RecordJob, seconds: number) => void handleResolution: (job: RecordJob, w: number, h: number) => void resolutionLabelOf: (job: RecordJob) => string onToggleSelected: (job: RecordJob) => void onHoverPreviewKeyChange?: (key: string | null) => void onOpenPlayer: (job: RecordJob) => void openPlayer: (job: RecordJob) => void startInline: (key: string) => void registerTeaserHost: (key: string) => (el: HTMLDivElement | null) => void modelsByKey: Record activeTagSet: Set onToggleTagFilter: (tag: string) => void onToggleHot?: (job: RecordJob) => void | Promise onToggleFavorite?: (job: RecordJob) => void | Promise onToggleLike?: (job: RecordJob) => void | Promise onToggleWatch?: (job: RecordJob) => void | Promise onSplit?: (job: RecordJob) => void | Promise onAddToDownloads?: (job: RecordJob) => void | Promise deleteVideo: (job: RecordJob) => Promise keepVideo: (job: RecordJob) => Promise renderPostworkBadge?: (badge?: FinishedPostworkSummary) => ReactNode renderIntegrityBadge?: (job: RecordJob) => ReactNode renderRatingOverlay?: (job: RecordJob) => ReactNode } function samePostworkSummaryLite( a?: FinishedPostworkSummary, b?: FinishedPostworkSummary ) { if (a === b) return true if (!a || !b) return !a && !b return ( a.file === b.file && a.state === b.state && a.label === b.label && a.ts === b.ts ) } function isPreviewActiveForKey( state: FinishedDownloadsTeaserState, key: string ) { return state.activeKey === key || state.hoverKey === key } function inlineStateForKey( inlinePlay: InlinePlayState, key: string ) { const active = inlinePlay?.key === key return { active, nonce: active ? inlinePlay?.nonce ?? 0 : 0, } } export function SelectionCheckboxOverlay({ selectionStore, rowKey, label, onToggle, }: { selectionStore: FinishedSelectionStore rowKey: string label: string onToggle: () => void }) { const checked = useSelectionChecked(selectionStore, rowKey) return (
e.stopPropagation()} onPointerDown={(e) => e.stopPropagation()} >
) } const FinishedDownloadsDesktopCard = memo( function FinishedDownloadsDesktopCard({ job: j, selectionStore, isDeleting, isKeeping, isRemoving, postworkBadge, blurPreviews, durationSeconds, teaserState, inlinePlay, assetNonce, forcePreviewMuted, keyFor, baseName, modelNameFromOutput, runtimeOf, sizeBytesOf, formatBytes, lower, jobForDetails, handleDuration, handleResolution, resolutionLabelOf, onToggleSelected, onHoverPreviewKeyChange, onOpenPlayer, openPlayer, startInline, registerTeaserHost, modelsByKey, activeTagSet, onToggleTagFilter, onToggleHot, onToggleFavorite, onToggleLike, onToggleWatch, onSplit, onAddToDownloads, deleteVideo, keepVideo, renderPostworkBadge, renderIntegrityBadge, renderRatingOverlay, }: FinishedDownloadsDesktopCardProps) { const k = keyFor(j) const busy = isDeleting || isKeeping || isRemoving const inlineActive = inlinePlay?.key === k const inlineNonce = inlineActive ? inlinePlay?.nonce ?? 0 : 0 const allowSound = shouldEnableTeaserAudio({ state: teaserState, itemKey: k, inlineActive, disabled: false, }) const handleSelectOrOpen = useCallback(() => { if (selectionStore.hasAnySelection()) { onToggleSelected(j) return true } return false }, [selectionStore, onToggleSelected, j]) const previewMuted = forcePreviewMuted ? true : !allowSound const model = modelNameFromOutput(j.output) const fileRaw = baseName(j.output || '') const isHot = isHotName(fileRaw) const { flags } = resolveFinishedModelFlags( j, modelsByKey, modelNameFromOutput, lower ) const tags = parseFinishedTags(flags?.tags) const modelCountry = resolveFinishedCountry(flags, j) const dur = runtimeOf(j) const size = formatBytes(sizeBytesOf(j)) const meta = parseJobMeta((j as any)?.meta) const resObj = (() => { const w = meta?.media?.video?.width ?? meta?.file?.video?.width const h = meta?.media?.video?.height ?? meta?.file?.video?.height if ( typeof w === 'number' && Number.isFinite(w) && w > 0 && typeof h === 'number' && Number.isFinite(h) && h > 0 ) { return { w, h } } return null })() const resLabelRaw = resolutionLabelOf(j) const resLabel = resLabelRaw && resLabelRaw !== '—' ? resLabelRaw : '' const inlineDomId = `inline-prev-${encodeURIComponent(k)}` return (
{ e.preventDefault() e.stopPropagation() if (handleSelectOrOpen()) return openPlayer(j) }} onMouseEnter={() => onHoverPreviewKeyChange?.(k)} onMouseLeave={() => onHoverPreviewKeyChange?.(null)} onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault() if (handleSelectOrOpen()) return onOpenPlayer(j) } }} > onToggleSelected(j)} />
{ e.preventDefault() e.stopPropagation() if (handleSelectOrOpen()) return startInline(k) }} >
{ e.preventDefault() e.stopPropagation() if (handleSelectOrOpen()) return openPlayer(j) }} >
{model}
{(renderIntegrityBadge || (renderPostworkBadge && postworkBadge)) ? (
{renderIntegrityBadge ? renderIntegrityBadge(j) : null} {renderPostworkBadge && postworkBadge ? renderPostworkBadge(postworkBadge) : null}
) : null}
{isHot ? ( HOT ) : null} {stripHotPrefix(fileRaw) || '—'}
e.stopPropagation()} onMouseDown={(e) => e.stopPropagation()} >
e.stopPropagation()} onMouseDown={(e) => e.stopPropagation()} >
) }, (prev, next) => { const prevKey = prev.keyFor(prev.job) const nextKey = next.keyFor(next.job) const prevPreviewActive = isPreviewActiveForKey(prev.teaserState, prevKey) const nextPreviewActive = isPreviewActiveForKey(next.teaserState, nextKey) const prevInline = inlineStateForKey(prev.inlinePlay, prevKey) const nextInline = inlineStateForKey(next.inlinePlay, nextKey) return ( prev.job === next.job && prev.isDeleting === next.isDeleting && prev.isKeeping === next.isKeeping && prev.isRemoving === next.isRemoving && samePostworkSummaryLite(prev.postworkBadge, next.postworkBadge) && prev.durationSeconds === next.durationSeconds && prev.assetNonce === next.assetNonce && prev.forcePreviewMuted === next.forcePreviewMuted && prev.blurPreviews === next.blurPreviews && prev.teaserState.mode === next.teaserState.mode && prev.teaserState.audioEnabled === next.teaserState.audioEnabled && prevPreviewActive === nextPreviewActive && prevInline.active === nextInline.active && prevInline.nonce === nextInline.nonce && prev.modelsByKey === next.modelsByKey && prev.activeTagSet === next.activeTagSet && prev.renderPostworkBadge === next.renderPostworkBadge ) } ) export default FinishedDownloadsDesktopCard