feat: replace grey dropdowns with ColoredSelect in Convert & Merge modules (batch 1)

This is the first batch focusing on critical Convert module dropdowns and
some Merge module dropdowns. All dropdowns now have vibrant color coding
for faster visual navigation.

Convert Module - Replaced:
- Quality presets (simple & advanced) - quality gradient colors
- Bitrate mode select - generic rainbow colors
- CRF preset select - quality gradient colors
- Bitrate preset selects (×2) - quality gradient colors
- Target file size select - generic rainbow colors
- DVD aspect select - generic rainbow colors

Merge Module - Replaced:
- DVD region select (NTSC/PAL) - rainbow colors
- DVD aspect select (16:9/4:3) - rainbow colors
- Format select - semantic format colors (MKV=teal, MP4=blue, etc)
- Frame rate select - rainbow colors

ColoredSelect enhancements:
- Added UpdateOptions() method for dynamic option updates
- Added Enable() and Disable() methods
- Added disabled state tracking and visual feedback
- Fixed Selected() method to be callable (not a field)

Build status:  Successful
This commit is contained in:
Stu Leak 2026-01-02 12:16:09 -05:00
parent 1f9d5b4043
commit 24c17ff61d
2 changed files with 121 additions and 76 deletions

View File

@ -1037,6 +1037,7 @@ type ColoredSelect struct {
popup *widget.PopUp popup *widget.PopUp
window fyne.Window window fyne.Window
placeHolder string placeHolder string
disabled bool
} }
// NewColoredSelect creates a new colored select widget // NewColoredSelect creates a new colored select widget
@ -1066,11 +1067,41 @@ func (cs *ColoredSelect) SetSelected(option string) {
cs.Refresh() cs.Refresh()
} }
// UpdateOptions updates the available options and their colors
func (cs *ColoredSelect) UpdateOptions(options []string, colorMap map[string]color.Color) {
cs.options = options
cs.colorMap = colorMap
// If current selection is not in new options, select first option
found := false
for _, opt := range options {
if opt == cs.selected {
found = true
break
}
}
if !found && len(options) > 0 {
cs.selected = options[0]
}
cs.Refresh()
}
// Selected returns the currently selected option // Selected returns the currently selected option
func (cs *ColoredSelect) Selected() string { func (cs *ColoredSelect) Selected() string {
return cs.selected return cs.selected
} }
// Enable enables the widget
func (cs *ColoredSelect) Enable() {
cs.disabled = false
cs.Refresh()
}
// Disable disables the widget
func (cs *ColoredSelect) Disable() {
cs.disabled = true
cs.Refresh()
}
// CreateRenderer creates the renderer for the colored select // CreateRenderer creates the renderer for the colored select
func (cs *ColoredSelect) CreateRenderer() fyne.WidgetRenderer { func (cs *ColoredSelect) CreateRenderer() fyne.WidgetRenderer {
// Create the button that shows current selection // Create the button that shows current selection
@ -1153,7 +1184,9 @@ func (cs *ColoredSelect) showPopup() {
// Tapped implements the Tappable interface // Tapped implements the Tappable interface
func (cs *ColoredSelect) Tapped(*fyne.PointEvent) { func (cs *ColoredSelect) Tapped(*fyne.PointEvent) {
cs.showPopup() if !cs.disabled {
cs.showPopup()
}
} }
type coloredSelectRenderer struct { type coloredSelectRenderer struct {

162
main.go
View File

@ -3463,16 +3463,20 @@ func (s *appState) showMergeView() {
} }
// DVD-specific options // DVD-specific options
dvdRegionSelect := widget.NewSelect([]string{"NTSC", "PAL"}, func(val string) { dvdRegionOptions := []string{"NTSC", "PAL"}
dvdRegionColors := ui.BuildGenericColorMap(dvdRegionOptions)
dvdRegionSelect := ui.NewColoredSelect(dvdRegionOptions, dvdRegionColors, func(val string) {
s.mergeDVDRegion = val s.mergeDVDRegion = val
s.persistMergeConfig() s.persistMergeConfig()
}) }, s.window)
dvdRegionSelect.SetSelected(s.mergeDVDRegion) dvdRegionSelect.SetSelected(s.mergeDVDRegion)
dvdAspectSelect := widget.NewSelect([]string{"16:9", "4:3"}, func(val string) { dvdAspectOptions := []string{"16:9", "4:3"}
dvdAspectColors := ui.BuildGenericColorMap(dvdAspectOptions)
dvdAspectSelect := ui.NewColoredSelect(dvdAspectOptions, dvdAspectColors, func(val string) {
s.mergeDVDAspect = val s.mergeDVDAspect = val
s.persistMergeConfig() s.persistMergeConfig()
}) }, s.window)
dvdAspectSelect.SetSelected(s.mergeDVDAspect) dvdAspectSelect.SetSelected(s.mergeDVDAspect)
dvdOptionsRow := container.NewHBox( dvdOptionsRow := container.NewHBox(
@ -3486,7 +3490,8 @@ func (s *appState) showMergeView() {
dvdOptionsContainer := container.NewVBox(dvdOptionsRow) dvdOptionsContainer := container.NewVBox(dvdOptionsRow)
// Create format selector (after outputEntry and updateOutputExt are defined) // Create format selector (after outputEntry and updateOutputExt are defined)
formatSelect := widget.NewSelect(formatKeys, func(val string) { formatColors := ui.BuildFormatColorMap(formatKeys)
formatSelect := ui.NewColoredSelect(formatKeys, formatColors, func(val string) {
s.mergeFormat = formatMap[val] s.mergeFormat = formatMap[val]
// Show/hide DVD options based on selection // Show/hide DVD options based on selection
@ -3520,7 +3525,7 @@ func (s *appState) showMergeView() {
updateOutputExt() updateOutputExt()
} }
s.persistMergeConfig() s.persistMergeConfig()
}) }, s.window)
for label, val := range formatMap { for label, val := range formatMap {
if val == s.mergeFormat { if val == s.mergeFormat {
formatSelect.SetSelected(label) formatSelect.SetSelected(label)
@ -3536,10 +3541,12 @@ func (s *appState) showMergeView() {
} }
// Frame Rate controls // Frame Rate controls
frameRateSelect := widget.NewSelect([]string{"Source", "23.976", "24", "25", "29.97", "30", "50", "59.94", "60"}, func(val string) { frameRateOptions := []string{"Source", "23.976", "24", "25", "29.97", "30", "50", "59.94", "60"}
frameRateColors := ui.BuildGenericColorMap(frameRateOptions)
frameRateSelect := ui.NewColoredSelect(frameRateOptions, frameRateColors, func(val string) {
s.mergeFrameRate = val s.mergeFrameRate = val
s.persistMergeConfig() s.persistMergeConfig()
}) }, s.window)
frameRateSelect.SetSelected(s.mergeFrameRate) frameRateSelect.SetSelected(s.mergeFrameRate)
motionInterpCheck := widget.NewCheck("Use Motion Interpolation (slower, smoother)", func(checked bool) { motionInterpCheck := widget.NewCheck("Use Motion Interpolation (slower, smoother)", func(checked bool) {
@ -6528,10 +6535,12 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
outputHintContainer := container.NewPadded(outputHint) outputHintContainer := container.NewPadded(outputHint)
// DVD-specific aspect ratio selector (only shown for DVD formats) // DVD-specific aspect ratio selector (only shown for DVD formats)
dvdAspectSelect := widget.NewSelect([]string{"4:3", "16:9"}, func(value string) { dvdAspectOpts := []string{"4:3", "16:9"}
dvdAspectCols := ui.BuildGenericColorMap(dvdAspectOpts)
dvdAspectSelect := ui.NewColoredSelect(dvdAspectOpts, dvdAspectCols, func(value string) {
logging.Debug(logging.CatUI, "DVD aspect set to %s", value) logging.Debug(logging.CatUI, "DVD aspect set to %s", value)
state.convert.OutputAspect = value state.convert.OutputAspect = value
}) }, state.window)
dvdAspectSelect.SetSelected("16:9") dvdAspectSelect.SetSelected("16:9")
dvdAspectLabel := widget.NewLabelWithStyle("DVD Aspect Ratio", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}) dvdAspectLabel := widget.NewLabelWithStyle("DVD Aspect Ratio", fyne.TextAlignLeading, fyne.TextStyle{Bold: true})
@ -6554,20 +6563,20 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
// Forward declarations for encoding controls (used in reset/update callbacks) // Forward declarations for encoding controls (used in reset/update callbacks)
var ( var (
bitrateModeSelect *widget.Select bitrateModeSelect *ui.ColoredSelect
bitratePresetSelect *widget.Select bitratePresetSelect *ui.ColoredSelect
crfPresetSelect *widget.Select crfPresetSelect *ui.ColoredSelect
crfEntry *widget.Entry crfEntry *widget.Entry
manualCrfRow *fyne.Container manualCrfRow *fyne.Container
videoBitrateEntry *widget.Entry videoBitrateEntry *widget.Entry
manualBitrateRow *fyne.Container manualBitrateRow *fyne.Container
targetFileSizeSelect *widget.Select targetFileSizeSelect *ui.ColoredSelect
targetFileSizeEntry *widget.Entry targetFileSizeEntry *widget.Entry
qualitySelectSimple *widget.Select qualitySelectSimple *ui.ColoredSelect
qualitySelectAdv *widget.Select qualitySelectAdv *ui.ColoredSelect
qualitySectionSimple fyne.CanvasObject qualitySectionSimple fyne.CanvasObject
qualitySectionAdv fyne.CanvasObject qualitySectionAdv fyne.CanvasObject
simpleBitrateSelect *widget.Select simpleBitrateSelect *ui.ColoredSelect
crfContainer *fyne.Container crfContainer *fyne.Container
bitrateContainer *fyne.Container bitrateContainer *fyne.Container
targetSizeContainer *fyne.Container targetSizeContainer *fyne.Container
@ -6608,7 +6617,8 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
var syncingQuality bool var syncingQuality bool
qualitySelectSimple = widget.NewSelect(qualityOptions, func(value string) { qualityColors := ui.BuildQualityColorMap(qualityOptions)
qualitySelectSimple = ui.NewColoredSelect(qualityOptions, qualityColors, func(value string) {
if syncingQuality { if syncingQuality {
return return
} }
@ -6625,9 +6635,9 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
if buildCommandPreview != nil { if buildCommandPreview != nil {
buildCommandPreview() buildCommandPreview()
} }
}) }, state.window)
qualitySelectAdv = widget.NewSelect(qualityOptions, func(value string) { qualitySelectAdv = ui.NewColoredSelect(qualityOptions, qualityColors, func(value string) {
if syncingQuality { if syncingQuality {
return return
} }
@ -6644,7 +6654,7 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
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)"
@ -6667,12 +6677,11 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
} }
} }
qualitySelectSimple.Options = newOptions newColors := ui.BuildQualityColorMap(newOptions)
qualitySelectAdv.Options = newOptions qualitySelectSimple.UpdateOptions(newOptions, newColors)
qualitySelectAdv.UpdateOptions(newOptions, newColors)
qualitySelectSimple.SetSelected(state.convert.Quality) qualitySelectSimple.SetSelected(state.convert.Quality)
qualitySelectAdv.SetSelected(state.convert.Quality) qualitySelectAdv.SetSelected(state.convert.Quality)
qualitySelectSimple.Refresh()
qualitySelectAdv.Refresh()
} }
outputEntry := widget.NewEntry() outputEntry := widget.NewEntry()
@ -7253,7 +7262,8 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
"VBR": "VBR (Variable Bitrate)", "VBR": "VBR (Variable Bitrate)",
"Target Size": "Target Size (Calculate from file size)", "Target Size": "Target Size (Calculate from file size)",
} }
bitrateModeSelect = widget.NewSelect(bitrateModeOptions, func(value string) { bitrateModeColors := ui.BuildGenericColorMap(bitrateModeOptions)
bitrateModeSelect = ui.NewColoredSelect(bitrateModeOptions, bitrateModeColors, func(value string) {
// Extract short code from label // Extract short code from label
if shortCode, ok := bitrateModeMap[value]; ok { if shortCode, ok := bitrateModeMap[value]; ok {
state.convert.BitrateMode = shortCode state.convert.BitrateMode = shortCode
@ -7267,7 +7277,7 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
if buildCommandPreview != nil { if buildCommandPreview != nil {
buildCommandPreview() buildCommandPreview()
} }
}) }, state.window)
// Set selected using full label // Set selected using full label
if fullLabel, ok := reverseMap[state.convert.BitrateMode]; ok { if fullLabel, ok := reverseMap[state.convert.BitrateMode]; ok {
bitrateModeSelect.SetSelected(fullLabel) bitrateModeSelect.SetSelected(fullLabel)
@ -7300,7 +7310,8 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
"28 (Draft)", "28 (Draft)",
"Manual", "Manual",
} }
crfPresetSelect = widget.NewSelect(crfPresetOptions, func(value string) { crfPresetColors := ui.BuildQualityColorMap(crfPresetOptions)
crfPresetSelect = ui.NewColoredSelect(crfPresetOptions, crfPresetColors, func(value string) {
switch value { switch value {
case "Auto (from Quality preset)": case "Auto (from Quality preset)":
state.convert.CRF = "" state.convert.CRF = ""
@ -7328,7 +7339,7 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
if buildCommandPreview != nil { if buildCommandPreview != nil {
buildCommandPreview() buildCommandPreview()
} }
}) }, state.window)
switch state.convert.CRF { switch state.convert.CRF {
case "": case "":
crfPresetSelect.SetSelected("Auto (from Quality preset)") crfPresetSelect.SetSelected("Auto (from Quality preset)")
@ -7563,14 +7574,15 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
var setBitratePreset func(string) var setBitratePreset func(string)
var syncingBitratePreset bool var syncingBitratePreset bool
bitratePresetSelect = widget.NewSelect(bitratePresetLabels, func(value string) { bitratePresetColors := ui.BuildQualityColorMap(bitratePresetLabels)
bitratePresetSelect = ui.NewColoredSelect(bitratePresetLabels, bitratePresetColors, func(value string) {
if syncingBitratePreset { if syncingBitratePreset {
return return
} }
if setBitratePreset != nil { if setBitratePreset != nil {
setBitratePreset(value) setBitratePreset(value)
} }
}) }, state.window)
state.convert.BitratePreset = normalizePresetLabel(state.convert.BitratePreset) state.convert.BitratePreset = normalizePresetLabel(state.convert.BitratePreset)
if state.convert.BitratePreset == "" || bitratePresetLookup[state.convert.BitratePreset].Label == "" { if state.convert.BitratePreset == "" || bitratePresetLookup[state.convert.BitratePreset].Label == "" {
state.convert.BitratePreset = "2.5 Mbps - Medium" state.convert.BitratePreset = "2.5 Mbps - Medium"
@ -7578,14 +7590,14 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
bitratePresetSelect.SetSelected(state.convert.BitratePreset) bitratePresetSelect.SetSelected(state.convert.BitratePreset)
// Simple bitrate selector (shares presets) // Simple bitrate selector (shares presets)
simpleBitrateSelect = widget.NewSelect(bitratePresetLabels, func(value string) { simpleBitrateSelect = ui.NewColoredSelect(bitratePresetLabels, bitratePresetColors, func(value string) {
if syncingBitratePreset { if syncingBitratePreset {
return return
} }
if setBitratePreset != nil { if setBitratePreset != nil {
setBitratePreset(value) setBitratePreset(value)
} }
}) }, state.window)
simpleBitrateSelect.SetSelected(state.convert.BitratePreset) simpleBitrateSelect.SetSelected(state.convert.BitratePreset)
// Manual bitrate row (hidden by default) // Manual bitrate row (hidden by default)
@ -7732,50 +7744,50 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
} }
updateTargetSizeOptions := func() { updateTargetSizeOptions := func() {
if src == nil { options := []string{"Manual", "25MB", "50MB", "100MB", "200MB", "500MB", "1GB"}
targetFileSizeSelect.Options = []string{"Manual", "25MB", "50MB", "100MB", "200MB", "500MB", "1GB"}
return if src != nil {
// Calculate smart reduction options based on source file size
srcPath := src.Path
fileInfo, err := os.Stat(srcPath)
if err == nil {
srcSize := fileInfo.Size()
srcSizeMB := float64(srcSize) / (1024 * 1024)
// Calculate smart reductions
size25 := int(srcSizeMB * 0.75) // 25% reduction
size33 := int(srcSizeMB * 0.67) // 33% reduction
size50 := int(srcSizeMB * 0.50) // 50% reduction
size75 := int(srcSizeMB * 0.25) // 75% reduction
smartOptions := []string{"Manual"}
if size75 > 5 {
smartOptions = append(smartOptions, fmt.Sprintf("%dMB (75%% smaller)", size75))
}
if size50 > 10 {
smartOptions = append(smartOptions, fmt.Sprintf("%dMB (50%% smaller)", size50))
}
if size33 > 15 {
smartOptions = append(smartOptions, fmt.Sprintf("%dMB (33%% smaller)", size33))
}
if size25 > 20 {
smartOptions = append(smartOptions, fmt.Sprintf("%dMB (25%% smaller)", size25))
}
// Add common sizes
smartOptions = append(smartOptions, "25MB", "50MB", "100MB", "200MB", "500MB", "1GB")
options = smartOptions
}
} }
// Calculate smart reduction options based on source file size targetSizeColors := ui.BuildGenericColorMap(options)
srcPath := src.Path targetFileSizeSelect.UpdateOptions(options, targetSizeColors)
fileInfo, err := os.Stat(srcPath)
if err != nil {
targetFileSizeSelect.Options = []string{"Manual", "25MB", "50MB", "100MB", "200MB", "500MB", "1GB"}
return
}
srcSize := fileInfo.Size()
srcSizeMB := float64(srcSize) / (1024 * 1024)
// Calculate smart reductions
size25 := int(srcSizeMB * 0.75) // 25% reduction
size33 := int(srcSizeMB * 0.67) // 33% reduction
size50 := int(srcSizeMB * 0.50) // 50% reduction
size75 := int(srcSizeMB * 0.25) // 75% reduction
options := []string{"Manual"}
if size75 > 5 {
options = append(options, fmt.Sprintf("%dMB (75%% smaller)", size75))
}
if size50 > 10 {
options = append(options, fmt.Sprintf("%dMB (50%% smaller)", size50))
}
if size33 > 15 {
options = append(options, fmt.Sprintf("%dMB (33%% smaller)", size33))
}
if size25 > 20 {
options = append(options, fmt.Sprintf("%dMB (25%% smaller)", size25))
}
// Add common sizes
options = append(options, "25MB", "50MB", "100MB", "200MB", "500MB", "1GB")
targetFileSizeSelect.Options = options
} }
targetFileSizeSelect = widget.NewSelect([]string{"25MB", "50MB", "100MB", "200MB", "500MB", "1GB", "Manual"}, func(value string) { targetSizeOpts := []string{"25MB", "50MB", "100MB", "200MB", "500MB", "1GB", "Manual"}
targetSizeCols := ui.BuildGenericColorMap(targetSizeOpts)
targetFileSizeSelect = ui.NewColoredSelect(targetSizeOpts, targetSizeCols, func(value string) {
if value == "Manual" { if value == "Manual" {
targetSizeManualRow.Show() targetSizeManualRow.Show()
if state.convert.TargetFileSize != "" { if state.convert.TargetFileSize != "" {
@ -7810,7 +7822,7 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
targetSizeManualRow.Hide() targetSizeManualRow.Hide()
} }
logging.Debug(logging.CatUI, "target file size set to %s", state.convert.TargetFileSize) logging.Debug(logging.CatUI, "target file size set to %s", state.convert.TargetFileSize)
}) }, state.window)
targetFileSizeSelect.SetSelected("100MB") targetFileSizeSelect.SetSelected("100MB")
updateTargetSizeOptions() updateTargetSizeOptions()
@ -8164,7 +8176,7 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
) )
// Prefer the explicit DVD aspect select if set; otherwise derive from source // Prefer the explicit DVD aspect select if set; otherwise derive from source
targetAR = dvdAspectSelect.Selected targetAR = dvdAspectSelect.Selected()
if strings.Contains(state.convert.SelectedFormat.Label, "NTSC") { if strings.Contains(state.convert.SelectedFormat.Label, "NTSC") {
dvdNotes = "NTSC DVD: 720×480 @ 29.97fps, MPEG-2 Video, AC-3 Stereo 48kHz (bitrate 8000k, 9000k max PS2-safe)" dvdNotes = "NTSC DVD: 720×480 @ 29.97fps, MPEG-2 Video, AC-3 Stereo 48kHz (bitrate 8000k, 9000k max PS2-safe)"