diff --git a/filters_module.go b/filters_module.go new file mode 100644 index 0000000..6cc6ce0 --- /dev/null +++ b/filters_module.go @@ -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) +} diff --git a/main.go b/main.go index ccc5051..34dda29 100644 --- a/main.go +++ b/main.go @@ -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")