Add album detail list view (#1681)

This commit is contained in:
Jeff
2026-02-09 21:56:08 -08:00
committed by GitHub
parent 397610d8ab
commit f39a7f8d6f
79 changed files with 3462 additions and 364 deletions
@@ -137,14 +137,19 @@ const normalizeSong = (
discTitleMap?: Map<number, string>,
): Song => {
const participants = getParticipants(item);
const albumArtistsList = getArtistList(item.albumArtists, item.artistId, item.artist);
const albumArtistName =
item.albumArtists?.length > 0
? item.albumArtists.map((a) => a.name).join(', ')
: item.artist || '';
return {
_itemType: LibraryItem.SONG,
_serverId: server?.id || 'unknown',
_serverType: ServerType.SUBSONIC,
album: item.album || '',
albumArtistName: item.artist || '',
albumArtists: getArtistList(item.albumArtists, item.artistId, item.artist),
albumArtistName,
albumArtists: albumArtistsList,
albumId: item.albumId?.toString() || '',
artistName: item.artist || '',
artists: getArtistList(item.artists, item.artistId, item.artist, participants),
+1 -1
View File
@@ -139,7 +139,7 @@ export const getClientType = (): string => {
}
};
export const SEPARATOR_STRING = ' · ';
export const SEPARATOR_STRING = ' ';
export const sortSongList = (songs: Song[], sortBy: SongListSort, sortOrder: SortOrder) => {
let results: Song[] = songs;
+2
View File
@@ -58,6 +58,7 @@ import {
LuInfo,
LuKeyboard,
LuLayoutGrid,
LuLayoutList,
LuLibrary,
LuList,
LuListFilter,
@@ -186,6 +187,7 @@ export const AppIcon = {
itemSong: LuMusic,
keyboard: LuKeyboard,
lastPlayed: LuHeadphones,
layoutDetail: LuLayoutList,
layoutGrid: LuLayoutGrid,
layoutList: LuList,
layoutTable: LuTable,
@@ -0,0 +1,31 @@
.root {
display: inline-flex;
gap: 0.125rem;
align-items: center;
line-height: 1;
}
.root.interactive {
cursor: pointer;
}
.root.xs {
font-size: var(--theme-font-size-xs);
}
.root.sm {
font-size: var(--theme-font-size-sm);
}
.root.md {
font-size: var(--theme-font-size-md);
}
.filled {
color: var(--theme-colors-primary);
}
.empty {
color: var(--theme-colors-foreground-muted);
opacity: 0.6;
}
@@ -0,0 +1,81 @@
import clsx from 'clsx';
import { memo, useCallback, useState } from 'react';
import styles from './read-only-rating.module.css';
const MAX_STARS = 5;
interface ReadOnlyRatingProps {
className?: string;
onChange?: (value: number) => void;
size?: 'md' | 'sm' | 'xs';
value?: null | number;
}
function ReadOnlyRatingComponent({ className, onChange, size = 'sm', value }: ReadOnlyRatingProps) {
const [hoverIndex, setHoverIndex] = useState<null | number>(null);
const rating = Math.min(MAX_STARS, Math.max(0, value ?? 0));
const displayCount = hoverIndex !== null ? hoverIndex : Math.floor(rating);
const handlePointerMove = useCallback(
(e: React.PointerEvent) => {
if (!onChange) return;
const el = e.currentTarget;
const width = (el as HTMLElement).offsetWidth;
if (width <= 0) return;
const x = e.clientX - el.getBoundingClientRect().left;
const segment = Math.floor((x / width) * MAX_STARS);
const filled = segment < 0 ? 0 : Math.min(MAX_STARS, segment + 1);
setHoverIndex(filled);
},
[onChange],
);
const handlePointerLeave = useCallback(() => {
setHoverIndex(null);
}, []);
const handleClick = useCallback(
(e: React.MouseEvent) => {
if (!onChange) return;
e.preventDefault();
e.stopPropagation();
const el = e.currentTarget;
const width = (el as HTMLElement).offsetWidth;
if (width <= 0) return;
const x = e.clientX - el.getBoundingClientRect().left;
const segment = Math.floor((x / width) * MAX_STARS);
const clicked = segment < 0 ? 0 : Math.min(MAX_STARS, segment + 1);
onChange(clicked === rating ? 0 : clicked);
},
[onChange, rating],
);
const isInteractive = typeof onChange === 'function';
return (
<span
aria-label={isInteractive ? undefined : `${rating} out of ${MAX_STARS} stars`}
className={clsx(
styles.root,
size && styles[size],
isInteractive && styles.interactive,
className,
)}
onClick={isInteractive ? handleClick : undefined}
onPointerLeave={isInteractive ? handlePointerLeave : undefined}
onPointerMove={isInteractive ? handlePointerMove : undefined}
role={isInteractive ? undefined : 'img'}
>
{Array.from({ length: MAX_STARS }, (_, i) => (
<span className={i < displayCount ? styles.filled : styles.empty} key={i}>
</span>
))}
</span>
);
}
export const ReadOnlyRating = memo(ReadOnlyRatingComponent);
ReadOnlyRating.displayName = 'ReadOnlyRating';
+1
View File
@@ -34,6 +34,7 @@ export enum ItemListKey {
}
export enum ListDisplayType {
DETAIL = 'detail',
GRID = 'poster',
LIST = 'list',
TABLE = 'table',