Merge pull request #338 from dweymouth/feature/fullscreenpage

Add new Now Playing page with large cover image and new queue list
This commit is contained in:
Drew Weymouth
2024-03-03 14:12:32 -08:00
committed by GitHub
22 changed files with 1153 additions and 324 deletions
+14 -22
View File
@@ -69,10 +69,6 @@ type FavoritesPageConfig struct {
TracklistColumns []string TracklistColumns []string
} }
type NowPlayingPageConfig struct {
TracklistColumns []string
}
type PlaylistPageConfig struct { type PlaylistPageConfig struct {
TracklistColumns []string TracklistColumns []string
} }
@@ -117,21 +113,20 @@ type TranscodingConfig struct {
} }
type Config struct { type Config struct {
Application AppConfig Application AppConfig
Servers []*ServerConfig Servers []*ServerConfig
AlbumPage AlbumPageConfig AlbumPage AlbumPageConfig
AlbumsPage AlbumsPageConfig AlbumsPage AlbumsPageConfig
ArtistPage ArtistPageConfig ArtistPage ArtistPageConfig
FavoritesPage FavoritesPageConfig FavoritesPage FavoritesPageConfig
NowPlayingPage NowPlayingPageConfig PlaylistPage PlaylistPageConfig
PlaylistPage PlaylistPageConfig PlaylistsPage PlaylistsPageConfig
PlaylistsPage PlaylistsPageConfig TracksPage TracksPageConfig
TracksPage TracksPageConfig LocalPlayback LocalPlaybackConfig
LocalPlayback LocalPlaybackConfig Scrobbling ScrobbleConfig
Scrobbling ScrobbleConfig ReplayGain ReplayGainConfig
ReplayGain ReplayGainConfig Transcoding TranscodingConfig
Transcoding TranscodingConfig Theme ThemeConfig
Theme ThemeConfig
} }
var SupportedStartupPages = []string{"Albums", "Favorites", "Playlists"} var SupportedStartupPages = []string{"Albums", "Favorites", "Playlists"}
@@ -167,9 +162,6 @@ func DefaultConfig(appVersionTag string) *Config {
TracklistColumns: []string{"Artist", "Album", "Time", "Plays"}, TracklistColumns: []string{"Artist", "Album", "Time", "Plays"},
InitialView: "Albums", InitialView: "Albums",
}, },
NowPlayingPage: NowPlayingPageConfig{
TracklistColumns: []string{"Artist", "Album", "Time", "Plays"},
},
PlaylistPage: PlaylistPageConfig{ PlaylistPage: PlaylistPageConfig{
TracklistColumns: []string{"Artist", "Album", "Time", "Plays"}, TracklistColumns: []string{"Artist", "Album", "Time", "Plays"},
}, },
+46 -13
View File
@@ -136,20 +136,26 @@ func (i *ImageManager) GetFullSizeCoverArt(coverID string) (image.Image, error)
i.cachedFullSizeCoverAccessedAt = time.Now().UnixMilli() i.cachedFullSizeCoverAccessedAt = time.Now().UnixMilli()
return i.cachedFullSizeCover, nil return i.cachedFullSizeCover, nil
} }
if i.s.Server == nil { return i.getFullSizeCoverArtFromServer(context.Background(), coverID, nil)
return nil, errors.New("logged out") }
}
i.serverFetchSema <- struct{}{} // acquire // GetCoverThumbnailAsync asynchronously fetches the cover image for the given ID,
im, err := i.s.Server.GetCoverArt(coverID, 0) // and invokes the callback on completion. It returns a context.CancelFunc which can be used to
<-i.serverFetchSema // release // cancel the fetch. The callback will not be invoked if the fetch is cancelled before completion.
if err != nil { // The cancel func must be invoked to avoid resource leaks. Use GetCoverThumbnail if cancellation is not needed.
return nil, err func (i *ImageManager) GetFullSizeCoverArtAsync(coverID string, cb func(image.Image, error)) context.CancelFunc {
} ctx, cancel := context.WithCancel(context.Background())
i.cachedFullSizeCover = im go func() {
i.cachedFullSizeCoverID = coverID if i.cachedFullSizeCoverID == coverID {
i.cachedFullSizeCoverAccessedAt = time.Now().UnixMilli() i.cachedFullSizeCoverAccessedAt = time.Now().UnixMilli()
return im, nil if ctx.Err() == nil {
cb(i.cachedFullSizeCover, nil)
}
} else {
i.getFullSizeCoverArtFromServer(ctx, coverID, cb)
}
}()
return cancel
} }
// GetCoverArtURL returns the URL for the locally cached cover thumbnail, if it exists. // GetCoverArtURL returns the URL for the locally cached cover thumbnail, if it exists.
@@ -267,6 +273,33 @@ func (i *ImageManager) fetchAndCacheCoverFromServer(ctx context.Context, coverID
} }
} }
func (i *ImageManager) getFullSizeCoverArtFromServer(ctx context.Context, coverID string, cb func(image.Image, error)) (image.Image, error) {
if i.s.Server == nil {
err := errors.New("logged out")
if ctx.Err() == nil && cb != nil {
cb(nil, err)
}
return nil, err
}
select {
case <-ctx.Done():
return nil, context.Canceled
case i.serverFetchSema <- struct{}{}: // acquire
im, err := i.s.Server.GetCoverArt(coverID, 0)
<-i.serverFetchSema // release
if err == nil {
i.cachedFullSizeCover = im
i.cachedFullSizeCoverID = coverID
i.cachedFullSizeCoverAccessedAt = time.Now().UnixMilli()
}
if ctx.Err() == nil && cb != nil {
cb(im, err)
}
return im, err
}
}
func (i *ImageManager) checkRefreshLocalCover(stat os.FileInfo, coverID string, ttl time.Duration) { func (i *ImageManager) checkRefreshLocalCover(stat os.FileInfo, coverID string, ttl time.Duration) {
if time.Since(stat.ModTime()) > cachedImageValidTime { if time.Since(stat.ModTime()) > cachedImageValidTime {
i.fetchAndCacheCoverFromServer(context.Background(), coverID, ttl, nil) i.fetchAndCacheCoverFromServer(context.Background(), coverID, ttl, nil)
+10
View File
@@ -61,6 +61,7 @@ type playbackEngine struct {
onStopped []func() onStopped []func()
onPlaying []func() onPlaying []func()
onPlayerChange []func() onPlayerChange []func()
onQueueChange []func()
} }
func NewPlaybackEngine( func NewPlaybackEngine(
@@ -216,15 +217,21 @@ func (p *playbackEngine) LoadTracks(tracks []*mediaprovider.Track, appendToQueue
if needToSetNext { if needToSetNext {
p.setNextTrack(p.nowPlayingIdx + 1) p.setNextTrack(p.nowPlayingIdx + 1)
} }
p.invokeNoArgCallbacks(p.onQueueChange)
return nil return nil
} }
// Stop playback and clear the play queue. // Stop playback and clear the play queue.
func (p *playbackEngine) StopAndClearPlayQueue() { func (p *playbackEngine) StopAndClearPlayQueue() {
changed := len(p.playQueue) > 0
p.player.Stop() p.player.Stop()
p.doUpdateTimePos() p.doUpdateTimePos()
p.playQueue = nil p.playQueue = nil
p.nowPlayingIdx = -1 p.nowPlayingIdx = -1
if changed {
p.invokeNoArgCallbacks(p.onQueueChange)
}
} }
func (p *playbackEngine) GetPlayQueue() []*mediaprovider.Track { func (p *playbackEngine) GetPlayQueue() []*mediaprovider.Track {
@@ -273,6 +280,7 @@ func (p *playbackEngine) UpdatePlayQueue(tracks []*mediaprovider.Track) error {
p.setNextTrackAfterQueueUpdate() p.setNextTrackAfterQueueUpdate()
} }
p.invokeNoArgCallbacks(p.onQueueChange)
return nil return nil
} }
@@ -321,6 +329,8 @@ func (p *playbackEngine) RemoveTracksFromQueue(trackIDs []string) {
p.setNextTrack(-1) p.setNextTrack(-1)
} }
} }
p.invokeNoArgCallbacks(p.onQueueChange)
} }
func (p *playbackEngine) SetReplayGainOptions(config ReplayGainConfig) { func (p *playbackEngine) SetReplayGainOptions(config ReplayGainConfig) {
+5
View File
@@ -74,6 +74,11 @@ func (p *PlaybackManager) OnVolumeChange(cb func(int)) {
p.engine.onVolumeChange = append(p.engine.onVolumeChange, cb) p.engine.onVolumeChange = append(p.engine.onVolumeChange, cb)
} }
// Registers a callback that is notified whenever the play queue changes.
func (p *PlaybackManager) OnQueueChange(cb func()) {
p.engine.onQueueChange = append(p.engine.onQueueChange, cb)
}
// Registers a callback that is notified whenever the player has been seeked. // Registers a callback that is notified whenever the player has been seeked.
func (p *PlaybackManager) OnSeek(cb func()) { func (p *PlaybackManager) OnSeek(cb func()) {
p.engine.onSeek = append(p.engine.onSeek, cb) p.engine.onSeek = append(p.engine.onSeek, cb)
+7
View File
@@ -102,6 +102,13 @@ func TrackIDOrEmptyStr(track *mediaprovider.Track) string {
return track.ID return track.ID
} }
func AlbumIDOrEmptyStr(track *mediaprovider.Track) string {
if track == nil {
return ""
}
return track.AlbumID
}
func TracksToIDs(tracks []*mediaprovider.Track) []string { func TracksToIDs(tracks []*mediaprovider.Track) []string {
return MapSlice(tracks, func(tr *mediaprovider.Track) string { return MapSlice(tracks, func(tr *mediaprovider.Track) string {
return tr.ID return tr.ID
+2 -8
View File
@@ -2,7 +2,6 @@ package ui
import ( import (
"image" "image"
"log"
"time" "time"
"github.com/dweymouth/supersonic/backend" "github.com/dweymouth/supersonic/backend"
@@ -53,13 +52,8 @@ func NewBottomPanel(pm *backend.PlaybackManager, contr *controller.Controller) *
}) })
bp.NowPlaying = widgets.NewNowPlayingCard() bp.NowPlaying = widgets.NewNowPlayingCard()
bp.NowPlaying.OnShowCoverImage = func() { bp.NowPlaying.OnCoverTapped = func() {
im, err := bp.ImageManager.GetFullSizeCoverArt(bp.coverArtID) contr.NavigateTo(controller.NowPlayingRoute(""))
if err != nil {
log.Printf("error getting full size cover image: %s", err.Error())
} else {
contr.ShowPopUpImage(im)
}
} }
bp.NowPlaying.OnSetFavorite = func(fav bool) { bp.NowPlaying.OnSetFavorite = func(fav bool) {
contr.SetTrackFavorites([]string{pm.NowPlaying().ID}, fav) contr.SetTrackFavorites([]string{pm.NowPlaying().ID}, fav)
+14
View File
@@ -49,6 +49,10 @@ type CanShowPlayTime interface {
OnPlayTimeUpdate(curTime, totalTime float64) OnPlayTimeUpdate(curTime, totalTime float64)
} }
type CanShowPlayQueue interface {
OnPlayQueueChange()
}
type BrowsingPane struct { type BrowsingPane struct {
widget.BaseWidget widget.BaseWidget
@@ -80,6 +84,7 @@ func NewBrowsingPane(app *backend.App, contr *controller.Controller) *BrowsingPa
b.reload = widget.NewButtonWithIcon("", theme.ViewRefreshIcon(), b.Reload) b.reload = widget.NewButtonWithIcon("", theme.ViewRefreshIcon(), b.Reload)
b.app.PlaybackManager.OnSongChange(b.onSongChange) b.app.PlaybackManager.OnSongChange(b.onSongChange)
b.app.PlaybackManager.OnPlayTimeUpdate(b.onPlayTimeUpdate) b.app.PlaybackManager.OnPlayTimeUpdate(b.onPlayTimeUpdate)
b.app.PlaybackManager.OnQueueChange(b.onQueueChange)
bkgrnd := myTheme.NewThemedRectangle(myTheme.ColorNamePageBackground) bkgrnd := myTheme.NewThemedRectangle(myTheme.ColorNamePageBackground)
b.pageContainer = container.NewStack(bkgrnd, layout.NewSpacer()) b.pageContainer = container.NewStack(bkgrnd, layout.NewSpacer())
b.settingsBtn = widget.NewButtonWithIcon("", theme.SettingsIcon(), func() { b.settingsBtn = widget.NewButtonWithIcon("", theme.SettingsIcon(), func() {
@@ -222,6 +227,15 @@ func (b *BrowsingPane) onPlayTimeUpdate(cur, total float64) {
} }
} }
func (b *BrowsingPane) onQueueChange() {
if b.curPage == nil {
return
}
if p, ok := b.curPage.(CanShowPlayQueue); ok {
p.OnPlayQueueChange()
}
}
func (b *BrowsingPane) addPageToHistory(p Page, truncate bool) { func (b *BrowsingPane) addPageToHistory(p Page, truncate bool) {
if truncate { if truncate {
// allow garbage collection of pages that will be removed from the history // allow garbage collection of pages that will be removed from the history
+149 -113
View File
@@ -1,7 +1,9 @@
package browsing package browsing
import ( import (
"context"
"fmt" "fmt"
"image"
"log" "log"
"strings" "strings"
@@ -18,6 +20,7 @@ import (
"fyne.io/fyne/v2" "fyne.io/fyne/v2"
"fyne.io/fyne/v2/container" "fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/layout"
"fyne.io/fyne/v2/theme" "fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget" "fyne.io/fyne/v2/widget"
) )
@@ -27,34 +30,35 @@ type NowPlayingPage struct {
nowPlayingPageState nowPlayingPageState
queue []*mediaprovider.Track queue []*mediaprovider.Track
totalTime float64 queueList *widgets.PlayQueueList
lyricsViewer *widgets.LyricsViewer
title *widget.RichText imageLoadCancel context.CancelFunc
tracklist *widgets.Tracklist card *widgets.LargeNowPlayingCard
statusLabel *widget.Label statusLabel *widget.Label
nowPlayingID string totalTime float64
container *fyne.Container nowPlayingID string
albumID string
container *fyne.Container
} }
type nowPlayingPageState struct { type nowPlayingPageState struct {
contr *controller.Controller contr *controller.Controller
pool *util.WidgetPool pool *util.WidgetPool
conf *backend.NowPlayingPageConfig
pm *backend.PlaybackManager pm *backend.PlaybackManager
im *backend.ImageManager
canRate bool canRate bool
} }
func NewNowPlayingPage( func NewNowPlayingPage(
highlightedTrackID string,
contr *controller.Controller, contr *controller.Controller,
pool *util.WidgetPool, pool *util.WidgetPool,
conf *backend.NowPlayingPageConfig, im *backend.ImageManager,
pm *backend.PlaybackManager, pm *backend.PlaybackManager,
canRate bool, canRate bool,
) *NowPlayingPage { ) *NowPlayingPage {
a := &NowPlayingPage{nowPlayingPageState: nowPlayingPageState{ a := &NowPlayingPage{nowPlayingPageState: nowPlayingPageState{
contr: contr, pool: pool, conf: conf, pm: pm, canRate: canRate, contr: contr, pool: pool, im: im, pm: pm, canRate: canRate,
}} }}
a.ExtendBaseWidget(a) a.ExtendBaseWidget(a)
@@ -62,51 +66,89 @@ func NewNowPlayingPage(
pm.OnPlaying(a.formatStatusLine) pm.OnPlaying(a.formatStatusLine)
pm.OnStopped(a.formatStatusLine) pm.OnStopped(a.formatStatusLine)
if t := a.pool.Obtain(util.WidgetTypeTracklist); t != nil { a.card = widgets.NewLargeNowPlayingCard()
a.tracklist = t.(*widgets.Tracklist) a.card.DisableRating = !canRate
a.tracklist.Reset() a.card.OnAlbumNameTapped = func() {
} else { contr.NavigateTo(controller.AlbumRoute(a.albumID))
a.tracklist = widgets.NewTracklist(nil)
} }
a.tracklist.SetVisibleColumns(conf.TracklistColumns) a.card.OnArtistNameTapped = func(artistID string) {
a.tracklist.OnVisibleColumnsChanged = func(cols []string) { contr.NavigateTo(controller.ArtistRoute(artistID))
a.conf.TracklistColumns = cols
} }
remove := fyne.NewMenuItem("Remove from queue", a.onRemoveSelectedFromQueue) a.card.OnSetFavorite = func(fav bool) {
remove.Icon = theme.ContentRemoveIcon() a.contr.SetTrackFavorites([]string{a.nowPlayingID}, fav)
a.tracklist.Options = widgets.TracklistOptions{
AutoNumber: true,
DisablePlaybackMenu: true,
DisableRating: !canRate,
AuxiliaryMenuItems: []*fyne.MenuItem{
util.NewReorderTracksSubmenu(a.doSetNewTrackOrder),
remove,
},
} }
contr.ConnectTracklistActions(a.tracklist) a.card.OnSetRating = func(rating int) {
// override the default OnPlayTrackAt handler b/c we don't need to re-load the tracks into the queue a.contr.SetTrackRatings([]string{a.nowPlayingID}, rating)
a.tracklist.OnPlayTrackAt = a.onPlayTrackAt }
a.title = widget.NewRichTextWithText("Now Playing")
a.title.Segments[0].(*widget.TextSegment).Style.SizeName = widget.RichTextStyleHeading.SizeName a.queueList = widgets.NewPlayQueueList(a.im)
a.queueList.OnReorderTracks = a.doSetNewTrackOrder
a.queueList.OnDownload = contr.ShowDownloadDialog
a.queueList.OnAddToPlaylist = contr.DoAddTracksToPlaylistWorkflow
a.queueList.OnPlayTrackAt = func(tracknum int) {
_ = a.pm.PlayTrackAt(tracknum)
}
a.queueList.OnShowArtistPage = func(artistID string) {
a.contr.NavigateTo(controller.ArtistRoute(artistID))
}
a.queueList.OnRemoveFromQueue = func(trackIDs []string) {
a.queueList.UnselectAll()
a.pm.RemoveTracksFromQueue(trackIDs)
}
a.queueList.OnSetRating = func(trackIDs []string, rating int) {
contr.SetTrackRatings(trackIDs, rating)
if sharedutil.SliceContains(trackIDs, a.nowPlayingID) {
a.card.SetDisplayedRating(rating)
}
}
a.queueList.OnSetFavorite = func(trackIDs []string, fav bool) {
contr.SetTrackFavorites(trackIDs, fav)
if sharedutil.SliceContains(trackIDs, a.nowPlayingID) {
a.card.SetDisplayedFavorite(fav)
}
}
a.lyricsViewer = widgets.NewLyricsViewer()
a.statusLabel = widget.NewLabel("Stopped") a.statusLabel = widget.NewLabel("Stopped")
statusLabelCtr := container.New(&layouts.VboxCustomPadding{ExtraPad: -5},
myTheme.NewThemedRectangle(theme.ColorNameInputBorder), a.Reload()
a.statusLabel,
)
a.container = container.New(&layouts.MaxPadLayout{PadLeft: 15, PadRight: 15, PadTop: 5, PadBottom: 15},
container.NewBorder(a.title, statusLabelCtr, nil, nil, a.tracklist))
a.load(highlightedTrackID)
return a return a
} }
func (a *NowPlayingPage) CreateRenderer() fyne.WidgetRenderer { func (a *NowPlayingPage) CreateRenderer() fyne.WidgetRenderer {
if a.container == nil {
paddedLayout := &layouts.PercentPadLayout{
LeftRightObjectPercent: .8,
TopBottomObjectPercent: .8,
}
mainContent := container.NewGridWithColumns(2,
container.New(paddedLayout, a.card),
container.New(paddedLayout,
util.AddHeaderBackground(
container.NewAppTabs(
container.NewTabItem("Play Queue",
container.NewBorder(layout.NewSpacer(), nil, nil, nil, a.queueList)),
container.NewTabItem("Lyrics", a.lyricsViewer),
))))
a.container = container.NewStack(
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) return widget.NewSimpleRenderer(a.container)
} }
func (a *NowPlayingPage) Save() SavedPage { func (a *NowPlayingPage) Save() SavedPage {
a.tracklist.Clear() if a.imageLoadCancel != nil {
a.pool.Release(util.WidgetTypeTracklist, a.tracklist) a.imageLoadCancel()
}
nps := a.nowPlayingPageState nps := a.nowPlayingPageState
a.pool.Release(util.WidgetTypeNowPlayingPage, a)
return &nps return &nps
} }
@@ -114,20 +156,50 @@ func (a *NowPlayingPage) Route() controller.Route {
return controller.NowPlayingRoute("") return controller.NowPlayingRoute("")
} }
func (a *NowPlayingPage) Tapped(*fyne.PointEvent) {
a.tracklist.UnselectAll()
}
func (a *NowPlayingPage) SelectAll() {
a.tracklist.SelectAll()
}
var _ CanShowNowPlaying = (*NowPlayingPage)(nil) var _ CanShowNowPlaying = (*NowPlayingPage)(nil)
func (a *NowPlayingPage) OnSongChange(song, lastScrobbledIfAny *mediaprovider.Track) { func (a *NowPlayingPage) OnSongChange(song, lastScrobbledIfAny *mediaprovider.Track) {
if a.imageLoadCancel != nil {
a.imageLoadCancel()
}
a.nowPlayingID = sharedutil.TrackIDOrEmptyStr(song) a.nowPlayingID = sharedutil.TrackIDOrEmptyStr(song)
a.tracklist.SetNowPlaying(a.nowPlayingID) a.queueList.SetNowPlaying(a.nowPlayingID)
a.tracklist.IncrementPlayCount(sharedutil.TrackIDOrEmptyStr(lastScrobbledIfAny))
a.albumID = sharedutil.AlbumIDOrEmptyStr(song)
a.card.Update(song)
if song == nil {
a.card.SetCoverImage(nil)
return
}
a.imageLoadCancel = a.im.GetFullSizeCoverArtAsync(song.CoverArtID, func(img image.Image, err error) {
if err != nil {
log.Printf("error loading cover art: %v\n", err)
} else {
a.card.SetCoverImage(img)
}
})
}
func (a *NowPlayingPage) OnPlayQueueChange() {
a.Reload()
}
func (a *NowPlayingPage) Reload() {
a.queue = a.pm.GetPlayQueue()
a.queueList.SetTracks(a.queue)
a.totalTime = 0.0
for _, tr := range a.queue {
a.totalTime += float64(tr.Duration)
}
a.formatStatusLine()
}
func (s *nowPlayingPageState) Restore() Page {
if page := s.pool.Obtain(util.WidgetTypeNowPlayingPage).(*NowPlayingPage); page != nil {
page.Reload()
return page
}
return NewNowPlayingPage(s.contr, s.pool, s.im, s.pm, s.canRate)
} }
var _ CanShowPlayTime = (*NowPlayingPage)(nil) var _ CanShowPlayTime = (*NowPlayingPage)(nil)
@@ -136,6 +208,29 @@ func (a *NowPlayingPage) OnPlayTimeUpdate(_, _ float64) {
a.formatStatusLine() a.formatStatusLine()
} }
var _ CanSelectAll = (*NowPlayingPage)(nil)
func (a *NowPlayingPage) SelectAll() {
a.queueList.SelectAll()
}
var _ fyne.Tappable = (*NowPlayingPage)(nil)
func (a *NowPlayingPage) Tapped(*fyne.PointEvent) {
a.queueList.UnselectAll()
}
func (a *NowPlayingPage) doSetNewTrackOrder(trackIDs []string, op sharedutil.TrackReorderOp) {
idxs := make([]int, 0, len(trackIDs))
for i, tr := range a.queue {
if sharedutil.SliceContains(trackIDs, tr.ID) {
idxs = append(idxs, i)
}
}
newTracks := sharedutil.ReorderTracks(a.queue, idxs, op)
a.pm.UpdatePlayQueue(newTracks)
}
func (a *NowPlayingPage) formatStatusLine() { func (a *NowPlayingPage) formatStatusLine() {
curPlayer := a.pm.CurrentPlayer() curPlayer := a.pm.CurrentPlayer()
playerStats := curPlayer.GetStatus() playerStats := curPlayer.GetStatus()
@@ -196,62 +291,3 @@ func (a *NowPlayingPage) formatMediaInfoStr(player player.BasePlayer) string {
// i.e. 24 bit files get reported as 32 bit. Also b/c bit depth isn't meaningful for lossy. // 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) return fmt.Sprintf("%s %g kHz, %d kbps", codec, float64(audioInfo.Samplerate)/1000, audioInfo.Bitrate/1000)
} }
func (a *NowPlayingPage) Reload() {
a.load("")
}
var _ Scrollable = (*NowPlayingPage)(nil)
func (a *NowPlayingPage) Scroll(scrollAmt float32) {
a.tracklist.Scroll(scrollAmt)
}
func (a *NowPlayingPage) onPlayTrackAt(tracknum int) {
_ = a.pm.PlayTrackAt(tracknum)
}
func (a *NowPlayingPage) onRemoveSelectedFromQueue() {
a.pm.RemoveTracksFromQueue(a.tracklist.SelectedTrackIDs())
a.tracklist.UnselectAll()
a.Reload()
}
func (a *NowPlayingPage) doSetNewTrackOrder(op sharedutil.TrackReorderOp) {
// Since the tracklist view may be sorted in a different order than the
// actual running order, we need to get the IDs of the selected tracks
// from the tracklist and convert them to indices in the *original* run order
ids := a.tracklist.SelectedTrackIDs()
idxs := make([]int, 0, len(ids))
for i, tr := range a.queue {
if sharedutil.SliceContains(ids, tr.ID) {
idxs = append(idxs, i)
}
}
newTracks := sharedutil.ReorderTracks(a.queue, idxs, op)
a.pm.UpdatePlayQueue(newTracks)
// force-switch back to unsorted view to show new track order
a.tracklist.SetSorting(widgets.TracklistSort{})
a.tracklist.SetTracks(newTracks)
a.tracklist.UnselectAll()
}
// does not make calls to server - can safely be run in UI callbacks
func (a *NowPlayingPage) load(highlightedTrackID string) {
a.queue = a.pm.GetPlayQueue()
a.tracklist.SetTracks(a.queue)
a.tracklist.SetNowPlaying(a.nowPlayingID)
if highlightedTrackID != "" {
a.tracklist.SelectAndScrollToTrack(highlightedTrackID)
}
a.totalTime = 0.0
for _, tr := range a.queue {
a.totalTime += float64(tr.Duration)
}
a.formatStatusLine()
}
func (s *nowPlayingPageState) Restore() Page {
return NewNowPlayingPage("", s.contr, s.pool, s.conf, s.pm, s.canRate)
}
+4 -2
View File
@@ -30,6 +30,7 @@ func NewRouter(app *backend.App, controller *controller.Controller, nav Navigati
} }
func (r Router) CreatePage(rte controller.Route) Page { func (r Router) CreatePage(rte controller.Route) Page {
_, canRate := r.App.ServerManager.Server.(mediaprovider.SupportsRating)
switch rte.Page { switch rte.Page {
case controller.Album: case controller.Album:
return NewAlbumPage(rte.Arg, &r.App.Config.AlbumPage, r.widgetPool, r.App.PlaybackManager, r.App.ServerManager.Server, r.App.ImageManager, r.Controller) return NewAlbumPage(rte.Arg, &r.App.Config.AlbumPage, r.widgetPool, r.App.PlaybackManager, r.App.ServerManager.Server, r.App.ImageManager, r.Controller)
@@ -41,13 +42,14 @@ func (r Router) CreatePage(rte controller.Route) Page {
return NewArtistsPage(r.Controller, r.widgetPool, r.App.PlaybackManager, r.App.ServerManager.Server, r.App.ImageManager) return NewArtistsPage(r.Controller, r.widgetPool, r.App.PlaybackManager, r.App.ServerManager.Server, r.App.ImageManager)
case controller.Favorites: case controller.Favorites:
return NewFavoritesPage(&r.App.Config.FavoritesPage, r.widgetPool, r.Controller, r.App.ServerManager.Server, r.App.PlaybackManager, r.App.ImageManager) return NewFavoritesPage(&r.App.Config.FavoritesPage, r.widgetPool, r.Controller, r.App.ServerManager.Server, r.App.PlaybackManager, r.App.ImageManager)
case controller.Fullscreen:
return NewNowPlayingPage(r.Controller, r.widgetPool, r.App.ImageManager, r.App.PlaybackManager, canRate)
case controller.Genre: case controller.Genre:
return NewGenrePage(rte.Arg, r.widgetPool, r.Controller, r.App.PlaybackManager, r.App.ServerManager.Server, r.App.ImageManager) return NewGenrePage(rte.Arg, r.widgetPool, r.Controller, r.App.PlaybackManager, r.App.ServerManager.Server, r.App.ImageManager)
case controller.Genres: case controller.Genres:
return NewGenresPage(r.Controller, r.App.ServerManager.Server) return NewGenresPage(r.Controller, r.App.ServerManager.Server)
case controller.NowPlaying: case controller.NowPlaying:
_, canRate := r.App.ServerManager.Server.(mediaprovider.SupportsRating) return NewNowPlayingPage(r.Controller, r.widgetPool, r.App.ImageManager, r.App.PlaybackManager, canRate)
return NewNowPlayingPage(rte.Arg, r.Controller, r.widgetPool, &r.App.Config.NowPlayingPage, r.App.PlaybackManager, canRate)
case controller.Playlist: case controller.Playlist:
return NewPlaylistPage(rte.Arg, &r.App.Config.PlaylistPage, r.widgetPool, r.Controller, r.App.ServerManager, r.App.PlaybackManager, r.App.ImageManager) return NewPlaylistPage(rte.Arg, &r.App.Config.PlaylistPage, r.widgetPool, r.Controller, r.App.ServerManager, r.App.PlaybackManager, r.App.ImageManager)
case controller.Playlists: case controller.Playlists:
+1
View File
@@ -8,6 +8,7 @@ const (
Albums Albums
Artist Artist
Artists Artists
Fullscreen
Genre Genre
Genres Genres
Favorites Favorites
+44
View File
@@ -0,0 +1,44 @@
package layouts
import "fyne.io/fyne/v2"
// Centers and pads a given item by a percent (0-1)
// of the width and height that should be taken by the objects
type PercentPadLayout struct {
LeftRightObjectPercent float32
TopBottomObjectPercent float32
}
func (l *PercentPadLayout) MinSize(objects []fyne.CanvasObject) fyne.Size {
var objMinSize fyne.Size
for _, obj := range objects {
objMinSize = objMinSize.Max(obj.MinSize())
}
if objMinSize.IsZero() {
return objMinSize
}
return fyne.Size{
Width: objMinSize.Width / l.LeftRightObjectPercent,
Height: objMinSize.Height / l.LeftRightObjectPercent,
}
}
func (l *PercentPadLayout) Layout(objects []fyne.CanvasObject, size fyne.Size) {
if len(objects) == 0 {
return
}
objSize := fyne.NewSize(
size.Width*l.LeftRightObjectPercent,
size.Height*l.TopBottomObjectPercent,
)
objPos := fyne.NewPos(
size.Width*(1-l.LeftRightObjectPercent)/2,
size.Height*(1-l.TopBottomObjectPercent)/2,
)
for _, obj := range objects {
obj.Resize(objSize)
obj.Move(objPos)
}
}
+101
View File
@@ -0,0 +1,101 @@
package util
import (
"github.com/dweymouth/supersonic/backend/mediaprovider"
"github.com/dweymouth/supersonic/sharedutil"
)
type TrackListModel struct {
Track *mediaprovider.Track
Selected bool
}
func ToTrackListModels(trs []*mediaprovider.Track) []*TrackListModel {
return sharedutil.MapSlice(trs, func(tr *mediaprovider.Track) *TrackListModel {
return &TrackListModel{Track: tr, Selected: false}
})
}
func SelectedTrackModels(tracks []*TrackListModel) []*TrackListModel {
return sharedutil.FilterSlice(tracks, func(tm *TrackListModel) bool {
return tm.Selected
})
}
func SelectedTracks(tracks []*TrackListModel) []*mediaprovider.Track {
return sharedutil.MapSlice(SelectedTrackModels(tracks), func(tm *TrackListModel) *mediaprovider.Track {
return tm.Track
})
}
func SelectedTrackIDs(tracks []*TrackListModel) []string {
return sharedutil.MapSlice(SelectedTrackModels(tracks), func(tm *TrackListModel) string {
return tm.Track.ID
})
}
func SelectTrack(tracks []*TrackListModel, idx int) {
if tracks[idx].Selected {
return
}
UnselectAllTracks(tracks)
tracks[idx].Selected = true
}
func SelectAllTracks(tracks []*TrackListModel) {
for _, tm := range tracks {
tm.Selected = true
}
}
func UnselectAllTracks(tracks []*TrackListModel) {
for _, tm := range tracks {
tm.Selected = false
}
}
func SelectTrackRange(tracks []*TrackListModel, idx int) {
if tracks[idx].Selected {
return
}
lastSelected := -1
for i := len(tracks) - 1; i >= 0; i-- {
if tracks[i].Selected {
lastSelected = i
break
}
}
if lastSelected < 0 {
tracks[idx].Selected = true
return
}
from := minInt(idx, lastSelected)
to := maxInt(idx, lastSelected)
for i := from; i <= to; i++ {
tracks[i].Selected = true
}
}
func FindTrackByID(tracks []*TrackListModel, id string) (*mediaprovider.Track, int) {
idx := sharedutil.Find(tracks, func(tr *TrackListModel) bool {
return tr.Track.ID == id
})
if idx >= 0 {
return tracks[idx].Track, idx
}
return nil, -1
}
func minInt(a, b int) int {
if a < b {
return a
}
return b
}
func maxInt(a, b int) int {
if a > b {
return a
}
return b
}
+18
View File
@@ -212,6 +212,24 @@ func AddHeaderBackground(obj fyne.CanvasObject) *fyne.Container {
obj)) obj))
} }
func NewTruncatingRichText() *widget.RichText {
rt := widget.NewRichTextWithText("")
rt.Truncation = fyne.TextTruncateEllipsis
return rt
}
func NewTruncatingLabel() *widget.Label {
rt := widget.NewLabel("")
rt.Truncation = fyne.TextTruncateEllipsis
return rt
}
func NewTrailingAlignLabel() *widget.Label {
rt := widget.NewLabel("")
rt.Alignment = fyne.TextAlignTrailing
return rt
}
type HSpace struct { type HSpace struct {
widget.BaseWidget widget.BaseWidget
+1
View File
@@ -15,6 +15,7 @@ const (
WidgetTypeGridView WidgetTypeGridView
WidgetTypePlaylistPageHeader WidgetTypePlaylistPageHeader
WidgetTypeTracklist WidgetTypeTracklist
WidgetTypeNowPlayingPage
// keep at bottom // keep at bottom
numWidgetTypes numWidgetTypes
+113
View File
@@ -0,0 +1,113 @@
package widgets
import (
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/widget"
)
type Aspectable interface {
fyne.CanvasObject
Aspect() float32
}
type CaptionedImage struct {
widget.BaseWidget
Content Aspectable
Caption fyne.CanvasObject
}
var _ fyne.Widget = (*CaptionedImage)(nil)
func NewCaptionedImage(content Aspectable, caption fyne.CanvasObject) *CaptionedImage {
c := &CaptionedImage{Content: content, Caption: caption}
c.ExtendBaseWidget(c)
return c
}
func (c *CaptionedImage) CreateRenderer() fyne.WidgetRenderer {
return &captionedImageRenderer{
ci: c,
}
}
type captionedImageRenderer struct {
ci *CaptionedImage
}
var _ fyne.WidgetRenderer = (*captionedImageRenderer)(nil)
func (*captionedImageRenderer) Destroy() {
// intentionally blank
}
func (c *captionedImageRenderer) Layout(s fyne.Size) {
content := c.ci.Content
aspect := content.Aspect()
caption := c.ci.Caption
var captionHeight float32
if caption != nil {
captionHeight = caption.MinSize().Height
}
// max height that could be allocated to Content
contentMaxedHeight := s.Height
if caption != nil {
contentMaxedHeight -= captionHeight
}
// aspect ratio of Content if it were maxed out
maxedAspect := s.Width / contentMaxedHeight
if maxedAspect > aspect {
// Will use full height, but not full width
width := contentMaxedHeight * aspect
content.Resize(fyne.NewSize(width, contentMaxedHeight))
xStart := (s.Width - width) / 2
content.Move(fyne.NewPos(xStart, 0))
if caption != nil {
caption.Resize(fyne.NewSize(width, captionHeight))
caption.Move(fyne.NewPos(xStart, contentMaxedHeight))
}
return
}
// Content will not use full height
// Positioning of Content and Caption will be adjusted to center
contentH := s.Width / aspect
yStart := (s.Height - contentH - captionHeight) / 2
content.Resize(fyne.NewSize(s.Width, contentH))
content.Move(fyne.NewPos(0, yStart))
if caption != nil {
caption.Resize(fyne.NewSize(s.Width, captionHeight))
caption.Move(fyne.NewPos(0, yStart+contentH))
}
}
func (c *captionedImageRenderer) MinSize() fyne.Size {
objSize := c.ci.Content.MinSize()
if c.ci.Caption == nil {
return objSize
}
cptSize := c.ci.Caption.MinSize()
return fyne.NewSize(
fyne.Max(objSize.Width, cptSize.Width),
objSize.Height+cptSize.Height,
)
}
func (c *captionedImageRenderer) Objects() []fyne.CanvasObject {
if c.ci.Caption != nil {
return []fyne.CanvasObject{c.ci.Content, c.ci.Caption}
}
return []fyne.CanvasObject{c.ci.Content}
}
func (c *captionedImageRenderer) Refresh() {
c.ci.Content.Refresh()
if cap := c.ci.Caption; cap != nil {
cap.Refresh()
}
// aspect may have changed on Content,
// so we need to re-layout
c.Layout(c.ci.Size())
}
+27
View File
@@ -0,0 +1,27 @@
package widgets
import (
"github.com/dweymouth/supersonic/ui/theme"
)
type FavoriteIcon struct {
TappableIcon
Favorite bool
}
func NewFavoriteIcon() *FavoriteIcon {
f := &FavoriteIcon{}
f.Resource = theme.NotFavoriteIcon
f.ExtendBaseWidget(f)
return f
}
func (f *FavoriteIcon) Refresh() {
if f.Favorite {
f.Resource = theme.FavoriteIcon
} else {
f.Resource = theme.NotFavoriteIcon
}
f.BaseWidget.Refresh()
}
+7
View File
@@ -51,6 +51,13 @@ func NewImagePlaceholder(centerIcon fyne.Resource, minSize float32) *ImagePlaceh
return i return i
} }
func (i *ImagePlaceholder) Aspect() float32 {
if i.image != nil {
return i.imageDisp.Aspect()
}
return 1.0
}
func (i *ImagePlaceholder) HaveImage() bool { func (i *ImagePlaceholder) HaveImage() bool {
return i.image != nil return i.image != nil
} }
+132
View File
@@ -0,0 +1,132 @@
package widgets
import (
"image"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/canvas"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/layout"
"fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget"
"github.com/dweymouth/supersonic/backend/mediaprovider"
"github.com/dweymouth/supersonic/ui/layouts"
myTheme "github.com/dweymouth/supersonic/ui/theme"
)
// Shows the current album art, track name, artist name, and album name
// for the currently playing track. Placed into the left side of the BottomPanel.
type LargeNowPlayingCard struct {
CaptionedImage
DisableRating bool
trackName *widget.RichText
artistName *MultiHyperlink
albumName *widget.Hyperlink
rating *StarRating
favorite *FavoriteIcon
cover *ImagePlaceholder
OnArtistNameTapped func(artistID string)
OnAlbumNameTapped func()
OnSetRating func(rating int)
OnSetFavorite func(favorite bool)
}
func NewLargeNowPlayingCard() *LargeNowPlayingCard {
n := &LargeNowPlayingCard{
trackName: widget.NewRichTextWithText(""),
artistName: NewMultiHyperlink(),
albumName: widget.NewHyperlink("", nil),
rating: NewStarRating(),
favorite: NewFavoriteIcon(),
cover: NewImagePlaceholder(myTheme.TracksIcon, 300),
}
n.ExtendBaseWidget(n)
n.rating.StarSize = theme.IconInlineSize() + theme.InnerPadding()/2
n.rating.OnRatingChanged = n.onSetRating
n.favorite.OnTapped = n.onToggleFavorite
n.cover.ScaleMode = canvas.ImageScaleFastest
// set up the layout
n.Content = n.cover
n.Caption = container.New(&layouts.VboxCustomPadding{ExtraPad: -13},
n.trackName,
n.artistName,
n.albumName,
container.NewHBox(
layout.NewSpacer(),
n.favorite,
widget.NewLabel("·"),
n.rating,
layout.NewSpacer(),
),
)
n.trackName.Segments[0].(*widget.TextSegment).Style.SizeName = theme.SizeNameSubHeadingText
n.trackName.Truncation = fyne.TextTruncateEllipsis
n.albumName.Truncation = fyne.TextTruncateEllipsis
n.trackName.Truncation = fyne.TextTruncateEllipsis
n.albumName.OnTapped = n.onAlbumNameTapped
n.artistName.OnTapped = n.onArtistNameTapped
return n
}
func (n *LargeNowPlayingCard) onAlbumNameTapped() {
if n.OnAlbumNameTapped != nil {
n.OnAlbumNameTapped()
}
}
func (n *LargeNowPlayingCard) onArtistNameTapped(artistID string) {
if n.OnArtistNameTapped != nil {
n.OnArtistNameTapped(artistID)
}
}
func (n *LargeNowPlayingCard) onToggleFavorite() {
n.favorite.Favorite = !n.favorite.Favorite
n.favorite.Refresh()
if n.OnSetFavorite != nil {
n.OnSetFavorite(n.favorite.Favorite)
}
}
func (n *LargeNowPlayingCard) onSetRating(rating int) {
if n.OnSetRating != nil {
n.OnSetRating(rating)
}
}
func (n *LargeNowPlayingCard) SetDisplayedRating(rating int) {
n.rating.Rating = rating
n.rating.Refresh()
}
func (n *LargeNowPlayingCard) SetDisplayedFavorite(favorite bool) {
n.favorite.Favorite = favorite
n.favorite.Refresh()
}
func (n *LargeNowPlayingCard) Update(track *mediaprovider.Track) {
if track != nil {
n.trackName.Segments[0].(*widget.TextSegment).Text = track.Name
n.artistName.BuildSegments(track.ArtistNames, track.ArtistIDs)
n.albumName.Text = track.Album
n.rating.Rating = track.Rating
n.favorite.Favorite = track.Favorite
} else {
n.trackName.Segments[0].(*widget.TextSegment).Text = ""
n.artistName.BuildSegments([]string{}, []string{})
n.albumName.Text = ""
n.rating.Rating = 0
n.favorite.Favorite = false
}
n.Refresh()
}
func (n *LargeNowPlayingCard) SetCoverImage(im image.Image) {
n.cover.SetImage(im, false)
}
+15
View File
@@ -0,0 +1,15 @@
package widgets
import "fyne.io/fyne/v2/widget"
type LyricsViewer struct {
widget.Label
}
func NewLyricsViewer() *LyricsViewer {
l := &LyricsViewer{Label: widget.Label{
Text: "Lyrics not available",
}}
l.ExtendBaseWidget(l)
return l
}
+4 -3
View File
@@ -30,7 +30,7 @@ type NowPlayingCard struct {
OnTrackNameTapped func() OnTrackNameTapped func()
OnArtistNameTapped func(artistID string) OnArtistNameTapped func(artistID string)
OnAlbumNameTapped func() OnAlbumNameTapped func()
OnShowCoverImage func() OnCoverTapped func()
OnSetRating func(rating int) OnSetRating func(rating int)
OnSetFavorite func(favorite bool) OnSetFavorite func(favorite bool)
OnAddToPlaylist func() OnAddToPlaylist func()
@@ -52,6 +52,7 @@ func NewNowPlayingCard() *NowPlayingCard {
n.trackName.TextStyle.Bold = true n.trackName.TextStyle.Bold = true
n.cover.SetMinSize(fyne.NewSize(85, 85)) n.cover.SetMinSize(fyne.NewSize(85, 85))
n.cover.FillMode = canvas.ImageFillContain n.cover.FillMode = canvas.ImageFillContain
n.cover.ScaleMode = canvas.ImageScaleFastest
n.cover.Hidden = true n.cover.Hidden = true
n.albumName.OnTapped = n.onAlbumNameTapped n.albumName.OnTapped = n.onAlbumNameTapped
n.artistName.OnTapped = n.onArtistNameTapped n.artistName.OnTapped = n.onArtistNameTapped
@@ -84,8 +85,8 @@ func (n *NowPlayingCard) onTrackNameTapped() {
} }
func (n *NowPlayingCard) onShowCoverImage(*fyne.PointEvent) { func (n *NowPlayingCard) onShowCoverImage(*fyne.PointEvent) {
if n.OnShowCoverImage != nil { if n.OnCoverTapped != nil {
n.OnShowCoverImage() n.OnCoverTapped()
} }
} }
+374
View File
@@ -0,0 +1,374 @@
package widgets
import (
"image"
"strconv"
"sync"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/canvas"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/driver/desktop"
"fyne.io/fyne/v2/layout"
"fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget"
"github.com/dweymouth/supersonic/backend"
"github.com/dweymouth/supersonic/backend/mediaprovider"
"github.com/dweymouth/supersonic/sharedutil"
"github.com/dweymouth/supersonic/ui/layouts"
"github.com/dweymouth/supersonic/ui/os"
myTheme "github.com/dweymouth/supersonic/ui/theme"
"github.com/dweymouth/supersonic/ui/util"
)
const thumbnailSize = 52
type PlayQueueList struct {
widget.BaseWidget
DisableRating bool
// user action callbacks
OnAddToPlaylist func(trackIDs []string)
OnSetFavorite func(trackIDs []string, fav bool)
OnSetRating func(trackIDs []string, rating int)
OnRemoveFromQueue func(trackIDs []string)
OnDownload func(tracks []*mediaprovider.Track, downloadName string)
OnShowArtistPage func(artistID string)
OnPlayTrackAt func(idx int)
OnReorderTracks func(trackIDs []string, op sharedutil.TrackReorderOp)
list *FocusList
menu *widget.PopUpMenu
ratingSubmenu *fyne.MenuItem
nowPlayingID string
colLayout *layouts.ColumnsLayout
tracksMutex sync.RWMutex
tracks []*util.TrackListModel
}
func NewPlayQueueList(im *backend.ImageManager) *PlayQueueList {
p := &PlayQueueList{}
p.ExtendBaseWidget(p)
// #, Cover, Title/Artist, Time
coverWidth := NewPlayQueueListRow(p, im, layout.NewSpacer()).cover.MinSize().Width
p.colLayout = layouts.NewColumnsLayout([]float32{40, coverWidth, -1, 60})
playIconResource := theme.NewThemedResource(theme.MediaPlayIcon())
playIconResource.ColorName = theme.ColorNamePrimary
playIconImg := canvas.NewImageFromResource(playIconResource)
playIconImg.FillMode = canvas.ImageFillContain
playIconImg.SetMinSize(fyne.NewSquareSize(theme.IconInlineSize() * 1.5))
playingIcon := container.NewCenter(playIconImg)
p.list = NewFocusList(
p.lenTracks,
func() fyne.CanvasObject {
return NewPlayQueueListRow(p, im, playingIcon)
},
func(itemID widget.ListItemID, item fyne.CanvasObject) {
p.tracksMutex.RLock()
// we could have removed tracks from the list in between
// Fyne calling the length callback and this update callback
// so the itemID may be out of bounds. if so, do nothing.
if itemID >= len(p.tracks) {
p.tracksMutex.RUnlock()
return
}
model := p.tracks[itemID]
p.tracksMutex.RUnlock()
tr := item.(*PlayQueueListRow)
p.list.SetItemForID(itemID, tr)
if tr.trackID != model.Track.ID || tr.ListItemID != itemID {
tr.ListItemID = itemID
}
tr.Update(model, itemID+1)
},
)
return p
}
func (p *PlayQueueList) SetTracks(trs []*mediaprovider.Track) {
p.tracksMutex.Lock()
p.list.ClearItemForIDMap()
p.tracks = util.ToTrackListModels(trs)
p.tracksMutex.Unlock()
p.Refresh()
}
// Sets the currently playing track ID and updates the list rendering
func (p *PlayQueueList) SetNowPlaying(trackID string) {
prevNowPlaying := p.nowPlayingID
p.tracksMutex.RLock()
trPrev, idxPrev := util.FindTrackByID(p.tracks, prevNowPlaying)
tr, idx := util.FindTrackByID(p.tracks, trackID)
p.tracksMutex.RUnlock()
p.nowPlayingID = trackID
if trPrev != nil {
p.list.RefreshItem(idxPrev)
}
if tr != nil {
p.list.RefreshItem(idx)
}
}
func (p *PlayQueueList) SelectAll() {
p.tracksMutex.RLock()
util.SelectAllTracks(p.tracks)
p.tracksMutex.RUnlock()
p.list.Refresh()
}
func (p *PlayQueueList) UnselectAll() {
p.tracksMutex.RLock()
util.UnselectAllTracks(p.tracks)
p.tracksMutex.RUnlock()
p.Refresh()
}
func (p *PlayQueueList) lenTracks() int {
p.tracksMutex.RLock()
defer p.tracksMutex.RUnlock()
return len(p.tracks)
}
func (t *PlayQueueList) onArtistTapped(artistID string) {
if t.OnShowArtistPage != nil {
t.OnShowArtistPage(artistID)
}
}
func (p *PlayQueueList) onPlayTrackAt(idx int) {
if p.OnPlayTrackAt != nil {
p.OnPlayTrackAt(idx)
}
}
func (p *PlayQueueList) onSelectTrack(idx int) {
if d, ok := fyne.CurrentApp().Driver().(desktop.Driver); ok {
mod := d.CurrentKeyModifiers()
if mod&os.ControlModifier != 0 {
p.selectAddOrRemove(idx)
} else if mod&fyne.KeyModifierShift != 0 {
p.selectRange(idx)
} else {
p.selectTrack(idx)
}
} else {
p.selectTrack(idx)
}
p.Refresh()
}
func (p *PlayQueueList) selectTrack(idx int) {
p.tracksMutex.RLock()
defer p.tracksMutex.RUnlock()
util.SelectTrack(p.tracks, idx)
}
func (p *PlayQueueList) selectAddOrRemove(idx int) {
p.tracksMutex.RLock()
defer p.tracksMutex.RUnlock()
p.tracks[idx].Selected = !p.tracks[idx].Selected
}
func (p *PlayQueueList) selectRange(idx int) {
p.tracksMutex.RLock()
defer p.tracksMutex.RUnlock()
util.SelectTrackRange(p.tracks, idx)
}
func (p *PlayQueueList) onShowContextMenu(e *fyne.PointEvent, trackIdx int) {
p.selectTrack(trackIdx)
p.list.Refresh()
if p.menu == nil {
playlist := fyne.NewMenuItem("Add to playlist...", func() {
if p.OnAddToPlaylist != nil {
p.OnAddToPlaylist(p.selectedTrackIDs())
}
})
playlist.Icon = myTheme.PlaylistIcon
download := fyne.NewMenuItem("Download...", func() {
if p.OnDownload != nil {
p.OnDownload(p.selectedTracks(), "Selected tracks")
}
})
download.Icon = theme.DownloadIcon()
favorite := fyne.NewMenuItem("Set favorite", func() {
if p.OnSetFavorite != nil {
p.OnSetFavorite(p.selectedTrackIDs(), true)
}
})
favorite.Icon = myTheme.FavoriteIcon
unfavorite := fyne.NewMenuItem("Unset favorite", func() {
if p.OnSetFavorite != nil {
p.OnSetFavorite(p.selectedTrackIDs(), false)
}
})
unfavorite.Icon = myTheme.NotFavoriteIcon
p.ratingSubmenu = util.NewRatingSubmenu(func(rating int) {
if p.OnSetRating != nil {
p.OnSetRating(p.selectedTrackIDs(), rating)
}
})
remove := fyne.NewMenuItem("Remove from queue", func() {
if p.OnRemoveFromQueue != nil {
p.OnRemoveFromQueue(p.selectedTrackIDs())
}
})
remove.Icon = theme.ContentRemoveIcon()
reorder := util.NewReorderTracksSubmenu(func(tro sharedutil.TrackReorderOp) {
if p.OnReorderTracks != nil {
p.OnReorderTracks(p.selectedTrackIDs(), tro)
}
})
p.menu = widget.NewPopUpMenu(
fyne.NewMenu("",
playlist,
download,
fyne.NewMenuItemSeparator(),
favorite,
unfavorite,
p.ratingSubmenu,
fyne.NewMenuItemSeparator(),
reorder,
remove,
),
fyne.CurrentApp().Driver().CanvasForObject(p),
)
}
p.ratingSubmenu.Disabled = p.DisableRating
p.menu.ShowAtPosition(e.AbsolutePosition)
}
func (t *PlayQueueList) selectedTracks() []*mediaprovider.Track {
t.tracksMutex.RLock()
defer t.tracksMutex.RUnlock()
return util.SelectedTracks(t.tracks)
}
func (t *PlayQueueList) selectedTrackIDs() []string {
t.tracksMutex.RLock()
defer t.tracksMutex.RUnlock()
return util.SelectedTrackIDs(t.tracks)
}
func (p *PlayQueueList) CreateRenderer() fyne.WidgetRenderer {
return widget.NewSimpleRenderer(p.list)
}
type PlayQueueListRow struct {
FocusListRowBase
OnTappedSecondary func(e *fyne.PointEvent, trackIdx int)
imageLoader util.ThumbnailLoader
playQueueList *PlayQueueList
trackID string
isPlaying bool
playingIcon fyne.CanvasObject
num *widget.Label
cover *ImagePlaceholder
title *widget.Label
artist *MultiHyperlink
time *widget.Label
}
func NewPlayQueueListRow(playQueueList *PlayQueueList, im *backend.ImageManager, playingIcon fyne.CanvasObject) *PlayQueueListRow {
p := &PlayQueueListRow{
playingIcon: playingIcon,
playQueueList: playQueueList,
num: widget.NewLabel(""),
cover: NewImagePlaceholder(myTheme.TracksIcon, thumbnailSize),
title: util.NewTruncatingLabel(),
artist: NewMultiHyperlink(),
time: util.NewTrailingAlignLabel(),
}
p.ExtendBaseWidget(p)
p.cover.ScaleMode = canvas.ImageScaleFastest
p.artist.OnTapped = playQueueList.onArtistTapped
p.OnDoubleTapped = func() {
playQueueList.onPlayTrackAt(p.ItemID())
}
p.OnTapped = func() {
playQueueList.onSelectTrack(p.ItemID())
}
p.OnTappedSecondary = playQueueList.onShowContextMenu
p.OnFocusNeighbor = func(up bool) {
playQueueList.list.FocusNeighbor(p.ItemID(), up)
}
p.imageLoader = util.NewThumbnailLoader(im, func(i image.Image) {
p.cover.SetImage(i, false)
})
p.imageLoader.OnBeforeLoad = func() {
p.cover.SetImage(nil, false)
}
p.Content = container.New(playQueueList.colLayout,
container.NewCenter(p.num),
container.NewPadded(p.cover),
container.New(&layouts.VboxCustomPadding{ExtraPad: -15},
p.title, p.artist),
container.NewCenter(p.time),
)
return p
}
func (p *PlayQueueListRow) TappedSecondary(e *fyne.PointEvent) {
if p.OnTappedSecondary != nil {
p.OnTappedSecondary(e, p.ListItemID)
}
}
func (p *PlayQueueListRow) Update(tm *util.TrackListModel, rowNum int) {
changed := false
if tm.Selected != p.Selected {
p.Selected = tm.Selected
changed = true
}
if num := strconv.Itoa(rowNum); p.num.Text != num {
p.num.Text = num
changed = true
}
// Update info that can change if this row is bound to
// a new track (*mediaprovider.Track)
tr := tm.Track
if tr.ID != p.trackID {
p.imageLoader.Load(tm.Track.CoverArtID)
p.EnsureUnfocused()
p.trackID = tr.ID
p.title.Text = tr.Name
p.artist.BuildSegments(tr.ArtistNames, tr.ArtistIDs)
p.time.Text = util.SecondsToTimeString(float64(tr.Duration))
changed = true
}
// Render whether track is playing or not
if isPlaying := p.playQueueList.nowPlayingID == tr.ID; isPlaying != p.isPlaying {
p.isPlaying = isPlaying
p.title.TextStyle.Bold = isPlaying
if isPlaying {
p.Content.(*fyne.Container).Objects[0] = p.playingIcon
} else {
p.Content.(*fyne.Container).Objects[0] = container.NewCenter(p.num)
}
changed = true
}
if changed {
p.Refresh()
}
}
+65 -163
View File
@@ -99,8 +99,8 @@ type Tracklist struct {
sorting TracklistSort sorting TracklistSort
tracksMutex sync.RWMutex tracksMutex sync.RWMutex
tracks []*trackModel tracks []*util.TrackListModel
tracksOrigOrder []*trackModel tracksOrigOrder []*util.TrackListModel
nowPlayingID string nowPlayingID string
colLayout *layouts.ColumnsLayout colLayout *layouts.ColumnsLayout
@@ -111,11 +111,6 @@ type Tracklist struct {
container *fyne.Container container *fyne.Container
} }
type trackModel struct {
track *mediaprovider.Track
selected bool
}
func NewTracklist(tracks []*mediaprovider.Track) *Tracklist { func NewTracklist(tracks []*mediaprovider.Track) *Tracklist {
t := &Tracklist{visibleColumns: make([]bool, numColumns)} t := &Tracklist{visibleColumns: make([]bool, numColumns)}
t.ExtendBaseWidget(t) t.ExtendBaseWidget(t)
@@ -169,7 +164,7 @@ func NewTracklist(tracks []*mediaprovider.Track) *Tracklist {
tr := item.(*TrackRow) tr := item.(*TrackRow)
t.list.SetItemForID(itemID, tr) t.list.SetItemForID(itemID, tr)
if tr.trackID != model.track.ID || tr.ListItemID != itemID { if tr.trackID != model.Track.ID || tr.ListItemID != itemID {
tr.ListItemID = itemID tr.ListItemID = itemID
} }
i := -1 // signal that we want to display the actual track num. i := -1 // signal that we want to display the actual track num.
@@ -222,7 +217,7 @@ func (t *Tracklist) TrackAt(idx int) *mediaprovider.Track {
log.Println("error: Tracklist.TrackAt: index out of range") log.Println("error: Tracklist.TrackAt: index out of range")
return nil return nil
} }
return t.tracks[idx].track return t.tracks[idx].Track
} }
func (t *Tracklist) SetVisibleColumns(cols []string) { func (t *Tracklist) SetVisibleColumns(cols []string) {
@@ -284,8 +279,8 @@ func (t *Tracklist) SetSorting(sorting TracklistSort) {
func (t *Tracklist) SetNowPlaying(trackID string) { func (t *Tracklist) SetNowPlaying(trackID string) {
prevNowPlaying := t.nowPlayingID prevNowPlaying := t.nowPlayingID
t.tracksMutex.RLock() t.tracksMutex.RLock()
trPrev, idxPrev := t.findTrackByID(prevNowPlaying) trPrev, idxPrev := util.FindTrackByID(t.tracks, prevNowPlaying)
tr, idx := t.findTrackByID(trackID) tr, idx := util.FindTrackByID(t.tracks, trackID)
t.tracksMutex.RUnlock() t.tracksMutex.RUnlock()
t.nowPlayingID = trackID t.nowPlayingID = trackID
if trPrev != nil { if trPrev != nil {
@@ -299,7 +294,7 @@ func (t *Tracklist) SetNowPlaying(trackID string) {
// Increments the play count of the given track and updates the list rendering // Increments the play count of the given track and updates the list rendering
func (t *Tracklist) IncrementPlayCount(trackID string) { func (t *Tracklist) IncrementPlayCount(trackID string) {
t.tracksMutex.RLock() t.tracksMutex.RLock()
tr, idx := t.findTrackByID(trackID) tr, idx := util.FindTrackByID(t.tracks, trackID)
t.tracksMutex.RUnlock() t.tracksMutex.RUnlock()
if tr != nil { if tr != nil {
tr.PlayCount += 1 tr.PlayCount += 1
@@ -328,7 +323,7 @@ func (t *Tracklist) _setTracks(trs []*mediaprovider.Track) {
if t.list != nil { if t.list != nil {
t.list.ClearItemForIDMap() t.list.ClearItemForIDMap()
} }
t.tracksOrigOrder = toTrackModels(trs) t.tracksOrigOrder = util.ToTrackListModels(trs)
t.doSortTracks() t.doSortTracks()
} }
@@ -336,8 +331,8 @@ func (t *Tracklist) _setTracks(trs []*mediaprovider.Track) {
func (t *Tracklist) GetTracks() []*mediaprovider.Track { func (t *Tracklist) GetTracks() []*mediaprovider.Track {
t.tracksMutex.RLock() t.tracksMutex.RLock()
defer t.tracksMutex.RUnlock() defer t.tracksMutex.RUnlock()
return sharedutil.MapSlice(t.tracks, func(tm *trackModel) *mediaprovider.Track { return sharedutil.MapSlice(t.tracks, func(tm *util.TrackListModel) *mediaprovider.Track {
return tm.track return tm.Track
}) })
} }
@@ -345,15 +340,13 @@ func (t *Tracklist) GetTracks() []*mediaprovider.Track {
func (t *Tracklist) AppendTracks(trs []*mediaprovider.Track) { func (t *Tracklist) AppendTracks(trs []*mediaprovider.Track) {
t.tracksMutex.Lock() t.tracksMutex.Lock()
defer t.tracksMutex.Unlock() defer t.tracksMutex.Unlock()
t.tracksOrigOrder = append(t.tracks, toTrackModels(trs)...) t.tracksOrigOrder = append(t.tracks, util.ToTrackListModels(trs)...)
t.doSortTracks() t.doSortTracks()
} }
func (t *Tracklist) SelectAll() { func (t *Tracklist) SelectAll() {
t.tracksMutex.RLock() t.tracksMutex.RLock()
for _, tm := range t.tracks { util.SelectAllTracks(t.tracks)
tm.selected = true
}
t.tracksMutex.RUnlock() t.tracksMutex.RUnlock()
t.list.Refresh() t.list.Refresh()
} }
@@ -365,9 +358,7 @@ func (t *Tracklist) UnselectAll() {
func (t *Tracklist) unselectAll() { func (t *Tracklist) unselectAll() {
t.tracksMutex.RLock() t.tracksMutex.RLock()
for _, tm := range t.tracks { util.UnselectAllTracks(t.tracks)
tm.selected = false
}
t.tracksMutex.RUnlock() t.tracksMutex.RUnlock()
} }
@@ -375,11 +366,11 @@ func (t *Tracklist) SelectAndScrollToTrack(trackID string) {
t.tracksMutex.RLock() t.tracksMutex.RLock()
idx := -1 idx := -1
for i, tr := range t.tracks { for i, tr := range t.tracks {
if tr.track.ID == trackID { if tr.Track.ID == trackID {
idx = i idx = i
tr.selected = true tr.Selected = true
} else { } else {
tr.selected = false tr.Selected = false
} }
} }
t.tracksMutex.RUnlock() t.tracksMutex.RUnlock()
@@ -397,18 +388,12 @@ func (t *Tracklist) Refresh() {
t.BaseWidget.Refresh() t.BaseWidget.Refresh()
} }
func toTrackModels(trs []*mediaprovider.Track) []*trackModel {
return sharedutil.MapSlice(trs, func(tr *mediaprovider.Track) *trackModel {
return &trackModel{track: tr, selected: false}
})
}
// do nothing Tapped handler so that tapping the separator between rows // do nothing Tapped handler so that tapping the separator between rows
// doesn't fall through to the page (which calls UnselectAll on tracklist) // doesn't fall through to the page (which calls UnselectAll on tracklist)
func (t *Tracklist) Tapped(*fyne.PointEvent) {} func (t *Tracklist) Tapped(*fyne.PointEvent) {}
func (t *Tracklist) stringSort(fieldFn func(*trackModel) string) { func (t *Tracklist) stringSort(fieldFn func(*util.TrackListModel) string) {
new := make([]*trackModel, len(t.tracksOrigOrder)) new := make([]*util.TrackListModel, len(t.tracksOrigOrder))
copy(new, t.tracksOrigOrder) copy(new, t.tracksOrigOrder)
sort.SliceStable(new, func(i, j int) bool { sort.SliceStable(new, func(i, j int) bool {
cmp := strings.Compare(fieldFn(new[i]), fieldFn(new[j])) cmp := strings.Compare(fieldFn(new[i]), fieldFn(new[j]))
@@ -420,8 +405,8 @@ func (t *Tracklist) stringSort(fieldFn func(*trackModel) string) {
t.tracks = new t.tracks = new
} }
func (t *Tracklist) intSort(fieldFn func(*trackModel) int64) { func (t *Tracklist) intSort(fieldFn func(*util.TrackListModel) int64) {
new := make([]*trackModel, len(t.tracksOrigOrder)) new := make([]*util.TrackListModel, len(t.tracksOrigOrder))
copy(new, t.tracksOrigOrder) copy(new, t.tracksOrigOrder)
sort.SliceStable(new, func(i, j int) bool { sort.SliceStable(new, func(i, j int) bool {
if t.sorting.SortOrder == SortDescending { if t.sorting.SortOrder == SortDescending {
@@ -445,30 +430,30 @@ func (t *Tracklist) doSortTracks() {
t.tracks = t.tracksOrigOrder t.tracks = t.tracksOrigOrder
} }
case ColumnTitle: case ColumnTitle:
t.stringSort(func(tr *trackModel) string { return tr.track.Name }) t.stringSort(func(tr *util.TrackListModel) string { return tr.Track.Name })
case ColumnArtist: case ColumnArtist:
t.stringSort(func(tr *trackModel) string { return strings.Join(tr.track.ArtistNames, ", ") }) t.stringSort(func(tr *util.TrackListModel) string { return strings.Join(tr.Track.ArtistNames, ", ") })
case ColumnAlbum: case ColumnAlbum:
t.stringSort(func(tr *trackModel) string { return tr.track.Album }) t.stringSort(func(tr *util.TrackListModel) string { return tr.Track.Album })
case ColumnPath: case ColumnPath:
t.stringSort(func(tr *trackModel) string { return tr.track.FilePath }) t.stringSort(func(tr *util.TrackListModel) string { return tr.Track.FilePath })
case ColumnRating: case ColumnRating:
t.intSort(func(tr *trackModel) int64 { return int64(tr.track.Rating) }) t.intSort(func(tr *util.TrackListModel) int64 { return int64(tr.Track.Rating) })
case ColumnTime: case ColumnTime:
t.intSort(func(tr *trackModel) int64 { return int64(tr.track.Duration) }) t.intSort(func(tr *util.TrackListModel) int64 { return int64(tr.Track.Duration) })
case ColumnYear: case ColumnYear:
t.intSort(func(tr *trackModel) int64 { return int64(tr.track.Year) }) t.intSort(func(tr *util.TrackListModel) int64 { return int64(tr.Track.Year) })
case ColumnSize: case ColumnSize:
t.intSort(func(tr *trackModel) int64 { return tr.track.Size }) t.intSort(func(tr *util.TrackListModel) int64 { return tr.Track.Size })
case ColumnPlays: case ColumnPlays:
t.intSort(func(tr *trackModel) int64 { return int64(tr.track.PlayCount) }) t.intSort(func(tr *util.TrackListModel) int64 { return int64(tr.Track.PlayCount) })
case ColumnComment: case ColumnComment:
t.stringSort(func(tr *trackModel) string { return tr.track.Comment }) t.stringSort(func(tr *util.TrackListModel) string { return tr.Track.Comment })
case ColumnBitrate: case ColumnBitrate:
t.intSort(func(tr *trackModel) int64 { return int64(tr.track.BitRate) }) t.intSort(func(tr *util.TrackListModel) int64 { return int64(tr.Track.BitRate) })
case ColumnFavorite: case ColumnFavorite:
t.intSort(func(tr *trackModel) int64 { t.intSort(func(tr *util.TrackListModel) int64 {
if tr.track.Favorite { if tr.Track.Favorite {
return 1 return 1
} }
return 0 return 0
@@ -509,41 +494,19 @@ func (t *Tracklist) onSelectTrack(idx int) {
func (t *Tracklist) selectAddOrRemove(idx int) { func (t *Tracklist) selectAddOrRemove(idx int) {
t.tracksMutex.RLock() t.tracksMutex.RLock()
defer t.tracksMutex.RUnlock() defer t.tracksMutex.RUnlock()
t.tracks[idx].selected = !t.tracks[idx].selected t.tracks[idx].Selected = !t.tracks[idx].Selected
} }
func (t *Tracklist) selectTrack(idx int) { func (t *Tracklist) selectTrack(idx int) {
t.tracksMutex.RLock() t.tracksMutex.RLock()
defer t.tracksMutex.RUnlock() defer t.tracksMutex.RUnlock()
if t.tracks[idx].selected { util.SelectTrack(t.tracks, idx)
return
}
t.unselectAll()
t.tracks[idx].selected = true
} }
func (t *Tracklist) selectRange(idx int) { func (t *Tracklist) selectRange(idx int) {
t.tracksMutex.RLock() t.tracksMutex.RLock()
defer t.tracksMutex.RUnlock() defer t.tracksMutex.RUnlock()
if t.tracks[idx].selected { util.SelectTrackRange(t.tracks, idx)
return
}
lastSelected := -1
for i := len(t.tracks) - 1; i >= 0; i-- {
if t.tracks[i].selected {
lastSelected = i
break
}
}
if lastSelected < 0 {
t.tracks[idx].selected = true
return
}
from := minInt(idx, lastSelected)
to := maxInt(idx, lastSelected)
for i := from; i <= to; i++ {
t.tracks[i].selected = true
}
} }
func (t *Tracklist) onShowContextMenu(e *fyne.PointEvent, trackIdx int) { func (t *Tracklist) onShowContextMenu(e *fyne.PointEvent, trackIdx int) {
@@ -609,7 +572,7 @@ func (t *Tracklist) onShowContextMenu(e *fyne.PointEvent, trackIdx int) {
func (t *Tracklist) onSetFavorite(trackID string, fav bool) { func (t *Tracklist) onSetFavorite(trackID string, fav bool) {
t.tracksMutex.RLock() t.tracksMutex.RLock()
tr, _ := t.findTrackByID(trackID) tr, _ := util.FindTrackByID(t.tracks, trackID)
t.tracksMutex.RUnlock() t.tracksMutex.RUnlock()
t.onSetFavorites([]*mediaprovider.Track{tr}, fav, false) t.onSetFavorites([]*mediaprovider.Track{tr}, fav, false)
} }
@@ -630,7 +593,7 @@ func (t *Tracklist) onSetFavorites(tracks []*mediaprovider.Track, fav bool, need
func (t *Tracklist) onSetRating(trackID string, rating int) { func (t *Tracklist) onSetRating(trackID string, rating int) {
// update our own track model // update our own track model
t.tracksMutex.RLock() t.tracksMutex.RLock()
tr, _ := t.findTrackByID(trackID) tr, _ := util.FindTrackByID(t.tracks, trackID)
t.tracksMutex.RUnlock() t.tracksMutex.RUnlock()
t.onSetRatings([]*mediaprovider.Track{tr}, rating, false) t.onSetRatings([]*mediaprovider.Track{tr}, rating, false)
} }
@@ -666,34 +629,16 @@ func (t *Tracklist) onDownload(tracks []*mediaprovider.Track, downloadName strin
} }
} }
func (t *Tracklist) findTrackByID(id string) (*mediaprovider.Track, int) { func (t *Tracklist) selectedTracks() []*mediaprovider.Track {
idx := sharedutil.Find(t.tracks, func(tr *trackModel) bool {
return tr.track.ID == id
})
if idx >= 0 {
return t.tracks[idx].track, idx
}
return nil, -1
}
func (t *Tracklist) selectedTrackModels() []*trackModel {
t.tracksMutex.RLock() t.tracksMutex.RLock()
defer t.tracksMutex.RUnlock() defer t.tracksMutex.RUnlock()
return sharedutil.FilterSlice(t.tracks, func(tm *trackModel) bool { return util.SelectedTracks(t.tracks)
return tm.selected
})
}
func (t *Tracklist) selectedTracks() []*mediaprovider.Track {
return sharedutil.MapSlice(t.selectedTrackModels(), func(tm *trackModel) *mediaprovider.Track {
return tm.track
})
} }
func (t *Tracklist) SelectedTrackIDs() []string { func (t *Tracklist) SelectedTrackIDs() []string {
return sharedutil.MapSlice(t.selectedTrackModels(), func(tm *trackModel) string { t.tracksMutex.RLock()
return tm.track.ID defer t.tracksMutex.RUnlock()
}) return util.SelectedTrackIDs(t.tracks)
} }
func (t *Tracklist) lenTracks() int { func (t *Tracklist) lenTracks() int {
@@ -752,61 +697,43 @@ type TrackRow struct {
func NewTrackRow(tracklist *Tracklist, playingIcon fyne.CanvasObject) *TrackRow { func NewTrackRow(tracklist *Tracklist, playingIcon fyne.CanvasObject) *TrackRow {
t := &TrackRow{tracklist: tracklist, playingIcon: playingIcon} t := &TrackRow{tracklist: tracklist, playingIcon: playingIcon}
t.ExtendBaseWidget(t) t.ExtendBaseWidget(t)
t.num = newTrailingAlignLabel() t.num = util.NewTrailingAlignLabel()
t.name = newTruncatingRichText() t.name = util.NewTruncatingRichText()
t.artist = NewMultiHyperlink() t.artist = NewMultiHyperlink()
t.artist.OnTapped = tracklist.onArtistTapped t.artist.OnTapped = tracklist.onArtistTapped
t.album = widget.NewHyperlink("", nil) t.album = widget.NewHyperlink("", nil)
t.album.Truncation = fyne.TextTruncateEllipsis t.album.Truncation = fyne.TextTruncateEllipsis
t.album.OnTapped = func() { tracklist.onAlbumTapped(t.albumID) } t.album.OnTapped = func() { tracklist.onAlbumTapped(t.albumID) }
t.dur = newTrailingAlignLabel() t.dur = util.NewTrailingAlignLabel()
t.year = newTrailingAlignLabel() t.year = util.NewTrailingAlignLabel()
favorite := NewTappableIcon(myTheme.NotFavoriteIcon) favorite := NewFavoriteIcon()
favorite.OnTapped = t.toggleFavorited favorite.OnTapped = t.toggleFavorited
t.favorite = container.NewCenter(favorite) t.favorite = container.NewCenter(favorite)
t.rating = NewStarRating() t.rating = NewStarRating()
t.rating.IsDisabled = t.tracklist.Options.DisableRating t.rating.IsDisabled = t.tracklist.Options.DisableRating
t.rating.StarSize = 16 t.rating.StarSize = 16
t.rating.OnRatingChanged = t.setTrackRating t.rating.OnRatingChanged = t.setTrackRating
t.plays = newTrailingAlignLabel() t.plays = util.NewTrailingAlignLabel()
t.comment = newTruncatingLabel() t.comment = util.NewTruncatingLabel()
t.bitrate = newTrailingAlignLabel() t.bitrate = util.NewTrailingAlignLabel()
t.size = newTrailingAlignLabel() t.size = util.NewTrailingAlignLabel()
t.path = newTruncatingLabel() t.path = util.NewTruncatingLabel()
t.Content = container.New(tracklist.colLayout, t.Content = container.New(tracklist.colLayout,
t.num, t.name, t.artist, t.album, t.dur, t.year, t.favorite, t.rating, t.plays, t.comment, t.bitrate, t.size, t.path) t.num, t.name, t.artist, t.album, t.dur, t.year, t.favorite, t.rating, t.plays, t.comment, t.bitrate, t.size, t.path)
return t return t
} }
func newTruncatingRichText() *widget.RichText { func (t *TrackRow) Update(tm *util.TrackListModel, rowNum int) {
rt := widget.NewRichTextWithText("")
rt.Truncation = fyne.TextTruncateEllipsis
return rt
}
func newTruncatingLabel() *widget.Label {
rt := widget.NewLabel("")
rt.Truncation = fyne.TextTruncateEllipsis
return rt
}
func newTrailingAlignLabel() *widget.Label {
rt := widget.NewLabel("")
rt.Alignment = fyne.TextAlignTrailing
return rt
}
func (t *TrackRow) Update(tm *trackModel, rowNum int) {
changed := false changed := false
if tm.selected != t.Selected { if tm.Selected != t.Selected {
t.Selected = tm.selected t.Selected = tm.Selected
changed = true changed = true
} }
// Update info that can change if this row is bound to // Update info that can change if this row is bound to
// a new track (*mediaprovider.Track) // a new track (*mediaprovider.Track)
tr := tm.track tr := tm.Track
if tr.ID != t.trackID { if tr.ID != t.trackID {
t.EnsureUnfocused() t.EnsureUnfocused()
t.trackID = tr.ID t.trackID = tr.ID
@@ -859,7 +786,7 @@ func (t *TrackRow) Update(tm *trackModel, rowNum int) {
t.name.Segments[0].(*widget.TextSegment).Style.TextStyle.Bold = isPlaying t.name.Segments[0].(*widget.TextSegment).Style.TextStyle.Bold = isPlaying
if isPlaying { if isPlaying {
t.Content.(*fyne.Container).Objects[0] = container.NewCenter(t.playingIcon) t.Content.(*fyne.Container).Objects[0] = t.playingIcon
} else { } else {
t.Content.(*fyne.Container).Objects[0] = t.num t.Content.(*fyne.Container).Objects[0] = t.num
} }
@@ -868,13 +795,8 @@ func (t *TrackRow) Update(tm *trackModel, rowNum int) {
// Update favorite column // Update favorite column
if tr.Favorite != t.isFavorite { if tr.Favorite != t.isFavorite {
if tr.Favorite { t.isFavorite = tr.Favorite
t.isFavorite = true t.favorite.Objects[0].(*FavoriteIcon).Favorite = tr.Favorite
t.favorite.Objects[0].(*TappableIcon).Resource = myTheme.FavoriteIcon
} else {
t.isFavorite = false
t.favorite.Objects[0].(*TappableIcon).Resource = myTheme.NotFavoriteIcon
}
changed = true changed = true
} }
@@ -914,17 +836,11 @@ func (t *TrackRow) Update(tm *trackModel, rowNum int) {
} }
func (t *TrackRow) toggleFavorited() { func (t *TrackRow) toggleFavorited() {
if t.isFavorite { t.isFavorite = !t.isFavorite
t.favorite.Objects[0].(*TappableIcon).Resource = myTheme.NotFavoriteIcon favIcon := t.favorite.Objects[0].(*FavoriteIcon)
t.favorite.Refresh() favIcon.Favorite = t.isFavorite
t.isFavorite = false t.favorite.Refresh()
t.tracklist.onSetFavorite(t.trackID, false) t.tracklist.onSetFavorite(t.trackID, t.isFavorite)
} else {
t.favorite.Objects[0].(*TappableIcon).Resource = myTheme.FavoriteIcon
t.favorite.Refresh()
t.isFavorite = true
t.tracklist.onSetFavorite(t.trackID, true)
}
} }
func (t *TrackRow) setTrackRating(rating int) { func (t *TrackRow) setTrackRating(rating int) {
@@ -936,17 +852,3 @@ func (t *TrackRow) TappedSecondary(e *fyne.PointEvent) {
t.OnTappedSecondary(e, t.ListItemID) t.OnTappedSecondary(e, t.ListItemID)
} }
} }
func minInt(a, b int) int {
if a < b {
return a
}
return b
}
func maxInt(a, b int) int {
if a > b {
return a
}
return b
}