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()