Merge pull request #53 from dweymouth/feature/favorite-artists-songs
Feature/favorite artists songs
This commit is contained in:
@@ -18,7 +18,7 @@ Slightly outdated screenshots of Supersonic running against the Navidrome <a hre
|
|||||||
* [x] Artist view with biography, image, similar artists, and discography
|
* [x] Artist view with biography, image, similar artists, and discography
|
||||||
* [x] Create, play, and update playlists
|
* [x] Create, play, and update playlists
|
||||||
* [x] Configure visible tracklist columns
|
* [x] Configure visible tracklist columns
|
||||||
* [x] Set/unset favorite and browse by favorites (albums only; artists+songs coming soon)
|
* [x] Set/unset favorite and browse by favorite albums, artists, and songs
|
||||||
* [x] View and edit play queue (add and remove tracks; reorder support coming soon)
|
* [x] View and edit play queue (add and remove tracks; reorder support coming soon)
|
||||||
* [ ] Shuffle and repeat playback modes (planned)
|
* [ ] Shuffle and repeat playback modes (planned)
|
||||||
* [ ] Set and view five-star rating (planned)
|
* [ ] Set and view five-star rating (planned)
|
||||||
|
|||||||
+12
-2
@@ -24,6 +24,11 @@ type AlbumPageConfig struct {
|
|||||||
TracklistColumns []string
|
TracklistColumns []string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type FavoritesPageConfig struct {
|
||||||
|
InitialView string
|
||||||
|
TracklistColumns []string
|
||||||
|
}
|
||||||
|
|
||||||
type NowPlayingPageConfig struct {
|
type NowPlayingPageConfig struct {
|
||||||
TracklistColumns []string
|
TracklistColumns []string
|
||||||
}
|
}
|
||||||
@@ -40,6 +45,7 @@ type Config struct {
|
|||||||
Application AppConfig
|
Application AppConfig
|
||||||
Servers []*ServerConfig
|
Servers []*ServerConfig
|
||||||
AlbumPage AlbumPageConfig
|
AlbumPage AlbumPageConfig
|
||||||
|
FavoritesPage FavoritesPageConfig
|
||||||
NowPlayingPage NowPlayingPageConfig
|
NowPlayingPage NowPlayingPageConfig
|
||||||
PlaylistPage PlaylistPageConfig
|
PlaylistPage PlaylistPageConfig
|
||||||
LocalPlayback LocalPlaybackConfig
|
LocalPlayback LocalPlaybackConfig
|
||||||
@@ -52,10 +58,14 @@ func DefaultConfig() *Config {
|
|||||||
WindowHeight: 800,
|
WindowHeight: 800,
|
||||||
},
|
},
|
||||||
AlbumPage: AlbumPageConfig{
|
AlbumPage: AlbumPageConfig{
|
||||||
TracklistColumns: []string{"Artist", "Time", "Plays"},
|
TracklistColumns: []string{"Artist", "Time", "Plays", "Favorite"},
|
||||||
|
},
|
||||||
|
FavoritesPage: FavoritesPageConfig{
|
||||||
|
TracklistColumns: []string{"Artist", "Album", "Time", "Plays"},
|
||||||
|
InitialView: "Albums",
|
||||||
},
|
},
|
||||||
NowPlayingPage: NowPlayingPageConfig{
|
NowPlayingPage: NowPlayingPageConfig{
|
||||||
TracklistColumns: []string{"Artist", "Album", "Time"},
|
TracklistColumns: []string{"Artist", "Album", "Time", "Plays"},
|
||||||
},
|
},
|
||||||
PlaylistPage: PlaylistPageConfig{
|
PlaylistPage: PlaylistPageConfig{
|
||||||
TracklistColumns: []string{"Artist", "Album", "Time", "Plays"},
|
TracklistColumns: []string{"Artist", "Album", "Time", "Plays"},
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"supersonic/backend/util"
|
"supersonic/backend/util"
|
||||||
"supersonic/player"
|
"supersonic/player"
|
||||||
|
"supersonic/sharedutil"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/dweymouth/go-subsonic/subsonic"
|
"github.com/dweymouth/go-subsonic/subsonic"
|
||||||
@@ -141,7 +142,11 @@ func (p *PlaybackManager) LoadTracks(tracks []*subsonic.Child, appendToQueue, sh
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
p.player.AppendFile(url.String())
|
p.player.AppendFile(url.String())
|
||||||
p.playQueue = append(p.playQueue, tracks[i])
|
// ensure a deep copy of the track info so that we can maintain our own state
|
||||||
|
// (tracking play count increases, favorite, and rating) without messing up
|
||||||
|
// other views' track models
|
||||||
|
tr := *tracks[i]
|
||||||
|
p.playQueue = append(p.playQueue, &tr)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -176,10 +181,25 @@ func (p *PlaybackManager) PlayTrackAt(idx int) error {
|
|||||||
|
|
||||||
func (p *PlaybackManager) GetPlayQueue() []*subsonic.Child {
|
func (p *PlaybackManager) GetPlayQueue() []*subsonic.Child {
|
||||||
pq := make([]*subsonic.Child, len(p.playQueue))
|
pq := make([]*subsonic.Child, len(p.playQueue))
|
||||||
copy(pq, p.playQueue)
|
for i, tr := range p.playQueue {
|
||||||
|
copy := *tr
|
||||||
|
pq[i] = ©
|
||||||
|
}
|
||||||
return pq
|
return pq
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Any time the user changes the favorite status of a track elsewhere in the app,
|
||||||
|
// this should be called to ensure the in-memory track model is updated.
|
||||||
|
func (p *PlaybackManager) OnTrackFavoriteStatusChanged(id string, fav bool) {
|
||||||
|
if tr := sharedutil.FindTrackByID(id, p.playQueue); tr != nil {
|
||||||
|
if fav {
|
||||||
|
tr.Starred = time.Now()
|
||||||
|
} else {
|
||||||
|
tr.Starred = time.Time{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// trackIdxs must be sorted
|
// trackIdxs must be sorted
|
||||||
func (p *PlaybackManager) RemoveTracksFromQueue(trackIdxs []int) {
|
func (p *PlaybackManager) RemoveTracksFromQueue(trackIdxs []int) {
|
||||||
newQueue := make([]*subsonic.Child, 0, len(p.playQueue)-len(trackIdxs))
|
newQueue := make([]*subsonic.Child, 0, len(p.playQueue)-len(trackIdxs))
|
||||||
@@ -228,6 +248,7 @@ func (p *PlaybackManager) checkScrobble(playDur time.Duration) {
|
|||||||
song := p.playQueue[p.nowPlayingIdx]
|
song := p.playQueue[p.nowPlayingIdx]
|
||||||
if playDur.Seconds()/p.curTrackTime > ScrobbleThreshold {
|
if playDur.Seconds()/p.curTrackTime > ScrobbleThreshold {
|
||||||
log.Printf("Scrobbling %q", song.Title)
|
log.Printf("Scrobbling %q", song.Title)
|
||||||
|
song.PlayCount += 1
|
||||||
p.lastScrobbled = song
|
p.lastScrobbled = song
|
||||||
p.sm.Server.Scrobble(song.ID, map[string]string{"time": strconv.FormatInt(time.Now().Unix()*1000, 10)})
|
p.sm.Server.Scrobble(song.ID, map[string]string{"time": strconv.FormatInt(time.Now().Unix()*1000, 10)})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package sharedutil
|
||||||
|
|
||||||
|
import "github.com/dweymouth/go-subsonic/subsonic"
|
||||||
|
|
||||||
|
func FindTrackByID(id string, tracks []*subsonic.Child) *subsonic.Child {
|
||||||
|
for _, tr := range tracks {
|
||||||
|
if id == tr.ID {
|
||||||
|
return tr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TrackIDOrEmptyStr(track *subsonic.Child) string {
|
||||||
|
if track == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return track.ID
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"supersonic/backend"
|
"supersonic/backend"
|
||||||
"supersonic/res"
|
"supersonic/res"
|
||||||
|
"supersonic/sharedutil"
|
||||||
"supersonic/ui/controller"
|
"supersonic/ui/controller"
|
||||||
"supersonic/ui/layouts"
|
"supersonic/ui/layouts"
|
||||||
"supersonic/ui/util"
|
"supersonic/ui/util"
|
||||||
@@ -66,14 +67,7 @@ func NewAlbumPage(
|
|||||||
a.header = NewAlbumPageHeader(a)
|
a.header = NewAlbumPageHeader(a)
|
||||||
a.tracklist = widgets.NewTracklist(nil)
|
a.tracklist = widgets.NewTracklist(nil)
|
||||||
a.tracklist.SetVisibleColumns(a.cfg.TracklistColumns)
|
a.tracklist.SetVisibleColumns(a.cfg.TracklistColumns)
|
||||||
// connect tracklist actions
|
a.contr.ConnectTracklistActions(a.tracklist)
|
||||||
a.tracklist.OnPlayTrackAt = a.onPlayTrackAt
|
|
||||||
a.tracklist.OnAddToQueue = func(tracks []*subsonic.Child) { a.pm.LoadTracks(tracks, true, false) }
|
|
||||||
a.tracklist.OnPlaySelection = func(tracks []*subsonic.Child) {
|
|
||||||
a.pm.LoadTracks(tracks, false, false)
|
|
||||||
a.pm.PlayFromBeginning()
|
|
||||||
}
|
|
||||||
a.tracklist.OnAddToPlaylist = a.contr.DoAddTracksToPlaylistWorkflow
|
|
||||||
|
|
||||||
a.container = container.NewBorder(
|
a.container = container.NewBorder(
|
||||||
container.New(&layouts.MaxPadLayout{PadLeft: 15, PadRight: 15, PadTop: 15, PadBottom: 10}, a.header),
|
container.New(&layouts.MaxPadLayout{PadLeft: 15, PadRight: 15, PadTop: 15, PadBottom: 10}, a.header),
|
||||||
@@ -107,7 +101,7 @@ func (a *AlbumPage) OnSongChange(song *subsonic.Child, lastScrobbledIfAny *subso
|
|||||||
a.nowPlayingID = song.ID
|
a.nowPlayingID = song.ID
|
||||||
}
|
}
|
||||||
a.tracklist.SetNowPlaying(a.nowPlayingID)
|
a.tracklist.SetNowPlaying(a.nowPlayingID)
|
||||||
a.tracklist.IncrementPlayCount(lastScrobbledIfAny)
|
a.tracklist.IncrementPlayCount(sharedutil.TrackIDOrEmptyStr(lastScrobbledIfAny))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *AlbumPage) Reload() {
|
func (a *AlbumPage) Reload() {
|
||||||
@@ -122,11 +116,6 @@ func (a *AlbumPage) SelectAll() {
|
|||||||
a.tracklist.SelectAll()
|
a.tracklist.SelectAll()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *AlbumPage) onPlayTrackAt(tracknum int) {
|
|
||||||
a.pm.LoadTracks(a.tracklist.Tracks, false, false)
|
|
||||||
a.pm.PlayTrackAt(tracknum)
|
|
||||||
}
|
|
||||||
|
|
||||||
// should be called asynchronously
|
// should be called asynchronously
|
||||||
func (a *AlbumPage) load() {
|
func (a *AlbumPage) load() {
|
||||||
album, err := a.lm.GetAlbum(a.albumID)
|
album, err := a.lm.GetAlbum(a.albumID)
|
||||||
@@ -185,7 +174,7 @@ func NewAlbumPageHeader(page *AlbumPage) *AlbumPageHeader {
|
|||||||
}
|
}
|
||||||
a.miscLabel = widget.NewLabel("")
|
a.miscLabel = widget.NewLabel("")
|
||||||
playButton := widget.NewButtonWithIcon("Play", theme.MediaPlayIcon(), func() {
|
playButton := widget.NewButtonWithIcon("Play", theme.MediaPlayIcon(), func() {
|
||||||
page.onPlayTrackAt(0)
|
go page.pm.PlayAlbum(page.albumID, 0)
|
||||||
})
|
})
|
||||||
shuffleBtn := widget.NewButtonWithIcon(" Shuffle", res.ResShuffleInvertSvg, func() {
|
shuffleBtn := widget.NewButtonWithIcon(" Shuffle", res.ResShuffleInvertSvg, func() {
|
||||||
page.pm.LoadTracks(page.tracklist.Tracks, false, true)
|
page.pm.LoadTracks(page.tracklist.Tracks, false, true)
|
||||||
|
|||||||
@@ -124,6 +124,7 @@ type ArtistPageHeader struct {
|
|||||||
titleDisp *widget.RichText
|
titleDisp *widget.RichText
|
||||||
biographyDisp *widget.RichText
|
biographyDisp *widget.RichText
|
||||||
similarArtists *fyne.Container
|
similarArtists *fyne.Container
|
||||||
|
favoriteBtn *widgets.FavoriteButton
|
||||||
container *fyne.Container
|
container *fyne.Container
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -139,6 +140,7 @@ func NewArtistPageHeader(page *ArtistPage, nav func(Route)) *ArtistPageHeader {
|
|||||||
SizeName: theme.SizeNameHeadingText,
|
SizeName: theme.SizeNameHeadingText,
|
||||||
}
|
}
|
||||||
a.artistImage = widgets.NewImagePlaceholder(res.ResPeopleInvertPng, 225)
|
a.artistImage = widgets.NewImagePlaceholder(res.ResPeopleInvertPng, 225)
|
||||||
|
a.favoriteBtn = widgets.NewFavoriteButton(func() { go a.toggleFavorited() })
|
||||||
a.biographyDisp.Wrapping = fyne.TextWrapWord
|
a.biographyDisp.Wrapping = fyne.TextWrapWord
|
||||||
a.ExtendBaseWidget(a)
|
a.ExtendBaseWidget(a)
|
||||||
a.createContainer()
|
a.createContainer()
|
||||||
@@ -149,6 +151,7 @@ func (a *ArtistPageHeader) Update(artist *subsonic.ArtistID3) {
|
|||||||
if artist == nil {
|
if artist == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
a.favoriteBtn.IsFavorited = !artist.Starred.IsZero()
|
||||||
a.artistID = artist.ID
|
a.artistID = artist.ID
|
||||||
a.titleDisp.Segments[0].(*widget.TextSegment).Text = artist.Name
|
a.titleDisp.Segments[0].(*widget.TextSegment).Text = artist.Name
|
||||||
a.titleDisp.Refresh()
|
a.titleDisp.Refresh()
|
||||||
@@ -197,9 +200,20 @@ func (a *ArtistPageHeader) UpdateInfo(info *subsonic.ArtistInfo2) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *ArtistPageHeader) toggleFavorited() {
|
||||||
|
if a.favoriteBtn.IsFavorited {
|
||||||
|
a.artistPage.sm.Server.Star(subsonic.StarParameters{ArtistIDs: []string{a.artistID}})
|
||||||
|
} else {
|
||||||
|
a.artistPage.sm.Server.Unstar(subsonic.StarParameters{ArtistIDs: []string{a.artistID}})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (a *ArtistPageHeader) createContainer() {
|
func (a *ArtistPageHeader) createContainer() {
|
||||||
a.container = container.NewBorder(nil, nil, a.artistImage, nil,
|
a.container = container.NewBorder(nil, nil, a.artistImage, nil,
|
||||||
container.NewBorder(a.titleDisp, nil, nil, nil, container.NewVBox(a.biographyDisp, a.similarArtists)))
|
container.NewVBox(
|
||||||
|
container.New(&layouts.VboxCustomPadding{ExtraPad: -10},
|
||||||
|
a.titleDisp, a.biographyDisp, a.similarArtists),
|
||||||
|
container.NewHBox(widgets.NewHSpace(2), a.favoriteBtn)))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *ArtistPageHeader) CreateRenderer() fyne.WidgetRenderer {
|
func (a *ArtistPageHeader) CreateRenderer() fyne.WidgetRenderer {
|
||||||
|
|||||||
+221
-49
@@ -1,7 +1,11 @@
|
|||||||
package browsing
|
package browsing
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"log"
|
||||||
"supersonic/backend"
|
"supersonic/backend"
|
||||||
|
"supersonic/res"
|
||||||
|
"supersonic/ui/controller"
|
||||||
|
"supersonic/ui/layouts"
|
||||||
"supersonic/ui/widgets"
|
"supersonic/ui/widgets"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -16,45 +20,71 @@ import (
|
|||||||
type FavoritesPage struct {
|
type FavoritesPage struct {
|
||||||
widget.BaseWidget
|
widget.BaseWidget
|
||||||
|
|
||||||
pm *backend.PlaybackManager
|
cfg *backend.FavoritesPageConfig
|
||||||
im *backend.ImageManager
|
contr controller.Controller
|
||||||
sm *backend.ServerManager
|
pm *backend.PlaybackManager
|
||||||
lm *backend.LibraryManager
|
im *backend.ImageManager
|
||||||
nav func(Route)
|
sm *backend.ServerManager
|
||||||
grid *widgets.AlbumGrid
|
lm *backend.LibraryManager
|
||||||
searchGrid *widgets.AlbumGrid
|
nav func(Route)
|
||||||
searcher *widgets.Searcher
|
|
||||||
searchText string
|
searchText string
|
||||||
titleDisp *widget.RichText
|
nowPlayingID string
|
||||||
container *fyne.Container
|
pendingViewSwitch bool
|
||||||
|
|
||||||
|
grid *widgets.AlbumGrid
|
||||||
|
searchGrid *widgets.AlbumGrid
|
||||||
|
artistListCtr *fyne.Container
|
||||||
|
tracklistCtr *fyne.Container
|
||||||
|
searcher *widgets.Searcher
|
||||||
|
titleDisp *widget.RichText
|
||||||
|
toggleBtns *widgets.ToggleButtonGroup
|
||||||
|
container *fyne.Container
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewFavoritesPage(sm *backend.ServerManager, pm *backend.PlaybackManager, lm *backend.LibraryManager, im *backend.ImageManager, nav func(Route)) *FavoritesPage {
|
func NewFavoritesPage(cfg *backend.FavoritesPageConfig, contr controller.Controller, sm *backend.ServerManager, pm *backend.PlaybackManager, lm *backend.LibraryManager, im *backend.ImageManager, nav func(Route)) *FavoritesPage {
|
||||||
a := &FavoritesPage{
|
a := &FavoritesPage{
|
||||||
pm: pm,
|
cfg: cfg,
|
||||||
lm: lm,
|
contr: contr,
|
||||||
sm: sm,
|
pm: pm,
|
||||||
im: im,
|
lm: lm,
|
||||||
nav: nav,
|
sm: sm,
|
||||||
|
im: im,
|
||||||
|
nav: nav,
|
||||||
}
|
}
|
||||||
a.ExtendBaseWidget(a)
|
a.ExtendBaseWidget(a)
|
||||||
a.createTitle()
|
a.createHeader(0, "")
|
||||||
iter := lm.StarredIter()
|
a.grid = widgets.NewAlbumGrid(a.lm.StarredIter(), a.im, false)
|
||||||
a.grid = widgets.NewAlbumGrid(iter, im, false)
|
a.connectGridActions()
|
||||||
a.grid.OnPlayAlbum = a.onPlayAlbum
|
|
||||||
a.grid.OnShowAlbumPage = a.onShowAlbumPage
|
|
||||||
a.grid.OnShowArtistPage = a.onShowArtistPage
|
|
||||||
a.searcher = widgets.NewSearcher()
|
|
||||||
a.searcher.OnSearched = a.OnSearched
|
|
||||||
a.createContainer(false)
|
a.createContainer(false)
|
||||||
|
if cfg.InitialView == "Artists" {
|
||||||
|
a.toggleBtns.SetActivatedButton(1)
|
||||||
|
a.onShowFavoriteArtists()
|
||||||
|
} else if cfg.InitialView == "Songs" {
|
||||||
|
a.toggleBtns.SetActivatedButton(2)
|
||||||
|
a.onShowFavoriteSongs()
|
||||||
|
}
|
||||||
return a
|
return a
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *FavoritesPage) createTitle() {
|
func (a *FavoritesPage) createHeader(activeBtnIdx int, searchText string) {
|
||||||
a.titleDisp = widget.NewRichTextWithText("Favorites")
|
a.titleDisp = widget.NewRichTextWithText("Favorites")
|
||||||
a.titleDisp.Segments[0].(*widget.TextSegment).Style = widget.RichTextStyle{
|
a.titleDisp.Segments[0].(*widget.TextSegment).Style = widget.RichTextStyle{
|
||||||
SizeName: theme.SizeNameHeadingText,
|
SizeName: theme.SizeNameHeadingText,
|
||||||
}
|
}
|
||||||
|
a.toggleBtns = widgets.NewToggleButtonGroup(activeBtnIdx,
|
||||||
|
widget.NewButtonWithIcon("", res.ResDiscInvertPng, a.onShowFavoriteAlbums),
|
||||||
|
widget.NewButtonWithIcon("", res.ResPeopleInvertPng, a.onShowFavoriteArtists),
|
||||||
|
widget.NewButtonWithIcon("", res.ResMusicnotesInvertPng, a.onShowFavoriteSongs))
|
||||||
|
a.searcher = widgets.NewSearcher()
|
||||||
|
a.searcher.OnSearched = a.OnSearched
|
||||||
|
a.searcher.Entry.Text = searchText
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *FavoritesPage) connectGridActions() {
|
||||||
|
a.grid.OnPlayAlbum = a.onPlayAlbum
|
||||||
|
a.grid.OnShowAlbumPage = a.onShowAlbumPage
|
||||||
|
a.grid.OnShowArtistPage = a.onShowArtistPage
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *FavoritesPage) createContainer(searchGrid bool) {
|
func (a *FavoritesPage) createContainer(searchGrid bool) {
|
||||||
@@ -64,31 +94,34 @@ func (a *FavoritesPage) createContainer(searchGrid bool) {
|
|||||||
gr = a.searchGrid
|
gr = a.searchGrid
|
||||||
}
|
}
|
||||||
a.container = container.NewBorder(
|
a.container = container.NewBorder(
|
||||||
container.NewHBox(widgets.NewHSpace(9), a.titleDisp, layout.NewSpacer(), searchVbox, widgets.NewHSpace(15)),
|
container.NewHBox(widgets.NewHSpace(9), a.titleDisp, container.NewCenter(a.toggleBtns), layout.NewSpacer(), searchVbox, widgets.NewHSpace(15)),
|
||||||
nil, nil, nil, gr)
|
nil, nil, nil, gr)
|
||||||
}
|
}
|
||||||
|
|
||||||
func restoreFavoritesPage(saved *savedFavoritesPage) *FavoritesPage {
|
func restoreFavoritesPage(saved *savedFavoritesPage) *FavoritesPage {
|
||||||
a := &FavoritesPage{
|
a := &FavoritesPage{
|
||||||
pm: saved.pm,
|
cfg: saved.cfg,
|
||||||
lm: saved.lm,
|
contr: saved.contr,
|
||||||
sm: saved.sm,
|
pm: saved.pm,
|
||||||
im: saved.im,
|
lm: saved.lm,
|
||||||
nav: saved.nav,
|
sm: saved.sm,
|
||||||
|
im: saved.im,
|
||||||
|
nav: saved.nav,
|
||||||
}
|
}
|
||||||
a.ExtendBaseWidget(a)
|
a.ExtendBaseWidget(a)
|
||||||
a.createTitle()
|
a.createHeader(saved.activeToggleBtn, saved.searchText)
|
||||||
a.grid = widgets.NewAlbumGridFromState(saved.gridState)
|
a.grid = widgets.NewAlbumGridFromState(saved.gridState)
|
||||||
a.grid.OnPlayAlbum = a.onPlayAlbum
|
a.connectGridActions()
|
||||||
a.grid.OnShowAlbumPage = a.onShowAlbumPage
|
|
||||||
a.grid.OnShowArtistPage = a.onShowArtistPage
|
|
||||||
a.searcher = widgets.NewSearcher()
|
|
||||||
a.searcher.OnSearched = a.OnSearched
|
|
||||||
a.searcher.Entry.Text = saved.searchText
|
|
||||||
if saved.searchText != "" {
|
if saved.searchText != "" {
|
||||||
a.searchGrid = widgets.NewAlbumGridFromState(saved.searchGridState)
|
a.searchGrid = widgets.NewAlbumGridFromState(saved.searchGridState)
|
||||||
}
|
}
|
||||||
a.createContainer(saved.searchText != "")
|
a.createContainer(saved.searchText != "")
|
||||||
|
if saved.activeToggleBtn == 1 {
|
||||||
|
a.onShowFavoriteArtists()
|
||||||
|
} else if saved.activeToggleBtn == 2 {
|
||||||
|
a.onShowFavoriteSongs()
|
||||||
|
}
|
||||||
|
|
||||||
return a
|
return a
|
||||||
}
|
}
|
||||||
@@ -98,22 +131,58 @@ func (a *FavoritesPage) Route() Route {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a *FavoritesPage) Reload() {
|
func (a *FavoritesPage) Reload() {
|
||||||
|
// reload favorite albums view
|
||||||
if a.searchText != "" {
|
if a.searchText != "" {
|
||||||
a.doSearch(a.searchText)
|
a.doSearchAlbums(a.searchText)
|
||||||
} else {
|
} else {
|
||||||
a.grid.Reset(a.lm.StarredIter())
|
a.grid.Reset(a.lm.StarredIter())
|
||||||
}
|
}
|
||||||
|
if a.tracklistCtr != nil || a.artistListCtr != nil {
|
||||||
|
go func() {
|
||||||
|
// re-fetch starred info from server
|
||||||
|
starred, err := a.sm.Server.GetStarred2(nil)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("error getting starred items: %s", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if a.tracklistCtr != nil {
|
||||||
|
// refresh favorite songs view
|
||||||
|
tr := a.tracklistCtr.Objects[0].(*widgets.Tracklist)
|
||||||
|
tr.Tracks = starred.Song
|
||||||
|
if a.toggleBtns.ActivatedButtonIndex() == 2 {
|
||||||
|
// favorite songs view is visible
|
||||||
|
tr.Refresh()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if a.artistListCtr != nil {
|
||||||
|
// refresh favorite artists view
|
||||||
|
al := a.artistListCtr.Objects[0].(*widgets.ArtistGenrePlaylist)
|
||||||
|
al.Items = buildArtistListModel(starred.Artist)
|
||||||
|
if a.toggleBtns.ActivatedButtonIndex() == 1 {
|
||||||
|
// favorite artists view is visible
|
||||||
|
al.Refresh()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *FavoritesPage) Save() SavedPage {
|
func (a *FavoritesPage) Save() SavedPage {
|
||||||
|
if a.tracklistCtr != nil {
|
||||||
|
tl := a.tracklistCtr.Objects[0].(*widgets.Tracklist)
|
||||||
|
a.cfg.TracklistColumns = tl.VisibleColumns()
|
||||||
|
}
|
||||||
sf := &savedFavoritesPage{
|
sf := &savedFavoritesPage{
|
||||||
pm: a.pm,
|
cfg: a.cfg,
|
||||||
sm: a.sm,
|
contr: a.contr,
|
||||||
im: a.im,
|
pm: a.pm,
|
||||||
lm: a.lm,
|
sm: a.sm,
|
||||||
nav: a.nav,
|
im: a.im,
|
||||||
searchText: a.searchText,
|
lm: a.lm,
|
||||||
gridState: a.grid.SaveToState(),
|
nav: a.nav,
|
||||||
|
searchText: a.searchText,
|
||||||
|
gridState: a.grid.SaveToState(),
|
||||||
|
activeToggleBtn: a.toggleBtns.ActivatedButtonIndex(),
|
||||||
}
|
}
|
||||||
if a.searchGrid != nil {
|
if a.searchGrid != nil {
|
||||||
sf.searchGridState = a.searchGrid.SaveToState()
|
sf.searchGridState = a.searchGrid.SaveToState()
|
||||||
@@ -137,10 +206,22 @@ func (a *FavoritesPage) OnSearched(query string) {
|
|||||||
a.Refresh()
|
a.Refresh()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
a.doSearch(query)
|
a.doSearchAlbums(query)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *FavoritesPage) doSearch(query string) {
|
var _ CanShowNowPlaying = (*FavoritesPage)(nil)
|
||||||
|
|
||||||
|
func (a *FavoritesPage) OnSongChange(song *subsonic.Child, _ *subsonic.Child) {
|
||||||
|
a.nowPlayingID = ""
|
||||||
|
if song != nil {
|
||||||
|
a.nowPlayingID = song.ID
|
||||||
|
}
|
||||||
|
if a.tracklistCtr != nil {
|
||||||
|
a.tracklistCtr.Objects[0].(*widgets.Tracklist).SetNowPlaying(a.nowPlayingID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *FavoritesPage) doSearchAlbums(query string) {
|
||||||
iter := a.lm.SearchIterWithFilter(query, func(al *subsonic.AlbumID3) bool {
|
iter := a.lm.SearchIterWithFilter(query, func(al *subsonic.AlbumID3) bool {
|
||||||
return al.Starred.After(time.Time{})
|
return al.Starred.After(time.Time{})
|
||||||
})
|
})
|
||||||
@@ -156,6 +237,94 @@ func (a *FavoritesPage) doSearch(query string) {
|
|||||||
a.Refresh()
|
a.Refresh()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *FavoritesPage) onShowFavoriteAlbums() {
|
||||||
|
a.cfg.InitialView = "Albums" // save setting
|
||||||
|
a.searcher.Entry.Show()
|
||||||
|
if a.searchText == "" {
|
||||||
|
a.container.Objects[0] = a.grid
|
||||||
|
} else {
|
||||||
|
a.container.Objects[0] = a.searchGrid
|
||||||
|
}
|
||||||
|
a.Refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *FavoritesPage) onShowFavoriteArtists() {
|
||||||
|
a.cfg.InitialView = "Artists" // save setting
|
||||||
|
a.searcher.Entry.Hide() // disable search on artists for now
|
||||||
|
if a.artistListCtr == nil {
|
||||||
|
if a.pendingViewSwitch {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
a.pendingViewSwitch = true
|
||||||
|
go func() {
|
||||||
|
s, err := a.sm.Server.GetStarred2(nil)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("error getting starred items: %s", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
model := buildArtistListModel(s.Artist)
|
||||||
|
artistList := widgets.NewArtistGenrePlaylist(model)
|
||||||
|
artistList.ShowAlbumCount = true
|
||||||
|
artistList.OnNavTo = func(artistID string) {
|
||||||
|
a.nav(ArtistRoute(artistID))
|
||||||
|
}
|
||||||
|
a.artistListCtr = container.New(
|
||||||
|
&layouts.MaxPadLayout{PadLeft: 15, PadRight: 15, PadTop: 5, PadBottom: 15},
|
||||||
|
artistList)
|
||||||
|
a.container.Objects[0] = a.artistListCtr
|
||||||
|
a.Refresh()
|
||||||
|
a.pendingViewSwitch = false
|
||||||
|
}()
|
||||||
|
} else {
|
||||||
|
a.container.Objects[0] = a.artistListCtr
|
||||||
|
a.Refresh()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildArtistListModel(artists []*subsonic.ArtistID3) []widgets.ArtistGenrePlaylistItemModel {
|
||||||
|
model := make([]widgets.ArtistGenrePlaylistItemModel, 0)
|
||||||
|
for _, ar := range artists {
|
||||||
|
model = append(model, widgets.ArtistGenrePlaylistItemModel{
|
||||||
|
ID: ar.ID,
|
||||||
|
Name: ar.Name,
|
||||||
|
AlbumCount: ar.AlbumCount,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return model
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *FavoritesPage) onShowFavoriteSongs() {
|
||||||
|
a.cfg.InitialView = "Songs" // save setting
|
||||||
|
a.searcher.Entry.Hide() // disable search on songs for now
|
||||||
|
if a.tracklistCtr == nil {
|
||||||
|
if a.pendingViewSwitch {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
a.pendingViewSwitch = true
|
||||||
|
go func() {
|
||||||
|
s, err := a.sm.Server.GetStarred2(nil)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("error getting starred items: %s", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
tracklist := widgets.NewTracklist(s.Song)
|
||||||
|
tracklist.AutoNumber = true
|
||||||
|
tracklist.SetVisibleColumns(a.cfg.TracklistColumns)
|
||||||
|
tracklist.SetNowPlaying(a.nowPlayingID)
|
||||||
|
a.contr.ConnectTracklistActions(tracklist)
|
||||||
|
a.tracklistCtr = container.New(
|
||||||
|
&layouts.MaxPadLayout{PadLeft: 15, PadRight: 15, PadTop: 5, PadBottom: 15},
|
||||||
|
tracklist)
|
||||||
|
a.container.Objects[0] = a.tracklistCtr
|
||||||
|
a.Refresh()
|
||||||
|
a.pendingViewSwitch = false
|
||||||
|
}()
|
||||||
|
} else {
|
||||||
|
a.container.Objects[0] = a.tracklistCtr
|
||||||
|
a.Refresh()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (a *FavoritesPage) onPlayAlbum(albumID string) {
|
func (a *FavoritesPage) onPlayAlbum(albumID string) {
|
||||||
go a.pm.PlayAlbum(albumID, 0)
|
go a.pm.PlayAlbum(albumID, 0)
|
||||||
}
|
}
|
||||||
@@ -174,6 +343,8 @@ func (a *FavoritesPage) CreateRenderer() fyne.WidgetRenderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type savedFavoritesPage struct {
|
type savedFavoritesPage struct {
|
||||||
|
cfg *backend.FavoritesPageConfig
|
||||||
|
contr controller.Controller
|
||||||
pm *backend.PlaybackManager
|
pm *backend.PlaybackManager
|
||||||
sm *backend.ServerManager
|
sm *backend.ServerManager
|
||||||
im *backend.ImageManager
|
im *backend.ImageManager
|
||||||
@@ -181,6 +352,7 @@ type savedFavoritesPage struct {
|
|||||||
gridState widgets.AlbumGridState
|
gridState widgets.AlbumGridState
|
||||||
searchGridState widgets.AlbumGridState
|
searchGridState widgets.AlbumGridState
|
||||||
searchText string
|
searchText string
|
||||||
|
activeToggleBtn int
|
||||||
nav func(Route)
|
nav func(Route)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package browsing
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"supersonic/backend"
|
"supersonic/backend"
|
||||||
|
"supersonic/sharedutil"
|
||||||
"supersonic/ui/controller"
|
"supersonic/ui/controller"
|
||||||
"supersonic/ui/layouts"
|
"supersonic/ui/layouts"
|
||||||
"supersonic/ui/widgets"
|
"supersonic/ui/widgets"
|
||||||
@@ -44,8 +45,9 @@ func NewNowPlayingPage(
|
|||||||
a.tracklist.SetVisibleColumns(conf.TracklistColumns)
|
a.tracklist.SetVisibleColumns(conf.TracklistColumns)
|
||||||
a.tracklist.AutoNumber = true
|
a.tracklist.AutoNumber = true
|
||||||
a.tracklist.DisablePlaybackMenu = true
|
a.tracklist.DisablePlaybackMenu = true
|
||||||
|
contr.ConnectTracklistActions(a.tracklist)
|
||||||
|
// override the default OnPlayTrackAt handler b/c we don't need to re-load the tracks into the queue
|
||||||
a.tracklist.OnPlayTrackAt = a.onPlayTrackAt
|
a.tracklist.OnPlayTrackAt = a.onPlayTrackAt
|
||||||
a.tracklist.OnAddToPlaylist = a.contr.DoAddTracksToPlaylistWorkflow
|
|
||||||
a.tracklist.AuxiliaryMenuItems = []*fyne.MenuItem{
|
a.tracklist.AuxiliaryMenuItems = []*fyne.MenuItem{
|
||||||
fyne.NewMenuItem("Remove from queue", a.onRemoveSelectedFromQueue),
|
fyne.NewMenuItem("Remove from queue", a.onRemoveSelectedFromQueue),
|
||||||
}
|
}
|
||||||
@@ -86,7 +88,7 @@ func (a *NowPlayingPage) OnSongChange(song *subsonic.Child, lastScrobbledIfAny *
|
|||||||
a.nowPlayingID = song.ID
|
a.nowPlayingID = song.ID
|
||||||
}
|
}
|
||||||
a.tracklist.SetNowPlaying(a.nowPlayingID)
|
a.tracklist.SetNowPlaying(a.nowPlayingID)
|
||||||
a.tracklist.IncrementPlayCount(lastScrobbledIfAny)
|
a.tracklist.IncrementPlayCount(sharedutil.TrackIDOrEmptyStr(lastScrobbledIfAny))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *NowPlayingPage) Reload() {
|
func (a *NowPlayingPage) Reload() {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"supersonic/backend"
|
"supersonic/backend"
|
||||||
"supersonic/res"
|
"supersonic/res"
|
||||||
|
"supersonic/sharedutil"
|
||||||
"supersonic/ui/controller"
|
"supersonic/ui/controller"
|
||||||
"supersonic/ui/layouts"
|
"supersonic/ui/layouts"
|
||||||
"supersonic/ui/util"
|
"supersonic/ui/util"
|
||||||
@@ -57,13 +58,7 @@ func NewPlaylistPage(
|
|||||||
fyne.NewMenuItem("Remove from playlist", a.onRemoveSelectedFromPlaylist),
|
fyne.NewMenuItem("Remove from playlist", a.onRemoveSelectedFromPlaylist),
|
||||||
}
|
}
|
||||||
// connect tracklist actions
|
// connect tracklist actions
|
||||||
a.tracklist.OnPlayTrackAt = a.onPlayTrackAt
|
a.contr.ConnectTracklistActions(a.tracklist)
|
||||||
a.tracklist.OnAddToQueue = func(tracks []*subsonic.Child) { a.pm.LoadTracks(tracks, true, false) }
|
|
||||||
a.tracklist.OnPlaySelection = func(tracks []*subsonic.Child) {
|
|
||||||
a.pm.LoadTracks(tracks, false, false)
|
|
||||||
a.pm.PlayFromBeginning()
|
|
||||||
}
|
|
||||||
a.tracklist.OnAddToPlaylist = a.contr.DoAddTracksToPlaylistWorkflow
|
|
||||||
|
|
||||||
a.container = container.NewBorder(
|
a.container = container.NewBorder(
|
||||||
container.New(&layouts.MaxPadLayout{PadLeft: 15, PadRight: 15, PadTop: 15, PadBottom: 10}, a.header),
|
container.New(&layouts.MaxPadLayout{PadLeft: 15, PadRight: 15, PadTop: 15, PadBottom: 10}, a.header),
|
||||||
@@ -93,7 +88,7 @@ func (a *PlaylistPage) OnSongChange(song *subsonic.Child, lastScrobbledIfAny *su
|
|||||||
a.nowPlayingID = song.ID
|
a.nowPlayingID = song.ID
|
||||||
}
|
}
|
||||||
a.tracklist.SetNowPlaying(a.nowPlayingID)
|
a.tracklist.SetNowPlaying(a.nowPlayingID)
|
||||||
a.tracklist.IncrementPlayCount(lastScrobbledIfAny)
|
a.tracklist.IncrementPlayCount(sharedutil.TrackIDOrEmptyStr(lastScrobbledIfAny))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *PlaylistPage) Reload() {
|
func (a *PlaylistPage) Reload() {
|
||||||
@@ -108,11 +103,6 @@ func (a *PlaylistPage) SelectAll() {
|
|||||||
a.tracklist.SelectAll()
|
a.tracklist.SelectAll()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *PlaylistPage) onPlayTrackAt(tracknum int) {
|
|
||||||
a.pm.LoadTracks(a.tracklist.Tracks, false, false)
|
|
||||||
a.pm.PlayTrackAt(tracknum)
|
|
||||||
}
|
|
||||||
|
|
||||||
// should be called asynchronously
|
// should be called asynchronously
|
||||||
func (a *PlaylistPage) load() {
|
func (a *PlaylistPage) load() {
|
||||||
playlist, err := a.sm.Server.GetPlaylist(a.playlistID)
|
playlist, err := a.sm.Server.GetPlaylist(a.playlistID)
|
||||||
@@ -163,7 +153,8 @@ func NewPlaylistPageHeader(page *PlaylistPage) *PlaylistPageHeader {
|
|||||||
a.createdAtLabel = widget.NewLabel("")
|
a.createdAtLabel = widget.NewLabel("")
|
||||||
a.trackTimeLabel = widget.NewLabel("")
|
a.trackTimeLabel = widget.NewLabel("")
|
||||||
playButton := widget.NewButtonWithIcon("Play", theme.MediaPlayIcon(), func() {
|
playButton := widget.NewButtonWithIcon("Play", theme.MediaPlayIcon(), func() {
|
||||||
page.onPlayTrackAt(0)
|
page.pm.LoadTracks(page.tracklist.Tracks, false, false)
|
||||||
|
page.pm.PlayFromBeginning()
|
||||||
})
|
})
|
||||||
// TODO: find way to pad shuffle svg rather than using a space in the label string
|
// TODO: find way to pad shuffle svg rather than using a space in the label string
|
||||||
shuffleBtn := widget.NewButtonWithIcon(" Shuffle", res.ResShuffleInvertSvg, func() {
|
shuffleBtn := widget.NewButtonWithIcon(" Shuffle", res.ResShuffleInvertSvg, func() {
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ func (r Router) CreatePage(rte Route) Page {
|
|||||||
case Artists:
|
case Artists:
|
||||||
return NewArtistsGenresPage(false, r.App.ServerManager, r.OpenRoute)
|
return NewArtistsGenresPage(false, r.App.ServerManager, r.OpenRoute)
|
||||||
case Favorites:
|
case Favorites:
|
||||||
return NewFavoritesPage(r.App.ServerManager, r.App.PlaybackManager, r.App.LibraryManager, r.App.ImageManager, r.OpenRoute)
|
return NewFavoritesPage(&r.App.Config.FavoritesPage, r.Controller, r.App.ServerManager, r.App.PlaybackManager, r.App.LibraryManager, r.App.ImageManager, r.OpenRoute)
|
||||||
case Genre:
|
case Genre:
|
||||||
return NewGenrePage(rte.Arg, r.App.PlaybackManager, r.App.LibraryManager, r.App.ImageManager, r.OpenRoute)
|
return NewGenrePage(rte.Arg, r.App.PlaybackManager, r.App.LibraryManager, r.App.ImageManager, r.OpenRoute)
|
||||||
case Genres:
|
case Genres:
|
||||||
|
|||||||
@@ -6,12 +6,14 @@ import (
|
|||||||
"supersonic/backend"
|
"supersonic/backend"
|
||||||
"supersonic/ui/dialogs"
|
"supersonic/ui/dialogs"
|
||||||
"supersonic/ui/util"
|
"supersonic/ui/util"
|
||||||
|
"supersonic/ui/widgets"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"fyne.io/fyne/v2"
|
"fyne.io/fyne/v2"
|
||||||
"fyne.io/fyne/v2/canvas"
|
"fyne.io/fyne/v2/canvas"
|
||||||
"fyne.io/fyne/v2/dialog"
|
"fyne.io/fyne/v2/dialog"
|
||||||
"fyne.io/fyne/v2/widget"
|
"fyne.io/fyne/v2/widget"
|
||||||
|
"github.com/dweymouth/go-subsonic/subsonic"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Controller struct {
|
type Controller struct {
|
||||||
@@ -40,6 +42,32 @@ func (m Controller) ShowPopUpImage(img image.Image) {
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m Controller) ConnectTracklistActions(tracklist *widgets.Tracklist) {
|
||||||
|
tracklist.OnAddToPlaylist = m.DoAddTracksToPlaylistWorkflow
|
||||||
|
tracklist.OnAddToQueue = func(tracks []*subsonic.Child) {
|
||||||
|
m.App.PlaybackManager.LoadTracks(tracks, true, false)
|
||||||
|
}
|
||||||
|
tracklist.OnPlayTrackAt = func(idx int) {
|
||||||
|
m.App.PlaybackManager.LoadTracks(tracklist.Tracks, false, false)
|
||||||
|
m.App.PlaybackManager.PlayTrackAt(idx)
|
||||||
|
}
|
||||||
|
tracklist.OnPlaySelection = func(tracks []*subsonic.Child) {
|
||||||
|
m.App.PlaybackManager.LoadTracks(tracks, false, false)
|
||||||
|
m.App.PlaybackManager.PlayFromBeginning()
|
||||||
|
}
|
||||||
|
tracklist.OnSetFavorite = func(trackIDs []string, fav bool) {
|
||||||
|
s := m.App.ServerManager.Server
|
||||||
|
if fav {
|
||||||
|
go s.Star(subsonic.StarParameters{SongIDs: trackIDs})
|
||||||
|
} else {
|
||||||
|
go s.Unstar(subsonic.StarParameters{SongIDs: trackIDs})
|
||||||
|
}
|
||||||
|
for _, id := range trackIDs {
|
||||||
|
m.App.PlaybackManager.OnTrackFavoriteStatusChanged(id, fav)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (m Controller) PromptForFirstServer() {
|
func (m Controller) PromptForFirstServer() {
|
||||||
d := dialogs.NewAddEditServerDialog("Connect to Server", nil)
|
d := dialogs.NewAddEditServerDialog("Connect to Server", nil)
|
||||||
pop := widget.NewModalPopUp(d, m.MainWindow.Canvas())
|
pop := widget.NewModalPopUp(d, m.MainWindow.Canvas())
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
package layouts
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fyne.io/fyne/v2"
|
||||||
|
"fyne.io/fyne/v2/theme"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ fyne.Layout = (*VboxCustomPadding)(nil)
|
||||||
|
|
||||||
|
type HboxCustomPadding struct {
|
||||||
|
ExtraPad float32
|
||||||
|
DisableThemePad bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *HboxCustomPadding) MinSize(objects []fyne.CanvasObject) fyne.Size {
|
||||||
|
minSize := fyne.NewSize(0, 0)
|
||||||
|
for _, child := range objects {
|
||||||
|
if !child.Visible() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
minSize.Height = fyne.Max(child.MinSize().Height, minSize.Height)
|
||||||
|
minSize.Width += child.MinSize().Width
|
||||||
|
}
|
||||||
|
minSize.Width += (v.themePad() + v.ExtraPad) * float32(len(objects)-1)
|
||||||
|
return minSize
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *HboxCustomPadding) Layout(objects []fyne.CanvasObject, size fyne.Size) {
|
||||||
|
total := float32(0)
|
||||||
|
for _, child := range objects {
|
||||||
|
if !child.Visible() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
total += child.MinSize().Width
|
||||||
|
}
|
||||||
|
|
||||||
|
x, y := float32(0), float32(0)
|
||||||
|
|
||||||
|
extra := float32(0)
|
||||||
|
for _, child := range objects {
|
||||||
|
if !child.Visible() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
width := child.MinSize().Width
|
||||||
|
child.Move(fyne.NewPos(x+extra, y))
|
||||||
|
x += width
|
||||||
|
child.Resize(fyne.NewSize(width, size.Height))
|
||||||
|
extra += (v.themePad() + v.ExtraPad)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *HboxCustomPadding) themePad() float32 {
|
||||||
|
if v.DisableThemePad {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return theme.Padding()
|
||||||
|
}
|
||||||
@@ -3,7 +3,6 @@ package widgets
|
|||||||
import (
|
import (
|
||||||
"fyne.io/fyne/v2"
|
"fyne.io/fyne/v2"
|
||||||
"fyne.io/fyne/v2/container"
|
"fyne.io/fyne/v2/container"
|
||||||
"fyne.io/fyne/v2/driver/desktop"
|
|
||||||
"fyne.io/fyne/v2/layout"
|
"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"
|
||||||
@@ -56,37 +55,10 @@ func (v *volumeSlider) MinSize() fyne.Size {
|
|||||||
return fyne.NewSize(v.Width, h)
|
return fyne.NewSize(v.Width, h)
|
||||||
}
|
}
|
||||||
|
|
||||||
type tappableIcon struct {
|
|
||||||
widget.Icon
|
|
||||||
|
|
||||||
OnTapped func()
|
|
||||||
}
|
|
||||||
|
|
||||||
func newTappableIcon(res fyne.Resource) *tappableIcon {
|
|
||||||
icon := &tappableIcon{}
|
|
||||||
icon.ExtendBaseWidget(icon)
|
|
||||||
icon.SetResource(res)
|
|
||||||
|
|
||||||
return icon
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *tappableIcon) Tapped(_ *fyne.PointEvent) {
|
|
||||||
if t.OnTapped != nil {
|
|
||||||
t.OnTapped()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *tappableIcon) TappedSecondary(_ *fyne.PointEvent) {
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *tappableIcon) Cursor() desktop.Cursor {
|
|
||||||
return desktop.PointerCursor
|
|
||||||
}
|
|
||||||
|
|
||||||
type VolumeControl struct {
|
type VolumeControl struct {
|
||||||
widget.BaseWidget
|
widget.BaseWidget
|
||||||
|
|
||||||
icon *tappableIcon
|
icon *TappableIcon
|
||||||
slider *volumeSlider
|
slider *volumeSlider
|
||||||
|
|
||||||
OnVolumeChanged func(int)
|
OnVolumeChanged func(int)
|
||||||
@@ -100,7 +72,7 @@ type VolumeControl struct {
|
|||||||
func NewVolumeControl(initialVol int) *VolumeControl {
|
func NewVolumeControl(initialVol int) *VolumeControl {
|
||||||
v := &VolumeControl{}
|
v := &VolumeControl{}
|
||||||
v.ExtendBaseWidget(v)
|
v.ExtendBaseWidget(v)
|
||||||
v.icon = newTappableIcon(theme.VolumeUpIcon())
|
v.icon = NewTappbaleIcon(theme.VolumeUpIcon())
|
||||||
v.icon.OnTapped = v.toggleMute
|
v.icon.OnTapped = v.toggleMute
|
||||||
v.slider = NewVolumeSlider(100)
|
v.slider = NewVolumeSlider(100)
|
||||||
v.lastVol = initialVol
|
v.lastVol = initialVol
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package widgets
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fyne.io/fyne/v2"
|
||||||
|
"fyne.io/fyne/v2/driver/desktop"
|
||||||
|
"fyne.io/fyne/v2/widget"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TappableIcon struct {
|
||||||
|
widget.Icon
|
||||||
|
|
||||||
|
OnTapped func()
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewTappbaleIcon(res fyne.Resource) *TappableIcon {
|
||||||
|
icon := &TappableIcon{}
|
||||||
|
icon.ExtendBaseWidget(icon)
|
||||||
|
icon.SetResource(res)
|
||||||
|
|
||||||
|
return icon
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *TappableIcon) Tapped(_ *fyne.PointEvent) {
|
||||||
|
if t.OnTapped != nil {
|
||||||
|
t.OnTapped()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *TappableIcon) TappedSecondary(_ *fyne.PointEvent) {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *TappableIcon) Cursor() desktop.Cursor {
|
||||||
|
return desktop.PointerCursor
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
package widgets
|
||||||
|
|
||||||
|
import (
|
||||||
|
"supersonic/ui/layouts"
|
||||||
|
|
||||||
|
"fyne.io/fyne/v2"
|
||||||
|
"fyne.io/fyne/v2/container"
|
||||||
|
"fyne.io/fyne/v2/widget"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ToggleButtonGroup struct {
|
||||||
|
widget.BaseWidget
|
||||||
|
|
||||||
|
buttonContainer *fyne.Container
|
||||||
|
|
||||||
|
activeBtnIdx int
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewToggleButtonGroup(activatedBtnIdx int, buttons ...*widget.Button) *ToggleButtonGroup {
|
||||||
|
t := &ToggleButtonGroup{}
|
||||||
|
t.ExtendBaseWidget(t)
|
||||||
|
t.buttonContainer = container.New(&layouts.HboxCustomPadding{DisableThemePad: true})
|
||||||
|
for i, b := range buttons {
|
||||||
|
b.Importance = widget.MediumImportance
|
||||||
|
t.buttonContainer.Add(b)
|
||||||
|
prevOnTapped := b.OnTapped
|
||||||
|
b.OnTapped = func(i int) func() {
|
||||||
|
return func() {
|
||||||
|
if t.onTapped(i) && prevOnTapped != nil {
|
||||||
|
prevOnTapped()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}(i)
|
||||||
|
}
|
||||||
|
if activatedBtnIdx >= 0 && activatedBtnIdx <= len(buttons) {
|
||||||
|
buttons[activatedBtnIdx].Importance = widget.HighImportance
|
||||||
|
}
|
||||||
|
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *ToggleButtonGroup) ActivatedButtonIndex() int {
|
||||||
|
return t.activeBtnIdx
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *ToggleButtonGroup) SetActivatedButton(idx int) {
|
||||||
|
changed := t.activeBtnIdx != idx
|
||||||
|
t.activeBtnIdx = idx
|
||||||
|
for i, b := range t.buttonContainer.Objects {
|
||||||
|
if i == idx {
|
||||||
|
b.(*widget.Button).Importance = widget.HighImportance
|
||||||
|
} else {
|
||||||
|
b.(*widget.Button).Importance = widget.MediumImportance
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if changed {
|
||||||
|
t.Refresh()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *ToggleButtonGroup) onTapped(btnIdx int) bool {
|
||||||
|
changed := t.activeBtnIdx != btnIdx
|
||||||
|
t.SetActivatedButton(btnIdx)
|
||||||
|
return changed
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *ToggleButtonGroup) CreateRenderer() fyne.WidgetRenderer {
|
||||||
|
return widget.NewSimpleRenderer(t.buttonContainer)
|
||||||
|
}
|
||||||
+115
-44
@@ -4,6 +4,8 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"supersonic/res"
|
||||||
|
"supersonic/sharedutil"
|
||||||
"supersonic/ui/layouts"
|
"supersonic/ui/layouts"
|
||||||
"supersonic/ui/os"
|
"supersonic/ui/os"
|
||||||
"supersonic/ui/util"
|
"supersonic/ui/util"
|
||||||
@@ -19,12 +21,13 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
ColumnArtist = "Artist"
|
ColumnArtist = "Artist"
|
||||||
ColumnAlbum = "Album"
|
ColumnAlbum = "Album"
|
||||||
ColumnTime = "Time"
|
ColumnTime = "Time"
|
||||||
ColumnYear = "Year"
|
ColumnYear = "Year"
|
||||||
ColumnPlays = "Plays"
|
ColumnFavorite = "Favorite"
|
||||||
ColumnBitrate = "Bitrate"
|
ColumnPlays = "Plays"
|
||||||
|
ColumnBitrate = "Bitrate"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Tracklist struct {
|
type Tracklist struct {
|
||||||
@@ -41,6 +44,7 @@ type Tracklist struct {
|
|||||||
OnPlaySelection func(tracks []*subsonic.Child)
|
OnPlaySelection func(tracks []*subsonic.Child)
|
||||||
OnAddToQueue func(trackIDs []*subsonic.Child)
|
OnAddToQueue func(trackIDs []*subsonic.Child)
|
||||||
OnAddToPlaylist func(trackIDs []string)
|
OnAddToPlaylist func(trackIDs []string)
|
||||||
|
OnSetFavorite func(trackIDs []string, fav bool)
|
||||||
|
|
||||||
visibleColumns []bool
|
visibleColumns []bool
|
||||||
|
|
||||||
@@ -54,11 +58,12 @@ type Tracklist struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func NewTracklist(tracks []*subsonic.Child) *Tracklist {
|
func NewTracklist(tracks []*subsonic.Child) *Tracklist {
|
||||||
t := &Tracklist{Tracks: tracks, nowPlayingIdx: -1, visibleColumns: make([]bool, 8)}
|
t := &Tracklist{Tracks: tracks, nowPlayingIdx: -1, visibleColumns: make([]bool, 9)}
|
||||||
|
|
||||||
t.ExtendBaseWidget(t)
|
t.ExtendBaseWidget(t)
|
||||||
t.selectionMgr = util.NewListSelectionManager(func() int { return len(t.Tracks) })
|
t.selectionMgr = util.NewListSelectionManager(func() int { return len(t.Tracks) })
|
||||||
t.colLayout = layouts.NewColumnsLayout([]float32{35, -1, -1, -1, 60, 60, 65, 75})
|
// #, Title, Artist, Album, Time, Year, Favorite, Plays, Bitrate
|
||||||
|
t.colLayout = layouts.NewColumnsLayout([]float32{35, -1, -1, -1, 60, 60, 47, 65, 75})
|
||||||
t.buildHeader()
|
t.buildHeader()
|
||||||
t.hdr.OnColumnVisibilityChanged = t.setColumnVisible
|
t.hdr.OnColumnVisibilityChanged = t.setColumnVisible
|
||||||
playingIcon := container.NewCenter(container.NewHBox(NewHSpace(2), widget.NewIcon(theme.MediaPlayIcon())))
|
playingIcon := container.NewCenter(container.NewHBox(NewHSpace(2), widget.NewIcon(theme.MediaPlayIcon())))
|
||||||
@@ -93,6 +98,7 @@ func (t *Tracklist) buildHeader() {
|
|||||||
{Text: "Album", AlignTrailing: false, CanToggleVisible: true},
|
{Text: "Album", AlignTrailing: false, CanToggleVisible: true},
|
||||||
{Text: "Time", AlignTrailing: true, CanToggleVisible: true},
|
{Text: "Time", AlignTrailing: true, CanToggleVisible: true},
|
||||||
{Text: "Year", AlignTrailing: true, CanToggleVisible: true},
|
{Text: "Year", AlignTrailing: true, CanToggleVisible: true},
|
||||||
|
{Text: "Fav.", AlignTrailing: false, CanToggleVisible: true},
|
||||||
{Text: "Plays", AlignTrailing: true, CanToggleVisible: true},
|
{Text: "Plays", AlignTrailing: true, CanToggleVisible: true},
|
||||||
{Text: "Bitrate", AlignTrailing: true, CanToggleVisible: true}},
|
{Text: "Bitrate", AlignTrailing: true, CanToggleVisible: true}},
|
||||||
t.colLayout)
|
t.colLayout)
|
||||||
@@ -142,19 +148,13 @@ func (t *Tracklist) SetNowPlaying(trackID string) {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
t.list.Refresh()
|
t.Refresh()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Tracklist) IncrementPlayCount(track *subsonic.Child) {
|
func (t *Tracklist) IncrementPlayCount(trackID string) {
|
||||||
if track == nil {
|
if tr := sharedutil.FindTrackByID(trackID, t.Tracks); tr != nil {
|
||||||
return
|
tr.PlayCount += 1
|
||||||
}
|
t.Refresh()
|
||||||
for _, tr := range t.Tracks {
|
|
||||||
if tr.ID == track.ID {
|
|
||||||
tr.PlayCount += 1
|
|
||||||
t.Refresh()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -239,6 +239,20 @@ func (t *Tracklist) onShowContextMenu(e *fyne.PointEvent, trackIdx int) {
|
|||||||
widget.ShowPopUpMenuAtPosition(t.ctxMenu, fyne.CurrentApp().Driver().CanvasForObject(t), e.AbsolutePosition)
|
widget.ShowPopUpMenuAtPosition(t.ctxMenu, fyne.CurrentApp().Driver().CanvasForObject(t), e.AbsolutePosition)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t *Tracklist) onSetFavorite(trackID string, fav bool) {
|
||||||
|
// update our own track model
|
||||||
|
tr := sharedutil.FindTrackByID(trackID, t.Tracks)
|
||||||
|
if fav {
|
||||||
|
tr.Starred = time.Now()
|
||||||
|
} else {
|
||||||
|
tr.Starred = time.Time{}
|
||||||
|
}
|
||||||
|
// notify listener
|
||||||
|
if t.OnSetFavorite != nil {
|
||||||
|
t.OnSetFavorite([]string{trackID}, fav)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (t *Tracklist) selectedTracks() []*subsonic.Child {
|
func (t *Tracklist) selectedTracks() []*subsonic.Child {
|
||||||
sel := t.selectionMgr.GetSelection()
|
sel := t.selectionMgr.GetSelection()
|
||||||
tracks := make([]*subsonic.Child, 0, len(sel))
|
tracks := make([]*subsonic.Child, 0, len(sel))
|
||||||
@@ -272,10 +286,12 @@ func ColNumber(colName string) int {
|
|||||||
return 4
|
return 4
|
||||||
case ColumnYear:
|
case ColumnYear:
|
||||||
return 5
|
return 5
|
||||||
case ColumnPlays:
|
case ColumnFavorite:
|
||||||
return 6
|
return 6
|
||||||
case ColumnBitrate:
|
case ColumnPlays:
|
||||||
return 7
|
return 7
|
||||||
|
case ColumnBitrate:
|
||||||
|
return 8
|
||||||
default:
|
default:
|
||||||
log.Printf("error: Tracklist: invalid column name %s", colName)
|
log.Printf("error: Tracklist: invalid column name %s", colName)
|
||||||
return -100
|
return -100
|
||||||
@@ -294,8 +310,10 @@ func colName(i int) string {
|
|||||||
case 5:
|
case 5:
|
||||||
return ColumnYear
|
return ColumnYear
|
||||||
case 6:
|
case 6:
|
||||||
return ColumnPlays
|
return ColumnFavorite
|
||||||
case 7:
|
case 7:
|
||||||
|
return ColumnPlays
|
||||||
|
case 8:
|
||||||
return ColumnBitrate
|
return ColumnBitrate
|
||||||
default:
|
default:
|
||||||
return ""
|
return ""
|
||||||
@@ -306,21 +324,24 @@ type TrackRow struct {
|
|||||||
widget.BaseWidget
|
widget.BaseWidget
|
||||||
|
|
||||||
// internal state
|
// internal state
|
||||||
tracklist *Tracklist
|
tracklist *Tracklist
|
||||||
trackIdx int
|
trackIdx int
|
||||||
trackID string
|
trackNum int
|
||||||
isPlaying bool
|
trackID string
|
||||||
playCount int64
|
isPlaying bool
|
||||||
tappedAt int64 // unixMillis
|
isFavorite bool
|
||||||
|
playCount int64
|
||||||
|
tappedAt int64 // unixMillis
|
||||||
|
|
||||||
num *widget.RichText
|
num *widget.RichText
|
||||||
name *widget.RichText
|
name *widget.RichText
|
||||||
artist *widget.RichText
|
artist *widget.RichText
|
||||||
album *widget.RichText
|
album *widget.RichText
|
||||||
dur *widget.RichText
|
dur *widget.RichText
|
||||||
year *widget.RichText
|
year *widget.RichText
|
||||||
bitrate *widget.RichText
|
favorite *fyne.Container
|
||||||
plays *widget.RichText
|
bitrate *widget.RichText
|
||||||
|
plays *widget.RichText
|
||||||
|
|
||||||
OnTapped func()
|
OnTapped func()
|
||||||
OnDoubleTapped func()
|
OnDoubleTapped func()
|
||||||
@@ -346,6 +367,9 @@ func NewTrackRow(tracklist *Tracklist, playingIcon fyne.CanvasObject) *TrackRow
|
|||||||
t.dur.Segments[0].(*widget.TextSegment).Style.Alignment = fyne.TextAlignTrailing
|
t.dur.Segments[0].(*widget.TextSegment).Style.Alignment = fyne.TextAlignTrailing
|
||||||
t.year = widget.NewRichTextWithText("")
|
t.year = widget.NewRichTextWithText("")
|
||||||
t.year.Segments[0].(*widget.TextSegment).Style.Alignment = fyne.TextAlignTrailing
|
t.year.Segments[0].(*widget.TextSegment).Style.Alignment = fyne.TextAlignTrailing
|
||||||
|
favorite := NewTappbaleIcon(res.ResHeartOutlineInvertPng)
|
||||||
|
favorite.OnTapped = t.toggleFavorited
|
||||||
|
t.favorite = container.NewCenter(favorite)
|
||||||
t.plays = widget.NewRichTextWithText("")
|
t.plays = widget.NewRichTextWithText("")
|
||||||
t.plays.Segments[0].(*widget.TextSegment).Style.Alignment = fyne.TextAlignTrailing
|
t.plays.Segments[0].(*widget.TextSegment).Style.Alignment = fyne.TextAlignTrailing
|
||||||
t.bitrate = widget.NewRichTextWithText("")
|
t.bitrate = widget.NewRichTextWithText("")
|
||||||
@@ -355,20 +379,16 @@ func NewTrackRow(tracklist *Tracklist, playingIcon fyne.CanvasObject) *TrackRow
|
|||||||
t.selectionRect.Hidden = true
|
t.selectionRect.Hidden = true
|
||||||
t.container = container.NewMax(t.selectionRect,
|
t.container = container.NewMax(t.selectionRect,
|
||||||
container.New(tracklist.colLayout,
|
container.New(tracklist.colLayout,
|
||||||
t.num, t.name, t.artist, t.album, t.dur, t.year, t.plays, t.bitrate))
|
t.num, t.name, t.artist, t.album, t.dur, t.year, t.favorite, t.plays, t.bitrate))
|
||||||
return t
|
return t
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *TrackRow) Update(tr *subsonic.Child, isPlaying bool, rowNum int) {
|
func (t *TrackRow) Update(tr *subsonic.Child, isPlaying bool, rowNum int) {
|
||||||
if tr.ID != t.trackID || isPlaying != t.isPlaying || tr.PlayCount != t.playCount {
|
// Update info that can change if this row is bound to
|
||||||
t.isPlaying = isPlaying
|
// a new track (*subsonic.Child)
|
||||||
|
if tr.ID != t.trackID {
|
||||||
t.trackID = tr.ID
|
t.trackID = tr.ID
|
||||||
t.playCount = tr.PlayCount
|
|
||||||
|
|
||||||
if rowNum < 0 {
|
|
||||||
rowNum = tr.Track
|
|
||||||
}
|
|
||||||
t.num.Segments[0].(*widget.TextSegment).Text = strconv.Itoa(rowNum)
|
|
||||||
t.name.Segments[0].(*widget.TextSegment).Text = tr.Title
|
t.name.Segments[0].(*widget.TextSegment).Text = tr.Title
|
||||||
t.artist.Segments[0].(*widget.TextSegment).Text = tr.Artist
|
t.artist.Segments[0].(*widget.TextSegment).Text = tr.Artist
|
||||||
t.album.Segments[0].(*widget.TextSegment).Text = tr.Album
|
t.album.Segments[0].(*widget.TextSegment).Text = tr.Album
|
||||||
@@ -376,7 +396,27 @@ func (t *TrackRow) Update(tr *subsonic.Child, isPlaying bool, rowNum int) {
|
|||||||
t.year.Segments[0].(*widget.TextSegment).Text = strconv.Itoa(tr.Year)
|
t.year.Segments[0].(*widget.TextSegment).Text = strconv.Itoa(tr.Year)
|
||||||
t.plays.Segments[0].(*widget.TextSegment).Text = strconv.Itoa(int(tr.PlayCount))
|
t.plays.Segments[0].(*widget.TextSegment).Text = strconv.Itoa(int(tr.PlayCount))
|
||||||
t.bitrate.Segments[0].(*widget.TextSegment).Text = strconv.Itoa(tr.BitRate)
|
t.bitrate.Segments[0].(*widget.TextSegment).Text = strconv.Itoa(tr.BitRate)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update track num if needed
|
||||||
|
// (which can change based on bound *subsonic.Child or tracklist.AutoNumber)
|
||||||
|
if t.trackNum != rowNum {
|
||||||
|
if rowNum < 0 {
|
||||||
|
rowNum = tr.Track
|
||||||
|
}
|
||||||
|
t.trackNum = rowNum
|
||||||
|
t.num.Segments[0].(*widget.TextSegment).Text = strconv.Itoa(rowNum)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update play count if needed
|
||||||
|
if tr.PlayCount != t.playCount {
|
||||||
|
t.playCount = tr.PlayCount
|
||||||
|
t.plays.Segments[0].(*widget.TextSegment).Text = strconv.Itoa(int(tr.PlayCount))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render whether track is playing or not
|
||||||
|
if isPlaying != t.isPlaying {
|
||||||
|
t.isPlaying = isPlaying
|
||||||
t.name.Segments[0].(*widget.TextSegment).Style.TextStyle.Bold = isPlaying
|
t.name.Segments[0].(*widget.TextSegment).Style.TextStyle.Bold = isPlaying
|
||||||
t.artist.Segments[0].(*widget.TextSegment).Style.TextStyle.Bold = isPlaying
|
t.artist.Segments[0].(*widget.TextSegment).Style.TextStyle.Bold = isPlaying
|
||||||
t.album.Segments[0].(*widget.TextSegment).Style.TextStyle.Bold = isPlaying
|
t.album.Segments[0].(*widget.TextSegment).Style.TextStyle.Bold = isPlaying
|
||||||
@@ -392,16 +432,47 @@ func (t *TrackRow) Update(tr *subsonic.Child, isPlaying bool, rowNum int) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Render favorite column
|
||||||
|
// TODO: right now the only way for the favorite status to change while the tracklist is visible
|
||||||
|
// is by the user clicking on the heart icon in the favorites column
|
||||||
|
// If this changes in the future (e.g. context menu entry on tracklist), then we will
|
||||||
|
// need better state management/onChanged notif so we know to re-render the column
|
||||||
|
// (maybe update the Starred field directly on the track struct and issue a Refresh call -
|
||||||
|
// like we do to update the now playing value when scrobbles happen)
|
||||||
|
if tr.Starred.IsZero() {
|
||||||
|
t.isFavorite = false
|
||||||
|
t.favorite.Objects[0].(*TappableIcon).Resource = res.ResHeartOutlineInvertPng
|
||||||
|
} else {
|
||||||
|
t.isFavorite = true
|
||||||
|
t.favorite.Objects[0].(*TappableIcon).Resource = res.ResHeartFilledInvertPng
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show only columns configured to be visible
|
||||||
t.artist.Hidden = !t.tracklist.visibleColumns[ColNumber(ColumnArtist)]
|
t.artist.Hidden = !t.tracklist.visibleColumns[ColNumber(ColumnArtist)]
|
||||||
t.album.Hidden = !t.tracklist.visibleColumns[ColNumber(ColumnAlbum)]
|
t.album.Hidden = !t.tracklist.visibleColumns[ColNumber(ColumnAlbum)]
|
||||||
t.dur.Hidden = !t.tracklist.visibleColumns[ColNumber(ColumnTime)]
|
t.dur.Hidden = !t.tracklist.visibleColumns[ColNumber(ColumnTime)]
|
||||||
t.year.Hidden = !t.tracklist.visibleColumns[ColNumber(ColumnYear)]
|
t.year.Hidden = !t.tracklist.visibleColumns[ColNumber(ColumnYear)]
|
||||||
|
t.favorite.Hidden = !t.tracklist.visibleColumns[ColNumber(ColumnFavorite)]
|
||||||
t.plays.Hidden = !t.tracklist.visibleColumns[ColNumber(ColumnPlays)]
|
t.plays.Hidden = !t.tracklist.visibleColumns[ColNumber(ColumnPlays)]
|
||||||
t.bitrate.Hidden = !t.tracklist.visibleColumns[ColNumber(ColumnBitrate)]
|
t.bitrate.Hidden = !t.tracklist.visibleColumns[ColNumber(ColumnBitrate)]
|
||||||
|
|
||||||
t.Refresh()
|
t.Refresh()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t *TrackRow) toggleFavorited() {
|
||||||
|
if t.isFavorite {
|
||||||
|
t.favorite.Objects[0].(*TappableIcon).Resource = res.ResHeartOutlineInvertPng
|
||||||
|
t.favorite.Refresh()
|
||||||
|
t.isFavorite = false
|
||||||
|
t.tracklist.onSetFavorite(t.trackID, false)
|
||||||
|
} else {
|
||||||
|
t.favorite.Objects[0].(*TappableIcon).Resource = res.ResHeartFilledInvertPng
|
||||||
|
t.favorite.Refresh()
|
||||||
|
t.isFavorite = true
|
||||||
|
t.tracklist.onSetFavorite(t.trackID, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (t *TrackRow) CreateRenderer() fyne.WidgetRenderer {
|
func (t *TrackRow) CreateRenderer() fyne.WidgetRenderer {
|
||||||
return widget.NewSimpleRenderer(t.container)
|
return widget.NewSimpleRenderer(t.container)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user