Merge pull request #424 from dweymouth/feature/track-info-dialog

Add track info dialog
This commit is contained in:
Drew Weymouth
2024-07-18 17:45:39 -07:00
committed by GitHub
9 changed files with 249 additions and 10 deletions
+5 -1
View File
@@ -14,7 +14,11 @@ func GetSimilarSongsFallback(mp mediaprovider.MediaProvider, track *mediaprovide
tracks, _ = mp.GetSimilarTracks(track.ArtistIDs[0], count) tracks, _ = mp.GetSimilarTracks(track.ArtistIDs[0], count)
} }
if len(tracks) == 0 { if len(tracks) == 0 {
tracks, _ = mp.GetRandomTracks(track.Genre, count) genre := ""
if len(track.Genres) > 0 {
genre = track.Genres[0]
}
tracks, _ = mp.GetRandomTracks(genre, count)
} }
// make sure to exclude the song itself from the similar list // make sure to exclude the song itself from the similar list
+14 -1
View File
@@ -1,5 +1,7 @@
package mediaprovider package mediaprovider
import "time"
// Bit field flag for the ReleaseTypes property // Bit field flag for the ReleaseTypes property
type ReleaseType = int32 type ReleaseType = int32
@@ -90,7 +92,7 @@ type Track struct {
Duration int Duration int
TrackNumber int TrackNumber int
DiscNumber int DiscNumber int
Genre string Genres []string
ArtistIDs []string ArtistIDs []string
ArtistNames []string ArtistNames []string
Album string Album string
@@ -100,9 +102,20 @@ type Track struct {
Favorite bool Favorite bool
Size int64 Size int64
PlayCount int PlayCount int
LastPlayed time.Time
FilePath string FilePath string
BitRate int BitRate int
ContentType string
Comment string Comment string
BPM int
ReplayGain ReplayGainInfo
}
type ReplayGainInfo struct {
TrackGain float64
AlbumGain float64
TrackPeak float64
AlbumPeak float64
} }
type Playlist struct { type Playlist struct {
@@ -493,6 +493,22 @@ func toTrack(ch *subsonic.Child) *mediaprovider.Track {
artistIDs = append(artistIDs, ch.ArtistID) artistIDs = append(artistIDs, ch.ArtistID)
} }
var rGain mediaprovider.ReplayGainInfo
if rg := ch.ReplayGain; rg != nil {
rGain.AlbumGain = rg.AlbumGain
rGain.TrackGain = rg.TrackGain
rGain.AlbumPeak = rg.AlbumPeak
rGain.TrackPeak = rg.TrackPeak
}
var genres []string
if len(ch.Genres) > 0 {
genres = sharedutil.MapSlice(ch.Genres, func(idName subsonic.IDName) string {
return idName.Name
})
} else if ch.Genre != "" {
genres = []string{ch.Genre}
}
return &mediaprovider.Track{ return &mediaprovider.Track{
ID: ch.ID, ID: ch.ID,
CoverArtID: ch.CoverArt, CoverArtID: ch.CoverArt,
@@ -501,7 +517,7 @@ func toTrack(ch *subsonic.Child) *mediaprovider.Track {
Duration: ch.Duration, Duration: ch.Duration,
TrackNumber: ch.Track, TrackNumber: ch.Track,
DiscNumber: ch.DiscNumber, DiscNumber: ch.DiscNumber,
Genre: ch.Genre, Genres: genres,
ArtistIDs: artistIDs, ArtistIDs: artistIDs,
ArtistNames: artistNames, ArtistNames: artistNames,
Album: ch.Album, Album: ch.Album,
@@ -510,10 +526,14 @@ func toTrack(ch *subsonic.Child) *mediaprovider.Track {
Rating: ch.UserRating, Rating: ch.UserRating,
Favorite: !ch.Starred.IsZero(), Favorite: !ch.Starred.IsZero(),
PlayCount: int(ch.PlayCount), PlayCount: int(ch.PlayCount),
LastPlayed: ch.Played,
FilePath: ch.Path, FilePath: ch.Path,
Size: ch.Size, Size: ch.Size,
BitRate: ch.BitRate, BitRate: ch.BitRate,
ContentType: ch.ContentType,
Comment: ch.Comment, Comment: ch.Comment,
BPM: ch.BPM,
ReplayGain: rGain,
} }
} }
+3 -5
View File
@@ -254,7 +254,7 @@ func (m *MPRISHandler) Metadata() (types.Metadata, error) {
var meta mediaprovider.MediaItemMetadata var meta mediaprovider.MediaItemMetadata
// metadata that can come only from tracks // metadata that can come only from tracks
var discNumber, trackNumber, userRating, playCount, year int var discNumber, trackNumber, userRating, playCount, year int
var genre string var genres []string
if np := m.pm.NowPlaying(); np != nil && status.State != player.Stopped { if np := m.pm.NowPlaying(); np != nil && status.State != player.Stopped {
meta = np.Metadata() meta = np.Metadata()
@@ -264,7 +264,7 @@ func (m *MPRISHandler) Metadata() (types.Metadata, error) {
userRating = track.Rating userRating = track.Rating
playCount = track.PlayCount playCount = track.PlayCount
year = track.Year year = track.Year
genre = track.Genre genres = track.Genres
} }
} }
var artURL string var artURL string
@@ -284,9 +284,7 @@ func (m *MPRISHandler) Metadata() (types.Metadata, error) {
UserRating: float64(userRating) / 5, UserRating: float64(userRating) / 5,
UseCount: playCount, UseCount: playCount,
ArtUrl: artURL, ArtUrl: artURL,
} Genre: genres,
if genre != "" {
mprisMeta.Genre = []string{genre}
} }
if year != 0 { if year != 0 {
mprisMeta.ContentCreated = strconv.Itoa(year) mprisMeta.ContentCreated = strconv.Itoa(year)
+1 -1
View File
@@ -12,7 +12,7 @@ require (
github.com/dweymouth/fyne-lyrics v0.0.0-20240528234907-15eee7ce5e64 github.com/dweymouth/fyne-lyrics v0.0.0-20240528234907-15eee7ce5e64
github.com/dweymouth/go-jellyfin v0.0.0-20240517151952-5ceca61cb645 github.com/dweymouth/go-jellyfin v0.0.0-20240517151952-5ceca61cb645
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-20240603150834-605046e7c78a github.com/dweymouth/go-subsonic v0.0.0-20240716154859-c40a4519e1e0
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
github.com/pelletier/go-toml/v2 v2.0.8 github.com/pelletier/go-toml/v2 v2.0.8
+2
View File
@@ -87,6 +87,8 @@ github.com/dweymouth/go-mpv v0.0.0-20230406003141-7f1858e503ee h1:ZGyJ6wp7CAfT31
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-20240603150834-605046e7c78a h1:jcHDGzvpsyvEt8i0vqB92jHIfWJUt106QITrPoYjSWo= github.com/dweymouth/go-subsonic v0.0.0-20240603150834-605046e7c78a h1:jcHDGzvpsyvEt8i0vqB92jHIfWJUt106QITrPoYjSWo=
github.com/dweymouth/go-subsonic v0.0.0-20240603150834-605046e7c78a/go.mod h1:OWtcumdQsan8uM6wmx6PqKhldaCthH10CQ+vb+94kzo= github.com/dweymouth/go-subsonic v0.0.0-20240603150834-605046e7c78a/go.mod h1:OWtcumdQsan8uM6wmx6PqKhldaCthH10CQ+vb+94kzo=
github.com/dweymouth/go-subsonic v0.0.0-20240716154859-c40a4519e1e0 h1:Zf27NWT1N889/toYK7/x7y7ERw8iJfAHBHGiMyYxaLY=
github.com/dweymouth/go-subsonic v0.0.0-20240716154859-c40a4519e1e0/go.mod h1:OWtcumdQsan8uM6wmx6PqKhldaCthH10CQ+vb+94kzo=
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=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
+33
View File
@@ -146,6 +146,7 @@ func (m *Controller) connectTracklistActionsWithReplayGainMode(tracklist *widget
tracklist.OnShare = func(trackID string) { tracklist.OnShare = func(trackID string) {
go m.ShowShareDialog(trackID) go m.ShowShareDialog(trackID)
} }
tracklist.OnShowTrackInfo = m.ShowTrackInfoDialog
tracklist.OnPlaySongRadio = func(track *mediaprovider.Track) { tracklist.OnPlaySongRadio = func(track *mediaprovider.Track) {
go func() { go func() {
tracks, err := m.GetSongRadioTracks(track) tracks, err := m.GetSongRadioTracks(track)
@@ -858,6 +859,38 @@ func (c *Controller) ShowAlbumInfoDialog(albumID, albumName string, albumCover i
}() }()
} }
func (c *Controller) ShowTrackInfoDialog(track *mediaprovider.Track) {
info := dialogs.NewTrackInfoDialog(track)
pop := widget.NewModalPopUp(info, c.MainWindow.Canvas())
info.OnDismiss = func() {
pop.Hide()
c.doModalClosed()
}
info.OnNavigateToAlbum = func(albumID string) {
info.OnDismiss()
c.NavigateTo(AlbumRoute(albumID))
}
info.OnNavigateToArtist = func(artistID string) {
info.OnDismiss()
c.NavigateTo(ArtistRoute(artistID))
}
info.OnNavigateToGenre = func(genre string) {
info.OnDismiss()
c.NavigateTo(GenreRoute(genre))
}
info.OnCopyFilePath = func() {
c.MainWindow.Clipboard().SetContent(track.FilePath)
}
c.ClosePopUpOnEscape(pop)
winSize := c.MainWindow.Canvas().Size()
popMin := pop.MinSize()
width := fyne.Max(700, fyne.Max(popMin.Width, winSize.Width*0.6))
height := fyne.Max(500, fyne.Max(popMin.Height, winSize.Height*0.7))
pop.Resize(fyne.NewSize(width, height))
c.haveModal = true
pop.Show()
}
func (c *Controller) GetSongRadioTracks(sourceTrack *mediaprovider.Track) ([]*mediaprovider.Track, error) { func (c *Controller) GetSongRadioTracks(sourceTrack *mediaprovider.Track) ([]*mediaprovider.Track, error) {
radioTracks, err := c.App.ServerManager.Server.GetSongRadio(sourceTrack.ID, 100) radioTracks, err := c.App.ServerManager.Server.GetSongRadio(sourceTrack.ID, 100)
if err != nil { if err != nil {
+160
View File
@@ -0,0 +1,160 @@
package dialogs
import (
"fmt"
"strconv"
"time"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/layout"
"fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget"
"github.com/dweymouth/supersonic/backend/mediaprovider"
"github.com/dweymouth/supersonic/ui/util"
"github.com/dweymouth/supersonic/ui/widgets"
)
type TrackInfoDialog struct {
widget.BaseWidget
OnDismiss func()
OnNavigateToArtist func(artistID string)
OnNavigateToAlbum func(albumID string)
OnNavigateToGenre func(genre string)
OnCopyFilePath func()
track *mediaprovider.Track
}
func NewTrackInfoDialog(track *mediaprovider.Track) *TrackInfoDialog {
t := &TrackInfoDialog{track: track}
t.ExtendBaseWidget(t)
return t
}
func (t *TrackInfoDialog) CreateRenderer() fyne.WidgetRenderer {
c := container.New(layout.NewFormLayout())
addFormRow(c, "Title", t.track.Title)
c.Add(newFormText("Artist", true))
artists := widgets.NewMultiHyperlink()
artists.BuildSegments(t.track.ArtistNames, t.track.ArtistIDs)
artists.OnTapped = func(id string) {
if t.OnNavigateToArtist != nil {
t.OnNavigateToArtist(id)
}
}
c.Add(artists)
c.Add(newFormText("Album", true))
album := widget.NewHyperlink(t.track.Album, nil)
album.OnTapped = func() {
if t.OnNavigateToAlbum != nil {
t.OnNavigateToAlbum(t.track.AlbumID)
}
}
c.Add(album)
if len(t.track.Genres) > 0 {
c.Add(newFormText("Genres", true))
genres := widgets.NewMultiHyperlink()
genres.BuildSegments(t.track.Genres, t.track.Genres)
genres.OnTapped = func(g string) {
if t.OnNavigateToGenre != nil {
t.OnNavigateToGenre(g)
}
}
c.Add(genres)
}
addFormRow(c, "Duration", util.SecondsToTimeString(float64(t.track.Duration)))
copyBtn := widgets.NewIconButton(theme.ContentCopyIcon(), func() {
if t.OnCopyFilePath != nil {
t.OnCopyFilePath()
}
})
copyBtn.IconSize = widgets.IconButtonSizeSmaller
btnCtr := container.New(layout.NewCustomPaddedLayout(8, 0, 10, 0),
container.NewVBox(copyBtn, layout.NewSpacer()))
c.Add(container.NewHBox(btnCtr, newFormText("File path", true)))
c.Add(newFormText(t.track.FilePath, false))
addFormRow(c, "Comment", t.track.Comment)
addFormRow(c, "Year", strconv.Itoa(t.track.Year))
addFormRow(c, "Track number", strconv.Itoa(t.track.TrackNumber))
addFormRow(c, "Disc number", strconv.Itoa(t.track.DiscNumber))
if t.track.BPM > 0 {
addFormRow(c, "BPM", strconv.Itoa(t.track.BPM))
}
addFormRow(c, "Content type", t.track.ContentType)
addFormRow(c, "Bit rate", fmt.Sprintf("%d kbps", t.track.BitRate))
addFormRow(c, "File size", util.BytesToSizeString(t.track.Size))
addFormRow(c, "Play count", strconv.Itoa(t.track.PlayCount))
if !t.track.LastPlayed.IsZero() {
addFormRow(c, "Last played", t.track.LastPlayed.Format(time.RFC1123))
}
if t.track.ReplayGain.TrackPeak > 0 {
addFormRow(c, "Track gain", fmt.Sprintf("%0.2f dB", t.track.ReplayGain.TrackGain))
addFormRow(c, "Track peak", fmt.Sprintf("%0.6f", t.track.ReplayGain.TrackPeak))
}
if t.track.ReplayGain.AlbumPeak > 0 {
addFormRow(c, "Album gain", fmt.Sprintf("%0.2f dB", t.track.ReplayGain.AlbumGain))
addFormRow(c, "Album peak", fmt.Sprintf("%0.6f", t.track.ReplayGain.AlbumPeak))
}
title := widget.NewRichTextWithText("Track Info")
title.Segments[0].(*widget.TextSegment).Style.TextStyle.Bold = true
dismissBtn := widget.NewButton("Close", func() {
if t.OnDismiss != nil {
t.OnDismiss()
}
})
return widget.NewSimpleRenderer(
container.NewBorder(
/*top*/ container.NewHBox(layout.NewSpacer(), title, layout.NewSpacer()),
/*bottom*/ container.NewVBox(
widget.NewSeparator(),
container.NewHBox(layout.NewSpacer(), dismissBtn),
),
/*left/right*/ nil, nil,
/*center*/ container.New(layout.NewCustomPaddedLayout(10, 10, 15, 15),
container.NewScroll(c)),
),
)
}
func addFormRow(c *fyne.Container, left, right string) {
if right == "" {
return
}
c.Add(newFormText(left, true))
c.Add(newFormText(right, false))
}
func newFormText(text string, leftCol bool) *widget.RichText {
alignment := fyne.TextAlignLeading
if leftCol {
alignment = fyne.TextAlignTrailing
}
rt := widget.NewRichText(
&widget.TextSegment{
Text: text,
Style: widget.RichTextStyle{
TextStyle: fyne.TextStyle{Bold: leftCol},
Alignment: alignment,
},
},
)
if !leftCol {
rt.Wrapping = fyne.TextWrapWord
}
return rt
}
+10 -1
View File
@@ -78,6 +78,7 @@ type Tracklist struct {
OnShare func(trackID string) OnShare func(trackID string)
OnPlaySongRadio func(track *mediaprovider.Track) OnPlaySongRadio func(track *mediaprovider.Track)
OnReorderTracks func(trackIDs []string, insertPos int) OnReorderTracks func(trackIDs []string, insertPos int)
OnShowTrackInfo func(track *mediaprovider.Track)
OnShowArtistPage func(artistID string) OnShowArtistPage func(artistID string)
OnShowAlbumPage func(albumID string) OnShowAlbumPage func(albumID string)
@@ -101,6 +102,7 @@ type Tracklist struct {
ratingSubmenu *fyne.MenuItem ratingSubmenu *fyne.MenuItem
shareMenuItem *fyne.MenuItem shareMenuItem *fyne.MenuItem
songRadioMenuItem *fyne.MenuItem songRadioMenuItem *fyne.MenuItem
infoMenuItem *fyne.MenuItem
container *fyne.Container container *fyne.Container
} }
@@ -559,6 +561,12 @@ func (t *Tracklist) onShowContextMenu(e *fyne.PointEvent, trackIdx int) {
t.onDownload(t.selectedTracks(), "Selected tracks") t.onDownload(t.selectedTracks(), "Selected tracks")
}) })
download.Icon = theme.DownloadIcon() download.Icon = theme.DownloadIcon()
t.infoMenuItem = fyne.NewMenuItem("Show info...", func() {
if t.OnShowTrackInfo != nil {
t.OnShowTrackInfo(t.selectedTracks()[0])
}
})
t.infoMenuItem.Icon = theme.InfoIcon()
favorite := fyne.NewMenuItem("Set favorite", func() { favorite := fyne.NewMenuItem("Set favorite", func() {
t.onSetFavorites(t.selectedTracks(), true, true) t.onSetFavorites(t.selectedTracks(), true, true)
}) })
@@ -568,7 +576,7 @@ func (t *Tracklist) onShowContextMenu(e *fyne.PointEvent, trackIdx int) {
}) })
unfavorite.Icon = myTheme.NotFavoriteIcon unfavorite.Icon = myTheme.NotFavoriteIcon
t.ctxMenu.Items = append(t.ctxMenu.Items, fyne.NewMenuItemSeparator(), t.ctxMenu.Items = append(t.ctxMenu.Items, fyne.NewMenuItemSeparator(),
playlist, download) playlist, download, t.infoMenuItem)
t.shareMenuItem = fyne.NewMenuItem("Share...", func() { t.shareMenuItem = fyne.NewMenuItem("Share...", func() {
t.onShare(t.selectedTracks()) t.onShare(t.selectedTracks())
}) })
@@ -587,6 +595,7 @@ func (t *Tracklist) onShowContextMenu(e *fyne.PointEvent, trackIdx int) {
} }
t.ratingSubmenu.Disabled = t.Options.DisableRating t.ratingSubmenu.Disabled = t.Options.DisableRating
t.shareMenuItem.Disabled = t.Options.DisableSharing || len(t.selectedTracks()) != 1 t.shareMenuItem.Disabled = t.Options.DisableSharing || len(t.selectedTracks()) != 1
t.infoMenuItem.Disabled = len(t.selectedTracks()) != 1
widget.ShowPopUpMenuAtPosition(t.ctxMenu, fyne.CurrentApp().Driver().CanvasForObject(t), e.AbsolutePosition) widget.ShowPopUpMenuAtPosition(t.ctxMenu, fyne.CurrentApp().Driver().CanvasForObject(t), e.AbsolutePosition)
} }