Merge pull request #150 from dweymouth/feature/playlistgrid
Add grid view option to playlists page
This commit is contained in:
@@ -6,6 +6,7 @@
|
||||
- [#145](https://github.com/dweymouth/supersonic/issues/145) Add rating column to tracklist with 5-star rating widget
|
||||
- [b017995](https://github.com/dweymouth/supersonic/commit/b01799550ded0c6a8f33913827df23818f9a7353) Add Ctrl+W (Cmd+W on Mac) shortcut to close to tray if enabled
|
||||
- [#95](https://github.com/dweymouth/supersonic/issues/95) Enable click-to-seek behavior in seek bar
|
||||
- [#40](https://github.com/dweymouth/supersonic/issues/40) Add grid view option to playlists page
|
||||
|
||||
### Fixed
|
||||
- [#148](https://github.com/dweymouth/supersonic/issues/148) Fix potential crash when searching for albums
|
||||
|
||||
+1
-1
@@ -65,7 +65,7 @@ func StartupApp(appName, appVersionTag, configFile, latestReleaseURL string) (*A
|
||||
a.LibraryManager = NewLibraryManager(a.ServerManager)
|
||||
a.ImageManager = NewImageManager(a.bgrndCtx, a.ServerManager, configdir.LocalCache(a.appName))
|
||||
a.LibraryManager.PreCacheCoverFn = func(coverID string) {
|
||||
_, _ = a.ImageManager.GetAlbumThumbnail(coverID)
|
||||
_, _ = a.ImageManager.GetCoverThumbnail(coverID)
|
||||
}
|
||||
|
||||
return a, nil
|
||||
|
||||
@@ -59,6 +59,10 @@ type PlaylistPageConfig struct {
|
||||
TracklistColumns []string
|
||||
}
|
||||
|
||||
type PlaylistsPageConfig struct {
|
||||
InitialView string
|
||||
}
|
||||
|
||||
type TracksPageConfig struct {
|
||||
TracklistColumns []string
|
||||
}
|
||||
@@ -91,6 +95,7 @@ type Config struct {
|
||||
FavoritesPage FavoritesPageConfig
|
||||
NowPlayingPage NowPlayingPageConfig
|
||||
PlaylistPage PlaylistPageConfig
|
||||
PlaylistsPage PlaylistsPageConfig
|
||||
TracksPage TracksPageConfig
|
||||
LocalPlayback LocalPlaybackConfig
|
||||
Scrobbling ScrobbleConfig
|
||||
@@ -126,6 +131,9 @@ func DefaultConfig(appVersionTag string) *Config {
|
||||
PlaylistPage: PlaylistPageConfig{
|
||||
TracklistColumns: []string{"Artist", "Album", "Time", "Plays"},
|
||||
},
|
||||
PlaylistsPage: PlaylistsPageConfig{
|
||||
InitialView: "List",
|
||||
},
|
||||
TracksPage: TracksPageConfig{
|
||||
TracklistColumns: []string{"Artist", "Album", "Time", "Plays"},
|
||||
},
|
||||
|
||||
@@ -45,7 +45,7 @@ func NewImageManager(ctx context.Context, s *ServerManager, baseCacheDir string)
|
||||
return i
|
||||
}
|
||||
|
||||
func (i *ImageManager) GetAlbumThumbnailFromCache(coverID string) (image.Image, bool) {
|
||||
func (i *ImageManager) GetCoverThumbnailFromCache(coverID string) (image.Image, bool) {
|
||||
img, err := i.thumbnailCache.GetExtendTTL(coverID, i.thumbnailCache.DefaultTTL)
|
||||
if err == nil && img != nil {
|
||||
return img, true
|
||||
@@ -53,14 +53,14 @@ func (i *ImageManager) GetAlbumThumbnailFromCache(coverID string) (image.Image,
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func (i *ImageManager) GetAlbumThumbnail(coverID string) (image.Image, error) {
|
||||
if im, ok := i.GetAlbumThumbnailFromCache(coverID); ok {
|
||||
func (i *ImageManager) GetCoverThumbnail(coverID string) (image.Image, error) {
|
||||
if im, ok := i.GetCoverThumbnailFromCache(coverID); ok {
|
||||
return im, nil
|
||||
}
|
||||
return i.fetchAndCacheCoverFromDiskOrServer(coverID, i.thumbnailCache.DefaultTTL)
|
||||
}
|
||||
|
||||
func (i *ImageManager) GetAlbumThumbnailWithTTL(coverID string, ttl time.Duration) (image.Image, error) {
|
||||
func (i *ImageManager) GetCoverThumbnailWithTTL(coverID string, ttl time.Duration) (image.Image, error) {
|
||||
// in-memory cache
|
||||
if img, err := i.thumbnailCache.GetWithNewTTL(coverID, ttl); err == nil {
|
||||
return img, nil
|
||||
@@ -68,7 +68,7 @@ func (i *ImageManager) GetAlbumThumbnailWithTTL(coverID string, ttl time.Duratio
|
||||
return i.fetchAndCacheCoverFromDiskOrServer(coverID, ttl)
|
||||
}
|
||||
|
||||
func (i *ImageManager) GetFullSizeAlbumCover(coverID string) (image.Image, error) {
|
||||
func (i *ImageManager) GetFullSizeCoverArt(coverID string) (image.Image, error) {
|
||||
if i.cachedFullSizeCoverID == coverID {
|
||||
return i.cachedFullSizeCover, nil
|
||||
}
|
||||
|
||||
@@ -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 |
@@ -35,6 +35,14 @@ func FilterSlice[T any](ss []T, test func(T) bool) []T {
|
||||
return result
|
||||
}
|
||||
|
||||
func MapSlice[T any, U any](ts []T, f func(T) U) []U {
|
||||
result := make([]U, len(ts))
|
||||
for i, t := range ts {
|
||||
result[i] = f(t)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func FindTrackByID(id string, tracks []*subsonic.Child) *subsonic.Child {
|
||||
for _, tr := range tracks {
|
||||
if id == tr.ID {
|
||||
|
||||
+1
-1
@@ -95,7 +95,7 @@ func (bp *BottomPanel) onSongChange(song *subsonic.Child, _ *subsonic.Child) {
|
||||
// be in cache for the next song if it's from the same album, or
|
||||
// if the user navigates to the album page for the track
|
||||
imgTTLSec := song.Duration + 30
|
||||
im, _ = bp.ImageManager.GetAlbumThumbnailWithTTL(song.CoverArt, time.Duration(imgTTLSec)*time.Second)
|
||||
im, _ = bp.ImageManager.GetCoverThumbnailWithTTL(song.CoverArt, time.Duration(imgTTLSec)*time.Second)
|
||||
}
|
||||
bp.NowPlaying.Update(song.Title, song.Artist, song.Album, im)
|
||||
}
|
||||
|
||||
@@ -226,7 +226,7 @@ func (a *AlbumPageHeader) Update(album *subsonic.AlbumID3, im *backend.ImageMana
|
||||
a.Refresh()
|
||||
|
||||
go func() {
|
||||
if cover, err := im.GetAlbumThumbnail(album.CoverArt); err == nil {
|
||||
if cover, err := im.GetCoverThumbnail(album.CoverArt); err == nil {
|
||||
a.cover.Image.Image = cover
|
||||
a.cover.Refresh()
|
||||
} else {
|
||||
@@ -244,7 +244,7 @@ func (a *AlbumPageHeader) toggleFavorited() {
|
||||
}
|
||||
|
||||
func (a *AlbumPageHeader) showPopUpCover() {
|
||||
cover, err := a.page.im.GetFullSizeAlbumCover(a.coverID)
|
||||
cover, err := a.page.im.GetFullSizeCoverArt(a.coverID)
|
||||
if err != nil {
|
||||
log.Printf("error getting full size album cover: %s", err.Error())
|
||||
return
|
||||
|
||||
+19
-17
@@ -24,8 +24,8 @@ type AlbumsPage struct {
|
||||
pm *backend.PlaybackManager
|
||||
im *backend.ImageManager
|
||||
lm *backend.LibraryManager
|
||||
grid *widgets.AlbumGrid
|
||||
searchGrid *widgets.AlbumGrid
|
||||
grid *widgets.GridView
|
||||
searchGrid *widgets.GridView
|
||||
searcher *widgets.Searcher
|
||||
searchText string
|
||||
titleDisp *widget.RichText
|
||||
@@ -72,10 +72,10 @@ func NewAlbumsPage(cfg *backend.AlbumsPageConfig, contr *controller.Controller,
|
||||
}
|
||||
a.sortOrder.Selected = cfg.SortOrder
|
||||
iter := lm.AlbumsIter(backend.AlbumSortOrder(a.sortOrder.Selected))
|
||||
a.grid = widgets.NewAlbumGrid(iter, im, false /*showYear*/)
|
||||
a.grid.OnPlayAlbum = a.onPlayAlbum
|
||||
a.grid.OnShowArtistPage = a.onShowArtistPage
|
||||
a.grid.OnShowAlbumPage = a.onShowAlbumPage
|
||||
a.grid = widgets.NewGridView(widgets.NewGridViewAlbumIterator(iter), im)
|
||||
a.grid.OnPlay = a.onPlayAlbum
|
||||
a.grid.OnShowSecondaryPage = a.onShowArtistPage
|
||||
a.grid.OnShowItemPage = a.onShowAlbumPage
|
||||
a.searcher = widgets.NewSearcher()
|
||||
a.searcher.OnSearched = a.OnSearched
|
||||
a.createContainer(false)
|
||||
@@ -116,13 +116,13 @@ func restoreAlbumsPage(saved *savedAlbumsPage) *AlbumsPage {
|
||||
a.sortOrder = NewSelect(backend.AlbumSortOrders, nil)
|
||||
a.sortOrder.Selected = saved.sortOrder
|
||||
a.sortOrder.OnChanged = a.onSortOrderChanged
|
||||
a.grid = widgets.NewAlbumGridFromState(saved.gridState)
|
||||
a.grid = widgets.NewGridViewFromState(saved.gridState)
|
||||
a.searcher = widgets.NewSearcher()
|
||||
a.searcher.OnSearched = a.OnSearched
|
||||
a.searcher.Entry.Text = saved.searchText
|
||||
a.searchText = saved.searchText
|
||||
if a.searchText != "" {
|
||||
a.searchGrid = widgets.NewAlbumGridFromState(saved.searchGridState)
|
||||
a.searchGrid = widgets.NewGridViewFromState(saved.searchGridState)
|
||||
}
|
||||
a.createContainer(saved.searchText != "")
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -180,12 +181,12 @@ func (a *AlbumsPage) Save() SavedPage {
|
||||
|
||||
func (a *AlbumsPage) doSearch(query string) {
|
||||
if a.searchGrid == nil {
|
||||
a.searchGrid = widgets.NewAlbumGrid(a.lm.SearchIter(query), a.im, false /*showYear*/)
|
||||
a.searchGrid.OnPlayAlbum = a.onPlayAlbum
|
||||
a.searchGrid.OnShowAlbumPage = a.onShowAlbumPage
|
||||
a.searchGrid.OnShowArtistPage = a.onShowArtistPage
|
||||
a.searchGrid = widgets.NewGridView(widgets.NewGridViewAlbumIterator(a.lm.SearchIter(query)), a.im)
|
||||
a.searchGrid.OnPlay = a.onPlayAlbum
|
||||
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()
|
||||
@@ -225,8 +227,8 @@ type savedAlbumsPage struct {
|
||||
lm *backend.LibraryManager
|
||||
im *backend.ImageManager
|
||||
sortOrder string
|
||||
gridState widgets.AlbumGridState
|
||||
searchGridState widgets.AlbumGridState
|
||||
gridState widgets.GridViewState
|
||||
searchGridState widgets.GridViewState
|
||||
}
|
||||
|
||||
func (s *savedAlbumsPage) Restore() Page {
|
||||
|
||||
@@ -2,6 +2,7 @@ package browsing
|
||||
|
||||
import (
|
||||
"log"
|
||||
"strconv"
|
||||
"strings"
|
||||
"supersonic/backend"
|
||||
"supersonic/res"
|
||||
@@ -39,7 +40,7 @@ type ArtistPage struct {
|
||||
|
||||
artistInfo *subsonic.ArtistID3
|
||||
|
||||
albumGrid *widgets.AlbumGrid
|
||||
albumGrid *widgets.GridView
|
||||
tracklistCtr *fyne.Container
|
||||
nowPlayingID string
|
||||
header *ArtistPageHeader
|
||||
@@ -172,9 +173,17 @@ func (a *ArtistPage) showAlbumGrid() {
|
||||
a.activeView = 0 // if page still loading, will show discography view first
|
||||
return
|
||||
}
|
||||
a.albumGrid = widgets.NewFixedAlbumGrid(a.artistInfo.Album, a.im, true /*showYear*/)
|
||||
a.albumGrid.OnPlayAlbum = a.onPlayAlbum
|
||||
a.albumGrid.OnShowAlbumPage = a.onShowAlbumPage
|
||||
model := sharedutil.MapSlice(a.artistInfo.Album, func(al *subsonic.AlbumID3) widgets.GridViewItemModel {
|
||||
return widgets.GridViewItemModel{
|
||||
Name: al.Name,
|
||||
ID: al.ID,
|
||||
CoverArtID: al.CoverArt,
|
||||
Secondary: strconv.Itoa(al.Year),
|
||||
}
|
||||
})
|
||||
a.albumGrid = widgets.NewFixedGridView(model, a.im)
|
||||
a.albumGrid.OnPlay = a.onPlayAlbum
|
||||
a.albumGrid.OnShowItemPage = a.onShowAlbumPage
|
||||
}
|
||||
a.container.Objects[0].(*fyne.Container).Objects[0] = a.albumGrid
|
||||
a.container.Objects[0].Refresh()
|
||||
|
||||
@@ -32,8 +32,8 @@ type FavoritesPage struct {
|
||||
nowPlayingID string
|
||||
pendingViewSwitch bool
|
||||
|
||||
grid *widgets.AlbumGrid
|
||||
searchGrid *widgets.AlbumGrid
|
||||
grid *widgets.GridView
|
||||
searchGrid *widgets.GridView
|
||||
artistListCtr *fyne.Container
|
||||
tracklistCtr *fyne.Container
|
||||
searcher *widgets.Searcher
|
||||
@@ -53,7 +53,7 @@ func NewFavoritesPage(cfg *backend.FavoritesPageConfig, contr *controller.Contro
|
||||
}
|
||||
a.ExtendBaseWidget(a)
|
||||
a.createHeader(0, "")
|
||||
a.grid = widgets.NewAlbumGrid(a.lm.StarredIter(), a.im, false)
|
||||
a.grid = widgets.NewGridView(widgets.NewGridViewAlbumIterator(lm.StarredIter()), a.im)
|
||||
a.connectGridActions()
|
||||
if cfg.InitialView == "Artists" {
|
||||
a.toggleBtns.SetActivatedButton(1)
|
||||
@@ -82,9 +82,9 @@ func (a *FavoritesPage) createHeader(activeBtnIdx int, searchText string) {
|
||||
}
|
||||
|
||||
func (a *FavoritesPage) connectGridActions() {
|
||||
a.grid.OnPlayAlbum = a.onPlayAlbum
|
||||
a.grid.OnShowAlbumPage = a.onShowAlbumPage
|
||||
a.grid.OnShowArtistPage = a.onShowArtistPage
|
||||
a.grid.OnPlay = a.onPlayAlbum
|
||||
a.grid.OnShowItemPage = a.onShowAlbumPage
|
||||
a.grid.OnShowSecondaryPage = a.onShowArtistPage
|
||||
}
|
||||
|
||||
func (a *FavoritesPage) createContainer(initialView fyne.CanvasObject) {
|
||||
@@ -105,11 +105,11 @@ func restoreFavoritesPage(saved *savedFavoritesPage) *FavoritesPage {
|
||||
}
|
||||
a.ExtendBaseWidget(a)
|
||||
a.createHeader(saved.activeToggleBtn, saved.searchText)
|
||||
a.grid = widgets.NewAlbumGridFromState(saved.gridState)
|
||||
a.grid = widgets.NewGridViewFromState(saved.gridState)
|
||||
a.connectGridActions()
|
||||
|
||||
if saved.searchText != "" {
|
||||
a.searchGrid = widgets.NewAlbumGridFromState(saved.searchGridState)
|
||||
a.searchGrid = widgets.NewGridViewFromState(saved.searchGridState)
|
||||
}
|
||||
|
||||
if saved.activeToggleBtn == 1 {
|
||||
@@ -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() {
|
||||
@@ -220,12 +220,12 @@ func (a *FavoritesPage) doSearchAlbums(query string) {
|
||||
return al.Starred.After(time.Time{})
|
||||
})
|
||||
if a.searchGrid == nil {
|
||||
a.searchGrid = widgets.NewAlbumGrid(iter, a.im, false /*showYear*/)
|
||||
a.searchGrid.OnPlayAlbum = a.onPlayAlbum
|
||||
a.searchGrid.OnShowAlbumPage = a.onShowAlbumPage
|
||||
a.searchGrid.OnShowArtistPage = a.onShowArtistPage
|
||||
a.searchGrid = widgets.NewGridView(widgets.NewGridViewAlbumIterator(iter), a.im)
|
||||
a.searchGrid.OnPlay = a.onPlayAlbum
|
||||
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()
|
||||
@@ -352,8 +352,8 @@ type savedFavoritesPage struct {
|
||||
sm *backend.ServerManager
|
||||
im *backend.ImageManager
|
||||
lm *backend.LibraryManager
|
||||
gridState widgets.AlbumGridState
|
||||
searchGridState widgets.AlbumGridState
|
||||
gridState widgets.GridViewState
|
||||
searchGridState widgets.GridViewState
|
||||
searchText string
|
||||
activeToggleBtn int
|
||||
}
|
||||
|
||||
+16
-16
@@ -24,8 +24,8 @@ type GenrePage struct {
|
||||
im *backend.ImageManager
|
||||
pm *backend.PlaybackManager
|
||||
lm *backend.LibraryManager
|
||||
grid *widgets.AlbumGrid
|
||||
searchGrid *widgets.AlbumGrid
|
||||
grid *widgets.GridView
|
||||
searchGrid *widgets.GridView
|
||||
searcher *widgets.Searcher
|
||||
searchText string
|
||||
titleDisp *widget.RichText
|
||||
@@ -52,10 +52,10 @@ func NewGenrePage(genre string, contr *controller.Controller, pm *backend.Playba
|
||||
}
|
||||
g.playRandom = widget.NewButtonWithIcon("Play random", res.ResShuffleInvertSvg, g.playRandomSongs)
|
||||
iter := g.lm.GenreIter(g.genre)
|
||||
g.grid = widgets.NewAlbumGrid(iter, g.im, false)
|
||||
g.grid.OnPlayAlbum = g.onPlayAlbum
|
||||
g.grid.OnShowArtistPage = g.onShowArtistPage
|
||||
g.grid.OnShowAlbumPage = g.onShowAlbumPage
|
||||
g.grid = widgets.NewGridView(widgets.NewGridViewAlbumIterator(iter), g.im)
|
||||
g.grid.OnPlay = g.onPlayAlbum
|
||||
g.grid.OnShowSecondaryPage = g.onShowArtistPage
|
||||
g.grid.OnShowItemPage = g.onShowAlbumPage
|
||||
g.searcher = widgets.NewSearcher()
|
||||
g.searcher.OnSearched = g.OnSearched
|
||||
g.createContainer(false)
|
||||
@@ -94,13 +94,13 @@ func restoreGenrePage(saved *savedGenrePage) *GenrePage {
|
||||
SizeName: theme.SizeNameHeadingText,
|
||||
}
|
||||
g.playRandom = widget.NewButtonWithIcon("Play random", res.ResShuffleInvertSvg, g.playRandomSongs)
|
||||
g.grid = widgets.NewAlbumGridFromState(saved.gridState)
|
||||
g.grid = widgets.NewGridViewFromState(saved.gridState)
|
||||
g.searcher = widgets.NewSearcher()
|
||||
g.searcher.OnSearched = g.OnSearched
|
||||
g.searcher.Entry.Text = saved.searchText
|
||||
g.searchText = saved.searchText
|
||||
if g.searchText != "" {
|
||||
g.searchGrid = widgets.NewAlbumGridFromState(saved.searchGridState)
|
||||
g.searchGrid = widgets.NewGridViewFromState(saved.searchGridState)
|
||||
}
|
||||
g.createContainer(saved.searchText != "")
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -176,12 +176,12 @@ func (g *GenrePage) doSearch(query string) {
|
||||
return al.Genre == g.genre
|
||||
})
|
||||
if g.searchGrid == nil {
|
||||
g.searchGrid = widgets.NewAlbumGrid(iter, g.im, false /*showYear*/)
|
||||
g.searchGrid.OnPlayAlbum = g.onPlayAlbum
|
||||
g.searchGrid.OnShowAlbumPage = g.onShowAlbumPage
|
||||
g.searchGrid.OnShowArtistPage = g.onShowArtistPage
|
||||
g.searchGrid = widgets.NewGridView(widgets.NewGridViewAlbumIterator(iter), g.im)
|
||||
g.searchGrid.OnPlay = g.onPlayAlbum
|
||||
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()
|
||||
@@ -198,8 +198,8 @@ type savedGenrePage struct {
|
||||
pm *backend.PlaybackManager
|
||||
lm *backend.LibraryManager
|
||||
im *backend.ImageManager
|
||||
gridState widgets.AlbumGridState
|
||||
searchGridState widgets.AlbumGridState
|
||||
gridState widgets.GridViewState
|
||||
searchGridState widgets.GridViewState
|
||||
}
|
||||
|
||||
func (s *savedGenrePage) Restore() Page {
|
||||
|
||||
@@ -233,13 +233,13 @@ func (a *PlaylistPageHeader) Update(playlist *subsonic.Playlist) {
|
||||
|
||||
var haveCover bool
|
||||
if playlist.CoverArt != "" {
|
||||
if im, err := a.page.im.GetAlbumThumbnail(playlist.CoverArt); err == nil && im != nil {
|
||||
if im, err := a.page.im.GetCoverThumbnail(playlist.CoverArt); err == nil && im != nil {
|
||||
a.image.SetImage(im, false /*tappable*/)
|
||||
haveCover = true
|
||||
}
|
||||
}
|
||||
if !haveCover {
|
||||
if im, err := a.page.im.GetAlbumThumbnail(playlist.ID); err == nil && im != nil {
|
||||
if im, err := a.page.im.GetCoverThumbnail(playlist.ID); err == nil && im != nil {
|
||||
a.image.SetImage(im, false)
|
||||
}
|
||||
}
|
||||
|
||||
+125
-26
@@ -1,10 +1,12 @@
|
||||
package browsing
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"strconv"
|
||||
"strings"
|
||||
"supersonic/backend"
|
||||
"supersonic/res"
|
||||
"supersonic/sharedutil"
|
||||
"supersonic/ui/controller"
|
||||
"supersonic/ui/layouts"
|
||||
@@ -21,36 +23,52 @@ import (
|
||||
type PlaylistsPage struct {
|
||||
widget.BaseWidget
|
||||
|
||||
contr *controller.Controller
|
||||
sm *backend.ServerManager
|
||||
playlists []*subsonic.Playlist
|
||||
cfg *backend.PlaylistsPageConfig
|
||||
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, "")
|
||||
func NewPlaylistsPage(contr *controller.Controller, cfg *backend.PlaylistsPageConfig, sm *backend.ServerManager) *PlaylistsPage {
|
||||
activeView := 0
|
||||
if cfg.InitialView == "Grid" {
|
||||
activeView = 1
|
||||
}
|
||||
return newPlaylistsPage(contr, cfg, sm, "", activeView)
|
||||
}
|
||||
|
||||
func newPlaylistsPage(contr *controller.Controller, sm *backend.ServerManager, searchText string) *PlaylistsPage {
|
||||
func newPlaylistsPage(contr *controller.Controller, cfg *backend.PlaylistsPageConfig, sm *backend.ServerManager, searchText string, activeView int) *PlaylistsPage {
|
||||
a := &PlaylistsPage{
|
||||
cfg: cfg,
|
||||
sm: sm,
|
||||
contr: contr,
|
||||
titleDisp: widget.NewRichTextWithText("Playlists"),
|
||||
}
|
||||
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 +82,103 @@ 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() {
|
||||
a.cfg.InitialView = "List" // save setting
|
||||
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() {
|
||||
a.cfg.InitialView = "Grid" // save setting
|
||||
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,33 +192,37 @@ 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 {
|
||||
return &savedPlaylistsPage{
|
||||
contr: a.contr,
|
||||
cfg: a.cfg,
|
||||
sm: a.sm,
|
||||
searchText: a.searcher.Entry.Text,
|
||||
activeView: a.viewToggle.ActivatedButtonIndex(),
|
||||
}
|
||||
}
|
||||
|
||||
type savedPlaylistsPage struct {
|
||||
contr *controller.Controller
|
||||
cfg *backend.PlaylistsPageConfig
|
||||
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.cfg, 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 {
|
||||
|
||||
@@ -45,7 +45,7 @@ func (r Router) CreatePage(rte controller.Route) Page {
|
||||
case controller.Playlist:
|
||||
return NewPlaylistPage(rte.Arg, &r.App.Config.PlaylistPage, r.Controller, r.App.ServerManager, r.App.PlaybackManager, r.App.ImageManager)
|
||||
case controller.Playlists:
|
||||
return NewPlaylistsPage(r.Controller, r.App.ServerManager)
|
||||
return NewPlaylistsPage(r.Controller, &r.App.Config.PlaylistsPage, r.App.ServerManager)
|
||||
case controller.Tracks:
|
||||
return NewTracksPage(r.Controller, &r.App.Config.TracksPage, r.App.LibraryManager)
|
||||
}
|
||||
|
||||
@@ -1,193 +0,0 @@
|
||||
package widgets
|
||||
|
||||
import (
|
||||
"context"
|
||||
"image"
|
||||
"strconv"
|
||||
|
||||
"supersonic/res"
|
||||
"supersonic/ui/layouts"
|
||||
|
||||
"fyne.io/fyne/v2"
|
||||
"fyne.io/fyne/v2/canvas"
|
||||
"fyne.io/fyne/v2/container"
|
||||
"fyne.io/fyne/v2/driver/desktop"
|
||||
"fyne.io/fyne/v2/widget"
|
||||
|
||||
"github.com/dweymouth/go-subsonic/subsonic"
|
||||
)
|
||||
|
||||
var _ fyne.Widget = (*AlbumCard)(nil)
|
||||
|
||||
var _ fyne.Widget = (*albumCover)(nil)
|
||||
var _ fyne.Tappable = (*albumCover)(nil)
|
||||
|
||||
type albumCover struct {
|
||||
widget.BaseWidget
|
||||
|
||||
Im *canvas.Image
|
||||
playbtn *canvas.Image
|
||||
OnDoubleTapped func()
|
||||
OnTapped func()
|
||||
}
|
||||
|
||||
func newAlbumCover() *albumCover {
|
||||
a := &albumCover{}
|
||||
a.ExtendBaseWidget(a)
|
||||
a.Im = &canvas.Image{FillMode: canvas.ImageFillContain, ScaleMode: canvas.ImageScaleFastest}
|
||||
a.Im.SetMinSize(fyne.NewSize(200, 200))
|
||||
a.playbtn = &canvas.Image{FillMode: canvas.ImageFillContain, Resource: res.ResPlaybuttonPng}
|
||||
a.playbtn.SetMinSize(fyne.NewSize(60, 60))
|
||||
a.playbtn.Hidden = true
|
||||
return a
|
||||
}
|
||||
|
||||
func (a *albumCover) CreateRenderer() fyne.WidgetRenderer {
|
||||
return widget.NewSimpleRenderer(
|
||||
container.NewMax(a.Im, container.NewCenter(a.playbtn)),
|
||||
)
|
||||
}
|
||||
|
||||
func (a *albumCover) Cursor() desktop.Cursor {
|
||||
return desktop.PointerCursor
|
||||
}
|
||||
|
||||
func (a *albumCover) Tapped(e *fyne.PointEvent) {
|
||||
if isInside(a.center(), a.playbtn.Size().Height/2, e.Position) {
|
||||
if a.OnDoubleTapped != nil {
|
||||
a.OnDoubleTapped()
|
||||
}
|
||||
return
|
||||
}
|
||||
if a.OnTapped != nil {
|
||||
a.OnTapped()
|
||||
}
|
||||
}
|
||||
|
||||
func (a *albumCover) MouseIn(*desktop.MouseEvent) {
|
||||
a.playbtn.Hidden = false
|
||||
a.Refresh()
|
||||
}
|
||||
|
||||
func (a *albumCover) MouseOut() {
|
||||
a.playbtn.Hidden = true
|
||||
a.Refresh()
|
||||
}
|
||||
|
||||
// TODO: figure out why circle around play button isn't being displayed
|
||||
func (a *albumCover) MouseMoved(e *desktop.MouseEvent) {
|
||||
if isInside(a.center(), a.playbtn.MinSize().Height/2, e.Position) {
|
||||
a.playbtn.SetMinSize(fyne.NewSize(65, 65))
|
||||
} else {
|
||||
a.playbtn.SetMinSize(fyne.NewSize(60, 60))
|
||||
}
|
||||
a.Refresh()
|
||||
}
|
||||
|
||||
func (a *albumCover) center() fyne.Position {
|
||||
return fyne.NewPos(a.Size().Width/2, a.Size().Height/2)
|
||||
}
|
||||
|
||||
func (a *albumCover) SetImage(im image.Image) {
|
||||
a.Im.Resource = nil
|
||||
a.Im.Image = im
|
||||
a.Refresh()
|
||||
}
|
||||
|
||||
func (a *albumCover) SetImageResource(res *fyne.StaticResource) {
|
||||
a.Im.Image = nil
|
||||
a.Im.Resource = res
|
||||
a.Refresh()
|
||||
}
|
||||
|
||||
func isInside(origin fyne.Position, radius float32, point fyne.Position) bool {
|
||||
x, y := (point.X - origin.X), (point.Y - origin.Y)
|
||||
return x*x+y*y <= radius*radius
|
||||
}
|
||||
|
||||
type AlbumCard struct {
|
||||
widget.BaseWidget
|
||||
|
||||
albumID string
|
||||
artistID string
|
||||
title *CustomHyperlink
|
||||
artist *CustomHyperlink
|
||||
year *widget.Label
|
||||
container *fyne.Container
|
||||
|
||||
showYear bool
|
||||
|
||||
// updated by AlbumGrid
|
||||
Cover *albumCover
|
||||
|
||||
// these fields are used by AlbumGrid to track async update tasks
|
||||
PrevAlbumID string
|
||||
ImgLoadCancel context.CancelFunc
|
||||
|
||||
OnPlay func()
|
||||
OnShowAlbumPage func()
|
||||
OnShowArtistPage func()
|
||||
}
|
||||
|
||||
func NewAlbumCard(showYear bool) *AlbumCard {
|
||||
a := &AlbumCard{
|
||||
title: NewCustomHyperlink(),
|
||||
artist: NewCustomHyperlink(),
|
||||
year: widget.NewLabel(""),
|
||||
Cover: newAlbumCover(),
|
||||
showYear: showYear,
|
||||
}
|
||||
a.ExtendBaseWidget(a)
|
||||
a.Cover.OnDoubleTapped = func() {
|
||||
if a.OnPlay != nil {
|
||||
a.OnPlay()
|
||||
}
|
||||
}
|
||||
showAlbumFn := func() {
|
||||
if a.OnShowAlbumPage != nil {
|
||||
a.OnShowAlbumPage()
|
||||
}
|
||||
}
|
||||
a.Cover.OnTapped = showAlbumFn
|
||||
a.title.OnTapped = showAlbumFn
|
||||
a.artist.OnTapped = func() {
|
||||
if a.OnShowArtistPage != nil {
|
||||
a.OnShowArtistPage()
|
||||
}
|
||||
}
|
||||
|
||||
a.createContainer()
|
||||
return a
|
||||
}
|
||||
|
||||
func (a *AlbumCard) createContainer() {
|
||||
var secondLabel fyne.Widget = a.artist
|
||||
if a.showYear {
|
||||
secondLabel = a.year
|
||||
}
|
||||
info := container.New(&layouts.VboxCustomPadding{ExtraPad: -16}, a.title, secondLabel)
|
||||
c := container.New(&layouts.VboxCustomPadding{ExtraPad: -5}, a.Cover, info)
|
||||
pad := &layouts.CenterPadLayout{PadLeftRight: 20, PadTopBottom: 10}
|
||||
a.container = container.New(pad, c)
|
||||
}
|
||||
|
||||
func (a *AlbumCard) Update(al *subsonic.AlbumID3) {
|
||||
a.title.SetText(al.Name)
|
||||
a.artist.SetText(al.Artist)
|
||||
a.year.SetText(strconv.Itoa(al.Year))
|
||||
a.albumID = al.ID
|
||||
a.artistID = al.ArtistID
|
||||
a.Cover.playbtn.Hidden = true
|
||||
}
|
||||
|
||||
func (a *AlbumCard) AlbumID() string {
|
||||
return a.albumID
|
||||
}
|
||||
|
||||
func (a *AlbumCard) ArtistID() string {
|
||||
return a.artistID
|
||||
}
|
||||
|
||||
func (a *AlbumCard) CreateRenderer() fyne.WidgetRenderer {
|
||||
return widget.NewSimpleRenderer(a.container)
|
||||
}
|
||||
@@ -1,230 +0,0 @@
|
||||
package widgets
|
||||
|
||||
import (
|
||||
"context"
|
||||
"image"
|
||||
"log"
|
||||
"supersonic/backend"
|
||||
"supersonic/res"
|
||||
"sync"
|
||||
|
||||
"fyne.io/fyne/v2"
|
||||
"fyne.io/fyne/v2/widget"
|
||||
"github.com/dweymouth/go-subsonic/subsonic"
|
||||
)
|
||||
|
||||
const albumFetchBatchSize = 6
|
||||
|
||||
type ImageFetcher interface {
|
||||
GetAlbumThumbnailFromCache(string) (image.Image, bool)
|
||||
GetAlbumThumbnail(string) (image.Image, error)
|
||||
}
|
||||
|
||||
type AlbumGrid struct {
|
||||
widget.BaseWidget
|
||||
|
||||
AlbumGridState
|
||||
|
||||
grid *widget.GridWrapList
|
||||
}
|
||||
|
||||
type AlbumGridState struct {
|
||||
albums []*subsonic.AlbumID3
|
||||
albumsMutex sync.RWMutex
|
||||
iter *backend.BatchingIterator
|
||||
highestShown int
|
||||
fetching bool
|
||||
done bool
|
||||
showYear bool
|
||||
|
||||
imageFetcher ImageFetcher
|
||||
OnPlayAlbum func(string)
|
||||
OnShowAlbumPage func(string)
|
||||
OnShowArtistPage func(string)
|
||||
|
||||
scrollPos float32
|
||||
}
|
||||
|
||||
var _ fyne.Widget = (*AlbumGrid)(nil)
|
||||
|
||||
func NewFixedAlbumGrid(albums []*subsonic.AlbumID3, fetch ImageFetcher, showYear bool) *AlbumGrid {
|
||||
ag := &AlbumGrid{
|
||||
AlbumGridState: AlbumGridState{
|
||||
albums: albums,
|
||||
done: true,
|
||||
imageFetcher: fetch,
|
||||
showYear: showYear,
|
||||
},
|
||||
}
|
||||
ag.ExtendBaseWidget(ag)
|
||||
ag.createGridWrapList()
|
||||
return ag
|
||||
}
|
||||
|
||||
func NewAlbumGrid(iter backend.AlbumIterator, fetch ImageFetcher, showYear bool) *AlbumGrid {
|
||||
ag := &AlbumGrid{
|
||||
AlbumGridState: AlbumGridState{
|
||||
iter: backend.NewBatchingIterator(iter),
|
||||
imageFetcher: fetch,
|
||||
},
|
||||
}
|
||||
ag.ExtendBaseWidget(ag)
|
||||
|
||||
ag.createGridWrapList()
|
||||
|
||||
// fetch initial albums
|
||||
ag.fetchMoreAlbums(36)
|
||||
return ag
|
||||
}
|
||||
|
||||
func (ag *AlbumGrid) SaveToState() AlbumGridState {
|
||||
s := ag.AlbumGridState
|
||||
s.scrollPos = ag.grid.GetScrollOffset()
|
||||
return s
|
||||
}
|
||||
|
||||
func NewAlbumGridFromState(state AlbumGridState) *AlbumGrid {
|
||||
ag := &AlbumGrid{AlbumGridState: state}
|
||||
ag.ExtendBaseWidget(ag)
|
||||
ag.createGridWrapList()
|
||||
ag.Refresh() // needed to initialize the widget
|
||||
ag.grid.ScrollToOffset(state.scrollPos)
|
||||
return ag
|
||||
}
|
||||
|
||||
func (ag *AlbumGrid) Clear() {
|
||||
ag.albumsMutex.Lock()
|
||||
defer ag.albumsMutex.Unlock()
|
||||
ag.albums = nil
|
||||
ag.done = true
|
||||
}
|
||||
|
||||
func (ag *AlbumGrid) Reset(iter backend.AlbumIterator) {
|
||||
ag.albumsMutex.Lock()
|
||||
ag.albums = nil
|
||||
ag.albumsMutex.Unlock()
|
||||
ag.fetching = false
|
||||
ag.done = false
|
||||
ag.highestShown = 0
|
||||
ag.iter = backend.NewBatchingIterator(iter)
|
||||
ag.fetchMoreAlbums(36)
|
||||
}
|
||||
|
||||
func (ag *AlbumGrid) createGridWrapList() {
|
||||
g := widget.NewGridWrapList(
|
||||
func() int {
|
||||
return ag.lenAlbums()
|
||||
},
|
||||
// create func
|
||||
func() fyne.CanvasObject {
|
||||
ac := NewAlbumCard(ag.showYear)
|
||||
ac.OnPlay = func() {
|
||||
if ag.OnPlayAlbum != nil {
|
||||
ag.OnPlayAlbum(ac.AlbumID())
|
||||
}
|
||||
}
|
||||
ac.OnShowArtistPage = func() {
|
||||
if ag.OnShowArtistPage != nil {
|
||||
ag.OnShowArtistPage(ac.ArtistID())
|
||||
}
|
||||
}
|
||||
ac.OnShowAlbumPage = func() {
|
||||
if ag.OnShowAlbumPage != nil {
|
||||
ag.OnShowAlbumPage(ac.AlbumID())
|
||||
}
|
||||
}
|
||||
return ac
|
||||
},
|
||||
// update func
|
||||
func(itemID int, obj fyne.CanvasObject) {
|
||||
ac := obj.(*AlbumCard)
|
||||
ag.doUpdateAlbumCard(itemID, ac)
|
||||
},
|
||||
)
|
||||
ag.grid = g
|
||||
}
|
||||
|
||||
func (ag *AlbumGrid) doUpdateAlbumCard(albumIdx int, ac *AlbumCard) {
|
||||
if albumIdx > ag.highestShown {
|
||||
ag.highestShown = albumIdx
|
||||
}
|
||||
ag.albumsMutex.RLock()
|
||||
album := ag.albums[albumIdx]
|
||||
ag.albumsMutex.RUnlock()
|
||||
if ac.PrevAlbumID == album.ID {
|
||||
// nothing to do
|
||||
return
|
||||
}
|
||||
ac.Update(album)
|
||||
ac.PrevAlbumID = album.ID
|
||||
// cancel any previous image fetch
|
||||
if ac.ImgLoadCancel != nil {
|
||||
ac.ImgLoadCancel()
|
||||
ac.ImgLoadCancel = nil
|
||||
}
|
||||
if img, ok := ag.imageFetcher.GetAlbumThumbnailFromCache(album.CoverArt); ok {
|
||||
ac.Cover.SetImage(img)
|
||||
} else {
|
||||
ac.Cover.SetImageResource(res.ResAlbumplaceholderPng)
|
||||
// asynchronously fetch cover image
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
ac.ImgLoadCancel = cancel
|
||||
go func(ctx context.Context) {
|
||||
i, err := ag.imageFetcher.GetAlbumThumbnail(album.CoverArt)
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
default:
|
||||
if err == nil {
|
||||
ac.Cover.SetImage(i)
|
||||
} else {
|
||||
log.Printf("error fetching image: %s", err.Error())
|
||||
}
|
||||
}
|
||||
}(ctx)
|
||||
}
|
||||
|
||||
// if user has scrolled near the bottom, fetch more
|
||||
if !ag.done && !ag.fetching && albumIdx > ag.lenAlbums()-10 {
|
||||
ag.fetchMoreAlbums(20)
|
||||
}
|
||||
}
|
||||
|
||||
func (a *AlbumGrid) lenAlbums() int {
|
||||
a.albumsMutex.RLock()
|
||||
defer a.albumsMutex.RUnlock()
|
||||
return len(a.albums)
|
||||
}
|
||||
|
||||
// fetches at least count more albums
|
||||
func (a *AlbumGrid) fetchMoreAlbums(count int) {
|
||||
if a.iter == nil {
|
||||
a.done = true
|
||||
}
|
||||
a.fetching = true
|
||||
go func() {
|
||||
// keep repeating the fetch task as long as the user
|
||||
// has scrolled near the bottom
|
||||
for !a.done && a.highestShown >= a.lenAlbums()-10 {
|
||||
n := 0
|
||||
for !a.done && n < count {
|
||||
albums := a.iter.NextN(albumFetchBatchSize)
|
||||
a.albumsMutex.Lock()
|
||||
a.albums = append(a.albums, albums...)
|
||||
a.albumsMutex.Unlock()
|
||||
if len(albums) < albumFetchBatchSize {
|
||||
a.done = true
|
||||
}
|
||||
n += len(albums)
|
||||
if len(albums) > 0 {
|
||||
a.Refresh()
|
||||
}
|
||||
}
|
||||
}
|
||||
a.fetching = false
|
||||
}()
|
||||
}
|
||||
|
||||
func (a *AlbumGrid) CreateRenderer() fyne.WidgetRenderer {
|
||||
return widget.NewSimpleRenderer(a.grid)
|
||||
}
|
||||
@@ -0,0 +1,263 @@
|
||||
package widgets
|
||||
|
||||
import (
|
||||
"context"
|
||||
"image"
|
||||
"log"
|
||||
"supersonic/backend"
|
||||
"supersonic/res"
|
||||
"supersonic/sharedutil"
|
||||
"sync"
|
||||
|
||||
"fyne.io/fyne/v2"
|
||||
"fyne.io/fyne/v2/widget"
|
||||
"github.com/dweymouth/go-subsonic/subsonic"
|
||||
)
|
||||
|
||||
const batchFetchSize = 6
|
||||
|
||||
type ImageFetcher interface {
|
||||
GetCoverThumbnailFromCache(string) (image.Image, bool)
|
||||
GetCoverThumbnail(string) (image.Image, error)
|
||||
}
|
||||
|
||||
type GridViewIterator interface {
|
||||
NextN(int) []GridViewItemModel
|
||||
}
|
||||
|
||||
type gridViewAlbumIterator struct {
|
||||
iter *backend.BatchingIterator
|
||||
}
|
||||
|
||||
func (g gridViewAlbumIterator) NextN(n int) []GridViewItemModel {
|
||||
albums := g.iter.NextN(n)
|
||||
return sharedutil.MapSlice(albums, func(al *subsonic.AlbumID3) GridViewItemModel {
|
||||
return GridViewItemModel{
|
||||
Name: al.Name,
|
||||
ID: al.ID,
|
||||
CoverArtID: al.CoverArt,
|
||||
Secondary: al.Artist,
|
||||
SecondaryID: al.ArtistID,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func NewGridViewAlbumIterator(iter backend.AlbumIterator) GridViewIterator {
|
||||
return gridViewAlbumIterator{iter: backend.NewBatchingIterator(iter)}
|
||||
}
|
||||
|
||||
type GridView struct {
|
||||
widget.BaseWidget
|
||||
|
||||
GridViewState
|
||||
|
||||
grid *widget.GridWrapList
|
||||
}
|
||||
|
||||
type GridViewState struct {
|
||||
items []GridViewItemModel
|
||||
itemsMutex sync.RWMutex
|
||||
iter GridViewIterator
|
||||
highestShown int
|
||||
fetching bool
|
||||
done bool
|
||||
|
||||
imageFetcher ImageFetcher
|
||||
OnPlay func(string)
|
||||
OnShowItemPage func(string)
|
||||
OnShowSecondaryPage func(string)
|
||||
|
||||
scrollPos float32
|
||||
}
|
||||
|
||||
var _ fyne.Widget = (*GridView)(nil)
|
||||
|
||||
func NewFixedGridView(items []GridViewItemModel, fetch ImageFetcher) *GridView {
|
||||
g := &GridView{
|
||||
GridViewState: GridViewState{
|
||||
items: items,
|
||||
done: true,
|
||||
imageFetcher: fetch,
|
||||
},
|
||||
}
|
||||
g.ExtendBaseWidget(g)
|
||||
g.createGridWrapList()
|
||||
return g
|
||||
}
|
||||
|
||||
func NewGridView(iter GridViewIterator, fetch ImageFetcher) *GridView {
|
||||
g := &GridView{
|
||||
GridViewState: GridViewState{
|
||||
iter: iter,
|
||||
imageFetcher: fetch,
|
||||
},
|
||||
}
|
||||
g.ExtendBaseWidget(g)
|
||||
|
||||
g.createGridWrapList()
|
||||
|
||||
// fetch initial items
|
||||
g.fetchMoreItems(36)
|
||||
return g
|
||||
}
|
||||
|
||||
func (g *GridView) SaveToState() GridViewState {
|
||||
s := g.GridViewState
|
||||
s.scrollPos = g.grid.GetScrollOffset()
|
||||
return s
|
||||
}
|
||||
|
||||
func NewGridViewFromState(state GridViewState) *GridView {
|
||||
g := &GridView{GridViewState: state}
|
||||
g.ExtendBaseWidget(g)
|
||||
g.createGridWrapList()
|
||||
g.Refresh() // needed to initialize the widget
|
||||
g.grid.ScrollToOffset(state.scrollPos)
|
||||
return g
|
||||
}
|
||||
|
||||
func (g *GridView) Clear() {
|
||||
g.itemsMutex.Lock()
|
||||
defer g.itemsMutex.Unlock()
|
||||
g.items = nil
|
||||
g.done = true
|
||||
}
|
||||
|
||||
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 = 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 {
|
||||
return g.lenItems()
|
||||
},
|
||||
// create func
|
||||
func() fyne.CanvasObject {
|
||||
card := NewGridViewItem()
|
||||
card.OnPlay = func() {
|
||||
if g.OnPlay != nil {
|
||||
g.OnPlay(card.ItemID())
|
||||
}
|
||||
}
|
||||
card.OnShowSecondaryPage = func() {
|
||||
if g.OnShowSecondaryPage != nil {
|
||||
g.OnShowSecondaryPage(card.SecondaryID())
|
||||
}
|
||||
}
|
||||
card.OnShowItemPage = func() {
|
||||
if g.OnShowItemPage != nil {
|
||||
g.OnShowItemPage(card.ItemID())
|
||||
}
|
||||
}
|
||||
return card
|
||||
},
|
||||
// update func
|
||||
func(itemID int, obj fyne.CanvasObject) {
|
||||
ac := obj.(*GridViewItem)
|
||||
g.doUpdateItemCard(itemID, ac)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func (g *GridView) doUpdateItemCard(itemIdx int, card *GridViewItem) {
|
||||
if itemIdx > g.highestShown {
|
||||
g.highestShown = itemIdx
|
||||
}
|
||||
g.itemsMutex.RLock()
|
||||
item := g.items[itemIdx]
|
||||
g.itemsMutex.RUnlock()
|
||||
if card.PrevID == item.ID {
|
||||
// nothing to do
|
||||
return
|
||||
}
|
||||
card.Update(item)
|
||||
card.PrevID = item.ID
|
||||
// cancel any previous image fetch
|
||||
if card.ImgLoadCancel != nil {
|
||||
card.ImgLoadCancel()
|
||||
card.ImgLoadCancel = nil
|
||||
}
|
||||
if img, ok := g.imageFetcher.GetCoverThumbnailFromCache(item.CoverArtID); ok {
|
||||
card.Cover.SetImage(img)
|
||||
} else {
|
||||
card.Cover.SetImageResource(res.ResAlbumplaceholderPng)
|
||||
// asynchronously fetch cover image
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
card.ImgLoadCancel = cancel
|
||||
go func(ctx context.Context) {
|
||||
i, err := g.imageFetcher.GetCoverThumbnail(item.CoverArtID)
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
default:
|
||||
if err == nil {
|
||||
card.Cover.SetImage(i)
|
||||
} else {
|
||||
log.Printf("error fetching image: %s", err.Error())
|
||||
}
|
||||
}
|
||||
}(ctx)
|
||||
}
|
||||
|
||||
// if user has scrolled near the bottom, fetch more
|
||||
if !g.done && !g.fetching && itemIdx > g.lenItems()-10 {
|
||||
g.fetchMoreItems(20)
|
||||
}
|
||||
}
|
||||
|
||||
func (g *GridView) lenItems() int {
|
||||
g.itemsMutex.RLock()
|
||||
defer g.itemsMutex.RUnlock()
|
||||
return len(g.items)
|
||||
}
|
||||
|
||||
// fetches at least count more items
|
||||
func (g *GridView) fetchMoreItems(count int) {
|
||||
if g.iter == nil {
|
||||
g.done = true
|
||||
}
|
||||
g.fetching = true
|
||||
go func() {
|
||||
// keep repeating the fetch task as long as the user
|
||||
// has scrolled near the bottom
|
||||
for !g.done && g.highestShown >= g.lenItems()-10 {
|
||||
n := 0
|
||||
for !g.done && n < count {
|
||||
items := g.iter.NextN(batchFetchSize)
|
||||
g.itemsMutex.Lock()
|
||||
g.items = append(g.items, items...)
|
||||
g.itemsMutex.Unlock()
|
||||
if len(items) < batchFetchSize {
|
||||
g.done = true
|
||||
}
|
||||
n += len(items)
|
||||
if len(items) > 0 {
|
||||
g.Refresh()
|
||||
}
|
||||
}
|
||||
}
|
||||
g.fetching = false
|
||||
}()
|
||||
}
|
||||
|
||||
func (g *GridView) CreateRenderer() fyne.WidgetRenderer {
|
||||
return widget.NewSimpleRenderer(g.grid)
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
package widgets
|
||||
|
||||
import (
|
||||
"context"
|
||||
"image"
|
||||
|
||||
"supersonic/res"
|
||||
"supersonic/ui/layouts"
|
||||
|
||||
"fyne.io/fyne/v2"
|
||||
"fyne.io/fyne/v2/canvas"
|
||||
"fyne.io/fyne/v2/container"
|
||||
"fyne.io/fyne/v2/driver/desktop"
|
||||
"fyne.io/fyne/v2/widget"
|
||||
)
|
||||
|
||||
var _ fyne.Widget = (*GridViewItem)(nil)
|
||||
|
||||
var _ fyne.Widget = (*coverImage)(nil)
|
||||
var _ fyne.Tappable = (*coverImage)(nil)
|
||||
|
||||
type coverImage struct {
|
||||
widget.BaseWidget
|
||||
|
||||
Im *canvas.Image
|
||||
playbtn *canvas.Image
|
||||
OnDoubleTapped func()
|
||||
OnTapped func()
|
||||
}
|
||||
|
||||
func newCoverImage() *coverImage {
|
||||
c := &coverImage{}
|
||||
c.ExtendBaseWidget(c)
|
||||
c.Im = &canvas.Image{FillMode: canvas.ImageFillContain, ScaleMode: canvas.ImageScaleFastest}
|
||||
c.Im.SetMinSize(fyne.NewSize(200, 200))
|
||||
c.playbtn = &canvas.Image{FillMode: canvas.ImageFillContain, Resource: res.ResPlaybuttonPng}
|
||||
c.playbtn.SetMinSize(fyne.NewSize(60, 60))
|
||||
c.playbtn.Hidden = true
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *coverImage) CreateRenderer() fyne.WidgetRenderer {
|
||||
return widget.NewSimpleRenderer(
|
||||
container.NewMax(c.Im, container.NewCenter(c.playbtn)),
|
||||
)
|
||||
}
|
||||
|
||||
func (c *coverImage) Cursor() desktop.Cursor {
|
||||
return desktop.PointerCursor
|
||||
}
|
||||
|
||||
func (c *coverImage) Tapped(e *fyne.PointEvent) {
|
||||
if isInside(c.center(), c.playbtn.Size().Height/2, e.Position) {
|
||||
if c.OnDoubleTapped != nil {
|
||||
c.OnDoubleTapped()
|
||||
}
|
||||
return
|
||||
}
|
||||
if c.OnTapped != nil {
|
||||
c.OnTapped()
|
||||
}
|
||||
}
|
||||
|
||||
func (a *coverImage) MouseIn(*desktop.MouseEvent) {
|
||||
a.playbtn.Hidden = false
|
||||
a.Refresh()
|
||||
}
|
||||
|
||||
func (a *coverImage) MouseOut() {
|
||||
a.playbtn.Hidden = true
|
||||
a.Refresh()
|
||||
}
|
||||
|
||||
func (a *coverImage) MouseMoved(e *desktop.MouseEvent) {
|
||||
if isInside(a.center(), a.playbtn.MinSize().Height/2, e.Position) {
|
||||
a.playbtn.SetMinSize(fyne.NewSize(65, 65))
|
||||
} else {
|
||||
a.playbtn.SetMinSize(fyne.NewSize(60, 60))
|
||||
}
|
||||
a.Refresh()
|
||||
}
|
||||
|
||||
func (a *coverImage) center() fyne.Position {
|
||||
return fyne.NewPos(a.Size().Width/2, a.Size().Height/2)
|
||||
}
|
||||
|
||||
func (a *coverImage) SetImage(im image.Image) {
|
||||
a.Im.Resource = nil
|
||||
a.Im.Image = im
|
||||
a.Refresh()
|
||||
}
|
||||
|
||||
func (a *coverImage) SetImageResource(res *fyne.StaticResource) {
|
||||
a.Im.Image = nil
|
||||
a.Im.Resource = res
|
||||
a.Refresh()
|
||||
}
|
||||
|
||||
func isInside(origin fyne.Position, radius float32, point fyne.Position) bool {
|
||||
x, y := (point.X - origin.X), (point.Y - origin.Y)
|
||||
return x*x+y*y <= radius*radius
|
||||
}
|
||||
|
||||
type GridViewItemModel struct {
|
||||
Name string
|
||||
ID string
|
||||
CoverArtID string
|
||||
Secondary string
|
||||
SecondaryID string
|
||||
}
|
||||
|
||||
type GridViewItem struct {
|
||||
widget.BaseWidget
|
||||
|
||||
itemID string
|
||||
secondaryID string
|
||||
primaryText *CustomHyperlink
|
||||
secondaryText *CustomHyperlink
|
||||
container *fyne.Container
|
||||
|
||||
// updated by GridView
|
||||
Cover *coverImage
|
||||
|
||||
// these fields are used by GridView to track async update tasks
|
||||
PrevID string
|
||||
ImgLoadCancel context.CancelFunc
|
||||
|
||||
OnPlay func()
|
||||
OnShowItemPage func()
|
||||
OnShowSecondaryPage func()
|
||||
}
|
||||
|
||||
func NewGridViewItem() *GridViewItem {
|
||||
g := &GridViewItem{
|
||||
primaryText: NewCustomHyperlink(),
|
||||
secondaryText: NewCustomHyperlink(),
|
||||
Cover: newCoverImage(),
|
||||
}
|
||||
g.ExtendBaseWidget(g)
|
||||
g.Cover.OnDoubleTapped = func() {
|
||||
if g.OnPlay != nil {
|
||||
g.OnPlay()
|
||||
}
|
||||
}
|
||||
showItemFn := func() {
|
||||
if g.OnShowItemPage != nil {
|
||||
g.OnShowItemPage()
|
||||
}
|
||||
}
|
||||
g.Cover.OnTapped = showItemFn
|
||||
g.primaryText.OnTapped = showItemFn
|
||||
g.secondaryText.OnTapped = func() {
|
||||
if g.OnShowSecondaryPage != nil {
|
||||
g.OnShowSecondaryPage()
|
||||
}
|
||||
}
|
||||
|
||||
g.createContainer()
|
||||
return g
|
||||
}
|
||||
|
||||
func (g *GridViewItem) createContainer() {
|
||||
info := container.New(&layouts.VboxCustomPadding{ExtraPad: -16}, g.primaryText, g.secondaryText)
|
||||
c := container.New(&layouts.VboxCustomPadding{ExtraPad: -5}, g.Cover, info)
|
||||
pad := &layouts.CenterPadLayout{PadLeftRight: 20, PadTopBottom: 10}
|
||||
g.container = container.New(pad, c)
|
||||
}
|
||||
|
||||
func (g *GridViewItem) Update(model GridViewItemModel) {
|
||||
g.itemID = model.ID
|
||||
g.secondaryID = model.SecondaryID
|
||||
g.primaryText.SetText(model.Name)
|
||||
g.secondaryText.SetText(model.Secondary)
|
||||
g.secondaryText.Disabled = model.SecondaryID == ""
|
||||
g.secondaryText.Refresh()
|
||||
g.Cover.playbtn.Hidden = true
|
||||
}
|
||||
|
||||
func (g *GridViewItem) ItemID() string {
|
||||
return g.itemID
|
||||
}
|
||||
|
||||
func (g *GridViewItem) SecondaryID() string {
|
||||
return g.secondaryID
|
||||
}
|
||||
|
||||
func (g *GridViewItem) CreateRenderer() fyne.WidgetRenderer {
|
||||
return widget.NewSimpleRenderer(g.container)
|
||||
}
|
||||
Reference in New Issue
Block a user