Wire M5/M6/UX1: command preview, incognito mode, per-download options (Recommended SHOULD-fix-for-1.0)
M5 (Command preview): - Added 'Show command' button in DownloadBar that calls window.api.previewCommand() - Displays the exact yt-dlp command line that will run for the current form state - Helps users verify the exact behavior before download M6 (Incognito mode): - Added incognito checkbox in Advanced panel - Passes through AddOptions to enable no-logging, no-history, no-cookies mode - QueueItem already shows 'Private' badge when item.incognito is set UX1 (Per-download options): - Expanded Advanced panel now shows full DownloadOptionsForm - Allows overriding audio format, video codec, container, embed-subtitles, SponsorBlock, format-sort for any single download (not just global settings) - Options passed through AddOptions.options to startDownload - Same form component used for both Settings defaults + per-download overrides UI additions: - Advanced toggle button (Settings icon) in DownloadBar - Collapsible Advanced panel with incognito checkbox + DownloadOptionsForm - Command preview panel shows the yt-dlp argv (monospace) or error message - Both features only show for single videos (not playlists) All plumbing was already in place (previewCommand IPC, StartDownloadOptions.options, AddOptions.incognito, mockApi); only the renderer UI was missing. typecheck + 243 tests + eslint green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -25,13 +25,18 @@ import {
|
|||||||
AppsListRegular,
|
AppsListRegular,
|
||||||
CutRegular,
|
CutRegular,
|
||||||
CalendarClockRegular,
|
CalendarClockRegular,
|
||||||
WarningRegular
|
WarningRegular,
|
||||||
|
SettingsRegular,
|
||||||
|
WindowConsoleRegular
|
||||||
} from '@fluentui/react-icons'
|
} from '@fluentui/react-icons'
|
||||||
import {
|
import {
|
||||||
parseUrlShortcutContent,
|
parseUrlShortcutContent,
|
||||||
type MediaInfo,
|
type MediaInfo,
|
||||||
type FormatOption,
|
type FormatOption,
|
||||||
type PlaylistInfo
|
type PlaylistInfo,
|
||||||
|
type DownloadOptions,
|
||||||
|
DEFAULT_DOWNLOAD_OPTIONS,
|
||||||
|
type CommandPreviewResult
|
||||||
} from '@shared/ipc'
|
} from '@shared/ipc'
|
||||||
import { useDownloads, type MediaKind } from '../store/downloads'
|
import { useDownloads, type MediaKind } from '../store/downloads'
|
||||||
import { QUALITY_OPTIONS } from '../qualityOptions'
|
import { QUALITY_OPTIONS } from '../qualityOptions'
|
||||||
@@ -41,6 +46,7 @@ import { Select } from './Select'
|
|||||||
import { Hint } from './Hint'
|
import { Hint } from './Hint'
|
||||||
import { SegmentedControl } from './ui/SegmentedControl'
|
import { SegmentedControl } from './ui/SegmentedControl'
|
||||||
import { IconButton } from './ui/IconButton'
|
import { IconButton } from './ui/IconButton'
|
||||||
|
import { DownloadOptionsForm } from './DownloadOptionsForm'
|
||||||
import { useClipboardLink, looksLikeUrl, firstUrlInText } from '../useClipboardLink'
|
import { useClipboardLink, looksLikeUrl, firstUrlInText } from '../useClipboardLink'
|
||||||
import { logError } from '../reportError'
|
import { logError } from '../reportError'
|
||||||
import { THUMB_LG } from '../thumbSizes'
|
import { THUMB_LG } from '../thumbSizes'
|
||||||
@@ -246,6 +252,28 @@ const useStyles = makeStyles({
|
|||||||
backgroundColor: tokens.colorNeutralBackground1,
|
backgroundColor: tokens.colorNeutralBackground1,
|
||||||
color: tokens.colorNeutralForeground1,
|
color: tokens.colorNeutralForeground1,
|
||||||
colorScheme: 'light dark'
|
colorScheme: 'light dark'
|
||||||
|
},
|
||||||
|
// --- command preview & options panels ---
|
||||||
|
commandBlock: {
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: '8px'
|
||||||
|
},
|
||||||
|
commandPreviewPanel: {
|
||||||
|
padding: '12px',
|
||||||
|
backgroundColor: tokens.colorNeutralBackground2,
|
||||||
|
...shorthands.borderRadius(tokens.borderRadiusLarge),
|
||||||
|
border: `1px solid ${tokens.colorNeutralStroke2}`,
|
||||||
|
fontFamily: 'monospace',
|
||||||
|
fontSize: tokens.fontSizeBase200,
|
||||||
|
overflowX: 'auto',
|
||||||
|
color: tokens.colorNeutralForeground1
|
||||||
|
},
|
||||||
|
optionsPanel: {
|
||||||
|
padding: '12px',
|
||||||
|
backgroundColor: tokens.colorNeutralBackground2,
|
||||||
|
...shorthands.borderRadius(tokens.borderRadiusLarge),
|
||||||
|
border: `1px solid ${tokens.colorNeutralStroke2}`
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -285,6 +313,13 @@ export function DownloadBar(): React.JSX.Element {
|
|||||||
const [dup, setDup] = useState<string | null>(null)
|
const [dup, setDup] = useState<string | null>(null)
|
||||||
const confirmDup = useRef(false)
|
const confirmDup = useRef(false)
|
||||||
|
|
||||||
|
// Per-download options (M5/M6/UX1): advanced features.
|
||||||
|
const [showAdvanced, setShowAdvanced] = useState(false)
|
||||||
|
const [downloadOptions, setDownloadOptions] = useState<DownloadOptions>(DEFAULT_DOWNLOAD_OPTIONS)
|
||||||
|
const [incognito, setIncognito] = useState(false)
|
||||||
|
const [commandPreview, setCommandPreview] = useState<CommandPreviewResult | null>(null)
|
||||||
|
const [previewLoading, setPreviewLoading] = useState(false)
|
||||||
|
|
||||||
function onDragOver(e: React.DragEvent): void {
|
function onDragOver(e: React.DragEvent): void {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
if (!dragActive) setDragActive(true)
|
if (!dragActive) setDragActive(true)
|
||||||
@@ -368,11 +403,35 @@ export function DownloadBar(): React.JSX.Element {
|
|||||||
setItemKinds({})
|
setItemKinds({})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function loadCommandPreview(): Promise<void> {
|
||||||
|
const trimmed = url.trim()
|
||||||
|
if (!trimmed) {
|
||||||
|
setCommandPreview(null)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setPreviewLoading(true)
|
||||||
|
try {
|
||||||
|
const result = await window.api.previewCommand({
|
||||||
|
id: 'preview',
|
||||||
|
url: trimmed,
|
||||||
|
kind,
|
||||||
|
quality,
|
||||||
|
options: downloadOptions
|
||||||
|
})
|
||||||
|
setCommandPreview(result)
|
||||||
|
} catch (e) {
|
||||||
|
setCommandPreview({ ok: false, error: (e as Error).message })
|
||||||
|
} finally {
|
||||||
|
setPreviewLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function onUrlChange(next: string): void {
|
function onUrlChange(next: string): void {
|
||||||
setUrl(next)
|
setUrl(next)
|
||||||
// Any probed info -- or a duplicate warning -- is stale once the URL changes.
|
// Any probed info -- or a duplicate warning -- is stale once the URL changes.
|
||||||
if (info || probeError || playlist) clearProbe()
|
if (info || probeError || playlist) clearProbe()
|
||||||
if (dup) setDup(null)
|
if (dup) setDup(null)
|
||||||
|
if (commandPreview) setCommandPreview(null)
|
||||||
confirmDup.current = false
|
confirmDup.current = false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -489,6 +548,8 @@ export function DownloadBar(): React.JSX.Element {
|
|||||||
...meta,
|
...meta,
|
||||||
trim: trimSpec,
|
trim: trimSpec,
|
||||||
scheduledFor,
|
scheduledFor,
|
||||||
|
options: downloadOptions,
|
||||||
|
incognito,
|
||||||
format: {
|
format: {
|
||||||
id: selectedFormat.id,
|
id: selectedFormat.id,
|
||||||
hasAudio: selectedFormat.hasAudio,
|
hasAudio: selectedFormat.hasAudio,
|
||||||
@@ -496,7 +557,13 @@ export function DownloadBar(): React.JSX.Element {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
addFromUrl(trimmed, kind, quality, { ...meta, trim: trimSpec, scheduledFor })
|
addFromUrl(trimmed, kind, quality, {
|
||||||
|
...meta,
|
||||||
|
trim: trimSpec,
|
||||||
|
scheduledFor,
|
||||||
|
options: downloadOptions,
|
||||||
|
incognito
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
setUrl('')
|
setUrl('')
|
||||||
@@ -504,6 +571,8 @@ export function DownloadBar(): React.JSX.Element {
|
|||||||
setShowTrim(false)
|
setShowTrim(false)
|
||||||
setScheduleAt('')
|
setScheduleAt('')
|
||||||
setShowSchedule(false)
|
setShowSchedule(false)
|
||||||
|
setShowAdvanced(false)
|
||||||
|
setCommandPreview(null)
|
||||||
clearProbe()
|
clearProbe()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -771,6 +840,59 @@ export function DownloadBar(): React.JSX.Element {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{!playlist && (
|
||||||
|
<div className={styles.commandBlock}>
|
||||||
|
<div className={styles.optButtons}>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
appearance="subtle"
|
||||||
|
icon={<SettingsRegular />}
|
||||||
|
onClick={() => setShowAdvanced((v) => !v)}
|
||||||
|
>
|
||||||
|
{showAdvanced ? 'Advanced · on' : 'Advanced'}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
appearance="subtle"
|
||||||
|
icon={previewLoading ? <Spinner size="tiny" /> : <WindowConsoleRegular />}
|
||||||
|
onClick={loadCommandPreview}
|
||||||
|
disabled={!url.trim() || previewLoading}
|
||||||
|
>
|
||||||
|
{commandPreview ? 'Command · shown' : 'Show command'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{commandPreview && (
|
||||||
|
<div className={styles.commandPreviewPanel}>
|
||||||
|
{commandPreview.ok ? (
|
||||||
|
<>
|
||||||
|
<div>{commandPreview.command}</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<div style={{ color: tokens.colorStatusDangerForeground1 }}>
|
||||||
|
Error: {commandPreview.error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{showAdvanced && (
|
||||||
|
<div className={styles.optionsPanel}>
|
||||||
|
<Checkbox
|
||||||
|
checked={incognito}
|
||||||
|
onChange={(_, d) => setIncognito(!!d.checked)}
|
||||||
|
label="Incognito mode (no logging, no history, no cookies)"
|
||||||
|
/>
|
||||||
|
<div style={{ marginTop: '12px' }}>
|
||||||
|
<DownloadOptionsForm
|
||||||
|
value={downloadOptions}
|
||||||
|
onChange={setDownloadOptions}
|
||||||
|
kind={kind}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className={styles.controls}>
|
<div className={styles.controls}>
|
||||||
<div className={styles.control}>
|
<div className={styles.control}>
|
||||||
<Caption1>Format</Caption1>
|
<Caption1>Format</Caption1>
|
||||||
|
|||||||
Reference in New Issue
Block a user