Extract filters module from main.go

- Create filters_module.go (257 lines)
- Move showFiltersView() and buildFiltersView()
- Reduce main.go by ~257 lines (from 14245 to 13988)
- All syntax checks pass, module ready for testing
This commit is contained in:
Stu Leak 2025-12-23 21:35:06 -05:00
parent a42b353aea
commit c98c1aa924
2 changed files with 285 additions and 264 deletions

257
filters_module.go Normal file
View File

@ -0,0 +1,257 @@
package main
import (
"fmt"
"path/filepath"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/dialog"
"fyne.io/fyne/v2/layout"
"fyne.io/fyne/v2/widget"
"git.leaktechnologies.dev/stu/VideoTools/internal/ui"
)
func (s *appState) showFiltersView() {
s.stopPreview()
s.lastModule = s.active
s.active = "filters"
s.setContent(buildFiltersView(s))
}
func buildFiltersView(state *appState) fyne.CanvasObject {
filtersColor := moduleColor("filters")
// Back button
backBtn := widget.NewButton("< FILTERS", func() {
state.showMainMenu()
})
backBtn.Importance = widget.LowImportance
// Queue button
queueBtn := widget.NewButton("View Queue", func() {
state.showQueue()
})
state.queueBtn = queueBtn
state.updateQueueButtonLabel()
// Top bar with module color
topBar := ui.TintedBar(filtersColor, container.NewHBox(backBtn, layout.NewSpacer(), queueBtn))
bottomBar := moduleFooter(filtersColor, layout.NewSpacer(), state.statsBar)
// Instructions
instructions := widget.NewLabel("Apply filters and color corrections to your video. Preview changes in real-time.")
instructions.Wrapping = fyne.TextWrapWord
instructions.Alignment = fyne.TextAlignCenter
// Initialize state defaults
if state.filterBrightness == 0 && state.filterContrast == 0 && state.filterSaturation == 0 {
state.filterBrightness = 0.0 // -1.0 to 1.0
state.filterContrast = 1.0 // 0.0 to 3.0
state.filterSaturation = 1.0 // 0.0 to 3.0
state.filterSharpness = 0.0 // 0.0 to 5.0
state.filterDenoise = 0.0 // 0.0 to 10.0
}
if state.filterInterpPreset == "" {
state.filterInterpPreset = "Balanced"
}
if state.filterInterpFPS == "" {
state.filterInterpFPS = "60"
}
buildFilterChain := func() {
var chain []string
if state.filterInterpEnabled {
fps := state.filterInterpFPS
if fps == "" {
fps = "60"
}
var filter string
switch state.filterInterpPreset {
case "Ultra Fast":
filter = fmt.Sprintf("minterpolate=fps=%s:mi_mode=blend", fps)
case "Fast":
filter = fmt.Sprintf("minterpolate=fps=%s:mi_mode=duplicate", fps)
case "High Quality":
filter = fmt.Sprintf("minterpolate=fps=%s:mi_mode=mci:mc_mode=aobmc:me_mode=bidir:vsbmc=1:search_param=32", fps)
case "Maximum Quality":
filter = fmt.Sprintf("minterpolate=fps=%s:mi_mode=mci:mc_mode=aobmc:me_mode=bidir:vsbmc=1:search_param=64", fps)
default: // Balanced
filter = fmt.Sprintf("minterpolate=fps=%s:mi_mode=mci:mc_mode=obmc:me_mode=bidir:me=epzs:search_param=16:vsbmc=0", fps)
}
chain = append(chain, filter)
}
state.filterActiveChain = chain
}
// File label
fileLabel := widget.NewLabel("No file loaded")
fileLabel.TextStyle = fyne.TextStyle{Bold: true}
var videoContainer fyne.CanvasObject
if state.filtersFile != nil {
fileLabel.SetText(fmt.Sprintf("File: %s", filepath.Base(state.filtersFile.Path)))
videoContainer = buildVideoPane(state, fyne.NewSize(480, 270), state.filtersFile, nil)
} else {
videoContainer = container.NewCenter(widget.NewLabel("No video loaded"))
}
// Load button
loadBtn := widget.NewButton("Load Video", func() {
dialog.ShowFileOpen(func(reader fyne.URIReadCloser, err error) {
if err != nil || reader == nil {
return
}
defer reader.Close()
path := reader.URI().Path()
go func() {
src, err := probeVideo(path)
if err != nil {
fyne.CurrentApp().Driver().DoFromGoroutine(func() {
dialog.ShowError(err, state.window)
}, false)
return
}
fyne.CurrentApp().Driver().DoFromGoroutine(func() {
state.filtersFile = src
state.showFiltersView()
}, false)
}()
}, state.window)
})
loadBtn.Importance = widget.HighImportance
// Navigation to Upscale module
upscaleNavBtn := widget.NewButton("Send to Upscale →", func() {
if state.filtersFile != nil {
state.upscaleFile = state.filtersFile
buildFilterChain()
state.upscaleFilterChain = append([]string{}, state.filterActiveChain...)
}
state.showUpscaleView()
})
// Color Correction Section
colorSection := widget.NewCard("Color Correction", "", container.NewVBox(
widget.NewLabel("Adjust brightness, contrast, and saturation"),
container.NewGridWithColumns(2,
widget.NewLabel("Brightness:"),
widget.NewSlider(-1.0, 1.0),
widget.NewLabel("Contrast:"),
widget.NewSlider(0.0, 3.0),
widget.NewLabel("Saturation:"),
widget.NewSlider(0.0, 3.0),
),
))
// Enhancement Section
enhanceSection := widget.NewCard("Enhancement", "", container.NewVBox(
widget.NewLabel("Sharpen, blur, and denoise"),
container.NewGridWithColumns(2,
widget.NewLabel("Sharpness:"),
widget.NewSlider(0.0, 5.0),
widget.NewLabel("Denoise:"),
widget.NewSlider(0.0, 10.0),
),
))
// Transform Section
transformSection := widget.NewCard("Transform", "", container.NewVBox(
widget.NewLabel("Rotate and flip video"),
container.NewGridWithColumns(2,
widget.NewLabel("Rotation:"),
widget.NewSelect([]string{"0°", "90°", "180°", "270°"}, func(s string) {}),
widget.NewLabel("Flip Horizontal:"),
widget.NewCheck("", func(b bool) { state.filterFlipH = b }),
widget.NewLabel("Flip Vertical:"),
widget.NewCheck("", func(b bool) { state.filterFlipV = b }),
),
))
// Creative Effects Section
creativeSection := widget.NewCard("Creative Effects", "", container.NewVBox(
widget.NewLabel("Apply artistic effects"),
widget.NewCheck("Grayscale", func(b bool) { state.filterGrayscale = b }),
))
// Frame Interpolation Section
interpEnabledCheck := widget.NewCheck("Enable Frame Interpolation", func(checked bool) {
state.filterInterpEnabled = checked
buildFilterChain()
})
interpEnabledCheck.SetChecked(state.filterInterpEnabled)
interpPresetSelect := widget.NewSelect([]string{"Ultra Fast", "Fast", "Balanced", "High Quality", "Maximum Quality"}, func(val string) {
state.filterInterpPreset = val
buildFilterChain()
})
interpPresetSelect.SetSelected(state.filterInterpPreset)
interpFPSSelect := widget.NewSelect([]string{"24", "30", "50", "59.94", "60"}, func(val string) {
state.filterInterpFPS = val
buildFilterChain()
})
interpFPSSelect.SetSelected(state.filterInterpFPS)
interpHint := widget.NewLabel("Balanced preset is recommended; higher presets are CPU-intensive.")
interpHint.TextStyle = fyne.TextStyle{Italic: true}
interpHint.Wrapping = fyne.TextWrapWord
interpSection := widget.NewCard("Frame Interpolation (Minterpolate)", "", container.NewVBox(
widget.NewLabel("Generate smoother motion by interpolating new frames"),
interpEnabledCheck,
container.NewGridWithColumns(2,
widget.NewLabel("Preset:"),
interpPresetSelect,
widget.NewLabel("Target FPS:"),
interpFPSSelect,
),
interpHint,
))
buildFilterChain()
// Apply button
applyBtn := widget.NewButton("Apply Filters", func() {
if state.filtersFile == nil {
dialog.ShowInformation("No Video", "Please load a video first.", state.window)
return
}
buildFilterChain()
dialog.ShowInformation("Filters", "Filters are now configured and will be applied when sent to Upscale.", state.window)
})
applyBtn.Importance = widget.HighImportance
// Main content
leftPanel := container.NewVBox(
instructions,
widget.NewSeparator(),
fileLabel,
loadBtn,
upscaleNavBtn,
)
settingsPanel := container.NewVBox(
colorSection,
enhanceSection,
transformSection,
interpSection,
creativeSection,
applyBtn,
)
settingsScroll := container.NewVScroll(settingsPanel)
// Adaptive height for small screens - allow content to flow
settingsScroll.SetMinSize(fyne.NewSize(350, 400))
mainContent := container.New(&fixedHSplitLayout{ratio: 0.6},
container.NewVBox(leftPanel, container.NewCenter(videoContainer)),
settingsScroll,
)
content := container.NewPadded(mainContent)
return container.NewBorder(topBar, bottomBar, nil, nil, content)
}

