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
import (
"fyne.io/fyne/v2/widget"
"sync"
)
import "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 {
mu sync.RWMutex
// Current mode settings
crfMode CRFMode
vbrMode VBRMode
currentQuality string
currentBitrate string
currentCRFValue int64
currentVBRValue int64
// Registered widgets for synchronization
qualityWidgets []*widget.Select
bitrateWidgets []*widget.Select
mu sync.RWMutex
quality string
bitrateMode string
manualQualityOption string
onQualityChange []func(string)
onBitrateModeChange []func(string)
}
type CRFMode string
type VBRMode string
func NewStateManager(quality, bitrateMode, manualQualityOption string) *StateManager {
if manualQualityOption == "" {
manualQualityOption = "Manual (CRF)"
}
return &StateManager{
quality: quality,
bitrateMode: bitrateMode,
manualQualityOption: manualQualityOption,
}
}
const (
CRFManual CRFMode = "manual"
CRFQuality CRFMode = "quality"
CRFBitrate CRFMode = "bitrate"
VBRStandard VBRMode = "standard"
VBRHQ VBRMode = "hq"
VBRConstrained VBRMode = "constrained"
)
func (m *StateManager) Quality() string {
m.mu.RLock()
defer m.mu.RUnlock()
return m.quality
}
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/player"
"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/ui"
"git.leaktechnologies.dev/stu/VideoTools/internal/utils"
@ -1116,35 +1117,35 @@ type appState struct {
sidebarVisible bool
// Author module state
authorFile *videoSource
authorChapters []authorChapter
authorSceneThreshold float64
authorDetecting bool
authorClips []authorClip // Multiple video clips for compilation
authorOutputType string // "dvd" or "iso"
authorRegion string // "NTSC", "PAL", "AUTO"
authorAspectRatio string // "4:3", "16:9", "AUTO"
authorCreateMenu bool // Whether to create DVD menu
authorMenuTemplate string // "Simple", "Dark", "Poster"
authorMenuBackgroundImage string // Path to a user-selected background image
authorTitle string // DVD title
authorSubtitles []string // Subtitle file paths
authorAudioTracks []string // Additional audio tracks
authorSummaryLabel *widget.Label
authorTreatAsChapters bool // Treat multiple clips as chapters
authorChapterSource string // embedded, scenes, clips, manual
authorChaptersRefresh func() // Refresh hook for chapter list UI
authorDiscSize string // "DVD5" or "DVD9"
authorLogText string
authorLogLines []string // Circular buffer for last N lines
authorLogFilePath string // Path to log file for full viewing
authorLogEntry *widget.Entry
authorLogScroll *container.Scroll
authorProgress float64
authorProgressBar *widget.ProgressBar
authorStatusLabel *widget.Label
authorCancelBtn *widget.Button
authorVideoTSPath string
authorFile *videoSource
authorChapters []authorChapter
authorSceneThreshold float64
authorDetecting bool
authorClips []authorClip // Multiple video clips for compilation
authorOutputType string // "dvd" or "iso"
authorRegion string // "NTSC", "PAL", "AUTO"
authorAspectRatio string // "4:3", "16:9", "AUTO"
authorCreateMenu bool // Whether to create DVD menu
authorMenuTemplate string // "Simple", "Dark", "Poster"
authorMenuBackgroundImage string // Path to a user-selected background image
authorTitle string // DVD title
authorSubtitles []string // Subtitle file paths
authorAudioTracks []string // Additional audio tracks
authorSummaryLabel *widget.Label
authorTreatAsChapters bool // Treat multiple clips as chapters
authorChapterSource string // embedded, scenes, clips, manual
authorChaptersRefresh func() // Refresh hook for chapter list UI
authorDiscSize string // "DVD5" or "DVD9"
authorLogText string
authorLogLines []string // Circular buffer for last N lines
authorLogFilePath string // Path to log file for full viewing
authorLogEntry *widget.Entry
authorLogScroll *container.Scroll
authorProgress float64
authorProgressBar *widget.ProgressBar
authorStatusLabel *widget.Label
authorCancelBtn *widget.Button
authorVideoTSPath string
// Rip module state
ripSourcePath string
@ -7035,6 +7036,7 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
_ = registerCallback
manualQualityOption := "Manual (CRF)"
stateMgr := statepkg.NewStateManager(state.convert.Quality, state.convert.BitrateMode, manualQualityOption)
var crfEntry *widget.Entry
var manualCrfRow *fyne.Container
var crfContainer *fyne.Container
@ -7055,7 +7057,7 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
}
// State setters with automatic widget synchronization
setQuality := func(val string) {
applyQuality := func(val string) {
if uiState.quality == val {
return // No change
}
@ -7103,6 +7105,12 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
callCallback("updateEncodingControls")
}
stateMgr.OnQualityChange(applyQuality)
setQuality := func(val string) {
stateMgr.SetQuality(val)
}
setResolution := func(val string) {
if uiState.resolution == val {
return
@ -8093,24 +8101,9 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
"VBR": "VBR (Variable Bitrate)",
"Target Size": "Target Size (Calculate from file size)",
}
bitrateModeSelect = widget.NewSelect(bitrateModeOptions, func(value string) {
// Extract short code from label
if shortCode, ok := bitrateModeMap[value]; ok {
state.convert.BitrateMode = shortCode
} else {
state.convert.BitrateMode = value
}
applyBitrateMode := func(value string) {
state.convert.BitrateMode = normalizeBitrateMode(value)
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 {
updateEncodingControls()
}
@ -8120,6 +8113,15 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
if buildCommandPreview != nil {
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
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)
}
state.convert.BitrateMode = normalizeBitrateMode(state.convert.BitrateMode)
if state.convert.BitrateMode != "CRF" && manualCrfRow != nil {
manualCrfRow.Hide()
}
stateMgr.SetBitrateMode(state.convert.BitrateMode)
// Manual CRF entry
// 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
if preset.Bitrate != "" && state.convert.BitrateMode != "CBR" && state.convert.BitrateMode != "VBR" {
state.convert.BitrateMode = "CBR"
if preset.Bitrate != "" && stateMgr.BitrateMode() != "CBR" && stateMgr.BitrateMode() != "VBR" {
stateMgr.SetBitrateMode("CBR")
if label, ok := reverseMap["CBR"]; ok {
bitrateModeSelect.SetSelected(label)
} else {
@ -11172,7 +11172,8 @@ func (p *playSession) startLocked(offset float64) {
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)
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) {
@ -14818,11 +14819,11 @@ func buildCompareView(state *appState) fyne.CanvasObject {
// Scrollable metadata area for file 1 - use smaller minimum
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
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)
file1Column := container.NewBorder(