Fix #327: show loading dots when search in progress but no results yet

This commit is contained in:
Drew Weymouth
2024-02-17 11:17:06 -08:00
parent fb7f51d3ef
commit 268860b3ea
3 changed files with 142 additions and 8 deletions
+13 -6
View File
@@ -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
View File
@@ -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
+118
View File
@@ -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)
}