Merge pull request #355 from adamantike/misc/migrate-artists-page-to-gridview

misc: Migrate Artists page to GridView
This commit is contained in:
Drew Weymouth
2024-03-31 08:06:07 -07:00
committed by GitHub
12 changed files with 254 additions and 220 deletions
+53 -198
View File
@@ -1,223 +1,78 @@
package browsing
import (
"fmt"
"strings"
"slices"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/layout"
"fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget"
"github.com/dweymouth/supersonic/backend"
"github.com/dweymouth/supersonic/backend/mediaprovider"
"github.com/dweymouth/supersonic/sharedutil"
"github.com/dweymouth/supersonic/ui/controller"
myTheme "github.com/dweymouth/supersonic/ui/theme"
"github.com/dweymouth/supersonic/ui/util"
"github.com/dweymouth/supersonic/ui/widgets"
)
type ArtistsPage struct {
widget.BaseWidget
contr *controller.Controller
pool *util.WidgetPool
im *backend.ImageManager
pm *backend.PlaybackManager
mp mediaprovider.MediaProvider
artists []*mediaprovider.Artist
searchedArtists []*mediaprovider.Artist
grid *widgets.GridView
fullGridScrollPos float32
searchGridScrollPos float32
searcher *widgets.SearchEntry
searchText string
titleDisp *widget.RichText
container *fyne.Container
type artistsPageAdapter struct {
cfg *backend.ArtistsPageConfig
contr *controller.Controller
mp mediaprovider.MediaProvider
pm *backend.PlaybackManager
filter mediaprovider.ArtistFilter
}
func NewArtistsPage(
contr *controller.Controller,
pool *util.WidgetPool,
pm *backend.PlaybackManager,
mp mediaprovider.MediaProvider,
im *backend.ImageManager,
) *ArtistsPage {
return newArtistsPage(contr, pool, pm, mp, im, "", 0, 0)
func NewArtistsPage(cfg *backend.ArtistsPageConfig, pool *util.WidgetPool, contr *controller.Controller, pm *backend.PlaybackManager, mp mediaprovider.MediaProvider, im *backend.ImageManager) Page {
adapter := &artistsPageAdapter{cfg: cfg, contr: contr, mp: mp, pm: pm}
return NewGridViewPage(adapter, pool, mp, im)
}
func newArtistsPage(
contr *controller.Controller,
pool *util.WidgetPool,
pm *backend.PlaybackManager,
mp mediaprovider.MediaProvider,
im *backend.ImageManager,
searchText string,
fullGridScrollPos float32,
searchGridScrollPos float32,
) *ArtistsPage {
a := &ArtistsPage{
contr: contr,
pool: pool,
pm: pm,
mp: mp,
im: im,
searchText: searchText,
fullGridScrollPos: fullGridScrollPos,
searchGridScrollPos: searchGridScrollPos,
}
a.ExtendBaseWidget(a)
func (a *artistsPageAdapter) Title() string { return "Artists" }
a.titleDisp = widget.NewRichTextWithText("Artists")
a.titleDisp.Segments[0].(*widget.TextSegment).Style = widget.RichTextStyle{
SizeName: theme.SizeNameHeadingText,
func (a *artistsPageAdapter) Filter() mediaprovider.ArtistFilter {
if a.filter == nil {
a.filter = mediaprovider.NewArtistFilter(
mediaprovider.ArtistFilterOptions{},
)
}
a.searcher = widgets.NewSearchEntry()
a.searcher.PlaceHolder = "Search page"
a.searcher.OnSearched = func(query string) { a.onSearched(query, false /*firstLoad*/) }
a.searcher.Entry.Text = searchText
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)
return a.filter
}
func (a *artistsPageAdapter) FilterButton() widgets.FilterButton[mediaprovider.Artist, mediaprovider.ArtistFilterOptions] {
return nil
}
func (a *artistsPageAdapter) PlaceholderResource() fyne.Resource { return myTheme.ArtistIcon }
func (a *artistsPageAdapter) Route() controller.Route { return controller.ArtistsRoute() }
func (a *artistsPageAdapter) SortOrders() ([]string, string) {
orders := a.mp.ArtistSortOrders()
sortOrder := a.cfg.SortOrder
if !slices.Contains(orders, sortOrder) {
sortOrder = string(orders[0])
}
return orders, sortOrder
}
func (a *artistsPageAdapter) SaveSortOrder(order string) {
a.cfg.SortOrder = order
}
func (a *artistsPageAdapter) ActionButton() *widget.Button { return nil }
func (a *artistsPageAdapter) Iter(sortOrder string, filter mediaprovider.ArtistFilter) widgets.GridViewIterator {
return widgets.NewGridViewArtistIterator(a.mp.IterateArtists(sortOrder, filter))
}
func (a *artistsPageAdapter) SearchIter(query string, filter mediaprovider.ArtistFilter) widgets.GridViewIterator {
return widgets.NewGridViewArtistIterator(a.mp.SearchArtists(query, filter))
}
func (a *artistsPageAdapter) ConnectGridActions(gv *widgets.GridView) {
canShareArtists := false
if r, canShare := mp.(mediaprovider.SupportsSharing); canShare {
if r, canShare := a.mp.(mediaprovider.SupportsSharing); canShare {
canShareArtists = r.CanShareArtists()
}
a.grid.DisableSharing = !canShareArtists
a.contr.ConnectArtistGridActions(a.grid)
searchVbox := container.NewVBox(layout.NewSpacer(), a.searcher, layout.NewSpacer())
a.container = container.NewBorder(
container.NewHBox(util.NewHSpace(6),
a.titleDisp, layout.NewSpacer(), searchVbox, util.NewHSpace(15)),
nil, nil, nil, a.grid,
)
go a.load()
return a
}
func (a *ArtistsPage) Reload() {
a.searchGridScrollPos = 0
a.fullGridScrollPos = 0
go a.load()
}
func (a *ArtistsPage) load() {
iter := a.mp.IterateArtists("")
var artists []*mediaprovider.Artist
for {
artist := iter.Next()
if artist == nil {
break
}
artists = append(artists, artist)
}
a.artists = artists
a.onSearched(a.searcher.Entry.Text, true)
}
var _ Searchable = (*ArtistsPage)(nil)
func (a *ArtistsPage) SearchWidget() fyne.Focusable {
return a.searcher
}
func (a *ArtistsPage) onSearched(query string, firstLoad bool) {
// since the playlist list is returned in full non-paginated, we will do our own
// simple search based on the name, description, and owner, rather than calling a server API
var artists []*mediaprovider.Artist
scrollPos := float32(0)
if query == "" {
a.searchedArtists = nil
artists = a.artists
scrollPos = a.fullGridScrollPos
} else {
if firstLoad { // if reloading with a saved search state, set scroll position
scrollPos = a.searchGridScrollPos
}
if a.searchText == "" {
// if first search, capture scroll position of full, unsearched grid
a.fullGridScrollPos = a.grid.GetScrollOffset()
}
qLower := strings.ToLower(query)
a.searchedArtists = sharedutil.FilterSlice(a.artists, func(p *mediaprovider.Artist) bool {
return strings.Contains(strings.ToLower(p.Name), qLower)
})
artists = a.searchedArtists
}
a.searchText = query
a.grid.ResetFixed(createArtistsGridViewModel(artists))
a.grid.Refresh()
a.grid.ScrollToOffset(scrollPos)
}
func createArtistsGridViewModel(artists []*mediaprovider.Artist) []widgets.GridViewItemModel {
return sharedutil.MapSlice(artists, func(ar *mediaprovider.Artist) widgets.GridViewItemModel {
albums := "albums"
if ar.AlbumCount == 1 {
albums = "album"
}
return widgets.GridViewItemModel{
Name: ar.Name,
ID: ar.ID,
CoverArtID: ar.CoverArtID,
Secondary: []string{fmt.Sprintf("%d %s", ar.AlbumCount, albums)},
}
})
}
var _ Scrollable = (*ArtistsPage)(nil)
func (a *ArtistsPage) Scroll(scrollAmt float32) {
a.grid.ScrollToOffset(a.grid.GetScrollOffset() + scrollAmt)
}
func (a *ArtistsPage) CreateRenderer() fyne.WidgetRenderer {
return widget.NewSimpleRenderer(a.container)
}
func (a *ArtistsPage) Route() controller.Route {
return controller.ArtistsRoute()
}
func (a *ArtistsPage) Save() SavedPage {
s := &savedArtistsPage{
contr: a.contr,
pool: a.pool,
im: a.im,
pm: a.pm,
mp: a.mp,
searchText: a.searchText,
fullGridScrollPos: a.fullGridScrollPos,
}
if a.searchText == "" {
s.fullGridScrollPos = a.grid.GetScrollOffset()
} 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
searchText string
fullGridScrollPos float32
searchGridScrollPos float32
}
func (s *savedArtistsPage) Restore() Page {
return newArtistsPage(s.contr, s.pool, s.pm, s.mp, s.im, s.searchText, s.fullGridScrollPos, s.searchGridScrollPos)
gv.DisableSharing = !canShareArtists
a.contr.ConnectArtistGridActions(gv)
}
+1 -1
View File
@@ -40,7 +40,7 @@ func (r Router) CreatePage(rte controller.Route) Page {
case controller.Artist:
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.widgetPool, r.App.PlaybackManager, r.App.ServerManager.Server, r.App.ImageManager)
return NewArtistsPage(&r.App.Config.ArtistsPage, r.widgetPool, r.Controller, r.App.PlaybackManager, r.App.ServerManager.Server, r.App.ImageManager)
case controller.Favorites:
return NewFavoritesPage(&r.App.Config.FavoritesPage, r.widgetPool, r.Controller, r.App.ServerManager.Server, r.App.PlaybackManager, r.App.ImageManager)
case controller.Genre:
+3
View File
@@ -220,6 +220,9 @@ func (m *Controller) ConnectArtistGridActions(grid *widgets.GridView) {
m.ShowDownloadDialog(tracks, artist.Name)
}()
}
grid.OnShare = func(artistID string) {
go m.ShowShareDialog(artistID)
}
}
func (m *Controller) GetArtistTracks(artistID string) []*mediaprovider.Track {
+25
View File
@@ -2,6 +2,7 @@ package widgets
import (
"context"
"fmt"
"math"
"sync"
@@ -65,6 +66,30 @@ func NewGridViewAlbumIterator(iter mediaprovider.AlbumIterator) GridViewIterator
return gridViewAlbumIterator{iter: NewBatchingIterator(iter)}
}
type gridViewArtistIterator struct {
iter BatchingIterator[mediaprovider.Artist]
}
func (g gridViewArtistIterator) NextN(n int) []GridViewItemModel {
artists := g.iter.NextN(n)
return sharedutil.MapSlice(artists, func(ar *mediaprovider.Artist) GridViewItemModel {
albumsLabel := "albums"
if ar.AlbumCount == 1 {
albumsLabel = "album"
}
return GridViewItemModel{
Name: ar.Name,
ID: ar.ID,
CoverArtID: ar.CoverArtID,
Secondary: []string{fmt.Sprintf("%d %s", ar.AlbumCount, albumsLabel)},
}
})
}
func NewGridViewArtistIterator(iter mediaprovider.ArtistIterator) GridViewIterator {
return gridViewArtistIterator{iter: NewBatchingIterator(iter)}
}
type GridView struct {
widget.BaseWidget