From fc858e102457a8cec34e2f151cd65d0fdd78f9d7 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Tue, 27 Dec 2022 09:50:51 -0800 Subject: [PATCH 1/2] begin work on adding search to albums page --- backend/albumiterator.go | 7 +-- ui/albumgrid.go | 3 ++ ui/albumspage.go | 57 +++++++++++++++++++++ ui/browsingpane.go | 104 +++++++++++++++++++++++++++++++++++++++ ui/mainwindow.go | 21 ++++---- 5 files changed, 180 insertions(+), 12 deletions(-) create mode 100644 ui/albumspage.go create mode 100644 ui/browsingpane.go diff --git a/backend/albumiterator.go b/backend/albumiterator.go index 122fb52..3443d23 100644 --- a/backend/albumiterator.go +++ b/backend/albumiterator.go @@ -146,9 +146,10 @@ type searchIter struct { func (l *LibraryManager) newSearchIter(query string) *searchIter { return &searchIter{ - query: query, - l: l, - s: l.s.Server, + query: query, + l: l, + s: l.s.Server, + albumIDset: make(map[string]bool), } } diff --git a/ui/albumgrid.go b/ui/albumgrid.go index e867f36..e557e8d 100644 --- a/ui/albumgrid.go +++ b/ui/albumgrid.go @@ -2,6 +2,7 @@ package ui import ( "context" + "fmt" "image" "supersonic/backend" "supersonic/ui/widgets" @@ -84,7 +85,9 @@ func (ag *AlbumGrid) doUpdateAlbumCard(albumIdx int, ac *widgets.AlbumCard) { return default: if err == nil { + fmt.Println("setting image") ac.Cover.SetImage(i) + ag.Refresh() } } }(ctx) diff --git a/ui/albumspage.go b/ui/albumspage.go new file mode 100644 index 0000000..f798a66 --- /dev/null +++ b/ui/albumspage.go @@ -0,0 +1,57 @@ +package ui + +import ( + "log" + "supersonic/backend" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/container" + "fyne.io/fyne/v2/widget" +) + +var _ fyne.Widget = (*AlbumsPage)(nil) + +type AlbumsPage struct { + widget.BaseWidget + + im *backend.ImageManager + lm *backend.LibraryManager + grid *AlbumGrid + searchGrid *AlbumGrid + gridContainer *fyne.Container + OnPlayAlbum func(string) +} + +func NewAlbumsPage(title string, lm *backend.LibraryManager, im *backend.ImageManager) *AlbumsPage { + a := &AlbumsPage{ + lm: lm, + im: im, + } + a.ExtendBaseWidget(a) + a.grid = NewAlbumGrid(lm.RecentlyAddedIter(), im.GetAlbumThumbnail) + a.grid.OnPlayAlbum = func(id string) { + if a.OnPlayAlbum != nil { + a.OnPlayAlbum(id) + } + } + a.gridContainer = container.NewMax(a.grid) + return a +} + +func (a *AlbumsPage) OnSearched(query string) { + if query == "" { + a.gridContainer.Objects[0] = a.grid + a.searchGrid = nil + a.Refresh() + return + } + log.Printf("searched %s", query) + a.searchGrid = NewAlbumGrid(a.lm.SearchIter(query), a.im.GetAlbumThumbnail) + a.gridContainer.Objects[0] = a.searchGrid + a.Refresh() +} + +func (a *AlbumsPage) CreateRenderer() fyne.WidgetRenderer { + a.ExtendBaseWidget(a) + return widget.NewSimpleRenderer(a.gridContainer) +} diff --git a/ui/browsingpane.go b/ui/browsingpane.go new file mode 100644 index 0000000..8645d6e --- /dev/null +++ b/ui/browsingpane.go @@ -0,0 +1,104 @@ +package ui + +import ( + "context" + "time" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/container" + "fyne.io/fyne/v2/widget" +) + +type Page interface { + fyne.Widget +} + +type Searchable interface { + OnSearched(string) +} + +type BrowsingPane struct { + widget.BaseWidget + + searchBar *searchField + searchTimer time.Timer + pendingSearch bool + cancelPendingSearch context.CancelFunc + curPage Page + + container *fyne.Container + pageContaner *fyne.Container +} + +type blankPage struct { + widget.Separator +} + +type searchField struct { + widget.Entry + height float32 +} + +func NewSearchField() *searchField { + sf := &searchField{} + sf.ExtendBaseWidget(sf) + // this is a bit hacky + sf.height = widget.NewEntry().MinSize().Height + sf.PlaceHolder = "Search" + return sf +} + +func (s *searchField) MinSize() fyne.Size { + return fyne.NewSize(200, s.height) +} + +func NewBrowsingPane() *BrowsingPane { + b := &BrowsingPane{} + b.ExtendBaseWidget(b) + b.searchBar = NewSearchField() + b.searchBar.OnChanged = b.onSearchTextChanged + b.curPage = &blankPage{} + b.searchTimer = *time.NewTimer(0) + b.pageContaner = container.NewMax(b.curPage) + b.container = container.NewBorder(container.NewHBox(b.searchBar), nil, nil, nil, b.pageContaner) + return b +} + +func (b *BrowsingPane) SetPage(p Page) { + if b.cancelPendingSearch != nil { + b.cancelPendingSearch() + b.cancelPendingSearch = nil + } + b.curPage = p + b.pageContaner.Objects[0] = p + b.Refresh() +} + +func (b *BrowsingPane) onSearchTextChanged(text string) { + b.searchTimer.Reset(300 * time.Millisecond) + if s, ok := b.curPage.(Searchable); ok { + s.OnSearched(text) + } + if !b.pendingSearch { + /* + ctx, cancel := context.WithCancel(context.Background()) + b.cancelPendingSearch = cancel + b.pendingSearch = true + go func(ctx context.Context) { + select { + case <-ctx.Done(): + b.pendingSearch = false + case <-b.searchTimer.C: + if s, ok := b.curPage.(Searchable); ok { + s.OnSearched(b.searchBar.Text) + } + b.pendingSearch = false + } + }(ctx) + */ + } +} + +func (b *BrowsingPane) CreateRenderer() fyne.WidgetRenderer { + return widget.NewSimpleRenderer(b.container) +} diff --git a/ui/mainwindow.go b/ui/mainwindow.go index fa4713d..686637f 100644 --- a/ui/mainwindow.go +++ b/ui/mainwindow.go @@ -6,7 +6,6 @@ import ( "fyne.io/fyne/v2" "fyne.io/fyne/v2/container" - "fyne.io/fyne/v2/layout" "fyne.io/fyne/v2/widget" "github.com/dweymouth/go-subsonic" ) @@ -14,18 +13,22 @@ import ( type MainWindow struct { Window fyne.Window - BottomPanel *BottomPanel + BrowsingPane *BrowsingPane + BottomPanel *BottomPanel + + container *fyne.Container } func NewMainWindow(fyneApp fyne.App, appName string, app *backend.App) MainWindow { m := MainWindow{ - Window: fyneApp.NewWindow(appName), - BottomPanel: NewBottomPanel(app.Player), + Window: fyneApp.NewWindow(appName), + BrowsingPane: NewBrowsingPane(), + BottomPanel: NewBottomPanel(app.Player), } m.BottomPanel.SetPlaybackManager(app.PlaybackManager) m.BottomPanel.ImageManager = app.ImageManager - c := container.NewBorder(nil, m.BottomPanel, nil, nil, &layout.Spacer{}) - m.Window.SetContent(c) + m.container = container.NewBorder(nil, m.BottomPanel, nil, nil, m.BrowsingPane) + m.Window.SetContent(m.container) m.Window.Resize(fyne.NewSize(1000, 800)) app.PlaybackManager.OnSongChange(func(song *subsonic.Child) { if song == nil { @@ -35,11 +38,11 @@ func NewMainWindow(fyneApp fyne.App, appName string, app *backend.App) MainWindo m.Window.SetTitle(song.Title) }) app.ServerManager.OnServerConnected(func() { - ag := NewAlbumGrid(app.LibraryManager.RecentlyAddedIter(), app.ImageManager.GetAlbumThumbnail) - ag.OnPlayAlbum = func(albumID string) { + ap := NewAlbumsPage("Albums", app.LibraryManager, app.ImageManager) + ap.OnPlayAlbum = func(albumID string) { _ = app.PlaybackManager.PlayAlbum(albumID) } - m.Window.SetContent(container.NewBorder(nil, m.BottomPanel, nil, nil, ag)) + m.BrowsingPane.SetPage(ap) }) return m } From 7d0fcc78b258cdbcc55d65ffaae93d054d169061 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Tue, 27 Dec 2022 15:14:46 -0800 Subject: [PATCH 2/2] search now works correctly! --- go.mod | 2 +- go.sum | 4 +- ui/albumgrid.go | 3 +- ui/albumspage.go | 15 +++--- ui/browsingpane.go | 128 ++++++++++++++++++++++++++++++++++++--------- 5 files changed, 114 insertions(+), 38 deletions(-) diff --git a/go.mod b/go.mod index 58a0656..9c0e5a9 100644 --- a/go.mod +++ b/go.mod @@ -44,4 +44,4 @@ require ( honnef.co/go/js/dom v0.0.0-20210725211120-f030747120f2 // indirect ) -replace fyne.io/fyne/v2 v2.2.4 => github.com/dweymouth/fyne/v2 v2.2.5-0.20221220015227-bd9b07f93ac4 +replace fyne.io/fyne/v2 v2.2.4 => github.com/dweymouth/fyne/v2 v2.2.5-0.20221227225053-ae75dfbeb567 diff --git a/go.sum b/go.sum index 712f494..f846f2b 100644 --- a/go.sum +++ b/go.sum @@ -72,8 +72,8 @@ github.com/danieljoos/wincred v1.1.0/go.mod h1:XYlo+eRTsVA9aHGp7NGjFkPla4m+DCL7h github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dweymouth/fyne/v2 v2.2.5-0.20221220015227-bd9b07f93ac4 h1:Q/+eS7II+u1xjnWHXMgQ9G1pssn/Yi6BoxY6iJCVsfU= -github.com/dweymouth/fyne/v2 v2.2.5-0.20221220015227-bd9b07f93ac4/go.mod h1:MBoGuHzLLSXdQOWFAwWhIhYTEMp33zqtGCReSWhaQTA= +github.com/dweymouth/fyne/v2 v2.2.5-0.20221227225053-ae75dfbeb567 h1:JEBJ5YN74ryKQNW1TgH0GjHkVpgmHMb1oYuz8ZShjY8= +github.com/dweymouth/fyne/v2 v2.2.5-0.20221227225053-ae75dfbeb567/go.mod h1:MBoGuHzLLSXdQOWFAwWhIhYTEMp33zqtGCReSWhaQTA= github.com/dweymouth/go-subsonic v0.0.0-20221214005741-bd8048fa1863 h1:bOWMpFJ9zY839T1EngQ5nxVCiMlatPtpGkg/yHK6szg= github.com/dweymouth/go-subsonic v0.0.0-20221214005741-bd8048fa1863/go.mod h1:fUez6NFiEJiQTZizZ1BThZr5GJXAbigzGYjEPNm4tdI= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= diff --git a/ui/albumgrid.go b/ui/albumgrid.go index e557e8d..caf2f73 100644 --- a/ui/albumgrid.go +++ b/ui/albumgrid.go @@ -2,7 +2,6 @@ package ui import ( "context" - "fmt" "image" "supersonic/backend" "supersonic/ui/widgets" @@ -85,7 +84,6 @@ func (ag *AlbumGrid) doUpdateAlbumCard(albumIdx int, ac *widgets.AlbumCard) { return default: if err == nil { - fmt.Println("setting image") ac.Cover.SetImage(i) ag.Refresh() } @@ -115,6 +113,7 @@ func (a *AlbumGrid) fetchMoreAlbums(count int) { if i == count { a.fetching = false } + a.Refresh() }) } diff --git a/ui/albumspage.go b/ui/albumspage.go index f798a66..8777529 100644 --- a/ui/albumspage.go +++ b/ui/albumspage.go @@ -1,7 +1,6 @@ package ui import ( - "log" "supersonic/backend" "fyne.io/fyne/v2" @@ -29,11 +28,7 @@ func NewAlbumsPage(title string, lm *backend.LibraryManager, im *backend.ImageMa } a.ExtendBaseWidget(a) a.grid = NewAlbumGrid(lm.RecentlyAddedIter(), im.GetAlbumThumbnail) - a.grid.OnPlayAlbum = func(id string) { - if a.OnPlayAlbum != nil { - a.OnPlayAlbum(id) - } - } + a.grid.OnPlayAlbum = a.onPlayAlbum a.gridContainer = container.NewMax(a.grid) return a } @@ -45,12 +40,18 @@ func (a *AlbumsPage) OnSearched(query string) { a.Refresh() return } - log.Printf("searched %s", query) a.searchGrid = NewAlbumGrid(a.lm.SearchIter(query), a.im.GetAlbumThumbnail) + a.searchGrid.OnPlayAlbum = a.onPlayAlbum a.gridContainer.Objects[0] = a.searchGrid a.Refresh() } +func (a *AlbumsPage) onPlayAlbum(albumID string) { + if a.OnPlayAlbum != nil { + a.OnPlayAlbum(albumID) + } +} + func (a *AlbumsPage) CreateRenderer() fyne.WidgetRenderer { a.ExtendBaseWidget(a) return widget.NewSimpleRenderer(a.gridContainer) diff --git a/ui/browsingpane.go b/ui/browsingpane.go index 8645d6e..feba344 100644 --- a/ui/browsingpane.go +++ b/ui/browsingpane.go @@ -6,6 +6,8 @@ import ( "fyne.io/fyne/v2" "fyne.io/fyne/v2/container" + "fyne.io/fyne/v2/layout" + "fyne.io/fyne/v2/theme" "fyne.io/fyne/v2/widget" ) @@ -21,8 +23,7 @@ type BrowsingPane struct { widget.BaseWidget searchBar *searchField - searchTimer time.Timer - pendingSearch bool + searchTimer *time.Timer cancelPendingSearch context.CancelFunc curPage Page @@ -36,7 +37,49 @@ type blankPage struct { type searchField struct { widget.Entry - height float32 + height float32 + OnTextChanged func(string) +} + +var _ fyne.Tappable = (*clearTextButton)(nil) + +type clearTextButton struct { + widget.Icon + + OnTapped func() +} + +type hspace struct { + widget.BaseWidget + + Width float32 +} + +func newHSpace(w float32) *hspace { + h := &hspace{Width: w} + h.ExtendBaseWidget(h) + return h +} + +func (h *hspace) MinSize() fyne.Size { + return fyne.NewSize(h.Width, 0) +} + +func (h *hspace) CreateRenderer() fyne.WidgetRenderer { + return widget.NewSimpleRenderer(layout.NewSpacer()) +} + +func NewClearTextButton() *clearTextButton { + c := &clearTextButton{} + c.ExtendBaseWidget(c) + c.Resource = theme.SearchIcon() + return c +} + +func (c *clearTextButton) Tapped(*fyne.PointEvent) { + if c.OnTapped != nil { + c.OnTapped() + } } func NewSearchField() *searchField { @@ -45,6 +88,22 @@ func NewSearchField() *searchField { // this is a bit hacky sf.height = widget.NewEntry().MinSize().Height sf.PlaceHolder = "Search" + c := NewClearTextButton() + c.OnTapped = func() { + sf.SetText("") + } + sf.ActionItem = c + sf.OnChanged = func(s string) { + if s == "" { + c.Resource = theme.SearchIcon() + } else { + c.Resource = theme.ContentClearIcon() + } + c.Refresh() + if sf.OnTextChanged != nil { + sf.OnTextChanged(s) + } + } return sf } @@ -56,11 +115,12 @@ func NewBrowsingPane() *BrowsingPane { b := &BrowsingPane{} b.ExtendBaseWidget(b) b.searchBar = NewSearchField() - b.searchBar.OnChanged = b.onSearchTextChanged + b.searchBar.OnTextChanged = b.onSearchTextChanged b.curPage = &blankPage{} - b.searchTimer = *time.NewTimer(0) b.pageContaner = container.NewMax(b.curPage) - b.container = container.NewBorder(container.NewHBox(b.searchBar), nil, nil, nil, b.pageContaner) + b.container = container.NewBorder( + container.NewHBox(newHSpace(15), b.searchBar), + nil, nil, nil, b.pageContaner) return b } @@ -75,30 +135,46 @@ func (b *BrowsingPane) SetPage(p Page) { } func (b *BrowsingPane) onSearchTextChanged(text string) { - b.searchTimer.Reset(300 * time.Millisecond) - if s, ok := b.curPage.(Searchable); ok { - s.OnSearched(text) + if text == "" { + if b.cancelPendingSearch != nil { + b.cancelPendingSearch() + } + b.sendSearch("") } - if !b.pendingSearch { - /* - ctx, cancel := context.WithCancel(context.Background()) - b.cancelPendingSearch = cancel - b.pendingSearch = true - go func(ctx context.Context) { - select { - case <-ctx.Done(): - b.pendingSearch = false - case <-b.searchTimer.C: - if s, ok := b.curPage.(Searchable); ok { - s.OnSearched(b.searchBar.Text) - } - b.pendingSearch = false - } - }(ctx) - */ + if b.searchTimer == nil { + ctx, cancel := context.WithCancel(context.Background()) + b.cancelPendingSearch = cancel + b.searchTimer = time.NewTimer(200 * time.Millisecond) + go func(ctx context.Context, trigger <-chan time.Time) { + select { + case <-ctx.Done(): + b.searchTimer = nil + case <-trigger: + b.sendSearch(b.searchBar.Text) + b.searchTimer = nil + } + }(ctx, b.searchTimer.C) + } else { + b.resetSearchTimer(200 * time.Millisecond) + } +} + +func (b *BrowsingPane) sendSearch(query string) { + if s, ok := b.curPage.(Searchable); ok { + s.OnSearched(query) } } func (b *BrowsingPane) CreateRenderer() fyne.WidgetRenderer { return widget.NewSimpleRenderer(b.container) } + +func (b *BrowsingPane) resetSearchTimer(d time.Duration) { + if b.searchTimer == nil { + return + } + if !b.searchTimer.Stop() { + <-b.searchTimer.C + } + b.searchTimer.Reset(d) +}