From 2b146f8b4def8d964ce382dba9b947daef46a46c Mon Sep 17 00:00:00 2001 From: grafixeyehero Date: Sat, 8 Jul 2023 17:55:25 +0300 Subject: [PATCH] refactor: viewitemcontainer --- .../library}/AlphaPickerContainer.tsx | 31 +- .../library/GenresItemsContainer.tsx | 2 +- .../library/GenresSectionContainer.tsx | 2 +- .../components/library/ItemsContainer.tsx | 137 ++++++ .../library/NewCollectionButton.tsx | 34 ++ .../components/library/Pagination.tsx | 94 ++++ .../components/library/PlayAllButton.tsx | 32 ++ .../components/library/ShuffleButton.tsx | 31 ++ .../components/library/SortButton.tsx | 185 +++++++ .../library/SuggestionsItemsContainer.tsx | 2 +- .../library/SuggestionsSectionContainer.tsx | 2 +- .../components/library/ViewItemsContainer.tsx | 133 +++++ .../components/library/ViewSettingsButton.tsx | 216 +++++++++ .../library/filter/FilterButton.tsx | 457 ++++++++++++++++++ .../library/filter/FiltersEpisodesStatus.tsx | 75 +++ .../library/filter/FiltersFeatures.tsx | 81 ++++ .../library/filter/FiltersGenres.tsx | 71 +++ .../library/filter/FiltersOfficialRatings.tsx | 71 +++ .../library/filter/FiltersSeriesStatus.tsx | 74 +++ .../library/filter/FiltersStatus.tsx | 97 ++++ .../library/filter/FiltersStudios.tsx | 71 +++ .../components/library/filter/FiltersTags.tsx | 71 +++ .../library/filter/FiltersVideoTypes.tsx | 131 +++++ .../library/filter/FiltersYears.tsx | 71 +++ .../routes/movies/CollectionsView.tsx | 28 +- .../routes/movies/FavoritesView.tsx | 26 +- .../experimental/routes/movies/MoviesView.tsx | 28 +- .../routes/movies/TrailersView.tsx | 27 +- src/apps/experimental/routes/movies/index.tsx | 6 - src/components/common/Filter.tsx | 65 --- src/components/common/ItemsContainer.tsx | 33 -- src/components/common/NewCollection.tsx | 42 -- src/components/common/Pagination.tsx | 97 ---- src/components/common/SelectView.tsx | 54 --- src/components/common/Shuffle.tsx | 45 -- src/components/common/Sort.tsx | 58 --- src/components/common/ViewItemsContainer.tsx | 411 ---------------- src/hooks/useFetchItems.ts | 237 ++++++++- src/strings/en-us.json | 1 + src/types/cardOptions.ts | 3 +- src/types/interface.ts | 29 -- src/types/library.ts | 38 +- src/types/libraryTab.ts | 5 +- src/utils/items.ts | 207 ++++++++ 44 files changed, 2662 insertions(+), 949 deletions(-) rename src/{components/common => apps/experimental/components/library}/AlphaPickerContainer.tsx (56%) create mode 100644 src/apps/experimental/components/library/ItemsContainer.tsx create mode 100644 src/apps/experimental/components/library/NewCollectionButton.tsx create mode 100644 src/apps/experimental/components/library/Pagination.tsx create mode 100644 src/apps/experimental/components/library/PlayAllButton.tsx create mode 100644 src/apps/experimental/components/library/ShuffleButton.tsx create mode 100644 src/apps/experimental/components/library/SortButton.tsx create mode 100644 src/apps/experimental/components/library/ViewItemsContainer.tsx create mode 100644 src/apps/experimental/components/library/ViewSettingsButton.tsx create mode 100644 src/apps/experimental/components/library/filter/FilterButton.tsx create mode 100644 src/apps/experimental/components/library/filter/FiltersEpisodesStatus.tsx create mode 100644 src/apps/experimental/components/library/filter/FiltersFeatures.tsx create mode 100644 src/apps/experimental/components/library/filter/FiltersGenres.tsx create mode 100644 src/apps/experimental/components/library/filter/FiltersOfficialRatings.tsx create mode 100644 src/apps/experimental/components/library/filter/FiltersSeriesStatus.tsx create mode 100644 src/apps/experimental/components/library/filter/FiltersStatus.tsx create mode 100644 src/apps/experimental/components/library/filter/FiltersStudios.tsx create mode 100644 src/apps/experimental/components/library/filter/FiltersTags.tsx create mode 100644 src/apps/experimental/components/library/filter/FiltersVideoTypes.tsx create mode 100644 src/apps/experimental/components/library/filter/FiltersYears.tsx delete mode 100644 src/components/common/Filter.tsx delete mode 100644 src/components/common/ItemsContainer.tsx delete mode 100644 src/components/common/NewCollection.tsx delete mode 100644 src/components/common/Pagination.tsx delete mode 100644 src/components/common/SelectView.tsx delete mode 100644 src/components/common/Shuffle.tsx delete mode 100644 src/components/common/Sort.tsx delete mode 100644 src/components/common/ViewItemsContainer.tsx delete mode 100644 src/types/interface.ts create mode 100644 src/utils/items.ts diff --git a/src/components/common/AlphaPickerContainer.tsx b/src/apps/experimental/components/library/AlphaPickerContainer.tsx similarity index 56% rename from src/components/common/AlphaPickerContainer.tsx rename to src/apps/experimental/components/library/AlphaPickerContainer.tsx index 6b7c9a07183d..8a9348a61bb8 100644 --- a/src/components/common/AlphaPickerContainer.tsx +++ b/src/apps/experimental/components/library/AlphaPickerContainer.tsx @@ -1,38 +1,27 @@ import React, { FC, useCallback, useEffect, useRef, useState } from 'react'; -import AlphaPicker from '../alphaPicker/alphaPicker'; -import { ViewQuerySettings } from '../../types/interface'; +import AlphaPicker from 'components/alphaPicker/alphaPicker'; +import { LibraryViewSettings } from 'types/library'; interface AlphaPickerContainerProps { - viewQuerySettings: ViewQuerySettings; - setViewQuerySettings: React.Dispatch>; + libraryViewSettings: LibraryViewSettings; + setLibraryViewSettings: React.Dispatch>; } -const AlphaPickerContainer: FC = ({ viewQuerySettings, setViewQuerySettings }) => { +const AlphaPickerContainer: FC = ({ libraryViewSettings, setLibraryViewSettings }) => { const [ alphaPicker, setAlphaPicker ] = useState(); const element = useRef(null); - alphaPicker?.updateControls(viewQuerySettings); + alphaPicker?.updateControls(libraryViewSettings); const onAlphaPickerChange = useCallback((e) => { const newValue = (e as CustomEvent).detail.value; - let updatedValue: React.SetStateAction; - if (newValue === '#') { - updatedValue = { - NameLessThan: 'A', - NameStartsWith: undefined - }; - } else { - updatedValue = { - NameLessThan: undefined, - NameStartsWith: newValue - }; - } - setViewQuerySettings((prevState) => ({ + setLibraryViewSettings((prevState) => ({ ...prevState, StartIndex: 0, - ...updatedValue + NameLessThan: newValue === '#' ? 'A' : undefined, + NameStartsWith: newValue === '#' ? undefined : newValue })); - }, [setViewQuerySettings]); + }, [setLibraryViewSettings]); useEffect(() => { const alphaPickerElement = element.current; diff --git a/src/apps/experimental/components/library/GenresItemsContainer.tsx b/src/apps/experimental/components/library/GenresItemsContainer.tsx index 41fba412d14f..b45c7bc4c883 100644 --- a/src/apps/experimental/components/library/GenresItemsContainer.tsx +++ b/src/apps/experimental/components/library/GenresItemsContainer.tsx @@ -7,7 +7,7 @@ import GenresSectionContainer from './GenresSectionContainer'; import { CollectionType } from 'types/collectionType'; interface GenresItemsContainerProps { - parentId?: string | null; + parentId: string | null | undefined; collectionType?: CollectionType; itemType: BaseItemKind; } diff --git a/src/apps/experimental/components/library/GenresSectionContainer.tsx b/src/apps/experimental/components/library/GenresSectionContainer.tsx index 74f57782b193..78ded474765b 100644 --- a/src/apps/experimental/components/library/GenresSectionContainer.tsx +++ b/src/apps/experimental/components/library/GenresSectionContainer.tsx @@ -14,7 +14,7 @@ import SectionContainer from './SectionContainer'; import { CollectionType } from 'types/collectionType'; interface GenresSectionContainerProps { - parentId?: string | null; + parentId: string | null | undefined; collectionType?: CollectionType; itemType: BaseItemKind; genre: BaseItemDto; diff --git a/src/apps/experimental/components/library/ItemsContainer.tsx b/src/apps/experimental/components/library/ItemsContainer.tsx new file mode 100644 index 000000000000..658c8e7fad6c --- /dev/null +++ b/src/apps/experimental/components/library/ItemsContainer.tsx @@ -0,0 +1,137 @@ +import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client'; +import React, { FC, useEffect, useCallback, useRef } from 'react'; +import globalize from 'scripts/globalize'; +import cardBuilder from 'components/cardbuilder/cardBuilder'; +import listview from 'components/listview/listview'; +import imageLoader from 'components/images/imageLoader'; +import ItemsContainerElement from 'elements/ItemsContainerElement'; +import 'elements/emby-itemscontainer/emby-itemscontainer'; +import { LibraryViewSettings } from 'types/library'; +import { LibraryTab } from 'types/libraryTab'; +import { CollectionType } from 'types/collectionType'; +import { CardOptions } from 'types/cardOptions'; + +interface ItemsContainerI { + libraryViewSettings: LibraryViewSettings; + viewType: LibraryTab; + collectionType?: CollectionType; + noItemsMessage: string; + items: BaseItemDto[]; +} + +const ItemsContainer: FC = ({ libraryViewSettings, viewType, collectionType, noItemsMessage, items }) => { + const element = useRef(null); + + const getCardOptions = useCallback(() => { + let shape; + let preferThumb; + let preferDisc; + let preferLogo; + let lines = libraryViewSettings.ShowTitle ? 2 : 0; + + if (libraryViewSettings.ImageType === 'banner') { + shape = 'banner'; + } else if (libraryViewSettings.ImageType === 'disc') { + shape = 'square'; + preferDisc = true; + } else if (libraryViewSettings.ImageType === 'logo') { + shape = 'backdrop'; + preferLogo = true; + } else if (libraryViewSettings.ImageType === 'thumb') { + shape = 'backdrop'; + preferThumb = true; + } else { + shape = 'auto'; + } + + const cardOptions: CardOptions = { + shape: shape, + showTitle: libraryViewSettings.ShowTitle, + showYear: libraryViewSettings.ShowYear, + cardLayout: libraryViewSettings.CardLayout, + centerText: true, + context: collectionType, + coverImage: true, + preferThumb: preferThumb, + preferDisc: preferDisc, + preferLogo: preferLogo, + overlayPlayButton: false, + overlayMoreButton: true, + overlayText: !libraryViewSettings.ShowTitle + }; + + if ( + viewType === LibraryTab.Songs + || viewType === LibraryTab.Albums + || viewType === LibraryTab.Episodes + ) { + cardOptions.showParentTitle = libraryViewSettings.ShowTitle; + } else if (viewType === LibraryTab.Artists) { + cardOptions.showYear = false; + lines = 1; + } + + cardOptions.lines = lines; + cardOptions.items = items; + + return cardOptions; + }, [ + viewType, + collectionType, + items, + libraryViewSettings.CardLayout, + libraryViewSettings.ImageType, + libraryViewSettings.ShowTitle, + libraryViewSettings.ShowYear + ]); + + const getItemsHtml = useCallback(() => { + let html = ''; + + if (libraryViewSettings.ImageType === 'list') { + html = listview.getListViewHtml({ + items: items, + context: collectionType + }); + } else { + html = cardBuilder.getCardsHtml( + items, + getCardOptions() + ); + } + + if (!items.length) { + html += '
'; + html + += '

' + globalize.translate('MessageNothingHere') + '

'; + html += '

' + globalize.translate(noItemsMessage) + '

'; + html += '
'; + } + + return html; + }, [ + getCardOptions, + collectionType, + items, + noItemsMessage, + libraryViewSettings.ImageType + ]); + + useEffect(() => { + const itemsContainer = element.current?.querySelector('.itemsContainer') as HTMLDivElement; + itemsContainer.innerHTML = getItemsHtml(); + imageLoader.lazyChildren(itemsContainer); + }, [getItemsHtml]); + + const cssClass = libraryViewSettings.ImageType == 'list' ? 'vertical-list' : 'vertical-wrap'; + + return ( +
+ +
+ ); +}; + +export default ItemsContainer; diff --git a/src/apps/experimental/components/library/NewCollectionButton.tsx b/src/apps/experimental/components/library/NewCollectionButton.tsx new file mode 100644 index 000000000000..e337de7ddd2c --- /dev/null +++ b/src/apps/experimental/components/library/NewCollectionButton.tsx @@ -0,0 +1,34 @@ +import React, { FC, useCallback } from 'react'; +import { IconButton } from '@mui/material'; +import AddIcon from '@mui/icons-material/Add'; +import globalize from 'scripts/globalize'; + +const NewCollectionButton: FC = () => { + const showCollectionEditor = useCallback(() => { + import('components/collectionEditor/collectionEditor').then( + ({ default: CollectionEditor }) => { + const serverId = window.ApiClient.serverId(); + const collectionEditor = new CollectionEditor(); + collectionEditor.show({ + items: [], + serverId: serverId + }).catch(() => { + // closed collection editor + }); + }).catch(err => { + console.error('[NewCollection] failed to load collection editor', err); + }); + }, []); + + return ( + + + + ); +}; + +export default NewCollectionButton; diff --git a/src/apps/experimental/components/library/Pagination.tsx b/src/apps/experimental/components/library/Pagination.tsx new file mode 100644 index 000000000000..a01f8152b1e9 --- /dev/null +++ b/src/apps/experimental/components/library/Pagination.tsx @@ -0,0 +1,94 @@ +import React, { FC, useCallback } from 'react'; +import Box from '@mui/material/Box'; +import ArrowBackIcon from '@mui/icons-material/ArrowBack'; +import ArrowForwardIcon from '@mui/icons-material/ArrowForward'; +import IconButton from '@mui/material/IconButton'; + +import globalize from 'scripts/globalize'; +import * as userSettings from 'scripts/settings/userSettings'; +import { LibraryViewSettings } from 'types/library'; + +interface PaginationProps { + libraryViewSettings: LibraryViewSettings; + setLibraryViewSettings: React.Dispatch>; + totalRecordCount: number; +} + +const Pagination: FC = ({ + libraryViewSettings, + setLibraryViewSettings, + totalRecordCount +}) => { + const limit = userSettings.libraryPageSize(undefined); + const startIndex = libraryViewSettings.StartIndex || 0; + const recordsStart = totalRecordCount ? startIndex + 1 : 0; + const recordsEnd = limit ? + Math.min(startIndex + limit, totalRecordCount) : + totalRecordCount; + const showControls = limit > 0 && limit < totalRecordCount; + + const onNextPageClick = useCallback(() => { + if (limit > 0) { + const newIndex = startIndex + limit; + setLibraryViewSettings((prevState) => ({ + ...prevState, + StartIndex: newIndex + })); + } + }, [limit, setLibraryViewSettings, startIndex]); + + const onPreviousPageClick = useCallback(() => { + if (limit > 0) { + const newIndex = Math.max(0, startIndex - limit); + setLibraryViewSettings((prevState) => ({ + ...prevState, + StartIndex: newIndex + })); + } + }, [limit, setLibraryViewSettings, startIndex]); + + return ( + + + + {globalize.translate( + 'ListPaging', + recordsStart, + recordsEnd, + totalRecordCount + )} + + {showControls && ( + <> + + + + + = totalRecordCount ? + true : + false + } + onClick={onNextPageClick} + > + + + + )} + + + ); +}; + +export default Pagination; diff --git a/src/apps/experimental/components/library/PlayAllButton.tsx b/src/apps/experimental/components/library/PlayAllButton.tsx new file mode 100644 index 000000000000..ff4385255ee7 --- /dev/null +++ b/src/apps/experimental/components/library/PlayAllButton.tsx @@ -0,0 +1,32 @@ +import React, { FC, useCallback } from 'react'; +import PlayArrowIcon from '@mui/icons-material/PlayArrow'; +import IconButton from '@mui/material/IconButton'; +import { useGetItem } from 'hooks/useFetchItems'; +import { playbackManager } from 'components/playback/playbackmanager'; +import globalize from 'scripts/globalize'; + +interface PlayAllButtonProps { + parentId: string | null | undefined; +} + +const PlayAllButton: FC = ({ parentId }) => { + const { data: item } = useGetItem(parentId); + + const playAll = useCallback(() => { + playbackManager.play({ + items: [item] + }); + }, [item]); + + return ( + + + + ); +}; + +export default PlayAllButton; diff --git a/src/apps/experimental/components/library/ShuffleButton.tsx b/src/apps/experimental/components/library/ShuffleButton.tsx new file mode 100644 index 000000000000..d3567fdaac85 --- /dev/null +++ b/src/apps/experimental/components/library/ShuffleButton.tsx @@ -0,0 +1,31 @@ +import React, { FC, useCallback } from 'react'; +import { IconButton } from '@mui/material'; +import ShuffleIcon from '@mui/icons-material/Shuffle'; + +import { useGetItem } from 'hooks/useFetchItems'; +import { playbackManager } from 'components/playback/playbackmanager'; +import globalize from 'scripts/globalize'; + +interface ShuffleButtonProps { + parentId: string | null | undefined; +} + +const ShuffleButton: FC = ({ parentId }) => { + const { data: item } = useGetItem(parentId); + + const shuffle = useCallback(() => { + playbackManager.shuffle(item); + }, [item]); + + return ( + + + + ); +}; + +export default ShuffleButton; diff --git a/src/apps/experimental/components/library/SortButton.tsx b/src/apps/experimental/components/library/SortButton.tsx new file mode 100644 index 000000000000..7deeae349b09 --- /dev/null +++ b/src/apps/experimental/components/library/SortButton.tsx @@ -0,0 +1,185 @@ +import React, { FC, useCallback } from 'react'; +import IconButton from '@mui/material/IconButton'; +import MenuItem from '@mui/material/MenuItem'; +import Popover from '@mui/material/Popover'; +import Typography from '@mui/material/Typography'; +import Divider from '@mui/material/Divider'; +import Box from '@mui/material/Box'; +import InputLabel from '@mui/material/InputLabel'; +import FormControl from '@mui/material/FormControl'; +import Select, { SelectChangeEvent } from '@mui/material/Select'; +import SortByAlphaIcon from '@mui/icons-material/SortByAlpha'; + +import globalize from 'scripts/globalize'; +import { LibraryViewSettings } from 'types/library'; +import { LibraryTab } from 'types/libraryTab'; +import { ItemSortBy } from '@jellyfin/sdk/lib/models/api/item-sort-by'; +import { SortOrder } from '@jellyfin/sdk/lib/generated-client'; + +const sortMenuOptions = [ + { label: 'Name', value: ItemSortBy.SortName }, + { label: 'OptionRandom', value: ItemSortBy.Random }, + { label: 'OptionImdbRating', value: ItemSortBy.CommunityRating }, + { label: 'OptionCriticRating', value: ItemSortBy.CriticRating }, + { label: 'OptionDateAdded', value: ItemSortBy.DateCreated }, + { label: 'OptionDatePlayed', value: ItemSortBy.DatePlayed }, + { label: 'OptionParentalRating', value: ItemSortBy.OfficialRating }, + { label: 'OptionPlayCount', value: ItemSortBy.PlayCount }, + { label: 'OptionReleaseDate', value: ItemSortBy.PremiereDate }, + { label: 'Runtime', value: ItemSortBy.Runtime } +]; + +const sortOrderMenuOptions = [ + { label: 'Ascending', value: SortOrder.Ascending }, + { label: 'Descending', value: SortOrder.Descending } +]; + +interface SortButtonProps { + viewType: LibraryTab; + libraryViewSettings: LibraryViewSettings; + setLibraryViewSettings: React.Dispatch< + React.SetStateAction + >; +} + +const SortButton: FC = ({ + viewType, + libraryViewSettings, + setLibraryViewSettings +}) => { + const [anchorEl, setAnchorEl] = React.useState(null); + const open = Boolean(anchorEl); + const id = open ? 'sort-popover' : undefined; + + const handleClick = useCallback((event: React.MouseEvent) => { + setAnchorEl(event.currentTarget); + }, []); + + const handleClose = useCallback(() => { + setAnchorEl(null); + }, []); + + const onSelectChange = useCallback( + (event: SelectChangeEvent) => { + const name = event.target.name; + + setLibraryViewSettings((prevState) => ({ + ...prevState, + StartIndex: 0, + [name]: event.target.value + })); + }, + [setLibraryViewSettings] + ); + + const getVisibleSortMenu = () => { + const visibleSortMenu: ItemSortBy[] = [ItemSortBy.SortName, ItemSortBy.Random, ItemSortBy.DateCreated]; + + if ( + viewType !== LibraryTab.Photos + && viewType !== LibraryTab.Videos + && viewType !== LibraryTab.Books + ) { + visibleSortMenu.push(ItemSortBy.CommunityRating); + visibleSortMenu.push(ItemSortBy.CriticRating); + visibleSortMenu.push(ItemSortBy.DatePlayed); + visibleSortMenu.push(ItemSortBy.OfficialRating); + visibleSortMenu.push(ItemSortBy.PlayCount); + visibleSortMenu.push(ItemSortBy.PremiereDate); + visibleSortMenu.push(ItemSortBy.Runtime); + } + + return visibleSortMenu; + }; + + return ( + + + + + + + + + + {globalize.translate('LabelSortBy')} + + + + + + + + + + {globalize.translate('LabelSortOrder')} + + + + + + + ); +}; + +export default SortButton; diff --git a/src/apps/experimental/components/library/SuggestionsItemsContainer.tsx b/src/apps/experimental/components/library/SuggestionsItemsContainer.tsx index d985592a8ef0..e335ad3e7d36 100644 --- a/src/apps/experimental/components/library/SuggestionsItemsContainer.tsx +++ b/src/apps/experimental/components/library/SuggestionsItemsContainer.tsx @@ -178,7 +178,7 @@ const getSuggestionsSections = (): Sections[] => { }; interface SuggestionsItemsContainerProps { - parentId?: string | null; + parentId: string | null | undefined; sectionsView: SectionsView[]; } diff --git a/src/apps/experimental/components/library/SuggestionsSectionContainer.tsx b/src/apps/experimental/components/library/SuggestionsSectionContainer.tsx index 4c52d712e134..c7550b7a7424 100644 --- a/src/apps/experimental/components/library/SuggestionsSectionContainer.tsx +++ b/src/apps/experimental/components/library/SuggestionsSectionContainer.tsx @@ -9,7 +9,7 @@ import SectionContainer from './SectionContainer'; import { Sections } from 'types/suggestionsSections'; interface SuggestionsSectionContainerProps { - parentId?: string | null; + parentId: string | null | undefined; section: Sections; } diff --git a/src/apps/experimental/components/library/ViewItemsContainer.tsx b/src/apps/experimental/components/library/ViewItemsContainer.tsx new file mode 100644 index 000000000000..34484cab42bd --- /dev/null +++ b/src/apps/experimental/components/library/ViewItemsContainer.tsx @@ -0,0 +1,133 @@ +import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind'; +import React, { FC } from 'react'; +import Box from '@mui/material/Box'; +import { useLocalStorage } from 'hooks/useLocalStorage'; +import { useGetViewItemsByType } from 'hooks/useFetchItems'; +import { getDefaultLibraryViewSettings, getSettingsKey } from 'utils/items'; +import Loading from 'components/loading/LoadingComponent'; +import AlphaPickerContainer from './AlphaPickerContainer'; +import FilterButton from './filter/FilterButton'; +import ItemsContainer from './ItemsContainer'; +import NewCollectionButton from './NewCollectionButton'; +import Pagination from './Pagination'; +import PlayAllButton from './PlayAllButton'; +import ShuffleButton from './ShuffleButton'; +import SortButton from './SortButton'; +import ViewSettingsButton from './ViewSettingsButton'; +import { LibraryViewSettings } from 'types/library'; +import { CollectionType } from 'types/collectionType'; +import { LibraryTab } from 'types/libraryTab'; + +interface ViewItemsContainerProps { + viewType: LibraryTab; + parentId: string | null | undefined; + collectionType?: CollectionType; + isBtnPlayAllEnabled?: boolean; + isBtnShuffleEnabled?: boolean; + isBtnSelectViewEnabled?: boolean; + isBtnSortEnabled?: boolean; + isBtnFilterEnabled?: boolean; + isBtnNewCollectionEnabled?: boolean; + isAlphaPickerEnabled?: boolean; + itemType: BaseItemKind; + noItemsMessage: string; +} + +const ViewItemsContainer: FC = ({ + viewType, + parentId, + collectionType, + isBtnPlayAllEnabled = false, + isBtnShuffleEnabled = false, + isBtnSelectViewEnabled = true, + isBtnSortEnabled = true, + isBtnFilterEnabled = true, + isBtnNewCollectionEnabled = false, + isAlphaPickerEnabled = true, + itemType, + noItemsMessage +}) => { + const [libraryViewSettings, setLibraryViewSettings] = + useLocalStorage( + getSettingsKey(viewType, parentId), + getDefaultLibraryViewSettings(viewType) + ); + + const { isLoading, data: itemsResult } = useGetViewItemsByType( + viewType, + parentId, + itemType, + libraryViewSettings + ); + + if (isLoading) return ; + + return ( + + + + + {isBtnPlayAllEnabled && } + + {isBtnShuffleEnabled && } + + {isBtnSelectViewEnabled && ( + + )} + + {isBtnSortEnabled && ( + + )} + + {isBtnFilterEnabled && ( + + )} + + {isBtnNewCollectionEnabled && } + + + {isAlphaPickerEnabled && ( + + )} + + + + + + + + ); +}; + +export default ViewItemsContainer; diff --git a/src/apps/experimental/components/library/ViewSettingsButton.tsx b/src/apps/experimental/components/library/ViewSettingsButton.tsx new file mode 100644 index 000000000000..7030f592b507 --- /dev/null +++ b/src/apps/experimental/components/library/ViewSettingsButton.tsx @@ -0,0 +1,216 @@ +import React, { FC, useCallback } from 'react'; + +import IconButton from '@mui/material/IconButton'; +import MenuItem from '@mui/material/MenuItem'; +import Checkbox from '@mui/material/Checkbox'; +import Typography from '@mui/material/Typography'; +import Divider from '@mui/material/Divider'; +import Box from '@mui/material/Box'; +import InputLabel from '@mui/material/InputLabel'; +import FormControl from '@mui/material/FormControl'; +import FormControlLabel from '@mui/material/FormControlLabel'; +import FormGroup from '@mui/material/FormGroup'; +import Select, { SelectChangeEvent } from '@mui/material/Select'; +import Popover from '@mui/material/Popover'; +import ViewComfyIcon from '@mui/icons-material/ViewComfy'; + +import globalize from 'scripts/globalize'; +import { LibraryViewSettings } from 'types/library'; +import { LibraryTab } from 'types/libraryTab'; + +const imageTypesOptions = [ + { + label: globalize.translate('Primary'), + value: 'primary' + }, + { + label: globalize.translate('Banner'), + value: 'banner' + }, + { + label: globalize.translate('Disc'), + value: 'disc' + }, + { + label: globalize.translate('Logo'), + value: 'logo' + }, + { + label: globalize.translate('Thumb'), + value: 'thumb' + }, + { + label: globalize.translate('List'), + value: 'list' + } +]; + +interface ViewSettingsButtonProps { + viewType: LibraryTab; + libraryViewSettings: LibraryViewSettings; + setLibraryViewSettings: React.Dispatch>; +} + +const ViewSettingsButton: FC = ({ + viewType, + libraryViewSettings, + setLibraryViewSettings +}) => { + const [anchorEl, setAnchorEl] = React.useState(null); + const open = Boolean(anchorEl); + const id = open ? 'selectview-popover' : undefined; + + const handleClick = useCallback((event: React.MouseEvent) => { + setAnchorEl(event.currentTarget); + }, []); + + const handleClose = useCallback(() => { + setAnchorEl(null); + }, []); + + const handleChange = useCallback( + (event: React.ChangeEvent) => { + const name = event.target.name; + + setLibraryViewSettings((prevState) => ({ + ...prevState, + [name]: event.target.checked + })); + }, + [setLibraryViewSettings] + ); + + const onSelectChange = useCallback( + (event: SelectChangeEvent) => { + setLibraryViewSettings((prevState) => ({ + ...prevState, + ImageType: event.target.value as string + })); + }, + [setLibraryViewSettings] + ); + + const getVisibleImageType = () => { + const visibleImageType = ['primary', 'list']; + + if ( + viewType !== LibraryTab.Episodes + && viewType !== LibraryTab.Artists + && viewType !== LibraryTab.AlbumArtists + && viewType !== LibraryTab.Albums + ) { + visibleImageType.push('banner'); + visibleImageType.push('disc'); + visibleImageType.push('logo'); + visibleImageType.push('thumb'); + } + + return visibleImageType; + }; + + const isViewSettingsEnabled = () => { + return libraryViewSettings.ImageType !== 'list'; + }; + + return ( + + + + + + + + + {globalize.translate('LabelImageType')} + + + + + {isViewSettingsEnabled() && ( + <> + + + + + } + label={globalize.translate('ShowTitle')} + /> + + } + label={globalize.translate('ShowYear')} + /> + + } + label={globalize.translate( + 'EnableCardLayout' + )} + /> + + + + )} + + + ); +}; + +export default ViewSettingsButton; 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 000000000000..34ab839f0270 --- /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 ( + + + + + + + + + {globalize.translate('Filters')} + + + + + + + {isFiltersSeriesStatusEnabled() && ( + <> + + + + {globalize.translate('HeaderSeriesStatus')} + + + + + + + + )} + {isFiltersEpisodesStatusEnabled() && ( + <> + + + + {globalize.translate('HeaderEpisodesStatus')} + + + + + + + + )} + {isFiltersFeaturesEnabled() && ( + <> + + + + {globalize.translate('Features')} + + + + + + + + )} + + {isFiltersVideoTypesEnabled() && ( + <> + + + + {globalize.translate('HeaderVideoType')} + + + + + + + + )} + + {isFiltersLegacyEnabled() && ( + <> + {data?.Genres && data?.Genres?.length > 0 && ( + + + + {globalize.translate('Genres')} + + + + + + + )} + + {data?.OfficialRatings + && data?.OfficialRatings?.length > 0 && ( + + + + {globalize.translate( + 'HeaderParentalRatings' + )} + + + + + + + )} + + {data?.Tags && data?.Tags.length > 0 && ( + + + + {globalize.translate('Tags')} + + + + + + + )} + + {data?.Years && data?.Years?.length > 0 && ( + + + + {globalize.translate('HeaderYears')} + + + + + + + )} + + )} + {isFiltersStudiosEnabled() && ( + <> + + + + {globalize.translate('Studios')} + + + + + + + + )} + + + ); +}; + +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 000000000000..8360b2b5a85d --- /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 000000000000..51403504601a --- /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 000000000000..bd76f41e552b --- /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 000000000000..0476f8688d63 --- /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 000000000000..3420c8c7a008 --- /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 000000000000..72e313919951 --- /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 000000000000..da2b80c3a9d8 --- /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 000000000000..11b96533e27d --- /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 000000000000..3aa99cb0743b --- /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 000000000000..35f1505008bb --- /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/apps/experimental/routes/movies/CollectionsView.tsx b/src/apps/experimental/routes/movies/CollectionsView.tsx index ef574b916e52..6b873532174f 100644 --- a/src/apps/experimental/routes/movies/CollectionsView.tsx +++ b/src/apps/experimental/routes/movies/CollectionsView.tsx @@ -1,30 +1,22 @@ -import React, { FC, useCallback } from 'react'; +import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind'; +import React, { FC } from 'react'; -import ViewItemsContainer from 'components/common/ViewItemsContainer'; +import ViewItemsContainer from '../../components/library/ViewItemsContainer'; import { LibraryViewProps } from 'types/library'; +import { CollectionType } from 'types/collectionType'; +import { LibraryTab } from 'types/libraryTab'; const CollectionsView: FC = ({ parentId }) => { - const getBasekey = useCallback(() => { - return 'collections'; - }, []); - - const getItemTypes = useCallback(() => { - return ['BoxSet']; - }, []); - - const getNoItemsMessage = useCallback(() => { - return 'MessageNoCollectionsAvailable'; - }, []); - return ( ); }; diff --git a/src/apps/experimental/routes/movies/FavoritesView.tsx b/src/apps/experimental/routes/movies/FavoritesView.tsx index d22cad6e385f..da4ead4f5f2b 100644 --- a/src/apps/experimental/routes/movies/FavoritesView.tsx +++ b/src/apps/experimental/routes/movies/FavoritesView.tsx @@ -1,27 +1,17 @@ -import React, { FC, useCallback } from 'react'; +import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind'; +import React, { FC } from 'react'; -import ViewItemsContainer from 'components/common/ViewItemsContainer'; +import ViewItemsContainer from '../../components/library/ViewItemsContainer'; import { LibraryViewProps } from 'types/library'; +import { LibraryTab } from 'types/libraryTab'; const FavoritesView: FC = ({ parentId }) => { - const getBasekey = useCallback(() => { - return 'favorites'; - }, []); - - const getItemTypes = useCallback(() => { - return ['Movie']; - }, []); - - const getNoItemsMessage = useCallback(() => { - return 'MessageNoFavoritesAvailable'; - }, []); - return ( ); }; diff --git a/src/apps/experimental/routes/movies/MoviesView.tsx b/src/apps/experimental/routes/movies/MoviesView.tsx index 8796c9a71113..925e26d3c5a0 100644 --- a/src/apps/experimental/routes/movies/MoviesView.tsx +++ b/src/apps/experimental/routes/movies/MoviesView.tsx @@ -1,28 +1,20 @@ -import React, { FC, useCallback } from 'react'; +import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind'; +import React, { FC } from 'react'; -import ViewItemsContainer from 'components/common/ViewItemsContainer'; +import ViewItemsContainer from '../../components/library/ViewItemsContainer'; import { LibraryViewProps } from 'types/library'; +import { CollectionType } from 'types/collectionType'; +import { LibraryTab } from 'types/libraryTab'; const MoviesView: FC = ({ parentId }) => { - const getBasekey = useCallback(() => { - return 'movies'; - }, []); - - const getItemTypes = useCallback(() => { - return ['Movie']; - }, []); - - const getNoItemsMessage = useCallback(() => { - return 'MessageNoItemsAvailable'; - }, []); - return ( ); }; diff --git a/src/apps/experimental/routes/movies/TrailersView.tsx b/src/apps/experimental/routes/movies/TrailersView.tsx index ff0ff0e73e17..d5d93bd063b6 100644 --- a/src/apps/experimental/routes/movies/TrailersView.tsx +++ b/src/apps/experimental/routes/movies/TrailersView.tsx @@ -1,28 +1,17 @@ +import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind'; +import React, { FC } from 'react'; -import React, { FC, useCallback } from 'react'; - -import ViewItemsContainer from 'components/common/ViewItemsContainer'; +import ViewItemsContainer from '../../components/library/ViewItemsContainer'; import { LibraryViewProps } from 'types/library'; +import { LibraryTab } from 'types/libraryTab'; const TrailersView: FC = ({ parentId }) => { - const getBasekey = useCallback(() => { - return 'trailers'; - }, []); - - const getItemTypes = useCallback(() => { - return ['Trailer']; - }, []); - - const getNoItemsMessage = useCallback(() => { - return 'MessageNoTrailersFound'; - }, []); - return ( ); }; diff --git a/src/apps/experimental/routes/movies/index.tsx b/src/apps/experimental/routes/movies/index.tsx index e1a30dfb5937..9a792c279f19 100644 --- a/src/apps/experimental/routes/movies/index.tsx +++ b/src/apps/experimental/routes/movies/index.tsx @@ -1,12 +1,6 @@ -import 'elements/emby-scroller/emby-scroller'; -import 'elements/emby-itemscontainer/emby-itemscontainer'; -import 'elements/emby-tabs/emby-tabs'; -import 'elements/emby-button/emby-button'; - import React, { FC } from 'react'; import { useLocation, useSearchParams } from 'react-router-dom'; import Page from 'components/Page'; - import { getDefaultTabIndex } from '../../components/tabs/tabRoutes'; import CollectionsView from './CollectionsView'; import FavoritesView from './FavoritesView'; diff --git a/src/components/common/Filter.tsx b/src/components/common/Filter.tsx deleted file mode 100644 index c3ccdd62f3ab..000000000000 --- a/src/components/common/Filter.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import React, { FC, useCallback, useEffect, useRef } from 'react'; -import IconButtonElement from '../../elements/IconButtonElement'; -import { ViewQuerySettings } from '../../types/interface'; - -interface FilterProps { - topParentId?: string | null; - getItemTypes: () => string[]; - getFilterMenuOptions: () => Record; - getVisibleFilters: () => string[]; - viewQuerySettings: ViewQuerySettings; - setViewQuerySettings: React.Dispatch>; -} - -const Filter: FC = ({ - topParentId, - getItemTypes, - getVisibleFilters, - getFilterMenuOptions, - viewQuerySettings, - setViewQuerySettings -}) => { - const element = useRef(null); - - const showFilterMenu = useCallback(() => { - import('../filtermenu/filtermenu').then(({ default: FilterMenu }) => { - const filterMenu = new FilterMenu(); - filterMenu.show({ - settings: viewQuerySettings, - visibleSettings: getVisibleFilters(), - parentId: topParentId, - itemTypes: getItemTypes(), - serverId: window.ApiClient.serverId(), - filterMenuOptions: getFilterMenuOptions(), - setfilters: setViewQuerySettings - }).catch(() => { - // filter menu closed - }); - }).catch(err => { - console.error('[Filter] failed to load filter menu', err); - }); - }, [viewQuerySettings, getVisibleFilters, topParentId, getItemTypes, getFilterMenuOptions, setViewQuerySettings]); - - useEffect(() => { - const btnFilter = element.current?.querySelector('.btnFilter'); - - btnFilter?.addEventListener('click', showFilterMenu); - - return () => { - btnFilter?.removeEventListener('click', showFilterMenu); - }; - }, [showFilterMenu]); - - return ( -
- -
- ); -}; - -export default Filter; diff --git a/src/components/common/ItemsContainer.tsx b/src/components/common/ItemsContainer.tsx deleted file mode 100644 index 6289c1d81132..000000000000 --- a/src/components/common/ItemsContainer.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import React, { FC, useEffect, useRef } from 'react'; - -import ItemsContainerElement from '../../elements/ItemsContainerElement'; -import imageLoader from '../images/imageLoader'; -import '../../elements/emby-itemscontainer/emby-itemscontainer'; -import { ViewQuerySettings } from '../../types/interface'; - -interface ItemsContainerI { - viewQuerySettings: ViewQuerySettings; - getItemsHtml: () => string -} - -const ItemsContainer: FC = ({ viewQuerySettings, getItemsHtml }) => { - const element = useRef(null); - - useEffect(() => { - const itemsContainer = element.current?.querySelector('.itemsContainer') as HTMLDivElement; - itemsContainer.innerHTML = getItemsHtml(); - imageLoader.lazyChildren(itemsContainer); - }, [getItemsHtml]); - - const cssClass = viewQuerySettings.imageType == 'list' ? 'vertical-list' : 'vertical-wrap'; - - return ( -
- -
- ); -}; - -export default ItemsContainer; diff --git a/src/components/common/NewCollection.tsx b/src/components/common/NewCollection.tsx deleted file mode 100644 index 837fe85fd3fb..000000000000 --- a/src/components/common/NewCollection.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import React, { FC, useCallback, useEffect, useRef } from 'react'; - -import IconButtonElement from '../../elements/IconButtonElement'; - -const NewCollection: FC = () => { - const element = useRef(null); - - const showCollectionEditor = useCallback(() => { - import('../collectionEditor/collectionEditor').then(({ default: CollectionEditor }) => { - const serverId = window.ApiClient.serverId(); - const collectionEditor = new CollectionEditor(); - collectionEditor.show({ - items: [], - serverId: serverId - }).catch(() => { - // closed collection editor - }); - }).catch(err => { - console.error('[NewCollection] failed to load collection editor', err); - }); - }, []); - - useEffect(() => { - const btnNewCollection = element.current?.querySelector('.btnNewCollection'); - if (btnNewCollection) { - btnNewCollection.addEventListener('click', showCollectionEditor); - } - }, [showCollectionEditor]); - - return ( -
- -
- ); -}; - -export default NewCollection; diff --git a/src/components/common/Pagination.tsx b/src/components/common/Pagination.tsx deleted file mode 100644 index 3dd5a60ffd3d..000000000000 --- a/src/components/common/Pagination.tsx +++ /dev/null @@ -1,97 +0,0 @@ -import type { BaseItemDtoQueryResult } from '@jellyfin/sdk/lib/generated-client'; -import React, { FC, useCallback, useEffect, useRef } from 'react'; -import IconButtonElement from '../../elements/IconButtonElement'; -import globalize from '../../scripts/globalize'; -import * as userSettings from '../../scripts/settings/userSettings'; -import { ViewQuerySettings } from '../../types/interface'; - -interface PaginationProps { - viewQuerySettings: ViewQuerySettings; - setViewQuerySettings: React.Dispatch>; - itemsResult?: BaseItemDtoQueryResult; -} - -const Pagination: FC = ({ viewQuerySettings, setViewQuerySettings, itemsResult = {} }) => { - const limit = userSettings.libraryPageSize(undefined); - const totalRecordCount = itemsResult.TotalRecordCount || 0; - const startIndex = viewQuerySettings.StartIndex || 0; - const recordsStart = totalRecordCount ? startIndex + 1 : 0; - const recordsEnd = limit ? Math.min(startIndex + limit, totalRecordCount) : totalRecordCount; - const showControls = limit > 0 && limit < totalRecordCount; - const element = useRef(null); - - const onNextPageClick = useCallback(() => { - if (limit > 0) { - const newIndex = startIndex + limit; - setViewQuerySettings((prevState) => ({ - ...prevState, - StartIndex: newIndex - })); - } - }, [limit, setViewQuerySettings, startIndex]); - - const onPreviousPageClick = useCallback(() => { - if (limit > 0) { - const newIndex = Math.max(0, startIndex - limit); - setViewQuerySettings((prevState) => ({ - ...prevState, - StartIndex: newIndex - })); - } - }, [limit, setViewQuerySettings, startIndex]); - - useEffect(() => { - const btnNextPage = element.current?.querySelector('.btnNextPage') as HTMLButtonElement; - if (btnNextPage) { - if (startIndex + limit >= totalRecordCount) { - btnNextPage.disabled = true; - } else { - btnNextPage.disabled = false; - } - btnNextPage.addEventListener('click', onNextPageClick); - } - - const btnPreviousPage = element.current?.querySelector('.btnPreviousPage') as HTMLButtonElement; - if (btnPreviousPage) { - if (startIndex) { - btnPreviousPage.disabled = false; - } else { - btnPreviousPage.disabled = true; - } - btnPreviousPage.addEventListener('click', onPreviousPageClick); - } - - return () => { - btnNextPage?.removeEventListener('click', onNextPageClick); - btnPreviousPage?.removeEventListener('click', onPreviousPageClick); - }; - }, [totalRecordCount, onNextPageClick, onPreviousPageClick, limit, startIndex]); - - return ( -
-
-
- - {globalize.translate('ListPaging', recordsStart, recordsEnd, totalRecordCount)} - - {showControls && ( - <> - - - - )} -
-
-
- ); -}; - -export default Pagination; diff --git a/src/components/common/SelectView.tsx b/src/components/common/SelectView.tsx deleted file mode 100644 index bfb34555b87d..000000000000 --- a/src/components/common/SelectView.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import React, { FC, useCallback, useEffect, useRef } from 'react'; -import IconButtonElement from '../../elements/IconButtonElement'; -import { ViewQuerySettings } from '../../types/interface'; - -interface SelectViewProps { - getVisibleViewSettings: () => string[]; - viewQuerySettings: ViewQuerySettings; - setViewQuerySettings: React.Dispatch>; -} - -const SelectView: FC = ({ - getVisibleViewSettings, - viewQuerySettings, - setViewQuerySettings -}) => { - const element = useRef(null); - - const showViewSettingsMenu = useCallback(() => { - import('../viewSettings/viewSettings').then(({ default: ViewSettings }) => { - const viewsettings = new ViewSettings(); - viewsettings.show({ - settings: viewQuerySettings, - visibleSettings: getVisibleViewSettings(), - setviewsettings: setViewQuerySettings - }).catch(() => { - // view settings closed - }); - }).catch(err => { - console.error('[SelectView] failed to load view settings', err); - }); - }, [getVisibleViewSettings, viewQuerySettings, setViewQuerySettings]); - - useEffect(() => { - const btnSelectView = element.current?.querySelector('.btnSelectView') as HTMLButtonElement; - btnSelectView?.addEventListener('click', showViewSettingsMenu); - - return () => { - btnSelectView?.removeEventListener('click', showViewSettingsMenu); - }; - }, [showViewSettingsMenu]); - - return ( -
- -
- ); -}; - -export default SelectView; diff --git a/src/components/common/Shuffle.tsx b/src/components/common/Shuffle.tsx deleted file mode 100644 index 093dc7487448..000000000000 --- a/src/components/common/Shuffle.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import type { BaseItemDtoQueryResult } from '@jellyfin/sdk/lib/generated-client'; -import React, { FC, useCallback, useEffect, useRef } from 'react'; - -import { playbackManager } from '../playback/playbackmanager'; -import IconButtonElement from '../../elements/IconButtonElement'; - -interface ShuffleProps { - itemsResult?: BaseItemDtoQueryResult; - topParentId: string | null; -} - -const Shuffle: FC = ({ itemsResult = {}, topParentId }) => { - const element = useRef(null); - - const shuffle = useCallback(() => { - window.ApiClient.getItem( - window.ApiClient.getCurrentUserId(), - topParentId as string - ).then((item) => { - playbackManager.shuffle(item); - }).catch(err => { - console.error('[Shuffle] failed to fetch items', err); - }); - }, [topParentId]); - - useEffect(() => { - const btnShuffle = element.current?.querySelector('.btnShuffle'); - if (btnShuffle) { - btnShuffle.addEventListener('click', shuffle); - } - }, [itemsResult.TotalRecordCount, shuffle]); - - return ( -
- -
- ); -}; - -export default Shuffle; diff --git a/src/components/common/Sort.tsx b/src/components/common/Sort.tsx deleted file mode 100644 index db5cb8995624..000000000000 --- a/src/components/common/Sort.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import React, { FC, useCallback, useEffect, useRef } from 'react'; -import IconButtonElement from '../../elements/IconButtonElement'; -import { ViewQuerySettings } from '../../types/interface'; - -interface SortProps { - getSortMenuOptions: () => { - name: string; - value: string; - }[]; - viewQuerySettings: ViewQuerySettings; - setViewQuerySettings: React.Dispatch>; -} - -const Sort: FC = ({ - getSortMenuOptions, - viewQuerySettings, - setViewQuerySettings -}) => { - const element = useRef(null); - - const showSortMenu = useCallback(() => { - import('../sortmenu/sortmenu').then(({ default: SortMenu }) => { - const sortMenu = new SortMenu(); - sortMenu.show({ - settings: viewQuerySettings, - sortOptions: getSortMenuOptions(), - setSortValues: setViewQuerySettings - }).catch(() => { - // sort menu closed - }); - }).catch(err => { - console.error('[Sort] failed to load sort menu', err); - }); - }, [getSortMenuOptions, viewQuerySettings, setViewQuerySettings]); - - useEffect(() => { - const btnSort = element.current?.querySelector('.btnSort'); - - btnSort?.addEventListener('click', showSortMenu); - - return () => { - btnSort?.removeEventListener('click', showSortMenu); - }; - }, [showSortMenu]); - - return ( -
- -
- ); -}; - -export default Sort; diff --git a/src/components/common/ViewItemsContainer.tsx b/src/components/common/ViewItemsContainer.tsx deleted file mode 100644 index 1ea5ef9899d5..000000000000 --- a/src/components/common/ViewItemsContainer.tsx +++ /dev/null @@ -1,411 +0,0 @@ -import { type BaseItemDtoQueryResult, ItemFields, ItemFilter } from '@jellyfin/sdk/lib/generated-client'; -import React, { FC, useCallback, useEffect, useRef, useState } from 'react'; - -import loading from '../loading/loading'; -import * as userSettings from '../../scripts/settings/userSettings'; -import AlphaPickerContainer from './AlphaPickerContainer'; -import Filter from './Filter'; -import ItemsContainer from './ItemsContainer'; -import Pagination from './Pagination'; -import SelectView from './SelectView'; -import Shuffle from './Shuffle'; -import Sort from './Sort'; -import NewCollection from './NewCollection'; -import globalize from '../../scripts/globalize'; -import ServerConnections from '../ServerConnections'; -import { useLocalStorage } from '../../hooks/useLocalStorage'; -import listview from '../listview/listview'; -import cardBuilder from '../cardbuilder/cardBuilder'; - -import { ViewQuerySettings } from '../../types/interface'; -import { CardOptions } from '../../types/cardOptions'; - -interface ViewItemsContainerProps { - topParentId: string | null; - isBtnShuffleEnabled?: boolean; - isBtnFilterEnabled?: boolean; - isBtnNewCollectionEnabled?: boolean; - isAlphaPickerEnabled?: boolean; - getBasekey: () => string; - getItemTypes: () => string[]; - getNoItemsMessage: () => string; -} - -const getDefaultSortBy = () => { - return 'SortName'; -}; - -const getFields = (viewQuerySettings: ViewQuerySettings) => { - const fields: ItemFields[] = [ - ItemFields.BasicSyncInfo, - ItemFields.MediaSourceCount - ]; - - if (viewQuerySettings.imageType === 'primary') { - fields.push(ItemFields.PrimaryImageAspectRatio); - } - - return fields.join(','); -}; - -const getFilters = (viewQuerySettings: ViewQuerySettings) => { - const filters: ItemFilter[] = []; - - if (viewQuerySettings.IsPlayed) { - filters.push(ItemFilter.IsPlayed); - } - - if (viewQuerySettings.IsUnplayed) { - filters.push(ItemFilter.IsUnplayed); - } - - if (viewQuerySettings.IsFavorite) { - filters.push(ItemFilter.IsFavorite); - } - - if (viewQuerySettings.IsResumable) { - filters.push(ItemFilter.IsResumable); - } - - return filters; -}; - -const getVisibleViewSettings = () => { - return [ - 'showTitle', - 'showYear', - 'imageType', - 'cardLayout' - ]; -}; - -const getFilterMenuOptions = () => { - return {}; -}; - -const getVisibleFilters = () => { - return [ - 'IsUnplayed', - 'IsPlayed', - 'IsFavorite', - 'IsResumable', - 'VideoType', - 'HasSubtitles', - 'HasTrailer', - 'HasSpecialFeature', - 'HasThemeSong', - 'HasThemeVideo' - ]; -}; - -const getSortMenuOptions = () => { - return [{ - name: globalize.translate('Name'), - value: 'SortName,ProductionYear' - }, { - name: globalize.translate('OptionRandom'), - value: 'Random' - }, { - name: globalize.translate('OptionImdbRating'), - value: 'CommunityRating,SortName,ProductionYear' - }, { - name: globalize.translate('OptionCriticRating'), - value: 'CriticRating,SortName,ProductionYear' - }, { - name: globalize.translate('OptionDateAdded'), - value: 'DateCreated,SortName,ProductionYear' - }, { - name: globalize.translate('OptionDatePlayed'), - value: 'DatePlayed,SortName,ProductionYear' - }, { - name: globalize.translate('OptionParentalRating'), - value: 'OfficialRating,SortName,ProductionYear' - }, { - name: globalize.translate('OptionPlayCount'), - value: 'PlayCount,SortName,ProductionYear' - }, { - name: globalize.translate('OptionReleaseDate'), - value: 'PremiereDate,SortName,ProductionYear' - }, { - name: globalize.translate('Runtime'), - value: 'Runtime,SortName,ProductionYear' - }]; -}; - -const defaultViewQuerySettings: ViewQuerySettings = { - showTitle: true, - showYear: true, - imageType: 'primary', - viewType: '', - cardLayout: false, - SortBy: getDefaultSortBy(), - SortOrder: 'Ascending', - IsPlayed: false, - IsUnplayed: false, - IsFavorite: false, - IsResumable: false, - Is4K: null, - IsHD: null, - IsSD: null, - Is3D: null, - VideoTypes: '', - SeriesStatus: '', - HasSubtitles: null, - HasTrailer: null, - HasSpecialFeature: null, - HasThemeSong: null, - HasThemeVideo: null, - GenreIds: '', - StartIndex: 0 -}; - -const ViewItemsContainer: FC = ({ - topParentId, - isBtnShuffleEnabled = false, - isBtnFilterEnabled = true, - isBtnNewCollectionEnabled = false, - isAlphaPickerEnabled = true, - getBasekey, - getItemTypes, - getNoItemsMessage -}) => { - const getSettingsKey = useCallback(() => { - return `${topParentId} - ${getBasekey()}`; - }, [getBasekey, topParentId]); - - const [isLoading, setisLoading] = useState(false); - - const [viewQuerySettings, setViewQuerySettings] = useLocalStorage( - `viewQuerySettings - ${getSettingsKey()}`, - defaultViewQuerySettings - ); - - const [ itemsResult, setItemsResult ] = useState({}); - - const element = useRef(null); - - const getContext = useCallback(() => { - const itemType = getItemTypes().join(','); - if (itemType === 'Movie' || itemType === 'BoxSet') { - return 'movies'; - } - - return null; - }, [getItemTypes]); - - const getCardOptions = useCallback(() => { - let shape; - let preferThumb; - let preferDisc; - let preferLogo; - - if (viewQuerySettings.imageType === 'banner') { - shape = 'banner'; - } else if (viewQuerySettings.imageType === 'disc') { - shape = 'square'; - preferDisc = true; - } else if (viewQuerySettings.imageType === 'logo') { - shape = 'backdrop'; - preferLogo = true; - } else if (viewQuerySettings.imageType === 'thumb') { - shape = 'backdrop'; - preferThumb = true; - } else { - shape = 'autoVertical'; - } - - const cardOptions: CardOptions = { - shape: shape, - showTitle: viewQuerySettings.showTitle, - showYear: viewQuerySettings.showYear, - cardLayout: viewQuerySettings.cardLayout, - centerText: true, - context: getContext(), - coverImage: true, - preferThumb: preferThumb, - preferDisc: preferDisc, - preferLogo: preferLogo, - overlayPlayButton: false, - overlayMoreButton: true, - overlayText: !viewQuerySettings.showTitle - }; - - cardOptions.items = itemsResult.Items || []; - - return cardOptions; - }, [ - getContext, - itemsResult.Items, - viewQuerySettings.cardLayout, - viewQuerySettings.imageType, - viewQuerySettings.showTitle, - viewQuerySettings.showYear - ]); - - const getItemsHtml = useCallback(() => { - let html = ''; - - if (viewQuerySettings.imageType === 'list') { - html = listview.getListViewHtml({ - items: itemsResult.Items || [], - context: getContext() - }); - } else { - html = cardBuilder.getCardsHtml(itemsResult.Items || [], getCardOptions()); - } - - if (!itemsResult.Items?.length) { - html += '
'; - html += '

' + globalize.translate('MessageNothingHere') + '

'; - html += '

' + globalize.translate(getNoItemsMessage()) + '

'; - html += '
'; - } - - return html; - }, [getCardOptions, getContext, itemsResult.Items, getNoItemsMessage, viewQuerySettings.imageType]); - - const getQuery = useCallback(() => { - const queryFilters = getFilters(viewQuerySettings); - - let queryIsHD; - - if (viewQuerySettings.IsHD) { - queryIsHD = true; - } - - if (viewQuerySettings.IsSD) { - queryIsHD = false; - } - - return { - SortBy: viewQuerySettings.SortBy, - SortOrder: viewQuerySettings.SortOrder, - IncludeItemTypes: getItemTypes().join(','), - Recursive: true, - Fields: getFields(viewQuerySettings), - ImageTypeLimit: 1, - EnableImageTypes: 'Primary,Backdrop,Banner,Thumb,Disc,Logo', - Limit: userSettings.libraryPageSize(undefined) || undefined, - IsFavorite: getBasekey() === 'favorites' ? true : null, - VideoTypes: viewQuerySettings.VideoTypes, - GenreIds: viewQuerySettings.GenreIds, - Is4K: viewQuerySettings.Is4K ? true : null, - IsHD: queryIsHD, - Is3D: viewQuerySettings.Is3D ? true : null, - HasSubtitles: viewQuerySettings.HasSubtitles ? true : null, - HasTrailer: viewQuerySettings.HasTrailer ? true : null, - HasSpecialFeature: viewQuerySettings.HasSpecialFeature ? true : null, - HasThemeSong: viewQuerySettings.HasThemeSong ? true : null, - HasThemeVideo: viewQuerySettings.HasThemeVideo ? true : null, - Filters: queryFilters.length ? queryFilters.join(',') : null, - StartIndex: viewQuerySettings.StartIndex, - NameLessThan: viewQuerySettings.NameLessThan, - NameStartsWith: viewQuerySettings.NameStartsWith, - ParentId: topParentId - }; - }, [ - viewQuerySettings, - getItemTypes, - getBasekey, - topParentId - ]); - - const fetchData = useCallback(() => { - loading.show(); - - const apiClient = ServerConnections.getApiClient(window.ApiClient.serverId()); - return apiClient.getItems( - apiClient.getCurrentUserId(), - { - ...getQuery() - } - ); - }, [getQuery]); - - const reloadItems = useCallback(() => { - const page = element.current; - - if (!page) { - console.error('Unexpected null reference'); - return; - } - setisLoading(false); - fetchData().then((result) => { - setItemsResult(result); - - window.scrollTo(0, 0); - - import('../../components/autoFocuser').then(({ default: autoFocuser }) => { - autoFocuser.autoFocus(page); - }).catch(err => { - console.error('[ViewItemsContainer] failed to load autofocuser', err); - }); - loading.hide(); - setisLoading(true); - }).catch(err => { - console.error('[ViewItemsContainer] failed to fetch data', err); - }); - }, [fetchData]); - - useEffect(() => { - reloadItems(); - }, [reloadItems]); - - return ( -
-
- - - {isBtnShuffleEnabled && } - - - - - - {isBtnFilterEnabled && } - - {isBtnNewCollectionEnabled && } - -
- - {isAlphaPickerEnabled && } - - {isLoading && } - -
- -
-
- ); -}; - -export default ViewItemsContainer; diff --git a/src/hooks/useFetchItems.ts b/src/hooks/useFetchItems.ts index fcc2b2125c49..5ed41891e765 100644 --- a/src/hooks/useFetchItems.ts +++ b/src/hooks/useFetchItems.ts @@ -7,19 +7,25 @@ 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 { getArtistsApi } from '@jellyfin/sdk/lib/utils/api/artists-api'; +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'; import { JellyfinApiContext, useApi } from './useApi'; import { Sections, SectionsViewType } from 'types/suggestionsSections'; +import { LibraryViewSettings } from 'types/library'; +import { LibraryTab } from 'types/libraryTab'; +import { getAlphaPickerQuery, getEnableImageTypesQuery, getFieldsQuery, getFiltersQuery, getIsFavoriteQuery, getLimitQuery } from 'utils/items'; const fetchGetItem = async ( currentApi: JellyfinApiContext, - parentId?: string | null, + parentId: string | null | undefined, options?: AxiosRequestConfig ) => { const { api, user } = currentApi; @@ -37,7 +43,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 +89,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 +114,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 +127,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 +240,7 @@ const fetchGetItemsBySuggestionsType = async ( export const useGetItemsBySectionType = ( sections: Sections, - parentId?: string | null + parentId: string | null | undefined ) => { const currentApi = useApi(); return useQuery({ @@ -253,7 +259,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 +281,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 +290,218 @@ 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 fetchGetItemsByViewType = async ( + currentApi: JellyfinApiContext, + viewType: LibraryTab, + parentId: string | null | undefined, + itemType: BaseItemKind, + libraryViewSettings: LibraryViewSettings, + options?: AxiosRequestConfig +) => { + const { api, user } = currentApi; + if (api && user?.Id) { + let response; + switch (viewType) { + case LibraryTab.AlbumArtists: { + response = await getArtistsApi(api).getAlbumArtists( + { + userId: user.Id, + parentId: parentId ?? undefined, + ...getFieldsQuery(viewType, libraryViewSettings), + ...getEnableImageTypesQuery(libraryViewSettings), + ...getFiltersQuery(viewType, libraryViewSettings), + ...getLimitQuery(), + ...getAlphaPickerQuery(libraryViewSettings), + sortBy: [libraryViewSettings.SortBy], + sortOrder: [libraryViewSettings.SortOrder], + includeItemTypes: [itemType], + startIndex: libraryViewSettings.StartIndex + }, + { + signal: options?.signal + } + ); + break; + } + case LibraryTab.Artists: { + response = await getArtistsApi(api).getArtists( + { + userId: user.Id, + parentId: parentId ?? undefined, + ...getFieldsQuery(viewType, libraryViewSettings), + ...getEnableImageTypesQuery(libraryViewSettings), + ...getFiltersQuery(viewType, libraryViewSettings), + ...getLimitQuery(), + ...getAlphaPickerQuery(libraryViewSettings), + sortBy: [libraryViewSettings.SortBy], + sortOrder: [libraryViewSettings.SortOrder], + includeItemTypes: [itemType], + startIndex: libraryViewSettings.StartIndex + }, + { + signal: options?.signal + } + ); + break; + } + case LibraryTab.Networks: + response = await getStudiosApi(api).getStudios( + { + userId: user.Id, + parentId: parentId ?? undefined, + ...getFieldsQuery(viewType, libraryViewSettings), + includeItemTypes: [itemType], + startIndex: libraryViewSettings.StartIndex + }, + { + signal: options?.signal + } + ); + break; + default: { + response = await getItemsApi(api).getItems( + { + userId: user.Id, + recursive: true, + imageTypeLimit: 1, + parentId: parentId ?? undefined, + ...getFieldsQuery(viewType, libraryViewSettings), + ...getEnableImageTypesQuery(libraryViewSettings), + ...getFiltersQuery(viewType, libraryViewSettings), + ...getLimitQuery(), + ...getIsFavoriteQuery(viewType), + ...getAlphaPickerQuery(libraryViewSettings), + sortBy: [libraryViewSettings.SortBy], + sortOrder: [libraryViewSettings.SortOrder], + includeItemTypes: [itemType], + startIndex: libraryViewSettings.StartIndex + }, + { + signal: options?.signal + } + ); + break; + } + } + return response.data; + } +}; + +export const useGetViewItemsByType = ( + viewType: LibraryTab, + parentId: string | null | undefined, + itemType: BaseItemKind, + libraryViewSettings: LibraryViewSettings +) => { + const currentApi = useApi(); + return useQuery({ + queryKey: [ + 'ViewItemsByType', + viewType, + parentId, + itemType, + libraryViewSettings + ], + queryFn: ({ signal }) => + fetchGetItemsByViewType( + currentApi, + viewType, + parentId, + itemType, + libraryViewSettings, + { signal } + ), + refetchOnWindowFocus: false, + enabled: + [ + LibraryTab.Movies, + LibraryTab.Favorites, + LibraryTab.Collections, + LibraryTab.Trailers, + LibraryTab.Series, + LibraryTab.Episodes, + LibraryTab.Networks, + LibraryTab.Albums, + LibraryTab.AlbumArtists, + LibraryTab.Artists, + LibraryTab.Playlists, + LibraryTab.Songs + ].includes(viewType) && !!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 5cc2db0ca64e..5ef2b71fe02a 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/cardOptions.ts b/src/types/cardOptions.ts index 7864ab315771..6589782936b8 100644 --- a/src/types/cardOptions.ts +++ b/src/types/cardOptions.ts @@ -1,4 +1,5 @@ import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client'; +import { CollectionType } from './collectionType'; export interface CardOptions { itemsContainer?: HTMLElement | null; @@ -32,7 +33,7 @@ export interface CardOptions { showUnplayedIndicator?: boolean; showChildCountIndicator?: boolean; lines?: number; - context?: string | null; + context?: CollectionType; action?: string | null; defaultShape?: string; indexBy?: string; diff --git a/src/types/interface.ts b/src/types/interface.ts deleted file mode 100644 index c577f84e2a31..000000000000 --- a/src/types/interface.ts +++ /dev/null @@ -1,29 +0,0 @@ -export interface ViewQuerySettings { - showTitle?: boolean; - showYear?: boolean; - imageType?: string; - viewType?: string; - cardLayout?: boolean; - SortBy?: string | null; - SortOrder?: string | null; - IsPlayed?: boolean | null; - IsUnplayed?: boolean | null; - IsFavorite?: boolean | null; - IsResumable?: boolean | null; - Is4K?: boolean | null; - IsHD?: boolean | null; - IsSD?: boolean | null; - Is3D?: boolean | null; - VideoTypes?: string | null; - SeriesStatus?: string | null; - HasSubtitles?: boolean | null; - HasTrailer?: boolean | null; - HasSpecialFeature?: boolean | null; - ParentIndexNumber?: boolean | null; - HasThemeSong?: boolean | null; - HasThemeVideo?: boolean | null; - GenreIds?: string | null; - NameLessThan?: string | null; - NameStartsWith?: string | null; - StartIndex?: number; -} diff --git a/src/types/library.ts b/src/types/library.ts index 1c04dae30e91..52e9293a675e 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; + parentId: string | null | undefined; +} + +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; } diff --git a/src/types/libraryTab.ts b/src/types/libraryTab.ts index 8ccf93663ed1..1484ed9646d7 100644 --- a/src/types/libraryTab.ts +++ b/src/types/libraryTab.ts @@ -19,5 +19,8 @@ export enum LibraryTab { Songs = 'songs', Suggestions = 'suggestions', Trailers = 'trailers', - Upcoming = 'upcoming' + Upcoming = 'upcoming', + Photos = 'photos', + Videos = 'videos', + Books = 'books', } diff --git a/src/utils/items.ts b/src/utils/items.ts new file mode 100644 index 000000000000..95ceef5310b1 --- /dev/null +++ b/src/utils/items.ts @@ -0,0 +1,207 @@ +import { ItemFields } from '@jellyfin/sdk/lib/generated-client/models/item-fields'; +import { ImageType } from '@jellyfin/sdk/lib/generated-client/models/image-type'; +import { ItemSortBy } from '@jellyfin/sdk/lib/models/api/item-sort-by'; +import { SortOrder } from '@jellyfin/sdk/lib/generated-client/models/sort-order'; +import * as userSettings from 'scripts/settings/userSettings'; +import { LibraryViewSettings } from '../types/library'; +import { LibraryTab } from 'types/libraryTab'; + +const getImageTypesEnum = (libraryViewSettings: LibraryViewSettings) => { + const imageTypes: ImageType[] = [ImageType.Backdrop]; + + if ( + libraryViewSettings.ImageType === 'primary' + || libraryViewSettings.ImageType === 'list' + ) { + imageTypes.push(ImageType.Primary); + } + + if (libraryViewSettings.ImageType === 'banner') { + imageTypes.push(ImageType.Banner); + } + + if (libraryViewSettings.ImageType === 'disc') { + imageTypes.push(ImageType.Disc); + } + + if (libraryViewSettings.ImageType === 'logo') { + imageTypes.push(ImageType.Logo); + } + + if (libraryViewSettings.ImageType === 'thumb') { + imageTypes.push(ImageType.Thumb); + } + + return imageTypes; +}; + +const getHasVideoTypes = (libraryViewSettings: LibraryViewSettings) => { + let isHd; + + if (libraryViewSettings?.IsHD) { + isHd = true; + } + + if (libraryViewSettings?.IsSD) { + isHd = false; + } + + return { + isHd, + is4K: libraryViewSettings?.Is4K ? true : undefined, + is3D: libraryViewSettings?.Is3D ? true : undefined + }; +}; + +const getHasFeatures = (libraryViewSettings: LibraryViewSettings) => { + return { + hasSubtitles: libraryViewSettings?.Filters?.Features?.includes('HasSubtitles') ? + true : + undefined, + hasTrailer: libraryViewSettings?.Filters?.Features?.includes('HasTrailer') ? + true : + undefined, + hasSpecialFeature: libraryViewSettings?.Filters?.Features?.includes( + 'HasSpecialFeature' + ) ? + true : + undefined, + hasThemeSong: libraryViewSettings?.Filters?.Features?.includes('HasThemeSong') ? + true : + undefined, + hasThemeVideo: libraryViewSettings?.Filters?.Features?.includes( + 'HasThemeVideo' + ) ? + true : + undefined + }; +}; + +const getFiltersEpisodesStatus = ( + viewType: LibraryTab, + libraryViewSettings: LibraryViewSettings +) => { + return { + parentIndexNumber: libraryViewSettings?.Filters?.EpisodesStatus?.includes( + 'ParentIndexNumber' + ) ? + 0 : + undefined, + isMissing: + viewType === LibraryTab.Episodes ? + !!libraryViewSettings?.Filters?.EpisodesStatus?.includes('IsMissing') : + undefined, + isUnaired: libraryViewSettings?.Filters?.EpisodesStatus?.includes('IsUnaired') ? + true : + undefined + }; +}; + +const getItemFieldsEnum = ( + viewType: LibraryTab, + libraryViewSettings: LibraryViewSettings +) => { + const itemFields: ItemFields[] = []; + + if (viewType !== LibraryTab.Networks) { + itemFields.push(ItemFields.BasicSyncInfo, ItemFields.MediaSourceCount); + } + + if (libraryViewSettings.ImageType === 'primary') { + itemFields.push(ItemFields.PrimaryImageAspectRatio); + } + + if (viewType === LibraryTab.Networks) { + itemFields.push( + ItemFields.DateCreated, + ItemFields.PrimaryImageAspectRatio + ); + } + + return itemFields; +}; + +export const getEnableImageTypesQuery = (libraryViewSettings: LibraryViewSettings) => { + return { + enableImageTypes: getImageTypesEnum(libraryViewSettings) + }; +}; + +export const getFieldsQuery = ( + viewType: LibraryTab, + libraryViewSettings: LibraryViewSettings +) => { + return { + fields: getItemFieldsEnum(viewType, libraryViewSettings) + }; +}; + +export const getIsFavoriteQuery = (viewType: LibraryTab) => { + return { + isFavorite: viewType === LibraryTab.Favorites ? true : undefined + }; +}; + +export const getLimitQuery = () => { + return { + limit: userSettings.libraryPageSize(undefined) || undefined + }; +}; + +export const getAlphaPickerQuery = (libraryViewSettings: LibraryViewSettings) => { + return { + nameLessThan: libraryViewSettings.NameLessThan !== null ? + libraryViewSettings.NameLessThan : undefined, + nameStartsWith: libraryViewSettings.NameStartsWith !== null ? + libraryViewSettings.NameStartsWith : undefined + }; +}; + +export const getFiltersQuery = ( + viewType: LibraryTab, + libraryViewSettings: LibraryViewSettings +) => { + return { + ...getHasFeatures(libraryViewSettings), + ...getFiltersEpisodesStatus(viewType, libraryViewSettings), + ...getHasVideoTypes(libraryViewSettings), + seriesStatus: libraryViewSettings?.Filters?.SeriesStatus, + videoTypes: libraryViewSettings?.Filters?.VideoTypes, + filters: libraryViewSettings?.Filters?.Status, + genres: libraryViewSettings?.Filters?.Genres, + officialRatings: libraryViewSettings?.Filters?.OfficialRatings, + tags: libraryViewSettings?.Filters?.Tags, + years: libraryViewSettings?.Filters?.Years, + studioIds: libraryViewSettings?.Filters?.StudioIds + }; +}; + +export const getSettingsKey = (viewType: LibraryTab, parentId: string | null | undefined ) => { + return `${viewType} - ${parentId}`; +}; + +export const getDefaultLibraryViewSettings = ( + viewType: LibraryTab +): LibraryViewSettings => { + let imageType; + let showYear; + + if (viewType === LibraryTab.Networks) { + imageType = 'thumb'; + showYear = false; + } else if (viewType === LibraryTab.Songs) { + imageType = 'list'; + } else { + imageType = 'primary'; + } + + return { + ShowTitle: true, + ShowYear: showYear, + ImageType: imageType, + CardLayout: false, + SortBy: ItemSortBy.SortName, + SortOrder: SortOrder.Ascending, + StartIndex: 0 + }; +};