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:
+14
-22
@@ -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
@@ -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)
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
@@ -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)
|
||||||
|
|||||||
@@ -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
@@ -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)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ const (
|
|||||||
Albums
|
Albums
|
||||||
Artist
|
Artist
|
||||||
Artists
|
Artists
|
||||||
|
Fullscreen
|
||||||
Genre
|
Genre
|
||||||
Genres
|
Genres
|
||||||
Favorites
|
Favorites
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ const (
|
|||||||
WidgetTypeGridView
|
WidgetTypeGridView
|
||||||
WidgetTypePlaylistPageHeader
|
WidgetTypePlaylistPageHeader
|
||||||
WidgetTypeTracklist
|
WidgetTypeTracklist
|
||||||
|
WidgetTypeNowPlayingPage
|
||||||
|
|
||||||
// keep at bottom
|
// keep at bottom
|
||||||
numWidgetTypes
|
numWidgetTypes
|
||||||
|
|||||||
@@ -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())
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user