chore: Implement SearchDialog component
Currently, this component is used by QuickSearch dialog. And then, it will be used by the "Add to playlist" dialog. I added some setters and getters to the QuickSearch dialog to abstract the interaction between QuickSearch and SearchDialog from the Controller. After this PR, I will implement the 'Add to playlist' dialog to incorporate the SearchDialog.
This commit is contained in:
@@ -617,12 +617,12 @@ func (c *Controller) ShowSettingsDialog(themeUpdateCallbk func(), themeFiles map
|
|||||||
|
|
||||||
func (c *Controller) ShowQuickSearch() {
|
func (c *Controller) ShowQuickSearch() {
|
||||||
qs := dialogs.NewQuickSearch(c.App.ServerManager.Server, c.App.ImageManager)
|
qs := dialogs.NewQuickSearch(c.App.ServerManager.Server, c.App.ImageManager)
|
||||||
pop := widget.NewModalPopUp(qs, c.MainWindow.Canvas())
|
pop := widget.NewModalPopUp(qs.SearchDialog, c.MainWindow.Canvas())
|
||||||
qs.OnDismiss = func() {
|
qs.SetOnDismiss(func() {
|
||||||
pop.Hide()
|
pop.Hide()
|
||||||
c.doModalClosed()
|
c.doModalClosed()
|
||||||
}
|
})
|
||||||
qs.OnNavigateTo = func(contentType mediaprovider.ContentType, id string) {
|
qs.SetOnNavigateTo(func(contentType mediaprovider.ContentType, id string) {
|
||||||
pop.Hide()
|
pop.Hide()
|
||||||
c.doModalClosed()
|
c.doModalClosed()
|
||||||
switch contentType {
|
switch contentType {
|
||||||
@@ -637,14 +637,14 @@ func (c *Controller) ShowQuickSearch() {
|
|||||||
case mediaprovider.ContentTypeGenre:
|
case mediaprovider.ContentTypeGenre:
|
||||||
c.NavigateTo(GenreRoute(id))
|
c.NavigateTo(GenreRoute(id))
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
c.ClosePopUpOnEscape(pop)
|
c.ClosePopUpOnEscape(pop)
|
||||||
c.haveModal = true
|
c.haveModal = true
|
||||||
min := qs.MinSize()
|
min := qs.GetMinSize()
|
||||||
height := fyne.Max(min.Height, fyne.Min(min.Height*1.5, c.MainWindow.Canvas().Size().Height*0.7))
|
height := fyne.Max(min.Height, fyne.Min(min.Height*1.5, c.MainWindow.Canvas().Size().Height*0.7))
|
||||||
pop.Resize(fyne.NewSize(min.Width, height))
|
pop.Resize(fyne.NewSize(min.Width, height))
|
||||||
pop.Show()
|
pop.Show()
|
||||||
c.MainWindow.Canvas().Focus(qs.SearchEntry)
|
c.MainWindow.Canvas().Focus(qs.GetSearchEntry())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Controller) trySetPasswordAndConnectToServer(server *backend.ServerConfig, password string) error {
|
func (c *Controller) trySetPasswordAndConnectToServer(server *backend.ServerConfig, password string) error {
|
||||||
|
|||||||
+24
-234
@@ -2,133 +2,37 @@ package dialogs
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"image"
|
|
||||||
"log"
|
"log"
|
||||||
"sync"
|
|
||||||
|
|
||||||
"fyne.io/fyne/v2"
|
"fyne.io/fyne/v2"
|
||||||
"fyne.io/fyne/v2/container"
|
|
||||||
"fyne.io/fyne/v2/layout"
|
|
||||||
"fyne.io/fyne/v2/theme"
|
"fyne.io/fyne/v2/theme"
|
||||||
"fyne.io/fyne/v2/widget"
|
"fyne.io/fyne/v2/widget"
|
||||||
"github.com/dweymouth/supersonic/backend/mediaprovider"
|
"github.com/dweymouth/supersonic/backend/mediaprovider"
|
||||||
"github.com/dweymouth/supersonic/ui/layouts"
|
|
||||||
myTheme "github.com/dweymouth/supersonic/ui/theme"
|
|
||||||
"github.com/dweymouth/supersonic/ui/util"
|
"github.com/dweymouth/supersonic/ui/util"
|
||||||
"github.com/dweymouth/supersonic/ui/widgets"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type QuickSearch struct {
|
type QuickSearch struct {
|
||||||
widget.BaseWidget
|
SearchDialog *SearchDialog
|
||||||
|
mp mediaprovider.MediaProvider
|
||||||
OnDismiss func()
|
|
||||||
OnNavigateTo func(mediaprovider.ContentType, string)
|
|
||||||
|
|
||||||
SearchEntry fyne.Focusable // exported so it can be focused by the Controller
|
|
||||||
|
|
||||||
mp mediaprovider.MediaProvider
|
|
||||||
imgSource util.ImageFetcher
|
|
||||||
|
|
||||||
resultsMutex sync.RWMutex
|
|
||||||
searchResults []*mediaprovider.SearchResult
|
|
||||||
loadingDots *widgets.LoadingDots
|
|
||||||
list *widget.List
|
|
||||||
selectedIndex int
|
|
||||||
|
|
||||||
content *fyne.Container
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewQuickSearch(mp mediaprovider.MediaProvider, im util.ImageFetcher) *QuickSearch {
|
func NewQuickSearch(mp mediaprovider.MediaProvider, im util.ImageFetcher) *QuickSearch {
|
||||||
|
|
||||||
q := &QuickSearch{
|
q := &QuickSearch{
|
||||||
mp: mp,
|
mp: mp,
|
||||||
imgSource: im,
|
|
||||||
loadingDots: widgets.NewLoadingDots(),
|
|
||||||
}
|
}
|
||||||
q.ExtendBaseWidget(q)
|
|
||||||
|
|
||||||
se := newQuickSearchEntry()
|
sd := NewSearchDialog(
|
||||||
se.OnSearched = q.onSearched
|
im,
|
||||||
se.OnSubmitted = func(_ string) {
|
"Quick Search",
|
||||||
q.onSelected(q.selectedIndex)
|
q.onSearched,
|
||||||
}
|
q.onUpdateSearchResult,
|
||||||
se.OnTypedDown = q.moveSelectionDown
|
|
||||||
se.OnTypedUp = q.moveSelectionUp
|
|
||||||
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(q) },
|
|
||||||
func(lii widget.ListItemID, co fyne.CanvasObject) {
|
|
||||||
var result *mediaprovider.SearchResult
|
|
||||||
q.resultsMutex.RLock()
|
|
||||||
if len(q.searchResults) > lii {
|
|
||||||
result = q.searchResults[lii]
|
|
||||||
}
|
|
||||||
q.resultsMutex.RUnlock()
|
|
||||||
qs := co.(*quickSearchResult)
|
|
||||||
qs.index = lii
|
|
||||||
qs.Update(result)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
dismissBtn := widget.NewButton("Close", q.onDismiss)
|
|
||||||
title := widget.NewRichText(&widget.TextSegment{Text: "Quick Search", Style: util.BoldRichTextStyle})
|
|
||||||
title.Segments[0].(*widget.TextSegment).Style.Alignment = fyne.TextAlignCenter
|
|
||||||
q.content = container.NewStack(
|
|
||||||
container.NewBorder(
|
|
||||||
container.NewVBox(title, se),
|
|
||||||
container.NewVBox(widget.NewSeparator(), container.NewHBox(layout.NewSpacer(), dismissBtn)),
|
|
||||||
nil, nil, q.list),
|
|
||||||
container.NewCenter(q.loadingDots),
|
|
||||||
)
|
)
|
||||||
|
q.SearchDialog = sd
|
||||||
return q
|
return q
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *QuickSearch) onDismiss() {
|
func (q *QuickSearch) onSearched(query string) []*mediaprovider.SearchResult {
|
||||||
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) moveSelectionDown() {
|
|
||||||
q.resultsMutex.RLock()
|
|
||||||
if q.selectedIndex < len(q.searchResults)-1 {
|
|
||||||
q.selectedIndex++
|
|
||||||
}
|
|
||||||
q.resultsMutex.RUnlock()
|
|
||||||
q.list.Select(q.selectedIndex)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *QuickSearch) moveSelectionUp() {
|
|
||||||
q.resultsMutex.RLock()
|
|
||||||
if q.selectedIndex > 0 {
|
|
||||||
q.selectedIndex--
|
|
||||||
}
|
|
||||||
q.resultsMutex.RUnlock()
|
|
||||||
q.list.Select(q.selectedIndex)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *QuickSearch) onSearched(query string) {
|
|
||||||
q.loadingDots.Start()
|
|
||||||
var results []*mediaprovider.SearchResult
|
var results []*mediaprovider.SearchResult
|
||||||
if query != "" {
|
if query != "" {
|
||||||
if res, err := q.mp.SearchAll(query, 20); err != nil {
|
if res, err := q.mp.SearchAll(query, 20); err != nil {
|
||||||
@@ -137,74 +41,10 @@ func (q *QuickSearch) onSearched(query string) {
|
|||||||
results = res
|
results = res
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
q.loadingDots.Stop()
|
return results
|
||||||
q.resultsMutex.Lock()
|
|
||||||
q.searchResults = results
|
|
||||||
q.resultsMutex.Unlock()
|
|
||||||
q.list.Refresh()
|
|
||||||
q.list.ScrollToTop()
|
|
||||||
q.selectedIndex = 0
|
|
||||||
q.list.Select(0)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *QuickSearch) CreateRenderer() fyne.WidgetRenderer {
|
func (q *QuickSearch) onUpdateSearchResult(sr *searchResult, result *mediaprovider.SearchResult) {
|
||||||
return widget.NewSimpleRenderer(q.content)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *QuickSearch) MinSize() fyne.Size {
|
|
||||||
return fyne.NewSize(400, 350)
|
|
||||||
}
|
|
||||||
|
|
||||||
type quickSearchResult struct {
|
|
||||||
widget.BaseWidget
|
|
||||||
|
|
||||||
parent *QuickSearch
|
|
||||||
|
|
||||||
id string
|
|
||||||
index int
|
|
||||||
contentType mediaprovider.ContentType
|
|
||||||
|
|
||||||
imageLoader util.ThumbnailLoader
|
|
||||||
|
|
||||||
image *widgets.ImagePlaceholder
|
|
||||||
title *widget.Label
|
|
||||||
secondary *widget.RichText
|
|
||||||
|
|
||||||
content *fyne.Container
|
|
||||||
}
|
|
||||||
|
|
||||||
func newQuickSearchResult(parent *QuickSearch) *quickSearchResult {
|
|
||||||
qs := &quickSearchResult{
|
|
||||||
parent: parent,
|
|
||||||
image: widgets.NewImagePlaceholder(myTheme.AlbumIcon, 50),
|
|
||||||
title: widget.NewLabel(""),
|
|
||||||
secondary: widget.NewRichText(),
|
|
||||||
}
|
|
||||||
qs.title.Truncation = fyne.TextTruncateEllipsis
|
|
||||||
qs.secondary.Truncation = fyne.TextTruncateEllipsis
|
|
||||||
qs.ExtendBaseWidget(qs)
|
|
||||||
qs.imageLoader = util.NewThumbnailLoader(parent.imgSource, func(im image.Image) {
|
|
||||||
qs.image.SetImage(im, false)
|
|
||||||
})
|
|
||||||
qs.imageLoader.OnBeforeLoad = func() {
|
|
||||||
qs.image.SetImage(nil, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
return qs
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *quickSearchResult) Update(result *mediaprovider.SearchResult) {
|
|
||||||
if result == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if q.contentType == result.Type && q.id == result.ID {
|
|
||||||
return // nothing to do
|
|
||||||
}
|
|
||||||
q.id = result.ID
|
|
||||||
q.contentType = result.Type
|
|
||||||
q.image.CenterIcon = placeholderIconForContentType(result.Type)
|
|
||||||
q.imageLoader.Load(result.CoverID)
|
|
||||||
q.title.SetText(result.Name)
|
|
||||||
|
|
||||||
maybePluralize := func(s string, size int) string {
|
maybePluralize := func(s string, size int) string {
|
||||||
if size != 1 {
|
if size != 1 {
|
||||||
@@ -230,14 +70,14 @@ func (q *quickSearchResult) Update(result *mediaprovider.SearchResult) {
|
|||||||
secondaryText = ""
|
secondaryText = ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
q.secondary.Segments = []widget.RichTextSegment{
|
sr.secondary.Segments = []widget.RichTextSegment{
|
||||||
&widget.TextSegment{
|
&widget.TextSegment{
|
||||||
Text: result.Type.String(),
|
Text: result.Type.String(),
|
||||||
Style: widget.RichTextStyle{SizeName: theme.SizeNameCaptionText, TextStyle: fyne.TextStyle{Bold: true}, Inline: true},
|
Style: widget.RichTextStyle{SizeName: theme.SizeNameCaptionText, TextStyle: fyne.TextStyle{Bold: true}, Inline: true},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
if secondaryText != "" {
|
if secondaryText != "" {
|
||||||
q.secondary.Segments = append(q.secondary.Segments,
|
sr.secondary.Segments = append(sr.secondary.Segments,
|
||||||
&widget.TextSegment{
|
&widget.TextSegment{
|
||||||
Text: " · ",
|
Text: " · ",
|
||||||
Style: widget.RichTextStyle{SizeName: theme.SizeNameCaptionText, Inline: true},
|
Style: widget.RichTextStyle{SizeName: theme.SizeNameCaptionText, Inline: true},
|
||||||
@@ -248,71 +88,21 @@ func (q *quickSearchResult) Update(result *mediaprovider.SearchResult) {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
sr.secondary.Refresh()
|
||||||
q.secondary.Refresh()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *quickSearchResult) Tapped(_ *fyne.PointEvent) {
|
func (q *QuickSearch) SetOnDismiss(onDismiss func()) {
|
||||||
q.parent.onSelected(q.index)
|
q.SearchDialog.OnDismiss = onDismiss
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *quickSearchResult) CreateRenderer() fyne.WidgetRenderer {
|
func (q *QuickSearch) SetOnNavigateTo(onNavigateTo func(mediaprovider.ContentType, string)) {
|
||||||
if q.content == nil {
|
q.SearchDialog.OnNavigateTo = onNavigateTo
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *quickSearchResult) Refresh() {
|
func (q *QuickSearch) GetMinSize() fyne.Size {
|
||||||
q.BaseWidget.Refresh()
|
return q.SearchDialog.MinSize()
|
||||||
}
|
}
|
||||||
|
|
||||||
type quickSearchEntry struct {
|
func (q *QuickSearch) GetSearchEntry() fyne.Focusable {
|
||||||
widgets.SearchEntry
|
return q.SearchDialog.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:
|
|
||||||
return myTheme.AlbumIcon
|
|
||||||
case mediaprovider.ContentTypeArtist:
|
|
||||||
return myTheme.ArtistIcon
|
|
||||||
case mediaprovider.ContentTypeTrack:
|
|
||||||
return myTheme.TracksIcon
|
|
||||||
case mediaprovider.ContentTypeGenre:
|
|
||||||
return myTheme.GenreIcon
|
|
||||||
case mediaprovider.ContentTypePlaylist:
|
|
||||||
return myTheme.PlaylistIcon
|
|
||||||
default:
|
|
||||||
return theme.WarningIcon() // unreached
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,279 @@
|
|||||||
|
package dialogs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"image"
|
||||||
|
"log"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"fyne.io/fyne/v2"
|
||||||
|
"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/mediaprovider"
|
||||||
|
|
||||||
|
"github.com/dweymouth/supersonic/ui/layouts"
|
||||||
|
myTheme "github.com/dweymouth/supersonic/ui/theme"
|
||||||
|
"github.com/dweymouth/supersonic/ui/util"
|
||||||
|
"github.com/dweymouth/supersonic/ui/widgets"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SearchDialog struct {
|
||||||
|
widget.BaseWidget
|
||||||
|
|
||||||
|
SearchEntry fyne.Focusable // exported so it can be focused by the Controller
|
||||||
|
|
||||||
|
imgSource util.ImageFetcher
|
||||||
|
|
||||||
|
resultsMutex sync.RWMutex
|
||||||
|
searchResults []*mediaprovider.SearchResult
|
||||||
|
loadingDots *widgets.LoadingDots
|
||||||
|
list *widget.List
|
||||||
|
selectedIndex int
|
||||||
|
|
||||||
|
content *fyne.Container
|
||||||
|
|
||||||
|
OnDismiss func()
|
||||||
|
OnNavigateTo func(mediaprovider.ContentType, string)
|
||||||
|
OnSearched func(string) []*mediaprovider.SearchResult
|
||||||
|
OnUpdateSearchResults func(*searchResult, *mediaprovider.SearchResult)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSearchDialog(im util.ImageFetcher, placeholderTitle string, onSearched func(string) []*mediaprovider.SearchResult, onUpdateSearchResult func(*searchResult, *mediaprovider.SearchResult)) *SearchDialog {
|
||||||
|
sd := &SearchDialog{
|
||||||
|
imgSource: im,
|
||||||
|
loadingDots: widgets.NewLoadingDots(),
|
||||||
|
OnSearched: onSearched,
|
||||||
|
OnUpdateSearchResults: onUpdateSearchResult,
|
||||||
|
}
|
||||||
|
sd.ExtendBaseWidget(sd)
|
||||||
|
|
||||||
|
se := newSearchEntry()
|
||||||
|
se.OnSearched = sd.onSearched
|
||||||
|
se.OnSubmitted = func(_ string) {
|
||||||
|
sd.onSelected(sd.selectedIndex)
|
||||||
|
}
|
||||||
|
se.OnTypedDown = sd.moveSelectionDown
|
||||||
|
se.OnTypedUp = sd.moveSelectionUp
|
||||||
|
se.OnTypedEscape = sd.onDismiss
|
||||||
|
sd.SearchEntry = se
|
||||||
|
sd.list = widget.NewList(
|
||||||
|
func() int {
|
||||||
|
sd.resultsMutex.RLock()
|
||||||
|
defer sd.resultsMutex.RUnlock()
|
||||||
|
return len(sd.searchResults)
|
||||||
|
},
|
||||||
|
func() fyne.CanvasObject { return newSearchResult(sd) },
|
||||||
|
func(lii widget.ListItemID, co fyne.CanvasObject) {
|
||||||
|
var result *mediaprovider.SearchResult
|
||||||
|
sd.resultsMutex.RLock()
|
||||||
|
if len(sd.searchResults) > lii {
|
||||||
|
result = sd.searchResults[lii]
|
||||||
|
}
|
||||||
|
sd.resultsMutex.RUnlock()
|
||||||
|
sr := co.(*searchResult)
|
||||||
|
sr.index = lii
|
||||||
|
sd.update(sr, result)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
dismissBtn := widget.NewButton("Close", sd.onDismiss)
|
||||||
|
title := widget.NewRichText(&widget.TextSegment{Text: placeholderTitle, Style: util.BoldRichTextStyle})
|
||||||
|
title.Segments[0].(*widget.TextSegment).Style.Alignment = fyne.TextAlignCenter
|
||||||
|
sd.content = container.NewStack(
|
||||||
|
container.NewBorder(
|
||||||
|
container.NewVBox(title, se),
|
||||||
|
container.NewVBox(widget.NewSeparator(), container.NewHBox(layout.NewSpacer(), dismissBtn)),
|
||||||
|
nil, nil, sd.list),
|
||||||
|
container.NewCenter(sd.loadingDots),
|
||||||
|
)
|
||||||
|
return sd
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sd *SearchDialog) onDismiss() {
|
||||||
|
if sd.OnDismiss != nil {
|
||||||
|
sd.OnDismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sd *SearchDialog) onSelected(idx int) {
|
||||||
|
if sd.OnNavigateTo == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
sd.resultsMutex.RLock()
|
||||||
|
if len(sd.searchResults) <= idx {
|
||||||
|
sd.resultsMutex.RUnlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
id := sd.searchResults[idx].ID
|
||||||
|
typ := sd.searchResults[idx].Type
|
||||||
|
sd.resultsMutex.RUnlock()
|
||||||
|
sd.OnNavigateTo(typ, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sd *SearchDialog) moveSelectionDown() {
|
||||||
|
sd.resultsMutex.RLock()
|
||||||
|
if sd.selectedIndex < len(sd.searchResults)-1 {
|
||||||
|
sd.selectedIndex++
|
||||||
|
}
|
||||||
|
sd.resultsMutex.RUnlock()
|
||||||
|
sd.list.Select(sd.selectedIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sd *SearchDialog) moveSelectionUp() {
|
||||||
|
sd.resultsMutex.RLock()
|
||||||
|
if sd.selectedIndex > 0 {
|
||||||
|
sd.selectedIndex--
|
||||||
|
}
|
||||||
|
sd.resultsMutex.RUnlock()
|
||||||
|
sd.list.Select(sd.selectedIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sd *SearchDialog) onSearched(query string) {
|
||||||
|
sd.loadingDots.Start()
|
||||||
|
var results []*mediaprovider.SearchResult
|
||||||
|
if query != "" {
|
||||||
|
res := sd.OnSearched(query)
|
||||||
|
if len(res) == 0 {
|
||||||
|
log.Println("No results matched the query.")
|
||||||
|
} else {
|
||||||
|
results = res
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sd.loadingDots.Stop()
|
||||||
|
sd.resultsMutex.Lock()
|
||||||
|
sd.searchResults = results
|
||||||
|
sd.resultsMutex.Unlock()
|
||||||
|
sd.list.Refresh()
|
||||||
|
sd.list.ScrollToTop()
|
||||||
|
sd.selectedIndex = 0
|
||||||
|
sd.list.Select(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sd *SearchDialog) CreateRenderer() fyne.WidgetRenderer {
|
||||||
|
return widget.NewSimpleRenderer(sd.content)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sd *SearchDialog) MinSize() fyne.Size {
|
||||||
|
return fyne.NewSize(400, 350)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sd *SearchDialog) update(sr *searchResult, result *mediaprovider.SearchResult) {
|
||||||
|
if result == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if sr.contentType == result.Type && sr.id == result.ID {
|
||||||
|
return // nothing to do
|
||||||
|
}
|
||||||
|
sr.id = result.ID
|
||||||
|
sr.contentType = result.Type
|
||||||
|
sr.image.CenterIcon = placeholderIconForContentType(result.Type)
|
||||||
|
sr.imageLoader.Load(result.CoverID)
|
||||||
|
sr.title.SetText(result.Name)
|
||||||
|
|
||||||
|
sd.OnUpdateSearchResults(sr, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func placeholderIconForContentType(c mediaprovider.ContentType) fyne.Resource {
|
||||||
|
switch c {
|
||||||
|
case mediaprovider.ContentTypeAlbum:
|
||||||
|
return myTheme.AlbumIcon
|
||||||
|
case mediaprovider.ContentTypeArtist:
|
||||||
|
return myTheme.ArtistIcon
|
||||||
|
case mediaprovider.ContentTypeTrack:
|
||||||
|
return myTheme.TracksIcon
|
||||||
|
case mediaprovider.ContentTypeGenre:
|
||||||
|
return myTheme.GenreIcon
|
||||||
|
case mediaprovider.ContentTypePlaylist:
|
||||||
|
return myTheme.PlaylistIcon
|
||||||
|
default:
|
||||||
|
return theme.WarningIcon() // unreached
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type searchResult struct {
|
||||||
|
widget.BaseWidget
|
||||||
|
|
||||||
|
parent *SearchDialog
|
||||||
|
|
||||||
|
id string
|
||||||
|
index int
|
||||||
|
contentType mediaprovider.ContentType
|
||||||
|
|
||||||
|
imageLoader util.ThumbnailLoader
|
||||||
|
|
||||||
|
image *widgets.ImagePlaceholder
|
||||||
|
title *widget.Label
|
||||||
|
secondary *widget.RichText
|
||||||
|
|
||||||
|
content *fyne.Container
|
||||||
|
}
|
||||||
|
|
||||||
|
func newSearchResult(parent *SearchDialog) *searchResult {
|
||||||
|
qs := &searchResult{
|
||||||
|
parent: parent,
|
||||||
|
image: widgets.NewImagePlaceholder(myTheme.AlbumIcon, 50),
|
||||||
|
title: widget.NewLabel(""),
|
||||||
|
secondary: widget.NewRichText(),
|
||||||
|
}
|
||||||
|
qs.title.Truncation = fyne.TextTruncateEllipsis
|
||||||
|
qs.secondary.Truncation = fyne.TextTruncateEllipsis
|
||||||
|
qs.ExtendBaseWidget(qs)
|
||||||
|
qs.imageLoader = util.NewThumbnailLoader(parent.imgSource, func(im image.Image) {
|
||||||
|
qs.image.SetImage(im, false)
|
||||||
|
})
|
||||||
|
qs.imageLoader.OnBeforeLoad = func() {
|
||||||
|
qs.image.SetImage(nil, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
return qs
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *searchResult) Tapped(_ *fyne.PointEvent) {
|
||||||
|
q.parent.onSelected(q.index)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *searchResult) CreateRenderer() fyne.WidgetRenderer {
|
||||||
|
if q.content == nil {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *searchResult) Refresh() {
|
||||||
|
q.BaseWidget.Refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
type searchEntry struct {
|
||||||
|
widgets.SearchEntry
|
||||||
|
|
||||||
|
OnTypedUp func()
|
||||||
|
OnTypedDown func()
|
||||||
|
OnTypedEscape func()
|
||||||
|
}
|
||||||
|
|
||||||
|
func newSearchEntry() *searchEntry {
|
||||||
|
q := &searchEntry{}
|
||||||
|
q.ExtendBaseWidget(q)
|
||||||
|
q.SearchEntry.Init()
|
||||||
|
return q
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *searchEntry) 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user