feat(ui): Redesign Convert module with color-coded dropdown buttons

Major UI improvement: Integrate color indicators directly into dropdown buttons
instead of showing separate badge elements, creating a cleaner, more intuitive
interface where power users can quickly identify format/codec selections by color.

Changes:
- Add NewColorCodedSelectContainer() in internal/ui/components.go
  - Creates colored 4px left border on dropdowns
  - Returns container and border reference for dynamic color updates

- Update Format Selection:
  - Colored border matches container format (MKV=teal, MP4=blue, etc.)
  - Dynamic color updates when format changes
  - Remove old formatBadgeContainer approach

- Update Video Codec Selection:
  - Colored border matches codec (H.264=sky blue, H.265=lime, AV1=emerald, etc.)
  - Applied to Advanced tab

- Update Audio Codec Selection:
  - Colored border matches codec (AAC=purple, Opus=violet, MP3=rose, etc.)
  - Applied to Advanced tab

Color system provides instant visual feedback and helps power users navigate
settings quickly. Each format/codec has a unique color that's consistent
throughout the UI.

🤖 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 2026-01-01 19:45:27 -05:00
parent fad9ac2247
commit c7ac82f306
2 changed files with 80 additions and 35 deletions

View File

@ -998,3 +998,16 @@ func ColoredDivider(accentColor color.Color) fyne.CanvasObject {
divider.SetMinSize(fyne.NewSize(0, 2))
return divider
}
// NewColorCodedSelectContainer wraps a Select widget with a colored left border
// The colored border visually indicates the category/type of the selection
// Returns a container with the border and a pointer to the border rectangle for color updates
func NewColorCodedSelectContainer(selectWidget *widget.Select, accentColor color.Color) (*fyne.Container, *canvas.Rectangle) {
// Create colored left border rectangle
border := canvas.NewRectangle(accentColor)
border.SetMinSize(fyne.NewSize(4, 44))
// Return container with [ColoredBorder][Select] and the border for future updates
container := container.NewBorder(nil, nil, border, nil, selectWidget)
return container, border
}

102
main.go
View File

