some work on the quick search dialog + image load refactoring

This commit is contained in:
Drew Weymouth
2023-11-05 14:07:52 -08:00
parent 0439ec695b
commit e10b751a4f
4 changed files with 215 additions and 32 deletions
+49
View File
@@ -188,6 +188,55 @@ func (i *ImageManager) RefreshCachedArtistImageIfExpired(artistID string, imgURL
return err
}
// ThumbnailLoader is a utility type that exposes a single API to load
// a cover thumbnail by ID. If the image is immediately available in
// the cache, OnLoaded will be called immediately. If it is not,
// OnBeforeLoad will be called first, then OnLoaded will be called async
// once the image is available.
// Any subsequent calls to Load will cancel the previous load if not yet completed.
type ThumbnailLoader struct {
prevLoadCancel context.CancelFunc
im *ImageManager
OnBeforeLoad func()
OnLoaded func(image.Image)
}
func (i *ImageManager) NewThumbnailLoader(onLoaded func(image.Image)) ThumbnailLoader {
return ThumbnailLoader{im: i, OnLoaded: onLoaded}
}
func (i *ThumbnailLoader) Load(coverID string) {
if i.prevLoadCancel != nil {
i.prevLoadCancel()
}
if coverID == "" {
i.callOnLoaded(nil)
return
}
if img, ok := i.im.GetCoverThumbnailFromCache(coverID); ok {
i.callOnLoaded(img)
return
}
if i.OnBeforeLoad != nil {
i.OnBeforeLoad()
}
i.prevLoadCancel = i.im.GetCoverThumbnailAsync(coverID, func(img image.Image, err error) {
if err != nil {
log.Printf("Error loading cover image: %s", err.Error())
} else {
i.callOnLoaded(img)
}
i.prevLoadCancel() // Done. Release resources associated with un-cancelled ctx
})
}
func (i *ThumbnailLoader) callOnLoaded(im image.Image) {
if i.OnLoaded != nil {
i.OnLoaded(im)
}
}
func (i *ImageManager) ensureCoverCacheDir() string {
// if user logged out with pending fetches in progress,
// make sure we don't write to nil (00000000-*0) cache directory
+154
View File
@@ -0,0 +1,154 @@
package dialogs
import (
"image"
"sync"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/canvas"
"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"
myTheme "github.com/dweymouth/supersonic/ui/theme"
"github.com/dweymouth/supersonic/ui/widgets"
)
type QuickSearch struct {
widget.BaseWidget
mp *mediaprovider.MediaProvider
im *backend.ImageManager
resultsMutex sync.RWMutex
searchResults []*mediaprovider.SearchResult
list *widget.List
selectedIndex int
content *fyne.Container
}
func NewQuickSearch(mp *mediaprovider.MediaProvider, im *backend.ImageManager) *QuickSearch {
q := &QuickSearch{
mp: mp,
im: im,
}
q.ExtendBaseWidget(q)
searchEntry := widgets.NewSearchEntry()
searchEntry.OnChanged = q.onSearched
q.list = widget.NewList(
func() int {
q.resultsMutex.RLock()
defer q.resultsMutex.RUnlock()
return len(q.searchResults)
},
func() fyne.CanvasObject { return newQuickSearchResult(im) },
func(lii widget.ListItemID, co fyne.CanvasObject) {
var result *mediaprovider.SearchResult
q.resultsMutex.RLock()
if len(q.searchResults) > lii {
result = q.searchResults[lii]
}
q.resultsMutex.RUnlock()
co.(*quickSearchResult).Update(result)
},
)
q.content = container.NewVBox(
container.NewHBox(
layout.NewSpacer(),
widget.NewRichText(&widget.TextSegment{Text: "Quick Search", Style: boldStyle}),
layout.NewSpacer()),
container.NewBorder(searchEntry, nil, nil, nil, q.list),
)
return q
}
func (q *QuickSearch) onSearched(query string) {
}
func (q *QuickSearch) CreateRenderer() fyne.WidgetRenderer {
return widget.NewSimpleRenderer(q.content)
}
type quickSearchResult struct {
widget.BaseWidget
// parent *QuickSearch
imageLoader backend.ThumbnailLoader
image *widgets.ImagePlaceholder
title *widget.Label
secondary *widget.RichText
selection *canvas.Rectangle
content *fyne.Container
}
func newQuickSearchResult(im *backend.ImageManager) *quickSearchResult {
qs := &quickSearchResult{
image: widgets.NewImagePlaceholder(myTheme.AlbumIcon, 64),
title: widget.NewLabel(""),
secondary: widget.NewRichText(),
}
qs.ExtendBaseWidget(qs)
qs.imageLoader = im.NewThumbnailLoader(func(im image.Image) {
qs.image.SetImage(im, false)
})
qs.imageLoader.OnBeforeLoad = func() {
qs.image.SetImage(nil, false)
}
return qs
}
func (q *quickSearchResult) Update(result *mediaprovider.SearchResult) {
if result == nil {
return
}
q.image.CenterIcon = placeholderIconForContentType(result.Type)
q.imageLoader.Load(result.CoverID)
q.title.SetText(result.Name)
//TODO: q.secondary.Segments =
}
func (q *quickSearchResult) CreateRenderer() fyne.WidgetRenderer {
if q.selection == nil {
q.selection = canvas.NewRectangle(theme.SelectionColor())
}
if q.content == nil {
q.content = container.NewMax(
q.selection,
container.NewBorder(nil, nil, q.image, nil,
container.NewVBox(
q.title,
q.secondary,
)),
)
}
return widget.NewSimpleRenderer(q.content)
}
func (q *quickSearchResult) Refresh() {
q.BaseWidget.Refresh()
}
func placeholderIconForContentType(c mediaprovider.ContentType) fyne.Resource {
switch c {
case mediaprovider.ContentTypeAlbum:
return myTheme.AlbumIcon
case mediaprovider.ContentTypeArtist:
return myTheme.ArtistIcon
case mediaprovider.ContentTypeTrack:
return myTheme.TracksIcon
case mediaprovider.ContentTypeGenre:
return myTheme.GenreIcon
case mediaprovider.ContentTypePlaylist:
return myTheme.PlaylistIcon
default:
return theme.WarningIcon() // unreached
}
}
+9 -29
View File
@@ -3,9 +3,9 @@ package widgets
import (
"context"
"image"
"log"
"sync"
"github.com/dweymouth/supersonic/backend"
"github.com/dweymouth/supersonic/backend/mediaprovider"
"github.com/dweymouth/supersonic/sharedutil"
@@ -37,9 +37,8 @@ func (b *BatchingIterator) NextN(n int) []*mediaprovider.Album {
return results
}
type ImageFetcher interface {
GetCoverThumbnailFromCache(string) (image.Image, bool)
GetCoverThumbnailAsync(string, func(image.Image, error)) context.CancelFunc
type ThumbnailLoaderFactory interface {
NewThumbnailLoader(func(image.Image)) backend.ThumbnailLoader
}
type GridViewIterator interface {
@@ -82,7 +81,7 @@ type GridView struct {
type GridViewState struct {
items []GridViewItemModel
iter GridViewIterator
imageFetcher ImageFetcher
imageFetcher ThumbnailLoaderFactory
Placeholder fyne.Resource
highestShown int
done bool
@@ -99,7 +98,7 @@ type GridViewState struct {
var _ fyne.Widget = (*GridView)(nil)
func NewFixedGridView(items []GridViewItemModel, fetch ImageFetcher, placeholder fyne.Resource) *GridView {
func NewFixedGridView(items []GridViewItemModel, fetch ThumbnailLoaderFactory, placeholder fyne.Resource) *GridView {
g := &GridView{
GridViewState: GridViewState{
items: items,
@@ -113,7 +112,7 @@ func NewFixedGridView(items []GridViewItemModel, fetch ImageFetcher, placeholder
return g
}
func NewGridView(iter GridViewIterator, fetch ImageFetcher, placeholder fyne.Resource) *GridView {
func NewGridView(iter GridViewIterator, fetch ThumbnailLoaderFactory, placeholder fyne.Resource) *GridView {
g := &GridView{
GridViewState: GridViewState{
iter: iter,
@@ -203,6 +202,8 @@ func (g *GridView) createGridWrap() {
// create func
func() fyne.CanvasObject {
card := NewGridViewItem(g.Placeholder)
card.ImgLoader = g.imageFetcher.NewThumbnailLoader(card.Cover.SetImage)
card.ImgLoader.OnBeforeLoad = func() { card.Cover.SetImage(nil) }
card.OnPlay = func() { g.onPlay(card.ItemID(), false) }
card.OnShowSecondaryPage = func(id string) {
if g.OnShowSecondaryPage != nil {
@@ -245,28 +246,7 @@ func (g *GridView) doUpdateItemCard(itemIdx int, card *GridViewItem) {
return
}
card.Update(item)
// cancel any previous image fetch (no issues with possible double-invocations)
if card.ImgLoadCancel != nil {
card.ImgLoadCancel()
}
if item.CoverArtID != "" {
if img, ok := g.imageFetcher.GetCoverThumbnailFromCache(item.CoverArtID); ok {
card.Cover.SetImage(img)
} else {
card.Cover.SetImage(nil)
card.ImgLoadCancel = g.imageFetcher.GetCoverThumbnailAsync(item.CoverArtID, func(i image.Image, err error) {
if err == nil {
card.Cover.SetImage(i)
} else {
log.Printf("error fetching image: %s", err.Error())
}
card.ImgLoadCancel() // done. release resources associated with cancel channel
})
}
} else {
// use the placeholder for an item that has no cover art ID
card.Cover.SetImage(nil)
}
card.ImgLoader.Load(item.CoverArtID)
// if user has scrolled near the bottom, fetch more
if itemIdx > g.lenItems()-10 {
+3 -3
View File
@@ -1,9 +1,9 @@
package widgets
import (
"context"
"image"
"github.com/dweymouth/supersonic/backend"
"github.com/dweymouth/supersonic/res"
"github.com/dweymouth/supersonic/sharedutil"
"github.com/dweymouth/supersonic/ui/layouts"
@@ -140,8 +140,8 @@ type GridViewItem struct {
container *fyne.Container
// updated by GridView
Cover *coverImage
ImgLoadCancel context.CancelFunc
Cover *coverImage
ImgLoader backend.ThumbnailLoader
OnPlay func()
OnShowContextMenu func(fyne.Position)