1
0
Fork 0
mirror of https://github.com/jellyfin/jellyfin-web synced 2025-03-30 19:56:21 +00:00

Add Filter setting components

This commit is contained in:
grafixeyehero 2023-07-09 01:43:08 +03:00
parent 5598f49c32
commit 3ae27e05c7
14 changed files with 1392 additions and 8 deletions

View file

@ -0,0 +1,457 @@
import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind';
import React, { FC, useCallback } from 'react';
import ArrowForwardIosSharpIcon from '@mui/icons-material/ArrowForwardIosSharp';
import Box from '@mui/material/Box';
import FilterListIcon from '@mui/icons-material/FilterList';
import Popover from '@mui/material/Popover';
import MuiAccordion, { AccordionProps } from '@mui/material/Accordion';
import MuiAccordionDetails from '@mui/material/AccordionDetails';
import MuiAccordionSummary, {
AccordionSummaryProps
} from '@mui/material/AccordionSummary';
import IconButton from '@mui/material/IconButton';
import { styled } from '@mui/material/styles';
import Typography from '@mui/material/Typography';
import { useGetQueryFiltersLegacy, useGetStudios } from 'hooks/useFetchItems';
import globalize from 'scripts/globalize';
import FiltersFeatures from './FiltersFeatures';
import FiltersGenres from './FiltersGenres';
import FiltersOfficialRatings from './FiltersOfficialRatings';
import FiltersEpisodesStatus from './FiltersEpisodesStatus';
import FiltersSeriesStatus from './FiltersSeriesStatus';
import FiltersStatus from './FiltersStatus';
import FiltersStudios from './FiltersStudios';
import FiltersTags from './FiltersTags';
import FiltersVideoTypes from './FiltersVideoTypes';
import FiltersYears from './FiltersYears';
import { LibraryViewSettings } from 'types/library';
import { LibraryTab } from 'types/libraryTab';
const Accordion = styled((props: AccordionProps) => (
<MuiAccordion
disableGutters
elevation={0}
TransitionProps={{ unmountOnExit: true }}
square
{...props}
/>
))(({ theme }) => ({
border: `1px solid ${theme.palette.divider}`,
'&:not(:last-child)': {
borderBottom: 0
},
'&:before': {
display: 'none'
}
}));
const AccordionSummary = styled((props: AccordionSummaryProps) => (
<MuiAccordionSummary
expandIcon={<ArrowForwardIosSharpIcon sx={{ fontSize: '0.9rem' }} />}
{...props}
/>
))(({ theme }) => ({
backgroundColor:
theme.palette.mode === 'dark' ?
'rgba(255, 255, 255, .05)' :
'rgba(0, 0, 0, .03)',
'& .MuiAccordionSummary-expandIconWrapper.Mui-expanded': {
transform: 'rotate(90deg)'
},
'& .MuiAccordionSummary-content': {
marginLeft: theme.spacing(1)
}
}));
const AccordionDetails = styled(MuiAccordionDetails)(({ theme }) => ({
padding: theme.spacing(2),
borderTop: '1px solid rgba(0, 0, 0, .125)'
}));
interface FilterButtonProps {
parentId: string | null | undefined;
itemType: BaseItemKind;
viewType: LibraryTab;
libraryViewSettings: LibraryViewSettings;
setLibraryViewSettings: React.Dispatch<
React.SetStateAction<LibraryViewSettings>
>;
}
const FilterButton: FC<FilterButtonProps> = ({
parentId,
itemType,
viewType,
libraryViewSettings,
setLibraryViewSettings
}) => {
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
const [expanded, setExpanded] = React.useState<string | false>(false);
const open = Boolean(anchorEl);
const id = open ? 'filter-popover' : undefined;
const { data } = useGetQueryFiltersLegacy(parentId, itemType);
const { data: studios } = useGetStudios(parentId, itemType);
const handleChange =
(panel: string) =>
(event: React.SyntheticEvent, newExpanded: boolean) => {
setExpanded(newExpanded ? panel : false);
};
const handleClick = useCallback((event: React.MouseEvent<HTMLElement>) => {
setAnchorEl(event.currentTarget);
}, []);
const handleClose = useCallback(() => {
setAnchorEl(null);
}, []);
const isFiltersLegacyEnabled = () => {
return (
viewType === LibraryTab.Movies
|| viewType === LibraryTab.Series
|| viewType === LibraryTab.Albums
|| viewType === LibraryTab.AlbumArtists
|| viewType === LibraryTab.Artists
|| viewType === LibraryTab.Songs
|| viewType === LibraryTab.Episodes
);
};
const isFiltersStudiosEnabled = () => {
return (
viewType === LibraryTab.Movies
|| viewType === LibraryTab.Series
);
};
const isFiltersFeaturesEnabled = () => {
return (
viewType === LibraryTab.Movies
|| viewType === LibraryTab.Series
|| viewType === LibraryTab.Episodes
);
};
const isFiltersVideoTypesEnabled = () => {
return (
viewType === LibraryTab.Movies
|| viewType === LibraryTab.Episodes
);
};
const isFiltersSeriesStatusEnabled = () => {
return viewType === LibraryTab.Series;
};
const isFiltersEpisodesStatusEnabled = () => {
return viewType === LibraryTab.Episodes;
};
return (
<Box>
<IconButton
title={globalize.translate('Filter')}
sx={{ ml: 2 }}
aria-describedby={id}
className='paper-icon-button-light btnShuffle autoSize'
onClick={handleClick}
>
<FilterListIcon />
</IconButton>
<Popover
id={id}
open={open}
anchorEl={anchorEl}
onClose={handleClose}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'center'
}}
transformOrigin={{
vertical: 'top',
horizontal: 'center'
}}
PaperProps={{
style: {
maxHeight: '50%',
width: 250
}
}}
>
<Accordion
expanded={expanded === 'filtersStatus'}
onChange={handleChange('filtersStatus')}
>
<AccordionSummary
aria-controls='filtersStatus-content'
id='filtersStatus-header'
>
<Typography>
{globalize.translate('Filters')}
</Typography>
</AccordionSummary>
<AccordionDetails>
<FiltersStatus
viewType={viewType}
libraryViewSettings={libraryViewSettings}
setLibraryViewSettings={setLibraryViewSettings}
/>
</AccordionDetails>
</Accordion>
{isFiltersSeriesStatusEnabled() && (
<>
<Accordion
expanded={expanded === 'filtersSeriesStatus'}
onChange={handleChange('filtersSeriesStatus')}
>
<AccordionSummary
aria-controls='filtersSeriesStatus-content'
id='filtersSeriesStatus-header'
>
<Typography>
{globalize.translate('HeaderSeriesStatus')}
</Typography>
</AccordionSummary>
<AccordionDetails>
<FiltersSeriesStatus
libraryViewSettings={libraryViewSettings}
setLibraryViewSettings={
setLibraryViewSettings
}
/>
</AccordionDetails>
</Accordion>
</>
)}
{isFiltersEpisodesStatusEnabled() && (
<>
<Accordion
expanded={expanded === 'filtersEpisodesStatus'}
onChange={handleChange('filtersEpisodesStatus')}
>
<AccordionSummary
aria-controls='filtersEpisodesStatus-content'
id='filtersEpisodesStatus-header'
>
<Typography>
{globalize.translate('HeaderEpisodesStatus')}
</Typography>
</AccordionSummary>
<AccordionDetails>
<FiltersEpisodesStatus
libraryViewSettings={libraryViewSettings}
setLibraryViewSettings={
setLibraryViewSettings
}
/>
</AccordionDetails>
</Accordion>
</>
)}
{isFiltersFeaturesEnabled() && (
<>
<Accordion
expanded={expanded === 'filtersFeatures'}
onChange={handleChange('filtersFeatures')}
>
<AccordionSummary
aria-controls='filtersFeatures-content'
id='filtersFeatures-header'
>
<Typography>
{globalize.translate('Features')}
</Typography>
</AccordionSummary>
<AccordionDetails>
<FiltersFeatures
libraryViewSettings={libraryViewSettings}
setLibraryViewSettings={
setLibraryViewSettings
}
/>
</AccordionDetails>
</Accordion>
</>
)}
{isFiltersVideoTypesEnabled() && (
<>
<Accordion
expanded={expanded === 'filtersVideoTypes'}
onChange={handleChange('filtersVideoTypes')}
>
<AccordionSummary
aria-controls='filtersVideoTypes-content'
id='filtersVideoTypes-header'
>
<Typography>
{globalize.translate('HeaderVideoType')}
</Typography>
</AccordionSummary>
<AccordionDetails>
<FiltersVideoTypes
libraryViewSettings={libraryViewSettings}
setLibraryViewSettings={
setLibraryViewSettings
}
/>
</AccordionDetails>
</Accordion>
</>
)}
{isFiltersLegacyEnabled() && (
<>
{data?.Genres && data?.Genres?.length > 0 && (
<Accordion
expanded={expanded === 'filtersGenres'}
onChange={handleChange('filtersGenres')}
>
<AccordionSummary
aria-controls='filtersGenres-content'
id='filtersGenres-header'
>
<Typography>
{globalize.translate('Genres')}
</Typography>
</AccordionSummary>
<AccordionDetails>
<FiltersGenres
filtes={data}
libraryViewSettings={
libraryViewSettings
}
setLibraryViewSettings={
setLibraryViewSettings
}
/>
</AccordionDetails>
</Accordion>
)}
{data?.OfficialRatings
&& data?.OfficialRatings?.length > 0 && (
<Accordion
expanded={
expanded === 'filtersOfficialRatings'
}
onChange={handleChange(
'filtersOfficialRatings'
)}
>
<AccordionSummary
aria-controls='filtersOfficialRatings-content'
id='filtersOfficialRatings-header'
>
<Typography>
{globalize.translate(
'HeaderParentalRatings'
)}
</Typography>
</AccordionSummary>
<AccordionDetails>
<FiltersOfficialRatings
filtes={data}
libraryViewSettings={
libraryViewSettings
}
setLibraryViewSettings={
setLibraryViewSettings
}
/>
</AccordionDetails>
</Accordion>
)}
{data?.Tags && data?.Tags.length > 0 && (
<Accordion
expanded={expanded === 'filtersTags'}
onChange={handleChange('filtersTags')}
>
<AccordionSummary
aria-controls='filtersTags-content'
id='filtersTags-header'
>
<Typography>
{globalize.translate('Tags')}
</Typography>
</AccordionSummary>
<AccordionDetails>
<FiltersTags
filtes={data}
libraryViewSettings={
libraryViewSettings
}
setLibraryViewSettings={
setLibraryViewSettings
}
/>
</AccordionDetails>
</Accordion>
)}
{data?.Years && data?.Years?.length > 0 && (
<Accordion
expanded={expanded === 'filtersYears'}
onChange={handleChange('filtersYears')}
>
<AccordionSummary
aria-controls='filtersYears-content'
id='filtersYears-header'
>
<Typography>
{globalize.translate('HeaderYears')}
</Typography>
</AccordionSummary>
<AccordionDetails>
<FiltersYears
filtes={data}
libraryViewSettings={
libraryViewSettings
}
setLibraryViewSettings={
setLibraryViewSettings
}
/>
</AccordionDetails>
</Accordion>
)}
</>
)}
{isFiltersStudiosEnabled() && (
<>
<Accordion
expanded={expanded === 'filtersStudios'}
onChange={handleChange('filtersStudios')}
>
<AccordionSummary
aria-controls='filtersStudios-content'
id='filtersStudios-header'
>
<Typography>
{globalize.translate('Studios')}
</Typography>
</AccordionSummary>
<AccordionDetails>
<FiltersStudios
filtes={studios}
libraryViewSettings={libraryViewSettings}
setLibraryViewSettings={
setLibraryViewSettings
}
/>
</AccordionDetails>
</Accordion>
</>
)}
</Popover>
</Box>
);
};
export default FilterButton;

