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\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..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()