Wire convert state manager callbacks

This commit is contained in:
Stu Leak 2026-01-06 17:52:46 -05:00
parent 222e2f1414
commit 618cfd208e
2 changed files with 151 additions and 82 deletions

View File

@ -1,34 +1,102 @@
package state package state
import ( import "sync"
"fyne.io/fyne/v2/widget"
"sync"
)
// StateManager coordinates Convert UI state updates without direct widget coupling.
// Callbacks are registered by UI code to keep widgets in sync.
type StateManager struct { type StateManager struct {
mu sync.RWMutex mu sync.RWMutex
quality string
// Current mode settings bitrateMode string
crfMode CRFMode manualQualityOption string
vbrMode VBRMode onQualityChange []func(string)
currentQuality string onBitrateModeChange []func(string)
currentBitrate string
currentCRFValue int64
currentVBRValue int64
// Registered widgets for synchronization
qualityWidgets []*widget.Select
bitrateWidgets []*widget.Select
} }
type CRFMode string func NewStateManager(quality, bitrateMode, manualQualityOption string) *StateManager {
type VBRMode string if manualQualityOption == "" {
manualQualityOption = "Manual (CRF)"
}
return &StateManager{
quality: quality,
bitrateMode: bitrateMode,
manualQualityOption: manualQualityOption,
}
}
const ( func (m *StateManager) Quality() string {
CRFManual CRFMode = "manual" m.mu.RLock()
CRFQuality CRFMode = "quality" defer m.mu.RUnlock()
CRFBitrate CRFMode = "bitrate" return m.quality
VBRStandard VBRMode = "standard" }
VBRHQ VBRMode = "hq"
VBRConstrained VBRMode = "constrained" func (m *StateManager) BitrateMode() string {
) m.mu.RLock()
defer m.mu.RUnlock()
return m.bitrateMode
}
func (m *StateManager) ManualQualityOption() string {
m.mu.RLock()
defer m.mu.RUnlock()
return m.manualQualityOption
}
func (m *StateManager) SetQuality(val string) bool {
m.mu.Lock()
if m.quality == val {
m.mu.Unlock()
return false
}
m.quality = val
callbacks := append([]func(string){}, m.onQualityChange...)
m.mu.Unlock()
for _, cb := range callbacks {
cb(val)
}
return true
}
func (m *StateManager) SetBitrateMode(val string) bool {
m.mu.Lock()
if m.bitrateMode == val {
m.mu.Unlock()
return false
}
m.bitrateMode = val
callbacks := append([]func(string){}, m.onBitrateModeChange...)
m.mu.Unlock()
for _, cb := range callbacks {
cb(val)
}
return true
}
func (m *StateManager) OnQualityChange(fn func(string)) {
if fn == nil {
return
}
m.mu.Lock()
m.onQualityChange = append(m.onQualityChange, fn)
m.mu.Unlock()
}
func (m *StateManager) OnBitrateModeChange(fn func(string)) {
if fn == nil {
return
}
m.mu.Lock()
m.onBitrateModeChange = append(m.onBitrateModeChange, fn)
m.mu.Unlock()
}
func (m *StateManager) IsManualQuality(val string) bool {
m.mu.RLock()
defer m.mu.RUnlock()
if val != "" {
return val == m.manualQualityOption
}
return m.quality == m.manualQualityOption
}

111
main.go
View File