View file

@ -0,0 +1,75 @@
import React, { FC, useCallback } from 'react';
import FormGroup from '@mui/material/FormGroup';
import FormControlLabel from '@mui/material/FormControlLabel';
import Checkbox from '@mui/material/Checkbox';
import globalize from 'scripts/globalize';
import { LibraryViewSettings } from 'types/library';
const episodesStatusOptions = [
{ label: 'OptionSpecialEpisode', value: 'ParentIndexNumber' },
{ label: 'OptionMissingEpisode', value: 'IsMissing' },
{ label: 'OptionUnairedEpisode', value: 'IsUnaired' }
];
interface FiltersEpisodesStatusProps {
libraryViewSettings: LibraryViewSettings;
setLibraryViewSettings: React.Dispatch<React.SetStateAction<LibraryViewSettings>>;
}
const FiltersEpisodesStatus: FC<FiltersEpisodesStatusProps> = ({
libraryViewSettings,
setLibraryViewSettings
}) => {
const onFiltersEpisodesStatusChange = useCallback(
(event: React.ChangeEvent<HTMLInputElement>) => {
event.preventDefault();
const value = String(event.target.value);
const existingValue = libraryViewSettings?.Filters?.EpisodesStatus;
if (existingValue?.includes(value)) {
const newValue = existingValue?.filter(
(prevState: string) => prevState !== value
);
setLibraryViewSettings((prevState) => ({
...prevState,
StartIndex: 0,
Filters: { ...prevState.Filters, EpisodesStatus: newValue }
}));
} else {
setLibraryViewSettings((prevState) => ({
...prevState,
StartIndex: 0,
Filters: {
...prevState.Filters,
EpisodesStatus: [...(existingValue ?? []), value]
}
}));
}
},
[setLibraryViewSettings, libraryViewSettings?.Filters?.EpisodesStatus]
);
return (
<FormGroup>
{episodesStatusOptions.map((filter) => (
<FormControlLabel
key={filter.value}
control={
<Checkbox
checked={
!!libraryViewSettings?.Filters?.EpisodesStatus?.includes(
String( filter.value)
)
}
onChange={onFiltersEpisodesStatusChange}
value={String(filter.value)}
/>
}
label={globalize.translate(filter.label)}
/>
))}
</FormGroup>
);
};
export default FiltersEpisodesStatus;

