diff --git a/sharedutil/sharedutil.go b/sharedutil/sharedutil.go index e62703f..1729f9c 100644 --- a/sharedutil/sharedutil.go +++ b/sharedutil/sharedutil.go @@ -35,6 +35,14 @@ func FilterSlice[T any](ss []T, test func(T) bool) []T { return result } +func MapSlice[T any, U any](ts []T, f func(T) U) []U { + result := make([]U, len(ts)) + for i, t := range ts { + result[i] = f(t) + } + return result +} + func FindTrackByID(id string, tracks []*subsonic.Child) *subsonic.Child { for _, tr := range tracks { if id == tr.ID { diff --git a/ui/browsing/albumspage.go b/ui/browsing/albumspage.go index 9cba0a2..fa260bb 100644 --- a/ui/browsing/albumspage.go +++ b/ui/browsing/albumspage.go @@ -72,7 +72,7 @@ func NewAlbumsPage(cfg *backend.AlbumsPageConfig, contr *controller.Controller, } a.sortOrder.Selected = cfg.SortOrder iter := lm.AlbumsIter(backend.AlbumSortOrder(a.sortOrder.Selected)) - a.grid = widgets.NewAlbumGrid(iter, im, false /*showYear*/) + a.grid = widgets.NewGridView(widgets.NewGridViewAlbumIterator(iter), im) a.grid.OnPlay = a.onPlayAlbum a.grid.OnShowSecondaryPage = a.onShowArtistPage a.grid.OnShowItemPage = a.onShowAlbumPage @@ -180,7 +180,7 @@ func (a *AlbumsPage) Save() SavedPage { func (a *AlbumsPage) doSearch(query string) { if a.searchGrid == nil { - a.searchGrid = widgets.NewAlbumGrid(a.lm.SearchIter(query), a.im, false /*showYear*/) + a.searchGrid = widgets.NewGridView(widgets.NewGridViewAlbumIterator(a.lm.SearchIter(query)), a.im) a.searchGrid.OnPlay = a.onPlayAlbum a.searchGrid.OnShowItemPage = a.onShowAlbumPage a.searchGrid.OnShowSecondaryPage = a.onShowArtistPage diff --git a/ui/browsing/artistpage.go b/ui/browsing/artistpage.go index 99503ad..7eac001 100644 --- a/ui/browsing/artistpage.go +++ b/ui/browsing/artistpage.go @@ -2,6 +2,7 @@ package browsing import ( "log" + "strconv" "strings" "supersonic/backend" "supersonic/res" @@ -172,7 +173,15 @@ func (a *ArtistPage) showAlbumGrid() { a.activeView = 0 // if page still loading, will show discography view first return } - a.albumGrid = widgets.NewFixedAlbumGrid(a.artistInfo.Album, a.im, true /*showYear*/) + model := sharedutil.MapSlice(a.artistInfo.Album, func(al *subsonic.AlbumID3) widgets.GridViewItemModel { + return widgets.GridViewItemModel{ + Name: al.Name, + ID: al.ID, + CoverArtID: al.CoverArt, + Secondary: strconv.Itoa(al.Year), + } + }) + a.albumGrid = widgets.NewFixedGridView(model, a.im) a.albumGrid.OnPlay = a.onPlayAlbum a.albumGrid.OnShowItemPage = a.onShowAlbumPage } diff --git a/ui/browsing/favoritespage.go b/ui/browsing/favoritespage.go index 7dd8556..3a1decb 100644 --- a/ui/browsing/favoritespage.go +++ b/ui/browsing/favoritespage.go @@ -53,7 +53,7 @@ func NewFavoritesPage(cfg *backend.FavoritesPageConfig, contr *controller.Contro } a.ExtendBaseWidget(a) a.createHeader(0, "") - a.grid = widgets.NewAlbumGrid(a.lm.StarredIter(), a.im, false) + a.grid = widgets.NewGridView(widgets.NewGridViewAlbumIterator(lm.StarredIter()), a.im) a.connectGridActions() if cfg.InitialView == "Artists" { a.toggleBtns.SetActivatedButton(1) @@ -220,7 +220,7 @@ func (a *FavoritesPage) doSearchAlbums(query string) { return al.Starred.After(time.Time{}) }) if a.searchGrid == nil { - a.searchGrid = widgets.NewAlbumGrid(iter, a.im, false /*showYear*/) + a.searchGrid = widgets.NewGridView(widgets.NewGridViewAlbumIterator(iter), a.im) a.searchGrid.OnPlay = a.onPlayAlbum a.searchGrid.OnShowItemPage = a.onShowAlbumPage a.searchGrid.OnShowSecondaryPage = a.onShowArtistPage diff --git a/ui/browsing/genrepage.go b/ui/browsing/genrepage.go index 6b3534c..bdeb532 100644 --- a/ui/browsing/genrepage.go +++ b/ui/browsing/genrepage.go @@ -52,7 +52,7 @@ func NewGenrePage(genre string, contr *controller.Controller, pm *backend.Playba } g.playRandom = widget.NewButtonWithIcon("Play random", res.ResShuffleInvertSvg, g.playRandomSongs) iter := g.lm.GenreIter(g.genre) - g.grid = widgets.NewAlbumGrid(iter, g.im, false) + g.grid = widgets.NewGridView(widgets.NewGridViewAlbumIterator(iter), g.im) g.grid.OnPlay = g.onPlayAlbum g.grid.OnShowSecondaryPage = g.onShowArtistPage g.grid.OnShowItemPage = g.onShowAlbumPage @@ -176,7 +176,7 @@ func (g *GenrePage) doSearch(query string) { return al.Genre == g.genre }) if g.searchGrid == nil { - g.searchGrid = widgets.NewAlbumGrid(iter, g.im, false /*showYear*/) + g.searchGrid = widgets.NewGridView(widgets.NewGridViewAlbumIterator(iter), g.im) g.searchGrid.OnPlay = g.onPlayAlbum g.searchGrid.OnShowItemPage = g.onShowAlbumPage g.searchGrid.OnShowSecondaryPage = g.onShowArtistPage diff --git a/ui/widgets/gridview.go b/ui/widgets/gridview.go index e52ecb0..9785ca6 100644 --- a/ui/widgets/gridview.go +++ b/ui/widgets/gridview.go @@ -6,6 +6,7 @@ import ( "log" "supersonic/backend" "supersonic/res" + "supersonic/sharedutil" "sync" "fyne.io/fyne/v2" @@ -20,6 +21,31 @@ type ImageFetcher interface { GetCoverThumbnail(string) (image.Image, error) } +type GridViewIterator interface { + NextN(int) []GridViewItemModel +} + +type gridViewAlbumIterator struct { + iter *backend.BatchingIterator +} + +func (g gridViewAlbumIterator) NextN(n int) []GridViewItemModel { + albums := g.iter.NextN(n) + return sharedutil.MapSlice(albums, func(al *subsonic.AlbumID3) GridViewItemModel { + return GridViewItemModel{ + Name: al.Name, + ID: al.ID, + CoverArtID: al.CoverArt, + Secondary: al.Artist, + SecondaryID: al.ArtistID, + } + }) +} + +func NewGridViewAlbumIterator(iter backend.AlbumIterator) GridViewIterator { + return gridViewAlbumIterator{iter: backend.NewBatchingIterator(iter)} +} + type GridView struct { widget.BaseWidget @@ -29,13 +55,12 @@ type GridView struct { } type GridViewState struct { - items []*subsonic.AlbumID3 + items []GridViewItemModel itemsMutex sync.RWMutex - iter *backend.BatchingIterator + iter GridViewIterator highestShown int fetching bool done bool - showYear bool imageFetcher ImageFetcher OnPlay func(string) @@ -47,13 +72,12 @@ type GridViewState struct { var _ fyne.Widget = (*GridView)(nil) -func NewFixedAlbumGrid(albums []*subsonic.AlbumID3, fetch ImageFetcher, showYear bool) *GridView { +func NewFixedGridView(items []GridViewItemModel, fetch ImageFetcher) *GridView { g := &GridView{ GridViewState: GridViewState{ - items: albums, + items: items, done: true, imageFetcher: fetch, - showYear: showYear, }, } g.ExtendBaseWidget(g) @@ -61,10 +85,10 @@ func NewFixedAlbumGrid(albums []*subsonic.AlbumID3, fetch ImageFetcher, showYear return g } -func NewAlbumGrid(iter backend.AlbumIterator, fetch ImageFetcher, showYear bool) *GridView { +func NewGridView(iter GridViewIterator, fetch ImageFetcher) *GridView { g := &GridView{ GridViewState: GridViewState{ - iter: backend.NewBatchingIterator(iter), + iter: iter, imageFetcher: fetch, }, } @@ -106,7 +130,7 @@ func (g *GridView) Reset(iter backend.AlbumIterator) { g.fetching = false g.done = false g.highestShown = 0 - g.iter = backend.NewBatchingIterator(iter) + g.iter = gridViewAlbumIterator{iter: backend.NewBatchingIterator(iter)} g.fetchMoreItems(36) } @@ -117,7 +141,7 @@ func (g *GridView) createGridWrapList() { }, // create func func() fyne.CanvasObject { - card := NewGridViewCard(g.showYear) + card := NewGridViewItem() card.OnPlay = func() { if g.OnPlay != nil { g.OnPlay(card.ItemID()) @@ -137,31 +161,31 @@ func (g *GridView) createGridWrapList() { }, // update func func(itemID int, obj fyne.CanvasObject) { - ac := obj.(*GridViewCard) + ac := obj.(*GridViewItem) g.doUpdateItemCard(itemID, ac) }, ) } -func (g *GridView) doUpdateItemCard(itemIdx int, card *GridViewCard) { +func (g *GridView) doUpdateItemCard(itemIdx int, card *GridViewItem) { if itemIdx > g.highestShown { g.highestShown = itemIdx } g.itemsMutex.RLock() - album := g.items[itemIdx] + item := g.items[itemIdx] g.itemsMutex.RUnlock() - if card.PrevID == album.ID { + if card.PrevID == item.ID { // nothing to do return } - card.Update(album) - card.PrevID = album.ID + card.Update(item) + card.PrevID = item.ID // cancel any previous image fetch if card.ImgLoadCancel != nil { card.ImgLoadCancel() card.ImgLoadCancel = nil } - if img, ok := g.imageFetcher.GetCoverThumbnailFromCache(album.CoverArt); ok { + if img, ok := g.imageFetcher.GetCoverThumbnailFromCache(item.CoverArtID); ok { card.Cover.SetImage(img) } else { card.Cover.SetImageResource(res.ResAlbumplaceholderPng) @@ -169,7 +193,7 @@ func (g *GridView) doUpdateItemCard(itemIdx int, card *GridViewCard) { ctx, cancel := context.WithCancel(context.Background()) card.ImgLoadCancel = cancel go func(ctx context.Context) { - i, err := g.imageFetcher.GetCoverThumbnail(album.CoverArt) + i, err := g.imageFetcher.GetCoverThumbnail(item.CoverArtID) select { case <-ctx.Done(): return diff --git a/ui/widgets/gridviewcard.go b/ui/widgets/gridviewitem.go similarity index 74% rename from ui/widgets/gridviewcard.go rename to ui/widgets/gridviewitem.go index 853fd18..232cfdc 100644 --- a/ui/widgets/gridviewcard.go +++ b/ui/widgets/gridviewitem.go @@ -3,7 +3,6 @@ package widgets import ( "context" "image" - "strconv" "supersonic/res" "supersonic/ui/layouts" @@ -13,11 +12,9 @@ import ( "fyne.io/fyne/v2/container" "fyne.io/fyne/v2/driver/desktop" "fyne.io/fyne/v2/widget" - - "github.com/dweymouth/go-subsonic/subsonic" ) -var _ fyne.Widget = (*GridViewCard)(nil) +var _ fyne.Widget = (*GridViewItem)(nil) var _ fyne.Widget = (*coverImage)(nil) var _ fyne.Tappable = (*coverImage)(nil) @@ -104,7 +101,7 @@ func isInside(origin fyne.Position, radius float32, point fyne.Position) bool { return x*x+y*y <= radius*radius } -type GridViewCardModel struct { +type GridViewItemModel struct { Name string ID string CoverArtID string @@ -112,17 +109,14 @@ type GridViewCardModel struct { SecondaryID string } -type GridViewCard struct { +type GridViewItem struct { widget.BaseWidget - albumID string - artistID string - title *CustomHyperlink - artist *CustomHyperlink - year *widget.Label - container *fyne.Container - - showYear bool + itemID string + secondaryID string + primaryText *CustomHyperlink + secondaryText *CustomHyperlink + container *fyne.Container // updated by GridView Cover *coverImage @@ -136,13 +130,11 @@ type GridViewCard struct { OnShowSecondaryPage func() } -func NewGridViewCard(showYear bool) *GridViewCard { - g := &GridViewCard{ - title: NewCustomHyperlink(), - artist: NewCustomHyperlink(), - year: widget.NewLabel(""), - Cover: newCoverImage(), - showYear: showYear, +func NewGridViewItem() *GridViewItem { + g := &GridViewItem{ + primaryText: NewCustomHyperlink(), + secondaryText: NewCustomHyperlink(), + Cover: newCoverImage(), } g.ExtendBaseWidget(g) g.Cover.OnDoubleTapped = func() { @@ -156,8 +148,8 @@ func NewGridViewCard(showYear bool) *GridViewCard { } } g.Cover.OnTapped = showItemFn - g.title.OnTapped = showItemFn - g.artist.OnTapped = func() { + g.primaryText.OnTapped = showItemFn + g.secondaryText.OnTapped = func() { if g.OnShowSecondaryPage != nil { g.OnShowSecondaryPage() } @@ -167,34 +159,31 @@ func NewGridViewCard(showYear bool) *GridViewCard { return g } -func (g *GridViewCard) createContainer() { - var secondLabel fyne.Widget = g.artist - if g.showYear { - secondLabel = g.year - } - info := container.New(&layouts.VboxCustomPadding{ExtraPad: -16}, g.title, secondLabel) +func (g *GridViewItem) createContainer() { + info := container.New(&layouts.VboxCustomPadding{ExtraPad: -16}, g.primaryText, g.secondaryText) c := container.New(&layouts.VboxCustomPadding{ExtraPad: -5}, g.Cover, info) pad := &layouts.CenterPadLayout{PadLeftRight: 20, PadTopBottom: 10} g.container = container.New(pad, c) } -func (g *GridViewCard) Update(al *subsonic.AlbumID3) { - g.title.SetText(al.Name) - g.artist.SetText(al.Artist) - g.year.SetText(strconv.Itoa(al.Year)) - g.albumID = al.ID - g.artistID = al.ArtistID +func (g *GridViewItem) Update(model GridViewItemModel) { + g.itemID = model.ID + g.secondaryID = model.SecondaryID + g.primaryText.SetText(model.Name) + g.secondaryText.SetText(model.Secondary) + g.secondaryText.Disabled = model.SecondaryID == "" + g.secondaryText.Refresh() g.Cover.playbtn.Hidden = true } -func (g *GridViewCard) ItemID() string { - return g.albumID +func (g *GridViewItem) ItemID() string { + return g.itemID } -func (g *GridViewCard) SecondaryID() string { - return g.artistID +func (g *GridViewItem) SecondaryID() string { + return g.secondaryID } -func (g *GridViewCard) CreateRenderer() fyne.WidgetRenderer { +func (g *GridViewItem) CreateRenderer() fyne.WidgetRenderer { return widget.NewSimpleRenderer(g.container) }