diff --git a/src/view/components/Filter.tsx b/src/view/components/Filter.tsx index 248b6c9fbe..becaf0e615 100644 --- a/src/view/components/Filter.tsx +++ b/src/view/components/Filter.tsx @@ -1,38 +1,53 @@ import React, { FC, useCallback, useEffect, useRef } from 'react'; -import { Events } from 'jellyfin-apiclient'; import IconButtonElement from '../../elements/IconButtonElement'; -import { QueryI } from './interface'; +import { FiltersI } from './interface'; interface FilterI { - query: QueryI; - getFilterMode: () => string | null; + topParentId?: string | null; + getItemTypes: () => string[]; + getFilters: () => FiltersI; + getSettingsKey: () => string; + getFilterMenuOptions: () => Record; + getVisibleFilters: () => string[]; reloadItems: () => void; } -const Filter: FC = ({ query, getFilterMode, reloadItems }) => { +const Filter: FC = ({ + topParentId, + getItemTypes, + getSettingsKey, + getFilters, + getVisibleFilters, + getFilterMenuOptions, + reloadItems +}) => { const element = useRef(null); const showFilterMenu = useCallback(() => { - import('../../components/filterdialog/filterdialog').then(({default: filterDialogFactory}) => { - const filterDialog = new filterDialogFactory({ - query: query, - mode: getFilterMode(), - serverId: window.ApiClient.serverId() - }); - Events.on(filterDialog, 'filterchange', () => { - query.StartIndex = 0; + import('../../components/filtermenu/filtermenu').then(({default: FilterMenu}) => { + const filterMenu = new FilterMenu(); + filterMenu.show({ + settingsKey: getSettingsKey(), + settings: getFilters(), + visibleSettings: getVisibleFilters(), + parentId: topParentId, + itemTypes: getItemTypes(), + serverId: window.ApiClient.serverId(), + filterMenuOptions: getFilterMenuOptions() + }).then(() => { reloadItems(); }); - filterDialog.show(); }); - }, [getFilterMode, query, reloadItems]); + }, [getSettingsKey, getFilters, getVisibleFilters, topParentId, getItemTypes, getFilterMenuOptions, reloadItems]); useEffect(() => { const btnFilter = element.current?.querySelector('.btnFilter'); - if (btnFilter) { - btnFilter.addEventListener('click', showFilterMenu); - } + btnFilter?.addEventListener('click', showFilterMenu); + + return () => { + btnFilter?.removeEventListener('click', showFilterMenu); + }; }, [showFilterMenu]); return ( diff --git a/src/view/components/ViewItemsContainer.tsx b/src/view/components/ViewItemsContainer.tsx index 909da7be78..10a1046da2 100644 --- a/src/view/components/ViewItemsContainer.tsx +++ b/src/view/components/ViewItemsContainer.tsx @@ -21,8 +21,7 @@ interface ViewItemsContainerI { isBtnNewCollectionEnabled?: boolean; isAlphaPickerEnabled?: boolean; getBasekey: () => string; - getFilterMode: () => string; - getItemTypes: () => string; + getItemTypes: () => string[]; getNoItemsMessage: () => string; } @@ -33,7 +32,6 @@ const ViewItemsContainer: FC = ({ isBtnNewCollectionEnabled = false, isAlphaPickerEnabled = true, getBasekey, - getFilterMode, getItemTypes, getNoItemsMessage }) => { @@ -62,11 +60,52 @@ const ViewItemsContainer: FC = ({ }; }, [getDefaultSortBy, getSettingsKey]); + const getFilters = useCallback(() => { + const basekey = getSettingsKey(); + return { + IsPlayed: userSettings.getFilter(basekey + '-filter-IsPlayed') === 'true', + IsUnplayed: userSettings.getFilter(basekey + '-filter-IsUnplayed') === 'true', + IsFavorite: userSettings.getFilter(basekey + '-filter-IsFavorite') === 'true', + IsResumable: userSettings.getFilter(basekey + '-filter-IsResumable') === 'true', + Is4K: userSettings.getFilter(basekey + '-filter-Is4K') === 'true', + IsHD: userSettings.getFilter(basekey + '-filter-IsHD') === 'true', + IsSD: userSettings.getFilter(basekey + '-filter-IsSD') === 'true', + Is3D: userSettings.getFilter(basekey + '-filter-Is3D') === 'true', + VideoTypes: userSettings.getFilter(basekey + '-filter-VideoTypes'), + SeriesStatus: userSettings.getFilter(basekey + '-filter-SeriesStatus'), + HasSubtitles: userSettings.getFilter(basekey + '-filter-HasSubtitles'), + HasTrailer: userSettings.getFilter(basekey + '-filter-HasTrailer'), + HasSpecialFeature: userSettings.getFilter(basekey + '-filter-HasSpecialFeature'), + HasThemeSong: userSettings.getFilter(basekey + '-filter-HasThemeSong'), + HasThemeVideo: userSettings.getFilter(basekey + '-filter-HasThemeVideo'), + GenreIds: userSettings.getFilter(basekey + '-filter-GenreIds') + }; + }, [getSettingsKey]); + + const getFilterMenuOptions = useCallback(() => { + return {}; + }, []); + + const getVisibleFilters = useCallback(() => { + return [ + 'IsUnplayed', + 'IsPlayed', + 'IsFavorite', + 'IsResumable', + 'VideoType', + 'HasSubtitles', + 'HasTrailer', + 'HasSpecialFeature', + 'HasThemeSong', + 'HasThemeVideo' + ]; + }, []); + const getQuery = useCallback(() => { const query: QueryI = { SortBy: getSortValues().sortBy, SortOrder: getSortValues().sortOrder, - IncludeItemTypes: getItemTypes(), + IncludeItemTypes: getItemTypes().join(','), Recursive: true, Fields: 'PrimaryImageAspectRatio,MediaSourceCount,BasicSyncInfo', ImageTypeLimit: 1, @@ -84,6 +123,97 @@ const ViewItemsContainer: FC = ({ return query; }, [getSortValues, getItemTypes, topParentId, getBasekey, getSettingsKey]); + const getQueryWithFilters = useCallback(() => { + const query = getQuery(); + + const queryFilters = []; + let hasFilters; + + const filters = getFilters(); + + if (filters.IsPlayed) { + queryFilters.push('IsPlayed'); + hasFilters = true; + } + + if (filters.IsUnplayed) { + queryFilters.push('IsUnplayed'); + hasFilters = true; + } + + if (filters.IsFavorite) { + queryFilters.push('IsFavorite'); + hasFilters = true; + } + + if (filters.IsResumable) { + queryFilters.push('IsResumable'); + hasFilters = true; + } + + if (filters.VideoTypes) { + hasFilters = true; + query.VideoTypes = filters.VideoTypes; + } + + if (filters.GenreIds) { + hasFilters = true; + query.GenreIds = filters.GenreIds; + } + + if (filters.Is4K) { + query.Is4K = true; + hasFilters = true; + } + + if (filters.IsHD) { + query.IsHD = true; + hasFilters = true; + } + + if (filters.IsSD) { + query.IsHD = false; + hasFilters = true; + } + + if (filters.Is3D) { + query.Is3D = true; + hasFilters = true; + } + + if (filters.HasSubtitles) { + query.HasSubtitles = true; + hasFilters = true; + } + + if (filters.HasTrailer) { + query.HasTrailer = true; + hasFilters = true; + } + + if (filters.HasSpecialFeature) { + query.HasSpecialFeature = true; + hasFilters = true; + } + + if (filters.HasThemeSong) { + query.HasThemeSong = true; + hasFilters = true; + } + + if (filters.HasThemeVideo) { + query.HasThemeVideo = true; + hasFilters = true; + } + + query.Filters = queryFilters.length ? queryFilters.join(',') : null; + + return { + query: query, + hasFilters: hasFilters + }; + }, [getQuery, getFilters]); + const getSortMenuOptions = useCallback(() => { return [{ name: globalize.translate('Name'), @@ -123,7 +253,7 @@ const ViewItemsContainer: FC = ({ }, [getViewSettings]); const getContext = useCallback(() => { - const itemType = getItemTypes(); + const itemType = getItemTypes().join(','); if (itemType === 'Movie' || itemType === 'BoxSet') { return 'movies'; } @@ -139,8 +269,8 @@ const ViewItemsContainer: FC = ({ return; } loading.show(); - const query = getQuery(); - window.ApiClient.getItems(window.ApiClient.getCurrentUserId(), query).then((result) => { + const querywithfilters = getQueryWithFilters().query; + window.ApiClient.getItems(window.ApiClient.getCurrentUserId(), querywithfilters).then((result) => { setItemsResult(result); window.scrollTo(0, 0); @@ -150,7 +280,7 @@ const ViewItemsContainer: FC = ({ autoFocuser.autoFocus(page); }); }); - }, [getQuery]); + }, [getQueryWithFilters]); useEffect(() => { reloadItems(); @@ -171,7 +301,15 @@ const ViewItemsContainer: FC = ({ reloadItems={reloadItems} /> - {isBtnFilterEnabled && } + {isBtnFilterEnabled && } {isBtnNewCollectionEnabled && } diff --git a/src/view/components/interface.ts b/src/view/components/interface.ts index c05945ef6f..ba1e459ce0 100644 --- a/src/view/components/interface.ts +++ b/src/view/components/interface.ts @@ -12,8 +12,39 @@ export interface QueryI { IsFavorite?: boolean; IsMissing?: boolean; Limit:number; + NameStartsWithOrGreater?: string; NameLessThan?: string; NameStartsWith?: string; + VideoTypes?: string; + GenreIds?: string; + Is4K?: boolean; + IsHD?: boolean; + Is3D?: boolean; + HasSubtitles?: boolean; + HasTrailer?: boolean; + HasSpecialFeature?: boolean; + HasThemeSong?: boolean; + HasThemeVideo?: boolean; + Filters?: string | null; +} + +export interface FiltersI { + IsPlayed: boolean; + IsUnplayed: boolean; + IsFavorite: boolean; + IsResumable: boolean; + Is4K: boolean; + IsHD: boolean; + IsSD: boolean; + Is3D: boolean; + VideoTypes: string; + SeriesStatus: string; + HasSubtitles: string; + HasTrailer: string; + HasSpecialFeature: string; + HasThemeSong: string; + HasThemeVideo: string; + GenreIds: string; } export interface CardOptionsI { diff --git a/src/view/movies/CollectionsView.tsx b/src/view/movies/CollectionsView.tsx index 059f6b0526..81316ed074 100644 --- a/src/view/movies/CollectionsView.tsx +++ b/src/view/movies/CollectionsView.tsx @@ -11,12 +11,8 @@ const CollectionsView: FC = ({ topParentId }) => { return 'collections'; }, []); - const getFilterMode = useCallback(() => { - return 'movies'; - }, []); - const getItemTypes = useCallback(() => { - return 'BoxSet'; + return ['BoxSet']; }, []); const getNoItemsMessage = useCallback(() => { @@ -30,7 +26,6 @@ const CollectionsView: FC = ({ topParentId }) => { isBtnNewCollectionEnabled={true} isAlphaPickerEnabled={false} getBasekey={getBasekey} - getFilterMode={getFilterMode} getItemTypes={getItemTypes} getNoItemsMessage={getNoItemsMessage} /> diff --git a/src/view/movies/FavoritesView.tsx b/src/view/movies/FavoritesView.tsx index 4254578b70..d1619c3450 100644 --- a/src/view/movies/FavoritesView.tsx +++ b/src/view/movies/FavoritesView.tsx @@ -11,12 +11,8 @@ const FavoritesView: FC = ({ topParentId }) => { return 'favorites'; }, []); - const getFilterMode = useCallback(() => { - return 'movies'; - }, []); - const getItemTypes = useCallback(() => { - return 'Movie'; + return ['Movie']; }, []); const getNoItemsMessage = useCallback(() => { @@ -27,7 +23,6 @@ const FavoritesView: FC = ({ topParentId }) => { diff --git a/src/view/movies/MoviesView.tsx b/src/view/movies/MoviesView.tsx index a5f2c69e25..30074ac310 100644 --- a/src/view/movies/MoviesView.tsx +++ b/src/view/movies/MoviesView.tsx @@ -11,12 +11,8 @@ const MoviesView: FC = ({ topParentId }) => { return 'movies'; }, []); - const getFilterMode = useCallback(() => { - return 'movies'; - }, []); - const getItemTypes = useCallback(() => { - return 'Movie'; + return ['Movie']; }, []); const getNoItemsMessage = useCallback(() => { @@ -28,7 +24,6 @@ const MoviesView: FC = ({ topParentId }) => { topParentId={topParentId} isBtnShuffleEnabled={true} getBasekey={getBasekey} - getFilterMode={getFilterMode} getItemTypes={getItemTypes} getNoItemsMessage={getNoItemsMessage} /> diff --git a/src/view/movies/TrailersView.tsx b/src/view/movies/TrailersView.tsx index 4896e6e778..958d591ad6 100644 --- a/src/view/movies/TrailersView.tsx +++ b/src/view/movies/TrailersView.tsx @@ -12,12 +12,8 @@ const TrailersView: FC = ({ topParentId }) => { return 'trailers'; }, []); - const getFilterMode = useCallback(() => { - return 'movies'; - }, []); - const getItemTypes = useCallback(() => { - return 'Trailer'; + return ['Trailer']; }, []); const getNoItemsMessage = useCallback(() => { @@ -28,7 +24,6 @@ const TrailersView: FC = ({ topParentId }) => {