diff --git a/ui/browsing/albumspage.go b/ui/browsing/albumspage.go index 75bb5b1..5faf155 100644 --- a/ui/browsing/albumspage.go +++ b/ui/browsing/albumspage.go @@ -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() { diff --git a/ui/browsing/artistsgenrespage.go b/ui/browsing/artistsgenrespage.go index ee4b371..c694b3b 100644 --- a/ui/browsing/artistsgenrespage.go +++ b/ui/browsing/artistsgenrespage.go @@ -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}, diff --git a/ui/browsing/favoritespage.go b/ui/browsing/favoritespage.go index e6a48f0..ee398b6 100644 --- a/ui/browsing/favoritespage.go +++ b/ui/browsing/favoritespage.go @@ -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) { diff --git a/ui/browsing/genrepage.go b/ui/browsing/genrepage.go index 79fb63f..0b8ee76 100644 --- a/ui/browsing/genrepage.go +++ b/ui/browsing/genrepage.go @@ -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) { diff --git a/ui/browsing/playlistspage.go b/ui/browsing/playlistspage.go index 2cae787..c99d3b5 100644 --- a/ui/browsing/playlistspage.go +++ b/ui/browsing/playlistspage.go @@ -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), diff --git a/ui/browsing/trackspage.go b/ui/browsing/trackspage.go index 519b5b3..dc86e46 100644 --- a/ui/browsing/trackspage.go +++ b/ui/browsing/trackspage.go @@ -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) { diff --git a/ui/util/util.go b/ui/util/util.go index 10e3256..f00d9c7 100644 --- a/ui/util/util.go +++ b/ui/util/util.go @@ -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 diff --git a/ui/widgets/searcher.go b/ui/widgets/searchentry.go similarity index 53% rename from ui/widgets/searcher.go rename to ui/widgets/searchentry.go index 630ab61..33a6957 100644 --- a/ui/widgets/searcher.go +++ b/ui/widgets/searchentry.go @@ -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)