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:
parent
5598f49c32
commit
3ae27e05c7
14 changed files with 1392 additions and 8 deletions
457
src/apps/experimental/components/library/filter/FilterButton.tsx
Normal file
457
src/apps/experimental/components/library/filter/FilterButton.tsx
Normal 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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue