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
-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)
}