diff --git a/backend/config.go b/backend/config.go index a0a8a4d..d7c0fd3 100644 --- a/backend/config.go +++ b/backend/config.go @@ -70,7 +70,8 @@ type AlbumPageConfig struct { type AlbumsPageConfig struct { SortOrder string // only relevant for Albums page ShowYears bool - ShuffleMode string // only relevant for genre page + ShuffleMode string // only relevant for Genre page + PlayInOrder bool // only relevant for Albums page } type ArtistPageConfig struct { diff --git a/res/translations/en.json b/res/translations/en.json index a5bc43c..73fd979 100644 --- a/res/translations/en.json +++ b/res/translations/en.json @@ -108,6 +108,7 @@ "Home Page": "Home Page", "hr": "hr", "hrs": "hrs", + "In order": "In order", "Internet Radio Stations": "Internet Radio Stations", "Interview": "Interview", "Is favorite": "Is favorite", @@ -202,6 +203,7 @@ "Show play queue": "Show play queue", "Show year in album grid cards": "Show year in album grid cards", "Shuffle": "Shuffle", + "Shuffled": "Shuffled", "Shuffle albums": "Shuffle albums", "Shuffle tracks": "Shuffle tracks", "Similar artists": "Similar artists", @@ -240,6 +242,7 @@ "tracks": "tracks", "Transcode to": "Transcode to", "UI Scaling": "UI Scaling", + "Unable to play albums": "Unable to play albums", "Unable to play artist radio": "Unable to play artist radio", "Unable to play random tracks": "Unable to play random tracks", "URL": "URL", @@ -263,6 +266,7 @@ "Set favorite": "Set favorite", "Unset favorite": "Unset favorite", "Remove from queue": "Remove from queue", + "Play albums": "Play albums", "Play song radio": "Play song radio", "to": "to", "by": "by", diff --git a/ui/browsing/albumspage.go b/ui/browsing/albumspage.go index 1be958d..4c8dc6b 100644 --- a/ui/browsing/albumspage.go +++ b/ui/browsing/albumspage.go @@ -6,7 +6,7 @@ import ( "fyne.io/fyne/v2" "fyne.io/fyne/v2/lang" - "fyne.io/fyne/v2/widget" + "fyne.io/fyne/v2/theme" "github.com/dweymouth/supersonic/backend" "github.com/dweymouth/supersonic/backend/mediaprovider" "github.com/dweymouth/supersonic/ui/controller" @@ -22,6 +22,9 @@ type albumsPageAdapter struct { pm *backend.PlaybackManager filter mediaprovider.AlbumFilter filterBtn *widgets.AlbumFilterButton + + // dependency injected from the GridViewPage + itemsFn func() []widgets.GridViewItemModel } func NewAlbumsPage(cfg *backend.AlbumsPageConfig, pool *util.WidgetPool, contr *controller.Controller, pm *backend.PlaybackManager, mp mediaprovider.MediaProvider, im *backend.ImageManager) Page { @@ -29,6 +32,12 @@ func NewAlbumsPage(cfg *backend.AlbumsPageConfig, pool *util.WidgetPool, contr * return NewGridViewPage(adapter, pool, mp, im) } +var _ GridViewPageAdapterGetItems = (*albumsPageAdapter)(nil) + +func (a *albumsPageAdapter) SetItemsFunc(f func() []widgets.GridViewItemModel) { + a.itemsFn = f +} + func (a *albumsPageAdapter) Title() string { return lang.L("Albums") } func (a *albumsPageAdapter) Filter() mediaprovider.AlbumFilter { @@ -66,7 +75,7 @@ func (a *albumsPageAdapter) SaveSortOrder(orderIdx int) { } func (a *albumsPageAdapter) ActionButton() fyne.CanvasObject { - fn := func() { + shuffleAlbumsFn := func() { go func() { if err := a.pm.PlayRandomAlbums(""); err != nil { log.Printf("error playing random albums: %v", err) @@ -77,7 +86,66 @@ func (a *albumsPageAdapter) ActionButton() fyne.CanvasObject { }() } - return widget.NewButtonWithIcon(lang.L("Play random"), myTheme.ShuffleIcon, fn) + playAlbumsFn := func() { + if a.itemsFn == nil { + a.contr.ToastProvider.ShowErrorToast(lang.L("Unable to play albums")) + } + + go func() { + for i, item := range a.itemsFn() { + if i >= 20 { + break // don't load more than first 20 albums + } + if i == 0 { + a.pm.LoadAlbum(item.ID, backend.Replace, false) + a.pm.PlayFromBeginning() + } else { + a.pm.LoadAlbum(item.ID, backend.Append, false) + } + } + }() + } + + var btn *widgets.OptionButton + var inOrder, shuffled *fyne.MenuItem + inOrder = fyne.NewMenuItem(lang.L("In order"), func() { + shuffled.Checked = false + inOrder.Checked = true + a.cfg.PlayInOrder = true + btn.Text = lang.L("Play albums") + btn.Icon = theme.MediaPlayIcon() + btn.Refresh() + }) + inOrder.Icon = myTheme.AlbumIcon + shuffled = fyne.NewMenuItem(lang.L("Shuffled"), func() { + inOrder.Checked = false + shuffled.Checked = true + a.cfg.PlayInOrder = false + btn.Text = lang.L("Shuffle albums") + btn.Icon = myTheme.ShuffleIcon + btn.Refresh() + }) + shuffled.Icon = myTheme.ShuffleIcon + shuffled.Checked = !a.cfg.PlayInOrder + inOrder.Checked = a.cfg.PlayInOrder + + menu := fyne.NewMenu("", inOrder, shuffled) + icon := myTheme.ShuffleIcon + textKey := "Shuffle albums" + if a.cfg.PlayInOrder { + textKey = "Play albums" + icon = theme.MediaPlayIcon() + } + btn = widgets.NewOptionButton(lang.L(textKey), menu, func() { + if a.cfg.PlayInOrder { + playAlbumsFn() + } else { + shuffleAlbumsFn() + } + }) + btn.Icon = icon + + return btn } func (a *albumsPageAdapter) Iter(sortOrderIdx int, filter mediaprovider.AlbumFilter) widgets.GridViewIterator { diff --git a/ui/browsing/gridviewpage.go b/ui/browsing/gridviewpage.go index 124f970..d32de7c 100644 --- a/ui/browsing/gridviewpage.go +++ b/ui/browsing/gridviewpage.go @@ -76,6 +76,13 @@ type GridViewPageAdapter[M, F any] interface { RefreshGrid(*widgets.GridView) } +type GridViewPageAdapterGetItems interface { + // Optionally allows the GridViewPage to inject a function that + // can be used to retrieve the GridViewItemModels for the + // items currently loaded into the GridView. + SetItemsFunc(func() []widgets.GridViewItemModel) +} + type SortableGridViewPageAdapter interface { // Returns the list of sort orders // and the index of the initially selected sort order @@ -113,6 +120,12 @@ func NewGridViewPage[M, F any]( } gp.grid.DisableSharing = !canShare adapter.InitGrid(gp.grid) + + // If adapter supports SetItemsFunc, call it to inject the items dependency + if plfSetter, ok := adapter.(GridViewPageAdapterGetItems); ok { + plfSetter.SetItemsFunc(gp.grid.Items) + } + gp.createSearchAndFilter() gp.createContainer() return gp diff --git a/ui/widgets/gridview.go b/ui/widgets/gridview.go index cd6495c..03142d4 100644 --- a/ui/widgets/gridview.go +++ b/ui/widgets/gridview.go @@ -198,6 +198,16 @@ func NewGridView(iter GridViewIterator, fetch util.ImageFetcher, placeholder fyn return g } +// Items returns the items that are currently loaded into the GridView state. +func (g *GridView) Items() []GridViewItemModel { + g.stateMutex.Lock() + defer g.stateMutex.Unlock() + + items := make([]GridViewItemModel, len(g.items)) + copy(items, g.items) + return items +} + func (g *GridView) SaveToState() *GridViewState { g.stateMutex.RLock() s := g.GridViewState