The Grid view and related components were highly coupled to Albums, including the filter button that is currently used only for album filtering. As part of the migration of Artists to Grid view, this is the smallest possible change to convert many of the existing code to generics/interfaces that will allow different media to be displayed and filtered by using Grid views. The main changes in this diff are: * Introduction of generics for Media types (`M`) and Filter options (`F`), which can be later extended to other entities besides Albums. * Complete decoupling of `GridViewPage` and related components from Albums (making these pages also generic). * Refactoring of Subsonic/Jellyfin specific code to understand these new generics when dealing with filtering logic.
451 lines
13 KiB
Go
451 lines
13 KiB
Go
package browsing
|
|
|
|
import (
|
|
"fmt"
|
|
"log"
|
|
|
|
"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/layouts"
|
|
myTheme "github.com/dweymouth/supersonic/ui/theme"
|
|
"github.com/dweymouth/supersonic/ui/util"
|
|
"github.com/dweymouth/supersonic/ui/widgets"
|
|
|
|
"fyne.io/fyne/v2"
|
|
"fyne.io/fyne/v2/container"
|
|
"fyne.io/fyne/v2/layout"
|
|
"fyne.io/fyne/v2/theme"
|
|
"fyne.io/fyne/v2/widget"
|
|
)
|
|
|
|
type FavoritesPage struct {
|
|
widget.BaseWidget
|
|
|
|
cfg *backend.FavoritesPageConfig
|
|
pool *util.WidgetPool
|
|
contr *controller.Controller
|
|
pm *backend.PlaybackManager
|
|
im *backend.ImageManager
|
|
mp mediaprovider.MediaProvider
|
|
|
|
disposed bool
|
|
trackSort widgets.TracklistSort
|
|
filter mediaprovider.AlbumFilter
|
|
searchText string
|
|
nowPlayingID string
|
|
pendingViewSwitch bool
|
|
|
|
albumGrid *widgets.GridView
|
|
gridState *widgets.GridViewState
|
|
searchGridState *widgets.GridViewState
|
|
artistGrid *widgets.GridView
|
|
tracklistCtr *fyne.Container
|
|
searcher *widgets.SearchEntry
|
|
filterBtn *widgets.AlbumFilterButton
|
|
titleDisp *widget.RichText
|
|
toggleBtns *widgets.ToggleButtonGroup
|
|
container *fyne.Container
|
|
}
|
|
|
|
func NewFavoritesPage(cfg *backend.FavoritesPageConfig, pool *util.WidgetPool, contr *controller.Controller, mp mediaprovider.MediaProvider, pm *backend.PlaybackManager, im *backend.ImageManager) *FavoritesPage {
|
|
a := &FavoritesPage{
|
|
filter: mediaprovider.NewAlbumFilter(mediaprovider.AlbumFilterOptions{
|
|
ExcludeUnfavorited: true,
|
|
}),
|
|
cfg: cfg,
|
|
pool: pool,
|
|
contr: contr,
|
|
pm: pm,
|
|
mp: mp,
|
|
im: im,
|
|
}
|
|
a.ExtendBaseWidget(a)
|
|
a.createHeader(0)
|
|
iter := widgets.NewGridViewAlbumIterator(mp.IterateAlbums("", a.filter))
|
|
if g := pool.Obtain(util.WidgetTypeGridView); g != nil {
|
|
a.albumGrid = g.(*widgets.GridView)
|
|
a.albumGrid.Placeholder = myTheme.AlbumIcon
|
|
a.albumGrid.Reset(iter)
|
|
} else {
|
|
a.albumGrid = widgets.NewGridView(iter, a.im, myTheme.AlbumIcon)
|
|
}
|
|
a.contr.ConnectAlbumGridActions(a.albumGrid)
|
|
if cfg.InitialView == "Artists" {
|
|
a.toggleBtns.SetActivatedButton(1)
|
|
a.onShowFavoriteArtists()
|
|
} else if cfg.InitialView == "Songs" {
|
|
a.toggleBtns.SetActivatedButton(2)
|
|
a.onShowFavoriteSongs()
|
|
} else { // Albums view
|
|
a.createContainer(a.albumGrid)
|
|
}
|
|
return a
|
|
}
|
|
|
|
func (a *FavoritesPage) createHeader(activeBtnIdx int) {
|
|
a.titleDisp = widget.NewRichTextWithText("Favorites")
|
|
a.titleDisp.Segments[0].(*widget.TextSegment).Style = widget.RichTextStyle{
|
|
SizeName: theme.SizeNameHeadingText,
|
|
}
|
|
a.toggleBtns = widgets.NewToggleButtonGroup(activeBtnIdx,
|
|
widget.NewButtonWithIcon("", myTheme.AlbumIcon, a.onShowFavoriteAlbums),
|
|
widget.NewButtonWithIcon("", myTheme.ArtistIcon, a.onShowFavoriteArtists),
|
|
widget.NewButtonWithIcon("", myTheme.TracksIcon, a.onShowFavoriteSongs))
|
|
a.searcher = widgets.NewSearchEntry()
|
|
a.searcher.PlaceHolder = "Search page"
|
|
a.searcher.OnSearched = a.OnSearched
|
|
a.searcher.Entry.Text = a.searchText
|
|
a.filterBtn = widgets.NewAlbumFilterButton(a.filter, a.mp.GetGenres)
|
|
a.filterBtn.FavoriteDisabled = true
|
|
a.filterBtn.OnChanged = a.Reload
|
|
}
|
|
|
|
func (a *FavoritesPage) createContainer(initialView fyne.CanvasObject) {
|
|
searchVbox := container.NewVBox(layout.NewSpacer(), a.searcher, layout.NewSpacer())
|
|
a.container = container.NewBorder(container.NewHBox(util.NewHSpace(9),
|
|
a.titleDisp, container.NewCenter(a.toggleBtns), layout.NewSpacer(), container.NewCenter(a.filterBtn), searchVbox, util.NewHSpace(15)),
|
|
nil, nil, nil, initialView)
|
|
}
|
|
|
|
func restoreFavoritesPage(saved *savedFavoritesPage) *FavoritesPage {
|
|
a := &FavoritesPage{
|
|
cfg: saved.cfg,
|
|
contr: saved.contr,
|
|
pool: saved.pool,
|
|
pm: saved.pm,
|
|
mp: saved.mp,
|
|
im: saved.im,
|
|
gridState: saved.gridState,
|
|
searchGridState: saved.searchGridState,
|
|
searchText: saved.searchText,
|
|
filter: saved.filter,
|
|
trackSort: saved.trackSort,
|
|
}
|
|
a.ExtendBaseWidget(a)
|
|
a.createHeader(saved.activeToggleBtn)
|
|
state := saved.gridState
|
|
if saved.searchText != "" {
|
|
state = saved.searchGridState
|
|
}
|
|
if g := a.pool.Obtain(util.WidgetTypeGridView); g != nil {
|
|
a.albumGrid = g.(*widgets.GridView)
|
|
a.albumGrid.Placeholder = myTheme.AlbumIcon
|
|
a.albumGrid.ResetFromState(state)
|
|
} else {
|
|
a.albumGrid = widgets.NewGridViewFromState(state)
|
|
}
|
|
a.toggleBtns.SetActivatedButton(saved.activeToggleBtn)
|
|
|
|
if saved.activeToggleBtn == 1 {
|
|
a.onShowFavoriteArtists()
|
|
} else if saved.activeToggleBtn == 2 {
|
|
a.onShowFavoriteSongs()
|
|
} else {
|
|
a.createContainer(a.albumGrid)
|
|
}
|
|
|
|
return a
|
|
}
|
|
|
|
var _ Scrollable = (*FavoritesPage)(nil)
|
|
|
|
func (a *FavoritesPage) Scroll(amount float32) {
|
|
var grid *widgets.GridView
|
|
switch a.toggleBtns.ActivatedButtonIndex() {
|
|
case 0: // albums
|
|
grid = a.albumGrid
|
|
case 1: // artists
|
|
grid = a.artistGrid
|
|
default:
|
|
if tr := a.tracklistOrNil(); tr != nil {
|
|
tr.Scroll(amount)
|
|
}
|
|
return
|
|
}
|
|
if grid != nil {
|
|
grid.ScrollToOffset(grid.GetScrollOffset() + amount)
|
|
}
|
|
}
|
|
|
|
func (a *FavoritesPage) Route() controller.Route {
|
|
return controller.FavoritesRoute()
|
|
}
|
|
|
|
func (a *FavoritesPage) Tapped(*fyne.PointEvent) {
|
|
if tr := a.tracklistOrNil(); tr != nil {
|
|
tr.UnselectAll()
|
|
}
|
|
}
|
|
|
|
func (a *FavoritesPage) Reload() {
|
|
// reload favorite albums view
|
|
if a.searchText != "" {
|
|
a.doSearchAlbums(a.searchText)
|
|
} else {
|
|
iter := a.mp.IterateAlbums("", a.filter)
|
|
a.albumGrid.Reset(widgets.NewGridViewAlbumIterator(iter))
|
|
}
|
|
if a.tracklistCtr != nil || a.artistGrid != nil {
|
|
go func() {
|
|
// re-fetch starred info from server
|
|
starred, err := a.mp.GetFavorites()
|
|
if err != nil {
|
|
log.Printf("error getting starred items: %s", err.Error())
|
|
return
|
|
}
|
|
if a.disposed {
|
|
return
|
|
}
|
|
if tr := a.tracklistOrNil(); tr != nil {
|
|
// refresh favorite songs view
|
|
tr.SetTracks(starred.Tracks)
|
|
if a.toggleBtns.ActivatedButtonIndex() == 2 {
|
|
// favorite songs view is visible
|
|
tr.Refresh()
|
|
}
|
|
}
|
|
if a.artistGrid != nil {
|
|
// refresh favorite artists view
|
|
a.artistGrid.ResetFixed(buildArtistGridViewModel(starred.Artists))
|
|
if a.toggleBtns.ActivatedButtonIndex() == 1 {
|
|
// favorite artists view is visible
|
|
a.artistGrid.Refresh()
|
|
}
|
|
}
|
|
}()
|
|
}
|
|
}
|
|
|
|
func (a *FavoritesPage) Save() SavedPage {
|
|
a.disposed = true
|
|
sf := &savedFavoritesPage{
|
|
cfg: a.cfg,
|
|
contr: a.contr,
|
|
pool: a.pool,
|
|
pm: a.pm,
|
|
mp: a.mp,
|
|
im: a.im,
|
|
filter: a.filter,
|
|
searchText: a.searchText,
|
|
gridState: a.gridState,
|
|
searchGridState: a.searchGridState,
|
|
activeToggleBtn: a.toggleBtns.ActivatedButtonIndex(),
|
|
}
|
|
if a.searchText != "" {
|
|
sf.searchGridState = a.albumGrid.SaveToState()
|
|
} else {
|
|
sf.gridState = a.albumGrid.SaveToState()
|
|
}
|
|
a.albumGrid.Clear()
|
|
a.pool.Release(util.WidgetTypeGridView, a.albumGrid)
|
|
if a.artistGrid != nil {
|
|
a.artistGrid.Clear()
|
|
a.pool.Release(util.WidgetTypeGridView, a.artistGrid)
|
|
}
|
|
if tl := a.tracklistOrNil(); tl != nil {
|
|
sf.trackSort = tl.Sorting()
|
|
tl.Clear()
|
|
a.pool.Release(util.WidgetTypeTracklist, tl)
|
|
}
|
|
return sf
|
|
}
|
|
|
|
var _ Searchable = (*FavoritesPage)(nil)
|
|
|
|
func (a *FavoritesPage) SearchWidget() fyne.Focusable {
|
|
return a.searcher
|
|
}
|
|
|
|
func (a *FavoritesPage) OnSearched(query string) {
|
|
if query == "" {
|
|
a.albumGrid.ResetFromState(a.gridState)
|
|
a.searchGridState = nil
|
|
} else {
|
|
a.doSearchAlbums(query)
|
|
}
|
|
a.searchText = query
|
|
}
|
|
|
|
var _ CanShowNowPlaying = (*FavoritesPage)(nil)
|
|
|
|
func (a *FavoritesPage) OnSongChange(song, lastScrobbledIfAny *mediaprovider.Track) {
|
|
a.nowPlayingID = sharedutil.TrackIDOrEmptyStr(song)
|
|
if tracklist := a.tracklistOrNil(); tracklist != nil {
|
|
tracklist.SetNowPlaying(a.nowPlayingID)
|
|
tracklist.IncrementPlayCount(sharedutil.TrackIDOrEmptyStr(lastScrobbledIfAny))
|
|
}
|
|
}
|
|
|
|
var _ CanSelectAll = (*FavoritesPage)(nil)
|
|
|
|
func (a *FavoritesPage) SelectAll() {
|
|
if a.toggleBtns.ActivatedButtonIndex() == 2 /*songs*/ && a.tracklistCtr != nil {
|
|
a.tracklistOrNil().SelectAll() // can't be nil in this case
|
|
}
|
|
}
|
|
|
|
func (a *FavoritesPage) tracklistOrNil() *widgets.Tracklist {
|
|
if a.tracklistCtr != nil {
|
|
return a.tracklistCtr.Objects[0].(*widgets.Tracklist)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (a *FavoritesPage) doSearchAlbums(query string) {
|
|
if a.searchText == "" {
|
|
a.gridState = a.albumGrid.SaveToState()
|
|
}
|
|
iter := widgets.NewGridViewAlbumIterator(a.mp.SearchAlbums(query, a.filter))
|
|
a.albumGrid.Reset(iter)
|
|
}
|
|
|
|
func (a *FavoritesPage) onShowFavoriteAlbums() {
|
|
a.cfg.InitialView = "Albums" // save setting
|
|
a.searcher.Entry.Show()
|
|
a.filterBtn.Show()
|
|
a.container.Objects[0] = a.albumGrid
|
|
a.Refresh()
|
|
}
|
|
|
|
func (a *FavoritesPage) onShowFavoriteArtists() {
|
|
a.cfg.InitialView = "Artists" // save setting
|
|
a.searcher.Entry.Hide() // disable search on artists for now
|
|
a.filterBtn.Hide()
|
|
if a.artistGrid == nil {
|
|
if a.pendingViewSwitch {
|
|
return
|
|
}
|
|
a.pendingViewSwitch = true
|
|
if a.container == nil {
|
|
a.createContainer(layout.NewSpacer())
|
|
}
|
|
go func() {
|
|
fav, err := a.mp.GetFavorites()
|
|
if err != nil {
|
|
log.Printf("error getting starred items: %s", err.Error())
|
|
return
|
|
}
|
|
if a.disposed {
|
|
return
|
|
}
|
|
model := buildArtistGridViewModel(fav.Artists)
|
|
if g := a.pool.Obtain(util.WidgetTypeGridView); g != nil {
|
|
a.artistGrid = g.(*widgets.GridView)
|
|
a.artistGrid.Placeholder = myTheme.ArtistIcon
|
|
a.artistGrid.ResetFixed(model)
|
|
} else {
|
|
a.artistGrid = widgets.NewFixedGridView(model, a.im, myTheme.ArtistIcon)
|
|
}
|
|
canShareArtists := false
|
|
if r, canShare := a.mp.(mediaprovider.SupportsSharing); canShare {
|
|
canShareArtists = r.CanShareArtists()
|
|
}
|
|
a.artistGrid.DisableSharing = !canShareArtists
|
|
a.contr.ConnectArtistGridActions(a.artistGrid)
|
|
a.container.Objects[0] = a.artistGrid
|
|
a.Refresh()
|
|
a.pendingViewSwitch = false
|
|
}()
|
|
} else {
|
|
a.container.Objects[0] = a.artistGrid
|
|
a.Refresh()
|
|
}
|
|
}
|
|
|
|
func buildArtistGridViewModel(artists []*mediaprovider.Artist) []widgets.GridViewItemModel {
|
|
model := make([]widgets.GridViewItemModel, 0)
|
|
for _, ar := range artists {
|
|
albums := "albums"
|
|
if ar.AlbumCount == 1 {
|
|
albums = "album"
|
|
}
|
|
model = append(model, widgets.GridViewItemModel{
|
|
ID: ar.ID,
|
|
CoverArtID: ar.CoverArtID,
|
|
Name: ar.Name,
|
|
Secondary: []string{fmt.Sprintf("%d %s", ar.AlbumCount, albums)},
|
|
})
|
|
}
|
|
return model
|
|
}
|
|
|
|
func (a *FavoritesPage) onShowFavoriteSongs() {
|
|
a.cfg.InitialView = "Songs" // save setting
|
|
a.searcher.Entry.Hide() // disable search on songs for now
|
|
a.filterBtn.Hide()
|
|
if a.tracklistCtr == nil {
|
|
if a.pendingViewSwitch {
|
|
return
|
|
}
|
|
a.pendingViewSwitch = true
|
|
if a.container == nil {
|
|
a.createContainer(layout.NewSpacer())
|
|
}
|
|
go func() {
|
|
fav, err := a.mp.GetFavorites()
|
|
if err != nil {
|
|
log.Printf("error getting starred items: %s", err.Error())
|
|
return
|
|
}
|
|
if a.disposed {
|
|
return
|
|
}
|
|
var tracklist *widgets.Tracklist
|
|
if tl := a.pool.Obtain(util.WidgetTypeTracklist); tl != nil {
|
|
tracklist = tl.(*widgets.Tracklist)
|
|
tracklist.Reset()
|
|
tracklist.SetTracks(fav.Tracks)
|
|
} else {
|
|
tracklist = widgets.NewTracklist(fav.Tracks)
|
|
}
|
|
tracklist.Options = widgets.TracklistOptions{AutoNumber: true}
|
|
_, canRate := a.mp.(mediaprovider.SupportsRating)
|
|
_, canShare := a.mp.(mediaprovider.SupportsSharing)
|
|
tracklist.Options.DisableRating = !canRate
|
|
tracklist.Options.DisableSharing = !canShare
|
|
tracklist.SetVisibleColumns(a.cfg.TracklistColumns)
|
|
tracklist.SetSorting(a.trackSort)
|
|
tracklist.OnVisibleColumnsChanged = func(cols []string) {
|
|
a.cfg.TracklistColumns = cols
|
|
}
|
|
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) CreateRenderer() fyne.WidgetRenderer {
|
|
a.ExtendBaseWidget(a)
|
|
return widget.NewSimpleRenderer(a.container)
|
|
}
|
|
|
|
type savedFavoritesPage struct {
|
|
cfg *backend.FavoritesPageConfig
|
|
contr *controller.Controller
|
|
pool *util.WidgetPool
|
|
pm *backend.PlaybackManager
|
|
mp mediaprovider.MediaProvider
|
|
im *backend.ImageManager
|
|
gridState *widgets.GridViewState
|
|
searchGridState *widgets.GridViewState
|
|
filter mediaprovider.AlbumFilter
|
|
searchText string
|
|
activeToggleBtn int
|
|
trackSort widgets.TracklistSort
|
|
}
|
|
|
|
func (s *savedFavoritesPage) Restore() Page {
|
|
return restoreFavoritesPage(s)
|
|
}
|