Phase 2: Add FFmpeg command preview to Convert module UI

Integrated the FFmpegCommandWidget into the Convert module:

1. Added command preview section in buildConvertView():
   - Creates FFmpegCommandWidget displaying current settings as FFmpeg command
   - Uses INPUT/OUTPUT placeholders for portability
   - Positioned above action bar, after snippet section
   - Only shows when video is loaded

2. Command building logic:
   - Builds config map from current convertConfig state
   - Passes to buildFFmpegCommandFromJob() for command generation
   - Updates preview dynamically (foundation for real-time updates)
   - Includes all conversion settings (codecs, filters, quality, audio)

3. UI layout improvements:
   - Added labeled "FFmpeg Command Preview:" header
   - Scrollable monospace command display (80px min height)
   - Copy button with clipboard integration
   - Clean separation from other sections

Users can now see and copy the exact FFmpeg command that will be used
for their conversion before starting it. This makes it easy to reproduce
VideoTools' output in external tools or verify settings.

Next: Add Copy Command button to queue view for active/pending jobs.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Stu Leak 2025-12-17 19:18:18 -05:00
parent 5903b15c67
commit 9df622eb72

94
main.go
View File

@ -6481,6 +6481,84 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
dialog.ShowInformation("Config Saved", fmt.Sprintf("Saved to %s", defaultConvertConfigPath()), state.window)
})
// FFmpeg Command Preview
var commandPreviewWidget *ui.FFmpegCommandWidget
var commandPreviewRow *fyne.Container
buildCommandPreview := func() {
if src == nil {
if commandPreviewRow != nil {
commandPreviewRow.Hide()
}
return
}
// Build command from current state
cfg := state.convert
config := map[string]interface{}{
"quality": cfg.Quality,
"videoCodec": cfg.VideoCodec,
"encoderPreset": cfg.EncoderPreset,
"crf": cfg.CRF,
"bitrateMode": cfg.BitrateMode,
"videoBitrate": cfg.VideoBitrate,
"targetFileSize": cfg.TargetFileSize,
"targetResolution": cfg.TargetResolution,
"frameRate": cfg.FrameRate,
"useMotionInterpolation": cfg.UseMotionInterpolation,
"pixelFormat": cfg.PixelFormat,
"hardwareAccel": cfg.HardwareAccel,
"h264Profile": cfg.H264Profile,
"h264Level": cfg.H264Level,
"deinterlace": cfg.Deinterlace,
"deinterlaceMethod": cfg.DeinterlaceMethod,
"autoCrop": cfg.AutoCrop,
"cropWidth": cfg.CropWidth,
"cropHeight": cfg.CropHeight,
"cropX": cfg.CropX,
"cropY": cfg.CropY,
"flipHorizontal": cfg.FlipHorizontal,
"flipVertical": cfg.FlipVertical,
"rotation": cfg.Rotation,
"audioCodec": cfg.AudioCodec,
"audioBitrate": cfg.AudioBitrate,
"audioChannels": cfg.AudioChannels,
"normalizeAudio": cfg.NormalizeAudio,
"coverArtPath": cfg.CoverArtPath,
"aspectHandling": cfg.AspectHandling,
"outputAspect": cfg.OutputAspect,
"sourceWidth": src.Width,
"sourceHeight": src.Height,
"sourceDuration": src.Duration,
"fieldOrder": src.FieldOrder,
}
job := &queue.Job{
Type: queue.JobTypeConvert,
Config: config,
}
cmdStr := buildFFmpegCommandFromJob(job)
if commandPreviewWidget == nil {
commandPreviewWidget = ui.NewFFmpegCommandWidget(cmdStr, state.window)
commandLabel := widget.NewLabel("FFmpeg Command Preview:")
commandLabel.TextStyle = fyne.TextStyle{Bold: true}
commandPreviewRow = container.NewVBox(
widget.NewSeparator(),
commandLabel,
commandPreviewWidget,
)
} else {
commandPreviewWidget.SetCommand(cmdStr)
}
if commandPreviewRow != nil {
commandPreviewRow.Show()
}
}
// Build initial preview if source is loaded
buildCommandPreview()
leftControls := container.NewHBox(resetBtn, loadCfgBtn, saveCfgBtn, autoCompareCheck)
rightControls := container.NewHBox(cancelBtn, cancelQueueBtn, viewLogBtn, addQueueBtn, convertBtn)
actionBar := container.NewHBox(leftControls, layout.NewSpacer(), rightControls)
@ -6562,13 +6640,19 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
scrollableMain := container.NewVScroll(mainContent)
// Build footer sections
footerSections := []fyne.CanvasObject{
snippetConfigRow,
snippetRow,
widget.NewSeparator(),
}
if commandPreviewRow != nil {
footerSections = append(footerSections, commandPreviewRow)
}
mainWithFooter := container.NewBorder(
nil,
container.NewVBox(
snippetConfigRow,
snippetRow,
widget.NewSeparator(),
),
container.NewVBox(footerSections...),
nil, nil,
container.NewMax(scrollableMain),
)