mirror of
https://github.com/jeffvli/feishin.git
synced 2026-06-20 11:03:06 +02:00
Support subsonic song filters
This commit is contained in:
@@ -219,7 +219,7 @@ export const SubsonicController: ControllerEndpoint = {
|
|||||||
);
|
);
|
||||||
|
|
||||||
let results = artists.map((artist) =>
|
let results = artists.map((artist) =>
|
||||||
subsonicNormalize.albumArtist(artist, apiClientProps.server),
|
subsonicNormalize.albumArtist(artist, apiClientProps.server, 300),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (query.searchTerm) {
|
if (query.searchTerm) {
|
||||||
@@ -880,11 +880,39 @@ export const SubsonicController: ControllerEndpoint = {
|
|||||||
const artistDetailPromises = [];
|
const artistDetailPromises = [];
|
||||||
let results: any[] = [];
|
let results: any[] = [];
|
||||||
|
|
||||||
if (query.genreId) {
|
if (query.searchTerm) {
|
||||||
|
const res = await subsonicApiClient(apiClientProps).search3({
|
||||||
|
query: {
|
||||||
|
albumCount: 0,
|
||||||
|
albumOffset: 0,
|
||||||
|
artistCount: 0,
|
||||||
|
artistOffset: 0,
|
||||||
|
query: query.searchTerm || '""',
|
||||||
|
songCount: query.limit,
|
||||||
|
songOffset: query.startIndex,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.status !== 200) {
|
||||||
|
fsLog.error('Failed to get song list');
|
||||||
|
throw new Error('Failed to get song list');
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
items:
|
||||||
|
res.body['subsonic-response'].searchResult3?.song?.map((song) =>
|
||||||
|
subsonicNormalize.song(song, apiClientProps.server, ''),
|
||||||
|
) || [],
|
||||||
|
startIndex: query.startIndex,
|
||||||
|
totalRecordCount: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (query.genre) {
|
||||||
const res = await subsonicApiClient(apiClientProps).getSongsByGenre({
|
const res = await subsonicApiClient(apiClientProps).getSongsByGenre({
|
||||||
query: {
|
query: {
|
||||||
count: query.limit,
|
count: query.limit,
|
||||||
genre: query.genreId,
|
genre: query.genre,
|
||||||
musicFolderId: query.musicFolderId,
|
musicFolderId: query.musicFolderId,
|
||||||
offset: query.startIndex,
|
offset: query.startIndex,
|
||||||
},
|
},
|
||||||
@@ -896,14 +924,39 @@ export const SubsonicController: ControllerEndpoint = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
items: res.body['subsonic-response'].songsByGenre.song.map((song) =>
|
items:
|
||||||
subsonicNormalize.song(song, apiClientProps.server, ''),
|
res.body['subsonic-response'].songsByGenre.song?.map((song) =>
|
||||||
),
|
subsonicNormalize.song(song, apiClientProps.server, ''),
|
||||||
|
) || [],
|
||||||
startIndex: 0,
|
startIndex: 0,
|
||||||
totalRecordCount: null,
|
totalRecordCount: null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (query.isFavorite) {
|
||||||
|
const res = await subsonicApiClient(apiClientProps).getStarred({
|
||||||
|
query: {
|
||||||
|
musicFolderId: query.musicFolderId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.status !== 200) {
|
||||||
|
fsLog.error('Failed to get song list');
|
||||||
|
throw new Error('Failed to get song list');
|
||||||
|
}
|
||||||
|
|
||||||
|
const results =
|
||||||
|
res.body['subsonic-response'].starred.song?.map((song) =>
|
||||||
|
subsonicNormalize.song(song, apiClientProps.server, ''),
|
||||||
|
) || [];
|
||||||
|
|
||||||
|
return {
|
||||||
|
items: sortSongList(results, query.sortBy, query.sortOrder),
|
||||||
|
startIndex: 0,
|
||||||
|
totalRecordCount: res.body['subsonic-response'].starred.song?.length || 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (query.albumIds || query.artistIds) {
|
if (query.albumIds || query.artistIds) {
|
||||||
if (query.albumIds) {
|
if (query.albumIds) {
|
||||||
for (const albumId of query.albumIds) {
|
for (const albumId of query.albumIds) {
|
||||||
@@ -1009,13 +1062,48 @@ export const SubsonicController: ControllerEndpoint = {
|
|||||||
let fetchNextSection = true;
|
let fetchNextSection = true;
|
||||||
let sectionIndex = 0;
|
let sectionIndex = 0;
|
||||||
|
|
||||||
if (query.genreId) {
|
if (query.searchTerm) {
|
||||||
|
let fetchNextPage = true;
|
||||||
|
let startIndex = 0;
|
||||||
|
let totalRecordCount = 0;
|
||||||
|
|
||||||
|
while (fetchNextPage) {
|
||||||
|
const res = await subsonicApiClient(apiClientProps).search3({
|
||||||
|
query: {
|
||||||
|
albumCount: 0,
|
||||||
|
albumOffset: 0,
|
||||||
|
artistCount: 0,
|
||||||
|
artistOffset: 0,
|
||||||
|
query: query.searchTerm || '""',
|
||||||
|
songCount: 500,
|
||||||
|
songOffset: startIndex,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.status !== 200) {
|
||||||
|
fsLog.error('Failed to get song list count');
|
||||||
|
throw new Error('Failed to get song list count');
|
||||||
|
}
|
||||||
|
|
||||||
|
const songCount = res.body['subsonic-response'].searchResult3.song?.length;
|
||||||
|
|
||||||
|
totalRecordCount += songCount;
|
||||||
|
startIndex += songCount;
|
||||||
|
|
||||||
|
// The max limit size for Subsonic is 500
|
||||||
|
fetchNextPage = songCount === 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
return totalRecordCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (query.genre) {
|
||||||
let totalRecordCount = 0;
|
let totalRecordCount = 0;
|
||||||
while (fetchNextSection) {
|
while (fetchNextSection) {
|
||||||
const res = await subsonicApiClient(apiClientProps).getSongsByGenre({
|
const res = await subsonicApiClient(apiClientProps).getSongsByGenre({
|
||||||
query: {
|
query: {
|
||||||
count: 1,
|
count: 1,
|
||||||
genre: query.genreId,
|
genre: query.genre,
|
||||||
musicFolderId: query.musicFolderId,
|
musicFolderId: query.musicFolderId,
|
||||||
offset: sectionIndex,
|
offset: sectionIndex,
|
||||||
},
|
},
|
||||||
@@ -1042,7 +1130,7 @@ export const SubsonicController: ControllerEndpoint = {
|
|||||||
const res = await subsonicApiClient(apiClientProps).getSongsByGenre({
|
const res = await subsonicApiClient(apiClientProps).getSongsByGenre({
|
||||||
query: {
|
query: {
|
||||||
count: 500,
|
count: 500,
|
||||||
genre: query.genreId,
|
genre: query.genre,
|
||||||
musicFolderId: query.musicFolderId,
|
musicFolderId: query.musicFolderId,
|
||||||
offset: startIndex,
|
offset: startIndex,
|
||||||
},
|
},
|
||||||
@@ -1065,6 +1153,21 @@ export const SubsonicController: ControllerEndpoint = {
|
|||||||
return totalRecordCount;
|
return totalRecordCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (query.isFavorite) {
|
||||||
|
const res = await subsonicApiClient(apiClientProps).getStarred({
|
||||||
|
query: {
|
||||||
|
musicFolderId: query.musicFolderId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.status !== 200) {
|
||||||
|
fsLog.error('Failed to get song list');
|
||||||
|
throw new Error('Failed to get song list');
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.body['subsonic-response'].starred.song?.length || 0;
|
||||||
|
}
|
||||||
|
|
||||||
let totalRecordCount = 0;
|
let totalRecordCount = 0;
|
||||||
|
|
||||||
while (fetchNextSection) {
|
while (fetchNextSection) {
|
||||||
|
|||||||
@@ -38,13 +38,14 @@ const normalizeSong = (
|
|||||||
item: z.infer<typeof SubsonicApi._baseTypes.song>,
|
item: z.infer<typeof SubsonicApi._baseTypes.song>,
|
||||||
server: ServerListItem | null,
|
server: ServerListItem | null,
|
||||||
deviceId: string,
|
deviceId: string,
|
||||||
|
size?: number,
|
||||||
): QueueSong => {
|
): QueueSong => {
|
||||||
const imageUrl =
|
const imageUrl =
|
||||||
getCoverArtUrl({
|
getCoverArtUrl({
|
||||||
baseUrl: server?.url,
|
baseUrl: server?.url,
|
||||||
coverArtId: item.coverArt,
|
coverArtId: item.coverArt,
|
||||||
credential: server?.credential,
|
credential: server?.credential,
|
||||||
size: 100,
|
size: size || 300,
|
||||||
}) || null;
|
}) || null;
|
||||||
|
|
||||||
const streamUrl = `${server?.url}/rest/stream.view?id=${item.id}&v=1.13.0&c=feishin_${deviceId}&${server?.credential}`;
|
const streamUrl = `${server?.url}/rest/stream.view?id=${item.id}&v=1.13.0&c=feishin_${deviceId}&${server?.credential}`;
|
||||||
|
|||||||
@@ -492,8 +492,10 @@ export type SongListQuery = {
|
|||||||
};
|
};
|
||||||
albumIds?: string[];
|
albumIds?: string[];
|
||||||
artistIds?: string[];
|
artistIds?: string[];
|
||||||
|
genre?: string;
|
||||||
genreId?: string;
|
genreId?: string;
|
||||||
imageSize?: number;
|
imageSize?: number;
|
||||||
|
isFavorite?: boolean;
|
||||||
limit?: number;
|
limit?: number;
|
||||||
maxYear?: number;
|
maxYear?: number;
|
||||||
minYear?: number;
|
minYear?: number;
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ interface UseAgGridProps<TFilter> {
|
|||||||
columnType?: 'albumDetail' | 'generic';
|
columnType?: 'albumDetail' | 'generic';
|
||||||
contextMenu: SetContextMenuItems;
|
contextMenu: SetContextMenuItems;
|
||||||
customFilters?: Partial<TFilter>;
|
customFilters?: Partial<TFilter>;
|
||||||
|
isClientSide?: boolean;
|
||||||
isClientSideSort?: boolean;
|
isClientSideSort?: boolean;
|
||||||
isSearchParams?: boolean;
|
isSearchParams?: boolean;
|
||||||
itemCount?: number;
|
itemCount?: number;
|
||||||
@@ -43,6 +44,8 @@ interface UseAgGridProps<TFilter> {
|
|||||||
tableRef: MutableRefObject<AgGridReactType | null>;
|
tableRef: MutableRefObject<AgGridReactType | null>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const BLOCK_SIZE = 500;
|
||||||
|
|
||||||
export const useVirtualTable = <TFilter>({
|
export const useVirtualTable = <TFilter>({
|
||||||
server,
|
server,
|
||||||
tableRef,
|
tableRef,
|
||||||
@@ -52,6 +55,7 @@ export const useVirtualTable = <TFilter>({
|
|||||||
itemCount,
|
itemCount,
|
||||||
customFilters,
|
customFilters,
|
||||||
isSearchParams,
|
isSearchParams,
|
||||||
|
isClientSide,
|
||||||
isClientSideSort,
|
isClientSideSort,
|
||||||
columnType,
|
columnType,
|
||||||
}: UseAgGridProps<TFilter>) => {
|
}: UseAgGridProps<TFilter>) => {
|
||||||
@@ -183,7 +187,7 @@ export const useVirtualTable = <TFilter>({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (results.totalRecordCount === null) {
|
if (results.totalRecordCount === null) {
|
||||||
const hasMoreRows = results?.items?.length === properties.filter.limit;
|
const hasMoreRows = results?.items?.length === BLOCK_SIZE;
|
||||||
const lastRowIndex = hasMoreRows
|
const lastRowIndex = hasMoreRows
|
||||||
? undefined
|
? undefined
|
||||||
: (properties.filter.offset || 0) + results.items.length;
|
: (properties.filter.offset || 0) + results.items.length;
|
||||||
@@ -334,6 +338,7 @@ export const useVirtualTable = <TFilter>({
|
|||||||
alwaysShowHorizontalScroll: true,
|
alwaysShowHorizontalScroll: true,
|
||||||
autoFitColumns: properties.table.autoFit,
|
autoFitColumns: properties.table.autoFit,
|
||||||
blockLoadDebounceMillis: 200,
|
blockLoadDebounceMillis: 200,
|
||||||
|
cacheBlockSize: 500,
|
||||||
getRowId: (data: GetRowIdParams<any>) => data.data.id,
|
getRowId: (data: GetRowIdParams<any>) => data.data.id,
|
||||||
infiniteInitialRowCount: itemCount || 100,
|
infiniteInitialRowCount: itemCount || 100,
|
||||||
pagination: isPaginationEnabled,
|
pagination: isPaginationEnabled,
|
||||||
@@ -348,10 +353,11 @@ export const useVirtualTable = <TFilter>({
|
|||||||
: undefined,
|
: undefined,
|
||||||
rowBuffer: 20,
|
rowBuffer: 20,
|
||||||
rowHeight: properties.table.rowHeight || 40,
|
rowHeight: properties.table.rowHeight || 40,
|
||||||
rowModelType: 'infinite' as RowModelType,
|
rowModelType: isClientSide ? 'clientSide' : ('infinite' as RowModelType),
|
||||||
suppressRowDrag: true,
|
suppressRowDrag: true,
|
||||||
};
|
};
|
||||||
}, [
|
}, [
|
||||||
|
isClientSide,
|
||||||
isPaginationEnabled,
|
isPaginationEnabled,
|
||||||
isSearchParams,
|
isSearchParams,
|
||||||
itemCount,
|
itemCount,
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import { queryClient } from '/@/renderer/lib/react-query';
|
|||||||
import { SongListFilter, useCurrentServer, useListStoreActions } from '/@/renderer/store';
|
import { SongListFilter, useCurrentServer, useListStoreActions } from '/@/renderer/store';
|
||||||
import { ListDisplayType, Play, ServerType, TableColumn } from '/@/renderer/types';
|
import { ListDisplayType, Play, ServerType, TableColumn } from '/@/renderer/types';
|
||||||
import i18n from '/@/i18n/i18n';
|
import i18n from '/@/i18n/i18n';
|
||||||
|
import { SubsonicSongFilters } from '/@/renderer/features/songs/components/subsonic-song-filters';
|
||||||
|
|
||||||
const FILTERS = {
|
const FILTERS = {
|
||||||
jellyfin: [
|
jellyfin: [
|
||||||
@@ -400,25 +401,34 @@ export const SongListHeaderFilters = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleOpenFiltersModal = () => {
|
const handleOpenFiltersModal = () => {
|
||||||
|
let FilterComponent;
|
||||||
|
|
||||||
|
switch (server?.type) {
|
||||||
|
case ServerType.NAVIDROME:
|
||||||
|
FilterComponent = NavidromeSongFilters;
|
||||||
|
break;
|
||||||
|
case ServerType.JELLYFIN:
|
||||||
|
FilterComponent = JellyfinSongFilters;
|
||||||
|
break;
|
||||||
|
case ServerType.SUBSONIC:
|
||||||
|
FilterComponent = SubsonicSongFilters;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!FilterComponent) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
openModal({
|
openModal({
|
||||||
children: (
|
children: (
|
||||||
<>
|
<FilterComponent
|
||||||
{server?.type === ServerType.NAVIDROME ? (
|
customFilters={customFilters}
|
||||||
<NavidromeSongFilters
|
pageKey={pageKey}
|
||||||
customFilters={customFilters}
|
serverId={server?.id}
|
||||||
pageKey={pageKey}
|
onFilterChange={onFilterChange}
|
||||||
serverId={server?.id}
|
/>
|
||||||
onFilterChange={onFilterChange}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<JellyfinSongFilters
|
|
||||||
customFilters={customFilters}
|
|
||||||
pageKey={pageKey}
|
|
||||||
serverId={server?.id}
|
|
||||||
onFilterChange={onFilterChange}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
),
|
),
|
||||||
title: 'Song Filters',
|
title: 'Song Filters',
|
||||||
});
|
});
|
||||||
@@ -437,8 +447,17 @@ export const SongListHeaderFilters = ({
|
|||||||
.filter((value) => value !== 'Audio') // Don't account for includeItemTypes: Audio
|
.filter((value) => value !== 'Audio') // Don't account for includeItemTypes: Audio
|
||||||
.some((value) => value !== undefined);
|
.some((value) => value !== undefined);
|
||||||
|
|
||||||
return isNavidromeFilterApplied || isJellyfinFilterApplied;
|
const isSubsonicFilterApplied =
|
||||||
}, [filter?._custom?.jellyfin, filter?._custom?.navidrome, server?.type]);
|
server?.type === ServerType.SUBSONIC && (filter?.isFavorite || filter?.genre);
|
||||||
|
|
||||||
|
return isNavidromeFilterApplied || isJellyfinFilterApplied || isSubsonicFilterApplied;
|
||||||
|
}, [
|
||||||
|
filter._custom?.jellyfin,
|
||||||
|
filter._custom?.navidrome,
|
||||||
|
filter?.genre,
|
||||||
|
filter?.isFavorite,
|
||||||
|
server?.type,
|
||||||
|
]);
|
||||||
|
|
||||||
const isFolderFilterApplied = useMemo(() => {
|
const isFolderFilterApplied = useMemo(() => {
|
||||||
return filter.musicFolderId !== undefined;
|
return filter.musicFolderId !== undefined;
|
||||||
@@ -475,11 +494,15 @@ export const SongListHeaderFilters = ({
|
|||||||
))}
|
))}
|
||||||
</DropdownMenu.Dropdown>
|
</DropdownMenu.Dropdown>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
<Divider orientation="vertical" />
|
{server?.type !== ServerType.SUBSONIC && (
|
||||||
<OrderToggleButton
|
<>
|
||||||
sortOrder={filter.sortOrder}
|
<Divider orientation="vertical" />
|
||||||
onToggle={handleToggleSortOrder}
|
<OrderToggleButton
|
||||||
/>
|
sortOrder={filter.sortOrder}
|
||||||
|
onToggle={handleToggleSortOrder}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
{server?.type === ServerType.JELLYFIN && (
|
{server?.type === ServerType.JELLYFIN && (
|
||||||
<>
|
<>
|
||||||
<Divider orientation="vertical" />
|
<Divider orientation="vertical" />
|
||||||
|
|||||||
@@ -0,0 +1,109 @@
|
|||||||
|
import { ChangeEvent, useMemo } from 'react';
|
||||||
|
import { Divider, Group, Stack } from '@mantine/core';
|
||||||
|
import debounce from 'lodash/debounce';
|
||||||
|
import { GenreListSort, LibraryItem, SortOrder } from '/@/renderer/api/types';
|
||||||
|
import { Select, Switch, Text } from '/@/renderer/components';
|
||||||
|
import { useGenreList } from '/@/renderer/features/genres';
|
||||||
|
import { SongListFilter, useListFilterByKey, useListStoreActions } from '/@/renderer/store';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
interface SubsonicSongFiltersProps {
|
||||||
|
customFilters?: Partial<SongListFilter>;
|
||||||
|
onFilterChange: (filters: SongListFilter) => void;
|
||||||
|
pageKey: string;
|
||||||
|
serverId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SubsonicSongFilters = ({
|
||||||
|
customFilters,
|
||||||
|
onFilterChange,
|
||||||
|
pageKey,
|
||||||
|
serverId,
|
||||||
|
}: SubsonicSongFiltersProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { setFilter } = useListStoreActions();
|
||||||
|
const filter = useListFilterByKey({ key: pageKey });
|
||||||
|
|
||||||
|
const isGenrePage = customFilters?._custom?.navidrome?.genre_id !== undefined;
|
||||||
|
|
||||||
|
const genreListQuery = useGenreList({
|
||||||
|
query: {
|
||||||
|
sortBy: GenreListSort.NAME,
|
||||||
|
sortOrder: SortOrder.ASC,
|
||||||
|
startIndex: 0,
|
||||||
|
},
|
||||||
|
serverId,
|
||||||
|
});
|
||||||
|
|
||||||
|
const genreList = useMemo(() => {
|
||||||
|
if (!genreListQuery?.data) return [];
|
||||||
|
return genreListQuery.data.items.map((genre) => ({
|
||||||
|
label: genre.name,
|
||||||
|
value: genre.id,
|
||||||
|
}));
|
||||||
|
}, [genreListQuery.data]);
|
||||||
|
|
||||||
|
const handleGenresFilter = debounce((e: string | null) => {
|
||||||
|
const updatedFilters = setFilter({
|
||||||
|
customFilters,
|
||||||
|
data: {
|
||||||
|
genre: e || undefined,
|
||||||
|
},
|
||||||
|
itemType: LibraryItem.SONG,
|
||||||
|
key: pageKey,
|
||||||
|
}) as SongListFilter;
|
||||||
|
|
||||||
|
onFilterChange(updatedFilters);
|
||||||
|
}, 250);
|
||||||
|
|
||||||
|
const toggleFilters = [
|
||||||
|
{
|
||||||
|
label: t('filter.isFavorited', { postProcess: 'sentenceCase' }),
|
||||||
|
onChange: (e: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const updatedFilters = setFilter({
|
||||||
|
customFilters,
|
||||||
|
data: {
|
||||||
|
isFavorite: e.target.checked,
|
||||||
|
},
|
||||||
|
itemType: LibraryItem.SONG,
|
||||||
|
key: pageKey,
|
||||||
|
}) as SongListFilter;
|
||||||
|
|
||||||
|
onFilterChange(updatedFilters);
|
||||||
|
},
|
||||||
|
value: filter.isFavorite,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack p="0.8rem">
|
||||||
|
{toggleFilters.map((filter) => (
|
||||||
|
<Group
|
||||||
|
key={`ss-filter-${filter.label}`}
|
||||||
|
position="apart"
|
||||||
|
>
|
||||||
|
<Text>{filter.label}</Text>
|
||||||
|
<Switch
|
||||||
|
checked={filter?.value || false}
|
||||||
|
size="xs"
|
||||||
|
onChange={filter.onChange}
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
))}
|
||||||
|
<Divider my="0.5rem" />
|
||||||
|
<Group grow>
|
||||||
|
{!isGenrePage && (
|
||||||
|
<Select
|
||||||
|
clearable
|
||||||
|
searchable
|
||||||
|
data={genreList}
|
||||||
|
defaultValue={filter.genre}
|
||||||
|
label={t('entity.genre', { count: 1, postProcess: 'titleCase' })}
|
||||||
|
width={150}
|
||||||
|
onChange={handleGenresFilter}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -11,6 +11,8 @@ export const getSongListCountQuery = (query: SongListQuery) => {
|
|||||||
if (query.searchTerm) filter.searchTerm = query.searchTerm;
|
if (query.searchTerm) filter.searchTerm = query.searchTerm;
|
||||||
if (query.genreId) filter.genreId = query.genreId;
|
if (query.genreId) filter.genreId = query.genreId;
|
||||||
if (query.musicFolderId) filter.musicFolderId = query.musicFolderId;
|
if (query.musicFolderId) filter.musicFolderId = query.musicFolderId;
|
||||||
|
if (query.isFavorite) filter.isFavorite = query.isFavorite;
|
||||||
|
if (query.genre) filter.genre = query.genre;
|
||||||
|
|
||||||
if (Object.keys(filter).length === 0) return undefined;
|
if (Object.keys(filter).length === 0) return undefined;
|
||||||
|
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ const TrackListRoute = () => {
|
|||||||
genre_id: genreId,
|
genre_id: genreId,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
genre: genreId,
|
||||||
genreId,
|
genreId,
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ interface UseHandleListFilterChangeProps {
|
|||||||
server: ServerListItem | null;
|
server: ServerListItem | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const BLOCK_SIZE = 500;
|
||||||
|
|
||||||
export const useListFilterRefresh = ({
|
export const useListFilterRefresh = ({
|
||||||
server,
|
server,
|
||||||
itemType,
|
itemType,
|
||||||
@@ -108,7 +110,7 @@ export const useListFilterRefresh = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (results.totalRecordCount === null) {
|
if (results.totalRecordCount === null) {
|
||||||
const hasMoreRows = results?.items?.length === filter.limit;
|
const hasMoreRows = results?.items?.length === BLOCK_SIZE;
|
||||||
const lastRowIndex = hasMoreRows
|
const lastRowIndex = hasMoreRows
|
||||||
? undefined
|
? undefined
|
||||||
: (filter.offset || 0) + results.items.length;
|
: (filter.offset || 0) + results.items.length;
|
||||||
|
|||||||
Reference in New Issue
Block a user