From 4fce23fb4b3541d6b52135e607aa766c6da59f12 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Sat, 21 Jun 2025 12:29:03 -0700 Subject: [PATCH 01/11] begining of GroupedReleases widget --- ui/widgets/groupedreleases.go | 139 ++++++++++++++++++++++++++++++++++ 1 file changed, 139 insertions(+) create mode 100644 ui/widgets/groupedreleases.go diff --git a/ui/widgets/groupedreleases.go b/ui/widgets/groupedreleases.go new file mode 100644 index 0000000..2c1919e --- /dev/null +++ b/ui/widgets/groupedreleases.go @@ -0,0 +1,139 @@ +package widgets + +import ( + "sync" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/container" + "fyne.io/fyne/v2/lang" + "fyne.io/fyne/v2/theme" + "fyne.io/fyne/v2/widget" + "github.com/dweymouth/supersonic/backend" + myTheme "github.com/dweymouth/supersonic/ui/theme" + "github.com/dweymouth/supersonic/ui/util" +) + +type GroupedReleasesModel struct { + Albums []*GridViewItemModel + Compilations []*GridViewItemModel + EPs []*GridViewItemModel + Singles []*GridViewItemModel +} + +type GroupedReleases struct { + widget.BaseWidget + + Model GroupedReleasesModel + + OnPlay func(id string, shuffle bool) + OnPlayNext func(id string) + OnAddToQueue func(id string) + OnAddToPlaylist func(id string) + OnFavorite func(id string, fav bool) + OnDownload func(id string) + OnShare func(id string) + OnShowItemPage func(id string) + OnShowSecondaryPage func(id string) + + menu *widget.PopUpMenu + shareMenuItem *fyne.MenuItem + menuGridViewItemId string + imageFetcher util.ImageFetcher + cardPool sync.Pool + + content *fyne.Container +} + +func NewGroupedReleases(model GroupedReleasesModel, fetch util.ImageFetcher) *GroupedReleases { + g := &GroupedReleases{ + Model: model, + imageFetcher: fetch, + } + g.cardPool.New = func() any { return g.createNewItemCard() } + g.ExtendBaseWidget(g) + g.content = container.NewVBox() + return g +} + +func (g *GroupedReleases) CreateRenderer() fyne.WidgetRenderer { + return widget.NewSimpleRenderer(g.content) +} + +func (g *GroupedReleases) Refresh() { + +} + +func (g *GroupedReleases) createNewItemCard() fyne.CanvasObject { + card := NewGridViewItem(myTheme.AlbumIcon) + card.SetSize(backend.AppInstance().Config.GridView.CardSize) + card.ItemIndex = -1 + card.ImgLoader = util.NewThumbnailLoader(g.imageFetcher, card.Cover.SetImage) + card.ImgLoader.OnBeforeLoad = func() { card.Cover.SetImage(nil) } + card.OnPlay = func() { g.onPlay(card.ItemID(), false) } + card.OnFavorite = func(fav bool) { + if g.OnFavorite != nil { + g.OnFavorite(card.itemID, fav) + } + } + card.OnShowSecondaryPage = func(id string) { + if g.OnShowSecondaryPage != nil { + g.OnShowSecondaryPage(id) + } + } + card.OnShowItemPage = func() { + if g.OnShowItemPage != nil { + g.OnShowItemPage(card.ItemID()) + } + } + card.OnShowContextMenu = func(p fyne.Position) { + g.showContextMenu(card, p) + } + return card +} + +func (g *GroupedReleases) showContextMenu(card *GridViewItem, pos fyne.Position) { + g.menuGridViewItemId = card.ItemID() + if g.menu == nil { + play := fyne.NewMenuItem(lang.L("Play"), func() { g.onPlay(g.menuGridViewItemId, false) }) + play.Icon = theme.MediaPlayIcon() + shuffle := fyne.NewMenuItem(lang.L("Shuffle"), func() { g.onPlay(g.menuGridViewItemId, true) }) + shuffle.Icon = myTheme.ShuffleIcon + queueNext := fyne.NewMenuItem(lang.L("Play next"), func() { + if g.OnPlayNext != nil { + g.OnPlayNext(g.menuGridViewItemId) + } + }) + queueNext.Icon = myTheme.PlayNextIcon + queue := fyne.NewMenuItem(lang.L("Add to queue"), func() { + if g.OnAddToQueue != nil { + g.OnAddToQueue(g.menuGridViewItemId) + } + }) + queue.Icon = theme.ContentAddIcon() + playlist := fyne.NewMenuItem(lang.L("Add to playlist")+"...", func() { + if g.OnAddToPlaylist != nil { + g.OnAddToPlaylist(g.menuGridViewItemId) + } + }) + playlist.Icon = myTheme.PlaylistIcon + download := fyne.NewMenuItem(lang.L("Download")+"...", func() { + if g.OnDownload != nil { + g.OnDownload(g.menuGridViewItemId) + } + }) + download.Icon = theme.DownloadIcon() + g.shareMenuItem = fyne.NewMenuItem(lang.L("Share")+"...", func() { + g.OnShare(g.menuGridViewItemId) + }) + g.shareMenuItem.Icon = myTheme.ShareIcon + g.menu = widget.NewPopUpMenu(fyne.NewMenu("", play, shuffle, queueNext, queue, playlist, download, g.shareMenuItem), + fyne.CurrentApp().Driver().CanvasForObject(g)) + } + g.menu.ShowAtPosition(pos) +} + +func (g *GroupedReleases) onPlay(itemID string, shuffle bool) { + if g.OnPlay != nil { + g.OnPlay(itemID, shuffle) + } +} From f6f82a73bb675a679e485407173940625ffeed15 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Tue, 1 Jul 2025 16:46:05 -0700 Subject: [PATCH 02/11] more work on GroupedReleases widget --- ui/widgets/gridview.go | 2 +- ui/widgets/gridviewitem.go | 2 +- ui/widgets/groupedreleases.go | 69 +++++++++++++++++++++++++++++++++-- 3 files changed, 68 insertions(+), 5 deletions(-) diff --git a/ui/widgets/gridview.go b/ui/widgets/gridview.go index 5f502b1..cd6495c 100644 --- a/ui/widgets/gridview.go +++ b/ui/widgets/gridview.go @@ -362,7 +362,7 @@ func (g *GridView) doUpdateItemCard(itemIdx int, card *GridViewItem) { card.ShowSuffix = g.ShowSuffix card.Cover.Im.PlaceholderIcon = g.Placeholder - if !card.NeedsUpdate(item) { + if !card.NeedsUpdate(&item) { // nothing to do return } diff --git a/ui/widgets/gridviewitem.go b/ui/widgets/gridviewitem.go index 9559fd4..f8911d9 100644 --- a/ui/widgets/gridviewitem.go +++ b/ui/widgets/gridviewitem.go @@ -490,7 +490,7 @@ func NewGridViewItem(placeholderResource fyne.Resource) *GridViewItem { return g } -func (g *GridViewItem) NeedsUpdate(model GridViewItemModel) bool { +func (g *GridViewItem) NeedsUpdate(model *GridViewItemModel) bool { return g.itemID != model.ID || !slices.Equal(g.secondaryIDs, model.SecondaryIDs) || (g.ShowSuffix && g.secondaryText.Suffix != model.Suffix) || (!g.ShowSuffix && g.secondaryText.Suffix != "") diff --git a/ui/widgets/groupedreleases.go b/ui/widgets/groupedreleases.go index 2c1919e..d740929 100644 --- a/ui/widgets/groupedreleases.go +++ b/ui/widgets/groupedreleases.go @@ -23,6 +23,8 @@ type GroupedReleasesModel struct { type GroupedReleases struct { widget.BaseWidget + ShowSuffix bool + Model GroupedReleasesModel OnPlay func(id string, shuffle bool) @@ -41,7 +43,12 @@ type GroupedReleases struct { imageFetcher util.ImageFetcher cardPool sync.Pool - content *fyne.Container + sections [4]groupedReleasesSection +} + +type groupedReleasesSection struct { + title *widget.Label + container *fyne.Container } func NewGroupedReleases(model GroupedReleasesModel, fetch util.ImageFetcher) *GroupedReleases { @@ -51,16 +58,61 @@ func NewGroupedReleases(model GroupedReleasesModel, fetch util.ImageFetcher) *Gr } g.cardPool.New = func() any { return g.createNewItemCard() } g.ExtendBaseWidget(g) - g.content = container.NewVBox() + + cardSize := fyne.NewSquareSize(backend.AppInstance().Config.GridView.CardSize) + sections := []string{lang.L("Albums"), lang.L("Compilations"), lang.L("EPs"), lang.L("Singles")} + for i, s := range sections { + g.sections[i].title = widget.NewLabelWithStyle(s, fyne.TextAlignLeading, fyne.TextStyle{Bold: true}) + g.sections[i].container = container.NewGridWrap(cardSize) + } + return g } func (g *GroupedReleases) CreateRenderer() fyne.WidgetRenderer { - return widget.NewSimpleRenderer(g.content) + vbox := container.NewVBox() + for i := range g.sections { + vbox.Add(g.sections[i].title) + vbox.Add(g.sections[i].container) + } + return widget.NewSimpleRenderer(container.NewVScroll(vbox)) } func (g *GroupedReleases) Refresh() { + sectionItems := [4][]*GridViewItemModel{g.Model.Albums, g.Model.Compilations, g.Model.EPs, g.Model.Singles} + for i, items := range sectionItems { + lenItems := len(items) + objects := g.sections[i].container.Objects + // clear out excess cards in this section + for x := lenItems; x < len(objects); x++ { + g.cardPool.Put(objects[x]) + objects[x] = nil + } + if lenItems > len(objects) { + objects = objects[:lenItems] + } + // update existing cards + for x := 0; x < len(objects); x++ { + g.doUpdateItemCard(objects[x].(*GridViewItem), items[x]) + } + // append new ones as needed + for x := len(objects); x < lenItems; x++ { + card := g.cardPool.Get().(*GridViewItem) + g.doUpdateItemCard(card, items[x]) + } + + g.sections[i].container.Objects = objects + // if section has no albums in it, hide + if lenItems == 0 { + g.sections[i].title.Hide() + g.sections[i].container.Hide() + } else { + g.sections[i].title.Show() + g.sections[i].container.Show() + } + } + g.BaseWidget.Refresh() } func (g *GroupedReleases) createNewItemCard() fyne.CanvasObject { @@ -137,3 +189,14 @@ func (g *GroupedReleases) onPlay(itemID string, shuffle bool) { g.OnPlay(itemID, shuffle) } } + +func (g *GroupedReleases) doUpdateItemCard(card *GridViewItem, model *GridViewItemModel) { + card.ShowSuffix = g.ShowSuffix + if !card.NeedsUpdate(model) { + // nothing to do + return + } + + card.Update(model) + card.ImgLoader.Load(model.CoverArtID) +} From 295d87a3a59465fd10f866d9e417354211d63d15 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Sun, 6 Jul 2025 10:58:46 -0700 Subject: [PATCH 03/11] start updating artist page --- ui/browsing/artistpage.go | 62 ++++++++++++++++++++++++++++------- ui/widgets/groupedreleases.go | 14 ++++---- 2 files changed, 58 insertions(+), 18 deletions(-) diff --git a/ui/browsing/artistpage.go b/ui/browsing/artistpage.go index b5b95c1..8c11948 100644 --- a/ui/browsing/artistpage.go +++ b/ui/browsing/artistpage.go @@ -194,6 +194,54 @@ func (a *ArtistPage) getGridViewAlbumsModel() []widgets.GridViewItemModel { if a.artistInfo == nil { return nil } + + a.sortAlbumsSlices(a.artistInfo.Albums) + return sharedutil.MapSlice(a.artistInfo.Albums, a.albumToGridViewItemModel) +} + +func (a *ArtistPage) albumToGridViewItemModel(al *mediaprovider.Album) widgets.GridViewItemModel { + return widgets.GridViewItemModel{ + Name: al.Name, + ID: al.ID, + CoverArtID: al.CoverArtID, + Secondary: []string{strconv.Itoa(al.YearOrZero())}, + CanFavorite: true, + IsFavorite: al.Favorite, + } +} + +func (a *ArtistPage) getGroupedReleasesModel() widgets.GroupedReleasesModel { + if a.artistInfo == nil { + return widgets.GroupedReleasesModel{} + } + albums := []*mediaprovider.Album{} + compilations := []*mediaprovider.Album{} + eps := []*mediaprovider.Album{} + singles := []*mediaprovider.Album{} + + for _, album := range a.artistInfo.Albums { + switch rt := album.ReleaseTypes; { + case rt&mediaprovider.ReleaseTypeEP > 0: + eps = append(eps, album) + case rt&mediaprovider.ReleaseTypeCompilation > 0: + compilations = append(compilations, album) + case rt&mediaprovider.ReleaseTypeSingle > 0: + singles = append(singles, album) + default: + albums = append(albums, album) + } + } + + a.sortAlbumsSlices(albums, compilations, eps, singles) + return widgets.GroupedReleasesModel{ + Albums: sharedutil.MapSlice(albums, a.albumToGridViewItemModel), + Compilations: sharedutil.MapSlice(compilations, a.albumToGridViewItemModel), + EPs: sharedutil.MapSlice(eps, a.albumToGridViewItemModel), + Singles: sharedutil.MapSlice(singles, a.albumToGridViewItemModel), + } +} + +func (a *ArtistPage) sortAlbumsSlices(slices ...[]*mediaprovider.Album) { sortFunc := func(x, y int) bool { return a.artistInfo.Albums[y].Date.After(a.artistInfo.Albums[x].Date) } @@ -208,17 +256,9 @@ func (a *ArtistPage) getGridViewAlbumsModel() []widgets.GridViewItemModel { } } - sort.Slice(a.artistInfo.Albums, sortFunc) - return sharedutil.MapSlice(a.artistInfo.Albums, func(al *mediaprovider.Album) widgets.GridViewItemModel { - return widgets.GridViewItemModel{ - Name: al.Name, - ID: al.ID, - CoverArtID: al.CoverArtID, - Secondary: []string{strconv.Itoa(al.YearOrZero())}, - CanFavorite: true, - IsFavorite: al.Favorite, - } - }) + for _, slice := range slices { + sort.Slice(slice, sortFunc) + } } // should be called asynchronously diff --git a/ui/widgets/groupedreleases.go b/ui/widgets/groupedreleases.go index d740929..e22f277 100644 --- a/ui/widgets/groupedreleases.go +++ b/ui/widgets/groupedreleases.go @@ -14,10 +14,10 @@ import ( ) type GroupedReleasesModel struct { - Albums []*GridViewItemModel - Compilations []*GridViewItemModel - EPs []*GridViewItemModel - Singles []*GridViewItemModel + Albums []GridViewItemModel + Compilations []GridViewItemModel + EPs []GridViewItemModel + Singles []GridViewItemModel } type GroupedReleases struct { @@ -79,7 +79,7 @@ func (g *GroupedReleases) CreateRenderer() fyne.WidgetRenderer { } func (g *GroupedReleases) Refresh() { - sectionItems := [4][]*GridViewItemModel{g.Model.Albums, g.Model.Compilations, g.Model.EPs, g.Model.Singles} + sectionItems := [4][]GridViewItemModel{g.Model.Albums, g.Model.Compilations, g.Model.EPs, g.Model.Singles} for i, items := range sectionItems { lenItems := len(items) objects := g.sections[i].container.Objects @@ -94,12 +94,12 @@ func (g *GroupedReleases) Refresh() { // update existing cards for x := 0; x < len(objects); x++ { - g.doUpdateItemCard(objects[x].(*GridViewItem), items[x]) + g.doUpdateItemCard(objects[x].(*GridViewItem), &items[x]) } // append new ones as needed for x := len(objects); x < lenItems; x++ { card := g.cardPool.Get().(*GridViewItem) - g.doUpdateItemCard(card, items[x]) + g.doUpdateItemCard(card, &items[x]) } g.sections[i].container.Objects = objects From 6931ccb054a6393f4685c2085506b37ede4d0e05 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Sun, 6 Jul 2025 11:18:37 -0700 Subject: [PATCH 04/11] kind-of works but needs rendering work --- ui/browsing/artistpage.go | 60 ++++++++++++++++++------- ui/controller/connectactions.go | 80 +++++++++++++++++++++++---------- ui/util/widgetpool.go | 1 + ui/widgets/groupedreleases.go | 3 +- 4 files changed, 102 insertions(+), 42 deletions(-) diff --git a/ui/browsing/artistpage.go b/ui/browsing/artistpage.go index 8c11948..44b09b0 100644 --- a/ui/browsing/artistpage.go +++ b/ui/browsing/artistpage.go @@ -49,12 +49,13 @@ type ArtistPage struct { artistInfo *mediaprovider.ArtistWithAlbums - albumGrid *widgets.GridView - tracklistCtr *fyne.Container - sortButton *widgets.SortChooserButton - nowPlayingID string - header *ArtistPageHeader - container *fyne.Container + albumGrid *widgets.GridView + groupedReleases *widgets.GroupedReleases + tracklistCtr *fyne.Container + sortButton *widgets.SortChooserButton + nowPlayingID string + header *ArtistPageHeader + container *fyne.Container } const ( @@ -156,6 +157,10 @@ func (a *ArtistPage) Save() SavedPage { a.albumGrid.Clear() a.pool.Release(util.WidgetTypeGridView, a.albumGrid) } + if a.groupedReleases != nil { + a.groupedReleases.Model = widgets.GroupedReleasesModel{} + a.pool.Release(util.WidgetTypeGroupedReleases, a.groupedReleases) + } return &s } @@ -294,27 +299,48 @@ func (a *ArtistPage) load() { } func (a *ArtistPage) showAlbumGrid(reSort bool) { - if a.albumGrid == nil { + useGroupedReleases := a.artistInfo != nil && len(a.artistInfo.Albums) <= 50 + + if a.albumGrid == nil && a.groupedReleases == nil { if a.artistInfo == nil { // page not loaded yet or invalid artist a.activeView = 0 // if page still loading, will show discography view first return } - model := a.getGridViewAlbumsModel() - if g := a.pool.Obtain(util.WidgetTypeGridView); g != nil { - a.albumGrid = g.(*widgets.GridView) - a.albumGrid.Placeholder = myTheme.AlbumIcon - a.albumGrid.ResetFixed(model) + if useGroupedReleases { + model := a.getGroupedReleasesModel() + if g := a.pool.Obtain(util.WidgetTypeGroupedReleases); g != nil { + a.groupedReleases = g.(*widgets.GroupedReleases) + a.groupedReleases.Model = model + } else { + a.groupedReleases = widgets.NewGroupedReleases(model, a.im) + } + a.contr.ConnectGroupedReleasesActions(a.groupedReleases) } else { - a.albumGrid = widgets.NewFixedGridView(model, a.im, myTheme.AlbumIcon) + model := a.getGridViewAlbumsModel() + if g := a.pool.Obtain(util.WidgetTypeGridView); g != nil { + a.albumGrid = g.(*widgets.GridView) + a.albumGrid.Placeholder = myTheme.AlbumIcon + a.albumGrid.ResetFixed(model) + } else { + a.albumGrid = widgets.NewFixedGridView(model, a.im, myTheme.AlbumIcon) + } + a.contr.ConnectAlbumGridActions(a.albumGrid) } - a.contr.ConnectAlbumGridActions(a.albumGrid) } else if reSort { - model := a.getGridViewAlbumsModel() - a.albumGrid.ResetFixed(model) + if useGroupedReleases { + a.groupedReleases.Model = a.getGroupedReleasesModel() + } else { + model := a.getGridViewAlbumsModel() + a.albumGrid.ResetFixed(model) + } } a.sortButton.Show() - a.container.Objects[0].(*fyne.Container).Objects[0] = a.albumGrid + if useGroupedReleases { + a.container.Objects[0].(*fyne.Container).Objects[0] = a.groupedReleases + } else { + a.container.Objects[0].(*fyne.Container).Objects[0] = a.albumGrid + } a.container.Objects[0].Refresh() } diff --git a/ui/controller/connectactions.go b/ui/controller/connectactions.go index c7927ab..18b561c 100644 --- a/ui/controller/connectactions.go +++ b/ui/controller/connectactions.go @@ -97,35 +97,67 @@ func (m *Controller) ConnectAlbumGridActions(grid *widgets.GridView) { grid.OnShowSecondaryPage = func(artistID string) { m.NavigateTo(ArtistRoute(artistID)) } - grid.OnAddToPlaylist = func(albumID string) { - go func() { - album, err := m.App.ServerManager.Server.GetAlbum(albumID) - if err != nil { - log.Printf("error loading album: %s", err.Error()) - return - } - fyne.Do(func() { - m.DoAddTracksToPlaylistWorkflow(sharedutil.TracksToIDs(album.Tracks)) - }) - }() - } - grid.OnDownload = func(albumID string) { - go func() { - album, err := m.App.ServerManager.Server.GetAlbum(albumID) - if err != nil { - log.Printf("error loading album: %s", err.Error()) - return - } - fyne.Do(func() { - m.ShowDownloadDialog(album.Tracks, album.Name) - }) - }() - } + grid.OnAddToPlaylist = m.onAddAlbumToPlaylist + grid.OnDownload = m.onDownloadAlbum grid.OnShare = func(albumID string) { m.ShowShareDialog(albumID) } } +func (m *Controller) ConnectGroupedReleasesActions(grid *widgets.GroupedReleases) { + grid.OnAddToQueue = func(albumID string) { + go m.App.PlaybackManager.LoadAlbum(albumID, backend.Append, false) + } + grid.OnPlayNext = func(albumID string) { + go m.App.PlaybackManager.LoadAlbum(albumID, backend.InsertNext, false) + } + grid.OnPlay = func(albumID string, shuffle bool) { + go m.App.PlaybackManager.PlayAlbum(albumID, 0, shuffle) + } + grid.OnFavorite = func(albumID string, favorite bool) { + m.App.ServerManager.Server.SetFavorite(mediaprovider.RatingFavoriteParameters{ + AlbumIDs: []string{albumID}, + }, favorite) + } + grid.OnShowItemPage = func(albumID string) { + m.NavigateTo(AlbumRoute(albumID)) + } + grid.OnShowSecondaryPage = func(artistID string) { + m.NavigateTo(ArtistRoute(artistID)) + } + grid.OnAddToPlaylist = m.onAddAlbumToPlaylist + grid.OnDownload = m.onDownloadAlbum + grid.OnShare = func(albumID string) { + m.ShowShareDialog(albumID) + } +} + +func (m *Controller) onAddAlbumToPlaylist(albumID string) { + go func() { + album, err := m.App.ServerManager.Server.GetAlbum(albumID) + if err != nil { + log.Printf("error loading album: %s", err.Error()) + return + } + fyne.Do(func() { + m.DoAddTracksToPlaylistWorkflow(sharedutil.TracksToIDs(album.Tracks)) + }) + }() +} + +func (m *Controller) onDownloadAlbum(albumID string) { + go func() { + album, err := m.App.ServerManager.Server.GetAlbum(albumID) + if err != nil { + log.Printf("error loading album: %s", err.Error()) + return + } + fyne.Do(func() { + m.ShowDownloadDialog(album.Tracks, album.Name) + }) + }() +} + func (m *Controller) ConnectArtistGridActions(grid *widgets.GridView) { grid.OnShowItemPage = func(id string) { m.NavigateTo(ArtistRoute(id)) } grid.OnPlayNext = func(artistID string) { diff --git a/ui/util/widgetpool.go b/ui/util/widgetpool.go index f79d247..d41e69e 100644 --- a/ui/util/widgetpool.go +++ b/ui/util/widgetpool.go @@ -17,6 +17,7 @@ const ( WidgetTypeTracklist WidgetTypeCompactTracklist WidgetTypeNowPlayingPage + WidgetTypeGroupedReleases // keep at bottom numWidgetTypes diff --git a/ui/widgets/groupedreleases.go b/ui/widgets/groupedreleases.go index e22f277..aec0989 100644 --- a/ui/widgets/groupedreleases.go +++ b/ui/widgets/groupedreleases.go @@ -88,7 +88,7 @@ func (g *GroupedReleases) Refresh() { g.cardPool.Put(objects[x]) objects[x] = nil } - if lenItems > len(objects) { + if len(objects) > lenItems { objects = objects[:lenItems] } @@ -100,6 +100,7 @@ func (g *GroupedReleases) Refresh() { for x := len(objects); x < lenItems; x++ { card := g.cardPool.Get().(*GridViewItem) g.doUpdateItemCard(card, &items[x]) + objects = append(objects, card) } g.sections[i].container.Objects = objects From 9177c12f07c8cf5df39894d2aba1e7f0c472f686 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Sun, 6 Jul 2025 13:19:58 -0700 Subject: [PATCH 05/11] looks much better, sorting still off --- ui/browsing/artistpage.go | 1 + ui/widgets/groupedreleases.go | 27 ++++++++++++++++++++++----- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/ui/browsing/artistpage.go b/ui/browsing/artistpage.go index 44b09b0..ab2601d 100644 --- a/ui/browsing/artistpage.go +++ b/ui/browsing/artistpage.go @@ -338,6 +338,7 @@ func (a *ArtistPage) showAlbumGrid(reSort bool) { a.sortButton.Show() if useGroupedReleases { a.container.Objects[0].(*fyne.Container).Objects[0] = a.groupedReleases + a.container.Objects[0].Refresh() } else { a.container.Objects[0].(*fyne.Container).Objects[0] = a.albumGrid } diff --git a/ui/widgets/groupedreleases.go b/ui/widgets/groupedreleases.go index aec0989..b9f49e8 100644 --- a/ui/widgets/groupedreleases.go +++ b/ui/widgets/groupedreleases.go @@ -6,6 +6,7 @@ import ( "fyne.io/fyne/v2" "fyne.io/fyne/v2/container" "fyne.io/fyne/v2/lang" + "fyne.io/fyne/v2/layout" "fyne.io/fyne/v2/theme" "fyne.io/fyne/v2/widget" "github.com/dweymouth/supersonic/backend" @@ -47,7 +48,7 @@ type GroupedReleases struct { } type groupedReleasesSection struct { - title *widget.Label + titleRow fyne.CanvasObject container *fyne.Container } @@ -62,7 +63,9 @@ func NewGroupedReleases(model GroupedReleasesModel, fetch util.ImageFetcher) *Gr cardSize := fyne.NewSquareSize(backend.AppInstance().Config.GridView.CardSize) sections := []string{lang.L("Albums"), lang.L("Compilations"), lang.L("EPs"), lang.L("Singles")} for i, s := range sections { - g.sections[i].title = widget.NewLabelWithStyle(s, fyne.TextAlignLeading, fyne.TextStyle{Bold: true}) + title := widget.NewLabelWithStyle(s, fyne.TextAlignLeading, fyne.TextStyle{Bold: true}) + title.SizeName = theme.SizeNameSubHeadingText + g.sections[i].titleRow = container.NewHBox(util.NewHSpace(8), title) g.sections[i].container = container.NewGridWrap(cardSize) } @@ -72,14 +75,16 @@ func NewGroupedReleases(model GroupedReleasesModel, fetch util.ImageFetcher) *Gr func (g *GroupedReleases) CreateRenderer() fyne.WidgetRenderer { vbox := container.NewVBox() for i := range g.sections { - vbox.Add(g.sections[i].title) + vbox.Add(g.sections[i].titleRow) vbox.Add(g.sections[i].container) } return widget.NewSimpleRenderer(container.NewVScroll(vbox)) } func (g *GroupedReleases) Refresh() { + gridSize := backend.AppInstance().Config.GridView.CardSize sectionItems := [4][]GridViewItemModel{g.Model.Albums, g.Model.Compilations, g.Model.EPs, g.Model.Singles} + layoutSize := fyne.NewSize(0, 0) for i, items := range sectionItems { lenItems := len(items) objects := g.sections[i].container.Objects @@ -95,21 +100,33 @@ func (g *GroupedReleases) Refresh() { // update existing cards for x := 0; x < len(objects); x++ { g.doUpdateItemCard(objects[x].(*GridViewItem), &items[x]) + objects[x].(*GridViewItem).SetSize(gridSize) + if layoutSize.IsZero() { + layoutSize = objects[x].MinSize() + } } // append new ones as needed for x := len(objects); x < lenItems; x++ { card := g.cardPool.Get().(*GridViewItem) + card.SetSize(gridSize) g.doUpdateItemCard(card, &items[x]) objects = append(objects, card) + if layoutSize.IsZero() { + layoutSize = card.MinSize() + } } + l := layout.NewGridWrapLayout(layoutSize) + g.sections[i].container.Layout = l + // needed to initialize the col count so that MinSize is correct + l.Layout(objects, g.Size()) g.sections[i].container.Objects = objects // if section has no albums in it, hide if lenItems == 0 { - g.sections[i].title.Hide() + g.sections[i].titleRow.Hide() g.sections[i].container.Hide() } else { - g.sections[i].title.Show() + g.sections[i].titleRow.Show() g.sections[i].container.Show() } } From 6ed4cd95407436ce467cdd3e38262bd3fb697c79 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Sun, 6 Jul 2025 14:39:10 -0700 Subject: [PATCH 06/11] make ReleaseType sections collapsible --- ui/widgets/groupedreleases.go | 60 ++++++++++++++++++++++++++++++++--- 1 file changed, 56 insertions(+), 4 deletions(-) diff --git a/ui/widgets/groupedreleases.go b/ui/widgets/groupedreleases.go index b9f49e8..6650a3f 100644 --- a/ui/widgets/groupedreleases.go +++ b/ui/widgets/groupedreleases.go @@ -4,6 +4,7 @@ import ( "sync" "fyne.io/fyne/v2" + "fyne.io/fyne/v2/canvas" "fyne.io/fyne/v2/container" "fyne.io/fyne/v2/lang" "fyne.io/fyne/v2/layout" @@ -48,7 +49,7 @@ type GroupedReleases struct { } type groupedReleasesSection struct { - titleRow fyne.CanvasObject + titleRow *groupedReleasesHeader container *fyne.Container } @@ -63,9 +64,15 @@ func NewGroupedReleases(model GroupedReleasesModel, fetch util.ImageFetcher) *Gr cardSize := fyne.NewSquareSize(backend.AppInstance().Config.GridView.CardSize) sections := []string{lang.L("Albums"), lang.L("Compilations"), lang.L("EPs"), lang.L("Singles")} for i, s := range sections { - title := widget.NewLabelWithStyle(s, fyne.TextAlignLeading, fyne.TextStyle{Bold: true}) - title.SizeName = theme.SizeNameSubHeadingText - g.sections[i].titleRow = container.NewHBox(util.NewHSpace(8), title) + _i := i + g.sections[i].titleRow = newGroupedReleasesHeader(s, func(collapse bool) { + if collapse { + g.sections[_i].container.Hide() + } else { + g.sections[_i].container.Show() + } + canvas.Refresh(g) + }) g.sections[i].container = container.NewGridWrap(cardSize) } @@ -218,3 +225,48 @@ func (g *GroupedReleases) doUpdateItemCard(card *GridViewItem, model *GridViewIt card.Update(model) card.ImgLoader.Load(model.CoverArtID) } + +type groupedReleasesHeader struct { + widget.BaseWidget + + title string + onToggle func(bool) + + icon *widget.Icon + collapsed bool +} + +func newGroupedReleasesHeader(title string, onToggleVisibility func(bool)) *groupedReleasesHeader { + g := &groupedReleasesHeader{ + title: title, + icon: widget.NewIcon(theme.MenuDropDownIcon()), + onToggle: onToggleVisibility, + } + g.ExtendBaseWidget(g) + return g +} + +func (g *groupedReleasesHeader) CreateRenderer() fyne.WidgetRenderer { + titleText := widget.NewLabelWithStyle(g.title, fyne.TextAlignLeading, fyne.TextStyle{Bold: true}) + titleText.SizeName = theme.SizeNameSubHeadingText + return widget.NewSimpleRenderer( + container.NewHBox(util.NewHSpace(8), titleText, util.NewHSpace(-theme.Padding()*2), g.icon), + ) +} + +func (g *groupedReleasesHeader) Refresh() { + if g.collapsed { + g.icon.Resource = theme.MenuExpandIcon() + } else { + g.icon.Resource = theme.MenuDropDownIcon() + } + g.BaseWidget.Refresh() +} + +var _ fyne.Tappable = (*groupedReleasesHeader)(nil) + +func (g *groupedReleasesHeader) Tapped(*fyne.PointEvent) { + g.collapsed = !g.collapsed + g.onToggle(g.collapsed) + g.Refresh() +} From 6c2dfc9edc7c1fb5b23423e3091155d1def168d0 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Sun, 6 Jul 2025 14:56:43 -0700 Subject: [PATCH 07/11] don't use GroupedReleases if all artist's releases are albums --- ui/browsing/artistpage.go | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/ui/browsing/artistpage.go b/ui/browsing/artistpage.go index ab2601d..623dca5 100644 --- a/ui/browsing/artistpage.go +++ b/ui/browsing/artistpage.go @@ -2,6 +2,7 @@ package browsing import ( "log" + "slices" "sort" "strconv" @@ -299,7 +300,14 @@ func (a *ArtistPage) load() { } func (a *ArtistPage) showAlbumGrid(reSort bool) { - useGroupedReleases := a.artistInfo != nil && len(a.artistInfo.Albums) <= 50 + allAlbums := func() bool { + return slices.IndexFunc(a.artistInfo.Albums, func(al *mediaprovider.Album) bool { + return al.ReleaseTypes&mediaprovider.ReleaseTypeCompilation > 0 || + al.ReleaseTypes&mediaprovider.ReleaseTypeEP > 0 || + al.ReleaseTypes&mediaprovider.ReleaseTypeSingle > 0 + }) < 0 + } + useGroupedReleases := a.artistInfo != nil && len(a.artistInfo.Albums) <= 50 && !allAlbums() if a.albumGrid == nil && a.groupedReleases == nil { if a.artistInfo == nil { From 103a79668c6fc6a98c2b1efd50e7342896e4c0aa Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Sun, 6 Jul 2025 15:11:37 -0700 Subject: [PATCH 08/11] fix sorting --- ui/browsing/artistpage.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/ui/browsing/artistpage.go b/ui/browsing/artistpage.go index 623dca5..187e933 100644 --- a/ui/browsing/artistpage.go +++ b/ui/browsing/artistpage.go @@ -248,21 +248,23 @@ func (a *ArtistPage) getGroupedReleasesModel() widgets.GroupedReleasesModel { } func (a *ArtistPage) sortAlbumsSlices(slices ...[]*mediaprovider.Album) { + var curSlice []*mediaprovider.Album sortFunc := func(x, y int) bool { - return a.artistInfo.Albums[y].Date.After(a.artistInfo.Albums[x].Date) + return curSlice[y].Date.After(curSlice[x].Date) } switch a.cfg.DiscographySort { case discographySorts[1]: /*year descending*/ sortFunc = func(x, y int) bool { - return a.artistInfo.Albums[x].Date.After(a.artistInfo.Albums[y].Date) + return curSlice[x].Date.After(curSlice[y].Date) } case discographySorts[2]: /*name*/ sortFunc = func(x, y int) bool { - return a.artistInfo.Albums[x].Name < a.artistInfo.Albums[y].Name + return curSlice[x].Name < curSlice[y].Name } } for _, slice := range slices { + curSlice = slice sort.Slice(slice, sortFunc) } } From 02e7caec844d5ed3f0712424bfa6ff391e5bff95 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Sun, 6 Jul 2025 15:31:53 -0700 Subject: [PATCH 09/11] update translations for plurals of release types --- res/translations/de.json | 3 +++ res/translations/en.json | 3 +++ res/translations/es.json | 3 +++ res/translations/fr.json | 3 +++ res/translations/it.json | 3 +++ res/translations/ja.json | 3 +++ res/translations/ko.json | 3 +++ res/translations/nl.json | 3 +++ res/translations/pl.json | 3 +++ res/translations/pt_BR.json | 2 ++ res/translations/ro.json | 3 +++ res/translations/ru.json | 3 +++ res/translations/zh.json | 3 +++ res/translations/zhHans.json | 3 +++ res/translations/zhHant.json | 3 +++ 15 files changed, 44 insertions(+) diff --git a/res/translations/de.json b/res/translations/de.json index ff95f13..56a074f 100644 --- a/res/translations/de.json +++ b/res/translations/de.json @@ -38,6 +38,7 @@ "Close to system tray": "In Taskleiste minimieren", "Comment": "Kommentar", "Compilation": "Compilation", + "Compilations": "Compilations", "Composer": "Komponist", "Configure your music server to add radio stations": "Konfigurieren Sie Ihren Musik-Server, um Radiosender hinzuzufügen", "Confirm Delete Playlist": "Playlist löschen bestätigen", @@ -68,6 +69,7 @@ "Enabled": "Aktiviert", "Enter": "Eingeben", "EP": "EP", + "EPs": "EPs", "Equalizer": "Equalizer", "Exclusive mode": "Exklusiver Modus", "Favorites": "Favoriten", @@ -178,6 +180,7 @@ "Shuffle tracks": "Titel zufällig wiedergeben", "Similar artists": "Ähnliche Künstler", "Single": "Single", + "Singles": "Singles", "Size": "Größe", "Skip duplicate tracks": "Duplikate überspringen", "Skip this version": "Diese Version überspringen", diff --git a/res/translations/en.json b/res/translations/en.json index 4b182a5..e72fe8f 100644 --- a/res/translations/en.json +++ b/res/translations/en.json @@ -48,6 +48,7 @@ "Close to system tray": "Close to system tray", "Comment": "Comment", "Compilation": "Compilation", + "Compilations": "Compilations", "Composer": "Composer", "Configure your music server to add radio stations": "Configure your music server to add radio stations", "Confirm Delete Playlist": "Confirm Delete Playlist", @@ -81,6 +82,7 @@ "Enabled": "Enabled", "Enter": "Enter", "EP": "EP", + "EPs": "EPs", "Equalizer": "Equalizer", "Error": "Error", "Exclusive mode": "Exclusive mode", @@ -199,6 +201,7 @@ "Shuffle tracks": "Shuffle tracks", "Similar artists": "Similar artists", "Single": "Single", + "Singles": "Singles", "Size": "Size", "Skip duplicate tracks": "Skip duplicate tracks", "Skip SSL certificate verification": "Skip SSL certificate verification", diff --git a/res/translations/es.json b/res/translations/es.json index 5fcaedc..12b5883 100644 --- a/res/translations/es.json +++ b/res/translations/es.json @@ -39,6 +39,7 @@ "Close to system tray": "Cerrar a la bandeja del sistema", "Comment": "Comentario", "Compilation": "Compilación", + "Compilations": "Compilaciónes", "Composer": "Compositor", "Configure your music server to add radio stations": "Configura tu servidor de música para agregar estaciones de radio", "Confirm Delete Playlist": "Confirmar eliminación de la lista de reproducción", @@ -69,6 +70,7 @@ "Enabled": "Habilitado", "Enter": "Ingresar", "EP": "EP", + "EPs": "EPs", "Equalizer": "Ecualizador", "Error": "Error", "Exclusive mode": "Modo exclusivo", @@ -182,6 +184,7 @@ "Shuffle tracks": "Aleatorio pistas", "Similar artists": "Artistas similares", "Single": "Sencillo", + "Singles": "Sencillos", "Size": "Tamaño", "Skip duplicate tracks": "Saltar pistas duplicadas", "Skip this version": "Saltar esta versión", diff --git a/res/translations/fr.json b/res/translations/fr.json index 657d1f9..a337191 100644 --- a/res/translations/fr.json +++ b/res/translations/fr.json @@ -48,6 +48,7 @@ "Close to system tray": "Réduire dans la barre d'état à la fermeture", "Comment": "Commentaire", "Compilation": "Compilation", + "Compilations": "Compilations", "Composer": "Compositeur", "Configure your music server to add radio stations": "Configurez votre serveur de musique pour ajouter des stations de radio", "Confirm Delete Playlist": "Confirmer la suppression de la liste de lecture", @@ -81,6 +82,7 @@ "Enabled": "Activé", "Enter": "Entrée", "EP": "EP", + "EPs": "EPs", "Equalizer": "Égaliseur", "Error": "Erreur", "Exclusive mode": "Mode exclusif", @@ -199,6 +201,7 @@ "Shuffle tracks": "Mélanger les pistes", "Similar artists": "Artistes similaires", "Single": "Single", + "Singles": "Singles", "Size": "Taille", "Skip duplicate tracks": "Ne pas lire les doublons de pistes", "Skip SSL certificate verification": "Ne pas vérifier le certificat SSL", diff --git a/res/translations/it.json b/res/translations/it.json index 3a89d05..a60ebce 100644 --- a/res/translations/it.json +++ b/res/translations/it.json @@ -38,6 +38,7 @@ "Close to system tray": "Chiudi nella barra di sistema", "Comment": "Commento", "Compilation": "Compilation", + "Compilations": "Compilations", "Composer": "Compositore", "Configure your music server to add radio stations": "Configura il tuo server per aggiungere le stazioni radio", "Confirm Delete Playlist": "Eliminare la playlist?", @@ -68,6 +69,7 @@ "Enabled": "Abilita", "Enter": "Invio", "EP": "EP", + "EPs": "EPs", "Equalizer": "Equalizzatore", "Exclusive mode": "Modalità esclusiva", "Favorites": "Preferiti", @@ -177,6 +179,7 @@ "Shuffle tracks": "Tracce casuali", "Similar artists": "Artisti simili", "Single": "Single", + "Singles": "Singles", "Size": "Dimensione", "Skip duplicate tracks": "Salta tracce duplicate", "Skip this version": "Salta questa versione", diff --git a/res/translations/ja.json b/res/translations/ja.json index eadb55e..6f9c21f 100644 --- a/res/translations/ja.json +++ b/res/translations/ja.json @@ -48,6 +48,7 @@ "Close to system tray": "システムトレイに閉じる", "Comment": "コメント", "Compilation": "コンピレーション", + "Compilations": "コンピレーション", "Composer": "作曲家", "Configure your music server to add radio stations": "音楽サーバーを設定してラジオ局を追加する", "Confirm Delete Playlist": "プレイリストの削除を確認", @@ -81,6 +82,7 @@ "Enabled": "有効", "Enter": "入力", "EP": "EP", + "EPs": "EPs", "Equalizer": "イコライザー", "Error": "エラー", "Exclusive mode": "排他モード", @@ -199,6 +201,7 @@ "Shuffle tracks": "トラックをシャッフル", "Similar artists": "類似アーティスト", "Single": "シングル", + "Singles": "シングル", "Size": "サイズ", "Skip duplicate tracks": "重複トラックをスキップ", "Skip SSL certificate verification": "SSL証明書の検証をスキップ", diff --git a/res/translations/ko.json b/res/translations/ko.json index 1bdc8bf..334987c 100644 --- a/res/translations/ko.json +++ b/res/translations/ko.json @@ -48,6 +48,7 @@ "Close to system tray": "시스템 트레이로 최소화", "Comment": "코멘트", "Compilation": "컴필레이션", + "Compilations": "컴필레이션", "Composer": "작곡가", "Configure your music server to add radio stations": "라디오 방송국을 추가하기 위해서는 음악 서버를 구성하세요", "Confirm Delete Playlist": "재생 목록 삭제 확인", @@ -81,6 +82,7 @@ "Enabled": "활성화", "Enter": "입력", "EP": "EP", + "EPs": "EPs", "Equalizer": "이퀄라이저", "Error": "오류", "Exclusive mode": "독점 모드", @@ -199,6 +201,7 @@ "Shuffle tracks": "트랙 셔플하기", "Similar artists": "유사 아티스트", "Single": "싱글", + "Singles": "싱글", "Size": "크기", "Skip duplicate tracks": "중복 트랙 건너뛰기", "Skip SSL certificate verification": "SSL 인증서 검증 건너뛰기", diff --git a/res/translations/nl.json b/res/translations/nl.json index a70cbdb..4a2dd65 100644 --- a/res/translations/nl.json +++ b/res/translations/nl.json @@ -47,6 +47,7 @@ "Close to system tray": "Sluiten naar systeemvak", "Comment": "Commentaar", "Compilation": "Compilatie", + "Compilations": "Compilaties", "Composer": "Componist", "Configure your music server to add radio stations": "Configureer je muziekserver om radiostations toe te voegen", "Confirm Delete Playlist": "Bevestig afspeellijst wissen", @@ -79,6 +80,7 @@ "Enabled": "Ingeschakeld", "Enter": "Enter", "EP": "EP", + "EPs": "EPs", "Equalizer": "Equalizer", "Error": "Fout", "Exclusive mode": "Exclusieve modus", @@ -193,6 +195,7 @@ "Shuffle tracks": "Nummers door mekaar smijten", "Similar artists": "Gelijkaardige artiesten", "Single": "Single", + "Singles": "Singles", "Size": "Grootte", "Skip duplicate tracks": "Dubbele nummers overslaan", "Skip SSL certificate verification": "SSL-certificaat verificatie overslaan", diff --git a/res/translations/pl.json b/res/translations/pl.json index fed9ae9..d533797 100644 --- a/res/translations/pl.json +++ b/res/translations/pl.json @@ -44,6 +44,7 @@ "Close to system tray": "Zamknij do zasobnika systemowego", "Comment": "Komentarz", "Compilation": "Compilation", + "Compilations": "Compilations", "Composer": "Kompozytor", "Configure your music server to add radio stations": "Skonfiguruj swój serwer muzyki aby dodać stacje radiowe", "Confirm Delete Playlist": "Potwierdź usunięcie playlisty", @@ -76,6 +77,7 @@ "Enabled": "Włączony", "Enter": "Enter", "EP": "EP", + "EPs": "EPs", "Equalizer": "Equalizer", "Exclusive mode": "Tryb exclusive", "Favorites": "Ulubione", @@ -189,6 +191,7 @@ "Shuffle tracks": "Wymieszaj ścieżki", "Similar artists": "Podobni artyści", "Single": "Single", + "Singles": "Singles", "Size": "Wielkość", "Skip duplicate tracks": "Pomiń zduplikowane ścieżki", "Skip SSL certificate verification": "Pomiń weryfikację certyfikatu SSL", diff --git a/res/translations/pt_BR.json b/res/translations/pt_BR.json index 4f337cc..8fbfae1 100644 --- a/res/translations/pt_BR.json +++ b/res/translations/pt_BR.json @@ -47,6 +47,7 @@ "Close to system tray": "Fechar para a bandeja do sistema", "Comment": "Comentar", "Compilation": "Compilação", + "Compilations": "Compilaçãoes", "Composer": "Compositor", "Configure your music server to add radio stations": "Configure o seu servidor de música para adicionar estações de rádio", "Confirm Delete Playlist": "Confirmar remoção da lista de reprodução", @@ -196,6 +197,7 @@ "Shuffle tracks": "Faixas em ordem aleatória", "Similar artists": "Artistas similares", "Single": "Single", + "Singles": "Singles", "Size": "Tamanho", "Skip duplicate tracks": "Pular faixas duplicadas", "Skip SSL certificate verification": "Pular verificação do certificado SSL", diff --git a/res/translations/ro.json b/res/translations/ro.json index 43b16e8..484843b 100644 --- a/res/translations/ro.json +++ b/res/translations/ro.json @@ -37,6 +37,7 @@ "Close to system tray": "Închide în bara de sistem", "Comment": "Comentariu", "Compilation": "Compilație", + "Compilations": "Compilații", "Composer": "Compozitor", "Configure your music server to add radio stations": "Configurează-ți serverul de muzică pentru a adăuga stații radio", "Confirm Delete Playlist": "Confirmare ștergere playlist", @@ -67,6 +68,7 @@ "Enabled": "Activat", "Enter": "Enter", "EP": "EP", + "EPs": "EPs", "Equalizer": "Egalizator", "Exclusive mode": "Mod exclusiv", "Favorites": "Favorite", @@ -169,6 +171,7 @@ "Shuffle tracks": "Amestecă melodii", "Similar artists": "Similar artists", "Single": "Single", + "Singles": "Singles", "Size": "Mărime", "Skip duplicate tracks": "Sari peste melodii duplicate", "Skip this version": "Sari peste această versiune", diff --git a/res/translations/ru.json b/res/translations/ru.json index bbd38a4..2a242fa 100644 --- a/res/translations/ru.json +++ b/res/translations/ru.json @@ -49,6 +49,7 @@ "Close to system tray": "Закрывать в область уведомлений", "Comment": "Комментарий", "Compilation": "Сборник", + "Compilations": "сборники", "Composer": "Композитор", "Configure your music server to add radio stations": "Включите использование радиостанций на музыкальном сервере", "Confirm Delete Playlist": "Подтвердить удаление списка воспроизведения", @@ -82,6 +83,7 @@ "Enabled": "Включен", "Enter": "Войти", "EP": "EP", + "EPs": "EPs", "Equalizer": "Эквалайзер", "Error": "Ошибка", "Exclusive mode": "Исключительное использование", @@ -201,6 +203,7 @@ "Shuffle tracks": "Перемешать композиции", "Similar artists": "Похожие исполнители", "Single": "Сингл", + "Singles": "синглы", "Size": "Размер", "Skip duplicate tracks": "Не добавлять повторы", "Skip SSL certificate verification": "Не проверять сертификаты SSL", diff --git a/res/translations/zh.json b/res/translations/zh.json index 11411f3..db8db72 100644 --- a/res/translations/zh.json +++ b/res/translations/zh.json @@ -48,6 +48,7 @@ "Close to system tray": "关闭到系统托盘", "Comment": "评论", "Compilation": "合集", + "Compilations": "合集", "Composer": "作曲家", "Configure your music server to add radio stations": "配置您的音乐服务器以添加电台", "Confirm Delete Playlist": "确认删除播放列表", @@ -81,6 +82,7 @@ "Enabled": "已启用", "Enter": "输入", "EP": "EP", + "EPs": "EPs", "Equalizer": "均衡器", "Error": "错误", "Exclusive mode": "独占模式", @@ -199,6 +201,7 @@ "Shuffle tracks": "随机播放曲目", "Similar artists": "相似艺术家", "Single": "单曲", + "Singles": "单曲", "Size": "大小", "Skip duplicate tracks": "跳过重复曲目", "Skip SSL certificate verification": "跳过 SSL 证书验证", diff --git a/res/translations/zhHans.json b/res/translations/zhHans.json index 11411f3..db8db72 100644 --- a/res/translations/zhHans.json +++ b/res/translations/zhHans.json @@ -48,6 +48,7 @@ "Close to system tray": "关闭到系统托盘", "Comment": "评论", "Compilation": "合集", + "Compilations": "合集", "Composer": "作曲家", "Configure your music server to add radio stations": "配置您的音乐服务器以添加电台", "Confirm Delete Playlist": "确认删除播放列表", @@ -81,6 +82,7 @@ "Enabled": "已启用", "Enter": "输入", "EP": "EP", + "EPs": "EPs", "Equalizer": "均衡器", "Error": "错误", "Exclusive mode": "独占模式", @@ -199,6 +201,7 @@ "Shuffle tracks": "随机播放曲目", "Similar artists": "相似艺术家", "Single": "单曲", + "Singles": "单曲", "Size": "大小", "Skip duplicate tracks": "跳过重复曲目", "Skip SSL certificate verification": "跳过 SSL 证书验证", diff --git a/res/translations/zhHant.json b/res/translations/zhHant.json index 3032c55..8b0b7bf 100644 --- a/res/translations/zhHant.json +++ b/res/translations/zhHant.json @@ -37,6 +37,7 @@ "Close to system tray": "關閉到系統匣", "Comment": "評論", "Compilation": "合輯", + "Compilations": "合輯", "Composer": "作曲家", "Configure your music server to add radio stations": "配置您的音樂伺服器以新增電台", "Confirm Delete Playlist": "確認刪除播放清單", @@ -67,6 +68,7 @@ "Enabled": "已啟用", "Enter": "輸入", "EP": "EP", + "EPs": "EPs", "Equalizer": "均衡器", "Exclusive mode": "獨佔模式", "Favorites": "收藏", @@ -169,6 +171,7 @@ "Shuffle tracks": "隨機播放曲目", "Similar artists": "相似藝術家", "Single": "單曲", + "Singles": "單曲", "Skip duplicate tracks": "跳過重複曲目", "Skip this version": "跳過此版本", "Soundtrack": "原聲帶", From 2acd99b84153ed4bd4fbd98b4b78aa1e45fef5b8 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Sun, 6 Jul 2025 15:51:20 -0700 Subject: [PATCH 10/11] save scroll position in artist page state --- ui/browsing/artistpage.go | 40 +++++++++++++++++++++++++++-------- ui/widgets/groupedreleases.go | 22 ++++++++++++++----- 2 files changed, 48 insertions(+), 14 deletions(-) diff --git a/ui/browsing/artistpage.go b/ui/browsing/artistpage.go index 187e933..d3fe1dd 100644 --- a/ui/browsing/artistpage.go +++ b/ui/browsing/artistpage.go @@ -40,6 +40,9 @@ type artistPageState struct { mp mediaprovider.MediaProvider im *backend.ImageManager contr *controller.Controller + + gridScrollPos float32 // for album grid (or grouped releases) + listScrollPos float32 // for Top Tracks list } type ArtistPage struct { @@ -69,11 +72,7 @@ func NewArtistPage(artistID string, cfg *backend.ArtistPageConfig, pool *util.Wi if cfg.InitialView == viewTopTracks { activeView = 1 } - return newArtistPage(artistID, cfg, pool, pm, mp, im, contr, activeView, widgets.TracklistSort{}) -} - -func newArtistPage(artistID string, cfg *backend.ArtistPageConfig, pool *util.WidgetPool, pm *backend.PlaybackManager, mp mediaprovider.MediaProvider, im *backend.ImageManager, contr *controller.Controller, activeView int, sort widgets.TracklistSort) *ArtistPage { - a := &ArtistPage{artistPageState: artistPageState{ + return newArtistPage(artistPageState{ artistID: artistID, cfg: cfg, pool: pool, @@ -82,8 +81,11 @@ func newArtistPage(artistID string, cfg *backend.ArtistPageConfig, pool *util.Wi im: im, contr: contr, activeView: activeView, - trackSort: sort, - }} + }) +} + +func newArtistPage(state artistPageState) *ArtistPage { + a := &ArtistPage{artistPageState: state} a.ExtendBaseWidget(a) if h := a.pool.Obtain(util.WidgetTypeArtistPageHeader); h != nil { a.header = h.(*ArtistPageHeader) @@ -92,7 +94,7 @@ func newArtistPage(artistID string, cfg *backend.ArtistPageConfig, pool *util.Wi a.header = NewArtistPageHeader(a) } a.header.artistPage = a - if img, ok := im.GetCachedArtistImage(artistID); ok { + if img, ok := state.im.GetCachedArtistImage(state.artistID); ok { a.header.artistImage.SetImage(img, true /*tappable*/) } viewToggle := widgets.NewToggleText(0, []string{lang.L("Discography"), lang.L("Top Tracks")}) @@ -148,6 +150,7 @@ func (a *ArtistPage) Save() SavedPage { s := a.artistPageState if a.tracklistCtr != nil { tl := a.tracklistCtr.Objects[0].(*widgets.Tracklist) + s.listScrollPos = tl.GetScrollOffset() s.trackSort = tl.Sorting() tl.Clear() a.pool.Release(util.WidgetTypeTracklist, tl) @@ -155,10 +158,12 @@ func (a *ArtistPage) Save() SavedPage { a.header.artistPage = nil a.pool.Release(util.WidgetTypeArtistPageHeader, a.header) if a.albumGrid != nil { + s.gridScrollPos = a.albumGrid.GetScrollOffset() a.albumGrid.Clear() a.pool.Release(util.WidgetTypeGridView, a.albumGrid) } if a.groupedReleases != nil { + s.gridScrollPos = a.groupedReleases.GetScrollOffset() a.groupedReleases.Model = widgets.GroupedReleasesModel{} a.pool.Release(util.WidgetTypeGroupedReleases, a.groupedReleases) } @@ -181,6 +186,11 @@ var _ Scrollable = (*ArtistPage)(nil) func (g *ArtistPage) Scroll(scrollAmt float32) { if g.activeView == 0 && g.albumGrid != nil { g.albumGrid.ScrollToOffset(g.albumGrid.GetScrollOffset() + scrollAmt) + } else if g.activeView == 0 && g.groupedReleases != nil { + g.groupedReleases.ScrollToOffset(g.groupedReleases.GetScrollOffset() + scrollAmt) + } else if g.activeView == 1 && g.tracklistCtr != nil { + tl := g.tracklistCtr.Objects[0].(*widgets.Tracklist) + tl.ScrollBy(scrollAmt) } } @@ -326,6 +336,10 @@ func (a *ArtistPage) showAlbumGrid(reSort bool) { a.groupedReleases = widgets.NewGroupedReleases(model, a.im) } a.contr.ConnectGroupedReleasesActions(a.groupedReleases) + if a.gridScrollPos != 0 { + a.groupedReleases.ScrollToOffset(a.gridScrollPos) + a.gridScrollPos = 0 + } } else { model := a.getGridViewAlbumsModel() if g := a.pool.Obtain(util.WidgetTypeGridView); g != nil { @@ -336,6 +350,10 @@ func (a *ArtistPage) showAlbumGrid(reSort bool) { a.albumGrid = widgets.NewFixedGridView(model, a.im, myTheme.AlbumIcon) } a.contr.ConnectAlbumGridActions(a.albumGrid) + if a.gridScrollPos != 0 { + a.albumGrid.ScrollToOffset(a.gridScrollPos) + a.gridScrollPos = 0 + } } } else if reSort { if useGroupedReleases { @@ -400,6 +418,10 @@ func (a *ArtistPage) showTopTracks() { } tl.SetNowPlaying(a.nowPlayingID) a.contr.ConnectTracklistActions(tl) + if a.listScrollPos != 0 { + tl.ScrollToOffset(a.listScrollPos) + a.listScrollPos = 0 + } a.tracklistCtr = container.New( &layout.CustomPaddedLayout{LeftPadding: 15, RightPadding: 15, BottomPadding: 10}, tl) @@ -434,7 +456,7 @@ func (a *ArtistPage) CreateRenderer() fyne.WidgetRenderer { } func (s *artistPageState) Restore() Page { - return newArtistPage(s.artistID, s.cfg, s.pool, s.pm, s.mp, s.im, s.contr, s.activeView, s.trackSort) + return newArtistPage(*s) } const artistBioNotAvailableKey = "Artist biography not available." diff --git a/ui/widgets/groupedreleases.go b/ui/widgets/groupedreleases.go index 6650a3f..5d786e2 100644 --- a/ui/widgets/groupedreleases.go +++ b/ui/widgets/groupedreleases.go @@ -46,6 +46,8 @@ type GroupedReleases struct { cardPool sync.Pool sections [4]groupedReleasesSection + + container *container.Scroll } type groupedReleasesSection struct { @@ -76,16 +78,26 @@ func NewGroupedReleases(model GroupedReleasesModel, fetch util.ImageFetcher) *Gr g.sections[i].container = container.NewGridWrap(cardSize) } - return g -} - -func (g *GroupedReleases) CreateRenderer() fyne.WidgetRenderer { vbox := container.NewVBox() for i := range g.sections { vbox.Add(g.sections[i].titleRow) vbox.Add(g.sections[i].container) } - return widget.NewSimpleRenderer(container.NewVScroll(vbox)) + g.container = container.NewVScroll(vbox) + + return g +} + +func (g *GroupedReleases) CreateRenderer() fyne.WidgetRenderer { + return widget.NewSimpleRenderer(g.container) +} + +func (g *GroupedReleases) GetScrollOffset() float32 { + return g.container.Offset.Y +} + +func (g *GroupedReleases) ScrollToOffset(offs float32) { + g.container.ScrollToOffset(fyne.NewPos(0, offs)) } func (g *GroupedReleases) Refresh() { From e6f921630c7caff4613f72f634354730916f1654 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Sun, 6 Jul 2025 16:26:23 -0700 Subject: [PATCH 11/11] remember GroupedReleases section visibility in nav history --- ui/browsing/artistpage.go | 9 +++++++ ui/widgets/groupedreleases.go | 46 +++++++++++++++++++++++++++++++---- 2 files changed, 50 insertions(+), 5 deletions(-) diff --git a/ui/browsing/artistpage.go b/ui/browsing/artistpage.go index d3fe1dd..f389658 100644 --- a/ui/browsing/artistpage.go +++ b/ui/browsing/artistpage.go @@ -43,6 +43,9 @@ type artistPageState struct { gridScrollPos float32 // for album grid (or grouped releases) listScrollPos float32 // for Top Tracks list + + sectionVis widgets.GroupedReleasesSectionVisibility + sectionVisNeedApply bool } type ArtistPage struct { @@ -164,6 +167,8 @@ func (a *ArtistPage) Save() SavedPage { } if a.groupedReleases != nil { s.gridScrollPos = a.groupedReleases.GetScrollOffset() + s.sectionVis = a.groupedReleases.GetSectionVisibility() + s.sectionVisNeedApply = true a.groupedReleases.Model = widgets.GroupedReleasesModel{} a.pool.Release(util.WidgetTypeGroupedReleases, a.groupedReleases) } @@ -336,6 +341,10 @@ func (a *ArtistPage) showAlbumGrid(reSort bool) { a.groupedReleases = widgets.NewGroupedReleases(model, a.im) } a.contr.ConnectGroupedReleasesActions(a.groupedReleases) + if a.sectionVisNeedApply { + a.groupedReleases.SetSectionVisibility(a.sectionVis, true) + a.sectionVisNeedApply = false + } if a.gridScrollPos != 0 { a.groupedReleases.ScrollToOffset(a.gridScrollPos) a.gridScrollPos = 0 diff --git a/ui/widgets/groupedreleases.go b/ui/widgets/groupedreleases.go index 5d786e2..9e22c9d 100644 --- a/ui/widgets/groupedreleases.go +++ b/ui/widgets/groupedreleases.go @@ -22,6 +22,13 @@ type GroupedReleasesModel struct { Singles []GridViewItemModel } +type GroupedReleasesSectionVisibility struct { + Albums bool + Compilations bool + EPs bool + Singles bool +} + type GroupedReleases struct { widget.BaseWidget @@ -88,6 +95,33 @@ func NewGroupedReleases(model GroupedReleasesModel, fetch util.ImageFetcher) *Gr return g } +func (g *GroupedReleases) GetSectionVisibility() GroupedReleasesSectionVisibility { + v := GroupedReleasesSectionVisibility{true, true, true, true} + if g.sections[0].titleRow.Collapsed { + v.Albums = false + } + if g.sections[1].titleRow.Collapsed { + v.Compilations = false + } + if g.sections[2].titleRow.Collapsed { + v.EPs = false + } + if g.sections[3].titleRow.Collapsed { + v.Singles = false + } + return v +} + +func (g *GroupedReleases) SetSectionVisibility(v GroupedReleasesSectionVisibility, refresh bool) { + for i, b := range []bool{v.Albums, v.Compilations, v.EPs, v.Singles} { + g.sections[i].titleRow.Collapsed = !b + g.sections[i].container.Hidden = !b + } + if refresh { + g.Refresh() + } +} + func (g *GroupedReleases) CreateRenderer() fyne.WidgetRenderer { return widget.NewSimpleRenderer(g.container) } @@ -146,7 +180,9 @@ func (g *GroupedReleases) Refresh() { g.sections[i].container.Hide() } else { g.sections[i].titleRow.Show() - g.sections[i].container.Show() + if !g.sections[i].titleRow.Collapsed { + g.sections[i].container.Show() + } } } g.BaseWidget.Refresh() @@ -245,7 +281,7 @@ type groupedReleasesHeader struct { onToggle func(bool) icon *widget.Icon - collapsed bool + Collapsed bool } func newGroupedReleasesHeader(title string, onToggleVisibility func(bool)) *groupedReleasesHeader { @@ -267,7 +303,7 @@ func (g *groupedReleasesHeader) CreateRenderer() fyne.WidgetRenderer { } func (g *groupedReleasesHeader) Refresh() { - if g.collapsed { + if g.Collapsed { g.icon.Resource = theme.MenuExpandIcon() } else { g.icon.Resource = theme.MenuDropDownIcon() @@ -278,7 +314,7 @@ func (g *groupedReleasesHeader) Refresh() { var _ fyne.Tappable = (*groupedReleasesHeader)(nil) func (g *groupedReleasesHeader) Tapped(*fyne.PointEvent) { - g.collapsed = !g.collapsed - g.onToggle(g.collapsed) + g.Collapsed = !g.Collapsed + g.onToggle(g.Collapsed) g.Refresh() }