diff --git a/src/apps/experimental/components/library/filter/FilterButton.tsx b/src/apps/experimental/components/library/filter/FilterButton.tsx
new file mode 100644
index 0000000000..4831180e64
--- /dev/null
+++ b/src/apps/experimental/components/library/filter/FilterButton.tsx
@@ -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) => (
+
+))(({ theme }) => ({
+ border: `1px solid ${theme.palette.divider}`,
+ '&:not(:last-child)': {
+ borderBottom: 0
+ },
+ '&:before': {
+ display: 'none'
+ }
+}));
+
+const AccordionSummary = styled((props: AccordionSummaryProps) => (
+ }
+ {...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
+ >;
+}
+
+const FilterButton: FC = ({
+ parentId,
+ itemType,
+ viewType,
+ libraryViewSettings,
+ setLibraryViewSettings
+}) => {
+ const [anchorEl, setAnchorEl] = React.useState(null);
+ const [expanded, setExpanded] = React.useState(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) => {
+ 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 (
+
+
+
+
+
+
+
+
+
+
+
+ {isFiltersSeriesStatusEnabled() && (
+ <>
+
+
+
+
+
+
+ >
+ )}
+ {isFiltersEpisodesStatusEnabled() && (
+ <>
+
+
+
+
+
+
+ >
+ )}
+ {isFiltersFeaturesEnabled() && (
+ <>
+
+
+
+
+
+
+ >
+ )}
+
+ {isFiltersVideoTypesEnabled() && (
+ <>
+
+
+
+
+
+
+ >
+ )}
+
+ {isFiltersLegacyEnabled() && (
+ <>
+ {data?.Genres && data?.Genres?.length > 0 && (
+
+
+
+
+
+
+ )}
+
+ {data?.OfficialRatings
+ && data?.OfficialRatings?.length > 0 && (
+
+
+
+
+
+
+ )}
+
+ {data?.Tags && data?.Tags.length > 0 && (
+
+
+
+
+
+
+ )}
+
+ {data?.Years && data?.Years?.length > 0 && (
+
+
+
+
+
+
+ )}
+ >
+ )}
+ {isFiltersStudiosEnabled() && (
+ <>
+
+
+
+
+
+
+ >
+ )}
+
+
+ );
+};
+
+export default FilterButton;
diff --git a/src/apps/experimental/components/library/filter/FiltersEpisodesStatus.tsx b/src/apps/experimental/components/library/filter/FiltersEpisodesStatus.tsx
new file mode 100644
index 0000000000..8360b2b5a8
--- /dev/null
+++ b/src/apps/experimental/components/library/filter/FiltersEpisodesStatus.tsx
@@ -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>;
+}
+
+const FiltersEpisodesStatus: FC = ({
+ libraryViewSettings,
+ setLibraryViewSettings
+}) => {
+ const onFiltersEpisodesStatusChange = useCallback(
+ (event: React.ChangeEvent) => {
+ 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 (
+
+ {episodesStatusOptions.map((filter) => (
+
+ }
+ label={globalize.translate(filter.label)}
+ />
+ ))}
+
+ );
+};
+
+export default FiltersEpisodesStatus;
diff --git a/src/apps/experimental/components/library/filter/FiltersFeatures.tsx b/src/apps/experimental/components/library/filter/FiltersFeatures.tsx
new file mode 100644
index 0000000000..5140350460
--- /dev/null
+++ b/src/apps/experimental/components/library/filter/FiltersFeatures.tsx
@@ -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
+ >;
+}
+
+const FiltersFeatures: FC = ({
+ libraryViewSettings,
+ setLibraryViewSettings
+}) => {
+ const onFiltersFeaturesChange = useCallback(
+ (event: React.ChangeEvent) => {
+ 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 (
+
+ {featuresOptions
+ .map((filter) => (
+
+ }
+ label={globalize.translate(filter.label)}
+ />
+ ))}
+
+ );
+};
+
+export default FiltersFeatures;
diff --git a/src/apps/experimental/components/library/filter/FiltersGenres.tsx b/src/apps/experimental/components/library/filter/FiltersGenres.tsx
new file mode 100644
index 0000000000..bd76f41e55
--- /dev/null
+++ b/src/apps/experimental/components/library/filter/FiltersGenres.tsx
@@ -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>;
+}
+
+const FiltersGenres: FC = ({
+ filtes,
+ libraryViewSettings,
+ setLibraryViewSettings
+}) => {
+ const onFiltersGenresChange = useCallback(
+ (event: React.ChangeEvent) => {
+ 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 (
+
+ {filtes?.Genres?.map((filter) => (
+
+ }
+ label={filter}
+ />
+ ))}
+
+ );
+};
+
+export default FiltersGenres;
diff --git a/src/apps/experimental/components/library/filter/FiltersOfficialRatings.tsx b/src/apps/experimental/components/library/filter/FiltersOfficialRatings.tsx
new file mode 100644
index 0000000000..0476f8688d
--- /dev/null
+++ b/src/apps/experimental/components/library/filter/FiltersOfficialRatings.tsx
@@ -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>;
+}
+
+const FiltersOfficialRatings: FC = ({
+ filtes,
+ libraryViewSettings,
+ setLibraryViewSettings
+}) => {
+ const onFiltersOfficialRatingsChange = useCallback(
+ (event: React.ChangeEvent) => {
+ 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 (
+
+ {filtes?.OfficialRatings?.map((filter) => (
+
+ }
+ label={filter}
+ />
+ ))}
+
+ );
+};
+
+export default FiltersOfficialRatings;
diff --git a/src/apps/experimental/components/library/filter/FiltersSeriesStatus.tsx b/src/apps/experimental/components/library/filter/FiltersSeriesStatus.tsx
new file mode 100644
index 0000000000..3420c8c7a0
--- /dev/null
+++ b/src/apps/experimental/components/library/filter/FiltersSeriesStatus.tsx
@@ -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>;
+}
+
+const FiltersSeriesStatus: FC = ({
+ libraryViewSettings,
+ setLibraryViewSettings
+}) => {
+ const onFiltersSeriesStatusChange = useCallback(
+ (event: React.ChangeEvent) => {
+ 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 (
+
+ {statusFiltersOptions.map((filter) => (
+
+ }
+ label={globalize.translate(filter.label)}
+ />
+ ))}
+
+ );
+};
+
+export default FiltersSeriesStatus;
diff --git a/src/apps/experimental/components/library/filter/FiltersStatus.tsx b/src/apps/experimental/components/library/filter/FiltersStatus.tsx
new file mode 100644
index 0000000000..72e3139199
--- /dev/null
+++ b/src/apps/experimental/components/library/filter/FiltersStatus.tsx
@@ -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>;
+}
+
+const FiltersStatus: FC = ({
+ viewType,
+ libraryViewSettings,
+ setLibraryViewSettings
+}) => {
+ const onFiltersStatusChange = useCallback(
+ (event: React.ChangeEvent) => {
+ 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 (
+
+ {statusFiltersOptions
+ .filter((filter) => getVisibleFiltersStatus().includes(filter.value))
+ .map((filter) => (
+
+ }
+ label={globalize.translate(filter.label)}
+ />
+ ))}
+
+ );
+};
+
+export default FiltersStatus;
diff --git a/src/apps/experimental/components/library/filter/FiltersStudios.tsx b/src/apps/experimental/components/library/filter/FiltersStudios.tsx
new file mode 100644
index 0000000000..da2b80c3a9
--- /dev/null
+++ b/src/apps/experimental/components/library/filter/FiltersStudios.tsx
@@ -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>;
+}
+
+const FiltersStudios: FC = ({
+ filtes,
+ libraryViewSettings,
+ setLibraryViewSettings
+}) => {
+ const onFiltersStudiosChange = useCallback(
+ (event: React.ChangeEvent) => {
+ 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 (
+
+ {filtes?.Items?.map((filter) => (
+
+ }
+ label={filter.Name}
+ />
+ ))}
+
+ );
+};
+
+export default FiltersStudios;
diff --git a/src/apps/experimental/components/library/filter/FiltersTags.tsx b/src/apps/experimental/components/library/filter/FiltersTags.tsx
new file mode 100644
index 0000000000..11b96533e2
--- /dev/null
+++ b/src/apps/experimental/components/library/filter/FiltersTags.tsx
@@ -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>;
+}
+
+const FiltersTags: FC = ({
+ filtes,
+ libraryViewSettings,
+ setLibraryViewSettings
+}) => {
+ const onFiltersTagsChange = useCallback(
+ (event: React.ChangeEvent) => {
+ 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 (
+
+ {filtes?.Tags?.map((filter) => (
+
+ }
+ label={filter}
+ />
+ ))}
+
+ );
+};
+
+export default FiltersTags;
diff --git a/src/apps/experimental/components/library/filter/FiltersVideoTypes.tsx b/src/apps/experimental/components/library/filter/FiltersVideoTypes.tsx
new file mode 100644
index 0000000000..3aa99cb074
--- /dev/null
+++ b/src/apps/experimental/components/library/filter/FiltersVideoTypes.tsx
@@ -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>;
+}
+
+const FiltersVideoTypes: FC = ({
+ libraryViewSettings,
+ setLibraryViewSettings
+}) => {
+ const onFiltersVideoTypesChange = useCallback(
+ (event: React.ChangeEvent) => {
+ 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) => {
+ const name = event.target.name;
+
+ setLibraryViewSettings((prevState) => ({
+ ...prevState,
+ [name]: event.target.checked
+ }));
+ },
+ [setLibraryViewSettings]
+ );
+
+ return (
+
+
+ }
+ label={globalize.translate('SD')}
+ />
+
+ }
+ label={globalize.translate('HD')}
+ />
+
+ }
+ label={globalize.translate('4K')}
+ />
+
+ }
+ label={globalize.translate('3D')}
+ />
+ {videoTypesOptions
+ .map((filter) => (
+
+ }
+ label={filter.label}
+ />
+ ))}
+
+ );
+};
+
+export default FiltersVideoTypes;
diff --git a/src/apps/experimental/components/library/filter/FiltersYears.tsx b/src/apps/experimental/components/library/filter/FiltersYears.tsx
new file mode 100644
index 0000000000..35f1505008
--- /dev/null
+++ b/src/apps/experimental/components/library/filter/FiltersYears.tsx
@@ -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>;
+}
+
+const FiltersYears: FC = ({
+ filtes,
+ libraryViewSettings,
+ setLibraryViewSettings
+}) => {
+ const onFiltersYearsChange = useCallback(
+ (event: React.ChangeEvent) => {
+ 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 (
+
+ {filtes?.Years?.map((filter) => (
+
+ }
+ label={filter}
+ />
+ ))}
+
+ );
+};
+
+export default FiltersYears;
diff --git a/src/hooks/useFetchItems.ts b/src/hooks/useFetchItems.ts
index fcc2b2125c..26ff408a5b 100644
--- a/src/hooks/useFetchItems.ts
+++ b/src/hooks/useFetchItems.ts
@@ -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 { SortOrder } from '@jellyfin/sdk/lib/generated-client/models/sort-order';
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 { getItemsApi } from '@jellyfin/sdk/lib/utils/api/items-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 { getUserLibraryApi } from '@jellyfin/sdk/lib/utils/api/user-library-api';
import { useQuery } from '@tanstack/react-query';
@@ -19,7 +21,7 @@ import { Sections, SectionsViewType } from 'types/suggestionsSections';
const fetchGetItem = async (
currentApi: JellyfinApiContext,
- parentId?: string | null,
+ parentId: string | null | undefined,
options?: AxiosRequestConfig
) => {
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();
return useQuery({
queryKey: ['Item', parentId],
@@ -83,7 +85,7 @@ export const useGetItems = (parametersOptions: ItemsApiGetItemsRequest) => {
const fetchGetMovieRecommendations = async (
currentApi: JellyfinApiContext,
- parentId?: string | null,
+ parentId: string | null | undefined,
options?: AxiosRequestConfig
) => {
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();
return useQuery({
queryKey: ['MovieRecommendations', parentId],
@@ -121,7 +123,7 @@ export const useGetMovieRecommendations = (parentId?: string | null) => {
const fetchGetItemsBySuggestionsType = async (
currentApi: JellyfinApiContext,
sections: Sections,
- parentId?: string | null,
+ parentId: string | null | undefined,
options?: AxiosRequestConfig
) => {
const { api, user } = currentApi;
@@ -234,7 +236,7 @@ const fetchGetItemsBySuggestionsType = async (
export const useGetItemsBySectionType = (
sections: Sections,
- parentId?: string | null
+ parentId: string | null | undefined
) => {
const currentApi = useApi();
return useQuery({
@@ -253,7 +255,7 @@ export const useGetItemsBySectionType = (
const fetchGetGenres = async (
currentApi: JellyfinApiContext,
itemType: BaseItemKind,
- parentId?: string | null,
+ parentId: string | null | undefined,
options?: AxiosRequestConfig
) => {
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();
return useQuery({
queryKey: ['Genres', parentId],
@@ -284,3 +286,78 @@ export const useGetGenres = (itemType: BaseItemKind, parentId?: string | null) =
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
+ });
+};
diff --git a/src/strings/en-us.json b/src/strings/en-us.json
index 5cc2db0ca6..5ef2b71fe0 100644
--- a/src/strings/en-us.json
+++ b/src/strings/en-us.json
@@ -369,6 +369,7 @@
"HeaderEditImages": "Edit Images",
"HeaderEnabledFields": "Enabled Fields",
"HeaderEnabledFieldsHelp": "Uncheck a field to lock it and prevent its data from being changed.",
+ "HeaderEpisodesStatus": "Episodes Status",
"HeaderError": "Error",
"HeaderExternalIds": "External IDs",
"HeaderFeatureAccess": "Feature access",
diff --git a/src/types/library.ts b/src/types/library.ts
index 1c04dae30e..5144bf7429 100644
--- a/src/types/library.ts
+++ b/src/types/library.ts
@@ -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 {
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;
+}