diff --git a/ui/browsing/albumpage.go b/ui/browsing/albumpage.go index a7ffa91..ac2a539 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 @@ -36,6 +37,7 @@ type albumPageState struct { albumID string sort widgets.TracklistSort cfg *backend.AlbumPageConfig + pool *util.WidgetPool mp mediaprovider.MediaProvider pm *backend.PlaybackManager im *backend.ImageManager @@ -45,17 +47,19 @@ type albumPageState struct { func NewAlbumPage( albumID string, cfg *backend.AlbumPageConfig, + pool *util.WidgetPool, 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, pool, pm, mp, im, contr, widgets.TracklistSort{}) } func newAlbumPage( albumID string, cfg *backend.AlbumPageConfig, + pool *util.WidgetPool, pm *backend.PlaybackManager, mp mediaprovider.MediaProvider, im *backend.ImageManager, @@ -66,6 +70,7 @@ func newAlbumPage( albumPageState: albumPageState{ albumID: albumID, cfg: cfg, + pool: pool, pm: pm, mp: mp, im: im, @@ -73,8 +78,20 @@ func newAlbumPage( }, } a.ExtendBaseWidget(a) - a.header = NewAlbumPageHeader(a) - a.tracklist = widgets.NewTracklist(nil) + if h := pool.Obtain(util.WidgetTypeAlbumPageHeader); h != nil { + a.header = h.(*AlbumPageHeader) + a.header.page = a + a.header.Clear() + } else { + a.header = NewAlbumPageHeader(a) + } + a.header.page = a + 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) { @@ -95,8 +112,13 @@ func (a *AlbumPage) CreateRenderer() fyne.WidgetRenderer { } func (a *AlbumPage) Save() SavedPage { + a.disposed = true s := a.albumPageState 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 } @@ -133,8 +155,13 @@ 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.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 +283,19 @@ 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 + a.cover.Refresh() +} + func (a *AlbumPageHeader) toggleFavorited() { params := mediaprovider.RatingFavoriteParameters{AlbumIDs: []string{a.albumID}} a.page.mp.SetFavorite(params, a.toggleFavButton.IsFavorited) @@ -267,7 +307,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 { @@ -283,5 +325,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.pool, s.pm, s.mp, s.im, s.contr, s.sort) } diff --git a/ui/browsing/albumspage.go b/ui/browsing/albumspage.go index 455594f..6cd3479 100644 --- a/ui/browsing/albumspage.go +++ b/ui/browsing/albumspage.go @@ -21,20 +21,22 @@ 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 + pool *util.WidgetPool + 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 { @@ -56,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, @@ -76,10 +79,16 @@ 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(false) + a.createContainer() return a } @@ -92,31 +101,30 @@ 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, + pool: saved.pool, + 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 +135,33 @@ 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) + state := saved.gridState if a.searchText != "" { - a.searchGrid = widgets.NewGridViewFromState(saved.searchGridState) + a.sortOrder.Disable() + 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(state) } 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 +180,45 @@ 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, + pool: a.pool, + 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() } + a.grid.Clear() + a.pool.Release(util.WidgetTypeGridView, a.grid) 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 { @@ -219,13 +230,14 @@ type savedAlbumsPage struct { searchText string filter mediaprovider.AlbumFilter cfg *backend.AlbumsPageConfig + pool *util.WidgetPool contr *controller.Controller pm *backend.PlaybackManager 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/artistpage.go b/ui/browsing/artistpage.go index 6a03606..1ec1789 100644 --- a/ui/browsing/artistpage.go +++ b/ui/browsing/artistpage.go @@ -28,6 +28,7 @@ type artistPageState struct { activeView int trackSort widgets.TracklistSort + pool *util.WidgetPool cfg *backend.ArtistPageConfig pm *backend.PlaybackManager mp mediaprovider.MediaProvider @@ -39,6 +40,7 @@ type ArtistPage struct { widget.BaseWidget artistPageState + disposed bool artistInfo *mediaprovider.ArtistWithAlbums @@ -49,18 +51,19 @@ 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, 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, 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, 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, + pool: pool, pm: pm, mp: mp, im: im, @@ -69,7 +72,14 @@ func newArtistPage(artistID string, cfg *backend.ArtistPageConfig, pm *backend.P trackSort: sort, }} a.ExtendBaseWidget(a) - a.header = NewArtistPageHeader(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) + } + a.header.artistPage = a if img, ok := im.GetCachedArtistImage(artistID); ok { a.header.artistImage.SetImage(img, true /*tappable*/) } @@ -112,9 +122,18 @@ 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() + tl.Clear() + 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 } @@ -141,6 +160,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 { @@ -170,7 +192,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 @@ -189,8 +217,18 @@ func (a *ArtistPage) showTopTracks() { log.Printf("error getting top songs: %s", err.Error()) return } - tl := widgets.NewTracklist(ts) - tl.AutoNumber = true + 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) tl.OnVisibleColumnsChanged = func(cols []string) { @@ -228,9 +266,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.pool, 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 +290,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 +313,17 @@ 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 + for _, obj := range a.similarArtists.Objects { + obj.Hide() + } + a.artistImage.SetImage(nil, false) +} + func (a *ArtistPageHeader) Update(artist *mediaprovider.ArtistWithAlbums) { if artist == nil { return @@ -299,21 +350,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() 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/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..368a036 100644 --- a/ui/browsing/favoritespage.go +++ b/ui/browsing/favoritespage.go @@ -23,32 +23,36 @@ 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 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, 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, @@ -56,8 +60,14 @@ func NewFavoritesPage(cfg *backend.FavoritesPageConfig, contr *controller.Contro } 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) @@ -97,21 +107,30 @@ func (a *FavoritesPage) createContainer(initialView fyne.CanvasObject) { func restoreFavoritesPage(saved *savedFavoritesPage) *FavoritesPage { a := &FavoritesPage{ - cfg: saved.cfg, - contr: saved.contr, - 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 + } + 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) @@ -119,8 +138,6 @@ func restoreFavoritesPage(saved *savedFavoritesPage) *FavoritesPage { a.onShowFavoriteArtists() } else if saved.activeToggleBtn == 2 { a.onShowFavoriteSongs() - } else if saved.searchText != "" { - a.createContainer(a.searchGrid) } else { a.createContainer(a.grid) } @@ -154,6 +171,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,22 +196,36 @@ 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, 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() + } + 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 { - sf.trackSort = a.tracklistCtr.Objects[0].(*widgets.Tracklist).Sorting() + tl := a.tracklistCtr.Objects[0].(*widgets.Tracklist) + sf.trackSort = tl.Sorting() + tl.Clear() + a.pool.Release(util.WidgetTypeTracklist, tl) } return sf } @@ -203,16 +237,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) @@ -236,26 +267,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() } @@ -277,8 +300,17 @@ 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) + 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() @@ -325,8 +357,18 @@ func (a *FavoritesPage) onShowFavoriteSongs() { log.Printf("error getting starred items: %s", err.Error()) return } - tracklist := widgets.NewTracklist(fav.Tracks) - tracklist.AutoNumber = true + 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) tracklist.OnVisibleColumnsChanged = func(cols []string) { @@ -355,11 +397,12 @@ 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 - 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..48b0275 100644 --- a/ui/browsing/genrepage.go +++ b/ui/browsing/genrepage.go @@ -19,26 +19,29 @@ 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 + pool *util.WidgetPool + 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 } -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, @@ -52,11 +55,17 @@ 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(false) + g.createContainer() return g } @@ -71,29 +80,28 @@ 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, + pool: saved.pool, + 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 +110,19 @@ 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() + state := saved.gridState if g.searchText != "" { - g.searchGrid = widgets.NewGridViewFromState(saved.searchGridState) + state = saved.searchGridState } - g.createContainer(saved.searchText != "") + 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(state) + } + g.createSearchAndFilter() + g.createContainer() return g } @@ -126,24 +141,29 @@ func (g *GenrePage) Reload() { } else { iter := g.mp.IterateAlbums("", g.filter) g.grid.Reset(widgets.NewGridViewAlbumIterator(iter)) - g.grid.Refresh() } } 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, + pool: g.pool, + 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() } + g.grid.Clear() + g.pool.Release(util.WidgetTypeGridView, g.grid) return sg } @@ -154,28 +174,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() { @@ -185,13 +197,14 @@ func (g *GenrePage) playRandomSongs() { type savedGenrePage struct { genre string searchText string + pool *util.WidgetPool filter mediaprovider.AlbumFilter contr *controller.Controller 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/browsing/nowplayingpage.go b/ui/browsing/nowplayingpage.go index dfe280c..f93f281 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,30 +47,40 @@ 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 } - 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") @@ -88,6 +99,8 @@ 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 } @@ -205,5 +218,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 39d5182..d28bd5a 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,41 @@ 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.page = a + 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) { @@ -77,8 +93,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) @@ -94,8 +112,12 @@ 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) + a.tracklist.Clear() + p.widgetPool.Release(util.WidgetTypeTracklist, a.tracklist) return &p } @@ -132,6 +154,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) @@ -200,7 +225,7 @@ func (a *PlaylistPage) onRemoveSelectedFromPlaylist() { } a.sm.Server.EditPlaylistTracks(a.playlistID, nil, idxs) a.tracklist.UnselectAll() - go a.Reload() + a.Reload() } type PlaylistPageHeader struct { @@ -280,6 +305,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) } @@ -325,5 +358,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/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 c91cf6a..631d6f8 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 { @@ -14,6 +15,7 @@ type Router struct { App *backend.App Controller *controller.Controller Nav NavigationHandler + widgetPool *util.WidgetPool } func NewRouter(app *backend.App, controller *controller.Controller, nav NavigationHandler) Router { @@ -21,6 +23,7 @@ func NewRouter(app *backend.App, controller *controller.Controller, nav Navigati App: app, Controller: controller, Nav: nav, + widgetPool: util.NewWidgetPool(), } return r } @@ -28,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.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) + 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.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) + 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.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) + 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.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) + 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.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 2c21197..2378a3f 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,18 +35,26 @@ 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) - t.tracklist.DisableSorting = true - t.tracklist.AutoNumber = true + 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, + } t.tracklist.SetVisibleColumns(conf.TracklistColumns) t.tracklist.OnVisibleColumnsChanged = func(cols []string) { t.conf.TracklistColumns = cols @@ -119,7 +128,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) { @@ -141,12 +150,15 @@ 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 } 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/widgetpool.go b/ui/util/widgetpool.go new file mode 100644 index 0000000..5a81d12 --- /dev/null +++ b/ui/util/widgetpool.go @@ -0,0 +1,99 @@ +package util + +import ( + "sync" + "time" + + "fyne.io/fyne/v2" +) + +type WidgetType int + +const ( + WidgetTypeAlbumPageHeader WidgetType = iota + WidgetTypeArtistPageHeader + WidgetTypeGridView + WidgetTypePlaylistPageHeader + WidgetTypeTracklist + + // keep at bottom + 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. +type WidgetPool struct { + mut sync.Mutex + pools [][]pooledWidget +} + +type pooledWidget struct { + widget fyne.CanvasObject + releasedAt int64 // unixMillis +} + +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.pools[typ]); l > 0 { + i := l - 1 + widget = w.pools[typ][i].widget + w.pools[typ][i].widget = nil + w.pools[typ] = w.pools[typ][: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) { + 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) + } +} diff --git a/ui/widgets/gridview.go b/ui/widgets/gridview.go index 8e3e1d4..36ada94 100644 --- a/ui/widgets/gridview.go +++ b/ui/widgets/gridview.go @@ -71,6 +71,8 @@ func NewGridViewAlbumIterator(iter mediaprovider.AlbumIterator) GridViewIterator type GridView struct { widget.BaseWidget + stateMutex sync.RWMutex + fetchCancel context.CancelFunc GridViewState grid *xwidget.GridWrap @@ -78,12 +80,10 @@ type GridView struct { type GridViewState struct { items []GridViewItemModel - itemsMutex sync.RWMutex iter GridViewIterator imageFetcher ImageFetcher - placeholder fyne.Resource + Placeholder fyne.Resource highestShown int - fetchCancel context.CancelFunc done bool OnPlay func(id string, shuffle 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) @@ -128,14 +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 @@ -144,8 +146,12 @@ func NewGridViewFromState(state GridViewState) *GridView { } func (g *GridView) Clear() { - g.itemsMutex.Lock() - defer g.itemsMutex.Unlock() + if g.fetchCancel != nil { + g.fetchCancel() + g.fetchCancel = nil + } + g.stateMutex.Lock() + defer g.stateMutex.Unlock() g.items = nil g.done = true } @@ -155,13 +161,27 @@ 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) + 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) { @@ -169,12 +189,13 @@ 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() + g.Refresh() } func (g *GridView) GetScrollOffset() float32 { @@ -192,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) @@ -220,7 +241,7 @@ func (g *GridView) createGridWrap() { } card.OnDownload = func() { if g.OnDownload != nil { - g.OnDownload(card.itemID) + g.OnDownload(card.ItemID()) } } return card @@ -238,13 +259,14 @@ 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() + card.Cover.Im.CenterIcon = g.Placeholder if card.PrevID == item.ID { // nothing to do return @@ -290,8 +312,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,15 +336,15 @@ 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 } n += len(items) if len(items) > 0 { - g.Refresh() + g.grid.Refresh() } } } diff --git a/ui/widgets/gridviewitem.go b/ui/widgets/gridviewitem.go index e978522..9d0ed83 100644 --- a/ui/widgets/gridviewitem.go +++ b/ui/widgets/gridviewitem.go @@ -211,6 +211,10 @@ func (g *GridViewItem) Update(model GridViewItemModel) { g.Cover.ResetPlayButton() } +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 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 } } 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()