Compare commits
No commits in common. "c98c1aa92484005db4032bbc8ac60dfecdd6fef1" and "c883a921558294ab3cc32ce1163c9b6eb69fc9ad" have entirely different histories.
c98c1aa924
...
c883a92155
|
|
@ -1,411 +0,0 @@
|
|||
# Main.go Modularization - Incremental Refactoring Prompt
|
||||
|
||||
## Current State
|
||||
|
||||
**Problem:** main.go is 14,116 lines (420KB) causing:
|
||||
- 5+ minute builds on Windows
|
||||
- Poor code maintainability
|
||||
- Difficult to navigate and modify
|
||||
|
||||
**Goal:** Extract modules into separate files following established patterns
|
||||
|
||||
**Already Extracted:**
|
||||
- ✅ `author_module.go` (1,421 lines) - Pattern reference
|
||||
- ✅ `subtitles_module.go` (756 lines) - Pattern reference
|
||||
- ✅ `inspect_module.go` (292 lines) - Pattern reference
|
||||
|
||||
**Remaining:** 9 modules to extract (~10,000 lines)
|
||||
|
||||
---
|
||||
|
||||
## Extraction Order (Least Important → Most Important)
|
||||
|
||||
Work through these **one module at a time**, testing after each extraction:
|
||||
|
||||
### Phase 1: Simple Modules (Start Here)
|
||||
|
||||
1. **filters_module.go** (~236 lines)
|
||||
- Lines: ~13261-13497
|
||||
- Functions: `showFiltersView()`, `buildFiltersView()`
|
||||
- State fields: `filtersFile`, `filterBrightness`, `filterContrast`, etc.
|
||||
- Why first: Simple UI-only, no job execution
|
||||
|
||||
2. **upscale_module.go** (~889 lines + job executor)
|
||||
- Lines: ~13498-14387 + job executor at ~4568-5023
|
||||
- Functions: `showUpscaleView()`, `buildUpscaleView()`, `executeUpscaleJob()`, AI helpers
|
||||
- State fields: All `upscale*` fields (~25 fields)
|
||||
- Includes: AI backend detection, model helpers
|
||||
|
||||
3. **thumb_module.go** (~335 lines + job executor)
|
||||
- Lines: ~12837-13172 + executeThumbJob at ~4222-4264
|
||||
- Functions: `showThumbView()`, `buildThumbView()`, `executeThumbJob()`
|
||||
- State fields: `thumbFile`, `thumbCount`, `thumbWidth`, etc.
|
||||
|
||||
### Phase 2: Moderate Complexity
|
||||
|
||||
4. **player_module.go** (~87 lines + playSession ~450 lines)
|
||||
- Lines: ~13173-13260 + playSession at ~8929-9329
|
||||
- Functions: `showPlayerView()`, `buildPlayerView()`, `stopPlayer()`, entire `playSession` type
|
||||
- State fields: `playerFile`, `player`, `playerReady`, `playerVolume`, etc.
|
||||
- Types: `playSession`, `playerSurface`, `playSession` methods
|
||||
- Note: Includes recent performance improvements
|
||||
|
||||
5. **compare_module.go** (~503 lines + fullscreen)
|
||||
- Lines: ~12068-12571 + fullscreen at ~14277-14387
|
||||
- Functions: `showCompareView()`, `buildCompareView()`, `buildCompareFullscreenView()`, `showCompareFullscreen()`
|
||||
- State fields: `compareFile1`, `compareFile2`, `autoCompare`
|
||||
|
||||
### Phase 3: Complex Infrastructure
|
||||
|
||||
6. **merge_module.go** (~374 lines + job executor)
|
||||
- Lines: ~2752-3126 + executeMergeJob at ~3232-3564
|
||||
- Functions: `showMergeView()` (inline UI builder), `addMergeToQueue()`, `executeMergeJob()`
|
||||
- State fields: `mergeClips`, `mergeFormat`, `mergeOutput`, etc.
|
||||
- Types: `mergeClip` struct
|
||||
- Note: UI builder is inline, needs extraction to `buildMergeView()`
|
||||
|
||||
7. **benchmark_module.go** (~326 lines)
|
||||
- Lines: ~1938-2264 + config persistence at ~700-729
|
||||
- Functions: `showBenchmark()`, `showBenchmarkHistory()`, benchmark helpers
|
||||
- Types: `benchmarkConfig`, `benchmarkRun`
|
||||
|
||||
8. **queue_module.go** (~scattered functions)
|
||||
- Functions: `showQueue()`, `refreshQueueView()`, `jobExecutor()`, `buildFFmpegCommandFromJob()`
|
||||
- Lines: Various, needs gathering
|
||||
|
||||
### Phase 4: Most Critical (Do Last)
|
||||
|
||||
9. **convert_module.go** (~6,384 lines - THE BIG ONE)
|
||||
- Lines: ~5683-12067
|
||||
- Functions: 50+ functions including `buildConvertView()`, `showConvertView()`, all convert helpers
|
||||
- State fields: All `convert*` fields, `source`, `loadedVideos`, etc.
|
||||
- Types: `convertConfig`, `formatOption`
|
||||
- Shared components: `buildMetadataPanel()`, `buildVideoPane()` (used by other modules)
|
||||
- May need to split into multiple files if > 3000 lines
|
||||
|
||||
---
|
||||
|
||||
## Instructions for Each Module Extraction
|
||||
|
||||
### Step-by-Step Process
|
||||
|
||||
For each module (do ONE at a time):
|
||||
|
||||
#### 1. Create the Module File
|
||||
|
||||
**File:** `{module}_module.go` (e.g., `filters_module.go`)
|
||||
|
||||
**Template:**
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
// Copy imports from main.go that this module needs
|
||||
"fyne.io/fyne/v2"
|
||||
"fyne.io/fyne/v2/container"
|
||||
// ... etc
|
||||
)
|
||||
|
||||
// Move the show{Module}View() method here
|
||||
func (s *appState) show{Module}View() {
|
||||
s.stopPreview()
|
||||
s.lastModule = s.active
|
||||
s.active = "{module}"
|
||||
s.setContent(build{Module}View(s))
|
||||
}
|
||||
|
||||
// Move the build{Module}View() function here
|
||||
func build{Module}View(state *appState) fyne.CanvasObject {
|
||||
// ... entire function body ...
|
||||
}
|
||||
|
||||
// Move any helper functions specific to this module
|
||||
// Move any job executor functions (execute{Module}Job)
|
||||
```
|
||||
|
||||
#### 2. Remove from main.go
|
||||
|
||||
Delete the exact same functions you just moved. Use line numbers as guide.
|
||||
|
||||
**IMPORTANT:** Do NOT remove:
|
||||
- Shared functions used by multiple modules (e.g., `moduleColor()`, `probeVideo()`)
|
||||
- State fields in `appState` struct (leave all state in main.go)
|
||||
- Type definitions used across modules
|
||||
|
||||
#### 3. Build and Test
|
||||
|
||||
```bash
|
||||
# Must succeed
|
||||
go build -o VideoTools .
|
||||
|
||||
# Check line count reduction
|
||||
wc -l main.go {module}_module.go
|
||||
|
||||
# Test the module works
|
||||
./VideoTools # Navigate to the module and test basic functionality
|
||||
```
|
||||
|
||||
#### 4. Commit
|
||||
|
||||
```bash
|
||||
git add {module}_module.go main.go
|
||||
git commit -m "Extract {module} module from main.go
|
||||
|
||||
- Create {module}_module.go (XXX lines)
|
||||
- Move show{Module}View() and build{Module}View()
|
||||
- Reduce main.go by XXX lines
|
||||
- All tests pass, module functions correctly
|
||||
"
|
||||
```
|
||||
|
||||
#### 5. Move to Next Module
|
||||
|
||||
Repeat the process for the next module in the order above.
|
||||
|
||||
---
|
||||
|
||||
## Reference Patterns
|
||||
|
||||
Look at these existing extracted modules for the pattern:
|
||||
|
||||
### Pattern 1: Simple Module (inspect_module.go)
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
// Necessary imports
|
||||
)
|
||||
|
||||
func (s *appState) showInspectView() {
|
||||
s.stopPreview()
|
||||
s.lastModule = s.active
|
||||
s.active = "inspect"
|
||||
s.setContent(buildInspectView(s))
|
||||
}
|
||||
|
||||
func buildInspectView(state *appState) fyne.CanvasObject {
|
||||
// UI building code
|
||||
}
|
||||
```
|
||||
|
||||
### Pattern 2: Module with Job Executor (subtitles_module.go)
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
// Necessary imports
|
||||
)
|
||||
|
||||
func (s *appState) showSubtitlesView() {
|
||||
// ...
|
||||
}
|
||||
|
||||
func buildSubtitlesView(state *appState) fyne.CanvasObject {
|
||||
// ...
|
||||
}
|
||||
|
||||
func (s *appState) executeSubtitlesJob(ctx context.Context, job *queue.Job, progressCallback func(float64)) error {
|
||||
// Job execution logic
|
||||
}
|
||||
|
||||
// Helper functions specific to subtitles
|
||||
```
|
||||
|
||||
### Pattern 3: Module with Types (author_module.go)
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
// ...
|
||||
)
|
||||
|
||||
// Types specific to this module
|
||||
type authorClip struct {
|
||||
// ...
|
||||
}
|
||||
|
||||
func (s *appState) showAuthorView() {
|
||||
// ...
|
||||
}
|
||||
|
||||
// Many helper functions specific to author module
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## What Stays in main.go
|
||||
|
||||
**DO NOT MOVE these to module files:**
|
||||
|
||||
### Core Application
|
||||
- `main()` function
|
||||
- `runGUI()` function
|
||||
- `type appState struct` definition (all fields stay)
|
||||
- Window setup and initialization
|
||||
|
||||
### Shared Utilities (Used by 3+ modules)
|
||||
- `moduleColor()` - Color scheme helper
|
||||
- `moduleFooter()` - Footer bar builder
|
||||
- `statusStrip()` - Status bar
|
||||
- `probeVideo()` - Video file analysis (CRITICAL - used everywhere)
|
||||
- `buildMetadataPanel()` - Used by convert and other modules
|
||||
- `buildVideoPane()` - Video preview pane (used by many modules)
|
||||
- `formatBitrate()`, `formatClock()`, etc. - Format helpers
|
||||
|
||||
### Navigation & State
|
||||
- `showMainMenu()` - Main menu builder
|
||||
- `setContent()` - Content switcher
|
||||
- History sidebar code
|
||||
- Stats bar updates
|
||||
- Drop handlers
|
||||
|
||||
### Shared Types
|
||||
- `videoSource` type and methods
|
||||
- Shared constants and enums
|
||||
|
||||
---
|
||||
|
||||
## Special Cases & Gotchas
|
||||
|
||||
### Convert Module (Do Last!)
|
||||
|
||||
The convert module is massive and may need splitting:
|
||||
|
||||
**Option A: Single File** (if < 3000 lines after extraction)
|
||||
- `convert_module.go`
|
||||
|
||||
**Option B: Split into Multiple Files** (if > 3000 lines)
|
||||
- `convert_module.go` - Main UI and show function
|
||||
- `convert_config.go` - Configuration types and persistence
|
||||
- `convert_job.go` - Job execution logic
|
||||
- `convert_helpers.go` - Codec/format helpers
|
||||
|
||||
**Shared Components:**
|
||||
- `buildMetadataPanel()` and `buildVideoPane()` might need to move to `shared_ui.go` if used by many modules
|
||||
|
||||
### Merge Module Inline UI
|
||||
|
||||
The merge module has UI building inline in `showMergeView()`. You'll need to:
|
||||
|
||||
1. Extract inline UI to `buildMergeView()` function
|
||||
2. Then follow standard pattern
|
||||
|
||||
### Player Module with playSession
|
||||
|
||||
The `playSession` type and all its methods should move to `player_module.go` together.
|
||||
|
||||
### Import Adjustments
|
||||
|
||||
Some modules might need internal package imports that aren't in main.go:
|
||||
- Check for missing imports after extraction
|
||||
- Run `go build` to catch import errors
|
||||
- Add necessary imports to the module file
|
||||
|
||||
---
|
||||
|
||||
## Success Criteria for Each Extraction
|
||||
|
||||
Before moving to next module, verify:
|
||||
|
||||
- ✅ `go build` succeeds with no errors
|
||||
- ✅ Module file has clear structure (show function, build function, helpers)
|
||||
- ✅ main.go reduced by expected lines
|
||||
- ✅ Module works when tested in GUI
|
||||
- ✅ No duplicate code between main.go and module file
|
||||
- ✅ Git commit created with clear message
|
||||
- ✅ Can navigate to module and use basic features
|
||||
|
||||
---
|
||||
|
||||
## Tracking Progress
|
||||
|
||||
After each extraction, update this checklist:
|
||||
|
||||
- [ ] 1. filters_module.go (~236 lines)
|
||||
- [ ] 2. upscale_module.go (~1200 lines)
|
||||
- [ ] 3. thumb_module.go (~400 lines)
|
||||
- [ ] 4. player_module.go (~800 lines)
|
||||
- [ ] 5. compare_module.go (~550 lines)
|
||||
- [ ] 6. merge_module.go (~700 lines)
|
||||
- [ ] 7. benchmark_module.go (~400 lines)
|
||||
- [ ] 8. queue_module.go (~500 lines)
|
||||
- [ ] 9. convert_module.go (~6400 lines - may split)
|
||||
|
||||
**Target:** main.go from 14,116 lines → ~4,000 lines
|
||||
|
||||
---
|
||||
|
||||
## Expected Timeline
|
||||
|
||||
- **Simple modules (1-3):** 20-30 min each
|
||||
- **Moderate modules (4-5):** 30-45 min each
|
||||
- **Complex modules (6-8):** 45-60 min each
|
||||
- **Convert module (9):** 2-3 hours (largest, most critical)
|
||||
|
||||
**Total:** ~8-12 hours of incremental work
|
||||
|
||||
---
|
||||
|
||||
## Final Build Performance Goal
|
||||
|
||||
After all extractions:
|
||||
|
||||
**Before:**
|
||||
- main.go: 14,329 lines (426KB)
|
||||
- Build time (Windows): 5+ minutes
|
||||
|
||||
**After:**
|
||||
- main.go: ~4,000 lines (~120KB)
|
||||
- Module files: 10-12 files (~800-1500 lines each)
|
||||
- Build time (Windows): < 2 minutes (hopefully < 1 minute)
|
||||
|
||||
---
|
||||
|
||||
## Questions to Ask If Stuck
|
||||
|
||||
1. **"Does this function use state from multiple modules?"**
|
||||
- Yes → Keep in main.go
|
||||
- No → Move to module file
|
||||
|
||||
2. **"Is this function called by 3+ different modules?"**
|
||||
- Yes → Keep in main.go as shared utility
|
||||
- No → Move to the module that uses it
|
||||
|
||||
3. **"Is this a type used across modules?"**
|
||||
- Yes → Keep type definition in main.go
|
||||
- No → Move to module file
|
||||
|
||||
4. **"Where are the line numbers for this module?"**
|
||||
- Check this document's module listing
|
||||
- Use grep to find function: `grep -n "func buildXView" main.go`
|
||||
|
||||
---
|
||||
|
||||
## Emergency Rollback
|
||||
|
||||
If an extraction breaks the build:
|
||||
|
||||
```bash
|
||||
# Restore main.go from backup
|
||||
git checkout HEAD -- main.go
|
||||
|
||||
# Remove the broken module file
|
||||
rm {module}_module.go
|
||||
|
||||
# Try again with more care
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Start Here
|
||||
|
||||
**First module to extract:** `filters_module.go`
|
||||
|
||||
1. Find lines ~13261-13497 in main.go
|
||||
2. Create `filters_module.go`
|
||||
3. Copy `showFiltersView()` and `buildFiltersView()`
|
||||
4. Remove from main.go
|
||||
5. Build, test, commit
|
||||
6. Move to next module
|
||||
|
||||
Good luck! Take it one module at a time, test after each extraction, and the convert module will be much easier once you've established the pattern with the simpler modules.
|
||||
|
|
@ -1,257 +0,0 @@
|
|||
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)
|
||||
}
|
||||
|
|
@ -761,12 +761,6 @@ func BuildModuleBadge(jobType queue.JobType) fyne.CanvasObject {
|
|||
case queue.JobTypeSnippet:
|
||||
badgeColor = utils.MustHex("#4AE2E2")
|
||||
badgeText = "SNIPPET"
|
||||
case queue.JobTypeAuthor:
|
||||
badgeColor = utils.MustHex("#FFAA44")
|
||||
badgeText = "AUTHOR"
|
||||
case queue.JobTypeRip:
|
||||
badgeColor = utils.MustHex("#FF9944")
|
||||
badgeText = "RIP"
|
||||
default:
|
||||
badgeColor = utils.MustHex("#808080")
|
||||
badgeText = "OTHER"
|
||||
|
|
|
|||
|
|
@ -398,10 +398,6 @@ func ModuleColor(t queue.JobType) color.Color {
|
|||
return color.RGBA{R: 255, G: 215, B: 68, A: 255} // Yellow (#FFD744)
|
||||
case queue.JobTypeThumb:
|
||||
return color.RGBA{R: 255, G: 136, B: 68, A: 255} // Orange (#FF8844)
|
||||
case queue.JobTypeAuthor:
|
||||
return color.RGBA{R: 255, G: 170, B: 68, A: 255} // Orange (#FFAA44)
|
||||
case queue.JobTypeRip:
|
||||
return color.RGBA{R: 255, G: 153, B: 68, A: 255} // Orange (#FF9944)
|
||||
default:
|
||||
return color.Gray{Y: 180}
|
||||
}
|
||||
|
|
|
|||
292
main.go
292
main.go
|
|
@ -2741,6 +2741,13 @@ 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
|
||||
|
|
@ -8948,30 +8955,26 @@ 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
|
||||
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
|
||||
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
|
||||
}
|
||||
|
||||
var audioCtxGlobal struct {
|
||||
|
|
@ -9193,10 +9196,6 @@ 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)
|
||||
|
|
@ -13095,6 +13094,243 @@ 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")
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user