From 9b1349b0acfd243fb0ce1e5d534e8f1c688e4141 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Fri, 28 Apr 2023 19:28:20 -0700 Subject: [PATCH] add grid view option to playlists page --- res/bundled.go | 10 +++ res/bundled_gen.sh | 2 + res/icons/publicdomain/grid.svg | 7 ++ res/icons/publicdomain/list.svg | 6 ++ ui/browsing/albumspage.go | 8 +- ui/browsing/favoritespage.go | 4 +- ui/browsing/genrepage.go | 4 +- ui/browsing/playlistspage.go | 139 ++++++++++++++++++++++++++------ ui/widgets/gridview.go | 26 ++++-- 9 files changed, 166 insertions(+), 40 deletions(-) create mode 100644 res/icons/publicdomain/grid.svg create mode 100644 res/icons/publicdomain/list.svg 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\r\n\r\n"), } +var ResGridSvg = &fyne.StaticResource{ + StaticName: "grid.svg", + StaticContent: []byte( + "\r\r\r\n\t\r\n\t\r\n\t\r\n\t\r\n\r\n"), +} +var ResListSvg = &fyne.StaticResource{ + StaticName: "list.svg", + StaticContent: []byte( + "\r\r\r\n\t\r\n\t\r\n\t\r\n\r\n"), +} 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() } }