View file

@ -0,0 +1,81 @@
import React, { FC, useCallback } from 'react';
import FormGroup from '@mui/material/FormGroup';
import FormControlLabel from '@mui/material/FormControlLabel';
import Checkbox from '@mui/material/Checkbox';
import globalize from 'scripts/globalize';
import { LibraryViewSettings } from 'types/library';
const featuresOptions = [
{ label: 'Subtitles', value: 'HasSubtitles' },
{ label: 'Trailers', value: 'HasTrailer' },
{ label: 'Extras', value: 'HasSpecialFeature' },
{ label: 'ThemeSongs', value: 'HasThemeSong' },
{ label: 'ThemeVideos', value: 'HasThemeVideo' }
];
interface FiltersFeaturesProps {
libraryViewSettings: LibraryViewSettings;
setLibraryViewSettings: React.Dispatch<
React.SetStateAction<LibraryViewSettings>
>;
}
const FiltersFeatures: FC<FiltersFeaturesProps> = ({
libraryViewSettings,
setLibraryViewSettings
}) => {
const onFiltersFeaturesChange = useCallback(
(event: React.ChangeEvent<HTMLInputElement>) => {
event.preventDefault();
const value = String(event.target.value);
const existingValue =
libraryViewSettings?.Filters?.Features;
if (existingValue?.includes(value)) {
const newValue = existingValue?.filter(
(prevState: string) => prevState !== value
);
setLibraryViewSettings((prevState) => ({
...prevState,
StartIndex: 0,
Filters: { ...prevState.Filters, Features: newValue }
}));
} else {
setLibraryViewSettings((prevState) => ({
...prevState,
StartIndex: 0,
Filters: {
...prevState.Filters,
Features: [...(existingValue ?? []), value]
}
}));
}
},
[setLibraryViewSettings, libraryViewSettings?.Filters?.Features]
);
return (
<FormGroup>
{featuresOptions
.map((filter) => (
<FormControlLabel
key={filter.value}
control={
<Checkbox
checked={
!!libraryViewSettings?.Filters?.Features?.includes(
String(filter.value)
)
}
onChange={onFiltersFeaturesChange}
value={String(filter.value)}
/>
}
label={globalize.translate(filter.label)}
/>
))}
</FormGroup>
);
};
export default FiltersFeatures;

View file

