Compare commits
13 Commits
d24fd7c281
...
cdf8b10769
| Author | SHA1 | Date | |
|---|---|---|---|
| cdf8b10769 | |||
| 685707e8d1 | |||
| 0ef618df55 | |||
| d20dcde5bb | |||
| 0da96bc743 | |||
| c1ccb38062 | |||
| c62b7867fd | |||
| c6feb239b9 | |||
| 4c43a13f9c | |||
| 67b838e9ad | |||
| 2dae75dd8e | |||
| 406709bec6 | |||
| 9af3ca0c1a |
47
DONE.md
47
DONE.md
|
|
@ -54,6 +54,47 @@ This file tracks completed features, fixes, and milestones.
|
|||
- Searches both current directory and executable directory
|
||||
- Added debug logging for icon loading troubleshooting
|
||||
|
||||
- ✅ **UI Scaling for 800x600 Windows** (2025-12-20 continuation)
|
||||
- Reduced module tile size from 220x110 to 150x65
|
||||
- Reduced title text size from 28 to 18
|
||||
- Reduced queue tile from 160x60 to 120x40
|
||||
- Reduced section padding from 14 to 4 pixels
|
||||
- Reduced category labels to 12px
|
||||
- Removed extra padding wrapper around tiles
|
||||
- Removed scrolling requirement - everything fits without scrolling
|
||||
- All UI elements fit within 800x600 default window
|
||||
|
||||
- ✅ **Header Layout Improvements** (2025-12-20 continuation)
|
||||
- Changed from HBox with spacer to border layout
|
||||
- Title on left, all controls grouped compactly on right
|
||||
- Shortened button labels for space efficiency
|
||||
- "☰ History" → "☰", "Run Benchmark" → "Benchmark", "View Results" → "Results"
|
||||
- Eliminates wasted horizontal space
|
||||
|
||||
- ✅ **Queue Clear Behavior Fix** (2025-12-20 continuation)
|
||||
- "Clear Completed" now always returns to main menu
|
||||
- "Clear All" now always returns to main menu
|
||||
- Prevents unwanted navigation to convert module after clearing queue
|
||||
- Consistent and predictable behavior
|
||||
|
||||
- ✅ **Threading Safety Fix** (2025-12-20 continuation)
|
||||
- Fixed Fyne threading errors in stats bar component
|
||||
- Removed Show()/Hide() calls from Layout() method
|
||||
- Layout() can be called from any thread during resize/redraw
|
||||
- Show/Hide logic remains only in Refresh() with proper DoFromGoroutine
|
||||
- Eliminates threading warnings during UI updates
|
||||
|
||||
- ✅ **Preset UX Improvements** (2025-12-20 continuation)
|
||||
- Moved "Manual" option to bottom of all preset dropdowns
|
||||
- Bitrate preset default: "2.5 Mbps - Medium Quality"
|
||||
- Target size preset default: "100MB"
|
||||
- Manual input fields hidden by default
|
||||
- Manual fields appear only when "Manual" is selected
|
||||
- Encourages preset usage while maintaining advanced control
|
||||
- Reversed encoding preset order: veryslow first, ultrafast last
|
||||
- Better quality options now appear at top of list
|
||||
- Applied consistently to both simple and advanced modes
|
||||
|
||||
### Features (2025-12-18 Session)
|
||||
- ✅ **History Sidebar Enhancements**
|
||||
- Delete button ("×") on each history entry
|
||||
|
|
@ -773,6 +814,12 @@ This file tracks completed features, fixes, and milestones.
|
|||
- ✅ Benchmark errors now show non-blocking notifications instead of OK popups
|
||||
- ✅ Fixed stats bar updates to run on the UI thread to avoid Fyne warnings
|
||||
- ✅ Defaulted Target Aspect Ratio back to Source unless user explicitly sets it
|
||||
- ✅ Synced Target Aspect Ratio between Simple and Advanced menus
|
||||
- ✅ Hide manual CRF input when Lossless quality is selected
|
||||
- ✅ Upscale now recomputes target dimensions from the preset to ensure 2X/4X apply
|
||||
- ✅ Added unit selector for manual video bitrate entry
|
||||
- ✅ Reset now restores full default convert settings even with no config file
|
||||
- ✅ Reset now forces resolution and frame rate back to Source
|
||||
- ✅ Stabilized video seeking and embedded rendering
|
||||
- ✅ Improved player window positioning
|
||||
- ✅ Fixed clear video functionality
|
||||
|
|
|
|||
6
TODO.md
6
TODO.md
|
|
@ -47,6 +47,12 @@ This file tracks upcoming features, improvements, and known issues.
|
|||
- Non-blocking benchmark error notifications
|
||||
- Stats bar updates run on the UI thread
|
||||
- Target aspect default enforced as Source unless user changes it
|
||||
- Target aspect sync across simple/advanced menus
|
||||
- Hide manual CRF entry when Lossless quality is active
|
||||
- Upscale target dimensions recomputed from preset for 2X/4X reliability
|
||||
- Manual video bitrate uses a unit selector (KB/MB/GB)
|
||||
- Reset restores full default convert settings
|
||||
- Reset forces resolution/frame rate back to Source
|
||||
|
||||
## Priority Features for dev20+
|
||||
|
||||
|
|
|
|||
|
|
@ -173,7 +173,7 @@ func (r *moduleTileRenderer) Layout(size fyne.Size) {
|
|||
}
|
||||
|
||||
func (r *moduleTileRenderer) MinSize() fyne.Size {
|
||||
return fyne.NewSize(220, 110)
|
||||
return fyne.NewSize(150, 65)
|
||||
}
|
||||
|
||||
func (r *moduleTileRenderer) Refresh() {
|
||||
|
|
|
|||
|
|
@ -47,14 +47,14 @@ type HistoryEntry struct {
|
|||
func BuildMainMenu(modules []ModuleInfo, onModuleClick func(string), onModuleDrop func(string, []fyne.URI), onQueueClick func(), onLogsClick func(), onBenchmarkClick func(), onBenchmarkHistoryClick func(), onToggleSidebar func(), sidebarVisible bool, sidebar fyne.CanvasObject, titleColor, queueColor, textColor color.Color, queueCompleted, queueTotal int, hasBenchmark bool) fyne.CanvasObject {
|
||||
title := canvas.NewText("VIDEOTOOLS", titleColor)
|
||||
title.TextStyle = fyne.TextStyle{Monospace: true, Bold: true}
|
||||
title.TextSize = 28
|
||||
title.TextSize = 18
|
||||
|
||||
queueTile := buildQueueTile(queueCompleted, queueTotal, queueColor, textColor, onQueueClick)
|
||||
|
||||
sidebarToggleBtn := widget.NewButton("☰ History", onToggleSidebar)
|
||||
sidebarToggleBtn := widget.NewButton("☰", onToggleSidebar)
|
||||
sidebarToggleBtn.Importance = widget.LowImportance
|
||||
|
||||
benchmarkBtn := widget.NewButton("Run Benchmark", onBenchmarkClick)
|
||||
benchmarkBtn := widget.NewButton("Benchmark", onBenchmarkClick)
|
||||
// Highlight the benchmark button if no benchmark has been run
|
||||
if !hasBenchmark {
|
||||
benchmarkBtn.Importance = widget.HighImportance
|
||||
|
|
@ -62,13 +62,19 @@ func BuildMainMenu(modules []ModuleInfo, onModuleClick func(string), onModuleDro
|
|||
benchmarkBtn.Importance = widget.LowImportance
|
||||
}
|
||||
|
||||
viewResultsBtn := widget.NewButton("View Results", onBenchmarkHistoryClick)
|
||||
viewResultsBtn := widget.NewButton("Results", onBenchmarkHistoryClick)
|
||||
viewResultsBtn.Importance = widget.LowImportance
|
||||
|
||||
logsBtn := widget.NewButton("Logs", onLogsClick)
|
||||
logsBtn.Importance = widget.LowImportance
|
||||
|
||||
header := container.NewHBox(title, layout.NewSpacer(), sidebarToggleBtn, benchmarkBtn, viewResultsBtn, logsBtn, queueTile)
|
||||
// Compact header - title on left, controls on right
|
||||
header := container.NewBorder(
|
||||
nil, nil,
|
||||
title,
|
||||
container.NewHBox(sidebarToggleBtn, logsBtn, benchmarkBtn, viewResultsBtn, queueTile),
|
||||
nil,
|
||||
)
|
||||
|
||||
categorized := map[string][]fyne.CanvasObject{}
|
||||
for i := range modules {
|
||||
|
|
@ -97,15 +103,19 @@ func BuildMainMenu(modules []ModuleInfo, onModuleClick func(string), onModuleDro
|
|||
|
||||
var sections []fyne.CanvasObject
|
||||
for _, cat := range sortedKeys(categorized) {
|
||||
catLabel := canvas.NewText(cat, textColor)
|
||||
catLabel.TextSize = 12
|
||||
catLabel.TextStyle = fyne.TextStyle{Bold: true}
|
||||
sections = append(sections,
|
||||
canvas.NewText(cat, textColor),
|
||||
catLabel,
|
||||
container.NewGridWithColumns(3, categorized[cat]...),
|
||||
)
|
||||
}
|
||||
|
||||
padding := canvas.NewRectangle(color.Transparent)
|
||||
padding.SetMinSize(fyne.NewSize(0, 14))
|
||||
padding.SetMinSize(fyne.NewSize(0, 4))
|
||||
|
||||
// Compact body without scrolling
|
||||
body := container.NewVBox(
|
||||
header,
|
||||
padding,
|
||||
|
|
@ -125,19 +135,19 @@ func BuildMainMenu(modules []ModuleInfo, onModuleClick func(string), onModuleDro
|
|||
// buildModuleTile creates a single module tile
|
||||
func buildModuleTile(mod ModuleInfo, tapped func(), dropped func([]fyne.URI)) fyne.CanvasObject {
|
||||
logging.Debug(logging.CatUI, "building tile %s color=%v enabled=%v", mod.ID, mod.Color, mod.Enabled)
|
||||
return container.NewPadded(NewModuleTile(mod.Label, mod.Color, mod.Enabled, tapped, dropped))
|
||||
return NewModuleTile(mod.Label, mod.Color, mod.Enabled, tapped, dropped)
|
||||
}
|
||||
|
||||
// buildQueueTile creates the queue status tile
|
||||
func buildQueueTile(completed, total int, queueColor, textColor color.Color, onClick func()) fyne.CanvasObject {
|
||||
rect := canvas.NewRectangle(queueColor)
|
||||
rect.CornerRadius = 8
|
||||
rect.SetMinSize(fyne.NewSize(160, 60))
|
||||
rect.CornerRadius = 6
|
||||
rect.SetMinSize(fyne.NewSize(120, 40))
|
||||
|
||||
text := canvas.NewText(fmt.Sprintf("QUEUE: %d/%d", completed, total), textColor)
|
||||
text.Alignment = fyne.TextAlignCenter
|
||||
text.TextStyle = fyne.TextStyle{Monospace: true, Bold: true}
|
||||
text.TextSize = 18
|
||||
text.TextSize = 14
|
||||
|
||||
tile := container.NewMax(rect, container.NewCenter(text))
|
||||
|
||||
|
|
|
|||
492
main.go
492
main.go
|
|
@ -505,6 +505,56 @@ func (c convertConfig) CoverLabel() string {
|
|||
return filepath.Base(c.CoverArtPath)
|
||||
}
|
||||
|
||||
func defaultConvertConfig() convertConfig {
|
||||
return convertConfig{
|
||||
SelectedFormat: formatOptions[0],
|
||||
OutputBase: "converted",
|
||||
Quality: "Standard (CRF 23)",
|
||||
Mode: "Simple",
|
||||
UseAutoNaming: false,
|
||||
AutoNameTemplate: "<actress> - <studio> - <scene>",
|
||||
|
||||
VideoCodec: "H.264",
|
||||
EncoderPreset: "medium",
|
||||
CRF: "",
|
||||
BitrateMode: "CRF",
|
||||
BitratePreset: "Manual",
|
||||
VideoBitrate: "5000k",
|
||||
TargetFileSize: "",
|
||||
TargetResolution: "Source",
|
||||
FrameRate: "Source",
|
||||
UseMotionInterpolation: false,
|
||||
PixelFormat: "yuv420p",
|
||||
HardwareAccel: "auto",
|
||||
TwoPass: false,
|
||||
H264Profile: "main",
|
||||
H264Level: "4.0",
|
||||
Deinterlace: "Auto",
|
||||
DeinterlaceMethod: "bwdif",
|
||||
AutoCrop: false,
|
||||
CropWidth: "",
|
||||
CropHeight: "",
|
||||
CropX: "",
|
||||
CropY: "",
|
||||
FlipHorizontal: false,
|
||||
FlipVertical: false,
|
||||
Rotation: "0",
|
||||
|
||||
AudioCodec: "AAC",
|
||||
AudioBitrate: "192k",
|
||||
AudioChannels: "Source",
|
||||
AudioSampleRate: "Source",
|
||||
NormalizeAudio: false,
|
||||
|
||||
InverseTelecine: true,
|
||||
InverseAutoNotes: "Default smoothing for interlaced footage.",
|
||||
CoverArtPath: "",
|
||||
AspectHandling: "Auto",
|
||||
OutputAspect: "Source",
|
||||
AspectUserSet: false,
|
||||
}
|
||||
}
|
||||
|
||||
// defaultConvertConfigPath returns the path to the persisted convert config.
|
||||
func defaultConvertConfigPath() string {
|
||||
configDir, err := os.UserConfigDir()
|
||||
|
|
@ -1548,13 +1598,9 @@ func (s *appState) refreshQueueView() {
|
|||
s.jobQueue.Clear()
|
||||
s.clearVideo()
|
||||
|
||||
// If queue is now empty, return to previous module
|
||||
// Always return to main menu after clearing
|
||||
if len(s.jobQueue.List()) == 0 {
|
||||
if s.lastModule != "" && s.lastModule != "queue" {
|
||||
s.showModule(s.lastModule)
|
||||
} else {
|
||||
s.showMainMenu()
|
||||
}
|
||||
s.showMainMenu()
|
||||
} else {
|
||||
s.refreshQueueView() // Refresh if jobs remain
|
||||
}
|
||||
|
|
@ -1562,12 +1608,8 @@ func (s *appState) refreshQueueView() {
|
|||
func() { // onClearAll
|
||||
s.jobQueue.ClearAll()
|
||||
s.clearVideo()
|
||||
// Return to previous module or main menu
|
||||
if s.lastModule != "" && s.lastModule != "queue" {
|
||||
s.showModule(s.lastModule)
|
||||
} else {
|
||||
s.showMainMenu()
|
||||
}
|
||||
// Always return to main menu after clearing all
|
||||
s.showMainMenu()
|
||||
},
|
||||
func(id string) { // onCopyError
|
||||
job, err := s.jobQueue.Get(id)
|
||||
|
|
@ -4330,6 +4372,9 @@ func (s *appState) executeUpscaleJob(ctx context.Context, job *queue.Job, progre
|
|||
method := cfg["method"].(string)
|
||||
targetWidth := int(cfg["targetWidth"].(float64))
|
||||
targetHeight := int(cfg["targetHeight"].(float64))
|
||||
targetPreset, _ := cfg["targetPreset"].(string)
|
||||
sourceWidth := int(toFloat(cfg["sourceWidth"]))
|
||||
sourceHeight := int(toFloat(cfg["sourceHeight"]))
|
||||
preserveAR := true
|
||||
if v, ok := cfg["preserveAR"].(bool); ok {
|
||||
preserveAR = v
|
||||
|
|
@ -4343,6 +4388,21 @@ func (s *appState) executeUpscaleJob(ctx context.Context, job *queue.Job, progre
|
|||
progressCallback(0)
|
||||
}
|
||||
|
||||
// Recompute target dimensions from preset to avoid stale values
|
||||
if targetPreset != "" && targetPreset != "Custom" {
|
||||
if sourceWidth <= 0 || sourceHeight <= 0 {
|
||||
if src, err := probeVideo(inputPath); err == nil && src != nil {
|
||||
sourceWidth = src.Width
|
||||
sourceHeight = src.Height
|
||||
}
|
||||
}
|
||||
if w, h, keepAR, err := parseResolutionPreset(targetPreset, sourceWidth, sourceHeight); err == nil {
|
||||
targetWidth = w
|
||||
targetHeight = h
|
||||
preserveAR = keepAR
|
||||
}
|
||||
}
|
||||
|
||||
// Build filter chain
|
||||
var filters []string
|
||||
|
||||
|
|
@ -4359,6 +4419,7 @@ func (s *appState) executeUpscaleJob(ctx context.Context, job *queue.Job, progre
|
|||
|
||||
// Add scale filter (preserve aspect by default)
|
||||
scaleFilter := buildUpscaleFilter(targetWidth, targetHeight, method, preserveAR)
|
||||
logging.Debug(logging.CatFFMPEG, "upscale: target=%dx%d preserveAR=%v method=%s filter=%s", targetWidth, targetHeight, preserveAR, method, scaleFilter)
|
||||
filters = append(filters, scaleFilter)
|
||||
|
||||
// Add frame rate conversion if requested
|
||||
|
|
@ -4854,47 +4915,8 @@ func runGUI() {
|
|||
logging.Debug(logging.CatUI, "window initialized at 800x600 (compact default), manual resizing enabled")
|
||||
|
||||
state := &appState{
|
||||
window: w,
|
||||
convert: convertConfig{
|
||||
OutputBase: "converted",
|
||||
SelectedFormat: formatOptions[0],
|
||||
Quality: "Standard (CRF 23)",
|
||||
Mode: "Simple",
|
||||
UseAutoNaming: false,
|
||||
AutoNameTemplate: "<actress> - <studio> - <scene>",
|
||||
|
||||
// Video encoding defaults
|
||||
VideoCodec: "H.264",
|
||||
EncoderPreset: "medium",
|
||||
CRF: "", // Empty means use Quality preset
|
||||
BitrateMode: "CRF",
|
||||
BitratePreset: "Manual",
|
||||
VideoBitrate: "5000k",
|
||||
TargetResolution: "Source",
|
||||
FrameRate: "Source",
|
||||
PixelFormat: "yuv420p",
|
||||
HardwareAccel: "auto",
|
||||
TwoPass: false,
|
||||
H264Profile: "main",
|
||||
H264Level: "4.0",
|
||||
Deinterlace: "Auto",
|
||||
DeinterlaceMethod: "bwdif",
|
||||
AutoCrop: false,
|
||||
|
||||
// Audio encoding defaults
|
||||
AudioCodec: "AAC",
|
||||
AudioBitrate: "192k",
|
||||
AudioChannels: "Source",
|
||||
AudioSampleRate: "Source",
|
||||
NormalizeAudio: false,
|
||||
|
||||
// Other defaults
|
||||
InverseTelecine: true,
|
||||
InverseAutoNotes: "Default smoothing for interlaced footage.",
|
||||
OutputAspect: "Source",
|
||||
AspectHandling: "Auto",
|
||||
AspectUserSet: false,
|
||||
},
|
||||
window: w,
|
||||
convert: defaultConvertConfig(),
|
||||
mergeChapters: true,
|
||||
player: player.New(),
|
||||
playerVolume: 100,
|
||||
|
|
@ -5249,6 +5271,7 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
|
|||
bitratePresetSelect *widget.Select
|
||||
crfEntry *widget.Entry
|
||||
videoBitrateEntry *widget.Entry
|
||||
manualBitrateRow *fyne.Container
|
||||
targetFileSizeSelect *widget.Select
|
||||
targetFileSizeEntry *widget.Entry
|
||||
qualitySelectSimple *widget.Select
|
||||
|
|
@ -5259,6 +5282,7 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
|
|||
crfContainer *fyne.Container
|
||||
bitrateContainer *fyne.Container
|
||||
targetSizeContainer *fyne.Container
|
||||
resetConvertDefaults func()
|
||||
)
|
||||
var (
|
||||
updateEncodingControls func()
|
||||
|
|
@ -5582,10 +5606,16 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
|
|||
transformHint.Wrapping = fyne.TextWrapWord
|
||||
|
||||
aspectTargets := []string{"Source", "16:9", "4:3", "5:4", "5:3", "1:1", "9:16", "21:9"}
|
||||
targetAspectSelect := widget.NewSelect(aspectTargets, func(value string) {
|
||||
logging.Debug(logging.CatUI, "target aspect set to %s", value)
|
||||
state.convert.OutputAspect = value
|
||||
state.convert.AspectUserSet = true
|
||||
var (
|
||||
targetAspectSelect *widget.Select
|
||||
targetAspectSelectSimple *widget.Select
|
||||
syncAspect func(string, bool)
|
||||
syncingAspect bool
|
||||
)
|
||||
targetAspectSelect = widget.NewSelect(aspectTargets, func(value string) {
|
||||
if syncAspect != nil {
|
||||
syncAspect(value, true)
|
||||
}
|
||||
})
|
||||
if state.convert.OutputAspect == "" {
|
||||
state.convert.OutputAspect = "Source"
|
||||
|
|
@ -5624,11 +5654,7 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
|
|||
}
|
||||
}
|
||||
updateAspectBoxVisibility()
|
||||
targetAspectSelect.OnChanged = func(value string) {
|
||||
logging.Debug(logging.CatUI, "target aspect set to %s", value)
|
||||
state.convert.OutputAspect = value
|
||||
updateAspectBoxVisibility()
|
||||
}
|
||||
|
||||
aspectOptions.OnChanged = func(value string) {
|
||||
logging.Debug(logging.CatUI, "aspect handling set to %s", value)
|
||||
state.convert.AspectHandling = value
|
||||
|
|
@ -5753,7 +5779,7 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
|
|||
encoderPresetHint.SetText(hint)
|
||||
}
|
||||
|
||||
encoderPresetSelect := widget.NewSelect([]string{"ultrafast", "superfast", "veryfast", "faster", "fast", "medium", "slow", "slower", "veryslow"}, func(value string) {
|
||||
encoderPresetSelect := widget.NewSelect([]string{"veryslow", "slower", "slow", "medium", "fast", "faster", "veryfast", "superfast", "ultrafast"}, func(value string) {
|
||||
state.convert.EncoderPreset = value
|
||||
logging.Debug(logging.CatUI, "encoder preset set to %s", value)
|
||||
updateEncoderPresetHint(value)
|
||||
|
|
@ -5765,7 +5791,7 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
|
|||
updateEncoderPresetHint(state.convert.EncoderPreset)
|
||||
|
||||
// Simple mode preset dropdown
|
||||
simplePresetSelect := widget.NewSelect([]string{"ultrafast", "superfast", "veryfast", "faster", "fast", "medium", "slow", "slower", "veryslow"}, func(value string) {
|
||||
simplePresetSelect := widget.NewSelect([]string{"veryslow", "slower", "slow", "medium", "fast", "faster", "veryfast", "superfast", "ultrafast"}, func(value string) {
|
||||
state.convert.EncoderPreset = value
|
||||
logging.Debug(logging.CatUI, "simple preset set to %s", value)
|
||||
updateEncoderPresetHint(value)
|
||||
|
|
@ -5780,50 +5806,8 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
|
|||
settingsInfoLabel.Alignment = fyne.TextAlignCenter
|
||||
|
||||
resetSettingsBtn := widget.NewButton("Reset to Defaults", func() {
|
||||
state.convert = convertConfig{
|
||||
SelectedFormat: formatOptions[0],
|
||||
OutputBase: "converted",
|
||||
Quality: "Standard (CRF 23)",
|
||||
InverseTelecine: false,
|
||||
OutputAspect: "Source",
|
||||
AspectHandling: "Auto",
|
||||
AspectUserSet: false,
|
||||
VideoCodec: "H.264",
|
||||
EncoderPreset: "medium",
|
||||
BitrateMode: "CRF",
|
||||
BitratePreset: "Manual",
|
||||
CRF: "",
|
||||
VideoBitrate: "",
|
||||
TargetResolution: "Source",
|
||||
FrameRate: "Source",
|
||||
PixelFormat: "yuv420p",
|
||||
HardwareAccel: "auto",
|
||||
AudioCodec: "AAC",
|
||||
AudioBitrate: "192k",
|
||||
AudioChannels: "Source",
|
||||
UseAutoNaming: false,
|
||||
AutoNameTemplate: "<actress> - <studio> - <scene>",
|
||||
}
|
||||
logging.Debug(logging.CatUI, "settings reset to defaults")
|
||||
formatSelect.SetSelected(state.convert.SelectedFormat.Label)
|
||||
videoCodecSelect.SetSelected(state.convert.VideoCodec)
|
||||
qualitySelectSimple.SetSelected(state.convert.Quality)
|
||||
qualitySelectAdv.SetSelected(state.convert.Quality)
|
||||
simplePresetSelect.SetSelected(state.convert.EncoderPreset)
|
||||
bitrateModeSelect.SetSelected(state.convert.BitrateMode)
|
||||
bitratePresetSelect.SetSelected(state.convert.BitratePreset)
|
||||
crfEntry.SetText(state.convert.CRF)
|
||||
videoBitrateEntry.SetText(state.convert.VideoBitrate)
|
||||
targetFileSizeSelect.SetSelected("Manual")
|
||||
targetFileSizeEntry.SetText(state.convert.TargetFileSize)
|
||||
autoNameCheck.SetChecked(state.convert.UseAutoNaming)
|
||||
autoNameTemplate.SetText(state.convert.AutoNameTemplate)
|
||||
outputEntry.SetText(state.convert.OutputBase)
|
||||
if updateEncodingControls != nil {
|
||||
updateEncodingControls()
|
||||
}
|
||||
if updateQualityVisibility != nil {
|
||||
updateQualityVisibility()
|
||||
if resetConvertDefaults != nil {
|
||||
resetConvertDefaults()
|
||||
}
|
||||
})
|
||||
resetSettingsBtn.Importance = widget.LowImportance
|
||||
|
|
@ -5908,15 +5892,109 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
|
|||
|
||||
// Video Bitrate entry (for CBR/VBR)
|
||||
videoBitrateEntry = widget.NewEntry()
|
||||
videoBitrateEntry.SetPlaceHolder("5000k")
|
||||
videoBitrateEntry.SetText(state.convert.VideoBitrate)
|
||||
videoBitrateEntry.OnChanged = func(val string) {
|
||||
state.convert.VideoBitrate = val
|
||||
videoBitrateEntry.SetPlaceHolder("5000")
|
||||
videoBitrateUnitSelect := widget.NewSelect([]string{"Kbps", "Mbps", "Gbps"}, func(value string) {})
|
||||
videoBitrateUnitSelect.SetSelected("Kbps")
|
||||
manualBitrateInput := container.NewBorder(nil, nil, nil, videoBitrateUnitSelect, videoBitrateEntry)
|
||||
|
||||
parseBitrateParts := func(input string) (string, string, bool) {
|
||||
trimmed := strings.TrimSpace(input)
|
||||
if trimmed == "" {
|
||||
return "", "", false
|
||||
}
|
||||
upper := strings.ToUpper(trimmed)
|
||||
var num float64
|
||||
var unit string
|
||||
if _, err := fmt.Sscanf(upper, "%f%s", &num, &unit); err != nil {
|
||||
return "", "", false
|
||||
}
|
||||
numStr := strconv.FormatFloat(num, 'f', -1, 64)
|
||||
switch unit {
|
||||
case "K", "KBPS":
|
||||
unit = "Kbps"
|
||||
case "M", "MBPS":
|
||||
unit = "Mbps"
|
||||
case "G", "GBPS":
|
||||
unit = "Gbps"
|
||||
}
|
||||
return numStr, unit, true
|
||||
}
|
||||
|
||||
normalizeBitrateUnit := func(label string) string {
|
||||
switch label {
|
||||
case "Kbps":
|
||||
return "k"
|
||||
case "Mbps":
|
||||
return "M"
|
||||
case "Gbps":
|
||||
return "G"
|
||||
default:
|
||||
return "k"
|
||||
}
|
||||
}
|
||||
|
||||
var syncingBitrate bool
|
||||
updateBitrateState := func() {
|
||||
if syncingBitrate {
|
||||
return
|
||||
}
|
||||
val := strings.TrimSpace(videoBitrateEntry.Text)
|
||||
if val == "" {
|
||||
state.convert.VideoBitrate = ""
|
||||
return
|
||||
}
|
||||
if num, unit, ok := parseBitrateParts(val); ok && unit != "" {
|
||||
if num != val {
|
||||
videoBitrateEntry.SetText(num)
|
||||
return
|
||||
}
|
||||
if unit != videoBitrateUnitSelect.Selected {
|
||||
videoBitrateUnitSelect.SetSelected(unit)
|
||||
return
|
||||
}
|
||||
val = num
|
||||
}
|
||||
unit := normalizeBitrateUnit(videoBitrateUnitSelect.Selected)
|
||||
state.convert.VideoBitrate = val + unit
|
||||
if buildCommandPreview != nil {
|
||||
buildCommandPreview()
|
||||
}
|
||||
}
|
||||
|
||||
setManualBitrate := func(value string) {
|
||||
syncingBitrate = true
|
||||
defer func() { syncingBitrate = false }()
|
||||
|
||||
if value == "" {
|
||||
videoBitrateEntry.SetText("")
|
||||
return
|
||||
}
|
||||
if num, unit, ok := parseBitrateParts(value); ok {
|
||||
videoBitrateEntry.SetText(num)
|
||||
if unit != "" {
|
||||
videoBitrateUnitSelect.SetSelected(unit)
|
||||
}
|
||||
} else {
|
||||
videoBitrateEntry.SetText(value)
|
||||
}
|
||||
state.convert.VideoBitrate = value
|
||||
}
|
||||
|
||||
videoBitrateUnitSelect.OnChanged = func(value string) {
|
||||
if manualBitrateRow != nil && manualBitrateRow.Hidden {
|
||||
return
|
||||
}
|
||||
updateBitrateState()
|
||||
}
|
||||
|
||||
videoBitrateEntry.OnChanged = func(val string) {
|
||||
updateBitrateState()
|
||||
}
|
||||
|
||||
if state.convert.VideoBitrate != "" {
|
||||
setManualBitrate(state.convert.VideoBitrate)
|
||||
}
|
||||
|
||||
// Create CRF container (crfEntry already initialized)
|
||||
crfContainer = container.NewVBox(
|
||||
widget.NewLabelWithStyle("Manual CRF (overrides Quality preset)", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}),
|
||||
|
|
@ -5932,12 +6010,12 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
|
|||
}
|
||||
|
||||
presets := []bitratePreset{
|
||||
{Label: "Manual", Bitrate: "", Codec: ""},
|
||||
{Label: "1.5 Mbps - Low Quality", Bitrate: "1500k", Codec: ""},
|
||||
{Label: "2.5 Mbps - Medium Quality", Bitrate: "2500k", Codec: ""},
|
||||
{Label: "4.0 Mbps - Good Quality", Bitrate: "4000k", Codec: ""},
|
||||
{Label: "6.0 Mbps - High Quality", Bitrate: "6000k", Codec: ""},
|
||||
{Label: "8.0 Mbps - Very High Quality", Bitrate: "8000k", Codec: ""},
|
||||
{Label: "Manual", Bitrate: "", Codec: ""},
|
||||
}
|
||||
|
||||
bitratePresetLookup := make(map[string]bitratePreset)
|
||||
|
|
@ -5955,7 +6033,7 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
|
|||
}
|
||||
})
|
||||
if state.convert.BitratePreset == "" || bitratePresetLookup[state.convert.BitratePreset].Label == "" {
|
||||
state.convert.BitratePreset = "Manual"
|
||||
state.convert.BitratePreset = "2.5 Mbps - Medium Quality"
|
||||
}
|
||||
bitratePresetSelect.SetSelected(state.convert.BitratePreset)
|
||||
|
||||
|
|
@ -5968,12 +6046,16 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
|
|||
})
|
||||
simpleBitrateSelect.SetSelected(state.convert.BitratePreset)
|
||||
|
||||
// Manual bitrate row (hidden by default)
|
||||
manualBitrateLabel := widget.NewLabelWithStyle("Manual Bitrate", fyne.TextAlignLeading, fyne.TextStyle{Bold: true})
|
||||
manualBitrateRow = container.NewVBox(manualBitrateLabel, manualBitrateInput)
|
||||
manualBitrateRow.Hide()
|
||||
|
||||
// Create bitrate container now that bitratePresetSelect is initialized
|
||||
bitrateContainer = container.NewVBox(
|
||||
widget.NewLabelWithStyle("Video Bitrate (for CBR/VBR)", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}),
|
||||
videoBitrateEntry,
|
||||
widget.NewLabelWithStyle("Recommended Bitrate Preset", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}),
|
||||
widget.NewLabelWithStyle("Bitrate Preset", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}),
|
||||
bitratePresetSelect,
|
||||
manualBitrateRow,
|
||||
)
|
||||
|
||||
// Simple resolution selector (separate widget to avoid double-parent issues)
|
||||
|
|
@ -5988,23 +6070,49 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
|
|||
resolutionSelectSimple.SetSelected(state.convert.TargetResolution)
|
||||
|
||||
// Simple aspect selector (separate widget)
|
||||
targetAspectSelectSimple := widget.NewSelect(aspectTargets, func(value string) {
|
||||
logging.Debug(logging.CatUI, "target aspect set to %s (simple)", value)
|
||||
state.convert.OutputAspect = value
|
||||
state.convert.AspectUserSet = true
|
||||
updateAspectBoxVisibility()
|
||||
targetAspectSelectSimple = widget.NewSelect(aspectTargets, func(value string) {
|
||||
if syncAspect != nil {
|
||||
syncAspect(value, true)
|
||||
}
|
||||
})
|
||||
if state.convert.OutputAspect == "" {
|
||||
state.convert.OutputAspect = "Source"
|
||||
}
|
||||
targetAspectSelectSimple.SetSelected(state.convert.OutputAspect)
|
||||
|
||||
syncAspect = func(value string, userSet bool) {
|
||||
if syncingAspect {
|
||||
return
|
||||
}
|
||||
if value == "" {
|
||||
value = "Source"
|
||||
}
|
||||
syncingAspect = true
|
||||
state.convert.OutputAspect = value
|
||||
if userSet {
|
||||
state.convert.AspectUserSet = true
|
||||
}
|
||||
if targetAspectSelectSimple != nil {
|
||||
targetAspectSelectSimple.SetSelected(value)
|
||||
}
|
||||
if targetAspectSelect != nil {
|
||||
targetAspectSelect.SetSelected(value)
|
||||
}
|
||||
if updateAspectBoxVisibility != nil {
|
||||
updateAspectBoxVisibility()
|
||||
}
|
||||
logging.Debug(logging.CatUI, "target aspect set to %s", value)
|
||||
syncingAspect = false
|
||||
}
|
||||
syncAspect(state.convert.OutputAspect, state.convert.AspectUserSet)
|
||||
|
||||
// Target File Size with smart presets + manual entry
|
||||
targetFileSizeEntry = widget.NewEntry()
|
||||
targetFileSizeEntry.SetPlaceHolder("e.g., 250")
|
||||
targetFileSizeUnitSelect := widget.NewSelect([]string{"KB", "MB", "GB"}, func(value string) {})
|
||||
targetFileSizeUnitSelect.SetSelected("MB")
|
||||
targetSizeManualRow := container.NewBorder(nil, nil, nil, targetFileSizeUnitSelect, targetFileSizeEntry)
|
||||
targetSizeManualRow.Hide() // Hidden by default, show only when "Manual" is selected
|
||||
|
||||
parseSizeParts := func(input string) (string, string, bool) {
|
||||
trimmed := strings.TrimSpace(input)
|
||||
|
|
@ -6021,7 +6129,11 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
|
|||
return numStr, unit, true
|
||||
}
|
||||
|
||||
var syncingTargetSize bool
|
||||
updateTargetSizeState := func() {
|
||||
if syncingTargetSize {
|
||||
return
|
||||
}
|
||||
val := strings.TrimSpace(targetFileSizeEntry.Text)
|
||||
if val == "" {
|
||||
state.convert.TargetFileSize = ""
|
||||
|
|
@ -6050,6 +6162,26 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
|
|||
}
|
||||
}
|
||||
|
||||
setTargetFileSize := func(value string) {
|
||||
syncingTargetSize = true
|
||||
defer func() { syncingTargetSize = false }()
|
||||
if value == "" {
|
||||
targetFileSizeEntry.SetText("")
|
||||
targetFileSizeUnitSelect.SetSelected("MB")
|
||||
state.convert.TargetFileSize = ""
|
||||
return
|
||||
}
|
||||
if num, unit, ok := parseSizeParts(value); ok {
|
||||
targetFileSizeEntry.SetText(num)
|
||||
if unit != "" {
|
||||
targetFileSizeUnitSelect.SetSelected(unit)
|
||||
}
|
||||
} else {
|
||||
targetFileSizeEntry.SetText(value)
|
||||
}
|
||||
state.convert.TargetFileSize = value
|
||||
}
|
||||
|
||||
targetFileSizeUnitSelect.OnChanged = func(value string) {
|
||||
if targetFileSizeEntry.Hidden {
|
||||
return
|
||||
|
|
@ -6097,7 +6229,7 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
|
|||
targetFileSizeSelect.Options = options
|
||||
}
|
||||
|
||||
targetFileSizeSelect = widget.NewSelect([]string{"Manual", "25MB", "50MB", "100MB", "200MB", "500MB", "1GB"}, func(value string) {
|
||||
targetFileSizeSelect = widget.NewSelect([]string{"25MB", "50MB", "100MB", "200MB", "500MB", "1GB", "Manual"}, func(value string) {
|
||||
if value == "Manual" {
|
||||
targetSizeManualRow.Show()
|
||||
if state.convert.TargetFileSize != "" {
|
||||
|
|
@ -6133,7 +6265,7 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
|
|||
}
|
||||
logging.Debug(logging.CatUI, "target file size set to %s", state.convert.TargetFileSize)
|
||||
})
|
||||
targetFileSizeSelect.SetSelected("Manual")
|
||||
targetFileSizeSelect.SetSelected("100MB")
|
||||
updateTargetSizeOptions()
|
||||
|
||||
targetFileSizeEntry.OnChanged = func(val string) {
|
||||
|
|
@ -6169,6 +6301,13 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
|
|||
|
||||
state.convert.BitratePreset = label
|
||||
|
||||
// Show/hide manual bitrate entry based on selection
|
||||
if label == "Manual" {
|
||||
manualBitrateRow.Show()
|
||||
} else {
|
||||
manualBitrateRow.Hide()
|
||||
}
|
||||
|
||||
// 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"
|
||||
|
|
@ -6177,7 +6316,11 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
|
|||
|
||||
if preset.Bitrate != "" {
|
||||
state.convert.VideoBitrate = preset.Bitrate
|
||||
videoBitrateEntry.SetText(preset.Bitrate)
|
||||
if setManualBitrate != nil {
|
||||
setManualBitrate(preset.Bitrate)
|
||||
} else {
|
||||
videoBitrateEntry.SetText(preset.Bitrate)
|
||||
}
|
||||
}
|
||||
|
||||
// Adjust codec to match the preset intent (user can change back)
|
||||
|
|
@ -6507,7 +6650,11 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
|
|||
videoCodecSelect.Disable()
|
||||
|
||||
state.convert.VideoBitrate = dvdBitrate
|
||||
videoBitrateEntry.SetText(dvdBitrate)
|
||||
if setManualBitrate != nil {
|
||||
setManualBitrate(dvdBitrate)
|
||||
} else {
|
||||
videoBitrateEntry.SetText(dvdBitrate)
|
||||
}
|
||||
videoBitrateEntry.Disable()
|
||||
state.convert.BitrateMode = "CBR"
|
||||
bitrateModeSelect.SetSelected("CBR")
|
||||
|
|
@ -6554,6 +6701,7 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
|
|||
updateQualityVisibility = func() {
|
||||
hide := strings.Contains(strings.ToLower(state.convert.SelectedFormat.Label), "h.265") ||
|
||||
strings.EqualFold(state.convert.VideoCodec, "H.265")
|
||||
hideCRF := strings.EqualFold(state.convert.Quality, "Lossless")
|
||||
|
||||
if qualitySectionSimple != nil {
|
||||
if hide {
|
||||
|
|
@ -6569,6 +6717,13 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
|
|||
qualitySectionAdv.Show()
|
||||
}
|
||||
}
|
||||
if crfContainer != nil {
|
||||
if hideCRF {
|
||||
crfContainer.Hide()
|
||||
} else {
|
||||
crfContainer.Show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Simple mode options - minimal controls, aspect locked to Source
|
||||
|
|
@ -6678,6 +6833,70 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
|
|||
layout.NewSpacer(),
|
||||
)
|
||||
|
||||
resetConvertDefaults = func() {
|
||||
state.convert = defaultConvertConfig()
|
||||
logging.Debug(logging.CatUI, "convert settings reset to defaults")
|
||||
|
||||
tabs.SelectIndex(0)
|
||||
state.convert.Mode = "Simple"
|
||||
|
||||
formatSelect.SetSelected(state.convert.SelectedFormat.Label)
|
||||
videoCodecSelect.SetSelected(state.convert.VideoCodec)
|
||||
qualitySelectSimple.SetSelected(state.convert.Quality)
|
||||
qualitySelectAdv.SetSelected(state.convert.Quality)
|
||||
simplePresetSelect.SetSelected(state.convert.EncoderPreset)
|
||||
encoderPresetSelect.SetSelected(state.convert.EncoderPreset)
|
||||
bitrateModeSelect.SetSelected(reverseMap[state.convert.BitrateMode])
|
||||
bitratePresetSelect.SetSelected(state.convert.BitratePreset)
|
||||
simpleBitrateSelect.SetSelected(state.convert.BitratePreset)
|
||||
crfEntry.SetText(state.convert.CRF)
|
||||
setManualBitrate(state.convert.VideoBitrate)
|
||||
targetFileSizeSelect.SetSelected("Manual")
|
||||
setTargetFileSize(state.convert.TargetFileSize)
|
||||
autoNameCheck.SetChecked(state.convert.UseAutoNaming)
|
||||
autoNameTemplate.SetText(state.convert.AutoNameTemplate)
|
||||
outputEntry.SetText(state.convert.OutputBase)
|
||||
outputHint.SetText(fmt.Sprintf("Output file: %s", state.convert.OutputFile()))
|
||||
resolutionSelectSimple.SetSelected(state.convert.TargetResolution)
|
||||
resolutionSelect.SetSelected(state.convert.TargetResolution)
|
||||
frameRateSelect.SetSelected(state.convert.FrameRate)
|
||||
updateFrameRateHint()
|
||||
motionInterpCheck.SetChecked(state.convert.UseMotionInterpolation)
|
||||
syncAspect(state.convert.OutputAspect, false)
|
||||
aspectOptions.SetSelected(state.convert.AspectHandling)
|
||||
pixelFormatSelect.SetSelected(state.convert.PixelFormat)
|
||||
hwAccelSelect.SetSelected(state.convert.HardwareAccel)
|
||||
twoPassCheck.SetChecked(state.convert.TwoPass)
|
||||
audioCodecSelect.SetSelected(state.convert.AudioCodec)
|
||||
audioBitrateSelect.SetSelected(state.convert.AudioBitrate)
|
||||
audioChannelsSelect.SetSelected(state.convert.AudioChannels)
|
||||
inverseCheck.SetChecked(state.convert.InverseTelecine)
|
||||
inverseHint.SetText(state.convert.InverseAutoNotes)
|
||||
coverLabel.SetText(state.convert.CoverLabel())
|
||||
if coverDisplay != nil {
|
||||
coverDisplay.SetText("Cover Art: " + state.convert.CoverLabel())
|
||||
}
|
||||
|
||||
updateAspectBoxVisibility()
|
||||
if updateDVDOptions != nil {
|
||||
updateDVDOptions()
|
||||
}
|
||||
// Re-apply defaults in case DVD options toggled any locks
|
||||
state.convert.TargetResolution = "Source"
|
||||
state.convert.FrameRate = "Source"
|
||||
resolutionSelectSimple.SetSelected("Source")
|
||||
resolutionSelect.SetSelected("Source")
|
||||
frameRateSelect.SetSelected("Source")
|
||||
updateFrameRateHint()
|
||||
if updateEncodingControls != nil {
|
||||
updateEncodingControls()
|
||||
}
|
||||
if updateQualityVisibility != nil {
|
||||
updateQualityVisibility()
|
||||
}
|
||||
state.persistConvertConfig()
|
||||
}
|
||||
|
||||
// Create tabs for Simple/Advanced modes
|
||||
// Wrap simple options with settings box at top
|
||||
simpleWithSettings := container.NewVBox(settingsBox, simpleOptions)
|
||||
|
|
@ -6905,23 +7124,9 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
|
|||
mainContent := container.NewMax(mainSplit)
|
||||
|
||||
resetBtn := widget.NewButton("Reset", func() {
|
||||
tabs.SelectIndex(0) // Select Simple tab
|
||||
state.convert.Mode = "Simple"
|
||||
formatSelect.SetSelected("MP4 (H.264)")
|
||||
state.convert.Quality = "Standard (CRF 23)"
|
||||
qualitySelectSimple.SetSelected("Standard (CRF 23)")
|
||||
qualitySelectAdv.SetSelected("Standard (CRF 23)")
|
||||
aspectOptions.SetSelected("Auto")
|
||||
targetAspectSelect.SetSelected("Source")
|
||||
updateAspectBoxVisibility()
|
||||
if updateEncodingControls != nil {
|
||||
updateEncodingControls()
|
||||
if resetConvertDefaults != nil {
|
||||
resetConvertDefaults()
|
||||
}
|
||||
if updateQualityVisibility != nil {
|
||||
updateQualityVisibility()
|
||||
}
|
||||
state.persistConvertConfig()
|
||||
logging.Debug(logging.CatUI, "convert settings reset to defaults")
|
||||
})
|
||||
statusLabel := widget.NewLabel("")
|
||||
statusLabel.Wrapping = fyne.TextTruncate // Prevent text wrapping to new line
|
||||
|
|
@ -12591,6 +12796,9 @@ func buildUpscaleView(state *appState) fyne.CanvasObject {
|
|||
"method": state.upscaleMethod,
|
||||
"targetWidth": float64(targetWidth),
|
||||
"targetHeight": float64(targetHeight),
|
||||
"targetPreset": state.upscaleTargetRes,
|
||||
"sourceWidth": float64(state.upscaleFile.Width),
|
||||
"sourceHeight": float64(state.upscaleFile.Height),
|
||||
"preserveAR": preserveAspect,
|
||||
"useAI": state.upscaleAIEnabled && state.upscaleAIAvailable,
|
||||
"aiModel": state.upscaleAIModel,
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user