feat: Add Share option for artists, albums, and tracks

This feature is only available on the Subsonic media provider. Using a
Jellyfin server will make Supersonic hide the "Share" option.

* Artists: Option available in grid view and in Artist view.
* Albums: Option available in grid view and in Album view.
* Tracks: Option available in tracklist, when a single track is selected.
This commit is contained in:
Michael Manganiello
2024-03-03 20:44:58 -03:00
parent ee0e18e118
commit 9d3725cb51
16 changed files with 174 additions and 6 deletions
+5
View File
@@ -3,6 +3,7 @@ package mediaprovider
import (
"image"
"io"
"net/url"
"strings"
)
@@ -148,6 +149,10 @@ type SupportsRating interface {
SetRating(params RatingFavoriteParameters, rating int) error
}
type SupportsSharing interface {
CreateShareURL(id string) (*url.URL, error)
}
type JukeboxProvider interface {
JukeboxStart() error
JukeboxStop() error
@@ -5,6 +5,7 @@ import (
"image"
"io"
"math"
"net/url"
"strconv"
"strings"
"sync"
@@ -315,6 +316,20 @@ func (s *subsonicMediaProvider) SetRating(params mediaprovider.RatingFavoritePar
return err
}
func (s *subsonicMediaProvider) CreateShareURL(id string) (*url.URL, error) {
share, err := s.client.CreateShare(id, nil)
if err != nil {
return nil, err
}
shareUrl, err := url.Parse(share.Url)
if err != nil {
return nil, err
}
return shareUrl, nil
}
func (s *subsonicMediaProvider) DownloadTrack(trackID string) (io.Reader, error) {
return s.client.Download(trackID)
}
+1 -1
View File
@@ -8,7 +8,7 @@ require (
github.com/deluan/sanitize v0.0.0-20230310221930-6e18967d9fc1
github.com/dweymouth/go-jellyfin v0.0.0-20231116161116-e800860bdacc
github.com/dweymouth/go-mpv v0.0.0-20230406003141-7f1858e503ee
github.com/dweymouth/go-subsonic v0.0.0-20231217175944-9b48c9ffc002
github.com/dweymouth/go-subsonic v0.0.0-20240225165422-3617754d751b
github.com/fsnotify/fsnotify v1.6.0
github.com/godbus/dbus/v5 v5.1.0
github.com/google/uuid v1.3.0
+2 -2
View File
@@ -75,8 +75,8 @@ github.com/dweymouth/go-jellyfin v0.0.0-20231116161116-e800860bdacc h1:wJy4U12Ys
github.com/dweymouth/go-jellyfin v0.0.0-20231116161116-e800860bdacc/go.mod h1:BMwS4vdjEYf1gmjPGSKCzWP/I6YlI6fkefJ9nsjBjaU=
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-subsonic v0.0.0-20231217175944-9b48c9ffc002 h1:DhWQZJObkCUSFmHu/eEzHRqy0R33DcR266nPAvZJE34=
github.com/dweymouth/go-subsonic v0.0.0-20231217175944-9b48c9ffc002/go.mod h1:OWtcumdQsan8uM6wmx6PqKhldaCthH10CQ+vb+94kzo=
github.com/dweymouth/go-subsonic v0.0.0-20240225165422-3617754d751b h1:LGUWXRcndcWwBN0HTFsCUlIjr8aakRsi9FlUX1CPSs8=
github.com/dweymouth/go-subsonic v0.0.0-20240225165422-3617754d751b/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.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
+5
View File
@@ -95,6 +95,11 @@ var ResShuffleSvg = &fyne.StaticResource{
StaticContent: []byte(
"<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<svg width=\"800px\" height=\"800px\" viewBox=\"0 0 24 24\" xmlns=\"http://www.w3.org/2000/svg\">\n <g>\n <path fill=\"none\" d=\"M0 0h24v24H0z\"/>\n <path d=\"M18 17.883V16l5 3-5 3v-2.09a9 9 0 0 1-6.997-5.365L11 14.54l-.003.006A9 9 0 0 1 2.725 20H2v-2h.725a7 7 0 0 0 6.434-4.243L9.912 12l-.753-1.757A7 7 0 0 0 2.725 6H2V4h.725a9 9 0 0 1 8.272 5.455L11 9.46l.003-.006A9 9 0 0 1 18 4.09V2l5 3-5 3V6.117a7 7 0 0 0-5.159 4.126L12.088 12l.753 1.757A7 7 0 0 0 18 17.883z\"/>\n </g>\n</svg>"),
}
var ResShareSvg = &fyne.StaticResource{
StaticName: "share.svg",
StaticContent: []byte(
"<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<svg width=\"800px\" height=\"800px\" viewBox=\"0 0 24 24\" xmlns=\"http://www.w3.org/2000/svg\">\n <g>\n <path d=\"M13.1202 17.0228L8.92129 14.7324C8.19135 15.5125 7.15261 16 6 16C3.79086 16 2 14.2091 2 12C2 9.79086 3.79086 8 6 8C7.15255 8 8.19125 8.48746 8.92118 9.26746L13.1202 6.97713C13.0417 6.66441 13 6.33707 13 6C13 3.79086 14.7909 2 17 2C19.2091 2 21 3.79086 21 6C21 8.20914 19.2091 10 17 10C15.8474 10 14.8087 9.51251 14.0787 8.73246L9.87977 11.0228C9.9583 11.3355 10 11.6629 10 12C10 12.3371 9.95831 12.6644 9.87981 12.9771L14.0788 15.2675C14.8087 14.4875 15.8474 14 17 14C19.2091 14 21 15.7909 21 18C21 20.2091 19.2091 22 17 22C14.7909 22 13 20.2091 13 18C13 17.6629 13.0417 17.3355 13.1202 17.0228ZM6 14C7.10457 14 8 13.1046 8 12C8 10.8954 7.10457 10 6 10C4.89543 10 4 10.8954 4 12C4 13.1046 4.89543 14 6 14ZM17 8C18.1046 8 19 7.10457 19 6C19 4.89543 18.1046 4 17 4C15.8954 4 15 4.89543 15 6C15 7.10457 15.8954 8 17 8ZM17 20C18.1046 20 19 19.1046 19 18C19 16.8954 18.1046 16 17 16C15.8954 16 15 16.8954 15 18C15 19.1046 15.8954 20 17 20Z\" />\n </g>\n</svg>\n"),
}
var ResUpdownarrowSvg = &fyne.StaticResource{
StaticName: "updownarrow.svg",
StaticContent: []byte(
+1
View File
@@ -18,6 +18,7 @@ fyne bundle -append -prefix Res icons/publicdomain/filter.svg >> bundled.go
fyne bundle -append -prefix Res icons/remix_design/repeat.svg >> bundled.go
fyne bundle -append -prefix Res icons/remix_design/repeatone.svg >> bundled.go
fyne bundle -append -prefix Res icons/remix_design/shuffle.svg >> bundled.go
fyne bundle -append -prefix Res icons/remix_design/share.svg >> bundled.go
fyne bundle -append -prefix Res icons/remix_design/updownarrow.svg >> bundled.go
fyne bundle -append -prefix Res themes/default.toml >> bundled.go
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<svg width="800px" height="800px" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<g>
<path d="M13.1202 17.0228L8.92129 14.7324C8.19135 15.5125 7.15261 16 6 16C3.79086 16 2 14.2091 2 12C2 9.79086 3.79086 8 6 8C7.15255 8 8.19125 8.48746 8.92118 9.26746L13.1202 6.97713C13.0417 6.66441 13 6.33707 13 6C13 3.79086 14.7909 2 17 2C19.2091 2 21 3.79086 21 6C21 8.20914 19.2091 10 17 10C15.8474 10 14.8087 9.51251 14.0787 8.73246L9.87977 11.0228C9.9583 11.3355 10 11.6629 10 12C10 12.3371 9.95831 12.6644 9.87981 12.9771L14.0788 15.2675C14.8087 14.4875 15.8474 14 17 14C19.2091 14 21 15.7909 21 18C21 20.2091 19.2091 22 17 22C14.7909 22 13 20.2091 13 18C13 17.6629 13.0417 17.3355 13.1202 17.0228ZM6 14C7.10457 14 8 13.1046 8 12C8 10.8954 7.10457 10 6 10C4.89543 10 4 10.8954 4 12C4 13.1046 4.89543 14 6 14ZM17 8C18.1046 8 19 7.10457 19 6C19 4.89543 18.1046 4 17 4C15.8954 4 15 4.89543 15 6C15 7.10457 15.8954 8 17 8ZM17 20C18.1046 20 19 19.1046 19 18C19 16.8954 18.1046 16 17 16C15.8954 16 15 16.8954 15 18C15 19.1046 15.8954 20 17 20Z" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

+13 -1
View File
@@ -94,7 +94,9 @@ func newAlbumPage(
a.tracklist.SetVisibleColumns(a.cfg.TracklistColumns)
a.tracklist.SetSorting(sort)
_, canRate := a.mp.(mediaprovider.SupportsRating)
_, canShare := a.mp.(mediaprovider.SupportsRating)
a.tracklist.Options.DisableRating = !canRate
a.tracklist.Options.HideSharing = !canShare
a.tracklist.OnVisibleColumnsChanged = func(cols []string) {
a.cfg.TracklistColumns = cols
}
@@ -246,7 +248,17 @@ func NewAlbumPageHeader(page *AlbumPage) *AlbumPageHeader {
a.page.contr.ShowAlbumInfoDialog(a.albumID, a.titleLabel.String(), a.cover.Image())
})
info.Icon = theme.InfoIcon()
menu := fyne.NewMenu("", queue, playlist, download, info)
menuItems := []*fyne.MenuItem{queue, playlist, download, info}
_, canShare := page.mp.(mediaprovider.SupportsSharing)
if canShare {
share := fyne.NewMenuItem("Share...", func() {
a.page.contr.CreateShareURL(a.albumID)
})
share.Icon = myTheme.ShareIcon
menuItems = append(menuItems, share)
}
menu := fyne.NewMenu("", menuItems...)
pop = widget.NewPopUpMenu(menu, fyne.CurrentApp().Driver().CanvasForObject(a))
}
pos := fyne.CurrentApp().Driver().AbsolutePositionForObject(menuBtn)
+28 -1
View File
@@ -237,7 +237,9 @@ func (a *ArtistPage) showTopTracks() {
}
tl.Options = widgets.TracklistOptions{AutoNumber: true}
_, canRate := a.mp.(mediaprovider.SupportsRating)
_, canShare := a.mp.(mediaprovider.SupportsSharing)
tl.Options.DisableRating = !canRate
tl.Options.HideSharing = !canShare
tl.SetVisibleColumns(a.cfg.TracklistColumns)
tl.SetSorting(a.trackSort)
tl.OnVisibleColumnsChanged = func(cols []string) {
@@ -292,6 +294,7 @@ type ArtistPageHeader struct {
favoriteBtn *widgets.FavoriteButton
playBtn *widget.Button
playRadioBtn *widget.Button
menuBtn *widget.Button
container *fyne.Container
}
@@ -318,6 +321,25 @@ func NewArtistPageHeader(page *ArtistPage) *ArtistPageHeader {
go a.artistPage.contr.PlayArtistDiscography(a.artistID, false /*shuffle*/)
})
a.playRadioBtn = widget.NewButtonWithIcon("Play Artist Radio", myTheme.ShuffleIcon, a.artistPage.playArtistRadio)
_, canShare := a.artistPage.mp.(mediaprovider.SupportsSharing)
if canShare {
var pop *widget.PopUpMenu
a.menuBtn = widget.NewButtonWithIcon("", theme.MoreHorizontalIcon(), nil)
a.menuBtn.OnTapped = func() {
if pop == nil {
share := fyne.NewMenuItem("Share...", func() {
a.artistPage.contr.CreateShareURL(a.artistID)
})
share.Icon = myTheme.ShareIcon
menu := fyne.NewMenu("", share)
pop = widget.NewPopUpMenu(menu, fyne.CurrentApp().Driver().CanvasForObject(a))
}
pos := fyne.CurrentApp().Driver().AbsolutePositionForObject(a.menuBtn)
pop.ShowAtPosition(fyne.NewPos(pos.X, pos.Y+a.menuBtn.Size().Height))
}
}
a.biographyDisp.Wrapping = fyne.TextWrapWord
a.biographyDisp.Truncation = fyne.TextTruncateEllipsis
a.ExtendBaseWidget(a)
@@ -407,12 +429,17 @@ func (a *ArtistPageHeader) toggleFavorited() {
}
func (a *ArtistPageHeader) createContainer() {
btnContainer := container.NewHBox(util.NewHSpace(2), a.favoriteBtn, a.playBtn, a.playRadioBtn)
if a.menuBtn != nil {
btnContainer.Add(a.menuBtn)
}
a.container = util.AddHeaderBackground(
container.NewBorder(nil, nil, a.artistImage, nil,
container.NewVBox(
container.New(&layouts.VboxCustomPadding{ExtraPad: -10},
a.titleDisp, a.biographyDisp, a.similarArtists),
container.NewHBox(util.NewHSpace(2), a.favoriteBtn, a.playBtn, a.playRadioBtn)),
btnContainer),
))
}
+2
View File
@@ -395,7 +395,9 @@ func (a *FavoritesPage) onShowFavoriteSongs() {
}
tracklist.Options = widgets.TracklistOptions{AutoNumber: true}
_, canRate := a.mp.(mediaprovider.SupportsRating)
_, canShare := a.mp.(mediaprovider.SupportsSharing)
tracklist.Options.DisableRating = !canRate
tracklist.Options.HideSharing = !canShare
tracklist.SetVisibleColumns(a.cfg.TracklistColumns)
tracklist.SetSorting(a.trackSort)
tracklist.OnVisibleColumnsChanged = func(cols []string) {
+2
View File
@@ -87,10 +87,12 @@ func newPlaylistPage(
conf.TracklistColumns = cols
}
_, canRate := a.sm.Server.(mediaprovider.SupportsRating)
_, canShare := a.sm.Server.(mediaprovider.SupportsSharing)
remove := fyne.NewMenuItem("Remove from playlist", a.onRemoveSelectedFromPlaylist)
remove.Icon = theme.ContentClearIcon()
a.tracklist.Options = widgets.TracklistOptions{
DisableRating: !canRate,
HideSharing: !canShare,
AuxiliaryMenuItems: []*fyne.MenuItem{
util.NewReorderTracksSubmenu(a.doSetNewTrackOrder),
remove,
+4
View File
@@ -40,6 +40,7 @@ type tracksPageState struct {
conf *backend.TracksPageConfig
mp mediaprovider.MediaProvider
canRate bool
canShare bool
}
func NewTracksPage(contr *controller.Controller, conf *backend.TracksPageConfig, pool *util.WidgetPool, mp mediaprovider.MediaProvider) *TracksPage {
@@ -48,9 +49,11 @@ func NewTracksPage(contr *controller.Controller, conf *backend.TracksPageConfig,
t.tracklist = t.obtainTracklist()
_, t.canRate = mp.(mediaprovider.SupportsRating)
_, t.canShare = mp.(mediaprovider.SupportsSharing)
t.tracklist.Options = widgets.TracklistOptions{
DisableSorting: true,
DisableRating: !t.canRate,
HideSharing: !t.canShare,
AutoNumber: true,
}
t.tracklist.SetVisibleColumns(conf.TracklistColumns)
@@ -137,6 +140,7 @@ func (t *TracksPage) doSearch(query string) {
AutoNumber: true,
DisableSorting: true,
DisableRating: !t.canRate,
HideSharing: !t.canShare,
}
t.searchTracklist.SetVisibleColumns(t.conf.TracklistColumns)
t.searchTracklist.SetNowPlaying(t.nowPlayingID)
+54
View File
@@ -22,8 +22,10 @@ import (
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/canvas"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/dialog"
"fyne.io/fyne/v2/layout"
"fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget"
)
@@ -138,6 +140,13 @@ func (m *Controller) connectTracklistActionsWithReplayGainMode(tracklist *widget
m.ClosePopUpOnEscape(pop)
}
tracklist.OnDownload = m.ShowDownloadDialog
_, canShare := m.App.ServerManager.Server.(mediaprovider.SupportsSharing)
if canShare {
tracklist.OnShare = func(trackID string) {
go m.CreateShareURL(trackID)
}
}
}
func (m *Controller) ConnectAlbumGridActions(grid *widgets.GridView) {
@@ -173,6 +182,13 @@ func (m *Controller) ConnectAlbumGridActions(grid *widgets.GridView) {
m.ShowDownloadDialog(album.Tracks, album.Name)
}()
}
_, canShare := m.App.ServerManager.Server.(mediaprovider.SupportsSharing)
if canShare {
grid.OnShare = func(albumID string) {
go m.CreateShareURL(albumID)
}
}
}
func (m *Controller) ConnectArtistGridActions(grid *widgets.GridView) {
@@ -662,6 +678,44 @@ func (c *Controller) SetTrackRatings(trackIDs []string, rating int) {
}
}
func (c *Controller) CreateShareURL(id string) {
go func() {
r, ok := c.App.ServerManager.Server.(mediaprovider.SupportsSharing)
if !ok {
return
}
shareUrl, err := r.CreateShareURL(id)
if err != nil {
log.Printf("error creating share URL: %v", err)
return
}
hyperlink := widget.NewHyperlink(shareUrl.String(), shareUrl)
dlg := dialog.NewCustom("Share content", "OK",
container.NewHBox(
hyperlink,
widget.NewButtonWithIcon("", theme.ContentCopyIcon(), func() {
c.MainWindow.Clipboard().SetContent(hyperlink.Text)
}),
widget.NewButtonWithIcon("", theme.ViewRefreshIcon(), func() {
shareUrl, err := r.CreateShareURL(id)
if err != nil {
log.Printf("error creating share URL: %v", err)
return
}
hyperlink.Text = shareUrl.String()
hyperlink.URL = shareUrl
hyperlink.Refresh()
}),
),
c.MainWindow,
)
dlg.Show()
}()
}
func (c *Controller) ShowDownloadDialog(tracks []*mediaprovider.Track, downloadName string) {
numTracks := len(tracks)
var fileName string
+1
View File
@@ -31,6 +31,7 @@ var (
NotFavoriteIcon fyne.Resource = theme.NewThemedResource(res.ResHeartOutlineSvg)
NowPlayingIcon fyne.Resource = theme.NewThemedResource(res.ResHeadphonesSvg)
PlaylistIcon fyne.Resource = theme.NewThemedResource(res.ResPlaylistSvg)
ShareIcon fyne.Resource = theme.NewThemedResource(res.ResShareSvg)
ShuffleIcon fyne.Resource = theme.NewThemedResource(res.ResShuffleSvg)
TracksIcon fyne.Resource = theme.NewThemedResource(res.ResMusicnotesSvg)
GenreIcon fyne.Resource = theme.NewThemedResource(res.ResTheatermasksSvg)
+11 -1
View File
@@ -93,6 +93,7 @@ type GridViewState struct {
OnAddToQueue func(id string)
OnAddToPlaylist func(id string)
OnDownload func(id string)
OnShare func(id string)
OnShowItemPage func(id string)
OnShowSecondaryPage func(id string)
@@ -407,8 +408,17 @@ func (g *GridView) showContextMenu(card *GridViewItem, pos fyne.Position) {
}
})
download.Icon = theme.DownloadIcon()
menuItems := []*fyne.MenuItem{play, shuffle, queue, playlist, download}
g.menu = widget.NewPopUpMenu(fyne.NewMenu("", play, shuffle, queue, playlist, download),
if g.OnShare != nil {
share := fyne.NewMenuItem("Share...", func() {
g.OnShare(g.menuGridViewItemId)
})
share.Icon = myTheme.ShareIcon
menuItems = append(menuItems, share)
}
g.menu = widget.NewPopUpMenu(fyne.NewMenu("", menuItems...),
fyne.CurrentApp().Driver().CanvasForObject(g))
}
g.menu.ShowAtPosition(pos)
+24
View File
@@ -72,6 +72,9 @@ type TracklistOptions struct {
// Disables the five star rating widget.
DisableRating bool
// Hides the sharing option.
HideSharing bool
}
type Tracklist struct {
@@ -87,6 +90,7 @@ type Tracklist struct {
OnSetFavorite func(trackIDs []string, fav bool)
OnSetRating func(trackIDs []string, rating int)
OnDownload func(tracks []*mediaprovider.Track, downloadName string)
OnShare func(trackID string)
OnShowArtistPage func(artistID string)
OnShowAlbumPage func(albumID string)
@@ -108,6 +112,7 @@ type Tracklist struct {
list *FocusList
ctxMenu *fyne.Menu
ratingSubmenu *fyne.MenuItem
shareMenuItem *fyne.MenuItem
container *fyne.Container
}
@@ -555,6 +560,13 @@ func (t *Tracklist) onShowContextMenu(e *fyne.PointEvent, trackIdx int) {
})
unfavorite.Icon = myTheme.NotFavoriteIcon
t.ctxMenu.Items = append(t.ctxMenu.Items, playlist, download)
if !t.Options.HideSharing {
t.shareMenuItem = fyne.NewMenuItem("Share...", func() {
t.onShare(t.selectedTracks())
})
t.shareMenuItem.Icon = myTheme.ShareIcon
t.ctxMenu.Items = append(t.ctxMenu.Items, t.shareMenuItem)
}
t.ctxMenu.Items = append(t.ctxMenu.Items, fyne.NewMenuItemSeparator())
t.ctxMenu.Items = append(t.ctxMenu.Items, favorite, unfavorite)
t.ratingSubmenu = util.NewRatingSubmenu(func(rating int) {
@@ -567,6 +579,9 @@ func (t *Tracklist) onShowContextMenu(e *fyne.PointEvent, trackIdx int) {
}
}
t.ratingSubmenu.Disabled = t.Options.DisableRating
if t.shareMenuItem != nil {
t.shareMenuItem.Disabled = len(t.selectedTracks()) != 1
}
widget.ShowPopUpMenuAtPosition(t.ctxMenu, fyne.CurrentApp().Driver().CanvasForObject(t), e.AbsolutePosition)
}
@@ -629,6 +644,15 @@ func (t *Tracklist) onDownload(tracks []*mediaprovider.Track, downloadName strin
}
}
func (t *Tracklist) onShare(tracks []*mediaprovider.Track) {
if t.OnShare != nil {
selectedTrackIDs := t.SelectedTrackIDs()
if len(selectedTrackIDs) > 0 {
t.OnShare(selectedTrackIDs[0])
}
}
}
func (t *Tracklist) selectedTracks() []*mediaprovider.Track {
t.tracksMutex.RLock()
defer t.tracksMutex.RUnlock()