Merge pull request #336 from adamantike/feat/add-share-option

feat: Add Share option for artists, albums, and tracks
This commit is contained in:
Drew Weymouth
2024-03-03 16:39:18 -08:00
committed by GitHub
21 changed files with 226 additions and 18 deletions
+6
View File
@@ -3,6 +3,7 @@ package mediaprovider
import ( import (
"image" "image"
"io" "io"
"net/url"
"strings" "strings"
) )
@@ -148,6 +149,11 @@ type SupportsRating interface {
SetRating(params RatingFavoriteParameters, rating int) error SetRating(params RatingFavoriteParameters, rating int) error
} }
type SupportsSharing interface {
CreateShareURL(id string) (*url.URL, error)
CanShareArtists() bool
}
type JukeboxProvider interface { type JukeboxProvider interface {
JukeboxStart() error JukeboxStart() error
JukeboxStop() error JukeboxStop() error
@@ -5,6 +5,7 @@ import (
"image" "image"
"io" "io"
"math" "math"
"net/url"
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
@@ -315,6 +316,26 @@ func (s *subsonicMediaProvider) SetRating(params mediaprovider.RatingFavoritePar
return err 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) CanShareArtists() bool {
// TODO: Change to true when we decide to allow sharing artists, in case an OpenSubsonic extension
// is approved to share artists in addition to albums and tracks.
return false
}
func (s *subsonicMediaProvider) DownloadTrack(trackID string) (io.Reader, error) { func (s *subsonicMediaProvider) DownloadTrack(trackID string) (io.Reader, error) {
return s.client.Download(trackID) 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/deluan/sanitize v0.0.0-20230310221930-6e18967d9fc1
github.com/dweymouth/go-jellyfin v0.0.0-20231116161116-e800860bdacc github.com/dweymouth/go-jellyfin v0.0.0-20231116161116-e800860bdacc
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-20231217175944-9b48c9ffc002 github.com/dweymouth/go-subsonic v0.0.0-20240225165422-3617754d751b
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
+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-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 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-20231217175944-9b48c9ffc002 h1:DhWQZJObkCUSFmHu/eEzHRqy0R33DcR266nPAvZJE34= github.com/dweymouth/go-subsonic v0.0.0-20240225165422-3617754d751b h1:LGUWXRcndcWwBN0HTFsCUlIjr8aakRsi9FlUX1CPSs8=
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/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=
+5
View File
@@ -95,6 +95,11 @@ var ResShuffleSvg = &fyne.StaticResource{
StaticContent: []byte( 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>"), "<?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{ var ResUpdownarrowSvg = &fyne.StaticResource{
StaticName: "updownarrow.svg", StaticName: "updownarrow.svg",
StaticContent: []byte( 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/repeat.svg >> bundled.go
fyne bundle -append -prefix Res icons/remix_design/repeatone.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/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 icons/remix_design/updownarrow.svg >> bundled.go
fyne bundle -append -prefix Res themes/default.toml >> 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

+10 -1
View File
@@ -94,7 +94,9 @@ func newAlbumPage(
a.tracklist.SetVisibleColumns(a.cfg.TracklistColumns) a.tracklist.SetVisibleColumns(a.cfg.TracklistColumns)
a.tracklist.SetSorting(sort) a.tracklist.SetSorting(sort)
_, canRate := a.mp.(mediaprovider.SupportsRating) _, canRate := a.mp.(mediaprovider.SupportsRating)
_, canShare := a.mp.(mediaprovider.SupportsSharing)
a.tracklist.Options.DisableRating = !canRate a.tracklist.Options.DisableRating = !canRate
a.tracklist.Options.DisableSharing = !canShare
a.tracklist.OnVisibleColumnsChanged = func(cols []string) { a.tracklist.OnVisibleColumnsChanged = func(cols []string) {
a.cfg.TracklistColumns = cols a.cfg.TracklistColumns = cols
} }
@@ -183,6 +185,7 @@ type AlbumPageHeader struct {
artistLabelSpace *util.HSpace // TODO: remove when no longer needed artistLabelSpace *util.HSpace // TODO: remove when no longer needed
genreLabel *widgets.MultiHyperlink genreLabel *widgets.MultiHyperlink
miscLabel *widget.Label miscLabel *widget.Label
shareMenuItem *fyne.MenuItem
toggleFavButton *widgets.FavoriteButton toggleFavButton *widgets.FavoriteButton
@@ -246,9 +249,15 @@ func NewAlbumPageHeader(page *AlbumPage) *AlbumPageHeader {
a.page.contr.ShowAlbumInfoDialog(a.albumID, a.titleLabel.String(), a.cover.Image()) a.page.contr.ShowAlbumInfoDialog(a.albumID, a.titleLabel.String(), a.cover.Image())
}) })
info.Icon = theme.InfoIcon() info.Icon = theme.InfoIcon()
menu := fyne.NewMenu("", queue, playlist, download, info) a.shareMenuItem = fyne.NewMenuItem("Share...", func() {
a.page.contr.ShowShareDialog(a.albumID)
})
a.shareMenuItem.Icon = myTheme.ShareIcon
menu := fyne.NewMenu("", queue, playlist, download, info, a.shareMenuItem)
pop = widget.NewPopUpMenu(menu, fyne.CurrentApp().Driver().CanvasForObject(a)) pop = widget.NewPopUpMenu(menu, fyne.CurrentApp().Driver().CanvasForObject(a))
} }
_, canShare := page.mp.(mediaprovider.SupportsSharing)
a.shareMenuItem.Disabled = !canShare
pos := fyne.CurrentApp().Driver().AbsolutePositionForObject(menuBtn) pos := fyne.CurrentApp().Driver().AbsolutePositionForObject(menuBtn)
pop.ShowAtPosition(fyne.NewPos(pos.X, pos.Y+menuBtn.Size().Height)) pop.ShowAtPosition(fyne.NewPos(pos.X, pos.Y+menuBtn.Size().Height))
} }
+32 -1
View File
@@ -237,7 +237,9 @@ func (a *ArtistPage) showTopTracks() {
} }
tl.Options = widgets.TracklistOptions{AutoNumber: true} tl.Options = widgets.TracklistOptions{AutoNumber: true}
_, canRate := a.mp.(mediaprovider.SupportsRating) _, canRate := a.mp.(mediaprovider.SupportsRating)
_, canShare := a.mp.(mediaprovider.SupportsSharing)
tl.Options.DisableRating = !canRate tl.Options.DisableRating = !canRate
tl.Options.DisableSharing = !canShare
tl.SetVisibleColumns(a.cfg.TracklistColumns) tl.SetVisibleColumns(a.cfg.TracklistColumns)
tl.SetSorting(a.trackSort) tl.SetSorting(a.trackSort)
tl.OnVisibleColumnsChanged = func(cols []string) { tl.OnVisibleColumnsChanged = func(cols []string) {
@@ -292,7 +294,9 @@ type ArtistPageHeader struct {
favoriteBtn *widgets.FavoriteButton favoriteBtn *widgets.FavoriteButton
playBtn *widget.Button playBtn *widget.Button
playRadioBtn *widget.Button playRadioBtn *widget.Button
menuBtn *widget.Button
container *fyne.Container container *fyne.Container
shareMenuItem *fyne.MenuItem
} }
func NewArtistPageHeader(page *ArtistPage) *ArtistPageHeader { func NewArtistPageHeader(page *ArtistPage) *ArtistPageHeader {
@@ -318,6 +322,28 @@ func NewArtistPageHeader(page *ArtistPage) *ArtistPageHeader {
go a.artistPage.contr.PlayArtistDiscography(a.artistID, false /*shuffle*/) go a.artistPage.contr.PlayArtistDiscography(a.artistID, false /*shuffle*/)
}) })
a.playRadioBtn = widget.NewButtonWithIcon("Play Artist Radio", myTheme.ShuffleIcon, a.artistPage.playArtistRadio) a.playRadioBtn = widget.NewButtonWithIcon("Play Artist Radio", myTheme.ShuffleIcon, a.artistPage.playArtistRadio)
// TODO: Uncomment when at least one media provider supports sharing artists.
// a.shareMenuItem = fyne.NewMenuItem("Share...", func() {
// a.artistPage.contr.ShowShareDialog(a.artistID)
// })
// a.shareMenuItem.Icon = myTheme.ShareIcon
// var pop *widget.PopUpMenu
// a.menuBtn = widget.NewButtonWithIcon("", theme.MoreHorizontalIcon(), nil)
// a.menuBtn.OnTapped = func() {
// if pop == nil {
// menu := fyne.NewMenu("", a.shareMenuItem)
// 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))
// }
// canShareArtists := false
// if r, canShare := a.artistPage.mp.(mediaprovider.SupportsSharing); canShare {
// canShareArtists = r.CanShareArtists()
// }
// a.shareMenuItem.Disabled = !canShareArtists
a.biographyDisp.Wrapping = fyne.TextWrapWord a.biographyDisp.Wrapping = fyne.TextWrapWord
a.biographyDisp.Truncation = fyne.TextTruncateEllipsis a.biographyDisp.Truncation = fyne.TextTruncateEllipsis
a.ExtendBaseWidget(a) a.ExtendBaseWidget(a)
@@ -407,12 +433,17 @@ func (a *ArtistPageHeader) toggleFavorited() {
} }
func (a *ArtistPageHeader) createContainer() { 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( a.container = util.AddHeaderBackground(
container.NewBorder(nil, nil, a.artistImage, nil, container.NewBorder(nil, nil, a.artistImage, nil,
container.NewVBox( container.NewVBox(
container.New(&layouts.VboxCustomPadding{ExtraPad: -10}, container.New(&layouts.VboxCustomPadding{ExtraPad: -10},
a.titleDisp, a.biographyDisp, a.similarArtists), a.titleDisp, a.biographyDisp, a.similarArtists),
container.NewHBox(util.NewHSpace(2), a.favoriteBtn, a.playBtn, a.playRadioBtn)), btnContainer),
)) ))
} }
+5
View File
@@ -86,6 +86,11 @@ func newArtistsPage(
} else { } else {
a.grid = widgets.NewFixedGridView(nil, a.im, myTheme.ArtistIcon) a.grid = widgets.NewFixedGridView(nil, a.im, myTheme.ArtistIcon)
} }
canShareArtists := false
if r, canShare := mp.(mediaprovider.SupportsSharing); canShare {
canShareArtists = r.CanShareArtists()
}
a.grid.DisableSharing = !canShareArtists
a.contr.ConnectArtistGridActions(a.grid) a.contr.ConnectArtistGridActions(a.grid)
searchVbox := container.NewVBox(layout.NewSpacer(), a.searcher, layout.NewSpacer()) searchVbox := container.NewVBox(layout.NewSpacer(), a.searcher, layout.NewSpacer())
+7
View File
@@ -336,6 +336,11 @@ func (a *FavoritesPage) onShowFavoriteArtists() {
} else { } else {
a.artistGrid = widgets.NewFixedGridView(model, a.im, myTheme.ArtistIcon) a.artistGrid = widgets.NewFixedGridView(model, a.im, myTheme.ArtistIcon)
} }
canShareArtists := false
if r, canShare := a.mp.(mediaprovider.SupportsSharing); canShare {
canShareArtists = r.CanShareArtists()
}
a.artistGrid.DisableSharing = !canShareArtists
a.contr.ConnectArtistGridActions(a.artistGrid) a.contr.ConnectArtistGridActions(a.artistGrid)
a.container.Objects[0] = a.artistGrid a.container.Objects[0] = a.artistGrid
a.Refresh() a.Refresh()
@@ -395,7 +400,9 @@ func (a *FavoritesPage) onShowFavoriteSongs() {
} }
tracklist.Options = widgets.TracklistOptions{AutoNumber: true} tracklist.Options = widgets.TracklistOptions{AutoNumber: true}
_, canRate := a.mp.(mediaprovider.SupportsRating) _, canRate := a.mp.(mediaprovider.SupportsRating)
_, canShare := a.mp.(mediaprovider.SupportsSharing)
tracklist.Options.DisableRating = !canRate tracklist.Options.DisableRating = !canRate
tracklist.Options.DisableSharing = !canShare
tracklist.SetVisibleColumns(a.cfg.TracklistColumns) tracklist.SetVisibleColumns(a.cfg.TracklistColumns)
tracklist.SetSorting(a.trackSort) tracklist.SetSorting(a.trackSort)
tracklist.OnVisibleColumnsChanged = func(cols []string) { tracklist.OnVisibleColumnsChanged = func(cols []string) {
+2
View File
@@ -112,6 +112,7 @@ func NewGridViewPage(
gp.ExtendBaseWidget(gp) gp.ExtendBaseWidget(gp)
gp.createTitleAndSort() gp.createTitleAndSort()
_, canShare := mp.(mediaprovider.SupportsSharing)
iter := adapter.Iter(gp.getSortOrder(), gp.getFilter()) iter := adapter.Iter(gp.getSortOrder(), gp.getFilter())
if g := pool.Obtain(util.WidgetTypeGridView); g != nil { if g := pool.Obtain(util.WidgetTypeGridView); g != nil {
gp.grid = g.(*widgets.GridView) gp.grid = g.(*widgets.GridView)
@@ -120,6 +121,7 @@ func NewGridViewPage(
} else { } else {
gp.grid = widgets.NewGridView(iter, im, adapter.PlaceholderResource()) gp.grid = widgets.NewGridView(iter, im, adapter.PlaceholderResource())
} }
gp.grid.DisableSharing = !canShare
adapter.ConnectGridActions(gp.grid) adapter.ConnectGridActions(gp.grid)
gp.createSearchAndFilter() gp.createSearchAndFilter()
gp.createContainer() gp.createContainer()
+11 -2
View File
@@ -48,6 +48,7 @@ type nowPlayingPageState struct {
pm *backend.PlaybackManager pm *backend.PlaybackManager
im *backend.ImageManager im *backend.ImageManager
canRate bool canRate bool
canShare bool
} }
func NewNowPlayingPage( func NewNowPlayingPage(
@@ -56,9 +57,10 @@ func NewNowPlayingPage(
im *backend.ImageManager, im *backend.ImageManager,
pm *backend.PlaybackManager, pm *backend.PlaybackManager,
canRate bool, canRate bool,
canShare bool,
) *NowPlayingPage { ) *NowPlayingPage {
a := &NowPlayingPage{nowPlayingPageState: nowPlayingPageState{ a := &NowPlayingPage{nowPlayingPageState: nowPlayingPageState{
contr: contr, pool: pool, im: im, pm: pm, canRate: canRate, contr: contr, pool: pool, im: im, pm: pm, canRate: canRate, canShare: canShare,
}} }}
a.ExtendBaseWidget(a) a.ExtendBaseWidget(a)
@@ -82,8 +84,15 @@ func NewNowPlayingPage(
} }
a.queueList = widgets.NewPlayQueueList(a.im) a.queueList = widgets.NewPlayQueueList(a.im)
a.queueList.DisableRating = !canRate
a.queueList.DisableSharing = !canShare
a.queueList.OnReorderTracks = a.doSetNewTrackOrder a.queueList.OnReorderTracks = a.doSetNewTrackOrder
a.queueList.OnDownload = contr.ShowDownloadDialog a.queueList.OnDownload = contr.ShowDownloadDialog
a.queueList.OnShare = func(tracks []*mediaprovider.Track) {
if len(tracks) > 0 {
a.contr.ShowShareDialog(tracks[0].ID)
}
}
a.queueList.OnAddToPlaylist = contr.DoAddTracksToPlaylistWorkflow a.queueList.OnAddToPlaylist = contr.DoAddTracksToPlaylistWorkflow
a.queueList.OnPlayTrackAt = func(tracknum int) { a.queueList.OnPlayTrackAt = func(tracknum int) {
_ = a.pm.PlayTrackAt(tracknum) _ = a.pm.PlayTrackAt(tracknum)
@@ -199,7 +208,7 @@ func (s *nowPlayingPageState) Restore() Page {
page.Reload() page.Reload()
return page return page
} }
return NewNowPlayingPage(s.contr, s.pool, s.im, s.pm, s.canRate) return NewNowPlayingPage(s.contr, s.pool, s.im, s.pm, s.canRate, s.canShare)
} }
var _ CanShowPlayTime = (*NowPlayingPage)(nil) var _ CanShowPlayTime = (*NowPlayingPage)(nil)
+2
View File
@@ -87,10 +87,12 @@ func newPlaylistPage(
conf.TracklistColumns = cols conf.TracklistColumns = cols
} }
_, canRate := a.sm.Server.(mediaprovider.SupportsRating) _, canRate := a.sm.Server.(mediaprovider.SupportsRating)
_, canShare := a.sm.Server.(mediaprovider.SupportsSharing)
remove := fyne.NewMenuItem("Remove from playlist", a.onRemoveSelectedFromPlaylist) remove := fyne.NewMenuItem("Remove from playlist", a.onRemoveSelectedFromPlaylist)
remove.Icon = theme.ContentClearIcon() remove.Icon = theme.ContentClearIcon()
a.tracklist.Options = widgets.TracklistOptions{ a.tracklist.Options = widgets.TracklistOptions{
DisableRating: !canRate, DisableRating: !canRate,
DisableSharing: !canShare,
AuxiliaryMenuItems: []*fyne.MenuItem{ AuxiliaryMenuItems: []*fyne.MenuItem{
util.NewReorderTracksSubmenu(a.doSetNewTrackOrder), util.NewReorderTracksSubmenu(a.doSetNewTrackOrder),
remove, remove,
+3 -2
View File
@@ -31,6 +31,7 @@ func NewRouter(app *backend.App, controller *controller.Controller, nav Navigati
func (r Router) CreatePage(rte controller.Route) Page { func (r Router) CreatePage(rte controller.Route) Page {
_, canRate := r.App.ServerManager.Server.(mediaprovider.SupportsRating) _, canRate := r.App.ServerManager.Server.(mediaprovider.SupportsRating)
_, canShare := r.App.ServerManager.Server.(mediaprovider.SupportsSharing)
switch rte.Page { switch rte.Page {
case controller.Album: case controller.Album:
return NewAlbumPage(rte.Arg, &r.App.Config.AlbumPage, r.widgetPool, r.App.PlaybackManager, r.App.ServerManager.Server, r.App.ImageManager, r.Controller) return NewAlbumPage(rte.Arg, &r.App.Config.AlbumPage, r.widgetPool, r.App.PlaybackManager, r.App.ServerManager.Server, r.App.ImageManager, r.Controller)
@@ -43,13 +44,13 @@ func (r Router) CreatePage(rte controller.Route) Page {
case controller.Favorites: case controller.Favorites:
return NewFavoritesPage(&r.App.Config.FavoritesPage, r.widgetPool, r.Controller, r.App.ServerManager.Server, r.App.PlaybackManager, r.App.ImageManager) return NewFavoritesPage(&r.App.Config.FavoritesPage, r.widgetPool, r.Controller, r.App.ServerManager.Server, r.App.PlaybackManager, r.App.ImageManager)
case controller.Fullscreen: case controller.Fullscreen:
return NewNowPlayingPage(r.Controller, r.widgetPool, r.App.ImageManager, r.App.PlaybackManager, canRate) return NewNowPlayingPage(r.Controller, r.widgetPool, r.App.ImageManager, r.App.PlaybackManager, canRate, canShare)
case controller.Genre: case controller.Genre:
return NewGenrePage(rte.Arg, r.widgetPool, r.Controller, r.App.PlaybackManager, r.App.ServerManager.Server, r.App.ImageManager) return NewGenrePage(rte.Arg, r.widgetPool, r.Controller, r.App.PlaybackManager, r.App.ServerManager.Server, r.App.ImageManager)
case controller.Genres: case controller.Genres:
return NewGenresPage(r.Controller, r.App.ServerManager.Server) return NewGenresPage(r.Controller, r.App.ServerManager.Server)
case controller.NowPlaying: case controller.NowPlaying:
return NewNowPlayingPage(r.Controller, r.widgetPool, r.App.ImageManager, r.App.PlaybackManager, canRate) return NewNowPlayingPage(r.Controller, r.widgetPool, r.App.ImageManager, r.App.PlaybackManager, canRate, canShare)
case controller.Playlist: case controller.Playlist:
return NewPlaylistPage(rte.Arg, &r.App.Config.PlaylistPage, r.widgetPool, r.Controller, r.App.ServerManager, r.App.PlaybackManager, r.App.ImageManager) return NewPlaylistPage(rte.Arg, &r.App.Config.PlaylistPage, r.widgetPool, r.Controller, r.App.ServerManager, r.App.PlaybackManager, r.App.ImageManager)
case controller.Playlists: case controller.Playlists:
+4
View File
@@ -40,6 +40,7 @@ type tracksPageState struct {
conf *backend.TracksPageConfig conf *backend.TracksPageConfig
mp mediaprovider.MediaProvider mp mediaprovider.MediaProvider
canRate bool canRate bool
canShare bool
} }
func NewTracksPage(contr *controller.Controller, conf *backend.TracksPageConfig, pool *util.WidgetPool, mp mediaprovider.MediaProvider) *TracksPage { 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.tracklist = t.obtainTracklist()
_, t.canRate = mp.(mediaprovider.SupportsRating) _, t.canRate = mp.(mediaprovider.SupportsRating)
_, t.canShare = mp.(mediaprovider.SupportsSharing)
t.tracklist.Options = widgets.TracklistOptions{ t.tracklist.Options = widgets.TracklistOptions{
DisableSorting: true, DisableSorting: true,
DisableRating: !t.canRate, DisableRating: !t.canRate,
DisableSharing: !t.canShare,
AutoNumber: true, AutoNumber: true,
} }
t.tracklist.SetVisibleColumns(conf.TracklistColumns) t.tracklist.SetVisibleColumns(conf.TracklistColumns)
@@ -137,6 +140,7 @@ func (t *TracksPage) doSearch(query string) {
AutoNumber: true, AutoNumber: true,
DisableSorting: true, DisableSorting: true,
DisableRating: !t.canRate, DisableRating: !t.canRate,
DisableSharing: !t.canShare,
} }
t.searchTracklist.SetVisibleColumns(t.conf.TracklistColumns) t.searchTracklist.SetVisibleColumns(t.conf.TracklistColumns)
t.searchTracklist.SetNowPlaying(t.nowPlayingID) t.searchTracklist.SetNowPlaying(t.nowPlayingID)
+60
View File
@@ -7,6 +7,7 @@ import (
"image" "image"
"io" "io"
"log" "log"
"net/url"
"os" "os"
"path/filepath" "path/filepath"
"time" "time"
@@ -22,8 +23,10 @@ import (
"fyne.io/fyne/v2" "fyne.io/fyne/v2"
"fyne.io/fyne/v2/canvas" "fyne.io/fyne/v2/canvas"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/dialog" "fyne.io/fyne/v2/dialog"
"fyne.io/fyne/v2/layout" "fyne.io/fyne/v2/layout"
"fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget" "fyne.io/fyne/v2/widget"
) )
@@ -138,6 +141,9 @@ func (m *Controller) connectTracklistActionsWithReplayGainMode(tracklist *widget
m.ClosePopUpOnEscape(pop) m.ClosePopUpOnEscape(pop)
} }
tracklist.OnDownload = m.ShowDownloadDialog tracklist.OnDownload = m.ShowDownloadDialog
tracklist.OnShare = func(trackID string) {
go m.ShowShareDialog(trackID)
}
} }
func (m *Controller) ConnectAlbumGridActions(grid *widgets.GridView) { func (m *Controller) ConnectAlbumGridActions(grid *widgets.GridView) {
@@ -173,6 +179,9 @@ func (m *Controller) ConnectAlbumGridActions(grid *widgets.GridView) {
m.ShowDownloadDialog(album.Tracks, album.Name) m.ShowDownloadDialog(album.Tracks, album.Name)
}() }()
} }
grid.OnShare = func(albumID string) {
go m.ShowShareDialog(albumID)
}
} }
func (m *Controller) ConnectArtistGridActions(grid *widgets.GridView) { func (m *Controller) ConnectArtistGridActions(grid *widgets.GridView) {
@@ -662,6 +671,52 @@ func (c *Controller) SetTrackRatings(trackIDs []string, rating int) {
} }
} }
func (c *Controller) ShowShareDialog(id string) {
go func() {
shareUrl, err := c.createShareURL(id)
if err != nil {
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() {
if shareUrl, err := c.createShareURL(id); err == nil {
hyperlink.Text = shareUrl.String()
hyperlink.URL = shareUrl
hyperlink.Refresh()
}
}),
),
c.MainWindow,
)
dlg.Show()
}()
}
func (c *Controller) createShareURL(id string) (*url.URL, error) {
r, ok := c.App.ServerManager.Server.(mediaprovider.SupportsSharing)
if !ok {
return nil, fmt.Errorf("server does not support sharing")
}
shareUrl, err := r.CreateShareURL(id)
if err != nil {
log.Printf("error creating share URL: %v", err)
c.showError(
"Failed to share content. This commonly occurs when the server does not support sharing," +
"or has the feature disabled.\nPlease check the server's settings and try again.",
)
return nil, err
}
return shareUrl, nil
}
func (c *Controller) ShowDownloadDialog(tracks []*mediaprovider.Track, downloadName string) { func (c *Controller) ShowDownloadDialog(tracks []*mediaprovider.Track, downloadName string) {
numTracks := len(tracks) numTracks := len(tracks)
var fileName string var fileName string
@@ -762,6 +817,11 @@ func (c *Controller) sendNotification(title, content string) {
}) })
} }
func (c *Controller) showError(content string) {
// TODO: display an in-app toast message instead of a dialog.
dialog.ShowError(fmt.Errorf(content), c.MainWindow)
}
func (c *Controller) ShowAlbumInfoDialog(albumID, albumName string, albumCover image.Image) { func (c *Controller) ShowAlbumInfoDialog(albumID, albumName string, albumCover image.Image) {
go func() { go func() {
albumInfo, err := c.App.ServerManager.Server.GetAlbumInfo(albumID) albumInfo, err := c.App.ServerManager.Server.GetAlbumInfo(albumID)
+1
View File
@@ -31,6 +31,7 @@ var (
NotFavoriteIcon fyne.Resource = theme.NewThemedResource(res.ResHeartOutlineSvg) NotFavoriteIcon fyne.Resource = theme.NewThemedResource(res.ResHeartOutlineSvg)
NowPlayingIcon fyne.Resource = theme.NewThemedResource(res.ResHeadphonesSvg) NowPlayingIcon fyne.Resource = theme.NewThemedResource(res.ResHeadphonesSvg)
PlaylistIcon fyne.Resource = theme.NewThemedResource(res.ResPlaylistSvg) PlaylistIcon fyne.Resource = theme.NewThemedResource(res.ResPlaylistSvg)
ShareIcon fyne.Resource = theme.NewThemedResource(res.ResShareSvg)
ShuffleIcon fyne.Resource = theme.NewThemedResource(res.ResShuffleSvg) ShuffleIcon fyne.Resource = theme.NewThemedResource(res.ResShuffleSvg)
TracksIcon fyne.Resource = theme.NewThemedResource(res.ResMusicnotesSvg) TracksIcon fyne.Resource = theme.NewThemedResource(res.ResMusicnotesSvg)
GenreIcon fyne.Resource = theme.NewThemedResource(res.ResTheatermasksSvg) GenreIcon fyne.Resource = theme.NewThemedResource(res.ResTheatermasksSvg)
+10 -2
View File
@@ -79,6 +79,7 @@ type GridView struct {
itemForIndex map[int]*GridViewItem itemForIndex map[int]*GridViewItem
itemWidth float32 itemWidth float32
numColsCached int numColsCached int
shareMenuItem *fyne.MenuItem
} }
type GridViewState struct { type GridViewState struct {
@@ -89,10 +90,13 @@ type GridViewState struct {
highestShown int highestShown int
done bool done bool
DisableSharing bool
OnPlay func(id string, shuffle bool) OnPlay func(id string, shuffle bool)
OnAddToQueue func(id string) OnAddToQueue func(id string)
OnAddToPlaylist func(id string) OnAddToPlaylist func(id string)
OnDownload func(id string) OnDownload func(id string)
OnShare func(id string)
OnShowItemPage func(id string) OnShowItemPage func(id string)
OnShowSecondaryPage func(id string) OnShowSecondaryPage func(id string)
@@ -407,10 +411,14 @@ func (g *GridView) showContextMenu(card *GridViewItem, pos fyne.Position) {
} }
}) })
download.Icon = theme.DownloadIcon() download.Icon = theme.DownloadIcon()
g.shareMenuItem = fyne.NewMenuItem("Share...", func() {
g.menu = widget.NewPopUpMenu(fyne.NewMenu("", play, shuffle, queue, playlist, download), g.OnShare(g.menuGridViewItemId)
})
g.shareMenuItem.Icon = myTheme.ShareIcon
g.menu = widget.NewPopUpMenu(fyne.NewMenu("", play, shuffle, queue, playlist, download, g.shareMenuItem),
fyne.CurrentApp().Driver().CanvasForObject(g)) fyne.CurrentApp().Driver().CanvasForObject(g))
} }
g.shareMenuItem.Disabled = g.DisableSharing
g.menu.ShowAtPosition(pos) g.menu.ShowAtPosition(pos)
} }
+11
View File
@@ -27,6 +27,7 @@ type PlayQueueList struct {
widget.BaseWidget widget.BaseWidget
DisableRating bool DisableRating bool
DisableSharing bool
// user action callbacks // user action callbacks
OnAddToPlaylist func(trackIDs []string) OnAddToPlaylist func(trackIDs []string)
@@ -34,6 +35,7 @@ type PlayQueueList struct {
OnSetRating func(trackIDs []string, rating int) OnSetRating func(trackIDs []string, rating int)
OnRemoveFromQueue func(trackIDs []string) OnRemoveFromQueue func(trackIDs []string)
OnDownload func(tracks []*mediaprovider.Track, downloadName string) OnDownload func(tracks []*mediaprovider.Track, downloadName string)
OnShare func(tracks []*mediaprovider.Track)
OnShowArtistPage func(artistID string) OnShowArtistPage func(artistID string)
OnPlayTrackAt func(idx int) OnPlayTrackAt func(idx int)
OnReorderTracks func(trackIDs []string, op sharedutil.TrackReorderOp) OnReorderTracks func(trackIDs []string, op sharedutil.TrackReorderOp)
@@ -41,6 +43,7 @@ type PlayQueueList struct {
list *FocusList list *FocusList
menu *widget.PopUpMenu menu *widget.PopUpMenu
ratingSubmenu *fyne.MenuItem ratingSubmenu *fyne.MenuItem
shareMenuItem *fyne.MenuItem
nowPlayingID string nowPlayingID string
colLayout *layouts.ColumnsLayout colLayout *layouts.ColumnsLayout
@@ -200,6 +203,12 @@ func (p *PlayQueueList) onShowContextMenu(e *fyne.PointEvent, trackIdx int) {
} }
}) })
download.Icon = theme.DownloadIcon() download.Icon = theme.DownloadIcon()
p.shareMenuItem = fyne.NewMenuItem("Share...", func() {
if p.OnShare != nil {
p.OnShare(p.selectedTracks())
}
})
p.shareMenuItem.Icon = myTheme.ShareIcon
favorite := fyne.NewMenuItem("Set favorite", func() { favorite := fyne.NewMenuItem("Set favorite", func() {
if p.OnSetFavorite != nil { if p.OnSetFavorite != nil {
p.OnSetFavorite(p.selectedTrackIDs(), true) p.OnSetFavorite(p.selectedTrackIDs(), true)
@@ -233,6 +242,7 @@ func (p *PlayQueueList) onShowContextMenu(e *fyne.PointEvent, trackIdx int) {
fyne.NewMenu("", fyne.NewMenu("",
playlist, playlist,
download, download,
p.shareMenuItem,
fyne.NewMenuItemSeparator(), fyne.NewMenuItemSeparator(),
favorite, favorite,
unfavorite, unfavorite,
@@ -245,6 +255,7 @@ func (p *PlayQueueList) onShowContextMenu(e *fyne.PointEvent, trackIdx int) {
) )
} }
p.ratingSubmenu.Disabled = p.DisableRating p.ratingSubmenu.Disabled = p.DisableRating
p.shareMenuItem.Disabled = p.DisableSharing || len(p.selectedTracks()) != 1
p.menu.ShowAtPosition(e.AbsolutePosition) p.menu.ShowAtPosition(e.AbsolutePosition)
} }
+19
View File
@@ -72,6 +72,9 @@ type TracklistOptions struct {
// Disables the five star rating widget. // Disables the five star rating widget.
DisableRating bool DisableRating bool
// Disables the sharing option.
DisableSharing bool
} }
type Tracklist struct { type Tracklist struct {
@@ -87,6 +90,7 @@ type Tracklist struct {
OnSetFavorite func(trackIDs []string, fav bool) OnSetFavorite func(trackIDs []string, fav bool)
OnSetRating func(trackIDs []string, rating int) OnSetRating func(trackIDs []string, rating int)
OnDownload func(tracks []*mediaprovider.Track, downloadName string) OnDownload func(tracks []*mediaprovider.Track, downloadName string)
OnShare func(trackID string)
OnShowArtistPage func(artistID string) OnShowArtistPage func(artistID string)
OnShowAlbumPage func(albumID string) OnShowAlbumPage func(albumID string)
@@ -108,6 +112,7 @@ type Tracklist struct {
list *FocusList list *FocusList
ctxMenu *fyne.Menu ctxMenu *fyne.Menu
ratingSubmenu *fyne.MenuItem ratingSubmenu *fyne.MenuItem
shareMenuItem *fyne.MenuItem
container *fyne.Container container *fyne.Container
} }
@@ -555,6 +560,11 @@ func (t *Tracklist) onShowContextMenu(e *fyne.PointEvent, trackIdx int) {
}) })
unfavorite.Icon = myTheme.NotFavoriteIcon unfavorite.Icon = myTheme.NotFavoriteIcon
t.ctxMenu.Items = append(t.ctxMenu.Items, playlist, download) t.ctxMenu.Items = append(t.ctxMenu.Items, playlist, download)
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, fyne.NewMenuItemSeparator())
t.ctxMenu.Items = append(t.ctxMenu.Items, favorite, unfavorite) t.ctxMenu.Items = append(t.ctxMenu.Items, favorite, unfavorite)
t.ratingSubmenu = util.NewRatingSubmenu(func(rating int) { t.ratingSubmenu = util.NewRatingSubmenu(func(rating int) {
@@ -567,6 +577,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
widget.ShowPopUpMenuAtPosition(t.ctxMenu, fyne.CurrentApp().Driver().CanvasForObject(t), e.AbsolutePosition) widget.ShowPopUpMenuAtPosition(t.ctxMenu, fyne.CurrentApp().Driver().CanvasForObject(t), e.AbsolutePosition)
} }
@@ -629,6 +640,14 @@ func (t *Tracklist) onDownload(tracks []*mediaprovider.Track, downloadName strin
} }
} }
func (t *Tracklist) onShare(tracks []*mediaprovider.Track) {
if t.OnShare != nil {
if len(tracks) > 0 {
t.OnShare(tracks[0].ID)
}
}
}
func (t *Tracklist) selectedTracks() []*mediaprovider.Track { func (t *Tracklist) selectedTracks() []*mediaprovider.Track {
t.tracksMutex.RLock() t.tracksMutex.RLock()
defer t.tracksMutex.RUnlock() defer t.tracksMutex.RUnlock()