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,
|
||||
CutRegular,
|
||||
CalendarClockRegular,
|
||||
WarningRegular
|
||||
WarningRegular,
|
||||
SettingsRegular,
|
||||
WindowConsoleRegular
|
||||
} from '@fluentui/react-icons'
|
||||
import {
|
||||
parseUrlShortcutContent,
|
||||
type MediaInfo,
|
||||
type FormatOption,
|
||||
type PlaylistInfo
|
||||
type PlaylistInfo,
|
||||
type DownloadOptions,
|
||||
DEFAULT_DOWNLOAD_OPTIONS,
|
||||
type CommandPreviewResult
|
||||
} from '@shared/ipc'
|
||||
import { useDownloads, type MediaKind } from '../store/downloads'
|
||||
import { QUALITY_OPTIONS } from '../qualityOptions'
|
||||
@@ -41,6 +46,7 @@ import { Select } from './Select'
|
||||
import { Hint } from './Hint'
|
||||
import { SegmentedControl } from './ui/SegmentedControl'
|
||||
import { IconButton } from './ui/IconButton'
|
||||
import { DownloadOptionsForm } from './DownloadOptionsForm'
|
||||
import { useClipboardLink, looksLikeUrl, firstUrlInText } from '../useClipboardLink'
|
||||
import { logError } from '../reportError'
|
||||
import { THUMB_LG } from '../thumbSizes'
|
||||
@@ -246,6 +252,28 @@ const useStyles = makeStyles({
|
||||
backgroundColor: tokens.colorNeutralBackground1,
|
||||
color: tokens.colorNeutralForeground1,
|
||||
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 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 {
|
||||
e.preventDefault()
|
||||
if (!dragActive) setDragActive(true)
|
||||
@@ -368,11 +403,35 @@ export function DownloadBar(): React.JSX.Element {
|
||||
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 {
|
||||
setUrl(next)
|
||||
// Any probed info -- or a duplicate warning -- is stale once the URL changes.
|
||||
if (info || probeError || playlist) clearProbe()
|
||||
if (dup) setDup(null)
|
||||
if (commandPreview) setCommandPreview(null)
|
||||
confirmDup.current = false
|
||||
}
|
||||
|
||||
@@ -489,6 +548,8 @@ export function DownloadBar(): React.JSX.Element {
|
||||
...meta,
|
||||
trim: trimSpec,
|
||||
scheduledFor,
|
||||
options: downloadOptions,
|
||||
incognito,
|
||||
format: {
|
||||
id: selectedFormat.id,
|
||||
hasAudio: selectedFormat.hasAudio,
|
||||
@@ -496,7 +557,13 @@ export function DownloadBar(): React.JSX.Element {
|
||||
}
|
||||
})
|
||||
} else {
|
||||
addFromUrl(trimmed, kind, quality, { ...meta, trim: trimSpec, scheduledFor })
|
||||
addFromUrl(trimmed, kind, quality, {
|
||||
...meta,
|
||||
trim: trimSpec,
|
||||
scheduledFor,
|
||||
options: downloadOptions,
|
||||
incognito
|
||||
})
|
||||
}
|
||||
|
||||
setUrl('')
|
||||
@@ -504,6 +571,8 @@ export function DownloadBar(): React.JSX.Element {
|
||||
setShowTrim(false)
|
||||
setScheduleAt('')
|
||||
setShowSchedule(false)
|
||||
setShowAdvanced(false)
|
||||
setCommandPreview(null)
|
||||
clearProbe()
|
||||
}
|
||||
|
||||
@@ -771,6 +840,59 @@ export function DownloadBar(): React.JSX.Element {
|
||||
</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.control}>
|
||||
<Caption1>Format</Caption1>
|
||||
|
||||
Reference in New Issue
Block a user