diff --git a/backend/mediaprovider/mediaprovider.go b/backend/mediaprovider/mediaprovider.go index 23cc1f8..46ea267 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,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 diff --git a/backend/mediaprovider/subsonic/subsonicmediaprovider.go b/backend/mediaprovider/subsonic/subsonicmediaprovider.go index 9809a28..0058849 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,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) } 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\n \n \n \n \n"), } +var ResShareSvg = &fyne.StaticResource{ + StaticName: "share.svg", + StaticContent: []byte( + "\n\n \n \n \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..fda8b75 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.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) diff --git a/ui/browsing/artistpage.go b/ui/browsing/artistpage.go index 333387b..5201444 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.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), )) } diff --git a/ui/browsing/favoritespage.go b/ui/browsing/favoritespage.go index 4fbc4b4..17c1165 100644 --- a/ui/browsing/favoritespage.go +++ b/ui/browsing/favoritespage.go @@ -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) { diff --git a/ui/browsing/playlistpage.go b/ui/browsing/playlistpage.go index fed891e..e990643 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, + HideSharing: !canShare, AuxiliaryMenuItems: []*fyne.MenuItem{ util.NewReorderTracksSubmenu(a.doSetNewTrackOrder), remove, diff --git a/ui/browsing/trackspage.go b/ui/browsing/trackspage.go index 95e4386..98a9108 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, + 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) diff --git a/ui/controller/controller.go b/ui/controller/controller.go index b641cb5..303105f 100644 --- a/ui/controller/controller.go +++ b/ui/controller/controller.go @@ -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 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..53d5419 100644 --- a/ui/widgets/gridview.go +++ b/ui/widgets/gridview.go @@ -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) diff --git a/ui/widgets/tracklist.go b/ui/widgets/tracklist.go index 2a78968..576ee0d 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 + + // 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()