Files
AeroFetch/src/renderer/src/components/DownloadBar.tsx
T
debont80 36699531cf Format code with Prettier (8 modified files from H1 decomposition)
Aligns with CC2 enforcement: ESLint + Prettier + noImplicitAny now enforced.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-07-01 07:54:02 -04:00

390 lines
12 KiB
TypeScript

import {
Input,
Textarea,
Field,
Button,
Checkbox,
Spinner,
Text,
Caption1,
mergeClasses
} from '@fluentui/react-components'
import {
ArrowDownloadRegular,
ClipboardPasteRegular,
SearchRegular,
VideoClipRegular,
MusicNote2Regular,
ErrorCircleRegular,
LinkRegular,
LibraryRegular,
DismissRegular,
CutRegular,
CalendarClockRegular,
WarningRegular,
SettingsRegular,
WindowConsoleRegular
} from '@fluentui/react-icons'
import { type MediaKind } from '../store/downloads'
import { QUALITY_OPTIONS } from '../qualityOptions'
import { Select } from './Select'
import { Hint } from './Hint'
import { SegmentedControl } from './ui/SegmentedControl'
import { DownloadOptionsForm } from './DownloadOptionsForm'
import { useDownloadBarStyles } from './downloadBar/styles'
import { useDownloadBar, toLocalDatetimeValue } from './downloadBar/useDownloadBar'
import { PlaylistPanel } from './downloadBar/PlaylistPanel'
export function DownloadBar(): React.JSX.Element {
const styles = useDownloadBarStyles()
const bar = useDownloadBar()
const {
url,
kind,
quality,
showTrim,
trim,
showSchedule,
scheduleAt,
dragActive,
dup,
showAdvanced,
downloadOptions,
incognito,
commandPreview,
previewLoading,
info,
thumbFailed,
probing,
probeError,
usingFormats,
selectedFormat,
playlist,
selected,
allSelected,
suggestion,
suggestionSource,
channelHint
} = bar
return (
<div
className={mergeClasses(styles.root, dragActive && styles.rootDragging)}
onDragOver={bar.onDragOver}
onDragLeave={() => bar.setDragActive(false)}
onDrop={bar.onDrop}
>
<div className={styles.urlRow}>
<Input
className={styles.url}
input={{ id: 'aerofetch-url', 'aria-label': 'Video or playlist URL' }}
value={url}
onChange={(_, d) => bar.onUrlChange(d.value)}
onKeyDown={(e) => {
if (e.key !== 'Enter') return
// Enter is "go": start the download (or add a fetched playlist). Preview
// formats stays an explicit, optional click on the button (UX2).
if (playlist) {
if (selected.size > 0) bar.addPlaylist()
} else bar.download()
}}
placeholder="Paste a video or playlist URL, then press Enter to download"
size="large"
contentBefore={<ArrowDownloadRegular />}
/>
<Hint label="Preview available formats (optional)" placement="bottom" align="end">
<Button
size="large"
icon={probing ? <Spinner size="tiny" /> : <SearchRegular />}
onClick={bar.fetchFormats}
disabled={!url.trim() || probing}
aria-label="Preview available formats (optional)"
/>
</Hint>
<Hint label="Paste from clipboard" placement="bottom" align="end">
<Button
size="large"
icon={<ClipboardPasteRegular />}
onClick={bar.paste}
aria-label="Paste from clipboard"
/>
</Hint>
</div>
{suggestion && (
<div className={styles.suggestion}>
<LinkRegular />
<Caption1 className={styles.suggestionText}>
{suggestionSource === 'external' ? 'Link received: ' : 'Use copied link? '}
{suggestion}
</Caption1>
<Button size="small" appearance="primary" onClick={bar.acceptSuggestion}>
Use
</Button>
<Button
size="small"
appearance="subtle"
icon={<DismissRegular />}
onClick={bar.dismissSuggestion}
aria-label="Dismiss suggested link"
/>
</div>
)}
{channelHint && (
<div className={styles.channelHint}>
<LibraryRegular />
<Caption1 className={styles.channelHintText}>
This looks like a channel or playlist. Add it in the Library to download many videos at
once.
</Caption1>
<Button size="small" appearance="primary" onClick={bar.openInLibrary}>
Open in Library
</Button>
<Button
size="small"
appearance="subtle"
icon={<DismissRegular />}
onClick={bar.dismissChannelHint}
aria-label="Dismiss Library suggestion"
/>
</div>
)}
{dup && (
<div className={styles.dupRow}>
<WarningRegular />
<Caption1 className={styles.suggestionText}>Already in your queue: "{dup}".</Caption1>
<Button size="small" appearance="primary" onClick={bar.downloadAnyway}>
Download anyway
</Button>
<Button
size="small"
appearance="subtle"
icon={<DismissRegular />}
onClick={() => bar.setDup(null)}
aria-label="Dismiss duplicate warning"
/>
</div>
)}
{probing && (
<div className={styles.statusRow}>
<Spinner size="tiny" />
<Caption1>Fetching</Caption1>
</div>
)}
{probeError && (
<div className={mergeClasses(styles.statusRow, styles.errorRow)}>
<ErrorCircleRegular />
<Caption1>{probeError} -- you can still download with the presets below.</Caption1>
</div>
)}
{info && (
<div className={styles.preview}>
{info.thumbnail && thumbFailed !== info.thumbnail ? (
<img
className={styles.previewThumb}
src={info.thumbnail}
alt=""
onError={() => bar.setThumbFailed(info.thumbnail ?? null)}
/>
) : (
<div className={styles.previewThumb}>
{kind === 'audio' ? (
<MusicNote2Regular fontSize={28} />
) : (
<VideoClipRegular fontSize={28} />
)}
</div>
)}
<div className={styles.previewBody}>
<Text className={styles.previewTitle}>{info.title}</Text>
<Caption1 className={styles.previewMeta}>
{[info.channel, info.durationLabel].filter(Boolean).join(' • ')}
</Caption1>
</div>
</div>
)}
{playlist && (
<PlaylistPanel
playlist={playlist}
selected={selected}
allSelected={allSelected}
onToggleAll={bar.toggleAll}
onSetAllKinds={bar.setAllKinds}
onToggleEntry={bar.toggleEntry}
effKind={bar.effKind}
onToggleItemKind={bar.toggleItemKind}
/>
)}
{!playlist && (
<div className={styles.trimBlock}>
<div className={styles.optButtons}>
<Button
size="small"
appearance="subtle"
icon={<CutRegular />}
onClick={() => bar.setShowTrim((v) => !v)}
>
{trim.trim() ? 'Trim · on' : 'Trim'}
</Button>
<Button
size="small"
appearance="subtle"
icon={<CalendarClockRegular />}
onClick={() => bar.setShowSchedule((v) => !v)}
>
{scheduleAt ? 'Scheduled' : 'Schedule'}
</Button>
</div>
{showTrim && (
<div className={styles.trimPanel}>
<Field
label="Sections to keep"
hint="Time ranges like 1:30-2:00 -- one per line or comma-separated. Leave empty to download the whole thing."
>
<Textarea
value={trim}
onChange={(_, d) => bar.onTrimChange(d.value)}
placeholder={'0:30-1:45\n3:00-3:30'}
resize="vertical"
/>
</Field>
</div>
)}
{showSchedule && (
<div className={styles.trimPanel}>
<Field
label="Start at"
hint="Pick a date and time to start this download. Leave empty to start now. Scheduled downloads only fire while AeroFetch is running."
>
<input
type="datetime-local"
className={styles.dtInput}
value={scheduleAt}
min={toLocalDatetimeValue(new Date())}
onChange={(e) => bar.setScheduleAt(e.target.value)}
/>
</Field>
</div>
)}
</div>
)}
{!playlist && (
<div className={styles.commandBlock}>
<div className={styles.optButtons}>
<Button
size="small"
appearance="subtle"
icon={<SettingsRegular />}
onClick={() => bar.setShowAdvanced((v) => !v)}
>
{showAdvanced ? 'Advanced · on' : 'Advanced'}
</Button>
<Button
size="small"
appearance="subtle"
icon={previewLoading ? <Spinner size="tiny" /> : <WindowConsoleRegular />}
onClick={bar.toggleCommandPreview}
disabled={!url.trim() || previewLoading}
>
{commandPreview ? 'Hide command' : 'Show command'}
</Button>
</div>
{commandPreview && (
<div className={styles.commandPreviewPanel}>
{commandPreview.ok ? (
commandPreview.command
) : (
<span className={styles.commandError}>Error: {commandPreview.error}</span>
)}
</div>
)}
{showAdvanced && (
<div className={styles.optionsPanel}>
<Checkbox
checked={incognito}
onChange={(_, d) => bar.setIncognito(!!d.checked)}
label="Incognito mode (no logging, no history, no cookies)"
/>
<DownloadOptionsForm
value={downloadOptions}
onChange={bar.onDownloadOptionsChange}
kind={kind}
/>
</div>
)}
</div>
)}
<div className={styles.controls}>
<div className={styles.control}>
<Caption1>Format</Caption1>
<SegmentedControl<MediaKind>
value={kind}
options={[
{ value: 'video', label: 'Video' },
{ value: 'audio', label: 'Audio' }
]}
onChange={bar.onKindChange}
ariaLabel="Format"
/>
</div>
<div className={styles.control}>
<Caption1>{usingFormats ? 'Quality / format' : 'Quality'}</Caption1>
{usingFormats ? (
<Select
className={styles.quality}
aria-label="Quality / format"
size="large"
value={selectedFormat?.id ?? ''}
options={(info?.formats ?? []).map((f) => ({ value: f.id, label: f.label }))}
onChange={bar.onFormatIdChange}
/>
) : (
<Select
className={styles.quality}
aria-label="Quality"
size="large"
value={quality}
options={QUALITY_OPTIONS[kind].map((q) => ({ value: q, label: q }))}
onChange={bar.onQualityChange}
/>
)}
</div>
<div className={styles.spacer} />
{playlist ? (
<Button
appearance="primary"
size="large"
icon={<ArrowDownloadRegular />}
onClick={bar.addPlaylist}
disabled={selected.size === 0}
>
Add {selected.size} to queue
</Button>
) : (
<Button
appearance="primary"
size="large"
icon={scheduleAt ? <CalendarClockRegular /> : <ArrowDownloadRegular />}
onClick={bar.download}
disabled={!url.trim()}
>
{scheduleAt ? 'Schedule' : 'Download'}
</Button>
)}
</div>
</div>
)
}