Compare commits

..

No commits in common. "cdf8b10769ffe93a5f7cb577250e745df736b807" and "d24fd7c281ca4b6b12bdef73566aed548c7c8dce" have entirely different histories.

5 changed files with 154 additions and 425 deletions

47
DONE.md
View File

@ -54,47 +54,6 @@ 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
@ -814,12 +773,6 @@ 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

View File

@ -47,12 +47,6 @@ 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+

View File

@ -173,7 +173,7 @@ func (r *moduleTileRenderer) Layout(size fyne.Size) {
}
func (r *moduleTileRenderer) MinSize() fyne.Size {
return fyne.NewSize(150, 65)
return fyne.NewSize(220, 110)
}
func (r *moduleTileRenderer) Refresh() {

View File

@ -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 = 18
title.TextSize = 28
queueTile := buildQueueTile(queueCompleted, queueTotal, queueColor, textColor, onQueueClick)
sidebarToggleBtn := widget.NewButton("☰", onToggleSidebar)
sidebarToggleBtn := widget.NewButton("☰ History", onToggleSidebar)
sidebarToggleBtn.Importance = widget.LowImportance
benchmarkBtn := widget.NewButton("Benchmark", onBenchmarkClick)
benchmarkBtn := widget.NewButton("Run Benchmark", onBenchmarkClick)
// Highlight the benchmark button if no benchmark has been run
if !hasBenchmark {
benchmarkBtn.Importance = widget.HighImportance
@ -62,19 +62,13 @@ func BuildMainMenu(modules []ModuleInfo, onModuleClick func(string), onModuleDro
benchmarkBtn.Importance = widget.LowImportance
}
viewResultsBtn := widget.NewButton("Results", onBenchmarkHistoryClick)
viewResultsBtn := widget.NewButton("View Results", onBenchmarkHistoryClick)
viewResultsBtn.Importance = widget.LowImportance
logsBtn := widget.NewButton("Logs", onLogsClick)
logsBtn.Importance = widget.LowImportance
// Compact header - title on left, controls on right
header := container.NewBorder(
nil, nil,
title,
container.NewHBox(sidebarToggleBtn, logsBtn, benchmarkBtn, viewResultsBtn, queueTile),
nil,
)
header := container.NewHBox(title, layout.NewSpacer(), sidebarToggleBtn, benchmarkBtn, viewResultsBtn, logsBtn, queueTile)
categorized := map[string][]fyne.CanvasObject{}
for i := range modules {
@ -103,19 +97,15 @@ 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,
catLabel,
canvas.NewText(cat, textColor),
container.NewGridWithColumns(3, categorized[cat]...),
)
}
padding := canvas.NewRectangle(color.Transparent)
padding.SetMinSize(fyne.NewSize(0, 4))
padding.SetMinSize(fyne.NewSize(0, 14))
// Compact body without scrolling
body := container.NewVBox(
header,
padding,
@ -135,19 +125,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 NewModuleTile(mod.Label, mod.Color, mod.Enabled, tapped, dropped)
return container.NewPadded(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 = 6
rect.SetMinSize(fyne.NewSize(120, 40))
rect.CornerRadius = 8
rect.SetMinSize(fyne.NewSize(160, 60))
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 = 14
text.TextSize = 18
tile := container.NewMax(rect, container.NewCenter(text))

492
main.go
View File

@ -505,56 +505,6 @@ 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()
@ -1598,9 +1548,13 @@ func (s *appState) refreshQueueView() {
s.jobQueue.Clear()
s.clearVideo()
// Always return to main menu after clearing
// If queue is now empty, return to previous module
if len(s.jobQueue.List()) == 0 {
s.showMainMenu()
if s.lastModule != "" && s.lastModule != "queue" {
s.showModule(s.lastModule)
} else {
s.showMainMenu()
}
} else {
s.refreshQueueView() // Refresh if jobs remain
}
@ -1608,8 +1562,12 @@ func (s *appState) refreshQueueView() {
func() { // onClearAll
s.jobQueue.ClearAll()
s.clearVideo()
// Always return to main menu after clearing all
s.showMainMenu()
// Return to previous module or main menu
if s.lastModule != "" && s.lastModule != "queue" {
s.showModule(s.lastModule)
} else {
s.showMainMenu()
}
},
func(id string) { // onCopyError
job, err := s.jobQueue.Get(id)
@ -4372,9 +4330,6 @@ 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
@ -4388,21 +4343,6 @@ 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
@ -4419,7 +4359,6 @@ 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
@ -4915,8 +4854,47 @@ func runGUI() {
logging.Debug(logging.CatUI, "window initialized at 800x600 (compact default), manual resizing enabled")
state := &appState{
window: w,
convert: defaultConvertConfig(),
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,
},
mergeChapters: true,
player: player.New(),
playerVolume: 100,
@ -5271,7 +5249,6 @@ 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
@ -5282,7 +5259,6 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
crfContainer *fyne.Container
bitrateContainer *fyne.Container
targetSizeContainer *fyne.Container
resetConvertDefaults func()
)
var (
updateEncodingControls func()
@ -5606,16 +5582,10 @@ 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"}
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)
}
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
})
if state.convert.OutputAspect == "" {
state.convert.OutputAspect = "Source"
@ -5654,7 +5624,11 @@ 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
@ -5779,7 +5753,7 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
encoderPresetHint.SetText(hint)
}
encoderPresetSelect := widget.NewSelect([]string{"veryslow", "slower", "slow", "medium", "fast", "faster", "veryfast", "superfast", "ultrafast"}, func(value string) {
encoderPresetSelect := widget.NewSelect([]string{"ultrafast", "superfast", "veryfast", "faster", "fast", "medium", "slow", "slower", "veryslow"}, func(value string) {
state.convert.EncoderPreset = value
logging.Debug(logging.CatUI, "encoder preset set to %s", value)
updateEncoderPresetHint(value)
@ -5791,7 +5765,7 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
updateEncoderPresetHint(state.convert.EncoderPreset)
// Simple mode preset dropdown
simplePresetSelect := widget.NewSelect([]string{"veryslow", "slower", "slow", "medium", "fast", "faster", "veryfast", "superfast", "ultrafast"}, func(value string) {
simplePresetSelect := widget.NewSelect([]string{"ultrafast", "superfast", "veryfast", "faster", "fast", "medium", "slow", "slower", "veryslow"}, func(value string) {
state.convert.EncoderPreset = value
logging.Debug(logging.CatUI, "simple preset set to %s", value)
updateEncoderPresetHint(value)
@ -5806,8 +5780,50 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
settingsInfoLabel.Alignment = fyne.TextAlignCenter
resetSettingsBtn := widget.NewButton("Reset to Defaults", func() {
if resetConvertDefaults != nil {
resetConvertDefaults()
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()
}
})
resetSettingsBtn.Importance = widget.LowImportance
@ -5892,109 +5908,15 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
// Video Bitrate entry (for CBR/VBR)
videoBitrateEntry = widget.NewEntry()
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
videoBitrateEntry.SetPlaceHolder("5000k")
videoBitrateEntry.SetText(state.convert.VideoBitrate)
videoBitrateEntry.OnChanged = func(val string) {
state.convert.VideoBitrate = val
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}),
@ -6010,12 +5932,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)
@ -6033,7 +5955,7 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
}
})
if state.convert.BitratePreset == "" || bitratePresetLookup[state.convert.BitratePreset].Label == "" {
state.convert.BitratePreset = "2.5 Mbps - Medium Quality"
state.convert.BitratePreset = "Manual"
}
bitratePresetSelect.SetSelected(state.convert.BitratePreset)
@ -6046,16 +5968,12 @@ 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("Bitrate Preset", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}),
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}),
bitratePresetSelect,
manualBitrateRow,
)
// Simple resolution selector (separate widget to avoid double-parent issues)
@ -6070,49 +5988,23 @@ 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) {
if syncAspect != nil {
syncAspect(value, true)
}
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()
})
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)
@ -6129,11 +6021,7 @@ 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 = ""
@ -6162,26 +6050,6 @@ 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
@ -6229,7 +6097,7 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
targetFileSizeSelect.Options = options
}
targetFileSizeSelect = widget.NewSelect([]string{"25MB", "50MB", "100MB", "200MB", "500MB", "1GB", "Manual"}, func(value string) {
targetFileSizeSelect = widget.NewSelect([]string{"Manual", "25MB", "50MB", "100MB", "200MB", "500MB", "1GB"}, func(value string) {
if value == "Manual" {
targetSizeManualRow.Show()
if state.convert.TargetFileSize != "" {
@ -6265,7 +6133,7 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
}
logging.Debug(logging.CatUI, "target file size set to %s", state.convert.TargetFileSize)
})
targetFileSizeSelect.SetSelected("100MB")
targetFileSizeSelect.SetSelected("Manual")
updateTargetSizeOptions()
targetFileSizeEntry.OnChanged = func(val string) {
@ -6301,13 +6169,6 @@ 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"
@ -6316,11 +6177,7 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
if preset.Bitrate != "" {
state.convert.VideoBitrate = preset.Bitrate
if setManualBitrate != nil {
setManualBitrate(preset.Bitrate)
} else {
videoBitrateEntry.SetText(preset.Bitrate)
}
videoBitrateEntry.SetText(preset.Bitrate)
}
// Adjust codec to match the preset intent (user can change back)
@ -6650,11 +6507,7 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
videoCodecSelect.Disable()
state.convert.VideoBitrate = dvdBitrate
if setManualBitrate != nil {
setManualBitrate(dvdBitrate)
} else {
videoBitrateEntry.SetText(dvdBitrate)
}
videoBitrateEntry.SetText(dvdBitrate)
videoBitrateEntry.Disable()
state.convert.BitrateMode = "CBR"
bitrateModeSelect.SetSelected("CBR")
@ -6701,7 +6554,6 @@ 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 {
@ -6717,13 +6569,6 @@ 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
@ -6833,70 +6678,6 @@ 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)
@ -7124,9 +6905,23 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
mainContent := container.NewMax(mainSplit)
resetBtn := widget.NewButton("Reset", func() {
if resetConvertDefaults != nil {
resetConvertDefaults()
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 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
@ -12796,9 +12591,6 @@ 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,