import { formatBytes, formatDateTime, formatDuration } from '../../shared/lib/format'; import { chevronRightIcon, editIcon, refreshIcon } from '../../shared/ui/icons'; import { useId, useState } from 'react'; import { needsFormatAttention } from './files'; import { AudioPreview } from './AudioPreview'; import { batchSummary } from './batchSummary'; import { BatchRelease, type ReleaseGroup } from './BatchRelease'; import { UploadStatusBadge, useReasonText, type UploadFile } from './common'; import { useTranslation } from 'react-i18next'; import { Button, cx, Icon } from '../../shared/ui/ui'; import s from '../../pages/page.module.css'; import u from '../../pages/uploads/uploads.module.css'; export function BatchCard({ files, selectable, selected, onToggle, onEdit, onEditBatch, onRetry, }: { files: UploadFile[]; selectable: boolean; selected: Set; onToggle: (ids: string[], on: boolean) => void; onEdit: (f: UploadFile) => void; onEditBatch: (batchId: string) => void; onRetry: (id: string) => void; }) { const { t, i18n } = useTranslation('admin'); const [collapsed, setCollapsed] = useState(true); const [releaseGroups, setReleaseGroups] = useState([]); const contentId = useId(); const first = files[0]!; const ids = files.map((f) => f.id); const all = ids.every((id) => selected.has(id)); const size = files.reduce((n, f) => n + f.size_bytes, 0); const summary = batchSummary(files); const metadata = first.metadata; const albumArtist = metadata?.albumartist || metadata?.artist || ''; const singleAlbum = releaseGroups.length === 1 ? releaseGroups[0] : releaseGroups.length === 0 && metadata?.album && files.every((file) => file.metadata?.album.trim().toLowerCase() === metadata.album.trim().toLowerCase() && (file.metadata.albumartist || file.metadata.artist || '').trim().toLowerCase() === albumArtist.trim().toLowerCase()) ? { artist: albumArtist, album: metadata.album } : undefined; const albumTitle = singleAlbum?.album && singleAlbum.album.trim().toLowerCase() !== 'unknown album' ? [singleAlbum.artist.trim().toLowerCase() === 'unknown artist' ? '' : singleAlbum.artist, singleAlbum.album].filter(Boolean).join(' — ') : undefined; return (
{selectable && ( onToggle(ids, e.target.checked)} /> )} {albumTitle ?
{albumTitle} {first.author.username}
: {first.author.username}} {formatDateTime(first.created_at, i18n.language)} {t('uploads.batch_size', { count: files.length, size: formatBytes(size, i18n.language) })} {summary.formats.map(([format, count]) => `${count} × ${format || t('uploads.format_unknown')}`).join(' · ')} {summary.quality && {summary.quality}} {t(first.batch_has_cover ? 'uploads.batch_cover_present' : 'uploads.batch_cover_absent')} {selectable && } {selectable && ( )} {first.batch_comment && «{first.batch_comment}»}
); } function QueueRow({ file: f, active, selectable, checked, onCheck, onEdit, onRetry, }: { file: UploadFile; active: boolean; selectable: boolean; checked: boolean; onCheck: (on: boolean) => void; onEdit: () => void; onRetry: () => void; }) { const { t, i18n } = useTranslation('admin'); const reason = useReasonText()(f.status_reason); const m = f.metadata; const quality = [ f.format?.toUpperCase(), f.bitrate ? `${Math.round(f.bitrate / 1000)} kbps` : null, f.sample_rate ? `${(f.sample_rate / 1000).toFixed(1)} kHz` : null, f.bit_depth ? `${f.bit_depth} bit` : null, f.duration_ms ? formatDuration(f.duration_ms) : null, formatBytes(f.size_bytes, i18n.language), ] .filter(Boolean) .join(' · '); const editable = f.status === 'pending_review' || f.status === 'import_failed'; return (
{selectable ? ( onCheck(e.target.checked)} /> ) : ( )}
{m ? `${m.artist} — ${m.title}` : f.original_name} {m && ( {[m.album, m.year, m.disc && m.disc > 1 ? t('uploads.disc', { n: m.disc }) : null, m.track ? `#${m.track}` : null, m.genre] .filter(Boolean) .join(' · ')} {f.metadata_edited && ` · ${t('uploads.edited')}`} )} {quality} {f.client_rel_path ?? f.original_name}
{needsFormatAttention(f.format, f.original_name) && ( {t('uploads.flag_attention')} )} {f.possible_duplicate && {t('uploads.flag_possible')}} {f.quality_upgrade && {t('uploads.flag_quality')}} {f.matched_album_id ? ( {t('uploads.flag_existing_album')} ) : ( m && {t('uploads.flag_new_album')} )}
{reason && (!editable || f.target_rel_path) && {reason}} {f.duplicate_level && f.status === 'duplicate' && ( {t(`uploads.dup_level_${f.duplicate_level}`)} )}
{f.stream_url && ( )} {editable && ( )} {f.status === 'import_failed' && ( )}
{(editable || f.target_rel_path) && (
{t(f.status === 'imported' ? 'uploads.destination_imported' : 'uploads.destination')} {f.target_source_name ?? t(f.target_source_id ? 'uploads.target_selected' : 'uploads.target_not_selected')} {f.target_rel_path ? ( {f.target_rel_path} ) : ( <> {t('uploads.target_path_missing')} {reason && {reason}} )}
)}
); }