Wire convert state manager callbacks
This commit is contained in:
parent
222e2f1414
commit
618cfd208e
|
|
@ -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
111
main.go
|
|
@ -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(
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user