switch artists page to grid view
This commit is contained in:
@@ -20,6 +20,7 @@ type AlbumWithTracks struct {
|
|||||||
|
|
||||||
type Artist struct {
|
type Artist struct {
|
||||||
ID string
|
ID string
|
||||||
|
CoverArtID string
|
||||||
Name string
|
Name string
|
||||||
Favorite bool
|
Favorite bool
|
||||||
AlbumCount int
|
AlbumCount int
|
||||||
|
|||||||
@@ -311,6 +311,7 @@ func toArtistFromID3(ar *subsonic.ArtistID3) *mediaprovider.Artist {
|
|||||||
}
|
}
|
||||||
return &mediaprovider.Artist{
|
return &mediaprovider.Artist{
|
||||||
ID: ar.ID,
|
ID: ar.ID,
|
||||||
|
CoverArtID: ar.CoverArt,
|
||||||
Name: ar.Name,
|
Name: ar.Name,
|
||||||
Favorite: !ar.Starred.IsZero(),
|
Favorite: !ar.Starred.IsZero(),
|
||||||
AlbumCount: ar.AlbumCount,
|
AlbumCount: ar.AlbumCount,
|
||||||
|
|||||||
@@ -126,15 +126,6 @@ func (a *ArtistPage) OnSongChange(track, lastScrobbledIfAny *mediaprovider.Track
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *ArtistPage) playAllTracks() {
|
|
||||||
if a.artistInfo != nil { // page loaded
|
|
||||||
for i, album := range a.artistInfo.Albums {
|
|
||||||
a.pm.LoadAlbum(album.ID, i > 0 /*append*/, false /*shuffle*/)
|
|
||||||
}
|
|
||||||
a.pm.PlayFromBeginning()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *ArtistPage) playArtistRadio() {
|
func (a *ArtistPage) playArtistRadio() {
|
||||||
go a.pm.PlaySimilarSongs(a.artistID)
|
go a.pm.PlaySimilarSongs(a.artistID)
|
||||||
}
|
}
|
||||||
@@ -267,7 +258,9 @@ func NewArtistPageHeader(page *ArtistPage) *ArtistPageHeader {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
a.favoriteBtn = widgets.NewFavoriteButton(func() { go a.toggleFavorited() })
|
a.favoriteBtn = widgets.NewFavoriteButton(func() { go a.toggleFavorited() })
|
||||||
a.playBtn = widget.NewButtonWithIcon("Play Discography", theme.MediaPlayIcon(), page.playAllTracks)
|
a.playBtn = widget.NewButtonWithIcon("Play Discography", theme.MediaPlayIcon(), func() {
|
||||||
|
go a.artistPage.contr.PlayArtistDiscography(a.artistID, false /*shuffle*/)
|
||||||
|
})
|
||||||
a.playRadioBtn = widget.NewButtonWithIcon(" Play Artist Radio", myTheme.ShuffleIcon, page.playArtistRadio)
|
a.playRadioBtn = widget.NewButtonWithIcon(" Play Artist Radio", myTheme.ShuffleIcon, page.playArtistRadio)
|
||||||
a.biographyDisp.Wrapping = fyne.TextWrapWord
|
a.biographyDisp.Wrapping = fyne.TextWrapWord
|
||||||
a.ExtendBaseWidget(a)
|
a.ExtendBaseWidget(a)
|
||||||
|
|||||||
@@ -0,0 +1,199 @@
|
|||||||
|
package browsing
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"fyne.io/fyne/v2"
|
||||||
|
"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"
|
||||||
|
"github.com/dweymouth/supersonic/backend/mediaprovider"
|
||||||
|
"github.com/dweymouth/supersonic/sharedutil"
|
||||||
|
"github.com/dweymouth/supersonic/ui/controller"
|
||||||
|
"github.com/dweymouth/supersonic/ui/util"
|
||||||
|
"github.com/dweymouth/supersonic/ui/widgets"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ArtistsPage struct {
|
||||||
|
widget.BaseWidget
|
||||||
|
|
||||||
|
contr *controller.Controller
|
||||||
|
im *backend.ImageManager
|
||||||
|
pm *backend.PlaybackManager
|
||||||
|
mp mediaprovider.MediaProvider
|
||||||
|
artists []*mediaprovider.Artist
|
||||||
|
searchedArtists []*mediaprovider.Artist
|
||||||
|
grid *widgets.GridView
|
||||||
|
fullGridScrollPos float32
|
||||||
|
searchGridScrollPos float32
|
||||||
|
searcher *widgets.SearchEntry
|
||||||
|
searchText string
|
||||||
|
titleDisp *widget.RichText
|
||||||
|
|
||||||
|
container *fyne.Container
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewArtistsPage(
|
||||||
|
contr *controller.Controller,
|
||||||
|
pm *backend.PlaybackManager,
|
||||||
|
mp mediaprovider.MediaProvider,
|
||||||
|
im *backend.ImageManager,
|
||||||
|
) *ArtistsPage {
|
||||||
|
return newArtistsPage(contr, pm, mp, im, "", 0, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newArtistsPage(
|
||||||
|
contr *controller.Controller,
|
||||||
|
pm *backend.PlaybackManager,
|
||||||
|
mp mediaprovider.MediaProvider,
|
||||||
|
im *backend.ImageManager,
|
||||||
|
searchText string,
|
||||||
|
fullGridScrollPos float32,
|
||||||
|
searchGridScrollPos float32,
|
||||||
|
) *ArtistsPage {
|
||||||
|
a := &ArtistsPage{
|
||||||
|
contr: contr,
|
||||||
|
pm: pm,
|
||||||
|
mp: mp,
|
||||||
|
im: im,
|
||||||
|
searchText: searchText,
|
||||||
|
fullGridScrollPos: fullGridScrollPos,
|
||||||
|
searchGridScrollPos: searchGridScrollPos,
|
||||||
|
}
|
||||||
|
a.ExtendBaseWidget(a)
|
||||||
|
|
||||||
|
log.Printf("Scroll pos: full %0.2f search %0.2f", fullGridScrollPos, searchGridScrollPos)
|
||||||
|
a.titleDisp = widget.NewRichTextWithText("Artists")
|
||||||
|
a.titleDisp.Segments[0].(*widget.TextSegment).Style = widget.RichTextStyle{
|
||||||
|
SizeName: theme.SizeNameHeadingText,
|
||||||
|
}
|
||||||
|
a.searcher = widgets.NewSearchEntry()
|
||||||
|
a.searcher.OnSearched = func(query string) { a.onSearched(query, false /*firstLoad*/) }
|
||||||
|
a.searcher.Entry.Text = searchText
|
||||||
|
a.grid = widgets.NewFixedGridView(nil, a.im)
|
||||||
|
a.grid.OnShowItemPage = a.showArtistPage
|
||||||
|
a.grid.OnPlay = func(artistID string, shuffle bool) { go a.contr.PlayArtistDiscography(artistID, shuffle) }
|
||||||
|
a.grid.OnAddToQueue = func(artistID string) {
|
||||||
|
go a.pm.LoadTracks(a.contr.GetArtistTracks(artistID), true /*append*/, false /*shuffle*/)
|
||||||
|
}
|
||||||
|
a.grid.OnAddToPlaylist = func(artistID string) {
|
||||||
|
go a.contr.DoAddTracksToPlaylistWorkflow(
|
||||||
|
sharedutil.TracksToIDs(a.contr.GetArtistTracks(artistID)))
|
||||||
|
}
|
||||||
|
|
||||||
|
searchVbox := container.NewVBox(layout.NewSpacer(), a.searcher, layout.NewSpacer())
|
||||||
|
a.container = container.NewBorder(
|
||||||
|
container.NewHBox(util.NewHSpace(6),
|
||||||
|
a.titleDisp, layout.NewSpacer(), searchVbox, util.NewHSpace(15)),
|
||||||
|
nil, nil, nil, a.grid,
|
||||||
|
)
|
||||||
|
|
||||||
|
go a.load()
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *ArtistsPage) Reload() {
|
||||||
|
a.searchGridScrollPos = 0
|
||||||
|
a.fullGridScrollPos = 0
|
||||||
|
go a.load()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *ArtistsPage) load() {
|
||||||
|
artists, err := a.mp.GetArtists()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("error loading artists: %v", err.Error())
|
||||||
|
}
|
||||||
|
a.artists = artists
|
||||||
|
a.onSearched(a.searcher.Entry.Text, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *ArtistsPage) onSearched(query string, firstLoad bool) {
|
||||||
|
// since the playlist list is returned in full non-paginated, we will do our own
|
||||||
|
// simple search based on the name, description, and owner, rather than calling a server API
|
||||||
|
var artists []*mediaprovider.Artist
|
||||||
|
scrollPos := float32(0)
|
||||||
|
if query == "" {
|
||||||
|
a.searchedArtists = nil
|
||||||
|
artists = a.artists
|
||||||
|
scrollPos = a.fullGridScrollPos
|
||||||
|
} else {
|
||||||
|
if firstLoad { // if reloading with a saved search state, set scroll position
|
||||||
|
scrollPos = a.searchGridScrollPos
|
||||||
|
}
|
||||||
|
if a.searchText == "" {
|
||||||
|
// if first search, capture scroll position of full, unsearched grid
|
||||||
|
a.fullGridScrollPos = a.grid.GetScrollOffset()
|
||||||
|
}
|
||||||
|
qLower := strings.ToLower(query)
|
||||||
|
a.searchedArtists = sharedutil.FilterSlice(a.artists, func(p *mediaprovider.Artist) bool {
|
||||||
|
return strings.Contains(strings.ToLower(p.Name), qLower)
|
||||||
|
})
|
||||||
|
artists = a.searchedArtists
|
||||||
|
}
|
||||||
|
a.searchText = query
|
||||||
|
a.grid.ResetFixed(createArtistsGridViewModel(artists))
|
||||||
|
a.grid.Refresh()
|
||||||
|
a.grid.ScrollToOffset(scrollPos)
|
||||||
|
}
|
||||||
|
|
||||||
|
func createArtistsGridViewModel(artists []*mediaprovider.Artist) []widgets.GridViewItemModel {
|
||||||
|
return sharedutil.MapSlice(artists, func(ar *mediaprovider.Artist) widgets.GridViewItemModel {
|
||||||
|
albums := "albums"
|
||||||
|
if ar.AlbumCount == 1 {
|
||||||
|
albums = "album"
|
||||||
|
}
|
||||||
|
return widgets.GridViewItemModel{
|
||||||
|
Name: ar.Name,
|
||||||
|
ID: ar.ID,
|
||||||
|
CoverArtID: ar.CoverArtID,
|
||||||
|
Secondary: fmt.Sprintf("%d %s", ar.AlbumCount, albums),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *ArtistsPage) CreateRenderer() fyne.WidgetRenderer {
|
||||||
|
return widget.NewSimpleRenderer(a.container)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *ArtistsPage) Route() controller.Route {
|
||||||
|
return controller.ArtistsRoute()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *ArtistsPage) showArtistPage(id string) {
|
||||||
|
a.contr.NavigateTo(controller.ArtistRoute(id))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *ArtistsPage) Save() SavedPage {
|
||||||
|
s := &savedArtistsPage{
|
||||||
|
contr: a.contr,
|
||||||
|
im: a.im,
|
||||||
|
pm: a.pm,
|
||||||
|
mp: a.mp,
|
||||||
|
searchText: a.searchText,
|
||||||
|
fullGridScrollPos: a.fullGridScrollPos,
|
||||||
|
}
|
||||||
|
if a.searchText == "" {
|
||||||
|
s.fullGridScrollPos = a.grid.GetScrollOffset()
|
||||||
|
} else {
|
||||||
|
s.searchGridScrollPos = a.grid.GetScrollOffset()
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
type savedArtistsPage struct {
|
||||||
|
contr *controller.Controller
|
||||||
|
im *backend.ImageManager
|
||||||
|
pm *backend.PlaybackManager
|
||||||
|
mp mediaprovider.MediaProvider
|
||||||
|
searchText string
|
||||||
|
fullGridScrollPos float32
|
||||||
|
searchGridScrollPos float32
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *savedArtistsPage) Restore() Page {
|
||||||
|
return newArtistsPage(s.contr, s.pm, s.mp, s.im, s.searchText, s.fullGridScrollPos, s.searchGridScrollPos)
|
||||||
|
}
|
||||||
@@ -33,8 +33,6 @@ type GenrePage struct {
|
|||||||
titleDisp *widget.RichText
|
titleDisp *widget.RichText
|
||||||
playRandom *widget.Button
|
playRandom *widget.Button
|
||||||
|
|
||||||
OnPlayAlbum func(string, int)
|
|
||||||
|
|
||||||
container *fyne.Container
|
container *fyne.Container
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ func (r Router) CreatePage(rte controller.Route) Page {
|
|||||||
case controller.Artist:
|
case controller.Artist:
|
||||||
return NewArtistPage(rte.Arg, &r.App.Config.ArtistPage, r.App.PlaybackManager, r.App.ServerManager.Server, r.App.ImageManager, r.Controller)
|
return NewArtistPage(rte.Arg, &r.App.Config.ArtistPage, r.App.PlaybackManager, r.App.ServerManager.Server, r.App.ImageManager, r.Controller)
|
||||||
case controller.Artists:
|
case controller.Artists:
|
||||||
return NewArtistsGenresPage(false, r.Controller, r.App.ServerManager.Server)
|
return NewArtistsPage(r.Controller, r.App.PlaybackManager, r.App.ServerManager.Server, r.App.ImageManager)
|
||||||
case controller.Favorites:
|
case controller.Favorites:
|
||||||
return NewFavoritesPage(&r.App.Config.FavoritesPage, r.Controller, r.App.ServerManager.Server, r.App.PlaybackManager, r.App.ImageManager)
|
return NewFavoritesPage(&r.App.Config.FavoritesPage, r.Controller, r.App.ServerManager.Server, r.App.PlaybackManager, r.App.ImageManager)
|
||||||
case controller.Genre:
|
case controller.Genre:
|
||||||
|
|||||||
@@ -140,6 +140,35 @@ func (m *Controller) ConnectAlbumGridActions(grid *widgets.GridView) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *Controller) GetArtistTracks(artistID string) []*mediaprovider.Track {
|
||||||
|
server := m.App.ServerManager.Server
|
||||||
|
if server == nil {
|
||||||
|
log.Println("error playing artist discography: logged out")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
artist, err := server.GetArtist(artistID)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("error getting artist discography: %v", err.Error())
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var allTracks []*mediaprovider.Track
|
||||||
|
for _, album := range artist.Albums {
|
||||||
|
album, err := server.GetAlbum(album.ID)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("error loading album tracks: %v", err.Error())
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
allTracks = append(allTracks, album.Tracks...)
|
||||||
|
}
|
||||||
|
return allTracks
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Controller) PlayArtistDiscography(artistID string, shuffle bool) {
|
||||||
|
m.App.PlaybackManager.LoadTracks(m.GetArtistTracks(artistID), false, shuffle)
|
||||||
|
m.App.PlaybackManager.PlayFromBeginning()
|
||||||
|
}
|
||||||
|
|
||||||
func (m *Controller) PromptForFirstServer() {
|
func (m *Controller) PromptForFirstServer() {
|
||||||
d := dialogs.NewAddEditServerDialog("Connect to Server", false, nil, m.MainWindow.Canvas().Focus)
|
d := dialogs.NewAddEditServerDialog("Connect to Server", false, nil, m.MainWindow.Canvas().Focus)
|
||||||
pop := widget.NewModalPopUp(d, m.MainWindow.Canvas())
|
pop := widget.NewModalPopUp(d, m.MainWindow.Canvas())
|
||||||
|
|||||||
Reference in New Issue
Block a user