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
+20 -6
View File
@@ -14,10 +14,12 @@ import (
)
type albumsPageAdapter struct {
cfg *backend.AlbumsPageConfig
contr *controller.Controller
mp mediaprovider.MediaProvider
pm *backend.PlaybackManager
cfg *backend.AlbumsPageConfig
contr *controller.Controller
mp mediaprovider.MediaProvider
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 {
@@ -27,8 +29,20 @@ func NewAlbumsPage(cfg *backend.AlbumsPageConfig, pool *util.WidgetPool, contr *
func (a *albumsPageAdapter) Title() string { return "Albums" }
func (a *albumsPageAdapter) Filter() *mediaprovider.AlbumFilter {
return &mediaprovider.AlbumFilter{}
func (a *albumsPageAdapter) Filter() 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 }
+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 {
a := &FavoritesPage{
filter: mediaprovider.AlbumFilter{ExcludeUnfavorited: true},
cfg: cfg,
pool: pool,
contr: contr,
pm: pm,
mp: mp,
im: im,
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)
@@ -95,7 +97,7 @@ func (a *FavoritesPage) createHeader(activeBtnIdx int) {
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 = widgets.NewAlbumFilterButton(a.filter, a.mp.GetGenres)
a.filterBtn.FavoriteDisabled = true
a.filterBtn.OnChanged = a.Reload
}
+23 -6
View File
@@ -13,10 +13,12 @@ import (
)
type genrePageAdapter struct {
genre string
contr *controller.Controller
mp mediaprovider.MediaProvider
pm *backend.PlaybackManager
genre string
contr *controller.Controller
mp mediaprovider.MediaProvider
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 {
@@ -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) Filter() *mediaprovider.AlbumFilter {
return &mediaprovider.AlbumFilter{Genres: []string{g.genre}}
func (g *genrePageAdapter) Filter() mediaprovider.AlbumFilter {
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 {
+47 -60
View File
@@ -14,13 +14,11 @@ import (
"fyne.io/fyne/v2/widget"
)
var _ Page = (*GridViewPage)(nil)
// Base widget for grid view pages
type GridViewPage struct {
type GridViewPage[M, F any] struct {
widget.BaseWidget
adapter GridViewPageAdapter
adapter GridViewPageAdapter[M, F]
pool *util.WidgetPool
mp mediaprovider.MediaProvider
im *backend.ImageManager
@@ -31,8 +29,8 @@ type GridViewPage struct {
title *widget.RichText
sortOrder *sortOrderSelect
filterBtn *widgets.AlbumFilterButton
filter *mediaprovider.AlbumFilter
filterBtn widgets.FilterButton[M, F]
filter mediaprovider.MediaFilter[M, F]
searcher *widgets.SearchEntry
searchText string
@@ -40,14 +38,17 @@ type GridViewPage struct {
}
// Base type for pages that show an iterable GridView
type GridViewPageAdapter interface {
type GridViewPageAdapter[M, F any] interface {
// Returns the title for the page
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
// *AlbumFilter, *not* nil. (Nil means unfilterable and no filter button created.)
Filter() *mediaprovider.AlbumFilter
// filter pointer, *not* nil. (Nil means unfilterable and no filter button created.)
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
PlaceholderResource() fyne.Resource
@@ -59,11 +60,11 @@ type GridViewPageAdapter interface {
ActionButton() *widget.Button
// Returns the iterator for the given sortOrder and filter.
// (Non-album pages can ignore the filter argument)
Iter(sortOrder string, filter mediaprovider.AlbumFilter) widgets.GridViewIterator
// (Non-media pages can ignore the filter argument)
Iter(sortOrder string, filter mediaprovider.MediaFilter[M, F]) widgets.GridViewIterator
// 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.
ConnectGridActions(*widgets.GridView)
@@ -96,18 +97,19 @@ func (s *sortOrderSelect) MinSize() fyne.Size {
return fyne.NewSize(170, s.Select.MinSize().Height)
}
func NewGridViewPage(
adapter GridViewPageAdapter,
func NewGridViewPage[M, F any](
adapter GridViewPageAdapter[M, F],
pool *util.WidgetPool,
mp mediaprovider.MediaProvider,
im *backend.ImageManager,
) *GridViewPage {
gp := &GridViewPage{
adapter: adapter,
pool: pool,
mp: mp,
im: im,
filter: adapter.Filter(),
) *GridViewPage[M, F] {
gp := &GridViewPage[M, F]{
adapter: adapter,
pool: pool,
mp: mp,
im: im,
filter: adapter.Filter(),
filterBtn: adapter.FilterButton(),
}
gp.ExtendBaseWidget(gp)
gp.createTitleAndSort()
@@ -128,7 +130,7 @@ func NewGridViewPage(
return gp
}
func (g *GridViewPage) createTitleAndSort() {
func (g *GridViewPage[M, F]) createTitleAndSort() {
g.title = widget.NewRichText(&widget.TextSegment{
Text: g.adapter.Title(),
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.PlaceHolder = "Search page"
g.searcher.Text = g.searchText
g.searcher.OnSearched = g.OnSearched
if g.filter != nil {
disableGenres := len(g.filter.Genres) > 0
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
if g.filterBtn != nil {
g.filterBtn.SetOnChanged(g.Reload)
}
}
func (g *GridViewPage) createContainer() {
func (g *GridViewPage[M, F]) createContainer() {
header := container.NewHBox(util.NewHSpace(6), g.title)
if g.sortOrder != nil {
header.Add(container.NewCenter(g.sortOrder))
@@ -175,7 +169,7 @@ func (g *GridViewPage) createContainer() {
g.container = container.NewBorder(header, nil, nil, nil, g.grid)
}
func (g *GridViewPage) Reload() {
func (g *GridViewPage[M, F]) Reload() {
if g.searchText != "" {
g.doSearch(g.searchText)
} 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()
}
var _ Searchable = (*GridViewPage)(nil)
func (g *GridViewPage) SearchWidget() fyne.Focusable {
func (g *GridViewPage[M, F]) SearchWidget() fyne.Focusable {
return g.searcher
}
var _ Scrollable = (*GridViewPage)(nil)
func (g *GridViewPage) Scroll(scrollAmt float32) {
func (g *GridViewPage[M, F]) Scroll(scrollAmt float32) {
g.grid.ScrollToOffset(g.grid.GetScrollOffset() + scrollAmt)
}
func (g *GridViewPage) OnSearched(query string) {
func (g *GridViewPage[M, F]) OnSearched(query string) {
if query == "" {
if g.sortOrder != nil {
g.sortOrder.Enable()
@@ -215,50 +205,47 @@ func (g *GridViewPage) OnSearched(query string) {
g.searchText = query
}
func (g *GridViewPage) doSearch(query string) {
func (g *GridViewPage[M, F]) doSearch(query string) {
if g.searchText == "" {
g.gridState = g.grid.SaveToState()
}
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.grid.Reset(g.adapter.Iter(g.getSortOrder(), g.getFilter()))
}
func (g *GridViewPage) getFilter() mediaprovider.AlbumFilter {
if g.filter != nil {
return *g.filter
}
return mediaprovider.AlbumFilter{}
func (g *GridViewPage[M, F]) getFilter() mediaprovider.MediaFilter[M, F] {
return g.filter
}
func (g *GridViewPage) getSortOrder() string {
func (g *GridViewPage[M, F]) getSortOrder() string {
if g.sortOrder != nil {
return g.sortOrder.Selected
}
return ""
}
func (g *GridViewPage) CreateRenderer() fyne.WidgetRenderer {
func (g *GridViewPage[M, F]) CreateRenderer() fyne.WidgetRenderer {
return widget.NewSimpleRenderer(g.container)
}
type savedGridViewPage struct {
adapter GridViewPageAdapter
type savedGridViewPage[M, F any] struct {
adapter GridViewPageAdapter[M, F]
im *backend.ImageManager
mp mediaprovider.MediaProvider
searchText string
filter *mediaprovider.AlbumFilter
filter mediaprovider.MediaFilter[M, F]
pool *util.WidgetPool
sortOrder string
gridState *widgets.GridViewState
searchGridState *widgets.GridViewState
}
func (g *GridViewPage) Save() SavedPage {
sa := &savedGridViewPage{
func (g *GridViewPage[M, F]) Save() SavedPage {
sa := &savedGridViewPage[M, F]{
adapter: g.adapter,
pool: g.pool,
mp: g.mp,
@@ -279,8 +266,8 @@ func (g *GridViewPage) Save() SavedPage {
return sa
}
func (s *savedGridViewPage) Restore() Page {
gp := &GridViewPage{
func (s *savedGridViewPage[M, F]) Restore() Page {
gp := &GridViewPage[M, F]{
adapter: s.adapter,
pool: s.pool,
mp: s.mp,
+37 -17
View File
@@ -30,11 +30,11 @@ type AlbumFilterButton struct {
genreListChan chan []string
filter *mediaprovider.AlbumFilter
filter mediaprovider.AlbumFilter
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{
filter: filter,
Button: widget.Button{
@@ -66,10 +66,19 @@ func (a *AlbumFilterButton) 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 {
return a.filter.MinYear == 0 && a.filter.MaxYear == 0 &&
(a.FavoriteDisabled || !a.filter.ExcludeFavorited && !a.filter.ExcludeUnfavorited) &&
(a.GenreDisabled || len(a.filter.Genres) == 0)
filterOptions := a.filter.Options()
return filterOptions.MinYear == 0 && filterOptions.MaxYear == 0 &&
(a.FavoriteDisabled || !filterOptions.ExcludeFavorited && !filterOptions.ExcludeUnfavorited) &&
(a.GenreDisabled || len(filterOptions.Genres) == 0)
}
func (a *AlbumFilterButton) onFilterChanged() {
@@ -115,53 +124,64 @@ func NewAlbumFilterPopup(filter *AlbumFilterButton) *AlbumFilterPopup {
minYear := NewTextRestrictedEntry(yearValidator)
minYear.SetMinCharWidth(4)
minYear.OnChanged = func(yearStr string) {
filterOptions := a.filterBtn.filter.Options()
if yearStr == "" {
a.filterBtn.filter.MinYear = 0
filterOptions.MinYear = 0
} else if i, err := strconv.Atoi(yearStr); err == nil {
a.filterBtn.filter.MinYear = i
filterOptions.MinYear = i
}
a.filterBtn.filter.SetOptions(filterOptions)
debounceOnChanged()
}
if a.filterBtn.filter.MinYear > 0 {
minYear.Text = strconv.Itoa(a.filterBtn.filter.MinYear)
filterOptions := a.filterBtn.filter.Options()
if filterOptions.MinYear > 0 {
minYear.Text = strconv.Itoa(filterOptions.MinYear)
}
maxYear := NewTextRestrictedEntry(yearValidator)
maxYear.SetMinCharWidth(4)
maxYear.OnChanged = func(yearStr string) {
filterOptions := a.filterBtn.filter.Options()
if yearStr == "" {
a.filterBtn.filter.MaxYear = 0
filterOptions.MaxYear = 0
} else if i, err := strconv.Atoi(yearStr); err == nil {
a.filterBtn.filter.MaxYear = i
filterOptions.MaxYear = i
}
a.filterBtn.filter.SetOptions(filterOptions)
debounceOnChanged()
}
if a.filterBtn.filter.MaxYear > 0 {
maxYear.Text = strconv.Itoa(a.filterBtn.filter.MaxYear)
if filterOptions.MaxYear > 0 {
maxYear.Text = strconv.Itoa(filterOptions.MaxYear)
}
// setup is favorite/not favorite filters
a.isFavorite = widget.NewCheck("Is favorite", func(fav bool) {
filterOptions := a.filterBtn.filter.Options()
if fav {
a.isNotFavorite.SetChecked(false)
}
a.filterBtn.filter.ExcludeUnfavorited = fav
filterOptions.ExcludeUnfavorited = fav
a.filterBtn.filter.SetOptions(filterOptions)
debounceOnChanged()
})
a.isFavorite.Hidden = a.filterBtn.FavoriteDisabled
a.isNotFavorite = widget.NewCheck("Is not favorite", func(fav bool) {
filterOptions := a.filterBtn.filter.Options()
if fav {
a.isFavorite.SetChecked(false)
}
a.filterBtn.filter.ExcludeFavorited = fav
filterOptions.ExcludeFavorited = fav
a.filterBtn.filter.SetOptions(filterOptions)
debounceOnChanged()
})
a.isNotFavorite.Hidden = a.filterBtn.FavoriteDisabled
// create genre filter subsection
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()
}, a.filterBtn.filter.Genres)
}, filterOptions.Genres)
a.genreFilter.Hidden = a.filterBtn.GenreDisabled
// 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
type BatchingIterator struct {
iter mediaprovider.AlbumIterator
type BatchingIterator[M any] struct {
iter mediaprovider.MediaIterator[M]
}
func NewBatchingIterator(iter mediaprovider.AlbumIterator) BatchingIterator {
return BatchingIterator{iter}
func NewBatchingIterator[M any](iter mediaprovider.MediaIterator[M]) BatchingIterator[M] {
return BatchingIterator[M]{iter}
}
func (b *BatchingIterator) NextN(n int) []*mediaprovider.Album {
results := make([]*mediaprovider.Album, 0, n)
func (b *BatchingIterator[M]) NextN(n int) []*M {
results := make([]*M, 0, n)
i := 0
for i < n {
album := b.iter.Next()
if album == nil {
value := b.iter.Next()
if value == nil {
break
}
results = append(results, album)
results = append(results, value)
i++
}
return results
@@ -45,7 +45,7 @@ type GridViewIterator interface {
}
type gridViewAlbumIterator struct {
iter BatchingIterator
iter BatchingIterator[mediaprovider.Album]
}
func (g gridViewAlbumIterator) NextN(n int) []GridViewItemModel {