feat(upscale): redesign layout and encoding controls

This commit is contained in:
Stu Leak 2026-01-06 17:37:32 -05:00
parent 3ef115aab1
commit 1a9e0e0d05

441
main.go
View File

@ -1098,6 +1098,10 @@ type appState struct {
upscaleMotionInterpolation bool // Use motion interpolation for frame rate changes upscaleMotionInterpolation bool // Use motion interpolation for frame rate changes
upscaleBlurEnabled bool // Apply blur in upscale pipeline upscaleBlurEnabled bool // Apply blur in upscale pipeline
upscaleBlurSigma float64 // Blur strength (sigma) upscaleBlurSigma float64 // Blur strength (sigma)
upscaleEncoderPreset string // libx264 preset for upscale output
upscaleBitrateMode string // CRF, CBR, VBR
upscaleBitratePreset string // preset label for bitrate modes
upscaleManualBitrate string // manual bitrate value (e.g., 2500k)
// Snippet settings // Snippet settings
snippetLength int // Length of snippet in seconds (default: 20) snippetLength int // Length of snippet in seconds (default: 20)
@ -1121,6 +1125,8 @@ type appState struct {
authorRegion string // "NTSC", "PAL", "AUTO" authorRegion string // "NTSC", "PAL", "AUTO"
authorAspectRatio string // "4:3", "16:9", "AUTO" authorAspectRatio string // "4:3", "16:9", "AUTO"
authorCreateMenu bool // Whether to create DVD menu authorCreateMenu bool // Whether to create DVD menu
authorMenuTemplate string // "Simple", "Dark", "Poster"
authorMenuBackgroundImage string // Path to a user-selected background image
authorTitle string // DVD title authorTitle string // DVD title
authorSubtitles []string // Subtitle file paths authorSubtitles []string // Subtitle file paths
authorAudioTracks []string // Additional audio tracks authorAudioTracks []string // Additional audio tracks
@ -5558,6 +5564,10 @@ func (s *appState) executeUpscaleJob(ctx context.Context, job *queue.Job, progre
useMotionInterp, _ := cfg["useMotionInterpolation"].(bool) useMotionInterp, _ := cfg["useMotionInterpolation"].(bool)
sourceFrameRate := toFloat(cfg["sourceFrameRate"]) sourceFrameRate := toFloat(cfg["sourceFrameRate"])
qualityPreset, _ := cfg["qualityPreset"].(string) qualityPreset, _ := cfg["qualityPreset"].(string)
encoderPreset, _ := cfg["encoderPreset"].(string)
bitrateMode, _ := cfg["bitrateMode"].(string)
bitratePreset, _ := cfg["bitratePreset"].(string)
manualBitrate, _ := cfg["manualBitrate"].(string)
blurEnabled, _ := cfg["blurEnabled"].(bool) blurEnabled, _ := cfg["blurEnabled"].(bool)
blurSigma := toFloat(cfg["blurSigma"]) blurSigma := toFloat(cfg["blurSigma"])
@ -5590,6 +5600,91 @@ func (s *appState) executeUpscaleJob(ctx context.Context, job *queue.Job, progre
crfValue = 16 crfValue = 16
} }
resolveBitrate := func() string {
if strings.TrimSpace(manualBitrate) != "" {
return manualBitrate
}
switch bitratePreset {
case "0.5 Mbps - Ultra Low":
return "500k"
case "1.0 Mbps - Very Low":
return "1000k"
case "1.5 Mbps - Low":
return "1500k"
case "2.0 Mbps - Medium-Low":
return "2000k"
case "2.5 Mbps - Medium":
return "2500k"
case "4.0 Mbps - Good":
return "4000k"
case "6.0 Mbps - High":
return "6000k"
case "8.0 Mbps - Very High":
return "8000k"
default:
return "2500k"
}
}
parseBitrateKbps := func(val string) int {
v := strings.TrimSpace(strings.ToLower(val))
if v == "" {
return 0
}
mult := 1.0
if strings.HasSuffix(v, "k") {
v = strings.TrimSuffix(v, "k")
mult = 1
} else if strings.HasSuffix(v, "m") {
v = strings.TrimSuffix(v, "m")
mult = 1000
}
num, err := strconv.ParseFloat(v, 64)
if err != nil {
return 0
}
return int(num * mult)
}
normalizeBitrateMode := func(mode string) string {
switch {
case strings.HasPrefix(strings.ToUpper(mode), "CBR"):
return "CBR"
case strings.HasPrefix(strings.ToUpper(mode), "VBR"):
return "VBR"
default:
return "CRF"
}
}
appendEncodingArgs := func(args []string) []string {
preset := encoderPreset
if strings.TrimSpace(preset) == "" {
preset = "slow"
}
mode := normalizeBitrateMode(bitrateMode)
args = append(args, "-preset", preset)
switch mode {
case "CBR", "VBR":
bitrateVal := resolveBitrate()
args = append(args, "-b:v", bitrateVal)
kbps := parseBitrateKbps(bitrateVal)
if kbps > 0 {
maxrate := kbps
bufsize := kbps * 2
if mode == "VBR" {
maxrate = kbps * 2
bufsize = kbps * 4
}
args = append(args, "-maxrate", fmt.Sprintf("%dk", maxrate))
args = append(args, "-bufsize", fmt.Sprintf("%dk", bufsize))
}
default:
args = append(args, "-crf", strconv.Itoa(crfValue))
}
return args
}
// Build filter chain // Build filter chain
var baseFilters []string var baseFilters []string
@ -5849,10 +5944,9 @@ func (s *appState) executeUpscaleJob(ctx context.Context, job *queue.Job, progre
reassembleArgs = append(reassembleArgs, "-vf", finalScale) reassembleArgs = append(reassembleArgs, "-vf", finalScale)
} }
reassembleArgs = append(reassembleArgs, "-c:v", "libx264")
reassembleArgs = appendEncodingArgs(reassembleArgs)
reassembleArgs = append(reassembleArgs, reassembleArgs = append(reassembleArgs,
"-c:v", "libx264",
"-preset", "slow",
"-crf", strconv.Itoa(crfValue),
"-pix_fmt", "yuv420p", "-pix_fmt", "yuv420p",
"-c:a", "copy", "-c:a", "copy",
"-shortest", "-shortest",
@ -5907,11 +6001,10 @@ func (s *appState) executeUpscaleJob(ctx context.Context, job *queue.Job, progre
args = append(args, "-vf", vfilter) args = append(args, "-vf", vfilter)
} }
// Use lossless MKV by default for upscales; copy audio // Use MKV container by default; copy audio
args = append(args, "-c:v", "libx264")
args = appendEncodingArgs(args)
args = append(args, args = append(args,
"-c:v", "libx264",
"-preset", "slow",
"-crf", strconv.Itoa(crfValue),
"-pix_fmt", "yuv420p", "-pix_fmt", "yuv420p",
"-c:a", "copy", "-c:a", "copy",
"-progress", "pipe:1", "-progress", "pipe:1",
@ -14929,6 +15022,18 @@ func buildUpscaleView(state *appState) fyne.CanvasObject {
if state.upscaleQualityPreset == "" { if state.upscaleQualityPreset == "" {
state.upscaleQualityPreset = "Near-lossless (CRF 16)" state.upscaleQualityPreset = "Near-lossless (CRF 16)"
} }
if state.upscaleEncoderPreset == "" {
state.upscaleEncoderPreset = "slow"
}
if state.upscaleBitrateMode == "" {
state.upscaleBitrateMode = "CRF"
}
if state.upscaleBitratePreset == "" {
state.upscaleBitratePreset = "2.5 Mbps - Medium"
}
if state.upscaleManualBitrate == "" {
state.upscaleManualBitrate = "2500k"
}
if state.upscaleAIPreset == "" { if state.upscaleAIPreset == "" {
state.upscaleAIPreset = "Balanced" state.upscaleAIPreset = "Balanced"
state.upscaleAIScale = 4.0 state.upscaleAIScale = 4.0
@ -15007,7 +15112,23 @@ func buildUpscaleView(state *appState) fyne.CanvasObject {
state.showFiltersView() state.showFiltersView()
}) })
// Traditional Scaling Section mediumBlue := utils.MustHex("#13182B")
navyBlue := utils.MustHex("#191F35")
buildUpscaleBox := func(title string, content fyne.CanvasObject) fyne.CanvasObject {
bg := canvas.NewRectangle(navyBlue)
bg.CornerRadius = 10
bg.StrokeColor = gridColor
bg.StrokeWidth = 1
body := container.NewVBox(
widget.NewLabelWithStyle(title, fyne.TextAlignLeading, fyne.TextStyle{Bold: true}),
widget.NewSeparator(),
content,
)
return container.NewMax(bg, container.NewPadded(body))
}
// Scaling (method + blur)
methodLabel := widget.NewLabel(fmt.Sprintf("Method: %s", state.upscaleMethod)) methodLabel := widget.NewLabel(fmt.Sprintf("Method: %s", state.upscaleMethod))
methodSelect := widget.NewSelect([]string{ methodSelect := widget.NewSelect([]string{
"lanczos", // Sharp, best general purpose "lanczos", // Sharp, best general purpose
@ -15024,62 +15145,6 @@ func buildUpscaleView(state *appState) fyne.CanvasObject {
methodInfo.TextStyle = fyne.TextStyle{Italic: true} methodInfo.TextStyle = fyne.TextStyle{Italic: true}
methodInfo.Wrapping = fyne.TextWrapWord methodInfo.Wrapping = fyne.TextWrapWord
traditionalSection := widget.NewCard("Traditional Scaling (FFmpeg)", "", container.NewVBox(
widget.NewLabel("Classic upscaling methods - always available"),
container.NewGridWithColumns(2,
widget.NewLabel("Scaling Algorithm:"),
methodSelect,
),
methodLabel,
widget.NewSeparator(),
methodInfo,
))
// Resolution Selection Section
resLabel := widget.NewLabel(fmt.Sprintf("Target: %s", state.upscaleTargetRes))
resSelect := widget.NewSelect([]string{
"Match Source",
"2X (relative)",
"4X (relative)",
"720p (1280x720)",
"1080p (1920x1080)",
"1440p (2560x1440)",
"4K (3840x2160)",
"8K (7680x4320)",
"Custom",
}, func(s string) {
state.upscaleTargetRes = s
resLabel.SetText(fmt.Sprintf("Target: %s", s))
})
resSelect.SetSelected(state.upscaleTargetRes)
resolutionSection := widget.NewCard("Target Resolution", "", container.NewVBox(
widget.NewLabel("Select output resolution"),
container.NewGridWithColumns(2,
widget.NewLabel("Resolution:"),
resSelect,
),
resLabel,
sourceResLabel,
))
qualitySelect := widget.NewSelect([]string{
"Lossless (CRF 0)",
"Near-lossless (CRF 16)",
"High (CRF 18)",
}, func(s string) {
state.upscaleQualityPreset = s
})
qualitySelect.SetSelected(state.upscaleQualityPreset)
qualitySection := widget.NewCard("Output Quality", "", container.NewVBox(
container.NewGridWithColumns(2,
widget.NewLabel("Quality:"),
qualitySelect,
),
widget.NewLabel("Lower CRF = higher quality/larger files"),
))
blurLabel := widget.NewLabel(fmt.Sprintf("Blur Strength: %.2f", state.upscaleBlurSigma)) blurLabel := widget.NewLabel(fmt.Sprintf("Blur Strength: %.2f", state.upscaleBlurSigma))
blurSlider := widget.NewSlider(0.0, 8.0) blurSlider := widget.NewSlider(0.0, 8.0)
blurSlider.Step = 0.1 blurSlider.Step = 0.1
@ -15104,13 +15169,165 @@ func buildUpscaleView(state *appState) fyne.CanvasObject {
blurSlider.Disable() blurSlider.Disable()
} }
blurSection := widget.NewCard("Blur (Optional)", "", container.NewVBox( // Resolution
widget.NewLabel("Apply a soft blur during upscale processing"), resLabel := widget.NewLabel(fmt.Sprintf("Target: %s", state.upscaleTargetRes))
blurCheck, resSelect := widget.NewSelect([]string{
container.NewVBox(blurLabel, blurSlider), "Match Source",
"2X (relative)",
"4X (relative)",
"720p (1280x720)",
"1080p (1920x1080)",
"1440p (2560x1440)",
"4K (3840x2160)",
"8K (7680x4320)",
"Custom",
}, func(s string) {
state.upscaleTargetRes = s
resLabel.SetText(fmt.Sprintf("Target: %s", s))
})
resSelect.SetSelected(state.upscaleTargetRes)
resolutionSection := buildUpscaleBox("Target Resolution", container.NewVBox(
container.NewGridWithColumns(2,
widget.NewLabel("Resolution:"),
resSelect,
),
resLabel,
sourceResLabel,
)) ))
// Frame Rate Section // Video Encoding
qualitySelect := widget.NewSelect([]string{
"Lossless (CRF 0)",
"Near-lossless (CRF 16)",
"High (CRF 18)",
}, func(s string) {
state.upscaleQualityPreset = s
})
qualitySelect.SetSelected(state.upscaleQualityPreset)
encoderPresetSelect := widget.NewSelect([]string{
"ultrafast", "superfast", "veryfast", "faster", "fast", "medium", "slow", "slower", "veryslow",
}, func(s string) {
state.upscaleEncoderPreset = s
})
encoderPresetSelect.SetSelected(state.upscaleEncoderPreset)
var updateEncodingVisibility func()
bitrateModeSelect := widget.NewSelect([]string{
"CRF (Constant Rate Factor)",
"CBR (Constant Bitrate)",
"VBR (Variable Bitrate)",
}, func(s string) {
switch {
case strings.HasPrefix(s, "CRF"):
state.upscaleBitrateMode = "CRF"
case strings.HasPrefix(s, "CBR"):
state.upscaleBitrateMode = "CBR"
case strings.HasPrefix(s, "VBR"):
state.upscaleBitrateMode = "VBR"
default:
state.upscaleBitrateMode = s
}
if updateEncodingVisibility != nil {
updateEncodingVisibility()
}
})
switch state.upscaleBitrateMode {
case "CBR":
bitrateModeSelect.SetSelected("CBR (Constant Bitrate)")
case "VBR":
bitrateModeSelect.SetSelected("VBR (Variable Bitrate)")
default:
bitrateModeSelect.SetSelected("CRF (Constant Rate Factor)")
}
type bitratePreset struct {
Label string
Bitrate string
}
presets := []bitratePreset{
{Label: "0.5 Mbps - Ultra Low", Bitrate: "500k"},
{Label: "1.0 Mbps - Very Low", Bitrate: "1000k"},
{Label: "1.5 Mbps - Low", Bitrate: "1500k"},
{Label: "2.0 Mbps - Medium-Low", Bitrate: "2000k"},
{Label: "2.5 Mbps - Medium", Bitrate: "2500k"},
{Label: "4.0 Mbps - Good", Bitrate: "4000k"},
{Label: "6.0 Mbps - High", Bitrate: "6000k"},
{Label: "8.0 Mbps - Very High", Bitrate: "8000k"},
{Label: "Manual", Bitrate: ""},
}
bitratePresetLookup := make(map[string]bitratePreset)
var bitratePresetLabels []string
for _, p := range presets {
bitratePresetLookup[p.Label] = p
bitratePresetLabels = append(bitratePresetLabels, p.Label)
}
manualBitrateEntry := widget.NewEntry()
manualBitrateEntry.SetPlaceHolder("e.g., 2500k")
manualBitrateEntry.SetText(state.upscaleManualBitrate)
manualBitrateEntry.OnChanged = func(val string) {
state.upscaleManualBitrate = val
}
bitratePresetSelect := widget.NewSelect(bitratePresetLabels, func(s string) {
state.upscaleBitratePreset = s
preset := bitratePresetLookup[s]
if preset.Bitrate == "" {
manualBitrateEntry.Show()
} else {
state.upscaleManualBitrate = preset.Bitrate
manualBitrateEntry.SetText(preset.Bitrate)
manualBitrateEntry.Hide()
}
})
bitratePresetSelect.SetSelected(state.upscaleBitratePreset)
if bitratePresetLookup[state.upscaleBitratePreset].Bitrate == "" {
manualBitrateEntry.Show()
} else {
manualBitrateEntry.Hide()
}
updateEncodingVisibility = func() {
mode := state.upscaleBitrateMode
if mode == "" || mode == "CRF" {
qualitySelect.Enable()
bitratePresetSelect.Hide()
manualBitrateEntry.Hide()
} else {
qualitySelect.Disable()
bitratePresetSelect.Show()
if bitratePresetLookup[state.upscaleBitratePreset].Bitrate == "" {
manualBitrateEntry.Show()
}
}
}
updateEncodingVisibility()
encodingSection := buildUpscaleBox("Video Encoding", container.NewVBox(
container.NewGridWithColumns(2,
widget.NewLabel("Encoder Preset:"),
encoderPresetSelect,
),
container.NewGridWithColumns(2,
widget.NewLabel("Quality Preset:"),
qualitySelect,
),
container.NewGridWithColumns(2,
widget.NewLabel("Bitrate Mode:"),
bitrateModeSelect,
),
container.NewGridWithColumns(2,
widget.NewLabel("Bitrate Preset:"),
bitratePresetSelect,
),
manualBitrateEntry,
widget.NewLabel("CRF mode controls quality; bitrate modes control size."),
))
// Frame Rate
frameRateLabel := widget.NewLabel(fmt.Sprintf("Frame Rate: %s", state.upscaleFrameRate)) frameRateLabel := widget.NewLabel(fmt.Sprintf("Frame Rate: %s", state.upscaleFrameRate))
frameRateSelect := widget.NewSelect([]string{"Source", "23.976", "24", "25", "29.97", "30", "50", "59.94", "60"}, func(s string) { frameRateSelect := widget.NewSelect([]string{"Source", "23.976", "24", "25", "29.97", "30", "50", "59.94", "60"}, func(s string) {
state.upscaleFrameRate = s state.upscaleFrameRate = s
@ -15123,8 +15340,7 @@ func buildUpscaleView(state *appState) fyne.CanvasObject {
}) })
motionInterpCheck.SetChecked(state.upscaleMotionInterpolation) motionInterpCheck.SetChecked(state.upscaleMotionInterpolation)
frameRateSection := widget.NewCard("Frame Rate", "", container.NewVBox( frameRateSection := buildUpscaleBox("Frame Rate", container.NewVBox(
widget.NewLabel("Convert frame rate (optional)"),
container.NewGridWithColumns(2, container.NewGridWithColumns(2,
widget.NewLabel("Target FPS:"), widget.NewLabel("Target FPS:"),
frameRateSelect, frameRateSelect,
@ -15140,8 +15356,8 @@ func buildUpscaleView(state *appState) fyne.CanvasObject {
aiModelLabel = aiModelOptions[0] aiModelLabel = aiModelOptions[0]
} }
// AI Upscaling Section // AI Upscaling Section (nested under Scaling)
var aiSection *widget.Card var aiContent fyne.CanvasObject
if state.upscaleAIAvailable { if state.upscaleAIAvailable {
var aiTileSelect *widget.Select var aiTileSelect *widget.Select
var aiTTACheck *widget.Check var aiTTACheck *widget.Check
@ -15350,7 +15566,7 @@ func buildUpscaleView(state *appState) fyne.CanvasObject {
denoiseHint.TextStyle = fyne.TextStyle{Italic: true} denoiseHint.TextStyle = fyne.TextStyle{Italic: true}
updateDenoiseAvailability(state.upscaleAIModel) updateDenoiseAvailability(state.upscaleAIModel)
aiSection = widget.NewCard("AI Upscaling", "✓ Available", container.NewVBox( aiContent = container.NewVBox(
widget.NewLabel("Real-ESRGAN detected - enhanced quality available"), widget.NewLabel("Real-ESRGAN detected - enhanced quality available"),
aiEnabledCheck, aiEnabledCheck,
container.NewGridWithColumns(2, container.NewGridWithColumns(2,
@ -15388,26 +15604,49 @@ func buildUpscaleView(state *appState) fyne.CanvasObject {
container.NewGridWithColumns(3, aiThreadsLoad, aiThreadsProc, aiThreadsSave), container.NewGridWithColumns(3, aiThreadsLoad, aiThreadsProc, aiThreadsSave),
), ),
widget.NewLabel("Note: AI upscaling is slower but produces higher quality results"), widget.NewLabel("Note: AI upscaling is slower but produces higher quality results"),
)) )
} else { } else {
backendNote := "Real-ESRGAN not detected. Install for enhanced quality:" backendNote := "Real-ESRGAN not detected. Install for enhanced quality:"
if state.upscaleAIBackend == "python" { if state.upscaleAIBackend == "python" {
backendNote = "Python Real-ESRGAN detected, but the ncnn backend is required for now." backendNote = "Python Real-ESRGAN detected, but the ncnn backend is required for now."
} }
aiSection = widget.NewCard("AI Upscaling", "Not Available", container.NewVBox( aiContent = container.NewVBox(
widget.NewLabel(backendNote), widget.NewLabel(backendNote),
widget.NewLabel("https://github.com/xinntao/Real-ESRGAN"), widget.NewLabel("https://github.com/xinntao/Real-ESRGAN"),
widget.NewLabel("Traditional scaling methods will be used."), widget.NewLabel("Traditional scaling methods will be used."),
)) )
} }
aiSection := container.NewVBox(
widget.NewLabelWithStyle("AI Upscaling", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}),
widget.NewSeparator(),
aiContent,
)
traditionalSection := buildUpscaleBox("Scaling", container.NewVBox(
widget.NewLabel("Classic upscaling methods - always available"),
container.NewGridWithColumns(2,
widget.NewLabel("Scaling Algorithm:"),
methodSelect,
),
methodLabel,
widget.NewSeparator(),
methodInfo,
widget.NewSeparator(),
widget.NewLabel("Optional blur"),
blurCheck,
container.NewVBox(blurLabel, blurSlider),
widget.NewSeparator(),
aiSection,
))
// Filter Integration Section // Filter Integration Section
applyFiltersCheck := widget.NewCheck("Apply filters before upscaling", func(checked bool) { applyFiltersCheck := widget.NewCheck("Apply filters before upscaling", func(checked bool) {
state.upscaleApplyFilters = checked state.upscaleApplyFilters = checked
}) })
applyFiltersCheck.SetChecked(state.upscaleApplyFilters) applyFiltersCheck.SetChecked(state.upscaleApplyFilters)
filterIntegrationSection := widget.NewCard("Filter Integration", "", container.NewVBox( filterIntegrationSection := buildUpscaleBox("Filter Integration", container.NewVBox(
widget.NewLabel("Apply color correction and filters from Filters module"), widget.NewLabel("Apply color correction and filters from Filters module"),
applyFiltersCheck, applyFiltersCheck,
widget.NewLabel("Filters will be applied before upscaling for best quality"), widget.NewLabel("Filters will be applied before upscaling for best quality"),
@ -15452,6 +15691,10 @@ func buildUpscaleView(state *appState) fyne.CanvasObject {
"inputPath": state.upscaleFile.Path, "inputPath": state.upscaleFile.Path,
"outputPath": outputPath, "outputPath": outputPath,
"method": state.upscaleMethod, "method": state.upscaleMethod,
"encoderPreset": state.upscaleEncoderPreset,
"bitrateMode": state.upscaleBitrateMode,
"bitratePreset": state.upscaleBitratePreset,
"manualBitrate": state.upscaleManualBitrate,
"targetWidth": float64(targetWidth), "targetWidth": float64(targetWidth),
"targetHeight": float64(targetHeight), "targetHeight": float64(targetHeight),
"targetPreset": state.upscaleTargetRes, "targetPreset": state.upscaleTargetRes,
@ -15521,22 +15764,38 @@ func buildUpscaleView(state *appState) fyne.CanvasObject {
addQueueBtn.Importance = widget.MediumImportance addQueueBtn.Importance = widget.MediumImportance
// Main content // Main content
spacing := func() fyne.CanvasObject {
spacer := canvas.NewRectangle(color.Transparent)
spacer.SetMinSize(fyne.NewSize(0, 10))
return spacer
}
metaPanel, _ := buildMetadataPanel(state, state.upscaleFile, fyne.NewSize(0, 200))
leftPanel := container.NewVBox( leftPanel := container.NewVBox(
instructions, instructions,
widget.NewSeparator(), spacing(),
fileLabel, buildUpscaleBox("Video", container.NewVBox(
loadBtn, fileLabel,
filtersNavBtn, loadBtn,
filtersNavBtn,
videoContainer,
)),
spacing(),
metaPanel,
) )
settingsPanel := container.NewVBox( settingsPanel := container.NewVBox(
traditionalSection, traditionalSection,
spacing(),
resolutionSection, resolutionSection,
qualitySection, spacing(),
blurSection, encodingSection,
spacing(),
frameRateSection, frameRateSection,
aiSection, spacing(),
filterIntegrationSection, filterIntegrationSection,
spacing(),
container.NewGridWithColumns(2, applyBtn, addQueueBtn), container.NewGridWithColumns(2, applyBtn, addQueueBtn),
) )
@ -15544,12 +15803,14 @@ func buildUpscaleView(state *appState) fyne.CanvasObject {
// Adaptive height for small screens // Adaptive height for small screens
// Avoid rigid min sizes so window snapping works across modules. // Avoid rigid min sizes so window snapping works across modules.
mainContent := container.New(&fixedHSplitLayout{ratio: 0.6}, split := container.NewHSplit(leftPanel, settingsScroll)
container.NewVBox(leftPanel, videoContainer), split.Offset = 0.58
settingsScroll, mainContent := split
)
content := container.NewPadded(mainContent) content := container.NewMax(
canvas.NewRectangle(mediumBlue),
container.NewPadded(mainContent),
)
return container.NewBorder(topBar, bottomBar, nil, nil, content) return container.NewBorder(topBar, bottomBar, nil, nil, content)
} }