add configuration to show secondary release types

This commit is contained in:
jeffvli
2025-12-27 15:19:00 -08:00
parent 7db15c7c72
commit 23e4574667
3 changed files with 175 additions and 66 deletions
+2
View File
@@ -410,6 +410,8 @@
"albumArtistDetail": { "albumArtistDetail": {
"about": "About {{artist}}", "about": "About {{artist}}",
"appearsOn": "appears on", "appearsOn": "appears on",
"groupingTypeAll": "all release types",
"groupingTypePrimary": "primary release types",
"recentReleases": "recent releases", "recentReleases": "recent releases",
"viewDiscography": "view discography", "viewDiscography": "view discography",
"relatedArtists": "related $t(entity.artist_other)", "relatedArtists": "related $t(entity.artist_other)",
@@ -44,11 +44,13 @@ import {
usePlayerSong, usePlayerSong,
} from '/@/renderer/store'; } from '/@/renderer/store';
import { useGeneralSettings, useSettingsStore } from '/@/renderer/store/settings.store'; import { useGeneralSettings, useSettingsStore } from '/@/renderer/store/settings.store';
import { titleCase } from '/@/renderer/utils';
import { sanitize } from '/@/renderer/utils/sanitize'; import { sanitize } from '/@/renderer/utils/sanitize';
import { sortAlbumList } from '/@/shared/api/utils'; import { sortAlbumList } from '/@/shared/api/utils';
import { ActionIcon, ActionIconGroup } from '/@/shared/components/action-icon/action-icon'; import { ActionIcon, ActionIconGroup } from '/@/shared/components/action-icon/action-icon';
import { Badge } from '/@/shared/components/badge/badge'; import { Badge } from '/@/shared/components/badge/badge';
import { Button } from '/@/shared/components/button/button'; import { Button } from '/@/shared/components/button/button';
import { DropdownMenu } from '/@/shared/components/dropdown-menu/dropdown-menu';
import { Grid } from '/@/shared/components/grid/grid'; import { Grid } from '/@/shared/components/grid/grid';
import { Group } from '/@/shared/components/group/group'; import { Group } from '/@/shared/components/group/group';
import { Icon } from '/@/shared/components/icon/icon'; import { Icon } from '/@/shared/components/icon/icon';
@@ -687,13 +689,14 @@ interface AlbumSectionProps {
albums: Album[]; albums: Album[];
controls: ItemControls; controls: ItemControls;
cq: ReturnType<typeof useContainerQuery>; cq: ReturnType<typeof useContainerQuery>;
releaseType: string;
rows: DataRow[] | undefined; rows: DataRow[] | undefined;
title: React.ReactNode | string; title: React.ReactNode | string;
} }
const MAX_SECTION_CARDS = 20; const MAX_SECTION_CARDS = 20;
const AlbumSection = ({ albums, controls, cq, rows, title }: AlbumSectionProps) => { const AlbumSection = ({ albums, controls, cq, releaseType, rows, title }: AlbumSectionProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const span = cq.isXl ? 3 : cq.isLg ? 4 : cq.isMd ? 6 : cq.isSm ? 8 : cq.isXs ? 12 : 12; const span = cq.isXl ? 3 : cq.isLg ? 4 : cq.isMd ? 6 : cq.isSm ? 8 : cq.isXs ? 12 : 12;
const albumCount = albums.length; const albumCount = albums.length;
@@ -798,7 +801,7 @@ const AlbumSection = ({ albums, controls, cq, rows, title }: AlbumSectionProps)
<Grid.Col key={album.id} span={span}> <Grid.Col key={album.id} span={span}>
<motion.div <motion.div
layout layout
layoutId={album.id} layoutId={`${releaseType}-${album.id}`}
transition={{ transition={{
duration: 0.5, duration: 0.5,
ease: 'easeInOut', ease: 'easeInOut',
@@ -829,6 +832,125 @@ const AlbumSection = ({ albums, controls, cq, rows, title }: AlbumSectionProps)
); );
}; };
type GroupingType = 'all' | 'primary';
const groupAlbumsByReleaseType = (
albums: Album[],
routeId: string,
groupingType: GroupingType = 'primary',
): Record<string, Album[]> => {
if (groupingType === 'all') {
// Group by all individual release types
const grouped = albums.reduce(
(acc, album) => {
// Priority 1: Appears on - artist is not an album artist
const isAlbumArtist = album.albumArtists?.some((artist) => artist.id === routeId);
if (!isAlbumArtist) {
const appearsOnKey = 'appears-on';
if (!acc[appearsOnKey]) {
acc[appearsOnKey] = [];
}
acc[appearsOnKey].push(album);
return acc;
}
// Priority 2: Compilations
if (album.isCompilation) {
const compilationKey = 'compilation';
if (!acc[compilationKey]) {
acc[compilationKey] = [];
}
acc[compilationKey].push(album);
return acc;
}
// Group by all release types
const releaseTypes = album.releaseTypes || [];
if (releaseTypes.length > 0) {
releaseTypes.forEach((type) => {
const normalizedType = type.toLowerCase();
if (!acc[normalizedType]) {
acc[normalizedType] = [];
}
acc[normalizedType].push(album);
});
} else {
// If no release types, use "other" as fallback
const otherKey = 'other';
if (!acc[otherKey]) {
acc[otherKey] = [];
}
acc[otherKey].push(album);
}
return acc;
},
{} as Record<string, Album[]>,
);
return grouped;
}
// Primary grouping (original behavior)
const grouped = albums.reduce(
(acc, album) => {
// Priority 1: Appears on - artist is not an album artist
const isAlbumArtist = album.albumArtists?.some((artist) => artist.id === routeId);
if (!isAlbumArtist) {
const appearsOnKey = 'appears-on';
if (!acc[appearsOnKey]) {
acc[appearsOnKey] = [];
}
acc[appearsOnKey].push(album);
return acc;
}
// Priority 2: Compilations
if (album.isCompilation) {
const compilationKey = 'compilation';
if (!acc[compilationKey]) {
acc[compilationKey] = [];
}
acc[compilationKey].push(album);
return acc;
}
// Priority 3: EP
const hasEPType = album.releaseTypes?.some((type) => type.toLowerCase() === 'ep');
if (hasEPType) {
const epKey = 'ep';
if (!acc[epKey]) {
acc[epKey] = [];
}
acc[epKey].push(album);
return acc;
}
// Priority 4: Single (other non-album types)
const hasAlbumType = album.releaseTypes?.some((type) => type.toLowerCase() === 'album');
if (!hasAlbumType) {
const singleKey = 'single';
if (!acc[singleKey]) {
acc[singleKey] = [];
}
acc[singleKey].push(album);
return acc;
}
// Priority 5: Album
const albumKey = 'album';
if (!acc[albumKey]) {
acc[albumKey] = [];
}
acc[albumKey].push(album);
return acc;
},
{} as Record<string, Album[]>,
);
return grouped;
};
const ArtistAlbums = () => { const ArtistAlbums = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const serverId = useCurrentServerId(); const serverId = useCurrentServerId();
@@ -838,6 +960,7 @@ const ArtistAlbums = () => {
const setAlbumArtistDetailSort = useAppStore((state) => state.actions.setAlbumArtistDetailSort); const setAlbumArtistDetailSort = useAppStore((state) => state.actions.setAlbumArtistDetailSort);
const sortBy = albumArtistDetailSort.sortBy; const sortBy = albumArtistDetailSort.sortBy;
const sortOrder = albumArtistDetailSort.sortOrder; const sortOrder = albumArtistDetailSort.sortOrder;
const groupingType = albumArtistDetailSort.groupingType;
const { albumArtistId, artistId } = useParams() as { const { albumArtistId, artistId } = useParams() as {
albumArtistId?: string; albumArtistId?: string;
@@ -868,68 +991,8 @@ const ArtistAlbums = () => {
}, [albumsQuery.data?.items, debouncedSearchTerm, sortBy, sortOrder]); }, [albumsQuery.data?.items, debouncedSearchTerm, sortBy, sortOrder]);
const albumsByReleaseType = useMemo(() => { const albumsByReleaseType = useMemo(() => {
const albums = filteredAndSortedAlbums; return groupAlbumsByReleaseType(filteredAndSortedAlbums, routeId, groupingType);
}, [filteredAndSortedAlbums, routeId, groupingType]);
const grouped = albums.reduce(
(acc, album) => {
// Priority 1: Appears on - artist is not an album artist
const isAlbumArtist = album.albumArtists?.some((artist) => artist.id === routeId);
if (!isAlbumArtist) {
const appearsOnKey = 'appears-on';
if (!acc[appearsOnKey]) {
acc[appearsOnKey] = [];
}
acc[appearsOnKey].push(album);
return acc;
}
// Priority 2: Compilations
if (album.isCompilation) {
const compilationKey = 'compilation';
if (!acc[compilationKey]) {
acc[compilationKey] = [];
}
acc[compilationKey].push(album);
return acc;
}
// Priority 3: EP
const hasEPType = album.releaseTypes?.some((type) => type.toLowerCase() === 'ep');
if (hasEPType) {
const epKey = 'ep';
if (!acc[epKey]) {
acc[epKey] = [];
}
acc[epKey].push(album);
return acc;
}
// Priority 4: Single (other non-album types)
const hasAlbumType = album.releaseTypes?.some(
(type) => type.toLowerCase() === 'album',
);
if (!hasAlbumType) {
const singleKey = 'single';
if (!acc[singleKey]) {
acc[singleKey] = [];
}
acc[singleKey].push(album);
return acc;
}
// Priority 5: Album
const albumKey = 'album';
if (!acc[albumKey]) {
acc[albumKey] = [];
}
acc[albumKey].push(album);
return acc;
},
{} as Record<string, Album[]>,
);
return grouped;
}, [filteredAndSortedAlbums, routeId]);
const releaseTypeEntries = useMemo(() => { const releaseTypeEntries = useMemo(() => {
const priorityOrder = ['album', 'ep', 'single', 'compilation', 'appears-on']; const priorityOrder = ['album', 'ep', 'single', 'compilation', 'appears-on'];
@@ -968,7 +1031,7 @@ const ArtistAlbums = () => {
}); });
break; break;
default: default:
displayName = releaseType; displayName = titleCase(releaseType);
} }
return { albums, displayName, releaseType }; return { albums, displayName, releaseType };
}) })
@@ -1033,6 +1096,7 @@ const ArtistAlbums = () => {
setSortOrder={(value) => setAlbumArtistDetailSort(sortBy, value as SortOrder)} setSortOrder={(value) => setAlbumArtistDetailSort(sortBy, value as SortOrder)}
sortOrder={sortOrder} sortOrder={sortOrder}
/> />
<GroupingTypeSelector />
</Group> </Group>
<div className={styles.albumSectionContainer} ref={cq.ref}> <div className={styles.albumSectionContainer} ref={cq.ref}>
{cq.isCalculated && ( {cq.isCalculated && (
@@ -1043,6 +1107,7 @@ const ArtistAlbums = () => {
controls={controls} controls={controls}
cq={cq} cq={cq}
key={releaseType} key={releaseType}
releaseType={releaseType}
rows={rows} rows={rows}
title={displayName} title={displayName}
/> />
@@ -1053,3 +1118,37 @@ const ArtistAlbums = () => {
</Stack> </Stack>
); );
}; };
function GroupingTypeSelector() {
const { t } = useTranslation();
const groupingType = useAppStore((state) => state.albumArtistDetailSort.groupingType);
const setAlbumArtistDetailGroupingType = useAppStore(
(state) => state.actions.setAlbumArtistDetailGroupingType,
);
return (
<DropdownMenu>
<DropdownMenu.Target>
<ActionIcon icon="settings" variant="subtle" />
</DropdownMenu.Target>
<DropdownMenu.Dropdown>
<DropdownMenu.Item
isSelected={groupingType === 'all'}
onClick={() => setAlbumArtistDetailGroupingType('all')}
>
{t('page.albumArtistDetail.groupingTypeAll', {
postProcess: 'sentenceCase',
})}
</DropdownMenu.Item>
<DropdownMenu.Item
isSelected={groupingType === 'primary'}
onClick={() => setAlbumArtistDetailGroupingType('primary')}
>
{t('page.albumArtistDetail.groupingTypePrimary', {
postProcess: 'sentenceCase',
})}
</DropdownMenu.Item>
</DropdownMenu.Dropdown>
</DropdownMenu>
);
}
+9 -1
View File
@@ -8,6 +8,7 @@ import { Platform } from '/@/shared/types/types';
export interface AppSlice extends AppState { export interface AppSlice extends AppState {
actions: { actions: {
setAlbumArtistDetailGroupingType: (groupingType: 'all' | 'primary') => void;
setAlbumArtistDetailSort: (sortBy: AlbumListSort, sortOrder: SortOrder) => void; setAlbumArtistDetailSort: (sortBy: AlbumListSort, sortOrder: SortOrder) => void;
setAppStore: (data: Partial<AppSlice>) => void; setAppStore: (data: Partial<AppSlice>) => void;
setPageSidebar: (key: string, value: boolean) => void; setPageSidebar: (key: string, value: boolean) => void;
@@ -20,6 +21,7 @@ export interface AppSlice extends AppState {
export interface AppState { export interface AppState {
albumArtistDetailSort: { albumArtistDetailSort: {
groupingType: 'all' | 'primary';
sortBy: AlbumListSort; sortBy: AlbumListSort;
sortOrder: SortOrder; sortOrder: SortOrder;
}; };
@@ -59,9 +61,14 @@ export const useAppStore = createWithEqualityFn<AppSlice>()(
devtools( devtools(
immer((set, get) => ({ immer((set, get) => ({
actions: { actions: {
setAlbumArtistDetailGroupingType: (groupingType) => {
set((state) => {
state.albumArtistDetailSort.groupingType = groupingType;
});
},
setAlbumArtistDetailSort: (sortBy, sortOrder) => { setAlbumArtistDetailSort: (sortBy, sortOrder) => {
set((state) => { set((state) => {
state.albumArtistDetailSort = { sortBy, sortOrder }; state.albumArtistDetailSort = { ...state.albumArtistDetailSort, sortBy, sortOrder };
}); });
}, },
setAppStore: (data) => { setAppStore: (data) => {
@@ -98,6 +105,7 @@ export const useAppStore = createWithEqualityFn<AppSlice>()(
}, },
}, },
albumArtistDetailSort: { albumArtistDetailSort: {
groupingType: 'primary',
sortBy: AlbumListSort.RELEASE_DATE, sortBy: AlbumListSort.RELEASE_DATE,
sortOrder: SortOrder.DESC, sortOrder: SortOrder.DESC,
}, },