From 155ee877e3ec98867ade58667ac5ddbb2dcbb6ae Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Tue, 31 Oct 2023 20:16:22 -0700 Subject: [PATCH] mostly working now, except for track result action, and results ranking --- ui/controller/controller.go | 3 + ui/dialogs/quicksearch.go | 130 +++++++++++++++++++++++++++++------- ui/widgets/searchentry.go | 7 +- 3 files changed, 116 insertions(+), 24 deletions(-) diff --git a/ui/controller/controller.go b/ui/controller/controller.go index 3e2cc67..ff08312 100644 --- a/ui/controller/controller.go +++ b/ui/controller/controller.go @@ -506,7 +506,10 @@ func (c *Controller) ShowQuickSearch() { c.NavigateTo(GenreRoute(id)) } } + c.ClosePopUpOnEscape(pop) + c.haveModal = true pop.Show() + c.MainWindow.Canvas().Focus(qs.SearchEntry) } func (c *Controller) trySetPasswordAndConnectToServer(server *backend.ServerConfig, password string) error { diff --git a/ui/dialogs/quicksearch.go b/ui/dialogs/quicksearch.go index 67399b0..defecc0 100644 --- a/ui/dialogs/quicksearch.go +++ b/ui/dialogs/quicksearch.go @@ -6,12 +6,13 @@ import ( "sync" "fyne.io/fyne/v2" - "fyne.io/fyne/v2/canvas" "fyne.io/fyne/v2/container" + "fyne.io/fyne/v2/layout" "fyne.io/fyne/v2/theme" "fyne.io/fyne/v2/widget" "github.com/dweymouth/supersonic/backend" "github.com/dweymouth/supersonic/backend/mediaprovider" + "github.com/dweymouth/supersonic/ui/layouts" myTheme "github.com/dweymouth/supersonic/ui/theme" "github.com/dweymouth/supersonic/ui/widgets" ) @@ -22,6 +23,8 @@ type QuickSearch struct { OnDismiss func() OnNavigateTo func(mediaprovider.ContentType, string) + SearchEntry fyne.Focusable // exported so it can be focused by the Controller + mp mediaprovider.MediaProvider im *backend.ImageManager @@ -40,15 +43,36 @@ func NewQuickSearch(mp mediaprovider.MediaProvider, im *backend.ImageManager) *Q } q.ExtendBaseWidget(q) - searchEntry := widgets.NewSearchEntry() - searchEntry.OnChanged = q.onSearched + se := newQuickSearchEntry() + se.OnSearched = q.onSearched + se.OnSubmitted = func(_ string) { + q.onSelected(q.selectedIndex) + } + se.OnTypedDown = func() { + q.resultsMutex.RLock() + if q.selectedIndex < len(q.searchResults)-1 { + q.selectedIndex++ + } + q.resultsMutex.RUnlock() + q.list.Select(q.selectedIndex) + } + se.OnTypedUp = func() { + q.resultsMutex.RLock() + if q.selectedIndex > 0 { + q.selectedIndex-- + } + q.resultsMutex.RUnlock() + q.list.Select(q.selectedIndex) + } + se.OnTypedEscape = q.onDismiss + q.SearchEntry = se q.list = widget.NewList( func() int { q.resultsMutex.RLock() defer q.resultsMutex.RUnlock() return len(q.searchResults) }, - func() fyne.CanvasObject { return newQuickSearchResult(im) }, + func() fyne.CanvasObject { return newQuickSearchResult(q) }, func(lii widget.ListItemID, co fyne.CanvasObject) { var result *mediaprovider.SearchResult q.resultsMutex.RLock() @@ -56,16 +80,43 @@ func NewQuickSearch(mp mediaprovider.MediaProvider, im *backend.ImageManager) *Q result = q.searchResults[lii] } q.resultsMutex.RUnlock() - co.(*quickSearchResult).Update(result) + qs := co.(*quickSearchResult) + qs.index = lii + qs.Update(result) }, ) + + dismissBtn := widget.NewButton("Close", q.onDismiss) title := widget.NewRichText(&widget.TextSegment{Text: "Quick Search", Style: boldStyle}) title.Segments[0].(*widget.TextSegment).Style.Alignment = fyne.TextAlignCenter q.content = container.NewBorder( - container.NewVBox(title, searchEntry), nil, nil, nil, q.list) + container.NewVBox(title, se), + container.NewVBox(widget.NewSeparator(), container.NewHBox(layout.NewSpacer(), dismissBtn)), + nil, nil, q.list) return q } +func (q *QuickSearch) onDismiss() { + if q.OnDismiss != nil { + q.OnDismiss() + } +} + +func (q *QuickSearch) onSelected(idx int) { + if q.OnNavigateTo == nil { + return + } + q.resultsMutex.RLock() + if len(q.searchResults) <= idx { + q.resultsMutex.RUnlock() + return + } + id := q.searchResults[idx].ID + typ := q.searchResults[idx].Type + q.resultsMutex.RUnlock() + q.OnNavigateTo(typ, id) +} + func (q *QuickSearch) onSearched(query string) { var results []*mediaprovider.SearchResult if query != "" { @@ -79,6 +130,8 @@ func (q *QuickSearch) onSearched(query string) { q.searchResults = results q.resultsMutex.Unlock() q.list.Refresh() + q.list.ScrollToTop() + q.list.Select(0) } func (q *QuickSearch) CreateRenderer() fyne.WidgetRenderer { @@ -86,15 +139,16 @@ func (q *QuickSearch) CreateRenderer() fyne.WidgetRenderer { } func (q *QuickSearch) MinSize() fyne.Size { - return fyne.NewSize(500, 350) + return fyne.NewSize(400, 350) } type quickSearchResult struct { widget.BaseWidget - // parent *QuickSearch + parent *QuickSearch id string + index int contentType mediaprovider.ContentType imageLoader backend.ThumbnailLoader @@ -102,19 +156,21 @@ type quickSearchResult struct { image *widgets.ImagePlaceholder title *widget.Label secondary *widget.RichText - selection *canvas.Rectangle content *fyne.Container } -func newQuickSearchResult(im *backend.ImageManager) *quickSearchResult { +func newQuickSearchResult(parent *QuickSearch) *quickSearchResult { qs := &quickSearchResult{ + parent: parent, image: widgets.NewImagePlaceholder(myTheme.AlbumIcon, 50), title: widget.NewLabel(""), secondary: widget.NewRichText(), } + qs.title.Wrapping = fyne.TextTruncate + qs.secondary.Wrapping = fyne.TextTruncate qs.ExtendBaseWidget(qs) - qs.imageLoader = im.NewThumbnailLoader(func(im image.Image) { + qs.imageLoader = parent.im.NewThumbnailLoader(func(im image.Image) { qs.image.SetImage(im, false) }) qs.imageLoader.OnBeforeLoad = func() { @@ -136,22 +192,21 @@ func (q *quickSearchResult) Update(result *mediaprovider.SearchResult) { q.image.CenterIcon = placeholderIconForContentType(result.Type) q.imageLoader.Load(result.CoverID) q.title.SetText(result.Name) - //TODO: q.secondary.Segments = + q.secondary.Segments = []widget.RichTextSegment{&widget.TextSegment{Text: result.Type.String()}} + q.secondary.Refresh() +} + +func (q *quickSearchResult) Tapped(_ *fyne.PointEvent) { + q.parent.onSelected(q.index) } func (q *quickSearchResult) CreateRenderer() fyne.WidgetRenderer { - if q.selection == nil { - q.selection = canvas.NewRectangle(theme.SelectionColor()) - } if q.content == nil { - q.content = container.NewMax( - q.selection, - container.NewBorder(nil, nil, q.image, nil, - container.NewVBox( - q.title, - q.secondary, - )), - ) + q.content = container.NewBorder(nil, nil, container.NewCenter(q.image), nil, + container.New(&layouts.VboxCustomPadding{ExtraPad: -15}, + q.title, + q.secondary, + )) } return widget.NewSimpleRenderer(q.content) } @@ -160,6 +215,35 @@ func (q *quickSearchResult) Refresh() { q.BaseWidget.Refresh() } +type quickSearchEntry struct { + widgets.SearchEntry + + OnTypedUp func() + OnTypedDown func() + OnTypedEscape func() +} + +func newQuickSearchEntry() *quickSearchEntry { + q := &quickSearchEntry{} + q.ExtendBaseWidget(q) + q.SearchEntry.Init() + return q +} + +func (q *quickSearchEntry) TypedKey(e *fyne.KeyEvent) { + switch { + case e.Name == fyne.KeyUp && q.OnTypedUp != nil: + q.OnTypedUp() + case e.Name == fyne.KeyDown && q.OnTypedDown != nil: + q.OnTypedDown() + case e.Name == fyne.KeyEscape && q.OnTypedEscape != nil: + q.OnTypedEscape() + default: + q.SearchEntry.TypedKey(e) + } + +} + func placeholderIconForContentType(c mediaprovider.ContentType) fyne.Resource { switch c { case mediaprovider.ContentTypeAlbum: diff --git a/ui/widgets/searchentry.go b/ui/widgets/searchentry.go index 41d932e..d5ed450 100644 --- a/ui/widgets/searchentry.go +++ b/ui/widgets/searchentry.go @@ -21,6 +21,12 @@ type SearchEntry struct { func NewSearchEntry() *SearchEntry { sf := &SearchEntry{} sf.ExtendBaseWidget(sf) + sf.Init() + return sf +} + +// For use only by extending widgets +func (sf *SearchEntry) Init() { sf.PlaceHolder = "Search" sf.ActionItem = NewClearTextButton(func() { sf.SetText("") @@ -34,7 +40,6 @@ func NewSearchEntry() *SearchEntry { } debounceFunc() } - return sf } func (s *SearchEntry) TypedKey(e *fyne.KeyEvent) {