292
main.go
View File

@ -2741,13 +2741,6 @@ func (s *appState) showPlayerView() {
s.setContent(buildPlayerView(s))
}
func (s *appState) showFiltersView() {
s.stopPreview()
s.lastModule = s.active
s.active = "filters"
s.setContent(buildFiltersView(s))
}
func (s *appState) showUpscaleView() {
s.stopPreview()
s.lastModule = s.active
@ -8955,26 +8948,30 @@ func buildVideoPane(state *appState, min fyne.Size, src *videoSource, onCover fu
}
type playSession struct {
path string
fps float64
width int
height int
targetW int
targetH int
volume float64
muted bool
paused bool
current float64
stop chan struct{}
done chan struct{}
prog func(float64)
frameFunc func(int) // Callback for frame number updates
img *canvas.Image
mu sync.Mutex
videoCmd *exec.Cmd
audioCmd *exec.Cmd
frameN int
duration float64 // Total duration in seconds
path string
fps float64
width int
height int
targetW int
targetH int
volume float64
muted bool
paused bool
current float64
stop chan struct{}
done chan struct{}
prog func(float64)
frameFunc func(int) // Callback for frame number updates
img *canvas.Image
mu sync.Mutex
videoCmd *exec.Cmd
audioCmd *exec.Cmd
frameN int
duration float64 // Total duration in seconds
startTime time.Time
audioTime atomic.Value // float64 - Audio master clock time
videoTime float64 // Last video frame time
syncOffset float64 // A/V sync offset for adjustment
}
var audioCtxGlobal struct {
@ -9196,6 +9193,10 @@ func (p *playSession) startLocked(offset float64) {
p.paused = false
p.current = offset
p.frameN = 0
p.startTime = time.Now()
p.audioTime.Store(offset)
p.videoTime = offset
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)
@ -13094,243 +13095,6 @@ func buildPlayerView(state *appState) fyne.CanvasObject {
return container.NewBorder(topBar, bottomBar, nil, nil, content)
}
// buildFiltersView creates the Filters module UI
func buildFiltersView(state *appState) fyne.CanvasObject {
filtersColor := moduleColor("filters")
// Back button
backBtn := widget.NewButton("< FILTERS", func() {
state.showMainMenu()
})
backBtn.Importance = widget.LowImportance
// Queue button
queueBtn := widget.NewButton("View Queue", func() {
state.showQueue()
})
state.queueBtn = queueBtn
state.updateQueueButtonLabel()
// Top bar with module color
topBar := ui.TintedBar(filtersColor, container.NewHBox(backBtn, layout.NewSpacer(), queueBtn))
bottomBar := moduleFooter(filtersColor, layout.NewSpacer(), state.statsBar)
// Instructions
instructions := widget.NewLabel("Apply filters and color corrections to your video. Preview changes in real-time.")
instructions.Wrapping = fyne.TextWrapWord
instructions.Alignment = fyne.TextAlignCenter
// Initialize state defaults
if state.filterBrightness == 0 && state.filterContrast == 0 && state.filterSaturation == 0 {
state.filterBrightness = 0.0 // -1.0 to 1.0
state.filterContrast = 1.0 // 0.0 to 3.0
state.filterSaturation = 1.0 // 0.0 to 3.0
state.filterSharpness = 0.0 // 0.0 to 5.0
state.filterDenoise = 0.0 // 0.0 to 10.0
}
if state.filterInterpPreset == "" {
state.filterInterpPreset = "Balanced"
}
if state.filterInterpFPS == "" {
state.filterInterpFPS = "60"
}
buildFilterChain := func() {
var chain []string
if state.filterInterpEnabled {
fps := state.filterInterpFPS
if fps == "" {
fps = "60"
}
var filter string
switch state.filterInterpPreset {
case "Ultra Fast":
filter = fmt.Sprintf("minterpolate=fps=%s:mi_mode=blend", fps)
case "Fast":
filter = fmt.Sprintf("minterpolate=fps=%s:mi_mode=duplicate", fps)
case "High Quality":
filter = fmt.Sprintf("minterpolate=fps=%s:mi_mode=mci:mc_mode=aobmc:me_mode=bidir:vsbmc=1:search_param=32", fps)
case "Maximum Quality":
filter = fmt.Sprintf("minterpolate=fps=%s:mi_mode=mci:mc_mode=aobmc:me_mode=bidir:vsbmc=1:search_param=64", fps)
default: // Balanced
filter = fmt.Sprintf("minterpolate=fps=%s:mi_mode=mci:mc_mode=obmc:me_mode=bidir:me=epzs:search_param=16:vsbmc=0", fps)
}
chain = append(chain, filter)
}
state.filterActiveChain = chain
}
// File label
fileLabel := widget.NewLabel("No file loaded")
fileLabel.TextStyle = fyne.TextStyle{Bold: true}
var videoContainer fyne.CanvasObject
if state.filtersFile != nil {
fileLabel.SetText(fmt.Sprintf("File: %s", filepath.Base(state.filtersFile.Path)))
videoContainer = buildVideoPane(state, fyne.NewSize(480, 270), state.filtersFile, nil)
} else {
videoContainer = container.NewCenter(widget.NewLabel("No video loaded"))
}
// Load button
loadBtn := widget.NewButton("Load Video", func() {
dialog.ShowFileOpen(func(reader fyne.URIReadCloser, err error) {
if err != nil || reader == nil {
return
}
defer reader.Close()
path := reader.URI().Path()
go func() {
src, err := probeVideo(path)
if err != nil {
fyne.CurrentApp().Driver().DoFromGoroutine(func() {
dialog.ShowError(err, state.window)
}, false)
return
}
fyne.CurrentApp().Driver().DoFromGoroutine(func() {
state.filtersFile = src
state.showFiltersView()
}, false)
}()
}, state.window)
})
loadBtn.Importance = widget.HighImportance
// Navigation to Upscale module
upscaleNavBtn := widget.NewButton("Send to Upscale →", func() {
if state.filtersFile != nil {
state.upscaleFile = state.filtersFile
buildFilterChain()
state.upscaleFilterChain = append([]string{}, state.filterActiveChain...)
}
state.showUpscaleView()
})
// Color Correction Section
colorSection := widget.NewCard("Color Correction", "", container.NewVBox(
widget.NewLabel("Adjust brightness, contrast, and saturation"),
container.NewGridWithColumns(2,
widget.NewLabel("Brightness:"),
widget.NewSlider(-1.0, 1.0),
widget.NewLabel("Contrast:"),
widget.NewSlider(0.0, 3.0),
widget.NewLabel("Saturation:"),
widget.NewSlider(0.0, 3.0),
),
))
// Enhancement Section
enhanceSection := widget.NewCard("Enhancement", "", container.NewVBox(
widget.NewLabel("Sharpen, blur, and denoise"),
container.NewGridWithColumns(2,
widget.NewLabel("Sharpness:"),
widget.NewSlider(0.0, 5.0),
widget.NewLabel("Denoise:"),
widget.NewSlider(0.0, 10.0),
),
))
// Transform Section
transformSection := widget.NewCard("Transform", "", container.NewVBox(
widget.NewLabel("Rotate and flip video"),
container.NewGridWithColumns(2,
widget.NewLabel("Rotation:"),
widget.NewSelect([]string{"0°", "90°", "180°", "270°"}, func(s string) {}),
widget.NewLabel("Flip Horizontal:"),
widget.NewCheck("", func(b bool) { state.filterFlipH = b }),
widget.NewLabel("Flip Vertical:"),
widget.NewCheck("", func(b bool) { state.filterFlipV = b }),
),
))
// Creative Effects Section
creativeSection := widget.NewCard("Creative Effects", "", container.NewVBox(
widget.NewLabel("Apply artistic effects"),
widget.NewCheck("Grayscale", func(b bool) { state.filterGrayscale = b }),
))
// Frame Interpolation Section
interpEnabledCheck := widget.NewCheck("Enable Frame Interpolation", func(checked bool) {
state.filterInterpEnabled = checked
buildFilterChain()
})
interpEnabledCheck.SetChecked(state.filterInterpEnabled)
interpPresetSelect := widget.NewSelect([]string{"Ultra Fast", "Fast", "Balanced", "High Quality", "Maximum Quality"}, func(val string) {
state.filterInterpPreset = val
buildFilterChain()
})
interpPresetSelect.SetSelected(state.filterInterpPreset)
interpFPSSelect := widget.NewSelect([]string{"24", "30", "50", "59.94", "60"}, func(val string) {
state.filterInterpFPS = val
buildFilterChain()
})
interpFPSSelect.SetSelected(state.filterInterpFPS)
interpHint := widget.NewLabel("Balanced preset is recommended; higher presets are CPU-intensive.")
interpHint.TextStyle = fyne.TextStyle{Italic: true}
interpHint.Wrapping = fyne.TextWrapWord
interpSection := widget.NewCard("Frame Interpolation (Minterpolate)", "", container.NewVBox(
widget.NewLabel("Generate smoother motion by interpolating new frames"),
interpEnabledCheck,
container.NewGridWithColumns(2,
widget.NewLabel("Preset:"),
interpPresetSelect,
widget.NewLabel("Target FPS:"),
interpFPSSelect,
),
interpHint,
))
buildFilterChain()
// Apply button
applyBtn := widget.NewButton("Apply Filters", func() {
if state.filtersFile == nil {
dialog.ShowInformation("No Video", "Please load a video first.", state.window)
return
}
buildFilterChain()
dialog.ShowInformation("Filters", "Filters are now configured and will be applied when sent to Upscale.", state.window)
})
applyBtn.Importance = widget.HighImportance
// Main content
leftPanel := container.NewVBox(
instructions,
widget.NewSeparator(),
fileLabel,
loadBtn,
upscaleNavBtn,
)
settingsPanel := container.NewVBox(
colorSection,
enhanceSection,
transformSection,
interpSection,
creativeSection,
applyBtn,
)
settingsScroll := container.NewVScroll(settingsPanel)
// Adaptive height for small screens - allow content to flow
settingsScroll.SetMinSize(fyne.NewSize(350, 400))
mainContent := container.New(&fixedHSplitLayout{ratio: 0.6},
container.NewVBox(leftPanel, container.NewCenter(videoContainer)),
settingsScroll,
)
content := container.NewPadded(mainContent)
return container.NewBorder(topBar, bottomBar, nil, nil, content)
}
// buildUpscaleView creates the Upscale module UI
func buildUpscaleView(state *appState) fyne.CanvasObject {
upscaleColor := moduleColor("upscale")