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": "原聲帶", diff --git a/ui/browsing/artistpage.go b/ui/browsing/artistpage.go index b5b95c1..f389658 100644 --- a/ui/browsing/artistpage.go +++ b/ui/browsing/artistpage.go @@ -2,6 +2,7 @@ package browsing import ( "log" + "slices" "sort" "strconv" @@ -39,6 +40,12 @@ 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 + + sectionVis widgets.GroupedReleasesSectionVisibility + sectionVisNeedApply bool } type ArtistPage struct { @@ -49,12 +56,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 ( @@ -67,11 +75,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, @@ -80,8 +84,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) @@ -90,7 +97,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")}) @@ -146,6 +153,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) @@ -153,9 +161,17 @@ 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() + s.sectionVis = a.groupedReleases.GetSectionVisibility() + s.sectionVisNeedApply = true + a.groupedReleases.Model = widgets.GroupedReleasesModel{} + a.pool.Release(util.WidgetTypeGroupedReleases, a.groupedReleases) + } return &s } @@ -175,6 +191,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) } } @@ -194,31 +215,73 @@ 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) { + 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 } } - 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 { + curSlice = slice + sort.Slice(slice, sortFunc) + } } // should be called asynchronously @@ -254,27 +317,68 @@ func (a *ArtistPage) load() { } func (a *ArtistPage) showAlbumGrid(reSort bool) { - if a.albumGrid == nil { + 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 { // 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) + if a.sectionVisNeedApply { + a.groupedReleases.SetSectionVisibility(a.sectionVis, true) + a.sectionVisNeedApply = false + } + if a.gridScrollPos != 0 { + a.groupedReleases.ScrollToOffset(a.gridScrollPos) + a.gridScrollPos = 0 + } } 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) + if a.gridScrollPos != 0 { + a.albumGrid.ScrollToOffset(a.gridScrollPos) + a.gridScrollPos = 0 + } } - 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 + a.container.Objects[0].Refresh() + } else { + a.container.Objects[0].(*fyne.Container).Objects[0] = a.albumGrid + } a.container.Objects[0].Refresh() } @@ -323,6 +427,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) @@ -357,7 +465,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/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/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 new file mode 100644 index 0000000..9e22c9d --- /dev/null +++ b/ui/widgets/groupedreleases.go @@ -0,0 +1,320 @@ +package widgets + +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" + "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 GroupedReleasesSectionVisibility struct { + Albums bool + Compilations bool + EPs bool + Singles bool +} + +type GroupedReleases struct { + widget.BaseWidget + + ShowSuffix bool + + 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 + + sections [4]groupedReleasesSection + + container *container.Scroll +} + +type groupedReleasesSection struct { + titleRow *groupedReleasesHeader + container *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) + + 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 { + _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) + } + + vbox := container.NewVBox() + for i := range g.sections { + vbox.Add(g.sections[i].titleRow) + vbox.Add(g.sections[i].container) + } + g.container = container.NewVScroll(vbox) + + 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) +} + +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() { + 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 + // clear out excess cards in this section + for x := lenItems; x < len(objects); x++ { + g.cardPool.Put(objects[x]) + objects[x] = nil + } + if len(objects) > lenItems { + objects = objects[:lenItems] + } + + // 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].titleRow.Hide() + g.sections[i].container.Hide() + } else { + g.sections[i].titleRow.Show() + if !g.sections[i].titleRow.Collapsed { + g.sections[i].container.Show() + } + } + } + g.BaseWidget.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) + } +} + +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) +} + +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() +}