Merge pull request #259 from dweymouth/feature/multi-artist-genre
Add support for OpenSubsonic multi-valued artists and genres
This commit is contained in:
@@ -276,6 +276,18 @@ func toTrack(ch *subsonic.Child) *mediaprovider.Track {
|
|||||||
if ch == nil {
|
if ch == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
var artistNames, artistIDs []string
|
||||||
|
if len(ch.Artists) > 0 {
|
||||||
|
// OpenSubsonic extension
|
||||||
|
for _, a := range ch.Artists {
|
||||||
|
artistIDs = append(artistIDs, a.ID)
|
||||||
|
artistNames = append(artistNames, a.Name)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
artistNames = append(artistNames, ch.Artist)
|
||||||
|
artistIDs = append(artistIDs, ch.ArtistID)
|
||||||
|
}
|
||||||
|
|
||||||
return &mediaprovider.Track{
|
return &mediaprovider.Track{
|
||||||
ID: ch.ID,
|
ID: ch.ID,
|
||||||
CoverArtID: ch.CoverArt,
|
CoverArtID: ch.CoverArt,
|
||||||
@@ -285,8 +297,8 @@ func toTrack(ch *subsonic.Child) *mediaprovider.Track {
|
|||||||
TrackNumber: ch.Track,
|
TrackNumber: ch.Track,
|
||||||
DiscNumber: ch.DiscNumber,
|
DiscNumber: ch.DiscNumber,
|
||||||
Genre: ch.Genre,
|
Genre: ch.Genre,
|
||||||
ArtistIDs: []string{ch.ArtistID},
|
ArtistIDs: artistIDs,
|
||||||
ArtistNames: []string{ch.Artist},
|
ArtistNames: artistNames,
|
||||||
Album: ch.Album,
|
Album: ch.Album,
|
||||||
AlbumID: ch.AlbumID,
|
AlbumID: ch.AlbumID,
|
||||||
Year: ch.Year,
|
Year: ch.Year,
|
||||||
@@ -309,15 +321,37 @@ func toAlbum(al *subsonic.AlbumID3) *mediaprovider.Album {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func fillAlbum(subAlbum *subsonic.AlbumID3, album *mediaprovider.Album) {
|
func fillAlbum(subAlbum *subsonic.AlbumID3, album *mediaprovider.Album) {
|
||||||
|
var artistNames, artistIDs []string
|
||||||
|
if len(subAlbum.Artists) > 0 {
|
||||||
|
// OpenSubsonic extension
|
||||||
|
for _, a := range subAlbum.Artists {
|
||||||
|
artistIDs = append(artistIDs, a.ID)
|
||||||
|
artistNames = append(artistNames, a.Name)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
artistNames = append(artistNames, subAlbum.Artist)
|
||||||
|
artistIDs = append(artistIDs, subAlbum.ArtistID)
|
||||||
|
}
|
||||||
|
|
||||||
|
var genres []string
|
||||||
|
if len(subAlbum.Genres) > 0 {
|
||||||
|
// OpenSubsonic extension
|
||||||
|
for _, g := range subAlbum.Genres {
|
||||||
|
genres = append(genres, g.Name)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
genres = append(genres, subAlbum.Genre)
|
||||||
|
}
|
||||||
|
|
||||||
album.ID = subAlbum.ID
|
album.ID = subAlbum.ID
|
||||||
album.CoverArtID = subAlbum.CoverArt
|
album.CoverArtID = subAlbum.CoverArt
|
||||||
album.Name = subAlbum.Name
|
album.Name = subAlbum.Name
|
||||||
album.Duration = subAlbum.Duration
|
album.Duration = subAlbum.Duration
|
||||||
album.ArtistIDs = []string{subAlbum.ArtistID}
|
album.ArtistIDs = artistIDs
|
||||||
album.ArtistNames = []string{subAlbum.Artist}
|
album.ArtistNames = artistNames
|
||||||
album.Year = subAlbum.Year
|
album.Year = subAlbum.Year
|
||||||
album.TrackCount = subAlbum.SongCount
|
album.TrackCount = subAlbum.SongCount
|
||||||
album.Genres = []string{subAlbum.Genre}
|
album.Genres = genres
|
||||||
album.Favorite = !subAlbum.Starred.IsZero()
|
album.Favorite = !subAlbum.Starred.IsZero()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ require (
|
|||||||
fyne.io/x/fyne v0.0.0-20230611151101-afdcd6b92cf3
|
fyne.io/x/fyne v0.0.0-20230611151101-afdcd6b92cf3
|
||||||
github.com/20after4/configdir v0.1.1
|
github.com/20after4/configdir v0.1.1
|
||||||
github.com/dweymouth/go-mpv v0.0.0-20230406003141-7f1858e503ee
|
github.com/dweymouth/go-mpv v0.0.0-20230406003141-7f1858e503ee
|
||||||
github.com/dweymouth/go-subsonic v0.0.0-20231013011542-14b66c5a1fff
|
github.com/dweymouth/go-subsonic v0.0.0-20231014003317-6a0c88db38ce
|
||||||
github.com/fsnotify/fsnotify v1.6.0
|
github.com/fsnotify/fsnotify v1.6.0
|
||||||
github.com/godbus/dbus/v5 v5.1.0
|
github.com/godbus/dbus/v5 v5.1.0
|
||||||
github.com/google/uuid v1.3.0
|
github.com/google/uuid v1.3.0
|
||||||
@@ -47,4 +47,4 @@ require (
|
|||||||
honnef.co/go/js/dom v0.0.0-20210725211120-f030747120f2 // indirect
|
honnef.co/go/js/dom v0.0.0-20210725211120-f030747120f2 // indirect
|
||||||
)
|
)
|
||||||
|
|
||||||
replace fyne.io/fyne/v2 v2.3.5 => github.com/dweymouth/fyne/v2 v2.3.0-rc1.0.20230505012127-ca61c153b2a5
|
replace fyne.io/fyne/v2 v2.3.5 => github.com/dweymouth/fyne/v2 v2.3.0-rc1.0.20231019154213-5f32d36c2968
|
||||||
|
|||||||
@@ -74,12 +74,12 @@ github.com/danieljoos/wincred v1.1.0/go.mod h1:XYlo+eRTsVA9aHGp7NGjFkPla4m+DCL7h
|
|||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/dweymouth/fyne/v2 v2.3.0-rc1.0.20230505012127-ca61c153b2a5 h1:uXbHzg9HfefA7OVHu9gahobCP5B43df3L1MwU0GbdRs=
|
github.com/dweymouth/fyne/v2 v2.3.0-rc1.0.20231019154213-5f32d36c2968 h1:RYHUvyoVXfXprkVU06XkzupfldcZ9vlhslxFYi00YCY=
|
||||||
github.com/dweymouth/fyne/v2 v2.3.0-rc1.0.20230505012127-ca61c153b2a5/go.mod h1:X2+NrR+62mvAiAt2fwKT7035zQsE77KVV1NlvWo4vW8=
|
github.com/dweymouth/fyne/v2 v2.3.0-rc1.0.20231019154213-5f32d36c2968/go.mod h1:X2+NrR+62mvAiAt2fwKT7035zQsE77KVV1NlvWo4vW8=
|
||||||
github.com/dweymouth/go-mpv v0.0.0-20230406003141-7f1858e503ee h1:ZGyJ6wp7CAfT31BugypcF/TPKEy2RrGR9JFq1JOjOpY=
|
github.com/dweymouth/go-mpv v0.0.0-20230406003141-7f1858e503ee h1:ZGyJ6wp7CAfT31BugypcF/TPKEy2RrGR9JFq1JOjOpY=
|
||||||
github.com/dweymouth/go-mpv v0.0.0-20230406003141-7f1858e503ee/go.mod h1:Ov0ieN90M7i+0k3OxhA/g1dozGs+UcPHDsMKqPgRDk0=
|
github.com/dweymouth/go-mpv v0.0.0-20230406003141-7f1858e503ee/go.mod h1:Ov0ieN90M7i+0k3OxhA/g1dozGs+UcPHDsMKqPgRDk0=
|
||||||
github.com/dweymouth/go-subsonic v0.0.0-20231013011542-14b66c5a1fff h1:71TfIg5TMXMi9rGQYFhW8Gvs/GmBGK9O2gSmgzYXl4I=
|
github.com/dweymouth/go-subsonic v0.0.0-20231014003317-6a0c88db38ce h1:R12c/hE0licao74iAu2YDPJNI0jhGNuTyzmu3dMgFHE=
|
||||||
github.com/dweymouth/go-subsonic v0.0.0-20231013011542-14b66c5a1fff/go.mod h1:fUez6NFiEJiQTZizZ1BThZr5GJXAbigzGYjEPNm4tdI=
|
github.com/dweymouth/go-subsonic v0.0.0-20231014003317-6a0c88db38ce/go.mod h1:fUez6NFiEJiQTZizZ1BThZr5GJXAbigzGYjEPNm4tdI=
|
||||||
github.com/eclipse/paho.mqtt.golang v1.3.5/go.mod h1:eTzb4gxwwyWpqBUHGQZ4ABAV7+Jgm1PklsYT/eo8Hcc=
|
github.com/eclipse/paho.mqtt.golang v1.3.5/go.mod h1:eTzb4gxwwyWpqBUHGQZ4ABAV7+Jgm1PklsYT/eo8Hcc=
|
||||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||||
|
|||||||
@@ -7,6 +7,18 @@ import (
|
|||||||
"github.com/dweymouth/supersonic/backend/mediaprovider"
|
"github.com/dweymouth/supersonic/backend/mediaprovider"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func SliceEqual[T comparable](a []T, b []T) bool {
|
||||||
|
if len(a) != len(b) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for i := 0; i < len(a); i++ {
|
||||||
|
if a[i] != b[i] {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
func SliceContains[T comparable](ts []T, t T) bool {
|
func SliceContains[T comparable](ts []T, t T) bool {
|
||||||
for _, x := range ts {
|
for _, x := range ts {
|
||||||
if x == t {
|
if x == t {
|
||||||
|
|||||||
+4
-4
@@ -76,8 +76,8 @@ func NewBottomPanel(p *player.Player, pm *backend.PlaybackManager, contr *contro
|
|||||||
bp.NowPlaying.OnAlbumNameTapped(func() {
|
bp.NowPlaying.OnAlbumNameTapped(func() {
|
||||||
contr.NavigateTo(controller.AlbumRoute(bp.playbackManager.NowPlaying().AlbumID))
|
contr.NavigateTo(controller.AlbumRoute(bp.playbackManager.NowPlaying().AlbumID))
|
||||||
})
|
})
|
||||||
bp.NowPlaying.OnArtistNameTapped(func() {
|
bp.NowPlaying.OnArtistNameTapped(func(artistID string) {
|
||||||
contr.NavigateTo(controller.ArtistRoute(bp.playbackManager.NowPlaying().ArtistIDs[0]))
|
contr.NavigateTo(controller.ArtistRoute(artistID))
|
||||||
})
|
})
|
||||||
bp.NowPlaying.OnTrackNameTapped(func() {
|
bp.NowPlaying.OnTrackNameTapped(func() {
|
||||||
contr.NavigateTo(controller.NowPlayingRoute(bp.playbackManager.NowPlaying().ID))
|
contr.NavigateTo(controller.NowPlayingRoute(bp.playbackManager.NowPlaying().ID))
|
||||||
@@ -113,7 +113,7 @@ func NewBottomPanel(p *player.Player, pm *backend.PlaybackManager, contr *contro
|
|||||||
|
|
||||||
func (bp *BottomPanel) onSongChange(song, _ *mediaprovider.Track) {
|
func (bp *BottomPanel) onSongChange(song, _ *mediaprovider.Track) {
|
||||||
if song == nil {
|
if song == nil {
|
||||||
bp.NowPlaying.Update("", "", false, "", nil)
|
bp.NowPlaying.Update("", []string{}, []string{}, "", nil)
|
||||||
} else {
|
} else {
|
||||||
bp.coverArtID = song.CoverArtID
|
bp.coverArtID = song.CoverArtID
|
||||||
var im image.Image
|
var im image.Image
|
||||||
@@ -125,7 +125,7 @@ func (bp *BottomPanel) onSongChange(song, _ *mediaprovider.Track) {
|
|||||||
imgTTLSec := song.Duration + 30
|
imgTTLSec := song.Duration + 30
|
||||||
im, _ = bp.ImageManager.GetCoverThumbnailWithTTL(song.CoverArtID, time.Duration(imgTTLSec)*time.Second)
|
im, _ = bp.ImageManager.GetCoverThumbnailWithTTL(song.CoverArtID, time.Duration(imgTTLSec)*time.Second)
|
||||||
}
|
}
|
||||||
bp.NowPlaying.Update(song.Name, song.ArtistNames[0], song.ArtistIDs[0] != "", song.Album, im)
|
bp.NowPlaying.Update(song.Name, song.ArtistNames, song.ArtistIDs, song.Album, im)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+14
-19
@@ -166,17 +166,15 @@ func (a *AlbumPage) load() {
|
|||||||
type AlbumPageHeader struct {
|
type AlbumPageHeader struct {
|
||||||
widget.BaseWidget
|
widget.BaseWidget
|
||||||
|
|
||||||
albumID string
|
albumID string
|
||||||
coverID string
|
coverID string
|
||||||
artistID string
|
|
||||||
genre string
|
|
||||||
|
|
||||||
page *AlbumPage
|
page *AlbumPage
|
||||||
|
|
||||||
cover *widgets.TappableImage
|
cover *widgets.TappableImage
|
||||||
titleLabel *widget.RichText
|
titleLabel *widget.RichText
|
||||||
artistLabel *widgets.CustomHyperlink
|
artistLabel *widgets.MultiHyperlink
|
||||||
genreLabel *widgets.CustomHyperlink
|
genreLabel *widgets.MultiHyperlink
|
||||||
miscLabel *widget.Label
|
miscLabel *widget.Label
|
||||||
|
|
||||||
toggleFavButton *widgets.FavoriteButton
|
toggleFavButton *widgets.FavoriteButton
|
||||||
@@ -200,13 +198,13 @@ func NewAlbumPageHeader(page *AlbumPage) *AlbumPageHeader {
|
|||||||
a.titleLabel.Segments[0].(*widget.TextSegment).Style = widget.RichTextStyle{
|
a.titleLabel.Segments[0].(*widget.TextSegment).Style = widget.RichTextStyle{
|
||||||
SizeName: theme.SizeNameHeadingText,
|
SizeName: theme.SizeNameHeadingText,
|
||||||
}
|
}
|
||||||
a.artistLabel = widgets.NewCustomHyperlink()
|
a.artistLabel = widgets.NewMultiHyperlink()
|
||||||
a.artistLabel.OnTapped = func() {
|
a.artistLabel.OnTapped = func(id string) {
|
||||||
a.page.contr.NavigateTo(controller.ArtistRoute(a.artistID))
|
a.page.contr.NavigateTo(controller.ArtistRoute(id))
|
||||||
}
|
}
|
||||||
a.genreLabel = widgets.NewCustomHyperlink()
|
a.genreLabel = widgets.NewMultiHyperlink()
|
||||||
a.genreLabel.OnTapped = func() {
|
a.genreLabel.OnTapped = func(genre string) {
|
||||||
a.page.contr.NavigateTo(controller.GenreRoute(a.genre))
|
a.page.contr.NavigateTo(controller.GenreRoute(genre))
|
||||||
}
|
}
|
||||||
a.miscLabel = widget.NewLabel("")
|
a.miscLabel = widget.NewLabel("")
|
||||||
playButton := widget.NewButtonWithIcon("Play", theme.MediaPlayIcon(), func() {
|
playButton := widget.NewButtonWithIcon("Play", theme.MediaPlayIcon(), func() {
|
||||||
@@ -264,11 +262,9 @@ func (a *AlbumPageHeader) CreateRenderer() fyne.WidgetRenderer {
|
|||||||
func (a *AlbumPageHeader) Update(album *mediaprovider.AlbumWithTracks, im *backend.ImageManager) {
|
func (a *AlbumPageHeader) Update(album *mediaprovider.AlbumWithTracks, im *backend.ImageManager) {
|
||||||
a.albumID = album.ID
|
a.albumID = album.ID
|
||||||
a.coverID = album.CoverArtID
|
a.coverID = album.CoverArtID
|
||||||
a.artistID = album.ArtistIDs[0]
|
|
||||||
a.titleLabel.Segments[0].(*widget.TextSegment).Text = album.Name
|
a.titleLabel.Segments[0].(*widget.TextSegment).Text = album.Name
|
||||||
a.artistLabel.SetText(album.ArtistNames[0])
|
a.artistLabel.BuildSegments(album.ArtistNames, album.ArtistIDs)
|
||||||
a.genre = album.Genres[0]
|
a.genreLabel.BuildSegments(album.Genres, album.Genres)
|
||||||
a.genreLabel.SetText(album.Genres[0])
|
|
||||||
a.miscLabel.SetText(formatMiscLabelStr(album))
|
a.miscLabel.SetText(formatMiscLabelStr(album))
|
||||||
a.toggleFavButton.IsFavorited = album.Favorite
|
a.toggleFavButton.IsFavorited = album.Favorite
|
||||||
a.Refresh()
|
a.Refresh()
|
||||||
@@ -286,10 +282,9 @@ func (a *AlbumPageHeader) Update(album *mediaprovider.AlbumWithTracks, im *backe
|
|||||||
func (a *AlbumPageHeader) Clear() {
|
func (a *AlbumPageHeader) Clear() {
|
||||||
a.albumID = ""
|
a.albumID = ""
|
||||||
a.coverID = ""
|
a.coverID = ""
|
||||||
a.artistID = ""
|
|
||||||
a.titleLabel.Segments[0].(*widget.TextSegment).Text = ""
|
a.titleLabel.Segments[0].(*widget.TextSegment).Text = ""
|
||||||
a.artistLabel.SetText("")
|
a.artistLabel.Segments = nil
|
||||||
a.genreLabel.SetText("")
|
a.genreLabel.Segments = nil
|
||||||
a.miscLabel.SetText("")
|
a.miscLabel.SetText("")
|
||||||
a.toggleFavButton.IsFavorited = false
|
a.toggleFavButton.IsFavorited = false
|
||||||
a.fullSizeCoverFetching = false
|
a.fullSizeCoverFetching = false
|
||||||
|
|||||||
@@ -189,7 +189,7 @@ func (a *ArtistPage) showAlbumGrid() {
|
|||||||
Name: al.Name,
|
Name: al.Name,
|
||||||
ID: al.ID,
|
ID: al.ID,
|
||||||
CoverArtID: al.CoverArtID,
|
CoverArtID: al.CoverArtID,
|
||||||
Secondary: strconv.Itoa(al.Year),
|
Secondary: []string{strconv.Itoa(al.Year)},
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
if g := a.pool.Obtain(util.WidgetTypeGridView); g != nil {
|
if g := a.pool.Obtain(util.WidgetTypeGridView); g != nil {
|
||||||
@@ -366,10 +366,9 @@ func (a *ArtistPageHeader) UpdateInfo(info *mediaprovider.ArtistInfo) {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
if len(a.similarArtists.Objects) <= i+1 {
|
if len(a.similarArtists.Objects) <= i+1 {
|
||||||
a.similarArtists.Add(widgets.NewCustomHyperlink())
|
a.similarArtists.Add(widget.NewHyperlink("", nil))
|
||||||
}
|
}
|
||||||
h := a.similarArtists.Objects[i+1].(*widgets.CustomHyperlink)
|
h := a.similarArtists.Objects[i+1].(*widget.Hyperlink)
|
||||||
h.NoTruncate = true
|
|
||||||
h.SetText(art.Name)
|
h.SetText(art.Name)
|
||||||
h.OnTapped = func(id string) func() {
|
h.OnTapped = func(id string) func() {
|
||||||
return func() { a.artistPage.contr.NavigateTo(controller.ArtistRoute(id)) }
|
return func() { a.artistPage.contr.NavigateTo(controller.ArtistRoute(id)) }
|
||||||
|
|||||||
@@ -158,7 +158,7 @@ func createArtistsGridViewModel(artists []*mediaprovider.Artist) []widgets.GridV
|
|||||||
Name: ar.Name,
|
Name: ar.Name,
|
||||||
ID: ar.ID,
|
ID: ar.ID,
|
||||||
CoverArtID: ar.CoverArtID,
|
CoverArtID: ar.CoverArtID,
|
||||||
Secondary: fmt.Sprintf("%d %s", ar.AlbumCount, albums),
|
Secondary: []string{fmt.Sprintf("%d %s", ar.AlbumCount, albums)},
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -333,7 +333,7 @@ func buildArtistGridViewModel(artists []*mediaprovider.Artist) []widgets.GridVie
|
|||||||
ID: ar.ID,
|
ID: ar.ID,
|
||||||
CoverArtID: ar.CoverArtID,
|
CoverArtID: ar.CoverArtID,
|
||||||
Name: ar.Name,
|
Name: ar.Name,
|
||||||
Secondary: fmt.Sprintf("%d %s", ar.AlbumCount, albums),
|
Secondary: []string{fmt.Sprintf("%d %s", ar.AlbumCount, albums)},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return model
|
return model
|
||||||
|
|||||||
@@ -175,7 +175,7 @@ func createPlaylistGridViewModel(playlists []*mediaprovider.Playlist) []widgets.
|
|||||||
Name: pl.Name,
|
Name: pl.Name,
|
||||||
ID: pl.ID,
|
ID: pl.ID,
|
||||||
CoverArtID: pl.CoverArtID,
|
CoverArtID: pl.CoverArtID,
|
||||||
Secondary: fmt.Sprintf("%d %s", pl.TrackCount, tracks),
|
Secondary: []string{fmt.Sprintf("%d %s", pl.TrackCount, tracks)},
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-1
@@ -2,6 +2,7 @@ package ui
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/20after4/configdir"
|
"github.com/20after4/configdir"
|
||||||
"github.com/dweymouth/supersonic/backend"
|
"github.com/dweymouth/supersonic/backend"
|
||||||
@@ -86,7 +87,7 @@ func NewMainWindow(fyneApp fyne.App, appName, displayAppName, appVersion string,
|
|||||||
m.Window.SetTitle(displayAppName)
|
m.Window.SetTitle(displayAppName)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
m.Window.SetTitle(fmt.Sprintf("%s – %s · %s", song.Name, song.ArtistNames[0], displayAppName))
|
m.Window.SetTitle(fmt.Sprintf("%s – %s · %s", song.Name, strings.Join(song.ArtistNames, ", "), displayAppName))
|
||||||
})
|
})
|
||||||
app.ServerManager.OnServerConnected(func() {
|
app.ServerManager.OnServerConnected(func() {
|
||||||
m.BrowsingPane.EnableNavigationButtons()
|
m.BrowsingPane.EnableNavigationButtons()
|
||||||
|
|||||||
@@ -1,133 +0,0 @@
|
|||||||
package widgets
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fyne.io/fyne/v2"
|
|
||||||
"fyne.io/fyne/v2/container"
|
|
||||||
"fyne.io/fyne/v2/layout"
|
|
||||||
"fyne.io/fyne/v2/theme"
|
|
||||||
"fyne.io/fyne/v2/widget"
|
|
||||||
)
|
|
||||||
|
|
||||||
type hyperlinkWrapper struct {
|
|
||||||
widget.Hyperlink
|
|
||||||
|
|
||||||
textWidthCached float32
|
|
||||||
MaxWidth float32
|
|
||||||
}
|
|
||||||
|
|
||||||
func newHyperlinkWrapper() *hyperlinkWrapper {
|
|
||||||
h := &hyperlinkWrapper{
|
|
||||||
Hyperlink: widget.Hyperlink{
|
|
||||||
Text: "",
|
|
||||||
Wrapping: fyne.TextTruncate,
|
|
||||||
},
|
|
||||||
textWidthCached: -1,
|
|
||||||
}
|
|
||||||
h.ExtendBaseWidget(h)
|
|
||||||
return h
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *hyperlinkWrapper) MinSize() fyne.Size {
|
|
||||||
if h.textWidthCached < 0 {
|
|
||||||
s := fyne.MeasureText(h.Text, theme.TextSize(), h.TextStyle)
|
|
||||||
// the 2.7 factor is a bit of a magic number but it works ¯\_(ツ)_/¯
|
|
||||||
h.textWidthCached = s.Width + theme.Padding()*2.7
|
|
||||||
}
|
|
||||||
return fyne.NewSize(fyne.Min(h.MaxWidth, h.textWidthCached), h.Hyperlink.MinSize().Height)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *hyperlinkWrapper) SetText(text string) {
|
|
||||||
h.Text = text
|
|
||||||
h.textWidthCached = -1
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *hyperlinkWrapper) TypedKey(e *fyne.KeyEvent) {
|
|
||||||
if e.Name == fyne.KeySpace {
|
|
||||||
if h.OnTapped != nil {
|
|
||||||
h.OnTapped()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type CustomHyperlink struct {
|
|
||||||
widget.BaseWidget
|
|
||||||
h *hyperlinkWrapper
|
|
||||||
l *widget.Label
|
|
||||||
|
|
||||||
OnTapped func()
|
|
||||||
NoTruncate bool
|
|
||||||
Disabled bool
|
|
||||||
|
|
||||||
lastDisabled bool
|
|
||||||
container *fyne.Container
|
|
||||||
fullTextWidth float32
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewCustomHyperlink() *CustomHyperlink {
|
|
||||||
c := &CustomHyperlink{
|
|
||||||
h: newHyperlinkWrapper(),
|
|
||||||
l: widget.NewLabel(""),
|
|
||||||
container: container.NewMax(),
|
|
||||||
}
|
|
||||||
c.h.OnTapped = func() {
|
|
||||||
if c.OnTapped != nil {
|
|
||||||
c.OnTapped()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
c.fullTextWidth = c.l.MinSize().Width
|
|
||||||
c.ExtendBaseWidget(c)
|
|
||||||
c.updateContainer(c.Disabled)
|
|
||||||
return c
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *CustomHyperlink) SetText(text string) {
|
|
||||||
lastWrapping := c.l.Wrapping
|
|
||||||
c.l.Wrapping = fyne.TextWrapOff
|
|
||||||
c.l.SetText(text)
|
|
||||||
c.fullTextWidth = c.l.MinSize().Width
|
|
||||||
c.h.SetText(text)
|
|
||||||
c.l.Wrapping = lastWrapping
|
|
||||||
c.Refresh()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *CustomHyperlink) SetTextStyle(style fyne.TextStyle) {
|
|
||||||
c.h.TextStyle = style
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *CustomHyperlink) Resize(size fyne.Size) {
|
|
||||||
c.h.MaxWidth = size.Width
|
|
||||||
c.BaseWidget.Resize(size)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *CustomHyperlink) Refresh() {
|
|
||||||
if c.NoTruncate {
|
|
||||||
c.l.Wrapping = fyne.TextWrapOff
|
|
||||||
} else {
|
|
||||||
c.l.Wrapping = fyne.TextTruncate
|
|
||||||
}
|
|
||||||
if c.lastDisabled != c.Disabled {
|
|
||||||
c.updateContainer(c.Disabled)
|
|
||||||
c.lastDisabled = c.Disabled
|
|
||||||
}
|
|
||||||
c.container.Refresh()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *CustomHyperlink) MinSize() fyne.Size {
|
|
||||||
if c.NoTruncate {
|
|
||||||
return fyne.NewSize(c.fullTextWidth, c.l.MinSize().Height)
|
|
||||||
}
|
|
||||||
return fyne.NewSize(0, c.l.MinSize().Height)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *CustomHyperlink) CreateRenderer() fyne.WidgetRenderer {
|
|
||||||
return widget.NewSimpleRenderer(c.container)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *CustomHyperlink) updateContainer(linkDisabled bool) {
|
|
||||||
c.container.RemoveAll()
|
|
||||||
if linkDisabled {
|
|
||||||
c.container.Add(c.l)
|
|
||||||
} else {
|
|
||||||
c.container.Add(container.NewHBox(c.h, layout.NewSpacer()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -55,11 +55,11 @@ func (g gridViewAlbumIterator) NextN(n int) []GridViewItemModel {
|
|||||||
albums := g.iter.NextN(n)
|
albums := g.iter.NextN(n)
|
||||||
return sharedutil.MapSlice(albums, func(al *mediaprovider.Album) GridViewItemModel {
|
return sharedutil.MapSlice(albums, func(al *mediaprovider.Album) GridViewItemModel {
|
||||||
return GridViewItemModel{
|
return GridViewItemModel{
|
||||||
Name: al.Name,
|
Name: al.Name,
|
||||||
ID: al.ID,
|
ID: al.ID,
|
||||||
CoverArtID: al.CoverArtID,
|
CoverArtID: al.CoverArtID,
|
||||||
Secondary: al.ArtistNames[0],
|
Secondary: al.ArtistNames,
|
||||||
SecondaryID: al.ArtistIDs[0],
|
SecondaryIDs: al.ArtistIDs,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -205,9 +205,9 @@ func (g *GridView) createGridWrap() {
|
|||||||
func() fyne.CanvasObject {
|
func() fyne.CanvasObject {
|
||||||
card := NewGridViewItem(g.Placeholder)
|
card := NewGridViewItem(g.Placeholder)
|
||||||
card.OnPlay = func() { g.onPlay(card.ItemID(), false) }
|
card.OnPlay = func() { g.onPlay(card.ItemID(), false) }
|
||||||
card.OnShowSecondaryPage = func() {
|
card.OnShowSecondaryPage = func(id string) {
|
||||||
if g.OnShowSecondaryPage != nil {
|
if g.OnShowSecondaryPage != nil {
|
||||||
g.OnShowSecondaryPage(card.SecondaryID())
|
g.OnShowSecondaryPage(id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
card.OnShowItemPage = func() {
|
card.OnShowItemPage = func() {
|
||||||
|
|||||||
+20
-22
@@ -5,6 +5,7 @@ import (
|
|||||||
"image"
|
"image"
|
||||||
|
|
||||||
"github.com/dweymouth/supersonic/res"
|
"github.com/dweymouth/supersonic/res"
|
||||||
|
"github.com/dweymouth/supersonic/sharedutil"
|
||||||
"github.com/dweymouth/supersonic/ui/layouts"
|
"github.com/dweymouth/supersonic/ui/layouts"
|
||||||
|
|
||||||
"fyne.io/fyne/v2"
|
"fyne.io/fyne/v2"
|
||||||
@@ -122,20 +123,20 @@ func isInside(origin fyne.Position, radius float32, point fyne.Position) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type GridViewItemModel struct {
|
type GridViewItemModel struct {
|
||||||
Name string
|
Name string
|
||||||
ID string
|
ID string
|
||||||
CoverArtID string
|
CoverArtID string
|
||||||
Secondary string
|
Secondary []string
|
||||||
SecondaryID string
|
SecondaryIDs []string
|
||||||
}
|
}
|
||||||
|
|
||||||
type GridViewItem struct {
|
type GridViewItem struct {
|
||||||
widget.BaseWidget
|
widget.BaseWidget
|
||||||
|
|
||||||
itemID string
|
itemID string
|
||||||
secondaryID string
|
secondaryIDs []string
|
||||||
primaryText *CustomHyperlink
|
primaryText *widget.Hyperlink
|
||||||
secondaryText *CustomHyperlink
|
secondaryText *MultiHyperlink
|
||||||
container *fyne.Container
|
container *fyne.Container
|
||||||
|
|
||||||
// updated by GridView
|
// updated by GridView
|
||||||
@@ -145,16 +146,17 @@ type GridViewItem struct {
|
|||||||
OnPlay func()
|
OnPlay func()
|
||||||
OnShowContextMenu func(fyne.Position)
|
OnShowContextMenu func(fyne.Position)
|
||||||
OnShowItemPage func()
|
OnShowItemPage func()
|
||||||
OnShowSecondaryPage func()
|
OnShowSecondaryPage func(string)
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewGridViewItem(placeholderResource fyne.Resource) *GridViewItem {
|
func NewGridViewItem(placeholderResource fyne.Resource) *GridViewItem {
|
||||||
g := &GridViewItem{
|
g := &GridViewItem{
|
||||||
primaryText: NewCustomHyperlink(),
|
primaryText: widget.NewHyperlink("", nil),
|
||||||
secondaryText: NewCustomHyperlink(),
|
secondaryText: NewMultiHyperlink(),
|
||||||
Cover: newCoverImage(placeholderResource),
|
Cover: newCoverImage(placeholderResource),
|
||||||
}
|
}
|
||||||
g.primaryText.SetTextStyle(fyne.TextStyle{Bold: true})
|
g.primaryText.TextStyle.Bold = true
|
||||||
|
g.primaryText.Wrapping = fyne.TextTruncate
|
||||||
g.ExtendBaseWidget(g)
|
g.ExtendBaseWidget(g)
|
||||||
g.Cover.OnPlay = func() {
|
g.Cover.OnPlay = func() {
|
||||||
if g.OnPlay != nil {
|
if g.OnPlay != nil {
|
||||||
@@ -173,9 +175,9 @@ func NewGridViewItem(placeholderResource fyne.Resource) *GridViewItem {
|
|||||||
}
|
}
|
||||||
g.Cover.OnShowPage = showItemFn
|
g.Cover.OnShowPage = showItemFn
|
||||||
g.primaryText.OnTapped = showItemFn
|
g.primaryText.OnTapped = showItemFn
|
||||||
g.secondaryText.OnTapped = func() {
|
g.secondaryText.OnTapped = func(s string) {
|
||||||
if g.OnShowSecondaryPage != nil {
|
if g.OnShowSecondaryPage != nil {
|
||||||
g.OnShowSecondaryPage()
|
g.OnShowSecondaryPage(s)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -191,15 +193,15 @@ func (g *GridViewItem) createContainer() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (g *GridViewItem) NeedsUpdate(model GridViewItemModel) bool {
|
func (g *GridViewItem) NeedsUpdate(model GridViewItemModel) bool {
|
||||||
return g.itemID != model.ID || g.secondaryID != model.SecondaryID
|
return g.itemID != model.ID || !sharedutil.SliceEqual(g.secondaryIDs, model.SecondaryIDs)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *GridViewItem) Update(model GridViewItemModel) {
|
func (g *GridViewItem) Update(model GridViewItemModel) {
|
||||||
g.itemID = model.ID
|
g.itemID = model.ID
|
||||||
g.secondaryID = model.SecondaryID
|
g.secondaryIDs = model.SecondaryIDs
|
||||||
g.primaryText.SetText(model.Name)
|
g.primaryText.SetText(model.Name)
|
||||||
g.secondaryText.Disabled = model.SecondaryID == ""
|
g.secondaryText.BuildSegments(model.Secondary, model.SecondaryIDs)
|
||||||
g.secondaryText.SetText(model.Secondary)
|
g.secondaryText.Refresh()
|
||||||
g.Cover.ResetPlayButton()
|
g.Cover.ResetPlayButton()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -211,10 +213,6 @@ func (g *GridViewItem) ItemID() string {
|
|||||||
return g.itemID
|
return g.itemID
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *GridViewItem) SecondaryID() string {
|
|
||||||
return g.secondaryID
|
|
||||||
}
|
|
||||||
|
|
||||||
func (g *GridViewItem) CreateRenderer() fyne.WidgetRenderer {
|
func (g *GridViewItem) CreateRenderer() fyne.WidgetRenderer {
|
||||||
return widget.NewSimpleRenderer(g.container)
|
return widget.NewSimpleRenderer(g.container)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,243 @@
|
|||||||
|
package widgets
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fyne.io/fyne/v2"
|
||||||
|
"fyne.io/fyne/v2/container"
|
||||||
|
"fyne.io/fyne/v2/theme"
|
||||||
|
"fyne.io/fyne/v2/widget"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MultiHyperlink struct {
|
||||||
|
widget.BaseWidget
|
||||||
|
|
||||||
|
Segments []MultiHyperlinkSegment
|
||||||
|
OnTapped func(string)
|
||||||
|
|
||||||
|
minSegWidthCached float32
|
||||||
|
minHeightCached float32
|
||||||
|
separatorWCached float32
|
||||||
|
|
||||||
|
// TODO: Once https://github.com/fyne-io/fyne/issues/4336 is resolved,
|
||||||
|
// we can switch to the much cleaner RichText implementation
|
||||||
|
//provider *widget.RichText
|
||||||
|
content *fyne.Container
|
||||||
|
}
|
||||||
|
|
||||||
|
type MultiHyperlinkSegment struct {
|
||||||
|
Text string
|
||||||
|
LinkID string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMultiHyperlink() *MultiHyperlink {
|
||||||
|
c := &MultiHyperlink{
|
||||||
|
//provider: widget.NewRichText(),
|
||||||
|
content: container.NewWithoutLayout(),
|
||||||
|
}
|
||||||
|
c.ExtendBaseWidget(c)
|
||||||
|
//c.provider.Wrapping = fyne.TextTruncate
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MultiHyperlink) BuildSegments(texts, links []string) {
|
||||||
|
l := len(links)
|
||||||
|
m.Segments = nil
|
||||||
|
for i, text := range texts {
|
||||||
|
link := ""
|
||||||
|
if l > i {
|
||||||
|
link = links[i]
|
||||||
|
}
|
||||||
|
m.Segments = append(m.Segments, MultiHyperlinkSegment{Text: text, LinkID: link})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *MultiHyperlink) getMinSegWidth() float32 {
|
||||||
|
if c.minSegWidthCached == 0 {
|
||||||
|
c.minSegWidthCached = fyne.MeasureText(", W", theme.TextSize(), fyne.TextStyle{}).Width
|
||||||
|
}
|
||||||
|
return c.minSegWidthCached
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *MultiHyperlink) getSeparatorWidth() float32 {
|
||||||
|
if c.separatorWCached == 0 {
|
||||||
|
c.separatorWCached = fyne.MeasureText(",", theme.TextSize(), fyne.TextStyle{}).Width
|
||||||
|
}
|
||||||
|
return c.separatorWCached
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *MultiHyperlink) layoutObjects() {
|
||||||
|
if len(c.Segments) == 0 {
|
||||||
|
c.content.RemoveAll()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
x := float32(0)
|
||||||
|
width := c.Size().Width
|
||||||
|
l := len(c.content.Objects)
|
||||||
|
|
||||||
|
var i int // at end of loop should be index of last seg that was laid out for display
|
||||||
|
var seg MultiHyperlinkSegment
|
||||||
|
for i, seg = range c.Segments {
|
||||||
|
if i > 0 {
|
||||||
|
// check if we have enough room to show another segment
|
||||||
|
if x+c.getMinSegWidth() > width {
|
||||||
|
i -= 1 // we are not showing this segment
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
appendingSegments := 2*i >= l
|
||||||
|
var obj fyne.CanvasObject
|
||||||
|
if !appendingSegments {
|
||||||
|
obj = c.content.Objects[2*i]
|
||||||
|
} else if i > 0 {
|
||||||
|
c.content.Objects = append(c.content.Objects, c.newSeparatorLabel())
|
||||||
|
}
|
||||||
|
if seg.LinkID == "" {
|
||||||
|
obj = c.updateOrReplaceLabel(obj, seg.Text)
|
||||||
|
} else {
|
||||||
|
obj = c.updateOrReplaceHyperlink(obj, seg.Text, seg.LinkID)
|
||||||
|
}
|
||||||
|
if appendingSegments {
|
||||||
|
c.content.Objects = append(c.content.Objects, obj)
|
||||||
|
} else {
|
||||||
|
c.content.Objects[2*i] = obj
|
||||||
|
}
|
||||||
|
|
||||||
|
if i > 0 {
|
||||||
|
// move and resize separator
|
||||||
|
obj = c.content.Objects[2*i-1]
|
||||||
|
ms := obj.MinSize()
|
||||||
|
obj.Resize(ms)
|
||||||
|
obj.Move(fyne.NewPos(x-ms.Width+c.getSeparatorWidth()+1, 0)) // this is really ugly
|
||||||
|
x -= theme.Padding() * 2
|
||||||
|
}
|
||||||
|
// move and resize text object
|
||||||
|
textW := fyne.MeasureText(seg.Text, theme.TextSize(), fyne.TextStyle{}).Width + theme.Padding()*2 + theme.InnerPadding()
|
||||||
|
obj = c.content.Objects[2*i]
|
||||||
|
ms := obj.MinSize()
|
||||||
|
w := fyne.Min(width-x, textW)
|
||||||
|
obj.Resize(fyne.NewSize(w, ms.Height))
|
||||||
|
obj.Move(fyne.NewPos(x, 0))
|
||||||
|
x += w
|
||||||
|
}
|
||||||
|
|
||||||
|
i += 1
|
||||||
|
c.content.Objects = c.content.Objects[:2*i-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *MultiHyperlink) updateOrReplaceLabel(obj fyne.CanvasObject, text string) fyne.CanvasObject {
|
||||||
|
if obj != nil {
|
||||||
|
if label, ok := obj.(*widget.Label); ok {
|
||||||
|
label.Text = text
|
||||||
|
return label
|
||||||
|
}
|
||||||
|
}
|
||||||
|
l := widget.NewLabel(text)
|
||||||
|
l.Wrapping = fyne.TextTruncate
|
||||||
|
return l
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *MultiHyperlink) updateOrReplaceHyperlink(obj fyne.CanvasObject, text, link string) fyne.CanvasObject {
|
||||||
|
if obj != nil {
|
||||||
|
if l, ok := obj.(*widget.Hyperlink); ok {
|
||||||
|
l.Text = text
|
||||||
|
l.OnTapped = func() { c.onSegmentTapped(link) }
|
||||||
|
return l
|
||||||
|
}
|
||||||
|
}
|
||||||
|
l := widget.NewHyperlink(text, nil)
|
||||||
|
l.Wrapping = fyne.TextTruncate
|
||||||
|
l.OnTapped = func() { c.onSegmentTapped(link) }
|
||||||
|
return l
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *MultiHyperlink) newSeparatorLabel() *widget.Label {
|
||||||
|
return widget.NewLabel(", ")
|
||||||
|
}
|
||||||
|
|
||||||
|
/***
|
||||||
|
* RichText implementation
|
||||||
|
|
||||||
|
func (c *MultiHyperlink) syncSegments() {
|
||||||
|
l := len(c.provider.Segments)
|
||||||
|
for i, seg := range c.Segments {
|
||||||
|
appendingSegments := 2*i >= l // true if we need to extend the RichText provider with new segments
|
||||||
|
var rtSeg widget.RichTextSegment
|
||||||
|
if !appendingSegments {
|
||||||
|
rtSeg = c.provider.Segments[2*i]
|
||||||
|
} else if i > 0 {
|
||||||
|
// append new separator segment
|
||||||
|
c.provider.Segments = append(c.provider.Segments, c.newSeparatorSegment())
|
||||||
|
}
|
||||||
|
if seg.LinkID == "" {
|
||||||
|
rtSeg = c.updateOrReplaceTextSegment(rtSeg, seg.Text)
|
||||||
|
} else {
|
||||||
|
rtSeg = c.updateOrReplaceHyperlinkSegment(rtSeg, seg.Text, seg.LinkID)
|
||||||
|
}
|
||||||
|
if appendingSegments {
|
||||||
|
c.provider.Segments = append(c.provider.Segments, rtSeg)
|
||||||
|
} else {
|
||||||
|
c.provider.Segments[2*i] = rtSeg
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// discard extra segments if shortening the multihyperlink
|
||||||
|
for i := 2 * len(c.Segments); i < l; i++ {
|
||||||
|
c.provider.Segments[i] = nil
|
||||||
|
}
|
||||||
|
c.provider.Segments = c.provider.Segments[:2*len(c.Segments)-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *MultiHyperlink) newSeparatorSegment() widget.RichTextSegment {
|
||||||
|
return &widget.TextSegment{Text: ", ", Style: widget.RichTextStyle{Inline: true}}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *MultiHyperlink) updateOrReplaceTextSegment(seg widget.RichTextSegment, text string) widget.RichTextSegment {
|
||||||
|
if seg != nil {
|
||||||
|
if ts, ok := seg.(*widget.TextSegment); ok {
|
||||||
|
ts.Text = text
|
||||||
|
return seg
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return &widget.TextSegment{Text: text, Style: widget.RichTextStyle{Inline: true}}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *MultiHyperlink) updateOrReplaceHyperlinkSegment(seg widget.RichTextSegment, text, linkID string) widget.RichTextSegment {
|
||||||
|
if seg != nil {
|
||||||
|
if ts, ok := seg.(*widget.HyperlinkSegment); ok {
|
||||||
|
ts.Text = text
|
||||||
|
// TODO: OnTapped
|
||||||
|
return seg
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return &widget.HyperlinkSegment{Text: text} // TODO: OnTapped
|
||||||
|
}
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
func (c *MultiHyperlink) onSegmentTapped(linkID string) {
|
||||||
|
if c.OnTapped != nil {
|
||||||
|
c.OnTapped(linkID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *MultiHyperlink) MinSize() fyne.Size {
|
||||||
|
if c.minHeightCached == 0 {
|
||||||
|
c.minHeightCached = widget.NewLabel("").MinSize().Height
|
||||||
|
}
|
||||||
|
return fyne.NewSize(1, c.minHeightCached)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *MultiHyperlink) Resize(size fyne.Size) {
|
||||||
|
c.BaseWidget.Resize(size)
|
||||||
|
c.layoutObjects()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *MultiHyperlink) Refresh() {
|
||||||
|
//c.syncSegments()
|
||||||
|
c.layoutObjects()
|
||||||
|
c.BaseWidget.Refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *MultiHyperlink) CreateRenderer() fyne.WidgetRenderer {
|
||||||
|
return widget.NewSimpleRenderer(c.content)
|
||||||
|
//return widget.NewSimpleRenderer(c.provider)
|
||||||
|
}
|
||||||
@@ -17,9 +17,9 @@ import (
|
|||||||
type NowPlayingCard struct {
|
type NowPlayingCard struct {
|
||||||
widget.BaseWidget
|
widget.BaseWidget
|
||||||
|
|
||||||
trackName *CustomHyperlink
|
trackName *widget.Hyperlink
|
||||||
artistName *CustomHyperlink
|
artistName *MultiHyperlink
|
||||||
albumName *CustomHyperlink
|
albumName *widget.Hyperlink
|
||||||
cover *TappableImage
|
cover *TappableImage
|
||||||
menu *widget.PopUpMenu
|
menu *widget.PopUpMenu
|
||||||
|
|
||||||
@@ -33,17 +33,18 @@ type NowPlayingCard struct {
|
|||||||
|
|
||||||
func NewNowPlayingCard() *NowPlayingCard {
|
func NewNowPlayingCard() *NowPlayingCard {
|
||||||
n := &NowPlayingCard{
|
n := &NowPlayingCard{
|
||||||
trackName: NewCustomHyperlink(),
|
trackName: widget.NewHyperlink("", nil),
|
||||||
artistName: NewCustomHyperlink(),
|
artistName: NewMultiHyperlink(),
|
||||||
albumName: NewCustomHyperlink(),
|
albumName: widget.NewHyperlink("", nil),
|
||||||
}
|
}
|
||||||
n.ExtendBaseWidget(n)
|
n.ExtendBaseWidget(n)
|
||||||
n.cover = NewTappableImage(n.onShowCoverImage)
|
n.cover = NewTappableImage(n.onShowCoverImage)
|
||||||
n.cover.OnTappedSecondary = n.showMenu
|
n.cover.OnTappedSecondary = n.showMenu
|
||||||
n.trackName.Hidden = true
|
n.trackName.Hidden = true
|
||||||
n.artistName.Hidden = true
|
|
||||||
n.albumName.Hidden = true
|
n.albumName.Hidden = true
|
||||||
n.trackName.SetTextStyle(fyne.TextStyle{Bold: true})
|
n.albumName.Wrapping = fyne.TextTruncate
|
||||||
|
n.trackName.Wrapping = fyne.TextTruncate
|
||||||
|
n.trackName.TextStyle.Bold = true
|
||||||
n.cover.SetMinSize(fyne.NewSize(85, 85))
|
n.cover.SetMinSize(fyne.NewSize(85, 85))
|
||||||
n.cover.FillMode = canvas.ImageFillContain
|
n.cover.FillMode = canvas.ImageFillContain
|
||||||
|
|
||||||
@@ -83,19 +84,17 @@ func (n *NowPlayingCard) CreateRenderer() fyne.WidgetRenderer {
|
|||||||
return widget.NewSimpleRenderer(n.c)
|
return widget.NewSimpleRenderer(n.c)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *NowPlayingCard) Update(track, artist string, artistNavigable bool, album string, cover image.Image) {
|
func (n *NowPlayingCard) Update(track string, artists, artistIDs []string, album string, cover image.Image) {
|
||||||
n.trackName.SetText(track)
|
n.trackName.SetText(track)
|
||||||
n.trackName.Hidden = track == ""
|
n.trackName.Hidden = track == ""
|
||||||
n.artistName.SetText(artist)
|
n.artistName.BuildSegments(artists, artistIDs)
|
||||||
n.artistName.Hidden = artist == ""
|
|
||||||
n.artistName.Disabled = !artistNavigable
|
|
||||||
n.albumName.SetText(album)
|
n.albumName.SetText(album)
|
||||||
n.albumName.Hidden = album == ""
|
n.albumName.Hidden = album == ""
|
||||||
n.cover.Image.Image = cover
|
n.cover.Image.Image = cover
|
||||||
n.c.Refresh()
|
n.c.Refresh()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *NowPlayingCard) OnArtistNameTapped(f func()) {
|
func (n *NowPlayingCard) OnArtistNameTapped(f func(string)) {
|
||||||
n.artistName.OnTapped = f
|
n.artistName.OnTapped = f
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+40
-40
@@ -396,7 +396,7 @@ func (t *Tracklist) doSortTracks() {
|
|||||||
case ColumnTitle:
|
case ColumnTitle:
|
||||||
t.stringSort(func(tr *trackModel) string { return tr.track.Name })
|
t.stringSort(func(tr *trackModel) string { return tr.track.Name })
|
||||||
case ColumnArtist:
|
case ColumnArtist:
|
||||||
t.stringSort(func(tr *trackModel) string { return tr.track.ArtistNames[0] })
|
t.stringSort(func(tr *trackModel) string { return strings.Join(tr.track.ArtistNames, ", ") })
|
||||||
case ColumnAlbum:
|
case ColumnAlbum:
|
||||||
t.stringSort(func(tr *trackModel) string { return tr.track.Album })
|
t.stringSort(func(tr *trackModel) string { return tr.track.Album })
|
||||||
case ColumnPath:
|
case ColumnPath:
|
||||||
@@ -674,18 +674,18 @@ type TrackRow struct {
|
|||||||
isFavorite bool
|
isFavorite bool
|
||||||
playCount int
|
playCount int
|
||||||
|
|
||||||
num *widget.RichText
|
num *widget.Label
|
||||||
name *widget.RichText
|
name *widget.RichText // for bold support
|
||||||
artist *CustomHyperlink
|
artist *MultiHyperlink
|
||||||
album *CustomHyperlink
|
album *widget.Hyperlink
|
||||||
dur *widget.RichText
|
dur *widget.Label
|
||||||
year *widget.RichText
|
year *widget.Label
|
||||||
favorite *fyne.Container
|
favorite *fyne.Container
|
||||||
rating *StarRating
|
rating *StarRating
|
||||||
bitrate *widget.RichText
|
bitrate *widget.Label
|
||||||
plays *widget.RichText
|
plays *widget.Label
|
||||||
size *widget.RichText
|
size *widget.Label
|
||||||
path *widget.RichText
|
path *widget.Label
|
||||||
|
|
||||||
OnTappedSecondary func(e *fyne.PointEvent, trackIdx int)
|
OnTappedSecondary func(e *fyne.PointEvent, trackIdx int)
|
||||||
|
|
||||||
@@ -695,24 +695,25 @@ type TrackRow struct {
|
|||||||
func NewTrackRow(tracklist *Tracklist, playingIcon fyne.CanvasObject) *TrackRow {
|
func NewTrackRow(tracklist *Tracklist, playingIcon fyne.CanvasObject) *TrackRow {
|
||||||
t := &TrackRow{tracklist: tracklist, playingIcon: playingIcon}
|
t := &TrackRow{tracklist: tracklist, playingIcon: playingIcon}
|
||||||
t.ExtendBaseWidget(t)
|
t.ExtendBaseWidget(t)
|
||||||
t.num = newTrailingAlignRichText()
|
t.num = newTrailingAlignLabel()
|
||||||
t.name = newTruncatingRichText()
|
t.name = newTruncatingRichText()
|
||||||
t.artist = NewCustomHyperlink()
|
t.artist = NewMultiHyperlink()
|
||||||
t.artist.OnTapped = func() { tracklist.onArtistTapped(t.artistID) }
|
t.artist.OnTapped = tracklist.onArtistTapped
|
||||||
t.album = NewCustomHyperlink()
|
t.album = widget.NewHyperlink("", nil)
|
||||||
|
t.album.Wrapping = fyne.TextTruncate
|
||||||
t.album.OnTapped = func() { tracklist.onAlbumTapped(t.albumID) }
|
t.album.OnTapped = func() { tracklist.onAlbumTapped(t.albumID) }
|
||||||
t.dur = newTrailingAlignRichText()
|
t.dur = newTrailingAlignLabel()
|
||||||
t.year = newTrailingAlignRichText()
|
t.year = newTrailingAlignLabel()
|
||||||
favorite := NewTappableIcon(myTheme.NotFavoriteIcon)
|
favorite := NewTappableIcon(myTheme.NotFavoriteIcon)
|
||||||
favorite.OnTapped = t.toggleFavorited
|
favorite.OnTapped = t.toggleFavorited
|
||||||
t.favorite = container.NewCenter(favorite)
|
t.favorite = container.NewCenter(favorite)
|
||||||
t.rating = NewStarRating()
|
t.rating = NewStarRating()
|
||||||
t.rating.StarSize = 16
|
t.rating.StarSize = 16
|
||||||
t.rating.OnRatingChanged = t.setTrackRating
|
t.rating.OnRatingChanged = t.setTrackRating
|
||||||
t.plays = newTrailingAlignRichText()
|
t.plays = newTrailingAlignLabel()
|
||||||
t.bitrate = newTrailingAlignRichText()
|
t.bitrate = newTrailingAlignLabel()
|
||||||
t.size = newTrailingAlignRichText()
|
t.size = newTrailingAlignLabel()
|
||||||
t.path = newTruncatingRichText()
|
t.path = newTruncatingLabel()
|
||||||
|
|
||||||
t.Content = container.New(tracklist.colLayout,
|
t.Content = container.New(tracklist.colLayout,
|
||||||
t.num, t.name, t.artist, t.album, t.dur, t.year, t.favorite, t.rating, t.plays, t.bitrate, t.size, t.path)
|
t.num, t.name, t.artist, t.album, t.dur, t.year, t.favorite, t.rating, t.plays, t.bitrate, t.size, t.path)
|
||||||
@@ -725,9 +726,15 @@ func newTruncatingRichText() *widget.RichText {
|
|||||||
return rt
|
return rt
|
||||||
}
|
}
|
||||||
|
|
||||||
func newTrailingAlignRichText() *widget.RichText {
|
func newTruncatingLabel() *widget.Label {
|
||||||
rt := widget.NewRichTextWithText("")
|
rt := widget.NewLabel("")
|
||||||
rt.Segments[0].(*widget.TextSegment).Style.Alignment = fyne.TextAlignTrailing
|
rt.Wrapping = fyne.TextTruncate
|
||||||
|
return rt
|
||||||
|
}
|
||||||
|
|
||||||
|
func newTrailingAlignLabel() *widget.Label {
|
||||||
|
rt := widget.NewLabel("")
|
||||||
|
rt.Alignment = fyne.TextAlignTrailing
|
||||||
return rt
|
return rt
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -747,15 +754,14 @@ func (t *TrackRow) Update(tm *trackModel, rowNum int) {
|
|||||||
t.albumID = tr.AlbumID
|
t.albumID = tr.AlbumID
|
||||||
|
|
||||||
t.name.Segments[0].(*widget.TextSegment).Text = tr.Name
|
t.name.Segments[0].(*widget.TextSegment).Text = tr.Name
|
||||||
t.artist.SetText(tr.ArtistNames[0])
|
t.artist.BuildSegments(tr.ArtistNames, tr.ArtistIDs)
|
||||||
t.artist.Disabled = tr.ArtistIDs[0] == ""
|
|
||||||
t.album.SetText(tr.Album)
|
t.album.SetText(tr.Album)
|
||||||
t.dur.Segments[0].(*widget.TextSegment).Text = util.SecondsToTimeString(float64(tr.Duration))
|
t.dur.Text = util.SecondsToTimeString(float64(tr.Duration))
|
||||||
t.year.Segments[0].(*widget.TextSegment).Text = strconv.Itoa(tr.Year)
|
t.year.Text = strconv.Itoa(tr.Year)
|
||||||
t.plays.Segments[0].(*widget.TextSegment).Text = strconv.Itoa(int(tr.PlayCount))
|
t.plays.Text = strconv.Itoa(int(tr.PlayCount))
|
||||||
t.bitrate.Segments[0].(*widget.TextSegment).Text = strconv.Itoa(tr.BitRate)
|
t.bitrate.Text = strconv.Itoa(tr.BitRate)
|
||||||
t.size.Segments[0].(*widget.TextSegment).Text = util.BytesToSizeString(tr.Size)
|
t.size.Text = util.BytesToSizeString(tr.Size)
|
||||||
t.path.Segments[0].(*widget.TextSegment).Text = tr.FilePath
|
t.path.Text = tr.FilePath
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update track num if needed
|
// Update track num if needed
|
||||||
@@ -775,25 +781,19 @@ func (t *TrackRow) Update(tm *trackModel, rowNum int) {
|
|||||||
} else {
|
} else {
|
||||||
str = strconv.Itoa(rowNum)
|
str = strconv.Itoa(rowNum)
|
||||||
}
|
}
|
||||||
t.num.Segments[0].(*widget.TextSegment).Text = str
|
t.num.Text = str
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update play count if needed
|
// Update play count if needed
|
||||||
if tr.PlayCount != t.playCount {
|
if tr.PlayCount != t.playCount {
|
||||||
t.playCount = tr.PlayCount
|
t.playCount = tr.PlayCount
|
||||||
t.plays.Segments[0].(*widget.TextSegment).Text = strconv.Itoa(int(tr.PlayCount))
|
t.plays.Text = strconv.Itoa(int(tr.PlayCount))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Render whether track is playing or not
|
// Render whether track is playing or not
|
||||||
if isPlaying := t.tracklist.nowPlayingID == tr.ID; isPlaying != t.isPlaying {
|
if isPlaying := t.tracklist.nowPlayingID == tr.ID; isPlaying != t.isPlaying {
|
||||||
t.isPlaying = isPlaying
|
t.isPlaying = isPlaying
|
||||||
t.name.Segments[0].(*widget.TextSegment).Style.TextStyle.Bold = isPlaying
|
t.name.Segments[0].(*widget.TextSegment).Style.TextStyle.Bold = isPlaying
|
||||||
t.dur.Segments[0].(*widget.TextSegment).Style.TextStyle.Bold = isPlaying
|
|
||||||
t.year.Segments[0].(*widget.TextSegment).Style.TextStyle.Bold = isPlaying
|
|
||||||
t.plays.Segments[0].(*widget.TextSegment).Style.TextStyle.Bold = isPlaying
|
|
||||||
t.bitrate.Segments[0].(*widget.TextSegment).Style.TextStyle.Bold = isPlaying
|
|
||||||
t.size.Segments[0].(*widget.TextSegment).Style.TextStyle.Bold = isPlaying
|
|
||||||
t.path.Segments[0].(*widget.TextSegment).Style.TextStyle.Bold = isPlaying
|
|
||||||
|
|
||||||
if isPlaying {
|
if isPlaying {
|
||||||
t.Content.(*fyne.Container).Objects[0] = container.NewCenter(t.playingIcon)
|
t.Content.(*fyne.Container).Objects[0] = container.NewCenter(t.playingIcon)
|
||||||
|
|||||||
Reference in New Issue
Block a user