@ -0,0 +1,71 @@
import type { QueryFiltersLegacy } from '@jellyfin/sdk/lib/generated-client';
import React, { FC, useCallback } from 'react';
import FormGroup from '@mui/material/FormGroup';
import FormControlLabel from '@mui/material/FormControlLabel';
import Checkbox from '@mui/material/Checkbox';
import { LibraryViewSettings } from 'types/library';
interface FiltersGenresProps {
filtes?: QueryFiltersLegacy;
libraryViewSettings: LibraryViewSettings;
setLibraryViewSettings: React.Dispatch<React.SetStateAction<LibraryViewSettings>>;
}
const FiltersGenres: FC<FiltersGenresProps> = ({
filtes,
libraryViewSettings,
setLibraryViewSettings
}) => {
const onFiltersGenresChange = useCallback(
(event: React.ChangeEvent<HTMLInputElement>) => {
event.preventDefault();
const value = String(event.target.value);
const existingValue = libraryViewSettings?.Filters?.Genres;
if (existingValue?.includes(value)) {
const newValue = existingValue?.filter(
(prevState: string) => prevState !== value
);
setLibraryViewSettings((prevState) => ({
...prevState,
StartIndex: 0,
Filters: { ...prevState.Filters, Genres: newValue }
}));
} else {
setLibraryViewSettings((prevState) => ({
...prevState,
StartIndex: 0,
Filters: {
...prevState.Filters,
Genres: [...(existingValue ?? []), value]
}
}));
}
},
[setLibraryViewSettings, libraryViewSettings?.Filters?.Genres]
);
return (
<FormGroup>
{filtes?.Genres?.map((filter) => (
<FormControlLabel
key={filter}
control={
<Checkbox
checked={
!!libraryViewSettings?.Filters?.Genres?.includes(
String(filter)
)
}
onChange={onFiltersGenresChange}
value={String(filter)}
/>
}
label={filter}
/>
))}
</FormGroup>
);
};
export default FiltersGenres;

View file

@ -0,0 +1,71 @@
import type { QueryFiltersLegacy } from '@jellyfin/sdk/lib/generated-client';
import React, { FC, useCallback } from 'react';
import FormGroup from '@mui/material/FormGroup';
import FormControlLabel from '@mui/material/FormControlLabel';
import Checkbox from '@mui/material/Checkbox';
import { LibraryViewSettings } from 'types/library';
interface FiltersOfficialRatingsProps {
filtes?: QueryFiltersLegacy;
libraryViewSettings: LibraryViewSettings;
setLibraryViewSettings: React.Dispatch<React.SetStateAction<LibraryViewSettings>>;
}
const FiltersOfficialRatings: FC<FiltersOfficialRatingsProps> = ({
filtes,
libraryViewSettings,
setLibraryViewSettings
}) => {
const onFiltersOfficialRatingsChange = useCallback(
(event: React.ChangeEvent<HTMLInputElement>) => {
event.preventDefault();
const value = String(event.target.value);
const existingValue = libraryViewSettings?.Filters?.OfficialRatings;
if (existingValue?.includes(value)) {
const newValue = existingValue?.filter(
(prevState: string) => prevState !== value
);
setLibraryViewSettings((prevState) => ({
...prevState,
StartIndex: 0,
Filters: { ...prevState.Filters, OfficialRatings: newValue }
}));
} else {
setLibraryViewSettings((prevState) => ({
...prevState,
StartIndex: 0,
Filters: {
...prevState.Filters,
OfficialRatings: [...(existingValue ?? []), value]
}
}));
}
},
[setLibraryViewSettings, libraryViewSettings?.Filters?.OfficialRatings]
);
return (
<FormGroup>
{filtes?.OfficialRatings?.map((filter) => (
<FormControlLabel
key={filter}
control={
<Checkbox
checked={
!!libraryViewSettings?.Filters?.OfficialRatings?.includes(
String(filter)
)
}
onChange={onFiltersOfficialRatingsChange}
value={String(filter)}
/>
}
label={filter}
/>
))}
</FormGroup>
);
};
export default FiltersOfficialRatings;

View file

@ -0,0 +1,74 @@
import React, { FC, useCallback } from 'react';
import FormGroup from '@mui/material/FormGroup';
import FormControlLabel from '@mui/material/FormControlLabel';
import Checkbox from '@mui/material/Checkbox';
import globalize from 'scripts/globalize';
import { LibraryViewSettings } from 'types/library';
import { SeriesStatus } from '@jellyfin/sdk/lib/generated-client';
const statusFiltersOptions = [
{ label: 'Continuing', value: SeriesStatus.Continuing },
{ label: 'Ended', value: SeriesStatus.Ended },
{ label: 'Unreleased', value: SeriesStatus.Unreleased }
];
interface FiltersSeriesStatusProps {
libraryViewSettings: LibraryViewSettings;
setLibraryViewSettings: React.Dispatch<React.SetStateAction<LibraryViewSettings>>;
}
const FiltersSeriesStatus: FC<FiltersSeriesStatusProps> = ({
libraryViewSettings,
setLibraryViewSettings
}) => {
const onFiltersSeriesStatusChange = useCallback(
(event: React.ChangeEvent<HTMLInputElement>) => {
event.preventDefault();
const value = event.target.value as SeriesStatus;
const existingValue = libraryViewSettings?.Filters?.SeriesStatus;
if (existingValue?.includes(value)) {
const newValue = existingValue?.filter(
(prevState: SeriesStatus) => prevState !== value
);
setLibraryViewSettings((prevState) => ({
...prevState,
StartIndex: 0,
Filters: { ...prevState.Filters, SeriesStatus: newValue }
}));
} else {
setLibraryViewSettings((prevState) => ({
...prevState,
StartIndex: 0,
Filters: {
...prevState.Filters,
SeriesStatus: [...(existingValue ?? []), value]
}
}));
}
},
[setLibraryViewSettings, libraryViewSettings?.Filters?.SeriesStatus]
);
return (
<FormGroup>
{statusFiltersOptions.map((filter) => (
<FormControlLabel
key={filter.value}
control={
<Checkbox
checked={
!!libraryViewSettings?.Filters?.SeriesStatus?.includes( filter.value)
}
onChange={onFiltersSeriesStatusChange}
value={filter.value}
/>
}
label={globalize.translate(filter.label)}
/>
))}
</FormGroup>
);
};
export default FiltersSeriesStatus;

