Merge pull request #517 from dweymouth/feat/gridview-iconbuttons

Add favorite + menu icon-buttons to bottom right of album/artist grid cards when hovered
This commit is contained in:
Drew Weymouth
2024-12-29 07:52:09 -08:00
committed by GitHub
7 changed files with 552 additions and 35 deletions
+6 -4
View File
@@ -219,10 +219,12 @@ func (a *ArtistPage) getGridViewAlbumsModel() []widgets.GridViewItemModel {
sort.Slice(a.artistInfo.Albums, sortFunc) sort.Slice(a.artistInfo.Albums, sortFunc)
return sharedutil.MapSlice(a.artistInfo.Albums, func(al *mediaprovider.Album) widgets.GridViewItemModel { return sharedutil.MapSlice(a.artistInfo.Albums, func(al *mediaprovider.Album) widgets.GridViewItemModel {
return widgets.GridViewItemModel{ return widgets.GridViewItemModel{
Name: al.Name, Name: al.Name,
ID: al.ID, ID: al.ID,
CoverArtID: al.CoverArtID, CoverArtID: al.CoverArtID,
Secondary: []string{strconv.Itoa(al.YearOrZero())}, Secondary: []string{strconv.Itoa(al.YearOrZero())},
CanFavorite: true,
IsFavorite: al.Favorite,
} }
}) })
} }
+8 -6
View File
@@ -104,7 +104,7 @@ func (a *FavoritesPage) createHeader(activeBtnIdx int) {
a.shuffleBtn.Hidden = activeBtnIdx != 2 /*favorite songs*/ a.shuffleBtn.Hidden = activeBtnIdx != 2 /*favorite songs*/
a.searcher = widgets.NewSearchEntry() a.searcher = widgets.NewSearchEntry()
a.searcher.PlaceHolder = lang.L("Search page") a.searcher.PlaceHolder = lang.L("Search page")
a.searcher.OnSearched = a.OnSearched a.searcher.OnSearched = a.onSearched
a.searcher.Entry.Text = a.searchText a.searcher.Entry.Text = a.searchText
a.filterBtn = widgets.NewAlbumFilterButton(a.filter, a.mp.GetGenres) a.filterBtn = widgets.NewAlbumFilterButton(a.filter, a.mp.GetGenres)
a.filterBtn.FavoriteDisabled = true a.filterBtn.FavoriteDisabled = true
@@ -267,7 +267,7 @@ func (a *FavoritesPage) SearchWidget() fyne.Focusable {
return a.searcher return a.searcher
} }
func (a *FavoritesPage) OnSearched(query string) { func (a *FavoritesPage) onSearched(query string) {
if query == "" { if query == "" {
a.albumGrid.ResetFromState(a.gridState) a.albumGrid.ResetFromState(a.gridState)
a.searchGridState = nil a.searchGridState = nil
@@ -386,10 +386,12 @@ func buildArtistGridViewModel(artists []*mediaprovider.Artist) []widgets.GridVie
albums = lang.L("album") albums = lang.L("album")
} }
model = append(model, widgets.GridViewItemModel{ model = append(model, widgets.GridViewItemModel{
ID: ar.ID, ID: ar.ID,
CoverArtID: ar.CoverArtID, CoverArtID: ar.CoverArtID,
Name: ar.Name, Name: ar.Name,
Secondary: []string{fmt.Sprintf("%d %s", ar.AlbumCount, albums)}, Secondary: []string{fmt.Sprintf("%d %s", ar.AlbumCount, albums)},
CanFavorite: true,
IsFavorite: ar.Favorite,
}) })
} }
return model return model
+10
View File
@@ -84,6 +84,11 @@ func (m *Controller) ConnectAlbumGridActions(grid *widgets.GridView) {
grid.OnPlay = func(albumID string, shuffle bool) { grid.OnPlay = func(albumID string, shuffle bool) {
go m.App.PlaybackManager.PlayAlbum(albumID, 0, shuffle) go m.App.PlaybackManager.PlayAlbum(albumID, 0, shuffle)
} }
grid.OnFavorite = func(albumID string, favorite bool) {
m.App.ServerManager.Server.SetFavorite(mediaprovider.RatingFavoriteParameters{
AlbumIDs: []string{albumID},
}, favorite)
}
grid.OnShowItemPage = func(albumID string) { grid.OnShowItemPage = func(albumID string) {
m.NavigateTo(AlbumRoute(albumID)) m.NavigateTo(AlbumRoute(albumID))
} }
@@ -128,6 +133,11 @@ func (m *Controller) ConnectArtistGridActions(grid *widgets.GridView) {
go m.DoAddTracksToPlaylistWorkflow( go m.DoAddTracksToPlaylistWorkflow(
sharedutil.TracksToIDs(m.GetArtistTracks(artistID))) sharedutil.TracksToIDs(m.GetArtistTracks(artistID)))
} }
grid.OnFavorite = func(artistID string, favorite bool) {
m.App.ServerManager.Server.SetFavorite(mediaprovider.RatingFavoriteParameters{
ArtistIDs: []string{artistID},
}, favorite)
}
grid.OnDownload = func(artistID string) { grid.OnDownload = func(artistID string) {
go func() { go func() {
tracks := m.GetArtistTracks(artistID) tracks := m.GetArtistTracks(artistID)
+3
View File
@@ -33,6 +33,9 @@ const (
) )
var ( var (
GridViewHoveredIconColor color.Color = color.White
GridViewIconColor color.Color = darkenColor(color.White, 0.2)
AlbumIcon fyne.Resource = theme.NewThemedResource(res.ResDiscSvg) AlbumIcon fyne.Resource = theme.NewThemedResource(res.ResDiscSvg)
ArtistIcon fyne.Resource = theme.NewThemedResource(res.ResPeopleSvg) ArtistIcon fyne.Resource = theme.NewThemedResource(res.ResPeopleSvg)
RadioIcon fyne.Resource = theme.NewThemedResource(res.ResBroadcastSvg) RadioIcon fyne.Resource = theme.NewThemedResource(res.ResBroadcastSvg)
+321
View File
@@ -0,0 +1,321 @@
package util
// Contents of this file come from the fyne internal SVG package
// Once https://github.com/fyne-io/fyne/pull/5345 is available in main,
// this file can be retired, and the ColorizeSVG func can be replaced with
// `canvas.ColorizeSVG`
import (
"bytes"
"encoding/hex"
"encoding/xml"
"fmt"
"image/color"
"io"
"strconv"
)
// ColorizeSVG creates a new SVG from a given one by replacing all fill colors by the given color.
func ColorizeSVG(src []byte, clr color.Color) ([]byte, error) {
rdr := bytes.NewReader(src)
s, err := svgFromXML(rdr)
if err != nil {
return src, fmt.Errorf("could not load SVG, falling back to static content: %v", err)
}
if err := s.replaceFillColor(clr); err != nil {
return src, fmt.Errorf("could not replace fill color, falling back to static content: %v", err)
}
colorized, err := xml.Marshal(s)
if err != nil {
return src, fmt.Errorf("could not marshal svg, falling back to static content: %v", err)
}
return colorized, nil
}
// svg holds the unmarshaled XML from a Scalable Vector Graphic
type svg struct {
XMLName xml.Name `xml:"svg"`
XMLNS string `xml:"xmlns,attr"`
Width string `xml:"width,attr,omitempty"`
Height string `xml:"height,attr,omitempty"`
ViewBox string `xml:"viewBox,attr,omitempty"`
Paths []*pathObj `xml:"path"`
Rects []*rectObj `xml:"rect"`
Circles []*circleObj `xml:"circle"`
Ellipses []*ellipseObj `xml:"ellipse"`
Polygons []*polygonObj `xml:"polygon"`
Groups []*objGroup `xml:"g"`
}
type pathObj struct {
XMLName xml.Name `xml:"path"`
Fill string `xml:"fill,attr,omitempty"`
FillOpacity string `xml:"fill-opacity,attr,omitempty"`
Stroke string `xml:"stroke,attr,omitempty"`
StrokeWidth string `xml:"stroke-width,attr,omitempty"`
StrokeLineCap string `xml:"stroke-linecap,attr,omitempty"`
StrokeLineJoin string `xml:"stroke-linejoin,attr,omitempty"`
StrokeDashArray string `xml:"stroke-dasharray,attr,omitempty"`
D string `xml:"d,attr"`
Transform string `xml:"transform,attr,omitempty"`
}
type rectObj struct {
XMLName xml.Name `xml:"rect"`
Fill string `xml:"fill,attr,omitempty"`
FillOpacity string `xml:"fill-opacity,attr,omitempty"`
Stroke string `xml:"stroke,attr,omitempty"`
StrokeWidth string `xml:"stroke-width,attr,omitempty"`
StrokeLineCap string `xml:"stroke-linecap,attr,omitempty"`
StrokeLineJoin string `xml:"stroke-linejoin,attr,omitempty"`
StrokeDashArray string `xml:"stroke-dasharray,attr,omitempty"`
X string `xml:"x,attr,omitempty"`
Y string `xml:"y,attr,omitempty"`
Width string `xml:"width,attr,omitempty"`
Height string `xml:"height,attr,omitempty"`
Transform string `xml:"transform,attr,omitempty"`
}
type circleObj struct {
XMLName xml.Name `xml:"circle"`
Fill string `xml:"fill,attr,omitempty"`
FillOpacity string `xml:"fill-opacity,attr,omitempty"`
Stroke string `xml:"stroke,attr,omitempty"`
StrokeWidth string `xml:"stroke-width,attr,omitempty"`
StrokeLineCap string `xml:"stroke-linecap,attr,omitempty"`
StrokeLineJoin string `xml:"stroke-linejoin,attr,omitempty"`
StrokeDashArray string `xml:"stroke-dasharray,attr,omitempty"`
CX string `xml:"cx,attr,omitempty"`
CY string `xml:"cy,attr,omitempty"`
R string `xml:"r,attr,omitempty"`
Transform string `xml:"transform,attr,omitempty"`
}
type ellipseObj struct {
XMLName xml.Name `xml:"ellipse"`
Fill string `xml:"fill,attr,omitempty"`
FillOpacity string `xml:"fill-opacity,attr,omitempty"`
Stroke string `xml:"stroke,attr,omitempty"`
StrokeWidth string `xml:"stroke-width,attr,omitempty"`
StrokeLineCap string `xml:"stroke-linecap,attr,omitempty"`
StrokeLineJoin string `xml:"stroke-linejoin,attr,omitempty"`
StrokeDashArray string `xml:"stroke-dasharray,attr,omitempty"`
CX string `xml:"cx,attr,omitempty"`
CY string `xml:"cy,attr,omitempty"`
RX string `xml:"rx,attr,omitempty"`
RY string `xml:"ry,attr,omitempty"`
Transform string `xml:"transform,attr,omitempty"`
}
type polygonObj struct {
XMLName xml.Name `xml:"polygon"`
Fill string `xml:"fill,attr,omitempty"`
FillOpacity string `xml:"fill-opacity,attr,omitempty"`
Stroke string `xml:"stroke,attr,omitempty"`
StrokeWidth string `xml:"stroke-width,attr,omitempty"`
StrokeLineCap string `xml:"stroke-linecap,attr,omitempty"`
StrokeLineJoin string `xml:"stroke-linejoin,attr,omitempty"`
StrokeDashArray string `xml:"stroke-dasharray,attr,omitempty"`
Points string `xml:"points,attr"`
Transform string `xml:"transform,attr,omitempty"`
}
type objGroup struct {
XMLName xml.Name `xml:"g"`
ID string `xml:"id,attr,omitempty"`
Fill string `xml:"fill,attr,omitempty"`
Stroke string `xml:"stroke,attr,omitempty"`
StrokeWidth string `xml:"stroke-width,attr,omitempty"`
StrokeLineCap string `xml:"stroke-linecap,attr,omitempty"`
StrokeLineJoin string `xml:"stroke-linejoin,attr,omitempty"`
StrokeDashArray string `xml:"stroke-dasharray,attr,omitempty"`
Transform string `xml:"transform,attr,omitempty"`
Paths []*pathObj `xml:"path"`
Circles []*circleObj `xml:"circle"`
Ellipses []*ellipseObj `xml:"ellipse"`
Rects []*rectObj `xml:"rect"`
Polygons []*polygonObj `xml:"polygon"`
Groups []*objGroup `xml:"g"`
}
func replacePathsFill(paths []*pathObj, hexColor string, opacity string) {
for _, path := range paths {
if path.Fill != "none" {
path.Fill = hexColor
path.FillOpacity = opacity
}
}
}
func replaceRectsFill(rects []*rectObj, hexColor string, opacity string) {
for _, rect := range rects {
if rect.Fill != "none" {
rect.Fill = hexColor
rect.FillOpacity = opacity
}
}
}
func replaceCirclesFill(circles []*circleObj, hexColor string, opacity string) {
for _, circle := range circles {
if circle.Fill != "none" {
circle.Fill = hexColor
circle.FillOpacity = opacity
}
}
}
func replaceEllipsesFill(ellipses []*ellipseObj, hexColor string, opacity string) {
for _, ellipse := range ellipses {
if ellipse.Fill != "none" {
ellipse.Fill = hexColor
ellipse.FillOpacity = opacity
}
}
}
func replacePolygonsFill(polys []*polygonObj, hexColor string, opacity string) {
for _, poly := range polys {
if poly.Fill != "none" {
poly.Fill = hexColor
poly.FillOpacity = opacity
}
}
}
func replaceGroupObjectFill(groups []*objGroup, hexColor string, opacity string) {
for _, grp := range groups {
replaceCirclesFill(grp.Circles, hexColor, opacity)
replaceEllipsesFill(grp.Ellipses, hexColor, opacity)
replacePathsFill(grp.Paths, hexColor, opacity)
replaceRectsFill(grp.Rects, hexColor, opacity)
replacePolygonsFill(grp.Polygons, hexColor, opacity)
replaceGroupObjectFill(grp.Groups, hexColor, opacity)
}
}
// replaceFillColor alters an svg objects fill color. Note that if an svg with multiple fill
// colors is being operated upon, all fills will be converted to a single color. Mostly used
// to recolor Icons to match the theme's IconColor.
func (s *svg) replaceFillColor(color color.Color) error {
hexColor, opacity := colorToHexAndOpacity(color)
replacePathsFill(s.Paths, hexColor, opacity)
replaceRectsFill(s.Rects, hexColor, opacity)
replaceCirclesFill(s.Circles, hexColor, opacity)
replaceEllipsesFill(s.Ellipses, hexColor, opacity)
replacePolygonsFill(s.Polygons, hexColor, opacity)
replaceGroupObjectFill(s.Groups, hexColor, opacity)
return nil
}
func svgFromXML(reader io.Reader) (*svg, error) {
var s svg
bSlice, err := io.ReadAll(reader)
if err != nil {
return nil, err
}
if err := xml.Unmarshal(bSlice, &s); err != nil {
return nil, err
}
return &s, nil
}
func colorToHexAndOpacity(color color.Color) (hexStr, aStr string) {
r, g, b, a := toNRGBA(color)
cBytes := []byte{byte(r), byte(g), byte(b)}
hexStr, aStr = "#"+hex.EncodeToString(cBytes), strconv.FormatFloat(float64(a)/0xff, 'f', 6, 64)
return
}
// toNRGBA converts a color to RGBA values which are not premultiplied, unlike color.RGBA().
func toNRGBA(c color.Color) (r, g, b, a int) {
// We use UnmultiplyAlpha with RGBA, RGBA64, and unrecognized implementations of Color.
// It works for all Colors whose RGBA() method is implemented according to spec, but is only necessary for those.
// Only RGBA and RGBA64 have components which are already premultiplied.
switch col := c.(type) {
// NRGBA and NRGBA64 are not premultiplied
case color.NRGBA:
r = int(col.R)
g = int(col.G)
b = int(col.B)
a = int(col.A)
case *color.NRGBA:
r = int(col.R)
g = int(col.G)
b = int(col.B)
a = int(col.A)
case color.NRGBA64:
r = int(col.R) >> 8
g = int(col.G) >> 8
b = int(col.B) >> 8
a = int(col.A) >> 8
case *color.NRGBA64:
r = int(col.R) >> 8
g = int(col.G) >> 8
b = int(col.B) >> 8
a = int(col.A) >> 8
// Gray and Gray16 have no alpha component
case *color.Gray:
r = int(col.Y)
g = int(col.Y)
b = int(col.Y)
a = 0xff
case color.Gray:
r = int(col.Y)
g = int(col.Y)
b = int(col.Y)
a = 0xff
case *color.Gray16:
r = int(col.Y) >> 8
g = int(col.Y) >> 8
b = int(col.Y) >> 8
a = 0xff
case color.Gray16:
r = int(col.Y) >> 8
g = int(col.Y) >> 8
b = int(col.Y) >> 8
a = 0xff
// Alpha and Alpha16 contain only an alpha component.
case color.Alpha:
r = 0xff
g = 0xff
b = 0xff
a = int(col.A)
case *color.Alpha:
r = 0xff
g = 0xff
b = 0xff
a = int(col.A)
case color.Alpha16:
r = 0xff
g = 0xff
b = 0xff
a = int(col.A) >> 8
case *color.Alpha16:
r = 0xff
g = 0xff
b = 0xff
a = int(col.A) >> 8
default: // RGBA, RGBA64, and unknown implementations of Color
r, g, b, a = unmultiplyAlpha(c)
}
return
}
// unmultiplyAlpha returns a color's RGBA components as 8-bit integers by calling c.RGBA() and then removing the alpha premultiplication.
// It is only used by ToRGBA.
func unmultiplyAlpha(c color.Color) (r, g, b, a int) {
red, green, blue, alpha := c.RGBA()
if alpha != 0 && alpha != 0xffff {
red = (red * 0xffff) / alpha
green = (green * 0xffff) / alpha
blue = (blue * 0xffff) / alpha
}
// Convert from range 0-65535 to range 0-255
r = int(red >> 8)
g = int(green >> 8)
b = int(blue >> 8)
a = int(alpha >> 8)
return
}
+15 -5
View File
@@ -60,6 +60,8 @@ func (g gridViewAlbumIterator) NextN(n int) []GridViewItemModel {
CoverArtID: al.CoverArtID, CoverArtID: al.CoverArtID,
Secondary: al.ArtistNames, Secondary: al.ArtistNames,
SecondaryIDs: al.ArtistIDs, SecondaryIDs: al.ArtistIDs,
CanFavorite: true,
IsFavorite: al.Favorite,
} }
if y := al.Date.Year; y != nil { if y := al.Date.Year; y != nil {
model.Suffix = strconv.Itoa(*al.Date.Year) model.Suffix = strconv.Itoa(*al.Date.Year)
@@ -84,10 +86,12 @@ func (g gridViewArtistIterator) NextN(n int) []GridViewItemModel {
albumsLabel = lang.L("album") albumsLabel = lang.L("album")
} }
return GridViewItemModel{ return GridViewItemModel{
Name: ar.Name, Name: ar.Name,
ID: ar.ID, ID: ar.ID,
CoverArtID: ar.CoverArtID, CoverArtID: ar.CoverArtID,
Secondary: []string{fmt.Sprintf("%d %s", ar.AlbumCount, albumsLabel)}, Secondary: []string{fmt.Sprintf("%d %s", ar.AlbumCount, albumsLabel)},
CanFavorite: true,
IsFavorite: ar.Favorite,
} }
}) })
} }
@@ -129,6 +133,7 @@ type GridViewState struct {
OnPlayNext func(id string) OnPlayNext func(id string)
OnAddToQueue func(id string) OnAddToQueue func(id string)
OnAddToPlaylist func(id string) OnAddToPlaylist func(id string)
OnFavorite func(id string, fav bool)
OnDownload func(id string) OnDownload func(id string)
OnShare func(id string) OnShare func(id string)
OnShowItemPage func(id string) OnShowItemPage func(id string)
@@ -276,6 +281,11 @@ func (g *GridView) createNewItemCard() fyne.CanvasObject {
card.ImgLoader = util.NewThumbnailLoader(g.imageFetcher, card.Cover.SetImage) card.ImgLoader = util.NewThumbnailLoader(g.imageFetcher, card.Cover.SetImage)
card.ImgLoader.OnBeforeLoad = func() { card.Cover.SetImage(nil) } card.ImgLoader.OnBeforeLoad = func() { card.Cover.SetImage(nil) }
card.OnPlay = func() { g.onPlay(card.ItemID(), false) } card.OnPlay = func() { g.onPlay(card.ItemID(), false) }
card.OnFavorite = func(fav bool) {
if g.OnFavorite != nil {
g.OnFavorite(card.itemID, fav)
}
}
card.OnShowSecondaryPage = func(id string) { card.OnShowSecondaryPage = func(id string) {
if g.OnShowSecondaryPage != nil { if g.OnShowSecondaryPage != nil {
g.OnShowSecondaryPage(id) g.OnShowSecondaryPage(id)
@@ -338,7 +348,7 @@ func (g *GridView) doUpdateItemCard(itemIdx int, card *GridViewItem) {
} }
card.Cover.Im.PlaceholderIcon = g.Placeholder card.Cover.Im.PlaceholderIcon = g.Placeholder
g.stateMutex.Unlock() g.stateMutex.Unlock()
card.Update(item) card.Update(&item)
card.ImgLoader.Load(item.CoverArtID) card.ImgLoader.Load(item.CoverArtID)
// if user has scrolled near the bottom, fetch more // if user has scrolled near the bottom, fetch more
+189 -20
View File
@@ -27,20 +27,65 @@ var _ fyne.Widget = (*coverImage)(nil)
type coverImage struct { type coverImage struct {
widget.BaseWidget widget.BaseWidget
Im *ImagePlaceholder EnableFavorite bool
playbtn *canvas.Image IsFavorite bool
mouseInsideBtn bool
OnPlay func() OnPlay func()
OnFavorite func(bool)
OnShowPage func() OnShowPage func()
OnShowContextMenu func(fyne.Position) OnShowContextMenu func(fyne.Position)
Im *ImagePlaceholder
playbtn *canvas.Image
favoriteButton *canvas.Image
moreButton *canvas.Image
prevTheme fyne.ThemeVariant
bottomPanel *fyne.Container
mouseInsidePlay bool
mouseInsideFav bool
mouseInsideMore bool
} }
var ( var (
playBtnSize = fyne.NewSize(60, 60) playBtnSize = fyne.NewSize(60, 60)
playBtnHoveredSize = fyne.NewSize(65, 65) playBtnHoveredSize = fyne.NewSize(65, 65)
resourcesInitted bool
heartFilledResource fyne.Resource
heartFilledHoveredResource fyne.Resource
heartUnfilledResource fyne.Resource
heartUnfilledHoveredResource fyne.Resource
moreVerticalResource fyne.Resource
moreVerticalHoveredResource fyne.Resource
inlineIconSize float32
) )
func initResources() {
if resourcesInitted {
return
}
resourcesInitted = true
inlineIconSize = fyne.CurrentApp().Settings().Theme().Size(theme.SizeNameInlineIcon)
// TODO: replace util.ColorizeSVG with Fyne's canvas.ColorizeSVG once
// https://github.com/fyne-io/fyne/pull/5345 is available in Fyne
c, _ := util.ColorizeSVG(myTheme.NotFavoriteIcon.Content(), myTheme.GridViewIconColor)
heartUnfilledResource = fyne.NewStaticResource("gridviewnotfavorite", c)
c, _ = util.ColorizeSVG(myTheme.NotFavoriteIcon.Content(), myTheme.GridViewHoveredIconColor)
heartUnfilledHoveredResource = fyne.NewStaticResource("gridviewnotfavorite_hover", c)
c, _ = util.ColorizeSVG(myTheme.FavoriteIcon.Content(), myTheme.GridViewIconColor)
heartFilledResource = fyne.NewStaticResource("gridviewfavorite", c)
c, _ = util.ColorizeSVG(myTheme.FavoriteIcon.Content(), myTheme.GridViewHoveredIconColor)
heartFilledHoveredResource = fyne.NewStaticResource("gridviewfavorite_hover", c)
c, _ = util.ColorizeSVG(theme.MoreVerticalIcon().Content(), myTheme.GridViewIconColor)
moreVerticalResource = fyne.NewStaticResource("gridviewmore", c)
c, _ = util.ColorizeSVG(theme.MoreVerticalIcon().Content(), myTheme.GridViewHoveredIconColor)
moreVerticalHoveredResource = fyne.NewStaticResource("gridviewmore_hover", c)
}
func newCoverImage(placeholderResource fyne.Resource) *coverImage { func newCoverImage(placeholderResource fyne.Resource) *coverImage {
initResources()
c := &coverImage{} c := &coverImage{}
c.Im = NewImagePlaceholder(placeholderResource, 200) c.Im = NewImagePlaceholder(placeholderResource, 200)
c.Im.OnTapped = c.Tapped c.Im.OnTapped = c.Tapped
@@ -49,13 +94,43 @@ func newCoverImage(placeholderResource fyne.Resource) *coverImage {
c.playbtn = &canvas.Image{FillMode: canvas.ImageFillContain, Resource: res.ResPlaybuttonPng} c.playbtn = &canvas.Image{FillMode: canvas.ImageFillContain, Resource: res.ResPlaybuttonPng}
c.playbtn.SetMinSize(playBtnSize) c.playbtn.SetMinSize(playBtnSize)
c.playbtn.Hidden = true c.playbtn.Hidden = true
c.favoriteButton = canvas.NewImageFromResource(heartUnfilledResource)
c.favoriteButton.SetMinSize(fyne.NewSquareSize(inlineIconSize))
c.moreButton = canvas.NewImageFromResource(moreVerticalResource)
c.moreButton.SetMinSize(fyne.NewSquareSize(inlineIconSize))
c.bottomPanel = container.NewStack(
canvas.NewVerticalGradient(color.Transparent, color.Black),
container.NewVBox(
layout.NewSpacer(), // keep the HBox pushed down
container.NewHBox(
layout.NewSpacer(),
c.favoriteButton,
c.moreButton,
util.NewHSpace(0),
),
container.New(
layout.NewCustomPaddedLayout(0, theme.Padding()*2, 0, 0),
layout.NewSpacer(),
),
),
)
c.bottomPanel.Hidden = true
c.ExtendBaseWidget(c) c.ExtendBaseWidget(c)
return c return c
} }
func (c *coverImage) CreateRenderer() fyne.WidgetRenderer { func (c *coverImage) CreateRenderer() fyne.WidgetRenderer {
return widget.NewSimpleRenderer( return widget.NewSimpleRenderer(
container.NewStack(c.Im, container.NewCenter(c.playbtn)), container.NewStack(
c.Im,
container.NewGridWithRows(2,
layout.NewSpacer(),
c.bottomPanel,
),
container.NewCenter(c.playbtn),
),
) )
} }
@@ -68,10 +143,18 @@ func (c *coverImage) Tapped(e *fyne.PointEvent) {
if c.OnPlay != nil { if c.OnPlay != nil {
c.OnPlay() c.OnPlay()
} }
return } else if c.mouseInsideFav {
} if c.OnFavorite != nil {
if c.OnShowPage != nil { c.IsFavorite = !c.IsFavorite
c.OnShowPage() c.updateFavoriteIcon(true)
c.OnFavorite(c.IsFavorite)
}
} else if c.mouseInsideMore {
c.TappedSecondary(e)
} else {
if c.OnShowPage != nil {
c.OnShowPage()
}
} }
} }
@@ -83,28 +166,100 @@ func (c *coverImage) TappedSecondary(e *fyne.PointEvent) {
func (a *coverImage) MouseIn(*desktop.MouseEvent) { func (a *coverImage) MouseIn(*desktop.MouseEvent) {
a.playbtn.Hidden = false a.playbtn.Hidden = false
a.updateFavoriteIcon(false)
a.favoriteButton.Hidden = !a.EnableFavorite
a.bottomPanel.Hidden = false
a.Refresh() a.Refresh()
} }
func (a *coverImage) MouseOut() { func (a *coverImage) MouseOut() {
a.mouseInsideBtn = false a.mouseInsidePlay = false
a.mouseInsideFav = false
a.mouseInsideMore = false
a.playbtn.Hidden = true a.playbtn.Hidden = true
a.bottomPanel.Hidden = true
a.Refresh() a.Refresh()
} }
func (a *coverImage) MouseMoved(e *desktop.MouseEvent) { func (a *coverImage) MouseMoved(e *desktop.MouseEvent) {
if isInside(a.center(), a.playbtn.MinSize().Height/2, e.Position) { updateMouseInsidePlay := func(in bool) {
if !a.mouseInsideBtn { if in == a.mouseInsidePlay {
return
}
if in {
a.playbtn.SetMinSize(playBtnHoveredSize) a.playbtn.SetMinSize(playBtnHoveredSize)
a.playbtn.Refresh() } else {
}
a.mouseInsideBtn = true
} else {
if a.mouseInsideBtn {
a.playbtn.SetMinSize(playBtnSize) a.playbtn.SetMinSize(playBtnSize)
a.playbtn.Refresh()
} }
a.mouseInsideBtn = false a.playbtn.Refresh()
a.mouseInsidePlay = in
}
updateMouseInsideFav := func(in bool) {
if in == a.mouseInsideFav {
return
}
if a.IsFavorite {
if in {
a.favoriteButton.Resource = heartFilledHoveredResource
} else {
a.favoriteButton.Resource = heartFilledResource
}
} else {
if in {
a.favoriteButton.Resource = heartUnfilledHoveredResource
} else {
a.favoriteButton.Resource = heartUnfilledResource
}
}
a.favoriteButton.Refresh()
a.mouseInsideFav = in
}
updateMouseInsideMore := func(in bool) {
if in == a.mouseInsideMore {
return
}
if in {
a.moreButton.Resource = moreVerticalHoveredResource
} else {
a.moreButton.Resource = moreVerticalResource
}
a.moreButton.Refresh()
a.mouseInsideMore = in
}
pad := theme.Padding()
overFavBtn := e.Position.Y > a.Size().Height-inlineIconSize-pad*3 &&
e.Position.X > a.Size().Width-inlineIconSize*2-pad*3 &&
e.Position.X < a.Size().Height-inlineIconSize-pad
overMoreBtn := e.Position.Y > a.Size().Height-inlineIconSize-pad*3 &&
e.Position.X > a.Size().Width-inlineIconSize-pad
if isInside(a.center(), a.playbtn.MinSize().Height/2, e.Position) {
updateMouseInsidePlay(true)
updateMouseInsideFav(false)
updateMouseInsideMore(false)
} else if overFavBtn {
updateMouseInsideFav(true)
updateMouseInsidePlay(false)
updateMouseInsideMore(false)
} else if overMoreBtn {
updateMouseInsideMore(true)
updateMouseInsideFav(false)
updateMouseInsidePlay(false)
} else {
updateMouseInsideFav(false)
updateMouseInsidePlay(false)
updateMouseInsideMore(false)
}
}
func (a *coverImage) updateFavoriteIcon(refresh bool) {
if a.IsFavorite {
a.favoriteButton.Resource = heartFilledResource
} else {
a.favoriteButton.Resource = heartUnfilledResource
}
if refresh {
a.favoriteButton.Refresh()
} }
} }
@@ -118,7 +273,7 @@ func (a *coverImage) SetImage(im image.Image) {
func (a *coverImage) ResetPlayButton() { func (a *coverImage) ResetPlayButton() {
a.playbtn.SetMinSize(playBtnSize) a.playbtn.SetMinSize(playBtnSize)
a.mouseInsideBtn = false a.mouseInsidePlay = false
a.playbtn.Hidden = true a.playbtn.Hidden = true
} }
@@ -134,6 +289,8 @@ type GridViewItemModel struct {
Secondary []string Secondary []string
SecondaryIDs []string SecondaryIDs []string
Suffix string Suffix string
CanFavorite bool
IsFavorite bool
} }
type GridViewItem struct { type GridViewItem struct {
@@ -141,6 +298,7 @@ type GridViewItem struct {
ShowSuffix bool ShowSuffix bool
model *GridViewItemModel
itemID string itemID string
secondaryIDs []string secondaryIDs []string
primaryText *ttwidget.Hyperlink primaryText *ttwidget.Hyperlink
@@ -155,6 +313,7 @@ type GridViewItem struct {
ItemIndex int ItemIndex int
OnPlay func() OnPlay func()
OnFavorite func(bool)
OnShowContextMenu func(fyne.Position) OnShowContextMenu func(fyne.Position)
OnShowItemPage func() OnShowItemPage func()
OnShowSecondaryPage func(string) OnShowSecondaryPage func(string)
@@ -179,6 +338,14 @@ func NewGridViewItem(placeholderResource fyne.Resource) *GridViewItem {
g.OnPlay() g.OnPlay()
} }
} }
g.Cover.OnFavorite = func(fav bool) {
if g.model != nil {
g.model.IsFavorite = fav
}
if g.OnFavorite != nil {
g.OnFavorite(fav)
}
}
g.Cover.OnShowContextMenu = func(pos fyne.Position) { g.Cover.OnShowContextMenu = func(pos fyne.Position) {
if g.OnShowContextMenu != nil { if g.OnShowContextMenu != nil {
g.OnShowContextMenu(pos) g.OnShowContextMenu(pos)
@@ -211,7 +378,9 @@ func (g *GridViewItem) NeedsUpdate(model GridViewItemModel) bool {
(!g.ShowSuffix && g.secondaryText.Suffix != "") (!g.ShowSuffix && g.secondaryText.Suffix != "")
} }
func (g *GridViewItem) Update(model GridViewItemModel) { func (g *GridViewItem) Update(model *GridViewItemModel) {
g.Cover.IsFavorite = model.IsFavorite
g.Cover.EnableFavorite = model.CanFavorite
g.itemID = model.ID g.itemID = model.ID
g.secondaryIDs = model.SecondaryIDs g.secondaryIDs = model.SecondaryIDs
g.primaryText.SetText(model.Name) g.primaryText.SetText(model.Name)