Fix #327: show loading dots when search in progress but no results yet
This commit is contained in:
@@ -31,6 +31,7 @@ type QuickSearch struct {
|
||||
|
||||
resultsMutex sync.RWMutex
|
||||
searchResults []*mediaprovider.SearchResult
|
||||
loadingDots *widgets.LoadingDots
|
||||
list *widget.List
|
||||
selectedIndex int
|
||||
|
||||
@@ -39,8 +40,9 @@ type QuickSearch struct {
|
||||
|
||||
func NewQuickSearch(mp mediaprovider.MediaProvider, im util.ImageFetcher) *QuickSearch {
|
||||
q := &QuickSearch{
|
||||
mp: mp,
|
||||
imgSource: im,
|
||||
mp: mp,
|
||||
imgSource: im,
|
||||
loadingDots: widgets.NewLoadingDots(),
|
||||
}
|
||||
q.ExtendBaseWidget(q)
|
||||
|
||||
@@ -76,10 +78,13 @@ func NewQuickSearch(mp mediaprovider.MediaProvider, im util.ImageFetcher) *Quick
|
||||
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.NewBorder(
|
||||
container.NewVBox(title, se),
|
||||
container.NewVBox(widget.NewSeparator(), container.NewHBox(layout.NewSpacer(), dismissBtn)),
|
||||
nil, nil, q.list)
|
||||
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),
|
||||
)
|
||||
return q
|
||||
}
|
||||
|
||||
@@ -123,6 +128,7 @@ func (q *QuickSearch) moveSelectionUp() {
|
||||
}
|
||||
|
||||
func (q *QuickSearch) onSearched(query string) {
|
||||
q.loadingDots.Start()
|
||||
var results []*mediaprovider.SearchResult
|
||||
if query != "" {
|
||||
if res, err := q.mp.SearchAll(query, 20); err != nil {
|
||||
@@ -131,6 +137,7 @@ func (q *QuickSearch) onSearched(query string) {
|
||||
results = res
|
||||
}
|
||||
}
|
||||
q.loadingDots.Stop()
|
||||
q.resultsMutex.Lock()
|
||||
q.searchResults = results
|
||||
q.resultsMutex.Unlock()
|
||||
|
||||
+11
-2
@@ -11,6 +11,7 @@ import (
|
||||
"github.com/dweymouth/supersonic/ui/util"
|
||||
|
||||
"fyne.io/fyne/v2"
|
||||
"fyne.io/fyne/v2/container"
|
||||
"fyne.io/fyne/v2/theme"
|
||||
"fyne.io/fyne/v2/widget"
|
||||
)
|
||||
@@ -72,6 +73,7 @@ type GridView struct {
|
||||
GridViewState
|
||||
|
||||
grid *disabledGridWrap
|
||||
loadingDots *LoadingDots
|
||||
menu *widget.PopUpMenu
|
||||
menuGridViewItemId string
|
||||
itemForIndex map[int]*GridViewItem
|
||||
@@ -100,10 +102,12 @@ type GridViewState struct {
|
||||
var _ fyne.Widget = (*GridView)(nil)
|
||||
|
||||
func newGridView() *GridView {
|
||||
return &GridView{
|
||||
g := &GridView{
|
||||
loadingDots: NewLoadingDots(),
|
||||
itemWidth: NewGridViewItem(nil).MinSize().Width,
|
||||
itemForIndex: make(map[int]*GridViewItem),
|
||||
}
|
||||
return g
|
||||
}
|
||||
|
||||
func NewFixedGridView(items []GridViewItemModel, fetch util.ImageFetcher, placeholder fyne.Resource) *GridView {
|
||||
@@ -128,6 +132,7 @@ func NewGridView(iter GridViewIterator, fetch util.ImageFetcher, placeholder fyn
|
||||
}
|
||||
g.ExtendBaseWidget(g)
|
||||
g.createGridWrap()
|
||||
g.loadingDots.Start()
|
||||
|
||||
// fetch initial items
|
||||
g.checkFetchMoreItems(36)
|
||||
@@ -170,6 +175,7 @@ func (g *GridView) Reset(iter GridViewIterator) {
|
||||
g.iter = iter
|
||||
g.stateMutex.Unlock()
|
||||
g.checkFetchMoreItems(36)
|
||||
g.loadingDots.Start()
|
||||
g.Refresh()
|
||||
}
|
||||
|
||||
@@ -345,6 +351,7 @@ func (g *GridView) checkFetchMoreItems(count int) {
|
||||
g.stateMutex.Lock()
|
||||
g.items = append(g.items, items...)
|
||||
g.stateMutex.Unlock()
|
||||
g.loadingDots.Stop()
|
||||
if len(items) < batchFetchSize {
|
||||
g.done = true
|
||||
}
|
||||
@@ -409,7 +416,9 @@ func (g *GridView) onPlay(itemID string, shuffle bool) {
|
||||
}
|
||||
|
||||
func (g *GridView) CreateRenderer() fyne.WidgetRenderer {
|
||||
return widget.NewSimpleRenderer(g.grid)
|
||||
return widget.NewSimpleRenderer(container.NewStack(
|
||||
g.grid, container.NewCenter(g.loadingDots),
|
||||
))
|
||||
}
|
||||
|
||||
// a disabled widget is not considered focusable by the focus manager
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
package widgets
|
||||
|
||||
import (
|
||||
"context"
|
||||
"image/color"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"fyne.io/fyne/v2"
|
||||
"fyne.io/fyne/v2/canvas"
|
||||
"fyne.io/fyne/v2/container"
|
||||
"fyne.io/fyne/v2/theme"
|
||||
"fyne.io/fyne/v2/widget"
|
||||
"github.com/dweymouth/supersonic/ui/layouts"
|
||||
)
|
||||
|
||||
type LoadingDots struct {
|
||||
widget.BaseWidget
|
||||
|
||||
running atomic.Bool
|
||||
animCancel context.CancelFunc
|
||||
|
||||
dots [3]minSizeCircle
|
||||
animPos int
|
||||
container *fyne.Container
|
||||
}
|
||||
|
||||
func NewLoadingDots() *LoadingDots {
|
||||
l := &LoadingDots{}
|
||||
for i := range l.dots {
|
||||
l.dots[i] = minSizeCircle{Circle: canvas.Circle{
|
||||
FillColor: theme.DisabledColor()},
|
||||
}
|
||||
}
|
||||
l.ExtendBaseWidget(l)
|
||||
l.Hide()
|
||||
return l
|
||||
}
|
||||
|
||||
func (l *LoadingDots) Start() {
|
||||
if !l.running.CompareAndSwap(false, true) {
|
||||
return // already started
|
||||
}
|
||||
for i := range l.dots {
|
||||
l.dots[i].Circle.FillColor = theme.DisabledColor()
|
||||
}
|
||||
l.animPos = 0
|
||||
l.Show()
|
||||
var ctx context.Context
|
||||
ctx, l.animCancel = context.WithCancel(context.Background())
|
||||
go l.animate(ctx)
|
||||
}
|
||||
|
||||
func (l *LoadingDots) Stop() {
|
||||
if !l.running.Load() {
|
||||
return
|
||||
}
|
||||
l.Hide()
|
||||
l.animCancel()
|
||||
}
|
||||
|
||||
func (l *LoadingDots) animate(ctx context.Context) {
|
||||
foreground := theme.ForegroundColor()
|
||||
disabled := theme.DisabledColor()
|
||||
l.doTick(foreground, disabled)
|
||||
ticker := time.NewTicker(333 * time.Millisecond)
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
ticker.Stop()
|
||||
l.running.Store(false)
|
||||
return
|
||||
case <-ticker.C:
|
||||
l.doTick(foreground, disabled)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (l *LoadingDots) doTick(foreground, disabled color.Color) {
|
||||
oldDot := l.animPos - 1
|
||||
if oldDot == -1 {
|
||||
oldDot = len(l.dots) - 1
|
||||
}
|
||||
l.dots[l.animPos].Circle.FillColor = foreground
|
||||
l.dots[oldDot].Circle.FillColor = disabled
|
||||
l.Refresh()
|
||||
l.animPos += 1
|
||||
if l.animPos >= len(l.dots) {
|
||||
l.animPos = 0
|
||||
}
|
||||
}
|
||||
|
||||
func (l *LoadingDots) CreateRenderer() fyne.WidgetRenderer {
|
||||
if l.container == nil {
|
||||
layout := &layouts.CenterPadLayout{
|
||||
PadLeftRight: 3,
|
||||
}
|
||||
l.container = container.NewHBox(
|
||||
container.New(layout, &l.dots[0]),
|
||||
container.New(layout, &l.dots[1]),
|
||||
container.New(layout, &l.dots[2]),
|
||||
)
|
||||
}
|
||||
return widget.NewSimpleRenderer(l.container)
|
||||
}
|
||||
|
||||
type minSizeCircle struct {
|
||||
widget.BaseWidget
|
||||
Circle canvas.Circle
|
||||
}
|
||||
|
||||
func (m *minSizeCircle) CreateRenderer() fyne.WidgetRenderer {
|
||||
return widget.NewSimpleRenderer(&m.Circle)
|
||||
}
|
||||
|
||||
func (m *minSizeCircle) MinSize() fyne.Size {
|
||||
return fyne.NewSquareSize(theme.IconInlineSize() / 2)
|
||||
}
|
||||
Reference in New Issue
Block a user