diff --git a/src/components/AlbumCard.tsx b/src/components/AlbumCard.tsx index e9a699fb..36c28ff3 100644 --- a/src/components/AlbumCard.tsx +++ b/src/components/AlbumCard.tsx @@ -83,14 +83,18 @@ function AlbumCard({ album, selected, selectionMode, onToggleSelect, showRating )} - {isOffline && !selectionMode && ( -
- -
- )} - {isNewAlbum && ( -
- {t('common.new', 'New')} + {(isNewAlbum || (isOffline && !selectionMode)) && ( +
+ {isNewAlbum && ( +
+ {t('common.new', 'New')} +
+ )} + {isOffline && !selectionMode && ( +
+ +
+ )}
)} {selectionMode && ( diff --git a/src/components/ContextMenu.tsx b/src/components/ContextMenu.tsx index e819a9ae..71f38c59 100644 --- a/src/components/ContextMenu.tsx +++ b/src/components/ContextMenu.tsx @@ -1231,10 +1231,28 @@ export default function ContextMenu() { const album = item as SubsonicAlbum; if (album.id === id) return userRatingOverrides[id] ?? album.userRating ?? 0; } + if (kind === 'album' && type === 'multi-album') { + const albums = item as SubsonicAlbum[]; + const compositeId = [...albums.map(a => a.id)].sort().join('\x1e'); + if (id !== compositeId) return userRatingOverrides[id] ?? 0; + if (albums.length === 0) return 0; + const vals = albums.map(a => userRatingOverrides[a.id] ?? a.userRating ?? 0); + const first = vals[0]; + return vals.every(v => v === first) ? first : 0; + } if (kind === 'artist' && type === 'artist') { const artist = item as SubsonicArtist; if (artist.id === id) return userRatingOverrides[id] ?? artist.userRating ?? 0; } + if (kind === 'artist' && type === 'multi-artist') { + const artists = item as SubsonicArtist[]; + const compositeId = [...artists.map(a => a.id)].sort().join('\x1e'); + if (id !== compositeId) return userRatingOverrides[id] ?? 0; + if (artists.length === 0) return 0; + const vals = artists.map(a => userRatingOverrides[a.id] ?? a.userRating ?? 0); + const first = vals[0]; + return vals.every(v => v === first) ? first : 0; + } return userRatingOverrides[id] ?? 0; }, [type, item, userRatingOverrides]); @@ -1247,8 +1265,22 @@ export default function ContextMenu() { applyAlbumRating(item as SubsonicAlbum, rating); return; } + if (kind === 'album' && type === 'multi-album') { + const albums = item as SubsonicAlbum[]; + const compositeId = [...albums.map(a => a.id)].sort().join('\x1e'); + if (id !== compositeId) return; + for (const a of albums) applyAlbumRating(a, rating); + return; + } if (kind === 'artist' && type === 'artist') { applyArtistRating(item as SubsonicArtist, rating); + return; + } + if (kind === 'artist' && type === 'multi-artist') { + const artists = item as SubsonicArtist[]; + const compositeId = [...artists.map(a => a.id)].sort().join('\x1e'); + if (id !== compositeId) return; + for (const a of artists) applyArtistRating(a, rating); } }, [applySongRating, applyAlbumRating, applyArtistRating, type, item]); @@ -1922,6 +1954,14 @@ export default function ContextMenu() { {type === 'multi-album' && (() => { const albums = item as SubsonicAlbum[]; const albumIds = albums.map(a => a.id); + const albumRatingDisabled = entityRatingSupport === 'track_only'; + const multiAlbumRatingId = [...albumIds].sort().join('\x1e'); + const unifiedAlbumRating = (() => { + if (albums.length === 0) return 0; + const vals = albums.map(a => userRatingOverrides[a.id] ?? a.userRating ?? 0); + const first = vals[0]; + return vals.every(v => v === first) ? first : 0; + })(); return ( <>
@@ -1948,6 +1988,28 @@ export default function ContextMenu() { { setPlaylistSubmenuOpen(false); closeContextMenu(); }} /> )}
+
e.stopPropagation()} + > + + { + setKeyboardRating({ kind: 'album', id: multiAlbumRatingId, value: r }); + for (const a of albums) applyAlbumRating(a, r); + }} + /> +
); })()} @@ -2008,6 +2070,14 @@ export default function ContextMenu() { {type === 'multi-artist' && (() => { const artists = item as SubsonicArtist[]; const artistIds = artists.map(a => a.id); + const artistRatingDisabled = entityRatingSupport === 'track_only'; + const multiArtistRatingId = [...artistIds].sort().join('\x1e'); + const unifiedArtistRating = (() => { + if (artists.length === 0) return 0; + const vals = artists.map(a => userRatingOverrides[a.id] ?? a.userRating ?? 0); + const first = vals[0]; + return vals.every(v => v === first) ? first : 0; + })(); return ( <>
@@ -2026,6 +2096,28 @@ export default function ContextMenu() { { setPlaylistSubmenuOpen(false); closeContextMenu(); }} /> )}
+
e.stopPropagation()} + > + + { + setKeyboardRating({ kind: 'artist', id: multiArtistRatingId, value: r }); + for (const a of artists) applyArtistRating(a, r); + }} + /> +
); })()} diff --git a/src/locales/de.ts b/src/locales/de.ts index 7a75f294..60a05aa3 100644 --- a/src/locales/de.ts +++ b/src/locales/de.ts @@ -230,6 +230,8 @@ export const deTranslation = { artistShort: 'Künstlerbewertung', albumAriaLabel: 'Albumbewertung', artistAriaLabel: 'Künstlerbewertung', + selectedArtistsRatingAriaLabel: 'Sternebewertung für {{count}} ausgewählte Künstler', + selectedAlbumsRatingAriaLabel: 'Sternebewertung für {{count}} ausgewählte Alben', saveFailed: 'Bewertung konnte nicht gespeichert werden.', }, artistDetail: { diff --git a/src/locales/en.ts b/src/locales/en.ts index 0690e140..73e3af06 100644 --- a/src/locales/en.ts +++ b/src/locales/en.ts @@ -232,6 +232,8 @@ export const enTranslation = { artistShort: 'Artist rating', albumAriaLabel: 'Album rating', artistAriaLabel: 'Artist rating', + selectedArtistsRatingAriaLabel: 'Star rating for {{count}} selected artists', + selectedAlbumsRatingAriaLabel: 'Star rating for {{count}} selected albums', saveFailed: 'Could not save rating.', }, artistDetail: { diff --git a/src/locales/es.ts b/src/locales/es.ts index 9404008c..578c77d1 100644 --- a/src/locales/es.ts +++ b/src/locales/es.ts @@ -231,6 +231,8 @@ export const esTranslation = { artistShort: 'Calificación del artista', albumAriaLabel: 'Calificación del álbum', artistAriaLabel: 'Calificación del artista', + selectedArtistsRatingAriaLabel: 'Calificación con estrellas para {{count}} artistas seleccionados', + selectedAlbumsRatingAriaLabel: 'Calificación con estrellas para {{count}} álbumes seleccionados', saveFailed: 'No se pudo guardar la calificación.', }, artistDetail: { diff --git a/src/locales/fr.ts b/src/locales/fr.ts index 0438ff9f..48bf8076 100644 --- a/src/locales/fr.ts +++ b/src/locales/fr.ts @@ -230,6 +230,8 @@ export const frTranslation = { artistShort: 'Note de l’artiste', albumAriaLabel: 'Note de l’album', artistAriaLabel: 'Note de l’artiste', + selectedArtistsRatingAriaLabel: 'Note sur {{count}} artistes sélectionnés', + selectedAlbumsRatingAriaLabel: 'Note sur {{count}} albums sélectionnés', saveFailed: 'Impossible d’enregistrer la note.', }, artistDetail: { diff --git a/src/locales/nb.ts b/src/locales/nb.ts index a8fe3111..1358e861 100644 --- a/src/locales/nb.ts +++ b/src/locales/nb.ts @@ -230,6 +230,8 @@ export const nbTranslation = { artistShort: 'Artistvurdering', albumAriaLabel: 'Albumvurdering', artistAriaLabel: 'Artistvurdering', + selectedArtistsRatingAriaLabel: 'Stjernevurdering for {{count}} valgte artister', + selectedAlbumsRatingAriaLabel: 'Stjernevurdering for {{count}} valgte album', saveFailed: 'Kunne ikke lagre vurderingen.', }, artistDetail: { diff --git a/src/locales/nl.ts b/src/locales/nl.ts index 3b265784..b2bf39c0 100644 --- a/src/locales/nl.ts +++ b/src/locales/nl.ts @@ -229,6 +229,8 @@ export const nlTranslation = { artistShort: 'Artiestbeoordeling', albumAriaLabel: 'Albumbeoordeling', artistAriaLabel: 'Artiestbeoordeling', + selectedArtistsRatingAriaLabel: 'Sterrenbeoordeling voor {{count}} geselecteerde artiesten', + selectedAlbumsRatingAriaLabel: 'Sterrenbeoordeling voor {{count}} geselecteerde albums', saveFailed: 'Beoordeling opslaan mislukt.', }, artistDetail: { diff --git a/src/locales/ru.ts b/src/locales/ru.ts index ff00606f..712d0ee7 100644 --- a/src/locales/ru.ts +++ b/src/locales/ru.ts @@ -241,6 +241,8 @@ export const ruTranslation = { artistShort: 'Оценка исполнителя', albumAriaLabel: 'Оценка альбома', artistAriaLabel: 'Оценка исполнителя', + selectedArtistsRatingAriaLabel: 'Звёздная оценка для выбранных исполнителей ({{count}})', + selectedAlbumsRatingAriaLabel: 'Звёздная оценка для выбранных альбомов ({{count}})', saveFailed: 'Не удалось сохранить оценку.', }, artistDetail: { diff --git a/src/locales/zh.ts b/src/locales/zh.ts index d691f8ef..0a451091 100644 --- a/src/locales/zh.ts +++ b/src/locales/zh.ts @@ -228,6 +228,8 @@ export const zhTranslation = { artistShort: '艺人评分', albumAriaLabel: '专辑评分', artistAriaLabel: '艺人评分', + selectedArtistsRatingAriaLabel: '为所选 {{count}} 位艺人设置星级评分', + selectedAlbumsRatingAriaLabel: '为所选 {{count}} 张专辑设置星级评分', saveFailed: '无法保存评分。', }, artistDetail: { diff --git a/src/pages/RandomAlbums.tsx b/src/pages/RandomAlbums.tsx index 9f31f30c..7ed0a6de 100644 --- a/src/pages/RandomAlbums.tsx +++ b/src/pages/RandomAlbums.tsx @@ -189,6 +189,7 @@ export default function RandomAlbums() { selectionMode={selectionMode} selected={selectedIds.has(a.id)} onToggleSelect={toggleSelect} + selectedAlbums={selectedAlbums} /> ))}
diff --git a/src/styles/components.css b/src/styles/components.css index 2da38062..2a973614 100644 --- a/src/styles/components.css +++ b/src/styles/components.css @@ -437,10 +437,20 @@ box-shadow: var(--shadow-sm); } -.album-card-offline-badge { +.album-card-cover-badges-tr { position: absolute; top: 6px; right: 6px; + display: flex; + flex-direction: column; + align-items: flex-end; + gap: 4px; + z-index: 2; + pointer-events: none; +} + +.album-card-offline-badge { + flex-shrink: 0; background: color-mix(in srgb, var(--accent) 85%, transparent); color: var(--ctp-crust); border-radius: var(--radius-sm); @@ -448,14 +458,10 @@ display: flex; align-items: center; justify-content: center; - z-index: 2; - pointer-events: none; } .album-card-new-badge { - position: absolute; - top: 6px; - left: 6px; + flex-shrink: 0; background: var(--accent); color: var(--ctp-crust); border: 1px solid color-mix(in srgb, var(--accent) 42%, transparent); @@ -465,8 +471,6 @@ font-weight: 700; letter-spacing: 0.02em; line-height: 1; - z-index: 2; - pointer-events: none; } .album-card-play-overlay {