diff --git a/res/bundled.go b/res/bundled.go
index d02d826..4bf4716 100644
--- a/res/bundled.go
+++ b/res/bundled.go
@@ -130,6 +130,16 @@ var ResStarFilledSvg = &fyne.StaticResource{
StaticContent: []byte(
"\r\n"),
}
+var ResGridSvg = &fyne.StaticResource{
+ StaticName: "grid.svg",
+ StaticContent: []byte(
+ "\r"),
+}
+var ResListSvg = &fyne.StaticResource{
+ StaticName: "list.svg",
+ StaticContent: []byte(
+ "\r"),
+}
var ResLICENSE = &fyne.StaticResource{
StaticName: "LICENSE",
StaticContent: []byte(
diff --git a/res/bundled_gen.sh b/res/bundled_gen.sh
index f21f5fe..3bb5b19 100755
--- a/res/bundled_gen.sh
+++ b/res/bundled_gen.sh
@@ -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
diff --git a/res/icons/publicdomain/grid.svg b/res/icons/publicdomain/grid.svg
new file mode 100644
index 0000000..a4476a9
--- /dev/null
+++ b/res/icons/publicdomain/grid.svg
@@ -0,0 +1,7 @@
+
\ No newline at end of file
diff --git a/res/icons/publicdomain/list.svg b/res/icons/publicdomain/list.svg
new file mode 100644
index 0000000..c19cdc1
--- /dev/null
+++ b/res/icons/publicdomain/list.svg
@@ -0,0 +1,6 @@
+
\ No newline at end of file
diff --git a/ui/browsing/albumspage.go b/ui/browsing/albumspage.go
index fa260bb..8c2ed16 100644
--- a/ui/browsing/albumspage.go
+++ b/ui/browsing/albumspage.go
@@ -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()
diff --git a/ui/browsing/favoritespage.go b/ui/browsing/favoritespage.go
index 3a1decb..389682c 100644
--- a/ui/browsing/favoritespage.go
+++ b/ui/browsing/favoritespage.go
@@ -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()
diff --git a/ui/browsing/genrepage.go b/ui/browsing/genrepage.go
index bdeb532..70d2730 100644
--- a/ui/browsing/genrepage.go
+++ b/ui/browsing/genrepage.go
@@ -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()
diff --git a/ui/browsing/playlistspage.go b/ui/browsing/playlistspage.go
index 0a7b189..f8828c7 100644
--- a/ui/browsing/playlistspage.go
+++ b/ui/browsing/playlistspage.go
@@ -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 {
diff --git a/ui/widgets/gridview.go b/ui/widgets/gridview.go
index 9785ca6..3c4969a 100644
--- a/ui/widgets/gridview.go
+++ b/ui/widgets/gridview.go
@@ -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()
}
}