diff --git a/.gitignore b/.gitignore index d2deb00..3421027 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,3 @@ videotools.log .gocache/ .gomodcache/ -VideoTools diff --git a/VideoTools b/VideoTools new file mode 100755 index 0000000..ea556ab Binary files /dev/null and b/VideoTools differ diff --git a/internal/ui/components.go b/internal/ui/components.go index 8b741e3..c131f1f 100644 --- a/internal/ui/components.go +++ b/internal/ui/components.go @@ -424,7 +424,8 @@ func (r *conversionStatsRenderer) Layout(size fyne.Size) { } func (r *conversionStatsRenderer) MinSize() fyne.Size { - return fyne.NewSize(400, 32) + // Only constrain height, allow width to flex + return fyne.NewSize(0, 32) } func (r *conversionStatsRenderer) Refresh() { diff --git a/main.go b/main.go index 6a22718..dfd4ed4 100644 --- a/main.go +++ b/main.go @@ -1159,8 +1159,10 @@ func runGUI() { } else { logging.Debug(logging.CatUI, "app icon not found; continuing without custom icon") } - w.Resize(fyne.NewSize(1120, 640)) - logging.Debug(logging.CatUI, "window initialized at 1120x640") + // Use a generous default window size that fits typical desktops without overflowing. + w.Resize(fyne.NewSize(1280, 800)) + w.SetFixedSize(false) // Allow manual resizing + logging.Debug(logging.CatUI, "window initialized with manual resizing enabled") state := &appState{ window: w, @@ -1420,8 +1422,9 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject { } } - videoPanel := buildVideoPane(state, fyne.NewSize(360, 230), src, updateCover) - metaPanel, metaCoverUpdate := buildMetadataPanel(state, src, fyne.NewSize(360, 150)) + // Make panel sizes responsive with modest minimums to avoid forcing the window beyond the screen + videoPanel := buildVideoPane(state, fyne.NewSize(460, 260), src, updateCover) + metaPanel, metaCoverUpdate := buildMetadataPanel(state, src, fyne.NewSize(0, 200)) updateMetaCover = metaCoverUpdate var formatLabels []string @@ -1537,7 +1540,8 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject { // Settings management for batch operations settingsInfoLabel := widget.NewLabel("Settings persist across videos. Change them anytime to affect all subsequent videos.") - settingsInfoLabel.Wrapping = fyne.TextWrapWord + // Don't wrap - let text scroll or truncate if needed + settingsInfoLabel.Alignment = fyne.TextAlignCenter resetSettingsBtn := widget.NewButton("Reset to Defaults", func() { // Reset to default settings @@ -1569,10 +1573,33 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject { }) resetSettingsBtn.Importance = widget.LowImportance - settingsBox := container.NewVBox( - widget.NewLabelWithStyle("═══ BATCH SETTINGS ═══", fyne.TextAlignCenter, fyne.TextStyle{Bold: true}), + // Create collapsible batch settings section + settingsContent := container.NewVBox( settingsInfoLabel, resetSettingsBtn, + ) + settingsContent.Hide() // Hidden by default + + // Use a pointer to track visibility state + settingsVisible := false + + var toggleSettingsBtn *widget.Button + toggleSettingsBtn = widget.NewButton("Show Batch Settings", func() { + if settingsVisible { + settingsContent.Hide() + toggleSettingsBtn.SetText("Show Batch Settings") + settingsVisible = false + } else { + settingsContent.Show() + toggleSettingsBtn.SetText("Hide Batch Settings") + settingsVisible = true + } + }) + toggleSettingsBtn.Importance = widget.LowImportance + + settingsBox := container.NewVBox( + toggleSettingsBtn, + settingsContent, widget.NewSeparator(), ) @@ -1780,9 +1807,13 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject { simpleOptions, ) + // Avoid nested scrolls capturing wheel events; let the outer page scroll handle overflow. + simpleScrollBox := simpleWithSettings + advancedScrollBox := advancedOptions + tabs := container.NewAppTabs( - container.NewTabItem("Simple", container.NewVScroll(simpleWithSettings)), - container.NewTabItem("Advanced", container.NewVScroll(advancedOptions)), + container.NewTabItem("Simple", simpleScrollBox), + container.NewTabItem("Advanced", advancedScrollBox), ) tabs.SetTabLocation(container.TabLocationTop) @@ -1831,15 +1862,16 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject { } snippetHint := widget.NewLabel("Creates a 20s clip centred on the timeline midpoint.") snippetRow := container.NewHBox(snippetBtn, layout.NewSpacer(), snippetHint) - // Use VSplit to make panels expand vertically and fill available space - leftColumn := container.NewVSplit(videoPanel, metaPanel) - leftColumn.Offset = 0.65 // Video pane gets 65% of space, metadata gets 35% + + // Stack video and metadata directly so metadata sits immediately under the player. + leftColumn := container.NewVBox(videoPanel, metaPanel) + + // Split: left side (video + metadata VSplit) takes 55% | right side (options) takes 45% mainSplit := container.NewHSplit(leftColumn, optionsPanel) - mainSplit.Offset = 0.45 // Give the options panel extra horizontal room - mainArea := container.NewPadded(container.NewVBox( - mainSplit, - snippetRow, - )) + mainSplit.Offset = 0.55 // Video/metadata column gets 55%, options gets 45% + + // Core content now just the split; ancillary controls stack in bottomSection. + mainContent := container.NewMax(mainSplit) resetBtn := widget.NewButton("Reset", func() { tabs.SelectIndex(0) // Select Simple tab @@ -1922,12 +1954,12 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject { addQueueBtn.Enable() } - actionInner := container.NewHBox(resetBtn, activity, statusLabel, layout.NewSpacer(), cancelBtn, addQueueBtn, convertBtn) + leftControls := container.NewHBox(resetBtn) + centerStatus := container.NewHBox(activity, statusLabel) + rightControls := container.NewHBox(cancelBtn, addQueueBtn, convertBtn) + actionInner := container.NewBorder(nil, nil, leftControls, rightControls, centerStatus) actionBar := ui.TintedBar(convertColor, actionInner) - // Wrap mainArea in a scroll container to prevent content from forcing window resize - scrollableMain := container.NewScroll(mainArea) - // Start a UI refresh ticker to update widgets from state while conversion is active // This ensures progress updates even when navigating between modules go func() { @@ -1993,20 +2025,12 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject { // Update stats bar state.updateStatsBar() - // Add stats bar above the action bar at the bottom - bottomSection := container.NewVBox( - state.statsBar, - widget.NewSeparator(), - actionBar, - ) + // Stack status + snippet + actions tightly to avoid dead air, outside the scroll area. + bottomSection := container.NewVBox(state.statsBar, snippetRow, widget.NewSeparator(), actionBar) - return container.NewBorder( - backBar, - bottomSection, - nil, - nil, - scrollableMain, - ) + scrollableMain := container.NewVScroll(mainContent) + + return container.NewBorder(backBar, bottomSection, nil, nil, container.NewMax(scrollableMain)) } func makeLabeledPanel(title, body string, min fyne.Size) *fyne.Container { @@ -2249,11 +2273,13 @@ func buildVideoPane(state *appState, min fyne.Size, src *videoSource, onCover fu img = canvas.NewImageFromResource(nil) } img.FillMode = canvas.ImageFillContain - img.SetMinSize(fyne.NewSize(targetWidth-28, targetHeight-40)) + // Let the image grow with the available stage size + img.SetMinSize(fyne.NewSize(targetWidth, targetHeight)) stage := canvas.NewRectangle(utils.MustHex("#0F1529")) stage.CornerRadius = 6 - stage.SetMinSize(fyne.NewSize(targetWidth-12, targetHeight-12)) - videoStage := container.NewMax(stage, container.NewPadded(container.NewCenter(img))) + stage.SetMinSize(fyne.NewSize(targetWidth, targetHeight)) + // Overlay the image directly so it fills the stage while preserving aspect. + videoStage := container.NewMax(stage, img) coverBtn := utils.MakeIconButton("⌾", "Set current frame as cover art", func() { path, err := state.captureCoverFromCurrent() @@ -2454,7 +2480,7 @@ func buildVideoPane(state *appState, min fyne.Size, src *videoSource, onCover fu stack := container.NewVBox( container.NewPadded(videoWithOverlay), ) - return container.NewMax(outer, container.NewCenter(container.NewPadded(stack))) + return container.NewMax(outer, container.NewPadded(stack)) } type playSession struct { diff --git a/screenshot.png b/screenshot.png new file mode 100644 index 0000000..6848c61 Binary files /dev/null and b/screenshot.png differ diff --git a/scripts/build.sh b/scripts/build.sh index 6b03a9e..c9d1dab 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -38,7 +38,9 @@ echo "✓ Dependencies verified" echo "" echo "🔨 Building VideoTools..." -if CGO_ENABLED=0 go build -o "$BUILD_OUTPUT" .; then +# Fyne needs cgo for GLFW/OpenGL bindings; build with CGO enabled. +export CGO_ENABLED=1 +if go build -o "$BUILD_OUTPUT" .; then echo "✓ Build successful!" echo "" echo "════════════════════════════════════════════════════════════════"