add grid view option to playlists page

This commit is contained in:
Drew Weymouth
2023-04-28 19:28:20 -07:00
parent 753e7f9bcc
commit 9b1349b0ac
9 changed files with 166 additions and 40 deletions
+10
View File
@@ -130,6 +130,16 @@ var ResStarFilledSvg = &fyne.StaticResource{
StaticContent: []byte( StaticContent: []byte(
"<?xml version=\"1.0\" encoding=\"utf-8\"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->\r\n<svg width=\"800px\" height=\"800px\" viewBox=\"2 2 22 22\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\r\n<path d=\"M21.12 9.88005C21.0781 9.74719 20.9996 9.62884 20.8935 9.53862C20.7873 9.4484 20.6579 9.38997 20.52 9.37005L15.1 8.58005L12.67 3.67005C12.6008 3.55403 12.5027 3.45795 12.3853 3.39123C12.2678 3.32451 12.1351 3.28943 12 3.28943C11.8649 3.28943 11.7322 3.32451 11.6147 3.39123C11.4973 3.45795 11.3991 3.55403 11.33 3.67005L8.89999 8.58005L3.47999 9.37005C3.34211 9.38997 3.21266 9.4484 3.10652 9.53862C3.00038 9.62884 2.92186 9.74719 2.87999 9.88005C2.83529 10.0124 2.82846 10.1547 2.86027 10.2907C2.89207 10.4268 2.96124 10.5512 3.05999 10.6501L6.99999 14.4701L6.06999 19.8701C6.04642 20.0091 6.06199 20.1519 6.11497 20.2826C6.16796 20.4133 6.25625 20.5267 6.36999 20.6101C6.48391 20.6912 6.61825 20.7389 6.75785 20.7478C6.89746 20.7566 7.03675 20.7262 7.15999 20.6601L12 18.1101L16.85 20.6601C16.9573 20.7189 17.0776 20.7499 17.2 20.7501C17.3573 20.7482 17.5105 20.6995 17.64 20.6101C17.7537 20.5267 17.842 20.4133 17.895 20.2826C17.948 20.1519 17.9636 20.0091 17.94 19.8701L17 14.4701L20.93 10.6501C21.0305 10.5523 21.1015 10.4283 21.1351 10.2922C21.1687 10.1561 21.1634 10.0133 21.12 9.88005Z\" fill=\"#000000\"/>\r\n</svg>"), "<?xml version=\"1.0\" encoding=\"utf-8\"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->\r\n<svg width=\"800px\" height=\"800px\" viewBox=\"2 2 22 22\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\r\n<path d=\"M21.12 9.88005C21.0781 9.74719 20.9996 9.62884 20.8935 9.53862C20.7873 9.4484 20.6579 9.38997 20.52 9.37005L15.1 8.58005L12.67 3.67005C12.6008 3.55403 12.5027 3.45795 12.3853 3.39123C12.2678 3.32451 12.1351 3.28943 12 3.28943C11.8649 3.28943 11.7322 3.32451 11.6147 3.39123C11.4973 3.45795 11.3991 3.55403 11.33 3.67005L8.89999 8.58005L3.47999 9.37005C3.34211 9.38997 3.21266 9.4484 3.10652 9.53862C3.00038 9.62884 2.92186 9.74719 2.87999 9.88005C2.83529 10.0124 2.82846 10.1547 2.86027 10.2907C2.89207 10.4268 2.96124 10.5512 3.05999 10.6501L6.99999 14.4701L6.06999 19.8701C6.04642 20.0091 6.06199 20.1519 6.11497 20.2826C6.16796 20.4133 6.25625 20.5267 6.36999 20.6101C6.48391 20.6912 6.61825 20.7389 6.75785 20.7478C6.89746 20.7566 7.03675 20.7262 7.15999 20.6601L12 18.1101L16.85 20.6601C16.9573 20.7189 17.0776 20.7499 17.2 20.7501C17.3573 20.7482 17.5105 20.6995 17.64 20.6101C17.7537 20.5267 17.842 20.4133 17.895 20.2826C17.948 20.1519 17.9636 20.0091 17.94 19.8701L17 14.4701L20.93 10.6501C21.0305 10.5523 21.1015 10.4283 21.1351 10.2922C21.1687 10.1561 21.1634 10.0133 21.12 9.88005Z\" fill=\"#000000\"/>\r\n</svg>"),
} }
var ResGridSvg = &fyne.StaticResource{
StaticName: "grid.svg",
StaticContent: []byte(
"<?xml version=\"1.0\" encoding=\"utf-8\"?>\r<svg width=\"800px\" height=\"800px\" viewBox=\"0 0 48 48\" xmlns=\"http://www.w3.org/2000/svg\" >\r<g id=\"grid\">\r\n\t<rect x=\"4\" y=\"4\" width=\"18\" height=\"18\"/>\r\n\t<rect x=\"26\" y=\"4\" width=\"18\" height=\"18\"/>\r\n\t<rect x=\"4\" y=\"26\" width=\"18\" height=\"18\"/>\r\n\t<rect x=\"26\" y=\"26\" width=\"18\" height=\"18\"/>\r\n</g>\r\n</svg>"),
}
var ResListSvg = &fyne.StaticResource{
StaticName: "list.svg",
StaticContent: []byte(
"<?xml version=\"1.0\" encoding=\"utf-8\"?>\r<svg width=\"800px\" height=\"800px\" viewBox=\"0 0 48 48\" xmlns=\"http://www.w3.org/2000/svg\" >\r<g id=\"grid\">\r\n\t<rect x=\"4\" y=\"6\" width=\"40\" height=\"8\"/>\r\n\t<rect x=\"4\" y=\"20\" width=\"40\" height=\"8\"/>\r\n\t<rect x=\"4\" y=\"34\" width=\"40\" height=\"8\"/>\r\n</g>\r\n</svg>"),
}
var ResLICENSE = &fyne.StaticResource{ var ResLICENSE = &fyne.StaticResource{
StaticName: "LICENSE", StaticName: "LICENSE",
StaticContent: []byte( StaticContent: []byte(
+2
View File
@@ -25,6 +25,8 @@ fyne bundle -append -prefix Res icons/publicdomain/shuffle.svg >> bundled.go
fyne bundle -append -prefix Res icons/publicdomain/shuffle-invert.svg >> bundled.go fyne bundle -append -prefix Res icons/publicdomain/shuffle-invert.svg >> bundled.go
fyne bundle -append -prefix Res icons/publicdomain/star-outline.svg >> bundled.go fyne bundle -append -prefix Res icons/publicdomain/star-outline.svg >> bundled.go
fyne bundle -append -prefix Res icons/publicdomain/star-filled.svg >> bundled.go fyne bundle -append -prefix Res icons/publicdomain/star-filled.svg >> bundled.go
fyne bundle -append -prefix Res icons/publicdomain/grid.svg >> bundled.go
fyne bundle -append -prefix Res icons/publicdomain/list.svg >> bundled.go
fyne bundle -append -prefix Res ../LICENSE >> bundled.go fyne bundle -append -prefix Res ../LICENSE >> bundled.go
fyne bundle -append -prefix Res licenses/BSDLICENSE >> bundled.go fyne bundle -append -prefix Res licenses/BSDLICENSE >> bundled.go
+7
View File
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?><svg width="800px" height="800px" viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg" ><g id="grid">
<rect x="4" y="4" width="18" height="18"/>
<rect x="26" y="4" width="18" height="18"/>
<rect x="4" y="26" width="18" height="18"/>
<rect x="26" y="26" width="18" height="18"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 341 B

+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?><svg width="800px" height="800px" viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg" ><g id="grid">
<rect x="4" y="6" width="40" height="8"/>
<rect x="4" y="20" width="40" height="8"/>
<rect x="4" y="34" width="40" height="8"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 291 B

+5 -3
View File
@@ -156,7 +156,8 @@ func (a *AlbumsPage) Reload() {
if a.searchText != "" { if a.searchText != "" {
a.doSearch(a.searchText) a.doSearch(a.searchText)
} else { } else {
a.grid.Reset(a.lm.AlbumsIter(backend.AlbumSortOrder(a.sortOrder.Selected))) iter := a.lm.AlbumsIter(backend.AlbumSortOrder(a.sortOrder.Selected))
a.grid.Reset(widgets.NewGridViewAlbumIterator(iter))
a.grid.Refresh() a.grid.Refresh()
} }
} }
@@ -185,7 +186,7 @@ func (a *AlbumsPage) doSearch(query string) {
a.searchGrid.OnShowItemPage = a.onShowAlbumPage a.searchGrid.OnShowItemPage = a.onShowAlbumPage
a.searchGrid.OnShowSecondaryPage = a.onShowArtistPage a.searchGrid.OnShowSecondaryPage = a.onShowArtistPage
} else { } else {
a.searchGrid.Reset(a.lm.SearchIter(query)) a.searchGrid.Reset(widgets.NewGridViewAlbumIterator(a.lm.SearchIter(query)))
} }
a.container.Objects[0] = a.searchGrid a.container.Objects[0] = a.searchGrid
a.Refresh() a.Refresh()
@@ -205,7 +206,8 @@ func (a *AlbumsPage) onShowAlbumPage(albumID string) {
func (a *AlbumsPage) onSortOrderChanged(order string) { func (a *AlbumsPage) onSortOrderChanged(order string) {
a.cfg.SortOrder = a.sortOrder.Selected a.cfg.SortOrder = a.sortOrder.Selected
a.grid.Reset(a.lm.AlbumsIter(backend.AlbumSortOrder(order))) iter := a.lm.AlbumsIter(backend.AlbumSortOrder(order))
a.grid.Reset(widgets.NewGridViewAlbumIterator(iter))
if a.searchText == "" { if a.searchText == "" {
a.container.Objects[0] = a.grid a.container.Objects[0] = a.grid
a.Refresh() a.Refresh()
+2 -2
View File
@@ -134,7 +134,7 @@ func (a *FavoritesPage) Reload() {
if a.searchText != "" { if a.searchText != "" {
a.doSearchAlbums(a.searchText) a.doSearchAlbums(a.searchText)
} else { } else {
a.grid.Reset(a.lm.StarredIter()) a.grid.Reset(widgets.NewGridViewAlbumIterator(a.lm.StarredIter()))
} }
if a.tracklistCtr != nil || a.artistListCtr != nil { if a.tracklistCtr != nil || a.artistListCtr != nil {
go func() { go func() {
@@ -225,7 +225,7 @@ func (a *FavoritesPage) doSearchAlbums(query string) {
a.searchGrid.OnShowItemPage = a.onShowAlbumPage a.searchGrid.OnShowItemPage = a.onShowAlbumPage
a.searchGrid.OnShowSecondaryPage = a.onShowArtistPage a.searchGrid.OnShowSecondaryPage = a.onShowArtistPage
} else { } else {
a.searchGrid.Reset(iter) a.searchGrid.Reset(widgets.NewGridViewAlbumIterator(iter))
} }
a.container.Objects[0] = a.searchGrid a.container.Objects[0] = a.searchGrid
a.Refresh() a.Refresh()
+2 -2
View File
@@ -119,7 +119,7 @@ func (g *GenrePage) Reload() {
if g.searchText != "" { if g.searchText != "" {
g.doSearch(g.searchText) g.doSearch(g.searchText)
} else { } else {
g.grid.Reset(g.lm.GenreIter(g.genre)) g.grid.Reset(widgets.NewGridViewAlbumIterator(g.lm.GenreIter(g.genre)))
g.grid.Refresh() g.grid.Refresh()
} }
} }
@@ -181,7 +181,7 @@ func (g *GenrePage) doSearch(query string) {
g.searchGrid.OnShowItemPage = g.onShowAlbumPage g.searchGrid.OnShowItemPage = g.onShowAlbumPage
g.searchGrid.OnShowSecondaryPage = g.onShowArtistPage g.searchGrid.OnShowSecondaryPage = g.onShowArtistPage
} else { } else {
g.searchGrid.Reset(iter) g.searchGrid.Reset(widgets.NewGridViewAlbumIterator(iter))
} }
g.container.Objects[0] = g.searchGrid g.container.Objects[0] = g.searchGrid
g.Refresh() g.Refresh()
+114 -25
View File
@@ -1,10 +1,12 @@
package browsing package browsing
import ( import (
"fmt"
"log" "log"
"strconv" "strconv"
"strings" "strings"
"supersonic/backend" "supersonic/backend"
"supersonic/res"
"supersonic/sharedutil" "supersonic/sharedutil"
"supersonic/ui/controller" "supersonic/ui/controller"
"supersonic/ui/layouts" "supersonic/ui/layouts"
@@ -21,21 +23,24 @@ import (
type PlaylistsPage struct { type PlaylistsPage struct {
widget.BaseWidget widget.BaseWidget
contr *controller.Controller contr *controller.Controller
sm *backend.ServerManager sm *backend.ServerManager
playlists []*subsonic.Playlist playlists []*subsonic.Playlist
searchedPlaylists []*subsonic.Playlist
searcher *widgets.Searcher viewToggle *widgets.ToggleButtonGroup
titleDisp *widget.RichText searcher *widgets.Searcher
container *fyne.Container titleDisp *widget.RichText
list *PlaylistList container *fyne.Container
listView *PlaylistList
gridView *widgets.GridView
} }
func NewPlaylistsPage(contr *controller.Controller, sm *backend.ServerManager) *PlaylistsPage { func NewPlaylistsPage(contr *controller.Controller, sm *backend.ServerManager) *PlaylistsPage {
return newPlaylistsPage(contr, sm, "") return newPlaylistsPage(contr, sm, "", 0)
} }
func newPlaylistsPage(contr *controller.Controller, sm *backend.ServerManager, searchText string) *PlaylistsPage { func newPlaylistsPage(contr *controller.Controller, sm *backend.ServerManager, searchText string, activeView int) *PlaylistsPage {
a := &PlaylistsPage{ a := &PlaylistsPage{
sm: sm, sm: sm,
contr: contr, contr: contr,
@@ -43,14 +48,21 @@ func newPlaylistsPage(contr *controller.Controller, sm *backend.ServerManager, s
} }
a.ExtendBaseWidget(a) a.ExtendBaseWidget(a)
a.titleDisp.Segments[0].(*widget.TextSegment).Style.SizeName = theme.SizeNameHeadingText a.titleDisp.Segments[0].(*widget.TextSegment).Style.SizeName = theme.SizeNameHeadingText
a.list = NewPlaylistList()
a.list.OnNavTo = func(id string) {
a.contr.NavigateTo(controller.PlaylistRoute(id))
}
a.searcher = widgets.NewSearcher() a.searcher = widgets.NewSearcher()
a.searcher.OnSearched = a.onSearched a.searcher.OnSearched = a.onSearched
a.searcher.Entry.Text = searchText a.searcher.Entry.Text = searchText
a.buildContainer() a.viewToggle = widgets.NewToggleButtonGroup(0,
widget.NewButtonWithIcon("", theme.NewThemedResource(res.ResListSvg), a.showListView),
widget.NewButtonWithIcon("", theme.NewThemedResource(res.ResGridSvg), a.showGridView))
a.viewToggle.SetActivatedButton(activeView)
if activeView == 0 {
a.createListView()
a.buildContainer(a.listView)
} else {
a.createGridView(nil)
a.buildContainer(a.gridView)
}
go a.load(searchText != "") go a.load(searchText != "")
return a return a
} }
@@ -64,26 +76,101 @@ func (a *PlaylistsPage) load(searchOnLoad bool) {
if searchOnLoad { if searchOnLoad {
a.onSearched(a.searcher.Entry.Text) a.onSearched(a.searcher.Entry.Text)
} else { } else {
a.list.Playlists = playlists a.refreshView(playlists)
a.list.Refresh()
} }
} }
func (a *PlaylistsPage) createListView() {
a.listView = NewPlaylistList()
a.listView.OnNavTo = a.showPlaylistPage
}
func (a *PlaylistsPage) createGridView(playlists []*subsonic.Playlist) {
model := createPlaylistGridViewModel(playlists)
a.gridView = widgets.NewFixedGridView(model, a.contr.App.ImageManager)
a.gridView.OnPlay = func(id string) {
a.contr.App.PlaybackManager.PlayPlaylist(id, 0)
}
a.gridView.OnShowItemPage = a.showPlaylistPage
}
func (a *PlaylistsPage) showListView() {
if a.listView == nil {
a.createListView()
if a.searcher.Entry.Text != "" {
a.listView.Playlists = a.searchedPlaylists
} else {
a.listView.Playlists = a.playlists
}
}
a.container.Objects[0].(*fyne.Container).Objects[0] = a.listView
a.container.Objects[0].Refresh()
}
func (a *PlaylistsPage) showGridView() {
if a.gridView == nil {
playlists := a.playlists
if a.searcher.Entry.Text != "" {
playlists = a.searchedPlaylists
}
a.createGridView(playlists)
}
a.container.Objects[0].(*fyne.Container).Objects[0] = a.gridView
a.container.Objects[0].Refresh()
}
func createPlaylistGridViewModel(playlists []*subsonic.Playlist) []widgets.GridViewItemModel {
return sharedutil.MapSlice(playlists, func(pl *subsonic.Playlist) widgets.GridViewItemModel {
tracks := "tracks"
if pl.SongCount == 1 {
tracks = "track"
}
return widgets.GridViewItemModel{
Name: pl.Name,
ID: pl.ID,
CoverArtID: pl.CoverArt,
Secondary: fmt.Sprintf("%d %s", pl.SongCount, tracks),
}
})
}
func (a *PlaylistsPage) showPlaylistPage(id string) {
a.contr.NavigateTo(controller.PlaylistRoute(id))
}
func (a *PlaylistsPage) onSearched(query string) { func (a *PlaylistsPage) onSearched(query string) {
// since the playlist list is returned in full non-paginated, we will do our own // since the playlist list is returned in full non-paginated, we will do our own
// simple search based on the name, description, and owner, rather than calling a server API // simple search based on the name, description, and owner, rather than calling a server API
var playlists []*subsonic.Playlist
if query == "" { if query == "" {
a.list.Playlists = a.playlists a.searchedPlaylists = nil
playlists = a.playlists
} else { } else {
result := sharedutil.FilterSlice(a.playlists, func(p *subsonic.Playlist) bool { a.searchedPlaylists = sharedutil.FilterSlice(a.playlists, func(p *subsonic.Playlist) bool {
qLower := strings.ToLower(query) qLower := strings.ToLower(query)
return strings.Contains(strings.ToLower(p.Name), qLower) || return strings.Contains(strings.ToLower(p.Name), qLower) ||
strings.Contains(strings.ToLower(p.Comment), qLower) || strings.Contains(strings.ToLower(p.Comment), qLower) ||
strings.Contains(strings.ToLower(p.Owner), qLower) strings.Contains(strings.ToLower(p.Owner), qLower)
}) })
a.list.Playlists = result playlists = a.searchedPlaylists
}
a.refreshView(playlists)
}
// update the model for both views if initialized,
// refresh the active view
func (a *PlaylistsPage) refreshView(playlists []*subsonic.Playlist) {
if a.listView != nil {
a.listView.Playlists = playlists
}
if a.gridView != nil {
a.gridView.ResetFixed(createPlaylistGridViewModel(playlists))
}
if a.viewToggle.ActivatedButtonIndex() == 0 {
a.listView.Refresh()
} else {
a.gridView.Refresh()
} }
a.list.Refresh()
} }
var _ Searchable = (*PlaylistsPage)(nil) var _ Searchable = (*PlaylistsPage)(nil)
@@ -97,7 +184,7 @@ func (a *PlaylistsPage) Route() controller.Route {
} }
func (a *PlaylistsPage) Reload() { func (a *PlaylistsPage) Reload() {
go a.load(false) go a.load(a.searcher.Entry.Text != "")
} }
func (a *PlaylistsPage) Save() SavedPage { func (a *PlaylistsPage) Save() SavedPage {
@@ -105,6 +192,7 @@ func (a *PlaylistsPage) Save() SavedPage {
contr: a.contr, contr: a.contr,
sm: a.sm, sm: a.sm,
searchText: a.searcher.Entry.Text, searchText: a.searcher.Entry.Text,
activeView: a.viewToggle.ActivatedButtonIndex(),
} }
} }
@@ -112,18 +200,19 @@ type savedPlaylistsPage struct {
contr *controller.Controller contr *controller.Controller
sm *backend.ServerManager sm *backend.ServerManager
searchText string searchText string
activeView int
} }
func (s *savedPlaylistsPage) Restore() Page { func (s *savedPlaylistsPage) Restore() Page {
return newPlaylistsPage(s.contr, s.sm, s.searchText) return newPlaylistsPage(s.contr, s.sm, s.searchText, s.activeView)
} }
func (a *PlaylistsPage) buildContainer() { func (a *PlaylistsPage) buildContainer(initialView fyne.CanvasObject) {
searchVbox := container.NewVBox(layout.NewSpacer(), a.searcher.Entry, layout.NewSpacer()) searchVbox := container.NewVBox(layout.NewSpacer(), a.searcher.Entry, layout.NewSpacer())
a.container = container.New(&layouts.MaxPadLayout{PadLeft: 15, PadRight: 15, PadTop: 5, PadBottom: 15}, a.container = container.New(&layouts.MaxPadLayout{PadLeft: 15, PadRight: 15, PadTop: 5, PadBottom: 15},
container.NewBorder( container.NewBorder(
container.NewHBox(a.titleDisp, layout.NewSpacer(), searchVbox), container.NewHBox(a.titleDisp, container.NewCenter(a.viewToggle), layout.NewSpacer(), searchVbox),
nil, nil, nil, a.list)) nil, nil, nil, initialView))
} }
func (a *PlaylistsPage) CreateRenderer() fyne.WidgetRenderer { func (a *PlaylistsPage) CreateRenderer() fyne.WidgetRenderer {
+18 -8
View File
@@ -14,7 +14,7 @@ import (
"github.com/dweymouth/go-subsonic/subsonic" "github.com/dweymouth/go-subsonic/subsonic"
) )
const albumFetchBatchSize = 6 const batchFetchSize = 6
type ImageFetcher interface { type ImageFetcher interface {
GetCoverThumbnailFromCache(string) (image.Image, bool) GetCoverThumbnailFromCache(string) (image.Image, bool)
@@ -123,17 +123,27 @@ func (g *GridView) Clear() {
g.done = true g.done = true
} }
func (g *GridView) Reset(iter backend.AlbumIterator) { func (g *GridView) Reset(iter GridViewIterator) {
g.itemsMutex.Lock() g.itemsMutex.Lock()
g.items = nil g.items = nil
g.itemsMutex.Unlock() g.itemsMutex.Unlock()
g.fetching = false g.fetching = false
g.done = false g.done = false
g.highestShown = 0 g.highestShown = 0
g.iter = gridViewAlbumIterator{iter: backend.NewBatchingIterator(iter)} g.iter = iter
g.fetchMoreItems(36) g.fetchMoreItems(36)
} }
func (g *GridView) ResetFixed(items []GridViewItemModel) {
g.itemsMutex.Lock()
g.items = items
g.itemsMutex.Unlock()
g.fetching = false
g.done = true
g.highestShown = 0
g.iter = nil
}
func (g *GridView) createGridWrapList() { func (g *GridView) createGridWrapList() {
g.grid = widget.NewGridWrapList( g.grid = widget.NewGridWrapList(
func() int { func() int {
@@ -231,15 +241,15 @@ func (g *GridView) fetchMoreItems(count int) {
for !g.done && g.highestShown >= g.lenItems()-10 { for !g.done && g.highestShown >= g.lenItems()-10 {
n := 0 n := 0
for !g.done && n < count { for !g.done && n < count {
albums := g.iter.NextN(albumFetchBatchSize) items := g.iter.NextN(batchFetchSize)
g.itemsMutex.Lock() g.itemsMutex.Lock()
g.items = append(g.items, albums...) g.items = append(g.items, items...)
g.itemsMutex.Unlock() g.itemsMutex.Unlock()
if len(albums) < albumFetchBatchSize { if len(items) < batchFetchSize {
g.done = true g.done = true
} }
n += len(albums) n += len(items)
if len(albums) > 0 { if len(items) > 0 {
g.Refresh() g.Refresh()
} }
} }