@ -6972,15 +6972,17 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
// Cover art display on one line
coverDisplay = widget.NewLabel("Cover Art: " + state.convert.CoverLabel())
// Create video codec badge with semantic color
videoCodecBadgeContainer := container.NewMax()
updateVideoCodecBadge := func(codecName string) {
videoCodecBadgeContainer.Objects = []fyne.CanvasObject{buildVideoCodecBadge(codecName)}
videoCodecBadgeContainer.Refresh()
}
// Create video codec select widget with color-coded left border
videoCodecSelect := widget.NewSelect([]string{"H.264", "H.265", "VP9", "AV1", "MPEG-2", "Copy"}, nil) // Callback set below
// Video Codec selection
videoCodecSelect := widget.NewSelect([]string{"H.264", "H.265", "VP9", "AV1", "MPEG-2", "Copy"}, func(value string) {
// Get initial color for selected video codec
initialVideoCodecColor := ui.GetVideoCodecColor(state.convert.VideoCodec)
// Wrap in color-coded container
videoCodecContainer, videoCodecBorder := ui.NewColorCodedSelectContainer(videoCodecSelect, initialVideoCodecColor)
// Set video codec select callback (now that we have videoCodecBorder reference)
videoCodecSelect.OnChanged = func(value string) {
state.convert.VideoCodec = value
logging.Debug(logging.CatUI, "video codec set to %s", value)
if updateQualityOptions != nil {
@ -6995,10 +6997,13 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
if buildCommandPreview != nil {
buildCommandPreview()
}
updateVideoCodecBadge(value)
})
// Update border color to match new codec
newColor := ui.GetVideoCodecColor(value)
videoCodecBorder.FillColor = newColor
videoCodecBorder.Refresh()
}
videoCodecSelect.SetSelected(state.convert.VideoCodec)
updateVideoCodecBadge(state.convert.VideoCodec)
// Map format preset codec names to the UI-facing codec selector value
mapFormatCodec := func(codec string) string {
@ -7035,14 +7040,32 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
}
}
// Create format badge with semantic color
formatBadgeContainer := container.NewMax()
updateFormatBadge := func(label string) {
formatBadgeContainer.Objects = []fyne.CanvasObject{buildFormatBadge(label)}
formatBadgeContainer.Refresh()
// Create format select widget with color-coded left border
formatSelect := widget.NewSelect(formatLabels, nil) // Callback set below
// Parse format name from label (e.g., "MKV (AV1)" -> "mkv")
parseFormat := func(label string) string {
// Extract container format from label
parts := strings.Split(label, " ")
if len(parts) > 0 {
format := strings.ToLower(parts[0])
// Special case: "REMUX" should use remux color
if strings.Contains(strings.ToUpper(label), "REMUX") {
return "remux"
}
return format
}
return "mp4" // fallback
}
updateFormatBadge(state.convert.SelectedFormat.Label)
formatSelect := widget.NewSelect(formatLabels, func(value string) {
// Get initial color for selected format
initialFormatColor := ui.GetContainerColor(parseFormat(state.convert.SelectedFormat.Label))
// Wrap in color-coded container
formatContainer, formatBorder := ui.NewColorCodedSelectContainer(formatSelect, initialFormatColor)
// Set format select callback (now that we have formatBorder reference)
formatSelect.OnChanged = func(value string) {
for _, opt := range formatOptions {
if opt.Label == value {
logging.Debug(logging.CatUI, "format set to %s", value)
@ -7076,11 +7099,15 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
if buildCommandPreview != nil {
buildCommandPreview()
}
updateFormatBadge(value)
// Update border color to match new format
newColor := ui.GetContainerColor(parseFormat(value))
formatBorder.FillColor = newColor
formatBorder.Refresh()
break
}
}
})
}
formatSelect.SetSelected(state.convert.SelectedFormat.Label)
updateChapterWarning() // Initial visibility
@ -8038,21 +8065,26 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
})
twoPassCheck.Checked = state.convert.TwoPass
// Create audio codec badge with semantic color
audioCodecBadgeContainer := container.NewMax()
updateAudioCodecBadge := func(codecName string) {
audioCodecBadgeContainer.Objects = []fyne.CanvasObject{buildAudioCodecBadge(codecName)}
audioCodecBadgeContainer.Refresh()
}
// Create audio codec select widget with color-coded left border
audioCodecSelect = widget.NewSelect([]string{"AAC", "Opus", "MP3", "FLAC", "Copy"}, nil) // Callback set below
// Audio Codec
audioCodecSelect = widget.NewSelect([]string{"AAC", "Opus", "MP3", "FLAC", "Copy"}, func(value string) {
// Get initial color for selected audio codec
initialAudioCodecColor := ui.GetAudioCodecColor(state.convert.AudioCodec)
// Wrap in color-coded container
audioCodecContainer, audioCodecBorder := ui.NewColorCodedSelectContainer(audioCodecSelect, initialAudioCodecColor)
// Set audio codec select callback (now that we have audioCodecBorder reference)
audioCodecSelect.OnChanged = func(value string) {
state.convert.AudioCodec = value
logging.Debug(logging.CatUI, "audio codec set to %s", value)
updateAudioCodecBadge(value)
})
// Update border color to match new codec
newColor := ui.GetAudioCodecColor(value)
audioCodecBorder.FillColor = newColor
audioCodecBorder.Refresh()
}
audioCodecSelect.SetSelected(state.convert.AudioCodec)
updateAudioCodecBadge(state.convert.AudioCodec)
// Audio Bitrate
audioBitrateSelect := widget.NewSelect([]string{"128k", "192k", "256k", "320k"}, func(value string) {
@ -8334,7 +8366,7 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
simpleOptions := container.NewVBox(
widget.NewLabelWithStyle("═══ OUTPUT ═══", fyne.TextAlignCenter, fyne.TextStyle{Bold: true}),
widget.NewLabelWithStyle("Format", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}),
container.NewHBox(formatSelect, formatBadgeContainer),
formatContainer,
chapterWarningLabel, // Warning when converting chapters to DVD
preserveChaptersCheck,
dvdAspectBox, // DVD options appear here when DVD format selected
@ -8359,7 +8391,7 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
advancedVideoEncodingBlock = container.NewVBox(
widget.NewLabelWithStyle("═══ VIDEO ENCODING ═══", fyne.TextAlignCenter, fyne.TextStyle{Bold: true}),
widget.NewLabelWithStyle("Video Codec", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}),
container.NewHBox(videoCodecSelect, videoCodecBadgeContainer),
videoCodecContainer,
widget.NewLabelWithStyle("Encoder Preset (speed vs quality)", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}),
encoderPresetSelect,
encoderPresetHintContainer,
@ -8387,7 +8419,7 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
audioEncodingSection = container.NewVBox(
widget.NewLabelWithStyle("═══ AUDIO ENCODING ═══", fyne.TextAlignCenter, fyne.TextStyle{Bold: true}),
widget.NewLabelWithStyle("Audio Codec", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}),
container.NewHBox(audioCodecSelect, audioCodecBadgeContainer),
audioCodecContainer,
widget.NewLabelWithStyle("Audio Bitrate", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}),
audioBitrateSelect,
widget.NewLabelWithStyle("Audio Channels", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}),
@ -8397,7 +8429,7 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
advancedOptions := container.NewVBox(
widget.NewLabelWithStyle("═══ OUTPUT ═══", fyne.TextAlignCenter, fyne.TextStyle{Bold: true}),
widget.NewLabelWithStyle("Format", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}),
container.NewHBox(formatSelect, formatBadgeContainer),
formatContainer,
chapterWarningLabel, // Warning when converting chapters to DVD
preserveChaptersCheck,
dvdAspectBox, // DVD options appear here when DVD format selected