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:
@@ -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
@@ -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
@@ -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()
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user