fix(ui): split album and track artists (OpenSubsonic) (#696)

* fix(ui): split OpenSubsonic album and track artists in header and player

Album detail header uses albumArtists from album or child songs; player bar,
mobile player, and mini player use structured track artists with per-id links.
Adds deriveAlbumHeaderArtistRefs helper and OpenArtistRefInline.

Fixes #552

* docs: changelog and credits for OpenSubsonic artist links (PR #696)
This commit is contained in:
cucadmuh
2026-05-14 21:56:39 +03:00
committed by GitHub
parent ecdbe0cf2a
commit 3cc172723d
20 changed files with 315 additions and 29 deletions
+81
View File
@@ -0,0 +1,81 @@
import React, { Fragment } from 'react';
import type { SubsonicOpenArtistRef } from '../api/subsonicTypes';
interface Props {
refs: SubsonicOpenArtistRef[];
/** Used when `refs` is empty (callers should normally avoid that). */
fallbackName: string;
/** Invoked with Subsonic artist id when a ref has an id. */
onGoArtist: (artistId: string) => void;
/** Wrapper element: `span` (default) or `fragment` children only. */
as?: 'span' | 'none';
/** `button` for album header; `span` matches dense player / track rows. */
linkTag?: 'button' | 'span';
outerClassName?: string;
linkClassName?: string;
separatorClassName?: string;
}
/**
* Renders OpenSubsonic `artists` / `albumArtists` refs as ·-separated names with
* per-artist navigation when `id` is present (same interaction model as album
* track rows).
*/
export function OpenArtistRefInline({
refs,
fallbackName,
onGoArtist,
as = 'span',
linkTag = 'button',
outerClassName,
linkClassName,
separatorClassName = 'open-artist-ref-sep',
}: Props) {
const list = refs.length > 0 ? refs : [{ name: fallbackName }];
const inner = (
<>
{list.map((a, i) => (
<Fragment key={a.id ?? `n:${a.name ?? ''}:${i}`}>
{i > 0 && <span className={separatorClassName} aria-hidden="true"> · </span>}
{a.id ? (
linkTag === 'span' ? (
<span
role="link"
tabIndex={0}
className={linkClassName}
onClick={e => {
e.stopPropagation();
onGoArtist(a.id!);
}}
onKeyDown={e => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
e.stopPropagation();
onGoArtist(a.id!);
}
}}
>
{a.name ?? fallbackName}
</span>
) : (
<button
type="button"
className={linkClassName}
onClick={e => {
e.stopPropagation();
onGoArtist(a.id!);
}}
>
{a.name ?? fallbackName}
</button>
)
) : (
<span>{a.name ?? fallbackName}</span>
)}
</Fragment>
))}
</>
);
if (as === 'none') return inner;
return <span className={outerClassName}>{inner}</span>;
}