add grid view option to playlists page
This commit is contained in:
@@ -130,6 +130,16 @@ var ResStarFilledSvg = &fyne.StaticResource{
|
||||
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>"),
|
||||
}
|
||||
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{
|
||||
StaticName: "LICENSE",
|
||||
StaticContent: []byte(
|
||||
|
||||
@@ -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/star-outline.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 licenses/BSDLICENSE >> bundled.go
|
||||
|
||||
@@ -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 |
@@ -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 |
@@ -156,7 +156,8 @@ func (a *AlbumsPage) Reload() {
|
||||
if a.searchText != "" {
|
||||
a.doSearch(a.searchText)
|
||||
} 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()
|
||||
}
|
||||
}
|
||||
@@ -185,7 +186,7 @@ func (a *AlbumsPage) doSearch(query string) {
|
||||
a.searchGrid.OnShowItemPage = a.onShowAlbumPage
|
||||
a.searchGrid.OnShowSecondaryPage = a.onShowArtistPage
|
||||
} else {
|
||||
a.searchGrid.Reset(a.lm.SearchIter(query))
|
||||
a.searchGrid.Reset(widgets.NewGridViewAlbumIterator(a.lm.SearchIter(query)))
|
||||
}
|
||||
a.container.Objects[0] = a.searchGrid
|
||||
a.Refresh()
|
||||
@@ -205,7 +206,8 @@ func (a *AlbumsPage) onShowAlbumPage(albumID string) {
|
||||
|
||||
func (a *AlbumsPage) onSortOrderChanged(order string) {
|
||||
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 == "" {
|
||||
a.container.Objects[0] = a.grid
|
||||
a.Refresh()
|
||||
|
||||
@@ -134,7 +134,7 @@ func (a *FavoritesPage) Reload() {
|
||||
if a.searchText != "" {
|
||||
a.doSearchAlbums(a.searchText)
|
||||
} else {
|
||||
a.grid.Reset(a.lm.StarredIter())
|
||||
a.grid.Reset(widgets.NewGridViewAlbumIterator(a.lm.StarredIter()))
|
||||
}
|
||||
if a.tracklistCtr != nil || a.artistListCtr != nil {
|
||||
go func() {
|
||||
@@ -225,7 +225,7 @@ func (a *FavoritesPage) doSearchAlbums(query string) {
|
||||
a.searchGrid.OnShowItemPage = a.onShowAlbumPage
|
||||
a.searchGrid.OnShowSecondaryPage = a.onShowArtistPage
|
||||
} else {
|
||||
a.searchGrid.Reset(iter)
|
||||
a.searchGrid.Reset(widgets.NewGridViewAlbumIterator(iter))
|
||||
}
|
||||
a.container.Objects[0] = a.searchGrid
|
||||
a.Refresh()
|
||||
|
||||
@@ -119,7 +119,7 @@ func (g *GenrePage) Reload() {
|
||||
if g.searchText != "" {
|
||||
g.doSearch(g.searchText)
|
||||
} else {
|
||||
g.grid.Reset(g.lm.GenreIter(g.genre))
|
||||
g.grid.Reset(widgets.NewGridViewAlbumIterator(g.lm.GenreIter(g.genre)))
|
||||
g.grid.Refresh()
|
||||
}
|
||||
}
|
||||
@@ -181,7 +181,7 @@ func (g *GenrePage) doSearch(query string) {
|
||||
g.searchGrid.OnShowItemPage = g.onShowAlbumPage
|
||||
g.searchGrid.OnShowSecondaryPage = g.onShowArtistPage
|
||||
} else {
|
||||
g.searchGrid.Reset(iter)
|
||||
g.searchGrid.Reset(widgets.NewGridViewAlbumIterator(iter))
|
||||
}
|
||||
g.container.Objects[0] = g.searchGrid
|
||||
g.Refresh()
|
||||
|
||||
+114
-25
@@ -1,10 +1,12 @@
|
||||
package browsing
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"strconv"
|
||||
"strings"
|
||||
"supersonic/backend"
|
||||
"supersonic/res"
|
||||
"supersonic/sharedutil"
|
||||
"supersonic/ui/controller"
|
||||
"supersonic/ui/layouts"
|
||||
@@ -21,21 +23,24 @@ import (
|
||||
type PlaylistsPage struct {
|
||||
widget.BaseWidget
|
||||
|
||||
contr *controller.Controller
|
||||
sm *backend.ServerManager
|
||||
playlists []*subsonic.Playlist
|
||||
contr *controller.Controller
|
||||
sm *backend.ServerManager
|
||||
playlists []*subsonic.Playlist
|
||||
searchedPlaylists []*subsonic.Playlist
|
||||
|
||||
searcher *widgets.Searcher
|
||||
titleDisp *widget.RichText
|
||||
container *fyne.Container
|
||||
list *PlaylistList
|
||||
viewToggle *widgets.ToggleButtonGroup
|
||||
searcher *widgets.Searcher
|
||||
titleDisp *widget.RichText
|
||||
container *fyne.Container
|
||||
listView *PlaylistList
|
||||
gridView *widgets.GridView
|
||||
}
|
||||
|
||||
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{
|
||||
sm: sm,
|
||||
contr: contr,
|
||||
@@ -43,14 +48,21 @@ func newPlaylistsPage(contr *controller.Controller, sm *backend.ServerManager, s
|
||||
}
|
||||
a.ExtendBaseWidget(a)
|
||||
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.OnSearched = a.onSearched
|
||||
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 != "")
|
||||
return a
|
||||
}
|
||||
@@ -64,26 +76,101 @@ func (a *PlaylistsPage) load(searchOnLoad bool) {
|
||||
if searchOnLoad {
|
||||
a.onSearched(a.searcher.Entry.Text)
|
||||
} else {
|
||||
a.list.Playlists = playlists
|
||||
a.list.Refresh()
|
||||
a.refreshView(playlists)
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
// 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
|
||||
var playlists []*subsonic.Playlist
|
||||
if query == "" {
|
||||
a.list.Playlists = a.playlists
|
||||
a.searchedPlaylists = nil
|
||||
playlists = a.playlists
|
||||
} 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)
|
||||
return strings.Contains(strings.ToLower(p.Name), qLower) ||
|
||||
strings.Contains(strings.ToLower(p.Comment), 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)
|
||||
@@ -97,7 +184,7 @@ func (a *PlaylistsPage) Route() controller.Route {
|
||||
}
|
||||
|
||||
func (a *PlaylistsPage) Reload() {
|
||||
go a.load(false)
|
||||
go a.load(a.searcher.Entry.Text != "")
|
||||
}
|
||||
|
||||
func (a *PlaylistsPage) Save() SavedPage {
|
||||
@@ -105,6 +192,7 @@ func (a *PlaylistsPage) Save() SavedPage {
|
||||
contr: a.contr,
|
||||
sm: a.sm,
|
||||
searchText: a.searcher.Entry.Text,
|
||||
activeView: a.viewToggle.ActivatedButtonIndex(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,18 +200,19 @@ type savedPlaylistsPage struct {
|
||||
contr *controller.Controller
|
||||
sm *backend.ServerManager
|
||||
searchText string
|
||||
activeView int
|
||||
}
|
||||
|
||||
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())
|
||||
a.container = container.New(&layouts.MaxPadLayout{PadLeft: 15, PadRight: 15, PadTop: 5, PadBottom: 15},
|
||||
container.NewBorder(
|
||||
container.NewHBox(a.titleDisp, layout.NewSpacer(), searchVbox),
|
||||
nil, nil, nil, a.list))
|
||||
container.NewHBox(a.titleDisp, container.NewCenter(a.viewToggle), layout.NewSpacer(), searchVbox),
|
||||
nil, nil, nil, initialView))
|
||||
}
|
||||
|
||||
func (a *PlaylistsPage) CreateRenderer() fyne.WidgetRenderer {
|
||||
|
||||
+18
-8
@@ -14,7 +14,7 @@ import (
|
||||
"github.com/dweymouth/go-subsonic/subsonic"
|
||||
)
|
||||
|
||||
const albumFetchBatchSize = 6
|
||||
const batchFetchSize = 6
|
||||
|
||||
type ImageFetcher interface {
|
||||
GetCoverThumbnailFromCache(string) (image.Image, bool)
|
||||
@@ -123,17 +123,27 @@ func (g *GridView) Clear() {
|
||||
g.done = true
|
||||
}
|
||||
|
||||
func (g *GridView) Reset(iter backend.AlbumIterator) {
|
||||
func (g *GridView) Reset(iter GridViewIterator) {
|
||||
g.itemsMutex.Lock()
|
||||
g.items = nil
|
||||
g.itemsMutex.Unlock()
|
||||
g.fetching = false
|
||||
g.done = false
|
||||
g.highestShown = 0
|
||||
g.iter = gridViewAlbumIterator{iter: backend.NewBatchingIterator(iter)}
|
||||
g.iter = iter
|
||||
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() {
|
||||
g.grid = widget.NewGridWrapList(
|
||||
func() int {
|
||||
@@ -231,15 +241,15 @@ func (g *GridView) fetchMoreItems(count int) {
|
||||
for !g.done && g.highestShown >= g.lenItems()-10 {
|
||||
n := 0
|
||||
for !g.done && n < count {
|
||||
albums := g.iter.NextN(albumFetchBatchSize)
|
||||
items := g.iter.NextN(batchFetchSize)
|
||||
g.itemsMutex.Lock()
|
||||
g.items = append(g.items, albums...)
|
||||
g.items = append(g.items, items...)
|
||||
g.itemsMutex.Unlock()
|
||||
if len(albums) < albumFetchBatchSize {
|
||||
if len(items) < batchFetchSize {
|
||||
g.done = true
|
||||
}
|
||||
n += len(albums)
|
||||
if len(albums) > 0 {
|
||||
n += len(items)
|
||||
if len(items) > 0 {
|
||||
g.Refresh()
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user