Initial work: support showing studios for jellyfin, allow pill to be clickable (#1566)

This commit is contained in:
Kendall Garner
2026-01-18 21:53:34 +00:00
committed by GitHub
parent cf428a14a3
commit 5c06624f8c
11 changed files with 228 additions and 315 deletions
@@ -1,17 +1,16 @@
import { useQuery, useSuspenseQuery } from '@tanstack/react-query';
import { useSuspenseQuery } from '@tanstack/react-query';
import { useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { getItemImageUrl } from '/@/renderer/components/item-image/item-image';
import { MultiSelectWithInvalidData } from '/@/renderer/components/select-with-invalid-data';
import { useListContext } from '/@/renderer/context/list-context';
import { artistsQueries } from '/@/renderer/features/artists/api/artists-api';
import { useGenreList } from '/@/renderer/features/genres/api/genres-api';
import { sharedQueries } from '/@/renderer/features/shared/api/shared-api';
import {
ArtistMultiSelectRow,
GenreMultiSelectRow,
} from '/@/renderer/features/shared/components/multi-select-rows';
import { TagFilters } from '/@/renderer/features/shared/components/tag-filter';
import { useSongListFilters } from '/@/renderer/features/songs/hooks/use-song-list-filters';
import { useCurrentServer } from '/@/renderer/store';
import { useAppStore, useAppStoreActions } from '/@/renderer/store/app.store';
@@ -87,25 +86,12 @@ export const JellyfinSongFilters = ({ disableArtistFilter }: JellyfinSongFilters
}));
}, [albumArtistListQuery.data?.items]);
const tagsQuery = useQuery(
sharedQueries.tagList({
query: {
type: LibraryItem.SONG,
},
serverId,
}),
);
const selectedArtistIds = useMemo(() => query.artistIds || [], [query.artistIds]);
const selectedGenres = useMemo(() => {
return query._custom?.GenreIds?.split(',') || [];
}, [query._custom?.GenreIds]);
const selectedTags = useMemo(() => {
return query._custom?.Tags?.split('|');
}, [query._custom?.Tags]);
const yesNoFilters = [
{
label: t('filter.isFavorited', { postProcess: 'sentenceCase' }),
@@ -181,13 +167,6 @@ export const JellyfinSongFilters = ({ disableArtistFilter }: JellyfinSongFilters
[setCustom],
);
const handleTagFilter = useCallback(
(e: null | string[]) => {
setCustom({ Tags: e && e.length > 0 ? e.join('|') : null });
},
[setCustom],
);
const artistSelectMode = useAppStore((state) => state.artistSelectMode);
const genreSelectMode = useAppStore((state) => state.genreSelectMode);
const { setArtistSelectMode, setGenreSelectMode } = useAppStoreActions();
@@ -339,16 +318,7 @@ export const JellyfinSongFilters = ({ disableArtistFilter }: JellyfinSongFilters
value={query.maxYear ?? undefined}
/>
</Group>
{tagsQuery.data?.boolTags && tagsQuery.data.boolTags.length > 0 && (
<MultiSelectWithInvalidData
clearable
data={tagsQuery.data.boolTags}
label={t('common.tags', { postProcess: 'sentenceCase' })}
onChange={handleTagFilter}
searchable
value={selectedTags}
/>
)}
<TagFilters query={query} setCustom={setCustom} type={LibraryItem.SONG} />
<Divider my="md" />
<Button fullWidth onClick={clear} variant="subtle">
{t('common.reset', { postProcess: 'sentenceCase' })}
@@ -3,20 +3,17 @@ import { useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { getItemImageUrl } from '/@/renderer/components/item-image/item-image';
import { MultiSelectWithInvalidData } from '/@/renderer/components/select-with-invalid-data';
import { useListContext } from '/@/renderer/context/list-context';
import { artistsQueries } from '/@/renderer/features/artists/api/artists-api';
import { useGenreList } from '/@/renderer/features/genres/api/genres-api';
import { sharedQueries } from '/@/renderer/features/shared/api/shared-api';
import {
ArtistMultiSelectRow,
GenreMultiSelectRow,
} from '/@/renderer/features/shared/components/multi-select-rows';
import { TagFilters } from '/@/renderer/features/shared/components/tag-filter';
import { useSongListFilters } from '/@/renderer/features/songs/hooks/use-song-list-filters';
import { useCurrentServer, useCurrentServerId } from '/@/renderer/store';
import { useCurrentServer } from '/@/renderer/store';
import { useAppStore, useAppStoreActions } from '/@/renderer/store/app.store';
import { titleCase } from '/@/renderer/utils';
import { NDSongQueryFieldsLabelMap } from '/@/shared/api/navidrome/navidrome-types';
import { Button } from '/@/shared/components/button/button';
import { Divider } from '/@/shared/components/divider/divider';
import { Group } from '/@/shared/components/group/group';
@@ -32,8 +29,16 @@ export const NavidromeSongFilters = () => {
const { t } = useTranslation();
const server = useCurrentServer();
const serverId = server.id;
const { clear, query, setArtistIds, setFavorite, setGenreId, setMaxYear, setMinYear } =
useSongListFilters();
const {
clear,
query,
setArtistIds,
setCustom,
setFavorite,
setGenreId,
setMaxYear,
setMinYear,
} = useSongListFilters();
const { customFilters } = useListContext();
@@ -292,8 +297,7 @@ export const NavidromeSongFilters = () => {
onChange={(e) => debouncedHandleYearFilter(e)}
value={query.minYear ?? undefined}
/>
<Divider my="md" />
<TagFilters />
<TagFilters query={query} setCustom={setCustom} type={LibraryItem.SONG} />
<Divider my="md" />
<Button fullWidth onClick={clear} variant="subtle">
{t('common.reset', { postProcess: 'sentenceCase' })}
@@ -301,105 +305,3 @@ export const NavidromeSongFilters = () => {
</Stack>
);
};
interface TagFilterItemProps {
label: string;
onChange: (value: null | string[]) => void;
options: Array<{ id: string; name: string }>;
tagValue: string;
value: string | string[] | undefined;
}
const TagFilterItem = ({ label, onChange, options, tagValue, value }: TagFilterItemProps) => {
const selectData = useMemo(
() =>
options.map((option) => ({
label: option.name,
value: option.id,
})),
[options],
);
const currentValue = useMemo(() => {
if (!value) return [];
return Array.isArray(value) ? value : [value];
}, [value]);
const handleChange = useCallback(
(e: null | string[]) => {
if (e && e.length > 0) {
onChange(e);
} else {
onChange(null);
}
},
[onChange],
);
return (
<MultiSelectWithInvalidData
clearable
data={selectData}
key={tagValue}
label={label}
limit={100}
onChange={handleChange}
searchable
value={currentValue}
/>
);
};
TagFilterItem.displayName = 'TagFilterItem';
const TagFilters = () => {
const { query, setCustom } = useSongListFilters();
const serverId = useCurrentServerId();
const tagsQuery = useSuspenseQuery(
sharedQueries.tagList({
query: { type: LibraryItem.SONG },
serverId,
}),
);
const handleTagFilter = useMemo(
() => (tag: string, e: null | string[]) => {
setCustom({ [tag]: e });
},
[setCustom],
);
const tags = useMemo(() => {
const results: { label: string; options: { id: string; name: string }[]; value: string }[] =
[];
for (const tag of tagsQuery.data?.enumTags || []) {
if (!tagsQuery.data?.excluded.song.includes(tag.name)) {
results.push({
label: NDSongQueryFieldsLabelMap[tag.name] ?? titleCase(tag.name),
options: tag.options,
value: tag.name,
});
}
}
return results;
}, [tagsQuery.data]);
return (
<>
{tags.map((tag) => (
<TagFilterItem
key={tag.value}
label={tag.label}
onChange={(e) => handleTagFilter(tag.value, e)}
options={tag.options}
tagValue={tag.value}
value={query._custom?.[tag.value] as string | string[] | undefined}
/>
))}
</>
);
};