diff --git a/backend/mediaprovider/mediaprovider.go b/backend/mediaprovider/mediaprovider.go
index 23cc1f8..f7fe0b1 100644
--- a/backend/mediaprovider/mediaprovider.go
+++ b/backend/mediaprovider/mediaprovider.go
@@ -3,6 +3,7 @@ package mediaprovider
import (
"image"
"io"
+ "net/url"
"strings"
)
@@ -148,6 +149,11 @@ type SupportsRating interface {
SetRating(params RatingFavoriteParameters, rating int) error
}
+type SupportsSharing interface {
+ CreateShareURL(id string) (*url.URL, error)
+ CanShareArtists() bool
+}
+
type JukeboxProvider interface {
JukeboxStart() error
JukeboxStop() error
diff --git a/backend/mediaprovider/subsonic/subsonicmediaprovider.go b/backend/mediaprovider/subsonic/subsonicmediaprovider.go
index 9809a28..5c858eb 100644
--- a/backend/mediaprovider/subsonic/subsonicmediaprovider.go
+++ b/backend/mediaprovider/subsonic/subsonicmediaprovider.go
@@ -5,6 +5,7 @@ import (
"image"
"io"
"math"
+ "net/url"
"strconv"
"strings"
"sync"
@@ -315,6 +316,26 @@ 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) 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) {
return s.client.Download(trackID)
}
diff --git a/go.mod b/go.mod
index ffebbef..abde19e 100644
--- a/go.mod
+++ b/go.mod
@@ -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
diff --git a/go.sum b/go.sum
index 3ee4cce..45ed838 100644
--- a/go.sum
+++ b/go.sum
@@ -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=
diff --git a/res/bundled.go b/res/bundled.go
index 87e5d94..d34c469 100644
--- a/res/bundled.go
+++ b/res/bundled.go
@@ -95,6 +95,11 @@ var ResShuffleSvg = &fyne.StaticResource{
StaticContent: []byte(
"\n"),
}
+var ResShareSvg = &fyne.StaticResource{
+ StaticName: "share.svg",
+ StaticContent: []byte(
+ "\n\n"),
+}
var ResUpdownarrowSvg = &fyne.StaticResource{
StaticName: "updownarrow.svg",
StaticContent: []byte(
diff --git a/res/bundled_gen.sh b/res/bundled_gen.sh
index 633d175..c0bdb70 100755
--- a/res/bundled_gen.sh
+++ b/res/bundled_gen.sh
@@ -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
diff --git a/res/icons/remix_design/share.svg b/res/icons/remix_design/share.svg
new file mode 100644
index 0000000..93a2ee6
--- /dev/null
+++ b/res/icons/remix_design/share.svg
@@ -0,0 +1,6 @@
+
+
diff --git a/ui/browsing/albumpage.go b/ui/browsing/albumpage.go
index b5a214a..13078dd 100644
--- a/ui/browsing/albumpage.go
+++ b/ui/browsing/albumpage.go
@@ -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.SupportsSharing)
a.tracklist.Options.DisableRating = !canRate
+ a.tracklist.Options.DisableSharing = !canShare
a.tracklist.OnVisibleColumnsChanged = func(cols []string) {
a.cfg.TracklistColumns = cols
}
@@ -183,6 +185,7 @@ type AlbumPageHeader struct {
artistLabelSpace *util.HSpace // TODO: remove when no longer needed
genreLabel *widgets.MultiHyperlink
miscLabel *widget.Label
+ shareMenuItem *fyne.MenuItem
toggleFavButton *widgets.FavoriteButton
@@ -246,9 +249,15 @@ 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)
+ 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))
}
+ _, canShare := page.mp.(mediaprovider.SupportsSharing)
+ a.shareMenuItem.Disabled = !canShare
pos := fyne.CurrentApp().Driver().AbsolutePositionForObject(menuBtn)
pop.ShowAtPosition(fyne.NewPos(pos.X, pos.Y+menuBtn.Size().Height))
}
diff --git a/ui/browsing/artistpage.go b/ui/browsing/artistpage.go
index 333387b..b237e50 100644
--- a/ui/browsing/artistpage.go
+++ b/ui/browsing/artistpage.go
@@ -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.DisableSharing = !canShare
tl.SetVisibleColumns(a.cfg.TracklistColumns)
tl.SetSorting(a.trackSort)
tl.OnVisibleColumnsChanged = func(cols []string) {
@@ -292,7 +294,9 @@ type ArtistPageHeader struct {
favoriteBtn *widgets.FavoriteButton
playBtn *widget.Button
playRadioBtn *widget.Button
+ menuBtn *widget.Button
container *fyne.Container
+ shareMenuItem *fyne.MenuItem
}
func NewArtistPageHeader(page *ArtistPage) *ArtistPageHeader {
@@ -318,6 +322,28 @@ 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)
+
+ // 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.Truncation = fyne.TextTruncateEllipsis
a.ExtendBaseWidget(a)
@@ -407,12 +433,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),
))
}
diff --git a/ui/browsing/artistspage.go b/ui/browsing/artistspage.go
index dc8ae58..40d610f 100644
--- a/ui/browsing/artistspage.go
+++ b/ui/browsing/artistspage.go
@@ -86,6 +86,11 @@ func newArtistsPage(
} else {
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)
searchVbox := container.NewVBox(layout.NewSpacer(), a.searcher, layout.NewSpacer())
diff --git a/ui/browsing/favoritespage.go b/ui/browsing/favoritespage.go
index 4fbc4b4..50a0195 100644
--- a/ui/browsing/favoritespage.go
+++ b/ui/browsing/favoritespage.go
@@ -336,6 +336,11 @@ func (a *FavoritesPage) onShowFavoriteArtists() {
} else {
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.container.Objects[0] = a.artistGrid
a.Refresh()
@@ -395,7 +400,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.DisableSharing = !canShare
tracklist.SetVisibleColumns(a.cfg.TracklistColumns)
tracklist.SetSorting(a.trackSort)
tracklist.OnVisibleColumnsChanged = func(cols []string) {
diff --git a/ui/browsing/gridviewpage.go b/ui/browsing/gridviewpage.go
index 4884c76..7815765 100644
--- a/ui/browsing/gridviewpage.go
+++ b/ui/browsing/gridviewpage.go
@@ -112,6 +112,7 @@ func NewGridViewPage(
gp.ExtendBaseWidget(gp)
gp.createTitleAndSort()
+ _, canShare := mp.(mediaprovider.SupportsSharing)
iter := adapter.Iter(gp.getSortOrder(), gp.getFilter())
if g := pool.Obtain(util.WidgetTypeGridView); g != nil {
gp.grid = g.(*widgets.GridView)
@@ -120,6 +121,7 @@ func NewGridViewPage(
} else {
gp.grid = widgets.NewGridView(iter, im, adapter.PlaceholderResource())
}
+ gp.grid.DisableSharing = !canShare
adapter.ConnectGridActions(gp.grid)
gp.createSearchAndFilter()
gp.createContainer()
diff --git a/ui/browsing/nowplayingpage.go b/ui/browsing/nowplayingpage.go
index e14afa3..702f7b3 100644
--- a/ui/browsing/nowplayingpage.go
+++ b/ui/browsing/nowplayingpage.go
@@ -43,11 +43,12 @@ type NowPlayingPage struct {
}
type nowPlayingPageState struct {
- contr *controller.Controller
- pool *util.WidgetPool
- pm *backend.PlaybackManager
- im *backend.ImageManager
- canRate bool
+ contr *controller.Controller
+ pool *util.WidgetPool
+ pm *backend.PlaybackManager
+ im *backend.ImageManager
+ canRate bool
+ canShare bool
}
func NewNowPlayingPage(
@@ -56,9 +57,10 @@ func NewNowPlayingPage(
im *backend.ImageManager,
pm *backend.PlaybackManager,
canRate bool,
+ canShare bool,
) *NowPlayingPage {
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)
@@ -82,8 +84,15 @@ func NewNowPlayingPage(
}
a.queueList = widgets.NewPlayQueueList(a.im)
+ a.queueList.DisableRating = !canRate
+ a.queueList.DisableSharing = !canShare
a.queueList.OnReorderTracks = a.doSetNewTrackOrder
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.OnPlayTrackAt = func(tracknum int) {
_ = a.pm.PlayTrackAt(tracknum)
@@ -199,7 +208,7 @@ func (s *nowPlayingPageState) Restore() Page {
page.Reload()
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)
diff --git a/ui/browsing/playlistpage.go b/ui/browsing/playlistpage.go
index fed891e..63b394e 100644
--- a/ui/browsing/playlistpage.go
+++ b/ui/browsing/playlistpage.go
@@ -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,
+ DisableRating: !canRate,
+ DisableSharing: !canShare,
AuxiliaryMenuItems: []*fyne.MenuItem{
util.NewReorderTracksSubmenu(a.doSetNewTrackOrder),
remove,
diff --git a/ui/browsing/router.go b/ui/browsing/router.go
index 900ce7e..c92d2e9 100644
--- a/ui/browsing/router.go
+++ b/ui/browsing/router.go
@@ -31,6 +31,7 @@ func NewRouter(app *backend.App, controller *controller.Controller, nav Navigati
func (r Router) CreatePage(rte controller.Route) Page {
_, canRate := r.App.ServerManager.Server.(mediaprovider.SupportsRating)
+ _, canShare := r.App.ServerManager.Server.(mediaprovider.SupportsSharing)
switch rte.Page {
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)
@@ -43,13 +44,13 @@ func (r Router) CreatePage(rte controller.Route) Page {
case controller.Favorites:
return NewFavoritesPage(&r.App.Config.FavoritesPage, r.widgetPool, r.Controller, r.App.ServerManager.Server, r.App.PlaybackManager, r.App.ImageManager)
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:
return NewGenrePage(rte.Arg, r.widgetPool, r.Controller, r.App.PlaybackManager, r.App.ServerManager.Server, r.App.ImageManager)
case controller.Genres:
return NewGenresPage(r.Controller, r.App.ServerManager.Server)
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:
return NewPlaylistPage(rte.Arg, &r.App.Config.PlaylistPage, r.widgetPool, r.Controller, r.App.ServerManager, r.App.PlaybackManager, r.App.ImageManager)
case controller.Playlists:
diff --git a/ui/browsing/trackspage.go b/ui/browsing/trackspage.go
index 95e4386..4b6884a 100644
--- a/ui/browsing/trackspage.go
+++ b/ui/browsing/trackspage.go
@@ -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,
+ DisableSharing: !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,
+ DisableSharing: !t.canShare,
}
t.searchTracklist.SetVisibleColumns(t.conf.TracklistColumns)
t.searchTracklist.SetNowPlaying(t.nowPlayingID)
diff --git a/ui/controller/controller.go b/ui/controller/controller.go
index b641cb5..f600144 100644
--- a/ui/controller/controller.go
+++ b/ui/controller/controller.go
@@ -7,6 +7,7 @@ import (
"image"
"io"
"log"
+ "net/url"
"os"
"path/filepath"
"time"
@@ -22,8 +23,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 +141,9 @@ func (m *Controller) connectTracklistActionsWithReplayGainMode(tracklist *widget
m.ClosePopUpOnEscape(pop)
}
tracklist.OnDownload = m.ShowDownloadDialog
+ tracklist.OnShare = func(trackID string) {
+ go m.ShowShareDialog(trackID)
+ }
}
func (m *Controller) ConnectAlbumGridActions(grid *widgets.GridView) {
@@ -173,6 +179,9 @@ func (m *Controller) ConnectAlbumGridActions(grid *widgets.GridView) {
m.ShowDownloadDialog(album.Tracks, album.Name)
}()
}
+ grid.OnShare = func(albumID string) {
+ go m.ShowShareDialog(albumID)
+ }
}
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) {
numTracks := len(tracks)
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) {
go func() {
albumInfo, err := c.App.ServerManager.Server.GetAlbumInfo(albumID)
diff --git a/ui/theme/theme.go b/ui/theme/theme.go
index e5c6d1a..b0773db 100644
--- a/ui/theme/theme.go
+++ b/ui/theme/theme.go
@@ -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)
diff --git a/ui/widgets/gridview.go b/ui/widgets/gridview.go
index 5101c2a..ec88fbf 100644
--- a/ui/widgets/gridview.go
+++ b/ui/widgets/gridview.go
@@ -79,6 +79,7 @@ type GridView struct {
itemForIndex map[int]*GridViewItem
itemWidth float32
numColsCached int
+ shareMenuItem *fyne.MenuItem
}
type GridViewState struct {
@@ -89,10 +90,13 @@ type GridViewState struct {
highestShown int
done bool
+ DisableSharing bool
+
OnPlay func(id string, shuffle bool)
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,10 +411,14 @@ func (g *GridView) showContextMenu(card *GridViewItem, pos fyne.Position) {
}
})
download.Icon = theme.DownloadIcon()
-
- g.menu = widget.NewPopUpMenu(fyne.NewMenu("", play, shuffle, queue, playlist, download),
+ g.shareMenuItem = fyne.NewMenuItem("Share...", func() {
+ 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))
}
+ g.shareMenuItem.Disabled = g.DisableSharing
g.menu.ShowAtPosition(pos)
}
diff --git a/ui/widgets/playqueuelist.go b/ui/widgets/playqueuelist.go
index c8855d9..0ab5926 100644
--- a/ui/widgets/playqueuelist.go
+++ b/ui/widgets/playqueuelist.go
@@ -26,7 +26,8 @@ const thumbnailSize = 52
type PlayQueueList struct {
widget.BaseWidget
- DisableRating bool
+ DisableRating bool
+ DisableSharing bool
// user action callbacks
OnAddToPlaylist func(trackIDs []string)
@@ -34,6 +35,7 @@ type PlayQueueList struct {
OnSetRating func(trackIDs []string, rating int)
OnRemoveFromQueue func(trackIDs []string)
OnDownload func(tracks []*mediaprovider.Track, downloadName string)
+ OnShare func(tracks []*mediaprovider.Track)
OnShowArtistPage func(artistID string)
OnPlayTrackAt func(idx int)
OnReorderTracks func(trackIDs []string, op sharedutil.TrackReorderOp)
@@ -41,6 +43,7 @@ type PlayQueueList struct {
list *FocusList
menu *widget.PopUpMenu
ratingSubmenu *fyne.MenuItem
+ shareMenuItem *fyne.MenuItem
nowPlayingID string
colLayout *layouts.ColumnsLayout
@@ -200,6 +203,12 @@ func (p *PlayQueueList) onShowContextMenu(e *fyne.PointEvent, trackIdx int) {
}
})
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() {
if p.OnSetFavorite != nil {
p.OnSetFavorite(p.selectedTrackIDs(), true)
@@ -233,6 +242,7 @@ func (p *PlayQueueList) onShowContextMenu(e *fyne.PointEvent, trackIdx int) {
fyne.NewMenu("",
playlist,
download,
+ p.shareMenuItem,
fyne.NewMenuItemSeparator(),
favorite,
unfavorite,
@@ -245,6 +255,7 @@ func (p *PlayQueueList) onShowContextMenu(e *fyne.PointEvent, trackIdx int) {
)
}
p.ratingSubmenu.Disabled = p.DisableRating
+ p.shareMenuItem.Disabled = p.DisableSharing || len(p.selectedTracks()) != 1
p.menu.ShowAtPosition(e.AbsolutePosition)
}
diff --git a/ui/widgets/tracklist.go b/ui/widgets/tracklist.go
index 2a78968..27c4076 100644
--- a/ui/widgets/tracklist.go
+++ b/ui/widgets/tracklist.go
@@ -72,6 +72,9 @@ type TracklistOptions struct {
// Disables the five star rating widget.
DisableRating bool
+
+ // Disables the sharing option.
+ DisableSharing 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,11 @@ func (t *Tracklist) onShowContextMenu(e *fyne.PointEvent, trackIdx int) {
})
unfavorite.Icon = myTheme.NotFavoriteIcon
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, favorite, unfavorite)
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.shareMenuItem.Disabled = t.Options.DisableSharing || len(t.selectedTracks()) != 1
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 {
t.tracksMutex.RLock()
defer t.tracksMutex.RUnlock()