From 7fe4f78b94aea99c182c40671cc52381698087f4 Mon Sep 17 00:00:00 2001 From: Stu Date: Thu, 4 Dec 2025 06:12:02 -0500 Subject: [PATCH] Add compare view and drop-to-play only handler --- main.go | 163 +++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 162 insertions(+), 1 deletion(-) diff --git a/main.go b/main.go index 72450fc..4391519 100644 --- a/main.go +++ b/main.go @@ -178,6 +178,8 @@ type appState struct { playerPaused bool playerPos float64 playerLast time.Time + compareSess1 *playSession + compareSess2 *playSession progressQuit chan struct{} convertCancel context.CancelFunc playerSurf *playerSurface @@ -415,10 +417,144 @@ func (s *appState) showMainMenu() { s.showPlayerView() } +// showCompareView renders a simple side-by-side player for the first two loaded videos. +func (s *appState) showCompareView() { + s.stopPreview() + s.stopPlayer() + s.stopCompareSessions() + s.active = "compare" + + header := widget.NewLabelWithStyle("Compare", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}) + backBtn := widget.NewButton("Back to Player", func() { s.showPlayerView() }) + headerRow := container.NewHBox(header, layout.NewSpacer(), backBtn) + + if len(s.loadedVideos) < 2 { + icon := canvas.NewText("⬆", utils.MustHex("#4CE870")) + icon.TextStyle = fyne.TextStyle{Monospace: true, Bold: true} + icon.TextSize = 28 + msg := widget.NewLabel("Load at least two videos to compare.") + msg.Alignment = fyne.TextAlignCenter + s.setContent(container.NewBorder(container.NewPadded(headerRow), nil, nil, nil, container.NewCenter(container.NewVBox(container.NewCenter(icon), container.NewCenter(msg))))) + return + } + + src1 := s.loadedVideos[0] + src2 := s.loadedVideos[1] + + // Left player + left := s.buildComparePane(src1, func() { s.stopCompareSessions() }, func(ps *playSession) { s.compareSess1 = ps }) + // Right player + right := s.buildComparePane(src2, func() { s.stopCompareSessions() }, func(ps *playSession) { s.compareSess2 = ps }) + + body := container.NewGridWithColumns(2, left, right) + s.setContent(container.NewBorder(container.NewPadded(headerRow), nil, nil, nil, container.NewPadded(body))) +} + +// buildComparePane builds a simple player pane for compare view. +func (s *appState) buildComparePane(src *videoSource, onStop func(), setSess func(*playSession)) fyne.CanvasObject { + stageBG := canvas.NewRectangle(utils.MustHex("#0F1529")) + stageBG.SetMinSize(fyne.NewSize(640, 360)) + videoImg := canvas.NewImageFromResource(nil) + videoImg.FillMode = canvas.ImageFillContain + stage := container.NewMax(stageBG, videoImg) + + currentTime := widget.NewLabel("0:00") + totalTime := widget.NewLabel(src.DurationString()) + totalTime.Alignment = fyne.TextAlignTrailing + slider := widget.NewSlider(0, math.Max(1, src.Duration)) + slider.Step = 0.5 + var updatingProgress bool + var sess *playSession + + updateProgress := func(val float64) { + fyne.Do(func() { + updatingProgress = true + currentTime.SetText(formatClock(val)) + slider.SetValue(val) + updatingProgress = false + }) + } + + ensureSession := func() *playSession { + if sess == nil { + ps := newPlaySession(src.Path, src.Width, src.Height, src.FrameRate, 640, 360, updateProgress, videoImg) + ps.SetVolume(100) + setSess(ps) + sess = ps + } + return sess + } + + slider.OnChanged = func(val float64) { + if updatingProgress { + return + } + updateProgress(val) + if ps := ensureSession(); ps != nil { + ps.Seek(val) + } + } + + playBtn := utils.MakeIconButton("▶/⏸", "Play/Pause", func() { + if ps := ensureSession(); ps != nil { + if ps.paused { + ps.Play() + ps.paused = false + } else { + ps.Pause() + ps.paused = true + } + } + }) + + var volIcon *widget.Button + volIcon = utils.MakeIconButton("🔊", "Mute/Unmute", func() { + if ps := ensureSession(); ps != nil { + if ps.muted || ps.volume <= 0 { + ps.SetVolume(50) + } else { + ps.SetVolume(0) + } + if ps.muted || ps.volume <= 0 { + volIcon.SetText("🔇") + } else { + volIcon.SetText("🔊") + } + } + }) + + volSlider := widget.NewSlider(0, 100) + volSlider.Step = 1 + volSlider.Value = 100 + volSlider.OnChanged = func(val float64) { + if ps := ensureSession(); ps != nil { + ps.SetVolume(val) + if ps.muted || ps.volume <= 0 { + volIcon.SetText("🔇") + } else { + volIcon.SetText("🔊") + } + } + } + + progressBar := container.NewBorder(nil, nil, currentTime, totalTime, container.NewMax(slider)) + controlRow := container.NewHBox(playBtn, layout.NewSpacer(), volIcon, container.NewMax(volSlider)) + + title := widget.NewLabelWithStyle(filepath.Base(src.Path), fyne.TextAlignLeading, fyne.TextStyle{Bold: true}) + + return container.NewVBox( + title, + container.NewPadded(stage), + container.NewPadded(progressBar), + container.NewPadded(controlRow), + ) +} + // showPlayerView renders the player-focused UI with a lightweight playlist. func (s *appState) showPlayerView() { s.stopPreview() s.stopPlayer() + s.stopCompareSessions() s.active = "player" header := widget.NewLabelWithStyle("VT Player", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}) @@ -462,7 +598,19 @@ func (s *appState) showPlayerView() { }) clearList.Importance = widget.LowImportance - controlsBar := container.NewHBox(openFile, addFolder, clearList, layout.NewSpacer()) + var compareBtn *widget.Button + if len(s.loadedVideos) >= 2 { + compareBtn = widget.NewButton("Compare View", func() { + s.showCompareView() + }) + } + + barItems := []fyne.CanvasObject{openFile, addFolder, clearList} + if compareBtn != nil { + barItems = append(barItems, compareBtn) + } + barItems = append(barItems, layout.NewSpacer()) + controlsBar := container.NewHBox(barItems...) // Player area var playerArea fyne.CanvasObject @@ -675,6 +823,7 @@ func (s *appState) showPlayerView() { func (s *appState) showQueue() { s.stopPreview() s.stopPlayer() + s.stopCompareSessions() s.lastModule = s.active s.active = "queue" s.refreshQueueView() @@ -1587,6 +1736,7 @@ func (s *appState) shutdown() { } s.stopPlayer() + s.stopCompareSessions() if s.player != nil { s.player.Close() } @@ -1605,6 +1755,17 @@ func (s *appState) stopPlayer() { s.playerPaused = true } +func (s *appState) stopCompareSessions() { + if s.compareSess1 != nil { + s.compareSess1.Stop() + s.compareSess1 = nil + } + if s.compareSess2 != nil { + s.compareSess2.Stop() + s.compareSess2 = nil + } +} + func main() { logging.Init() defer logging.Close()