handle multiple artists properly in UI

This commit is contained in:
Drew Weymouth
2023-10-22 11:40:06 -07:00
parent d6a1f70823
commit 4f002574c2
13 changed files with 73 additions and 48 deletions
+2 -1
View File
@@ -18,6 +18,7 @@ import (
import ( import (
"fmt" "fmt"
"log" "log"
"strings"
"unsafe" "unsafe"
"github.com/dweymouth/supersonic/backend/mediaprovider" "github.com/dweymouth/supersonic/backend/mediaprovider"
@@ -79,7 +80,7 @@ func NewMPMediaHandler(player *player.Player, playbackManager *PlaybackManager)
var artist string var artist string
if len(track.ArtistNames) > 0 { if len(track.ArtistNames) > 0 {
artist = track.ArtistNames[0] artist = strings.Join(track.ArtistNames, ", ")
} }
cArtist := C.CString(artist) cArtist := C.CString(artist)
+12
View File
@@ -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
View File
@@ -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)
} }
} }
+1 -1
View File
@@ -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 {
+1 -1
View File
@@ -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)},
} }
}) })
} }
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
View File
@@ -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()
+7 -7
View File
@@ -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() {
+13 -16
View File
@@ -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,18 +123,18 @@ 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 *widget.Hyperlink primaryText *widget.Hyperlink
secondaryText *MultiHyperlink secondaryText *MultiHyperlink
container *fyne.Container container *fyne.Container
@@ -145,7 +146,7 @@ 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 {
@@ -174,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(_ string) { // TODO g.secondaryText.OnTapped = func(s string) {
if g.OnShowSecondaryPage != nil { if g.OnShowSecondaryPage != nil {
g.OnShowSecondaryPage() g.OnShowSecondaryPage(s)
} }
} }
@@ -192,14 +193,14 @@ 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.Segments = []MultiHyperlinkSegment{{Text: model.Secondary, LinkID: model.SecondaryID}} g.secondaryText.BuildSegments(model.Secondary, model.SecondaryIDs)
g.secondaryText.Refresh() g.secondaryText.Refresh()
g.Cover.ResetPlayButton() g.Cover.ResetPlayButton()
} }
@@ -212,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)
} }
+22 -6
View File
@@ -38,6 +38,18 @@ func NewMultiHyperlink() *MultiHyperlink {
return c 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 { func (c *MultiHyperlink) getMinSegWidth() float32 {
if c.minSegWidthCached == 0 { if c.minSegWidthCached == 0 {
c.minSegWidthCached = fyne.MeasureText(", W", theme.TextSize(), fyne.TextStyle{}).Width c.minSegWidthCached = fyne.MeasureText(", W", theme.TextSize(), fyne.TextStyle{}).Width
@@ -82,7 +94,7 @@ func (c *MultiHyperlink) layoutObjects() {
if seg.LinkID == "" { if seg.LinkID == "" {
obj = c.updateOrReplaceLabel(obj, seg.Text) obj = c.updateOrReplaceLabel(obj, seg.Text)
} else { } else {
obj = c.updateOrReplaceHyperlink(obj, seg.Text) obj = c.updateOrReplaceHyperlink(obj, seg.Text, seg.LinkID)
} }
if appendingSegments { if appendingSegments {
c.content.Objects = append(c.content.Objects, obj) c.content.Objects = append(c.content.Objects, obj)
@@ -124,15 +136,17 @@ func (c *MultiHyperlink) updateOrReplaceLabel(obj fyne.CanvasObject, text string
return l return l
} }
func (c *MultiHyperlink) updateOrReplaceHyperlink(obj fyne.CanvasObject, text string) fyne.CanvasObject { func (c *MultiHyperlink) updateOrReplaceHyperlink(obj fyne.CanvasObject, text, link string) fyne.CanvasObject {
if obj != nil { if obj != nil {
if link, ok := obj.(*widget.Hyperlink); ok { if l, ok := obj.(*widget.Hyperlink); ok {
link.Text = text l.Text = text
return link l.OnTapped = func() { c.onSegmentTapped(link) }
return l
} }
} }
l := widget.NewHyperlink(text, nil) l := widget.NewHyperlink(text, nil)
l.Wrapping = fyne.TextTruncate l.Wrapping = fyne.TextTruncate
l.OnTapped = func() { c.onSegmentTapped(link) }
return l return l
} }
@@ -200,7 +214,9 @@ func (c *MultiHyperlink) updateOrReplaceHyperlinkSegment(seg widget.RichTextSegm
*/ */
func (c *MultiHyperlink) onSegmentTapped(linkID string) { func (c *MultiHyperlink) onSegmentTapped(linkID string) {
// TODO if c.OnTapped != nil {
c.OnTapped(linkID)
}
} }
func (c *MultiHyperlink) MinSize() fyne.Size { func (c *MultiHyperlink) MinSize() fyne.Size {
+4 -6
View File
@@ -41,7 +41,6 @@ func NewNowPlayingCard() *NowPlayingCard {
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.albumName.Wrapping = fyne.TextTruncate n.albumName.Wrapping = fyne.TextTruncate
n.trackName.Wrapping = fyne.TextTruncate n.trackName.Wrapping = fyne.TextTruncate
@@ -85,19 +84,18 @@ 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.Segments = []MultiHyperlinkSegment{{Text: artist}} n.artistName.BuildSegments(artists, artistIDs)
n.artistName.Hidden = artist == ""
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
} }
func (n *NowPlayingCard) OnAlbumNameTapped(f func()) { func (n *NowPlayingCard) OnAlbumNameTapped(f func()) {
+3 -3
View File
@@ -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:
@@ -698,7 +698,7 @@ func NewTrackRow(tracklist *Tracklist, playingIcon fyne.CanvasObject) *TrackRow
t.num = newTrailingAlignRichText() t.num = newTrailingAlignRichText()
t.name = newTruncatingRichText() t.name = newTruncatingRichText()
t.artist = NewMultiHyperlink() t.artist = NewMultiHyperlink()
//t.artist.OnTapped = func() { tracklist.onArtistTapped(t.artistID) } // TODO t.artist.OnTapped = tracklist.onArtistTapped
t.album = widget.NewHyperlink("", nil) t.album = widget.NewHyperlink("", nil)
t.album.Wrapping = fyne.TextTruncate t.album.Wrapping = fyne.TextTruncate
t.album.OnTapped = func() { tracklist.onAlbumTapped(t.albumID) } t.album.OnTapped = func() { tracklist.onAlbumTapped(t.albumID) }
@@ -748,7 +748,7 @@ 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.Segments = []MultiHyperlinkSegment{{Text: tr.ArtistNames[0], LinkID: tr.ArtistIDs[0]}} t.artist.BuildSegments(tr.ArtistNames, tr.ArtistIDs)
t.album.SetText(tr.Album) t.album.SetText(tr.Album)
t.dur.Segments[0].(*widget.TextSegment).Text = util.SecondsToTimeString(float64(tr.Duration)) t.dur.Segments[0].(*widget.TextSegment).Text = util.SecondsToTimeString(float64(tr.Duration))
t.year.Segments[0].(*widget.TextSegment).Text = strconv.Itoa(tr.Year) t.year.Segments[0].(*widget.TextSegment).Text = strconv.Itoa(tr.Year)