Files
AeroFetch/src/renderer/src/components/downloadBar/PlaylistPanel.tsx
T
debont80 4c2b54a599 feat(playlist): exact-format-per-item selection
Each video row in the playlist panel gets an exact-format button that
lazily probes just that entry's URL (reusing the existing single-video
probe IPC, no new main-process code) and shows a native Select of its
real formats. The choice flows through addMany -> formatId per queue
item. Probing is on-demand per row, never all up front, since a
playlist can hold thousands of entries and per-entry probes are
rate-limited.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-02 16:55:58 -04:00

172 lines
6.3 KiB
TypeScript

import { Button, Checkbox, Caption1, Spinner, mergeClasses } from '@fluentui/react-components'
import {
AppsListRegular,
VideoClipRegular,
MusicNote2Regular,
OptionsRegular
} from '@fluentui/react-icons'
import { BEST_FORMAT_ID, type FormatOption, type PlaylistInfo } from '@shared/ipc'
import { type MediaKind } from '../../store/downloads'
import { type ChosenFormat } from '../../store/downloadTypes'
import { Hint } from '../Hint'
import { Select } from '../Select'
import { IconButton } from '../ui/IconButton'
import { useTextStyles } from '../ui/text'
import { META_SEP } from '../ui/tokens'
import { useDownloadBarStyles } from './styles'
interface PlaylistPanelProps {
playlist: PlaylistInfo
selected: Set<number>
allSelected: boolean
onToggleAll: () => void
onSetAllKinds: (k: MediaKind) => void
onToggleEntry: (index: number, on: boolean) => void
effKind: (index: number) => MediaKind
onToggleItemKind: (index: number) => void
// per-item exact-format picker (video rows only)
formatExpanded: Set<number>
itemFormatOptions: Record<number, FormatOption[]>
itemFormat: (index: number) => ChosenFormat | undefined
formatProbing: Set<number>
formatProbeError: Record<number, string>
onToggleFormat: (index: number) => void
onSetItemFormat: (index: number, formatId: string) => void
}
export function PlaylistPanel({
playlist,
selected,
allSelected,
onToggleAll,
onSetAllKinds,
onToggleEntry,
effKind,
onToggleItemKind,
formatExpanded,
itemFormatOptions,
itemFormat,
formatProbing,
formatProbeError,
onToggleFormat,
onSetItemFormat
}: PlaylistPanelProps): React.JSX.Element {
const styles = useDownloadBarStyles()
const text = useTextStyles()
return (
<div className={styles.plPanel}>
<div className={styles.plHeader}>
<AppsListRegular />
<Caption1 className={mergeClasses(styles.plHeaderGrow, text.title)}>
{playlist.title}
{playlist.uploader ? `${META_SEP}${playlist.uploader}` : ''}
</Caption1>
<Caption1 className={text.muted}>
{selected.size} of {playlist.count} selected
</Caption1>
<Button size="small" appearance="subtle" onClick={onToggleAll}>
{allSelected ? 'Select none' : 'Select all'}
</Button>
<Button size="small" appearance="subtle" onClick={() => onSetAllKinds('video')}>
All video
</Button>
<Button size="small" appearance="subtle" onClick={() => onSetAllKinds('audio')}>
All audio
</Button>
</div>
<div className={styles.plList}>
{playlist.entries.map((e) => {
const isVideo = effKind(e.index) === 'video'
const chosen = itemFormat(e.index)
const expanded = formatExpanded.has(e.index)
const probing = formatProbing.has(e.index)
const error = formatProbeError[e.index]
const options = itemFormatOptions[e.index]
return (
<div key={e.index} className={styles.plItemGroup}>
<div className={styles.plItemRow}>
<Checkbox
className={styles.plItem}
checked={selected.has(e.index)}
onChange={(_, d) => onToggleEntry(e.index, !!d.checked)}
label={
<span className={styles.plItemLabel}>
<span className={text.truncate}>
{e.index}. {e.title}
</span>
{(e.durationLabel || e.uploader || chosen) && (
<Caption1 className={text.muted}>
{[e.durationLabel, e.uploader, chosen?.label]
.filter(Boolean)
.join(META_SEP)}
</Caption1>
)}
</span>
}
/>
{isVideo && (
<Hint
label={expanded ? 'Hide format' : 'Choose exact format'}
placement="top"
align="end"
>
<IconButton
size="sm"
style={{ flexShrink: 0 }}
className={chosen ? styles.plFormatBtnActive : undefined}
aria-pressed={expanded}
icon={<OptionsRegular />}
onClick={() => onToggleFormat(e.index)}
aria-label={`Choose format for ${e.title}`}
/>
</Hint>
)}
<Hint
label={
effKind(e.index) === 'audio'
? 'Audio -- click for video'
: 'Video -- click for audio'
}
placement="top"
align="end"
>
<IconButton
size="sm"
style={{ flexShrink: 0 }}
icon={
effKind(e.index) === 'audio' ? <MusicNote2Regular /> : <VideoClipRegular />
}
onClick={() => onToggleItemKind(e.index)}
aria-label={`Download type for ${e.title}: ${effKind(e.index)}`}
/>
</Hint>
</div>
{isVideo && expanded && (
<div className={styles.plFormatPanel}>
{probing ? (
<Caption1 className={styles.plFormatStatus}>
<Spinner size="tiny" /> Fetching formats
</Caption1>
) : error ? (
<Caption1 className={styles.plFormatError}>{error}</Caption1>
) : options && options.length > 0 ? (
<Select
className={styles.plFormatSelect}
aria-label={`Format for ${e.title}`}
value={chosen?.id ?? BEST_FORMAT_ID}
options={options.map((o) => ({ value: o.id, label: o.label }))}
onChange={(v) => onSetItemFormat(e.index, v)}
/>
) : (
<Caption1 className={text.muted}>No selectable formats.</Caption1>
)}
</div>
)}
</div>
)
})}
</div>
</div>
)
}