From c6c33ecfed20917adc3c559812a8dc08b03e3150 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Fri, 7 Jul 2023 12:54:20 -0700 Subject: [PATCH 01/13] 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 } } From d76fc41230e37622656ecff9b935185f6ff6e928 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Fri, 7 Jul 2023 17:11:03 -0700 Subject: [PATCH 02/13] more pooling of widgets, rename and rework a few things --- ui/browsing/albumpage.go | 61 +++++++++++++++++++------------- ui/browsing/artistpage.go | 65 ++++++++++++++++++++++------------- ui/browsing/favoritespage.go | 31 +++++++++++++++-- ui/browsing/nowplayingpage.go | 16 +++++++-- ui/browsing/playlistpage.go | 41 ++++++++++++++++++---- ui/browsing/router.go | 28 +++++++-------- ui/browsing/trackspage.go | 17 ++++++--- ui/util/widgetcache.go | 50 --------------------------- ui/util/widgetpool.go | 60 ++++++++++++++++++++++++++++++++ ui/widgets/tracklistloader.go | 16 ++++++++- 10 files changed, 257 insertions(+), 128 deletions(-) delete mode 100644 ui/util/widgetcache.go create mode 100644 ui/util/widgetpool.go diff --git a/ui/browsing/albumpage.go b/ui/browsing/albumpage.go index c5d9175..972f6d3 100644 --- a/ui/browsing/albumpage.go +++ b/ui/browsing/albumpage.go @@ -25,6 +25,7 @@ type AlbumPage struct { albumPageState + disposed bool header *AlbumPageHeader tracks []*mediaprovider.Track tracklist *widgets.Tracklist @@ -33,32 +34,32 @@ type AlbumPage struct { } type albumPageState struct { - albumID string - sort widgets.TracklistSort - cfg *backend.AlbumPageConfig - widgetCache *util.WidgetCache - mp mediaprovider.MediaProvider - pm *backend.PlaybackManager - im *backend.ImageManager - contr *controller.Controller + albumID string + sort widgets.TracklistSort + cfg *backend.AlbumPageConfig + pool *util.WidgetPool + mp mediaprovider.MediaProvider + pm *backend.PlaybackManager + im *backend.ImageManager + contr *controller.Controller } func NewAlbumPage( albumID string, cfg *backend.AlbumPageConfig, - cache *util.WidgetCache, + pool *util.WidgetPool, pm *backend.PlaybackManager, mp mediaprovider.MediaProvider, im *backend.ImageManager, contr *controller.Controller, ) *AlbumPage { - return newAlbumPage(albumID, cfg, cache, pm, mp, im, contr, widgets.TracklistSort{}) + return newAlbumPage(albumID, cfg, pool, pm, mp, im, contr, widgets.TracklistSort{}) } func newAlbumPage( albumID string, cfg *backend.AlbumPageConfig, - cache *util.WidgetCache, + pool *util.WidgetPool, pm *backend.PlaybackManager, mp mediaprovider.MediaProvider, im *backend.ImageManager, @@ -67,24 +68,29 @@ func newAlbumPage( ) *AlbumPage { a := &AlbumPage{ albumPageState: albumPageState{ - albumID: albumID, - cfg: cfg, - widgetCache: cache, - pm: pm, - mp: mp, - im: im, - contr: contr, + albumID: albumID, + cfg: cfg, + pool: pool, + pm: pm, + mp: mp, + im: im, + contr: contr, }, } a.ExtendBaseWidget(a) - if h := a.widgetCache.Obtain(util.WidgetTypeAlbumPageHeader); h != nil { + if h := a.pool.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) + if t := a.pool.Obtain(util.WidgetTypeTracklist); t != nil { + a.tracklist = t.(*widgets.Tracklist) + a.tracklist.Reset() + } else { + a.tracklist = widgets.NewTracklist(nil) + } a.tracklist.SetVisibleColumns(a.cfg.TracklistColumns) a.tracklist.SetSorting(sort) a.tracklist.OnVisibleColumnsChanged = func(cols []string) { @@ -105,9 +111,12 @@ func (a *AlbumPage) CreateRenderer() fyne.WidgetRenderer { } func (a *AlbumPage) Save() SavedPage { + a.disposed = true s := a.albumPageState s.sort = a.tracklist.Sorting() - a.widgetCache.Release(util.WidgetTypeAlbumPageHeader, a.header) + a.header.page = nil + a.pool.Release(util.WidgetTypeAlbumPageHeader, a.header) + a.pool.Release(util.WidgetTypeTracklist, a.tracklist) return &s } @@ -144,6 +153,9 @@ func (a *AlbumPage) load() { log.Printf("Failed to get album: %s", err.Error()) return } + if a.disposed { + return + } a.header.Update(album, a.im) a.tracklist.Options = widgets.TracklistOptions{ ShowDiscNumber: album.Tracks[0].DiscNumber != album.Tracks[len(album.Tracks)-1].DiscNumber, @@ -279,6 +291,7 @@ func (a *AlbumPageHeader) Clear() { a.miscLabel.SetText("") a.toggleFavButton.IsFavorited = false a.cover.Image.Image = nil + a.cover.Refresh() } func (a *AlbumPageHeader) toggleFavorited() { @@ -292,7 +305,9 @@ func (a *AlbumPageHeader) showPopUpCover() { log.Printf("error getting full size album cover: %s", err.Error()) return } - a.page.contr.ShowPopUpImage(cover) + if a.page != nil { + a.page.contr.ShowPopUpImage(cover) + } } func formatMiscLabelStr(a *mediaprovider.AlbumWithTracks) string { @@ -308,5 +323,5 @@ func formatMiscLabelStr(a *mediaprovider.AlbumWithTracks) string { } func (s *albumPageState) Restore() Page { - return newAlbumPage(s.albumID, s.cfg, s.widgetCache, s.pm, s.mp, s.im, s.contr, s.sort) + return newAlbumPage(s.albumID, s.cfg, s.pool, s.pm, s.mp, s.im, s.contr, s.sort) } diff --git a/ui/browsing/artistpage.go b/ui/browsing/artistpage.go index f20e15b..924d19e 100644 --- a/ui/browsing/artistpage.go +++ b/ui/browsing/artistpage.go @@ -28,18 +28,19 @@ type artistPageState struct { activeView int trackSort widgets.TracklistSort - widgetCache *util.WidgetCache - cfg *backend.ArtistPageConfig - pm *backend.PlaybackManager - mp mediaprovider.MediaProvider - im *backend.ImageManager - contr *controller.Controller + pool *util.WidgetPool + cfg *backend.ArtistPageConfig + pm *backend.PlaybackManager + mp mediaprovider.MediaProvider + im *backend.ImageManager + contr *controller.Controller } type ArtistPage struct { widget.BaseWidget artistPageState + disposed bool artistInfo *mediaprovider.ArtistWithAlbums @@ -50,34 +51,34 @@ type ArtistPage struct { container *fyne.Container } -func NewArtistPage(artistID string, cfg *backend.ArtistPageConfig, cache *util.WidgetCache, pm *backend.PlaybackManager, mp mediaprovider.MediaProvider, im *backend.ImageManager, contr *controller.Controller) *ArtistPage { +func NewArtistPage(artistID string, cfg *backend.ArtistPageConfig, pool *util.WidgetPool, 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, cache, pm, mp, im, contr, activeView, widgets.TracklistSort{}) + return newArtistPage(artistID, cfg, pool, pm, mp, im, contr, activeView, widgets.TracklistSort{}) } -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 { +func newArtistPage(artistID string, cfg *backend.ArtistPageConfig, pool *util.WidgetPool, 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, - widgetCache: cache, - pm: pm, - mp: mp, - im: im, - contr: contr, - activeView: activeView, - trackSort: sort, + artistID: artistID, + cfg: cfg, + pool: pool, + pm: pm, + mp: mp, + im: im, + contr: contr, + activeView: activeView, + trackSort: sort, }} a.ExtendBaseWidget(a) - if h := a.widgetCache.Obtain(util.WidgetTypeArtistPageHeader); h != nil { + if h := a.pool.Obtain(util.WidgetTypeArtistPageHeader); h != nil { a.header = h.(*ArtistPageHeader) + a.header.Clear() } 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*/) } @@ -120,11 +121,14 @@ func (a *ArtistPage) Reload() { } func (a *ArtistPage) Save() SavedPage { + a.disposed = true s := a.artistPageState if a.tracklistCtr != nil { - s.trackSort = a.tracklistCtr.Objects[0].(*widgets.Tracklist).Sorting() + tl := a.tracklistCtr.Objects[0].(*widgets.Tracklist) + s.trackSort = tl.Sorting() + a.pool.Release(util.WidgetTypeTracklist, tl) } - a.widgetCache.Release(util.WidgetTypeArtistPageHeader, a.header) + a.pool.Release(util.WidgetTypeArtistPageHeader, a.header) return &s } @@ -150,6 +154,9 @@ func (a *ArtistPage) load() { log.Printf("Failed to get artist: %s", err.Error()) return } + if a.disposed { + return + } a.artistInfo = artist a.header.Update(artist) if a.activeView == 0 { @@ -198,7 +205,17 @@ func (a *ArtistPage) showTopTracks() { log.Printf("error getting top songs: %s", err.Error()) return } - tl := widgets.NewTracklist(ts) + if a.disposed { + return + } + var tl *widgets.Tracklist + if t := a.pool.Obtain(util.WidgetTypeTracklist); t != nil { + tl = t.(*widgets.Tracklist) + tl.Reset() + tl.SetTracks(ts) + } else { + tl = widgets.NewTracklist(ts) + } tl.Options = widgets.TracklistOptions{AutoNumber: true} tl.SetVisibleColumns(a.cfg.TracklistColumns) tl.SetSorting(a.trackSort) @@ -237,7 +254,7 @@ func (a *ArtistPage) CreateRenderer() fyne.WidgetRenderer { } func (s *artistPageState) Restore() Page { - return newArtistPage(s.artistID, s.cfg, s.widgetCache, s.pm, s.mp, s.im, s.contr, s.activeView, s.trackSort) + return newArtistPage(s.artistID, s.cfg, s.pool, s.pm, s.mp, s.im, s.contr, s.activeView, s.trackSort) } const artistBioNotAvailableStr = "Artist biography not available." diff --git a/ui/browsing/favoritespage.go b/ui/browsing/favoritespage.go index f21d6f3..35b6808 100644 --- a/ui/browsing/favoritespage.go +++ b/ui/browsing/favoritespage.go @@ -23,11 +23,13 @@ type FavoritesPage struct { widget.BaseWidget cfg *backend.FavoritesPageConfig + pool *util.WidgetPool contr *controller.Controller pm *backend.PlaybackManager im *backend.ImageManager mp mediaprovider.MediaProvider + disposed bool trackSort widgets.TracklistSort filter mediaprovider.AlbumFilter searchText string @@ -45,10 +47,11 @@ type FavoritesPage struct { container *fyne.Container } -func NewFavoritesPage(cfg *backend.FavoritesPageConfig, contr *controller.Controller, mp mediaprovider.MediaProvider, pm *backend.PlaybackManager, im *backend.ImageManager) *FavoritesPage { +func NewFavoritesPage(cfg *backend.FavoritesPageConfig, pool *util.WidgetPool, contr *controller.Controller, mp mediaprovider.MediaProvider, pm *backend.PlaybackManager, im *backend.ImageManager) *FavoritesPage { a := &FavoritesPage{ filter: mediaprovider.AlbumFilter{ExcludeUnfavorited: true}, cfg: cfg, + pool: pool, contr: contr, pm: pm, mp: mp, @@ -99,6 +102,7 @@ func restoreFavoritesPage(saved *savedFavoritesPage) *FavoritesPage { a := &FavoritesPage{ cfg: saved.cfg, contr: saved.contr, + pool: saved.pool, pm: saved.pm, mp: saved.mp, im: saved.im, @@ -154,6 +158,9 @@ func (a *FavoritesPage) Reload() { log.Printf("error getting starred items: %s", err.Error()) return } + if a.disposed { + return + } if a.tracklistCtr != nil { // refresh favorite songs view tr := a.tracklistCtr.Objects[0].(*widgets.Tracklist) @@ -176,9 +183,11 @@ func (a *FavoritesPage) Reload() { } func (a *FavoritesPage) Save() SavedPage { + a.disposed = true sf := &savedFavoritesPage{ cfg: a.cfg, contr: a.contr, + pool: a.pool, pm: a.pm, mp: a.mp, im: a.im, @@ -191,7 +200,9 @@ func (a *FavoritesPage) Save() SavedPage { sf.searchGridState = a.searchGrid.SaveToState() } if a.tracklistCtr != nil { - sf.trackSort = a.tracklistCtr.Objects[0].(*widgets.Tracklist).Sorting() + tl := a.tracklistCtr.Objects[0].(*widgets.Tracklist) + sf.trackSort = tl.Sorting() + a.pool.Release(util.WidgetTypeTracklist, tl) } return sf } @@ -277,6 +288,9 @@ func (a *FavoritesPage) onShowFavoriteArtists() { log.Printf("error getting starred items: %s", err.Error()) return } + if a.disposed { + return + } model := buildArtistGridViewModel(fav.Artists) a.artistGrid = widgets.NewFixedGridView(model, a.im, myTheme.ArtistIcon) a.contr.ConnectArtistGridActions(a.artistGrid) @@ -325,7 +339,17 @@ func (a *FavoritesPage) onShowFavoriteSongs() { log.Printf("error getting starred items: %s", err.Error()) return } - tracklist := widgets.NewTracklist(fav.Tracks) + if a.disposed { + return + } + var tracklist *widgets.Tracklist + if tl := a.pool.Obtain(util.WidgetTypeTracklist); tl != nil { + tracklist = tl.(*widgets.Tracklist) + tracklist.Reset() + tracklist.SetTracks(fav.Tracks) + } else { + tracklist = widgets.NewTracklist(fav.Tracks) + } tracklist.Options = widgets.TracklistOptions{AutoNumber: true} tracklist.SetVisibleColumns(a.cfg.TracklistColumns) tracklist.SetSorting(a.trackSort) @@ -355,6 +379,7 @@ func (a *FavoritesPage) CreateRenderer() fyne.WidgetRenderer { type savedFavoritesPage struct { cfg *backend.FavoritesPageConfig contr *controller.Controller + pool *util.WidgetPool pm *backend.PlaybackManager mp mediaprovider.MediaProvider im *backend.ImageManager diff --git a/ui/browsing/nowplayingpage.go b/ui/browsing/nowplayingpage.go index c40f30e..c4787d2 100644 --- a/ui/browsing/nowplayingpage.go +++ b/ui/browsing/nowplayingpage.go @@ -38,6 +38,7 @@ type NowPlayingPage struct { type nowPlayingPageState struct { contr *controller.Controller + pool *util.WidgetPool conf *backend.NowPlayingPageConfig pm *backend.PlaybackManager p *player.Player @@ -46,18 +47,26 @@ type nowPlayingPageState struct { func NewNowPlayingPage( highlightedTrackID string, contr *controller.Controller, + pool *util.WidgetPool, conf *backend.NowPlayingPageConfig, pm *backend.PlaybackManager, p *player.Player, // TODO: once other player backends are supported (eg uPnP), refactor ) *NowPlayingPage { - a := &NowPlayingPage{nowPlayingPageState: nowPlayingPageState{contr: contr, conf: conf, pm: pm, p: p}} + a := &NowPlayingPage{nowPlayingPageState: nowPlayingPageState{ + contr: contr, pool: pool, conf: conf, pm: pm, p: p, + }} a.ExtendBaseWidget(a) p.OnPaused(a.formatStatusLine) p.OnPlaying(a.formatStatusLine) p.OnStopped(a.formatStatusLine) - a.tracklist = widgets.NewTracklist(nil) + if t := a.pool.Obtain(util.WidgetTypeTracklist); t != nil { + a.tracklist = t.(*widgets.Tracklist) + a.tracklist.Reset() + } else { + a.tracklist = widgets.NewTracklist(nil) + } a.tracklist.SetVisibleColumns(conf.TracklistColumns) a.tracklist.OnVisibleColumnsChanged = func(cols []string) { a.conf.TracklistColumns = cols @@ -90,6 +99,7 @@ func (a *NowPlayingPage) CreateRenderer() fyne.WidgetRenderer { } func (a *NowPlayingPage) Save() SavedPage { + a.pool.Release(util.WidgetTypeTracklist, a.tracklist) nps := a.nowPlayingPageState return &nps } @@ -207,5 +217,5 @@ func (a *NowPlayingPage) load(highlightedTrackID string) { } func (s *nowPlayingPageState) Restore() Page { - return NewNowPlayingPage("", s.contr, s.conf, s.pm, s.p) + return NewNowPlayingPage("", s.contr, s.pool, s.conf, s.pm, s.p) } diff --git a/ui/browsing/playlistpage.go b/ui/browsing/playlistpage.go index b113b3a..f1b30f2 100644 --- a/ui/browsing/playlistpage.go +++ b/ui/browsing/playlistpage.go @@ -24,6 +24,7 @@ type PlaylistPage struct { playlistPageState + disposed bool header *PlaylistPageHeader tracklist *widgets.Tracklist tracks []*mediaprovider.Track @@ -35,6 +36,7 @@ type playlistPageState struct { playlistID string conf *backend.PlaylistPageConfig contr *controller.Controller + widgetPool *util.WidgetPool sm *backend.ServerManager pm *backend.PlaybackManager im *backend.ImageManager @@ -44,27 +46,40 @@ type playlistPageState struct { func NewPlaylistPage( playlistID string, conf *backend.PlaylistPageConfig, + pool *util.WidgetPool, contr *controller.Controller, sm *backend.ServerManager, pm *backend.PlaybackManager, im *backend.ImageManager, ) *PlaylistPage { - return newPlaylistPage(playlistID, conf, contr, sm, pm, im, widgets.TracklistSort{}) + return newPlaylistPage(playlistID, conf, contr, pool, sm, pm, im, widgets.TracklistSort{}) } func newPlaylistPage( playlistID string, conf *backend.PlaylistPageConfig, contr *controller.Controller, + pool *util.WidgetPool, sm *backend.ServerManager, pm *backend.PlaybackManager, im *backend.ImageManager, trackSort widgets.TracklistSort, ) *PlaylistPage { - a := &PlaylistPage{playlistPageState: playlistPageState{playlistID: playlistID, conf: conf, contr: contr, sm: sm, pm: pm, im: im}} + a := &PlaylistPage{playlistPageState: playlistPageState{playlistID: playlistID, conf: conf, contr: contr, widgetPool: pool, sm: sm, pm: pm, im: im}} a.ExtendBaseWidget(a) - a.header = NewPlaylistPageHeader(a) - a.tracklist = widgets.NewTracklist(nil) + if h := a.widgetPool.Obtain(util.WidgetTypePlaylistPageHeader); h != nil { + a.header = h.(*PlaylistPageHeader) + a.header.Clear() + } else { + a.header = NewPlaylistPageHeader(a) + } + a.header.page = a + if tl := a.widgetPool.Obtain(util.WidgetTypeTracklist); tl != nil { + a.tracklist = tl.(*widgets.Tracklist) + a.tracklist.Reset() + } else { + a.tracklist = widgets.NewTracklist(nil) + } a.tracklist.SetVisibleColumns(conf.TracklistColumns) a.tracklist.SetSorting(trackSort) a.tracklist.OnVisibleColumnsChanged = func(cols []string) { @@ -96,8 +111,11 @@ func (a *PlaylistPage) CreateRenderer() fyne.WidgetRenderer { } func (a *PlaylistPage) Save() SavedPage { + a.disposed = true p := a.playlistPageState p.trackSort = a.tracklist.Sorting() + p.widgetPool.Release(util.WidgetTypePlaylistPageHeader, a.header) + p.widgetPool.Release(util.WidgetTypeTracklist, a.tracklist) return &p } @@ -134,6 +152,9 @@ func (a *PlaylistPage) load() { log.Printf("Failed to get playlist: %s", err.Error()) return } + if a.disposed { + return + } renumberTracks(playlist.Tracks) a.tracks = playlist.Tracks a.tracklist.SetTracks(playlist.Tracks) @@ -202,7 +223,7 @@ func (a *PlaylistPage) onRemoveSelectedFromPlaylist() { } a.sm.Server.EditPlaylistTracks(a.playlistID, nil, idxs) a.tracklist.UnselectAll() - go a.Reload() + a.Reload() } type PlaylistPageHeader struct { @@ -282,6 +303,14 @@ func NewPlaylistPageHeader(page *PlaylistPage) *PlaylistPageHeader { return a } +func (a *PlaylistPageHeader) Clear() { + a.titleLabel.Segments[0].(*widget.TextSegment).Text = "" + a.createdAtLabel.Text = "" + a.descriptionLabel.Text = "" + a.ownerLabel.Text = "" + a.image.SetImage(nil, false) +} + func (a *PlaylistPageHeader) CreateRenderer() fyne.WidgetRenderer { return widget.NewSimpleRenderer(a.container) } @@ -327,5 +356,5 @@ func (a *PlaylistPageHeader) formatPlaylistTrackTimeStr(p *mediaprovider.Playlis } func (s *playlistPageState) Restore() Page { - return newPlaylistPage(s.playlistID, s.conf, s.contr, s.sm, s.pm, s.im, s.trackSort) + return newPlaylistPage(s.playlistID, s.conf, s.contr, s.widgetPool, s.sm, s.pm, s.im, s.trackSort) } diff --git a/ui/browsing/router.go b/ui/browsing/router.go index 9a6bf85..79ac547 100644 --- a/ui/browsing/router.go +++ b/ui/browsing/router.go @@ -12,18 +12,18 @@ type NavigationHandler interface { } type Router struct { - App *backend.App - Controller *controller.Controller - Nav NavigationHandler - widgetCache util.WidgetCache + App *backend.App + Controller *controller.Controller + Nav NavigationHandler + widgetPool util.WidgetPool } func NewRouter(app *backend.App, controller *controller.Controller, nav NavigationHandler) Router { r := Router{ - App: app, - Controller: controller, - Nav: nav, - widgetCache: util.NewWidgetCache(), + App: app, + Controller: controller, + Nav: nav, + widgetPool: util.NewWidgetPool(), } return r } @@ -31,27 +31,27 @@ 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.widgetCache, r.App.PlaybackManager, r.App.ServerManager.Server, r.App.ImageManager, r.Controller) + return NewAlbumPage(rte.Arg, &r.App.Config.AlbumPage, &r.widgetPool, 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.widgetCache, r.App.PlaybackManager, r.App.ServerManager.Server, r.App.ImageManager, r.Controller) + return NewArtistPage(rte.Arg, &r.App.Config.ArtistPage, &r.widgetPool, 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: - return NewFavoritesPage(&r.App.Config.FavoritesPage, r.Controller, r.App.ServerManager.Server, r.App.PlaybackManager, r.App.ImageManager) + return NewFavoritesPage(&r.App.Config.FavoritesPage, &r.widgetPool, r.Controller, r.App.ServerManager.Server, r.App.PlaybackManager, r.App.ImageManager) case controller.Genre: return NewGenrePage(rte.Arg, r.Controller, r.App.PlaybackManager, r.App.ServerManager.Server, r.App.ImageManager) case controller.Genres: return NewGenresPage(r.Controller, r.App.ServerManager.Server) case controller.NowPlaying: - return NewNowPlayingPage(rte.Arg, r.Controller, &r.App.Config.NowPlayingPage, r.App.PlaybackManager, r.App.Player) + return NewNowPlayingPage(rte.Arg, r.Controller, &r.widgetPool, &r.App.Config.NowPlayingPage, r.App.PlaybackManager, r.App.Player) case controller.Playlist: - return NewPlaylistPage(rte.Arg, &r.App.Config.PlaylistPage, r.Controller, r.App.ServerManager, r.App.PlaybackManager, r.App.ImageManager) + return NewPlaylistPage(rte.Arg, &r.App.Config.PlaylistPage, &r.widgetPool, r.Controller, r.App.ServerManager, r.App.PlaybackManager, r.App.ImageManager) case controller.Playlists: return NewPlaylistsPage(r.Controller, &r.App.Config.PlaylistsPage, r.App.ServerManager.Server) case controller.Tracks: - return NewTracksPage(r.Controller, &r.App.Config.TracksPage, r.App.ServerManager.Server) + return NewTracksPage(r.Controller, &r.App.Config.TracksPage, &r.widgetPool, r.App.ServerManager.Server) } return nil } diff --git a/ui/browsing/trackspage.go b/ui/browsing/trackspage.go index 415e228..420e793 100644 --- a/ui/browsing/trackspage.go +++ b/ui/browsing/trackspage.go @@ -7,6 +7,7 @@ import ( "github.com/dweymouth/supersonic/ui/controller" "github.com/dweymouth/supersonic/ui/layouts" "github.com/dweymouth/supersonic/ui/theme" + "github.com/dweymouth/supersonic/ui/util" "github.com/dweymouth/supersonic/ui/widgets" "fyne.io/fyne/v2" @@ -34,16 +35,22 @@ type TracksPage struct { type tracksPageState struct { searchText string + widgetPool *util.WidgetPool contr *controller.Controller conf *backend.TracksPageConfig mp mediaprovider.MediaProvider } -func NewTracksPage(contr *controller.Controller, conf *backend.TracksPageConfig, mp mediaprovider.MediaProvider) *TracksPage { - t := &TracksPage{tracksPageState: tracksPageState{contr: contr, conf: conf, mp: mp}} +func NewTracksPage(contr *controller.Controller, conf *backend.TracksPageConfig, pool *util.WidgetPool, mp mediaprovider.MediaProvider) *TracksPage { + t := &TracksPage{tracksPageState: tracksPageState{contr: contr, conf: conf, widgetPool: pool, mp: mp}} t.ExtendBaseWidget(t) - t.tracklist = widgets.NewTracklist(nil) + if tl := t.widgetPool.Obtain(util.WidgetTypeTracklist); tl != nil { + t.tracklist = tl.(*widgets.Tracklist) + t.tracklist.Reset() + } else { + t.tracklist = widgets.NewTracklist(nil) + } t.tracklist.Options = widgets.TracklistOptions{ DisableSorting: true, AutoNumber: true, @@ -143,12 +150,14 @@ func (t *TracksPage) CreateRenderer() fyne.WidgetRenderer { } func (t *TracksPage) Save() SavedPage { + t.loader.Dispose() + t.widgetPool.Release(util.WidgetTypeTracklist, t.tracklist) state := t.tracksPageState return &state } func (s *tracksPageState) Restore() Page { - t := NewTracksPage(s.contr, s.conf, s.mp) + t := NewTracksPage(s.contr, s.conf, s.widgetPool, s.mp) t.searchText = s.searchText if t.searchText != "" { t.searcher.Entry.Text = t.searchText diff --git a/ui/util/widgetcache.go b/ui/util/widgetcache.go deleted file mode 100644 index c83bd97..0000000 --- a/ui/util/widgetcache.go +++ /dev/null @@ -1,50 +0,0 @@ -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/util/widgetpool.go b/ui/util/widgetpool.go new file mode 100644 index 0000000..66320fd --- /dev/null +++ b/ui/util/widgetpool.go @@ -0,0 +1,60 @@ +package util + +import ( + "time" + + "fyne.io/fyne/v2" +) + +type WidgetType int + +const ( + WidgetTypeAlbumPageHeader WidgetType = iota + WidgetTypeArtistPageHeader + WidgetTypePlaylistPageHeader + WidgetTypeTracklist +) + +// A pool to share commonly-used widgets across pages to reduce +// creation of new widgets and memory allocations. +// It is not thread-safe, which is fine for its current use. +type WidgetPool struct { + cache map[WidgetType][]cachedWidget +} + +type cachedWidget struct { + widget fyne.CanvasObject + releasedAt int64 // unixMillis +} + +func NewWidgetPool() WidgetPool { + return WidgetPool{ + cache: make(map[WidgetType][]cachedWidget), + } +} + +// Obtain obtains a widget of the given type from the pool, if one exists. +// Returns nil if there is no available widget. +func (w *WidgetPool) 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 +} + +// Release releases a widget into the pool. +// The widget must not be modified by the releaser after release, +// since it may be Obtained for a new use at any time. +func (w *WidgetPool) 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/tracklistloader.go b/ui/widgets/tracklistloader.go index 5997281..9f55bbf 100644 --- a/ui/widgets/tracklistloader.go +++ b/ui/widgets/tracklistloader.go @@ -7,6 +7,8 @@ import ( // Component that manages lazily loading more tracks into a Tracklist // as the user scrolls near the bottom. type TracklistLoader struct { + disposed bool + tracklist *Tracklist iter mediaprovider.TrackIterator @@ -28,11 +30,17 @@ func NewTracklistLoader(tracklist *Tracklist, iter mediaprovider.TrackIterator) return t } +// Cancels all asynchronous loads so that they will no longer modify the tracklist. +func (t *TracklistLoader) Dispose() { + t.disposed = true + t.tracklist.OnTrackShown = nil +} + func (t *TracklistLoader) onTrackShown(tracknum int) { if tracknum > t.highestShown { t.highestShown = tracknum } - if t.highestShown >= t.len-25 && !t.fetching && !t.done { + if t.highestShown >= t.len-25 && !t.fetching && !t.done && !t.disposed { t.fetching = true go t.loadMoreTracks(25) } @@ -52,6 +60,12 @@ func (t *TracklistLoader) loadMoreTracks(num int) { break } t.trackBuffer = append(t.trackBuffer, tr) + if t.disposed { + break + } + } + if t.disposed { + return } t.tracklist.AppendTracks(t.trackBuffer) t.tracklist.Refresh() From 56225548426dd51eea2cb485c027d079a594f1b8 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Sat, 8 Jul 2023 11:58:22 -0700 Subject: [PATCH 03/13] move gridview mutex out of state --- ui/widgets/gridview.go | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/ui/widgets/gridview.go b/ui/widgets/gridview.go index 8e3e1d4..5161fe4 100644 --- a/ui/widgets/gridview.go +++ b/ui/widgets/gridview.go @@ -71,6 +71,7 @@ func NewGridViewAlbumIterator(iter mediaprovider.AlbumIterator) GridViewIterator type GridView struct { widget.BaseWidget + stateMutex sync.RWMutex GridViewState grid *xwidget.GridWrap @@ -78,7 +79,6 @@ type GridView struct { type GridViewState struct { items []GridViewItemModel - itemsMutex sync.RWMutex iter GridViewIterator imageFetcher ImageFetcher placeholder fyne.Resource @@ -129,7 +129,9 @@ func NewGridView(iter GridViewIterator, fetch ImageFetcher, placeholder fyne.Res } func (g *GridView) SaveToState() GridViewState { + g.stateMutex.RLock() s := g.GridViewState + g.stateMutex.RUnlock() s.scrollPos = g.grid.GetScrollOffset() return s } @@ -144,8 +146,8 @@ func NewGridViewFromState(state GridViewState) *GridView { } func (g *GridView) Clear() { - g.itemsMutex.Lock() - defer g.itemsMutex.Unlock() + g.stateMutex.Lock() + defer g.stateMutex.Unlock() g.items = nil g.done = true } @@ -155,12 +157,12 @@ func (g *GridView) Reset(iter GridViewIterator) { g.fetchCancel() g.fetchCancel = nil } - g.itemsMutex.Lock() + g.stateMutex.Lock() g.items = nil g.done = false g.highestShown = 0 g.iter = iter - g.itemsMutex.Unlock() + g.stateMutex.Unlock() g.fetchMoreItems(36) } @@ -169,12 +171,12 @@ func (g *GridView) ResetFixed(items []GridViewItemModel) { g.fetchCancel() g.fetchCancel = nil } - g.itemsMutex.Lock() + g.stateMutex.Lock() g.items = items - g.itemsMutex.Unlock() g.done = true g.highestShown = 0 g.iter = nil + g.stateMutex.Unlock() } func (g *GridView) GetScrollOffset() float32 { @@ -238,13 +240,13 @@ func (g *GridView) doUpdateItemCard(itemIdx int, card *GridViewItem) { g.highestShown = itemIdx } var item GridViewItemModel - g.itemsMutex.RLock() + g.stateMutex.RLock() // itemIdx can rarely be out of range if the data is being updated // as the view is requested to refresh if itemIdx < len(g.items) { item = g.items[itemIdx] } - g.itemsMutex.RUnlock() + g.stateMutex.RUnlock() if card.PrevID == item.ID { // nothing to do return @@ -290,8 +292,8 @@ func (g *GridView) doUpdateItemCard(itemIdx int, card *GridViewItem) { } func (g *GridView) lenItems() int { - g.itemsMutex.RLock() - defer g.itemsMutex.RUnlock() + g.stateMutex.RLock() + defer g.stateMutex.RUnlock() return len(g.items) } @@ -314,9 +316,9 @@ func (g *GridView) fetchMoreItems(count int) { case <-ctx.Done(): return default: - g.itemsMutex.Lock() + g.stateMutex.Lock() g.items = append(g.items, items...) - g.itemsMutex.Unlock() + g.stateMutex.Unlock() if len(items) < batchFetchSize { g.done = true } From 673f86f7924c03acacfae02a6e9376b6490bead2 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Sat, 8 Jul 2023 13:44:55 -0700 Subject: [PATCH 04/13] reuse hyperlinks in Similar Artists list --- ui/browsing/artistpage.go | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/ui/browsing/artistpage.go b/ui/browsing/artistpage.go index 924d19e..8290e1b 100644 --- a/ui/browsing/artistpage.go +++ b/ui/browsing/artistpage.go @@ -306,7 +306,9 @@ func (a *ArtistPageHeader) Clear() { a.favoriteBtn.IsFavorited = false a.titleDisp.Segments[0].(*widget.TextSegment).Text = "" a.biographyDisp.Segments[0].(*widget.TextSegment).Text = artistBioNotAvailableStr - a.similarArtists.RemoveAll() + for _, obj := range a.similarArtists.Objects { + obj.Hide() + } a.artistImage.SetImage(nil, false) } @@ -336,21 +338,29 @@ func (a *ArtistPageHeader) UpdateInfo(info *mediaprovider.ArtistInfo) { } } - a.similarArtists.RemoveAll() + if len(a.similarArtists.Objects) == 0 { + a.similarArtists.Add(widget.NewLabel("Similar Artists:")) + } + for _, obj := range a.similarArtists.Objects { + obj.Hide() + } for i, art := range info.SimilarArtists { if i == 0 { - a.similarArtists.Add(widget.NewLabel("Similar Artists:")) + a.similarArtists.Objects[0].Show() // "Similar Artists:" label } if i == 4 { break } - h := widgets.NewCustomHyperlink() + if len(a.similarArtists.Objects) <= i+1 { + a.similarArtists.Add(widgets.NewCustomHyperlink()) + } + h := a.similarArtists.Objects[i+1].(*widgets.CustomHyperlink) h.NoTruncate = true h.SetText(art.Name) h.OnTapped = func(id string) func() { return func() { a.artistPage.contr.NavigateTo(controller.ArtistRoute(id)) } }(art.ID) - a.similarArtists.Add(h) + h.Show() } a.similarArtists.Refresh() From 83b6d48560881c81797d4db9a2dadb864be18780 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Sat, 8 Jul 2023 14:02:15 -0700 Subject: [PATCH 05/13] widgetPool - switch from map to slice --- ui/util/widgetpool.go | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/ui/util/widgetpool.go b/ui/util/widgetpool.go index 66320fd..a65133d 100644 --- a/ui/util/widgetpool.go +++ b/ui/util/widgetpool.go @@ -13,23 +13,26 @@ const ( WidgetTypeArtistPageHeader WidgetTypePlaylistPageHeader WidgetTypeTracklist + + // keep at bottom + numWidgetTypes ) // A pool to share commonly-used widgets across pages to reduce // creation of new widgets and memory allocations. // It is not thread-safe, which is fine for its current use. type WidgetPool struct { - cache map[WidgetType][]cachedWidget + pool [][]pooledWidget } -type cachedWidget struct { +type pooledWidget struct { widget fyne.CanvasObject releasedAt int64 // unixMillis } func NewWidgetPool() WidgetPool { return WidgetPool{ - cache: make(map[WidgetType][]cachedWidget), + pool: make([][]pooledWidget, numWidgetTypes), } } @@ -37,11 +40,11 @@ func NewWidgetPool() WidgetPool { // Returns nil if there is no available widget. func (w *WidgetPool) 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] + if l := len(w.pool[typ]); l > 0 { + i := l - 1 + widget = w.pool[typ][i].widget + w.pool[typ][i].widget = nil + w.pool[typ] = w.pool[typ][:i] } return widget } @@ -50,10 +53,7 @@ func (w *WidgetPool) Obtain(typ WidgetType) fyne.CanvasObject { // The widget must not be modified by the releaser after release, // since it may be Obtained for a new use at any time. func (w *WidgetPool) 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{ + w.pool[typ] = append(w.pool[typ], pooledWidget{ widget: wid, releasedAt: time.Now().UnixMilli(), }) From 1c771b083288b2a700d7adf3c8f7a5a2e87a9288 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Sat, 8 Jul 2023 15:18:37 -0700 Subject: [PATCH 06/13] reuse grid view for searching on albums page --- ui/browsing/albumspage.go | 124 +++++++++++++++++------------------ ui/browsing/favoritespage.go | 4 +- ui/browsing/genrepage.go | 5 +- ui/widgets/gridview.go | 35 +++++++--- 4 files changed, 90 insertions(+), 78 deletions(-) diff --git a/ui/browsing/albumspage.go b/ui/browsing/albumspage.go index 455594f..e859b65 100644 --- a/ui/browsing/albumspage.go +++ b/ui/browsing/albumspage.go @@ -21,20 +21,21 @@ var _ fyne.Widget = (*AlbumsPage)(nil) type AlbumsPage struct { widget.BaseWidget - cfg *backend.AlbumsPageConfig - contr *controller.Controller - pm *backend.PlaybackManager - im *backend.ImageManager - mp mediaprovider.MediaProvider - grid *widgets.GridView - searchGrid *widgets.GridView - searcher *widgets.SearchEntry - filterBtn *widgets.AlbumFilterButton - searchText string - filter mediaprovider.AlbumFilter - titleDisp *widget.RichText - sortOrder *selectWidget - container *fyne.Container + cfg *backend.AlbumsPageConfig + contr *controller.Controller + pm *backend.PlaybackManager + im *backend.ImageManager + mp mediaprovider.MediaProvider + grid *widgets.GridView + gridState *widgets.GridViewState + searchGridState *widgets.GridViewState + searcher *widgets.SearchEntry + filterBtn *widgets.AlbumFilterButton + searchText string + filter mediaprovider.AlbumFilter + titleDisp *widget.RichText + sortOrder *selectWidget + container *fyne.Container } type selectWidget struct { @@ -79,7 +80,7 @@ func NewAlbumsPage(cfg *backend.AlbumsPageConfig, contr *controller.Controller, a.grid = widgets.NewGridView(widgets.NewGridViewAlbumIterator(iter), im, myTheme.AlbumIcon) contr.ConnectAlbumGridActions(a.grid) a.createSearchAndFilter() - a.createContainer(false) + a.createContainer() return a } @@ -92,31 +93,29 @@ func (a *AlbumsPage) createSearchAndFilter() { a.filterBtn.OnChanged = a.Reload } -func (a *AlbumsPage) createContainer(searchgrid bool) { +func (a *AlbumsPage) createContainer() { searchVbox := container.NewVBox(layout.NewSpacer(), a.searcher, layout.NewSpacer()) sortVbox := container.NewVBox(layout.NewSpacer(), a.sortOrder, layout.NewSpacer()) - g := a.grid - if searchgrid { - g = a.searchGrid - } a.container = container.NewBorder( container.NewHBox(util.NewHSpace(6), a.titleDisp, sortVbox, layout.NewSpacer(), container.NewCenter(a.filterBtn), searchVbox, util.NewHSpace(12)), nil, nil, nil, - g, + a.grid, ) } func restoreAlbumsPage(saved *savedAlbumsPage) *AlbumsPage { a := &AlbumsPage{ - cfg: saved.cfg, - contr: saved.contr, - pm: saved.pm, - mp: saved.mp, - im: saved.im, - searchText: saved.searchText, - filter: saved.filter, + cfg: saved.cfg, + contr: saved.contr, + pm: saved.pm, + mp: saved.mp, + im: saved.im, + gridState: saved.gridState, + searchGridState: saved.searchGridState, + searchText: saved.searchText, + filter: saved.filter, } a.ExtendBaseWidget(a) @@ -127,27 +126,28 @@ func restoreAlbumsPage(saved *savedAlbumsPage) *AlbumsPage { a.sortOrder = NewSelect(a.mp.AlbumSortOrders(), nil) a.sortOrder.Selected = saved.sortOrder a.sortOrder.OnChanged = a.onSortOrderChanged - a.grid = widgets.NewGridViewFromState(saved.gridState) if a.searchText != "" { - a.searchGrid = widgets.NewGridViewFromState(saved.searchGridState) + a.sortOrder.Disable() + a.grid = widgets.NewGridViewFromState(saved.searchGridState) + } else { + a.grid = widgets.NewGridViewFromState(saved.gridState) } a.createSearchAndFilter() - a.createContainer(saved.searchText != "") + a.createContainer() return a } func (a *AlbumsPage) OnSearched(query string) { - a.searchText = query if query == "" { - a.container.Objects[0] = a.grid - if a.searchGrid != nil { - a.searchGrid.Clear() - } - a.Refresh() - return + a.sortOrder.Enable() + a.grid.ResetFromState(a.gridState) + a.searchGridState = nil + } else { + a.sortOrder.Disable() + a.doSearch(query) } - a.doSearch(query) + a.searchText = query } func (a *AlbumsPage) Route() controller.Route { @@ -166,48 +166,42 @@ func (a *AlbumsPage) Reload() { } else { iter := a.mp.IterateAlbums(a.sortOrder.Selected, a.filter) a.grid.Reset(widgets.NewGridViewAlbumIterator(iter)) - a.grid.Refresh() } } func (a *AlbumsPage) Save() SavedPage { sa := &savedAlbumsPage{ - cfg: a.cfg, - contr: a.contr, - pm: a.pm, - mp: a.mp, - im: a.im, - searchText: a.searchText, - filter: a.filter, - sortOrder: a.sortOrder.Selected, - gridState: a.grid.SaveToState(), + cfg: a.cfg, + contr: a.contr, + pm: a.pm, + mp: a.mp, + im: a.im, + searchText: a.searchText, + filter: a.filter, + sortOrder: a.sortOrder.Selected, + gridState: a.gridState, + searchGridState: a.searchGridState, } - if a.searchGrid != nil { - sa.searchGridState = a.searchGrid.SaveToState() + if a.searchText == "" { + sa.gridState = a.grid.SaveToState() + } else { + sa.searchGridState = a.grid.SaveToState() } return sa } func (a *AlbumsPage) doSearch(query string) { - iter := widgets.NewGridViewAlbumIterator(a.mp.SearchAlbums(query, a.filter)) - if a.searchGrid == nil { - a.searchGrid = widgets.NewGridView(iter, a.im, myTheme.AlbumIcon) - a.contr.ConnectAlbumGridActions(a.searchGrid) - } else { - a.searchGrid.Reset(iter) + if a.searchText == "" { + a.gridState = a.grid.SaveToState() } - a.container.Objects[0] = a.searchGrid - a.Refresh() + iter := widgets.NewGridViewAlbumIterator(a.mp.SearchAlbums(query, a.filter)) + a.grid.Reset(iter) } func (a *AlbumsPage) onSortOrderChanged(order string) { a.cfg.SortOrder = a.sortOrder.Selected iter := a.mp.IterateAlbums(order, a.filter) a.grid.Reset(widgets.NewGridViewAlbumIterator(iter)) - if a.searchText == "" { - a.container.Objects[0] = a.grid - a.Refresh() - } } func (a *AlbumsPage) CreateRenderer() fyne.WidgetRenderer { @@ -224,8 +218,8 @@ type savedAlbumsPage struct { mp mediaprovider.MediaProvider im *backend.ImageManager sortOrder string - gridState widgets.GridViewState - searchGridState widgets.GridViewState + gridState *widgets.GridViewState + searchGridState *widgets.GridViewState } func (s *savedAlbumsPage) Restore() Page { diff --git a/ui/browsing/favoritespage.go b/ui/browsing/favoritespage.go index 35b6808..ecab761 100644 --- a/ui/browsing/favoritespage.go +++ b/ui/browsing/favoritespage.go @@ -383,8 +383,8 @@ type savedFavoritesPage struct { pm *backend.PlaybackManager mp mediaprovider.MediaProvider im *backend.ImageManager - gridState widgets.GridViewState - searchGridState widgets.GridViewState + gridState *widgets.GridViewState + searchGridState *widgets.GridViewState filter mediaprovider.AlbumFilter searchText string activeToggleBtn int diff --git a/ui/browsing/genrepage.go b/ui/browsing/genrepage.go index 26304d6..4307238 100644 --- a/ui/browsing/genrepage.go +++ b/ui/browsing/genrepage.go @@ -126,7 +126,6 @@ func (g *GenrePage) Reload() { } else { iter := g.mp.IterateAlbums("", g.filter) g.grid.Reset(widgets.NewGridViewAlbumIterator(iter)) - g.grid.Refresh() } } @@ -190,8 +189,8 @@ type savedGenrePage struct { pm *backend.PlaybackManager mp mediaprovider.MediaProvider im *backend.ImageManager - gridState widgets.GridViewState - searchGridState widgets.GridViewState + gridState *widgets.GridViewState + searchGridState *widgets.GridViewState } func (s *savedGenrePage) Restore() Page { diff --git a/ui/widgets/gridview.go b/ui/widgets/gridview.go index 5161fe4..a0c957d 100644 --- a/ui/widgets/gridview.go +++ b/ui/widgets/gridview.go @@ -71,7 +71,8 @@ func NewGridViewAlbumIterator(iter mediaprovider.AlbumIterator) GridViewIterator type GridView struct { widget.BaseWidget - stateMutex sync.RWMutex + stateMutex sync.RWMutex + fetchCancel context.CancelFunc GridViewState grid *xwidget.GridWrap @@ -83,7 +84,6 @@ type GridViewState struct { imageFetcher ImageFetcher placeholder fyne.Resource highestShown int - fetchCancel context.CancelFunc done bool OnPlay func(id string, shuffle bool) @@ -128,16 +128,16 @@ func NewGridView(iter GridViewIterator, fetch ImageFetcher, placeholder fyne.Res return g } -func (g *GridView) SaveToState() GridViewState { +func (g *GridView) SaveToState() *GridViewState { g.stateMutex.RLock() s := g.GridViewState g.stateMutex.RUnlock() s.scrollPos = g.grid.GetScrollOffset() - return s + return &s } -func NewGridViewFromState(state GridViewState) *GridView { - g := &GridView{GridViewState: state} +func NewGridViewFromState(state *GridViewState) *GridView { + g := &GridView{GridViewState: *state} g.ExtendBaseWidget(g) g.createGridWrap() g.Refresh() // needed to initialize the widget @@ -146,6 +146,10 @@ func NewGridViewFromState(state GridViewState) *GridView { } func (g *GridView) Clear() { + if g.fetchCancel != nil { + g.fetchCancel() + g.fetchCancel = nil + } g.stateMutex.Lock() defer g.stateMutex.Unlock() g.items = nil @@ -164,6 +168,20 @@ func (g *GridView) Reset(iter GridViewIterator) { g.iter = iter g.stateMutex.Unlock() g.fetchMoreItems(36) + g.Refresh() +} + +func (g *GridView) ResetFromState(state *GridViewState) { + if g.fetchCancel != nil { + g.fetchCancel() + g.fetchCancel = nil + } + g.stateMutex.Lock() + g.GridViewState = *state + g.stateMutex.Unlock() + g.grid.Refresh() + g.grid.ScrollToOffset(state.scrollPos) + g.grid.Refresh() } func (g *GridView) ResetFixed(items []GridViewItemModel) { @@ -177,6 +195,7 @@ func (g *GridView) ResetFixed(items []GridViewItemModel) { g.highestShown = 0 g.iter = nil g.stateMutex.Unlock() + g.Refresh() } func (g *GridView) GetScrollOffset() float32 { @@ -222,7 +241,7 @@ func (g *GridView) createGridWrap() { } card.OnDownload = func() { if g.OnDownload != nil { - g.OnDownload(card.itemID) + g.OnDownload(card.ItemID()) } } return card @@ -324,7 +343,7 @@ func (g *GridView) fetchMoreItems(count int) { } n += len(items) if len(items) > 0 { - g.Refresh() + g.grid.Refresh() } } } From be41bdc72c9ca9133f54e5a729d85ff735205ef6 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Sun, 9 Jul 2023 13:18:05 -0700 Subject: [PATCH 07/13] reuse grid for searching in genre page --- ui/browsing/genrepage.go | 109 +++++++++++++++++++-------------------- 1 file changed, 52 insertions(+), 57 deletions(-) diff --git a/ui/browsing/genrepage.go b/ui/browsing/genrepage.go index 4307238..68c90f6 100644 --- a/ui/browsing/genrepage.go +++ b/ui/browsing/genrepage.go @@ -19,19 +19,20 @@ import ( type GenrePage struct { widget.BaseWidget - genre string - contr *controller.Controller - im *backend.ImageManager - pm *backend.PlaybackManager - mp mediaprovider.MediaProvider - grid *widgets.GridView - searchGrid *widgets.GridView - searcher *widgets.SearchEntry - searchText string - filter mediaprovider.AlbumFilter - filterBtn *widgets.AlbumFilterButton - titleDisp *widget.RichText - playRandom *widget.Button + genre string + contr *controller.Controller + im *backend.ImageManager + pm *backend.PlaybackManager + mp mediaprovider.MediaProvider + grid *widgets.GridView + gridState *widgets.GridViewState + searchGridState *widgets.GridViewState + searcher *widgets.SearchEntry + searchText string + filter mediaprovider.AlbumFilter + filterBtn *widgets.AlbumFilterButton + titleDisp *widget.RichText + playRandom *widget.Button container *fyne.Container } @@ -56,7 +57,7 @@ func NewGenrePage(genre string, contr *controller.Controller, pm *backend.Playba g.grid = widgets.NewGridView(widgets.NewGridViewAlbumIterator(iter), g.im, myTheme.AlbumIcon) g.contr.ConnectAlbumGridActions(g.grid) g.createSearchAndFilter() - g.createContainer(false) + g.createContainer() return g } @@ -71,29 +72,27 @@ func (g *GenrePage) createSearchAndFilter() { g.filterBtn.OnChanged = g.Reload } -func (g *GenrePage) createContainer(searchGrid bool) { +func (g *GenrePage) createContainer() { searchVbox := container.NewVBox(layout.NewSpacer(), g.searcher, layout.NewSpacer()) - gr := g.grid - if searchGrid { - gr = g.searchGrid - } playRandomVbox := container.NewVBox(layout.NewSpacer(), g.playRandom, layout.NewSpacer()) g.container = container.NewBorder( container.NewHBox(util.NewHSpace(6), g.titleDisp, playRandomVbox, layout.NewSpacer(), container.NewCenter(g.filterBtn), searchVbox, util.NewHSpace(15)), - nil, nil, nil, gr, + nil, nil, nil, g.grid, ) } func restoreGenrePage(saved *savedGenrePage) *GenrePage { g := &GenrePage{ - genre: saved.genre, - contr: saved.contr, - pm: saved.pm, - mp: saved.mp, - im: saved.im, - searchText: saved.searchText, - filter: saved.filter, + genre: saved.genre, + contr: saved.contr, + pm: saved.pm, + mp: saved.mp, + im: saved.im, + gridState: saved.gridState, + searchGridState: saved.searchGridState, + searchText: saved.searchText, + filter: saved.filter, } g.ExtendBaseWidget(g) @@ -102,12 +101,13 @@ func restoreGenrePage(saved *savedGenrePage) *GenrePage { SizeName: theme.SizeNameHeadingText, } g.playRandom = widget.NewButtonWithIcon(" Play random", myTheme.ShuffleIcon, g.playRandomSongs) - g.grid = widgets.NewGridViewFromState(saved.gridState) - g.createSearchAndFilter() if g.searchText != "" { - g.searchGrid = widgets.NewGridViewFromState(saved.searchGridState) + g.grid = widgets.NewGridViewFromState(saved.searchGridState) + } else { + g.grid = widgets.NewGridViewFromState(saved.gridState) } - g.createContainer(saved.searchText != "") + g.createSearchAndFilter() + g.createContainer() return g } @@ -131,17 +131,20 @@ func (g *GenrePage) Reload() { func (g *GenrePage) Save() SavedPage { sg := &savedGenrePage{ - genre: g.genre, - filter: g.filter, - searchText: g.searchText, - contr: g.contr, - pm: g.pm, - mp: g.mp, - im: g.im, - gridState: g.grid.SaveToState(), + genre: g.genre, + filter: g.filter, + searchText: g.searchText, + contr: g.contr, + pm: g.pm, + mp: g.mp, + im: g.im, + gridState: g.gridState, + searchGridState: g.searchGridState, } - if g.searchGrid != nil { - sg.searchGridState = g.searchGrid.SaveToState() + if g.searchText != "" { + sg.searchGridState = g.grid.SaveToState() + } else { + sg.gridState = g.grid.SaveToState() } return sg } @@ -153,28 +156,20 @@ func (g *GenrePage) SearchWidget() fyne.Focusable { } func (g *GenrePage) OnSearched(query string) { - g.searchText = query if query == "" { - g.container.Objects[0] = g.grid - if g.searchGrid != nil { - g.searchGrid.Clear() - } - g.Refresh() - return + g.grid.ResetFromState(g.gridState) + } else { + g.doSearch(query) } - g.doSearch(query) + g.searchText = query } func (g *GenrePage) doSearch(query string) { - iter := g.mp.SearchAlbums(query, g.filter) - if g.searchGrid == nil { - g.searchGrid = widgets.NewGridView(widgets.NewGridViewAlbumIterator(iter), g.im, myTheme.AlbumIcon) - g.contr.ConnectAlbumGridActions(g.searchGrid) - } else { - g.searchGrid.Reset(widgets.NewGridViewAlbumIterator(iter)) + if g.searchText == "" { + g.gridState = g.grid.SaveToState() } - g.container.Objects[0] = g.searchGrid - g.Refresh() + iter := g.mp.SearchAlbums(query, g.filter) + g.grid.Reset(widgets.NewGridViewAlbumIterator(iter)) } func (g *GenrePage) playRandomSongs() { From 203070328cf60bd3bfdef67d3ff4fd82bfe464ba Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Sun, 9 Jul 2023 14:14:23 -0700 Subject: [PATCH 08/13] reuse grid view across several pages --- ui/browsing/albumpage.go | 2 +- ui/browsing/albumspage.go | 26 ++++++++++++++++++++++---- ui/browsing/artistpage.go | 12 +++++++++++- ui/browsing/artistspage.go | 20 +++++++++++++++++--- ui/browsing/genrepage.go | 29 ++++++++++++++++++++++++----- ui/browsing/playlistspage.go | 29 +++++++++++++++++++++++------ ui/browsing/router.go | 8 ++++---- ui/util/widgetpool.go | 1 + ui/widgets/gridview.go | 9 +++++---- ui/widgets/gridviewitem.go | 4 ++++ ui/widgets/imageplaceholder.go | 6 ++++-- 11 files changed, 116 insertions(+), 30 deletions(-) diff --git a/ui/browsing/albumpage.go b/ui/browsing/albumpage.go index 972f6d3..97b39e4 100644 --- a/ui/browsing/albumpage.go +++ b/ui/browsing/albumpage.go @@ -78,7 +78,7 @@ func newAlbumPage( }, } a.ExtendBaseWidget(a) - if h := a.pool.Obtain(util.WidgetTypeAlbumPageHeader); h != nil { + if h := pool.Obtain(util.WidgetTypeAlbumPageHeader); h != nil { a.header = h.(*AlbumPageHeader) a.header.Clear() } else { diff --git a/ui/browsing/albumspage.go b/ui/browsing/albumspage.go index e859b65..6cd3479 100644 --- a/ui/browsing/albumspage.go +++ b/ui/browsing/albumspage.go @@ -22,6 +22,7 @@ type AlbumsPage struct { widget.BaseWidget cfg *backend.AlbumsPageConfig + pool *util.WidgetPool contr *controller.Controller pm *backend.PlaybackManager im *backend.ImageManager @@ -57,8 +58,9 @@ func (s *selectWidget) MinSize() fyne.Size { return fyne.NewSize(170, s.Select.MinSize().Height) } -func NewAlbumsPage(cfg *backend.AlbumsPageConfig, contr *controller.Controller, pm *backend.PlaybackManager, mp mediaprovider.MediaProvider, im *backend.ImageManager) *AlbumsPage { +func NewAlbumsPage(cfg *backend.AlbumsPageConfig, pool *util.WidgetPool, contr *controller.Controller, pm *backend.PlaybackManager, mp mediaprovider.MediaProvider, im *backend.ImageManager) *AlbumsPage { a := &AlbumsPage{ + pool: pool, cfg: cfg, contr: contr, pm: pm, @@ -77,7 +79,13 @@ func NewAlbumsPage(cfg *backend.AlbumsPageConfig, contr *controller.Controller, } a.sortOrder.Selected = cfg.SortOrder iter := mp.IterateAlbums(a.sortOrder.Selected, a.filter) - a.grid = widgets.NewGridView(widgets.NewGridViewAlbumIterator(iter), im, myTheme.AlbumIcon) + if g := pool.Obtain(util.WidgetTypeGridView); g != nil { + a.grid = g.(*widgets.GridView) + a.grid.Placeholder = myTheme.AlbumIcon + a.grid.Reset(widgets.NewGridViewAlbumIterator(iter)) + } else { + a.grid = widgets.NewGridView(widgets.NewGridViewAlbumIterator(iter), im, myTheme.AlbumIcon) + } contr.ConnectAlbumGridActions(a.grid) a.createSearchAndFilter() a.createContainer() @@ -108,6 +116,7 @@ func (a *AlbumsPage) createContainer() { func restoreAlbumsPage(saved *savedAlbumsPage) *AlbumsPage { a := &AlbumsPage{ cfg: saved.cfg, + pool: saved.pool, contr: saved.contr, pm: saved.pm, mp: saved.mp, @@ -126,11 +135,16 @@ func restoreAlbumsPage(saved *savedAlbumsPage) *AlbumsPage { a.sortOrder = NewSelect(a.mp.AlbumSortOrders(), nil) a.sortOrder.Selected = saved.sortOrder a.sortOrder.OnChanged = a.onSortOrderChanged + state := saved.gridState if a.searchText != "" { a.sortOrder.Disable() - a.grid = widgets.NewGridViewFromState(saved.searchGridState) + state = saved.searchGridState + } + if g := a.pool.Obtain(util.WidgetTypeGridView); g != nil { + a.grid = g.(*widgets.GridView) + a.grid.ResetFromState(state) } else { - a.grid = widgets.NewGridViewFromState(saved.gridState) + a.grid = widgets.NewGridViewFromState(state) } a.createSearchAndFilter() a.createContainer() @@ -172,6 +186,7 @@ func (a *AlbumsPage) Reload() { func (a *AlbumsPage) Save() SavedPage { sa := &savedAlbumsPage{ cfg: a.cfg, + pool: a.pool, contr: a.contr, pm: a.pm, mp: a.mp, @@ -187,6 +202,8 @@ func (a *AlbumsPage) Save() SavedPage { } else { sa.searchGridState = a.grid.SaveToState() } + a.grid.Clear() + a.pool.Release(util.WidgetTypeGridView, a.grid) return sa } @@ -213,6 +230,7 @@ type savedAlbumsPage struct { searchText string filter mediaprovider.AlbumFilter cfg *backend.AlbumsPageConfig + pool *util.WidgetPool contr *controller.Controller pm *backend.PlaybackManager mp mediaprovider.MediaProvider diff --git a/ui/browsing/artistpage.go b/ui/browsing/artistpage.go index 8290e1b..3132b43 100644 --- a/ui/browsing/artistpage.go +++ b/ui/browsing/artistpage.go @@ -129,6 +129,10 @@ func (a *ArtistPage) Save() SavedPage { a.pool.Release(util.WidgetTypeTracklist, tl) } a.pool.Release(util.WidgetTypeArtistPageHeader, a.header) + if a.albumGrid != nil { + a.albumGrid.Clear() + a.pool.Release(util.WidgetTypeGridView, a.albumGrid) + } return &s } @@ -186,7 +190,13 @@ func (a *ArtistPage) showAlbumGrid() { Secondary: strconv.Itoa(al.Year), } }) - a.albumGrid = widgets.NewFixedGridView(model, a.im, myTheme.AlbumIcon) + if g := a.pool.Obtain(util.WidgetTypeGridView); g != nil { + a.albumGrid = g.(*widgets.GridView) + a.albumGrid.Placeholder = myTheme.AlbumIcon + a.albumGrid.ResetFixed(model) + } else { + a.albumGrid = widgets.NewFixedGridView(model, a.im, myTheme.AlbumIcon) + } a.contr.ConnectAlbumGridActions(a.albumGrid) } a.container.Objects[0].(*fyne.Container).Objects[0] = a.albumGrid diff --git a/ui/browsing/artistspage.go b/ui/browsing/artistspage.go index 7e770fa..144f52a 100644 --- a/ui/browsing/artistspage.go +++ b/ui/browsing/artistspage.go @@ -23,6 +23,7 @@ type ArtistsPage struct { widget.BaseWidget contr *controller.Controller + pool *util.WidgetPool im *backend.ImageManager pm *backend.PlaybackManager mp mediaprovider.MediaProvider @@ -40,15 +41,17 @@ type ArtistsPage struct { func NewArtistsPage( contr *controller.Controller, + pool *util.WidgetPool, pm *backend.PlaybackManager, mp mediaprovider.MediaProvider, im *backend.ImageManager, ) *ArtistsPage { - return newArtistsPage(contr, pm, mp, im, "", 0, 0) + return newArtistsPage(contr, pool, pm, mp, im, "", 0, 0) } func newArtistsPage( contr *controller.Controller, + pool *util.WidgetPool, pm *backend.PlaybackManager, mp mediaprovider.MediaProvider, im *backend.ImageManager, @@ -58,6 +61,7 @@ func newArtistsPage( ) *ArtistsPage { a := &ArtistsPage{ contr: contr, + pool: pool, pm: pm, mp: mp, im: im, @@ -74,7 +78,13 @@ func newArtistsPage( a.searcher = widgets.NewSearchEntry() a.searcher.OnSearched = func(query string) { a.onSearched(query, false /*firstLoad*/) } a.searcher.Entry.Text = searchText - a.grid = widgets.NewFixedGridView(nil, a.im, myTheme.ArtistIcon) + if g := pool.Obtain(util.WidgetTypeGridView); g != nil { + a.grid = g.(*widgets.GridView) + a.grid.Placeholder = myTheme.ArtistIcon + a.grid.Clear() + } else { + a.grid = widgets.NewFixedGridView(nil, a.im, myTheme.ArtistIcon) + } a.contr.ConnectArtistGridActions(a.grid) searchVbox := container.NewVBox(layout.NewSpacer(), a.searcher, layout.NewSpacer()) @@ -158,6 +168,7 @@ func (a *ArtistsPage) Route() controller.Route { func (a *ArtistsPage) Save() SavedPage { s := &savedArtistsPage{ contr: a.contr, + pool: a.pool, im: a.im, pm: a.pm, mp: a.mp, @@ -169,11 +180,14 @@ func (a *ArtistsPage) Save() SavedPage { } else { s.searchGridScrollPos = a.grid.GetScrollOffset() } + a.grid.Clear() + a.pool.Release(util.WidgetTypeGridView, a.grid) return s } type savedArtistsPage struct { contr *controller.Controller + pool *util.WidgetPool im *backend.ImageManager pm *backend.PlaybackManager mp mediaprovider.MediaProvider @@ -183,5 +197,5 @@ type savedArtistsPage struct { } func (s *savedArtistsPage) Restore() Page { - return newArtistsPage(s.contr, s.pm, s.mp, s.im, s.searchText, s.fullGridScrollPos, s.searchGridScrollPos) + return newArtistsPage(s.contr, s.pool, s.pm, s.mp, s.im, s.searchText, s.fullGridScrollPos, s.searchGridScrollPos) } diff --git a/ui/browsing/genrepage.go b/ui/browsing/genrepage.go index 68c90f6..48b0275 100644 --- a/ui/browsing/genrepage.go +++ b/ui/browsing/genrepage.go @@ -20,6 +20,7 @@ type GenrePage struct { widget.BaseWidget genre string + pool *util.WidgetPool contr *controller.Controller im *backend.ImageManager pm *backend.PlaybackManager @@ -37,9 +38,10 @@ type GenrePage struct { container *fyne.Container } -func NewGenrePage(genre string, contr *controller.Controller, pm *backend.PlaybackManager, mp mediaprovider.MediaProvider, im *backend.ImageManager) *GenrePage { +func NewGenrePage(genre string, pool *util.WidgetPool, contr *controller.Controller, pm *backend.PlaybackManager, mp mediaprovider.MediaProvider, im *backend.ImageManager) *GenrePage { g := &GenrePage{ genre: genre, + pool: pool, filter: mediaprovider.AlbumFilter{Genres: []string{genre}}, contr: contr, pm: pm, @@ -53,8 +55,14 @@ func NewGenrePage(genre string, contr *controller.Controller, pm *backend.Playba SizeName: theme.SizeNameHeadingText, } g.playRandom = widget.NewButtonWithIcon(" Play random", myTheme.ShuffleIcon, g.playRandomSongs) - iter := g.mp.IterateAlbums("", g.filter) - g.grid = widgets.NewGridView(widgets.NewGridViewAlbumIterator(iter), g.im, myTheme.AlbumIcon) + iter := widgets.NewGridViewAlbumIterator(g.mp.IterateAlbums("", g.filter)) + if gv := pool.Obtain(util.WidgetTypeGridView); gv != nil { + g.grid = gv.(*widgets.GridView) + g.grid.Placeholder = myTheme.AlbumIcon + g.grid.Reset(iter) + } else { + g.grid = widgets.NewGridView(iter, g.im, myTheme.AlbumIcon) + } g.contr.ConnectAlbumGridActions(g.grid) g.createSearchAndFilter() g.createContainer() @@ -85,6 +93,7 @@ func (g *GenrePage) createContainer() { func restoreGenrePage(saved *savedGenrePage) *GenrePage { g := &GenrePage{ genre: saved.genre, + pool: saved.pool, contr: saved.contr, pm: saved.pm, mp: saved.mp, @@ -101,10 +110,16 @@ func restoreGenrePage(saved *savedGenrePage) *GenrePage { SizeName: theme.SizeNameHeadingText, } g.playRandom = widget.NewButtonWithIcon(" Play random", myTheme.ShuffleIcon, g.playRandomSongs) + state := saved.gridState if g.searchText != "" { - g.grid = widgets.NewGridViewFromState(saved.searchGridState) + state = saved.searchGridState + } + if gv := g.pool.Obtain(util.WidgetTypeGridView); gv != nil { + g.grid = gv.(*widgets.GridView) + g.grid.Placeholder = myTheme.AlbumIcon + g.grid.ResetFromState(state) } else { - g.grid = widgets.NewGridViewFromState(saved.gridState) + g.grid = widgets.NewGridViewFromState(state) } g.createSearchAndFilter() g.createContainer() @@ -132,6 +147,7 @@ func (g *GenrePage) Reload() { func (g *GenrePage) Save() SavedPage { sg := &savedGenrePage{ genre: g.genre, + pool: g.pool, filter: g.filter, searchText: g.searchText, contr: g.contr, @@ -146,6 +162,8 @@ func (g *GenrePage) Save() SavedPage { } else { sg.gridState = g.grid.SaveToState() } + g.grid.Clear() + g.pool.Release(util.WidgetTypeGridView, g.grid) return sg } @@ -179,6 +197,7 @@ func (g *GenrePage) playRandomSongs() { type savedGenrePage struct { genre string searchText string + pool *util.WidgetPool filter mediaprovider.AlbumFilter contr *controller.Controller pm *backend.PlaybackManager diff --git a/ui/browsing/playlistspage.go b/ui/browsing/playlistspage.go index 46869c5..e88444e 100644 --- a/ui/browsing/playlistspage.go +++ b/ui/browsing/playlistspage.go @@ -13,6 +13,7 @@ import ( "github.com/dweymouth/supersonic/ui/controller" "github.com/dweymouth/supersonic/ui/layouts" myTheme "github.com/dweymouth/supersonic/ui/theme" + "github.com/dweymouth/supersonic/ui/util" "github.com/dweymouth/supersonic/ui/widgets" "fyne.io/fyne/v2" @@ -25,6 +26,7 @@ import ( type PlaylistsPage struct { widget.BaseWidget + pool *util.WidgetPool cfg *backend.PlaylistsPageConfig contr *controller.Controller mp mediaprovider.MediaProvider @@ -39,16 +41,17 @@ type PlaylistsPage struct { gridView *widgets.GridView } -func NewPlaylistsPage(contr *controller.Controller, cfg *backend.PlaylistsPageConfig, mp mediaprovider.MediaProvider) *PlaylistsPage { +func NewPlaylistsPage(contr *controller.Controller, pool *util.WidgetPool, cfg *backend.PlaylistsPageConfig, mp mediaprovider.MediaProvider) *PlaylistsPage { activeView := 0 if cfg.InitialView == "Grid" { activeView = 1 } - return newPlaylistsPage(contr, cfg, mp, "", activeView) + return newPlaylistsPage(contr, pool, cfg, mp, "", activeView) } -func newPlaylistsPage(contr *controller.Controller, cfg *backend.PlaylistsPageConfig, mp mediaprovider.MediaProvider, searchText string, activeView int) *PlaylistsPage { +func newPlaylistsPage(contr *controller.Controller, pool *util.WidgetPool, cfg *backend.PlaylistsPageConfig, mp mediaprovider.MediaProvider, searchText string, activeView int) *PlaylistsPage { a := &PlaylistsPage{ + pool: pool, cfg: cfg, mp: mp, contr: contr, @@ -95,7 +98,13 @@ func (a *PlaylistsPage) createListView() { func (a *PlaylistsPage) createGridView(playlists []*mediaprovider.Playlist) { model := createPlaylistGridViewModel(playlists) - a.gridView = widgets.NewFixedGridView(model, a.contr.App.ImageManager, myTheme.PlaylistIcon) + if g := a.pool.Obtain(util.WidgetTypeGridView); g != nil { + a.gridView = g.(*widgets.GridView) + a.gridView.Placeholder = myTheme.PlaylistIcon + a.gridView.ResetFixed(model) + } else { + a.gridView = widgets.NewFixedGridView(model, a.contr.App.ImageManager, myTheme.PlaylistIcon) + } a.gridView.OnPlay = func(id string, shuffle bool) { go a.contr.App.PlaybackManager.PlayPlaylist(id, 0, shuffle) } @@ -103,6 +112,7 @@ func (a *PlaylistsPage) createGridView(playlists []*mediaprovider.Playlist) { go a.contr.App.PlaybackManager.LoadPlaylist(id, true, false) } a.gridView.OnShowItemPage = a.showPlaylistPage + a.gridView.OnShowSecondaryPage = nil a.gridView.OnAddToPlaylist = func(id string) { go func() { pl, err := a.contr.App.ServerManager.Server.GetPlaylist(id) @@ -221,17 +231,24 @@ func (a *PlaylistsPage) Reload() { } func (a *PlaylistsPage) Save() SavedPage { - return &savedPlaylistsPage{ + s := &savedPlaylistsPage{ contr: a.contr, + pool: a.pool, cfg: a.cfg, mp: a.mp, searchText: a.searcher.Entry.Text, activeView: a.viewToggle.ActivatedButtonIndex(), } + if a.gridView != nil { + a.gridView.Clear() + a.pool.Release(util.WidgetTypeGridView, a.gridView) + } + return s } type savedPlaylistsPage struct { contr *controller.Controller + pool *util.WidgetPool cfg *backend.PlaylistsPageConfig mp mediaprovider.MediaProvider searchText string @@ -239,7 +256,7 @@ type savedPlaylistsPage struct { } func (s *savedPlaylistsPage) Restore() Page { - return newPlaylistsPage(s.contr, s.cfg, s.mp, s.searchText, s.activeView) + return newPlaylistsPage(s.contr, s.pool, s.cfg, s.mp, s.searchText, s.activeView) } func (a *PlaylistsPage) buildContainer(initialView fyne.CanvasObject) { diff --git a/ui/browsing/router.go b/ui/browsing/router.go index 79ac547..67dfa99 100644 --- a/ui/browsing/router.go +++ b/ui/browsing/router.go @@ -33,15 +33,15 @@ func (r Router) CreatePage(rte controller.Route) Page { case controller.Album: return NewAlbumPage(rte.Arg, &r.App.Config.AlbumPage, &r.widgetPool, 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) + return NewAlbumsPage(&r.App.Config.AlbumsPage, &r.widgetPool, r.Controller, r.App.PlaybackManager, r.App.ServerManager.Server, r.App.ImageManager) case controller.Artist: return NewArtistPage(rte.Arg, &r.App.Config.ArtistPage, &r.widgetPool, 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) + return NewArtistsPage(r.Controller, &r.widgetPool, r.App.PlaybackManager, r.App.ServerManager.Server, r.App.ImageManager) case controller.Favorites: return NewFavoritesPage(&r.App.Config.FavoritesPage, &r.widgetPool, r.Controller, r.App.ServerManager.Server, r.App.PlaybackManager, r.App.ImageManager) case controller.Genre: - return NewGenrePage(rte.Arg, r.Controller, r.App.PlaybackManager, r.App.ServerManager.Server, r.App.ImageManager) + return NewGenrePage(rte.Arg, &r.widgetPool, r.Controller, r.App.PlaybackManager, r.App.ServerManager.Server, r.App.ImageManager) case controller.Genres: return NewGenresPage(r.Controller, r.App.ServerManager.Server) case controller.NowPlaying: @@ -49,7 +49,7 @@ func (r Router) CreatePage(rte controller.Route) Page { case controller.Playlist: return NewPlaylistPage(rte.Arg, &r.App.Config.PlaylistPage, &r.widgetPool, r.Controller, r.App.ServerManager, r.App.PlaybackManager, r.App.ImageManager) case controller.Playlists: - return NewPlaylistsPage(r.Controller, &r.App.Config.PlaylistsPage, r.App.ServerManager.Server) + return NewPlaylistsPage(r.Controller, &r.widgetPool, &r.App.Config.PlaylistsPage, r.App.ServerManager.Server) case controller.Tracks: return NewTracksPage(r.Controller, &r.App.Config.TracksPage, &r.widgetPool, r.App.ServerManager.Server) } diff --git a/ui/util/widgetpool.go b/ui/util/widgetpool.go index a65133d..ea73306 100644 --- a/ui/util/widgetpool.go +++ b/ui/util/widgetpool.go @@ -11,6 +11,7 @@ type WidgetType int const ( WidgetTypeAlbumPageHeader WidgetType = iota WidgetTypeArtistPageHeader + WidgetTypeGridView WidgetTypePlaylistPageHeader WidgetTypeTracklist diff --git a/ui/widgets/gridview.go b/ui/widgets/gridview.go index a0c957d..36ada94 100644 --- a/ui/widgets/gridview.go +++ b/ui/widgets/gridview.go @@ -82,7 +82,7 @@ type GridViewState struct { items []GridViewItemModel iter GridViewIterator imageFetcher ImageFetcher - placeholder fyne.Resource + Placeholder fyne.Resource highestShown int done bool @@ -104,7 +104,7 @@ func NewFixedGridView(items []GridViewItemModel, fetch ImageFetcher, placeholder items: items, done: true, imageFetcher: fetch, - placeholder: placeholder, + Placeholder: placeholder, }, } g.ExtendBaseWidget(g) @@ -117,7 +117,7 @@ func NewGridView(iter GridViewIterator, fetch ImageFetcher, placeholder fyne.Res GridViewState: GridViewState{ iter: iter, imageFetcher: fetch, - placeholder: placeholder, + Placeholder: placeholder, }, } g.ExtendBaseWidget(g) @@ -213,7 +213,7 @@ func (g *GridView) createGridWrap() { }, // create func func() fyne.CanvasObject { - card := NewGridViewItem(g.placeholder) + card := NewGridViewItem(g.Placeholder) card.OnPlay = func(shuffle bool) { if g.OnPlay != nil { g.OnPlay(card.ItemID(), shuffle) @@ -266,6 +266,7 @@ func (g *GridView) doUpdateItemCard(itemIdx int, card *GridViewItem) { item = g.items[itemIdx] } g.stateMutex.RUnlock() + card.Cover.Im.CenterIcon = g.Placeholder if card.PrevID == item.ID { // nothing to do return diff --git a/ui/widgets/gridviewitem.go b/ui/widgets/gridviewitem.go index edd3cba..4792479 100644 --- a/ui/widgets/gridviewitem.go +++ b/ui/widgets/gridviewitem.go @@ -191,6 +191,10 @@ func (g *GridViewItem) Update(model GridViewItemModel) { g.Cover.playbtn.Hidden = true } +func (g *GridViewItem) Refresh() { + g.BaseWidget.Refresh() +} + func (g *GridViewItem) ItemID() string { return g.itemID } diff --git a/ui/widgets/imageplaceholder.go b/ui/widgets/imageplaceholder.go index 4d0de6e..531d64c 100644 --- a/ui/widgets/imageplaceholder.go +++ b/ui/widgets/imageplaceholder.go @@ -15,7 +15,8 @@ import ( // a placeholder with a rectangular border frame // and an icon positioned in the center of the frame. type ImagePlaceholder struct { - ScaleMode canvas.ImageScale + ScaleMode canvas.ImageScale + CenterIcon fyne.Resource widget.BaseWidget content *fyne.Container @@ -30,7 +31,7 @@ type ImagePlaceholder struct { } func NewImagePlaceholder(centerIcon fyne.Resource, minSize float32) *ImagePlaceholder { - i := &ImagePlaceholder{minSize: minSize} + i := &ImagePlaceholder{minSize: minSize, CenterIcon: centerIcon} i.ExtendBaseWidget(i) i.iconImage = canvas.NewImageFromResource(centerIcon) i.iconImage.FillMode = canvas.ImageFillContain @@ -86,6 +87,7 @@ func (i *ImagePlaceholder) MinSize() fyne.Size { func (i *ImagePlaceholder) Refresh() { i.border.Hidden = i.HaveImage() + i.iconImage.Resource = i.CenterIcon i.iconImage.Hidden = i.HaveImage() i.imageDisp.Hidden = !i.HaveImage() i.imageDisp.ScaleMode = i.ScaleMode From a86a81f1e6501746f5a6172c0ed2a67d2a70a08b Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Sun, 9 Jul 2023 16:52:08 -0700 Subject: [PATCH 09/13] fix bug with not resetting page reference when reusing headers --- ui/browsing/albumpage.go | 1 + ui/browsing/artistpage.go | 1 + ui/browsing/playlistpage.go | 1 + 3 files changed, 3 insertions(+) diff --git a/ui/browsing/albumpage.go b/ui/browsing/albumpage.go index 97b39e4..8b58650 100644 --- a/ui/browsing/albumpage.go +++ b/ui/browsing/albumpage.go @@ -80,6 +80,7 @@ func newAlbumPage( a.ExtendBaseWidget(a) if h := pool.Obtain(util.WidgetTypeAlbumPageHeader); h != nil { a.header = h.(*AlbumPageHeader) + a.header.page = a a.header.Clear() } else { a.header = NewAlbumPageHeader(a) diff --git a/ui/browsing/artistpage.go b/ui/browsing/artistpage.go index 3132b43..167677c 100644 --- a/ui/browsing/artistpage.go +++ b/ui/browsing/artistpage.go @@ -74,6 +74,7 @@ func newArtistPage(artistID string, cfg *backend.ArtistPageConfig, pool *util.Wi a.ExtendBaseWidget(a) if h := a.pool.Obtain(util.WidgetTypeArtistPageHeader); h != nil { a.header = h.(*ArtistPageHeader) + a.header.artistPage = a a.header.Clear() } else { a.header = NewArtistPageHeader(a) diff --git a/ui/browsing/playlistpage.go b/ui/browsing/playlistpage.go index f1b30f2..e6021fe 100644 --- a/ui/browsing/playlistpage.go +++ b/ui/browsing/playlistpage.go @@ -69,6 +69,7 @@ func newPlaylistPage( a.ExtendBaseWidget(a) if h := a.widgetPool.Obtain(util.WidgetTypePlaylistPageHeader); h != nil { a.header = h.(*PlaylistPageHeader) + a.header.page = a a.header.Clear() } else { a.header = NewPlaylistPageHeader(a) From f75a5e27a505f7c8bed49825fa0591556239ecfe Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Mon, 10 Jul 2023 13:51:09 -0700 Subject: [PATCH 10/13] clear tracks from tracklists before releasing to widget pool --- ui/browsing/albumpage.go | 1 + ui/browsing/artistpage.go | 1 + ui/browsing/favoritespage.go | 1 + ui/browsing/nowplayingpage.go | 1 + ui/browsing/playlistpage.go | 1 + ui/browsing/trackspage.go | 1 + 6 files changed, 6 insertions(+) diff --git a/ui/browsing/albumpage.go b/ui/browsing/albumpage.go index 8b58650..ac2a539 100644 --- a/ui/browsing/albumpage.go +++ b/ui/browsing/albumpage.go @@ -117,6 +117,7 @@ func (a *AlbumPage) Save() SavedPage { s.sort = a.tracklist.Sorting() a.header.page = nil a.pool.Release(util.WidgetTypeAlbumPageHeader, a.header) + a.tracklist.Clear() a.pool.Release(util.WidgetTypeTracklist, a.tracklist) return &s } diff --git a/ui/browsing/artistpage.go b/ui/browsing/artistpage.go index 167677c..1ec1789 100644 --- a/ui/browsing/artistpage.go +++ b/ui/browsing/artistpage.go @@ -127,6 +127,7 @@ func (a *ArtistPage) Save() SavedPage { if a.tracklistCtr != nil { tl := a.tracklistCtr.Objects[0].(*widgets.Tracklist) s.trackSort = tl.Sorting() + tl.Clear() a.pool.Release(util.WidgetTypeTracklist, tl) } a.pool.Release(util.WidgetTypeArtistPageHeader, a.header) diff --git a/ui/browsing/favoritespage.go b/ui/browsing/favoritespage.go index ecab761..8cfaf12 100644 --- a/ui/browsing/favoritespage.go +++ b/ui/browsing/favoritespage.go @@ -202,6 +202,7 @@ func (a *FavoritesPage) Save() SavedPage { if a.tracklistCtr != nil { tl := a.tracklistCtr.Objects[0].(*widgets.Tracklist) sf.trackSort = tl.Sorting() + tl.Clear() a.pool.Release(util.WidgetTypeTracklist, tl) } return sf diff --git a/ui/browsing/nowplayingpage.go b/ui/browsing/nowplayingpage.go index c4787d2..f93f281 100644 --- a/ui/browsing/nowplayingpage.go +++ b/ui/browsing/nowplayingpage.go @@ -99,6 +99,7 @@ func (a *NowPlayingPage) CreateRenderer() fyne.WidgetRenderer { } func (a *NowPlayingPage) Save() SavedPage { + a.tracklist.Clear() a.pool.Release(util.WidgetTypeTracklist, a.tracklist) nps := a.nowPlayingPageState return &nps diff --git a/ui/browsing/playlistpage.go b/ui/browsing/playlistpage.go index e6021fe..d28bd5a 100644 --- a/ui/browsing/playlistpage.go +++ b/ui/browsing/playlistpage.go @@ -116,6 +116,7 @@ func (a *PlaylistPage) Save() SavedPage { p := a.playlistPageState p.trackSort = a.tracklist.Sorting() p.widgetPool.Release(util.WidgetTypePlaylistPageHeader, a.header) + a.tracklist.Clear() p.widgetPool.Release(util.WidgetTypeTracklist, a.tracklist) return &p } diff --git a/ui/browsing/trackspage.go b/ui/browsing/trackspage.go index 420e793..2378a3f 100644 --- a/ui/browsing/trackspage.go +++ b/ui/browsing/trackspage.go @@ -151,6 +151,7 @@ func (t *TracksPage) CreateRenderer() fyne.WidgetRenderer { func (t *TracksPage) Save() SavedPage { t.loader.Dispose() + t.tracklist.Clear() t.widgetPool.Release(util.WidgetTypeTracklist, t.tracklist) state := t.tracksPageState return &state From 2df05144688f847103ee2422dec0c744056b235c Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Tue, 11 Jul 2023 16:38:50 -0700 Subject: [PATCH 11/13] add cleanup task to widget pool --- ui/browsing/router.go | 22 +++++++-------- ui/util/widgetpool.go | 58 +++++++++++++++++++++++++++++++------- ui/util/widgetpool_test.go | 24 ++++++++++++++++ 3 files changed, 83 insertions(+), 21 deletions(-) create mode 100644 ui/util/widgetpool_test.go diff --git a/ui/browsing/router.go b/ui/browsing/router.go index 67dfa99..631d6f8 100644 --- a/ui/browsing/router.go +++ b/ui/browsing/router.go @@ -15,7 +15,7 @@ type Router struct { App *backend.App Controller *controller.Controller Nav NavigationHandler - widgetPool util.WidgetPool + widgetPool *util.WidgetPool } func NewRouter(app *backend.App, controller *controller.Controller, nav NavigationHandler) Router { @@ -31,27 +31,27 @@ 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.widgetPool, r.App.PlaybackManager, r.App.ServerManager.Server, r.App.ImageManager, r.Controller) + return NewAlbumPage(rte.Arg, &r.App.Config.AlbumPage, r.widgetPool, r.App.PlaybackManager, r.App.ServerManager.Server, r.App.ImageManager, r.Controller) case controller.Albums: - return NewAlbumsPage(&r.App.Config.AlbumsPage, &r.widgetPool, r.Controller, r.App.PlaybackManager, r.App.ServerManager.Server, r.App.ImageManager) + return NewAlbumsPage(&r.App.Config.AlbumsPage, r.widgetPool, r.Controller, r.App.PlaybackManager, r.App.ServerManager.Server, r.App.ImageManager) case controller.Artist: - return NewArtistPage(rte.Arg, &r.App.Config.ArtistPage, &r.widgetPool, r.App.PlaybackManager, r.App.ServerManager.Server, r.App.ImageManager, r.Controller) + return NewArtistPage(rte.Arg, &r.App.Config.ArtistPage, r.widgetPool, r.App.PlaybackManager, r.App.ServerManager.Server, r.App.ImageManager, r.Controller) case controller.Artists: - return NewArtistsPage(r.Controller, &r.widgetPool, r.App.PlaybackManager, r.App.ServerManager.Server, r.App.ImageManager) + return NewArtistsPage(r.Controller, r.widgetPool, r.App.PlaybackManager, r.App.ServerManager.Server, r.App.ImageManager) case controller.Favorites: - return NewFavoritesPage(&r.App.Config.FavoritesPage, &r.widgetPool, r.Controller, r.App.ServerManager.Server, r.App.PlaybackManager, r.App.ImageManager) + return NewFavoritesPage(&r.App.Config.FavoritesPage, r.widgetPool, r.Controller, r.App.ServerManager.Server, r.App.PlaybackManager, r.App.ImageManager) case controller.Genre: - return NewGenrePage(rte.Arg, &r.widgetPool, r.Controller, r.App.PlaybackManager, r.App.ServerManager.Server, r.App.ImageManager) + return NewGenrePage(rte.Arg, r.widgetPool, r.Controller, r.App.PlaybackManager, r.App.ServerManager.Server, r.App.ImageManager) case controller.Genres: return NewGenresPage(r.Controller, r.App.ServerManager.Server) case controller.NowPlaying: - return NewNowPlayingPage(rte.Arg, r.Controller, &r.widgetPool, &r.App.Config.NowPlayingPage, r.App.PlaybackManager, r.App.Player) + return NewNowPlayingPage(rte.Arg, r.Controller, r.widgetPool, &r.App.Config.NowPlayingPage, r.App.PlaybackManager, r.App.Player) case controller.Playlist: - return NewPlaylistPage(rte.Arg, &r.App.Config.PlaylistPage, &r.widgetPool, r.Controller, r.App.ServerManager, r.App.PlaybackManager, r.App.ImageManager) + return NewPlaylistPage(rte.Arg, &r.App.Config.PlaylistPage, r.widgetPool, r.Controller, r.App.ServerManager, r.App.PlaybackManager, r.App.ImageManager) case controller.Playlists: - return NewPlaylistsPage(r.Controller, &r.widgetPool, &r.App.Config.PlaylistsPage, r.App.ServerManager.Server) + return NewPlaylistsPage(r.Controller, r.widgetPool, &r.App.Config.PlaylistsPage, r.App.ServerManager.Server) case controller.Tracks: - return NewTracksPage(r.Controller, &r.App.Config.TracksPage, &r.widgetPool, r.App.ServerManager.Server) + return NewTracksPage(r.Controller, &r.App.Config.TracksPage, r.widgetPool, r.App.ServerManager.Server) } return nil } diff --git a/ui/util/widgetpool.go b/ui/util/widgetpool.go index ea73306..5a81d12 100644 --- a/ui/util/widgetpool.go +++ b/ui/util/widgetpool.go @@ -1,6 +1,7 @@ package util import ( + "sync" "time" "fyne.io/fyne/v2" @@ -19,11 +20,16 @@ const ( numWidgetTypes ) +const ( + multipleItemsExpiry = 2 * time.Minute + singleItemExpiry = 5 * time.Minute +) + // A pool to share commonly-used widgets across pages to reduce // creation of new widgets and memory allocations. -// It is not thread-safe, which is fine for its current use. type WidgetPool struct { - pool [][]pooledWidget + mut sync.Mutex + pools [][]pooledWidget } type pooledWidget struct { @@ -31,21 +37,30 @@ type pooledWidget struct { releasedAt int64 // unixMillis } -func NewWidgetPool() WidgetPool { - return WidgetPool{ - pool: make([][]pooledWidget, numWidgetTypes), +func NewWidgetPool() *WidgetPool { + p := &WidgetPool{ + pools: make([][]pooledWidget, numWidgetTypes), } + go func() { + t := time.NewTicker(2 * time.Minute) + for range t.C { + p.cleanUpExpiredItems() + } + }() + return p } // Obtain obtains a widget of the given type from the pool, if one exists. // Returns nil if there is no available widget. func (w *WidgetPool) Obtain(typ WidgetType) fyne.CanvasObject { + w.mut.Lock() + defer w.mut.Unlock() var widget fyne.CanvasObject - if l := len(w.pool[typ]); l > 0 { + if l := len(w.pools[typ]); l > 0 { i := l - 1 - widget = w.pool[typ][i].widget - w.pool[typ][i].widget = nil - w.pool[typ] = w.pool[typ][:i] + widget = w.pools[typ][i].widget + w.pools[typ][i].widget = nil + w.pools[typ] = w.pools[typ][:i] } return widget } @@ -54,8 +69,31 @@ func (w *WidgetPool) Obtain(typ WidgetType) fyne.CanvasObject { // The widget must not be modified by the releaser after release, // since it may be Obtained for a new use at any time. func (w *WidgetPool) Release(typ WidgetType, wid fyne.CanvasObject) { - w.pool[typ] = append(w.pool[typ], pooledWidget{ + w.mut.Lock() + defer w.mut.Unlock() + w.pools[typ] = append(w.pools[typ], pooledWidget{ widget: wid, releasedAt: time.Now().UnixMilli(), }) } + +func (w *WidgetPool) cleanUpExpiredItems() { + w.mut.Lock() + defer w.mut.Unlock() + for widTyp, pool := range w.pools { + newP := make([]pooledWidget, 0, len(pool)) + l := len(pool) + for _, wid := range pool { + timeSinceRelease := time.Since(time.UnixMilli(wid.releasedAt)) + if l > 1 && timeSinceRelease > multipleItemsExpiry { + l-- // let expire if >1 item in pool and released long enough ago + } else if l == 1 && timeSinceRelease > singleItemExpiry { + l-- // let expire if last item in pool and released long enough ago + } else { + newP = append(newP, wid) // not expired + } + } + // re-assign non-expired items back to this widget type pool + w.pools[widTyp] = newP + } +} diff --git a/ui/util/widgetpool_test.go b/ui/util/widgetpool_test.go new file mode 100644 index 0000000..79ea851 --- /dev/null +++ b/ui/util/widgetpool_test.go @@ -0,0 +1,24 @@ +package util + +import ( + "testing" + "time" +) + +func Test_WidgetPool_CleanupExpiredItems(t *testing.T) { + now := time.Now() + threeMinAgo := now.Add(-3 * time.Minute) + w := &WidgetPool{ + pools: [][]pooledWidget{ + { + {releasedAt: threeMinAgo.UnixMilli()}, + {releasedAt: threeMinAgo.UnixMilli()}, + {releasedAt: threeMinAgo.UnixMilli()}, + }, + }, + } + w.cleanUpExpiredItems() + if l := len(w.pools[0]); l != 1 { + t.Errorf("Expected one widget in pool after cleanup, got %d", l) + } +} From 8ac7b1935c22c32772caf73f148dc281c8052a8c Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Tue, 11 Jul 2023 16:50:16 -0700 Subject: [PATCH 12/13] reuse album grid on favorites page for searching --- ui/browsing/favoritespage.go | 87 +++++++++++++++++------------------- 1 file changed, 40 insertions(+), 47 deletions(-) diff --git a/ui/browsing/favoritespage.go b/ui/browsing/favoritespage.go index 8cfaf12..1b43d48 100644 --- a/ui/browsing/favoritespage.go +++ b/ui/browsing/favoritespage.go @@ -36,15 +36,16 @@ type FavoritesPage struct { nowPlayingID string pendingViewSwitch bool - grid *widgets.GridView - searchGrid *widgets.GridView - artistGrid *widgets.GridView - tracklistCtr *fyne.Container - searcher *widgets.SearchEntry - filterBtn *widgets.AlbumFilterButton - titleDisp *widget.RichText - toggleBtns *widgets.ToggleButtonGroup - container *fyne.Container + grid *widgets.GridView + gridState *widgets.GridViewState + searchGridState *widgets.GridViewState + artistGrid *widgets.GridView + tracklistCtr *fyne.Container + searcher *widgets.SearchEntry + filterBtn *widgets.AlbumFilterButton + titleDisp *widget.RichText + toggleBtns *widgets.ToggleButtonGroup + container *fyne.Container } func NewFavoritesPage(cfg *backend.FavoritesPageConfig, pool *util.WidgetPool, contr *controller.Controller, mp mediaprovider.MediaProvider, pm *backend.PlaybackManager, im *backend.ImageManager) *FavoritesPage { @@ -100,31 +101,31 @@ func (a *FavoritesPage) createContainer(initialView fyne.CanvasObject) { func restoreFavoritesPage(saved *savedFavoritesPage) *FavoritesPage { a := &FavoritesPage{ - cfg: saved.cfg, - contr: saved.contr, - pool: saved.pool, - pm: saved.pm, - mp: saved.mp, - im: saved.im, - searchText: saved.searchText, - filter: saved.filter, - trackSort: saved.trackSort, + cfg: saved.cfg, + contr: saved.contr, + pool: saved.pool, + pm: saved.pm, + mp: saved.mp, + im: saved.im, + gridState: saved.gridState, + searchGridState: saved.searchGridState, + searchText: saved.searchText, + filter: saved.filter, + trackSort: saved.trackSort, } a.ExtendBaseWidget(a) a.createHeader(saved.activeToggleBtn) - a.grid = widgets.NewGridViewFromState(saved.gridState) - + state := saved.gridState if saved.searchText != "" { - a.searchGrid = widgets.NewGridViewFromState(saved.searchGridState) + state = saved.searchGridState } + a.grid = widgets.NewGridViewFromState(state) a.toggleBtns.SetActivatedButton(saved.activeToggleBtn) if saved.activeToggleBtn == 1 { a.onShowFavoriteArtists() } else if saved.activeToggleBtn == 2 { a.onShowFavoriteSongs() - } else if saved.searchText != "" { - a.createContainer(a.searchGrid) } else { a.createContainer(a.grid) } @@ -193,11 +194,14 @@ func (a *FavoritesPage) Save() SavedPage { im: a.im, filter: a.filter, searchText: a.searchText, - gridState: a.grid.SaveToState(), + gridState: a.gridState, + searchGridState: a.searchGridState, activeToggleBtn: a.toggleBtns.ActivatedButtonIndex(), } - if a.searchGrid != nil { - sf.searchGridState = a.searchGrid.SaveToState() + if a.searchText != "" { + sf.searchGridState = a.grid.SaveToState() + } else { + sf.gridState = a.grid.SaveToState() } if a.tracklistCtr != nil { tl := a.tracklistCtr.Objects[0].(*widgets.Tracklist) @@ -215,16 +219,13 @@ func (a *FavoritesPage) SearchWidget() fyne.Focusable { } func (a *FavoritesPage) OnSearched(query string) { - a.searchText = query if query == "" { - a.container.Objects[0] = a.grid - if a.searchGrid != nil { - a.searchGrid.Clear() - } - a.Refresh() - return + a.grid.ResetFromState(a.gridState) + a.searchGridState = nil + } else { + a.doSearchAlbums(query) } - a.doSearchAlbums(query) + a.searchText = query } var _ CanShowNowPlaying = (*FavoritesPage)(nil) @@ -248,26 +249,18 @@ func (a *FavoritesPage) SelectAll() { } func (a *FavoritesPage) doSearchAlbums(query string) { - iter := a.mp.SearchAlbums(query, a.filter) - if a.searchGrid == nil { - a.searchGrid = widgets.NewGridView(widgets.NewGridViewAlbumIterator(iter), a.im, myTheme.AlbumIcon) - a.contr.ConnectAlbumGridActions(a.searchGrid) - } else { - a.searchGrid.Reset(widgets.NewGridViewAlbumIterator(iter)) + if a.searchText == "" { + a.gridState = a.grid.SaveToState() } - a.container.Objects[0] = a.searchGrid - a.Refresh() + iter := widgets.NewGridViewAlbumIterator(a.mp.SearchAlbums(query, a.filter)) + a.grid.Reset(iter) } func (a *FavoritesPage) onShowFavoriteAlbums() { a.cfg.InitialView = "Albums" // save setting a.searcher.Entry.Show() a.filterBtn.Show() - if a.searchText == "" { - a.container.Objects[0] = a.grid - } else { - a.container.Objects[0] = a.searchGrid - } + a.container.Objects[0] = a.grid a.Refresh() } From caad7c7a2732bc18c5c3cd0e20e360ccee861bae Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Tue, 11 Jul 2023 16:58:47 -0700 Subject: [PATCH 13/13] use GridView widgets from pool in favorites page --- ui/browsing/favoritespage.go | 32 ++++++++++++++++++++++++++++---- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/ui/browsing/favoritespage.go b/ui/browsing/favoritespage.go index 1b43d48..368a036 100644 --- a/ui/browsing/favoritespage.go +++ b/ui/browsing/favoritespage.go @@ -60,8 +60,14 @@ func NewFavoritesPage(cfg *backend.FavoritesPageConfig, pool *util.WidgetPool, c } a.ExtendBaseWidget(a) a.createHeader(0) - iter := mp.IterateAlbums("", a.filter) - a.grid = widgets.NewGridView(widgets.NewGridViewAlbumIterator(iter), a.im, myTheme.AlbumIcon) + iter := widgets.NewGridViewAlbumIterator(mp.IterateAlbums("", a.filter)) + if g := pool.Obtain(util.WidgetTypeGridView); g != nil { + a.grid = g.(*widgets.GridView) + a.grid.Placeholder = myTheme.AlbumIcon + a.grid.Reset(iter) + } else { + a.grid = widgets.NewGridView(iter, a.im, myTheme.AlbumIcon) + } a.contr.ConnectAlbumGridActions(a.grid) if cfg.InitialView == "Artists" { a.toggleBtns.SetActivatedButton(1) @@ -119,7 +125,13 @@ func restoreFavoritesPage(saved *savedFavoritesPage) *FavoritesPage { if saved.searchText != "" { state = saved.searchGridState } - a.grid = widgets.NewGridViewFromState(state) + if g := a.pool.Obtain(util.WidgetTypeGridView); g != nil { + a.grid = g.(*widgets.GridView) + a.grid.Placeholder = myTheme.AlbumIcon + a.grid.ResetFromState(state) + } else { + a.grid = widgets.NewGridViewFromState(state) + } a.toggleBtns.SetActivatedButton(saved.activeToggleBtn) if saved.activeToggleBtn == 1 { @@ -203,6 +215,12 @@ func (a *FavoritesPage) Save() SavedPage { } else { sf.gridState = a.grid.SaveToState() } + a.grid.Clear() + a.pool.Release(util.WidgetTypeGridView, a.grid) + if a.artistGrid != nil { + a.artistGrid.Clear() + a.pool.Release(util.WidgetTypeGridView, a.artistGrid) + } if a.tracklistCtr != nil { tl := a.tracklistCtr.Objects[0].(*widgets.Tracklist) sf.trackSort = tl.Sorting() @@ -286,7 +304,13 @@ func (a *FavoritesPage) onShowFavoriteArtists() { return } model := buildArtistGridViewModel(fav.Artists) - a.artistGrid = widgets.NewFixedGridView(model, a.im, myTheme.ArtistIcon) + if g := a.pool.Obtain(util.WidgetTypeGridView); g != nil { + a.artistGrid = g.(*widgets.GridView) + a.artistGrid.Placeholder = myTheme.ArtistIcon + a.artistGrid.ResetFixed(model) + } else { + a.artistGrid = widgets.NewFixedGridView(model, a.im, myTheme.ArtistIcon) + } a.contr.ConnectArtistGridActions(a.artistGrid) a.container.Objects[0] = a.artistGrid a.Refresh()