diff --git a/src/apps/experimental/components/library/filter/FilterButton.tsx b/src/apps/experimental/components/library/filter/FilterButton.tsx index ce9be77f0f..fd0e54d545 100644 --- a/src/apps/experimental/components/library/filter/FilterButton.tsx +++ b/src/apps/experimental/components/library/filter/FilterButton.tsx @@ -10,6 +10,7 @@ import MuiAccordionSummary, { AccordionSummaryProps } from '@mui/material/AccordionSummary'; import IconButton from '@mui/material/IconButton'; +import { Badge } from '@mui/material'; import { styled } from '@mui/material/styles'; import Typography from '@mui/material/Typography'; @@ -152,6 +153,9 @@ const FilterButton: FC = ({ return viewType === LibraryTab.Episodes; }; + const hasFilters = + Object.values(libraryViewSettings.Filters || {}).some((filter) => !!filter); + return ( = ({ className='paper-icon-button-light btnShuffle autoSize' onClick={handleClick} > - + + + = ({ id='filtersEpisodesStatus-header' > - {globalize.translate('HeaderEpisodesStatus')} + {globalize.translate( + 'HeaderEpisodesStatus' + )} diff --git a/src/apps/experimental/components/library/filter/FiltersEpisodesStatus.tsx b/src/apps/experimental/components/library/filter/FiltersEpisodesStatus.tsx index 8360b2b5a8..fb3f068503 100644 --- a/src/apps/experimental/components/library/filter/FiltersEpisodesStatus.tsx +++ b/src/apps/experimental/components/library/filter/FiltersEpisodesStatus.tsx @@ -3,12 +3,12 @@ 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 { EpisodeFilter, LibraryViewSettings } from 'types/library'; -const episodesStatusOptions = [ - { label: 'OptionSpecialEpisode', value: 'ParentIndexNumber' }, - { label: 'OptionMissingEpisode', value: 'IsMissing' }, - { label: 'OptionUnairedEpisode', value: 'IsUnaired' } +const episodeFilterOptions = [ + { label: 'OptionSpecialEpisode', value: EpisodeFilter.ParentIndexNumber }, + { label: 'OptionMissingEpisode', value: EpisodeFilter.IsMissing }, + { label: 'OptionUnairedEpisode', value: EpisodeFilter.IsUnaired } ]; interface FiltersEpisodesStatusProps { @@ -23,46 +23,39 @@ const FiltersEpisodesStatus: FC = ({ const onFiltersEpisodesStatusChange = useCallback( (event: React.ChangeEvent) => { event.preventDefault(); - const value = String(event.target.value); - const existingValue = libraryViewSettings?.Filters?.EpisodesStatus; + const value = event.target.value as EpisodeFilter; + const existingEpisodeFilter = libraryViewSettings?.Filters?.EpisodeFilter ?? []; - 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] - } - })); - } + const updatedEpisodeFilter = existingEpisodeFilter.includes(value) ? + existingEpisodeFilter.filter((filter) => filter !== value) : + [...existingEpisodeFilter, value]; + + setLibraryViewSettings((prevState) => ({ + ...prevState, + StartIndex: 0, + Filters: { + ...prevState.Filters, + EpisodeFilter: updatedEpisodeFilter.length ? updatedEpisodeFilter : undefined + } + })); }, - [setLibraryViewSettings, libraryViewSettings?.Filters?.EpisodesStatus] + [setLibraryViewSettings, libraryViewSettings?.Filters?.EpisodeFilter] ); return ( - {episodesStatusOptions.map((filter) => ( + {episodeFilterOptions.map((filter) => ( } label={globalize.translate(filter.label)} diff --git a/src/apps/experimental/components/library/filter/FiltersFeatures.tsx b/src/apps/experimental/components/library/filter/FiltersFeatures.tsx index 5140350460..6bcced0cd7 100644 --- a/src/apps/experimental/components/library/filter/FiltersFeatures.tsx +++ b/src/apps/experimental/components/library/filter/FiltersFeatures.tsx @@ -3,14 +3,14 @@ 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 { FeatureFilters, 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' } + { label: 'Subtitles', value: FeatureFilters.HasSubtitles }, + { label: 'Trailers', value: FeatureFilters.HasTrailer }, + { label: 'Extras', value: FeatureFilters.HasSpecialFeature }, + { label: 'ThemeSongs', value: FeatureFilters.HasThemeSong }, + { label: 'ThemeVideos', value: FeatureFilters.HasThemeVideo } ]; interface FiltersFeaturesProps { @@ -27,29 +27,21 @@ const FiltersFeatures: FC = ({ const onFiltersFeaturesChange = useCallback( (event: React.ChangeEvent) => { event.preventDefault(); - const value = String(event.target.value); - const existingValue = - libraryViewSettings?.Filters?.Features; + const value = event.target.value as FeatureFilters; + const existingFeatures = 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] - } - })); - } + const updatedFeatures = existingFeatures.includes(value) ? + existingFeatures.filter((filter) => filter !== value) : + [...existingFeatures, value]; + + setLibraryViewSettings((prevState) => ({ + ...prevState, + StartIndex: 0, + Filters: { + ...prevState.Filters, + Features: updatedFeatures.length ? updatedFeatures : undefined + } + })); }, [setLibraryViewSettings, libraryViewSettings?.Filters?.Features] ); @@ -64,11 +56,11 @@ const FiltersFeatures: FC = ({ } label={globalize.translate(filter.label)} diff --git a/src/apps/experimental/components/library/filter/FiltersGenres.tsx b/src/apps/experimental/components/library/filter/FiltersGenres.tsx index 8d58aab63f..81e7638cb9 100644 --- a/src/apps/experimental/components/library/filter/FiltersGenres.tsx +++ b/src/apps/experimental/components/library/filter/FiltersGenres.tsx @@ -19,28 +19,21 @@ const FiltersGenres: FC = ({ const onFiltersGenresChange = useCallback( (event: React.ChangeEvent) => { event.preventDefault(); - const value = String(event.target.value); - const existingValue = libraryViewSettings?.Filters?.Genres; + const value = event.target.value; + const existingGenres = 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] - } - })); - } + const updatedGenres = existingGenres.includes(value) ? + existingGenres.filter((filter) => filter !== value) : + [...existingGenres, value]; + + setLibraryViewSettings((prevState) => ({ + ...prevState, + StartIndex: 0, + Filters: { + ...prevState.Filters, + Genres: updatedGenres.length ? updatedGenres : undefined + } + })); }, [setLibraryViewSettings, libraryViewSettings?.Filters?.Genres] ); diff --git a/src/apps/experimental/components/library/filter/FiltersOfficialRatings.tsx b/src/apps/experimental/components/library/filter/FiltersOfficialRatings.tsx index 1dc793f07b..2d90299a22 100644 --- a/src/apps/experimental/components/library/filter/FiltersOfficialRatings.tsx +++ b/src/apps/experimental/components/library/filter/FiltersOfficialRatings.tsx @@ -19,28 +19,21 @@ const FiltersOfficialRatings: FC = ({ const onFiltersOfficialRatingsChange = useCallback( (event: React.ChangeEvent) => { event.preventDefault(); - const value = String(event.target.value); - const existingValue = libraryViewSettings?.Filters?.OfficialRatings; + const value = event.target.value; + const existingOfficialRatings = 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] - } - })); - } + const updatedOfficialRatings = existingOfficialRatings.includes(value) ? + existingOfficialRatings.filter((filter) => filter !== value) : + [...existingOfficialRatings, value]; + + setLibraryViewSettings((prevState) => ({ + ...prevState, + StartIndex: 0, + Filters: { + ...prevState.Filters, + OfficialRatings: updatedOfficialRatings.length ? updatedOfficialRatings : undefined + } + })); }, [setLibraryViewSettings, libraryViewSettings?.Filters?.OfficialRatings] ); diff --git a/src/apps/experimental/components/library/filter/FiltersSeriesStatus.tsx b/src/apps/experimental/components/library/filter/FiltersSeriesStatus.tsx index 3420c8c7a0..ae731792b2 100644 --- a/src/apps/experimental/components/library/filter/FiltersSeriesStatus.tsx +++ b/src/apps/experimental/components/library/filter/FiltersSeriesStatus.tsx @@ -25,27 +25,20 @@ const FiltersSeriesStatus: FC = ({ (event: React.ChangeEvent) => { event.preventDefault(); const value = event.target.value as SeriesStatus; - const existingValue = libraryViewSettings?.Filters?.SeriesStatus; + const existingSeriesStatus = 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] - } - })); - } + const updatedSeriesStatus = existingSeriesStatus.includes(value) ? + existingSeriesStatus.filter((filter) => filter !== value) : + [...existingSeriesStatus, value]; + + setLibraryViewSettings((prevState) => ({ + ...prevState, + StartIndex: 0, + Filters: { + ...prevState.Filters, + SeriesStatus: updatedSeriesStatus.length ? updatedSeriesStatus : undefined + } + })); }, [setLibraryViewSettings, libraryViewSettings?.Filters?.SeriesStatus] ); diff --git a/src/apps/experimental/components/library/filter/FiltersStatus.tsx b/src/apps/experimental/components/library/filter/FiltersStatus.tsx index 72e3139199..a347262e6d 100644 --- a/src/apps/experimental/components/library/filter/FiltersStatus.tsx +++ b/src/apps/experimental/components/library/filter/FiltersStatus.tsx @@ -29,27 +29,20 @@ const FiltersStatus: FC = ({ (event: React.ChangeEvent) => { event.preventDefault(); const value = event.target.value as ItemFilter; - const existingValue = libraryViewSettings?.Filters?.Status; + const existingStatus = 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] - } - })); - } + const updatedStatus = existingStatus.includes(value) ? + existingStatus.filter((filter) => filter !== value) : + [...existingStatus, value]; + + setLibraryViewSettings((prevState) => ({ + ...prevState, + StartIndex: 0, + Filters: { + ...prevState.Filters, + Status: updatedStatus.length ? updatedStatus : undefined + } + })); }, [setLibraryViewSettings, libraryViewSettings?.Filters?.Status] ); diff --git a/src/apps/experimental/components/library/filter/FiltersStudios.tsx b/src/apps/experimental/components/library/filter/FiltersStudios.tsx index 0e8e2defc3..306ab1a51c 100644 --- a/src/apps/experimental/components/library/filter/FiltersStudios.tsx +++ b/src/apps/experimental/components/library/filter/FiltersStudios.tsx @@ -19,30 +19,23 @@ const FiltersStudios: FC = ({ const onFiltersStudiosChange = useCallback( (event: React.ChangeEvent) => { event.preventDefault(); - const value = String(event.target.value); - const existingValue = libraryViewSettings?.Filters?.StudioIds; + const value = event.target.value; + const existingStudioIds = 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] - } - })); - } + const updatedStudioIds = existingStudioIds.includes(value) ? + existingStudioIds.filter((filter) => filter !== value) : + [...existingStudioIds, value]; + + setLibraryViewSettings((prevState) => ({ + ...prevState, + StartIndex: 0, + Filters: { + ...prevState.Filters, + StudioIds: updatedStudioIds.length ? updatedStudioIds : undefined + } + })); }, - [setLibraryViewSettings, libraryViewSettings?.Filters?.StudioIds] + [setLibraryViewSettings, libraryViewSettings.Filters?.StudioIds] ); return ( diff --git a/src/apps/experimental/components/library/filter/FiltersTags.tsx b/src/apps/experimental/components/library/filter/FiltersTags.tsx index f626eb03ae..ce2113f8ec 100644 --- a/src/apps/experimental/components/library/filter/FiltersTags.tsx +++ b/src/apps/experimental/components/library/filter/FiltersTags.tsx @@ -19,30 +19,23 @@ const FiltersTags: FC = ({ const onFiltersTagsChange = useCallback( (event: React.ChangeEvent) => { event.preventDefault(); - const value = String(event.target.value); - const existingValue = libraryViewSettings?.Filters?.Tags; + const value = event.target.value; + const existingTags = 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] - } - })); - } + const updatedTags = existingTags.includes(value) ? + existingTags.filter((filter) => filter !== value) : + [...existingTags, value]; + + setLibraryViewSettings((prevState) => ({ + ...prevState, + StartIndex: 0, + Filters: { + ...prevState.Filters, + Tags: updatedTags.length ? updatedTags : undefined + } + })); }, - [setLibraryViewSettings, libraryViewSettings?.Filters?.Tags] + [setLibraryViewSettings, libraryViewSettings.Filters?.Tags] ); return ( diff --git a/src/apps/experimental/components/library/filter/FiltersVideoTypes.tsx b/src/apps/experimental/components/library/filter/FiltersVideoTypes.tsx index 3aa99cb074..d97d7a59a7 100644 --- a/src/apps/experimental/components/library/filter/FiltersVideoTypes.tsx +++ b/src/apps/experimental/components/library/filter/FiltersVideoTypes.tsx @@ -2,9 +2,15 @@ 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 { LibraryViewSettings, VideoBasicFilter } from 'types/library'; import { VideoType } from '@jellyfin/sdk/lib/generated-client'; -import globalize from 'scripts/globalize'; + +const videoBasicFilterOptions = [ + { label: 'SD', value: VideoBasicFilter.IsSD }, + { label: 'HD', value: VideoBasicFilter.IsHD }, + { label: '4K', value: VideoBasicFilter.Is4K }, + { label: '3D', value: VideoBasicFilter.Is3D } +]; const videoTypesOptions = [ { label: 'DVD', value: VideoType.Dvd }, @@ -21,93 +27,67 @@ const FiltersVideoTypes: FC = ({ libraryViewSettings, setLibraryViewSettings }) => { + const onFiltersvideoStandardChange = useCallback( + (event: React.ChangeEvent) => { + event.preventDefault(); + const value = event.target.value as VideoBasicFilter; + const existingVideoBasicFilter = libraryViewSettings?.Filters?.VideoBasicFilter ?? []; + + const updatedVideoBasicFilter = existingVideoBasicFilter.includes(value) ? + existingVideoBasicFilter.filter((filter) => filter !== value) : + [...existingVideoBasicFilter, value]; + + setLibraryViewSettings((prevState) => ({ + ...prevState, + Filters: { + ...prevState.Filters, + VideoBasicFilter: updatedVideoBasicFilter.length ? updatedVideoBasicFilter : undefined + } + })); + }, + [setLibraryViewSettings, libraryViewSettings?.Filters?.VideoBasicFilter] + ); + const onFiltersVideoTypesChange = useCallback( (event: React.ChangeEvent) => { event.preventDefault(); const value = event.target.value as VideoType; - const existingValue = libraryViewSettings?.Filters?.VideoTypes; + const existingVideoTypes = 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] - } - })); - } + const updatedVideoTypes = existingVideoTypes.includes(value) ? + existingVideoTypes.filter((filter) => filter !== value) : + [...existingVideoTypes, value]; + + setLibraryViewSettings((prevState) => ({ + ...prevState, + StartIndex: 0, + Filters: { + ...prevState.Filters, + VideoTypes: updatedVideoTypes.length ? updatedVideoTypes : undefined + } + })); }, [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')} - /> - ( + } - onChange={handleChange} - name='Is4K' + label={filter.label} /> - } - label={globalize.translate('4K')} - /> - - } - label={globalize.translate('3D')} - /> + ))} {videoTypesOptions .map((filter) => ( = ({ (event: React.ChangeEvent) => { event.preventDefault(); const value = Number(event.target.value); - const existingValue = libraryViewSettings?.Filters?.Years; + const existingYears = 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] - } - })); - } + const updatedYears = existingYears.includes(value) ? + existingYears.filter((filter) => filter !== value) : + [...existingYears, value]; + + setLibraryViewSettings((prevState) => ({ + ...prevState, + StartIndex: 0, + Filters: { + ...prevState.Filters, + Years: updatedYears.length ? updatedYears : undefined + } + })); }, [setLibraryViewSettings, libraryViewSettings?.Filters?.Years] ); diff --git a/src/types/library.ts b/src/types/library.ts index 2994dd3ca5..4bf275abe3 100644 --- a/src/types/library.ts +++ b/src/types/library.ts @@ -2,8 +2,8 @@ import type { ItemFilter } from '@jellyfin/sdk/lib/generated-client/models/item- 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 type { ImageType } from '@jellyfin/sdk/lib/generated-client'; import { ItemSortBy } from '@jellyfin/sdk/lib/models/api/item-sort-by'; -import { ImageType } from '@jellyfin/sdk/lib/generated-client'; export type ParentId = string | null | undefined; @@ -11,15 +11,38 @@ export interface LibraryViewProps { parentId: string | null; } -interface Filters { - Features?: string[]; +export enum FeatureFilters { + HasSubtitles = 'HasSubtitles', + HasTrailer = 'HasTrailer', + HasSpecialFeature = 'HasSpecialFeature', + HasThemeSong = 'HasThemeSong', + HasThemeVideo = 'HasThemeVideo', +} + +export enum EpisodeFilter { + ParentIndexNumber = 'ParentIndexNumber', + IsMissing = 'IsMissing', + IsUnaired = 'IsUnaired', +} + +export enum VideoBasicFilter { + IsSD = 'IsSD', + IsHD = 'IsHD', + Is4K = 'Is4K', + Is3D = 'Is3D', +} + +export interface Filters { + Features?: FeatureFilters[]; Genres?: string[]; OfficialRatings?: string[]; + EpisodeFilter?: EpisodeFilter[]; Status?: ItemFilter[]; EpisodesStatus?: string[]; SeriesStatus?: SeriesStatus[]; StudioIds?: string[]; Tags?: string[]; + VideoBasicFilter?: VideoBasicFilter[]; VideoTypes?: VideoType[]; Years?: number[]; } @@ -39,10 +62,6 @@ export interface LibraryViewSettings { ShowTitle: boolean; ShowYear?: boolean; Filters?: Filters; - IsSD?: boolean; - IsHD?: boolean; - Is4K?: boolean; - Is3D?: boolean; NameLessThan?: string | null; NameStartsWith?: string | null; }