mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-10 04:30:25 +02:00
fix subsonic / jellyfin filters
This commit is contained in:
@@ -413,7 +413,7 @@ export const jfApiClient = (args: {
|
|||||||
return {
|
return {
|
||||||
body: response?.data,
|
body: response?.data,
|
||||||
headers: response?.headers as any,
|
headers: response?.headers as any,
|
||||||
status: response.status,
|
status: response?.status,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
throw e;
|
throw e;
|
||||||
|
|||||||
@@ -46,9 +46,41 @@ const ALBUM_LIST_SORT_MAPPING: Record<AlbumListSort, AlbumListSortType | undefin
|
|||||||
};
|
};
|
||||||
|
|
||||||
const MAX_SUBSONIC_ITEMS = 500;
|
const MAX_SUBSONIC_ITEMS = 500;
|
||||||
// A trick to skip ahead 10x
|
|
||||||
const SUBSONIC_FAST_BATCH_SIZE = MAX_SUBSONIC_ITEMS * 10;
|
const SUBSONIC_FAST_BATCH_SIZE = MAX_SUBSONIC_ITEMS * 10;
|
||||||
|
|
||||||
|
function sortAndPaginate<T>(
|
||||||
|
items: T[],
|
||||||
|
options: {
|
||||||
|
limit?: number;
|
||||||
|
sortBy?: any;
|
||||||
|
sortFn?: (items: T[], sortBy: any, sortOrder: SortOrder) => T[];
|
||||||
|
sortOrder?: SortOrder;
|
||||||
|
startIndex?: number;
|
||||||
|
},
|
||||||
|
): {
|
||||||
|
items: T[];
|
||||||
|
startIndex: number;
|
||||||
|
totalRecordCount: number;
|
||||||
|
} {
|
||||||
|
let sortedItems = items;
|
||||||
|
|
||||||
|
if (options.sortFn && options.sortBy) {
|
||||||
|
const sortOrder = options.sortOrder || SortOrder.ASC;
|
||||||
|
sortedItems = options.sortFn(items, options.sortBy, sortOrder);
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalCount = sortedItems.length;
|
||||||
|
const startIndex = options.startIndex || 0;
|
||||||
|
const limit = options.limit || totalCount;
|
||||||
|
const paginatedItems = sortedItems.slice(startIndex, startIndex + limit);
|
||||||
|
|
||||||
|
return {
|
||||||
|
items: paginatedItems,
|
||||||
|
startIndex: startIndex,
|
||||||
|
totalRecordCount: totalCount,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export const SubsonicController: InternalControllerEndpoint = {
|
export const SubsonicController: InternalControllerEndpoint = {
|
||||||
addToPlaylist: async ({ apiClientProps, body, query }) => {
|
addToPlaylist: async ({ apiClientProps, body, query }) => {
|
||||||
const res = await ssApiClient(apiClientProps).updatePlaylist({
|
const res = await ssApiClient(apiClientProps).updatePlaylist({
|
||||||
@@ -246,7 +278,7 @@ export const SubsonicController: InternalControllerEndpoint = {
|
|||||||
return {
|
return {
|
||||||
items: results,
|
items: results,
|
||||||
startIndex: query.startIndex,
|
startIndex: query.startIndex,
|
||||||
totalRecordCount: results?.length || 0,
|
totalRecordCount: artists.length,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
getAlbumArtistListCount: (args) =>
|
getAlbumArtistListCount: (args) =>
|
||||||
@@ -346,16 +378,18 @@ export const SubsonicController: InternalControllerEndpoint = {
|
|||||||
throw new Error('Failed to get album list');
|
throw new Error('Failed to get album list');
|
||||||
}
|
}
|
||||||
|
|
||||||
const results =
|
const allResults =
|
||||||
res.body.starred?.album?.map((album) =>
|
res.body.starred?.album?.map((album) =>
|
||||||
ssNormalize.album(album, apiClientProps.server),
|
ssNormalize.album(album, apiClientProps.server),
|
||||||
) || [];
|
) || [];
|
||||||
|
|
||||||
return {
|
return sortAndPaginate(allResults, {
|
||||||
items: sortAlbumList(results, query.sortBy, query.sortOrder),
|
limit: query.limit,
|
||||||
startIndex: 0,
|
sortBy: query.sortBy,
|
||||||
totalRecordCount: res.body.starred?.album?.length || 0,
|
sortFn: sortAlbumList,
|
||||||
};
|
sortOrder: query.sortOrder,
|
||||||
|
startIndex: query.startIndex,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (query.genreIds?.length) {
|
if (query.genreIds?.length) {
|
||||||
@@ -592,15 +626,13 @@ export const SubsonicController: InternalControllerEndpoint = {
|
|||||||
results = searchResults;
|
results = searchResults;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (query.sortBy) {
|
return sortAndPaginate(results, {
|
||||||
results = sortAlbumArtistList(results, query.sortBy, query.sortOrder);
|
limit: query.limit,
|
||||||
}
|
sortBy: query.sortBy,
|
||||||
|
sortFn: query.sortBy ? sortAlbumArtistList : undefined,
|
||||||
return {
|
sortOrder: query.sortOrder,
|
||||||
items: results,
|
|
||||||
startIndex: query.startIndex,
|
startIndex: query.startIndex,
|
||||||
totalRecordCount: results?.length || 0,
|
});
|
||||||
};
|
|
||||||
},
|
},
|
||||||
getArtistListCount: async (args) =>
|
getArtistListCount: async (args) =>
|
||||||
SubsonicController.getArtistList({
|
SubsonicController.getArtistList({
|
||||||
@@ -647,11 +679,10 @@ export const SubsonicController: InternalControllerEndpoint = {
|
|||||||
|
|
||||||
const genres = results.map((genre) => ssNormalize.genre(genre, apiClientProps.server));
|
const genres = results.map((genre) => ssNormalize.genre(genre, apiClientProps.server));
|
||||||
|
|
||||||
return {
|
return sortAndPaginate(genres, {
|
||||||
items: genres,
|
limit: query.limit,
|
||||||
startIndex: 0,
|
startIndex: query.startIndex,
|
||||||
totalRecordCount: genres.length,
|
});
|
||||||
};
|
|
||||||
},
|
},
|
||||||
getMusicFolderList: async (args) => {
|
getMusicFolderList: async (args) => {
|
||||||
const { apiClientProps } = args;
|
const { apiClientProps } = args;
|
||||||
@@ -728,11 +759,14 @@ export const SubsonicController: InternalControllerEndpoint = {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
const playlists = results.map((playlist) =>
|
||||||
items: results.map((playlist) => ssNormalize.playlist(playlist, apiClientProps.server)),
|
ssNormalize.playlist(playlist, apiClientProps.server),
|
||||||
startIndex: 0,
|
);
|
||||||
totalRecordCount: results.length,
|
|
||||||
};
|
return sortAndPaginate(playlists, {
|
||||||
|
limit: query.limit,
|
||||||
|
startIndex: query.startIndex,
|
||||||
|
});
|
||||||
},
|
},
|
||||||
getPlaylistListCount: async ({ apiClientProps, query }) => {
|
getPlaylistListCount: async ({ apiClientProps, query }) => {
|
||||||
const res = await ssApiClient(apiClientProps).getPlaylists({});
|
const res = await ssApiClient(apiClientProps).getPlaylists({});
|
||||||
@@ -792,11 +826,14 @@ export const SubsonicController: InternalControllerEndpoint = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const results = res.body.randomSongs?.song || [];
|
const results = res.body.randomSongs?.song || [];
|
||||||
|
const normalizedResults = results.map((song) =>
|
||||||
|
ssNormalize.song(song, apiClientProps.server),
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
items: results.map((song) => ssNormalize.song(song, apiClientProps.server)),
|
items: normalizedResults,
|
||||||
startIndex: 0,
|
startIndex: 0,
|
||||||
totalRecordCount: res.body.randomSongs?.song?.length || 0,
|
totalRecordCount: normalizedResults.length,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
getRoles: async (args) => {
|
getRoles: async (args) => {
|
||||||
@@ -965,16 +1002,18 @@ export const SubsonicController: InternalControllerEndpoint = {
|
|||||||
throw new Error('Failed to get song list');
|
throw new Error('Failed to get song list');
|
||||||
}
|
}
|
||||||
|
|
||||||
const results =
|
const allResults =
|
||||||
(res.body.starred?.song || []).map((song) =>
|
(res.body.starred?.song || []).map((song) =>
|
||||||
ssNormalize.song(song, apiClientProps.server),
|
ssNormalize.song(song, apiClientProps.server),
|
||||||
) || [];
|
) || [];
|
||||||
|
|
||||||
return {
|
return sortAndPaginate(allResults, {
|
||||||
items: sortSongList(results, query.sortBy, query.sortOrder),
|
limit: query.limit,
|
||||||
startIndex: 0,
|
sortBy: query.sortBy,
|
||||||
totalRecordCount: (res.body.starred?.song || []).length || 0,
|
sortFn: sortSongList,
|
||||||
};
|
sortOrder: query.sortOrder,
|
||||||
|
startIndex: query.startIndex,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const artistIds = query.albumArtistIds || query.artistIds;
|
const artistIds = query.albumArtistIds || query.artistIds;
|
||||||
@@ -1041,7 +1080,7 @@ export const SubsonicController: InternalControllerEndpoint = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
items: results.map((song) => ssNormalize.song(song, apiClientProps.server)),
|
items: results.map((song) => ssNormalize.song(song, apiClientProps.server)) || [],
|
||||||
startIndex: 0,
|
startIndex: 0,
|
||||||
totalRecordCount: results.length,
|
totalRecordCount: results.length,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import debounce from 'lodash/debounce';
|
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
@@ -8,13 +7,12 @@ import { useAlbumListFilters } from '/@/renderer/features/albums/hooks/use-album
|
|||||||
import { artistsQueries } from '/@/renderer/features/artists/api/artists-api';
|
import { artistsQueries } from '/@/renderer/features/artists/api/artists-api';
|
||||||
import { genresQueries } from '/@/renderer/features/genres/api/genres-api';
|
import { genresQueries } from '/@/renderer/features/genres/api/genres-api';
|
||||||
import { sharedQueries } from '/@/renderer/features/shared/api/shared-api';
|
import { sharedQueries } from '/@/renderer/features/shared/api/shared-api';
|
||||||
import { AlbumListFilter } from '/@/renderer/store';
|
import { AlbumListFilter, useCurrentServerId } from '/@/renderer/store';
|
||||||
import { Divider } from '/@/shared/components/divider/divider';
|
import { Divider } from '/@/shared/components/divider/divider';
|
||||||
import { Group } from '/@/shared/components/group/group';
|
import { Group } from '/@/shared/components/group/group';
|
||||||
import { NumberInput } from '/@/shared/components/number-input/number-input';
|
import { NumberInput } from '/@/shared/components/number-input/number-input';
|
||||||
import { SpinnerIcon } from '/@/shared/components/spinner/spinner';
|
import { SpinnerIcon } from '/@/shared/components/spinner/spinner';
|
||||||
import { Stack } from '/@/shared/components/stack/stack';
|
import { Stack } from '/@/shared/components/stack/stack';
|
||||||
import { Text } from '/@/shared/components/text/text';
|
|
||||||
import { YesNoSelect } from '/@/shared/components/yes-no-select/yes-no-select';
|
import { YesNoSelect } from '/@/shared/components/yes-no-select/yes-no-select';
|
||||||
import {
|
import {
|
||||||
AlbumArtistListSort,
|
AlbumArtistListSort,
|
||||||
@@ -27,14 +25,11 @@ interface JellyfinAlbumFiltersProps {
|
|||||||
customFilters?: Partial<AlbumListFilter>;
|
customFilters?: Partial<AlbumListFilter>;
|
||||||
disableArtistFilter?: boolean;
|
disableArtistFilter?: boolean;
|
||||||
onFilterChange: (filters: AlbumListFilter) => void;
|
onFilterChange: (filters: AlbumListFilter) => void;
|
||||||
serverId: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const JellyfinAlbumFilters = ({
|
export const JellyfinAlbumFilters = ({ disableArtistFilter }: JellyfinAlbumFiltersProps) => {
|
||||||
disableArtistFilter,
|
|
||||||
serverId,
|
|
||||||
}: JellyfinAlbumFiltersProps) => {
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const serverId = useCurrentServerId();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
query,
|
query,
|
||||||
@@ -110,21 +105,50 @@ export const JellyfinAlbumFilters = ({
|
|||||||
setCompilation,
|
setCompilation,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const handleMinYearFilter = debounce((e: number | string) => {
|
const handleMinYearFilter = useMemo(
|
||||||
if (typeof e === 'number' && (e < 1700 || e > 2300)) return;
|
() => (e: number | string) => {
|
||||||
const year = e === '' ? undefined : (e as number);
|
// Handle empty string, null, undefined, or invalid numbers as clearing
|
||||||
setMinYear(year ?? null);
|
if (e === '' || e === null || e === undefined || isNaN(Number(e))) {
|
||||||
}, 500);
|
setMinYear(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const handleMaxYearFilter = debounce((e: number | string) => {
|
const year = typeof e === 'number' ? e : Number(e);
|
||||||
if (typeof e === 'number' && (e < 1700 || e > 2300)) return;
|
// If it's a valid number within range, set it; otherwise clear
|
||||||
const year = e === '' ? undefined : (e as number);
|
if (!isNaN(year) && isFinite(year) && year >= 1700 && year <= 2300) {
|
||||||
setMaxYear(year ?? null);
|
setMinYear(year);
|
||||||
}, 500);
|
} else {
|
||||||
|
setMinYear(null);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[setMinYear],
|
||||||
|
);
|
||||||
|
|
||||||
const handleGenresFilter = debounce((e: string[] | undefined) => {
|
const handleMaxYearFilter = useMemo(
|
||||||
setGenreId(e ?? null);
|
() => (e: number | string) => {
|
||||||
}, 250);
|
// Handle empty string, null, undefined, or invalid numbers as clearing
|
||||||
|
if (e === '' || e === null || e === undefined || isNaN(Number(e))) {
|
||||||
|
setMaxYear(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const year = typeof e === 'number' ? e : Number(e);
|
||||||
|
// If it's a valid number within range, set it; otherwise clear
|
||||||
|
if (!isNaN(year) && isFinite(year) && year >= 1700 && year <= 2300) {
|
||||||
|
setMaxYear(year);
|
||||||
|
} else {
|
||||||
|
setMaxYear(null);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[setMaxYear],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleGenresFilter = useMemo(
|
||||||
|
() => (e: string[] | undefined) => {
|
||||||
|
setGenreId(e && e.length > 0 ? e : null);
|
||||||
|
},
|
||||||
|
[setGenreId],
|
||||||
|
);
|
||||||
|
|
||||||
const albumArtistListQuery = useQuery(
|
const albumArtistListQuery = useQuery(
|
||||||
artistsQueries.albumArtistList({
|
artistsQueries.albumArtistList({
|
||||||
@@ -154,24 +178,46 @@ export const JellyfinAlbumFilters = ({
|
|||||||
setAlbumArtist(e ?? null);
|
setAlbumArtist(e ?? null);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleTagFilter = debounce((e: string[] | undefined) => {
|
const handleTagFilter = useMemo(
|
||||||
setCustom((prev) => ({
|
() => (e: string[] | undefined) => {
|
||||||
...prev,
|
setCustom((prev) => {
|
||||||
[e?.join('|') || '']: e?.join('|') || undefined,
|
if (!prev) {
|
||||||
}));
|
return e && e.length > 0 ? { [e.join('|')]: e.join('|') } : null;
|
||||||
}, 250);
|
}
|
||||||
|
|
||||||
|
if (!e || e.length === 0) {
|
||||||
|
// Remove all tag-related properties (they use '|' joined keys)
|
||||||
|
const rest = Object.fromEntries(
|
||||||
|
Object.entries(prev).filter(([key]) => !key.includes('|')),
|
||||||
|
);
|
||||||
|
|
||||||
|
return Object.keys(rest).length === 0 ? null : rest;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove old tag entries and add new one
|
||||||
|
const rest = Object.fromEntries(
|
||||||
|
Object.entries(prev).filter(([key]) => !key.includes('|')),
|
||||||
|
);
|
||||||
|
const tagKey = e.join('|');
|
||||||
|
|
||||||
|
return {
|
||||||
|
...rest,
|
||||||
|
[tagKey]: tagKey,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[setCustom],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack p="0.8rem">
|
<Stack p="0.8rem">
|
||||||
{yesNoFilter.map((filter) => (
|
{yesNoFilter.map((filter) => (
|
||||||
<Group justify="space-between" key={`jf-filter-${filter.label}`}>
|
<YesNoSelect
|
||||||
<Text>{filter.label}</Text>
|
key={`jf-filter-${filter.label}`}
|
||||||
<YesNoSelect
|
label={filter.label}
|
||||||
onChange={filter.onChange}
|
onChange={filter.onChange}
|
||||||
size="xs"
|
value={filter.value ?? undefined}
|
||||||
value={filter.value ?? undefined}
|
/>
|
||||||
/>
|
|
||||||
</Group>
|
|
||||||
))}
|
))}
|
||||||
<Divider my="0.5rem" />
|
<Divider my="0.5rem" />
|
||||||
<Group grow>
|
<Group grow>
|
||||||
@@ -181,7 +227,7 @@ export const JellyfinAlbumFilters = ({
|
|||||||
label={t('filter.fromYear', { postProcess: 'sentenceCase' })}
|
label={t('filter.fromYear', { postProcess: 'sentenceCase' })}
|
||||||
max={2300}
|
max={2300}
|
||||||
min={1700}
|
min={1700}
|
||||||
onChange={(e) => handleMinYearFilter(e)}
|
onBlur={(e) => handleMinYearFilter(e.currentTarget.value)}
|
||||||
required={!!query.minYear}
|
required={!!query.minYear}
|
||||||
/>
|
/>
|
||||||
<NumberInput
|
<NumberInput
|
||||||
@@ -190,49 +236,40 @@ export const JellyfinAlbumFilters = ({
|
|||||||
label={t('filter.toYear', { postProcess: 'sentenceCase' })}
|
label={t('filter.toYear', { postProcess: 'sentenceCase' })}
|
||||||
max={2300}
|
max={2300}
|
||||||
min={1700}
|
min={1700}
|
||||||
onChange={(e) => handleMaxYearFilter(e)}
|
onBlur={(e) => handleMaxYearFilter(e.currentTarget.value)}
|
||||||
required={!!query.minYear}
|
required={!!query.minYear}
|
||||||
/>
|
/>
|
||||||
</Group>
|
</Group>
|
||||||
<Group grow>
|
<MultiSelectWithInvalidData
|
||||||
<MultiSelectWithInvalidData
|
clearable
|
||||||
clearable
|
data={genreList}
|
||||||
data={genreList}
|
defaultValue={query.genreIds ?? undefined}
|
||||||
defaultValue={query.genreId ?? undefined}
|
label={t('entity.genre', { count: 2, postProcess: 'sentenceCase' })}
|
||||||
label={t('entity.genre', { count: 2, postProcess: 'sentenceCase' })}
|
onChange={(e) => handleGenresFilter(e)}
|
||||||
onChange={handleGenresFilter}
|
searchable
|
||||||
searchable
|
/>
|
||||||
/>
|
|
||||||
</Group>
|
|
||||||
|
|
||||||
<Group grow>
|
<MultiSelectWithInvalidData
|
||||||
|
clearable
|
||||||
|
data={selectableAlbumArtists}
|
||||||
|
defaultValue={query.artistIds ?? undefined}
|
||||||
|
disabled={disableArtistFilter}
|
||||||
|
label={t('entity.artist', { count: 2, postProcess: 'sentenceCase' })}
|
||||||
|
limit={300}
|
||||||
|
onChange={handleAlbumArtistFilter}
|
||||||
|
rightSection={albumArtistListQuery.isFetching ? <SpinnerIcon /> : undefined}
|
||||||
|
searchable
|
||||||
|
/>
|
||||||
|
{tagsQuery.data?.boolTags && tagsQuery.data.boolTags.length > 0 && (
|
||||||
<MultiSelectWithInvalidData
|
<MultiSelectWithInvalidData
|
||||||
clearable
|
clearable
|
||||||
data={selectableAlbumArtists}
|
data={tagsQuery.data.boolTags}
|
||||||
defaultValue={query.artistIds ?? undefined}
|
defaultValue={query._custom?.[tagsQuery.data.boolTags.join('|')] ?? undefined}
|
||||||
disabled={disableArtistFilter}
|
label={t('common.tags', { postProcess: 'sentenceCase' })}
|
||||||
label={t('entity.artist', { count: 2, postProcess: 'sentenceCase' })}
|
onChange={handleTagFilter}
|
||||||
limit={300}
|
|
||||||
onChange={handleAlbumArtistFilter}
|
|
||||||
placeholder="Type to search for an artist"
|
|
||||||
rightSection={albumArtistListQuery.isFetching ? <SpinnerIcon /> : undefined}
|
|
||||||
searchable
|
searchable
|
||||||
|
width={250}
|
||||||
/>
|
/>
|
||||||
</Group>
|
|
||||||
{tagsQuery.data?.boolTags && tagsQuery.data.boolTags.length > 0 && (
|
|
||||||
<Group grow>
|
|
||||||
<MultiSelectWithInvalidData
|
|
||||||
clearable
|
|
||||||
data={tagsQuery.data.boolTags}
|
|
||||||
defaultValue={
|
|
||||||
query._custom?.[tagsQuery.data.boolTags.join('|')] ?? undefined
|
|
||||||
}
|
|
||||||
label={t('common.tags', { postProcess: 'sentenceCase' })}
|
|
||||||
onChange={handleTagFilter}
|
|
||||||
searchable
|
|
||||||
width={250}
|
|
||||||
/>
|
|
||||||
</Group>
|
|
||||||
)}
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,14 +1,12 @@
|
|||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import debounce from 'lodash/debounce';
|
|
||||||
import { parseAsArrayOf, parseAsBoolean, parseAsInteger, parseAsString, useQueryState } from 'nuqs';
|
|
||||||
import { ChangeEvent, useMemo, useState } from 'react';
|
import { ChangeEvent, useMemo, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { MultiSelectWithInvalidData } from '/@/renderer/components/select-with-invalid-data';
|
import { MultiSelectWithInvalidData } from '/@/renderer/components/select-with-invalid-data';
|
||||||
|
import { useAlbumListFilters } from '/@/renderer/features/albums/hooks/use-album-list-filters';
|
||||||
import { artistsQueries } from '/@/renderer/features/artists/api/artists-api';
|
import { artistsQueries } from '/@/renderer/features/artists/api/artists-api';
|
||||||
import { genresQueries } from '/@/renderer/features/genres/api/genres-api';
|
import { genresQueries } from '/@/renderer/features/genres/api/genres-api';
|
||||||
import { FILTER_KEYS } from '/@/renderer/features/shared/utils';
|
import { useCurrentServerId } from '/@/renderer/store';
|
||||||
import { AlbumListFilter } from '/@/renderer/store';
|
|
||||||
import { Divider } from '/@/shared/components/divider/divider';
|
import { Divider } from '/@/shared/components/divider/divider';
|
||||||
import { Group } from '/@/shared/components/group/group';
|
import { Group } from '/@/shared/components/group/group';
|
||||||
import { NumberInput } from '/@/shared/components/number-input/number-input';
|
import { NumberInput } from '/@/shared/components/number-input/number-input';
|
||||||
@@ -21,29 +19,15 @@ import { AlbumArtistListSort, GenreListSort, SortOrder } from '/@/shared/types/d
|
|||||||
|
|
||||||
interface SubsonicAlbumFiltersProps {
|
interface SubsonicAlbumFiltersProps {
|
||||||
disableArtistFilter?: boolean;
|
disableArtistFilter?: boolean;
|
||||||
onFilterChange: (filters: AlbumListFilter) => void;
|
|
||||||
serverId: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SubsonicAlbumFilters = ({
|
export const SubsonicAlbumFilters = ({ disableArtistFilter }: SubsonicAlbumFiltersProps) => {
|
||||||
disableArtistFilter,
|
|
||||||
onFilterChange,
|
|
||||||
serverId,
|
|
||||||
}: SubsonicAlbumFiltersProps) => {
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const [favorite, setFavorite] = useQueryState(FILTER_KEYS.ALBUM.FAVORITE, parseAsBoolean);
|
const serverId = useCurrentServerId();
|
||||||
|
|
||||||
const [minYear, setMinYear] = useQueryState(FILTER_KEYS.ALBUM.MIN_YEAR, parseAsInteger);
|
const { query, setAlbumArtist, setFavorite, setGenreId, setMaxYear, setMinYear } =
|
||||||
|
useAlbumListFilters();
|
||||||
const [maxYear, setMaxYear] = useQueryState(FILTER_KEYS.ALBUM.MAX_YEAR, parseAsInteger);
|
|
||||||
|
|
||||||
const [genreId, setGenreId] = useQueryState(FILTER_KEYS.ALBUM.GENRE_ID, parseAsString);
|
|
||||||
|
|
||||||
const [artistIds, setArtistIds] = useQueryState(
|
|
||||||
FILTER_KEYS.ALBUM.ARTIST_IDS,
|
|
||||||
parseAsArrayOf(parseAsString),
|
|
||||||
);
|
|
||||||
|
|
||||||
const [albumArtistSearchTerm, setAlbumArtistSearchTerm] = useState<string>('');
|
const [albumArtistSearchTerm, setAlbumArtistSearchTerm] = useState<string>('');
|
||||||
|
|
||||||
@@ -73,13 +57,12 @@ export const SubsonicAlbumFilters = ({
|
|||||||
}));
|
}));
|
||||||
}, [items]);
|
}, [items]);
|
||||||
|
|
||||||
const handleAlbumArtistFilter = (e: null | string[]) => {
|
const handleAlbumArtistFilter = useMemo(
|
||||||
setArtistIds(e ?? null);
|
() => (e: null | string[]) => {
|
||||||
const updatedFilters: Partial<AlbumListFilter> = {
|
setAlbumArtist(e ?? null);
|
||||||
artistIds: e?.length ? e : undefined,
|
},
|
||||||
};
|
[setAlbumArtist],
|
||||||
onFilterChange(updatedFilters as AlbumListFilter);
|
);
|
||||||
};
|
|
||||||
|
|
||||||
const genreListQuery = useQuery(
|
const genreListQuery = useQuery(
|
||||||
genresQueries.list({
|
genresQueries.list({
|
||||||
@@ -104,43 +87,64 @@ export const SubsonicAlbumFilters = ({
|
|||||||
}));
|
}));
|
||||||
}, [genreListQuery.data]);
|
}, [genreListQuery.data]);
|
||||||
|
|
||||||
const handleGenresFilter = debounce((e: null | string) => {
|
const handleGenresFilter = useMemo(
|
||||||
setGenreId(e ?? null);
|
() => (e: null | string) => {
|
||||||
const updatedFilters: Partial<AlbumListFilter> = {
|
setGenreId(e ? [e] : null);
|
||||||
genreIds: e ? [e] : undefined,
|
|
||||||
};
|
|
||||||
onFilterChange(updatedFilters as AlbumListFilter);
|
|
||||||
}, 250);
|
|
||||||
|
|
||||||
const toggleFilters = [
|
|
||||||
{
|
|
||||||
label: t('filter.isFavorited', { postProcess: 'sentenceCase' }),
|
|
||||||
onChange: (e: ChangeEvent<HTMLInputElement>) => {
|
|
||||||
const favoriteValue = e.target.checked ? true : undefined;
|
|
||||||
setFavorite(favoriteValue ?? null);
|
|
||||||
const updatedFilters: Partial<AlbumListFilter> = {
|
|
||||||
favorite: favoriteValue,
|
|
||||||
};
|
|
||||||
onFilterChange(updatedFilters as AlbumListFilter);
|
|
||||||
},
|
|
||||||
value: favorite,
|
|
||||||
},
|
},
|
||||||
];
|
[setGenreId],
|
||||||
|
);
|
||||||
|
|
||||||
const handleYearFilter = debounce((e: number | string, type: 'max' | 'min') => {
|
const toggleFilters = useMemo(
|
||||||
const year = e ? Number(e) : undefined;
|
() => [
|
||||||
|
{
|
||||||
|
label: t('filter.isFavorited', { postProcess: 'sentenceCase' }),
|
||||||
|
onChange: (e: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const favoriteValue = e.target.checked ? true : undefined;
|
||||||
|
setFavorite(favoriteValue ?? null);
|
||||||
|
},
|
||||||
|
value: query.favorite,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[t, query.favorite, setFavorite],
|
||||||
|
);
|
||||||
|
|
||||||
if (type === 'min') {
|
const handleMinYearFilter = useMemo(
|
||||||
setMinYear(year ?? null);
|
() => (e: number | string) => {
|
||||||
} else {
|
// Handle empty string, null, undefined, or invalid numbers as clearing
|
||||||
setMaxYear(year ?? null);
|
if (e === '' || e === null || e === undefined || isNaN(Number(e))) {
|
||||||
}
|
setMinYear(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const updatedFilters: Partial<AlbumListFilter> = {
|
const year = typeof e === 'number' ? e : Number(e);
|
||||||
[type === 'min' ? 'minYear' : 'maxYear']: year,
|
// If it's a valid number, set it; otherwise clear
|
||||||
};
|
if (!isNaN(year) && isFinite(year) && year > 0) {
|
||||||
onFilterChange(updatedFilters as AlbumListFilter);
|
setMinYear(year);
|
||||||
}, 500);
|
} else {
|
||||||
|
setMinYear(null);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[setMinYear],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleMaxYearFilter = useMemo(
|
||||||
|
() => (e: number | string) => {
|
||||||
|
// Handle empty string, null, undefined, or invalid numbers as clearing
|
||||||
|
if (e === '' || e === null || e === undefined || isNaN(Number(e))) {
|
||||||
|
setMaxYear(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const year = typeof e === 'number' ? e : Number(e);
|
||||||
|
// If it's a valid number, set it; otherwise clear
|
||||||
|
if (!isNaN(year) && isFinite(year) && year > 0) {
|
||||||
|
setMaxYear(year);
|
||||||
|
} else {
|
||||||
|
setMaxYear(null);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[setMaxYear],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack p="0.8rem">
|
<Stack p="0.8rem">
|
||||||
@@ -153,51 +157,46 @@ export const SubsonicAlbumFilters = ({
|
|||||||
<Divider my="0.5rem" />
|
<Divider my="0.5rem" />
|
||||||
<Group grow>
|
<Group grow>
|
||||||
<NumberInput
|
<NumberInput
|
||||||
defaultValue={minYear ?? undefined}
|
defaultValue={query.minYear ?? undefined}
|
||||||
disabled={genreId !== null}
|
disabled={Boolean(query.genreIds && query.genreIds.length > 0)}
|
||||||
hideControls={false}
|
hideControls={false}
|
||||||
label={t('filter.fromYear', { postProcess: 'sentenceCase' })}
|
label={t('filter.fromYear', { postProcess: 'sentenceCase' })}
|
||||||
max={5000}
|
max={5000}
|
||||||
min={0}
|
min={0}
|
||||||
onChange={(e) => handleYearFilter(e, 'min')}
|
onBlur={(e) => handleMinYearFilter(e.currentTarget.value)}
|
||||||
/>
|
/>
|
||||||
<NumberInput
|
<NumberInput
|
||||||
defaultValue={maxYear ?? undefined}
|
defaultValue={query.maxYear ?? undefined}
|
||||||
disabled={genreId !== null}
|
disabled={Boolean(query.genreIds && query.genreIds.length > 0)}
|
||||||
hideControls={false}
|
hideControls={false}
|
||||||
label={t('filter.toYear', { postProcess: 'sentenceCase' })}
|
label={t('filter.toYear', { postProcess: 'sentenceCase' })}
|
||||||
max={5000}
|
max={5000}
|
||||||
min={0}
|
min={0}
|
||||||
onChange={(e) => handleYearFilter(e, 'max')}
|
onBlur={(e) => handleMaxYearFilter(e.currentTarget.value)}
|
||||||
/>
|
|
||||||
</Group>
|
|
||||||
<Group grow>
|
|
||||||
<Select
|
|
||||||
clearable
|
|
||||||
data={genreList}
|
|
||||||
defaultValue={genreId ?? undefined}
|
|
||||||
disabled={Boolean(minYear || maxYear)}
|
|
||||||
label={t('entity.genre', { count: 1, postProcess: 'titleCase' })}
|
|
||||||
onChange={handleGenresFilter}
|
|
||||||
searchable
|
|
||||||
/>
|
|
||||||
</Group>
|
|
||||||
<Group grow>
|
|
||||||
<MultiSelectWithInvalidData
|
|
||||||
clearable
|
|
||||||
data={selectableAlbumArtists}
|
|
||||||
defaultValue={artistIds ?? undefined}
|
|
||||||
disabled={disableArtistFilter}
|
|
||||||
label={t('entity.artist', { count: 2, postProcess: 'sentenceCase' })}
|
|
||||||
limit={300}
|
|
||||||
onChange={handleAlbumArtistFilter}
|
|
||||||
onSearchChange={setAlbumArtistSearchTerm}
|
|
||||||
placeholder="Type to search for an artist"
|
|
||||||
rightSection={albumArtistListQuery.isFetching ? <SpinnerIcon /> : undefined}
|
|
||||||
searchable
|
|
||||||
searchValue={albumArtistSearchTerm}
|
|
||||||
/>
|
/>
|
||||||
</Group>
|
</Group>
|
||||||
|
<Select
|
||||||
|
clearable
|
||||||
|
data={genreList}
|
||||||
|
defaultValue={query.genreIds?.[0] ?? undefined}
|
||||||
|
disabled={Boolean(query.minYear || query.maxYear)}
|
||||||
|
label={t('entity.genre', { count: 1, postProcess: 'titleCase' })}
|
||||||
|
onChange={(e) => handleGenresFilter(e)}
|
||||||
|
searchable
|
||||||
|
/>
|
||||||
|
<MultiSelectWithInvalidData
|
||||||
|
clearable
|
||||||
|
data={selectableAlbumArtists}
|
||||||
|
defaultValue={query.artistIds ?? undefined}
|
||||||
|
disabled={disableArtistFilter}
|
||||||
|
label={t('entity.artist', { count: 2, postProcess: 'sentenceCase' })}
|
||||||
|
limit={300}
|
||||||
|
onChange={handleAlbumArtistFilter}
|
||||||
|
onSearchChange={setAlbumArtistSearchTerm}
|
||||||
|
rightSection={albumArtistListQuery.isFetching ? <SpinnerIcon /> : undefined}
|
||||||
|
searchable
|
||||||
|
searchValue={albumArtistSearchTerm}
|
||||||
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import {
|
|||||||
export const genresQueries = {
|
export const genresQueries = {
|
||||||
list: (args: QueryHookArgs<GenreListQuery>) => {
|
list: (args: QueryHookArgs<GenreListQuery>) => {
|
||||||
return queryOptions({
|
return queryOptions({
|
||||||
gcTime: 1000 * 5,
|
gcTime: 1000 * 60 * 60,
|
||||||
queryFn: ({ signal }) => {
|
queryFn: ({ signal }) => {
|
||||||
return api.controller.getGenreList({
|
return api.controller.getGenreList({
|
||||||
apiClientProps: { serverId: args.serverId, signal },
|
apiClientProps: { serverId: args.serverId, signal },
|
||||||
@@ -22,6 +22,7 @@ export const genresQueries = {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
queryKey: queryKeys.genres.list(args.serverId, args.query),
|
queryKey: queryKeys.genres.list(args.serverId, args.query),
|
||||||
|
staleTime: 1000 * 60 * 60,
|
||||||
...args.options,
|
...args.options,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ export const sharedQueries = {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
queryKey: queryKeys.tags.list(args.serverId || '', args.query.type),
|
queryKey: queryKeys.tags.list(args.serverId || '', args.query.type),
|
||||||
|
staleTime: 1000 * 60,
|
||||||
...args.options,
|
...args.options,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import debounce from 'lodash/debounce';
|
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
@@ -7,7 +6,7 @@ import { MultiSelectWithInvalidData } from '/@/renderer/components/select-with-i
|
|||||||
import { useGenreList } from '/@/renderer/features/genres/api/genres-api';
|
import { useGenreList } from '/@/renderer/features/genres/api/genres-api';
|
||||||
import { sharedQueries } from '/@/renderer/features/shared/api/shared-api';
|
import { sharedQueries } from '/@/renderer/features/shared/api/shared-api';
|
||||||
import { useSongListFilters } from '/@/renderer/features/songs/hooks/use-song-list-filters';
|
import { useSongListFilters } from '/@/renderer/features/songs/hooks/use-song-list-filters';
|
||||||
import { SongListFilter, useCurrentServer } from '/@/renderer/store';
|
import { SongListFilter, useCurrentServerId } from '/@/renderer/store';
|
||||||
import { Divider } from '/@/shared/components/divider/divider';
|
import { Divider } from '/@/shared/components/divider/divider';
|
||||||
import { Group } from '/@/shared/components/group/group';
|
import { Group } from '/@/shared/components/group/group';
|
||||||
import { NumberInput } from '/@/shared/components/number-input/number-input';
|
import { NumberInput } from '/@/shared/components/number-input/number-input';
|
||||||
@@ -21,7 +20,7 @@ interface JellyfinSongFiltersProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const JellyfinSongFilters = ({ customFilters }: JellyfinSongFiltersProps) => {
|
export const JellyfinSongFilters = ({ customFilters }: JellyfinSongFiltersProps) => {
|
||||||
const server = useCurrentServer();
|
const serverId = useCurrentServerId();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { query, setCustom, setFavorite, setMaxYear, setMinYear } = useSongListFilters();
|
const { query, setCustom, setFavorite, setMaxYear, setMinYear } = useSongListFilters();
|
||||||
|
|
||||||
@@ -44,7 +43,7 @@ export const JellyfinSongFilters = ({ customFilters }: JellyfinSongFiltersProps)
|
|||||||
query: {
|
query: {
|
||||||
type: LibraryItem.SONG,
|
type: LibraryItem.SONG,
|
||||||
},
|
},
|
||||||
serverId: server.id,
|
serverId,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -66,33 +65,91 @@ export const JellyfinSongFilters = ({ customFilters }: JellyfinSongFiltersProps)
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const handleMinYearFilter = debounce((e: number | string) => {
|
const handleMinYearFilter = useMemo(
|
||||||
if (typeof e === 'number' && (e < 1700 || e > 2300)) return;
|
() => (e: number | string) => {
|
||||||
setMinYear(e === '' ? null : (e as number));
|
// Handle empty string, null, undefined, or invalid numbers as clearing
|
||||||
}, 500);
|
if (e === '' || e === null || e === undefined || isNaN(Number(e))) {
|
||||||
|
setMinYear(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const handleMaxYearFilter = debounce((e: number | string) => {
|
const year = typeof e === 'number' ? e : Number(e);
|
||||||
if (typeof e === 'number' && (e < 1700 || e > 2300)) return;
|
// If it's a valid number within range, set it; otherwise clear
|
||||||
setMaxYear(e === '' ? null : (e as number));
|
if (!isNaN(year) && isFinite(year) && year >= 1700 && year <= 2300) {
|
||||||
}, 500);
|
setMinYear(year);
|
||||||
|
} else {
|
||||||
|
setMinYear(null);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[setMinYear],
|
||||||
|
);
|
||||||
|
|
||||||
const handleGenresFilter = debounce((e: string[] | undefined) => {
|
const handleMaxYearFilter = useMemo(
|
||||||
setCustom((prev) => ({
|
() => (e: number | string) => {
|
||||||
...prev,
|
// Handle empty string, null, undefined, or invalid numbers as clearing
|
||||||
GenreIds: e?.join(',') || undefined,
|
if (e === '' || e === null || e === undefined || isNaN(Number(e))) {
|
||||||
IncludeItemTypes: 'Audio',
|
setMaxYear(null);
|
||||||
...prev?.jellyfin,
|
return;
|
||||||
}));
|
}
|
||||||
}, 250);
|
|
||||||
|
|
||||||
const handleTagFilter = debounce((e: string[] | undefined) => {
|
const year = typeof e === 'number' ? e : Number(e);
|
||||||
setCustom((prev) => ({
|
// If it's a valid number within range, set it; otherwise clear
|
||||||
...prev,
|
if (!isNaN(year) && isFinite(year) && year >= 1700 && year <= 2300) {
|
||||||
IncludeItemTypes: 'Audio',
|
setMaxYear(year);
|
||||||
Tags: e?.join('|') || undefined,
|
} else {
|
||||||
...prev?.jellyfin,
|
setMaxYear(null);
|
||||||
}));
|
}
|
||||||
}, 250);
|
},
|
||||||
|
[setMaxYear],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleGenresFilter = useMemo(
|
||||||
|
() => (e: string[] | undefined) => {
|
||||||
|
setCustom((prev) => {
|
||||||
|
if (!e || e.length === 0) {
|
||||||
|
// Remove GenreIds and IncludeItemTypes if genres are cleared
|
||||||
|
const rest = { ...prev };
|
||||||
|
delete rest.GenreIds;
|
||||||
|
delete rest.IncludeItemTypes;
|
||||||
|
// Keep jellyfin-specific properties
|
||||||
|
return Object.keys(rest).length === 0 ? null : rest;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...prev,
|
||||||
|
GenreIds: e.join(','),
|
||||||
|
IncludeItemTypes: 'Audio',
|
||||||
|
...prev?.jellyfin,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[setCustom],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleTagFilter = useMemo(
|
||||||
|
() => (e: string[] | undefined) => {
|
||||||
|
setCustom((prev) => {
|
||||||
|
if (!e || e.length === 0) {
|
||||||
|
// Remove Tags if cleared
|
||||||
|
const rest = { ...prev };
|
||||||
|
delete rest.Tags;
|
||||||
|
// Keep IncludeItemTypes and jellyfin-specific properties
|
||||||
|
if (rest.IncludeItemTypes) {
|
||||||
|
return rest;
|
||||||
|
}
|
||||||
|
return Object.keys(rest).length === 0 ? null : rest;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...prev,
|
||||||
|
IncludeItemTypes: 'Audio',
|
||||||
|
Tags: e.join('|'),
|
||||||
|
...prev?.jellyfin,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[setCustom],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack p="0.8rem">
|
<Stack p="0.8rem">
|
||||||
@@ -105,19 +162,21 @@ export const JellyfinSongFilters = ({ customFilters }: JellyfinSongFiltersProps)
|
|||||||
<Divider my="0.5rem" />
|
<Divider my="0.5rem" />
|
||||||
<Group grow>
|
<Group grow>
|
||||||
<NumberInput
|
<NumberInput
|
||||||
defaultValue={query.minYear}
|
defaultValue={query.minYear ?? undefined}
|
||||||
|
hideControls={false}
|
||||||
label={t('filter.fromYear', { postProcess: 'sentenceCase' })}
|
label={t('filter.fromYear', { postProcess: 'sentenceCase' })}
|
||||||
max={2300}
|
max={2300}
|
||||||
min={1700}
|
min={1700}
|
||||||
onChange={handleMinYearFilter}
|
onBlur={(e) => handleMinYearFilter(e.currentTarget.value)}
|
||||||
required={!!query.minYear}
|
required={!!query.minYear}
|
||||||
/>
|
/>
|
||||||
<NumberInput
|
<NumberInput
|
||||||
defaultValue={query.maxYear}
|
defaultValue={query.maxYear ?? undefined}
|
||||||
|
hideControls={false}
|
||||||
label={t('filter.toYear', { postProcess: 'sentenceCase' })}
|
label={t('filter.toYear', { postProcess: 'sentenceCase' })}
|
||||||
max={2300}
|
max={2300}
|
||||||
min={1700}
|
min={1700}
|
||||||
onChange={handleMaxYearFilter}
|
onBlur={(e) => handleMaxYearFilter(e.currentTarget.value)}
|
||||||
required={!!query.minYear}
|
required={!!query.minYear}
|
||||||
/>
|
/>
|
||||||
</Group>
|
</Group>
|
||||||
@@ -128,7 +187,7 @@ export const JellyfinSongFilters = ({ customFilters }: JellyfinSongFiltersProps)
|
|||||||
data={genreList}
|
data={genreList}
|
||||||
defaultValue={selectedGenres}
|
defaultValue={selectedGenres}
|
||||||
label={t('entity.genre', { count: 1, postProcess: 'sentenceCase' })}
|
label={t('entity.genre', { count: 1, postProcess: 'sentenceCase' })}
|
||||||
onChange={handleGenresFilter}
|
onChange={(e) => handleGenresFilter(e)}
|
||||||
searchable
|
searchable
|
||||||
width={250}
|
width={250}
|
||||||
/>
|
/>
|
||||||
@@ -141,7 +200,7 @@ export const JellyfinSongFilters = ({ customFilters }: JellyfinSongFiltersProps)
|
|||||||
data={tagsQuery.data.boolTags}
|
data={tagsQuery.data.boolTags}
|
||||||
defaultValue={selectedTags}
|
defaultValue={selectedTags}
|
||||||
label={t('common.tags', { postProcess: 'sentenceCase' })}
|
label={t('common.tags', { postProcess: 'sentenceCase' })}
|
||||||
onChange={handleTagFilter}
|
onChange={(e) => handleTagFilter(e)}
|
||||||
searchable
|
searchable
|
||||||
width={250}
|
width={250}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import debounce from 'lodash/debounce';
|
import { ChangeEvent, useMemo } from 'react';
|
||||||
import { useMemo } from 'react';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { SelectWithInvalidData } from '/@/renderer/components/select-with-invalid-data';
|
import { SelectWithInvalidData } from '/@/renderer/components/select-with-invalid-data';
|
||||||
@@ -9,8 +8,8 @@ import { SongListFilter } from '/@/renderer/store';
|
|||||||
import { Divider } from '/@/shared/components/divider/divider';
|
import { Divider } from '/@/shared/components/divider/divider';
|
||||||
import { Group } from '/@/shared/components/group/group';
|
import { Group } from '/@/shared/components/group/group';
|
||||||
import { Stack } from '/@/shared/components/stack/stack';
|
import { Stack } from '/@/shared/components/stack/stack';
|
||||||
|
import { Switch } from '/@/shared/components/switch/switch';
|
||||||
import { Text } from '/@/shared/components/text/text';
|
import { Text } from '/@/shared/components/text/text';
|
||||||
import { YesNoSelect } from '/@/shared/components/yes-no-select/yes-no-select';
|
|
||||||
|
|
||||||
interface SubsonicSongFiltersProps {
|
interface SubsonicSongFiltersProps {
|
||||||
customFilters?: Partial<SongListFilter>;
|
customFilters?: Partial<SongListFilter>;
|
||||||
@@ -32,26 +31,33 @@ export const SubsonicSongFilters = ({ customFilters }: SubsonicSongFiltersProps)
|
|||||||
}));
|
}));
|
||||||
}, [genreListQuery.data]);
|
}, [genreListQuery.data]);
|
||||||
|
|
||||||
const handleGenresFilter = debounce((e: null | string) => {
|
const handleGenresFilter = useMemo(
|
||||||
setGenreId(e ? [e] : null);
|
() => (e: null | string) => {
|
||||||
}, 250);
|
setGenreId(e ? [e] : null);
|
||||||
|
|
||||||
const toggleFilters = [
|
|
||||||
{
|
|
||||||
label: t('filter.isFavorited', { postProcess: 'sentenceCase' }),
|
|
||||||
onChange: (favorite: boolean | undefined) => {
|
|
||||||
setFavorite(favorite ?? null);
|
|
||||||
},
|
|
||||||
value: query.favorite,
|
|
||||||
},
|
},
|
||||||
];
|
[setGenreId],
|
||||||
|
);
|
||||||
|
|
||||||
|
const toggleFilters = useMemo(
|
||||||
|
() => [
|
||||||
|
{
|
||||||
|
label: t('filter.isFavorited', { postProcess: 'sentenceCase' }),
|
||||||
|
onChange: (e: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const favoriteValue = e.target.checked ? true : undefined;
|
||||||
|
setFavorite(favoriteValue ?? null);
|
||||||
|
},
|
||||||
|
value: query.favorite,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[t, query.favorite, setFavorite],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack p="0.8rem">
|
<Stack p="0.8rem">
|
||||||
{toggleFilters.map((filter) => (
|
{toggleFilters.map((filter) => (
|
||||||
<Group justify="space-between" key={`ss-filter-${filter.label}`}>
|
<Group justify="space-between" key={`ss-filter-${filter.label}`}>
|
||||||
<Text>{filter.label}</Text>
|
<Text>{filter.label}</Text>
|
||||||
<YesNoSelect onChange={filter.onChange} size="xs" value={filter.value} />
|
<Switch checked={filter.value ?? false} onChange={filter.onChange} />
|
||||||
</Group>
|
</Group>
|
||||||
))}
|
))}
|
||||||
<Divider my="0.5rem" />
|
<Divider my="0.5rem" />
|
||||||
|
|||||||
Reference in New Issue
Block a user