Merge pull request #217 from dweymouth/perf/reuse-widgets

Reuse complex widgets across pages to reduce memory spikes when browsing
This commit is contained in:
Drew Weymouth
2023-07-12 17:01:07 -07:00
committed by GitHub
19 changed files with 726 additions and 285 deletions
+48 -6
View File
@@ -25,6 +25,7 @@ type AlbumPage struct {
albumPageState albumPageState
disposed bool
header *AlbumPageHeader header *AlbumPageHeader
tracks []*mediaprovider.Track tracks []*mediaprovider.Track
tracklist *widgets.Tracklist tracklist *widgets.Tracklist
@@ -36,6 +37,7 @@ type albumPageState struct {
albumID string albumID string
sort widgets.TracklistSort sort widgets.TracklistSort
cfg *backend.AlbumPageConfig cfg *backend.AlbumPageConfig
pool *util.WidgetPool
mp mediaprovider.MediaProvider mp mediaprovider.MediaProvider
pm *backend.PlaybackManager pm *backend.PlaybackManager
im *backend.ImageManager im *backend.ImageManager
@@ -45,17 +47,19 @@ type albumPageState struct {
func NewAlbumPage( func NewAlbumPage(
albumID string, albumID string,
cfg *backend.AlbumPageConfig, cfg *backend.AlbumPageConfig,
pool *util.WidgetPool,
pm *backend.PlaybackManager, pm *backend.PlaybackManager,
mp mediaprovider.MediaProvider, mp mediaprovider.MediaProvider,
im *backend.ImageManager, im *backend.ImageManager,
contr *controller.Controller, contr *controller.Controller,
) *AlbumPage { ) *AlbumPage {
return newAlbumPage(albumID, cfg, pm, mp, im, contr, widgets.TracklistSort{}) return newAlbumPage(albumID, cfg, pool, pm, mp, im, contr, widgets.TracklistSort{})
} }
func newAlbumPage( func newAlbumPage(
albumID string, albumID string,
cfg *backend.AlbumPageConfig, cfg *backend.AlbumPageConfig,
pool *util.WidgetPool,
pm *backend.PlaybackManager, pm *backend.PlaybackManager,
mp mediaprovider.MediaProvider, mp mediaprovider.MediaProvider,
im *backend.ImageManager, im *backend.ImageManager,
@@ -66,6 +70,7 @@ func newAlbumPage(
albumPageState: albumPageState{ albumPageState: albumPageState{
albumID: albumID, albumID: albumID,
cfg: cfg, cfg: cfg,
pool: pool,
pm: pm, pm: pm,
mp: mp, mp: mp,
im: im, im: im,
@@ -73,8 +78,20 @@ func newAlbumPage(
}, },
} }
a.ExtendBaseWidget(a) a.ExtendBaseWidget(a)
a.header = NewAlbumPageHeader(a) if h := pool.Obtain(util.WidgetTypeAlbumPageHeader); h != nil {
a.tracklist = widgets.NewTracklist(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.SetVisibleColumns(a.cfg.TracklistColumns)
a.tracklist.SetSorting(sort) a.tracklist.SetSorting(sort)
a.tracklist.OnVisibleColumnsChanged = func(cols []string) { a.tracklist.OnVisibleColumnsChanged = func(cols []string) {
@@ -95,8 +112,13 @@ func (a *AlbumPage) CreateRenderer() fyne.WidgetRenderer {
} }
func (a *AlbumPage) Save() SavedPage { func (a *AlbumPage) Save() SavedPage {
a.disposed = true
s := a.albumPageState s := a.albumPageState
s.sort = a.tracklist.Sorting() 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 return &s
} }
@@ -133,8 +155,13 @@ func (a *AlbumPage) load() {
log.Printf("Failed to get album: %s", err.Error()) log.Printf("Failed to get album: %s", err.Error())
return return
} }
if a.disposed {
return
}
a.header.Update(album, a.im) 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.tracks = album.Tracks
a.tracklist.SetTracks(album.Tracks) a.tracklist.SetTracks(album.Tracks)
a.tracklist.SetNowPlaying(a.nowPlayingID) 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() { func (a *AlbumPageHeader) toggleFavorited() {
params := mediaprovider.RatingFavoriteParameters{AlbumIDs: []string{a.albumID}} params := mediaprovider.RatingFavoriteParameters{AlbumIDs: []string{a.albumID}}
a.page.mp.SetFavorite(params, a.toggleFavButton.IsFavorited) 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()) log.Printf("error getting full size album cover: %s", err.Error())
return return
} }
a.page.contr.ShowPopUpImage(cover) if a.page != nil {
a.page.contr.ShowPopUpImage(cover)
}
} }
func formatMiscLabelStr(a *mediaprovider.AlbumWithTracks) string { func formatMiscLabelStr(a *mediaprovider.AlbumWithTracks) string {
@@ -283,5 +325,5 @@ func formatMiscLabelStr(a *mediaprovider.AlbumWithTracks) string {
} }
func (s *albumPageState) Restore() Page { 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)
} }
+79 -67
View File
@@ -21,20 +21,22 @@ var _ fyne.Widget = (*AlbumsPage)(nil)
type AlbumsPage struct { type AlbumsPage struct {
widget.BaseWidget widget.BaseWidget
cfg *backend.AlbumsPageConfig cfg *backend.AlbumsPageConfig
contr *controller.Controller pool *util.WidgetPool
pm *backend.PlaybackManager contr *controller.Controller
im *backend.ImageManager pm *backend.PlaybackManager
mp mediaprovider.MediaProvider im *backend.ImageManager
grid *widgets.GridView mp mediaprovider.MediaProvider
searchGrid *widgets.GridView grid *widgets.GridView
searcher *widgets.SearchEntry gridState *widgets.GridViewState
filterBtn *widgets.AlbumFilterButton searchGridState *widgets.GridViewState
searchText string searcher *widgets.SearchEntry
filter mediaprovider.AlbumFilter filterBtn *widgets.AlbumFilterButton
titleDisp *widget.RichText searchText string
sortOrder *selectWidget filter mediaprovider.AlbumFilter
container *fyne.Container titleDisp *widget.RichText
sortOrder *selectWidget
container *fyne.Container
} }
type selectWidget struct { type selectWidget struct {
@@ -56,8 +58,9 @@ func (s *selectWidget) MinSize() fyne.Size {
return fyne.NewSize(170, s.Select.MinSize().Height) 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{ a := &AlbumsPage{
pool: pool,
cfg: cfg, cfg: cfg,
contr: contr, contr: contr,
pm: pm, pm: pm,
@@ -76,10 +79,16 @@ func NewAlbumsPage(cfg *backend.AlbumsPageConfig, contr *controller.Controller,
} }
a.sortOrder.Selected = cfg.SortOrder a.sortOrder.Selected = cfg.SortOrder
iter := mp.IterateAlbums(a.sortOrder.Selected, a.filter) 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) contr.ConnectAlbumGridActions(a.grid)
a.createSearchAndFilter() a.createSearchAndFilter()
a.createContainer(false) a.createContainer()
return a return a
} }
@@ -92,31 +101,30 @@ func (a *AlbumsPage) createSearchAndFilter() {
a.filterBtn.OnChanged = a.Reload a.filterBtn.OnChanged = a.Reload
} }
func (a *AlbumsPage) createContainer(searchgrid bool) { func (a *AlbumsPage) createContainer() {
searchVbox := container.NewVBox(layout.NewSpacer(), a.searcher, layout.NewSpacer()) searchVbox := container.NewVBox(layout.NewSpacer(), a.searcher, layout.NewSpacer())
sortVbox := container.NewVBox(layout.NewSpacer(), a.sortOrder, layout.NewSpacer()) sortVbox := container.NewVBox(layout.NewSpacer(), a.sortOrder, layout.NewSpacer())
g := a.grid
if searchgrid {
g = a.searchGrid
}
a.container = container.NewBorder( a.container = container.NewBorder(
container.NewHBox(util.NewHSpace(6), a.titleDisp, sortVbox, layout.NewSpacer(), container.NewCenter(a.filterBtn), searchVbox, util.NewHSpace(12)), container.NewHBox(util.NewHSpace(6), a.titleDisp, sortVbox, layout.NewSpacer(), container.NewCenter(a.filterBtn), searchVbox, util.NewHSpace(12)),
nil, nil,
nil, nil,
nil, nil,
g, a.grid,
) )
} }
func restoreAlbumsPage(saved *savedAlbumsPage) *AlbumsPage { func restoreAlbumsPage(saved *savedAlbumsPage) *AlbumsPage {
a := &AlbumsPage{ a := &AlbumsPage{
cfg: saved.cfg, cfg: saved.cfg,
contr: saved.contr, pool: saved.pool,
pm: saved.pm, contr: saved.contr,
mp: saved.mp, pm: saved.pm,
im: saved.im, mp: saved.mp,
searchText: saved.searchText, im: saved.im,
filter: saved.filter, gridState: saved.gridState,
searchGridState: saved.searchGridState,
searchText: saved.searchText,
filter: saved.filter,
} }
a.ExtendBaseWidget(a) a.ExtendBaseWidget(a)
@@ -127,27 +135,33 @@ func restoreAlbumsPage(saved *savedAlbumsPage) *AlbumsPage {
a.sortOrder = NewSelect(a.mp.AlbumSortOrders(), nil) a.sortOrder = NewSelect(a.mp.AlbumSortOrders(), nil)
a.sortOrder.Selected = saved.sortOrder a.sortOrder.Selected = saved.sortOrder
a.sortOrder.OnChanged = a.onSortOrderChanged a.sortOrder.OnChanged = a.onSortOrderChanged
a.grid = widgets.NewGridViewFromState(saved.gridState) state := saved.gridState
if a.searchText != "" { 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.createSearchAndFilter()
a.createContainer(saved.searchText != "") a.createContainer()
return a return a
} }
func (a *AlbumsPage) OnSearched(query string) { func (a *AlbumsPage) OnSearched(query string) {
a.searchText = query
if query == "" { if query == "" {
a.container.Objects[0] = a.grid a.sortOrder.Enable()
if a.searchGrid != nil { a.grid.ResetFromState(a.gridState)
a.searchGrid.Clear() a.searchGridState = nil
} } else {
a.Refresh() a.sortOrder.Disable()
return a.doSearch(query)
} }
a.doSearch(query) a.searchText = query
} }
func (a *AlbumsPage) Route() controller.Route { func (a *AlbumsPage) Route() controller.Route {
@@ -166,48 +180,45 @@ func (a *AlbumsPage) Reload() {
} else { } else {
iter := a.mp.IterateAlbums(a.sortOrder.Selected, a.filter) iter := a.mp.IterateAlbums(a.sortOrder.Selected, a.filter)
a.grid.Reset(widgets.NewGridViewAlbumIterator(iter)) a.grid.Reset(widgets.NewGridViewAlbumIterator(iter))
a.grid.Refresh()
} }
} }
func (a *AlbumsPage) Save() SavedPage { func (a *AlbumsPage) Save() SavedPage {
sa := &savedAlbumsPage{ sa := &savedAlbumsPage{
cfg: a.cfg, cfg: a.cfg,
contr: a.contr, pool: a.pool,
pm: a.pm, contr: a.contr,
mp: a.mp, pm: a.pm,
im: a.im, mp: a.mp,
searchText: a.searchText, im: a.im,
filter: a.filter, searchText: a.searchText,
sortOrder: a.sortOrder.Selected, filter: a.filter,
gridState: a.grid.SaveToState(), sortOrder: a.sortOrder.Selected,
gridState: a.gridState,
searchGridState: a.searchGridState,
} }
if a.searchGrid != nil { if a.searchText == "" {
sa.searchGridState = a.searchGrid.SaveToState() sa.gridState = a.grid.SaveToState()
} else {
sa.searchGridState = a.grid.SaveToState()
} }
a.grid.Clear()
a.pool.Release(util.WidgetTypeGridView, a.grid)
return sa return sa
} }
func (a *AlbumsPage) doSearch(query string) { func (a *AlbumsPage) doSearch(query string) {
iter := widgets.NewGridViewAlbumIterator(a.mp.SearchAlbums(query, a.filter)) if a.searchText == "" {
if a.searchGrid == nil { a.gridState = a.grid.SaveToState()
a.searchGrid = widgets.NewGridView(iter, a.im, myTheme.AlbumIcon)
a.contr.ConnectAlbumGridActions(a.searchGrid)
} else {
a.searchGrid.Reset(iter)
} }
a.container.Objects[0] = a.searchGrid iter := widgets.NewGridViewAlbumIterator(a.mp.SearchAlbums(query, a.filter))
a.Refresh() a.grid.Reset(iter)
} }
func (a *AlbumsPage) onSortOrderChanged(order string) { func (a *AlbumsPage) onSortOrderChanged(order string) {
a.cfg.SortOrder = a.sortOrder.Selected a.cfg.SortOrder = a.sortOrder.Selected
iter := a.mp.IterateAlbums(order, a.filter) iter := a.mp.IterateAlbums(order, a.filter)
a.grid.Reset(widgets.NewGridViewAlbumIterator(iter)) a.grid.Reset(widgets.NewGridViewAlbumIterator(iter))
if a.searchText == "" {
a.container.Objects[0] = a.grid
a.Refresh()
}
} }
func (a *AlbumsPage) CreateRenderer() fyne.WidgetRenderer { func (a *AlbumsPage) CreateRenderer() fyne.WidgetRenderer {
@@ -219,13 +230,14 @@ type savedAlbumsPage struct {
searchText string searchText string
filter mediaprovider.AlbumFilter filter mediaprovider.AlbumFilter
cfg *backend.AlbumsPageConfig cfg *backend.AlbumsPageConfig
pool *util.WidgetPool
contr *controller.Controller contr *controller.Controller
pm *backend.PlaybackManager pm *backend.PlaybackManager
mp mediaprovider.MediaProvider mp mediaprovider.MediaProvider
im *backend.ImageManager im *backend.ImageManager
sortOrder string sortOrder string
gridState widgets.GridViewState gridState *widgets.GridViewState
searchGridState widgets.GridViewState searchGridState *widgets.GridViewState
} }
func (s *savedAlbumsPage) Restore() Page { func (s *savedAlbumsPage) Restore() Page {
+73 -14
View File
@@ -28,6 +28,7 @@ type artistPageState struct {
activeView int activeView int
trackSort widgets.TracklistSort trackSort widgets.TracklistSort
pool *util.WidgetPool
cfg *backend.ArtistPageConfig cfg *backend.ArtistPageConfig
pm *backend.PlaybackManager pm *backend.PlaybackManager
mp mediaprovider.MediaProvider mp mediaprovider.MediaProvider
@@ -39,6 +40,7 @@ type ArtistPage struct {
widget.BaseWidget widget.BaseWidget
artistPageState artistPageState
disposed bool
artistInfo *mediaprovider.ArtistWithAlbums artistInfo *mediaprovider.ArtistWithAlbums
@@ -49,18 +51,19 @@ type ArtistPage struct {
container *fyne.Container 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 activeView := 0
if cfg.InitialView == "Top Tracks" { if cfg.InitialView == "Top Tracks" {
activeView = 1 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{ a := &ArtistPage{artistPageState: artistPageState{
artistID: artistID, artistID: artistID,
cfg: cfg, cfg: cfg,
pool: pool,
pm: pm, pm: pm,
mp: mp, mp: mp,
im: im, im: im,
@@ -69,7 +72,14 @@ func newArtistPage(artistID string, cfg *backend.ArtistPageConfig, pm *backend.P
trackSort: sort, trackSort: sort,
}} }}
a.ExtendBaseWidget(a) 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 { if img, ok := im.GetCachedArtistImage(artistID); ok {
a.header.artistImage.SetImage(img, true /*tappable*/) a.header.artistImage.SetImage(img, true /*tappable*/)
} }
@@ -112,9 +122,18 @@ func (a *ArtistPage) Reload() {
} }
func (a *ArtistPage) Save() SavedPage { func (a *ArtistPage) Save() SavedPage {
a.disposed = true
s := a.artistPageState s := a.artistPageState
if a.tracklistCtr != nil { 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 return &s
} }
@@ -141,6 +160,9 @@ func (a *ArtistPage) load() {
log.Printf("Failed to get artist: %s", err.Error()) log.Printf("Failed to get artist: %s", err.Error())
return return
} }
if a.disposed {
return
}
a.artistInfo = artist a.artistInfo = artist
a.header.Update(artist) a.header.Update(artist)
if a.activeView == 0 { if a.activeView == 0 {
@@ -170,7 +192,13 @@ func (a *ArtistPage) showAlbumGrid() {
Secondary: strconv.Itoa(al.Year), 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.contr.ConnectAlbumGridActions(a.albumGrid)
} }
a.container.Objects[0].(*fyne.Container).Objects[0] = 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()) log.Printf("error getting top songs: %s", err.Error())
return return
} }
tl := widgets.NewTracklist(ts) if a.disposed {
tl.AutoNumber = true 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.SetVisibleColumns(a.cfg.TracklistColumns)
tl.SetSorting(a.trackSort) tl.SetSorting(a.trackSort)
tl.OnVisibleColumnsChanged = func(cols []string) { tl.OnVisibleColumnsChanged = func(cols []string) {
@@ -228,9 +266,11 @@ func (a *ArtistPage) CreateRenderer() fyne.WidgetRenderer {
} }
func (s *artistPageState) Restore() Page { 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 { type ArtistPageHeader struct {
widget.BaseWidget widget.BaseWidget
@@ -250,7 +290,7 @@ func NewArtistPageHeader(page *ArtistPage) *ArtistPageHeader {
a := &ArtistPageHeader{ a := &ArtistPageHeader{
artistPage: page, artistPage: page,
titleDisp: widget.NewRichTextWithText(""), titleDisp: widget.NewRichTextWithText(""),
biographyDisp: widget.NewRichTextWithText("Artist biography not available."), biographyDisp: widget.NewRichTextWithText(artistBioNotAvailableStr),
similarArtists: container.NewHBox(), similarArtists: container.NewHBox(),
} }
a.titleDisp.Segments[0].(*widget.TextSegment).Style = widget.RichTextStyle{ a.titleDisp.Segments[0].(*widget.TextSegment).Style = widget.RichTextStyle{
@@ -273,6 +313,17 @@ func NewArtistPageHeader(page *ArtistPage) *ArtistPageHeader {
return a 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) { func (a *ArtistPageHeader) Update(artist *mediaprovider.ArtistWithAlbums) {
if artist == nil { if artist == nil {
return 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 { for i, art := range info.SimilarArtists {
if i == 0 { if i == 0 {
a.similarArtists.Add(widget.NewLabel("Similar Artists:")) a.similarArtists.Objects[0].Show() // "Similar Artists:" label
} }
if i == 4 { if i == 4 {
break 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.NoTruncate = true
h.SetText(art.Name) h.SetText(art.Name)
h.OnTapped = func(id string) func() { h.OnTapped = func(id string) func() {
return func() { a.artistPage.contr.NavigateTo(controller.ArtistRoute(id)) } return func() { a.artistPage.contr.NavigateTo(controller.ArtistRoute(id)) }
}(art.ID) }(art.ID)
a.similarArtists.Add(h) h.Show()
} }
a.similarArtists.Refresh() a.similarArtists.Refresh()
+17 -3
View File
@@ -23,6 +23,7 @@ type ArtistsPage struct {
widget.BaseWidget widget.BaseWidget
contr *controller.Controller contr *controller.Controller
pool *util.WidgetPool
im *backend.ImageManager im *backend.ImageManager
pm *backend.PlaybackManager pm *backend.PlaybackManager
mp mediaprovider.MediaProvider mp mediaprovider.MediaProvider
@@ -40,15 +41,17 @@ type ArtistsPage struct {
func NewArtistsPage( func NewArtistsPage(
contr *controller.Controller, contr *controller.Controller,
pool *util.WidgetPool,
pm *backend.PlaybackManager, pm *backend.PlaybackManager,
mp mediaprovider.MediaProvider, mp mediaprovider.MediaProvider,
im *backend.ImageManager, im *backend.ImageManager,
) *ArtistsPage { ) *ArtistsPage {
return newArtistsPage(contr, pm, mp, im, "", 0, 0) return newArtistsPage(contr, pool, pm, mp, im, "", 0, 0)
} }
func newArtistsPage( func newArtistsPage(
contr *controller.Controller, contr *controller.Controller,
pool *util.WidgetPool,
pm *backend.PlaybackManager, pm *backend.PlaybackManager,
mp mediaprovider.MediaProvider, mp mediaprovider.MediaProvider,
im *backend.ImageManager, im *backend.ImageManager,
@@ -58,6 +61,7 @@ func newArtistsPage(
) *ArtistsPage { ) *ArtistsPage {
a := &ArtistsPage{ a := &ArtistsPage{
contr: contr, contr: contr,
pool: pool,
pm: pm, pm: pm,
mp: mp, mp: mp,
im: im, im: im,
@@ -74,7 +78,13 @@ func newArtistsPage(
a.searcher = widgets.NewSearchEntry() a.searcher = widgets.NewSearchEntry()
a.searcher.OnSearched = func(query string) { a.onSearched(query, false /*firstLoad*/) } a.searcher.OnSearched = func(query string) { a.onSearched(query, false /*firstLoad*/) }
a.searcher.Entry.Text = searchText 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) a.contr.ConnectArtistGridActions(a.grid)
searchVbox := container.NewVBox(layout.NewSpacer(), a.searcher, layout.NewSpacer()) searchVbox := container.NewVBox(layout.NewSpacer(), a.searcher, layout.NewSpacer())
@@ -158,6 +168,7 @@ func (a *ArtistsPage) Route() controller.Route {
func (a *ArtistsPage) Save() SavedPage { func (a *ArtistsPage) Save() SavedPage {
s := &savedArtistsPage{ s := &savedArtistsPage{
contr: a.contr, contr: a.contr,
pool: a.pool,
im: a.im, im: a.im,
pm: a.pm, pm: a.pm,
mp: a.mp, mp: a.mp,
@@ -169,11 +180,14 @@ func (a *ArtistsPage) Save() SavedPage {
} else { } else {
s.searchGridScrollPos = a.grid.GetScrollOffset() s.searchGridScrollPos = a.grid.GetScrollOffset()
} }
a.grid.Clear()
a.pool.Release(util.WidgetTypeGridView, a.grid)
return s return s
} }
type savedArtistsPage struct { type savedArtistsPage struct {
contr *controller.Controller contr *controller.Controller
pool *util.WidgetPool
im *backend.ImageManager im *backend.ImageManager
pm *backend.PlaybackManager pm *backend.PlaybackManager
mp mediaprovider.MediaProvider mp mediaprovider.MediaProvider
@@ -183,5 +197,5 @@ type savedArtistsPage struct {
} }
func (s *savedArtistsPage) Restore() Page { 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)
} }
+6 -2
View File
@@ -233,17 +233,21 @@ func (b *BrowsingPane) GoHome() {
func (b *BrowsingPane) GoBack() { func (b *BrowsingPane) GoBack() {
if b.historyIdx > 0 { 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.addPageToHistory(b.curPage, false)
b.historyIdx -= 2 b.historyIdx -= 2
b.doSetPage(b.history[b.historyIdx].Restore()) b.doSetPage(p)
b.updateHistoryButtons() b.updateHistoryButtons()
} }
} }
func (b *BrowsingPane) GoForward() { func (b *BrowsingPane) GoForward() {
if b.historyIdx < len(b.history)-1 { if b.historyIdx < len(b.history)-1 {
p := b.history[b.historyIdx+1].Restore()
b.addPageToHistory(b.curPage, false) b.addPageToHistory(b.curPage, false)
b.doSetPage(b.history[b.historyIdx].Restore()) b.doSetPage(p)
b.updateHistoryButtons() b.updateHistoryButtons()
} }
} }
+98 -55
View File
@@ -23,32 +23,36 @@ type FavoritesPage struct {
widget.BaseWidget widget.BaseWidget
cfg *backend.FavoritesPageConfig cfg *backend.FavoritesPageConfig
pool *util.WidgetPool
contr *controller.Controller contr *controller.Controller
pm *backend.PlaybackManager pm *backend.PlaybackManager
im *backend.ImageManager im *backend.ImageManager
mp mediaprovider.MediaProvider mp mediaprovider.MediaProvider
disposed bool
trackSort widgets.TracklistSort trackSort widgets.TracklistSort
filter mediaprovider.AlbumFilter filter mediaprovider.AlbumFilter
searchText string searchText string
nowPlayingID string nowPlayingID string
pendingViewSwitch bool pendingViewSwitch bool
grid *widgets.GridView grid *widgets.GridView
searchGrid *widgets.GridView gridState *widgets.GridViewState
artistGrid *widgets.GridView searchGridState *widgets.GridViewState
tracklistCtr *fyne.Container artistGrid *widgets.GridView
searcher *widgets.SearchEntry tracklistCtr *fyne.Container
filterBtn *widgets.AlbumFilterButton searcher *widgets.SearchEntry
titleDisp *widget.RichText filterBtn *widgets.AlbumFilterButton
toggleBtns *widgets.ToggleButtonGroup titleDisp *widget.RichText
container *fyne.Container 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{ a := &FavoritesPage{
filter: mediaprovider.AlbumFilter{ExcludeUnfavorited: true}, filter: mediaprovider.AlbumFilter{ExcludeUnfavorited: true},
cfg: cfg, cfg: cfg,
pool: pool,
contr: contr, contr: contr,
pm: pm, pm: pm,
mp: mp, mp: mp,
@@ -56,8 +60,14 @@ func NewFavoritesPage(cfg *backend.FavoritesPageConfig, contr *controller.Contro
} }
a.ExtendBaseWidget(a) a.ExtendBaseWidget(a)
a.createHeader(0) a.createHeader(0)
iter := mp.IterateAlbums("", a.filter) iter := widgets.NewGridViewAlbumIterator(mp.IterateAlbums("", a.filter))
a.grid = widgets.NewGridView(widgets.NewGridViewAlbumIterator(iter), a.im, myTheme.AlbumIcon) 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) a.contr.ConnectAlbumGridActions(a.grid)
if cfg.InitialView == "Artists" { if cfg.InitialView == "Artists" {
a.toggleBtns.SetActivatedButton(1) a.toggleBtns.SetActivatedButton(1)
@@ -97,21 +107,30 @@ func (a *FavoritesPage) createContainer(initialView fyne.CanvasObject) {
func restoreFavoritesPage(saved *savedFavoritesPage) *FavoritesPage { func restoreFavoritesPage(saved *savedFavoritesPage) *FavoritesPage {
a := &FavoritesPage{ a := &FavoritesPage{
cfg: saved.cfg, cfg: saved.cfg,
contr: saved.contr, contr: saved.contr,
pm: saved.pm, pool: saved.pool,
mp: saved.mp, pm: saved.pm,
im: saved.im, mp: saved.mp,
searchText: saved.searchText, im: saved.im,
filter: saved.filter, gridState: saved.gridState,
trackSort: saved.trackSort, searchGridState: saved.searchGridState,
searchText: saved.searchText,
filter: saved.filter,
trackSort: saved.trackSort,
} }
a.ExtendBaseWidget(a) a.ExtendBaseWidget(a)
a.createHeader(saved.activeToggleBtn) a.createHeader(saved.activeToggleBtn)
a.grid = widgets.NewGridViewFromState(saved.gridState) state := saved.gridState
if saved.searchText != "" { 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) a.toggleBtns.SetActivatedButton(saved.activeToggleBtn)
@@ -119,8 +138,6 @@ func restoreFavoritesPage(saved *savedFavoritesPage) *FavoritesPage {
a.onShowFavoriteArtists() a.onShowFavoriteArtists()
} else if saved.activeToggleBtn == 2 { } else if saved.activeToggleBtn == 2 {
a.onShowFavoriteSongs() a.onShowFavoriteSongs()
} else if saved.searchText != "" {
a.createContainer(a.searchGrid)
} else { } else {
a.createContainer(a.grid) a.createContainer(a.grid)
} }
@@ -154,6 +171,9 @@ func (a *FavoritesPage) Reload() {
log.Printf("error getting starred items: %s", err.Error()) log.Printf("error getting starred items: %s", err.Error())
return return
} }
if a.disposed {
return
}
if a.tracklistCtr != nil { if a.tracklistCtr != nil {
// refresh favorite songs view // refresh favorite songs view
tr := a.tracklistCtr.Objects[0].(*widgets.Tracklist) tr := a.tracklistCtr.Objects[0].(*widgets.Tracklist)
@@ -176,22 +196,36 @@ func (a *FavoritesPage) Reload() {
} }
func (a *FavoritesPage) Save() SavedPage { func (a *FavoritesPage) Save() SavedPage {
a.disposed = true
sf := &savedFavoritesPage{ sf := &savedFavoritesPage{
cfg: a.cfg, cfg: a.cfg,
contr: a.contr, contr: a.contr,
pool: a.pool,
pm: a.pm, pm: a.pm,
mp: a.mp, mp: a.mp,
im: a.im, im: a.im,
filter: a.filter, filter: a.filter,
searchText: a.searchText, searchText: a.searchText,
gridState: a.grid.SaveToState(), gridState: a.gridState,
searchGridState: a.searchGridState,
activeToggleBtn: a.toggleBtns.ActivatedButtonIndex(), activeToggleBtn: a.toggleBtns.ActivatedButtonIndex(),
} }
if a.searchGrid != nil { if a.searchText != "" {
sf.searchGridState = a.searchGrid.SaveToState() 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 { 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 return sf
} }
@@ -203,16 +237,13 @@ func (a *FavoritesPage) SearchWidget() fyne.Focusable {
} }
func (a *FavoritesPage) OnSearched(query string) { func (a *FavoritesPage) OnSearched(query string) {
a.searchText = query
if query == "" { if query == "" {
a.container.Objects[0] = a.grid a.grid.ResetFromState(a.gridState)
if a.searchGrid != nil { a.searchGridState = nil
a.searchGrid.Clear() } else {
} a.doSearchAlbums(query)
a.Refresh()
return
} }
a.doSearchAlbums(query) a.searchText = query
} }
var _ CanShowNowPlaying = (*FavoritesPage)(nil) var _ CanShowNowPlaying = (*FavoritesPage)(nil)
@@ -236,26 +267,18 @@ func (a *FavoritesPage) SelectAll() {
} }
func (a *FavoritesPage) doSearchAlbums(query string) { func (a *FavoritesPage) doSearchAlbums(query string) {
iter := a.mp.SearchAlbums(query, a.filter) if a.searchText == "" {
if a.searchGrid == nil { a.gridState = a.grid.SaveToState()
a.searchGrid = widgets.NewGridView(widgets.NewGridViewAlbumIterator(iter), a.im, myTheme.AlbumIcon)
a.contr.ConnectAlbumGridActions(a.searchGrid)
} else {
a.searchGrid.Reset(widgets.NewGridViewAlbumIterator(iter))
} }
a.container.Objects[0] = a.searchGrid iter := widgets.NewGridViewAlbumIterator(a.mp.SearchAlbums(query, a.filter))
a.Refresh() a.grid.Reset(iter)
} }
func (a *FavoritesPage) onShowFavoriteAlbums() { func (a *FavoritesPage) onShowFavoriteAlbums() {
a.cfg.InitialView = "Albums" // save setting a.cfg.InitialView = "Albums" // save setting
a.searcher.Entry.Show() a.searcher.Entry.Show()
a.filterBtn.Show() a.filterBtn.Show()
if a.searchText == "" { a.container.Objects[0] = a.grid
a.container.Objects[0] = a.grid
} else {
a.container.Objects[0] = a.searchGrid
}
a.Refresh() a.Refresh()
} }
@@ -277,8 +300,17 @@ func (a *FavoritesPage) onShowFavoriteArtists() {
log.Printf("error getting starred items: %s", err.Error()) log.Printf("error getting starred items: %s", err.Error())
return return
} }
if a.disposed {
return
}
model := buildArtistGridViewModel(fav.Artists) 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.contr.ConnectArtistGridActions(a.artistGrid)
a.container.Objects[0] = a.artistGrid a.container.Objects[0] = a.artistGrid
a.Refresh() a.Refresh()
@@ -325,8 +357,18 @@ func (a *FavoritesPage) onShowFavoriteSongs() {
log.Printf("error getting starred items: %s", err.Error()) log.Printf("error getting starred items: %s", err.Error())
return return
} }
tracklist := widgets.NewTracklist(fav.Tracks) if a.disposed {
tracklist.AutoNumber = true 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.SetVisibleColumns(a.cfg.TracklistColumns)
tracklist.SetSorting(a.trackSort) tracklist.SetSorting(a.trackSort)
tracklist.OnVisibleColumnsChanged = func(cols []string) { tracklist.OnVisibleColumnsChanged = func(cols []string) {
@@ -355,11 +397,12 @@ func (a *FavoritesPage) CreateRenderer() fyne.WidgetRenderer {
type savedFavoritesPage struct { type savedFavoritesPage struct {
cfg *backend.FavoritesPageConfig cfg *backend.FavoritesPageConfig
contr *controller.Controller contr *controller.Controller
pool *util.WidgetPool
pm *backend.PlaybackManager pm *backend.PlaybackManager
mp mediaprovider.MediaProvider mp mediaprovider.MediaProvider
im *backend.ImageManager im *backend.ImageManager
gridState widgets.GridViewState gridState *widgets.GridViewState
searchGridState widgets.GridViewState searchGridState *widgets.GridViewState
filter mediaprovider.AlbumFilter filter mediaprovider.AlbumFilter
searchText string searchText string
activeToggleBtn int activeToggleBtn int
+76 -63
View File
@@ -19,26 +19,29 @@ import (
type GenrePage struct { type GenrePage struct {
widget.BaseWidget widget.BaseWidget
genre string genre string
contr *controller.Controller pool *util.WidgetPool
im *backend.ImageManager contr *controller.Controller
pm *backend.PlaybackManager im *backend.ImageManager
mp mediaprovider.MediaProvider pm *backend.PlaybackManager
grid *widgets.GridView mp mediaprovider.MediaProvider
searchGrid *widgets.GridView grid *widgets.GridView
searcher *widgets.SearchEntry gridState *widgets.GridViewState
searchText string searchGridState *widgets.GridViewState
filter mediaprovider.AlbumFilter searcher *widgets.SearchEntry
filterBtn *widgets.AlbumFilterButton searchText string
titleDisp *widget.RichText filter mediaprovider.AlbumFilter
playRandom *widget.Button filterBtn *widgets.AlbumFilterButton
titleDisp *widget.RichText
playRandom *widget.Button
container *fyne.Container 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{ g := &GenrePage{
genre: genre, genre: genre,
pool: pool,
filter: mediaprovider.AlbumFilter{Genres: []string{genre}}, filter: mediaprovider.AlbumFilter{Genres: []string{genre}},
contr: contr, contr: contr,
pm: pm, pm: pm,
@@ -52,11 +55,17 @@ func NewGenrePage(genre string, contr *controller.Controller, pm *backend.Playba
SizeName: theme.SizeNameHeadingText, SizeName: theme.SizeNameHeadingText,
} }
g.playRandom = widget.NewButtonWithIcon(" Play random", myTheme.ShuffleIcon, g.playRandomSongs) g.playRandom = widget.NewButtonWithIcon(" Play random", myTheme.ShuffleIcon, g.playRandomSongs)
iter := g.mp.IterateAlbums("", g.filter) iter := widgets.NewGridViewAlbumIterator(g.mp.IterateAlbums("", g.filter))
g.grid = widgets.NewGridView(widgets.NewGridViewAlbumIterator(iter), g.im, myTheme.AlbumIcon) 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.contr.ConnectAlbumGridActions(g.grid)
g.createSearchAndFilter() g.createSearchAndFilter()
g.createContainer(false) g.createContainer()
return g return g
} }
@@ -71,29 +80,28 @@ func (g *GenrePage) createSearchAndFilter() {
g.filterBtn.OnChanged = g.Reload g.filterBtn.OnChanged = g.Reload
} }
func (g *GenrePage) createContainer(searchGrid bool) { func (g *GenrePage) createContainer() {
searchVbox := container.NewVBox(layout.NewSpacer(), g.searcher, layout.NewSpacer()) 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()) playRandomVbox := container.NewVBox(layout.NewSpacer(), g.playRandom, layout.NewSpacer())
g.container = container.NewBorder( g.container = container.NewBorder(
container.NewHBox(util.NewHSpace(6), container.NewHBox(util.NewHSpace(6),
g.titleDisp, playRandomVbox, layout.NewSpacer(), container.NewCenter(g.filterBtn), searchVbox, util.NewHSpace(15)), 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 { func restoreGenrePage(saved *savedGenrePage) *GenrePage {
g := &GenrePage{ g := &GenrePage{
genre: saved.genre, genre: saved.genre,
contr: saved.contr, pool: saved.pool,
pm: saved.pm, contr: saved.contr,
mp: saved.mp, pm: saved.pm,
im: saved.im, mp: saved.mp,
searchText: saved.searchText, im: saved.im,
filter: saved.filter, gridState: saved.gridState,
searchGridState: saved.searchGridState,
searchText: saved.searchText,
filter: saved.filter,
} }
g.ExtendBaseWidget(g) g.ExtendBaseWidget(g)
@@ -102,12 +110,19 @@ func restoreGenrePage(saved *savedGenrePage) *GenrePage {
SizeName: theme.SizeNameHeadingText, SizeName: theme.SizeNameHeadingText,
} }
g.playRandom = widget.NewButtonWithIcon(" Play random", myTheme.ShuffleIcon, g.playRandomSongs) g.playRandom = widget.NewButtonWithIcon(" Play random", myTheme.ShuffleIcon, g.playRandomSongs)
g.grid = widgets.NewGridViewFromState(saved.gridState) state := saved.gridState
g.createSearchAndFilter()
if g.searchText != "" { 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 return g
} }
@@ -126,24 +141,29 @@ func (g *GenrePage) Reload() {
} else { } else {
iter := g.mp.IterateAlbums("", g.filter) iter := g.mp.IterateAlbums("", g.filter)
g.grid.Reset(widgets.NewGridViewAlbumIterator(iter)) g.grid.Reset(widgets.NewGridViewAlbumIterator(iter))
g.grid.Refresh()
} }
} }
func (g *GenrePage) Save() SavedPage { func (g *GenrePage) Save() SavedPage {
sg := &savedGenrePage{ sg := &savedGenrePage{
genre: g.genre, genre: g.genre,
filter: g.filter, pool: g.pool,
searchText: g.searchText, filter: g.filter,
contr: g.contr, searchText: g.searchText,
pm: g.pm, contr: g.contr,
mp: g.mp, pm: g.pm,
im: g.im, mp: g.mp,
gridState: g.grid.SaveToState(), im: g.im,
gridState: g.gridState,
searchGridState: g.searchGridState,
} }
if g.searchGrid != nil { if g.searchText != "" {
sg.searchGridState = g.searchGrid.SaveToState() sg.searchGridState = g.grid.SaveToState()
} else {
sg.gridState = g.grid.SaveToState()
} }
g.grid.Clear()
g.pool.Release(util.WidgetTypeGridView, g.grid)
return sg return sg
} }
@@ -154,28 +174,20 @@ func (g *GenrePage) SearchWidget() fyne.Focusable {
} }
func (g *GenrePage) OnSearched(query string) { func (g *GenrePage) OnSearched(query string) {
g.searchText = query
if query == "" { if query == "" {
g.container.Objects[0] = g.grid g.grid.ResetFromState(g.gridState)
if g.searchGrid != nil { } else {
g.searchGrid.Clear() g.doSearch(query)
}
g.Refresh()
return
} }
g.doSearch(query) g.searchText = query
} }
func (g *GenrePage) doSearch(query string) { func (g *GenrePage) doSearch(query string) {
iter := g.mp.SearchAlbums(query, g.filter) if g.searchText == "" {
if g.searchGrid == nil { g.gridState = g.grid.SaveToState()
g.searchGrid = widgets.NewGridView(widgets.NewGridViewAlbumIterator(iter), g.im, myTheme.AlbumIcon)
g.contr.ConnectAlbumGridActions(g.searchGrid)
} else {
g.searchGrid.Reset(widgets.NewGridViewAlbumIterator(iter))
} }
g.container.Objects[0] = g.searchGrid iter := g.mp.SearchAlbums(query, g.filter)
g.Refresh() g.grid.Reset(widgets.NewGridViewAlbumIterator(iter))
} }
func (g *GenrePage) playRandomSongs() { func (g *GenrePage) playRandomSongs() {
@@ -185,13 +197,14 @@ func (g *GenrePage) playRandomSongs() {
type savedGenrePage struct { type savedGenrePage struct {
genre string genre string
searchText string searchText string
pool *util.WidgetPool
filter mediaprovider.AlbumFilter filter mediaprovider.AlbumFilter
contr *controller.Controller contr *controller.Controller
pm *backend.PlaybackManager pm *backend.PlaybackManager
mp mediaprovider.MediaProvider mp mediaprovider.MediaProvider
im *backend.ImageManager im *backend.ImageManager
gridState widgets.GridViewState gridState *widgets.GridViewState
searchGridState widgets.GridViewState searchGridState *widgets.GridViewState
} }
func (s *savedGenrePage) Restore() Page { func (s *savedGenrePage) Restore() Page {
+21 -8
View File
@@ -38,6 +38,7 @@ type NowPlayingPage struct {
type nowPlayingPageState struct { type nowPlayingPageState struct {
contr *controller.Controller contr *controller.Controller
pool *util.WidgetPool
conf *backend.NowPlayingPageConfig conf *backend.NowPlayingPageConfig
pm *backend.PlaybackManager pm *backend.PlaybackManager
p *player.Player p *player.Player
@@ -46,30 +47,40 @@ type nowPlayingPageState struct {
func NewNowPlayingPage( func NewNowPlayingPage(
highlightedTrackID string, highlightedTrackID string,
contr *controller.Controller, contr *controller.Controller,
pool *util.WidgetPool,
conf *backend.NowPlayingPageConfig, conf *backend.NowPlayingPageConfig,
pm *backend.PlaybackManager, pm *backend.PlaybackManager,
p *player.Player, // TODO: once other player backends are supported (eg uPnP), refactor p *player.Player, // TODO: once other player backends are supported (eg uPnP), refactor
) *NowPlayingPage { ) *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) a.ExtendBaseWidget(a)
p.OnPaused(a.formatStatusLine) p.OnPaused(a.formatStatusLine)
p.OnPlaying(a.formatStatusLine) p.OnPlaying(a.formatStatusLine)
p.OnStopped(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.SetVisibleColumns(conf.TracklistColumns)
a.tracklist.OnVisibleColumnsChanged = func(cols []string) { a.tracklist.OnVisibleColumnsChanged = func(cols []string) {
a.conf.TracklistColumns = cols a.conf.TracklistColumns = cols
} }
a.tracklist.AutoNumber = true a.tracklist.Options = widgets.TracklistOptions{
a.tracklist.DisablePlaybackMenu = true AutoNumber: true,
DisablePlaybackMenu: true,
AuxiliaryMenuItems: []*fyne.MenuItem{
fyne.NewMenuItem("Remove from queue", a.onRemoveSelectedFromQueue),
},
}
contr.ConnectTracklistActions(a.tracklist) contr.ConnectTracklistActions(a.tracklist)
// override the default OnPlayTrackAt handler b/c we don't need to re-load the tracks into the queue // 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.OnPlayTrackAt = a.onPlayTrackAt
a.tracklist.AuxiliaryMenuItems = []*fyne.MenuItem{
fyne.NewMenuItem("Remove from queue", a.onRemoveSelectedFromQueue),
}
a.title = widget.NewRichTextWithText("Now Playing") a.title = widget.NewRichTextWithText("Now Playing")
a.title.Segments[0].(*widget.TextSegment).Style.SizeName = widget.RichTextStyleHeading.SizeName a.title.Segments[0].(*widget.TextSegment).Style.SizeName = widget.RichTextStyleHeading.SizeName
a.statusLabel = widget.NewRichTextWithText("Stopped") a.statusLabel = widget.NewRichTextWithText("Stopped")
@@ -88,6 +99,8 @@ func (a *NowPlayingPage) CreateRenderer() fyne.WidgetRenderer {
} }
func (a *NowPlayingPage) Save() SavedPage { func (a *NowPlayingPage) Save() SavedPage {
a.tracklist.Clear()
a.pool.Release(util.WidgetTypeTracklist, a.tracklist)
nps := a.nowPlayingPageState nps := a.nowPlayingPageState
return &nps return &nps
} }
@@ -205,5 +218,5 @@ func (a *NowPlayingPage) load(highlightedTrackID string) {
} }
func (s *nowPlayingPageState) Restore() Page { 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)
} }
+41 -8
View File
@@ -24,6 +24,7 @@ type PlaylistPage struct {
playlistPageState playlistPageState
disposed bool
header *PlaylistPageHeader header *PlaylistPageHeader
tracklist *widgets.Tracklist tracklist *widgets.Tracklist
tracks []*mediaprovider.Track tracks []*mediaprovider.Track
@@ -35,6 +36,7 @@ type playlistPageState struct {
playlistID string playlistID string
conf *backend.PlaylistPageConfig conf *backend.PlaylistPageConfig
contr *controller.Controller contr *controller.Controller
widgetPool *util.WidgetPool
sm *backend.ServerManager sm *backend.ServerManager
pm *backend.PlaybackManager pm *backend.PlaybackManager
im *backend.ImageManager im *backend.ImageManager
@@ -44,27 +46,41 @@ type playlistPageState struct {
func NewPlaylistPage( func NewPlaylistPage(
playlistID string, playlistID string,
conf *backend.PlaylistPageConfig, conf *backend.PlaylistPageConfig,
pool *util.WidgetPool,
contr *controller.Controller, contr *controller.Controller,
sm *backend.ServerManager, sm *backend.ServerManager,
pm *backend.PlaybackManager, pm *backend.PlaybackManager,
im *backend.ImageManager, im *backend.ImageManager,
) *PlaylistPage { ) *PlaylistPage {
return newPlaylistPage(playlistID, conf, contr, sm, pm, im, widgets.TracklistSort{}) return newPlaylistPage(playlistID, conf, contr, pool, sm, pm, im, widgets.TracklistSort{})
} }
func newPlaylistPage( func newPlaylistPage(
playlistID string, playlistID string,
conf *backend.PlaylistPageConfig, conf *backend.PlaylistPageConfig,
contr *controller.Controller, contr *controller.Controller,
pool *util.WidgetPool,
sm *backend.ServerManager, sm *backend.ServerManager,
pm *backend.PlaybackManager, pm *backend.PlaybackManager,
im *backend.ImageManager, im *backend.ImageManager,
trackSort widgets.TracklistSort, trackSort widgets.TracklistSort,
) *PlaylistPage { ) *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.ExtendBaseWidget(a)
a.header = NewPlaylistPageHeader(a) if h := a.widgetPool.Obtain(util.WidgetTypePlaylistPageHeader); h != nil {
a.tracklist = widgets.NewTracklist(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.SetVisibleColumns(conf.TracklistColumns)
a.tracklist.SetSorting(trackSort) a.tracklist.SetSorting(trackSort)
a.tracklist.OnVisibleColumnsChanged = func(cols []string) { a.tracklist.OnVisibleColumnsChanged = func(cols []string) {
@@ -77,8 +93,10 @@ func newPlaylistPage(
fyne.NewMenuItem("Move down", a.onMoveSelectedDown), fyne.NewMenuItem("Move down", a.onMoveSelectedDown),
fyne.NewMenuItem("Move to bottom", a.onMoveSelectedToBottom), fyne.NewMenuItem("Move to bottom", a.onMoveSelectedToBottom),
}...) }...)
a.tracklist.AuxiliaryMenuItems = []*fyne.MenuItem{reorderMenu, a.tracklist.Options = widgets.TracklistOptions{
fyne.NewMenuItem("Remove from playlist", a.onRemoveSelectedFromPlaylist)} AuxiliaryMenuItems: []*fyne.MenuItem{reorderMenu,
fyne.NewMenuItem("Remove from playlist", a.onRemoveSelectedFromPlaylist)},
}
// connect tracklist actions // connect tracklist actions
a.contr.ConnectTracklistActions(a.tracklist) a.contr.ConnectTracklistActions(a.tracklist)
@@ -94,8 +112,12 @@ func (a *PlaylistPage) CreateRenderer() fyne.WidgetRenderer {
} }
func (a *PlaylistPage) Save() SavedPage { func (a *PlaylistPage) Save() SavedPage {
a.disposed = true
p := a.playlistPageState p := a.playlistPageState
p.trackSort = a.tracklist.Sorting() p.trackSort = a.tracklist.Sorting()
p.widgetPool.Release(util.WidgetTypePlaylistPageHeader, a.header)
a.tracklist.Clear()
p.widgetPool.Release(util.WidgetTypeTracklist, a.tracklist)
return &p return &p
} }
@@ -132,6 +154,9 @@ func (a *PlaylistPage) load() {
log.Printf("Failed to get playlist: %s", err.Error()) log.Printf("Failed to get playlist: %s", err.Error())
return return
} }
if a.disposed {
return
}
renumberTracks(playlist.Tracks) renumberTracks(playlist.Tracks)
a.tracks = playlist.Tracks a.tracks = playlist.Tracks
a.tracklist.SetTracks(playlist.Tracks) a.tracklist.SetTracks(playlist.Tracks)
@@ -200,7 +225,7 @@ func (a *PlaylistPage) onRemoveSelectedFromPlaylist() {
} }
a.sm.Server.EditPlaylistTracks(a.playlistID, nil, idxs) a.sm.Server.EditPlaylistTracks(a.playlistID, nil, idxs)
a.tracklist.UnselectAll() a.tracklist.UnselectAll()
go a.Reload() a.Reload()
} }
type PlaylistPageHeader struct { type PlaylistPageHeader struct {
@@ -280,6 +305,14 @@ func NewPlaylistPageHeader(page *PlaylistPage) *PlaylistPageHeader {
return a 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 { func (a *PlaylistPageHeader) CreateRenderer() fyne.WidgetRenderer {
return widget.NewSimpleRenderer(a.container) return widget.NewSimpleRenderer(a.container)
} }
@@ -325,5 +358,5 @@ func (a *PlaylistPageHeader) formatPlaylistTrackTimeStr(p *mediaprovider.Playlis
} }
func (s *playlistPageState) Restore() Page { 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)
} }
+23 -6
View File
@@ -13,6 +13,7 @@ import (
"github.com/dweymouth/supersonic/ui/controller" "github.com/dweymouth/supersonic/ui/controller"
"github.com/dweymouth/supersonic/ui/layouts" "github.com/dweymouth/supersonic/ui/layouts"
myTheme "github.com/dweymouth/supersonic/ui/theme" myTheme "github.com/dweymouth/supersonic/ui/theme"
"github.com/dweymouth/supersonic/ui/util"
"github.com/dweymouth/supersonic/ui/widgets" "github.com/dweymouth/supersonic/ui/widgets"
"fyne.io/fyne/v2" "fyne.io/fyne/v2"
@@ -25,6 +26,7 @@ import (
type PlaylistsPage struct { type PlaylistsPage struct {
widget.BaseWidget widget.BaseWidget
pool *util.WidgetPool
cfg *backend.PlaylistsPageConfig cfg *backend.PlaylistsPageConfig
contr *controller.Controller contr *controller.Controller
mp mediaprovider.MediaProvider mp mediaprovider.MediaProvider
@@ -39,16 +41,17 @@ type PlaylistsPage struct {
gridView *widgets.GridView 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 activeView := 0
if cfg.InitialView == "Grid" { if cfg.InitialView == "Grid" {
activeView = 1 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{ a := &PlaylistsPage{
pool: pool,
cfg: cfg, cfg: cfg,
mp: mp, mp: mp,
contr: contr, contr: contr,
@@ -95,7 +98,13 @@ func (a *PlaylistsPage) createListView() {
func (a *PlaylistsPage) createGridView(playlists []*mediaprovider.Playlist) { func (a *PlaylistsPage) createGridView(playlists []*mediaprovider.Playlist) {
model := createPlaylistGridViewModel(playlists) 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) { a.gridView.OnPlay = func(id string, shuffle bool) {
go a.contr.App.PlaybackManager.PlayPlaylist(id, 0, shuffle) 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) go a.contr.App.PlaybackManager.LoadPlaylist(id, true, false)
} }
a.gridView.OnShowItemPage = a.showPlaylistPage a.gridView.OnShowItemPage = a.showPlaylistPage
a.gridView.OnShowSecondaryPage = nil
a.gridView.OnAddToPlaylist = func(id string) { a.gridView.OnAddToPlaylist = func(id string) {
go func() { go func() {
pl, err := a.contr.App.ServerManager.Server.GetPlaylist(id) pl, err := a.contr.App.ServerManager.Server.GetPlaylist(id)
@@ -221,17 +231,24 @@ func (a *PlaylistsPage) Reload() {
} }
func (a *PlaylistsPage) Save() SavedPage { func (a *PlaylistsPage) Save() SavedPage {
return &savedPlaylistsPage{ s := &savedPlaylistsPage{
contr: a.contr, contr: a.contr,
pool: a.pool,
cfg: a.cfg, cfg: a.cfg,
mp: a.mp, mp: a.mp,
searchText: a.searcher.Entry.Text, searchText: a.searcher.Entry.Text,
activeView: a.viewToggle.ActivatedButtonIndex(), activeView: a.viewToggle.ActivatedButtonIndex(),
} }
if a.gridView != nil {
a.gridView.Clear()
a.pool.Release(util.WidgetTypeGridView, a.gridView)
}
return s
} }
type savedPlaylistsPage struct { type savedPlaylistsPage struct {
contr *controller.Controller contr *controller.Controller
pool *util.WidgetPool
cfg *backend.PlaylistsPageConfig cfg *backend.PlaylistsPageConfig
mp mediaprovider.MediaProvider mp mediaprovider.MediaProvider
searchText string searchText string
@@ -239,7 +256,7 @@ type savedPlaylistsPage struct {
} }
func (s *savedPlaylistsPage) Restore() Page { 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) { func (a *PlaylistsPage) buildContainer(initialView fyne.CanvasObject) {
+13 -10
View File
@@ -3,6 +3,7 @@ package browsing
import ( import (
"github.com/dweymouth/supersonic/backend" "github.com/dweymouth/supersonic/backend"
"github.com/dweymouth/supersonic/ui/controller" "github.com/dweymouth/supersonic/ui/controller"
"github.com/dweymouth/supersonic/ui/util"
) )
type NavigationHandler interface { type NavigationHandler interface {
@@ -14,6 +15,7 @@ type Router struct {
App *backend.App App *backend.App
Controller *controller.Controller Controller *controller.Controller
Nav NavigationHandler Nav NavigationHandler
widgetPool *util.WidgetPool
} }
func NewRouter(app *backend.App, controller *controller.Controller, nav NavigationHandler) Router { 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, App: app,
Controller: controller, Controller: controller,
Nav: nav, Nav: nav,
widgetPool: util.NewWidgetPool(),
} }
return r return r
} }
@@ -28,27 +31,27 @@ func NewRouter(app *backend.App, controller *controller.Controller, nav Navigati
func (r Router) CreatePage(rte controller.Route) Page { func (r Router) CreatePage(rte controller.Route) Page {
switch rte.Page { switch rte.Page {
case controller.Album: 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: 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: 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: 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: 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: 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: case controller.Genres:
return NewGenresPage(r.Controller, r.App.ServerManager.Server) return NewGenresPage(r.Controller, r.App.ServerManager.Server)
case controller.NowPlaying: 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: 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: 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: 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 return nil
} }
+19 -7
View File
@@ -7,6 +7,7 @@ import (
"github.com/dweymouth/supersonic/ui/controller" "github.com/dweymouth/supersonic/ui/controller"
"github.com/dweymouth/supersonic/ui/layouts" "github.com/dweymouth/supersonic/ui/layouts"
"github.com/dweymouth/supersonic/ui/theme" "github.com/dweymouth/supersonic/ui/theme"
"github.com/dweymouth/supersonic/ui/util"
"github.com/dweymouth/supersonic/ui/widgets" "github.com/dweymouth/supersonic/ui/widgets"
"fyne.io/fyne/v2" "fyne.io/fyne/v2"
@@ -34,18 +35,26 @@ type TracksPage struct {
type tracksPageState struct { type tracksPageState struct {
searchText string searchText string
widgetPool *util.WidgetPool
contr *controller.Controller contr *controller.Controller
conf *backend.TracksPageConfig conf *backend.TracksPageConfig
mp mediaprovider.MediaProvider mp mediaprovider.MediaProvider
} }
func NewTracksPage(contr *controller.Controller, conf *backend.TracksPageConfig, mp mediaprovider.MediaProvider) *TracksPage { func NewTracksPage(contr *controller.Controller, conf *backend.TracksPageConfig, pool *util.WidgetPool, mp mediaprovider.MediaProvider) *TracksPage {
t := &TracksPage{tracksPageState: tracksPageState{contr: contr, conf: conf, mp: mp}} t := &TracksPage{tracksPageState: tracksPageState{contr: contr, conf: conf, widgetPool: pool, mp: mp}}
t.ExtendBaseWidget(t) t.ExtendBaseWidget(t)
t.tracklist = widgets.NewTracklist(nil) if tl := t.widgetPool.Obtain(util.WidgetTypeTracklist); tl != nil {
t.tracklist.DisableSorting = true t.tracklist = tl.(*widgets.Tracklist)
t.tracklist.AutoNumber = true 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.SetVisibleColumns(conf.TracklistColumns)
t.tracklist.OnVisibleColumnsChanged = func(cols []string) { t.tracklist.OnVisibleColumnsChanged = func(cols []string) {
t.conf.TracklistColumns = cols t.conf.TracklistColumns = cols
@@ -119,7 +128,7 @@ func (t *TracksPage) OnSearched(query string) {
func (t *TracksPage) doSearch(query string) { func (t *TracksPage) doSearch(query string) {
if t.searchTracklist == nil { if t.searchTracklist == nil {
t.searchTracklist = widgets.NewTracklist(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.SetVisibleColumns(t.conf.TracklistColumns)
t.searchTracklist.SetNowPlaying(t.nowPlayingID) t.searchTracklist.SetNowPlaying(t.nowPlayingID)
t.searchTracklist.OnVisibleColumnsChanged = func(cols []string) { t.searchTracklist.OnVisibleColumnsChanged = func(cols []string) {
@@ -141,12 +150,15 @@ func (t *TracksPage) CreateRenderer() fyne.WidgetRenderer {
} }
func (t *TracksPage) Save() SavedPage { func (t *TracksPage) Save() SavedPage {
t.loader.Dispose()
t.tracklist.Clear()
t.widgetPool.Release(util.WidgetTypeTracklist, t.tracklist)
state := t.tracksPageState state := t.tracksPageState
return &state return &state
} }
func (s *tracksPageState) Restore() Page { 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 t.searchText = s.searchText
if t.searchText != "" { if t.searchText != "" {
t.searcher.Entry.Text = t.searchText t.searcher.Entry.Text = t.searchText
+99
View File
@@ -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
}
}
+24
View File
@@ -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)
}
}
+46 -24
View File
@@ -71,6 +71,8 @@ func NewGridViewAlbumIterator(iter mediaprovider.AlbumIterator) GridViewIterator
type GridView struct { type GridView struct {
widget.BaseWidget widget.BaseWidget
stateMutex sync.RWMutex
fetchCancel context.CancelFunc
GridViewState GridViewState
grid *xwidget.GridWrap grid *xwidget.GridWrap
@@ -78,12 +80,10 @@ type GridView struct {
type GridViewState struct { type GridViewState struct {
items []GridViewItemModel items []GridViewItemModel
itemsMutex sync.RWMutex
iter GridViewIterator iter GridViewIterator
imageFetcher ImageFetcher imageFetcher ImageFetcher
placeholder fyne.Resource Placeholder fyne.Resource
highestShown int highestShown int
fetchCancel context.CancelFunc
done bool done bool
OnPlay func(id string, shuffle bool) OnPlay func(id string, shuffle bool)
@@ -104,7 +104,7 @@ func NewFixedGridView(items []GridViewItemModel, fetch ImageFetcher, placeholder
items: items, items: items,
done: true, done: true,
imageFetcher: fetch, imageFetcher: fetch,
placeholder: placeholder, Placeholder: placeholder,
}, },
} }
g.ExtendBaseWidget(g) g.ExtendBaseWidget(g)
@@ -117,7 +117,7 @@ func NewGridView(iter GridViewIterator, fetch ImageFetcher, placeholder fyne.Res
GridViewState: GridViewState{ GridViewState: GridViewState{
iter: iter, iter: iter,
imageFetcher: fetch, imageFetcher: fetch,
placeholder: placeholder, Placeholder: placeholder,
}, },
} }
g.ExtendBaseWidget(g) g.ExtendBaseWidget(g)
@@ -128,14 +128,16 @@ func NewGridView(iter GridViewIterator, fetch ImageFetcher, placeholder fyne.Res
return g return g
} }
func (g *GridView) SaveToState() GridViewState { func (g *GridView) SaveToState() *GridViewState {
g.stateMutex.RLock()
s := g.GridViewState s := g.GridViewState
g.stateMutex.RUnlock()
s.scrollPos = g.grid.GetScrollOffset() s.scrollPos = g.grid.GetScrollOffset()
return s return &s
} }
func NewGridViewFromState(state GridViewState) *GridView { func NewGridViewFromState(state *GridViewState) *GridView {
g := &GridView{GridViewState: state} g := &GridView{GridViewState: *state}
g.ExtendBaseWidget(g) g.ExtendBaseWidget(g)
g.createGridWrap() g.createGridWrap()
g.Refresh() // needed to initialize the widget g.Refresh() // needed to initialize the widget
@@ -144,8 +146,12 @@ func NewGridViewFromState(state GridViewState) *GridView {
} }
func (g *GridView) Clear() { func (g *GridView) Clear() {
g.itemsMutex.Lock() if g.fetchCancel != nil {
defer g.itemsMutex.Unlock() g.fetchCancel()
g.fetchCancel = nil
}
g.stateMutex.Lock()
defer g.stateMutex.Unlock()
g.items = nil g.items = nil
g.done = true g.done = true
} }
@@ -155,13 +161,27 @@ func (g *GridView) Reset(iter GridViewIterator) {
g.fetchCancel() g.fetchCancel()
g.fetchCancel = nil g.fetchCancel = nil
} }
g.itemsMutex.Lock() g.stateMutex.Lock()
g.items = nil g.items = nil
g.done = false g.done = false
g.highestShown = 0 g.highestShown = 0
g.iter = iter g.iter = iter
g.itemsMutex.Unlock() g.stateMutex.Unlock()
g.fetchMoreItems(36) 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) { func (g *GridView) ResetFixed(items []GridViewItemModel) {
@@ -169,12 +189,13 @@ func (g *GridView) ResetFixed(items []GridViewItemModel) {
g.fetchCancel() g.fetchCancel()
g.fetchCancel = nil g.fetchCancel = nil
} }
g.itemsMutex.Lock() g.stateMutex.Lock()
g.items = items g.items = items
g.itemsMutex.Unlock()
g.done = true g.done = true
g.highestShown = 0 g.highestShown = 0
g.iter = nil g.iter = nil
g.stateMutex.Unlock()
g.Refresh()
} }
func (g *GridView) GetScrollOffset() float32 { func (g *GridView) GetScrollOffset() float32 {
@@ -192,7 +213,7 @@ func (g *GridView) createGridWrap() {
}, },
// create func // create func
func() fyne.CanvasObject { func() fyne.CanvasObject {
card := NewGridViewItem(g.placeholder) card := NewGridViewItem(g.Placeholder)
card.OnPlay = func(shuffle bool) { card.OnPlay = func(shuffle bool) {
if g.OnPlay != nil { if g.OnPlay != nil {
g.OnPlay(card.ItemID(), shuffle) g.OnPlay(card.ItemID(), shuffle)
@@ -220,7 +241,7 @@ func (g *GridView) createGridWrap() {
} }
card.OnDownload = func() { card.OnDownload = func() {
if g.OnDownload != nil { if g.OnDownload != nil {
g.OnDownload(card.itemID) g.OnDownload(card.ItemID())
} }
} }
return card return card
@@ -238,13 +259,14 @@ func (g *GridView) doUpdateItemCard(itemIdx int, card *GridViewItem) {
g.highestShown = itemIdx g.highestShown = itemIdx
} }
var item GridViewItemModel var item GridViewItemModel
g.itemsMutex.RLock() g.stateMutex.RLock()
// itemIdx can rarely be out of range if the data is being updated // itemIdx can rarely be out of range if the data is being updated
// as the view is requested to refresh // as the view is requested to refresh
if itemIdx < len(g.items) { if itemIdx < len(g.items) {
item = g.items[itemIdx] item = g.items[itemIdx]
} }
g.itemsMutex.RUnlock() g.stateMutex.RUnlock()
card.Cover.Im.CenterIcon = g.Placeholder
if card.PrevID == item.ID { if card.PrevID == item.ID {
// nothing to do // nothing to do
return return
@@ -290,8 +312,8 @@ func (g *GridView) doUpdateItemCard(itemIdx int, card *GridViewItem) {
} }
func (g *GridView) lenItems() int { func (g *GridView) lenItems() int {
g.itemsMutex.RLock() g.stateMutex.RLock()
defer g.itemsMutex.RUnlock() defer g.stateMutex.RUnlock()
return len(g.items) return len(g.items)
} }
@@ -314,15 +336,15 @@ func (g *GridView) fetchMoreItems(count int) {
case <-ctx.Done(): case <-ctx.Done():
return return
default: default:
g.itemsMutex.Lock() g.stateMutex.Lock()
g.items = append(g.items, items...) g.items = append(g.items, items...)
g.itemsMutex.Unlock() g.stateMutex.Unlock()
if len(items) < batchFetchSize { if len(items) < batchFetchSize {
g.done = true g.done = true
} }
n += len(items) n += len(items)
if len(items) > 0 { if len(items) > 0 {
g.Refresh() g.grid.Refresh()
} }
} }
} }
+4
View File
@@ -211,6 +211,10 @@ func (g *GridViewItem) Update(model GridViewItemModel) {
g.Cover.ResetPlayButton() g.Cover.ResetPlayButton()
} }
func (g *GridViewItem) Refresh() {
g.BaseWidget.Refresh()
}
func (g *GridViewItem) ItemID() string { func (g *GridViewItem) ItemID() string {
return g.itemID return g.itemID
} }
+4 -2
View File
@@ -15,7 +15,8 @@ import (
// a placeholder with a rectangular border frame // a placeholder with a rectangular border frame
// and an icon positioned in the center of the frame. // and an icon positioned in the center of the frame.
type ImagePlaceholder struct { type ImagePlaceholder struct {
ScaleMode canvas.ImageScale ScaleMode canvas.ImageScale
CenterIcon fyne.Resource
widget.BaseWidget widget.BaseWidget
content *fyne.Container content *fyne.Container
@@ -30,7 +31,7 @@ type ImagePlaceholder struct {
} }
func NewImagePlaceholder(centerIcon fyne.Resource, minSize float32) *ImagePlaceholder { func NewImagePlaceholder(centerIcon fyne.Resource, minSize float32) *ImagePlaceholder {
i := &ImagePlaceholder{minSize: minSize} i := &ImagePlaceholder{minSize: minSize, CenterIcon: centerIcon}
i.ExtendBaseWidget(i) i.ExtendBaseWidget(i)
i.iconImage = canvas.NewImageFromResource(centerIcon) i.iconImage = canvas.NewImageFromResource(centerIcon)
i.iconImage.FillMode = canvas.ImageFillContain i.iconImage.FillMode = canvas.ImageFillContain
@@ -86,6 +87,7 @@ func (i *ImagePlaceholder) MinSize() fyne.Size {
func (i *ImagePlaceholder) Refresh() { func (i *ImagePlaceholder) Refresh() {
i.border.Hidden = i.HaveImage() i.border.Hidden = i.HaveImage()
i.iconImage.Resource = i.CenterIcon
i.iconImage.Hidden = i.HaveImage() i.iconImage.Hidden = i.HaveImage()
i.imageDisp.Hidden = !i.HaveImage() i.imageDisp.Hidden = !i.HaveImage()
i.imageDisp.ScaleMode = i.ScaleMode i.imageDisp.ScaleMode = i.ScaleMode
+20 -9
View File
@@ -47,9 +47,7 @@ type TracklistSort struct {
ColumnName string ColumnName string
} }
type Tracklist struct { type TracklistOptions struct {
widget.BaseWidget
// AutoNumber sets whether to auto-number the tracks 1..N in display order, // AutoNumber sets whether to auto-number the tracks 1..N in display order,
// or to use the number from the track's metadata // or to use the number from the track's metadata
AutoNumber bool AutoNumber bool
@@ -68,6 +66,12 @@ type Tracklist struct {
// Disables sorting the tracklist by clicking individual columns. // Disables sorting the tracklist by clicking individual columns.
DisableSorting bool DisableSorting bool
}
type Tracklist struct {
widget.BaseWidget
Options TracklistOptions
// user action callbacks // user action callbacks
OnPlayTrackAt func(int) OnPlayTrackAt func(int)
@@ -141,7 +145,7 @@ func NewTracklist(tracks []*mediaprovider.Track) *Tracklist {
tr := item.(*TrackRow) tr := item.(*TrackRow)
tr.trackIdx = itemID tr.trackIdx = itemID
i := -1 // signal that we want to display the actual track num. i := -1 // signal that we want to display the actual track num.
if t.AutoNumber { if t.Options.AutoNumber {
i = itemID + 1 i = itemID + 1
} }
tr.Update(t.trackModelAt(itemID), i) tr.Update(t.trackModelAt(itemID), i)
@@ -153,6 +157,13 @@ func NewTracklist(tracks []*mediaprovider.Track) *Tracklist {
return t return t
} }
func (t *Tracklist) Reset() {
t.Clear()
t.Options = TracklistOptions{}
t.ctxMenu = nil
t.SetSorting(TracklistSort{})
}
func (t *Tracklist) buildHeader() { func (t *Tracklist) buildHeader() {
t.hdr = NewListHeader([]ListColumn{ t.hdr = NewListHeader([]ListColumn{
{Text: "#", Alignment: fyne.TextAlignTrailing, CanToggleVisible: false}, {Text: "#", Alignment: fyne.TextAlignTrailing, CanToggleVisible: false},
@@ -331,7 +342,7 @@ func (t *Tracklist) CreateRenderer() fyne.WidgetRenderer {
} }
func (t *Tracklist) Refresh() { func (t *Tracklist) Refresh() {
t.hdr.DisableSorting = t.DisableSorting t.hdr.DisableSorting = t.Options.DisableSorting
t.BaseWidget.Refresh() t.BaseWidget.Refresh()
} }
@@ -487,7 +498,7 @@ func (t *Tracklist) onShowContextMenu(e *fyne.PointEvent, trackIdx int) {
t.list.Refresh() t.list.Refresh()
if t.ctxMenu == nil { if t.ctxMenu == nil {
t.ctxMenu = fyne.NewMenu("") t.ctxMenu = fyne.NewMenu("")
if !t.DisablePlaybackMenu { if !t.Options.DisablePlaybackMenu {
t.ctxMenu.Items = append(t.ctxMenu.Items, t.ctxMenu.Items = append(t.ctxMenu.Items,
fyne.NewMenuItem("Play", func() { fyne.NewMenuItem("Play", func() {
if t.OnPlaySelection != nil { if t.OnPlaySelection != nil {
@@ -530,9 +541,9 @@ func (t *Tracklist) onShowContextMenu(e *fyne.PointEvent, trackIdx int) {
t.onSetRatings(t.selectedTracks(), rating, true) t.onSetRatings(t.selectedTracks(), rating, true)
}) })
t.ctxMenu.Items = append(t.ctxMenu.Items, ratingMenu) 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, 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) 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 var str string
if rowNum < 0 { if rowNum < 0 {
rowNum = tr.TrackNumber rowNum = tr.TrackNumber
if t.tracklist.ShowDiscNumber { if t.tracklist.Options.ShowDiscNumber {
discNum = tr.DiscNumber discNum = tr.DiscNumber
} }
} }
+15 -1
View File
@@ -7,6 +7,8 @@ import (
// Component that manages lazily loading more tracks into a Tracklist // Component that manages lazily loading more tracks into a Tracklist
// as the user scrolls near the bottom. // as the user scrolls near the bottom.
type TracklistLoader struct { type TracklistLoader struct {
disposed bool
tracklist *Tracklist tracklist *Tracklist
iter mediaprovider.TrackIterator iter mediaprovider.TrackIterator
@@ -28,11 +30,17 @@ func NewTracklistLoader(tracklist *Tracklist, iter mediaprovider.TrackIterator)
return t 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) { func (t *TracklistLoader) onTrackShown(tracknum int) {
if tracknum > t.highestShown { if tracknum > t.highestShown {
t.highestShown = tracknum 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 t.fetching = true
go t.loadMoreTracks(25) go t.loadMoreTracks(25)
} }
@@ -52,6 +60,12 @@ func (t *TracklistLoader) loadMoreTracks(num int) {
break break
} }
t.trackBuffer = append(t.trackBuffer, tr) t.trackBuffer = append(t.trackBuffer, tr)
if t.disposed {
break
}
}
if t.disposed {
return
} }
t.tracklist.AppendTracks(t.trackBuffer) t.tracklist.AppendTracks(t.trackBuffer)
t.tracklist.Refresh() t.tracklist.Refresh()