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/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 e867f36..caf2f73 100644 --- a/ui/albumgrid.go +++ b/ui/albumgrid.go @@ -85,6 +85,7 @@ func (ag *AlbumGrid) doUpdateAlbumCard(albumIdx int, ac *widgets.AlbumCard) { default: if err == nil { ac.Cover.SetImage(i) + ag.Refresh() } } }(ctx) @@ -112,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 new file mode 100644 index 0000000..8777529 --- /dev/null +++ b/ui/albumspage.go @@ -0,0 +1,58 @@ +package ui + +import ( + "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 = a.onPlayAlbum + 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 + } + 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 new file mode 100644 index 0000000..feba344 --- /dev/null +++ b/ui/browsingpane.go @@ -0,0 +1,180 @@ +package ui + +import ( + "context" + "time" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/container" + "fyne.io/fyne/v2/layout" + "fyne.io/fyne/v2/theme" + "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 + cancelPendingSearch context.CancelFunc + curPage Page + + container *fyne.Container + pageContaner *fyne.Container +} + +type blankPage struct { + widget.Separator +} + +type searchField struct { + widget.Entry + 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 { + sf := &searchField{} + sf.ExtendBaseWidget(sf) + // 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 +} + +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.OnTextChanged = b.onSearchTextChanged + b.curPage = &blankPage{} + b.pageContaner = container.NewMax(b.curPage) + b.container = container.NewBorder( + container.NewHBox(newHSpace(15), 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) { + if text == "" { + if b.cancelPendingSearch != nil { + b.cancelPendingSearch() + } + b.sendSearch("") + } + 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) +} 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 }