View file

@ -0,0 +1,97 @@
import React, { FC, useCallback } from 'react';
import FormGroup from '@mui/material/FormGroup';
import FormControlLabel from '@mui/material/FormControlLabel';
import Checkbox from '@mui/material/Checkbox';
import globalize from 'scripts/globalize';
import { LibraryViewSettings } from 'types/library';
import { ItemFilter } from '@jellyfin/sdk/lib/generated-client';
import { LibraryTab } from 'types/libraryTab';
const statusFiltersOptions = [
{ label: 'Played', value: ItemFilter.IsPlayed },
{ label: 'Unplayed', value: ItemFilter.IsUnplayed },
{ label: 'Favorite', value: ItemFilter.IsFavorite },
{ label: 'ContinueWatching', value: ItemFilter.IsResumable }
];
interface FiltersStatusProps {
viewType: LibraryTab;
libraryViewSettings: LibraryViewSettings;
setLibraryViewSettings: React.Dispatch<React.SetStateAction<LibraryViewSettings>>;
}
const FiltersStatus: FC<FiltersStatusProps> = ({
viewType,
libraryViewSettings,
setLibraryViewSettings
}) => {
const onFiltersStatusChange = useCallback(
(event: React.ChangeEvent<HTMLInputElement>) => {
event.preventDefault();
const value = event.target.value as ItemFilter;
const existingValue = libraryViewSettings?.Filters?.Status;
if (existingValue?.includes(value)) {
const newValue = existingValue?.filter(
(prevState: ItemFilter) => prevState !== value
);
setLibraryViewSettings((prevState) => ({
...prevState,
StartIndex: 0,
Filters: { ...prevState.Filters, Status: newValue }
}));
} else {
setLibraryViewSettings((prevState) => ({
...prevState,
StartIndex: 0,
Filters: {
...prevState.Filters,
Status: [...(existingValue ?? []), value]
}
}));
}
},
[setLibraryViewSettings, libraryViewSettings?.Filters?.Status]
);
const getVisibleFiltersStatus = () => {
const visibleFiltersStatus: ItemFilter[] = [ItemFilter.IsFavorite];
if (
viewType !== LibraryTab.Albums
&& viewType !== LibraryTab.Artists
&& viewType !== LibraryTab.AlbumArtists
&& viewType !== LibraryTab.Songs
) {
visibleFiltersStatus.push(ItemFilter.IsUnplayed);
visibleFiltersStatus.push(ItemFilter.IsPlayed);
visibleFiltersStatus.push(ItemFilter.IsResumable);
}
return visibleFiltersStatus;
};
return (
<FormGroup>
{statusFiltersOptions
.filter((filter) => getVisibleFiltersStatus().includes(filter.value))
.map((filter) => (
<FormControlLabel
key={filter.value}
control={
<Checkbox
checked={
!!libraryViewSettings?.Filters?.Status?.includes(filter.value)
}
onChange={onFiltersStatusChange}
value={filter.value}
/>
}
label={globalize.translate(filter.label)}
/>
))}
</FormGroup>
);
};
export default FiltersStatus;

View file

@ -0,0 +1,71 @@
import type { BaseItemDtoQueryResult } from '@jellyfin/sdk/lib/generated-client';
import React, { FC, useCallback } from 'react';
import FormGroup from '@mui/material/FormGroup';
import FormControlLabel from '@mui/material/FormControlLabel';
import Checkbox from '@mui/material/Checkbox';
import { LibraryViewSettings } from 'types/library';
interface FiltersStudiosProps {
filtes?: BaseItemDtoQueryResult;
libraryViewSettings: LibraryViewSettings;
setLibraryViewSettings: React.Dispatch<React.SetStateAction<LibraryViewSettings>>;
}
const FiltersStudios: FC<FiltersStudiosProps> = ({
filtes,
libraryViewSettings,
setLibraryViewSettings
}) => {
const onFiltersStudiosChange = useCallback(
(event: React.ChangeEvent<HTMLInputElement>) => {
event.preventDefault();
const value = String(event.target.value);
const existingValue = libraryViewSettings?.Filters?.StudioIds;
if (existingValue?.includes(value)) {
const newValue = existingValue?.filter(
(prevState: string) => prevState !== value
);
setLibraryViewSettings((prevState) => ({
...prevState,
StartIndex: 0,
Filters: { ...prevState.Filters, StudioIds: newValue }
}));
} else {
setLibraryViewSettings((prevState) => ({
...prevState,
StartIndex: 0,
Filters: {
...prevState.Filters,
StudioIds: [...(existingValue ?? []), value]
}
}));
}
},
[setLibraryViewSettings, libraryViewSettings?.Filters?.StudioIds]
);
return (
<FormGroup>
{filtes?.Items?.map((filter) => (
<FormControlLabel
key={filter.Id}
control={
<Checkbox
checked={
!!libraryViewSettings?.Filters?.StudioIds?.includes(
String(filter.Id)
)
}
onChange={onFiltersStudiosChange}
value={String(filter.Id)}
/>
}
label={filter.Name}
/>
))}
</FormGroup>
);
};
export default FiltersStudios;

