From 5aa8d36acea1ded894756c148d34ac1691c18afe Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Sat, 1 Nov 2025 11:26:58 -0700 Subject: [PATCH] add lyrics to sidebar --- ui/mainwindow.go | 6 +-- ui/sidebar.go | 107 +++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 103 insertions(+), 10 deletions(-) diff --git a/ui/mainwindow.go b/ui/mainwindow.go index 206286a..4fa3120 100644 --- a/ui/mainwindow.go +++ b/ui/mainwindow.go @@ -69,7 +69,7 @@ func NewMainWindow(fyneApp fyne.App, appName, displayAppName, appVersion string, } m.Controller = controller.New(app, appVersion, m.Window) m.BrowsingPane = browsing.NewBrowsingPane(app.PlaybackManager, m.Controller, func() { m.Router.NavigateTo(m.StartupPage()) }) - m.Sidebar = NewSidebar(m.Controller, m.App.ImageManager) + m.Sidebar = NewSidebar(m.Controller, m.App.PlaybackManager, m.App.ImageManager, m.App.LyricsManager) m.ToastOverlay = NewToastOverlay() m.Router = browsing.NewRouter(app, m.Controller, m.BrowsingPane) goHomeFn := func() { m.Router.NavigateTo(m.StartupPage()) } @@ -167,12 +167,12 @@ func NewMainWindow(fyneApp fyne.App, appName, displayAppName, appVersion string, func (m *MainWindow) UpdateOnTrackChange(item mediaprovider.MediaItem) { if item == nil { m.Window.SetTitle(res.DisplayName) - m.Sidebar.SetNowPlaying("") + m.Sidebar.SetNowPlaying(nil) return } meta := item.Metadata() - m.Sidebar.SetNowPlaying(meta.ID) + m.Sidebar.SetNowPlaying(item) artistDisp := "" if tr, ok := item.(*mediaprovider.Track); ok { artistDisp = " – " + strings.Join(tr.ArtistNames, ", ") diff --git a/ui/sidebar.go b/ui/sidebar.go index f3fa7a9..aa5bf3d 100644 --- a/ui/sidebar.go +++ b/ui/sidebar.go @@ -15,15 +15,58 @@ import ( type Sidebar struct { widget.BaseWidget - queueList *widgets.PlayQueueList + lm *backend.LyricsManager + + queueList *widgets.PlayQueueList + lyricsViewer *widgets.LyricsViewer + lyricsLoading *widgets.LoadingDots + tabs *container.AppTabs + + nowPlaying mediaprovider.MediaItem + nowPlayingID string + curLyrics *mediaprovider.Lyrics + curLyricsID string + lastPlayPos float64 } -func NewSidebar(contr *controller.Controller, im *backend.ImageManager) *Sidebar { +func NewSidebar(contr *controller.Controller, pm *backend.PlaybackManager, im *backend.ImageManager, lm *backend.LyricsManager) *Sidebar { s := &Sidebar{ + lm: lm, queueList: widgets.NewPlayQueueList(im, false), } s.queueList.Reorderable = true contr.ConnectPlayQueuelistActions(s.queueList) + + s.lyricsViewer = widgets.NewLyricsViewer(func(i int) { + if s.curLyrics != nil && len(s.curLyrics.Lines) > i-1 { + time := s.curLyrics.Lines[i-1].Start + pm.SeekSeconds(time) + } + }) + s.lyricsLoading = widgets.NewLoadingDots() + + s.tabs = container.NewAppTabs( + container.NewTabItem(lang.L("Play Queue"), container.NewPadded(s.queueList)), + container.NewTabItem(lang.L("Lyrics"), container.NewStack( + s.lyricsViewer, + container.NewCenter(s.lyricsLoading))), + ) + s.tabs.OnSelected = func(*container.TabItem) { + idx := s.tabs.SelectedIndex() + if idx == 1 /*lyrics*/ { + s.updateLyrics() + } + } + + pm.OnPlayTimeUpdate(func(curTime, _ float64, seeked bool) { + s.lastPlayPos = curTime + if seeked { + fyne.Do(func() { s.lyricsViewer.OnSeeked(curTime) }) + } else { + fyne.Do(func() { s.lyricsViewer.UpdatePlayPos(curTime) }) + } + }) + s.ExtendBaseWidget(s) return s } @@ -32,15 +75,65 @@ func (s *Sidebar) SetQueueTracks(items []mediaprovider.MediaItem) { s.queueList.SetItems(items) } -func (s *Sidebar) SetNowPlaying(itemID string) { - s.queueList.SetNowPlaying(itemID) +func (s *Sidebar) SetNowPlaying(item mediaprovider.MediaItem) { + s.nowPlaying = item + id := "" + if item != nil { + id = item.Metadata().ID + } + s.queueList.SetNowPlaying(id) + s.nowPlayingID = id + if s.tabs.SelectedIndex() == 1 /*lyrics*/ { + s.updateLyrics() + } +} + +// TODO: this is more or less copy-paste from the Now Playing page +// refactor this shared logic somewhere else? +func (s *Sidebar) updateLyrics() { + if s.nowPlayingID == s.curLyricsID { + if s.nowPlayingID != "" { + // just need to sync the current time + s.lyricsViewer.OnSeeked(s.lastPlayPos) + return + } + } + if s.nowPlaying == nil || s.nowPlaying.Metadata().Type == mediaprovider.MediaItemTypeRadioStation { + s.lyricsViewer.SetLyrics(nil) + s.curLyrics = nil + s.curLyricsID = "" + return + } + s.curLyricsID = s.nowPlayingID + s.lyricsLoading.Start() + // set the widget to an empty (not nil) lyric during fetch + // to keep it from showing "Lyrics not available" + s.lyricsViewer.DisableTapToSeek() + s.lyricsViewer.SetLyrics(&mediaprovider.Lyrics{ + Synced: true, + Lines: []mediaprovider.LyricLine{{Text: ""}}, + }) + tr, _ := s.nowPlaying.(*mediaprovider.Track) + + s.lm.FetchLyricsAsync(tr, func(id string, lyrics *mediaprovider.Lyrics) { + if id != s.nowPlayingID { + return + } + fyne.Do(func() { + s.lyricsLoading.Stop() + s.lyricsViewer.EnableTapToSeek() + s.lyricsViewer.SetLyrics(lyrics) + s.curLyrics = lyrics + if lyrics != nil { + s.lyricsViewer.OnSeeked(s.lastPlayPos) + } + }) + }) } func (s *Sidebar) CreateRenderer() fyne.WidgetRenderer { return widget.NewSimpleRenderer(container.NewStack( theme.NewThemedRectangle(theme.ColorNamePageBackground), - container.NewAppTabs( - container.NewTabItem(lang.L("Play Queue"), container.NewPadded(s.queueList)), - ), + container.NewPadded(s.tabs), )) }