add features
This commit is contained in:
@@ -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) }
|
||||
Reference in New Issue
Block a user