add features

This commit is contained in:
2026-07-29 04:35:25 -05:00
parent 23c6113b33
commit cab5a987b0
29 changed files with 1179 additions and 89 deletions
+9 -2
View File
@@ -53,6 +53,8 @@ func NewBottomPanel(pm *backend.PlaybackManager, im *backend.ImageManager, contr
}))
bp.NowPlaying = widgets.NewNowPlayingCard()
_, canShare := contr.App.ServerManager.Server.(mediaprovider.SupportsSharing)
bp.NowPlaying.DisableSharing = !canShare
bp.NowPlaying.ShowAlbumYear = cfg.AlbumsPage.ShowYears
bp.NowPlaying.OnCoverTapped = func() {
contr.NavigateTo(controller.NowPlayingRoute())
@@ -87,8 +89,13 @@ func NewBottomPanel(pm *backend.PlaybackManager, im *backend.ImageManager, contr
}
}
bp.NowPlaying.OnShare = func() {
if tr, ok := pm.NowPlaying().(*mediaprovider.Track); ok {
contr.ShowShareDialog(tr.ID)
switch item := pm.NowPlaying().(type) {
case *mediaprovider.Track:
contr.ShowShareDialog(item.ID)
case *mediaprovider.PodcastEpisode:
if item.StreamID != "" {
contr.ShowShareDialog(item.StreamID)
}
}
}
bp.Controls = widgets.NewPlayerControls(cfg.Playback.UseWaveformSeekbar, pm.GetLoopMode(), pm.IsShuffle())
+1 -12
View File
@@ -686,7 +686,7 @@ func (a *ArtistPageHeader) Clear() {
a.fullSizeCoverFetching = false
}
func (a *ArtistPageHeader) Update(artist *mediaprovider.ArtistWithAlbums, im *backend.ImageManager) {
func (a *ArtistPageHeader) Update(artist *mediaprovider.ArtistWithAlbums, _ *backend.ImageManager) {
if artist == nil {
return
}
@@ -695,17 +695,6 @@ func (a *ArtistPageHeader) Update(artist *mediaprovider.ArtistWithAlbums, im *ba
a.artistID = artist.ID
a.titleDisp.Segments[0].(*widget.TextSegment).Text = artist.Name
a.titleDisp.Refresh()
if artist.CoverArtID == "" {
return
}
a.artistImageID = artist.CoverArtID
go func() {
if cover, err := im.GetCoverThumbnail(artist.CoverArtID); err == nil {
fyne.Do(func() { a.artistImage.SetImage(cover, true) })
} else {
log.Printf("error fetching cover: %v", err)
}
}()
}
func (a *ArtistPageHeader) UpdateInfo(info *mediaprovider.ArtistInfo) {
+4 -3
View File
@@ -402,7 +402,10 @@ func (a *NowPlayingPage) updateLyrics() {
return
}
}
if a.nowPlaying == nil || a.nowPlaying.Metadata().Type == mediaprovider.MediaItemTypeRadioStation {
tr, isTrack := a.nowPlaying.(*mediaprovider.Track)
if !isTrack {
a.lyricsLoading.Stop()
a.lyricsViewer.EnableTapToSeek()
a.lyricsViewer.SetLyrics(nil)
a.curLyrics = nil
a.curLyricsID = ""
@@ -417,8 +420,6 @@ func (a *NowPlayingPage) updateLyrics() {
Synced: true,
Lines: []mediaprovider.LyricLine{{Text: ""}},
})
tr, _ := a.nowPlaying.(*mediaprovider.Track)
a.lm.FetchLyricsAsync(tr, func(id string, lyrics *mediaprovider.Lyrics) {
if id != a.nowPlayingID {
return
+248
View File
@@ -0,0 +1,248 @@
package browsing
import (
"fmt"
"html"
"log"
"strings"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/lang"
"fyne.io/fyne/v2/layout"
"fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget"
"github.com/deluan/sanitize"
"github.com/dweymouth/supersonic/backend"
"github.com/dweymouth/supersonic/backend/mediaprovider"
"github.com/dweymouth/supersonic/ui/controller"
myTheme "github.com/dweymouth/supersonic/ui/theme"
"github.com/dweymouth/supersonic/ui/util"
appWidgets "github.com/dweymouth/supersonic/ui/widgets"
)
type PodcastsPage struct {
widget.BaseWidget
contr *controller.Controller
provider mediaprovider.PodcastProvider
pm *backend.PlaybackManager
channels []*mediaprovider.PodcastChannel
episodes []*mediaprovider.PodcastEpisode
channelList, episodeList *widget.List
channelTitle *widget.Label
content *fyne.Container
}
func NewPodcastsPage(c *controller.Controller, p mediaprovider.PodcastProvider, pm *backend.PlaybackManager) *PodcastsPage {
x := &PodcastsPage{contr: c, provider: p, pm: pm, channelTitle: widget.NewLabel(lang.L("Newest episodes"))}
x.ExtendBaseWidget(x)
x.channelTitle.TextStyle.Bold = true
x.channelList = widget.NewList(func() int { return len(x.channels) }, func() fyne.CanvasObject {
title := widget.NewButton("", nil)
more := appWidgets.NewIconButton(theme.MoreVerticalIcon(), nil)
more.IconSize = appWidgets.IconButtonSizeSmaller
more.SetToolTip(lang.L("More"))
return container.NewBorder(nil, nil, nil, more, title)
}, func(id widget.ListItemID, o fyne.CanvasObject) {
ch := x.channels[id]
row := o.(*fyne.Container)
b := row.Objects[0].(*widget.Button)
more := row.Objects[1].(*appWidgets.IconButton)
b.SetText(ch.Title)
b.OnTapped = func() { go x.loadChannel(ch.ID) }
more.OnTapped = func() {
pos := fyne.CurrentApp().Driver().AbsolutePositionForObject(more)
x.showChannelMenu(ch, pos)
}
})
x.episodeList = widget.NewList(func() int { return len(x.episodes) }, x.newEpisodeRow, x.updateEpisodeRow)
header := container.NewHBox(widget.NewLabelWithStyle(lang.L("Podcasts"), fyne.TextAlignLeading, fyne.TextStyle{Bold: true}), layout.NewSpacer())
left := container.NewBorder(widget.NewLabel(lang.L("Channels")), nil, nil, nil, x.channelList)
right := container.NewBorder(x.channelTitle, nil, nil, nil, x.episodeList)
split := container.NewHSplit(left, right)
split.Offset = .27
x.content = container.NewBorder(header, nil, nil, nil, split)
go x.load()
return x
}
func (p *PodcastsPage) showChannelMenu(ch *mediaprovider.PodcastChannel, pos fyne.Position) {
share := fyne.NewMenuItem(lang.L("Share")+"...", func() {
p.contr.ShowShareDialog(ch.ID)
})
share.Icon = myTheme.ShareIcon
_, canShare := p.provider.(mediaprovider.SupportsSharing)
share.Disabled = ch.ID == "" || !canShare
menu := fyne.NewMenu("", share)
widget.ShowPopUpMenuAtPosition(menu, fyne.CurrentApp().Driver().CanvasForObject(p), pos)
}
func (p *PodcastsPage) newEpisodeRow() fyne.CanvasObject {
title := widget.NewLabel("")
title.TextStyle.Bold = true
desc := widget.NewLabel("")
// List rows are virtualized and have a fixed template height. Letting an
// arbitrary feed description wrap makes it paint over following rows.
desc.Wrapping = fyne.TextWrapOff
desc.Truncation = fyne.TextTruncateEllipsis
play := appWidgets.NewIconButton(theme.MediaPlayIcon(), nil)
play.IconSize = appWidgets.IconButtonSizeSlightlyBigger
play.SetToolTip(lang.L("Play"))
playArea := container.NewPadded(container.NewCenter(play))
more := appWidgets.NewIconButton(theme.MoreVerticalIcon(), nil)
more.IconSize = appWidgets.IconButtonSizeSmaller
more.SetToolTip(lang.L("More"))
moreArea := container.NewPadded(container.NewCenter(more))
texts := container.New(layout.NewCustomPaddedVBoxLayout(0), title, desc)
return container.NewBorder(nil, nil, playArea, moreArea, texts)
}
func (p *PodcastsPage) updateEpisodeRow(id widget.ListItemID, obj fyne.CanvasObject) {
e := p.episodes[id]
border := obj.(*fyne.Container)
texts := border.Objects[0].(*fyne.Container)
playArea := border.Objects[1].(*fyne.Container)
playCenter := playArea.Objects[0].(*fyne.Container)
play := playCenter.Objects[0].(*appWidgets.IconButton)
moreArea := border.Objects[2].(*fyne.Container)
moreCenter := moreArea.Objects[0].(*fyne.Container)
more := moreCenter.Objects[0].(*appWidgets.IconButton)
date := ""
if !e.PublishDate.IsZero() {
date = e.PublishDate.Local().Format("Jan 2, 2006")
}
title := e.Title
if date != "" {
title = fmt.Sprintf("%s (%s)", title, date)
}
texts.Objects[0].(*widget.Label).SetText(title)
texts.Objects[1].(*widget.Label).SetText(podcastDescriptionText(e.Description))
play.OnTapped = func() { p.pm.LoadItems([]mediaprovider.MediaItem{e}, backend.Replace, false); p.pm.PlayTrackAt(0) }
more.OnTapped = func() {
pos := fyne.CurrentApp().Driver().AbsolutePositionForObject(more)
p.showEpisodeMenu(e, pos)
}
if e.Playable() {
play.Enable()
} else {
play.Disable()
}
}
func (p *PodcastsPage) showEpisodeMenu(e *mediaprovider.PodcastEpisode, pos fyne.Position) {
play := fyne.NewMenuItem(lang.L("Play"), func() {
p.pm.LoadItems([]mediaprovider.MediaItem{e}, backend.Replace, false)
p.pm.PlayTrackAt(0)
})
play.Icon = theme.MediaPlayIcon()
playNext := fyne.NewMenuItem(lang.L("Play next"), func() {
p.pm.LoadItems([]mediaprovider.MediaItem{e}, backend.InsertNext, false)
})
playNext.Icon = myTheme.PlayNextIcon
addToQueue := fyne.NewMenuItem(lang.L("Add to queue"), func() {
p.pm.LoadItems([]mediaprovider.MediaItem{e}, backend.Append, false)
})
addToQueue.Icon = theme.ContentAddIcon()
addToPlaylist := fyne.NewMenuItem(lang.L("Add to playlist")+"...", func() {
p.contr.DoAddTracksToPlaylistWorkflow([]string{e.StreamID})
})
addToPlaylist.Icon = myTheme.PlaylistIcon
share := fyne.NewMenuItem(lang.L("Share")+"...", func() {
p.contr.ShowShareDialog(e.StreamID)
})
share.Icon = myTheme.ShareIcon
favorite := fyne.NewMenuItem(lang.L("Set favorite"), func() {
p.contr.SetTrackFavorites([]string{e.StreamID}, true)
})
favorite.Icon = myTheme.FavoriteIcon
unfavorite := fyne.NewMenuItem(lang.L("Unset favorite"), func() {
p.contr.SetTrackFavorites([]string{e.StreamID}, false)
})
unfavorite.Icon = myTheme.NotFavoriteIcon
rating := util.NewRatingSubmenu(func(value int) {
p.contr.SetTrackRatings([]string{e.StreamID}, value)
})
play.Disabled = !e.Playable()
playNext.Disabled = !e.Playable()
addToQueue.Disabled = !e.Playable()
mediaActionsDisabled := e.StreamID == ""
addToPlaylist.Disabled = mediaActionsDisabled
favorite.Disabled = mediaActionsDisabled
unfavorite.Disabled = mediaActionsDisabled
_, canRate := p.provider.(mediaprovider.SupportsRating)
rating.Disabled = mediaActionsDisabled || !canRate
_, canShare := p.provider.(mediaprovider.SupportsSharing)
share.Disabled = mediaActionsDisabled || !canShare
menu := fyne.NewMenu("", play, playNext, addToQueue, fyne.NewMenuItemSeparator(),
addToPlaylist, share, fyne.NewMenuItemSeparator(), favorite, unfavorite, rating)
widget.ShowPopUpMenuAtPosition(menu, fyne.CurrentApp().Driver().CanvasForObject(p), pos)
}
func podcastDescriptionText(description string) string {
// Some servers return RSS descriptions still wrapped in CDATA and HTML.
// Labels display plain text, so normalize it before rendering the preview.
description = strings.TrimSpace(description)
description = strings.TrimPrefix(description, "<![CDATA[")
description = strings.TrimSuffix(description, "]]>")
description = html.UnescapeString(sanitize.HTML(description))
return strings.Join(strings.Fields(description), " ")
}
func (p *PodcastsPage) load() {
if p.provider == nil {
return
}
channels, err := p.provider.GetPodcastChannels()
if err != nil {
log.Printf("load podcast channels: %v", err)
return
}
episodes, err := p.provider.GetNewestPodcastEpisodes(20)
if err != nil {
log.Printf("load newest podcasts: %v", err)
}
byID := make(map[string]*mediaprovider.PodcastChannel, len(channels))
for _, ch := range channels {
byID[ch.ID] = ch
}
for _, ep := range episodes {
if ch := byID[ep.ChannelID]; ch != nil {
ep.ChannelTitle = ch.Title
if ep.CoverArtID == "" {
ep.CoverArtID = ch.CoverArtID
}
ep.OriginalImageURL = ch.OriginalImageURL
}
}
fyne.Do(func() {
p.channels = channels
p.episodes = episodes
p.channelTitle.SetText(lang.L("Newest episodes"))
p.channelList.Refresh()
p.episodeList.Refresh()
})
}
func (p *PodcastsPage) loadChannel(id string) {
ch, err := p.provider.GetPodcastChannel(id)
if err != nil {
log.Printf("load podcast channel: %v", err)
return
}
fyne.Do(func() { p.episodes = ch.Episodes; p.channelTitle.SetText(ch.Title); p.episodeList.Refresh() })
}
func (p *PodcastsPage) Reload() { go p.load() }
func (p *PodcastsPage) Route() controller.Route { return controller.PodcastsRoute() }
func (p *PodcastsPage) Save() SavedPage { return &savedPodcastsPage{p.contr, p.provider, p.pm} }
func (p *PodcastsPage) CreateRenderer() fyne.WidgetRenderer {
return widget.NewSimpleRenderer(p.content)
}
type savedPodcastsPage struct {
c *controller.Controller
p mediaprovider.PodcastProvider
pm *backend.PlaybackManager
}
func (s *savedPodcastsPage) Restore() Page { return NewPodcastsPage(s.c, s.p, s.pm) }
+10
View File
@@ -0,0 +1,10 @@
package browsing
import "testing"
func TestPodcastDescriptionText(t *testing.T) {
got := podcastDescriptionText(`<![CDATA[<p>Hello <em>podcast</em> &amp; listeners.</p>]]>`)
if want := "Hello podcast & listeners."; got != want {
t.Fatalf("got %q, want %q", got, want)
}
}
+3
View File
@@ -61,6 +61,9 @@ func (r Router) CreatePage(rte controller.Route) Page {
var rp mediaprovider.RadioProvider
rp, _ = r.App.ServerManager.Server.(mediaprovider.RadioProvider)
return NewRadiosPage(r.Controller, rp, r.App.PlaybackManager)
case controller.Podcasts:
pp, _ := r.App.ServerManager.Server.(mediaprovider.PodcastProvider)
return NewPodcastsPage(r.Controller, pp, r.App.PlaybackManager)
}
return nil
}
+5
View File
@@ -16,6 +16,7 @@ const (
Playlists
Tracks
Radios
Podcasts
)
func (p PageName) String() string {
@@ -44,6 +45,8 @@ func (p PageName) String() string {
return "All Tracks"
case Radios:
return "Internet Radio Stations"
case Podcasts:
return "Podcasts"
default:
return ""
}
@@ -102,6 +105,8 @@ func RadiosRoute() Route {
return Route{Page: Radios}
}
func PodcastsRoute() Route { return Route{Page: Podcasts} }
func NowPlayingRoute() Route {
return Route{Page: NowPlaying}
}
+9 -32
View File
@@ -35,6 +35,7 @@ func (m *Controller) PromptForFirstServer() {
SkipSSLVerify: d.SkipSSLVerify,
}
server := m.App.ServerManager.AddServer(d.Nickname, conn)
server.EnablePodcasts = d.EnablePodcasts
if err := m.trySetPasswordAndConnectToServer(server, d.Password); err != nil {
log.Printf("error connecting to server: %s", err.Error())
}
@@ -67,41 +68,15 @@ func (c *Controller) DoConnectToServerWorkflow(server *backend.ServerConfig) {
}
func (c *Controller) doConnectWithPassword(server *backend.ServerConfig, pass string) {
canceled := false
ctx, cancel := context.WithCancel(context.Background())
dlg := dialog.NewCustom(lang.L("Connecting"), lang.L("Cancel"),
widget.NewLabel(fmt.Sprintf(lang.L("Connecting to")+" %s", server.Nickname)), c.MainWindow)
dlg.SetOnClosed(func() {
canceled = true
cancel()
})
c.haveModal = true
dlg.Show()
// try to connect
go func() {
defer cancel() // make sure to free up ctx resources if user does not cancel
if err := c.tryConnectToServer(ctx, server, pass); err != nil {
if err := c.tryConnectToServer(context.Background(), server, pass); err != nil {
fyne.Do(func() {
dlg.Hide()
c.haveModal = false
if canceled {
dlg := dialog.NewError(err, c.MainWindow)
dlg.SetOnClosed(func() {
c.PromptForLoginAndConnect()
} else {
// connection failure
dlg := dialog.NewError(err, c.MainWindow)
dlg.SetOnClosed(func() {
c.PromptForLoginAndConnect()
})
c.haveModal = true
dlg.Show()
}
})
} else {
fyne.Do(func() {
dlg.Hide()
c.haveModal = false
})
c.haveModal = true
dlg.Show()
})
}
}()
@@ -150,6 +125,7 @@ func (m *Controller) PromptForLoginAndConnect() {
server.Username = editD.Username
server.LegacyAuth = editD.LegacyAuth
server.SkipSSLVerify = editD.SkipSSLVerify
server.EnablePodcasts = editD.EnablePodcasts
m.trySetPasswordAndConnectToServer(server, editD.Password)
m.doModalClosed()
}
@@ -184,6 +160,7 @@ func (m *Controller) PromptForLoginAndConnect() {
SkipSSLVerify: newD.SkipSSLVerify,
}
server := m.App.ServerManager.AddServer(newD.Nickname, conn)
server.EnablePodcasts = newD.EnablePodcasts
m.trySetPasswordAndConnectToServer(server, newD.Password)
m.doModalClosed()
}
+16 -11
View File
@@ -17,16 +17,17 @@ import (
type AddEditServerDialog struct {
widget.BaseWidget
ServerType backend.ServerType
Nickname string
Host string
AltHost string
Username string
Password string
LegacyAuth bool
SkipSSLVerify bool
OnSubmit func()
OnCancel func()
ServerType backend.ServerType
Nickname string
Host string
AltHost string
Username string
Password string
LegacyAuth bool
SkipSSLVerify bool
EnablePodcasts bool
OnSubmit func()
OnCancel func()
passField *widget.Entry
submitBtn *widget.Button
@@ -47,17 +48,21 @@ func NewAddEditServerDialog(title string, cancelable bool, prefillServer *backen
a.Username = prefillServer.Username
a.LegacyAuth = prefillServer.LegacyAuth
a.SkipSSLVerify = prefillServer.SkipSSLVerify
a.EnablePodcasts = prefillServer.EnablePodcasts
}
titleLabel := widget.NewLabel(title)
titleLabel.TextStyle.Bold = true
legacyAuthCheck := widget.NewCheckWithData(lang.L("Use legacy authentication"), binding.BindBool(&a.LegacyAuth))
podcastsCheck := widget.NewCheckWithData(lang.L("Enable podcasts"), binding.BindBool(&a.EnablePodcasts))
serverTypeChoice := widget.NewRadioGroup([]string{"Subsonic", "Jellyfin"}, func(s string) {
a.ServerType = backend.ServerType(s)
if s == string(backend.ServerTypeSubsonic) {
legacyAuthCheck.Show()
podcastsCheck.Show()
} else {
legacyAuthCheck.Hide()
podcastsCheck.Hide()
}
})
skipSSLCheck := widget.NewCheckWithData(lang.L("Skip SSL certificate verification"), binding.BindBool(&a.SkipSSLVerify))
@@ -116,7 +121,7 @@ func NewAddEditServerDialog(title string, cancelable bool, prefillServer *backen
widget.NewLabel(lang.L("Password")),
a.passField,
),
container.NewHBox(layout.NewSpacer(), legacyAuthCheck, skipSSLCheck),
container.NewVBox(podcastsCheck, legacyAuthCheck, skipSSLCheck),
widget.NewSeparator(),
bottomRow,
)
+6
View File
@@ -801,6 +801,11 @@ func (s *SettingsDialog) createAdvancedTab() *container.TabItem {
multi := widget.NewCheckWithData(lang.L("Allow multiple app instances"), binding.BindBool(&s.config.Application.AllowMultiInstance))
update := widget.NewCheckWithData(lang.L("Automatically check for updates"), binding.BindBool(&s.config.Application.EnableAutoUpdateChecker))
lrclib := widget.NewCheckWithData(lang.L("Enable LrcLib lyrics fetcher"), binding.BindBool(&s.config.Application.EnableLrcLib))
savePasswords := widget.NewCheck(lang.L("Save passwords on this device"), func(enabled bool) {
s.config.Application.EnablePasswordStorage = enabled
s.setRestartRequired()
})
savePasswords.Checked = s.config.Application.EnablePasswordStorage
threeDigitValidator := func(text, selText string, r rune) bool {
return unicode.IsDigit(r) && len(text)-len(selText) < 3
@@ -842,6 +847,7 @@ func (s *SettingsDialog) createAdvancedTab() *container.TabItem {
multi,
update,
lrclib,
savePasswords,
osMediaAPIs,
preventScreensaver,
imgCacheCfg,
+2
View File
@@ -369,6 +369,8 @@ func (m *MainWindow) RunOnServerConnectedTasks(serverConf *backend.ServerConfig,
_, supportsRadio := m.App.ServerManager.Server.(mediaprovider.RadioProvider)
m.Toolbar.SetRadioButtonVisible(supportsRadio)
_, supportsPodcasts := m.App.ServerManager.Server.(mediaprovider.PodcastProvider)
m.Toolbar.SetPodcastButtonVisible(serverConf.EnablePodcasts && supportsPodcasts)
})
m.App.SaveConfigFile()
+4 -3
View File
@@ -106,7 +106,10 @@ func (s *Sidebar) updateLyrics() {
return
}
}
if s.nowPlaying == nil || s.nowPlaying.Metadata().Type == mediaprovider.MediaItemTypeRadioStation {
tr, isTrack := s.nowPlaying.(*mediaprovider.Track)
if !isTrack {
s.lyricsLoading.Stop()
s.lyricsViewer.EnableTapToSeek()
s.lyricsViewer.SetLyrics(nil)
s.curLyrics = nil
s.curLyricsID = ""
@@ -121,8 +124,6 @@ func (s *Sidebar) updateLyrics() {
Synced: true,
Lines: []mediaprovider.LyricLine{{Text: ""}},
})
tr, _ := s.nowPlaying.(*mediaprovider.Track)
s.lm.FetchLyricsAsync(tr, func(id string, lyrics *mediaprovider.Lyrics) {
if id != s.nowPlayingID {
return
+13
View File
@@ -28,6 +28,7 @@ type Toolbar struct {
navBtnsContainer *fyne.Container
navBtnsPageMap map[controller.PageName]fyne.Resource
radioBtn fyne.CanvasObject
podcastBtn fyne.CanvasObject
quickSearchBtn *ttwidget.Button
sidebarBtn *ttwidget.Button
@@ -82,6 +83,14 @@ func (t *Toolbar) SetRadioButtonVisible(vis bool) {
}
}
func (t *Toolbar) SetPodcastButtonVisible(vis bool) {
if vis {
t.podcastBtn.Show()
} else {
t.podcastBtn.Hide()
}
}
// AddSettingsMenuItem adds an item to the Settings menu
func (t *Toolbar) AddSettingsMenuItem(label string, icon fyne.Resource, action func()) {
item := fyne.NewMenuItem(label, action)
@@ -170,6 +179,10 @@ func (t *Toolbar) setupNavigationButtons(navigateFn func(controller.Route)) {
t.radioBtn = t.addNavigationButton(myTheme.RadioIcon, controller.Radios, func() {
navigateFn(controller.RadiosRoute())
})
t.podcastBtn = t.addNavigationButton(myTheme.HeadphonesIcon, controller.Podcasts, func() {
navigateFn(controller.PodcastsRoute())
})
t.podcastBtn.Hide()
}
func (t *Toolbar) addNavigationButton(icon fyne.Resource, pageName controller.PageName, action func()) *ttwidget.Button {
+28 -6
View File
@@ -23,8 +23,9 @@ import (
type NowPlayingCard struct {
widget.BaseWidget
DisableRating bool
ShowAlbumYear bool
DisableRating bool
DisableSharing bool
ShowAlbumYear bool
trackName *OptionHyperlink
artistName *MultiHyperlink
@@ -33,7 +34,9 @@ type NowPlayingCard struct {
menu *widget.PopUpMenu
ratingMenu *fyne.MenuItem
albumYear string
albumYear string
isPodcast bool
podcastSharingAvailable bool
OnTrackNameTapped func()
OnArtistNameTapped func(artistID string)
@@ -144,6 +147,8 @@ func (n *NowPlayingCard) Update(track mediaprovider.MediaItem) {
n.artistName.BuildSegments([]string{}, []string{})
n.albumName.BuildSegments([]string{}, []string{})
n.albumYear = ""
n.isPodcast = false
n.podcastSharingAvailable = false
n.cover.Hidden = true
} else {
n.cover.Hidden = false
@@ -153,15 +158,25 @@ func (n *NowPlayingCard) Update(track mediaprovider.MediaItem) {
n.albumName.BuildSegments([]string{tr.Album}, []string{tr.AlbumID})
n.albumYear = strconv.Itoa(tr.Year)
n.cover.PlaceholderIcon = myTheme.TracksIcon
} else if episode, ok := track.(*mediaprovider.PodcastEpisode); ok {
n.artistName.BuildSegments([]string{episode.ChannelTitle}, nil)
n.albumName.BuildSegments([]string{}, []string{})
n.albumYear = ""
n.cover.PlaceholderIcon = myTheme.RadioIcon
n.isPodcast = true
n.podcastSharingAvailable = episode.StreamID != ""
} else {
n.artistName.BuildSegments([]string{}, []string{})
n.albumName.BuildSegments([]string{}, []string{})
n.albumName.Suffix = ""
n.cover.PlaceholderIcon = myTheme.RadioIcon
n.isPodcast = false
n.podcastSharingAvailable = false
}
}
n.trackName.Hidden = n.trackName.Text() == ""
n.trackName.SetMenuBtnEnabled(n.cover.PlaceholderIcon != myTheme.RadioIcon)
n.trackName.SetMenuBtnEnabled(n.cover.PlaceholderIcon != myTheme.RadioIcon ||
(n.isPodcast && n.podcastSharingAvailable && !n.DisableSharing))
n.artistName.Hidden = len(n.artistName.Segments) == 0
n.albumName.Hidden = len(n.albumName.Segments) == 0
n.Refresh()
@@ -181,7 +196,11 @@ func (n *NowPlayingCard) Refresh() {
}
func (n *NowPlayingCard) showMenu(btnPos fyne.Position) {
if n.menu == nil {
if n.isPodcast {
share := fyne.NewMenuItem(lang.L("Share")+"...", func() { n.onShare() })
share.Icon = myTheme.ShareIcon
n.menu = widget.NewPopUpMenu(fyne.NewMenu("", share), fyne.CurrentApp().Driver().CanvasForObject(n))
} else {
n.ratingMenu = util.NewRatingSubmenu(n.onSetRating)
favorite := fyne.NewMenuItem(lang.L("Set favorite"), func() { n.onSetFavorite(true) })
favorite.Icon = myTheme.FavoriteIcon
@@ -193,12 +212,15 @@ func (n *NowPlayingCard) showMenu(btnPos fyne.Position) {
info.Icon = theme.InfoIcon()
share := fyne.NewMenuItem(lang.L("Share")+"...", func() { n.onShare() })
share.Icon = myTheme.ShareIcon
share.Disabled = n.DisableSharing
m := fyne.NewMenu("", favorite, unfavorite, n.ratingMenu, playlist, info, share)
n.menu = widget.NewPopUpMenu(m, fyne.CurrentApp().Driver().CanvasForObject(n))
}
menuSize := n.menu.MinSize()
n.ratingMenu.Disabled = n.DisableRating
if !n.isPodcast {
n.ratingMenu.Disabled = n.DisableRating
}
btnPos.Y -= (menuSize.Height + theme.Padding()*3)
btnPos.X -= menuSize.Width / 2
n.menu.ShowAtPosition(btnPos)