diff --git a/src/apps/experimental/components/library/GenresItemsContainer.tsx b/src/apps/experimental/components/library/GenresItemsContainer.tsx index 41fba412d14..0348beb9435 100644 --- a/src/apps/experimental/components/library/GenresItemsContainer.tsx +++ b/src/apps/experimental/components/library/GenresItemsContainer.tsx @@ -5,10 +5,11 @@ import globalize from 'scripts/globalize'; import Loading from 'components/loading/LoadingComponent'; import GenresSectionContainer from './GenresSectionContainer'; import { CollectionType } from 'types/collectionType'; +import { ParentId } from 'types/library'; interface GenresItemsContainerProps { - parentId?: string | null; - collectionType?: CollectionType; + parentId: ParentId; + collectionType: CollectionType; itemType: BaseItemKind; } diff --git a/src/apps/experimental/components/library/GenresSectionContainer.tsx b/src/apps/experimental/components/library/GenresSectionContainer.tsx index 74f57782b19..ebfd20d33d6 100644 --- a/src/apps/experimental/components/library/GenresSectionContainer.tsx +++ b/src/apps/experimental/components/library/GenresSectionContainer.tsx @@ -12,10 +12,11 @@ import Loading from 'components/loading/LoadingComponent'; import { appRouter } from 'components/router/appRouter'; import SectionContainer from './SectionContainer'; import { CollectionType } from 'types/collectionType'; +import { ParentId } from 'types/library'; interface GenresSectionContainerProps { - parentId?: string | null; - collectionType?: CollectionType; + parentId: ParentId; + collectionType: CollectionType; itemType: BaseItemKind; genre: BaseItemDto; } diff --git a/src/components/common/ItemsContainer.tsx b/src/apps/experimental/components/library/ItemsContainer.tsx similarity index 58% rename from src/components/common/ItemsContainer.tsx rename to src/apps/experimental/components/library/ItemsContainer.tsx index 6289c1d8113..4c3b28c71c8 100644 --- a/src/components/common/ItemsContainer.tsx +++ b/src/apps/experimental/components/library/ItemsContainer.tsx @@ -1,16 +1,16 @@ 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'; +import ItemsContainerElement from 'elements/ItemsContainerElement'; +import imageLoader from 'components/images/imageLoader'; +import 'elements/emby-itemscontainer/emby-itemscontainer'; +import { LibraryViewSettings, ViewMode } from 'types/library'; interface ItemsContainerI { - viewQuerySettings: ViewQuerySettings; + libraryViewSettings: LibraryViewSettings; getItemsHtml: () => string } -const ItemsContainer: FC = ({ viewQuerySettings, getItemsHtml }) => { +const ItemsContainer: FC = ({ libraryViewSettings, getItemsHtml }) => { const element = useRef(null); useEffect(() => { @@ -19,7 +19,7 @@ const ItemsContainer: FC = ({ viewQuerySettings, getItemsHtml } imageLoader.lazyChildren(itemsContainer); }, [getItemsHtml]); - const cssClass = viewQuerySettings.imageType == 'list' ? 'vertical-list' : 'vertical-wrap'; + const cssClass = libraryViewSettings.ViewMode === ViewMode.ListView ? 'vertical-list' : 'vertical-wrap'; return (
diff --git a/src/apps/experimental/components/library/ItemsView.tsx b/src/apps/experimental/components/library/ItemsView.tsx new file mode 100644 index 00000000000..bdbb54fc774 --- /dev/null +++ b/src/apps/experimental/components/library/ItemsView.tsx @@ -0,0 +1,272 @@ +import type { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind'; +import { ImageType } from '@jellyfin/sdk/lib/generated-client'; +import { ItemSortBy } from '@jellyfin/sdk/lib/models/api/item-sort-by'; +import React, { FC, useCallback } from 'react'; +import Box from '@mui/material/Box'; +import { useLocalStorage } from 'hooks/useLocalStorage'; +import { useGetItem, useGetItemsViewByType } from 'hooks/useFetchItems'; +import { getDefaultLibraryViewSettings, getSettingsKey } from 'utils/items'; +import Loading from 'components/loading/LoadingComponent'; +import listview from 'components/listview/listview'; +import cardBuilder from 'components/cardbuilder/cardBuilder'; +import { playbackManager } from 'components/playback/playbackmanager'; +import globalize from 'scripts/globalize'; +import AlphabetPicker from './AlphabetPicker'; +import FilterButton from './filter/FilterButton'; +import ItemsContainer from './ItemsContainer'; +import NewCollectionButton from './NewCollectionButton'; +import Pagination from './Pagination'; +import PlayAllButton from './PlayAllButton'; +import QueueButton from './QueueButton'; +import ShuffleButton from './ShuffleButton'; +import SortButton from './SortButton'; +import GridListViewButton from './GridListViewButton'; +import { LibraryViewSettings, ParentId, ViewMode } from 'types/library'; +import { CollectionType } from 'types/collectionType'; +import { LibraryTab } from 'types/libraryTab'; + +import { CardOptions } from 'types/cardOptions'; + +interface ItemsViewProps { + viewType: LibraryTab; + parentId: ParentId; + itemType: BaseItemKind[]; + collectionType?: CollectionType; + isBtnPlayAllEnabled?: boolean; + isBtnQueueEnabled?: boolean; + isBtnShuffleEnabled?: boolean; + isBtnSortEnabled?: boolean; + isBtnFilterEnabled?: boolean; + isBtnNewCollectionEnabled?: boolean; + isBtnGridListEnabled?: boolean; + isAlphabetPickerEnabled?: boolean; + noItemsMessage: string; +} + +const ItemsView: FC = ({ + viewType, + parentId, + collectionType, + isBtnPlayAllEnabled = false, + isBtnQueueEnabled = false, + isBtnShuffleEnabled = false, + isBtnSortEnabled = true, + isBtnFilterEnabled = true, + isBtnNewCollectionEnabled = false, + isBtnGridListEnabled = true, + isAlphabetPickerEnabled = true, + itemType, + noItemsMessage +}) => { + const [libraryViewSettings, setLibraryViewSettings] = + useLocalStorage( + getSettingsKey(viewType, parentId), + getDefaultLibraryViewSettings(viewType) + ); + + const { + isLoading, + data: itemsResult, + isPreviousData + } = useGetItemsViewByType( + viewType, + parentId, + itemType, + libraryViewSettings + ); + const { data: item } = useGetItem(parentId); + + const getCardOptions = useCallback(() => { + let shape; + let preferThumb; + let preferDisc; + let preferLogo; + let lines = libraryViewSettings.ShowTitle ? 2 : 0; + + if (libraryViewSettings.ImageType === ImageType.Banner) { + shape = 'banner'; + } else if (libraryViewSettings.ImageType === ImageType.Disc) { + shape = 'square'; + preferDisc = true; + } else if (libraryViewSettings.ImageType === ImageType.Logo) { + shape = 'backdrop'; + preferLogo = true; + } else if (libraryViewSettings.ImageType === 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; + + return cardOptions; + }, [ + libraryViewSettings.ShowTitle, + libraryViewSettings.ImageType, + libraryViewSettings.ShowYear, + libraryViewSettings.CardLayout, + collectionType, + viewType + ]); + + const getItemsHtml = useCallback(() => { + let html = ''; + + if (libraryViewSettings.ViewMode === ViewMode.ListView) { + html = listview.getListViewHtml({ + items: itemsResult?.Items ?? [], + context: collectionType + }); + } else { + html = cardBuilder.getCardsHtml( + itemsResult?.Items ?? [], + getCardOptions() + ); + } + + if (!itemsResult?.Items?.length) { + html += '
'; + html += '

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

'; + html += '

' + globalize.translate(noItemsMessage) + '

'; + html += '
'; + } + + return html; + }, [ + libraryViewSettings.ViewMode, + itemsResult?.Items, + collectionType, + getCardOptions, + noItemsMessage + ]); + + const totalRecordCount = itemsResult?.TotalRecordCount ?? 0; + const items = itemsResult?.Items ?? []; + const hasFilters = Object.values(libraryViewSettings.Filters ?? {}).some( + (filter) => !!filter + ); + const hasSortName = libraryViewSettings.SortBy.includes( + ItemSortBy.SortName + ); + + return ( + + + + + {isBtnPlayAllEnabled && ( + + )} + {isBtnQueueEnabled + && item + && playbackManager.canQueue(item) && ( + + )} + {isBtnShuffleEnabled && totalRecordCount > 1 && ( + + )} + {isBtnSortEnabled && ( + + )} + {isBtnFilterEnabled && ( + + )} + {isBtnNewCollectionEnabled && } + {isBtnGridListEnabled && ( + + )} + + + {isAlphabetPickerEnabled && hasSortName && ( + + )} + + {isLoading ? ( + + ) : ( + + )} + + + + + + ); +}; + +export default ItemsView; diff --git a/src/apps/experimental/components/library/Pagination.tsx b/src/apps/experimental/components/library/Pagination.tsx index 3d1026254f4..1e6a2ab2619 100644 --- a/src/apps/experimental/components/library/Pagination.tsx +++ b/src/apps/experimental/components/library/Pagination.tsx @@ -13,15 +13,17 @@ interface PaginationProps { libraryViewSettings: LibraryViewSettings; setLibraryViewSettings: React.Dispatch>; totalRecordCount: number; + isPreviousData: boolean } const Pagination: FC = ({ libraryViewSettings, setLibraryViewSettings, - totalRecordCount + totalRecordCount, + isPreviousData }) => { const limit = userSettings.libraryPageSize(undefined); - const startIndex = libraryViewSettings.StartIndex || 0; + const startIndex = libraryViewSettings.StartIndex ?? 0; const recordsStart = totalRecordCount ? startIndex + 1 : 0; const recordsEnd = limit ? Math.min(startIndex + limit, totalRecordCount) : @@ -29,23 +31,19 @@ const Pagination: FC = ({ const showControls = limit > 0 && limit < totalRecordCount; const onNextPageClick = useCallback(() => { - if (limit > 0) { - const newIndex = startIndex + limit; - setLibraryViewSettings((prevState) => ({ - ...prevState, - StartIndex: newIndex - })); - } + 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 - })); - } + const newIndex = Math.max(0, startIndex - limit); + setLibraryViewSettings((prevState) => ({ + ...prevState, + StartIndex: newIndex + })); }, [limit, setLibraryViewSettings, startIndex]); return ( @@ -67,7 +65,7 @@ const Pagination: FC = ({ @@ -76,7 +74,7 @@ const Pagination: FC = ({ = totalRecordCount } + disabled={startIndex + limit >= totalRecordCount || isPreviousData } onClick={onNextPageClick} > diff --git a/src/apps/experimental/components/library/RecommendationContainer.tsx b/src/apps/experimental/components/library/RecommendationContainer.tsx index 4ec8102f57a..6c8ab5cc53e 100644 --- a/src/apps/experimental/components/library/RecommendationContainer.tsx +++ b/src/apps/experimental/components/library/RecommendationContainer.tsx @@ -49,7 +49,7 @@ const RecommendationContainer: FC = ({ return ( { return [ @@ -178,7 +179,7 @@ const getSuggestionsSections = (): Sections[] => { }; interface SuggestionsItemsContainerProps { - parentId?: string | null; + parentId: ParentId; sectionsView: SectionsView[]; } diff --git a/src/apps/experimental/components/library/SuggestionsSectionContainer.tsx b/src/apps/experimental/components/library/SuggestionsSectionContainer.tsx index 4c52d712e13..7c09bf489f3 100644 --- a/src/apps/experimental/components/library/SuggestionsSectionContainer.tsx +++ b/src/apps/experimental/components/library/SuggestionsSectionContainer.tsx @@ -7,9 +7,10 @@ import { appRouter } from 'components/router/appRouter'; import SectionContainer from './SectionContainer'; import { Sections } from 'types/suggestionsSections'; +import { ParentId } from 'types/library'; interface SuggestionsSectionContainerProps { - parentId?: string | null; + parentId: ParentId; section: Sections; } @@ -37,7 +38,7 @@ const SuggestionsSectionContainer: FC = ({ return ( ( @@ -73,9 +73,10 @@ const AccordionDetails = styled(MuiAccordionDetails)(({ theme }) => ({ })); interface FilterButtonProps { - parentId: string | null | undefined; - itemType: BaseItemKind; + parentId: ParentId; + itemType: BaseItemKind[]; viewType: LibraryTab; + hasFilters: boolean; libraryViewSettings: LibraryViewSettings; setLibraryViewSettings: React.Dispatch< React.SetStateAction @@ -86,6 +87,7 @@ const FilterButton: FC = ({ parentId, itemType, viewType, + hasFilters, libraryViewSettings, setLibraryViewSettings }) => { @@ -153,16 +155,13 @@ const FilterButton: FC = ({ return viewType === LibraryTab.Episodes; }; - const hasFilters = - Object.values(libraryViewSettings.Filters || {}).some((filter) => !!filter); - return ( diff --git a/src/apps/experimental/routes/home.tsx b/src/apps/experimental/routes/home.tsx index 3ab9c8bed7b..837db942b13 100644 --- a/src/apps/experimental/routes/home.tsx +++ b/src/apps/experimental/routes/home.tsx @@ -27,7 +27,7 @@ type ControllerProps = { const Home: FunctionComponent = () => { const [ searchParams ] = useSearchParams(); - const initialTabIndex = parseInt(searchParams.get('tab') || '0', 10); + const initialTabIndex = parseInt(searchParams.get('tab') ?? '0', 10); const tabController = useRef(); const tabControllers = useMemo(() => [], []); diff --git a/src/apps/experimental/routes/movies/CollectionsView.tsx b/src/apps/experimental/routes/movies/CollectionsView.tsx index ef574b916e5..82ce0155654 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 ItemsView from '../../components/library/ItemsView'; 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 d22cad6e385..7bb89edb15a 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 ItemsView from '../../components/library/ItemsView'; 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 8796c9a7111..b09f468db10 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 ItemsView from '../../components/library/ItemsView'; 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 ff0ff0e73e1..6acfd1c8ca5 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 ItemsView from '../../components/library/ItemsView'; 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 e1a30dfb593..8e74b7a3e47 100644 --- a/src/apps/experimental/routes/movies/index.tsx +++ b/src/apps/experimental/routes/movies/index.tsx @@ -1,13 +1,8 @@ -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 Page from 'components/Page'; import CollectionsView from './CollectionsView'; import FavoritesView from './FavoritesView'; import GenresView from './GenresView'; diff --git a/src/components/common/AlphaPickerContainer.tsx b/src/components/common/AlphaPickerContainer.tsx deleted file mode 100644 index 6b7c9a07183..00000000000 --- a/src/components/common/AlphaPickerContainer.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import React, { FC, useCallback, useEffect, useRef, useState } from 'react'; -import AlphaPicker from '../alphaPicker/alphaPicker'; -import { ViewQuerySettings } from '../../types/interface'; - -interface AlphaPickerContainerProps { - viewQuerySettings: ViewQuerySettings; - setViewQuerySettings: React.Dispatch>; -} - -const AlphaPickerContainer: FC = ({ viewQuerySettings, setViewQuerySettings }) => { - const [ alphaPicker, setAlphaPicker ] = useState(); - const element = useRef(null); - - alphaPicker?.updateControls(viewQuerySettings); - - 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) => ({ - ...prevState, - StartIndex: 0, - ...updatedValue - })); - }, [setViewQuerySettings]); - - useEffect(() => { - const alphaPickerElement = element.current; - - setAlphaPicker(new AlphaPicker({ - element: alphaPickerElement, - valueChangeEvent: 'click' - })); - - if (alphaPickerElement) { - alphaPickerElement.addEventListener('alphavaluechanged', onAlphaPickerChange); - } - - return () => { - alphaPickerElement?.removeEventListener('alphavaluechanged', onAlphaPickerChange); - }; - }, [onAlphaPickerChange]); - - return ( -
- ); -}; - -export default AlphaPickerContainer; diff --git a/src/components/common/Filter.tsx b/src/components/common/Filter.tsx deleted file mode 100644 index c3ccdd62f3a..00000000000 --- 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/NewCollection.tsx b/src/components/common/NewCollection.tsx deleted file mode 100644 index 837fe85fd3f..00000000000 --- 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 3dd5a60ffd3..00000000000 --- 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 bfb34555b87..00000000000 --- 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 093dc748744..00000000000 --- 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 db5cb899562..00000000000 --- 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 1ea5ef9899d..00000000000 --- 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 c0ed89b85c9..6ddd77a1207 100644 --- a/src/hooks/useFetchItems.ts +++ b/src/hooks/useFetchItems.ts @@ -1,12 +1,11 @@ import type { ItemsApiGetItemsRequest } from '@jellyfin/sdk/lib/generated-client'; -import { AxiosRequestConfig } from 'axios'; - -import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind'; +import type { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind'; import { ImageType } from '@jellyfin/sdk/lib/generated-client/models/image-type'; import { ItemFields } from '@jellyfin/sdk/lib/generated-client/models/item-fields'; 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'; @@ -14,11 +13,14 @@ 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 { AxiosRequestConfig } from 'axios'; import { useQuery } from '@tanstack/react-query'; import { JellyfinApiContext, useApi } from './useApi'; +import { getAlphaPickerQuery, getFieldsQuery, getFiltersQuery, getLimitQuery } from 'utils/items'; import { Sections, SectionsViewType } from 'types/suggestionsSections'; -import { ParentId } from 'types/library'; +import { LibraryViewSettings, ParentId } from 'types/library'; +import { LibraryTab } from 'types/libraryTab'; const fetchGetItem = async ( currentApi: JellyfinApiContext, @@ -291,7 +293,7 @@ export const useGetGenres = (itemType: BaseItemKind, parentId: ParentId) => { const fetchGetStudios = async ( currentApi: JellyfinApiContext, parentId: ParentId, - itemType: BaseItemKind, + itemType: BaseItemKind[], options?: AxiosRequestConfig ) => { const { api, user } = currentApi; @@ -299,7 +301,7 @@ const fetchGetStudios = async ( const response = await getStudiosApi(api).getStudios( { userId: user.Id, - includeItemTypes: [itemType], + includeItemTypes: itemType, fields: [ ItemFields.DateCreated, ItemFields.PrimaryImageAspectRatio @@ -316,7 +318,7 @@ const fetchGetStudios = async ( } }; -export const useGetStudios = (parentId: ParentId, itemType: BaseItemKind) => { +export const useGetStudios = (parentId: ParentId, itemType: BaseItemKind[]) => { const currentApi = useApi(); return useQuery({ queryKey: ['Studios', parentId, itemType], @@ -329,7 +331,7 @@ export const useGetStudios = (parentId: ParentId, itemType: BaseItemKind) => { const fetchGetQueryFiltersLegacy = async ( currentApi: JellyfinApiContext, parentId: ParentId, - itemType: BaseItemKind, + itemType: BaseItemKind[], options?: AxiosRequestConfig ) => { const { api, user } = currentApi; @@ -338,7 +340,7 @@ const fetchGetQueryFiltersLegacy = async ( { userId: user.Id, parentId: parentId ?? undefined, - includeItemTypes: [itemType] + includeItemTypes: itemType }, { signal: options?.signal @@ -350,7 +352,7 @@ const fetchGetQueryFiltersLegacy = async ( export const useGetQueryFiltersLegacy = ( parentId: ParentId, - itemType: BaseItemKind + itemType: BaseItemKind[] ) => { const currentApi = useApi(); return useQuery({ @@ -362,3 +364,148 @@ export const useGetQueryFiltersLegacy = ( enabled: !!parentId }); }; + +const fetchGetItemsViewByType = async ( + currentApi: JellyfinApiContext, + viewType: LibraryTab, + parentId: ParentId, + 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, + enableImageTypes: [libraryViewSettings.ImageType, ImageType.Backdrop], + ...getFieldsQuery(viewType, 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, + enableImageTypes: [libraryViewSettings.ImageType, ImageType.Backdrop], + ...getFieldsQuery(viewType, 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, + enableImageTypes: [ImageType.Thumb], + startIndex: libraryViewSettings.StartIndex + }, + { + signal: options?.signal + } + ); + break; + default: { + response = await getItemsApi(api).getItems( + { + userId: user.Id, + recursive: true, + imageTypeLimit: 1, + parentId: parentId ?? undefined, + enableImageTypes: [libraryViewSettings.ImageType, ImageType.Backdrop], + ...getFieldsQuery(viewType, libraryViewSettings), + ...getFiltersQuery(viewType, libraryViewSettings), + ...getLimitQuery(), + ...getAlphaPickerQuery(libraryViewSettings), + isFavorite: viewType === LibraryTab.Favorites ? true : undefined, + sortBy: [libraryViewSettings.SortBy], + sortOrder: [libraryViewSettings.SortOrder], + includeItemTypes: itemType, + startIndex: libraryViewSettings.StartIndex + }, + { + signal: options?.signal + } + ); + break; + } + } + return response.data; + } +}; + +export const useGetItemsViewByType = ( + viewType: LibraryTab, + parentId: ParentId, + itemType: BaseItemKind[], + libraryViewSettings: LibraryViewSettings +) => { + const currentApi = useApi(); + return useQuery({ + queryKey: [ + 'ItemsViewByType', + viewType, + parentId, + itemType, + libraryViewSettings + ], + queryFn: ({ signal }) => + fetchGetItemsViewByType( + currentApi, + viewType, + parentId, + itemType, + libraryViewSettings, + { signal } + ), + refetchOnWindowFocus: false, + keepPreviousData : true, + 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, + LibraryTab.Books, + LibraryTab.Photos, + LibraryTab.Videos + ].includes(viewType) && !!parentId + }); +}; diff --git a/src/types/cardOptions.ts b/src/types/cardOptions.ts index 7864ab31577..6589782936b 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 c577f84e2a3..00000000000 --- 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 862ee3d6d3b..37d0781cba0 100644 --- a/src/types/library.ts +++ b/src/types/library.ts @@ -8,7 +8,7 @@ import { ItemSortBy } from '@jellyfin/sdk/lib/models/api/item-sort-by'; export type ParentId = string | null | undefined; export interface LibraryViewProps { - parentId: string | null; + parentId: ParentId; } export enum FeatureFilters { diff --git a/src/utils/items.ts b/src/utils/items.ts index 08936a3fde0..b80c8ed057d 100644 --- a/src/utils/items.ts +++ b/src/utils/items.ts @@ -144,12 +144,12 @@ export const getSettingsKey = (viewType: LibraryTab, parentId: ParentId) => { return `${viewType} - ${parentId}`; }; -export const getDefaultLibraryViewSettings = (): LibraryViewSettings => { +export const getDefaultLibraryViewSettings = (viewType: LibraryTab): LibraryViewSettings => { return { ShowTitle: true, ShowYear: false, - ViewMode: ViewMode.GridView, - ImageType: ImageType.Primary, + ViewMode: viewType === LibraryTab.Songs ? ViewMode.ListView : ViewMode.GridView, + ImageType: viewType === LibraryTab.Networks ? ImageType.Thumb : ImageType.Primary, CardLayout: false, SortBy: ItemSortBy.SortName, SortOrder: SortOrder.Ascending,