add genres to album filters panel
This commit is contained in:
@@ -87,7 +87,7 @@ func (a *AlbumsPage) createSearchAndFilter() {
|
|||||||
a.searcher = widgets.NewSearchEntry()
|
a.searcher = widgets.NewSearchEntry()
|
||||||
a.searcher.Text = a.searchText
|
a.searcher.Text = a.searchText
|
||||||
a.searcher.OnSearched = a.OnSearched
|
a.searcher.OnSearched = a.OnSearched
|
||||||
a.filterBtn = widgets.NewAlbumFilterButton(&a.filter)
|
a.filterBtn = widgets.NewAlbumFilterButton(&a.filter, a.mp.GetGenres)
|
||||||
a.filterBtn.OnChanged = a.Reload
|
a.filterBtn.OnChanged = a.Reload
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ func (a *FavoritesPage) createHeader(activeBtnIdx int) {
|
|||||||
a.searcher = widgets.NewSearchEntry()
|
a.searcher = widgets.NewSearchEntry()
|
||||||
a.searcher.OnSearched = a.OnSearched
|
a.searcher.OnSearched = a.OnSearched
|
||||||
a.searcher.Entry.Text = a.searchText
|
a.searcher.Entry.Text = a.searchText
|
||||||
a.filterBtn = widgets.NewAlbumFilterButton(&a.filter)
|
a.filterBtn = widgets.NewAlbumFilterButton(&a.filter, a.mp.GetGenres)
|
||||||
a.filterBtn.FavoriteDisabled = true
|
a.filterBtn.FavoriteDisabled = true
|
||||||
a.filterBtn.OnChanged = a.Reload
|
a.filterBtn.OnChanged = a.Reload
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,7 +67,8 @@ func (g *GenrePage) createSearchAndFilter() {
|
|||||||
g.searcher = widgets.NewSearchEntry()
|
g.searcher = widgets.NewSearchEntry()
|
||||||
g.searcher.Text = g.searchText
|
g.searcher.Text = g.searchText
|
||||||
g.searcher.OnSearched = g.OnSearched
|
g.searcher.OnSearched = g.OnSearched
|
||||||
g.filterBtn = widgets.NewAlbumFilterButton(&g.filter)
|
// genre filter is disabled for this page, so no need to actually call genre list fetching function
|
||||||
|
g.filterBtn = widgets.NewAlbumFilterButton(&g.filter, func() ([]*mediaprovider.Genre, error) { return nil, nil })
|
||||||
g.filterBtn.GenreDisabled = true
|
g.filterBtn.GenreDisabled = true
|
||||||
g.filterBtn.OnChanged = g.Reload
|
g.filterBtn.OnChanged = g.Reload
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,23 @@
|
|||||||
package widgets
|
package widgets
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
"unicode"
|
"unicode"
|
||||||
|
|
||||||
"fyne.io/fyne/v2"
|
"fyne.io/fyne/v2"
|
||||||
"fyne.io/fyne/v2/container"
|
"fyne.io/fyne/v2/container"
|
||||||
"fyne.io/fyne/v2/layout"
|
"fyne.io/fyne/v2/layout"
|
||||||
|
"fyne.io/fyne/v2/theme"
|
||||||
"fyne.io/fyne/v2/widget"
|
"fyne.io/fyne/v2/widget"
|
||||||
"github.com/dweymouth/supersonic/backend/mediaprovider"
|
"github.com/dweymouth/supersonic/backend/mediaprovider"
|
||||||
"github.com/dweymouth/supersonic/ui/theme"
|
"github.com/dweymouth/supersonic/sharedutil"
|
||||||
|
"github.com/dweymouth/supersonic/ui/layouts"
|
||||||
|
myTheme "github.com/dweymouth/supersonic/ui/theme"
|
||||||
"github.com/dweymouth/supersonic/ui/util"
|
"github.com/dweymouth/supersonic/ui/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -21,19 +28,31 @@ type AlbumFilterButton struct {
|
|||||||
GenreDisabled bool
|
GenreDisabled bool
|
||||||
FavoriteDisabled bool
|
FavoriteDisabled bool
|
||||||
|
|
||||||
|
genreListChan chan []string
|
||||||
|
|
||||||
filter *mediaprovider.AlbumFilter
|
filter *mediaprovider.AlbumFilter
|
||||||
dialog *widget.PopUp
|
dialog *widget.PopUp
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewAlbumFilterButton(filter *mediaprovider.AlbumFilter) *AlbumFilterButton {
|
func NewAlbumFilterButton(filter *mediaprovider.AlbumFilter, fetchGenresFunc func() ([]*mediaprovider.Genre, error)) *AlbumFilterButton {
|
||||||
a := &AlbumFilterButton{
|
a := &AlbumFilterButton{
|
||||||
filter: filter,
|
filter: filter,
|
||||||
Button: widget.Button{
|
Button: widget.Button{
|
||||||
Icon: theme.FilterIcon,
|
Icon: myTheme.FilterIcon,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
a.OnTapped = a.showFilterDialog
|
a.OnTapped = a.showFilterDialog
|
||||||
a.ExtendBaseWidget(a)
|
a.ExtendBaseWidget(a)
|
||||||
|
a.genreListChan = make(chan []string)
|
||||||
|
go func() {
|
||||||
|
if genres, err := fetchGenresFunc(); err == nil {
|
||||||
|
genreNames := sharedutil.MapSlice(genres, func(g *mediaprovider.Genre) string {
|
||||||
|
return g.Name
|
||||||
|
})
|
||||||
|
sort.Strings(genreNames)
|
||||||
|
a.genreListChan <- genreNames
|
||||||
|
}
|
||||||
|
}()
|
||||||
return a
|
return a
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,6 +95,7 @@ type AlbumFilterPopup struct {
|
|||||||
|
|
||||||
isFavorite *widget.Check
|
isFavorite *widget.Check
|
||||||
isNotFavorite *widget.Check
|
isNotFavorite *widget.Check
|
||||||
|
genreFilter *GenreFilterSubsection
|
||||||
filterBtn *AlbumFilterButton
|
filterBtn *AlbumFilterButton
|
||||||
container *fyne.Container
|
container *fyne.Container
|
||||||
}
|
}
|
||||||
@@ -136,6 +156,13 @@ func NewAlbumFilterPopup(filter *AlbumFilterButton) *AlbumFilterPopup {
|
|||||||
})
|
})
|
||||||
a.isNotFavorite.Hidden = a.filterBtn.FavoriteDisabled
|
a.isNotFavorite.Hidden = a.filterBtn.FavoriteDisabled
|
||||||
|
|
||||||
|
// create genre filter subsection
|
||||||
|
a.genreFilter = NewGenreFilterSubsection(func(selectedGenres []string) {
|
||||||
|
a.filterBtn.filter.Genres = selectedGenres
|
||||||
|
debounceOnChanged()
|
||||||
|
}, a.filterBtn.filter.Genres)
|
||||||
|
a.genreFilter.Hidden = a.filterBtn.GenreDisabled
|
||||||
|
|
||||||
// setup container
|
// setup container
|
||||||
title := widget.NewLabel("Album filters")
|
title := widget.NewLabel("Album filters")
|
||||||
title.TextStyle.Bold = true
|
title.TextStyle.Bold = true
|
||||||
@@ -143,14 +170,25 @@ func NewAlbumFilterPopup(filter *AlbumFilterButton) *AlbumFilterPopup {
|
|||||||
container.NewHBox(layout.NewSpacer(), title, layout.NewSpacer()),
|
container.NewHBox(layout.NewSpacer(), title, layout.NewSpacer()),
|
||||||
container.NewHBox(widget.NewLabel("Year from"), minYear, widget.NewLabel("to"), maxYear),
|
container.NewHBox(widget.NewLabel("Year from"), minYear, widget.NewLabel("to"), maxYear),
|
||||||
container.NewHBox(a.isFavorite, a.isNotFavorite),
|
container.NewHBox(a.isFavorite, a.isNotFavorite),
|
||||||
|
a.genreFilter,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
a.genreFilter.SetGenreList(<-a.filterBtn.genreListChan)
|
||||||
|
}()
|
||||||
|
|
||||||
return a
|
return a
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *AlbumFilterPopup) Tapped(_ *fyne.PointEvent) {
|
||||||
|
// swallow the Tapped event so that the popup is
|
||||||
|
// only dismissed by clicking outside of it
|
||||||
|
}
|
||||||
|
|
||||||
func (a *AlbumFilterPopup) Refresh() {
|
func (a *AlbumFilterPopup) Refresh() {
|
||||||
a.isFavorite.Hidden = a.filterBtn.FavoriteDisabled
|
a.isFavorite.Hidden = a.filterBtn.FavoriteDisabled
|
||||||
a.isNotFavorite.Hidden = a.filterBtn.FavoriteDisabled
|
a.isNotFavorite.Hidden = a.filterBtn.FavoriteDisabled
|
||||||
|
a.genreFilter.Hidden = a.filterBtn.GenreDisabled
|
||||||
a.BaseWidget.Refresh()
|
a.BaseWidget.Refresh()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,3 +201,175 @@ func (a *AlbumFilterPopup) emitOnChanged() {
|
|||||||
func (a *AlbumFilterPopup) CreateRenderer() fyne.WidgetRenderer {
|
func (a *AlbumFilterPopup) CreateRenderer() fyne.WidgetRenderer {
|
||||||
return widget.NewSimpleRenderer(a.container)
|
return widget.NewSimpleRenderer(a.container)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type GenreFilterSubsection struct {
|
||||||
|
widget.BaseWidget
|
||||||
|
|
||||||
|
genreList []string
|
||||||
|
onChanged func([]string)
|
||||||
|
|
||||||
|
selectedGenres map[string]interface{}
|
||||||
|
selectedGenresMutex sync.RWMutex
|
||||||
|
|
||||||
|
filterText *widget.Entry
|
||||||
|
numSelectedText *widget.Label
|
||||||
|
allBtn *widget.Button
|
||||||
|
noneBtn *widget.Button
|
||||||
|
listModelMutex sync.RWMutex
|
||||||
|
genreListViewModel []string
|
||||||
|
genreListView *widget.List
|
||||||
|
|
||||||
|
container *fyne.Container
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewGenreFilterSubsection(onChanged func([]string), initialSelectedGenres []string) *GenreFilterSubsection {
|
||||||
|
g := &GenreFilterSubsection{
|
||||||
|
onChanged: onChanged,
|
||||||
|
selectedGenres: make(map[string]interface{}),
|
||||||
|
}
|
||||||
|
g.ExtendBaseWidget(g)
|
||||||
|
|
||||||
|
for _, genre := range initialSelectedGenres {
|
||||||
|
g.selectedGenres[genre] = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
g.genreListView = widget.NewList(
|
||||||
|
func() int {
|
||||||
|
g.listModelMutex.RLock()
|
||||||
|
defer g.listModelMutex.RUnlock()
|
||||||
|
return len(g.genreListViewModel)
|
||||||
|
},
|
||||||
|
func() fyne.CanvasObject {
|
||||||
|
return newGenreListViewRow(g.onGenreSelected)
|
||||||
|
},
|
||||||
|
func(id widget.ListItemID, obj fyne.CanvasObject) {
|
||||||
|
g.listModelMutex.RLock()
|
||||||
|
defer g.listModelMutex.RUnlock()
|
||||||
|
genre := g.genreListViewModel[id]
|
||||||
|
g.selectedGenresMutex.RLock()
|
||||||
|
_, selected := g.selectedGenres[genre]
|
||||||
|
g.selectedGenresMutex.RUnlock()
|
||||||
|
row := obj.(*genreListViewRow)
|
||||||
|
row.ID = id
|
||||||
|
row.Text = genre
|
||||||
|
row.Checked = selected
|
||||||
|
row.Refresh()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
g.filterText = widget.NewEntry()
|
||||||
|
g.filterText.SetPlaceHolder("Filter genres")
|
||||||
|
i := NewTappbaleIcon(theme.ContentClearIcon())
|
||||||
|
i.NoPointerCursor = true
|
||||||
|
i.OnTapped = func() { g.filterText.SetText("") }
|
||||||
|
g.filterText.ActionItem = i
|
||||||
|
debouncer := util.NewDebouncer(300*time.Millisecond, g.updateGenreListView)
|
||||||
|
g.filterText.OnChanged = func(_ string) {
|
||||||
|
debouncer()
|
||||||
|
}
|
||||||
|
g.allBtn = widget.NewButton("All", func() { g.selectAllOrNoneInView(false) })
|
||||||
|
g.noneBtn = widget.NewButton("None", func() { g.selectAllOrNoneInView(true) })
|
||||||
|
|
||||||
|
title := widget.NewRichTextWithText("Genres")
|
||||||
|
title.Segments[0].(*widget.TextSegment).Style.TextStyle.Bold = true
|
||||||
|
g.numSelectedText = widget.NewLabel("(none selected)")
|
||||||
|
g.updateNumSelectedText()
|
||||||
|
titleRow := container.New(&layouts.HboxCustomPadding{ExtraPad: -10}, title, g.numSelectedText)
|
||||||
|
|
||||||
|
filterRow := container.NewBorder(nil, nil, nil, container.NewHBox(g.allBtn, g.noneBtn), g.filterText)
|
||||||
|
g.container = container.NewBorder(titleRow, nil, nil, nil,
|
||||||
|
container.New(&layouts.MaxPadLayout{PadLeft: 5, PadRight: 5},
|
||||||
|
container.NewBorder(filterRow, nil, nil, nil, g.genreListView),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return g
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *GenreFilterSubsection) MinSize() fyne.Size {
|
||||||
|
return fyne.NewSize(g.BaseWidget.MinSize().Width, 250)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *GenreFilterSubsection) SetGenreList(genres []string) {
|
||||||
|
g.genreList = genres
|
||||||
|
g.updateGenreListView()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *GenreFilterSubsection) updateGenreListView() {
|
||||||
|
g.listModelMutex.Lock()
|
||||||
|
if g.filterText.Text == "" {
|
||||||
|
g.genreListViewModel = g.genreList
|
||||||
|
} else {
|
||||||
|
filterText := strings.ToLower(g.filterText.Text)
|
||||||
|
g.genreListViewModel = sharedutil.FilterSlice(g.genreList, func(genre string) bool {
|
||||||
|
return strings.Contains(strings.ToLower(genre), filterText)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
g.listModelMutex.Unlock()
|
||||||
|
g.genreListView.Refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *GenreFilterSubsection) onGenreSelected(row widget.ListItemID, selected bool) {
|
||||||
|
g.listModelMutex.RLock()
|
||||||
|
g.selectedGenresMutex.Lock()
|
||||||
|
if selected {
|
||||||
|
g.selectedGenres[g.genreListViewModel[row]] = nil
|
||||||
|
} else {
|
||||||
|
delete(g.selectedGenres, g.genreListViewModel[row])
|
||||||
|
}
|
||||||
|
g.selectedGenresMutex.Unlock()
|
||||||
|
g.listModelMutex.RUnlock()
|
||||||
|
g.invokeOnChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *GenreFilterSubsection) selectAllOrNoneInView(none bool) {
|
||||||
|
g.listModelMutex.RLock()
|
||||||
|
g.selectedGenresMutex.Lock()
|
||||||
|
for _, genre := range g.genreListViewModel {
|
||||||
|
if none {
|
||||||
|
delete(g.selectedGenres, genre)
|
||||||
|
} else {
|
||||||
|
g.selectedGenres[genre] = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
g.selectedGenresMutex.Unlock()
|
||||||
|
g.listModelMutex.RUnlock()
|
||||||
|
g.genreListView.Refresh()
|
||||||
|
g.invokeOnChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *GenreFilterSubsection) invokeOnChanged() {
|
||||||
|
g.selectedGenresMutex.RLock()
|
||||||
|
g.updateNumSelectedText()
|
||||||
|
genres := make([]string, 0, len(g.selectedGenres))
|
||||||
|
for genre := range g.selectedGenres {
|
||||||
|
genres = append(genres, genre)
|
||||||
|
}
|
||||||
|
g.selectedGenresMutex.RUnlock()
|
||||||
|
g.onChanged(genres)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *GenreFilterSubsection) updateNumSelectedText() {
|
||||||
|
numText := "none"
|
||||||
|
if l := len(g.selectedGenres); l > 0 {
|
||||||
|
numText = strconv.Itoa(l)
|
||||||
|
}
|
||||||
|
g.numSelectedText.SetText(fmt.Sprintf("(%s selected)", numText))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *GenreFilterSubsection) CreateRenderer() fyne.WidgetRenderer {
|
||||||
|
return widget.NewSimpleRenderer(g.container)
|
||||||
|
}
|
||||||
|
|
||||||
|
type genreListViewRow struct {
|
||||||
|
widget.Check
|
||||||
|
|
||||||
|
ID widget.ListItemID
|
||||||
|
}
|
||||||
|
|
||||||
|
func newGenreListViewRow(onChanged func(widget.ListItemID, bool)) *genreListViewRow {
|
||||||
|
g := &genreListViewRow{}
|
||||||
|
g.ExtendBaseWidget(g)
|
||||||
|
g.OnChanged = func(b bool) {
|
||||||
|
onChanged(g.ID, b)
|
||||||
|
}
|
||||||
|
return g
|
||||||
|
}
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ import (
|
|||||||
type TappableIcon struct {
|
type TappableIcon struct {
|
||||||
widget.Icon
|
widget.Icon
|
||||||
|
|
||||||
OnTapped func()
|
NoPointerCursor bool
|
||||||
|
OnTapped func()
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewTappbaleIcon(res fyne.Resource) *TappableIcon {
|
func NewTappbaleIcon(res fyne.Resource) *TappableIcon {
|
||||||
@@ -42,6 +43,9 @@ func (t *TappableIcon) MouseOut() {}
|
|||||||
func (t *TappableIcon) MouseMoved(*desktop.MouseEvent) {}
|
func (t *TappableIcon) MouseMoved(*desktop.MouseEvent) {}
|
||||||
|
|
||||||
func (t *TappableIcon) Cursor() desktop.Cursor {
|
func (t *TappableIcon) Cursor() desktop.Cursor {
|
||||||
|
if t.NoPointerCursor {
|
||||||
|
return desktop.DefaultCursor
|
||||||
|
}
|
||||||
return desktop.PointerCursor
|
return desktop.PointerCursor
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user