more work on album filters

This commit is contained in:
Drew Weymouth
2023-05-11 16:45:47 -07:00
parent b2dce99714
commit 87ae111cb2
10 changed files with 250 additions and 206 deletions
+31 -29
View File
@@ -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)
+21 -15
View File
@@ -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
+1 -4
View File
@@ -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)
+1 -5
View File
@@ -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)
-76
View File
@@ -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)
}
+3 -3
View File
@@ -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))
+147
View File
@@ -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)
}
-71
View File
@@ -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()
}
+41
View File
@@ -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()
}
+5 -3
View File
@@ -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)
}
}