* fix: ensure load callback fires on error Calls callOnLoaded(nil) when image loading fails. This fixes the bottomplayer silently retaining stale images by ensuring consumers always get a callback and can clear their display state. * fix: clear background layers on missing art nil state now hides the blurred background widgets or animates the gradient to transparent, depending on the active configuration
636 lines
17 KiB
Go
636 lines
17 KiB
Go
package browsing
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"image"
|
|
"image/color"
|
|
"log"
|
|
"net/url"
|
|
"slices"
|
|
"strings"
|
|
|
|
"github.com/boxes-ltd/imaging"
|
|
"github.com/cenkalti/dominantcolor"
|
|
"github.com/dweymouth/supersonic/backend"
|
|
"github.com/dweymouth/supersonic/backend/mediaprovider"
|
|
"github.com/dweymouth/supersonic/backend/player"
|
|
"github.com/dweymouth/supersonic/backend/player/mpv"
|
|
"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/lang"
|
|
"fyne.io/fyne/v2/layout"
|
|
"fyne.io/fyne/v2/theme"
|
|
"fyne.io/fyne/v2/widget"
|
|
)
|
|
|
|
type NowPlayingPage struct {
|
|
widget.BaseWidget
|
|
|
|
nowPlayingPageState
|
|
|
|
// volatile state
|
|
nowPlaying mediaprovider.MediaItem
|
|
nowPlayingID string
|
|
curLyrics *mediaprovider.Lyrics
|
|
curLyricsID string // id of track currently shown in lyrics
|
|
curRelatedID string // id of track currrently used to populate related list
|
|
curCoverArt string // id of cover art currently shown in background / card
|
|
totalTime float64
|
|
lastPlayPos float64
|
|
queue []mediaprovider.MediaItem
|
|
related []*mediaprovider.Track
|
|
alreadyLoaded bool
|
|
|
|
// widgets for render
|
|
backgroundImgA *canvas.Image
|
|
backgroundImgB *canvas.Image
|
|
backgroundGradient *canvas.LinearGradient
|
|
queueList *widgets.PlayQueueList
|
|
relatedList *widgets.PlayQueueList
|
|
lyricsViewer *widgets.LyricsViewer
|
|
card *widgets.LargeNowPlayingCard
|
|
statusLabel *widget.Label
|
|
tabs *container.AppTabs
|
|
lyricsLoading *widgets.LoadingDots
|
|
relatedLoading *widgets.LoadingDots
|
|
container *fyne.Container
|
|
|
|
// cancel funcs for background fetch tasks
|
|
imageLoadCancel context.CancelFunc
|
|
relatedFetchCancel context.CancelFunc
|
|
}
|
|
|
|
type nowPlayingPageState struct {
|
|
conf *backend.NowPlayingPageConfig
|
|
contr *controller.Controller
|
|
pool *util.WidgetPool
|
|
sm *backend.ServerManager
|
|
pm *backend.PlaybackManager
|
|
lm *backend.LyricsManager
|
|
im *backend.ImageManager
|
|
mp mediaprovider.MediaProvider
|
|
canRate bool
|
|
canShare bool
|
|
cfg *backend.Config
|
|
}
|
|
|
|
func NewNowPlayingPage(
|
|
conf *backend.NowPlayingPageConfig,
|
|
contr *controller.Controller,
|
|
pool *util.WidgetPool,
|
|
sm *backend.ServerManager,
|
|
lm *backend.LyricsManager,
|
|
im *backend.ImageManager,
|
|
pm *backend.PlaybackManager,
|
|
mp mediaprovider.MediaProvider,
|
|
canRate bool,
|
|
canShare bool,
|
|
cfg *backend.Config,
|
|
) *NowPlayingPage {
|
|
state := nowPlayingPageState{
|
|
conf: conf, contr: contr, pool: pool, sm: sm, lm: lm, im: im, pm: pm, mp: mp, canRate: canRate, canShare: canShare, cfg: cfg,
|
|
}
|
|
if page, ok := pool.Obtain(util.WidgetTypeNowPlayingPage).(*NowPlayingPage); ok && page != nil {
|
|
page.nowPlayingPageState = state
|
|
page.Reload()
|
|
return page
|
|
}
|
|
a := &NowPlayingPage{nowPlayingPageState: state}
|
|
a.ExtendBaseWidget(a)
|
|
|
|
doFmtStatus := util.FyneDoFunc(a.formatStatusLine)
|
|
pm.OnPaused(doFmtStatus)
|
|
pm.OnPlaying(doFmtStatus)
|
|
pm.OnStopped(doFmtStatus)
|
|
|
|
a.card = widgets.NewLargeNowPlayingCard()
|
|
a.card.OnAlbumNameTapped = func() {
|
|
contr.NavigateTo(controller.AlbumRoute(a.nowPlaying.Metadata().AlbumID))
|
|
}
|
|
a.card.OnArtistNameTapped = func(artistID string) {
|
|
contr.NavigateTo(controller.ArtistRoute(artistID))
|
|
}
|
|
a.card.OnRadioURLTapped = func(urlText string) {
|
|
if u, err := url.Parse(urlText); err == nil {
|
|
fyne.CurrentApp().OpenURL(u)
|
|
}
|
|
}
|
|
a.card.OnSetFavorite = func(fav bool) {
|
|
a.contr.SetTrackFavorites([]string{a.nowPlayingID}, fav)
|
|
}
|
|
a.card.OnSetRating = func(rating int) {
|
|
a.contr.SetTrackRatings([]string{a.nowPlayingID}, rating)
|
|
}
|
|
a.card.ShowAlbumYear = cfg.AlbumsPage.ShowYears
|
|
|
|
a.queueList = widgets.NewPlayQueueList(a.im, false)
|
|
a.relatedList = widgets.NewPlayQueueList(a.im, true)
|
|
a.queueList.Reorderable = true
|
|
|
|
a.contr.ConnectPlayQueuelistActions(a.queueList)
|
|
// override OnSetRating and Favorite so we can also update the
|
|
// LargeNowPlayingCard's displayed rating and favorite
|
|
a.queueList.OnSetRating = func(trackIDs []string, rating int) {
|
|
contr.SetTrackRatings(trackIDs, rating)
|
|
if slices.Contains(trackIDs, a.nowPlayingID) {
|
|
a.card.SetDisplayedRating(rating)
|
|
}
|
|
}
|
|
a.queueList.OnSetFavorite = func(trackIDs []string, fav bool) {
|
|
contr.SetTrackFavorites(trackIDs, fav)
|
|
if slices.Contains(trackIDs, a.nowPlayingID) {
|
|
a.card.SetDisplayedFavorite(fav)
|
|
}
|
|
}
|
|
|
|
a.relatedList.OnDownload = a.queueList.OnDownload
|
|
a.relatedList.OnShare = a.queueList.OnShare
|
|
a.relatedList.OnAddToPlaylist = a.queueList.OnAddToPlaylist
|
|
a.relatedList.OnShowArtistPage = a.queueList.OnShowArtistPage
|
|
a.relatedList.OnSetRating = a.queueList.OnSetRating
|
|
a.relatedList.OnSetFavorite = a.queueList.OnSetFavorite
|
|
a.relatedList.OnPlayItemAt = func(idx int) {
|
|
a.pm.LoadTracks(a.related, backend.Replace, false)
|
|
a.pm.PlayTrackAt(idx)
|
|
}
|
|
a.relatedList.OnAddToQueue = func(items []mediaprovider.MediaItem) {
|
|
a.pm.LoadItems(items, backend.Append, false)
|
|
}
|
|
a.relatedList.OnPlaySelection = func(items []mediaprovider.MediaItem, shuffle bool) {
|
|
a.pm.LoadItems(items, backend.Replace, shuffle)
|
|
a.pm.PlayFromBeginning()
|
|
}
|
|
a.relatedList.OnPlaySelectionNext = func(items []mediaprovider.MediaItem) {
|
|
a.pm.LoadItems(items, backend.InsertNext, false)
|
|
}
|
|
a.relatedList.OnPlaySongRadio = func(track *mediaprovider.Track) {
|
|
go func() {
|
|
if err := a.pm.PlaySimilarSongs(track.ID); err != nil {
|
|
fyne.Do(func() {
|
|
a.contr.ToastProvider.ShowErrorToast(lang.L("Unable to play song radio"))
|
|
})
|
|
}
|
|
}()
|
|
}
|
|
a.relatedList.OnShowTrackInfo = func(track *mediaprovider.Track) {
|
|
a.contr.ShowTrackInfoDialog(track)
|
|
}
|
|
a.lyricsViewer = widgets.NewLyricsViewer(a.onSeekToLyricLine)
|
|
a.statusLabel = widget.NewLabel(lang.L("Stopped"))
|
|
|
|
a.Reload()
|
|
return a
|
|
}
|
|
|
|
func (a *NowPlayingPage) onSeekToLyricLine(lineNum int) {
|
|
if a.curLyrics != nil && len(a.curLyrics.Lines) > lineNum-1 {
|
|
time := a.curLyrics.Lines[lineNum-1].Start
|
|
a.pm.SeekSeconds(time)
|
|
}
|
|
}
|
|
|
|
func (a *NowPlayingPage) CreateRenderer() fyne.WidgetRenderer {
|
|
if a.container == nil {
|
|
initialTab := 0
|
|
if a.conf.InitialView == "Lyrics" {
|
|
initialTab = 1
|
|
} else if a.conf.InitialView == "Related" {
|
|
initialTab = 2
|
|
}
|
|
_ = initialTab
|
|
paddedLayout := &layouts.PercentPadLayout{
|
|
LeftRightObjectPercent: .8,
|
|
TopBottomObjectPercent: .8,
|
|
}
|
|
a.lyricsLoading = widgets.NewLoadingDots()
|
|
a.relatedLoading = widgets.NewLoadingDots()
|
|
a.tabs = container.NewAppTabs(
|
|
container.NewTabItem(lang.L("Play Queue"),
|
|
container.NewBorder(layout.NewSpacer(), nil, nil, nil, a.queueList)),
|
|
container.NewTabItem(lang.L("Lyrics"), container.NewStack(
|
|
a.lyricsViewer,
|
|
container.NewCenter(a.lyricsLoading))),
|
|
container.NewTabItem(lang.L("Related"), container.NewStack(
|
|
a.relatedList,
|
|
container.NewCenter(a.relatedLoading))),
|
|
)
|
|
a.tabs.SelectIndex(initialTab)
|
|
a.tabs.OnSelected = func(*container.TabItem) {
|
|
idx := a.tabs.SelectedIndex()
|
|
a.saveSelectedTab(idx)
|
|
if idx == 1 /*lyrics*/ {
|
|
a.updateLyrics()
|
|
} else if idx == 2 /*related*/ {
|
|
a.updateRelatedList()
|
|
}
|
|
}
|
|
if initialTab == 1 /*lyrics*/ {
|
|
a.updateLyrics()
|
|
} else if initialTab == 2 /*related*/ {
|
|
a.updateRelatedList()
|
|
}
|
|
c := theme.Color(myTheme.ColorNamePageBackground)
|
|
a.backgroundGradient = canvas.NewLinearGradient(c, c, 0)
|
|
a.backgroundImgA = canvas.NewImageFromImage(nil)
|
|
a.backgroundImgB = canvas.NewImageFromImage(nil)
|
|
|
|
mainContent := container.NewGridWithColumns(2,
|
|
container.New(paddedLayout, a.card),
|
|
container.New(paddedLayout,
|
|
util.AddHeaderBackgroundWithColorName(
|
|
a.tabs, myTheme.ColorNameNowPlayingPanel)))
|
|
a.container = container.NewStack(
|
|
a.backgroundImgA,
|
|
a.backgroundImgB,
|
|
a.backgroundGradient,
|
|
mainContent,
|
|
container.NewVBox(
|
|
layout.NewSpacer(),
|
|
container.NewBorder(nil, nil, util.NewHSpace(1), util.NewHSpace(1),
|
|
myTheme.NewThemedRectangle(theme.ColorNameInputBorder)),
|
|
a.statusLabel,
|
|
),
|
|
)
|
|
}
|
|
return widget.NewSimpleRenderer(a.container)
|
|
}
|
|
|
|
func (a *NowPlayingPage) Save() SavedPage {
|
|
if a.imageLoadCancel != nil {
|
|
a.imageLoadCancel()
|
|
}
|
|
a.alreadyLoaded = false
|
|
nps := a.nowPlayingPageState
|
|
a.pool.Release(util.WidgetTypeNowPlayingPage, a)
|
|
return &nps
|
|
}
|
|
|
|
func (a *NowPlayingPage) Route() controller.Route {
|
|
return controller.NowPlayingRoute()
|
|
}
|
|
|
|
var _ Scrollable = (*NowPlayingPage)(nil)
|
|
|
|
func (a *NowPlayingPage) Scroll(delta float32) {
|
|
switch a.tabs.SelectedIndex() {
|
|
case 0: /*play queue*/
|
|
a.queueList.Scroll(delta)
|
|
case 1: /*lyrics*/
|
|
case 2: /*related*/
|
|
a.relatedList.Scroll(delta)
|
|
}
|
|
}
|
|
|
|
var _ CanShowNowPlaying = (*NowPlayingPage)(nil)
|
|
|
|
func (a *NowPlayingPage) OnSongChange(song mediaprovider.MediaItem, lastScrobbledIfAny *mediaprovider.Track) {
|
|
a.nowPlaying = song
|
|
if a.imageLoadCancel != nil {
|
|
a.imageLoadCancel()
|
|
}
|
|
a.nowPlayingID = sharedutil.MediaItemIDOrEmptyStr(song)
|
|
a.queueList.SetNowPlaying(a.nowPlayingID)
|
|
if !a.alreadyLoaded {
|
|
a.queueList.ScrollToNowPlaying()
|
|
a.alreadyLoaded = true
|
|
}
|
|
a.relatedList.SetNowPlaying(a.nowPlayingID)
|
|
|
|
a.card.Update(song)
|
|
if song == nil {
|
|
a.card.SetCoverImage(nil)
|
|
a.curCoverArt = ""
|
|
} else if artID := song.Metadata().CoverArtID; artID != a.curCoverArt {
|
|
a.imageLoadCancel = a.im.GetFullSizeCoverArtAsync(song.Metadata().CoverArtID, func(img image.Image, err error) {
|
|
a.curCoverArt = artID
|
|
a.onImageLoaded(img, err)
|
|
})
|
|
}
|
|
|
|
if a.tabs != nil && a.tabs.SelectedIndex() == 1 /*lyrics*/ {
|
|
a.updateLyrics()
|
|
} else if a.tabs != nil && a.tabs.SelectedIndex() == 2 /*related*/ {
|
|
a.updateRelatedList()
|
|
}
|
|
}
|
|
|
|
func (a *NowPlayingPage) onImageLoaded(img image.Image, err error) {
|
|
if err != nil {
|
|
log.Printf("error loading cover art: %v\n", err)
|
|
}
|
|
|
|
fyne.Do(func() { a.card.SetCoverImage(img) })
|
|
if img == nil {
|
|
if a.conf.UseBackgroundImage {
|
|
fyne.Do(func() {
|
|
if !a.backgroundImgA.Hidden {
|
|
a.backgroundImgA.Hide()
|
|
}
|
|
if !a.backgroundImgB.Hidden {
|
|
a.backgroundImgB.Hide()
|
|
}
|
|
})
|
|
} else {
|
|
if a.backgroundGradient.StartColor != color.Transparent {
|
|
anim := canvas.NewColorRGBAAnimation(
|
|
a.backgroundGradient.StartColor, color.Transparent, myTheme.AnimationDurationMedium, func(c color.Color) {
|
|
a.backgroundGradient.StartColor = c
|
|
a.backgroundGradient.Refresh()
|
|
})
|
|
anim.Start()
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
if a.conf.UseBackgroundImage {
|
|
resized := imaging.Resize(img, 300, 0, imaging.NearestNeighbor)
|
|
blurred := imaging.Blur(resized, 10.0)
|
|
fyne.Do(func() {
|
|
if a.backgroundGradient.StartColor != color.Transparent {
|
|
a.backgroundGradient.StartColor = color.Transparent
|
|
a.backgroundGradient.Refresh()
|
|
}
|
|
a.backgroundImgA.Hidden = false
|
|
a.backgroundImgB.Hidden = false
|
|
a.backgroundImgA.Image = a.backgroundImgB.Image
|
|
a.backgroundImgB.Image = blurred
|
|
fyne.NewAnimation(myTheme.AnimationDurationMedium, func(f float32) {
|
|
a.backgroundImgA.Translucency = float64(f)
|
|
a.backgroundImgB.Translucency = float64(1 - f)
|
|
a.backgroundImgA.Refresh()
|
|
a.backgroundImgB.Refresh()
|
|
}).Start()
|
|
})
|
|
} else {
|
|
c := dominantcolor.Find(img)
|
|
if c == a.backgroundGradient.StartColor {
|
|
return
|
|
}
|
|
if !a.backgroundImgA.Hidden {
|
|
a.backgroundImgA.Hide()
|
|
}
|
|
if !a.backgroundImgB.Hidden {
|
|
a.backgroundImgB.Hide()
|
|
}
|
|
// Fyne animation starting is currently thread-safe,
|
|
// despite not being marked as such
|
|
// TODO: if this changes, use fyne.Do
|
|
anim := canvas.NewColorRGBAAnimation(
|
|
a.backgroundGradient.StartColor, c, myTheme.AnimationDurationMedium, func(c color.Color) {
|
|
a.backgroundGradient.StartColor = c
|
|
a.backgroundGradient.Refresh()
|
|
})
|
|
anim.Start()
|
|
}
|
|
}
|
|
|
|
func (a *NowPlayingPage) updateLyrics() {
|
|
if a.nowPlayingID == a.curLyricsID {
|
|
if a.nowPlayingID != "" {
|
|
// just need to sync the current time
|
|
a.lyricsViewer.OnSeeked(a.lastPlayPos)
|
|
return
|
|
}
|
|
}
|
|
if a.nowPlaying == nil || a.nowPlaying.Metadata().Type == mediaprovider.MediaItemTypeRadioStation {
|
|
a.lyricsViewer.SetLyrics(nil)
|
|
a.curLyrics = nil
|
|
a.curLyricsID = ""
|
|
return
|
|
}
|
|
a.curLyricsID = a.nowPlayingID
|
|
a.lyricsLoading.Start()
|
|
// set the widget to an empty (not nil) lyric during fetch
|
|
// to keep it from showing "Lyrics not available"
|
|
a.lyricsViewer.DisableTapToSeek()
|
|
a.lyricsViewer.SetLyrics(&mediaprovider.Lyrics{
|
|
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
|
|
}
|
|
fyne.Do(func() {
|
|
a.lyricsLoading.Stop()
|
|
a.lyricsViewer.EnableTapToSeek()
|
|
a.lyricsViewer.SetLyrics(lyrics)
|
|
a.curLyrics = lyrics
|
|
if lyrics != nil {
|
|
a.lyricsViewer.OnSeeked(a.lastPlayPos)
|
|
}
|
|
})
|
|
})
|
|
}
|
|
|
|
func (a *NowPlayingPage) updateRelatedList() {
|
|
if a.relatedFetchCancel != nil {
|
|
a.relatedFetchCancel()
|
|
}
|
|
if a.curRelatedID == a.nowPlayingID {
|
|
return
|
|
}
|
|
a.curRelatedID = a.nowPlayingID
|
|
if a.nowPlayingID == "" {
|
|
a.relatedList.SetTracks(nil)
|
|
}
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
a.relatedFetchCancel = cancel
|
|
a.relatedLoading.Start()
|
|
a.relatedList.SetTracks(nil)
|
|
go func(ctx context.Context) {
|
|
related, err := a.mp.GetSongRadio(a.nowPlayingID, 30)
|
|
if err != nil {
|
|
log.Printf("failed to get similar tracks: %s", err.Error())
|
|
}
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
default:
|
|
fyne.Do(func() {
|
|
a.related = related
|
|
a.relatedList.SetTracks(a.related)
|
|
a.relatedLoading.Stop()
|
|
})
|
|
}
|
|
}(ctx)
|
|
}
|
|
|
|
func (a *NowPlayingPage) OnPlayQueueChange() {
|
|
a.Reload()
|
|
}
|
|
|
|
func (a *NowPlayingPage) Reload() {
|
|
a.card.DisableRating = !a.canRate
|
|
a.queueList.DisableRating = !a.canRate
|
|
a.queueList.DisableSharing = !a.canShare
|
|
a.relatedList.DisableRating = !a.canRate
|
|
a.relatedList.DisableSharing = !a.canShare
|
|
|
|
a.queue = a.pm.GetActivePlayQueue()
|
|
a.queueList.SetItems(a.queue)
|
|
a.totalTime = 0.0
|
|
for _, tr := range a.queue {
|
|
a.totalTime += tr.Metadata().Duration.Seconds()
|
|
}
|
|
a.formatStatusLine()
|
|
|
|
if a.tabs == nil {
|
|
return
|
|
}
|
|
switch a.tabs.SelectedIndex() {
|
|
case 1: /*lyrics*/
|
|
a.lastPlayPos = a.pm.PlaybackStatus().TimePos
|
|
a.updateLyrics()
|
|
case 2: /*related*/
|
|
a.updateRelatedList()
|
|
}
|
|
}
|
|
|
|
func (s *nowPlayingPageState) Restore() Page {
|
|
return NewNowPlayingPage(s.conf, s.contr, s.pool, s.sm, s.lm, s.im, s.pm, s.mp, s.canRate, s.canShare, s.cfg)
|
|
}
|
|
|
|
var _ CanShowPlayTime = (*NowPlayingPage)(nil)
|
|
|
|
func (a *NowPlayingPage) OnPlayTimeUpdate(curTime, _ float64, seeked bool) {
|
|
a.lastPlayPos = curTime
|
|
a.formatStatusLine()
|
|
if a.tabs == nil || a.tabs.SelectedIndex() != 1 /*lyrics*/ {
|
|
return
|
|
}
|
|
if seeked {
|
|
a.lyricsViewer.OnSeeked(curTime)
|
|
} else {
|
|
a.lyricsViewer.UpdatePlayPos(curTime)
|
|
}
|
|
}
|
|
|
|
func (a *NowPlayingPage) currentTracklistOrNil() *widgets.PlayQueueList {
|
|
if a.tabs != nil {
|
|
switch a.tabs.SelectedIndex() {
|
|
case 0: /*queue*/
|
|
return a.queueList
|
|
case 2: /*related*/
|
|
return a.relatedList
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
var _ CanSelectAll = (*NowPlayingPage)(nil)
|
|
|
|
func (a *NowPlayingPage) SelectAll() {
|
|
if l := a.currentTracklistOrNil(); l != nil {
|
|
l.SelectAll()
|
|
}
|
|
}
|
|
|
|
func (a *NowPlayingPage) UnselectAll() {
|
|
if l := a.currentTracklistOrNil(); l != nil {
|
|
l.UnselectAll()
|
|
}
|
|
}
|
|
|
|
func (a *NowPlayingPage) Refresh() {
|
|
if a.backgroundGradient != nil {
|
|
c := theme.Color(myTheme.ColorNamePageBackground)
|
|
if c != a.backgroundGradient.EndColor {
|
|
a.backgroundGradient.EndColor = c
|
|
a.backgroundGradient.Refresh()
|
|
}
|
|
}
|
|
a.BaseWidget.Refresh()
|
|
|
|
a.card.ShowAlbumYear = a.cfg.AlbumsPage.ShowYears
|
|
a.card.Update(a.nowPlaying)
|
|
}
|
|
|
|
func (a *NowPlayingPage) saveSelectedTab(tabNum int) {
|
|
var tabName string
|
|
switch tabNum {
|
|
case 0:
|
|
tabName = "Play Queue"
|
|
case 1:
|
|
tabName = "Lyrics"
|
|
case 2:
|
|
tabName = "Related"
|
|
}
|
|
a.conf.InitialView = tabName
|
|
}
|
|
|
|
func (a *NowPlayingPage) formatStatusLine() {
|
|
curPlayer := a.pm.CurrentPlayer()
|
|
playerStats := a.pm.PlaybackStatus()
|
|
lastStatus := a.statusLabel.Text
|
|
stopped := lang.L("Stopped")
|
|
state := stopped
|
|
switch playerStats.State {
|
|
case player.Paused:
|
|
state = lang.L("Paused")
|
|
case player.Playing:
|
|
state = lang.L("Playing")
|
|
}
|
|
|
|
dur := 0.0
|
|
if np := a.pm.NowPlaying(); np != nil {
|
|
dur = np.Metadata().Duration.Seconds()
|
|
}
|
|
statusSuffix := ""
|
|
trackNum := 0
|
|
if state != stopped {
|
|
trackNum = a.pm.NowPlayingIndex() + 1
|
|
statusSuffix = fmt.Sprintf(" %s/%s",
|
|
util.SecondsToMMSS(playerStats.TimePos),
|
|
util.SecondsToMMSS(dur))
|
|
}
|
|
status := fmt.Sprintf("%s (%d/%d)%s · %s: %s", state, trackNum,
|
|
len(a.queue), statusSuffix, lang.L("Total time"), util.SecondsToTimeString(a.totalTime))
|
|
|
|
mediaInfo := ""
|
|
if state != stopped {
|
|
mediaInfo = a.formatMediaInfoStr(curPlayer)
|
|
}
|
|
if mediaInfo != "" {
|
|
mediaInfo = " | " + mediaInfo
|
|
}
|
|
|
|
a.statusLabel.Text = fmt.Sprintf("%s%s", status, mediaInfo)
|
|
if lastStatus != a.statusLabel.Text {
|
|
a.statusLabel.Refresh()
|
|
}
|
|
}
|
|
|
|
func (a *NowPlayingPage) formatMediaInfoStr(player player.BasePlayer) string {
|
|
mpv, ok := player.(*mpv.Player)
|
|
if !ok {
|
|
return ""
|
|
}
|
|
audioInfo, err := mpv.GetMediaInfo()
|
|
if err != nil {
|
|
log.Printf("error getting playback status: %s", err.Error())
|
|
return ""
|
|
}
|
|
codec := audioInfo.Codec
|
|
if len(codec) <= 4 && !strings.EqualFold(codec, "opus") {
|
|
codec = strings.ToUpper(codec) // FLAC, MP3, AAC, etc
|
|
}
|
|
|
|
// Note: bit depth intentionally omitted since MPV reports the decoded bit depth
|
|
// i.e. 24 bit files get reported as 32 bit. Also b/c bit depth isn't meaningful for lossy.
|
|
return fmt.Sprintf("%s %g kHz, %d kbps", codec, float64(audioInfo.Samplerate)/1000, audioInfo.Bitrate/1000)
|
|
}
|