@ -44,6 +44,7 @@ import (
"git.leaktechnologies.dev/stu/VideoTools/internal/modules" "git.leaktechnologies.dev/stu/VideoTools/internal/modules"
"git.leaktechnologies.dev/stu/VideoTools/internal/player" "git.leaktechnologies.dev/stu/VideoTools/internal/player"
"git.leaktechnologies.dev/stu/VideoTools/internal/queue" "git.leaktechnologies.dev/stu/VideoTools/internal/queue"
statepkg "git.leaktechnologies.dev/stu/VideoTools/internal/state"
"git.leaktechnologies.dev/stu/VideoTools/internal/sysinfo" "git.leaktechnologies.dev/stu/VideoTools/internal/sysinfo"
"git.leaktechnologies.dev/stu/VideoTools/internal/ui" "git.leaktechnologies.dev/stu/VideoTools/internal/ui"
"git.leaktechnologies.dev/stu/VideoTools/internal/utils" "git.leaktechnologies.dev/stu/VideoTools/internal/utils"
@ -1116,35 +1117,35 @@ type appState struct {
sidebarVisible bool sidebarVisible bool
// Author module state // Author module state
authorFile *videoSource authorFile *videoSource
authorChapters []authorChapter authorChapters []authorChapter
authorSceneThreshold float64 authorSceneThreshold float64
authorDetecting bool authorDetecting bool
authorClips []authorClip // Multiple video clips for compilation authorClips []authorClip // Multiple video clips for compilation
authorOutputType string // "dvd" or "iso" authorOutputType string // "dvd" or "iso"
authorRegion string // "NTSC", "PAL", "AUTO" authorRegion string // "NTSC", "PAL", "AUTO"
authorAspectRatio string // "4:3", "16:9", "AUTO" authorAspectRatio string // "4:3", "16:9", "AUTO"
authorCreateMenu bool // Whether to create DVD menu authorCreateMenu bool // Whether to create DVD menu
authorMenuTemplate string // "Simple", "Dark", "Poster" authorMenuTemplate string // "Simple", "Dark", "Poster"
authorMenuBackgroundImage string // Path to a user-selected background image authorMenuBackgroundImage string // Path to a user-selected background image
authorTitle string // DVD title authorTitle string // DVD title
authorSubtitles []string // Subtitle file paths authorSubtitles []string // Subtitle file paths
authorAudioTracks []string // Additional audio tracks authorAudioTracks []string // Additional audio tracks
authorSummaryLabel *widget.Label authorSummaryLabel *widget.Label
authorTreatAsChapters bool // Treat multiple clips as chapters authorTreatAsChapters bool // Treat multiple clips as chapters
authorChapterSource string // embedded, scenes, clips, manual authorChapterSource string // embedded, scenes, clips, manual
authorChaptersRefresh func() // Refresh hook for chapter list UI authorChaptersRefresh func() // Refresh hook for chapter list UI
authorDiscSize string // "DVD5" or "DVD9" authorDiscSize string // "DVD5" or "DVD9"
authorLogText string authorLogText string
authorLogLines []string // Circular buffer for last N lines authorLogLines []string // Circular buffer for last N lines
authorLogFilePath string // Path to log file for full viewing authorLogFilePath string // Path to log file for full viewing
authorLogEntry *widget.Entry authorLogEntry *widget.Entry
authorLogScroll *container.Scroll authorLogScroll *container.Scroll
authorProgress float64 authorProgress float64
authorProgressBar *widget.ProgressBar authorProgressBar *widget.ProgressBar
authorStatusLabel *widget.Label authorStatusLabel *widget.Label
authorCancelBtn *widget.Button authorCancelBtn *widget.Button
authorVideoTSPath string authorVideoTSPath string
// Rip module state // Rip module state
ripSourcePath string ripSourcePath string
@ -7035,6 +7036,7 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
_ = registerCallback _ = registerCallback
manualQualityOption := "Manual (CRF)" manualQualityOption := "Manual (CRF)"
stateMgr := statepkg.NewStateManager(state.convert.Quality, state.convert.BitrateMode, manualQualityOption)
var crfEntry *widget.Entry var crfEntry *widget.Entry
var manualCrfRow *fyne.Container var manualCrfRow *fyne.Container
var crfContainer *fyne.Container var crfContainer *fyne.Container
@ -7055,7 +7057,7 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
} }
// State setters with automatic widget synchronization // State setters with automatic widget synchronization
setQuality := func(val string) { applyQuality := func(val string) {
if uiState.quality == val { if uiState.quality == val {
return // No change return // No change
} }
@ -7103,6 +7105,12 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
callCallback("updateEncodingControls") callCallback("updateEncodingControls")
} }
stateMgr.OnQualityChange(applyQuality)
setQuality := func(val string) {
stateMgr.SetQuality(val)
}
setResolution := func(val string) { setResolution := func(val string) {
if uiState.resolution == val { if uiState.resolution == val {
return return
@ -8093,24 +8101,9 @@ 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) { applyBitrateMode := func(value string) {
// Extract short code from label state.convert.BitrateMode = normalizeBitrateMode(value)
if shortCode, ok := bitrateModeMap[value]; ok {
state.convert.BitrateMode = shortCode
} else {
state.convert.BitrateMode = value
}
logging.Debug(logging.CatUI, "bitrate mode set to %s", state.convert.BitrateMode) logging.Debug(logging.CatUI, "bitrate mode set to %s", state.convert.BitrateMode)
if state.convert.BitrateMode == "CRF" && state.convert.Quality == manualQualityOption {
if crfEntry != nil {
crfEntry.Enable()
}
if manualCrfRow != nil {
manualCrfRow.Show()
}
} else if manualCrfRow != nil {
manualCrfRow.Hide()
}
if updateEncodingControls != nil { if updateEncodingControls != nil {
updateEncodingControls() updateEncodingControls()
} }
@ -8120,6 +8113,15 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
if buildCommandPreview != nil { if buildCommandPreview != nil {
buildCommandPreview() buildCommandPreview()
} }
}
stateMgr.OnBitrateModeChange(applyBitrateMode)
bitrateModeSelect = widget.NewSelect(bitrateModeOptions, func(value string) {
// Extract short code from label
if shortCode, ok := bitrateModeMap[value]; ok {
stateMgr.SetBitrateMode(shortCode)
} else {
stateMgr.SetBitrateMode(value)
}
}) })
// 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 {
@ -8128,9 +8130,7 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
bitrateModeSelect.SetSelected(state.convert.BitrateMode) bitrateModeSelect.SetSelected(state.convert.BitrateMode)
} }
state.convert.BitrateMode = normalizeBitrateMode(state.convert.BitrateMode) state.convert.BitrateMode = normalizeBitrateMode(state.convert.BitrateMode)
if state.convert.BitrateMode != "CRF" && manualCrfRow != nil { stateMgr.SetBitrateMode(state.convert.BitrateMode)
manualCrfRow.Hide()
}
// Manual CRF entry // Manual CRF entry
// CRF entry with debouncing (300ms delay) and validation // CRF entry with debouncing (300ms delay) and validation
@ -8654,8 +8654,8 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
} }
// Move to CBR for predictable output when a preset is chosen // Move to CBR for predictable output when a preset is chosen
if preset.Bitrate != "" && state.convert.BitrateMode != "CBR" && state.convert.BitrateMode != "VBR" { if preset.Bitrate != "" && stateMgr.BitrateMode() != "CBR" && stateMgr.BitrateMode() != "VBR" {
state.convert.BitrateMode = "CBR" stateMgr.SetBitrateMode("CBR")
if label, ok := reverseMap["CBR"]; ok { if label, ok := reverseMap["CBR"]; ok {
bitrateModeSelect.SetSelected(label) bitrateModeSelect.SetSelected(label)
} else { } else {
@ -11172,7 +11172,8 @@ func (p *playSession) startLocked(offset float64) {
p.syncOffset = 0 p.syncOffset = 0
logging.Debug(logging.CatFFMPEG, "playSession start path=%s offset=%.3f fps=%.3f target=%dx%d", p.path, offset, p.fps, p.targetW, p.targetH) logging.Debug(logging.CatFFMPEG, "playSession start path=%s offset=%.3f fps=%.3f target=%dx%d", p.path, offset, p.fps, p.targetW, p.targetH)
p.runVideo(offset) p.runVideo(offset)
p.runAudio(offset) // TEMPORARY: Disable audio to prevent A/V sync crashes
// p.runAudio(offset) will be re-enabled when UnifiedPlayer is properly integrated
} }
func (p *playSession) runVideo(offset float64) { func (p *playSession) runVideo(offset float64) {
@ -14818,11 +14819,11 @@ func buildCompareView(state *appState) fyne.CanvasObject {
// Scrollable metadata area for file 1 - use smaller minimum // Scrollable metadata area for file 1 - use smaller minimum
file1InfoScroll := container.NewVScroll(file1Info) file1InfoScroll := container.NewVScroll(file1Info)
// Avoid rigid min sizes so window snapping works across modules. // Avoid rigid min sizes so window snapping works across modules.
// Scrollable metadata area for file 2 - use smaller minimum // Scrollable metadata area for file 2 - use smaller minimum
file2InfoScroll := container.NewVScroll(file2Info) file2InfoScroll := container.NewVScroll(file2Info)
// Avoid rigid min sizes so window snapping works across modules. // Avoid rigid min sizes so window snapping works across modules.
// File 1 column: header, video player, metadata (using Border to make metadata expand) // File 1 column: header, video player, metadata (using Border to make metadata expand)
file1Column := container.NewBorder( file1Column := container.NewBorder(