View file

@ -0,0 +1,71 @@
import type { QueryFiltersLegacy } from '@jellyfin/sdk/lib/generated-client';
import React, { FC, useCallback } from 'react';
import FormGroup from '@mui/material/FormGroup';
import FormControlLabel from '@mui/material/FormControlLabel';
import Checkbox from '@mui/material/Checkbox';
import { LibraryViewSettings } from 'types/library';
interface FiltersTagsProps {
filtes?: QueryFiltersLegacy;
libraryViewSettings: LibraryViewSettings;
setLibraryViewSettings: React.Dispatch<React.SetStateAction<LibraryViewSettings>>;
}
const FiltersTags: FC<FiltersTagsProps> = ({
filtes,
libraryViewSettings,
setLibraryViewSettings
}) => {
const onFiltersTagsChange = useCallback(
(event: React.ChangeEvent<HTMLInputElement>) => {
event.preventDefault();
const value = String(event.target.value);
const existingValue = libraryViewSettings?.Filters?.Tags;
if (existingValue?.includes(value)) {
const newValue = existingValue?.filter(
(prevState: string) => prevState !== value
);
setLibraryViewSettings((prevState) => ({
...prevState,
StartIndex: 0,
Filters: { ...prevState.Filters, Tags: newValue }
}));
} else {
setLibraryViewSettings((prevState) => ({
...prevState,
StartIndex: 0,
Filters: {
...prevState.Filters,
Tags: [...(existingValue ?? []), value]
}
}));
}
},
[setLibraryViewSettings, libraryViewSettings?.Filters?.Tags]
);
return (
<FormGroup>
{filtes?.Tags?.map((filter) => (
<FormControlLabel
key={filter}
control={
<Checkbox
checked={
!!libraryViewSettings?.Filters?.Tags?.includes(
String(filter)
)
}
onChange={onFiltersTagsChange}
value={String(filter)}
/>
}
label={filter}
/>
))}
</FormGroup>
);
};
export default FiltersTags;

View file

@ -0,0 +1,131 @@
import React, { FC, useCallback } from 'react';
import FormGroup from '@mui/material/FormGroup';
import FormControlLabel from '@mui/material/FormControlLabel';
import Checkbox from '@mui/material/Checkbox';
import { LibraryViewSettings } from 'types/library';
import { VideoType } from '@jellyfin/sdk/lib/generated-client';
import globalize from 'scripts/globalize';
const videoTypesOptions = [
{ label: 'DVD', value: VideoType.Dvd },
{ label: 'Blu-ray', value: VideoType.BluRay },
{ label: 'ISO', value: VideoType.Iso }
];
interface FiltersVideoTypesProps {
libraryViewSettings: LibraryViewSettings;
setLibraryViewSettings: React.Dispatch<React.SetStateAction<LibraryViewSettings>>;
}
const FiltersVideoTypes: FC<FiltersVideoTypesProps> = ({
libraryViewSettings,
setLibraryViewSettings
}) => {
const onFiltersVideoTypesChange = useCallback(
(event: React.ChangeEvent<HTMLInputElement>) => {
event.preventDefault();
const value = event.target.value as VideoType;
const existingValue = libraryViewSettings?.Filters?.VideoTypes;
if (existingValue?.includes(value)) {
const newValue = existingValue?.filter(
(prevState: VideoType) => prevState !== value
);
setLibraryViewSettings((prevState) => ({
...prevState,
StartIndex: 0,
Filters: { ...prevState.Filters, VideoTypes: newValue }
}));
} else {
setLibraryViewSettings((prevState) => ({
...prevState,
StartIndex: 0,
Filters: {
...prevState.Filters,
VideoTypes: [...(existingValue ?? []), value]
}
}));
}
},
[setLibraryViewSettings, libraryViewSettings?.Filters?.VideoTypes]
);
const handleChange = useCallback(
(event: React.ChangeEvent<HTMLInputElement>) => {
const name = event.target.name;
setLibraryViewSettings((prevState) => ({
...prevState,
[name]: event.target.checked
}));
},
[setLibraryViewSettings]
);
return (
<FormGroup>
<FormControlLabel
control={
<Checkbox
checked={libraryViewSettings.IsSD}
onChange={handleChange}
name='IsSD'
/>
}
label={globalize.translate('SD')}
/>
<FormControlLabel
control={
<Checkbox
checked={libraryViewSettings.IsHD}
onChange={handleChange}
name='IsHD'
/>
}
label={globalize.translate('HD')}
/>
<FormControlLabel
control={
<Checkbox
checked={
libraryViewSettings.Is4K
}
onChange={handleChange}
name='Is4K'
/>
}
label={globalize.translate('4K')}
/>
<FormControlLabel
control={
<Checkbox
checked={
libraryViewSettings.Is3D
}
onChange={handleChange}
name='Is3D'
/>
}
label={globalize.translate('3D')}
/>
{videoTypesOptions
.map((filter) => (
<FormControlLabel
key={filter.value}
control={
<Checkbox
checked={
!!libraryViewSettings?.Filters?.VideoTypes?.includes(filter.value)
}
onChange={onFiltersVideoTypesChange}
value={filter.value}
/>
}
label={filter.label}
/>
))}
</FormGroup>
);
};
export default FiltersVideoTypes;

View file

