reorder album artist page
This commit is contained in:
parent
eb50c69a35
commit
f7dd634f67
5 changed files with 201 additions and 98 deletions
|
@ -457,6 +457,8 @@
|
||||||
"albumBackgroundBlur_description": "adjusts the amount of blur applied to the album background image",
|
"albumBackgroundBlur_description": "adjusts the amount of blur applied to the album background image",
|
||||||
"applicationHotkeys": "application hotkeys",
|
"applicationHotkeys": "application hotkeys",
|
||||||
"applicationHotkeys_description": "configure application hotkeys. toggle the checkbox to set as a global hotkey (desktop only)",
|
"applicationHotkeys_description": "configure application hotkeys. toggle the checkbox to set as a global hotkey (desktop only)",
|
||||||
|
"artistConfiguration": "album artist page configuration",
|
||||||
|
"artistConfiguration_description": "configure what items are shown, and in what order, on the album artist page",
|
||||||
"audioDevice": "audio device",
|
"audioDevice": "audio device",
|
||||||
"audioDevice_description": "select the audio device to use for playback (web player only)",
|
"audioDevice_description": "select the audio device to use for playback (web player only)",
|
||||||
"audioExclusiveMode": "audio exclusive mode",
|
"audioExclusiveMode": "audio exclusive mode",
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { ColDef, RowDoubleClickedEvent } from '@ag-grid-community/core';
|
import { ColDef, RowDoubleClickedEvent } from '@ag-grid-community/core';
|
||||||
import { Box, Group, Stack } from '@mantine/core';
|
import { Box, Grid, Group, Stack } from '@mantine/core';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { FaLastfmSquare } from 'react-icons/fa';
|
import { FaLastfmSquare } from 'react-icons/fa';
|
||||||
import { RiHeartFill, RiHeartLine, RiMoreFill } from 'react-icons/ri';
|
import { RiHeartFill, RiHeartLine, RiMoreFill } from 'react-icons/ri';
|
||||||
|
@ -36,7 +36,7 @@ import { PlayButton, useCreateFavorite, useDeleteFavorite } from '/@/renderer/fe
|
||||||
import { LibraryBackgroundOverlay } from '/@/renderer/features/shared/components/library-background-overlay';
|
import { LibraryBackgroundOverlay } from '/@/renderer/features/shared/components/library-background-overlay';
|
||||||
import { useContainerQuery } from '/@/renderer/hooks';
|
import { useContainerQuery } from '/@/renderer/hooks';
|
||||||
import { AppRoute } from '/@/renderer/router/routes';
|
import { AppRoute } from '/@/renderer/router/routes';
|
||||||
import { useCurrentServer } from '/@/renderer/store';
|
import { ArtistItem, useCurrentServer } from '/@/renderer/store';
|
||||||
import { useGeneralSettings, usePlayButtonBehavior } from '/@/renderer/store/settings.store';
|
import { useGeneralSettings, usePlayButtonBehavior } from '/@/renderer/store/settings.store';
|
||||||
import { CardRow, Play, TableColumn } from '/@/renderer/types';
|
import { CardRow, Play, TableColumn } from '/@/renderer/types';
|
||||||
import { sanitize } from '/@/renderer/utils/sanitize';
|
import { sanitize } from '/@/renderer/utils/sanitize';
|
||||||
|
@ -65,13 +65,25 @@ interface AlbumArtistDetailContentProps {
|
||||||
|
|
||||||
export const AlbumArtistDetailContent = ({ background }: AlbumArtistDetailContentProps) => {
|
export const AlbumArtistDetailContent = ({ background }: AlbumArtistDetailContentProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { externalLinks } = useGeneralSettings();
|
const { artistItems, externalLinks } = useGeneralSettings();
|
||||||
const { albumArtistId } = useParams() as { albumArtistId: string };
|
const { albumArtistId } = useParams() as { albumArtistId: string };
|
||||||
const cq = useContainerQuery();
|
const cq = useContainerQuery();
|
||||||
const handlePlayQueueAdd = usePlayQueueAdd();
|
const handlePlayQueueAdd = usePlayQueueAdd();
|
||||||
const server = useCurrentServer();
|
const server = useCurrentServer();
|
||||||
const genrePath = useGenreRoute();
|
const genrePath = useGenreRoute();
|
||||||
|
|
||||||
|
const [enabledItem, itemOrder] = useMemo(() => {
|
||||||
|
const enabled: { [key in ArtistItem]?: boolean } = {};
|
||||||
|
const order: { [key in ArtistItem]?: number } = {};
|
||||||
|
|
||||||
|
for (const [idx, item] of artistItems.entries()) {
|
||||||
|
enabled[item.id] = !item.disabled;
|
||||||
|
order[item.id] = idx + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [enabled, order];
|
||||||
|
}, [artistItems]);
|
||||||
|
|
||||||
const detailQuery = useAlbumArtistDetail({
|
const detailQuery = useAlbumArtistDetail({
|
||||||
query: { id: albumArtistId },
|
query: { id: albumArtistId },
|
||||||
serverId: server?.id,
|
serverId: server?.id,
|
||||||
|
@ -95,6 +107,9 @@ export const AlbumArtistDetailContent = ({ background }: AlbumArtistDetailConten
|
||||||
})}`;
|
})}`;
|
||||||
|
|
||||||
const recentAlbumsQuery = useAlbumList({
|
const recentAlbumsQuery = useAlbumList({
|
||||||
|
options: {
|
||||||
|
enabled: enabledItem.recentAlbums,
|
||||||
|
},
|
||||||
query: {
|
query: {
|
||||||
_custom: {
|
_custom: {
|
||||||
jellyfin: {
|
jellyfin: {
|
||||||
|
@ -117,6 +132,9 @@ export const AlbumArtistDetailContent = ({ background }: AlbumArtistDetailConten
|
||||||
});
|
});
|
||||||
|
|
||||||
const compilationAlbumsQuery = useAlbumList({
|
const compilationAlbumsQuery = useAlbumList({
|
||||||
|
options: {
|
||||||
|
enabled: enabledItem.compilations,
|
||||||
|
},
|
||||||
query: {
|
query: {
|
||||||
_custom: {
|
_custom: {
|
||||||
jellyfin: {
|
jellyfin: {
|
||||||
|
@ -140,7 +158,7 @@ export const AlbumArtistDetailContent = ({ background }: AlbumArtistDetailConten
|
||||||
|
|
||||||
const topSongsQuery = useTopSongsList({
|
const topSongsQuery = useTopSongsList({
|
||||||
options: {
|
options: {
|
||||||
enabled: !!detailQuery?.data?.name,
|
enabled: !!detailQuery?.data?.name && enabledItem.topSongs,
|
||||||
},
|
},
|
||||||
query: {
|
query: {
|
||||||
artist: detailQuery?.data?.name || '',
|
artist: detailQuery?.data?.name || '',
|
||||||
|
@ -207,9 +225,10 @@ export const AlbumArtistDetailContent = ({ background }: AlbumArtistDetailConten
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
data: recentAlbumsQuery?.data?.items,
|
data: recentAlbumsQuery?.data?.items,
|
||||||
isHidden: !recentAlbumsQuery?.data?.items?.length,
|
isHidden: !recentAlbumsQuery?.data?.items?.length || !enabledItem.recentAlbums,
|
||||||
itemType: LibraryItem.ALBUM,
|
itemType: LibraryItem.ALBUM,
|
||||||
loading: recentAlbumsQuery?.isLoading || recentAlbumsQuery.isFetching,
|
loading: recentAlbumsQuery?.isLoading || recentAlbumsQuery.isFetching,
|
||||||
|
order: itemOrder.recentAlbums,
|
||||||
title: (
|
title: (
|
||||||
<Group align="flex-end">
|
<Group align="flex-end">
|
||||||
<TextTitle
|
<TextTitle
|
||||||
|
@ -235,9 +254,10 @@ export const AlbumArtistDetailContent = ({ background }: AlbumArtistDetailConten
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
data: compilationAlbumsQuery?.data?.items,
|
data: compilationAlbumsQuery?.data?.items,
|
||||||
isHidden: !compilationAlbumsQuery?.data?.items?.length,
|
isHidden: !compilationAlbumsQuery?.data?.items?.length || !enabledItem.compilations,
|
||||||
itemType: LibraryItem.ALBUM,
|
itemType: LibraryItem.ALBUM,
|
||||||
loading: compilationAlbumsQuery?.isLoading || compilationAlbumsQuery.isFetching,
|
loading: compilationAlbumsQuery?.isLoading || compilationAlbumsQuery.isFetching,
|
||||||
|
order: itemOrder.compilations,
|
||||||
title: (
|
title: (
|
||||||
<TextTitle
|
<TextTitle
|
||||||
order={2}
|
order={2}
|
||||||
|
@ -250,8 +270,9 @@ export const AlbumArtistDetailContent = ({ background }: AlbumArtistDetailConten
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
data: detailQuery?.data?.similarArtists || [],
|
data: detailQuery?.data?.similarArtists || [],
|
||||||
isHidden: !detailQuery?.data?.similarArtists,
|
isHidden: !detailQuery?.data?.similarArtists || !enabledItem.similarArtists,
|
||||||
itemType: LibraryItem.ALBUM_ARTIST,
|
itemType: LibraryItem.ALBUM_ARTIST,
|
||||||
|
order: itemOrder.similarArtists,
|
||||||
title: (
|
title: (
|
||||||
<TextTitle
|
<TextTitle
|
||||||
order={2}
|
order={2}
|
||||||
|
@ -271,6 +292,12 @@ export const AlbumArtistDetailContent = ({ background }: AlbumArtistDetailConten
|
||||||
compilationAlbumsQuery.isFetching,
|
compilationAlbumsQuery.isFetching,
|
||||||
compilationAlbumsQuery?.isLoading,
|
compilationAlbumsQuery?.isLoading,
|
||||||
detailQuery?.data?.similarArtists,
|
detailQuery?.data?.similarArtists,
|
||||||
|
enabledItem.compilations,
|
||||||
|
enabledItem.recentAlbums,
|
||||||
|
enabledItem.similarArtists,
|
||||||
|
itemOrder.compilations,
|
||||||
|
itemOrder.recentAlbums,
|
||||||
|
itemOrder.similarArtists,
|
||||||
recentAlbumsQuery?.data?.items,
|
recentAlbumsQuery?.data?.items,
|
||||||
recentAlbumsQuery.isFetching,
|
recentAlbumsQuery.isFetching,
|
||||||
recentAlbumsQuery?.isLoading,
|
recentAlbumsQuery?.isLoading,
|
||||||
|
@ -336,11 +363,11 @@ export const AlbumArtistDetailContent = ({ background }: AlbumArtistDetailConten
|
||||||
const biography = useMemo(() => {
|
const biography = useMemo(() => {
|
||||||
const bio = detailQuery?.data?.biography;
|
const bio = detailQuery?.data?.biography;
|
||||||
|
|
||||||
if (!bio) return null;
|
if (!bio || !enabledItem.biography) return null;
|
||||||
return sanitize(bio);
|
return sanitize(bio);
|
||||||
}, [detailQuery?.data?.biography]);
|
}, [detailQuery?.data?.biography, enabledItem.biography]);
|
||||||
|
|
||||||
const showTopSongs = topSongsQuery?.data?.items?.length;
|
const showTopSongs = topSongsQuery?.data?.items?.length && enabledItem.topSongs;
|
||||||
const showGenres = detailQuery?.data?.genres ? detailQuery?.data?.genres.length !== 0 : false;
|
const showGenres = detailQuery?.data?.genres ? detailQuery?.data?.genres.length !== 0 : false;
|
||||||
const mbzId = detailQuery?.data?.mbz;
|
const mbzId = detailQuery?.data?.mbz;
|
||||||
|
|
||||||
|
@ -467,7 +494,12 @@ export const AlbumArtistDetailContent = ({ background }: AlbumArtistDetailConten
|
||||||
</Group>
|
</Group>
|
||||||
</Box>
|
</Box>
|
||||||
) : null}
|
) : null}
|
||||||
|
<Grid>
|
||||||
{biography ? (
|
{biography ? (
|
||||||
|
<Grid.Col
|
||||||
|
order={itemOrder.biography}
|
||||||
|
span={12}
|
||||||
|
>
|
||||||
<Box
|
<Box
|
||||||
component="section"
|
component="section"
|
||||||
maw="1280px"
|
maw="1280px"
|
||||||
|
@ -482,8 +514,13 @@ export const AlbumArtistDetailContent = ({ background }: AlbumArtistDetailConten
|
||||||
</TextTitle>
|
</TextTitle>
|
||||||
<Spoiler dangerouslySetInnerHTML={{ __html: biography }} />
|
<Spoiler dangerouslySetInnerHTML={{ __html: biography }} />
|
||||||
</Box>
|
</Box>
|
||||||
|
</Grid.Col>
|
||||||
) : null}
|
) : null}
|
||||||
{showTopSongs ? (
|
{showTopSongs ? (
|
||||||
|
<Grid.Col
|
||||||
|
order={itemOrder.topSongs}
|
||||||
|
span={12}
|
||||||
|
>
|
||||||
<Box component="section">
|
<Box component="section">
|
||||||
<Group
|
<Group
|
||||||
noWrap
|
noWrap
|
||||||
|
@ -538,19 +575,31 @@ export const AlbumArtistDetailContent = ({ background }: AlbumArtistDetailConten
|
||||||
onRowDoubleClicked={handleRowDoubleClick}
|
onRowDoubleClicked={handleRowDoubleClick}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
</Grid.Col>
|
||||||
) : null}
|
) : null}
|
||||||
<Box component="section">
|
|
||||||
<Stack spacing="xl">
|
|
||||||
{carousels
|
{carousels
|
||||||
.filter((c) => !c.isHidden)
|
.filter((c) => !c.isHidden)
|
||||||
.map((carousel) => (
|
.map((carousel) => (
|
||||||
<MemoizedSwiperGridCarousel
|
<Grid.Col
|
||||||
key={`carousel-${carousel.uniqueId}`}
|
key={`carousel-${carousel.uniqueId}`}
|
||||||
cardRows={cardRows[carousel.itemType as keyof typeof cardRows]}
|
order={carousel.order}
|
||||||
|
span={12}
|
||||||
|
>
|
||||||
|
<Box component="section">
|
||||||
|
<Stack spacing="xl">
|
||||||
|
<MemoizedSwiperGridCarousel
|
||||||
|
cardRows={
|
||||||
|
cardRows[carousel.itemType as keyof typeof cardRows]
|
||||||
|
}
|
||||||
data={carousel.data}
|
data={carousel.data}
|
||||||
isLoading={carousel.loading}
|
isLoading={carousel.loading}
|
||||||
itemType={carousel.itemType}
|
itemType={carousel.itemType}
|
||||||
route={cardRoutes[carousel.itemType as keyof typeof cardRoutes]}
|
route={
|
||||||
|
cardRoutes[
|
||||||
|
carousel.itemType as keyof typeof cardRoutes
|
||||||
|
]
|
||||||
|
}
|
||||||
swiperProps={{
|
swiperProps={{
|
||||||
grid: {
|
grid: {
|
||||||
rows: 2,
|
rows: 2,
|
||||||
|
@ -561,9 +610,11 @@ export const AlbumArtistDetailContent = ({ background }: AlbumArtistDetailConten
|
||||||
}}
|
}}
|
||||||
uniqueId={carousel.uniqueId}
|
uniqueId={carousel.uniqueId}
|
||||||
/>
|
/>
|
||||||
))}
|
|
||||||
</Stack>
|
</Stack>
|
||||||
</Box>
|
</Box>
|
||||||
|
</Grid.Col>
|
||||||
|
))}
|
||||||
|
</Grid>
|
||||||
</DetailContainer>
|
</DetailContainer>
|
||||||
</ContentContainer>
|
</ContentContainer>
|
||||||
);
|
);
|
||||||
|
|
|
@ -0,0 +1,25 @@
|
||||||
|
import { DraggableItems } from '/@/renderer/features/settings/components/general/draggable-items';
|
||||||
|
import { ArtistItem, useGeneralSettings, useSettingsStoreActions } from '/@/renderer/store';
|
||||||
|
|
||||||
|
const ARTIST_ITEMS: Array<[ArtistItem, string]> = [
|
||||||
|
[ArtistItem.BIOGRAPHY, 'table.column.biography'],
|
||||||
|
[ArtistItem.TOP_SONGS, 'page.albumArtistDetail.topSongs'],
|
||||||
|
[ArtistItem.RECENT_ALBUMS, 'page.albumArtistDetail.recentReleases'],
|
||||||
|
[ArtistItem.COMPILATIONS, 'page.albumArtistDetail.appearsOn'],
|
||||||
|
[ArtistItem.SIMILAR_ARTISTS, 'page.albumArtistDetail.relatedArtists'],
|
||||||
|
];
|
||||||
|
|
||||||
|
export const ArtistSettings = () => {
|
||||||
|
const { artistItems } = useGeneralSettings();
|
||||||
|
const { setArtistItems } = useSettingsStoreActions();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DraggableItems
|
||||||
|
description="setting.artistConfiguration"
|
||||||
|
itemLabels={ARTIST_ITEMS}
|
||||||
|
setItems={setArtistItems}
|
||||||
|
settings={artistItems}
|
||||||
|
title="setting.artistConfiguration"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
|
@ -9,6 +9,7 @@ import isElectron from 'is-electron';
|
||||||
import { HomeSettings } from '/@/renderer/features/settings/components/general/home-settings';
|
import { HomeSettings } from '/@/renderer/features/settings/components/general/home-settings';
|
||||||
import { SidebarReorder } from '/@/renderer/features/settings/components/general/sidebar-reorder';
|
import { SidebarReorder } from '/@/renderer/features/settings/components/general/sidebar-reorder';
|
||||||
import { ContextMenuSettings } from '/@/renderer/features/settings/components/general/context-menu-settings';
|
import { ContextMenuSettings } from '/@/renderer/features/settings/components/general/context-menu-settings';
|
||||||
|
import { ArtistSettings } from '/@/renderer/features/settings/components/general/artist-settings';
|
||||||
|
|
||||||
export const GeneralTab = () => {
|
export const GeneralTab = () => {
|
||||||
return (
|
return (
|
||||||
|
@ -17,6 +18,7 @@ export const GeneralTab = () => {
|
||||||
<ThemeSettings />
|
<ThemeSettings />
|
||||||
<ControlSettings />
|
<ControlSettings />
|
||||||
<HomeSettings />
|
<HomeSettings />
|
||||||
|
<ArtistSettings />
|
||||||
<SidebarReorder />
|
<SidebarReorder />
|
||||||
<SidebarSettings />
|
<SidebarSettings />
|
||||||
<ContextMenuSettings />
|
<ContextMenuSettings />
|
||||||
|
|
|
@ -105,7 +105,22 @@ export enum HomeItem {
|
||||||
RECENTLY_PLAYED = 'recentlyPlayed',
|
RECENTLY_PLAYED = 'recentlyPlayed',
|
||||||
}
|
}
|
||||||
|
|
||||||
export const homeItems = Object.values(HomeItem).map((item) => ({
|
const homeItems = Object.values(HomeItem).map((item) => ({
|
||||||
|
disabled: false,
|
||||||
|
id: item,
|
||||||
|
}));
|
||||||
|
|
||||||
|
/* eslint-disable typescript-sort-keys/string-enum */
|
||||||
|
export enum ArtistItem {
|
||||||
|
BIOGRAPHY = 'biography',
|
||||||
|
TOP_SONGS = 'topSongs',
|
||||||
|
RECENT_ALBUMS = 'recentAlbums',
|
||||||
|
COMPILATIONS = 'compilations',
|
||||||
|
SIMILAR_ARTISTS = 'similarArtists',
|
||||||
|
}
|
||||||
|
/* eslint-enable typescript-sort-keys/string-enum */
|
||||||
|
|
||||||
|
const artistItems = Object.values(ArtistItem).map((item) => ({
|
||||||
disabled: false,
|
disabled: false,
|
||||||
id: item,
|
id: item,
|
||||||
}));
|
}));
|
||||||
|
@ -207,6 +222,7 @@ export interface SettingsState {
|
||||||
albumArtRes?: number | null;
|
albumArtRes?: number | null;
|
||||||
albumBackground: boolean;
|
albumBackground: boolean;
|
||||||
albumBackgroundBlur: number;
|
albumBackgroundBlur: number;
|
||||||
|
artistItems: SortableItem<ArtistItem>[];
|
||||||
buttonSize: number;
|
buttonSize: number;
|
||||||
defaultFullPlaylist: boolean;
|
defaultFullPlaylist: boolean;
|
||||||
disabledContextMenu: { [k in ContextMenuItemType]?: boolean };
|
disabledContextMenu: { [k in ContextMenuItemType]?: boolean };
|
||||||
|
@ -305,6 +321,7 @@ export interface SettingsSlice extends SettingsState {
|
||||||
actions: {
|
actions: {
|
||||||
reset: () => void;
|
reset: () => void;
|
||||||
resetSampleRate: () => void;
|
resetSampleRate: () => void;
|
||||||
|
setArtistItems: (item: SortableItem<ArtistItem>[]) => void;
|
||||||
setGenreBehavior: (target: GenreTarget) => void;
|
setGenreBehavior: (target: GenreTarget) => void;
|
||||||
setHomeItems: (item: SortableItem<HomeItem>[]) => void;
|
setHomeItems: (item: SortableItem<HomeItem>[]) => void;
|
||||||
setSettings: (data: Partial<SettingsState>) => void;
|
setSettings: (data: Partial<SettingsState>) => void;
|
||||||
|
@ -347,6 +364,7 @@ const initialState: SettingsState = {
|
||||||
albumArtRes: undefined,
|
albumArtRes: undefined,
|
||||||
albumBackground: false,
|
albumBackground: false,
|
||||||
albumBackgroundBlur: 6,
|
albumBackgroundBlur: 6,
|
||||||
|
artistItems,
|
||||||
buttonSize: 20,
|
buttonSize: 20,
|
||||||
defaultFullPlaylist: true,
|
defaultFullPlaylist: true,
|
||||||
disabledContextMenu: {},
|
disabledContextMenu: {},
|
||||||
|
@ -656,6 +674,11 @@ export const useSettingsStore = create<SettingsSlice>()(
|
||||||
state.playback.mpvProperties.audioSampleRateHz = 0;
|
state.playback.mpvProperties.audioSampleRateHz = 0;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
setArtistItems: (items) => {
|
||||||
|
set((state) => {
|
||||||
|
state.general.artistItems = items;
|
||||||
|
});
|
||||||
|
},
|
||||||
setGenreBehavior: (target: GenreTarget) => {
|
setGenreBehavior: (target: GenreTarget) => {
|
||||||
set((state) => {
|
set((state) => {
|
||||||
state.general.genreTarget = target;
|
state.general.genreTarget = target;
|
||||||
|
|
Reference in a new issue