add features
This commit is contained in:
+9
-2
@@ -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())
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) }
|
||||
@@ -0,0 +1,10 @@
|
||||
package browsing
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestPodcastDescriptionText(t *testing.T) {
|
||||
got := podcastDescriptionText(`<![CDATA[<p>Hello <em>podcast</em> & listeners.</p>]]>`)
|
||||
if want := "Hello podcast & listeners."; got != want {
|
||||
t.Fatalf("got %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user