Merge pull request #347 from adamantike/misc/decouple-gridview-from-albums

misc: Decouple Grid view from Albums
This commit is contained in:
Drew Weymouth
2024-03-19 13:02:13 -07:00
committed by GitHub
12 changed files with 310 additions and 193 deletions
+24 -21
View File
@@ -7,17 +7,12 @@ import (
"github.com/dweymouth/supersonic/sharedutil" "github.com/dweymouth/supersonic/sharedutil"
) )
type Filter[T any] interface { type baseIter[M, F any] struct {
IsNil() bool filter mediaprovider.MediaFilter[M, F]
Matches(*T) bool prefetchCB func(*M)
}
type baseIter[T any] struct {
filter Filter[T]
prefetchCB func(*T)
serverPos int serverPos int
fetcher func(offset, limit int) ([]*T, error) fetcher func(offset, limit int) ([]*M, error)
prefetched []*T prefetched []*M
prefetchedPos int prefetchedPos int
done bool done bool
} }
@@ -25,7 +20,7 @@ type baseIter[T any] struct {
type AlbumFetchFn func(offset, limit int) ([]*mediaprovider.Album, error) type AlbumFetchFn func(offset, limit int) ([]*mediaprovider.Album, error)
func NewAlbumIterator(fetchFn AlbumFetchFn, filter mediaprovider.AlbumFilter, cb func(string)) mediaprovider.AlbumIterator { func NewAlbumIterator(fetchFn AlbumFetchFn, filter mediaprovider.AlbumFilter, cb func(string)) mediaprovider.AlbumIterator {
return &baseIter[mediaprovider.Album]{ return &baseIter[mediaprovider.Album, mediaprovider.AlbumFilterOptions]{
prefetchCB: func(a *mediaprovider.Album) { cb(a.CoverArtID) }, prefetchCB: func(a *mediaprovider.Album) { cb(a.CoverArtID) },
filter: filter, filter: filter,
fetcher: fetchFn, fetcher: fetchFn,
@@ -35,7 +30,7 @@ func NewAlbumIterator(fetchFn AlbumFetchFn, filter mediaprovider.AlbumFilter, cb
type ArtistFetchFn func(offset, limit int) ([]*mediaprovider.Artist, error) type ArtistFetchFn func(offset, limit int) ([]*mediaprovider.Artist, error)
func NewArtistIterator(fetchFn ArtistFetchFn) mediaprovider.ArtistIterator { func NewArtistIterator(fetchFn ArtistFetchFn) mediaprovider.ArtistIterator {
return &baseIter[mediaprovider.Artist]{ return &baseIter[mediaprovider.Artist, nilFilterOptions]{
fetcher: fetchFn, fetcher: fetchFn,
filter: nilFilter[mediaprovider.Artist]{}, filter: nilFilter[mediaprovider.Artist]{},
} }
@@ -44,14 +39,14 @@ func NewArtistIterator(fetchFn ArtistFetchFn) mediaprovider.ArtistIterator {
type TrackFetchFn func(offset, limit int) ([]*mediaprovider.Track, error) type TrackFetchFn func(offset, limit int) ([]*mediaprovider.Track, error)
func NewTrackIterator(fetchFn TrackFetchFn, cb func(string)) mediaprovider.TrackIterator { func NewTrackIterator(fetchFn TrackFetchFn, cb func(string)) mediaprovider.TrackIterator {
return &baseIter[mediaprovider.Track]{ return &baseIter[mediaprovider.Track, nilFilterOptions]{
prefetchCB: func(a *mediaprovider.Track) { cb(a.CoverArtID) }, prefetchCB: func(a *mediaprovider.Track) { cb(a.CoverArtID) },
filter: nilFilter[mediaprovider.Track]{}, filter: nilFilter[mediaprovider.Track]{},
fetcher: fetchFn, fetcher: fetchFn,
} }
} }
func (r *baseIter[T]) Next() *T { func (r *baseIter[M, F]) Next() *M {
if r.done { if r.done {
return nil return nil
} }
@@ -89,7 +84,7 @@ func (r *baseIter[T]) Next() *T {
return r.prefetched[0] return r.prefetched[0]
} }
type randomIter struct { type randomAlbumIter struct {
filter mediaprovider.AlbumFilter filter mediaprovider.AlbumFilter
prefetchCB func(coverArtID string) prefetchCB func(coverArtID string)
albumIDSet map[string]bool albumIDSet map[string]bool
@@ -108,8 +103,8 @@ type randomIter struct {
done bool done bool
} }
func NewRandomAlbumIter(deterministicFetcher, randomFetcher AlbumFetchFn, filter mediaprovider.AlbumFilter, prefetchCoverCB func(string)) *randomIter { func NewRandomAlbumIter(deterministicFetcher, randomFetcher AlbumFetchFn, filter mediaprovider.AlbumFilter, prefetchCoverCB func(string)) *randomAlbumIter {
return &randomIter{ return &randomAlbumIter{
filter: filter, filter: filter,
prefetchCB: prefetchCoverCB, prefetchCB: prefetchCoverCB,
deterministicFetcher: deterministicFetcher, deterministicFetcher: deterministicFetcher,
@@ -118,7 +113,7 @@ func NewRandomAlbumIter(deterministicFetcher, randomFetcher AlbumFetchFn, filter
} }
} }
func (r *randomIter) Next() *mediaprovider.Album { func (r *randomAlbumIter) Next() *mediaprovider.Album {
if r.done { if r.done {
return nil return nil
} }
@@ -192,8 +187,16 @@ func (r *randomIter) Next() *mediaprovider.Album {
return nil return nil
} }
type nilFilter[T any] struct{} type nilFilterOptions struct{}
func (n nilFilter[T]) IsNil() bool { return true } type nilFilter[M any] struct{}
func (n nilFilter[T]) Matches(*T) bool { return true } func (n nilFilter[M]) IsNil() bool { return true }
func (n nilFilter[M]) Matches(*M) bool { return true }
func (n nilFilter[M]) Clone() mediaprovider.MediaFilter[M, nilFilterOptions] { return n }
func (n nilFilter[M]) Options() nilFilterOptions { return nilFilterOptions{} }
func (n nilFilter[M]) SetOptions(options nilFilterOptions) {}
+29 -19
View File
@@ -58,7 +58,7 @@ func (j *jellyfinMediaProvider) IterateAlbums(sortOrder string, filter mediaprov
jfSort.Field = jellyfin.SortByYear jfSort.Field = jellyfin.SortByYear
jfSort.Mode = jellyfin.SortDesc jfSort.Mode = jellyfin.SortDesc
} }
jfFilt := jfFilterFromFilter(&filter) jfFilt, modifiedFilter := jfFilterFromFilter(filter)
fetcher := func(offs, limit int) ([]*mediaprovider.Album, error) { fetcher := func(offs, limit int) ([]*mediaprovider.Album, error) {
al, err := j.client.GetAlbums(jellyfin.QueryOpts{ al, err := j.client.GetAlbums(jellyfin.QueryOpts{
@@ -84,9 +84,9 @@ func (j *jellyfinMediaProvider) IterateAlbums(sortOrder string, filter mediaprov
} }
return sharedutil.MapSlice(al, toAlbum), nil return sharedutil.MapSlice(al, toAlbum), nil
} }
return helpers.NewRandomAlbumIter(determFetcher, fetcher, filter, j.prefetchCoverCB) return helpers.NewRandomAlbumIter(determFetcher, fetcher, modifiedFilter, j.prefetchCoverCB)
} }
return helpers.NewAlbumIterator(fetcher, filter, j.prefetchCoverCB) return helpers.NewAlbumIterator(fetcher, modifiedFilter, j.prefetchCoverCB)
} }
func (j *jellyfinMediaProvider) SearchAlbums(searchQuery string, filter mediaprovider.AlbumFilter) mediaprovider.AlbumIterator { func (j *jellyfinMediaProvider) SearchAlbums(searchQuery string, filter mediaprovider.AlbumFilter) mediaprovider.AlbumIterator {
@@ -151,24 +151,34 @@ func (j *jellyfinMediaProvider) IterateArtists(sortOrder string) mediaprovider.A
} }
// Creates the Jellyfin filter to implement the given mediaprovider filter, // Creates the Jellyfin filter to implement the given mediaprovider filter,
// and zeros out the now-unneeded fields in the mediaprovider filter. // and returns a modified mediaprovider filter, with now-unneeded fields zeroed out.
func jfFilterFromFilter(filter *mediaprovider.AlbumFilter) jellyfin.Filter { func jfFilterFromFilter(filter mediaprovider.AlbumFilter) (jellyfin.Filter, mediaprovider.AlbumFilter) {
var jfFilt jellyfin.Filter var jfFilt jellyfin.Filter
if filter.ExcludeUnfavorited {
// Clone the original filter to not modify its options.
// Set filters must be maintained in the original filter, as they are used for the UI.
// Modified filter options are used to ignore further filtering that was already handled by the
// Jellyfin API.
modifiedFilter := filter.Clone()
filterOptions := modifiedFilter.Options()
if filterOptions.ExcludeUnfavorited {
jfFilt.Favorite = true jfFilt.Favorite = true
filter.ExcludeUnfavorited = false // Jellyfin will handle this filter filterOptions.ExcludeUnfavorited = false
} }
if filter.MinYear > 0 && filter.MaxYear > 0 { if filterOptions.MinYear > 0 && filterOptions.MaxYear > 0 {
jfFilt.YearRange = [2]int{filter.MinYear, filter.MaxYear} jfFilt.YearRange = [2]int{filterOptions.MinYear, filterOptions.MaxYear}
filter.MinYear, filter.MaxYear = 0, 0 filterOptions.MinYear, filterOptions.MaxYear = 0, 0
} else if filter.MinYear > 0 { } else if filterOptions.MinYear > 0 {
jfFilt.YearRange = [2]int{filter.MinYear, time.Now().Year()} jfFilt.YearRange = [2]int{filterOptions.MinYear, time.Now().Year()}
filter.MinYear, filter.MaxYear = 0, 0 filterOptions.MinYear, filterOptions.MaxYear = 0, 0
} else if filter.MaxYear > 0 { } else if filterOptions.MaxYear > 0 {
jfFilt.YearRange = [2]int{1900, filter.MaxYear} jfFilt.YearRange = [2]int{1900, filterOptions.MaxYear}
filter.MinYear, filter.MaxYear = 0, 0 filterOptions.MinYear, filterOptions.MaxYear = 0, 0
} }
jfFilt.Genres = filter.Genres jfFilt.Genres = filterOptions.Genres
filter.Genres = nil filterOptions.Genres = nil
return jfFilt
modifiedFilter.SetOptions(filterOptions)
return jfFilt, modifiedFilter
} }
+64 -24
View File
@@ -7,7 +7,25 @@ import (
"strings" "strings"
) )
type AlbumFilter struct { type MediaIterator[M any] interface {
Next() *M
}
type ArtistIterator = MediaIterator[Artist]
type AlbumIterator = MediaIterator[Album]
type TrackIterator = MediaIterator[Track]
type MediaFilter[M, F any] interface {
Options() F
SetOptions(F)
Clone() MediaFilter[M, F]
IsNil() bool
Matches(*M) bool
}
type AlbumFilter = MediaFilter[Album, AlbumFilterOptions]
type AlbumFilterOptions struct {
MinYear int MinYear int
MaxYear int // 0 == unset/match any MaxYear int // 0 == unset/match any
Genres []string // len(0) == unset/match any Genres []string // len(0) == unset/match any
@@ -16,42 +34,64 @@ type AlbumFilter struct {
ExcludeUnfavorited bool // mut. exc. with ExcludeFavorited ExcludeUnfavorited bool // mut. exc. with ExcludeFavorited
} }
// Returns true if the filter is the nil filter - i.e. matches everything // Clone returns a deep copy of the filter options
func (a AlbumFilter) IsNil() bool { func (o AlbumFilterOptions) Clone() AlbumFilterOptions {
return a.MinYear == 0 && a.MaxYear == 0 && genres := make([]string, len(o.Genres))
len(a.Genres) == 0 && copy(genres, o.Genres)
!a.ExcludeFavorited && !a.ExcludeUnfavorited return AlbumFilterOptions{
MinYear: o.MinYear,
MaxYear: o.MaxYear,
Genres: genres,
ExcludeFavorited: o.ExcludeFavorited,
ExcludeUnfavorited: o.ExcludeUnfavorited,
}
} }
func (f AlbumFilter) Matches(album *Album) bool { type albumFilter struct {
options AlbumFilterOptions
}
func NewAlbumFilter(options AlbumFilterOptions) *albumFilter {
return &albumFilter{options}
}
func (a albumFilter) Options() AlbumFilterOptions {
return a.options
}
func (a *albumFilter) SetOptions(options AlbumFilterOptions) {
a.options = options
}
// Clone returns a deep copy of the filter
func (a albumFilter) Clone() AlbumFilter {
return NewAlbumFilter(a.options.Clone())
}
// Returns true if the filter is the nil filter - i.e. matches everything
func (a albumFilter) IsNil() bool {
return a.options.MinYear == 0 && a.options.MaxYear == 0 &&
len(a.options.Genres) == 0 &&
!a.options.ExcludeFavorited && !a.options.ExcludeUnfavorited
}
func (f albumFilter) Matches(album *Album) bool {
if album == nil { if album == nil {
return false return false
} }
if f.ExcludeFavorited && album.Favorite { if f.options.ExcludeFavorited && album.Favorite {
return false return false
} }
if f.ExcludeUnfavorited && !album.Favorite { if f.options.ExcludeUnfavorited && !album.Favorite {
return false return false
} }
if y := album.Year; y < f.MinYear || (f.MaxYear > 0 && y > f.MaxYear) { if y := album.Year; y < f.options.MinYear || (f.options.MaxYear > 0 && y > f.options.MaxYear) {
return false return false
} }
if len(f.Genres) == 0 { if len(f.options.Genres) == 0 {
return true return true
} }
return genresMatch(f.Genres, album.Genres) return genresMatch(f.options.Genres, album.Genres)
}
type ArtistIterator interface {
Next() *Artist
}
type AlbumIterator interface {
Next() *Album
}
type TrackIterator interface {
Next() *Track
} }
type RatingFavoriteParameters struct { type RatingFavoriteParameters struct {
+28 -20
View File
@@ -35,23 +35,24 @@ func (s *subsonicMediaProvider) AlbumSortOrders() []string {
} }
} }
func filterMatches(f mediaprovider.AlbumFilter, album *subsonic.AlbumID3, ignoreGenre bool) bool { func filterAlbumMatches(f mediaprovider.AlbumFilter, album *subsonic.AlbumID3, ignoreGenre bool) bool {
filterOptions := f.Options()
if album == nil { if album == nil {
return false return false
} }
if f.ExcludeFavorited && !album.Starred.IsZero() { if filterOptions.ExcludeFavorited && !album.Starred.IsZero() {
return false return false
} }
if f.ExcludeUnfavorited && album.Starred.IsZero() { if filterOptions.ExcludeUnfavorited && album.Starred.IsZero() {
return false return false
} }
if y := album.Year; y < f.MinYear || (f.MaxYear > 0 && y > f.MaxYear) { if y := album.Year; y < filterOptions.MinYear || (filterOptions.MaxYear > 0 && y > filterOptions.MaxYear) {
return false return false
} }
if ignoreGenre || len(f.Genres) == 0 { if ignoreGenre || len(filterOptions.Genres) == 0 {
return true return true
} }
for _, g := range f.Genres { for _, g := range filterOptions.Genres {
if strings.EqualFold(g, album.Genre) { if strings.EqualFold(g, album.Genre) {
return true return true
} }
@@ -60,22 +61,29 @@ func filterMatches(f mediaprovider.AlbumFilter, album *subsonic.AlbumID3, ignore
} }
func (s *subsonicMediaProvider) IterateAlbums(sortOrder string, filter mediaprovider.AlbumFilter) mediaprovider.AlbumIterator { func (s *subsonicMediaProvider) IterateAlbums(sortOrder string, filter mediaprovider.AlbumFilter) mediaprovider.AlbumIterator {
if sortOrder == "" && len(filter.Genres) == 1 { filterOptions := filter.Options()
genre := filter.Genres[0] if sortOrder == "" && len(filterOptions.Genres) == 1 {
genre := filterOptions.Genres[0]
// The Subsonic API (non-OpenSubsonic) returns only the first genre for multi-genre albums, // The Subsonic API (non-OpenSubsonic) returns only the first genre for multi-genre albums,
// but servers do internally match against all the genres the album is categorized with. // but servers do internally match against all the genres the album is categorized with.
// So we must not additionally filter by genre to avoid excluding results where // So we must not additionally filter by genre to avoid excluding results where
// the single genre returned by Subsonic isn't the one we're iterating on. // the single genre returned by Subsonic isn't the one we're iterating on.
filter.Genres = nil modifiedFilter := filter.Clone()
modifiedOptions := modifiedFilter.Options()
modifiedOptions.Genres = nil
modifiedFilter.SetOptions(modifiedOptions)
fetchFn := func(offset, limit int) ([]*subsonic.AlbumID3, error) { fetchFn := func(offset, limit int) ([]*subsonic.AlbumID3, error) {
return s.client.GetAlbumList2("byGenre", return s.client.GetAlbumList2("byGenre",
map[string]string{"genre": genre, "offset": strconv.Itoa(offset), "limit": strconv.Itoa(limit)}) map[string]string{"genre": genre, "offset": strconv.Itoa(offset), "limit": strconv.Itoa(limit)})
} }
return helpers.NewAlbumIterator(makeFetchFn(fetchFn), filter, s.prefetchCoverCB) return helpers.NewAlbumIterator(makeFetchFn(fetchFn), modifiedFilter, s.prefetchCoverCB)
} }
if sortOrder == "" && filter.ExcludeUnfavorited { if sortOrder == "" && filterOptions.ExcludeUnfavorited {
filter.ExcludeUnfavorited = false // we're already filtering by this modifiedFilter := filter.Clone()
return s.baseIterFromSimpleSortOrder("starred", filter) modifiedOptions := modifiedFilter.Options()
modifiedOptions.ExcludeUnfavorited = false // we're already filtering by this
modifiedFilter.SetOptions(modifiedOptions)
return s.baseIterFromSimpleSortOrder("starred", modifiedFilter)
} }
if sortOrder == "" { if sortOrder == "" {
sortOrder = AlbumSortRecentlyAdded // default sortOrder = AlbumSortRecentlyAdded // default
@@ -112,10 +120,10 @@ func (s *subsonicMediaProvider) IterateAlbums(sortOrder string, filter mediaprov
} }
func (s *subsonicMediaProvider) SearchAlbums(searchQuery string, filter mediaprovider.AlbumFilter) mediaprovider.AlbumIterator { func (s *subsonicMediaProvider) SearchAlbums(searchQuery string, filter mediaprovider.AlbumFilter) mediaprovider.AlbumIterator {
return s.newSearchIter(searchQuery, filter, s.prefetchCoverCB) return s.newSearchAlbumIter(searchQuery, filter, s.prefetchCoverCB)
} }
type searchIter struct { type searchAlbumIter struct {
searchIterBase searchIterBase
prefetchCB func(string) prefetchCB func(string)
@@ -126,8 +134,8 @@ type searchIter struct {
done bool done bool
} }
func (s *subsonicMediaProvider) newSearchIter(query string, filter mediaprovider.AlbumFilter, cb func(string)) *searchIter { func (s *subsonicMediaProvider) newSearchAlbumIter(query string, filter mediaprovider.AlbumFilter, cb func(string)) *searchAlbumIter {
return &searchIter{ return &searchAlbumIter{
searchIterBase: searchIterBase{ searchIterBase: searchIterBase{
query: query, query: query,
s: s.client, s: s.client,
@@ -138,7 +146,7 @@ func (s *subsonicMediaProvider) newSearchIter(query string, filter mediaprovider
} }
} }
func (s *searchIter) Next() *mediaprovider.Album { func (s *searchAlbumIter) Next() *mediaprovider.Album {
if s.done { if s.done {
return nil return nil
} }
@@ -197,12 +205,12 @@ func (s *searchIter) Next() *mediaprovider.Album {
return nil return nil
} }
func (s *searchIter) addNewAlbums(al []*subsonic.AlbumID3) { func (s *searchAlbumIter) addNewAlbums(al []*subsonic.AlbumID3) {
for _, album := range al { for _, album := range al {
if _, have := s.albumIDset[album.ID]; have { if _, have := s.albumIDset[album.ID]; have {
continue continue
} }
if !filterMatches(s.filter, album, false) { if !filterAlbumMatches(s.filter, album, false) {
continue continue
} }
s.prefetched = append(s.prefetched, album) s.prefetched = append(s.prefetched, album)
@@ -10,8 +10,11 @@ import (
func (s *subsonicMediaProvider) IterateTracks(searchQuery string) mediaprovider.TrackIterator { func (s *subsonicMediaProvider) IterateTracks(searchQuery string) mediaprovider.TrackIterator {
if searchQuery == "" { if searchQuery == "" {
return &allTracksIterator{ return &allTracksIterator{
s: s, s: s,
albumIter: s.IterateAlbums(AlbumSortArtistAZ, mediaprovider.AlbumFilter{}), albumIter: s.IterateAlbums(
AlbumSortArtistAZ,
mediaprovider.NewAlbumFilter(mediaprovider.AlbumFilterOptions{}),
),
} }
} }
return &searchTracksIterator{ return &searchTracksIterator{
+20 -6
View File
@@ -14,10 +14,12 @@ import (
) )
type albumsPageAdapter struct { type albumsPageAdapter struct {
cfg *backend.AlbumsPageConfig cfg *backend.AlbumsPageConfig
contr *controller.Controller contr *controller.Controller
mp mediaprovider.MediaProvider mp mediaprovider.MediaProvider
pm *backend.PlaybackManager pm *backend.PlaybackManager
filter mediaprovider.AlbumFilter
filterBtn *widgets.AlbumFilterButton
} }
func NewAlbumsPage(cfg *backend.AlbumsPageConfig, pool *util.WidgetPool, contr *controller.Controller, pm *backend.PlaybackManager, mp mediaprovider.MediaProvider, im *backend.ImageManager) Page { func NewAlbumsPage(cfg *backend.AlbumsPageConfig, pool *util.WidgetPool, contr *controller.Controller, pm *backend.PlaybackManager, mp mediaprovider.MediaProvider, im *backend.ImageManager) Page {
@@ -27,8 +29,20 @@ func NewAlbumsPage(cfg *backend.AlbumsPageConfig, pool *util.WidgetPool, contr *
func (a *albumsPageAdapter) Title() string { return "Albums" } func (a *albumsPageAdapter) Title() string { return "Albums" }
func (a *albumsPageAdapter) Filter() *mediaprovider.AlbumFilter { func (a *albumsPageAdapter) Filter() mediaprovider.AlbumFilter {
return &mediaprovider.AlbumFilter{} if a.filter == nil {
a.filter = mediaprovider.NewAlbumFilter(
mediaprovider.AlbumFilterOptions{},
)
}
return a.filter
}
func (a *albumsPageAdapter) FilterButton() widgets.FilterButton[mediaprovider.Album, mediaprovider.AlbumFilterOptions] {
if a.filterBtn == nil {
a.filterBtn = widgets.NewAlbumFilterButton(a.Filter(), a.mp.GetGenres)
}
return a.filterBtn
} }
func (a *albumsPageAdapter) PlaceholderResource() fyne.Resource { return myTheme.AlbumIcon } func (a *albumsPageAdapter) PlaceholderResource() fyne.Resource { return myTheme.AlbumIcon }
+10 -8
View File
@@ -51,13 +51,15 @@ type FavoritesPage struct {
func NewFavoritesPage(cfg *backend.FavoritesPageConfig, pool *util.WidgetPool, contr *controller.Controller, mp mediaprovider.MediaProvider, pm *backend.PlaybackManager, im *backend.ImageManager) *FavoritesPage { func NewFavoritesPage(cfg *backend.FavoritesPageConfig, pool *util.WidgetPool, contr *controller.Controller, mp mediaprovider.MediaProvider, pm *backend.PlaybackManager, im *backend.ImageManager) *FavoritesPage {
a := &FavoritesPage{ a := &FavoritesPage{
filter: mediaprovider.AlbumFilter{ExcludeUnfavorited: true}, filter: mediaprovider.NewAlbumFilter(mediaprovider.AlbumFilterOptions{
cfg: cfg, ExcludeUnfavorited: true,
pool: pool, }),
contr: contr, cfg: cfg,
pm: pm, pool: pool,
mp: mp, contr: contr,
im: im, pm: pm,
mp: mp,
im: im,
} }
a.ExtendBaseWidget(a) a.ExtendBaseWidget(a)
a.createHeader(0) a.createHeader(0)
@@ -95,7 +97,7 @@ func (a *FavoritesPage) createHeader(activeBtnIdx int) {
a.searcher.PlaceHolder = "Search page" a.searcher.PlaceHolder = "Search page"
a.searcher.OnSearched = a.OnSearched a.searcher.OnSearched = a.OnSearched
a.searcher.Entry.Text = a.searchText a.searcher.Entry.Text = a.searchText
a.filterBtn = widgets.NewAlbumFilterButton(&a.filter, a.mp.GetGenres) a.filterBtn = widgets.NewAlbumFilterButton(a.filter, a.mp.GetGenres)
a.filterBtn.FavoriteDisabled = true a.filterBtn.FavoriteDisabled = true
a.filterBtn.OnChanged = a.Reload a.filterBtn.OnChanged = a.Reload
} }
+23 -6
View File
@@ -13,10 +13,12 @@ import (
) )
type genrePageAdapter struct { type genrePageAdapter struct {
genre string genre string
contr *controller.Controller contr *controller.Controller
mp mediaprovider.MediaProvider mp mediaprovider.MediaProvider
pm *backend.PlaybackManager pm *backend.PlaybackManager
filter mediaprovider.AlbumFilter
filterBtn *widgets.AlbumFilterButton
} }
func NewGenrePage(genre string, pool *util.WidgetPool, contr *controller.Controller, pm *backend.PlaybackManager, mp mediaprovider.MediaProvider, im *backend.ImageManager) Page { func NewGenrePage(genre string, pool *util.WidgetPool, contr *controller.Controller, pm *backend.PlaybackManager, mp mediaprovider.MediaProvider, im *backend.ImageManager) Page {
@@ -26,8 +28,23 @@ func NewGenrePage(genre string, pool *util.WidgetPool, contr *controller.Control
func (g *genrePageAdapter) Title() string { return g.genre } func (g *genrePageAdapter) Title() string { return g.genre }
func (g *genrePageAdapter) Filter() *mediaprovider.AlbumFilter { func (g *genrePageAdapter) Filter() mediaprovider.AlbumFilter {
return &mediaprovider.AlbumFilter{Genres: []string{g.genre}} if g.filter == nil {
g.filter = mediaprovider.NewAlbumFilter(
mediaprovider.AlbumFilterOptions{
Genres: []string{g.genre},
},
)
}
return g.filter
}
func (g *genrePageAdapter) FilterButton() widgets.FilterButton[mediaprovider.Album, mediaprovider.AlbumFilterOptions] {
if g.filterBtn == nil {
g.filterBtn = widgets.NewAlbumFilterButton(g.Filter(), func() ([]*mediaprovider.Genre, error) { return nil, nil })
g.filterBtn.GenreDisabled = true
}
return g.filterBtn
} }
func (g *genrePageAdapter) PlaceholderResource() fyne.Resource { func (g *genrePageAdapter) PlaceholderResource() fyne.Resource {
+47 -60
View File
@@ -14,13 +14,11 @@ import (
"fyne.io/fyne/v2/widget" "fyne.io/fyne/v2/widget"
) )
var _ Page = (*GridViewPage)(nil)
// Base widget for grid view pages // Base widget for grid view pages
type GridViewPage struct { type GridViewPage[M, F any] struct {
widget.BaseWidget widget.BaseWidget
adapter GridViewPageAdapter adapter GridViewPageAdapter[M, F]
pool *util.WidgetPool pool *util.WidgetPool
mp mediaprovider.MediaProvider mp mediaprovider.MediaProvider
im *backend.ImageManager im *backend.ImageManager
@@ -31,8 +29,8 @@ type GridViewPage struct {
title *widget.RichText title *widget.RichText
sortOrder *sortOrderSelect sortOrder *sortOrderSelect
filterBtn *widgets.AlbumFilterButton filterBtn widgets.FilterButton[M, F]
filter *mediaprovider.AlbumFilter filter mediaprovider.MediaFilter[M, F]
searcher *widgets.SearchEntry searcher *widgets.SearchEntry
searchText string searchText string
@@ -40,14 +38,17 @@ type GridViewPage struct {
} }
// Base type for pages that show an iterable GridView // Base type for pages that show an iterable GridView
type GridViewPageAdapter interface { type GridViewPageAdapter[M, F any] interface {
// Returns the title for the page // Returns the title for the page
Title() string Title() string
// Returns the base album filter for this page, if any. // Returns the base media filter for this page, if any.
// A filterable page with no base filters applied should return a zero-valued // A filterable page with no base filters applied should return a zero-valued
// *AlbumFilter, *not* nil. (Nil means unfilterable and no filter button created.) // filter pointer, *not* nil. (Nil means unfilterable and no filter button created.)
Filter() *mediaprovider.AlbumFilter Filter() mediaprovider.MediaFilter[M, F]
// Returns the filter button for the page, if any.
FilterButton() widgets.FilterButton[M, F]
// Returns the cover placeholder resource for the page // Returns the cover placeholder resource for the page
PlaceholderResource() fyne.Resource PlaceholderResource() fyne.Resource
@@ -59,11 +60,11 @@ type GridViewPageAdapter interface {
ActionButton() *widget.Button ActionButton() *widget.Button
// Returns the iterator for the given sortOrder and filter. // Returns the iterator for the given sortOrder and filter.
// (Non-album pages can ignore the filter argument) // (Non-media pages can ignore the filter argument)
Iter(sortOrder string, filter mediaprovider.AlbumFilter) widgets.GridViewIterator Iter(sortOrder string, filter mediaprovider.MediaFilter[M, F]) widgets.GridViewIterator
// Returns the iterator for the given search query and filter. // Returns the iterator for the given search query and filter.
SearchIter(query string, filter mediaprovider.AlbumFilter) widgets.GridViewIterator SearchIter(query string, filter mediaprovider.MediaFilter[M, F]) widgets.GridViewIterator
// Function that connects the GridView callbacks to the appropriate action handlers. // Function that connects the GridView callbacks to the appropriate action handlers.
ConnectGridActions(*widgets.GridView) ConnectGridActions(*widgets.GridView)
@@ -96,18 +97,19 @@ func (s *sortOrderSelect) MinSize() fyne.Size {
return fyne.NewSize(170, s.Select.MinSize().Height) return fyne.NewSize(170, s.Select.MinSize().Height)
} }
func NewGridViewPage( func NewGridViewPage[M, F any](
adapter GridViewPageAdapter, adapter GridViewPageAdapter[M, F],
pool *util.WidgetPool, pool *util.WidgetPool,
mp mediaprovider.MediaProvider, mp mediaprovider.MediaProvider,
im *backend.ImageManager, im *backend.ImageManager,
) *GridViewPage { ) *GridViewPage[M, F] {
gp := &GridViewPage{ gp := &GridViewPage[M, F]{
adapter: adapter, adapter: adapter,
pool: pool, pool: pool,
mp: mp, mp: mp,
im: im, im: im,
filter: adapter.Filter(), filter: adapter.Filter(),
filterBtn: adapter.FilterButton(),
} }
gp.ExtendBaseWidget(gp) gp.ExtendBaseWidget(gp)
gp.createTitleAndSort() gp.createTitleAndSort()
@@ -128,7 +130,7 @@ func NewGridViewPage(
return gp return gp
} }
func (g *GridViewPage) createTitleAndSort() { func (g *GridViewPage[M, F]) createTitleAndSort() {
g.title = widget.NewRichText(&widget.TextSegment{ g.title = widget.NewRichText(&widget.TextSegment{
Text: g.adapter.Title(), Text: g.adapter.Title(),
Style: widget.RichTextStyle{SizeName: theme.SizeNameHeadingText}, Style: widget.RichTextStyle{SizeName: theme.SizeNameHeadingText},
@@ -140,25 +142,17 @@ func (g *GridViewPage) createTitleAndSort() {
} }
} }
func (g *GridViewPage) createSearchAndFilter() { func (g *GridViewPage[M, F]) createSearchAndFilter() {
g.searcher = widgets.NewSearchEntry() g.searcher = widgets.NewSearchEntry()
g.searcher.PlaceHolder = "Search page" g.searcher.PlaceHolder = "Search page"
g.searcher.Text = g.searchText g.searcher.Text = g.searchText
g.searcher.OnSearched = g.OnSearched g.searcher.OnSearched = g.OnSearched
if g.filter != nil { if g.filterBtn != nil {
disableGenres := len(g.filter.Genres) > 0 g.filterBtn.SetOnChanged(g.Reload)
genreFn := g.mp.GetGenres
if disableGenres {
// genre filter is disabled for this page, so no need to actually call genre list fetching function
genreFn = func() ([]*mediaprovider.Genre, error) { return nil, nil }
}
g.filterBtn = widgets.NewAlbumFilterButton(g.filter, genreFn)
g.filterBtn.GenreDisabled = disableGenres
g.filterBtn.OnChanged = g.Reload
} }
} }
func (g *GridViewPage) createContainer() { func (g *GridViewPage[M, F]) createContainer() {
header := container.NewHBox(util.NewHSpace(6), g.title) header := container.NewHBox(util.NewHSpace(6), g.title)
if g.sortOrder != nil { if g.sortOrder != nil {
header.Add(container.NewCenter(g.sortOrder)) header.Add(container.NewCenter(g.sortOrder))
@@ -175,7 +169,7 @@ func (g *GridViewPage) createContainer() {
g.container = container.NewBorder(header, nil, nil, nil, g.grid) g.container = container.NewBorder(header, nil, nil, nil, g.grid)
} }
func (g *GridViewPage) Reload() { func (g *GridViewPage[M, F]) Reload() {
if g.searchText != "" { if g.searchText != "" {
g.doSearch(g.searchText) g.doSearch(g.searchText)
} else { } else {
@@ -183,23 +177,19 @@ func (g *GridViewPage) Reload() {
} }
} }
func (g *GridViewPage) Route() controller.Route { func (g *GridViewPage[M, F]) Route() controller.Route {
return g.adapter.Route() return g.adapter.Route()
} }
var _ Searchable = (*GridViewPage)(nil) func (g *GridViewPage[M, F]) SearchWidget() fyne.Focusable {
func (g *GridViewPage) SearchWidget() fyne.Focusable {
return g.searcher return g.searcher
} }
var _ Scrollable = (*GridViewPage)(nil) func (g *GridViewPage[M, F]) Scroll(scrollAmt float32) {
func (g *GridViewPage) Scroll(scrollAmt float32) {
g.grid.ScrollToOffset(g.grid.GetScrollOffset() + scrollAmt) g.grid.ScrollToOffset(g.grid.GetScrollOffset() + scrollAmt)
} }
func (g *GridViewPage) OnSearched(query string) { func (g *GridViewPage[M, F]) OnSearched(query string) {
if query == "" { if query == "" {
if g.sortOrder != nil { if g.sortOrder != nil {
g.sortOrder.Enable() g.sortOrder.Enable()
@@ -215,50 +205,47 @@ func (g *GridViewPage) OnSearched(query string) {
g.searchText = query g.searchText = query
} }
func (g *GridViewPage) doSearch(query string) { func (g *GridViewPage[M, F]) doSearch(query string) {
if g.searchText == "" { if g.searchText == "" {
g.gridState = g.grid.SaveToState() g.gridState = g.grid.SaveToState()
} }
g.grid.Reset(g.adapter.SearchIter(query, g.getFilter())) g.grid.Reset(g.adapter.SearchIter(query, g.getFilter()))
} }
func (g *GridViewPage) onSortOrderChanged(order string) { func (g *GridViewPage[M, F]) onSortOrderChanged(order string) {
g.adapter.(SortableGridViewPageAdapter).SaveSortOrder(g.getSortOrder()) g.adapter.(SortableGridViewPageAdapter).SaveSortOrder(g.getSortOrder())
g.grid.Reset(g.adapter.Iter(g.getSortOrder(), g.getFilter())) g.grid.Reset(g.adapter.Iter(g.getSortOrder(), g.getFilter()))
} }
func (g *GridViewPage) getFilter() mediaprovider.AlbumFilter { func (g *GridViewPage[M, F]) getFilter() mediaprovider.MediaFilter[M, F] {
if g.filter != nil { return g.filter
return *g.filter
}
return mediaprovider.AlbumFilter{}
} }
func (g *GridViewPage) getSortOrder() string { func (g *GridViewPage[M, F]) getSortOrder() string {
if g.sortOrder != nil { if g.sortOrder != nil {
return g.sortOrder.Selected return g.sortOrder.Selected
} }
return "" return ""
} }
func (g *GridViewPage) CreateRenderer() fyne.WidgetRenderer { func (g *GridViewPage[M, F]) CreateRenderer() fyne.WidgetRenderer {
return widget.NewSimpleRenderer(g.container) return widget.NewSimpleRenderer(g.container)
} }
type savedGridViewPage struct { type savedGridViewPage[M, F any] struct {
adapter GridViewPageAdapter adapter GridViewPageAdapter[M, F]
im *backend.ImageManager im *backend.ImageManager
mp mediaprovider.MediaProvider mp mediaprovider.MediaProvider
searchText string searchText string
filter *mediaprovider.AlbumFilter filter mediaprovider.MediaFilter[M, F]
pool *util.WidgetPool pool *util.WidgetPool
sortOrder string sortOrder string
gridState *widgets.GridViewState gridState *widgets.GridViewState
searchGridState *widgets.GridViewState searchGridState *widgets.GridViewState
} }
func (g *GridViewPage) Save() SavedPage { func (g *GridViewPage[M, F]) Save() SavedPage {
sa := &savedGridViewPage{ sa := &savedGridViewPage[M, F]{
adapter: g.adapter, adapter: g.adapter,
pool: g.pool, pool: g.pool,
mp: g.mp, mp: g.mp,
@@ -279,8 +266,8 @@ func (g *GridViewPage) Save() SavedPage {
return sa return sa
} }
func (s *savedGridViewPage) Restore() Page { func (s *savedGridViewPage[M, F]) Restore() Page {
gp := &GridViewPage{ gp := &GridViewPage[M, F]{
adapter: s.adapter, adapter: s.adapter,
pool: s.pool, pool: s.pool,
mp: s.mp, mp: s.mp,
+37 -17
View File
@@ -30,11 +30,11 @@ type AlbumFilterButton struct {
genreListChan chan []string genreListChan chan []string
filter *mediaprovider.AlbumFilter filter mediaprovider.AlbumFilter
dialog *widget.PopUp dialog *widget.PopUp
} }
func NewAlbumFilterButton(filter *mediaprovider.AlbumFilter, fetchGenresFunc func() ([]*mediaprovider.Genre, error)) *AlbumFilterButton { func NewAlbumFilterButton(filter mediaprovider.AlbumFilter, fetchGenresFunc func() ([]*mediaprovider.Genre, error)) *AlbumFilterButton {
a := &AlbumFilterButton{ a := &AlbumFilterButton{
filter: filter, filter: filter,
Button: widget.Button{ Button: widget.Button{
@@ -66,10 +66,19 @@ func (a *AlbumFilterButton) Refresh() {
a.Button.Refresh() a.Button.Refresh()
} }
func (a *AlbumFilterButton) Filter() mediaprovider.AlbumFilter {
return a.filter
}
func (a *AlbumFilterButton) SetOnChanged(fn func()) {
a.OnChanged = fn
}
func (a *AlbumFilterButton) filterEmpty() bool { func (a *AlbumFilterButton) filterEmpty() bool {
return a.filter.MinYear == 0 && a.filter.MaxYear == 0 && filterOptions := a.filter.Options()
(a.FavoriteDisabled || !a.filter.ExcludeFavorited && !a.filter.ExcludeUnfavorited) && return filterOptions.MinYear == 0 && filterOptions.MaxYear == 0 &&
(a.GenreDisabled || len(a.filter.Genres) == 0) (a.FavoriteDisabled || !filterOptions.ExcludeFavorited && !filterOptions.ExcludeUnfavorited) &&
(a.GenreDisabled || len(filterOptions.Genres) == 0)
} }
func (a *AlbumFilterButton) onFilterChanged() { func (a *AlbumFilterButton) onFilterChanged() {
@@ -115,53 +124,64 @@ func NewAlbumFilterPopup(filter *AlbumFilterButton) *AlbumFilterPopup {
minYear := NewTextRestrictedEntry(yearValidator) minYear := NewTextRestrictedEntry(yearValidator)
minYear.SetMinCharWidth(4) minYear.SetMinCharWidth(4)
minYear.OnChanged = func(yearStr string) { minYear.OnChanged = func(yearStr string) {
filterOptions := a.filterBtn.filter.Options()
if yearStr == "" { if yearStr == "" {
a.filterBtn.filter.MinYear = 0 filterOptions.MinYear = 0
} else if i, err := strconv.Atoi(yearStr); err == nil { } else if i, err := strconv.Atoi(yearStr); err == nil {
a.filterBtn.filter.MinYear = i filterOptions.MinYear = i
} }
a.filterBtn.filter.SetOptions(filterOptions)
debounceOnChanged() debounceOnChanged()
} }
if a.filterBtn.filter.MinYear > 0 { filterOptions := a.filterBtn.filter.Options()
minYear.Text = strconv.Itoa(a.filterBtn.filter.MinYear) if filterOptions.MinYear > 0 {
minYear.Text = strconv.Itoa(filterOptions.MinYear)
} }
maxYear := NewTextRestrictedEntry(yearValidator) maxYear := NewTextRestrictedEntry(yearValidator)
maxYear.SetMinCharWidth(4) maxYear.SetMinCharWidth(4)
maxYear.OnChanged = func(yearStr string) { maxYear.OnChanged = func(yearStr string) {
filterOptions := a.filterBtn.filter.Options()
if yearStr == "" { if yearStr == "" {
a.filterBtn.filter.MaxYear = 0 filterOptions.MaxYear = 0
} else if i, err := strconv.Atoi(yearStr); err == nil { } else if i, err := strconv.Atoi(yearStr); err == nil {
a.filterBtn.filter.MaxYear = i filterOptions.MaxYear = i
} }
a.filterBtn.filter.SetOptions(filterOptions)
debounceOnChanged() debounceOnChanged()
} }
if a.filterBtn.filter.MaxYear > 0 { if filterOptions.MaxYear > 0 {
maxYear.Text = strconv.Itoa(a.filterBtn.filter.MaxYear) maxYear.Text = strconv.Itoa(filterOptions.MaxYear)
} }
// setup is favorite/not favorite filters // setup is favorite/not favorite filters
a.isFavorite = widget.NewCheck("Is favorite", func(fav bool) { a.isFavorite = widget.NewCheck("Is favorite", func(fav bool) {
filterOptions := a.filterBtn.filter.Options()
if fav { if fav {
a.isNotFavorite.SetChecked(false) a.isNotFavorite.SetChecked(false)
} }
a.filterBtn.filter.ExcludeUnfavorited = fav filterOptions.ExcludeUnfavorited = fav
a.filterBtn.filter.SetOptions(filterOptions)
debounceOnChanged() debounceOnChanged()
}) })
a.isFavorite.Hidden = a.filterBtn.FavoriteDisabled a.isFavorite.Hidden = a.filterBtn.FavoriteDisabled
a.isNotFavorite = widget.NewCheck("Is not favorite", func(fav bool) { a.isNotFavorite = widget.NewCheck("Is not favorite", func(fav bool) {
filterOptions := a.filterBtn.filter.Options()
if fav { if fav {
a.isFavorite.SetChecked(false) a.isFavorite.SetChecked(false)
} }
a.filterBtn.filter.ExcludeFavorited = fav filterOptions.ExcludeFavorited = fav
a.filterBtn.filter.SetOptions(filterOptions)
debounceOnChanged() debounceOnChanged()
}) })
a.isNotFavorite.Hidden = a.filterBtn.FavoriteDisabled a.isNotFavorite.Hidden = a.filterBtn.FavoriteDisabled
// create genre filter subsection // create genre filter subsection
a.genreFilter = NewGenreFilterSubsection(func(selectedGenres []string) { a.genreFilter = NewGenreFilterSubsection(func(selectedGenres []string) {
a.filterBtn.filter.Genres = selectedGenres filterOptions := a.filterBtn.filter.Options()
filterOptions.Genres = selectedGenres
a.filterBtn.filter.SetOptions(filterOptions)
debounceOnChanged() debounceOnChanged()
}, a.filterBtn.filter.Genres) }, filterOptions.Genres)
a.genreFilter.Hidden = a.filterBtn.GenreDisabled a.genreFilter.Hidden = a.filterBtn.GenreDisabled
// setup container // setup container
+13
View File
@@ -0,0 +1,13 @@
package widgets
import (
"fyne.io/fyne/v2"
"github.com/dweymouth/supersonic/backend/mediaprovider"
)
type FilterButton[M, F any] interface {
fyne.CanvasObject
Filter() mediaprovider.MediaFilter[M, F]
SetOnChanged(func())
}
+10 -10
View File
@@ -18,23 +18,23 @@ import (
const batchFetchSize = 6 const batchFetchSize = 6
type BatchingIterator struct { type BatchingIterator[M any] struct {
iter mediaprovider.AlbumIterator iter mediaprovider.MediaIterator[M]
} }
func NewBatchingIterator(iter mediaprovider.AlbumIterator) BatchingIterator { func NewBatchingIterator[M any](iter mediaprovider.MediaIterator[M]) BatchingIterator[M] {
return BatchingIterator{iter} return BatchingIterator[M]{iter}
} }
func (b *BatchingIterator) NextN(n int) []*mediaprovider.Album { func (b *BatchingIterator[M]) NextN(n int) []*M {
results := make([]*mediaprovider.Album, 0, n) results := make([]*M, 0, n)
i := 0 i := 0
for i < n { for i < n {
album := b.iter.Next() value := b.iter.Next()
if album == nil { if value == nil {
break break
} }
results = append(results, album) results = append(results, value)
i++ i++
} }
return results return results
@@ -45,7 +45,7 @@ type GridViewIterator interface {
} }
type gridViewAlbumIterator struct { type gridViewAlbumIterator struct {
iter BatchingIterator iter BatchingIterator[mediaprovider.Album]
} }
func (g gridViewAlbumIterator) NextN(n int) []GridViewItemModel { func (g gridViewAlbumIterator) NextN(n int) []GridViewItemModel {