mirror of
https://github.com/jeffvli/feishin.git
synced 2026-06-12 23:32:19 +02:00
add personal/community toggle for artist top songs (#1372)
This commit is contained in:
@@ -425,7 +425,9 @@
|
|||||||
"viewDiscography": "view discography",
|
"viewDiscography": "view discography",
|
||||||
"relatedArtists": "related $t(entity.artist, {\"count\": 2})",
|
"relatedArtists": "related $t(entity.artist, {\"count\": 2})",
|
||||||
"topSongs": "top songs",
|
"topSongs": "top songs",
|
||||||
|
"topSongsCommunity": "community",
|
||||||
"topSongsFrom": "top songs from {{title}}",
|
"topSongsFrom": "top songs from {{title}}",
|
||||||
|
"topSongsPersonal": "personal",
|
||||||
"favoriteSongsFrom": "favorite songs from {{title}}",
|
"favoriteSongsFrom": "favorite songs from {{title}}",
|
||||||
"viewAll": "view all",
|
"viewAll": "view all",
|
||||||
"viewAllTracks": "view all $t(entity.track, {\"count\": 2})"
|
"viewAllTracks": "view all $t(entity.track, {\"count\": 2})"
|
||||||
|
|||||||
@@ -1281,17 +1281,26 @@ export const JellyfinController: InternalControllerEndpoint = {
|
|||||||
throw new Error('No userId found');
|
throw new Error('No userId found');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const type = query.type === 'personal' ? 'personal' : 'community';
|
||||||
|
|
||||||
const res = await jfApiClient(apiClientProps).getTopSongsList({
|
const res = await jfApiClient(apiClientProps).getTopSongsList({
|
||||||
params: {
|
params: {
|
||||||
userId: apiClientProps.server?.userId,
|
userId: apiClientProps.server?.userId,
|
||||||
},
|
},
|
||||||
query: {
|
query: {
|
||||||
ArtistIds: query.artistId,
|
ArtistIds: query.artistId,
|
||||||
Fields: 'Genres, DateCreated, MediaSources, ParentId, SortName',
|
Fields:
|
||||||
|
type === 'personal'
|
||||||
|
? 'Genres, DateCreated, MediaSources, ParentId, SortName, UserData'
|
||||||
|
: 'Genres, DateCreated, MediaSources, ParentId, SortName',
|
||||||
|
|
||||||
IncludeItemTypes: 'Audio',
|
IncludeItemTypes: 'Audio',
|
||||||
Limit: query.limit,
|
Limit: query.limit,
|
||||||
Recursive: true,
|
Recursive: true,
|
||||||
SortBy: 'CommunityRating,SortName',
|
SortBy:
|
||||||
|
type === 'personal'
|
||||||
|
? JFSongListSort.PLAY_COUNT
|
||||||
|
: JFSongListSort.COMMUNITY_RATING,
|
||||||
SortOrder: 'Descending',
|
SortOrder: 'Descending',
|
||||||
UserId: apiClientProps.server?.userId,
|
UserId: apiClientProps.server?.userId,
|
||||||
},
|
},
|
||||||
@@ -1301,15 +1310,31 @@ export const JellyfinController: InternalControllerEndpoint = {
|
|||||||
throw new Error('Failed to get top song list');
|
throw new Error('Failed to get top song list');
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
const items = res.body.Items.map((item) =>
|
||||||
items: res.body.Items.map((item) =>
|
jfNormalize.song(
|
||||||
jfNormalize.song(
|
item,
|
||||||
item,
|
apiClientProps.server,
|
||||||
apiClientProps.server,
|
args.context?.pathReplace,
|
||||||
args.context?.pathReplace,
|
args.context?.pathReplaceWith,
|
||||||
args.context?.pathReplaceWith,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (type === 'personal') {
|
||||||
|
const sorted = orderBy(
|
||||||
|
items,
|
||||||
|
['playCount', 'albumId', 'trackNumber'],
|
||||||
|
['desc', 'asc', 'asc'],
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
items: sorted,
|
||||||
|
startIndex: 0,
|
||||||
|
totalRecordCount: res.body.TotalRecordCount,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
items,
|
||||||
startIndex: 0,
|
startIndex: 0,
|
||||||
totalRecordCount: res.body.TotalRecordCount,
|
totalRecordCount: res.body.TotalRecordCount,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { set } from 'idb-keyval';
|
import { set } from 'idb-keyval';
|
||||||
|
import orderBy from 'lodash/orderBy';
|
||||||
|
|
||||||
import { ndApiClient } from '/@/renderer/api/navidrome/navidrome-api';
|
import { ndApiClient } from '/@/renderer/api/navidrome/navidrome-api';
|
||||||
import { ssApiClient } from '/@/renderer/api/subsonic/subsonic-api';
|
import { ssApiClient } from '/@/renderer/api/subsonic/subsonic-api';
|
||||||
@@ -17,7 +18,9 @@ import {
|
|||||||
PlaylistSongListArgs,
|
PlaylistSongListArgs,
|
||||||
PlaylistSongListResponse,
|
PlaylistSongListResponse,
|
||||||
ServerListItemWithCredential,
|
ServerListItemWithCredential,
|
||||||
|
SongListSort,
|
||||||
songListSortMap,
|
songListSortMap,
|
||||||
|
SortOrder,
|
||||||
sortOrderMap,
|
sortOrderMap,
|
||||||
tagListSortMap,
|
tagListSortMap,
|
||||||
userListSortMap,
|
userListSortMap,
|
||||||
@@ -807,7 +810,59 @@ export const NavidromeController: InternalControllerEndpoint = {
|
|||||||
tags,
|
tags,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
getTopSongs: SubsonicController.getTopSongs,
|
getTopSongs: async (args) => {
|
||||||
|
const { apiClientProps, query } = args;
|
||||||
|
|
||||||
|
const type = query.type === 'personal' ? 'personal' : 'community';
|
||||||
|
|
||||||
|
if (type === 'community') {
|
||||||
|
const res = await ssApiClient(apiClientProps).getTopSongsList({
|
||||||
|
query: {
|
||||||
|
artist: query.artist,
|
||||||
|
count: query.limit,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.status !== 200) {
|
||||||
|
throw new Error('Failed to get top songs');
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
items: (res.body.topSongs?.song || []).map((song) =>
|
||||||
|
ssNormalize.song(
|
||||||
|
song,
|
||||||
|
apiClientProps.server,
|
||||||
|
args.context?.pathReplace,
|
||||||
|
args.context?.pathReplaceWith,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
startIndex: 0,
|
||||||
|
totalRecordCount: res.body.topSongs?.song?.length || 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await NavidromeController.getSongList({
|
||||||
|
apiClientProps,
|
||||||
|
query: {
|
||||||
|
artistIds: [query.artistId],
|
||||||
|
sortBy: SongListSort.PLAY_COUNT,
|
||||||
|
sortOrder: SortOrder.DESC,
|
||||||
|
startIndex: 0,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const songsWithPlayCount = orderBy(
|
||||||
|
res.items.filter((song) => song.playCount > 0),
|
||||||
|
['playCount', 'albumId', 'trackNumber'],
|
||||||
|
['desc', 'asc', 'asc'],
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
items: songsWithPlayCount,
|
||||||
|
startIndex: 0,
|
||||||
|
totalRecordCount: res.totalRecordCount,
|
||||||
|
};
|
||||||
|
},
|
||||||
getUserInfo: SubsonicController.getUserInfo,
|
getUserInfo: SubsonicController.getUserInfo,
|
||||||
getUserList: async (args) => {
|
getUserList: async (args) => {
|
||||||
const { apiClientProps, query } = args;
|
const { apiClientProps, query } = args;
|
||||||
|
|||||||
@@ -1794,29 +1794,54 @@ export const SubsonicController: InternalControllerEndpoint = {
|
|||||||
getTopSongs: async (args) => {
|
getTopSongs: async (args) => {
|
||||||
const { apiClientProps, context, query } = args;
|
const { apiClientProps, context, query } = args;
|
||||||
|
|
||||||
const res = await ssApiClient(apiClientProps).getTopSongsList({
|
const type = query.type === 'personal' ? 'personal' : 'community';
|
||||||
query: {
|
|
||||||
artist: query.artist,
|
|
||||||
count: query.limit,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (res.status !== 200) {
|
if (type === 'community') {
|
||||||
throw new Error('Failed to get top songs');
|
const res = await ssApiClient(apiClientProps).getTopSongsList({
|
||||||
}
|
query: {
|
||||||
|
artist: query.artist,
|
||||||
|
count: query.limit,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
if (res.status !== 200) {
|
||||||
items:
|
throw new Error('Failed to get top songs');
|
||||||
res.body.topSongs?.song?.map((song) =>
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
items: (res.body.topSongs?.song || []).map((song) =>
|
||||||
ssNormalize.song(
|
ssNormalize.song(
|
||||||
song,
|
song,
|
||||||
apiClientProps.server,
|
apiClientProps.server,
|
||||||
context?.pathReplace,
|
context?.pathReplace,
|
||||||
context?.pathReplaceWith,
|
context?.pathReplaceWith,
|
||||||
),
|
),
|
||||||
) || [],
|
),
|
||||||
|
startIndex: 0,
|
||||||
|
totalRecordCount: res.body.topSongs?.song?.length || 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await SubsonicController.getSongList({
|
||||||
|
apiClientProps,
|
||||||
|
query: {
|
||||||
|
artistIds: [query.artistId],
|
||||||
|
sortBy: SongListSort.PLAY_COUNT,
|
||||||
|
sortOrder: SortOrder.DESC,
|
||||||
|
startIndex: 0,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const songsWithPlayCount = orderBy(
|
||||||
|
res.items.filter((song) => song.playCount > 0),
|
||||||
|
['playCount', 'albumId', 'trackNumber'],
|
||||||
|
['desc', 'asc', 'asc'],
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
items: songsWithPlayCount,
|
||||||
startIndex: 0,
|
startIndex: 0,
|
||||||
totalRecordCount: res.body.topSongs?.song?.length || 0,
|
totalRecordCount: res.totalRecordCount,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
getUserInfo: async (args) => {
|
getUserInfo: async (args) => {
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ 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';
|
||||||
|
import { SegmentedControl } from '/@/shared/components/segmented-control/segmented-control';
|
||||||
import { Spinner } from '/@/shared/components/spinner/spinner';
|
import { Spinner } from '/@/shared/components/spinner/spinner';
|
||||||
import { Spoiler } from '/@/shared/components/spoiler/spoiler';
|
import { Spoiler } from '/@/shared/components/spoiler/spoiler';
|
||||||
import { Stack } from '/@/shared/components/stack/stack';
|
import { Stack } from '/@/shared/components/stack/stack';
|
||||||
@@ -74,6 +75,7 @@ import { TextTitle } from '/@/shared/components/text-title/text-title';
|
|||||||
import { Text } from '/@/shared/components/text/text';
|
import { Text } from '/@/shared/components/text/text';
|
||||||
import { useDebouncedValue } from '/@/shared/hooks/use-debounced-value';
|
import { useDebouncedValue } from '/@/shared/hooks/use-debounced-value';
|
||||||
import { useHotkeys } from '/@/shared/hooks/use-hotkeys';
|
import { useHotkeys } from '/@/shared/hooks/use-hotkeys';
|
||||||
|
import { useLocalStorage } from '/@/shared/hooks/use-local-storage';
|
||||||
import {
|
import {
|
||||||
Album,
|
Album,
|
||||||
AlbumArtist,
|
AlbumArtist,
|
||||||
@@ -236,6 +238,10 @@ const AlbumArtistMetadataTopSongsContent = ({
|
|||||||
const [searchTerm, setSearchTerm] = useState('');
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
const [debouncedSearchTerm] = useDebouncedValue(searchTerm, 300);
|
const [debouncedSearchTerm] = useDebouncedValue(searchTerm, 300);
|
||||||
const [showAll, setShowAll] = useState(false);
|
const [showAll, setShowAll] = useState(false);
|
||||||
|
const [topSongsQueryType, setTopSongsQueryType] = useLocalStorage<'community' | 'personal'>({
|
||||||
|
defaultValue: 'community',
|
||||||
|
key: 'album-artist-top-songs-query-type',
|
||||||
|
});
|
||||||
const tableConfig = useSettingsStore((state) => state.lists[ItemListKey.SONG]?.table);
|
const tableConfig = useSettingsStore((state) => state.lists[ItemListKey.SONG]?.table);
|
||||||
const currentSong = usePlayerSong();
|
const currentSong = usePlayerSong();
|
||||||
const player = usePlayer();
|
const player = usePlayer();
|
||||||
@@ -249,6 +255,7 @@ const AlbumArtistMetadataTopSongsContent = ({
|
|||||||
query: {
|
query: {
|
||||||
artist: detailQuery.data?.name || '',
|
artist: detailQuery.data?.name || '',
|
||||||
artistId: routeId,
|
artistId: routeId,
|
||||||
|
type: topSongsQueryType,
|
||||||
},
|
},
|
||||||
serverId: serverId,
|
serverId: serverId,
|
||||||
}),
|
}),
|
||||||
@@ -316,15 +323,9 @@ const AlbumArtistMetadataTopSongsContent = ({
|
|||||||
onLongPress: () => handlePlay(LONG_PRESS_PLAY_BEHAVIOR[Play.LAST]),
|
onLongPress: () => handlePlay(LONG_PRESS_PLAY_BEHAVIOR[Play.LAST]),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (topSongsQuery.isLoading || !topSongsQuery.data) {
|
const isLoading = topSongsQuery.isLoading || !topSongsQuery.data;
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!topSongsQuery?.data?.items?.length) return null;
|
if (!isLoading && !tableConfig) return null;
|
||||||
|
|
||||||
if (!tableConfig) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentSongId = currentSong?.id;
|
const currentSongId = currentSong?.id;
|
||||||
|
|
||||||
@@ -338,7 +339,7 @@ const AlbumArtistMetadataTopSongsContent = ({
|
|||||||
postProcess: 'sentenceCase',
|
postProcess: 'sentenceCase',
|
||||||
})}
|
})}
|
||||||
</TextTitle>
|
</TextTitle>
|
||||||
<Badge>{songs.length}</Badge>
|
{!isLoading && <Badge>{songs.length}</Badge>}
|
||||||
</Group>
|
</Group>
|
||||||
<div className={styles.albumSectionDividerContainer}>
|
<div className={styles.albumSectionDividerContainer}>
|
||||||
<div className={styles.albumSectionDivider} />
|
<div className={styles.albumSectionDivider} />
|
||||||
@@ -365,6 +366,7 @@ const AlbumArtistMetadataTopSongsContent = ({
|
|||||||
variant="subtle"
|
variant="subtle"
|
||||||
{...handlePlayNow.handlers}
|
{...handlePlayNow.handlers}
|
||||||
{...handlePlayNow.props}
|
{...handlePlayNow.props}
|
||||||
|
disabled={isLoading}
|
||||||
/>
|
/>
|
||||||
</PlayTooltip>
|
</PlayTooltip>
|
||||||
<PlayTooltip type={Play.NEXT}>
|
<PlayTooltip type={Play.NEXT}>
|
||||||
@@ -375,6 +377,7 @@ const AlbumArtistMetadataTopSongsContent = ({
|
|||||||
variant="subtle"
|
variant="subtle"
|
||||||
{...handlePlayNext.handlers}
|
{...handlePlayNext.handlers}
|
||||||
{...handlePlayNext.props}
|
{...handlePlayNext.props}
|
||||||
|
disabled={isLoading}
|
||||||
/>
|
/>
|
||||||
</PlayTooltip>
|
</PlayTooltip>
|
||||||
<PlayTooltip type={Play.LAST}>
|
<PlayTooltip type={Play.LAST}>
|
||||||
@@ -385,78 +388,108 @@ const AlbumArtistMetadataTopSongsContent = ({
|
|||||||
variant="subtle"
|
variant="subtle"
|
||||||
{...handlePlayLast.handlers}
|
{...handlePlayLast.handlers}
|
||||||
{...handlePlayLast.props}
|
{...handlePlayLast.props}
|
||||||
|
disabled={isLoading}
|
||||||
/>
|
/>
|
||||||
</PlayTooltip>
|
</PlayTooltip>
|
||||||
</ActionIconGroup>
|
</ActionIconGroup>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Group gap="sm" w="100%">
|
{isLoading ? (
|
||||||
<TextInput
|
<Group justify="center" py="md">
|
||||||
flex={1}
|
<Spinner container />
|
||||||
leftSection={<Icon icon="search" />}
|
|
||||||
onChange={(e) => setSearchTerm(e.target.value)}
|
|
||||||
placeholder={t('common.search', { postProcess: 'sentenceCase' })}
|
|
||||||
radius="xl"
|
|
||||||
rightSection={
|
|
||||||
searchTerm ? (
|
|
||||||
<ActionIcon
|
|
||||||
icon="x"
|
|
||||||
onClick={() => setSearchTerm('')}
|
|
||||||
size="sm"
|
|
||||||
variant="transparent"
|
|
||||||
/>
|
|
||||||
) : null
|
|
||||||
}
|
|
||||||
styles={{
|
|
||||||
input: {
|
|
||||||
background: 'transparent',
|
|
||||||
border: '1px solid rgba(255, 255, 255, 0.05)',
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
value={searchTerm}
|
|
||||||
/>
|
|
||||||
<ListConfigMenu
|
|
||||||
displayTypes={[{ hidden: true, value: ListDisplayType.GRID }]}
|
|
||||||
listKey={ItemListKey.SONG}
|
|
||||||
optionsConfig={{
|
|
||||||
table: {
|
|
||||||
itemsPerPage: { hidden: true },
|
|
||||||
pagination: { hidden: true },
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
tableColumnsData={SONG_TABLE_COLUMNS}
|
|
||||||
/>
|
|
||||||
</Group>
|
|
||||||
<ItemTableList
|
|
||||||
activeRowId={currentSongId}
|
|
||||||
autoFitColumns={tableConfig.autoFitColumns}
|
|
||||||
CellComponent={ItemTableListColumn}
|
|
||||||
columns={columns}
|
|
||||||
data={filteredSongs}
|
|
||||||
enableAlternateRowColors={tableConfig.enableAlternateRowColors}
|
|
||||||
enableDrag
|
|
||||||
enableDragScroll={false}
|
|
||||||
enableExpansion={false}
|
|
||||||
enableHeader={tableConfig.enableHeader}
|
|
||||||
enableHorizontalBorders={tableConfig.enableHorizontalBorders}
|
|
||||||
enableRowHoverHighlight={tableConfig.enableRowHoverHighlight}
|
|
||||||
enableSelection
|
|
||||||
enableSelectionDialog={false}
|
|
||||||
enableVerticalBorders={tableConfig.enableVerticalBorders}
|
|
||||||
itemType={LibraryItem.SONG}
|
|
||||||
onColumnReordered={handleColumnReordered}
|
|
||||||
onColumnResized={handleColumnResized}
|
|
||||||
overrideControls={overrideControls}
|
|
||||||
size={tableConfig.size}
|
|
||||||
/>
|
|
||||||
{!searchTerm.trim() && songs.length > 5 && !showAll && (
|
|
||||||
<Group justify="center" w="100%">
|
|
||||||
<Button onClick={() => setShowAll(true)} variant="subtle">
|
|
||||||
{t('action.viewMore', { postProcess: 'sentenceCase' })}
|
|
||||||
</Button>
|
|
||||||
</Group>
|
</Group>
|
||||||
)}
|
) : tableConfig ? (
|
||||||
|
<>
|
||||||
|
<Group gap="sm" w="100%">
|
||||||
|
<TextInput
|
||||||
|
flex={1}
|
||||||
|
leftSection={<Icon icon="search" />}
|
||||||
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
|
placeholder={t('common.search', { postProcess: 'sentenceCase' })}
|
||||||
|
radius="xl"
|
||||||
|
rightSection={
|
||||||
|
searchTerm ? (
|
||||||
|
<ActionIcon
|
||||||
|
icon="x"
|
||||||
|
onClick={() => setSearchTerm('')}
|
||||||
|
size="sm"
|
||||||
|
variant="transparent"
|
||||||
|
/>
|
||||||
|
) : null
|
||||||
|
}
|
||||||
|
styles={{
|
||||||
|
input: {
|
||||||
|
background: 'transparent',
|
||||||
|
border: '1px solid rgba(255, 255, 255, 0.05)',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
value={searchTerm}
|
||||||
|
/>
|
||||||
|
<SegmentedControl
|
||||||
|
data={[
|
||||||
|
{
|
||||||
|
label: t('page.albumArtistDetail.topSongsCommunity', {
|
||||||
|
postProcess: 'sentenceCase',
|
||||||
|
}),
|
||||||
|
value: 'community',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('page.albumArtistDetail.topSongsPersonal', {
|
||||||
|
postProcess: 'sentenceCase',
|
||||||
|
}),
|
||||||
|
value: 'personal',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
onChange={(value) =>
|
||||||
|
setTopSongsQueryType(value as 'community' | 'personal')
|
||||||
|
}
|
||||||
|
size="xs"
|
||||||
|
value={topSongsQueryType}
|
||||||
|
/>
|
||||||
|
<ListConfigMenu
|
||||||
|
displayTypes={[{ hidden: true, value: ListDisplayType.GRID }]}
|
||||||
|
listKey={ItemListKey.SONG}
|
||||||
|
optionsConfig={{
|
||||||
|
table: {
|
||||||
|
itemsPerPage: { hidden: true },
|
||||||
|
pagination: { hidden: true },
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
tableColumnsData={SONG_TABLE_COLUMNS}
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
<ItemTableList
|
||||||
|
activeRowId={currentSongId}
|
||||||
|
autoFitColumns={tableConfig.autoFitColumns}
|
||||||
|
CellComponent={ItemTableListColumn}
|
||||||
|
columns={columns}
|
||||||
|
data={filteredSongs}
|
||||||
|
enableAlternateRowColors={tableConfig.enableAlternateRowColors}
|
||||||
|
enableDrag
|
||||||
|
enableDragScroll={false}
|
||||||
|
enableExpansion={false}
|
||||||
|
enableHeader={tableConfig.enableHeader}
|
||||||
|
enableHorizontalBorders={tableConfig.enableHorizontalBorders}
|
||||||
|
enableRowHoverHighlight={tableConfig.enableRowHoverHighlight}
|
||||||
|
enableSelection
|
||||||
|
enableSelectionDialog={false}
|
||||||
|
enableVerticalBorders={tableConfig.enableVerticalBorders}
|
||||||
|
itemType={LibraryItem.SONG}
|
||||||
|
onColumnReordered={handleColumnReordered}
|
||||||
|
onColumnResized={handleColumnResized}
|
||||||
|
overrideControls={overrideControls}
|
||||||
|
size={tableConfig.size}
|
||||||
|
/>
|
||||||
|
{!searchTerm.trim() && songs.length > 5 && !showAll && (
|
||||||
|
<Group justify="center" w="100%">
|
||||||
|
<Button onClick={() => setShowAll(true)} variant="subtle">
|
||||||
|
{t('action.viewMore', { postProcess: 'sentenceCase' })}
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
</Stack>
|
</Stack>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
@@ -569,15 +602,9 @@ const AlbumArtistMetadataFavoriteSongs = ({ routeId }: AlbumArtistMetadataFavori
|
|||||||
onLongPress: () => handlePlay(LONG_PRESS_PLAY_BEHAVIOR[Play.LAST]),
|
onLongPress: () => handlePlay(LONG_PRESS_PLAY_BEHAVIOR[Play.LAST]),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (favoriteSongsQuery.isLoading || !favoriteSongsQuery.data) {
|
const isLoading = favoriteSongsQuery.isLoading || !favoriteSongsQuery.data;
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!favoriteSongsQuery?.data?.items?.length) return null;
|
if (!isLoading && !tableConfig) return null;
|
||||||
|
|
||||||
if (!tableConfig) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentSongId = currentSong?.id;
|
const currentSongId = currentSong?.id;
|
||||||
|
|
||||||
@@ -591,7 +618,7 @@ const AlbumArtistMetadataFavoriteSongs = ({ routeId }: AlbumArtistMetadataFavori
|
|||||||
postProcess: 'sentenceCase',
|
postProcess: 'sentenceCase',
|
||||||
})}
|
})}
|
||||||
</TextTitle>
|
</TextTitle>
|
||||||
<Badge>{favoriteSongsQuery.data?.items?.length}</Badge>
|
{!isLoading && <Badge>{songs.length}</Badge>}
|
||||||
</Group>
|
</Group>
|
||||||
<div className={styles.albumSectionDividerContainer}>
|
<div className={styles.albumSectionDividerContainer}>
|
||||||
<div className={styles.albumSectionDivider} />
|
<div className={styles.albumSectionDivider} />
|
||||||
@@ -618,6 +645,7 @@ const AlbumArtistMetadataFavoriteSongs = ({ routeId }: AlbumArtistMetadataFavori
|
|||||||
variant="subtle"
|
variant="subtle"
|
||||||
{...handlePlayNow.handlers}
|
{...handlePlayNow.handlers}
|
||||||
{...handlePlayNow.props}
|
{...handlePlayNow.props}
|
||||||
|
disabled={isLoading}
|
||||||
/>
|
/>
|
||||||
</PlayTooltip>
|
</PlayTooltip>
|
||||||
<PlayTooltip type={Play.NEXT}>
|
<PlayTooltip type={Play.NEXT}>
|
||||||
@@ -628,6 +656,7 @@ const AlbumArtistMetadataFavoriteSongs = ({ routeId }: AlbumArtistMetadataFavori
|
|||||||
variant="subtle"
|
variant="subtle"
|
||||||
{...handlePlayNext.handlers}
|
{...handlePlayNext.handlers}
|
||||||
{...handlePlayNext.props}
|
{...handlePlayNext.props}
|
||||||
|
disabled={isLoading}
|
||||||
/>
|
/>
|
||||||
</PlayTooltip>
|
</PlayTooltip>
|
||||||
<PlayTooltip type={Play.LAST}>
|
<PlayTooltip type={Play.LAST}>
|
||||||
@@ -638,78 +667,87 @@ const AlbumArtistMetadataFavoriteSongs = ({ routeId }: AlbumArtistMetadataFavori
|
|||||||
variant="subtle"
|
variant="subtle"
|
||||||
{...handlePlayLast.handlers}
|
{...handlePlayLast.handlers}
|
||||||
{...handlePlayLast.props}
|
{...handlePlayLast.props}
|
||||||
|
disabled={isLoading}
|
||||||
/>
|
/>
|
||||||
</PlayTooltip>
|
</PlayTooltip>
|
||||||
</ActionIconGroup>
|
</ActionIconGroup>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Group gap="sm" w="100%">
|
{isLoading ? (
|
||||||
<TextInput
|
<Group justify="center" py="md">
|
||||||
flex={1}
|
<Spinner />
|
||||||
leftSection={<Icon icon="search" />}
|
|
||||||
onChange={(e) => setSearchTerm(e.target.value)}
|
|
||||||
placeholder={t('common.search', { postProcess: 'sentenceCase' })}
|
|
||||||
radius="xl"
|
|
||||||
rightSection={
|
|
||||||
searchTerm ? (
|
|
||||||
<ActionIcon
|
|
||||||
icon="x"
|
|
||||||
onClick={() => setSearchTerm('')}
|
|
||||||
size="sm"
|
|
||||||
variant="transparent"
|
|
||||||
/>
|
|
||||||
) : null
|
|
||||||
}
|
|
||||||
styles={{
|
|
||||||
input: {
|
|
||||||
background: 'transparent',
|
|
||||||
border: '1px solid rgba(255, 255, 255, 0.05)',
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
value={searchTerm}
|
|
||||||
/>
|
|
||||||
<ListConfigMenu
|
|
||||||
displayTypes={[{ hidden: true, value: ListDisplayType.GRID }]}
|
|
||||||
listKey={ItemListKey.SONG}
|
|
||||||
optionsConfig={{
|
|
||||||
table: {
|
|
||||||
itemsPerPage: { hidden: true },
|
|
||||||
pagination: { hidden: true },
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
tableColumnsData={SONG_TABLE_COLUMNS}
|
|
||||||
/>
|
|
||||||
</Group>
|
|
||||||
<ItemTableList
|
|
||||||
activeRowId={currentSongId}
|
|
||||||
autoFitColumns={tableConfig.autoFitColumns}
|
|
||||||
CellComponent={ItemTableListColumn}
|
|
||||||
columns={columns}
|
|
||||||
data={filteredSongs}
|
|
||||||
enableAlternateRowColors={tableConfig.enableAlternateRowColors}
|
|
||||||
enableDrag
|
|
||||||
enableDragScroll={false}
|
|
||||||
enableExpansion={false}
|
|
||||||
enableHeader={tableConfig.enableHeader}
|
|
||||||
enableHorizontalBorders={tableConfig.enableHorizontalBorders}
|
|
||||||
enableRowHoverHighlight={tableConfig.enableRowHoverHighlight}
|
|
||||||
enableSelection
|
|
||||||
enableSelectionDialog={false}
|
|
||||||
enableVerticalBorders={tableConfig.enableVerticalBorders}
|
|
||||||
itemType={LibraryItem.SONG}
|
|
||||||
onColumnReordered={handleColumnReordered}
|
|
||||||
onColumnResized={handleColumnResized}
|
|
||||||
overrideControls={overrideControls}
|
|
||||||
size={tableConfig.size}
|
|
||||||
/>
|
|
||||||
{!searchTerm.trim() && songs.length > 5 && !showAll && (
|
|
||||||
<Group justify="center" w="100%">
|
|
||||||
<Button onClick={() => setShowAll(true)} variant="subtle">
|
|
||||||
{t('action.viewMore', { postProcess: 'sentenceCase' })}
|
|
||||||
</Button>
|
|
||||||
</Group>
|
</Group>
|
||||||
)}
|
) : tableConfig ? (
|
||||||
|
<>
|
||||||
|
<Group gap="sm" w="100%">
|
||||||
|
<TextInput
|
||||||
|
flex={1}
|
||||||
|
leftSection={<Icon icon="search" />}
|
||||||
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
|
placeholder={t('common.search', { postProcess: 'sentenceCase' })}
|
||||||
|
radius="xl"
|
||||||
|
rightSection={
|
||||||
|
searchTerm ? (
|
||||||
|
<ActionIcon
|
||||||
|
icon="x"
|
||||||
|
onClick={() => setSearchTerm('')}
|
||||||
|
size="sm"
|
||||||
|
variant="transparent"
|
||||||
|
/>
|
||||||
|
) : null
|
||||||
|
}
|
||||||
|
styles={{
|
||||||
|
input: {
|
||||||
|
background: 'transparent',
|
||||||
|
border: '1px solid rgba(255, 255, 255, 0.05)',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
value={searchTerm}
|
||||||
|
/>
|
||||||
|
<ListConfigMenu
|
||||||
|
displayTypes={[{ hidden: true, value: ListDisplayType.GRID }]}
|
||||||
|
listKey={ItemListKey.SONG}
|
||||||
|
optionsConfig={{
|
||||||
|
table: {
|
||||||
|
itemsPerPage: { hidden: true },
|
||||||
|
pagination: { hidden: true },
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
tableColumnsData={SONG_TABLE_COLUMNS}
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
<ItemTableList
|
||||||
|
activeRowId={currentSongId}
|
||||||
|
autoFitColumns={tableConfig.autoFitColumns}
|
||||||
|
CellComponent={ItemTableListColumn}
|
||||||
|
columns={columns}
|
||||||
|
data={filteredSongs}
|
||||||
|
enableAlternateRowColors={tableConfig.enableAlternateRowColors}
|
||||||
|
enableDrag
|
||||||
|
enableDragScroll={false}
|
||||||
|
enableExpansion={false}
|
||||||
|
enableHeader={tableConfig.enableHeader}
|
||||||
|
enableHorizontalBorders={tableConfig.enableHorizontalBorders}
|
||||||
|
enableRowHoverHighlight={tableConfig.enableRowHoverHighlight}
|
||||||
|
enableSelection
|
||||||
|
enableSelectionDialog={false}
|
||||||
|
enableVerticalBorders={tableConfig.enableVerticalBorders}
|
||||||
|
itemType={LibraryItem.SONG}
|
||||||
|
onColumnReordered={handleColumnReordered}
|
||||||
|
onColumnResized={handleColumnResized}
|
||||||
|
overrideControls={overrideControls}
|
||||||
|
size={tableConfig.size}
|
||||||
|
/>
|
||||||
|
{!searchTerm.trim() && songs.length > 5 && !showAll && (
|
||||||
|
<Group justify="center" w="100%">
|
||||||
|
<Button onClick={() => setShowAll(true)} variant="subtle">
|
||||||
|
{t('action.viewMore', { postProcess: 'sentenceCase' })}
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
</Stack>
|
</Stack>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import { PageErrorBoundary } from '/@/renderer/features/shared/components/page-e
|
|||||||
import { usePlayerSong } from '/@/renderer/store';
|
import { usePlayerSong } from '/@/renderer/store';
|
||||||
import { useCurrentServer } from '/@/renderer/store/auth.store';
|
import { useCurrentServer } from '/@/renderer/store/auth.store';
|
||||||
import { useSettingsStore } from '/@/renderer/store/settings.store';
|
import { useSettingsStore } from '/@/renderer/store/settings.store';
|
||||||
|
import { useLocalStorage } from '/@/shared/hooks/use-local-storage';
|
||||||
import { LibraryItem, Song } from '/@/shared/types/domain-types';
|
import { LibraryItem, Song } from '/@/shared/types/domain-types';
|
||||||
import { ItemListKey, Play } from '/@/shared/types/types';
|
import { ItemListKey, Play } from '/@/shared/types/types';
|
||||||
|
|
||||||
@@ -28,6 +29,11 @@ const AlbumArtistDetailTopSongsListRoute = () => {
|
|||||||
const server = useCurrentServer();
|
const server = useCurrentServer();
|
||||||
const pageKey = LibraryItem.SONG;
|
const pageKey = LibraryItem.SONG;
|
||||||
|
|
||||||
|
const [topSongsQueryType] = useLocalStorage<'community' | 'personal'>({
|
||||||
|
defaultValue: 'community',
|
||||||
|
key: 'album-artist-top-songs-query-type',
|
||||||
|
});
|
||||||
|
|
||||||
const detailQuery = useQuery(
|
const detailQuery = useQuery(
|
||||||
artistsQueries.albumArtistDetail({
|
artistsQueries.albumArtistDetail({
|
||||||
query: { id: routeId },
|
query: { id: routeId },
|
||||||
@@ -38,7 +44,11 @@ const AlbumArtistDetailTopSongsListRoute = () => {
|
|||||||
const topSongsQuery = useQuery(
|
const topSongsQuery = useQuery(
|
||||||
artistsQueries.topSongs({
|
artistsQueries.topSongs({
|
||||||
options: { enabled: !!detailQuery?.data?.name },
|
options: { enabled: !!detailQuery?.data?.name },
|
||||||
query: { artist: detailQuery?.data?.name || '', artistId: routeId },
|
query: {
|
||||||
|
artist: detailQuery?.data?.name || '',
|
||||||
|
artistId: routeId,
|
||||||
|
type: topSongsQueryType,
|
||||||
|
},
|
||||||
serverId: server?.id,
|
serverId: server?.id,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -41,6 +41,16 @@ export const useSendScrobble = (options?: MutationOptions) => {
|
|||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
queryKey: ['home', 'mostPlayed'],
|
queryKey: ['home', 'mostPlayed'],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Invalidate album artist top songs
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: queryKeys.albumArtists.topSongs(serverId),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Invalidate album artist favorite songs
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: queryKeys.albumArtists.favoriteSongs(serverId),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
...options,
|
...options,
|
||||||
|
|||||||
@@ -1316,6 +1316,7 @@ export type TopSongListQuery = {
|
|||||||
artist: string;
|
artist: string;
|
||||||
artistId: string;
|
artistId: string;
|
||||||
limit?: number;
|
limit?: number;
|
||||||
|
type?: 'community' | 'personal';
|
||||||
};
|
};
|
||||||
|
|
||||||
// Top Songs List
|
// Top Songs List
|
||||||
|
|||||||
Reference in New Issue
Block a user