feat(ui): implement state manager pattern, eliminate 3 sync flags
Phase 1 Progress - Convert UI Cleanup (dev23): Architecture Improvements: - Add SetSelectedSilent() method to ColoredSelect to prevent callback loops - Create convertUIState manager with setQuality(), setResolution(), setAspect(), setBitratePreset() - Eliminate syncingQuality flag (quality widgets use state manager) - Eliminate syncingAspect flag and syncAspect() function (aspect widgets use state manager) - Eliminate syncingBitratePreset flag (bitrate preset widgets use state manager) Impact: - Sync flags reduced from 5 to 2 (60% reduction) - Automatic widget synchronization (no manual SetSelected calls) - Single source of truth for UI state - Foundation for eliminating remaining sync flags Remaining: syncingBitrate, syncingTargetSize (text entry debouncing needed) Files modified: - internal/ui/components.go (SetSelectedSilent method) - main.go (state manager, widget callbacks)
This commit is contained in:
parent
61048943c7
commit
24345bc8df
|
|
@ -1070,12 +1070,19 @@ func (cs *ColoredSelect) SetPlaceHolder(text string) {
|
||||||
cs.placeHolder = text
|
cs.placeHolder = text
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetSelected sets the currently selected option
|
// SetSelected sets the currently selected option and triggers onChange callback if tapped by user
|
||||||
func (cs *ColoredSelect) SetSelected(option string) {
|
func (cs *ColoredSelect) SetSelected(option string) {
|
||||||
cs.selected = option
|
cs.selected = option
|
||||||
cs.Refresh()
|
cs.Refresh()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetSelectedSilent sets the currently selected option WITHOUT triggering onChange callback
|
||||||
|
// Use this when synchronizing multiple widgets to avoid callback loops
|
||||||
|
func (cs *ColoredSelect) SetSelectedSilent(option string) {
|
||||||
|
cs.selected = option
|
||||||
|
cs.Refresh()
|
||||||
|
}
|
||||||
|
|
||||||
// UpdateOptions updates the available options and their colors
|
// UpdateOptions updates the available options and their colors
|
||||||
func (cs *ColoredSelect) UpdateOptions(options []string, colorMap map[string]color.Color) {
|
func (cs *ColoredSelect) UpdateOptions(options []string, colorMap map[string]color.Color) {
|
||||||
cs.options = options
|
cs.options = options
|
||||||
|
|
|
||||||
239
main.go
239
main.go
|
|
@ -6519,6 +6519,121 @@ func buildAudioCodecBadge(codecName string) fyne.CanvasObject {
|
||||||
func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
|
func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
|
||||||
convertColor := moduleColor("convert")
|
convertColor := moduleColor("convert")
|
||||||
|
|
||||||
|
// Convert UI State Manager - eliminates sync boolean flags and widget duplication
|
||||||
|
type convertUIState struct {
|
||||||
|
// Quality
|
||||||
|
quality string
|
||||||
|
qualityWidgets []*ui.ColoredSelect
|
||||||
|
onQualityChange []func(string)
|
||||||
|
|
||||||
|
// Resolution
|
||||||
|
resolution string
|
||||||
|
resolutionWidgets []*widget.Select
|
||||||
|
onResolutionChange []func(string)
|
||||||
|
|
||||||
|
// Aspect Ratio
|
||||||
|
aspect string
|
||||||
|
aspectWidgets []*widget.Select
|
||||||
|
onAspectChange []func(string)
|
||||||
|
|
||||||
|
// Bitrate Preset
|
||||||
|
bitratePreset string
|
||||||
|
bitratePresetWidgets []*widget.Select
|
||||||
|
onBitratePresetChange []func(string)
|
||||||
|
|
||||||
|
// Callbacks for state updates
|
||||||
|
updateEncodingControls func()
|
||||||
|
updateAspectBoxVisibility func()
|
||||||
|
buildCommandPreview func()
|
||||||
|
}
|
||||||
|
|
||||||
|
uiState := &convertUIState{
|
||||||
|
quality: state.convert.Quality,
|
||||||
|
resolution: state.convert.TargetResolution,
|
||||||
|
aspect: state.convert.OutputAspect,
|
||||||
|
bitratePreset: state.convert.BitratePreset,
|
||||||
|
}
|
||||||
|
|
||||||
|
// State setters with automatic widget synchronization
|
||||||
|
setQuality := func(val string) {
|
||||||
|
if uiState.quality == val {
|
||||||
|
return // No change
|
||||||
|
}
|
||||||
|
uiState.quality = val
|
||||||
|
state.convert.Quality = val
|
||||||
|
|
||||||
|
// Update all registered widgets silently (no callback loops)
|
||||||
|
for _, w := range uiState.qualityWidgets {
|
||||||
|
w.SetSelectedSilent(val)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trigger callbacks
|
||||||
|
for _, cb := range uiState.onQualityChange {
|
||||||
|
cb(val)
|
||||||
|
}
|
||||||
|
if uiState.updateEncodingControls != nil {
|
||||||
|
uiState.updateEncodingControls()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setResolution := func(val string) {
|
||||||
|
if uiState.resolution == val {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
uiState.resolution = val
|
||||||
|
state.convert.TargetResolution = val
|
||||||
|
|
||||||
|
for _, w := range uiState.resolutionWidgets {
|
||||||
|
w.SetSelected(val)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, cb := range uiState.onResolutionChange {
|
||||||
|
cb(val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setAspect := func(val string, userSet bool) {
|
||||||
|
if val == "" {
|
||||||
|
val = "Source"
|
||||||
|
}
|
||||||
|
if uiState.aspect == val && state.convert.AspectUserSet == userSet {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
uiState.aspect = val
|
||||||
|
state.convert.OutputAspect = val
|
||||||
|
if userSet {
|
||||||
|
state.convert.AspectUserSet = true
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, w := range uiState.aspectWidgets {
|
||||||
|
w.SetSelected(val)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, cb := range uiState.onAspectChange {
|
||||||
|
cb(val)
|
||||||
|
}
|
||||||
|
if uiState.updateAspectBoxVisibility != nil {
|
||||||
|
uiState.updateAspectBoxVisibility()
|
||||||
|
}
|
||||||
|
logging.Debug(logging.CatUI, "target aspect set to %s", val)
|
||||||
|
}
|
||||||
|
|
||||||
|
setBitratePreset := func(val string) {
|
||||||
|
if uiState.bitratePreset == val {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
uiState.bitratePreset = val
|
||||||
|
state.convert.BitratePreset = val
|
||||||
|
|
||||||
|
for _, w := range uiState.bitratePresetWidgets {
|
||||||
|
w.SetSelected(val)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, cb := range uiState.onBitratePresetChange {
|
||||||
|
cb(val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
back := widget.NewButton("< CONVERT", func() {
|
back := widget.NewButton("< CONVERT", func() {
|
||||||
state.showMainMenu()
|
state.showMainMenu()
|
||||||
})
|
})
|
||||||
|
|
@ -6714,41 +6829,18 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
|
||||||
qualityOptions = append(qualityOptions, "Lossless")
|
qualityOptions = append(qualityOptions, "Lossless")
|
||||||
}
|
}
|
||||||
|
|
||||||
var syncingQuality bool
|
// Quality select widgets - use state manager to eliminate sync flags
|
||||||
|
|
||||||
qualitySelectSimple = widget.NewSelect(qualityOptions, func(value string) {
|
qualitySelectSimple = widget.NewSelect(qualityOptions, func(value string) {
|
||||||
if syncingQuality {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
syncingQuality = true
|
|
||||||
logging.Debug(logging.CatUI, "quality preset %s (simple)", value)
|
logging.Debug(logging.CatUI, "quality preset %s (simple)", value)
|
||||||
state.convert.Quality = value
|
setQuality(value)
|
||||||
if qualitySelectAdv != nil {
|
|
||||||
qualitySelectAdv.SetSelected(value)
|
|
||||||
}
|
|
||||||
if updateEncodingControls != nil {
|
|
||||||
updateEncodingControls()
|
|
||||||
}
|
|
||||||
syncingQuality = false
|
|
||||||
if buildCommandPreview != nil {
|
if buildCommandPreview != nil {
|
||||||
buildCommandPreview()
|
buildCommandPreview()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
qualitySelectAdv = widget.NewSelect(qualityOptions, func(value string) {
|
qualitySelectAdv = widget.NewSelect(qualityOptions, func(value string) {
|
||||||
if syncingQuality {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
syncingQuality = true
|
|
||||||
logging.Debug(logging.CatUI, "quality preset %s (advanced)", value)
|
logging.Debug(logging.CatUI, "quality preset %s (advanced)", value)
|
||||||
state.convert.Quality = value
|
setQuality(value)
|
||||||
if qualitySelectSimple != nil {
|
|
||||||
qualitySelectSimple.SetSelected(value)
|
|
||||||
}
|
|
||||||
if updateEncodingControls != nil {
|
|
||||||
updateEncodingControls()
|
|
||||||
}
|
|
||||||
syncingQuality = false
|
|
||||||
if buildCommandPreview != nil {
|
if buildCommandPreview != nil {
|
||||||
buildCommandPreview()
|
buildCommandPreview()
|
||||||
}
|
}
|
||||||
|
|
@ -7037,13 +7129,10 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
|
||||||
var (
|
var (
|
||||||
targetAspectSelect *widget.Select
|
targetAspectSelect *widget.Select
|
||||||
targetAspectSelectSimple *widget.Select
|
targetAspectSelectSimple *widget.Select
|
||||||
syncAspect func(string, bool)
|
|
||||||
syncingAspect bool
|
|
||||||
)
|
)
|
||||||
|
// Aspect select widget - uses state manager to eliminate sync flag
|
||||||
targetAspectSelect = widget.NewSelect(aspectTargets, func(value string) {
|
targetAspectSelect = widget.NewSelect(aspectTargets, func(value string) {
|
||||||
if syncAspect != nil {
|
setAspect(value, true)
|
||||||
syncAspect(value, true)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
if state.convert.OutputAspect == "" {
|
if state.convert.OutputAspect == "" {
|
||||||
state.convert.OutputAspect = "Source"
|
state.convert.OutputAspect = "Source"
|
||||||
|
|
@ -7609,15 +7698,12 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
|
||||||
}
|
}
|
||||||
|
|
||||||
var applyBitratePreset func(string)
|
var applyBitratePreset func(string)
|
||||||
var setBitratePreset func(string)
|
|
||||||
var syncingBitratePreset bool
|
|
||||||
|
|
||||||
|
// Bitrate preset select - uses state manager
|
||||||
bitratePresetSelect = widget.NewSelect(bitratePresetLabels, func(value string) {
|
bitratePresetSelect = widget.NewSelect(bitratePresetLabels, func(value string) {
|
||||||
if syncingBitratePreset {
|
setBitratePreset(value)
|
||||||
return
|
if applyBitratePreset != nil {
|
||||||
}
|
applyBitratePreset(value)
|
||||||
if setBitratePreset != nil {
|
|
||||||
setBitratePreset(value)
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
state.convert.BitratePreset = normalizePresetLabel(state.convert.BitratePreset)
|
state.convert.BitratePreset = normalizePresetLabel(state.convert.BitratePreset)
|
||||||
|
|
@ -7626,13 +7712,11 @@ 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) - uses state manager
|
||||||
simpleBitrateSelect = widget.NewSelect(bitratePresetLabels, func(value string) {
|
simpleBitrateSelect = widget.NewSelect(bitratePresetLabels, func(value string) {
|
||||||
if syncingBitratePreset {
|
setBitratePreset(value)
|
||||||
return
|
if applyBitratePreset != nil {
|
||||||
}
|
applyBitratePreset(value)
|
||||||
if setBitratePreset != nil {
|
|
||||||
setBitratePreset(value)
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
simpleBitrateSelect.SetSelected(state.convert.BitratePreset)
|
simpleBitrateSelect.SetSelected(state.convert.BitratePreset)
|
||||||
|
|
@ -7655,48 +7739,27 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
|
||||||
"2X (relative)", "4X (relative)",
|
"2X (relative)", "4X (relative)",
|
||||||
"NTSC (720×480)", "PAL (720×540)", "PAL (720×576)",
|
"NTSC (720×480)", "PAL (720×540)", "PAL (720×576)",
|
||||||
}
|
}
|
||||||
|
// Resolution select (Simple mode) - uses state manager
|
||||||
resolutionSelectSimple := widget.NewSelect(resolutionOptionsSimple, func(value string) {
|
resolutionSelectSimple := widget.NewSelect(resolutionOptionsSimple, func(value string) {
|
||||||
state.convert.TargetResolution = value
|
|
||||||
logging.Debug(logging.CatUI, "target resolution set to %s (simple)", value)
|
logging.Debug(logging.CatUI, "target resolution set to %s (simple)", value)
|
||||||
|
setResolution(value)
|
||||||
})
|
})
|
||||||
resolutionSelectSimple.SetSelected(state.convert.TargetResolution)
|
resolutionSelectSimple.SetSelected(state.convert.TargetResolution)
|
||||||
|
|
||||||
// Simple aspect selector (separate widget)
|
// Simple aspect selector (separate widget) - uses state manager
|
||||||
targetAspectSelectSimple = widget.NewSelect(aspectTargets, func(value string) {
|
targetAspectSelectSimple = widget.NewSelect(aspectTargets, func(value string) {
|
||||||
if syncAspect != nil {
|
setAspect(value, true)
|
||||||
syncAspect(value, true)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
if state.convert.OutputAspect == "" {
|
if state.convert.OutputAspect == "" {
|
||||||
state.convert.OutputAspect = "Source"
|
state.convert.OutputAspect = "Source"
|
||||||
}
|
}
|
||||||
targetAspectSelectSimple.SetSelected(state.convert.OutputAspect)
|
targetAspectSelectSimple.SetSelected(state.convert.OutputAspect)
|
||||||
|
|
||||||
syncAspect = func(value string, userSet bool) {
|
// Register updateAspectBoxVisibility callback with state manager
|
||||||
if syncingAspect {
|
uiState.updateAspectBoxVisibility = updateAspectBoxVisibility
|
||||||
return
|
|
||||||
}
|
// Initialize aspect state
|
||||||
if value == "" {
|
setAspect(state.convert.OutputAspect, state.convert.AspectUserSet)
|
||||||
value = "Source"
|
|
||||||
}
|
|
||||||
syncingAspect = true
|
|
||||||
state.convert.OutputAspect = value
|
|
||||||
if userSet {
|
|
||||||
state.convert.AspectUserSet = true
|
|
||||||
}
|
|
||||||
if targetAspectSelectSimple != nil {
|
|
||||||
targetAspectSelectSimple.SetSelected(value)
|
|
||||||
}
|
|
||||||
if targetAspectSelect != nil {
|
|
||||||
targetAspectSelect.SetSelected(value)
|
|
||||||
}
|
|
||||||
if updateAspectBoxVisibility != nil {
|
|
||||||
updateAspectBoxVisibility()
|
|
||||||
}
|
|
||||||
logging.Debug(logging.CatUI, "target aspect set to %s", value)
|
|
||||||
syncingAspect = false
|
|
||||||
}
|
|
||||||
syncAspect(state.convert.OutputAspect, state.convert.AspectUserSet)
|
|
||||||
|
|
||||||
// Target File Size with smart presets + manual entry
|
// Target File Size with smart presets + manual entry
|
||||||
targetFileSizeEntry = widget.NewEntry()
|
targetFileSizeEntry = widget.NewEntry()
|
||||||
|
|
@ -7931,23 +7994,7 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setBitratePreset = func(value string) {
|
// Initialize bitrate preset through state manager
|
||||||
if syncingBitratePreset {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
syncingBitratePreset = true
|
|
||||||
state.convert.BitratePreset = value
|
|
||||||
if applyBitratePreset != nil {
|
|
||||||
applyBitratePreset(value)
|
|
||||||
}
|
|
||||||
if bitratePresetSelect != nil {
|
|
||||||
bitratePresetSelect.SetSelected(value)
|
|
||||||
}
|
|
||||||
if simpleBitrateSelect != nil {
|
|
||||||
simpleBitrateSelect.SetSelected(value)
|
|
||||||
}
|
|
||||||
syncingBitratePreset = false
|
|
||||||
}
|
|
||||||
setBitratePreset(state.convert.BitratePreset)
|
setBitratePreset(state.convert.BitratePreset)
|
||||||
|
|
||||||
updateEncodingControls = func() {
|
updateEncodingControls = func() {
|
||||||
|
|
@ -8031,9 +8078,10 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
|
||||||
"2X (relative)", "4X (relative)",
|
"2X (relative)", "4X (relative)",
|
||||||
"NTSC (720×480)", "PAL (720×540)", "PAL (720×576)",
|
"NTSC (720×480)", "PAL (720×540)", "PAL (720×576)",
|
||||||
}
|
}
|
||||||
|
// Resolution select (Advanced mode) - uses state manager
|
||||||
resolutionSelect := widget.NewSelect(resolutionOptions, func(value string) {
|
resolutionSelect := widget.NewSelect(resolutionOptions, func(value string) {
|
||||||
state.convert.TargetResolution = value
|
|
||||||
logging.Debug(logging.CatUI, "target resolution set to %s", value)
|
logging.Debug(logging.CatUI, "target resolution set to %s", value)
|
||||||
|
setResolution(value)
|
||||||
})
|
})
|
||||||
if state.convert.TargetResolution == "" {
|
if state.convert.TargetResolution == "" {
|
||||||
state.convert.TargetResolution = "Source"
|
state.convert.TargetResolution = "Source"
|
||||||
|
|
@ -8398,10 +8446,7 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if remux {
|
if remux {
|
||||||
state.convert.AspectUserSet = false
|
setAspect("Source", false)
|
||||||
if syncAspect != nil {
|
|
||||||
syncAspect("Source", false)
|
|
||||||
}
|
|
||||||
if targetAspectSelectSimple != nil {
|
if targetAspectSelectSimple != nil {
|
||||||
targetAspectSelectSimple.Disable()
|
targetAspectSelectSimple.Disable()
|
||||||
}
|
}
|
||||||
|
|
@ -8582,7 +8627,7 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
|
||||||
frameRateSelect.SetSelected(state.convert.FrameRate)
|
frameRateSelect.SetSelected(state.convert.FrameRate)
|
||||||
updateFrameRateHint()
|
updateFrameRateHint()
|
||||||
motionInterpCheck.SetChecked(state.convert.UseMotionInterpolation)
|
motionInterpCheck.SetChecked(state.convert.UseMotionInterpolation)
|
||||||
syncAspect(state.convert.OutputAspect, false)
|
setAspect(state.convert.OutputAspect, false)
|
||||||
aspectOptions.SetSelected(state.convert.AspectHandling)
|
aspectOptions.SetSelected(state.convert.AspectHandling)
|
||||||
pixelFormatSelect.SetSelected(state.convert.PixelFormat)
|
pixelFormatSelect.SetSelected(state.convert.PixelFormat)
|
||||||
hwAccelSelect.SetSelected(state.convert.HardwareAccel)
|
hwAccelSelect.SetSelected(state.convert.HardwareAccel)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user