mirror of
https://github.com/kilyabin/psysonic.git
synced 2026-07-21 22:15:40 +00:00
feat(album): show OpenSubsonic disc subtitles after the CD heading (#753)
* feat(album): show OpenSubsonic disc subtitles after the CD heading Multi-disc albums in OpenSubsonic / Navidrome carry a per-disc subtitle (`discTitles`) — e.g. "Sessions" on CD 3 of a deluxe edition. AlbumTrackList only rendered "CD N" and dropped the subtitle, so users couldn't tell two discs apart unless they read the track names. * `SubsonicAlbum.discTitles` typed; `getAlbum` forwards it as-is. * `AlbumTrackList` and `AlbumTrackListMobile` take a discTitleByNum map and render the subtitle in the disc separator after "CD N". * Heading bumped slightly (13 → 15 px, icon 16 → 18 px) so the disc separator stays legible next to the new subtitle. * docs(changelog): note disc subtitles after CD heading (#753)
This commit is contained in:
committed by
GitHub
parent
48c7b8b780
commit
6595c146a3
@@ -216,6 +216,12 @@ Foundational work: faster reviews, narrower diffs, and a safety net under the pa
|
||||
|
||||
* The Queue side panel's ETA label and the sleep-timer preview both go through **`formatClockTime`**, which just delegates to **`toLocaleTimeString`** — on en-US that meant AM/PM with no in-app override. **Settings → System → App Behavior** now exposes a tri-state **Clock Format** select: **`Auto`** (default — keeps existing locale-driven behaviour, so first launch after the update is a no-op for everyone), **`24h`**, and **`12h`**, the explicit values forcing **`hour12`** everywhere `formatClockTime` is used. Wired through `authStore` (`clockFormat` / `setClockFormat`) and consumed by both surfaces; all nine bundled locales ship the four new strings.
|
||||
|
||||
### Album page — OpenSubsonic disc subtitles after the CD heading
|
||||
|
||||
**By [@Psychotoxical](https://github.com/Psychotoxical), thanks to zunoz for the report on the Psysonic Discord, PR [#753](https://github.com/Psychotoxical/psysonic/pull/753)**
|
||||
|
||||
* Multi-disc albums in OpenSubsonic / Navidrome can carry a per-disc subtitle (`discTitles`, e.g. **"Sessions"** on CD 3 of a deluxe edition). The album tracklist previously dropped it and only showed **`CD N`**, so adjacent discs of a reissue read the same in the header. The disc separator now renders **`CD N — Subtitle`** in both desktop and mobile track lists, and the heading is bumped slightly so the subtitle stays legible next to the disc number.
|
||||
|
||||
## Changed
|
||||
|
||||
### Backend — Cargo workspace with 5 domain crates (Rust refactor)
|
||||
|
||||
@@ -22,6 +22,13 @@ export interface SubsonicAlbum {
|
||||
releaseTypes?: string[];
|
||||
/** OpenSubsonic: album-level credits (Navidrome may attach on album and/or child songs). */
|
||||
albumArtists?: SubsonicOpenArtistRef[];
|
||||
/** OpenSubsonic: per-disc subtitles (e.g. "Sessions" on CD 3 of a deluxe edition). */
|
||||
discTitles?: SubsonicDiscTitle[];
|
||||
}
|
||||
|
||||
export interface SubsonicDiscTitle {
|
||||
disc: number;
|
||||
title: string;
|
||||
}
|
||||
|
||||
/** OpenSubsonic `artists` / `albumArtists` entries on a child song (may include `userRating`). */
|
||||
|
||||
@@ -20,6 +20,8 @@ export type { SortKey } from '../utils/componentHelpers/albumTrackListHelpers';
|
||||
|
||||
interface AlbumTrackListProps {
|
||||
songs: SubsonicSong[];
|
||||
/** Per-disc subtitles from the album payload, rendered after "CD N". */
|
||||
discTitles?: { disc: number; title: string }[];
|
||||
sorted?: boolean;
|
||||
hasVariousArtists: boolean;
|
||||
currentTrack: Track | null;
|
||||
@@ -42,6 +44,7 @@ interface AlbumTrackListProps {
|
||||
|
||||
export default function AlbumTrackList({
|
||||
songs,
|
||||
discTitles,
|
||||
sorted,
|
||||
hasVariousArtists: _hasVariousArtists,
|
||||
currentTrack,
|
||||
@@ -90,6 +93,9 @@ export default function AlbumTrackList({
|
||||
}
|
||||
const discNums = sorted ? [1] : Array.from(discs.keys()).sort((a, b) => a - b);
|
||||
const isMultiDisc = !sorted && discNums.length > 1;
|
||||
const discTitleByNum = new Map<number, string>(
|
||||
(discTitles ?? []).filter(d => d.title?.trim()).map(d => [d.disc, d.title.trim()]),
|
||||
);
|
||||
|
||||
const currentTrackId = currentTrack?.id ?? null;
|
||||
|
||||
@@ -98,6 +104,7 @@ export default function AlbumTrackList({
|
||||
<AlbumTrackListMobile
|
||||
discNums={discNums}
|
||||
discs={discs}
|
||||
discTitleByNum={discTitleByNum}
|
||||
isMultiDisc={isMultiDisc}
|
||||
currentTrackId={currentTrackId}
|
||||
isPlaying={isPlaying}
|
||||
@@ -150,6 +157,9 @@ export default function AlbumTrackList({
|
||||
<div className="disc-header">
|
||||
<span className="disc-icon">💿</span>
|
||||
CD {discNum}
|
||||
{discTitleByNum.get(discNum) && (
|
||||
<span className="disc-subtitle">{discTitleByNum.get(discNum)}</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{discs.get(discNum)!.map(song => {
|
||||
|
||||
@@ -8,6 +8,7 @@ import { formatLongDuration } from '../../utils/format/formatDuration';
|
||||
interface Props {
|
||||
discNums: number[];
|
||||
discs: Map<number, SubsonicSong[]>;
|
||||
discTitleByNum: Map<number, string>;
|
||||
isMultiDisc: boolean;
|
||||
currentTrackId: string | null;
|
||||
isPlaying: boolean;
|
||||
@@ -31,6 +32,7 @@ interface Props {
|
||||
export function AlbumTrackListMobile({
|
||||
discNums,
|
||||
discs,
|
||||
discTitleByNum,
|
||||
isMultiDisc,
|
||||
currentTrackId,
|
||||
isPlaying,
|
||||
@@ -46,6 +48,9 @@ export function AlbumTrackListMobile({
|
||||
{isMultiDisc && (
|
||||
<div className="disc-header">
|
||||
<span className="disc-icon">💿</span> CD {discNum}
|
||||
{discTitleByNum.get(discNum) && (
|
||||
<span className="disc-subtitle">{discTitleByNum.get(discNum)}</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{discs.get(discNum)!.map(song => {
|
||||
|
||||
@@ -341,6 +341,7 @@ const handleShuffleAll = () => {
|
||||
|
||||
<AlbumTrackList
|
||||
songs={displayedSongs}
|
||||
discTitles={album?.album.discTitles}
|
||||
sorted={sortKey !== 'natural' || !!filterText.trim()}
|
||||
hasVariousArtists={hasVariousArtists}
|
||||
currentTrack={currentTrack}
|
||||
|
||||
@@ -536,7 +536,7 @@ html[data-track-previews-randommix="off"] [data-preview-loc="randomMix"] .pl
|
||||
gap: var(--space-3);
|
||||
padding: var(--space-4) var(--space-3) var(--space-2);
|
||||
font-family: var(--font-display);
|
||||
font-size: 13px;
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.06em;
|
||||
text-transform: uppercase;
|
||||
@@ -551,7 +551,19 @@ html[data-track-previews-randommix="off"] [data-preview-loc="randomMix"] .pl
|
||||
}
|
||||
|
||||
.disc-icon {
|
||||
font-size: 16px;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
/* Per-disc subtitle (OpenSubsonic `discTitles`), e.g. "Sessions" on a deluxe CD 3. */
|
||||
.disc-subtitle {
|
||||
color: var(--text-muted);
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
.disc-subtitle::before {
|
||||
content: '— ';
|
||||
margin-right: 2px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user