Merge pull request #654 from dweymouth/feat/group-releases

Group by release type on artist page
This commit is contained in:
Drew Weymouth
2025-07-06 16:33:42 -07:00
committed by GitHub
21 changed files with 571 additions and 66 deletions
+3
View File
@@ -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",
+3
View File
@@ -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",
+3
View File
@@ -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",
+3
View File
@@ -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",
+3
View File
@@ -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",
+3
View File
@@ -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証明書の検証をスキップ",
+3
View File
@@ -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 인증서 검증 건너뛰기",
+3
View File
@@ -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",
+3
View File
@@ -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",
+2
View File
@@ -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",
+3
View File
@@ -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",
+3
View File
@@ -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",
+3
View File
@@ -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 证书验证",
+3
View File
@@ -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 证书验证",
+3
View File
@@ -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": "原聲帶",
+148 -40
View File
@@ -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."
+56 -24
View File
@@ -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) {
+1
View File
@@ -17,6 +17,7 @@ const (
WidgetTypeTracklist
WidgetTypeCompactTracklist
WidgetTypeNowPlayingPage
WidgetTypeGroupedReleases
// keep at bottom
numWidgetTypes
+1 -1
View File
@@ -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
}
+1 -1
View File
@@ -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 != "")
+320
View File
@@ -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()
}