import { formatBytes, formatDateTime, formatDuration } from '../../shared/lib/format'; import { editIcon, refreshIcon } from '../../shared/ui/icons'; import { needsFormatAttention } from './files'; 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 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); return (
{selectable && ( onToggle(ids, e.target.checked)} /> )} {first.author.username} {formatDateTime(first.created_at, i18n.language)} {t('uploads.batch_size', { count: files.length, size: formatBytes(size, i18n.language) })} {first.batch_comment && «{first.batch_comment}»} {selectable && ( )}
{files.map((f) => ( onToggle([f.id], on)} onEdit={() => onEdit(f)} onRetry={() => onRetry(f.id)} /> ))}
); } function QueueRow({ file: f, selectable, checked, onCheck, onEdit, onRetry, }: { file: UploadFile; 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')} )}
{f.target_rel_path && {f.target_rel_path}} {reason && {reason}} {f.duplicate_level && f.status === 'duplicate' && ( {t(`uploads.dup_level_${f.duplicate_level}`)} )}
{f.stream_url && (
); }