@ -0,0 +1,71 @@
import type { QueryFiltersLegacy } from '@jellyfin/sdk/lib/generated-client';
import React, { FC, useCallback } from 'react';
import FormGroup from '@mui/material/FormGroup';
import FormControlLabel from '@mui/material/FormControlLabel';
import Checkbox from '@mui/material/Checkbox';
import { LibraryViewSettings } from 'types/library';
interface FiltersYearsProps {
filtes?: QueryFiltersLegacy;
libraryViewSettings: LibraryViewSettings;
setLibraryViewSettings: React.Dispatch<React.SetStateAction<LibraryViewSettings>>;
}
const FiltersYears: FC<FiltersYearsProps> = ({
filtes,
libraryViewSettings,
setLibraryViewSettings
}) => {
const onFiltersYearsChange = useCallback(
(event: React.ChangeEvent<HTMLInputElement>) => {
event.preventDefault();
const value = Number(event.target.value);
const existingValue = libraryViewSettings?.Filters?.Years;
if (existingValue?.includes(value)) {
const newValue = existingValue?.filter(
(prevState: number) => prevState !== value
);
setLibraryViewSettings((prevState) => ({
...prevState,
StartIndex: 0,
Filters: { ...prevState.Filters, Years: newValue }
}));
} else {
setLibraryViewSettings((prevState) => ({
...prevState,
StartIndex: 0,
Filters: {
...prevState.Filters,
Years: [...(existingValue ?? []), value]
}
}));
}
},
[setLibraryViewSettings, libraryViewSettings?.Filters?.Years]
);
return (
<FormGroup>
{filtes?.Years?.map((filter) => (
<FormControlLabel
key={filter}
control={
<Checkbox
checked={
!!libraryViewSettings?.Filters?.Years?.includes(
Number(filter)
)
}
onChange={onFiltersYearsChange}
value={String(filter)}
/>
}
label={filter}
/>
))}
</FormGroup>
);
};
export default FiltersYears;

View file

