Files
supersonic/ui/widgets/gridview.go
T

264 lines
5.5 KiB
Go

package widgets
import (
"context"
"image"
"log"
"supersonic/backend"
"supersonic/res"
"supersonic/sharedutil"
"sync"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/widget"
"github.com/dweymouth/go-subsonic/subsonic"
)
const batchFetchSize = 6
type ImageFetcher interface {
GetCoverThumbnailFromCache(string) (image.Image, bool)
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
GridViewState
grid *widget.GridWrapList
}
type GridViewState struct {
items []GridViewItemModel
itemsMutex sync.RWMutex
iter GridViewIterator
highestShown int
fetching bool
done bool
imageFetcher ImageFetcher
OnPlay func(string)
OnShowItemPage func(string)
OnShowSecondaryPage func(string)
scrollPos float32
}
var _ fyne.Widget = (*GridView)(nil)
func NewFixedGridView(items []GridViewItemModel, fetch ImageFetcher) *GridView {
g := &GridView{
GridViewState: GridViewState{
items: items,
done: true,
imageFetcher: fetch,
},
}
g.ExtendBaseWidget(g)
g.createGridWrapList()
return g
}
func NewGridView(iter GridViewIterator, fetch ImageFetcher) *GridView {
g := &GridView{
GridViewState: GridViewState{
iter: iter,
imageFetcher: fetch,
},
}
g.ExtendBaseWidget(g)
g.createGridWrapList()
// fetch initial items
g.fetchMoreItems(36)
return g
}
func (g *GridView) SaveToState() GridViewState {
s := g.GridViewState
s.scrollPos = g.grid.GetScrollOffset()
return s
}
func NewGridViewFromState(state GridViewState) *GridView {
g := &GridView{GridViewState: state}
g.ExtendBaseWidget(g)
g.createGridWrapList()
g.Refresh() // needed to initialize the widget
g.grid.ScrollToOffset(state.scrollPos)
return g
}
func (g *GridView) Clear() {
g.itemsMutex.Lock()
defer g.itemsMutex.Unlock()
g.items = nil
g.done = true
}
func (g *GridView) Reset(iter GridViewIterator) {
g.itemsMutex.Lock()
g.items = nil
g.itemsMutex.Unlock()
g.fetching = false
g.done = false
g.highestShown = 0
g.iter = iter
g.fetchMoreItems(36)
}
func (g *GridView) ResetFixed(items []GridViewItemModel) {
g.itemsMutex.Lock()
g.items = items
g.itemsMutex.Unlock()
g.fetching = false
g.done = true
g.highestShown = 0
g.iter = nil
}
func (g *GridView) createGridWrapList() {
g.grid = widget.NewGridWrapList(
func() int {
return g.lenItems()
},
// create func
func() fyne.CanvasObject {
card := NewGridViewItem()
card.OnPlay = func() {
if g.OnPlay != nil {
g.OnPlay(card.ItemID())
}
}
card.OnShowSecondaryPage = func() {
if g.OnShowSecondaryPage != nil {
g.OnShowSecondaryPage(card.SecondaryID())
}
}
card.OnShowItemPage = func() {
if g.OnShowItemPage != nil {
g.OnShowItemPage(card.ItemID())
}
}
return card
},
// update func
func(itemID int, obj fyne.CanvasObject) {
ac := obj.(*GridViewItem)
g.doUpdateItemCard(itemID, ac)
},
)
}
func (g *GridView) doUpdateItemCard(itemIdx int, card *GridViewItem) {
if itemIdx > g.highestShown {
g.highestShown = itemIdx
}
g.itemsMutex.RLock()
item := g.items[itemIdx]
g.itemsMutex.RUnlock()
if card.PrevID == item.ID {
// nothing to do
return
}
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(item.CoverArtID); ok {
card.Cover.SetImage(img)
} else {
card.Cover.SetImageResource(res.ResAlbumplaceholderPng)
// asynchronously fetch cover image
ctx, cancel := context.WithCancel(context.Background())
card.ImgLoadCancel = cancel
go func(ctx context.Context) {
i, err := g.imageFetcher.GetCoverThumbnail(item.CoverArtID)
select {
case <-ctx.Done():
return
default:
if err == nil {
card.Cover.SetImage(i)
} else {
log.Printf("error fetching image: %s", err.Error())
}
}
}(ctx)
}
// if user has scrolled near the bottom, fetch more
if !g.done && !g.fetching && itemIdx > g.lenItems()-10 {
g.fetchMoreItems(20)
}
}
func (g *GridView) lenItems() int {
g.itemsMutex.RLock()
defer g.itemsMutex.RUnlock()
return len(g.items)
}
// fetches at least count more items
func (g *GridView) fetchMoreItems(count int) {
if g.iter == nil {
g.done = true
}
g.fetching = true
go func() {
// keep repeating the fetch task as long as the user
// has scrolled near the bottom
for !g.done && g.highestShown >= g.lenItems()-10 {
n := 0
for !g.done && n < count {
items := g.iter.NextN(batchFetchSize)
g.itemsMutex.Lock()
g.items = append(g.items, items...)
g.itemsMutex.Unlock()
if len(items) < batchFetchSize {
g.done = true
}
n += len(items)
if len(items) > 0 {
g.Refresh()
}
}
}
g.fetching = false
}()
}
func (g *GridView) CreateRenderer() fyne.WidgetRenderer {
return widget.NewSimpleRenderer(g.grid)
}