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
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)
}
+79 -67
View File
@@ -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 {
+73 -14
View File
@@ -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()
+17 -3
View File
@@ -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)
}
+6 -2
View File
@@ -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()
}
}
+98 -55
View File
@@ -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
+76 -63
View File
@@ -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 {
+21 -8
View File
@@ -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)
}
+41 -8
View File
@@ -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)
}
+23 -6
View File
@@ -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) {
+13 -10
View File
@@ -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
}
+19 -7
View File
@@ -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
+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 {
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()
}
}
}
+4
View File
@@ -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
}
+4 -2
View File
@@ -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
+20 -9
View File
@@ -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
}
}
+15 -1
View File
@@ -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()