refactor 1: renaming

This commit is contained in:
Drew Weymouth
2023-04-28 16:50:35 -07:00
parent a9cec173dd
commit cf4540eb32
13 changed files with 479 additions and 473 deletions
+1 -1
View File
@@ -65,7 +65,7 @@ func StartupApp(appName, appVersionTag, configFile, latestReleaseURL string) (*A
a.LibraryManager = NewLibraryManager(a.ServerManager)
a.ImageManager = NewImageManager(a.bgrndCtx, a.ServerManager, configdir.LocalCache(a.appName))
a.LibraryManager.PreCacheCoverFn = func(coverID string) {
_, _ = a.ImageManager.GetAlbumThumbnail(coverID)
_, _ = a.ImageManager.GetCoverThumbnail(coverID)
}
return a, nil
+5 -5
View File
@@ -45,7 +45,7 @@ func NewImageManager(ctx context.Context, s *ServerManager, baseCacheDir string)
return i
}
func (i *ImageManager) GetAlbumThumbnailFromCache(coverID string) (image.Image, bool) {
func (i *ImageManager) GetCoverThumbnailFromCache(coverID string) (image.Image, bool) {
img, err := i.thumbnailCache.GetExtendTTL(coverID, i.thumbnailCache.DefaultTTL)
if err == nil && img != nil {
return img, true
@@ -53,14 +53,14 @@ func (i *ImageManager) GetAlbumThumbnailFromCache(coverID string) (image.Image,
return nil, false
}
func (i *ImageManager) GetAlbumThumbnail(coverID string) (image.Image, error) {
if im, ok := i.GetAlbumThumbnailFromCache(coverID); ok {
func (i *ImageManager) GetCoverThumbnail(coverID string) (image.Image, error) {
if im, ok := i.GetCoverThumbnailFromCache(coverID); ok {
return im, nil
}
return i.fetchAndCacheCoverFromDiskOrServer(coverID, i.thumbnailCache.DefaultTTL)
}
func (i *ImageManager) GetAlbumThumbnailWithTTL(coverID string, ttl time.Duration) (image.Image, error) {
func (i *ImageManager) GetCoverThumbnailWithTTL(coverID string, ttl time.Duration) (image.Image, error) {
// in-memory cache
if img, err := i.thumbnailCache.GetWithNewTTL(coverID, ttl); err == nil {
return img, nil
@@ -68,7 +68,7 @@ func (i *ImageManager) GetAlbumThumbnailWithTTL(coverID string, ttl time.Duratio
return i.fetchAndCacheCoverFromDiskOrServer(coverID, ttl)
}
func (i *ImageManager) GetFullSizeAlbumCover(coverID string) (image.Image, error) {
func (i *ImageManager) GetFullSizeCoverArt(coverID string) (image.Image, error) {
if i.cachedFullSizeCoverID == coverID {
return i.cachedFullSizeCover, nil
}
+1 -1
View File
@@ -95,7 +95,7 @@ func (bp *BottomPanel) onSongChange(song *subsonic.Child, _ *subsonic.Child) {
// be in cache for the next song if it's from the same album, or
// if the user navigates to the album page for the track
imgTTLSec := song.Duration + 30
im, _ = bp.ImageManager.GetAlbumThumbnailWithTTL(song.CoverArt, time.Duration(imgTTLSec)*time.Second)
im, _ = bp.ImageManager.GetCoverThumbnailWithTTL(song.CoverArt, time.Duration(imgTTLSec)*time.Second)
}
bp.NowPlaying.Update(song.Title, song.Artist, song.Album, im)
}
+2 -2
View File
@@ -226,7 +226,7 @@ func (a *AlbumPageHeader) Update(album *subsonic.AlbumID3, im *backend.ImageMana
a.Refresh()
go func() {
if cover, err := im.GetAlbumThumbnail(album.CoverArt); err == nil {
if cover, err := im.GetCoverThumbnail(album.CoverArt); err == nil {
a.cover.Image.Image = cover
a.cover.Refresh()
} else {
@@ -244,7 +244,7 @@ func (a *AlbumPageHeader) toggleFavorited() {
}
func (a *AlbumPageHeader) showPopUpCover() {
cover, err := a.page.im.GetFullSizeAlbumCover(a.coverID)
cover, err := a.page.im.GetFullSizeCoverArt(a.coverID)
if err != nil {
log.Printf("error getting full size album cover: %s", err.Error())
return
+12 -12
View File
@@ -24,8 +24,8 @@ type AlbumsPage struct {
pm *backend.PlaybackManager
im *backend.ImageManager
lm *backend.LibraryManager
grid *widgets.AlbumGrid
searchGrid *widgets.AlbumGrid
grid *widgets.GridView
searchGrid *widgets.GridView
searcher *widgets.Searcher
searchText string
titleDisp *widget.RichText
@@ -73,9 +73,9 @@ 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.OnPlayAlbum = a.onPlayAlbum
a.grid.OnShowArtistPage = a.onShowArtistPage
a.grid.OnShowAlbumPage = a.onShowAlbumPage
a.grid.OnPlay = a.onPlayAlbum
a.grid.OnShowSecondaryPage = a.onShowArtistPage
a.grid.OnShowItemPage = a.onShowAlbumPage
a.searcher = widgets.NewSearcher()
a.searcher.OnSearched = a.OnSearched
a.createContainer(false)
@@ -116,13 +116,13 @@ func restoreAlbumsPage(saved *savedAlbumsPage) *AlbumsPage {
a.sortOrder = NewSelect(backend.AlbumSortOrders, nil)
a.sortOrder.Selected = saved.sortOrder
a.sortOrder.OnChanged = a.onSortOrderChanged
a.grid = widgets.NewAlbumGridFromState(saved.gridState)
a.grid = widgets.NewGridViewFromState(saved.gridState)
a.searcher = widgets.NewSearcher()
a.searcher.OnSearched = a.OnSearched
a.searcher.Entry.Text = saved.searchText
a.searchText = saved.searchText
if a.searchText != "" {
a.searchGrid = widgets.NewAlbumGridFromState(saved.searchGridState)
a.searchGrid = widgets.NewGridViewFromState(saved.searchGridState)
}
a.createContainer(saved.searchText != "")
@@ -181,9 +181,9 @@ 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.OnPlayAlbum = a.onPlayAlbum
a.searchGrid.OnShowAlbumPage = a.onShowAlbumPage
a.searchGrid.OnShowArtistPage = a.onShowArtistPage
a.searchGrid.OnPlay = a.onPlayAlbum
a.searchGrid.OnShowItemPage = a.onShowAlbumPage
a.searchGrid.OnShowSecondaryPage = a.onShowArtistPage
} else {
a.searchGrid.Reset(a.lm.SearchIter(query))
}
@@ -225,8 +225,8 @@ type savedAlbumsPage struct {
lm *backend.LibraryManager
im *backend.ImageManager
sortOrder string
gridState widgets.AlbumGridState
searchGridState widgets.AlbumGridState
gridState widgets.GridViewState
searchGridState widgets.GridViewState
}
func (s *savedAlbumsPage) Restore() Page {
+3 -3
View File
@@ -39,7 +39,7 @@ type ArtistPage struct {
artistInfo *subsonic.ArtistID3
albumGrid *widgets.AlbumGrid
albumGrid *widgets.GridView
tracklistCtr *fyne.Container
nowPlayingID string
header *ArtistPageHeader
@@ -173,8 +173,8 @@ func (a *ArtistPage) showAlbumGrid() {
return
}
a.albumGrid = widgets.NewFixedAlbumGrid(a.artistInfo.Album, a.im, true /*showYear*/)
a.albumGrid.OnPlayAlbum = a.onPlayAlbum
a.albumGrid.OnShowAlbumPage = a.onShowAlbumPage
a.albumGrid.OnPlay = a.onPlayAlbum
a.albumGrid.OnShowItemPage = a.onShowAlbumPage
}
a.container.Objects[0].(*fyne.Container).Objects[0] = a.albumGrid
a.container.Objects[0].Refresh()
+12 -12
View File
@@ -32,8 +32,8 @@ type FavoritesPage struct {
nowPlayingID string
pendingViewSwitch bool
grid *widgets.AlbumGrid
searchGrid *widgets.AlbumGrid
grid *widgets.GridView
searchGrid *widgets.GridView
artistListCtr *fyne.Container
tracklistCtr *fyne.Container
searcher *widgets.Searcher
@@ -82,9 +82,9 @@ func (a *FavoritesPage) createHeader(activeBtnIdx int, searchText string) {
}
func (a *FavoritesPage) connectGridActions() {
a.grid.OnPlayAlbum = a.onPlayAlbum
a.grid.OnShowAlbumPage = a.onShowAlbumPage
a.grid.OnShowArtistPage = a.onShowArtistPage
a.grid.OnPlay = a.onPlayAlbum
a.grid.OnShowItemPage = a.onShowAlbumPage
a.grid.OnShowSecondaryPage = a.onShowArtistPage
}
func (a *FavoritesPage) createContainer(initialView fyne.CanvasObject) {
@@ -105,11 +105,11 @@ func restoreFavoritesPage(saved *savedFavoritesPage) *FavoritesPage {
}
a.ExtendBaseWidget(a)
a.createHeader(saved.activeToggleBtn, saved.searchText)
a.grid = widgets.NewAlbumGridFromState(saved.gridState)
a.grid = widgets.NewGridViewFromState(saved.gridState)
a.connectGridActions()
if saved.searchText != "" {
a.searchGrid = widgets.NewAlbumGridFromState(saved.searchGridState)
a.searchGrid = widgets.NewGridViewFromState(saved.searchGridState)
}
if saved.activeToggleBtn == 1 {
@@ -221,9 +221,9 @@ func (a *FavoritesPage) doSearchAlbums(query string) {
})
if a.searchGrid == nil {
a.searchGrid = widgets.NewAlbumGrid(iter, a.im, false /*showYear*/)
a.searchGrid.OnPlayAlbum = a.onPlayAlbum
a.searchGrid.OnShowAlbumPage = a.onShowAlbumPage
a.searchGrid.OnShowArtistPage = a.onShowArtistPage
a.searchGrid.OnPlay = a.onPlayAlbum
a.searchGrid.OnShowItemPage = a.onShowAlbumPage
a.searchGrid.OnShowSecondaryPage = a.onShowArtistPage
} else {
a.searchGrid.Reset(iter)
}
@@ -352,8 +352,8 @@ type savedFavoritesPage struct {
sm *backend.ServerManager
im *backend.ImageManager
lm *backend.LibraryManager
gridState widgets.AlbumGridState
searchGridState widgets.AlbumGridState
gridState widgets.GridViewState
searchGridState widgets.GridViewState
searchText string
activeToggleBtn int
}
+12 -12
View File
@@ -24,8 +24,8 @@ type GenrePage struct {
im *backend.ImageManager
pm *backend.PlaybackManager
lm *backend.LibraryManager
grid *widgets.AlbumGrid
searchGrid *widgets.AlbumGrid
grid *widgets.GridView
searchGrid *widgets.GridView
searcher *widgets.Searcher
searchText string
titleDisp *widget.RichText
@@ -53,9 +53,9 @@ 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.OnPlayAlbum = g.onPlayAlbum
g.grid.OnShowArtistPage = g.onShowArtistPage
g.grid.OnShowAlbumPage = g.onShowAlbumPage
g.grid.OnPlay = g.onPlayAlbum
g.grid.OnShowSecondaryPage = g.onShowArtistPage
g.grid.OnShowItemPage = g.onShowAlbumPage
g.searcher = widgets.NewSearcher()
g.searcher.OnSearched = g.OnSearched
g.createContainer(false)
@@ -94,13 +94,13 @@ func restoreGenrePage(saved *savedGenrePage) *GenrePage {
SizeName: theme.SizeNameHeadingText,
}
g.playRandom = widget.NewButtonWithIcon("Play random", res.ResShuffleInvertSvg, g.playRandomSongs)
g.grid = widgets.NewAlbumGridFromState(saved.gridState)
g.grid = widgets.NewGridViewFromState(saved.gridState)
g.searcher = widgets.NewSearcher()
g.searcher.OnSearched = g.OnSearched
g.searcher.Entry.Text = saved.searchText
g.searchText = saved.searchText
if g.searchText != "" {
g.searchGrid = widgets.NewAlbumGridFromState(saved.searchGridState)
g.searchGrid = widgets.NewGridViewFromState(saved.searchGridState)
}
g.createContainer(saved.searchText != "")
@@ -177,9 +177,9 @@ func (g *GenrePage) doSearch(query string) {
})
if g.searchGrid == nil {
g.searchGrid = widgets.NewAlbumGrid(iter, g.im, false /*showYear*/)
g.searchGrid.OnPlayAlbum = g.onPlayAlbum
g.searchGrid.OnShowAlbumPage = g.onShowAlbumPage
g.searchGrid.OnShowArtistPage = g.onShowArtistPage
g.searchGrid.OnPlay = g.onPlayAlbum
g.searchGrid.OnShowItemPage = g.onShowAlbumPage
g.searchGrid.OnShowSecondaryPage = g.onShowArtistPage
} else {
g.searchGrid.Reset(iter)
}
@@ -198,8 +198,8 @@ type savedGenrePage struct {
pm *backend.PlaybackManager
lm *backend.LibraryManager
im *backend.ImageManager
gridState widgets.AlbumGridState
searchGridState widgets.AlbumGridState
gridState widgets.GridViewState
searchGridState widgets.GridViewState
}
func (s *savedGenrePage) Restore() Page {
+2 -2
View File
@@ -233,13 +233,13 @@ func (a *PlaylistPageHeader) Update(playlist *subsonic.Playlist) {
var haveCover bool
if playlist.CoverArt != "" {
if im, err := a.page.im.GetAlbumThumbnail(playlist.CoverArt); err == nil && im != nil {
if im, err := a.page.im.GetCoverThumbnail(playlist.CoverArt); err == nil && im != nil {
a.image.SetImage(im, false /*tappable*/)
haveCover = true
}
}
if !haveCover {
if im, err := a.page.im.GetAlbumThumbnail(playlist.ID); err == nil && im != nil {
if im, err := a.page.im.GetCoverThumbnail(playlist.ID); err == nil && im != nil {
a.image.SetImage(im, false)
}
}
-193
View File
@@ -1,193 +0,0 @@
package widgets
import (
"context"
"image"
"strconv"
"supersonic/res"
"supersonic/ui/layouts"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/canvas"
"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 = (*AlbumCard)(nil)
var _ fyne.Widget = (*albumCover)(nil)
var _ fyne.Tappable = (*albumCover)(nil)
type albumCover struct {
widget.BaseWidget
Im *canvas.Image
playbtn *canvas.Image
OnDoubleTapped func()
OnTapped func()
}
func newAlbumCover() *albumCover {
a := &albumCover{}
a.ExtendBaseWidget(a)
a.Im = &canvas.Image{FillMode: canvas.ImageFillContain, ScaleMode: canvas.ImageScaleFastest}
a.Im.SetMinSize(fyne.NewSize(200, 200))
a.playbtn = &canvas.Image{FillMode: canvas.ImageFillContain, Resource: res.ResPlaybuttonPng}
a.playbtn.SetMinSize(fyne.NewSize(60, 60))
a.playbtn.Hidden = true
return a
}
func (a *albumCover) CreateRenderer() fyne.WidgetRenderer {
return widget.NewSimpleRenderer(
container.NewMax(a.Im, container.NewCenter(a.playbtn)),
)
}
func (a *albumCover) Cursor() desktop.Cursor {
return desktop.PointerCursor
}
func (a *albumCover) Tapped(e *fyne.PointEvent) {
if isInside(a.center(), a.playbtn.Size().Height/2, e.Position) {
if a.OnDoubleTapped != nil {
a.OnDoubleTapped()
}
return
}
if a.OnTapped != nil {
a.OnTapped()
}
}
func (a *albumCover) MouseIn(*desktop.MouseEvent) {
a.playbtn.Hidden = false
a.Refresh()
}
func (a *albumCover) MouseOut() {
a.playbtn.Hidden = true
a.Refresh()
}
// TODO: figure out why circle around play button isn't being displayed
func (a *albumCover) MouseMoved(e *desktop.MouseEvent) {
if isInside(a.center(), a.playbtn.MinSize().Height/2, e.Position) {
a.playbtn.SetMinSize(fyne.NewSize(65, 65))
} else {
a.playbtn.SetMinSize(fyne.NewSize(60, 60))
}
a.Refresh()
}
func (a *albumCover) center() fyne.Position {
return fyne.NewPos(a.Size().Width/2, a.Size().Height/2)
}
func (a *albumCover) SetImage(im image.Image) {
a.Im.Resource = nil
a.Im.Image = im
a.Refresh()
}
func (a *albumCover) SetImageResource(res *fyne.StaticResource) {
a.Im.Image = nil
a.Im.Resource = res
a.Refresh()
}
func isInside(origin fyne.Position, radius float32, point fyne.Position) bool {
x, y := (point.X - origin.X), (point.Y - origin.Y)
return x*x+y*y <= radius*radius
}
type AlbumCard struct {
widget.BaseWidget
albumID string
artistID string
title *CustomHyperlink
artist *CustomHyperlink
year *widget.Label
container *fyne.Container
showYear bool
// updated by AlbumGrid
Cover *albumCover
// these fields are used by AlbumGrid to track async update tasks
PrevAlbumID string
ImgLoadCancel context.CancelFunc
OnPlay func()
OnShowAlbumPage func()
OnShowArtistPage func()
}
func NewAlbumCard(showYear bool) *AlbumCard {
a := &AlbumCard{
title: NewCustomHyperlink(),
artist: NewCustomHyperlink(),
year: widget.NewLabel(""),
Cover: newAlbumCover(),
showYear: showYear,
}
a.ExtendBaseWidget(a)
a.Cover.OnDoubleTapped = func() {
if a.OnPlay != nil {
a.OnPlay()
}
}
showAlbumFn := func() {
if a.OnShowAlbumPage != nil {
a.OnShowAlbumPage()
}
}
a.Cover.OnTapped = showAlbumFn
a.title.OnTapped = showAlbumFn
a.artist.OnTapped = func() {
if a.OnShowArtistPage != nil {
a.OnShowArtistPage()
}
}
a.createContainer()
return a
}
func (a *AlbumCard) createContainer() {
var secondLabel fyne.Widget = a.artist
if a.showYear {
secondLabel = a.year
}
info := container.New(&layouts.VboxCustomPadding{ExtraPad: -16}, a.title, secondLabel)
c := container.New(&layouts.VboxCustomPadding{ExtraPad: -5}, a.Cover, info)
pad := &layouts.CenterPadLayout{PadLeftRight: 20, PadTopBottom: 10}
a.container = container.New(pad, c)
}
func (a *AlbumCard) Update(al *subsonic.AlbumID3) {
a.title.SetText(al.Name)
a.artist.SetText(al.Artist)
a.year.SetText(strconv.Itoa(al.Year))
a.albumID = al.ID
a.artistID = al.ArtistID
a.Cover.playbtn.Hidden = true
}
func (a *AlbumCard) AlbumID() string {
return a.albumID
}
func (a *AlbumCard) ArtistID() string {
return a.artistID
}
func (a *AlbumCard) CreateRenderer() fyne.WidgetRenderer {
return widget.NewSimpleRenderer(a.container)
}
-230
View File
@@ -1,230 +0,0 @@
package widgets
import (
"context"
"image"
"log"
"supersonic/backend"
"supersonic/res"
"sync"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/widget"
"github.com/dweymouth/go-subsonic/subsonic"
)
const albumFetchBatchSize = 6
type ImageFetcher interface {
GetAlbumThumbnailFromCache(string) (image.Image, bool)
GetAlbumThumbnail(string) (image.Image, error)
}
type AlbumGrid struct {
widget.BaseWidget
AlbumGridState
grid *widget.GridWrapList
}
type AlbumGridState struct {
albums []*subsonic.AlbumID3
albumsMutex sync.RWMutex
iter *backend.BatchingIterator
highestShown int
fetching bool
done bool
showYear bool
imageFetcher ImageFetcher
OnPlayAlbum func(string)
OnShowAlbumPage func(string)
OnShowArtistPage func(string)
scrollPos float32
}
var _ fyne.Widget = (*AlbumGrid)(nil)
func NewFixedAlbumGrid(albums []*subsonic.AlbumID3, fetch ImageFetcher, showYear bool) *AlbumGrid {
ag := &AlbumGrid{
AlbumGridState: AlbumGridState{
albums: albums,
done: true,
imageFetcher: fetch,
showYear: showYear,
},
}
ag.ExtendBaseWidget(ag)
ag.createGridWrapList()
return ag
}
func NewAlbumGrid(iter backend.AlbumIterator, fetch ImageFetcher, showYear bool) *AlbumGrid {
ag := &AlbumGrid{
AlbumGridState: AlbumGridState{
iter: backend.NewBatchingIterator(iter),
imageFetcher: fetch,
},
}
ag.ExtendBaseWidget(ag)
ag.createGridWrapList()
// fetch initial albums
ag.fetchMoreAlbums(36)
return ag
}
func (ag *AlbumGrid) SaveToState() AlbumGridState {
s := ag.AlbumGridState
s.scrollPos = ag.grid.GetScrollOffset()
return s
}
func NewAlbumGridFromState(state AlbumGridState) *AlbumGrid {
ag := &AlbumGrid{AlbumGridState: state}
ag.ExtendBaseWidget(ag)
ag.createGridWrapList()
ag.Refresh() // needed to initialize the widget
ag.grid.ScrollToOffset(state.scrollPos)
return ag
}
func (ag *AlbumGrid) Clear() {
ag.albumsMutex.Lock()
defer ag.albumsMutex.Unlock()
ag.albums = nil
ag.done = true
}
func (ag *AlbumGrid) Reset(iter backend.AlbumIterator) {
ag.albumsMutex.Lock()
ag.albums = nil
ag.albumsMutex.Unlock()
ag.fetching = false
ag.done = false
ag.highestShown = 0
ag.iter = backend.NewBatchingIterator(iter)
ag.fetchMoreAlbums(36)
}
func (ag *AlbumGrid) createGridWrapList() {
g := widget.NewGridWrapList(
func() int {
return ag.lenAlbums()
},
// create func
func() fyne.CanvasObject {
ac := NewAlbumCard(ag.showYear)
ac.OnPlay = func() {
if ag.OnPlayAlbum != nil {
ag.OnPlayAlbum(ac.AlbumID())
}
}
ac.OnShowArtistPage = func() {
if ag.OnShowArtistPage != nil {
ag.OnShowArtistPage(ac.ArtistID())
}
}
ac.OnShowAlbumPage = func() {
if ag.OnShowAlbumPage != nil {
ag.OnShowAlbumPage(ac.AlbumID())
}
}
return ac
},
// update func
func(itemID int, obj fyne.CanvasObject) {
ac := obj.(*AlbumCard)
ag.doUpdateAlbumCard(itemID, ac)
},
)
ag.grid = g
}
func (ag *AlbumGrid) doUpdateAlbumCard(albumIdx int, ac *AlbumCard) {
if albumIdx > ag.highestShown {
ag.highestShown = albumIdx
}
ag.albumsMutex.RLock()
album := ag.albums[albumIdx]
ag.albumsMutex.RUnlock()
if ac.PrevAlbumID == album.ID {
// nothing to do
return
}
ac.Update(album)
ac.PrevAlbumID = album.ID
// cancel any previous image fetch
if ac.ImgLoadCancel != nil {
ac.ImgLoadCancel()
ac.ImgLoadCancel = nil
}
if img, ok := ag.imageFetcher.GetAlbumThumbnailFromCache(album.CoverArt); ok {
ac.Cover.SetImage(img)
} else {
ac.Cover.SetImageResource(res.ResAlbumplaceholderPng)
// asynchronously fetch cover image
ctx, cancel := context.WithCancel(context.Background())
ac.ImgLoadCancel = cancel
go func(ctx context.Context) {
i, err := ag.imageFetcher.GetAlbumThumbnail(album.CoverArt)
select {
case <-ctx.Done():
return
default:
if err == nil {
ac.Cover.SetImage(i)
} else {
log.Printf("error fetching image: %s", err.Error())
}
}
}(ctx)
}
// if user has scrolled near the bottom, fetch more
if !ag.done && !ag.fetching && albumIdx > ag.lenAlbums()-10 {
ag.fetchMoreAlbums(20)
}
}
func (a *AlbumGrid) lenAlbums() int {
a.albumsMutex.RLock()
defer a.albumsMutex.RUnlock()
return len(a.albums)
}
// fetches at least count more albums
func (a *AlbumGrid) fetchMoreAlbums(count int) {
if a.iter == nil {
a.done = true
}
a.fetching = true
go func() {
// keep repeating the fetch task as long as the user
// has scrolled near the bottom
for !a.done && a.highestShown >= a.lenAlbums()-10 {
n := 0
for !a.done && n < count {
albums := a.iter.NextN(albumFetchBatchSize)
a.albumsMutex.Lock()
a.albums = append(a.albums, albums...)
a.albumsMutex.Unlock()
if len(albums) < albumFetchBatchSize {
a.done = true
}
n += len(albums)
if len(albums) > 0 {
a.Refresh()
}
}
}
a.fetching = false
}()
}
func (a *AlbumGrid) CreateRenderer() fyne.WidgetRenderer {
return widget.NewSimpleRenderer(a.grid)
}
+229
View File
@@ -0,0 +1,229 @@
package widgets
import (
"context"
"image"
"log"
"supersonic/backend"
"supersonic/res"
"sync"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/widget"
"github.com/dweymouth/go-subsonic/subsonic"
)
const albumFetchBatchSize = 6
type ImageFetcher interface {
GetCoverThumbnailFromCache(string) (image.Image, bool)
GetCoverThumbnail(string) (image.Image, error)
}
type GridView struct {
widget.BaseWidget
GridViewState
grid *widget.GridWrapList
}
type GridViewState struct {
items []*subsonic.AlbumID3
itemsMutex sync.RWMutex
iter *backend.BatchingIterator
highestShown int
fetching bool
done bool
showYear bool
imageFetcher ImageFetcher
OnPlay func(string)
OnShowItemPage func(string)
OnShowSecondaryPage func(string)
scrollPos float32
}
var _ fyne.Widget = (*GridView)(nil)
func NewFixedAlbumGrid(albums []*subsonic.AlbumID3, fetch ImageFetcher, showYear bool) *GridView {
g := &GridView{
GridViewState: GridViewState{
items: albums,
done: true,
imageFetcher: fetch,
showYear: showYear,
},
}
g.ExtendBaseWidget(g)
g.createGridWrapList()
return g
}
func NewAlbumGrid(iter backend.AlbumIterator, fetch ImageFetcher, showYear bool) *GridView {
g := &GridView{
GridViewState: GridViewState{
iter: backend.NewBatchingIterator(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 backend.AlbumIterator) {
g.itemsMutex.Lock()
g.items = nil
g.itemsMutex.Unlock()
g.fetching = false
g.done = false
g.highestShown = 0
g.iter = backend.NewBatchingIterator(iter)
g.fetchMoreItems(36)
}
func (g *GridView) createGridWrapList() {
g.grid = widget.NewGridWrapList(
func() int {
return g.lenItems()
},
// create func
func() fyne.CanvasObject {
card := NewGridViewCard(g.showYear)
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.(*GridViewCard)
g.doUpdateItemCard(itemID, ac)
},
)
}
func (g *GridView) doUpdateItemCard(itemIdx int, card *GridViewCard) {
if itemIdx > g.highestShown {
g.highestShown = itemIdx
}
g.itemsMutex.RLock()
album := g.items[itemIdx]
g.itemsMutex.RUnlock()
if card.PrevID == album.ID {
// nothing to do
return
}
card.Update(album)
card.PrevID = album.ID
// cancel any previous image fetch
if card.ImgLoadCancel != nil {
card.ImgLoadCancel()
card.ImgLoadCancel = nil
}
if img, ok := g.imageFetcher.GetCoverThumbnailFromCache(album.CoverArt); 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(album.CoverArt)
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 {
albums := g.iter.NextN(albumFetchBatchSize)
g.itemsMutex.Lock()
g.items = append(g.items, albums...)
g.itemsMutex.Unlock()
if len(albums) < albumFetchBatchSize {
g.done = true
}
n += len(albums)
if len(albums) > 0 {
g.Refresh()
}
}
}
g.fetching = false
}()
}
func (g *GridView) CreateRenderer() fyne.WidgetRenderer {
return widget.NewSimpleRenderer(g.grid)
}
+200
View File
@@ -0,0 +1,200 @@
package widgets
import (
"context"
"image"
"strconv"
"supersonic/res"
"supersonic/ui/layouts"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/canvas"
"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 = (*coverImage)(nil)
var _ fyne.Tappable = (*coverImage)(nil)
type coverImage struct {
widget.BaseWidget
Im *canvas.Image
playbtn *canvas.Image
OnDoubleTapped func()
OnTapped func()
}
func newCoverImage() *coverImage {
c := &coverImage{}
c.ExtendBaseWidget(c)
c.Im = &canvas.Image{FillMode: canvas.ImageFillContain, ScaleMode: canvas.ImageScaleFastest}
c.Im.SetMinSize(fyne.NewSize(200, 200))
c.playbtn = &canvas.Image{FillMode: canvas.ImageFillContain, Resource: res.ResPlaybuttonPng}
c.playbtn.SetMinSize(fyne.NewSize(60, 60))
c.playbtn.Hidden = true
return c
}
func (c *coverImage) CreateRenderer() fyne.WidgetRenderer {
return widget.NewSimpleRenderer(
container.NewMax(c.Im, container.NewCenter(c.playbtn)),
)
}
func (c *coverImage) Cursor() desktop.Cursor {
return desktop.PointerCursor
}
func (c *coverImage) Tapped(e *fyne.PointEvent) {
if isInside(c.center(), c.playbtn.Size().Height/2, e.Position) {
if c.OnDoubleTapped != nil {
c.OnDoubleTapped()
}
return
}
if c.OnTapped != nil {
c.OnTapped()
}
}
func (a *coverImage) MouseIn(*desktop.MouseEvent) {
a.playbtn.Hidden = false
a.Refresh()
}
func (a *coverImage) MouseOut() {
a.playbtn.Hidden = true
a.Refresh()
}
func (a *coverImage) MouseMoved(e *desktop.MouseEvent) {
if isInside(a.center(), a.playbtn.MinSize().Height/2, e.Position) {
a.playbtn.SetMinSize(fyne.NewSize(65, 65))
} else {
a.playbtn.SetMinSize(fyne.NewSize(60, 60))
}
a.Refresh()
}
func (a *coverImage) center() fyne.Position {
return fyne.NewPos(a.Size().Width/2, a.Size().Height/2)
}
func (a *coverImage) SetImage(im image.Image) {
a.Im.Resource = nil
a.Im.Image = im
a.Refresh()
}
func (a *coverImage) SetImageResource(res *fyne.StaticResource) {
a.Im.Image = nil
a.Im.Resource = res
a.Refresh()
}
func isInside(origin fyne.Position, radius float32, point fyne.Position) bool {
x, y := (point.X - origin.X), (point.Y - origin.Y)
return x*x+y*y <= radius*radius
}
type GridViewCardModel struct {
Name string
ID string
CoverArtID string
Secondary string
SecondaryID string
}
type GridViewCard struct {
widget.BaseWidget
albumID string
artistID string
title *CustomHyperlink
artist *CustomHyperlink
year *widget.Label
container *fyne.Container
showYear bool
// updated by GridView
Cover *coverImage
// these fields are used by GridView to track async update tasks
PrevID string
ImgLoadCancel context.CancelFunc
OnPlay func()
OnShowItemPage func()
OnShowSecondaryPage func()
}
func NewGridViewCard(showYear bool) *GridViewCard {
g := &GridViewCard{
title: NewCustomHyperlink(),
artist: NewCustomHyperlink(),
year: widget.NewLabel(""),
Cover: newCoverImage(),
showYear: showYear,
}
g.ExtendBaseWidget(g)
g.Cover.OnDoubleTapped = func() {
if g.OnPlay != nil {
g.OnPlay()
}
}
showItemFn := func() {
if g.OnShowItemPage != nil {
g.OnShowItemPage()
}
}
g.Cover.OnTapped = showItemFn
g.title.OnTapped = showItemFn
g.artist.OnTapped = func() {
if g.OnShowSecondaryPage != nil {
g.OnShowSecondaryPage()
}
}
g.createContainer()
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)
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
g.Cover.playbtn.Hidden = true
}
func (g *GridViewCard) ItemID() string {
return g.albumID
}
func (g *GridViewCard) SecondaryID() string {
return g.artistID
}
func (g *GridViewCard) CreateRenderer() fyne.WidgetRenderer {
return widget.NewSimpleRenderer(g.container)
}