From 882f951aa19aed283888097204423e8ca27927e7 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Mon, 8 May 2023 18:32:52 -0700 Subject: [PATCH 1/8] add AlbumFilter to backend album iterators --- backend/albumiterator.go | 64 ++++++++++++++++++++++++++++-------- backend/trackiterator.go | 2 +- sharedutil/sharedutil.go | 6 ++++ ui/browsing/albumspage.go | 6 ++-- ui/browsing/favoritespage.go | 6 ++-- ui/browsing/genrepage.go | 5 +-- 6 files changed, 68 insertions(+), 21 deletions(-) diff --git a/backend/albumiterator.go b/backend/albumiterator.go index d1d7cfa..ab47e6b 100644 --- a/backend/albumiterator.go +++ b/backend/albumiterator.go @@ -3,8 +3,10 @@ package backend import ( "log" "strconv" + "strings" "github.com/dweymouth/go-subsonic/subsonic" + "github.com/dweymouth/supersonic/sharedutil" ) type AlbumSortOrder string @@ -33,36 +35,69 @@ var ( } ) -func (l *LibraryManager) AlbumsIter(sort AlbumSortOrder) AlbumIterator { +type AlbumFilter struct { + MinYear int + MaxYear int // 0 == unset/match any + Genres []string // len(0) == unset/match any + + ExcludeFavorited bool // mut. exc. with ExcludeUnfavorited + ExcludeUnfavorited bool // mut. exc. with ExcludeFavorited +} + +func (f *AlbumFilter) Matches(album *subsonic.AlbumID3) bool { + if album == nil { + return false + } + if f.ExcludeFavorited && !album.Starred.IsZero() { + return false + } + if f.ExcludeUnfavorited && album.Starred.IsZero() { + return false + } + if y := album.Year; y < f.MinYear || (f.MaxYear > 0 && y > f.MaxYear) { + return false + } + if len(f.Genres) == 0 { + return true + } + for _, g := range f.Genres { + if strings.EqualFold(g, album.Genre) { + return true + } + } + return false +} + +func (l *LibraryManager) AlbumsIter(sort AlbumSortOrder, filter AlbumFilter) AlbumIterator { switch sort { case AlbumSortRecentlyAdded: - return l.newBaseIter("newest", make(map[string]string)) + return l.newBaseIter("newest", filter, make(map[string]string)) case AlbumSortRecentlyPlayed: - return l.newBaseIter("recent", make(map[string]string)) + return l.newBaseIter("recent", filter, make(map[string]string)) case AlbumSortFrequentlyPlayed: - return l.newBaseIter("frequent", make(map[string]string)) + return l.newBaseIter("frequent", filter, make(map[string]string)) case AlbumSortRandom: return l.newRandomIter() case AlbumSortTitleAZ: - return l.newBaseIter("alphabeticalByName", make(map[string]string)) + return l.newBaseIter("alphabeticalByName", filter, make(map[string]string)) case AlbumSortArtistAZ: - return l.newBaseIter("alphabeticalByArtist", make(map[string]string)) + return l.newBaseIter("alphabeticalByArtist", filter, make(map[string]string)) case AlbumSortYearAscending: - return l.newBaseIter("byYear", map[string]string{"fromYear": "0", "toYear": "3000"}) + return l.newBaseIter("byYear", filter, map[string]string{"fromYear": "0", "toYear": "3000"}) case AlbumSortYearDescending: - return l.newBaseIter("byYear", map[string]string{"fromYear": "3000", "toYear": "0"}) + return l.newBaseIter("byYear", filter, map[string]string{"fromYear": "3000", "toYear": "0"}) default: log.Printf("Undefined album sort order: %s", sort) return nil } } -func (l *LibraryManager) StarredIter() AlbumIterator { - return l.newBaseIter("starred", make(map[string]string)) +func (l *LibraryManager) StarredIter(filter AlbumFilter) AlbumIterator { + return l.newBaseIter("starred", filter, make(map[string]string)) } -func (l *LibraryManager) GenreIter(genre string) AlbumIterator { - return l.newBaseIter("byGenre", map[string]string{"genre": genre}) +func (l *LibraryManager) GenreIter(genre string, filter AlbumFilter) AlbumIterator { + return l.newBaseIter("byGenre", filter, map[string]string{"genre": genre}) } func (l *LibraryManager) SearchIter(query string) AlbumIterator { @@ -83,6 +118,7 @@ func (l *LibraryManager) GetAlbum(id string) (*subsonic.AlbumID3, error) { type baseIter struct { listType string + filter AlbumFilter pos int l *LibraryManager s *subsonic.Client @@ -92,9 +128,10 @@ type baseIter struct { done bool } -func (l *LibraryManager) newBaseIter(listType string, opts map[string]string) *baseIter { +func (l *LibraryManager) newBaseIter(listType string, filter AlbumFilter, opts map[string]string) *baseIter { return &baseIter{ listType: listType, + filter: filter, l: l, s: l.s.Server, opts: opts, @@ -123,6 +160,7 @@ func (r *baseIter) Next() *subsonic.AlbumID3 { log.Println(err) albums = nil } + albums = sharedutil.FilterSlice(albums, r.filter.Matches) if len(albums) == 0 { r.done = true return nil diff --git a/backend/trackiterator.go b/backend/trackiterator.go index 02ccefa..ef9fc98 100644 --- a/backend/trackiterator.go +++ b/backend/trackiterator.go @@ -9,7 +9,7 @@ import ( func (l *LibraryManager) AllTracksIterator() TrackIterator { return &allTracksIterator{ l: l, - albumIter: l.AlbumsIter(AlbumSortArtistAZ), + albumIter: l.AlbumsIter(AlbumSortArtistAZ, AlbumFilter{}), } } diff --git a/sharedutil/sharedutil.go b/sharedutil/sharedutil.go index 8502cde..d07098c 100644 --- a/sharedutil/sharedutil.go +++ b/sharedutil/sharedutil.go @@ -17,6 +17,9 @@ func SliceContains[T comparable](ts []T, t T) bool { } func FilterSlice[T any](ss []T, test func(T) bool) []T { + if ss == nil { + return nil + } result := make([]T, 0) for _, s := range ss { if test(s) { @@ -27,6 +30,9 @@ func FilterSlice[T any](ss []T, test func(T) bool) []T { } func MapSlice[T any, U any](ts []T, f func(T) U) []U { + if ts == nil { + return nil + } result := make([]U, len(ts)) for i, t := range ts { result[i] = f(t) diff --git a/ui/browsing/albumspage.go b/ui/browsing/albumspage.go index 5faf155..293851a 100644 --- a/ui/browsing/albumspage.go +++ b/ui/browsing/albumspage.go @@ -71,7 +71,7 @@ func NewAlbumsPage(cfg *backend.AlbumsPageConfig, contr *controller.Controller, cfg.SortOrder = string(backend.AlbumSortRecentlyAdded) } a.sortOrder.Selected = cfg.SortOrder - iter := lm.AlbumsIter(backend.AlbumSortOrder(a.sortOrder.Selected)) + iter := lm.AlbumsIter(backend.AlbumSortOrder(a.sortOrder.Selected), backend.AlbumFilter{}) a.grid = widgets.NewGridView(widgets.NewGridViewAlbumIterator(iter), im) contr.ConnectAlbumGridActions(a.grid) a.searcher = widgets.NewSearchEntry() @@ -154,7 +154,7 @@ func (a *AlbumsPage) Reload() { if a.searchText != "" { a.doSearch(a.searchText) } else { - iter := a.lm.AlbumsIter(backend.AlbumSortOrder(a.sortOrder.Selected)) + iter := a.lm.AlbumsIter(backend.AlbumSortOrder(a.sortOrder.Selected), backend.AlbumFilter{}) a.grid.Reset(widgets.NewGridViewAlbumIterator(iter)) a.grid.Refresh() } @@ -190,7 +190,7 @@ func (a *AlbumsPage) doSearch(query string) { func (a *AlbumsPage) onSortOrderChanged(order string) { a.cfg.SortOrder = a.sortOrder.Selected - iter := a.lm.AlbumsIter(backend.AlbumSortOrder(order)) + iter := a.lm.AlbumsIter(backend.AlbumSortOrder(order), backend.AlbumFilter{}) a.grid.Reset(widgets.NewGridViewAlbumIterator(iter)) if a.searchText == "" { a.container.Objects[0] = a.grid diff --git a/ui/browsing/favoritespage.go b/ui/browsing/favoritespage.go index ee398b6..ccb0eed 100644 --- a/ui/browsing/favoritespage.go +++ b/ui/browsing/favoritespage.go @@ -55,7 +55,8 @@ func NewFavoritesPage(cfg *backend.FavoritesPageConfig, contr *controller.Contro } a.ExtendBaseWidget(a) a.createHeader(0, "") - a.grid = widgets.NewGridView(widgets.NewGridViewAlbumIterator(lm.StarredIter()), a.im) + iter := lm.StarredIter(backend.AlbumFilter{}) + a.grid = widgets.NewGridView(widgets.NewGridViewAlbumIterator(iter), a.im) a.contr.ConnectAlbumGridActions(a.grid) if cfg.InitialView == "Artists" { a.toggleBtns.SetActivatedButton(1) @@ -129,7 +130,8 @@ func (a *FavoritesPage) Reload() { if a.searchText != "" { a.doSearchAlbums(a.searchText) } else { - a.grid.Reset(widgets.NewGridViewAlbumIterator(a.lm.StarredIter())) + iter := a.lm.StarredIter(backend.AlbumFilter{}) + a.grid.Reset(widgets.NewGridViewAlbumIterator(iter)) } if a.tracklistCtr != nil || a.artistListCtr != nil { go func() { diff --git a/ui/browsing/genrepage.go b/ui/browsing/genrepage.go index 0b8ee76..6e19da5 100644 --- a/ui/browsing/genrepage.go +++ b/ui/browsing/genrepage.go @@ -52,7 +52,7 @@ func NewGenrePage(genre string, contr *controller.Controller, pm *backend.Playba SizeName: theme.SizeNameHeadingText, } g.playRandom = widget.NewButtonWithIcon(" Play random", myTheme.ShuffleIcon, g.playRandomSongs) - iter := g.lm.GenreIter(g.genre) + iter := g.lm.GenreIter(g.genre, backend.AlbumFilter{}) g.grid = widgets.NewGridView(widgets.NewGridViewAlbumIterator(iter), g.im) g.contr.ConnectAlbumGridActions(g.grid) g.searcher = widgets.NewSearchEntry() @@ -118,7 +118,8 @@ func (g *GenrePage) Reload() { if g.searchText != "" { g.doSearch(g.searchText) } else { - g.grid.Reset(widgets.NewGridViewAlbumIterator(g.lm.GenreIter(g.genre))) + iter := g.lm.GenreIter(g.genre, backend.AlbumFilter{}) + g.grid.Reset(widgets.NewGridViewAlbumIterator(iter)) g.grid.Refresh() } } From 0a20299cf405598e8c3fcd0b16a5a1287546d574 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Thu, 11 May 2023 08:53:57 -0700 Subject: [PATCH 2/8] add albumfilterdialog --- ui/browsing/albumspage.go | 12 ++++-- ui/dialogs/albumfilterdialog.go | 76 +++++++++++++++++++++++++++++++++ ui/widgets/custombuttons.go | 71 ++++++++++++++++++++++++++++++ ui/widgets/favoritebutton.go | 41 ------------------ 4 files changed, 155 insertions(+), 45 deletions(-) create mode 100644 ui/dialogs/albumfilterdialog.go create mode 100644 ui/widgets/custombuttons.go delete mode 100644 ui/widgets/favoritebutton.go diff --git a/ui/browsing/albumspage.go b/ui/browsing/albumspage.go index 293851a..5c7bddd 100644 --- a/ui/browsing/albumspage.go +++ b/ui/browsing/albumspage.go @@ -4,6 +4,7 @@ import ( "github.com/dweymouth/supersonic/backend" "github.com/dweymouth/supersonic/sharedutil" "github.com/dweymouth/supersonic/ui/controller" + "github.com/dweymouth/supersonic/ui/dialogs" "github.com/dweymouth/supersonic/ui/util" "github.com/dweymouth/supersonic/ui/widgets" @@ -27,7 +28,9 @@ type AlbumsPage struct { grid *widgets.GridView searchGrid *widgets.GridView searcher *widgets.SearchEntry + filterBtn *widgets.AlbumFilterButton searchText string + filter backend.AlbumFilter titleDisp *widget.RichText sortOrder *selectWidget container *fyne.Container @@ -71,11 +74,12 @@ func NewAlbumsPage(cfg *backend.AlbumsPageConfig, contr *controller.Controller, cfg.SortOrder = string(backend.AlbumSortRecentlyAdded) } a.sortOrder.Selected = cfg.SortOrder - iter := lm.AlbumsIter(backend.AlbumSortOrder(a.sortOrder.Selected), backend.AlbumFilter{}) + iter := lm.AlbumsIter(backend.AlbumSortOrder(a.sortOrder.Selected), a.filter) a.grid = widgets.NewGridView(widgets.NewGridViewAlbumIterator(iter), im) contr.ConnectAlbumGridActions(a.grid) a.searcher = widgets.NewSearchEntry() a.searcher.OnSearched = a.OnSearched + a.filterBtn = widgets.NewAlbumFilterButton(&a.filter, func(filter *backend.AlbumFilter) fyne.CanvasObject { return dialogs.NewAlbumFilterDialog(filter) }) a.createContainer(false) return a @@ -89,7 +93,7 @@ func (a *AlbumsPage) createContainer(searchgrid bool) { g = a.searchGrid } a.container = container.NewBorder( - container.NewHBox(util.NewHSpace(6), a.titleDisp, sortVbox, layout.NewSpacer(), searchVbox, util.NewHSpace(12)), + container.NewHBox(util.NewHSpace(6), a.titleDisp, sortVbox, layout.NewSpacer(), a.filterBtn, searchVbox, util.NewHSpace(12)), nil, nil, nil, @@ -154,7 +158,7 @@ func (a *AlbumsPage) Reload() { if a.searchText != "" { a.doSearch(a.searchText) } else { - iter := a.lm.AlbumsIter(backend.AlbumSortOrder(a.sortOrder.Selected), backend.AlbumFilter{}) + iter := a.lm.AlbumsIter(backend.AlbumSortOrder(a.sortOrder.Selected), a.filter) a.grid.Reset(widgets.NewGridViewAlbumIterator(iter)) a.grid.Refresh() } @@ -190,7 +194,7 @@ func (a *AlbumsPage) doSearch(query string) { func (a *AlbumsPage) onSortOrderChanged(order string) { a.cfg.SortOrder = a.sortOrder.Selected - iter := a.lm.AlbumsIter(backend.AlbumSortOrder(order), backend.AlbumFilter{}) + iter := a.lm.AlbumsIter(backend.AlbumSortOrder(order), a.filter) a.grid.Reset(widgets.NewGridViewAlbumIterator(iter)) if a.searchText == "" { a.container.Objects[0] = a.grid diff --git a/ui/dialogs/albumfilterdialog.go b/ui/dialogs/albumfilterdialog.go new file mode 100644 index 0000000..88c503c --- /dev/null +++ b/ui/dialogs/albumfilterdialog.go @@ -0,0 +1,76 @@ +package dialogs + +import ( + "strconv" + "unicode" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/container" + "fyne.io/fyne/v2/widget" + "github.com/dweymouth/supersonic/backend" + "github.com/dweymouth/supersonic/ui/widgets" +) + +type AlbumFilterDialog struct { + widget.BaseWidget + + OnDismissed func() + + filter *backend.AlbumFilter + container *fyne.Container +} + +func NewAlbumFilterDialog(filter *backend.AlbumFilter) *AlbumFilterDialog { + a := &AlbumFilterDialog{filter: filter} + a.ExtendBaseWidget(a) + + // setup min and max year filters + yearValidator := func(curText string, r rune) bool { + return unicode.IsDigit(r) && len(curText) <= 3 + } + minYear := widgets.NewTextRestrictedEntry(yearValidator) + minYear.OnChanged = func(yearStr string) { + if i, err := strconv.Atoi(yearStr); err == nil { + a.filter.MinYear = i + } + } + if filter.MinYear > 0 { + minYear.Text = strconv.Itoa(filter.MinYear) + } + maxYear := widgets.NewTextRestrictedEntry(yearValidator) + maxYear.OnChanged = func(yearStr string) { + if i, err := strconv.Atoi(yearStr); err == nil { + a.filter.MaxYear = i + } + } + if filter.MaxYear > 0 { + maxYear.Text = strconv.Itoa(filter.MaxYear) + } + + var isNotFavorite *widget.Check + // setup is favorite/not favorite filters + isFavorite := widget.NewCheck("Is favorite", func(fav bool) { + if fav { + isNotFavorite.SetChecked(false) + } + a.filter.ExcludeUnfavorited = fav + }) + isNotFavorite = widget.NewCheck("Is not favorite", func(fav bool) { + if fav { + isFavorite.SetChecked(false) + } + a.filter.ExcludeFavorited = fav + }) + + a.container = container.NewVBox( + container.NewHBox(widget.NewLabel("Year from"), minYear, widget.NewLabel("to"), maxYear), + isFavorite, + isNotFavorite, + ) + + return a +} + +func (a *AlbumFilterDialog) CreateRenderer() fyne.WidgetRenderer { + return widget.NewSimpleRenderer(a.container) +} diff --git a/ui/widgets/custombuttons.go b/ui/widgets/custombuttons.go new file mode 100644 index 0000000..172b88c --- /dev/null +++ b/ui/widgets/custombuttons.go @@ -0,0 +1,71 @@ +package widgets + +import ( + "github.com/dweymouth/supersonic/backend" + "github.com/dweymouth/supersonic/res" + "github.com/dweymouth/supersonic/ui/theme" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/widget" +) + +type FavoriteButton struct { + widget.Button + + IsFavorited bool +} + +func NewFavoriteButton(onTapped func()) *FavoriteButton { + f := &FavoriteButton{ + Button: widget.Button{ + OnTapped: onTapped, + Icon: res.ResHeartOutlineInvertPng, + }, + } + f.ExtendBaseWidget(f) + return f +} + +func (f *FavoriteButton) Tapped(e *fyne.PointEvent) { + f.IsFavorited = !f.IsFavorited + f.Button.Tapped(e) + f.Refresh() +} + +func (f *FavoriteButton) Refresh() { + if f.IsFavorited { + f.Icon = theme.FavoriteIcon + } else { + f.Icon = theme.NotFavoriteIcon + } + f.Button.Refresh() +} + +type AlbumFilterButton struct { + widget.Button + + filter *backend.AlbumFilter + + dialogConstructor func(*backend.AlbumFilter) fyne.CanvasObject + dialog *widget.PopUp +} + +func NewAlbumFilterButton(filter *backend.AlbumFilter, dialogConstructor func(*backend.AlbumFilter) fyne.CanvasObject) *AlbumFilterButton { + a := &AlbumFilterButton{ + filter: filter, + dialogConstructor: dialogConstructor, + Button: widget.Button{ + Icon: theme.AlbumIcon, + }, + } + a.OnTapped = a.showFilterDialog + a.ExtendBaseWidget(a) + return a +} + +func (a *AlbumFilterButton) showFilterDialog() { + if a.dialog == nil { + a.dialog = widget.NewModalPopUp(a.dialogConstructor(a.filter), fyne.CurrentApp().Driver().CanvasForObject(a)) + } + a.dialog.Show() +} diff --git a/ui/widgets/favoritebutton.go b/ui/widgets/favoritebutton.go deleted file mode 100644 index 77265c4..0000000 --- a/ui/widgets/favoritebutton.go +++ /dev/null @@ -1,41 +0,0 @@ -package widgets - -import ( - "github.com/dweymouth/supersonic/res" - "github.com/dweymouth/supersonic/ui/theme" - - "fyne.io/fyne/v2" - "fyne.io/fyne/v2/widget" -) - -type FavoriteButton struct { - widget.Button - - IsFavorited bool -} - -func NewFavoriteButton(onTapped func()) *FavoriteButton { - f := &FavoriteButton{ - Button: widget.Button{ - OnTapped: onTapped, - Icon: res.ResHeartOutlineInvertPng, - }, - } - f.ExtendBaseWidget(f) - return f -} - -func (f *FavoriteButton) Tapped(e *fyne.PointEvent) { - f.IsFavorited = !f.IsFavorited - f.Button.Tapped(e) - f.Refresh() -} - -func (f *FavoriteButton) Refresh() { - if f.IsFavorited { - f.Icon = theme.FavoriteIcon - } else { - f.Icon = theme.NotFavoriteIcon - } - f.Button.Refresh() -} From b2dce99714a650707b4c2dcda51df47f07a98976 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Thu, 11 May 2023 16:43:10 -0700 Subject: [PATCH 3/8] fix race condition index out of bound crash in gridview --- ui/widgets/gridview.go | 52 +++++++++++++++++++++++++----------------- 1 file changed, 31 insertions(+), 21 deletions(-) diff --git a/ui/widgets/gridview.go b/ui/widgets/gridview.go index 11704ed..4d04f0e 100644 --- a/ui/widgets/gridview.go +++ b/ui/widgets/gridview.go @@ -129,11 +129,11 @@ func (g *GridView) Clear() { func (g *GridView) Reset(iter GridViewIterator) { g.itemsMutex.Lock() g.items = nil - g.itemsMutex.Unlock() g.fetching = false g.done = false g.highestShown = 0 g.iter = iter + g.itemsMutex.Unlock() g.fetchMoreItems(36) } @@ -194,8 +194,13 @@ func (g *GridView) doUpdateItemCard(itemIdx int, card *GridViewItem) { if itemIdx > g.highestShown { g.highestShown = itemIdx } + var item GridViewItemModel g.itemsMutex.RLock() - item := g.items[itemIdx] + // itemIdx can rarely be out of range if the data is being updated + // as the view is requested to refresh + if itemIdx < len(g.items) { + item = g.items[itemIdx] + } g.itemsMutex.RUnlock() if card.PrevID == item.ID { // nothing to do @@ -208,26 +213,31 @@ func (g *GridView) doUpdateItemCard(itemIdx int, card *GridViewItem) { card.ImgLoadCancel() card.ImgLoadCancel = nil } - if img, ok := g.imageFetcher.GetCoverThumbnailFromCache(item.CoverArtID); ok { - card.Cover.SetImage(img) - } else { - card.Cover.SetImageResource(res.ResAlbumplaceholderPng) - // asynchronously fetch cover image - ctx, cancel := context.WithCancel(context.Background()) - card.ImgLoadCancel = cancel - go func(ctx context.Context) { - i, err := g.imageFetcher.GetCoverThumbnail(item.CoverArtID) - select { - case <-ctx.Done(): - return - default: - if err == nil { - card.Cover.SetImage(i) - } else { - log.Printf("error fetching image: %s", err.Error()) + if item.CoverArtID != "" { + if img, ok := g.imageFetcher.GetCoverThumbnailFromCache(item.CoverArtID); ok { + card.Cover.SetImage(img) + } else { + card.Cover.SetImageResource(res.ResAlbumplaceholderPng) + // asynchronously fetch cover image + ctx, cancel := context.WithCancel(context.Background()) + card.ImgLoadCancel = cancel + go func(ctx context.Context) { + i, err := g.imageFetcher.GetCoverThumbnail(item.CoverArtID) + select { + case <-ctx.Done(): + return + default: + if err == nil { + card.Cover.SetImage(i) + } else { + log.Printf("error fetching image: %s", err.Error()) + } } - } - }(ctx) + }(ctx) + } + } else { + // use the placeholder image for an item that has no cover art ID + card.Cover.SetImageResource(res.ResAlbumplaceholderPng) } // if user has scrolled near the bottom, fetch more From 87ae111cb2de2085f1bf4dac726e5006715a6bd4 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Thu, 11 May 2023 16:45:47 -0700 Subject: [PATCH 4/8] more work on album filters --- backend/albumiterator.go | 60 ++++++------ ui/browsing/albumspage.go | 36 +++++--- ui/browsing/favoritespage.go | 5 +- ui/browsing/genrepage.go | 6 +- ui/dialogs/albumfilterdialog.go | 76 --------------- ui/dialogs/settingsdialog.go | 6 +- ui/widgets/albumfilterbutton.go | 147 ++++++++++++++++++++++++++++++ ui/widgets/custombuttons.go | 71 --------------- ui/widgets/favoritebutton.go | 41 +++++++++ ui/widgets/textrestrictedentry.go | 8 +- 10 files changed, 250 insertions(+), 206 deletions(-) delete mode 100644 ui/dialogs/albumfilterdialog.go create mode 100644 ui/widgets/albumfilterbutton.go delete mode 100644 ui/widgets/custombuttons.go create mode 100644 ui/widgets/favoritebutton.go diff --git a/backend/albumiterator.go b/backend/albumiterator.go index ab47e6b..70c7b05 100644 --- a/backend/albumiterator.go +++ b/backend/albumiterator.go @@ -68,6 +68,11 @@ func (f *AlbumFilter) Matches(album *subsonic.AlbumID3) bool { return false } +func (f *AlbumFilter) IsEmpty() bool { + return !f.ExcludeFavorited && !f.ExcludeUnfavorited && + f.MinYear == 0 && f.MaxYear == 0 && len(f.Genres) == 0 +} + func (l *LibraryManager) AlbumsIter(sort AlbumSortOrder, filter AlbumFilter) AlbumIterator { switch sort { case AlbumSortRecentlyAdded: @@ -101,10 +106,10 @@ func (l *LibraryManager) GenreIter(genre string, filter AlbumFilter) AlbumIterat } func (l *LibraryManager) SearchIter(query string) AlbumIterator { - return l.newSearchIter(query, func(*subsonic.AlbumID3) bool { return true }) + return l.newSearchIter(query, AlbumFilter{}) } -func (l *LibraryManager) SearchIterWithFilter(query string, filter func(*subsonic.AlbumID3) bool) AlbumIterator { +func (l *LibraryManager) SearchIterWithFilter(query string, filter AlbumFilter) AlbumIterator { return l.newSearchIter(query, filter) } @@ -119,7 +124,7 @@ func (l *LibraryManager) GetAlbum(id string) (*subsonic.AlbumID3, error) { type baseIter struct { listType string filter AlbumFilter - pos int + serverPos int l *LibraryManager s *subsonic.Client opts map[string]string @@ -142,36 +147,33 @@ func (r *baseIter) Next() *subsonic.AlbumID3 { if r.done { return nil } - if r.prefetched != nil { + if r.prefetched != nil && r.prefetchedPos < len(r.prefetched) { a := r.prefetched[r.prefetchedPos] r.prefetchedPos++ - if r.prefetchedPos == len(r.prefetched) { - r.prefetched = nil - r.prefetchedPos = 0 - r.pos++ - } - r.pos++ - return a } - r.opts["offset"] = strconv.Itoa(r.pos) - albums, err := r.s.GetAlbumList2(r.listType, r.opts) - if err != nil { - log.Println(err) - albums = nil + r.prefetched = nil + for { // keep fetching until we are done or have mathcing results + r.opts["offset"] = strconv.Itoa(r.serverPos) + albums, err := r.s.GetAlbumList2(r.listType, r.opts) + if err != nil { + log.Printf("error fetching albums: %s", err.Error()) + albums = nil + } + if len(albums) == 0 { + r.done = true + return nil + } + r.serverPos += len(albums) + albums = sharedutil.FilterSlice(albums, r.filter.Matches) + r.prefetched = albums + if len(albums) > 0 { + break + } } - albums = sharedutil.FilterSlice(albums, r.filter.Matches) - if len(albums) == 0 { - r.done = true - return nil - } else if len(albums) == 1 { - r.done = true - return albums[0] - } - r.prefetched = albums r.prefetchedPos = 1 if r.l.PreCacheCoverFn != nil { - for _, album := range albums { + for _, album := range r.prefetched { go r.l.PreCacheCoverFn(album.CoverArt) } } @@ -183,14 +185,14 @@ type searchIter struct { searchIterBase l *LibraryManager - filter func(*subsonic.AlbumID3) bool + filter AlbumFilter prefetched []*subsonic.AlbumID3 prefetchedPos int albumIDset map[string]bool done bool } -func (l *LibraryManager) newSearchIter(query string, filter func(*subsonic.AlbumID3) bool) *searchIter { +func (l *LibraryManager) newSearchIter(query string, filter AlbumFilter) *searchIter { return &searchIter{ searchIterBase: searchIterBase{ query: query, @@ -266,7 +268,7 @@ func (s *searchIter) addNewAlbums(al []*subsonic.AlbumID3) { if _, have := s.albumIDset[album.ID]; have { continue } - if !s.filter(album) { + if !s.filter.Matches(album) { continue } s.prefetched = append(s.prefetched, album) diff --git a/ui/browsing/albumspage.go b/ui/browsing/albumspage.go index 5c7bddd..f18b3cc 100644 --- a/ui/browsing/albumspage.go +++ b/ui/browsing/albumspage.go @@ -4,7 +4,6 @@ import ( "github.com/dweymouth/supersonic/backend" "github.com/dweymouth/supersonic/sharedutil" "github.com/dweymouth/supersonic/ui/controller" - "github.com/dweymouth/supersonic/ui/dialogs" "github.com/dweymouth/supersonic/ui/util" "github.com/dweymouth/supersonic/ui/widgets" @@ -77,14 +76,20 @@ func NewAlbumsPage(cfg *backend.AlbumsPageConfig, contr *controller.Controller, iter := lm.AlbumsIter(backend.AlbumSortOrder(a.sortOrder.Selected), a.filter) a.grid = widgets.NewGridView(widgets.NewGridViewAlbumIterator(iter), im) contr.ConnectAlbumGridActions(a.grid) - a.searcher = widgets.NewSearchEntry() - a.searcher.OnSearched = a.OnSearched - a.filterBtn = widgets.NewAlbumFilterButton(&a.filter, func(filter *backend.AlbumFilter) fyne.CanvasObject { return dialogs.NewAlbumFilterDialog(filter) }) + a.createSearchAndFilter("") a.createContainer(false) return a } +func (a *AlbumsPage) createSearchAndFilter(searchText string) { + a.searcher = widgets.NewSearchEntry() + a.searcher.Text = searchText + a.searcher.OnSearched = a.OnSearched + a.filterBtn = widgets.NewAlbumFilterButton(&a.filter) + a.filterBtn.OnChanged = a.Reload +} + func (a *AlbumsPage) createContainer(searchgrid bool) { searchVbox := container.NewVBox(layout.NewSpacer(), a.searcher, layout.NewSpacer()) sortVbox := container.NewVBox(layout.NewSpacer(), a.sortOrder, layout.NewSpacer()) @@ -93,7 +98,7 @@ func (a *AlbumsPage) createContainer(searchgrid bool) { g = a.searchGrid } a.container = container.NewBorder( - container.NewHBox(util.NewHSpace(6), a.titleDisp, sortVbox, layout.NewSpacer(), a.filterBtn, searchVbox, util.NewHSpace(12)), + container.NewHBox(util.NewHSpace(6), a.titleDisp, sortVbox, layout.NewSpacer(), container.NewCenter(a.filterBtn), searchVbox, util.NewHSpace(12)), nil, nil, nil, @@ -103,11 +108,13 @@ func (a *AlbumsPage) createContainer(searchgrid bool) { func restoreAlbumsPage(saved *savedAlbumsPage) *AlbumsPage { a := &AlbumsPage{ - cfg: saved.cfg, - contr: saved.contr, - pm: saved.pm, - lm: saved.lm, - im: saved.im, + cfg: saved.cfg, + contr: saved.contr, + pm: saved.pm, + lm: saved.lm, + im: saved.im, + searchText: saved.searchText, + filter: saved.filter, } a.ExtendBaseWidget(a) @@ -119,13 +126,10 @@ func restoreAlbumsPage(saved *savedAlbumsPage) *AlbumsPage { a.sortOrder.Selected = saved.sortOrder a.sortOrder.OnChanged = a.onSortOrderChanged a.grid = widgets.NewGridViewFromState(saved.gridState) - a.searcher = widgets.NewSearchEntry() - a.searcher.OnSearched = a.OnSearched - a.searcher.Entry.Text = saved.searchText - a.searchText = saved.searchText if a.searchText != "" { a.searchGrid = widgets.NewGridViewFromState(saved.searchGridState) } + a.createSearchAndFilter(saved.searchText) a.createContainer(saved.searchText != "") return a @@ -172,6 +176,7 @@ func (a *AlbumsPage) Save() SavedPage { lm: a.lm, im: a.im, searchText: a.searchText, + filter: a.filter, sortOrder: a.sortOrder.Selected, gridState: a.grid.SaveToState(), } @@ -186,7 +191,7 @@ func (a *AlbumsPage) doSearch(query string) { a.searchGrid = widgets.NewGridView(widgets.NewGridViewAlbumIterator(a.lm.SearchIter(query)), a.im) a.contr.ConnectAlbumGridActions(a.searchGrid) } else { - a.searchGrid.Reset(widgets.NewGridViewAlbumIterator(a.lm.SearchIter(query))) + a.searchGrid.Reset(widgets.NewGridViewAlbumIterator(a.lm.SearchIterWithFilter(query, a.filter))) } a.container.Objects[0] = a.searchGrid a.Refresh() @@ -209,6 +214,7 @@ func (a *AlbumsPage) CreateRenderer() fyne.WidgetRenderer { type savedAlbumsPage struct { searchText string + filter backend.AlbumFilter cfg *backend.AlbumsPageConfig contr *controller.Controller pm *backend.PlaybackManager diff --git a/ui/browsing/favoritespage.go b/ui/browsing/favoritespage.go index ccb0eed..71452d8 100644 --- a/ui/browsing/favoritespage.go +++ b/ui/browsing/favoritespage.go @@ -2,7 +2,6 @@ package browsing import ( "log" - "time" "github.com/dweymouth/supersonic/backend" "github.com/dweymouth/supersonic/ui/controller" @@ -213,9 +212,7 @@ func (a *FavoritesPage) OnSongChange(song *subsonic.Child, _ *subsonic.Child) { } func (a *FavoritesPage) doSearchAlbums(query string) { - iter := a.lm.SearchIterWithFilter(query, func(al *subsonic.AlbumID3) bool { - return al.Starred.After(time.Time{}) - }) + iter := a.lm.SearchIterWithFilter(query, backend.AlbumFilter{ExcludeUnfavorited: true}) if a.searchGrid == nil { a.searchGrid = widgets.NewGridView(widgets.NewGridViewAlbumIterator(iter), a.im) a.contr.ConnectAlbumGridActions(a.searchGrid) diff --git a/ui/browsing/genrepage.go b/ui/browsing/genrepage.go index 6e19da5..e901113 100644 --- a/ui/browsing/genrepage.go +++ b/ui/browsing/genrepage.go @@ -12,8 +12,6 @@ import ( "fyne.io/fyne/v2/layout" "fyne.io/fyne/v2/theme" "fyne.io/fyne/v2/widget" - - "github.com/dweymouth/go-subsonic/subsonic" ) // TODO: there is a lot of code duplication between this and albumspage. Refactor? @@ -160,9 +158,7 @@ func (g *GenrePage) OnSearched(query string) { } func (g *GenrePage) doSearch(query string) { - iter := g.lm.SearchIterWithFilter(query, func(al *subsonic.AlbumID3) bool { - return al.Genre == g.genre - }) + iter := g.lm.SearchIterWithFilter(query, backend.AlbumFilter{Genres: []string{g.genre}}) if g.searchGrid == nil { g.searchGrid = widgets.NewGridView(widgets.NewGridViewAlbumIterator(iter), g.im) g.contr.ConnectAlbumGridActions(g.searchGrid) diff --git a/ui/dialogs/albumfilterdialog.go b/ui/dialogs/albumfilterdialog.go deleted file mode 100644 index 88c503c..0000000 --- a/ui/dialogs/albumfilterdialog.go +++ /dev/null @@ -1,76 +0,0 @@ -package dialogs - -import ( - "strconv" - "unicode" - - "fyne.io/fyne/v2" - "fyne.io/fyne/v2/container" - "fyne.io/fyne/v2/widget" - "github.com/dweymouth/supersonic/backend" - "github.com/dweymouth/supersonic/ui/widgets" -) - -type AlbumFilterDialog struct { - widget.BaseWidget - - OnDismissed func() - - filter *backend.AlbumFilter - container *fyne.Container -} - -func NewAlbumFilterDialog(filter *backend.AlbumFilter) *AlbumFilterDialog { - a := &AlbumFilterDialog{filter: filter} - a.ExtendBaseWidget(a) - - // setup min and max year filters - yearValidator := func(curText string, r rune) bool { - return unicode.IsDigit(r) && len(curText) <= 3 - } - minYear := widgets.NewTextRestrictedEntry(yearValidator) - minYear.OnChanged = func(yearStr string) { - if i, err := strconv.Atoi(yearStr); err == nil { - a.filter.MinYear = i - } - } - if filter.MinYear > 0 { - minYear.Text = strconv.Itoa(filter.MinYear) - } - maxYear := widgets.NewTextRestrictedEntry(yearValidator) - maxYear.OnChanged = func(yearStr string) { - if i, err := strconv.Atoi(yearStr); err == nil { - a.filter.MaxYear = i - } - } - if filter.MaxYear > 0 { - maxYear.Text = strconv.Itoa(filter.MaxYear) - } - - var isNotFavorite *widget.Check - // setup is favorite/not favorite filters - isFavorite := widget.NewCheck("Is favorite", func(fav bool) { - if fav { - isNotFavorite.SetChecked(false) - } - a.filter.ExcludeUnfavorited = fav - }) - isNotFavorite = widget.NewCheck("Is not favorite", func(fav bool) { - if fav { - isFavorite.SetChecked(false) - } - a.filter.ExcludeFavorited = fav - }) - - a.container = container.NewVBox( - container.NewHBox(widget.NewLabel("Year from"), minYear, widget.NewLabel("to"), maxYear), - isFavorite, - isNotFavorite, - ) - - return a -} - -func (a *AlbumFilterDialog) CreateRenderer() fyne.WidgetRenderer { - return widget.NewSimpleRenderer(a.container) -} diff --git a/ui/dialogs/settingsdialog.go b/ui/dialogs/settingsdialog.go index fbb444e..0e8dbc2 100644 --- a/ui/dialogs/settingsdialog.go +++ b/ui/dialogs/settingsdialog.go @@ -113,8 +113,8 @@ func (s *SettingsDialog) createGeneralTab() *container.TabItem { // Scrobble settings - twoDigitValidator := func(text string, r rune) bool { - return unicode.IsDigit(r) && len(text) < 2 + twoDigitValidator := func(text, selText string, r rune) bool { + return unicode.IsDigit(r) && len(text)-len(selText) < 2 } percentEntry := widgets.NewTextRestrictedEntry(twoDigitValidator) @@ -247,7 +247,7 @@ func (s *SettingsDialog) createPlaybackTab() *container.TabItem { replayGainSelect.SetSelectedIndex(0) } - preampGain := widgets.NewTextRestrictedEntry(func(curText string, r rune) bool { + preampGain := widgets.NewTextRestrictedEntry(func(curText, _ string, r rune) bool { return (curText == "" && r == '-') || (curText == "" && unicode.IsDigit(r)) || ((curText == "-" || curText == "0") && unicode.IsDigit(r)) diff --git a/ui/widgets/albumfilterbutton.go b/ui/widgets/albumfilterbutton.go new file mode 100644 index 0000000..88adca3 --- /dev/null +++ b/ui/widgets/albumfilterbutton.go @@ -0,0 +1,147 @@ +package widgets + +import ( + "strconv" + "time" + "unicode" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/container" + "fyne.io/fyne/v2/layout" + "fyne.io/fyne/v2/widget" + "github.com/dweymouth/supersonic/backend" + "github.com/dweymouth/supersonic/ui/theme" + "github.com/dweymouth/supersonic/ui/util" +) + +type AlbumFilterButton struct { + widget.Button + + OnChanged func() + + filter *backend.AlbumFilter + dialog *widget.PopUp +} + +func NewAlbumFilterButton(filter *backend.AlbumFilter) *AlbumFilterButton { + a := &AlbumFilterButton{ + filter: filter, + Button: widget.Button{ + Icon: theme.AlbumIcon, + }, + } + a.OnTapped = a.showFilterDialog + a.ExtendBaseWidget(a) + return a +} + +func (a *AlbumFilterButton) Refresh() { + if a.filter.IsEmpty() { + a.Importance = widget.MediumImportance + } else { + a.Importance = widget.HighImportance + } + a.Button.Refresh() +} + +func (a *AlbumFilterButton) onFilterChanged() { + a.Refresh() + if a.OnChanged != nil { + a.OnChanged() + } +} + +func (a *AlbumFilterButton) showFilterDialog() { + if a.dialog == nil { + filterDlg := NewAlbumFilterPopup(a.filter) + filterDlg.OnChanged = a.onFilterChanged + a.dialog = widget.NewPopUp(filterDlg, fyne.CurrentApp().Driver().CanvasForObject(a)) + } + pos := fyne.CurrentApp().Driver().AbsolutePositionForObject(a) + a.dialog.ShowAtPosition(fyne.NewPos(pos.X+a.Size().Width/2-a.dialog.MinSize().Width/2, pos.Y+a.Size().Height)) +} + +type AlbumFilterPopup struct { + widget.BaseWidget + + OnChanged func() + + filter *backend.AlbumFilter + container *fyne.Container +} + +func NewAlbumFilterPopup(filter *backend.AlbumFilter) *AlbumFilterPopup { + a := &AlbumFilterPopup{filter: filter} + a.ExtendBaseWidget(a) + + debounceOnChanged := util.NewDebouncer(350*time.Millisecond, a.emitOnChanged) + + // setup min and max year filters + yearValidator := func(curText, selText string, r rune) bool { + return unicode.IsDigit(r) && (len(selText) > 0 || len(curText) <= 3) + } + minYear := NewTextRestrictedEntry(yearValidator) + minYear.SetMinCharWidth(4) + minYear.OnChanged = func(yearStr string) { + if yearStr == "" { + a.filter.MinYear = 0 + } else if i, err := strconv.Atoi(yearStr); err == nil { + a.filter.MinYear = i + } + debounceOnChanged() + } + if filter.MinYear > 0 { + minYear.Text = strconv.Itoa(filter.MinYear) + } + maxYear := NewTextRestrictedEntry(yearValidator) + maxYear.SetMinCharWidth(4) + maxYear.OnChanged = func(yearStr string) { + if yearStr == "" { + a.filter.MaxYear = 0 + } else if i, err := strconv.Atoi(yearStr); err == nil { + a.filter.MaxYear = i + } + debounceOnChanged() + } + if filter.MaxYear > 0 { + maxYear.Text = strconv.Itoa(filter.MaxYear) + } + + // setup is favorite/not favorite filters + var isNotFavorite *widget.Check + isFavorite := widget.NewCheck("Is favorite", func(fav bool) { + if fav { + isNotFavorite.SetChecked(false) + } + a.filter.ExcludeUnfavorited = fav + debounceOnChanged() + }) + isNotFavorite = widget.NewCheck("Is not favorite", func(fav bool) { + if fav { + isFavorite.SetChecked(false) + } + a.filter.ExcludeFavorited = fav + debounceOnChanged() + }) + + // setup container + title := widget.NewLabel("Album filters") + title.TextStyle.Bold = true + a.container = container.NewVBox( + container.NewHBox(layout.NewSpacer(), title, layout.NewSpacer()), + container.NewHBox(widget.NewLabel("Year from"), minYear, widget.NewLabel("to"), maxYear), + container.NewHBox(isFavorite, isNotFavorite), + ) + + return a +} + +func (a *AlbumFilterPopup) emitOnChanged() { + if a.OnChanged != nil { + a.OnChanged() + } +} + +func (a *AlbumFilterPopup) CreateRenderer() fyne.WidgetRenderer { + return widget.NewSimpleRenderer(a.container) +} diff --git a/ui/widgets/custombuttons.go b/ui/widgets/custombuttons.go deleted file mode 100644 index 172b88c..0000000 --- a/ui/widgets/custombuttons.go +++ /dev/null @@ -1,71 +0,0 @@ -package widgets - -import ( - "github.com/dweymouth/supersonic/backend" - "github.com/dweymouth/supersonic/res" - "github.com/dweymouth/supersonic/ui/theme" - - "fyne.io/fyne/v2" - "fyne.io/fyne/v2/widget" -) - -type FavoriteButton struct { - widget.Button - - IsFavorited bool -} - -func NewFavoriteButton(onTapped func()) *FavoriteButton { - f := &FavoriteButton{ - Button: widget.Button{ - OnTapped: onTapped, - Icon: res.ResHeartOutlineInvertPng, - }, - } - f.ExtendBaseWidget(f) - return f -} - -func (f *FavoriteButton) Tapped(e *fyne.PointEvent) { - f.IsFavorited = !f.IsFavorited - f.Button.Tapped(e) - f.Refresh() -} - -func (f *FavoriteButton) Refresh() { - if f.IsFavorited { - f.Icon = theme.FavoriteIcon - } else { - f.Icon = theme.NotFavoriteIcon - } - f.Button.Refresh() -} - -type AlbumFilterButton struct { - widget.Button - - filter *backend.AlbumFilter - - dialogConstructor func(*backend.AlbumFilter) fyne.CanvasObject - dialog *widget.PopUp -} - -func NewAlbumFilterButton(filter *backend.AlbumFilter, dialogConstructor func(*backend.AlbumFilter) fyne.CanvasObject) *AlbumFilterButton { - a := &AlbumFilterButton{ - filter: filter, - dialogConstructor: dialogConstructor, - Button: widget.Button{ - Icon: theme.AlbumIcon, - }, - } - a.OnTapped = a.showFilterDialog - a.ExtendBaseWidget(a) - return a -} - -func (a *AlbumFilterButton) showFilterDialog() { - if a.dialog == nil { - a.dialog = widget.NewModalPopUp(a.dialogConstructor(a.filter), fyne.CurrentApp().Driver().CanvasForObject(a)) - } - a.dialog.Show() -} diff --git a/ui/widgets/favoritebutton.go b/ui/widgets/favoritebutton.go new file mode 100644 index 0000000..77265c4 --- /dev/null +++ b/ui/widgets/favoritebutton.go @@ -0,0 +1,41 @@ +package widgets + +import ( + "github.com/dweymouth/supersonic/res" + "github.com/dweymouth/supersonic/ui/theme" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/widget" +) + +type FavoriteButton struct { + widget.Button + + IsFavorited bool +} + +func NewFavoriteButton(onTapped func()) *FavoriteButton { + f := &FavoriteButton{ + Button: widget.Button{ + OnTapped: onTapped, + Icon: res.ResHeartOutlineInvertPng, + }, + } + f.ExtendBaseWidget(f) + return f +} + +func (f *FavoriteButton) Tapped(e *fyne.PointEvent) { + f.IsFavorited = !f.IsFavorited + f.Button.Tapped(e) + f.Refresh() +} + +func (f *FavoriteButton) Refresh() { + if f.IsFavorited { + f.Icon = theme.FavoriteIcon + } else { + f.Icon = theme.NotFavoriteIcon + } + f.Button.Refresh() +} diff --git a/ui/widgets/textrestrictedentry.go b/ui/widgets/textrestrictedentry.go index c76989e..3df4a6d 100644 --- a/ui/widgets/textrestrictedentry.go +++ b/ui/widgets/textrestrictedentry.go @@ -13,19 +13,21 @@ import ( type TextRestrictedEntry struct { widget.Entry - charAllowed func(string, rune) bool + charAllowed CharAllowedFunc minWidth float32 } -func NewTextRestrictedEntry(charAllowed func(curText string, r rune) bool) *TextRestrictedEntry { +type CharAllowedFunc func(curText string, selectedText string, r rune) bool + +func NewTextRestrictedEntry(charAllowed CharAllowedFunc) *TextRestrictedEntry { e := &TextRestrictedEntry{charAllowed: charAllowed} e.ExtendBaseWidget(e) return e } func (e *TextRestrictedEntry) TypedRune(r rune) { - if e.charAllowed == nil || e.charAllowed(e.Text, r) { + if e.charAllowed == nil || e.charAllowed(e.Text, e.SelectedText(), r) { e.Entry.TypedRune(r) } } From 85701c568c7469084e2e840fb5ababf2fe504e25 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Thu, 11 May 2023 21:32:07 -0700 Subject: [PATCH 5/8] add filter icon --- res/bundled.go | 5 +++++ res/bundled_gen.sh | 1 + res/icons/publicdomain/filter.svg | 9 +++++++++ ui/theme/theme.go | 1 + ui/widgets/albumfilterbutton.go | 2 +- 5 files changed, 17 insertions(+), 1 deletion(-) create mode 100644 res/icons/publicdomain/filter.svg diff --git a/res/bundled.go b/res/bundled.go index 4bf4716..c7d7800 100644 --- a/res/bundled.go +++ b/res/bundled.go @@ -140,6 +140,11 @@ var ResListSvg = &fyne.StaticResource{ StaticContent: []byte( "\r\r\r\n\t\r\n\t\r\n\t\r\n\r\n"), } +var ResFilterSvg = &fyne.StaticResource{ + StaticName: "filter.svg", + StaticContent: []byte( + "\r\n\r\n\t\r\n\r\n\r\n"), +} var ResLICENSE = &fyne.StaticResource{ StaticName: "LICENSE", StaticContent: []byte( diff --git a/res/bundled_gen.sh b/res/bundled_gen.sh index 3bb5b19..f2e3cb3 100755 --- a/res/bundled_gen.sh +++ b/res/bundled_gen.sh @@ -27,6 +27,7 @@ fyne bundle -append -prefix Res icons/publicdomain/star-outline.svg >> bundled.g fyne bundle -append -prefix Res icons/publicdomain/star-filled.svg >> bundled.go fyne bundle -append -prefix Res icons/publicdomain/grid.svg >> bundled.go fyne bundle -append -prefix Res icons/publicdomain/list.svg >> bundled.go +fyne bundle -append -prefix Res icons/publicdomain/filter.svg >> bundled.go fyne bundle -append -prefix Res ../LICENSE >> bundled.go fyne bundle -append -prefix Res licenses/BSDLICENSE >> bundled.go diff --git a/res/icons/publicdomain/filter.svg b/res/icons/publicdomain/filter.svg new file mode 100644 index 0000000..c16beb3 --- /dev/null +++ b/res/icons/publicdomain/filter.svg @@ -0,0 +1,9 @@ + + + + + diff --git a/ui/theme/theme.go b/ui/theme/theme.go index a865d4f..b237c72 100644 --- a/ui/theme/theme.go +++ b/ui/theme/theme.go @@ -121,6 +121,7 @@ var ( PlaylistIcon fyne.Resource ShuffleIcon fyne.Resource TracksIcon fyne.Resource + FilterIcon fyne.Resource = theme.NewThemedResource(res.ResFilterSvg) ) // MUST be called at startup! diff --git a/ui/widgets/albumfilterbutton.go b/ui/widgets/albumfilterbutton.go index 88adca3..0d45e7e 100644 --- a/ui/widgets/albumfilterbutton.go +++ b/ui/widgets/albumfilterbutton.go @@ -27,7 +27,7 @@ func NewAlbumFilterButton(filter *backend.AlbumFilter) *AlbumFilterButton { a := &AlbumFilterButton{ filter: filter, Button: widget.Button{ - Icon: theme.AlbumIcon, + Icon: theme.FilterIcon, }, } a.OnTapped = a.showFilterDialog From 4730b7403906f4b3f211b4df1f8420dc2d1b4d9b Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Fri, 12 May 2023 09:22:49 -0700 Subject: [PATCH 6/8] add album filters to genre page --- ui/browsing/albumspage.go | 8 ++--- ui/browsing/genrepage.go | 58 +++++++++++++++++++-------------- ui/widgets/albumfilterbutton.go | 10 ++++-- 3 files changed, 46 insertions(+), 30 deletions(-) diff --git a/ui/browsing/albumspage.go b/ui/browsing/albumspage.go index f18b3cc..11e2c25 100644 --- a/ui/browsing/albumspage.go +++ b/ui/browsing/albumspage.go @@ -76,15 +76,15 @@ func NewAlbumsPage(cfg *backend.AlbumsPageConfig, contr *controller.Controller, iter := lm.AlbumsIter(backend.AlbumSortOrder(a.sortOrder.Selected), a.filter) a.grid = widgets.NewGridView(widgets.NewGridViewAlbumIterator(iter), im) contr.ConnectAlbumGridActions(a.grid) - a.createSearchAndFilter("") + a.createSearchAndFilter() a.createContainer(false) return a } -func (a *AlbumsPage) createSearchAndFilter(searchText string) { +func (a *AlbumsPage) createSearchAndFilter() { a.searcher = widgets.NewSearchEntry() - a.searcher.Text = searchText + a.searcher.Text = a.searchText a.searcher.OnSearched = a.OnSearched a.filterBtn = widgets.NewAlbumFilterButton(&a.filter) a.filterBtn.OnChanged = a.Reload @@ -129,7 +129,7 @@ func restoreAlbumsPage(saved *savedAlbumsPage) *AlbumsPage { if a.searchText != "" { a.searchGrid = widgets.NewGridViewFromState(saved.searchGridState) } - a.createSearchAndFilter(saved.searchText) + a.createSearchAndFilter() a.createContainer(saved.searchText != "") return a diff --git a/ui/browsing/genrepage.go b/ui/browsing/genrepage.go index e901113..f53de6e 100644 --- a/ui/browsing/genrepage.go +++ b/ui/browsing/genrepage.go @@ -27,6 +27,8 @@ type GenrePage struct { searchGrid *widgets.GridView searcher *widgets.SearchEntry searchText string + filter backend.AlbumFilter + filterBtn *widgets.AlbumFilterButton titleDisp *widget.RichText playRandom *widget.Button @@ -37,11 +39,12 @@ type GenrePage struct { func NewGenrePage(genre string, contr *controller.Controller, pm *backend.PlaybackManager, lm *backend.LibraryManager, im *backend.ImageManager) *GenrePage { g := &GenrePage{ - genre: genre, - contr: contr, - pm: pm, - lm: lm, - im: im, + genre: genre, + filter: backend.AlbumFilter{Genres: []string{genre}}, + contr: contr, + pm: pm, + lm: lm, + im: im, } g.ExtendBaseWidget(g) @@ -50,16 +53,24 @@ func NewGenrePage(genre string, contr *controller.Controller, pm *backend.Playba SizeName: theme.SizeNameHeadingText, } g.playRandom = widget.NewButtonWithIcon(" Play random", myTheme.ShuffleIcon, g.playRandomSongs) - iter := g.lm.GenreIter(g.genre, backend.AlbumFilter{}) + iter := g.lm.GenreIter(g.genre, g.filter) g.grid = widgets.NewGridView(widgets.NewGridViewAlbumIterator(iter), g.im) g.contr.ConnectAlbumGridActions(g.grid) - g.searcher = widgets.NewSearchEntry() - g.searcher.OnSearched = g.OnSearched + g.createSearchAndFilter() g.createContainer(false) return g } +func (g *GenrePage) createSearchAndFilter() { + g.searcher = widgets.NewSearchEntry() + g.searcher.Text = g.searchText + g.searcher.OnSearched = g.OnSearched + g.filterBtn = widgets.NewAlbumFilterButton(&g.filter) + g.filterBtn.GenreDisabled = true + g.filterBtn.OnChanged = g.Reload +} + func (g *GenrePage) createContainer(searchGrid bool) { searchVbox := container.NewVBox(layout.NewSpacer(), g.searcher, layout.NewSpacer()) gr := g.grid @@ -68,21 +79,21 @@ func (g *GenrePage) createContainer(searchGrid bool) { } playRandomVbox := container.NewVBox(layout.NewSpacer(), g.playRandom, layout.NewSpacer()) g.container = container.NewBorder( - container.NewHBox(util.NewHSpace(6), g.titleDisp, playRandomVbox, layout.NewSpacer(), searchVbox, util.NewHSpace(15)), - nil, - nil, - nil, - gr, + container.NewHBox(util.NewHSpace(6), + g.titleDisp, playRandomVbox, layout.NewSpacer(), container.NewCenter(g.filterBtn), searchVbox, util.NewHSpace(15)), + nil, nil, nil, gr, ) } func restoreGenrePage(saved *savedGenrePage) *GenrePage { g := &GenrePage{ - genre: saved.genre, - contr: saved.contr, - pm: saved.pm, - lm: saved.lm, - im: saved.im, + genre: saved.genre, + contr: saved.contr, + pm: saved.pm, + lm: saved.lm, + im: saved.im, + searchText: saved.searchText, + filter: saved.filter, } g.ExtendBaseWidget(g) @@ -92,10 +103,7 @@ func restoreGenrePage(saved *savedGenrePage) *GenrePage { } g.playRandom = widget.NewButtonWithIcon(" Play random", myTheme.ShuffleIcon, g.playRandomSongs) g.grid = widgets.NewGridViewFromState(saved.gridState) - g.searcher = widgets.NewSearchEntry() - g.searcher.OnSearched = g.OnSearched - g.searcher.Entry.Text = saved.searchText - g.searchText = saved.searchText + g.createSearchAndFilter() if g.searchText != "" { g.searchGrid = widgets.NewGridViewFromState(saved.searchGridState) } @@ -116,7 +124,7 @@ func (g *GenrePage) Reload() { if g.searchText != "" { g.doSearch(g.searchText) } else { - iter := g.lm.GenreIter(g.genre, backend.AlbumFilter{}) + iter := g.lm.GenreIter(g.genre, g.filter) g.grid.Reset(widgets.NewGridViewAlbumIterator(iter)) g.grid.Refresh() } @@ -125,6 +133,7 @@ func (g *GenrePage) Reload() { func (g *GenrePage) Save() SavedPage { sg := &savedGenrePage{ genre: g.genre, + filter: g.filter, searchText: g.searchText, contr: g.contr, pm: g.pm, @@ -158,7 +167,7 @@ func (g *GenrePage) OnSearched(query string) { } func (g *GenrePage) doSearch(query string) { - iter := g.lm.SearchIterWithFilter(query, backend.AlbumFilter{Genres: []string{g.genre}}) + iter := g.lm.SearchIterWithFilter(query, g.filter) if g.searchGrid == nil { g.searchGrid = widgets.NewGridView(widgets.NewGridViewAlbumIterator(iter), g.im) g.contr.ConnectAlbumGridActions(g.searchGrid) @@ -176,6 +185,7 @@ func (g *GenrePage) playRandomSongs() { type savedGenrePage struct { genre string searchText string + filter backend.AlbumFilter contr *controller.Controller pm *backend.PlaybackManager lm *backend.LibraryManager diff --git a/ui/widgets/albumfilterbutton.go b/ui/widgets/albumfilterbutton.go index 0d45e7e..e7d9e26 100644 --- a/ui/widgets/albumfilterbutton.go +++ b/ui/widgets/albumfilterbutton.go @@ -17,7 +17,8 @@ import ( type AlbumFilterButton struct { widget.Button - OnChanged func() + OnChanged func() + GenreDisabled bool filter *backend.AlbumFilter dialog *widget.PopUp @@ -36,7 +37,7 @@ func NewAlbumFilterButton(filter *backend.AlbumFilter) *AlbumFilterButton { } func (a *AlbumFilterButton) Refresh() { - if a.filter.IsEmpty() { + if a.filter.IsEmpty() || (a.GenreDisabled && a.filterEmptyExceptGenre()) { a.Importance = widget.MediumImportance } else { a.Importance = widget.HighImportance @@ -44,6 +45,11 @@ func (a *AlbumFilterButton) Refresh() { a.Button.Refresh() } +func (a *AlbumFilterButton) filterEmptyExceptGenre() bool { + return !a.filter.ExcludeFavorited && !a.filter.ExcludeUnfavorited && + a.filter.MinYear == 0 && a.filter.MaxYear == 0 +} + func (a *AlbumFilterButton) onFilterChanged() { a.Refresh() if a.OnChanged != nil { From dde8cd02675ab020af2fa58adbf02f324a7a1c3b Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Fri, 12 May 2023 19:08:35 -0700 Subject: [PATCH 7/8] update go-subsonic to get response body closing fix --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 9d85482..9f2b521 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( fyne.io/fyne/v2 v2.3.4 github.com/20after4/configdir v0.1.1 github.com/dweymouth/go-mpv v0.0.0-20230406003141-7f1858e503ee - github.com/dweymouth/go-subsonic v0.0.0-20230507163800-c92892982103 + github.com/dweymouth/go-subsonic v0.0.0-20230513020020-0790f53c2868 github.com/google/uuid v1.3.0 github.com/pelletier/go-toml v1.9.3 github.com/zalando/go-keyring v0.2.1 diff --git a/go.sum b/go.sum index fcb1a34..06cece0 100644 --- a/go.sum +++ b/go.sum @@ -74,8 +74,8 @@ github.com/dweymouth/fyne/v2 v2.3.0-rc1.0.20230505012127-ca61c153b2a5 h1:uXbHzg9 github.com/dweymouth/fyne/v2 v2.3.0-rc1.0.20230505012127-ca61c153b2a5/go.mod h1:X2+NrR+62mvAiAt2fwKT7035zQsE77KVV1NlvWo4vW8= github.com/dweymouth/go-mpv v0.0.0-20230406003141-7f1858e503ee h1:ZGyJ6wp7CAfT31BugypcF/TPKEy2RrGR9JFq1JOjOpY= github.com/dweymouth/go-mpv v0.0.0-20230406003141-7f1858e503ee/go.mod h1:Ov0ieN90M7i+0k3OxhA/g1dozGs+UcPHDsMKqPgRDk0= -github.com/dweymouth/go-subsonic v0.0.0-20230507163800-c92892982103 h1:tjDrQ5yy3YQA/sAnl9d40dhO/24LP8/jmHZc3KNpyJ0= -github.com/dweymouth/go-subsonic v0.0.0-20230507163800-c92892982103/go.mod h1:fUez6NFiEJiQTZizZ1BThZr5GJXAbigzGYjEPNm4tdI= +github.com/dweymouth/go-subsonic v0.0.0-20230513020020-0790f53c2868 h1:403Dden/cdQyDM8ydonHLxTBPjgQXZ76PUfmBG92+Dw= +github.com/dweymouth/go-subsonic v0.0.0-20230513020020-0790f53c2868/go.mod h1:fUez6NFiEJiQTZizZ1BThZr5GJXAbigzGYjEPNm4tdI= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= From 35b2bcb261afd3c709e5f3f951452839dd9a6572 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Fri, 12 May 2023 19:29:02 -0700 Subject: [PATCH 8/8] add album filters to favorites page --- ui/browsing/favoritespage.go | 55 +++++++++++++++++----------- ui/widgets/albumfilterbutton.go | 65 +++++++++++++++++++-------------- 2 files changed, 72 insertions(+), 48 deletions(-) diff --git a/ui/browsing/favoritespage.go b/ui/browsing/favoritespage.go index 71452d8..80a5b2f 100644 --- a/ui/browsing/favoritespage.go +++ b/ui/browsing/favoritespage.go @@ -29,6 +29,7 @@ type FavoritesPage struct { sm *backend.ServerManager lm *backend.LibraryManager + filter backend.AlbumFilter searchText string nowPlayingID string pendingViewSwitch bool @@ -38,6 +39,7 @@ type FavoritesPage struct { artistListCtr *fyne.Container tracklistCtr *fyne.Container searcher *widgets.SearchEntry + filterBtn *widgets.AlbumFilterButton titleDisp *widget.RichText toggleBtns *widgets.ToggleButtonGroup container *fyne.Container @@ -45,16 +47,17 @@ type FavoritesPage struct { func NewFavoritesPage(cfg *backend.FavoritesPageConfig, contr *controller.Controller, sm *backend.ServerManager, pm *backend.PlaybackManager, lm *backend.LibraryManager, im *backend.ImageManager) *FavoritesPage { a := &FavoritesPage{ - cfg: cfg, - contr: contr, - pm: pm, - lm: lm, - sm: sm, - im: im, + filter: backend.AlbumFilter{ExcludeUnfavorited: true}, + cfg: cfg, + contr: contr, + pm: pm, + lm: lm, + sm: sm, + im: im, } a.ExtendBaseWidget(a) - a.createHeader(0, "") - iter := lm.StarredIter(backend.AlbumFilter{}) + a.createHeader(0) + iter := lm.StarredIter(a.filter) a.grid = widgets.NewGridView(widgets.NewGridViewAlbumIterator(iter), a.im) a.contr.ConnectAlbumGridActions(a.grid) if cfg.InitialView == "Artists" { @@ -69,7 +72,7 @@ func NewFavoritesPage(cfg *backend.FavoritesPageConfig, contr *controller.Contro return a } -func (a *FavoritesPage) createHeader(activeBtnIdx int, searchText string) { +func (a *FavoritesPage) createHeader(activeBtnIdx int) { a.titleDisp = widget.NewRichTextWithText("Favorites") a.titleDisp.Segments[0].(*widget.TextSegment).Style = widget.RichTextStyle{ SizeName: theme.SizeNameHeadingText, @@ -80,27 +83,32 @@ func (a *FavoritesPage) createHeader(activeBtnIdx int, searchText string) { widget.NewButtonWithIcon("", myTheme.TracksIcon, a.onShowFavoriteSongs)) a.searcher = widgets.NewSearchEntry() a.searcher.OnSearched = a.OnSearched - a.searcher.Entry.Text = searchText + a.searcher.Entry.Text = a.searchText + a.filterBtn = widgets.NewAlbumFilterButton(&a.filter) + 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(), searchVbox, util.NewHSpace(15)), + 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, - pm: saved.pm, - lm: saved.lm, - sm: saved.sm, - im: saved.im, + cfg: saved.cfg, + contr: saved.contr, + pm: saved.pm, + lm: saved.lm, + sm: saved.sm, + im: saved.im, + searchText: saved.searchText, + filter: saved.filter, } a.ExtendBaseWidget(a) - a.createHeader(saved.activeToggleBtn, saved.searchText) + a.createHeader(saved.activeToggleBtn) a.grid = widgets.NewGridViewFromState(saved.gridState) if saved.searchText != "" { @@ -129,7 +137,7 @@ func (a *FavoritesPage) Reload() { if a.searchText != "" { a.doSearchAlbums(a.searchText) } else { - iter := a.lm.StarredIter(backend.AlbumFilter{}) + iter := a.lm.StarredIter(a.filter) a.grid.Reset(widgets.NewGridViewAlbumIterator(iter)) } if a.tracklistCtr != nil || a.artistListCtr != nil { @@ -170,6 +178,7 @@ func (a *FavoritesPage) Save() SavedPage { sm: a.sm, im: a.im, lm: a.lm, + filter: a.filter, searchText: a.searchText, gridState: a.grid.SaveToState(), activeToggleBtn: a.toggleBtns.ActivatedButtonIndex(), @@ -212,7 +221,7 @@ func (a *FavoritesPage) OnSongChange(song *subsonic.Child, _ *subsonic.Child) { } func (a *FavoritesPage) doSearchAlbums(query string) { - iter := a.lm.SearchIterWithFilter(query, backend.AlbumFilter{ExcludeUnfavorited: true}) + iter := a.lm.SearchIterWithFilter(query, a.filter) if a.searchGrid == nil { a.searchGrid = widgets.NewGridView(widgets.NewGridViewAlbumIterator(iter), a.im) a.contr.ConnectAlbumGridActions(a.searchGrid) @@ -226,6 +235,7 @@ func (a *FavoritesPage) doSearchAlbums(query string) { func (a *FavoritesPage) onShowFavoriteAlbums() { a.cfg.InitialView = "Albums" // save setting a.searcher.Entry.Show() + a.filterBtn.Show() if a.searchText == "" { a.container.Objects[0] = a.grid } else { @@ -237,6 +247,7 @@ func (a *FavoritesPage) onShowFavoriteAlbums() { 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.artistListCtr == nil { if a.pendingViewSwitch { return @@ -285,6 +296,7 @@ func buildArtistListModel(artists []*subsonic.ArtistID3) []widgets.ArtistGenreLi 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 @@ -334,6 +346,7 @@ type savedFavoritesPage struct { lm *backend.LibraryManager gridState widgets.GridViewState searchGridState widgets.GridViewState + filter backend.AlbumFilter searchText string activeToggleBtn int } diff --git a/ui/widgets/albumfilterbutton.go b/ui/widgets/albumfilterbutton.go index e7d9e26..0fc59b2 100644 --- a/ui/widgets/albumfilterbutton.go +++ b/ui/widgets/albumfilterbutton.go @@ -17,8 +17,9 @@ import ( type AlbumFilterButton struct { widget.Button - OnChanged func() - GenreDisabled bool + OnChanged func() + GenreDisabled bool + FavoriteDisabled bool filter *backend.AlbumFilter dialog *widget.PopUp @@ -37,7 +38,7 @@ func NewAlbumFilterButton(filter *backend.AlbumFilter) *AlbumFilterButton { } func (a *AlbumFilterButton) Refresh() { - if a.filter.IsEmpty() || (a.GenreDisabled && a.filterEmptyExceptGenre()) { + if a.filterEmpty() { a.Importance = widget.MediumImportance } else { a.Importance = widget.HighImportance @@ -45,9 +46,10 @@ func (a *AlbumFilterButton) Refresh() { a.Button.Refresh() } -func (a *AlbumFilterButton) filterEmptyExceptGenre() bool { - return !a.filter.ExcludeFavorited && !a.filter.ExcludeUnfavorited && - a.filter.MinYear == 0 && a.filter.MaxYear == 0 +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) } func (a *AlbumFilterButton) onFilterChanged() { @@ -59,7 +61,7 @@ func (a *AlbumFilterButton) onFilterChanged() { func (a *AlbumFilterButton) showFilterDialog() { if a.dialog == nil { - filterDlg := NewAlbumFilterPopup(a.filter) + filterDlg := NewAlbumFilterPopup(a) filterDlg.OnChanged = a.onFilterChanged a.dialog = widget.NewPopUp(filterDlg, fyne.CurrentApp().Driver().CanvasForObject(a)) } @@ -72,12 +74,14 @@ type AlbumFilterPopup struct { OnChanged func() - filter *backend.AlbumFilter - container *fyne.Container + isFavorite *widget.Check + isNotFavorite *widget.Check + filterBtn *AlbumFilterButton + container *fyne.Container } -func NewAlbumFilterPopup(filter *backend.AlbumFilter) *AlbumFilterPopup { - a := &AlbumFilterPopup{filter: filter} +func NewAlbumFilterPopup(filter *AlbumFilterButton) *AlbumFilterPopup { + a := &AlbumFilterPopup{filterBtn: filter} a.ExtendBaseWidget(a) debounceOnChanged := util.NewDebouncer(350*time.Millisecond, a.emitOnChanged) @@ -90,45 +94,46 @@ func NewAlbumFilterPopup(filter *backend.AlbumFilter) *AlbumFilterPopup { minYear.SetMinCharWidth(4) minYear.OnChanged = func(yearStr string) { if yearStr == "" { - a.filter.MinYear = 0 + a.filterBtn.filter.MinYear = 0 } else if i, err := strconv.Atoi(yearStr); err == nil { - a.filter.MinYear = i + a.filterBtn.filter.MinYear = i } debounceOnChanged() } - if filter.MinYear > 0 { - minYear.Text = strconv.Itoa(filter.MinYear) + if a.filterBtn.filter.MinYear > 0 { + minYear.Text = strconv.Itoa(a.filterBtn.filter.MinYear) } maxYear := NewTextRestrictedEntry(yearValidator) maxYear.SetMinCharWidth(4) maxYear.OnChanged = func(yearStr string) { if yearStr == "" { - a.filter.MaxYear = 0 + a.filterBtn.filter.MaxYear = 0 } else if i, err := strconv.Atoi(yearStr); err == nil { - a.filter.MaxYear = i + a.filterBtn.filter.MaxYear = i } debounceOnChanged() } - if filter.MaxYear > 0 { - maxYear.Text = strconv.Itoa(filter.MaxYear) + if a.filterBtn.filter.MaxYear > 0 { + maxYear.Text = strconv.Itoa(a.filterBtn.filter.MaxYear) } // setup is favorite/not favorite filters - var isNotFavorite *widget.Check - isFavorite := widget.NewCheck("Is favorite", func(fav bool) { + a.isFavorite = widget.NewCheck("Is favorite", func(fav bool) { if fav { - isNotFavorite.SetChecked(false) + a.isNotFavorite.SetChecked(false) } - a.filter.ExcludeUnfavorited = fav + a.filterBtn.filter.ExcludeUnfavorited = fav debounceOnChanged() }) - isNotFavorite = widget.NewCheck("Is not favorite", func(fav bool) { + a.isFavorite.Hidden = a.filterBtn.FavoriteDisabled + a.isNotFavorite = widget.NewCheck("Is not favorite", func(fav bool) { if fav { - isFavorite.SetChecked(false) + a.isFavorite.SetChecked(false) } - a.filter.ExcludeFavorited = fav + a.filterBtn.filter.ExcludeFavorited = fav debounceOnChanged() }) + a.isNotFavorite.Hidden = a.filterBtn.FavoriteDisabled // setup container title := widget.NewLabel("Album filters") @@ -136,12 +141,18 @@ func NewAlbumFilterPopup(filter *backend.AlbumFilter) *AlbumFilterPopup { a.container = container.NewVBox( container.NewHBox(layout.NewSpacer(), title, layout.NewSpacer()), container.NewHBox(widget.NewLabel("Year from"), minYear, widget.NewLabel("to"), maxYear), - container.NewHBox(isFavorite, isNotFavorite), + container.NewHBox(a.isFavorite, a.isNotFavorite), ) return a } +func (a *AlbumFilterPopup) Refresh() { + a.isFavorite.Hidden = a.filterBtn.FavoriteDisabled + a.isNotFavorite.Hidden = a.filterBtn.FavoriteDisabled + a.BaseWidget.Refresh() +} + func (a *AlbumFilterPopup) emitOnChanged() { if a.OnChanged != nil { a.OnChanged()