simplify SearchEntry and extract a debouncer util func
This commit is contained in:
@@ -26,7 +26,7 @@ type AlbumsPage struct {
|
||||
lm *backend.LibraryManager
|
||||
grid *widgets.GridView
|
||||
searchGrid *widgets.GridView
|
||||
searcher *widgets.Searcher
|
||||
searcher *widgets.SearchEntry
|
||||
searchText string
|
||||
titleDisp *widget.RichText
|
||||
sortOrder *selectWidget
|
||||
@@ -74,7 +74,7 @@ func NewAlbumsPage(cfg *backend.AlbumsPageConfig, contr *controller.Controller,
|
||||
iter := lm.AlbumsIter(backend.AlbumSortOrder(a.sortOrder.Selected))
|
||||
a.grid = widgets.NewGridView(widgets.NewGridViewAlbumIterator(iter), im)
|
||||
contr.ConnectAlbumGridActions(a.grid)
|
||||
a.searcher = widgets.NewSearcher()
|
||||
a.searcher = widgets.NewSearchEntry()
|
||||
a.searcher.OnSearched = a.OnSearched
|
||||
a.createContainer(false)
|
||||
|
||||
@@ -82,7 +82,7 @@ func NewAlbumsPage(cfg *backend.AlbumsPageConfig, contr *controller.Controller,
|
||||
}
|
||||
|
||||
func (a *AlbumsPage) createContainer(searchgrid bool) {
|
||||
searchVbox := container.NewVBox(layout.NewSpacer(), a.searcher.Entry, layout.NewSpacer())
|
||||
searchVbox := container.NewVBox(layout.NewSpacer(), a.searcher, layout.NewSpacer())
|
||||
sortVbox := container.NewVBox(layout.NewSpacer(), a.sortOrder, layout.NewSpacer())
|
||||
g := a.grid
|
||||
if searchgrid {
|
||||
@@ -115,7 +115,7 @@ func restoreAlbumsPage(saved *savedAlbumsPage) *AlbumsPage {
|
||||
a.sortOrder.Selected = saved.sortOrder
|
||||
a.sortOrder.OnChanged = a.onSortOrderChanged
|
||||
a.grid = widgets.NewGridViewFromState(saved.gridState)
|
||||
a.searcher = widgets.NewSearcher()
|
||||
a.searcher = widgets.NewSearchEntry()
|
||||
a.searcher.OnSearched = a.OnSearched
|
||||
a.searcher.Entry.Text = saved.searchText
|
||||
a.searchText = saved.searchText
|
||||
@@ -147,7 +147,7 @@ func (a *AlbumsPage) Route() controller.Route {
|
||||
var _ Searchable = (*AlbumsPage)(nil)
|
||||
|
||||
func (a *AlbumsPage) SearchWidget() fyne.Focusable {
|
||||
return a.searcher.Entry
|
||||
return a.searcher
|
||||
}
|
||||
|
||||
func (a *AlbumsPage) Reload() {
|
||||
|
||||
@@ -33,7 +33,7 @@ type ArtistsGenresPage struct {
|
||||
|
||||
titleDisp *widget.RichText
|
||||
container *fyne.Container
|
||||
searcher *widgets.Searcher
|
||||
searcher *widgets.SearchEntry
|
||||
}
|
||||
|
||||
func NewArtistsGenresPage(isGenresPage bool, contr *controller.Controller, sm *backend.ServerManager) *ArtistsGenresPage {
|
||||
@@ -63,7 +63,7 @@ func newArtistsGenresPage(isGenresPage bool, contr *controller.Controller, sm *b
|
||||
a.contr.NavigateTo(controller.ArtistRoute(id))
|
||||
}
|
||||
}
|
||||
a.searcher = widgets.NewSearcher()
|
||||
a.searcher = widgets.NewSearchEntry()
|
||||
a.searcher.OnSearched = a.onSearched
|
||||
a.searcher.Entry.Text = searchText
|
||||
a.buildContainer()
|
||||
@@ -111,7 +111,7 @@ func (a *ArtistsGenresPage) onSearched(query string) {
|
||||
var _ Searchable = (*ArtistsGenresPage)(nil)
|
||||
|
||||
func (a *ArtistsGenresPage) SearchWidget() fyne.Focusable {
|
||||
return a.searcher.Entry
|
||||
return a.searcher
|
||||
}
|
||||
|
||||
func (a *ArtistsGenresPage) Route() controller.Route {
|
||||
@@ -175,7 +175,7 @@ func (a *ArtistsGenresPage) buildGenresListModel(genres []*subsonic.Genre) []wid
|
||||
}
|
||||
|
||||
func (a *ArtistsGenresPage) buildContainer() {
|
||||
searchVbox := container.NewVBox(layout.NewSpacer(), a.searcher.Entry, layout.NewSpacer())
|
||||
searchVbox := container.NewVBox(layout.NewSpacer(), a.searcher, layout.NewSpacer())
|
||||
a.container = container.New(&layouts.MaxPadLayout{PadLeft: 15, PadRight: 15, PadTop: 5, PadBottom: 15},
|
||||
container.NewBorder(
|
||||
container.New(&layouts.MaxPadLayout{PadLeft: -5},
|
||||
|
||||
@@ -38,7 +38,7 @@ type FavoritesPage struct {
|
||||
searchGrid *widgets.GridView
|
||||
artistListCtr *fyne.Container
|
||||
tracklistCtr *fyne.Container
|
||||
searcher *widgets.Searcher
|
||||
searcher *widgets.SearchEntry
|
||||
titleDisp *widget.RichText
|
||||
toggleBtns *widgets.ToggleButtonGroup
|
||||
container *fyne.Container
|
||||
@@ -78,13 +78,13 @@ func (a *FavoritesPage) createHeader(activeBtnIdx int, searchText string) {
|
||||
widget.NewButtonWithIcon("", myTheme.AlbumIcon, a.onShowFavoriteAlbums),
|
||||
widget.NewButtonWithIcon("", myTheme.ArtistIcon, a.onShowFavoriteArtists),
|
||||
widget.NewButtonWithIcon("", myTheme.TracksIcon, a.onShowFavoriteSongs))
|
||||
a.searcher = widgets.NewSearcher()
|
||||
a.searcher = widgets.NewSearchEntry()
|
||||
a.searcher.OnSearched = a.OnSearched
|
||||
a.searcher.Entry.Text = searchText
|
||||
}
|
||||
|
||||
func (a *FavoritesPage) createContainer(initialView fyne.CanvasObject) {
|
||||
searchVbox := container.NewVBox(layout.NewSpacer(), a.searcher.Entry, layout.NewSpacer())
|
||||
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)),
|
||||
nil, nil, nil, initialView)
|
||||
@@ -182,7 +182,7 @@ func (a *FavoritesPage) Save() SavedPage {
|
||||
var _ Searchable = (*FavoritesPage)(nil)
|
||||
|
||||
func (a *FavoritesPage) SearchWidget() fyne.Focusable {
|
||||
return a.searcher.Entry
|
||||
return a.searcher
|
||||
}
|
||||
|
||||
func (a *FavoritesPage) OnSearched(query string) {
|
||||
|
||||
@@ -27,7 +27,7 @@ type GenrePage struct {
|
||||
lm *backend.LibraryManager
|
||||
grid *widgets.GridView
|
||||
searchGrid *widgets.GridView
|
||||
searcher *widgets.Searcher
|
||||
searcher *widgets.SearchEntry
|
||||
searchText string
|
||||
titleDisp *widget.RichText
|
||||
playRandom *widget.Button
|
||||
@@ -55,7 +55,7 @@ func NewGenrePage(genre string, contr *controller.Controller, pm *backend.Playba
|
||||
iter := g.lm.GenreIter(g.genre)
|
||||
g.grid = widgets.NewGridView(widgets.NewGridViewAlbumIterator(iter), g.im)
|
||||
g.contr.ConnectAlbumGridActions(g.grid)
|
||||
g.searcher = widgets.NewSearcher()
|
||||
g.searcher = widgets.NewSearchEntry()
|
||||
g.searcher.OnSearched = g.OnSearched
|
||||
g.createContainer(false)
|
||||
|
||||
@@ -63,7 +63,7 @@ func NewGenrePage(genre string, contr *controller.Controller, pm *backend.Playba
|
||||
}
|
||||
|
||||
func (g *GenrePage) createContainer(searchGrid bool) {
|
||||
searchVbox := container.NewVBox(layout.NewSpacer(), g.searcher.Entry, layout.NewSpacer())
|
||||
searchVbox := container.NewVBox(layout.NewSpacer(), g.searcher, layout.NewSpacer())
|
||||
gr := g.grid
|
||||
if searchGrid {
|
||||
gr = g.searchGrid
|
||||
@@ -94,7 +94,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.NewSearcher()
|
||||
g.searcher = widgets.NewSearchEntry()
|
||||
g.searcher.OnSearched = g.OnSearched
|
||||
g.searcher.Entry.Text = saved.searchText
|
||||
g.searchText = saved.searchText
|
||||
@@ -142,7 +142,7 @@ func (g *GenrePage) Save() SavedPage {
|
||||
var _ Searchable = (*AlbumsPage)(nil)
|
||||
|
||||
func (g *GenrePage) SearchWidget() fyne.Focusable {
|
||||
return g.searcher.Entry
|
||||
return g.searcher
|
||||
}
|
||||
|
||||
func (g *GenrePage) OnSearched(query string) {
|
||||
|
||||
@@ -32,7 +32,7 @@ type PlaylistsPage struct {
|
||||
searchedPlaylists []*subsonic.Playlist
|
||||
|
||||
viewToggle *widgets.ToggleButtonGroup
|
||||
searcher *widgets.Searcher
|
||||
searcher *widgets.SearchEntry
|
||||
titleDisp *widget.RichText
|
||||
container *fyne.Container
|
||||
listView *PlaylistList
|
||||
@@ -56,7 +56,7 @@ func newPlaylistsPage(contr *controller.Controller, cfg *backend.PlaylistsPageCo
|
||||
}
|
||||
a.ExtendBaseWidget(a)
|
||||
a.titleDisp.Segments[0].(*widget.TextSegment).Style.SizeName = theme.SizeNameHeadingText
|
||||
a.searcher = widgets.NewSearcher()
|
||||
a.searcher = widgets.NewSearchEntry()
|
||||
a.searcher.OnSearched = a.onSearched
|
||||
a.searcher.Entry.Text = searchText
|
||||
a.viewToggle = widgets.NewToggleButtonGroup(0,
|
||||
@@ -199,7 +199,7 @@ func (a *PlaylistsPage) refreshView(playlists []*subsonic.Playlist) {
|
||||
var _ Searchable = (*PlaylistsPage)(nil)
|
||||
|
||||
func (a *PlaylistsPage) SearchWidget() fyne.Focusable {
|
||||
return a.searcher.Entry
|
||||
return a.searcher
|
||||
}
|
||||
|
||||
func (a *PlaylistsPage) Route() controller.Route {
|
||||
@@ -233,7 +233,7 @@ func (s *savedPlaylistsPage) Restore() Page {
|
||||
}
|
||||
|
||||
func (a *PlaylistsPage) buildContainer(initialView fyne.CanvasObject) {
|
||||
searchVbox := container.NewVBox(layout.NewSpacer(), a.searcher.Entry, layout.NewSpacer())
|
||||
searchVbox := container.NewVBox(layout.NewSpacer(), a.searcher, layout.NewSpacer())
|
||||
a.container = container.New(&layouts.MaxPadLayout{PadLeft: 15, PadRight: 15, PadTop: 5, PadBottom: 15},
|
||||
container.NewBorder(
|
||||
container.NewHBox(a.titleDisp, container.NewCenter(a.viewToggle), layout.NewSpacer(), searchVbox),
|
||||
|
||||
@@ -24,7 +24,7 @@ type TracksPage struct {
|
||||
nowPlayingID string
|
||||
|
||||
title *widget.RichText
|
||||
searcher *widgets.Searcher
|
||||
searcher *widgets.SearchEntry
|
||||
tracklist *widgets.Tracklist
|
||||
loader widgets.TracklistLoader
|
||||
searchTracklist *widgets.Tracklist
|
||||
@@ -58,7 +58,7 @@ func NewTracksPage(contr *controller.Controller, conf *backend.TracksPageConfig,
|
||||
t.title = widget.NewRichTextWithText("All Tracks")
|
||||
t.title.Segments[0].(*widget.TextSegment).Style.SizeName = widget.RichTextStyleHeading.SizeName
|
||||
t.playRandom = widget.NewButtonWithIcon("Play random", theme.ShuffleIcon, t.playRandomSongs)
|
||||
t.searcher = widgets.NewSearcher()
|
||||
t.searcher = widgets.NewSearchEntry()
|
||||
t.searcher.OnSearched = t.OnSearched
|
||||
t.createContainer()
|
||||
t.Reload()
|
||||
@@ -67,7 +67,7 @@ func NewTracksPage(contr *controller.Controller, conf *backend.TracksPageConfig,
|
||||
|
||||
func (t *TracksPage) createContainer() {
|
||||
playRandomVbox := container.NewVBox(layout.NewSpacer(), t.playRandom, layout.NewSpacer())
|
||||
searchVbox := container.NewVBox(layout.NewSpacer(), t.searcher.Entry, layout.NewSpacer())
|
||||
searchVbox := container.NewVBox(layout.NewSpacer(), t.searcher, layout.NewSpacer())
|
||||
topRow := container.NewHBox(t.title, playRandomVbox, layout.NewSpacer(), searchVbox)
|
||||
t.container = container.New(&layouts.MaxPadLayout{PadLeft: 15, PadRight: 15, PadTop: 5, PadBottom: 15},
|
||||
container.NewBorder(topRow, nil, nil, nil, t.tracklist))
|
||||
@@ -100,7 +100,7 @@ func (t *TracksPage) OnSongChange(track *subsonic.Child, lastScrobbledIfAny *sub
|
||||
var _ Searchable = (*TracksPage)(nil)
|
||||
|
||||
func (t *TracksPage) SearchWidget() fyne.Focusable {
|
||||
return t.searcher.Entry
|
||||
return t.searcher
|
||||
}
|
||||
|
||||
func (t *TracksPage) OnSearched(query string) {
|
||||
|
||||
@@ -6,6 +6,8 @@ import (
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"fyne.io/fyne/v2"
|
||||
"fyne.io/fyne/v2/layout"
|
||||
@@ -60,6 +62,22 @@ func ImageAspect(im image.Image) float32 {
|
||||
return float32(b.Max.X-b.Min.X) / float32(b.Max.Y-b.Min.Y)
|
||||
}
|
||||
|
||||
// Debouncer returns a function that will call callOnDone when
|
||||
// it has not been invoked since the last dur interval.
|
||||
func NewDebouncer(dur time.Duration, callOnDone func()) func() {
|
||||
var mu sync.Mutex
|
||||
var timer *time.Timer
|
||||
return func() {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
|
||||
if timer != nil {
|
||||
timer.Stop()
|
||||
}
|
||||
timer = time.AfterFunc(dur, callOnDone)
|
||||
}
|
||||
}
|
||||
|
||||
func RichTextSegsFromHTMLString(s string) []widget.RichTextSegment {
|
||||
tokr := html.NewTokenizer(strings.NewReader(s))
|
||||
var segs []widget.RichTextSegment
|
||||
|
||||
@@ -1,76 +1,21 @@
|
||||
package widgets
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"fyne.io/fyne/v2"
|
||||
"fyne.io/fyne/v2/theme"
|
||||
"fyne.io/fyne/v2/widget"
|
||||
"github.com/dweymouth/supersonic/ui/util"
|
||||
)
|
||||
|
||||
// Searcher is a search entry widget that will issue a search command
|
||||
// SearchEntry is a search entry widget that will issue a search command
|
||||
// (aka call OnSearched) when a short span of time has elapsed since
|
||||
// the user typed into the widget.
|
||||
type Searcher struct {
|
||||
Entry *SearchEntry
|
||||
OnSearched func(string)
|
||||
|
||||
searchGoroutine bool
|
||||
pendingSearch bool
|
||||
pendingSearchLock sync.Mutex
|
||||
}
|
||||
|
||||
func NewSearcher() *Searcher {
|
||||
s := &Searcher{
|
||||
Entry: NewSearchEntry(),
|
||||
}
|
||||
s.Entry.OnChanged = s.onSearchTextChanged
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *Searcher) onSearchTextChanged(text string) {
|
||||
if text == "" {
|
||||
s.sendSearch("")
|
||||
return
|
||||
}
|
||||
s.pendingSearchLock.Lock()
|
||||
defer s.pendingSearchLock.Unlock()
|
||||
s.pendingSearch = true
|
||||
if !s.searchGoroutine {
|
||||
go s.waitAndSearch()
|
||||
s.searchGoroutine = true
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Searcher) waitAndSearch() {
|
||||
t := time.NewTicker(200 * time.Millisecond)
|
||||
var getReadyToSearch bool
|
||||
var done bool
|
||||
for !done {
|
||||
<-t.C
|
||||
s.pendingSearchLock.Lock()
|
||||
if s.pendingSearch {
|
||||
getReadyToSearch = true
|
||||
s.pendingSearch = false
|
||||
} else if getReadyToSearch {
|
||||
s.sendSearch(s.Entry.Text)
|
||||
t.Stop()
|
||||
s.searchGoroutine = false
|
||||
done = true
|
||||
}
|
||||
s.pendingSearchLock.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Searcher) sendSearch(text string) {
|
||||
if s.OnSearched != nil {
|
||||
s.OnSearched(text)
|
||||
}
|
||||
}
|
||||
|
||||
type SearchEntry struct {
|
||||
widget.Entry
|
||||
|
||||
OnSearched func(string)
|
||||
}
|
||||
|
||||
func NewSearchEntry() *SearchEntry {
|
||||
@@ -80,6 +25,10 @@ func NewSearchEntry() *SearchEntry {
|
||||
sf.ActionItem = NewClearTextButton(func() {
|
||||
sf.SetText("")
|
||||
})
|
||||
debounceFunc := util.NewDebouncer(200*time.Millisecond, func() {
|
||||
sf.sendSearch(sf.Entry.Text)
|
||||
})
|
||||
sf.Entry.OnChanged = func(_ string) { debounceFunc() }
|
||||
return sf
|
||||
}
|
||||
|
||||
@@ -104,6 +53,12 @@ type clearTextButton struct {
|
||||
OnTapped func()
|
||||
}
|
||||
|
||||
func (s *SearchEntry) sendSearch(text string) {
|
||||
if s.OnSearched != nil {
|
||||
s.OnSearched(text)
|
||||
}
|
||||
}
|
||||
|
||||
func NewClearTextButton(onTapped func()) *clearTextButton {
|
||||
c := &clearTextButton{OnTapped: onTapped}
|
||||
c.ExtendBaseWidget(c)
|
||||
Reference in New Issue
Block a user