@ -7,9 +7,11 @@ import { ItemFields } from '@jellyfin/sdk/lib/generated-client/models/item-field
import { ItemFilter } from '@jellyfin/sdk/lib/generated-client/models/item-filter'; import { ItemFilter } from '@jellyfin/sdk/lib/generated-client/models/item-filter';
import { SortOrder } from '@jellyfin/sdk/lib/generated-client/models/sort-order'; import { SortOrder } from '@jellyfin/sdk/lib/generated-client/models/sort-order';
import { ItemSortBy } from '@jellyfin/sdk/lib/models/api/item-sort-by'; import { ItemSortBy } from '@jellyfin/sdk/lib/models/api/item-sort-by';
import { getFilterApi } from '@jellyfin/sdk/lib/utils/api/filter-api';
import { getGenresApi } from '@jellyfin/sdk/lib/utils/api/genres-api'; import { getGenresApi } from '@jellyfin/sdk/lib/utils/api/genres-api';
import { getItemsApi } from '@jellyfin/sdk/lib/utils/api/items-api'; import { getItemsApi } from '@jellyfin/sdk/lib/utils/api/items-api';
import { getMoviesApi } from '@jellyfin/sdk/lib/utils/api/movies-api'; import { getMoviesApi } from '@jellyfin/sdk/lib/utils/api/movies-api';
import { getStudiosApi } from '@jellyfin/sdk/lib/utils/api/studios-api';
import { getTvShowsApi } from '@jellyfin/sdk/lib/utils/api/tv-shows-api'; import { getTvShowsApi } from '@jellyfin/sdk/lib/utils/api/tv-shows-api';
import { getUserLibraryApi } from '@jellyfin/sdk/lib/utils/api/user-library-api'; import { getUserLibraryApi } from '@jellyfin/sdk/lib/utils/api/user-library-api';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
@ -19,7 +21,7 @@ import { Sections, SectionsViewType } from 'types/suggestionsSections';
const fetchGetItem = async ( const fetchGetItem = async (
currentApi: JellyfinApiContext, currentApi: JellyfinApiContext,
parentId?: string | null, parentId: string | null | undefined,
options?: AxiosRequestConfig options?: AxiosRequestConfig
) => { ) => {
const { api, user } = currentApi; const { api, user } = currentApi;
@ -37,7 +39,7 @@ const fetchGetItem = async (
} }
}; };
export const useGetItem = (parentId?: string | null) => { export const useGetItem = (parentId: string | null | undefined) => {
const currentApi = useApi(); const currentApi = useApi();
return useQuery({ return useQuery({
queryKey: ['Item', parentId], queryKey: ['Item', parentId],
@ -83,7 +85,7 @@ export const useGetItems = (parametersOptions: ItemsApiGetItemsRequest) => {
const fetchGetMovieRecommendations = async ( const fetchGetMovieRecommendations = async (
currentApi: JellyfinApiContext, currentApi: JellyfinApiContext,
parentId?: string | null, parentId: string | null | undefined,
options?: AxiosRequestConfig options?: AxiosRequestConfig
) => { ) => {
const { api, user } = currentApi; const { api, user } = currentApi;
@ -108,7 +110,7 @@ const fetchGetMovieRecommendations = async (
} }
}; };
export const useGetMovieRecommendations = (parentId?: string | null) => { export const useGetMovieRecommendations = (parentId: string | null | undefined) => {
const currentApi = useApi(); const currentApi = useApi();
return useQuery({ return useQuery({
queryKey: ['MovieRecommendations', parentId], queryKey: ['MovieRecommendations', parentId],
@ -121,7 +123,7 @@ export const useGetMovieRecommendations = (parentId?: string | null) => {
const fetchGetItemsBySuggestionsType = async ( const fetchGetItemsBySuggestionsType = async (
currentApi: JellyfinApiContext, currentApi: JellyfinApiContext,
sections: Sections, sections: Sections,
parentId?: string | null, parentId: string | null | undefined,
options?: AxiosRequestConfig options?: AxiosRequestConfig
) => { ) => {
const { api, user } = currentApi; const { api, user } = currentApi;
@ -234,7 +236,7 @@ const fetchGetItemsBySuggestionsType = async (
export const useGetItemsBySectionType = ( export const useGetItemsBySectionType = (
sections: Sections, sections: Sections,
parentId?: string | null parentId: string | null | undefined
) => { ) => {
const currentApi = useApi(); const currentApi = useApi();
return useQuery({ return useQuery({
@ -253,7 +255,7 @@ export const useGetItemsBySectionType = (
const fetchGetGenres = async ( const fetchGetGenres = async (
currentApi: JellyfinApiContext, currentApi: JellyfinApiContext,
itemType: BaseItemKind, itemType: BaseItemKind,
parentId?: string | null, parentId: string | null | undefined,
options?: AxiosRequestConfig options?: AxiosRequestConfig
) => { ) => {
const { api, user } = currentApi; const { api, user } = currentApi;
@ -275,7 +277,7 @@ const fetchGetGenres = async (
} }
}; };
export const useGetGenres = (itemType: BaseItemKind, parentId?: string | null) => { export const useGetGenres = (itemType: BaseItemKind, parentId: string | null | undefined) => {
const currentApi = useApi(); const currentApi = useApi();
return useQuery({ return useQuery({
queryKey: ['Genres', parentId], queryKey: ['Genres', parentId],
@ -284,3 +286,78 @@ export const useGetGenres = (itemType: BaseItemKind, parentId?: string | null) =
enabled: !!parentId enabled: !!parentId
}); });
}; };
const fetchGetStudios = async (
currentApi: JellyfinApiContext,
parentId: string | null | undefined,
itemType: BaseItemKind,
options?: AxiosRequestConfig
) => {
const { api, user } = currentApi;
if (api && user?.Id) {
const response = await getStudiosApi(api).getStudios(
{
userId: user.Id,
includeItemTypes: [itemType],
fields: [
ItemFields.DateCreated,
ItemFields.PrimaryImageAspectRatio
],
enableImageTypes: [ImageType.Thumb],
parentId: parentId ?? undefined,
enableTotalRecordCount: false
},
{
signal: options?.signal
}
);
return response.data;
}
};
export const useGetStudios = (parentId: string | null | undefined, itemType: BaseItemKind) => {
const currentApi = useApi();
return useQuery({
queryKey: ['Studios', parentId, itemType],
queryFn: ({ signal }) =>
fetchGetStudios(currentApi, parentId, itemType, { signal }),
enabled: !!parentId
});
};
const fetchGetQueryFiltersLegacy = async (
currentApi: JellyfinApiContext,
parentId: string | null | undefined,
itemType: BaseItemKind,
options?: AxiosRequestConfig
) => {
const { api, user } = currentApi;
if (api && user?.Id) {
const response = await getFilterApi(api).getQueryFiltersLegacy(
{
userId: user.Id,
parentId: parentId ?? undefined,
includeItemTypes: [itemType]
},
{
signal: options?.signal
}
);
return response.data;
}
};
export const useGetQueryFiltersLegacy = (
parentId: string | null | undefined,
itemType: BaseItemKind
) => {
const currentApi = useApi();
return useQuery({
queryKey: ['QueryFiltersLegacy', parentId, itemType],
queryFn: ({ signal }) =>
fetchGetQueryFiltersLegacy(currentApi, parentId, itemType, {
signal
}),
enabled: !!parentId
});
};

View file

@ -369,6 +369,7 @@
"HeaderEditImages": "Edit Images", "HeaderEditImages": "Edit Images",
"HeaderEnabledFields": "Enabled Fields", "HeaderEnabledFields": "Enabled Fields",
"HeaderEnabledFieldsHelp": "Uncheck a field to lock it and prevent its data from being changed.", "HeaderEnabledFieldsHelp": "Uncheck a field to lock it and prevent its data from being changed.",
"HeaderEpisodesStatus": "Episodes Status",
"HeaderError": "Error", "HeaderError": "Error",
"HeaderExternalIds": "External IDs", "HeaderExternalIds": "External IDs",
"HeaderFeatureAccess": "Feature access", "HeaderFeatureAccess": "Feature access",

View file

@ -1,3 +1,39 @@
import type { ItemFilter } from '@jellyfin/sdk/lib/generated-client/models/item-filter';
import type { VideoType } from '@jellyfin/sdk/lib/generated-client/models/video-type';
import type { SortOrder } from '@jellyfin/sdk/lib/generated-client/models/sort-order';
import type { SeriesStatus } from '@jellyfin/sdk/lib/generated-client/models/series-status';
import { ItemSortBy } from '@jellyfin/sdk/lib/models/api/item-sort-by';
export interface LibraryViewProps { export interface LibraryViewProps {
parentId: string | null; parentId: string | null;
} }
interface Filters {
Features?: string[];
Genres?: string[];
OfficialRatings?: string[];
Status?: ItemFilter[];
EpisodesStatus?: string[];
SeriesStatus?: SeriesStatus[];
StudioIds?: string[];
Tags?: string[];
VideoTypes?: VideoType[];
Years?: number[];
}
export interface LibraryViewSettings {
SortBy: ItemSortBy;
SortOrder: SortOrder;
StartIndex: number;
CardLayout: boolean;
ImageType: string;
ShowTitle: boolean;
ShowYear?: boolean;
Filters?: Filters;
IsSD?: boolean;
IsHD?: boolean;
Is4K?: boolean;
Is3D?: boolean;
NameLessThan?: string | null;
NameStartsWith?: string | null;
}