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:
@@ -223,6 +223,8 @@ func (a *ArtistPage) getGridViewAlbumsModel() []widgets.GridViewItemModel {
|
|||||||
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,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -390,6 +390,8 @@ func buildArtistGridViewModel(artists []*mediaprovider.Artist) []widgets.GridVie
|
|||||||
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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
}
|
||||||
+11
-1
@@ -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)
|
||||||
@@ -88,6 +90,8 @@ func (g gridViewArtistIterator) NextN(n int) []GridViewItemModel {
|
|||||||
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
|
||||||
|
|||||||
+185
-16
@@ -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,11 +143,19 @@ 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 {
|
||||||
|
c.IsFavorite = !c.IsFavorite
|
||||||
|
c.updateFavoriteIcon(true)
|
||||||
|
c.OnFavorite(c.IsFavorite)
|
||||||
}
|
}
|
||||||
|
} else if c.mouseInsideMore {
|
||||||
|
c.TappedSecondary(e)
|
||||||
|
} else {
|
||||||
if c.OnShowPage != nil {
|
if c.OnShowPage != nil {
|
||||||
c.OnShowPage()
|
c.OnShowPage()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *coverImage) TappedSecondary(e *fyne.PointEvent) {
|
func (c *coverImage) TappedSecondary(e *fyne.PointEvent) {
|
||||||
@@ -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()
|
|
||||||
}
|
|
||||||
a.mouseInsideBtn = true
|
|
||||||
} else {
|
} 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)
|
||||||
|
|||||||
Reference in New Issue
Block a user