From 9df622eb720cae48a76062e44934f8c1ea5814ee Mon Sep 17 00:00:00 2001 From: Stu Leak Date: Wed, 17 Dec 2025 19:18:18 -0500 Subject: [PATCH] Phase 2: Add FFmpeg command preview to Convert module UI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- main.go | 94 ++++++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 89 insertions(+), 5 deletions(-) diff --git a/main.go b/main.go index c90da56..80400ce 100644 --- a/main.go +++ b/main.go @@ -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), )