more work on album filters
This commit is contained in:
+31
-29
@@ -68,6 +68,11 @@ func (f *AlbumFilter) Matches(album *subsonic.AlbumID3) bool {
|
|||||||
return false
|
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 {
|
func (l *LibraryManager) AlbumsIter(sort AlbumSortOrder, filter AlbumFilter) AlbumIterator {
|
||||||
switch sort {
|
switch sort {
|
||||||
case AlbumSortRecentlyAdded:
|
case AlbumSortRecentlyAdded:
|
||||||
@@ -101,10 +106,10 @@ func (l *LibraryManager) GenreIter(genre string, filter AlbumFilter) AlbumIterat
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (l *LibraryManager) SearchIter(query string) AlbumIterator {
|
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)
|
return l.newSearchIter(query, filter)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,7 +124,7 @@ func (l *LibraryManager) GetAlbum(id string) (*subsonic.AlbumID3, error) {
|
|||||||
type baseIter struct {
|
type baseIter struct {
|
||||||
listType string
|
listType string
|
||||||
filter AlbumFilter
|
filter AlbumFilter
|
||||||
pos int
|
serverPos int
|
||||||
l *LibraryManager
|
l *LibraryManager
|
||||||
s *subsonic.Client
|
s *subsonic.Client
|
||||||
opts map[string]string
|
opts map[string]string
|
||||||
@@ -142,36 +147,33 @@ func (r *baseIter) Next() *subsonic.AlbumID3 {
|
|||||||
if r.done {
|
if r.done {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if r.prefetched != nil {
|
if r.prefetched != nil && r.prefetchedPos < len(r.prefetched) {
|
||||||
a := r.prefetched[r.prefetchedPos]
|
a := r.prefetched[r.prefetchedPos]
|
||||||
r.prefetchedPos++
|
r.prefetchedPos++
|
||||||
if r.prefetchedPos == len(r.prefetched) {
|
|
||||||
r.prefetched = nil
|
|
||||||
r.prefetchedPos = 0
|
|
||||||
r.pos++
|
|
||||||
}
|
|
||||||
r.pos++
|
|
||||||
|
|
||||||
return a
|
return a
|
||||||
}
|
}
|
||||||
r.opts["offset"] = strconv.Itoa(r.pos)
|
r.prefetched = nil
|
||||||
albums, err := r.s.GetAlbumList2(r.listType, r.opts)
|
for { // keep fetching until we are done or have mathcing results
|
||||||
if err != nil {
|
r.opts["offset"] = strconv.Itoa(r.serverPos)
|
||||||
log.Println(err)
|
albums, err := r.s.GetAlbumList2(r.listType, r.opts)
|
||||||
albums = nil
|
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
|
r.prefetchedPos = 1
|
||||||
if r.l.PreCacheCoverFn != nil {
|
if r.l.PreCacheCoverFn != nil {
|
||||||
for _, album := range albums {
|
for _, album := range r.prefetched {
|
||||||
go r.l.PreCacheCoverFn(album.CoverArt)
|
go r.l.PreCacheCoverFn(album.CoverArt)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -183,14 +185,14 @@ type searchIter struct {
|
|||||||
searchIterBase
|
searchIterBase
|
||||||
|
|
||||||
l *LibraryManager
|
l *LibraryManager
|
||||||
filter func(*subsonic.AlbumID3) bool
|
filter AlbumFilter
|
||||||
prefetched []*subsonic.AlbumID3
|
prefetched []*subsonic.AlbumID3
|
||||||
prefetchedPos int
|
prefetchedPos int
|
||||||
albumIDset map[string]bool
|
albumIDset map[string]bool
|
||||||
done 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{
|
return &searchIter{
|
||||||
searchIterBase: searchIterBase{
|
searchIterBase: searchIterBase{
|
||||||
query: query,
|
query: query,
|
||||||
@@ -266,7 +268,7 @@ func (s *searchIter) addNewAlbums(al []*subsonic.AlbumID3) {
|
|||||||
if _, have := s.albumIDset[album.ID]; have {
|
if _, have := s.albumIDset[album.ID]; have {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if !s.filter(album) {
|
if !s.filter.Matches(album) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
s.prefetched = append(s.prefetched, album)
|
s.prefetched = append(s.prefetched, album)
|
||||||
|
|||||||
+21
-15
@@ -4,7 +4,6 @@ import (
|
|||||||
"github.com/dweymouth/supersonic/backend"
|
"github.com/dweymouth/supersonic/backend"
|
||||||
"github.com/dweymouth/supersonic/sharedutil"
|
"github.com/dweymouth/supersonic/sharedutil"
|
||||||
"github.com/dweymouth/supersonic/ui/controller"
|
"github.com/dweymouth/supersonic/ui/controller"
|
||||||
"github.com/dweymouth/supersonic/ui/dialogs"
|
|
||||||
"github.com/dweymouth/supersonic/ui/util"
|
"github.com/dweymouth/supersonic/ui/util"
|
||||||
"github.com/dweymouth/supersonic/ui/widgets"
|
"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)
|
iter := lm.AlbumsIter(backend.AlbumSortOrder(a.sortOrder.Selected), a.filter)
|
||||||
a.grid = widgets.NewGridView(widgets.NewGridViewAlbumIterator(iter), im)
|
a.grid = widgets.NewGridView(widgets.NewGridViewAlbumIterator(iter), im)
|
||||||
contr.ConnectAlbumGridActions(a.grid)
|
contr.ConnectAlbumGridActions(a.grid)
|
||||||
a.searcher = widgets.NewSearchEntry()
|
a.createSearchAndFilter("")
|
||||||
a.searcher.OnSearched = a.OnSearched
|
|
||||||
a.filterBtn = widgets.NewAlbumFilterButton(&a.filter, func(filter *backend.AlbumFilter) fyne.CanvasObject { return dialogs.NewAlbumFilterDialog(filter) })
|
|
||||||
a.createContainer(false)
|
a.createContainer(false)
|
||||||
|
|
||||||
return a
|
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) {
|
func (a *AlbumsPage) createContainer(searchgrid bool) {
|
||||||
searchVbox := container.NewVBox(layout.NewSpacer(), a.searcher, layout.NewSpacer())
|
searchVbox := container.NewVBox(layout.NewSpacer(), a.searcher, layout.NewSpacer())
|
||||||
sortVbox := container.NewVBox(layout.NewSpacer(), a.sortOrder, layout.NewSpacer())
|
sortVbox := container.NewVBox(layout.NewSpacer(), a.sortOrder, layout.NewSpacer())
|
||||||
@@ -93,7 +98,7 @@ func (a *AlbumsPage) createContainer(searchgrid bool) {
|
|||||||
g = a.searchGrid
|
g = a.searchGrid
|
||||||
}
|
}
|
||||||
a.container = container.NewBorder(
|
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,
|
nil,
|
||||||
nil,
|
nil,
|
||||||
@@ -103,11 +108,13 @@ func (a *AlbumsPage) createContainer(searchgrid bool) {
|
|||||||
|
|
||||||
func restoreAlbumsPage(saved *savedAlbumsPage) *AlbumsPage {
|
func restoreAlbumsPage(saved *savedAlbumsPage) *AlbumsPage {
|
||||||
a := &AlbumsPage{
|
a := &AlbumsPage{
|
||||||
cfg: saved.cfg,
|
cfg: saved.cfg,
|
||||||
contr: saved.contr,
|
contr: saved.contr,
|
||||||
pm: saved.pm,
|
pm: saved.pm,
|
||||||
lm: saved.lm,
|
lm: saved.lm,
|
||||||
im: saved.im,
|
im: saved.im,
|
||||||
|
searchText: saved.searchText,
|
||||||
|
filter: saved.filter,
|
||||||
}
|
}
|
||||||
a.ExtendBaseWidget(a)
|
a.ExtendBaseWidget(a)
|
||||||
|
|
||||||
@@ -119,13 +126,10 @@ func restoreAlbumsPage(saved *savedAlbumsPage) *AlbumsPage {
|
|||||||
a.sortOrder.Selected = saved.sortOrder
|
a.sortOrder.Selected = saved.sortOrder
|
||||||
a.sortOrder.OnChanged = a.onSortOrderChanged
|
a.sortOrder.OnChanged = a.onSortOrderChanged
|
||||||
a.grid = widgets.NewGridViewFromState(saved.gridState)
|
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 != "" {
|
if a.searchText != "" {
|
||||||
a.searchGrid = widgets.NewGridViewFromState(saved.searchGridState)
|
a.searchGrid = widgets.NewGridViewFromState(saved.searchGridState)
|
||||||
}
|
}
|
||||||
|
a.createSearchAndFilter(saved.searchText)
|
||||||
a.createContainer(saved.searchText != "")
|
a.createContainer(saved.searchText != "")
|
||||||
|
|
||||||
return a
|
return a
|
||||||
@@ -172,6 +176,7 @@ func (a *AlbumsPage) Save() SavedPage {
|
|||||||
lm: a.lm,
|
lm: a.lm,
|
||||||
im: a.im,
|
im: a.im,
|
||||||
searchText: a.searchText,
|
searchText: a.searchText,
|
||||||
|
filter: a.filter,
|
||||||
sortOrder: a.sortOrder.Selected,
|
sortOrder: a.sortOrder.Selected,
|
||||||
gridState: a.grid.SaveToState(),
|
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.searchGrid = widgets.NewGridView(widgets.NewGridViewAlbumIterator(a.lm.SearchIter(query)), a.im)
|
||||||
a.contr.ConnectAlbumGridActions(a.searchGrid)
|
a.contr.ConnectAlbumGridActions(a.searchGrid)
|
||||||
} else {
|
} 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.container.Objects[0] = a.searchGrid
|
||||||
a.Refresh()
|
a.Refresh()
|
||||||
@@ -209,6 +214,7 @@ func (a *AlbumsPage) CreateRenderer() fyne.WidgetRenderer {
|
|||||||
|
|
||||||
type savedAlbumsPage struct {
|
type savedAlbumsPage struct {
|
||||||
searchText string
|
searchText string
|
||||||
|
filter backend.AlbumFilter
|
||||||
cfg *backend.AlbumsPageConfig
|
cfg *backend.AlbumsPageConfig
|
||||||
contr *controller.Controller
|
contr *controller.Controller
|
||||||
pm *backend.PlaybackManager
|
pm *backend.PlaybackManager
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package browsing
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"log"
|
"log"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/dweymouth/supersonic/backend"
|
"github.com/dweymouth/supersonic/backend"
|
||||||
"github.com/dweymouth/supersonic/ui/controller"
|
"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) {
|
func (a *FavoritesPage) doSearchAlbums(query string) {
|
||||||
iter := a.lm.SearchIterWithFilter(query, func(al *subsonic.AlbumID3) bool {
|
iter := a.lm.SearchIterWithFilter(query, backend.AlbumFilter{ExcludeUnfavorited: true})
|
||||||
return al.Starred.After(time.Time{})
|
|
||||||
})
|
|
||||||
if a.searchGrid == nil {
|
if a.searchGrid == nil {
|
||||||
a.searchGrid = widgets.NewGridView(widgets.NewGridViewAlbumIterator(iter), a.im)
|
a.searchGrid = widgets.NewGridView(widgets.NewGridViewAlbumIterator(iter), a.im)
|
||||||
a.contr.ConnectAlbumGridActions(a.searchGrid)
|
a.contr.ConnectAlbumGridActions(a.searchGrid)
|
||||||
|
|||||||
@@ -12,8 +12,6 @@ import (
|
|||||||
"fyne.io/fyne/v2/layout"
|
"fyne.io/fyne/v2/layout"
|
||||||
"fyne.io/fyne/v2/theme"
|
"fyne.io/fyne/v2/theme"
|
||||||
"fyne.io/fyne/v2/widget"
|
"fyne.io/fyne/v2/widget"
|
||||||
|
|
||||||
"github.com/dweymouth/go-subsonic/subsonic"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// TODO: there is a lot of code duplication between this and albumspage. Refactor?
|
// 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) {
|
func (g *GenrePage) doSearch(query string) {
|
||||||
iter := g.lm.SearchIterWithFilter(query, func(al *subsonic.AlbumID3) bool {
|
iter := g.lm.SearchIterWithFilter(query, backend.AlbumFilter{Genres: []string{g.genre}})
|
||||||
return al.Genre == g.genre
|
|
||||||
})
|
|
||||||
if g.searchGrid == nil {
|
if g.searchGrid == nil {
|
||||||
g.searchGrid = widgets.NewGridView(widgets.NewGridViewAlbumIterator(iter), g.im)
|
g.searchGrid = widgets.NewGridView(widgets.NewGridViewAlbumIterator(iter), g.im)
|
||||||
g.contr.ConnectAlbumGridActions(g.searchGrid)
|
g.contr.ConnectAlbumGridActions(g.searchGrid)
|
||||||
|
|||||||
@@ -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)
|
|
||||||
}
|
|
||||||
@@ -113,8 +113,8 @@ func (s *SettingsDialog) createGeneralTab() *container.TabItem {
|
|||||||
|
|
||||||
// Scrobble settings
|
// Scrobble settings
|
||||||
|
|
||||||
twoDigitValidator := func(text string, r rune) bool {
|
twoDigitValidator := func(text, selText string, r rune) bool {
|
||||||
return unicode.IsDigit(r) && len(text) < 2
|
return unicode.IsDigit(r) && len(text)-len(selText) < 2
|
||||||
}
|
}
|
||||||
|
|
||||||
percentEntry := widgets.NewTextRestrictedEntry(twoDigitValidator)
|
percentEntry := widgets.NewTextRestrictedEntry(twoDigitValidator)
|
||||||
@@ -247,7 +247,7 @@ func (s *SettingsDialog) createPlaybackTab() *container.TabItem {
|
|||||||
replayGainSelect.SetSelectedIndex(0)
|
replayGainSelect.SetSelectedIndex(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
preampGain := widgets.NewTextRestrictedEntry(func(curText string, r rune) bool {
|
preampGain := widgets.NewTextRestrictedEntry(func(curText, _ string, r rune) bool {
|
||||||
return (curText == "" && r == '-') ||
|
return (curText == "" && r == '-') ||
|
||||||
(curText == "" && unicode.IsDigit(r)) ||
|
(curText == "" && unicode.IsDigit(r)) ||
|
||||||
((curText == "-" || curText == "0") && unicode.IsDigit(r))
|
((curText == "-" || curText == "0") && unicode.IsDigit(r))
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -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()
|
|
||||||
}
|
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
@@ -13,19 +13,21 @@ import (
|
|||||||
type TextRestrictedEntry struct {
|
type TextRestrictedEntry struct {
|
||||||
widget.Entry
|
widget.Entry
|
||||||
|
|
||||||
charAllowed func(string, rune) bool
|
charAllowed CharAllowedFunc
|
||||||
|
|
||||||
minWidth float32
|
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 := &TextRestrictedEntry{charAllowed: charAllowed}
|
||||||
e.ExtendBaseWidget(e)
|
e.ExtendBaseWidget(e)
|
||||||
return e
|
return e
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *TextRestrictedEntry) TypedRune(r rune) {
|
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)
|
e.Entry.TypedRune(r)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user