Add styled output folder and filename rows to convert UI

This commit is contained in:
Stu Leak 2026-01-04 04:45:21 -05:00
parent 2f76ffb9c4
commit 586b84c39d

150
main.go
View File

@ -628,6 +628,7 @@ var formatOptions = []formatOption{
type convertConfig struct { type convertConfig struct {
OutputBase string OutputBase string
OutputDir string
SelectedFormat formatOption SelectedFormat formatOption
Quality string // Preset quality (Draft/Standard/High/Lossless) Quality string // Preset quality (Draft/Standard/High/Lossless)
Mode string // Simple or Advanced Mode string // Simple or Advanced
@ -699,6 +700,7 @@ func defaultConvertConfig() convertConfig {
return convertConfig{ return convertConfig{
SelectedFormat: formatOptions[0], SelectedFormat: formatOptions[0],
OutputBase: "converted", OutputBase: "converted",
OutputDir: "",
Quality: "Standard (CRF 23)", Quality: "Standard (CRF 23)",
Mode: "Simple", Mode: "Simple",
UseAutoNaming: false, UseAutoNaming: false,
@ -2202,7 +2204,10 @@ func (s *appState) addConvertToQueueForSource(src *videoSource, addToTop bool) e
cfg := s.convert cfg := s.convert
cfg.OutputBase = outputBase cfg.OutputBase = outputBase
outDir := filepath.Dir(src.Path) outDir := strings.TrimSpace(cfg.OutputDir)
if outDir == "" {
outDir = filepath.Dir(src.Path)
}
outName := cfg.OutputFile() outName := cfg.OutputFile()
if outName == "" { if outName == "" {
outName = "converted" + cfg.SelectedFormat.Ext outName = "converted" + cfg.SelectedFormat.Ext
@ -2228,6 +2233,7 @@ func (s *appState) addConvertToQueueForSource(src *videoSource, addToTop bool) e
config := map[string]interface{}{ config := map[string]interface{}{
"inputPath": src.Path, "inputPath": src.Path,
"outputPath": outPath, "outputPath": outPath,
"outputDir": outDir,
"outputBase": cfg.OutputBase, "outputBase": cfg.OutputBase,
"selectedFormat": cfg.SelectedFormat, "selectedFormat": cfg.SelectedFormat,
"quality": cfg.Quality, "quality": cfg.Quality,
@ -6981,6 +6987,16 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
if updateDVDOptions != nil { if updateDVDOptions != nil {
updateDVDOptions() updateDVDOptions()
} }
if outputExtLabel != nil {
outputExtLabel.SetText(state.convert.SelectedFormat.Ext)
}
if outputExtBG != nil {
outputExtBG.FillColor = ui.GetContainerColor(strings.TrimPrefix(state.convert.SelectedFormat.Ext, "."))
outputExtBG.Refresh()
}
if updateOutputHint != nil {
updateOutputHint()
}
if buildCommandPreview != nil { if buildCommandPreview != nil {
buildCommandPreview() buildCommandPreview()
} }
@ -6990,11 +7006,37 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
}, state.window) }, state.window)
formatContainer.SetSelected(state.convert.SelectedFormat.Label) formatContainer.SetSelected(state.convert.SelectedFormat.Label)
outputHint := widget.NewLabel(fmt.Sprintf("Output file: %s", state.convert.OutputFile())) var outputExtLabel *widget.Label
var outputExtBG *canvas.Rectangle
var updateOutputHint func()
getOutputDir := func() string {
if strings.TrimSpace(state.convert.OutputDir) != "" {
return state.convert.OutputDir
}
if src != nil {
return filepath.Dir(src.Path)
}
return ""
}
getOutputPathPreview := func() string {
outDir := getOutputDir()
if outDir == "" {
return state.convert.OutputFile()
}
return filepath.Join(outDir, state.convert.OutputFile())
}
outputHint := widget.NewLabel(fmt.Sprintf("Output file: %s", getOutputPathPreview()))
outputHint.Wrapping = fyne.TextWrapWord outputHint.Wrapping = fyne.TextWrapWord
// Wrap hint in padded container to ensure proper text wrapping in narrow windows // Wrap hint in padded container to ensure proper text wrapping in narrow windows
outputHintContainer := container.NewPadded(outputHint) outputHintContainer := container.NewPadded(outputHint)
updateOutputHint = func() {
outputHint.SetText(fmt.Sprintf("Output file: %s", getOutputPathPreview()))
}
// DVD-specific aspect ratio selector (only shown for DVD formats) // DVD-specific aspect ratio selector (only shown for DVD formats)
dvdAspectOpts := []string{"4:3", "16:9"} dvdAspectOpts := []string{"4:3", "16:9"}
dvdAspectSelect := widget.NewSelect(dvdAspectOpts, func(value string) { dvdAspectSelect := widget.NewSelect(dvdAspectOpts, func(value string) {
@ -7072,21 +7114,24 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
} }
// Quality select widgets - use state manager to eliminate sync flags // Quality select widgets - use state manager to eliminate sync flags
qualitySelectSimple = widget.NewSelect(qualityOptions, func(value string) { // Convert quality selects to ColoredSelect and register with state manager
qualityColorMap := ui.BuildQualityColorMap(qualityOptions)
qualitySelectSimple = ui.NewColoredSelect(qualityOptions, qualityColorMap, func(value string) {
logging.Debug(logging.CatUI, "quality preset %s (simple)", value) logging.Debug(logging.CatUI, "quality preset %s (simple)", value)
setQuality(value) setQuality(value)
if buildCommandPreview != nil { if buildCommandPreview != nil {
buildCommandPreview() buildCommandPreview()
} }
}) }, state.window)
qualitySelectAdv = widget.NewSelect(qualityOptions, func(value string) { qualitySelectAdv = ui.NewColoredSelect(qualityOptions, qualityColorMap, func(value string) {
logging.Debug(logging.CatUI, "quality preset %s (advanced)", value) logging.Debug(logging.CatUI, "quality preset %s (advanced)", value)
setQuality(value) setQuality(value)
if buildCommandPreview != nil { if buildCommandPreview != nil {
buildCommandPreview() buildCommandPreview()
} }
}) }, state.window)
if !slices.Contains(qualityOptions, state.convert.Quality) { if !slices.Contains(qualityOptions, state.convert.Quality) {
state.convert.Quality = "Standard (CRF 23)" state.convert.Quality = "Standard (CRF 23)"
@ -7094,6 +7139,9 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
qualitySelectSimple.SetSelected(state.convert.Quality) qualitySelectSimple.SetSelected(state.convert.Quality)
qualitySelectAdv.SetSelected(state.convert.Quality) qualitySelectAdv.SetSelected(state.convert.Quality)
// Register both quality widgets with state manager for automatic synchronization
uiState.qualityWidgets = []*ui.ColoredSelect{qualitySelectSimple, qualitySelectAdv}
// Update quality options based on codec // Update quality options based on codec
updateQualityOptions = func() { updateQualityOptions = func() {
var newOptions []string var newOptions []string
@ -7109,12 +7157,16 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
} }
} }
qualitySelectSimple.Options = newOptions // Update options and color map for all registered quality widgets
qualitySelectAdv.Options = newOptions qualityColorMap := ui.BuildQualityColorMap(newOptions)
qualitySelectSimple.SetSelected(state.convert.Quality) for _, w := range uiState.qualityWidgets {
qualitySelectAdv.SetSelected(state.convert.Quality) w.Options = newOptions
qualitySelectSimple.Refresh() w.ColorMap = qualityColorMap
qualitySelectAdv.Refresh() w.Refresh()
}
// Use state manager to synchronize selected value across all widgets
setQuality(state.convert.Quality)
} }
outputEntry := widget.NewEntry() outputEntry := widget.NewEntry()
@ -7132,9 +7184,60 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
} }
} }
state.convert.OutputBase = val state.convert.OutputBase = val
outputHint.SetText(fmt.Sprintf("Output file: %s", state.convert.OutputFile())) updateOutputHint()
} }
outputDirEntry := widget.NewEntry()
outputDirEntry.SetPlaceHolder("Output folder path")
outputDirEntry.SetText(state.convert.OutputDir)
outputDirEntry.OnChanged = func(val string) {
state.convert.OutputDir = val
updateOutputHint()
state.persistConvertConfig()
}
browseOutputDir := func() {
dialog.ShowFolderOpen(func(uri fyne.ListableURI, err error) {
if err != nil {
dialog.ShowError(err, state.window)
return
}
if uri == nil {
return
}
state.convert.OutputDir = uri.Path()
outputDirEntry.SetText(state.convert.OutputDir)
updateOutputHint()
state.persistConvertConfig()
}, state.window)
}
outputDirBtnLabel := canvas.NewText("Browse", textColor)
outputDirBtnLabel.Alignment = fyne.TextAlignCenter
outputDirBtnLabel.TextSize = 14
outputDirBtnBG := canvas.NewRectangle(utils.MustHex("#344256"))
outputDirBtnBG.CornerRadius = 8
outputDirBtnBG.SetMinSize(fyne.NewSize(92, 36))
outputDirBtn := ui.NewTappable(container.NewMax(outputDirBtnBG, container.NewPadded(outputDirBtnLabel)), browseOutputDir)
outputExtLabel = widget.NewLabel(state.convert.SelectedFormat.Ext)
outputExtLabel.Alignment = fyne.TextAlignCenter
outputExtBG = canvas.NewRectangle(ui.GetContainerColor(strings.TrimPrefix(state.convert.SelectedFormat.Ext, ".")))
outputExtBG.CornerRadius = 8
outputExtBG.SetMinSize(fyne.NewSize(72, 36))
outputExtPill := container.NewMax(outputExtBG, container.NewPadded(outputExtLabel))
buildOutputRow := func(entry *widget.Entry, right fyne.CanvasObject) fyne.CanvasObject {
bg := canvas.NewRectangle(utils.MustHex("#344256"))
bg.CornerRadius = 8
bg.SetMinSize(fyne.NewSize(0, 36))
row := container.NewBorder(nil, nil, nil, right, entry)
return container.NewMax(bg, container.NewPadded(row))
}
outputDirRow := buildOutputRow(outputDirEntry, outputDirBtn)
outputNameRow := buildOutputRow(outputEntry, outputExtPill)
applyAutoName := func(force bool) { applyAutoName := func(force bool) {
if !force && !state.convert.UseAutoNaming { if !force && !state.convert.UseAutoNaming {
return return
@ -7144,7 +7247,7 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
state.convert.OutputBase = newBase state.convert.OutputBase = newBase
outputEntry.SetText(newBase) outputEntry.SetText(newBase)
updatingOutput = false updatingOutput = false
outputHint.SetText(fmt.Sprintf("Output file: %s", state.convert.OutputFile())) updateOutputHint()
} }
autoNameCheck = widget.NewCheck("Auto-name from metadata", func(checked bool) { autoNameCheck = widget.NewCheck("Auto-name from metadata", func(checked bool) {
@ -7182,7 +7285,7 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
updatingOutput = false updatingOutput = false
// Update output hint to show the change immediately // Update output hint to show the change immediately
if outputHint != nil { if outputHint != nil {
outputHint.SetText(fmt.Sprintf("Output file: %s", state.convert.OutputFile())) updateOutputHint()
} }
}) })
appendSuffixCheck.Checked = state.convert.AppendSuffix appendSuffixCheck.Checked = state.convert.AppendSuffix
@ -8741,8 +8844,10 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
chapterWarningLabel, // Warning when converting chapters to DVD chapterWarningLabel, // Warning when converting chapters to DVD
preserveChaptersCheck, preserveChaptersCheck,
dvdAspectBox, // DVD options appear here when DVD format selected dvdAspectBox, // DVD options appear here when DVD format selected
widget.NewLabelWithStyle("Output Name", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}), widget.NewLabelWithStyle("Output Folder", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}),
outputEntry, outputDirRow,
widget.NewLabelWithStyle("Output Filename", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}),
outputNameRow,
outputHintContainer, outputHintContainer,
appendSuffixCheck, appendSuffixCheck,
widget.NewSeparator(), widget.NewSeparator(),
@ -8807,8 +8912,10 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
chapterWarningLabel, // Warning when converting chapters to DVD chapterWarningLabel, // Warning when converting chapters to DVD
preserveChaptersCheck, preserveChaptersCheck,
dvdAspectBox, // DVD options appear here when DVD format selected dvdAspectBox, // DVD options appear here when DVD format selected
widget.NewLabelWithStyle("Output Name", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}), widget.NewLabelWithStyle("Output Folder", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}),
outputEntry, outputDirRow,
widget.NewLabelWithStyle("Output Filename", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}),
outputNameRow,
outputHintContainer, outputHintContainer,
appendSuffixCheck, appendSuffixCheck,
widget.NewSeparator(), widget.NewSeparator(),
@ -8876,7 +8983,8 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
autoNameTemplate.SetText(state.convert.AutoNameTemplate) autoNameTemplate.SetText(state.convert.AutoNameTemplate)
appendSuffixCheck.SetChecked(state.convert.AppendSuffix) appendSuffixCheck.SetChecked(state.convert.AppendSuffix)
outputEntry.SetText(state.convert.OutputBase) outputEntry.SetText(state.convert.OutputBase)
outputHint.SetText(fmt.Sprintf("Output file: %s", state.convert.OutputFile())) outputDirEntry.SetText(state.convert.OutputDir)
updateOutputHint()
preserveChaptersCheck.SetChecked(state.convert.PreserveChapters) preserveChaptersCheck.SetChecked(state.convert.PreserveChapters)
resolutionSelectSimple.SetSelected(state.convert.TargetResolution) resolutionSelectSimple.SetSelected(state.convert.TargetResolution)
resolutionSelect.SetSelected(state.convert.TargetResolution) resolutionSelect.SetSelected(state.convert.TargetResolution)
@ -9401,7 +9509,7 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
// Replace INPUT and OUTPUT placeholders with actual file paths for preview // Replace INPUT and OUTPUT placeholders with actual file paths for preview
inputPath := src.Path inputPath := src.Path
outputPath := state.convert.OutputFile() outputPath := getOutputPathPreview()
cmdStr = strings.ReplaceAll(cmdStr, "INPUT", inputPath) cmdStr = strings.ReplaceAll(cmdStr, "INPUT", inputPath)
cmdStr = strings.ReplaceAll(cmdStr, "OUTPUT", outputPath) cmdStr = strings.ReplaceAll(cmdStr, "OUTPUT", outputPath)
cmdStr = strings.ReplaceAll(cmdStr, "[COVER_ART]", state.convert.CoverArtPath) cmdStr = strings.ReplaceAll(cmdStr, "[COVER_ART]", state.convert.CoverArtPath)