From c6c33ecfed20917adc3c559812a8dc08b03e3150 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Fri, 7 Jul 2023 12:54:20 -0700 Subject: [PATCH] begin reuse of widgets across pages --- ui/browsing/albumpage.go | 59 ++++++++++++++++++++++++---------- ui/browsing/artistpage.go | 60 +++++++++++++++++++++++------------ ui/browsing/browsingpane.go | 8 +++-- ui/browsing/favoritespage.go | 2 +- ui/browsing/nowplayingpage.go | 12 ++++--- ui/browsing/playlistpage.go | 6 ++-- ui/browsing/router.go | 19 ++++++----- ui/browsing/trackspage.go | 8 +++-- ui/util/widgetcache.go | 50 +++++++++++++++++++++++++++++ ui/widgets/tracklist.go | 29 +++++++++++------ 10 files changed, 186 insertions(+), 67 deletions(-) create mode 100644 ui/util/widgetcache.go diff --git a/ui/browsing/albumpage.go b/ui/browsing/albumpage.go index a7ffa91..c5d9175 100644 --- a/ui/browsing/albumpage.go +++ b/ui/browsing/albumpage.go @@ -33,29 +33,32 @@ type AlbumPage struct { } type albumPageState struct { - albumID string - sort widgets.TracklistSort - cfg *backend.AlbumPageConfig - mp mediaprovider.MediaProvider - pm *backend.PlaybackManager - im *backend.ImageManager - contr *controller.Controller + albumID string + sort widgets.TracklistSort + cfg *backend.AlbumPageConfig + widgetCache *util.WidgetCache + mp mediaprovider.MediaProvider + pm *backend.PlaybackManager + im *backend.ImageManager + contr *controller.Controller } func NewAlbumPage( albumID string, cfg *backend.AlbumPageConfig, + cache *util.WidgetCache, pm *backend.PlaybackManager, mp mediaprovider.MediaProvider, im *backend.ImageManager, contr *controller.Controller, ) *AlbumPage { - return newAlbumPage(albumID, cfg, pm, mp, im, contr, widgets.TracklistSort{}) + return newAlbumPage(albumID, cfg, cache, pm, mp, im, contr, widgets.TracklistSort{}) } func newAlbumPage( albumID string, cfg *backend.AlbumPageConfig, + cache *util.WidgetCache, pm *backend.PlaybackManager, mp mediaprovider.MediaProvider, im *backend.ImageManager, @@ -64,16 +67,23 @@ func newAlbumPage( ) *AlbumPage { a := &AlbumPage{ albumPageState: albumPageState{ - albumID: albumID, - cfg: cfg, - pm: pm, - mp: mp, - im: im, - contr: contr, + albumID: albumID, + cfg: cfg, + widgetCache: cache, + pm: pm, + mp: mp, + im: im, + contr: contr, }, } a.ExtendBaseWidget(a) - a.header = NewAlbumPageHeader(a) + if h := a.widgetCache.Obtain(util.WidgetTypeAlbumPageHeader); h != nil { + a.header = h.(*AlbumPageHeader) + a.header.Clear() + } else { + a.header = NewAlbumPageHeader(a) + } + a.header.page = a a.tracklist = widgets.NewTracklist(nil) a.tracklist.SetVisibleColumns(a.cfg.TracklistColumns) a.tracklist.SetSorting(sort) @@ -97,6 +107,7 @@ func (a *AlbumPage) CreateRenderer() fyne.WidgetRenderer { func (a *AlbumPage) Save() SavedPage { s := a.albumPageState s.sort = a.tracklist.Sorting() + a.widgetCache.Release(util.WidgetTypeAlbumPageHeader, a.header) return &s } @@ -134,7 +145,9 @@ func (a *AlbumPage) load() { return } a.header.Update(album, a.im) - a.tracklist.ShowDiscNumber = album.Tracks[0].DiscNumber != album.Tracks[len(album.Tracks)-1].DiscNumber + a.tracklist.Options = widgets.TracklistOptions{ + ShowDiscNumber: album.Tracks[0].DiscNumber != album.Tracks[len(album.Tracks)-1].DiscNumber, + } a.tracks = album.Tracks a.tracklist.SetTracks(album.Tracks) a.tracklist.SetNowPlaying(a.nowPlayingID) @@ -256,6 +269,18 @@ func (a *AlbumPageHeader) Update(album *mediaprovider.AlbumWithTracks, im *backe }() } +func (a *AlbumPageHeader) Clear() { + a.albumID = "" + a.coverID = "" + a.artistID = "" + a.titleLabel.Segments[0].(*widget.TextSegment).Text = "" + a.artistLabel.SetText("") + a.genreLabel.SetText("") + a.miscLabel.SetText("") + a.toggleFavButton.IsFavorited = false + a.cover.Image.Image = nil +} + func (a *AlbumPageHeader) toggleFavorited() { params := mediaprovider.RatingFavoriteParameters{AlbumIDs: []string{a.albumID}} a.page.mp.SetFavorite(params, a.toggleFavButton.IsFavorited) @@ -283,5 +308,5 @@ func formatMiscLabelStr(a *mediaprovider.AlbumWithTracks) string { } func (s *albumPageState) Restore() Page { - return newAlbumPage(s.albumID, s.cfg, s.pm, s.mp, s.im, s.contr, s.sort) + return newAlbumPage(s.albumID, s.cfg, s.widgetCache, s.pm, s.mp, s.im, s.contr, s.sort) } diff --git a/ui/browsing/artistpage.go b/ui/browsing/artistpage.go index 6a03606..f20e15b 100644 --- a/ui/browsing/artistpage.go +++ b/ui/browsing/artistpage.go @@ -28,11 +28,12 @@ type artistPageState struct { activeView int trackSort widgets.TracklistSort - cfg *backend.ArtistPageConfig - pm *backend.PlaybackManager - mp mediaprovider.MediaProvider - im *backend.ImageManager - contr *controller.Controller + widgetCache *util.WidgetCache + cfg *backend.ArtistPageConfig + pm *backend.PlaybackManager + mp mediaprovider.MediaProvider + im *backend.ImageManager + contr *controller.Controller } type ArtistPage struct { @@ -49,27 +50,34 @@ type ArtistPage struct { container *fyne.Container } -func NewArtistPage(artistID string, cfg *backend.ArtistPageConfig, pm *backend.PlaybackManager, mp mediaprovider.MediaProvider, im *backend.ImageManager, contr *controller.Controller) *ArtistPage { +func NewArtistPage(artistID string, cfg *backend.ArtistPageConfig, cache *util.WidgetCache, pm *backend.PlaybackManager, mp mediaprovider.MediaProvider, im *backend.ImageManager, contr *controller.Controller) *ArtistPage { activeView := 0 if cfg.InitialView == "Top Tracks" { activeView = 1 } - return newArtistPage(artistID, cfg, pm, mp, im, contr, activeView, widgets.TracklistSort{}) + return newArtistPage(artistID, cfg, cache, pm, mp, im, contr, activeView, widgets.TracklistSort{}) } -func newArtistPage(artistID string, cfg *backend.ArtistPageConfig, pm *backend.PlaybackManager, mp mediaprovider.MediaProvider, im *backend.ImageManager, contr *controller.Controller, activeView int, sort widgets.TracklistSort) *ArtistPage { +func newArtistPage(artistID string, cfg *backend.ArtistPageConfig, cache *util.WidgetCache, pm *backend.PlaybackManager, mp mediaprovider.MediaProvider, im *backend.ImageManager, contr *controller.Controller, activeView int, sort widgets.TracklistSort) *ArtistPage { a := &ArtistPage{artistPageState: artistPageState{ - artistID: artistID, - cfg: cfg, - pm: pm, - mp: mp, - im: im, - contr: contr, - activeView: activeView, - trackSort: sort, + artistID: artistID, + cfg: cfg, + widgetCache: cache, + pm: pm, + mp: mp, + im: im, + contr: contr, + activeView: activeView, + trackSort: sort, }} a.ExtendBaseWidget(a) - a.header = NewArtistPageHeader(a) + if h := a.widgetCache.Obtain(util.WidgetTypeArtistPageHeader); h != nil { + a.header = h.(*ArtistPageHeader) + } else { + a.header = NewArtistPageHeader(a) + } + a.header.artistPage = a + a.header.Clear() if img, ok := im.GetCachedArtistImage(artistID); ok { a.header.artistImage.SetImage(img, true /*tappable*/) } @@ -116,6 +124,7 @@ func (a *ArtistPage) Save() SavedPage { if a.tracklistCtr != nil { s.trackSort = a.tracklistCtr.Objects[0].(*widgets.Tracklist).Sorting() } + a.widgetCache.Release(util.WidgetTypeArtistPageHeader, a.header) return &s } @@ -190,7 +199,7 @@ func (a *ArtistPage) showTopTracks() { return } tl := widgets.NewTracklist(ts) - tl.AutoNumber = true + tl.Options = widgets.TracklistOptions{AutoNumber: true} tl.SetVisibleColumns(a.cfg.TracklistColumns) tl.SetSorting(a.trackSort) tl.OnVisibleColumnsChanged = func(cols []string) { @@ -228,9 +237,11 @@ func (a *ArtistPage) CreateRenderer() fyne.WidgetRenderer { } func (s *artistPageState) Restore() Page { - return newArtistPage(s.artistID, s.cfg, s.pm, s.mp, s.im, s.contr, s.activeView, s.trackSort) + return newArtistPage(s.artistID, s.cfg, s.widgetCache, s.pm, s.mp, s.im, s.contr, s.activeView, s.trackSort) } +const artistBioNotAvailableStr = "Artist biography not available." + type ArtistPageHeader struct { widget.BaseWidget @@ -250,7 +261,7 @@ func NewArtistPageHeader(page *ArtistPage) *ArtistPageHeader { a := &ArtistPageHeader{ artistPage: page, titleDisp: widget.NewRichTextWithText(""), - biographyDisp: widget.NewRichTextWithText("Artist biography not available."), + biographyDisp: widget.NewRichTextWithText(artistBioNotAvailableStr), similarArtists: container.NewHBox(), } a.titleDisp.Segments[0].(*widget.TextSegment).Style = widget.RichTextStyle{ @@ -273,6 +284,15 @@ func NewArtistPageHeader(page *ArtistPage) *ArtistPageHeader { return a } +func (a *ArtistPageHeader) Clear() { + a.artistID = "" + a.favoriteBtn.IsFavorited = false + a.titleDisp.Segments[0].(*widget.TextSegment).Text = "" + a.biographyDisp.Segments[0].(*widget.TextSegment).Text = artistBioNotAvailableStr + a.similarArtists.RemoveAll() + a.artistImage.SetImage(nil, false) +} + func (a *ArtistPageHeader) Update(artist *mediaprovider.ArtistWithAlbums) { if artist == nil { return diff --git a/ui/browsing/browsingpane.go b/ui/browsing/browsingpane.go index c6c2621..9147ae8 100644 --- a/ui/browsing/browsingpane.go +++ b/ui/browsing/browsingpane.go @@ -233,17 +233,21 @@ func (b *BrowsingPane) GoHome() { func (b *BrowsingPane) GoBack() { if b.historyIdx > 0 { + // due to widget reuse between pages, + // we must create the new page before calling addPageToHistory + p := b.history[b.historyIdx-1].Restore() b.addPageToHistory(b.curPage, false) b.historyIdx -= 2 - b.doSetPage(b.history[b.historyIdx].Restore()) + b.doSetPage(p) b.updateHistoryButtons() } } func (b *BrowsingPane) GoForward() { if b.historyIdx < len(b.history)-1 { + p := b.history[b.historyIdx+1].Restore() b.addPageToHistory(b.curPage, false) - b.doSetPage(b.history[b.historyIdx].Restore()) + b.doSetPage(p) b.updateHistoryButtons() } } diff --git a/ui/browsing/favoritespage.go b/ui/browsing/favoritespage.go index a4abaad..f21d6f3 100644 --- a/ui/browsing/favoritespage.go +++ b/ui/browsing/favoritespage.go @@ -326,7 +326,7 @@ func (a *FavoritesPage) onShowFavoriteSongs() { return } tracklist := widgets.NewTracklist(fav.Tracks) - tracklist.AutoNumber = true + tracklist.Options = widgets.TracklistOptions{AutoNumber: true} tracklist.SetVisibleColumns(a.cfg.TracklistColumns) tracklist.SetSorting(a.trackSort) tracklist.OnVisibleColumnsChanged = func(cols []string) { diff --git a/ui/browsing/nowplayingpage.go b/ui/browsing/nowplayingpage.go index dfe280c..c40f30e 100644 --- a/ui/browsing/nowplayingpage.go +++ b/ui/browsing/nowplayingpage.go @@ -62,14 +62,16 @@ func NewNowPlayingPage( a.tracklist.OnVisibleColumnsChanged = func(cols []string) { a.conf.TracklistColumns = cols } - a.tracklist.AutoNumber = true - a.tracklist.DisablePlaybackMenu = true + a.tracklist.Options = widgets.TracklistOptions{ + AutoNumber: true, + DisablePlaybackMenu: true, + AuxiliaryMenuItems: []*fyne.MenuItem{ + fyne.NewMenuItem("Remove from queue", a.onRemoveSelectedFromQueue), + }, + } contr.ConnectTracklistActions(a.tracklist) // override the default OnPlayTrackAt handler b/c we don't need to re-load the tracks into the queue a.tracklist.OnPlayTrackAt = a.onPlayTrackAt - a.tracklist.AuxiliaryMenuItems = []*fyne.MenuItem{ - fyne.NewMenuItem("Remove from queue", a.onRemoveSelectedFromQueue), - } a.title = widget.NewRichTextWithText("Now Playing") a.title.Segments[0].(*widget.TextSegment).Style.SizeName = widget.RichTextStyleHeading.SizeName a.statusLabel = widget.NewRichTextWithText("Stopped") diff --git a/ui/browsing/playlistpage.go b/ui/browsing/playlistpage.go index 39d5182..b113b3a 100644 --- a/ui/browsing/playlistpage.go +++ b/ui/browsing/playlistpage.go @@ -77,8 +77,10 @@ func newPlaylistPage( fyne.NewMenuItem("Move down", a.onMoveSelectedDown), fyne.NewMenuItem("Move to bottom", a.onMoveSelectedToBottom), }...) - a.tracklist.AuxiliaryMenuItems = []*fyne.MenuItem{reorderMenu, - fyne.NewMenuItem("Remove from playlist", a.onRemoveSelectedFromPlaylist)} + a.tracklist.Options = widgets.TracklistOptions{ + AuxiliaryMenuItems: []*fyne.MenuItem{reorderMenu, + fyne.NewMenuItem("Remove from playlist", a.onRemoveSelectedFromPlaylist)}, + } // connect tracklist actions a.contr.ConnectTracklistActions(a.tracklist) diff --git a/ui/browsing/router.go b/ui/browsing/router.go index c91cf6a..9a6bf85 100644 --- a/ui/browsing/router.go +++ b/ui/browsing/router.go @@ -3,6 +3,7 @@ package browsing import ( "github.com/dweymouth/supersonic/backend" "github.com/dweymouth/supersonic/ui/controller" + "github.com/dweymouth/supersonic/ui/util" ) type NavigationHandler interface { @@ -11,16 +12,18 @@ type NavigationHandler interface { } type Router struct { - App *backend.App - Controller *controller.Controller - Nav NavigationHandler + App *backend.App + Controller *controller.Controller + Nav NavigationHandler + widgetCache util.WidgetCache } func NewRouter(app *backend.App, controller *controller.Controller, nav NavigationHandler) Router { r := Router{ - App: app, - Controller: controller, - Nav: nav, + App: app, + Controller: controller, + Nav: nav, + widgetCache: util.NewWidgetCache(), } return r } @@ -28,11 +31,11 @@ func NewRouter(app *backend.App, controller *controller.Controller, nav Navigati func (r Router) CreatePage(rte controller.Route) Page { switch rte.Page { case controller.Album: - return NewAlbumPage(rte.Arg, &r.App.Config.AlbumPage, r.App.PlaybackManager, r.App.ServerManager.Server, r.App.ImageManager, r.Controller) + return NewAlbumPage(rte.Arg, &r.App.Config.AlbumPage, &r.widgetCache, r.App.PlaybackManager, r.App.ServerManager.Server, r.App.ImageManager, r.Controller) case controller.Albums: return NewAlbumsPage(&r.App.Config.AlbumsPage, r.Controller, r.App.PlaybackManager, r.App.ServerManager.Server, r.App.ImageManager) case controller.Artist: - return NewArtistPage(rte.Arg, &r.App.Config.ArtistPage, r.App.PlaybackManager, r.App.ServerManager.Server, r.App.ImageManager, r.Controller) + return NewArtistPage(rte.Arg, &r.App.Config.ArtistPage, &r.widgetCache, r.App.PlaybackManager, r.App.ServerManager.Server, r.App.ImageManager, r.Controller) case controller.Artists: return NewArtistsPage(r.Controller, r.App.PlaybackManager, r.App.ServerManager.Server, r.App.ImageManager) case controller.Favorites: diff --git a/ui/browsing/trackspage.go b/ui/browsing/trackspage.go index 2c21197..415e228 100644 --- a/ui/browsing/trackspage.go +++ b/ui/browsing/trackspage.go @@ -44,8 +44,10 @@ func NewTracksPage(contr *controller.Controller, conf *backend.TracksPageConfig, t.ExtendBaseWidget(t) t.tracklist = widgets.NewTracklist(nil) - t.tracklist.DisableSorting = true - t.tracklist.AutoNumber = true + t.tracklist.Options = widgets.TracklistOptions{ + DisableSorting: true, + AutoNumber: true, + } t.tracklist.SetVisibleColumns(conf.TracklistColumns) t.tracklist.OnVisibleColumnsChanged = func(cols []string) { t.conf.TracklistColumns = cols @@ -119,7 +121,7 @@ func (t *TracksPage) OnSearched(query string) { func (t *TracksPage) doSearch(query string) { if t.searchTracklist == nil { t.searchTracklist = widgets.NewTracklist(nil) - t.searchTracklist.AutoNumber = true + t.searchTracklist.Options = widgets.TracklistOptions{AutoNumber: true} t.searchTracklist.SetVisibleColumns(t.conf.TracklistColumns) t.searchTracklist.SetNowPlaying(t.nowPlayingID) t.searchTracklist.OnVisibleColumnsChanged = func(cols []string) { diff --git a/ui/util/widgetcache.go b/ui/util/widgetcache.go new file mode 100644 index 0000000..c83bd97 --- /dev/null +++ b/ui/util/widgetcache.go @@ -0,0 +1,50 @@ +package util + +import ( + "time" + + "fyne.io/fyne/v2" +) + +type WidgetType string + +const ( + WidgetTypeAlbumPageHeader WidgetType = "AlbumPageHeader" + WidgetTypeArtistPageHeader WidgetType = "ArtistPageHeader" +) + +type WidgetCache struct { + cache map[WidgetType][]cachedWidget +} + +type cachedWidget struct { + widget fyne.CanvasObject + releasedAt int64 // unixMillis +} + +func NewWidgetCache() WidgetCache { + return WidgetCache{ + cache: make(map[WidgetType][]cachedWidget), + } +} + +func (w *WidgetCache) Obtain(typ WidgetType) fyne.CanvasObject { + var widget fyne.CanvasObject + if ws, ok := w.cache[typ]; ok && len(ws) > 0 { + i := len(ws) - 1 + widget = ws[i].widget + ws[i].widget = nil + w.cache[typ] = ws[:i] + } + return widget +} + +func (w *WidgetCache) Release(typ WidgetType, wid fyne.CanvasObject) { + if _, ok := w.cache[typ]; !ok { + w.cache[typ] = make([]cachedWidget, 0) + } + w.cache[typ] = append(w.cache[typ], cachedWidget{ + widget: wid, + releasedAt: time.Now().UnixMilli(), + }) +} diff --git a/ui/widgets/tracklist.go b/ui/widgets/tracklist.go index c1e89bc..45d18a0 100644 --- a/ui/widgets/tracklist.go +++ b/ui/widgets/tracklist.go @@ -47,9 +47,7 @@ type TracklistSort struct { ColumnName string } -type Tracklist struct { - widget.BaseWidget - +type TracklistOptions struct { // AutoNumber sets whether to auto-number the tracks 1..N in display order, // or to use the number from the track's metadata AutoNumber bool @@ -68,6 +66,12 @@ type Tracklist struct { // Disables sorting the tracklist by clicking individual columns. DisableSorting bool +} + +type Tracklist struct { + widget.BaseWidget + + Options TracklistOptions // user action callbacks OnPlayTrackAt func(int) @@ -141,7 +145,7 @@ func NewTracklist(tracks []*mediaprovider.Track) *Tracklist { tr := item.(*TrackRow) tr.trackIdx = itemID i := -1 // signal that we want to display the actual track num. - if t.AutoNumber { + if t.Options.AutoNumber { i = itemID + 1 } tr.Update(t.trackModelAt(itemID), i) @@ -153,6 +157,13 @@ func NewTracklist(tracks []*mediaprovider.Track) *Tracklist { return t } +func (t *Tracklist) Reset() { + t.Clear() + t.Options = TracklistOptions{} + t.ctxMenu = nil + t.SetSorting(TracklistSort{}) +} + func (t *Tracklist) buildHeader() { t.hdr = NewListHeader([]ListColumn{ {Text: "#", Alignment: fyne.TextAlignTrailing, CanToggleVisible: false}, @@ -331,7 +342,7 @@ func (t *Tracklist) CreateRenderer() fyne.WidgetRenderer { } func (t *Tracklist) Refresh() { - t.hdr.DisableSorting = t.DisableSorting + t.hdr.DisableSorting = t.Options.DisableSorting t.BaseWidget.Refresh() } @@ -487,7 +498,7 @@ func (t *Tracklist) onShowContextMenu(e *fyne.PointEvent, trackIdx int) { t.list.Refresh() if t.ctxMenu == nil { t.ctxMenu = fyne.NewMenu("") - if !t.DisablePlaybackMenu { + if !t.Options.DisablePlaybackMenu { t.ctxMenu.Items = append(t.ctxMenu.Items, fyne.NewMenuItem("Play", func() { if t.OnPlaySelection != nil { @@ -530,9 +541,9 @@ func (t *Tracklist) onShowContextMenu(e *fyne.PointEvent, trackIdx int) { t.onSetRatings(t.selectedTracks(), rating, true) }) t.ctxMenu.Items = append(t.ctxMenu.Items, ratingMenu) - if len(t.AuxiliaryMenuItems) > 0 { + if len(t.Options.AuxiliaryMenuItems) > 0 { t.ctxMenu.Items = append(t.ctxMenu.Items, fyne.NewMenuItemSeparator()) - t.ctxMenu.Items = append(t.ctxMenu.Items, t.AuxiliaryMenuItems...) + t.ctxMenu.Items = append(t.ctxMenu.Items, t.Options.AuxiliaryMenuItems...) } } widget.ShowPopUpMenuAtPosition(t.ctxMenu, fyne.CurrentApp().Driver().CanvasForObject(t), e.AbsolutePosition) @@ -754,7 +765,7 @@ func (t *TrackRow) Update(tm *trackModel, rowNum int) { var str string if rowNum < 0 { rowNum = tr.TrackNumber - if t.tracklist.ShowDiscNumber { + if t.tracklist.Options.ShowDiscNumber { discNum = tr.DiscNumber } }