Files
supersonic/ui/browsing/albumpage.go
T
natilou b4ab95f5b4 Ability to download album and playlist
Add in the "meatball menu" of the Album and Playlist pages the option to
download the tracks of the corresponding album or playlist in a zip file.
When download is clicked, a dialog window appears allowing you to choose
the file name and where to save the file.
2023-06-17 02:18:08 -03:00

285 lines
8.0 KiB
Go

package browsing
import (
"fmt"
"log"
"github.com/dweymouth/supersonic/backend"
"github.com/dweymouth/supersonic/backend/mediaprovider"
"github.com/dweymouth/supersonic/sharedutil"
"github.com/dweymouth/supersonic/ui/controller"
"github.com/dweymouth/supersonic/ui/layouts"
myTheme "github.com/dweymouth/supersonic/ui/theme"
"github.com/dweymouth/supersonic/ui/util"
"github.com/dweymouth/supersonic/ui/widgets"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/canvas"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget"
)
type AlbumPage struct {
widget.BaseWidget
albumPageState
header *AlbumPageHeader
tracks []*mediaprovider.Track
tracklist *widgets.Tracklist
nowPlayingID string
container *fyne.Container
}
type albumPageState struct {
albumID string
sort widgets.TracklistSort
cfg *backend.AlbumPageConfig
mp mediaprovider.MediaProvider
pm *backend.PlaybackManager
im *backend.ImageManager
contr *controller.Controller
}
func NewAlbumPage(
albumID string,
cfg *backend.AlbumPageConfig,
pm *backend.PlaybackManager,
mp mediaprovider.MediaProvider,
im *backend.ImageManager,
contr *controller.Controller,
) *AlbumPage {
return newAlbumPage(albumID, cfg, pm, mp, im, contr, widgets.TracklistSort{})
}
func newAlbumPage(
albumID string,
cfg *backend.AlbumPageConfig,
pm *backend.PlaybackManager,
mp mediaprovider.MediaProvider,
im *backend.ImageManager,
contr *controller.Controller,
sort widgets.TracklistSort,
) *AlbumPage {
a := &AlbumPage{
albumPageState: albumPageState{
albumID: albumID,
cfg: cfg,
pm: pm,
mp: mp,
im: im,
contr: contr,
},
}
a.ExtendBaseWidget(a)
a.header = NewAlbumPageHeader(a)
a.tracklist = widgets.NewTracklist(nil)
a.tracklist.SetVisibleColumns(a.cfg.TracklistColumns)
a.tracklist.SetSorting(sort)
a.tracklist.OnVisibleColumnsChanged = func(cols []string) {
a.cfg.TracklistColumns = cols
}
a.contr.ConnectTracklistActions(a.tracklist)
a.container = container.NewBorder(
container.New(&layouts.MaxPadLayout{PadLeft: 15, PadRight: 15, PadTop: 15, PadBottom: 10}, a.header),
nil, nil, nil, container.New(&layouts.MaxPadLayout{PadLeft: 15, PadRight: 15, PadBottom: 15}, a.tracklist))
go a.load()
return a
}
func (a *AlbumPage) CreateRenderer() fyne.WidgetRenderer {
return widget.NewSimpleRenderer(a.container)
}
func (a *AlbumPage) Save() SavedPage {
s := a.albumPageState
s.sort = a.tracklist.Sorting()
return &s
}
func (a *AlbumPage) Route() controller.Route {
return controller.AlbumRoute(a.albumID)
}
func (a *AlbumPage) OnSongChange(track, lastScrobbledIfAny *mediaprovider.Track) {
if track == nil {
a.nowPlayingID = ""
} else {
a.nowPlayingID = track.ID
}
a.tracklist.SetNowPlaying(a.nowPlayingID)
a.tracklist.IncrementPlayCount(sharedutil.TrackIDOrEmptyStr(lastScrobbledIfAny))
}
func (a *AlbumPage) Reload() {
go a.load()
}
func (a *AlbumPage) Tapped(*fyne.PointEvent) {
a.tracklist.UnselectAll()
}
func (a *AlbumPage) SelectAll() {
a.tracklist.SelectAll()
}
// should be called asynchronously
func (a *AlbumPage) load() {
album, err := a.mp.GetAlbum(a.albumID)
if err != nil {
log.Printf("Failed to get album: %s", err.Error())
return
}
a.header.Update(album, a.im)
a.tracklist.ShowDiscNumber = album.Tracks[0].DiscNumber != album.Tracks[len(album.Tracks)-1].DiscNumber
a.tracks = album.Tracks
a.tracklist.SetTracks(album.Tracks)
a.tracklist.SetNowPlaying(a.nowPlayingID)
}
type AlbumPageHeader struct {
widget.BaseWidget
albumID string
coverID string
artistID string
genre string
page *AlbumPage
cover *widgets.TappableImage
titleLabel *widget.RichText
artistLabel *widgets.CustomHyperlink
genreLabel *widgets.CustomHyperlink
miscLabel *widget.Label
toggleFavButton *widgets.FavoriteButton
container *fyne.Container
}
func NewAlbumPageHeader(page *AlbumPage) *AlbumPageHeader {
a := &AlbumPageHeader{page: page}
a.ExtendBaseWidget(a)
a.cover = widgets.NewTappableImage(func(*fyne.PointEvent) { go a.showPopUpCover() })
a.cover.FillMode = canvas.ImageFillContain
a.cover.SetMinSize(fyne.NewSize(225, 225))
a.titleLabel = widget.NewRichTextWithText("")
a.titleLabel.Wrapping = fyne.TextTruncate
a.titleLabel.Segments[0].(*widget.TextSegment).Style = widget.RichTextStyle{
SizeName: theme.SizeNameHeadingText,
}
a.artistLabel = widgets.NewCustomHyperlink()
a.artistLabel.OnTapped = func() {
page.contr.NavigateTo(controller.ArtistRoute(a.artistID))
}
a.genreLabel = widgets.NewCustomHyperlink()
a.genreLabel.OnTapped = func() {
page.contr.NavigateTo(controller.GenreRoute(a.genre))
}
a.miscLabel = widget.NewLabel("")
playButton := widget.NewButtonWithIcon("Play", theme.MediaPlayIcon(), func() {
go page.pm.PlayAlbum(page.albumID, 0, false)
})
shuffleBtn := widget.NewButtonWithIcon(" Shuffle", myTheme.ShuffleIcon, func() {
page.pm.LoadTracks(page.tracklist.GetTracks(), false, true)
page.pm.PlayFromBeginning()
})
var pop *widget.PopUpMenu
menuBtn := widget.NewButtonWithIcon("", theme.MoreHorizontalIcon(), nil)
menuBtn.OnTapped = func() {
if pop == nil {
menu := fyne.NewMenu("",
fyne.NewMenuItem("Add to queue", func() {
go a.page.pm.LoadAlbum(a.albumID, true /*append*/, false /*shuffle*/)
}),
fyne.NewMenuItem("Add to playlist...", func() {
a.page.contr.DoAddTracksToPlaylistWorkflow(
sharedutil.TracksToIDs(a.page.tracks))
}),
fyne.NewMenuItem("Download", func() {
a.page.contr.ShowDownloadDialog(a.page.tracks)
}))
pop = widget.NewPopUpMenu(menu, fyne.CurrentApp().Driver().CanvasForObject(a))
}
pos := fyne.CurrentApp().Driver().AbsolutePositionForObject(menuBtn)
pop.ShowAtPosition(fyne.NewPos(pos.X, pos.Y+menuBtn.Size().Height))
}
a.toggleFavButton = widgets.NewFavoriteButton(func() { go a.toggleFavorited() })
// Todo: there's got to be a way to make this less convoluted. Custom layout?
a.container = container.NewBorder(nil, nil, a.cover, nil,
container.New(&layouts.VboxCustomPadding{ExtraPad: -10},
a.titleLabel,
container.NewVBox(
container.New(&layouts.VboxCustomPadding{ExtraPad: -12}, a.artistLabel, a.genreLabel, a.miscLabel),
container.NewVBox(
container.NewHBox(util.NewHSpace(2), playButton, shuffleBtn, menuBtn),
container.NewHBox(util.NewHSpace(2), a.toggleFavButton),
),
),
),
)
return a
}
func (a *AlbumPageHeader) CreateRenderer() fyne.WidgetRenderer {
return widget.NewSimpleRenderer(a.container)
}
func (a *AlbumPageHeader) Update(album *mediaprovider.AlbumWithTracks, im *backend.ImageManager) {
a.albumID = album.ID
a.coverID = album.CoverArtID
a.artistID = album.ArtistIDs[0]
a.titleLabel.Segments[0].(*widget.TextSegment).Text = album.Name
a.artistLabel.SetText(album.ArtistNames[0])
a.genre = album.Genres[0]
a.genreLabel.SetText(album.Genres[0])
a.miscLabel.SetText(formatMiscLabelStr(album))
a.toggleFavButton.IsFavorited = album.Favorite
a.Refresh()
go func() {
if cover, err := im.GetCoverThumbnail(album.CoverArtID); err == nil {
a.cover.Image.Image = cover
a.cover.Refresh()
} else {
log.Printf("error fetching cover: %v", err)
}
}()
}
func (a *AlbumPageHeader) toggleFavorited() {
params := mediaprovider.RatingFavoriteParameters{AlbumIDs: []string{a.albumID}}
a.page.mp.SetFavorite(params, a.toggleFavButton.IsFavorited)
}
func (a *AlbumPageHeader) showPopUpCover() {
cover, err := a.page.im.GetFullSizeCoverArt(a.coverID)
if err != nil {
log.Printf("error getting full size album cover: %s", err.Error())
return
}
a.page.contr.ShowPopUpImage(cover)
}
func formatMiscLabelStr(a *mediaprovider.AlbumWithTracks) string {
var discs string
if discCount := a.Tracks[len(a.Tracks)-1].DiscNumber; discCount > 1 {
discs = fmt.Sprintf("%d discs · ", discCount)
}
tracks := "tracks"
if a.TrackCount == 1 {
tracks = "track"
}
return fmt.Sprintf("%d · %d %s · %s%s", a.Year, a.TrackCount, tracks, discs, util.SecondsToTimeString(float64(a.Duration)))
}
func (s *albumPageState) Restore() Page {
return newAlbumPage(s.albumID, s.cfg, s.pm, s.mp, s.im, s.contr, s.sort)
}