1
0
Fork 0
mirror of https://github.com/jellyfin/jellyfin-web synced 2025-03-30 19:56:21 +00:00

Refactor: viewitemcontainer

This commit is contained in:
grafixeyehero 2023-10-04 23:14:14 +03:00 committed by Bill Thornton
parent 550ad476af
commit c61df2eb92
28 changed files with 520 additions and 1001 deletions

View file

@ -5,10 +5,11 @@ import globalize from 'scripts/globalize';
import Loading from 'components/loading/LoadingComponent'; import Loading from 'components/loading/LoadingComponent';
import GenresSectionContainer from './GenresSectionContainer'; import GenresSectionContainer from './GenresSectionContainer';
import { CollectionType } from 'types/collectionType'; import { CollectionType } from 'types/collectionType';
import { ParentId } from 'types/library';
interface GenresItemsContainerProps { interface GenresItemsContainerProps {
parentId?: string | null; parentId: ParentId;
collectionType?: CollectionType; collectionType: CollectionType;
itemType: BaseItemKind; itemType: BaseItemKind;
} }

View file

@ -12,10 +12,11 @@ import Loading from 'components/loading/LoadingComponent';
import { appRouter } from 'components/router/appRouter'; import { appRouter } from 'components/router/appRouter';
import SectionContainer from './SectionContainer'; import SectionContainer from './SectionContainer';
import { CollectionType } from 'types/collectionType'; import { CollectionType } from 'types/collectionType';
import { ParentId } from 'types/library';
interface GenresSectionContainerProps { interface GenresSectionContainerProps {
parentId?: string | null; parentId: ParentId;
collectionType?: CollectionType; collectionType: CollectionType;
itemType: BaseItemKind; itemType: BaseItemKind;
genre: BaseItemDto; genre: BaseItemDto;
} }

View file

@ -1,16 +1,16 @@
import React, { FC, useEffect, useRef } from 'react'; import React, { FC, useEffect, useRef } from 'react';
import ItemsContainerElement from '../../elements/ItemsContainerElement'; import ItemsContainerElement from 'elements/ItemsContainerElement';
import imageLoader from '../images/imageLoader'; import imageLoader from 'components/images/imageLoader';
import '../../elements/emby-itemscontainer/emby-itemscontainer'; import 'elements/emby-itemscontainer/emby-itemscontainer';
import { ViewQuerySettings } from '../../types/interface'; import { LibraryViewSettings, ViewMode } from 'types/library';
interface ItemsContainerI { interface ItemsContainerI {
viewQuerySettings: ViewQuerySettings; libraryViewSettings: LibraryViewSettings;
getItemsHtml: () => string getItemsHtml: () => string
} }
const ItemsContainer: FC<ItemsContainerI> = ({ viewQuerySettings, getItemsHtml }) => { const ItemsContainer: FC<ItemsContainerI> = ({ libraryViewSettings, getItemsHtml }) => {
const element = useRef<HTMLDivElement>(null); const element = useRef<HTMLDivElement>(null);
useEffect(() => { useEffect(() => {
@ -19,7 +19,7 @@ const ItemsContainer: FC<ItemsContainerI> = ({ viewQuerySettings, getItemsHtml }
imageLoader.lazyChildren(itemsContainer); imageLoader.lazyChildren(itemsContainer);
}, [getItemsHtml]); }, [getItemsHtml]);
const cssClass = viewQuerySettings.imageType == 'list' ? 'vertical-list' : 'vertical-wrap'; const cssClass = libraryViewSettings.ViewMode === ViewMode.ListView ? 'vertical-list' : 'vertical-wrap';
return ( return (
<div ref={element}> <div ref={element}>

View file

@ -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<ItemsViewProps> = ({
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<LibraryViewSettings>(
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 += '<div class="noItemsMessage centerMessage">';
html += '<h1>' + globalize.translate('MessageNothingHere') + '</h1>';
html += '<p>' + globalize.translate(noItemsMessage) + '</p>';
html += '</div>';
}
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 (
<Box>
<Box className='flex align-items-center justify-content-center flex-wrap-wrap padded-top padded-left padded-right padded-bottom focuscontainer-x'>
<Pagination
totalRecordCount={totalRecordCount}
libraryViewSettings={libraryViewSettings}
isPreviousData={isPreviousData}
setLibraryViewSettings={setLibraryViewSettings}
/>
{isBtnPlayAllEnabled && (
<PlayAllButton
item={item}
items={items}
viewType={viewType}
hasFilters={hasFilters}
libraryViewSettings={libraryViewSettings}
/>
)}
{isBtnQueueEnabled
&& item
&& playbackManager.canQueue(item) && (
<QueueButton
item={item}
items={items}
hasFilters={hasFilters}
/>
)}
{isBtnShuffleEnabled && totalRecordCount > 1 && (
<ShuffleButton
item={item}
items={items}
viewType={viewType}
hasFilters={hasFilters}
libraryViewSettings={libraryViewSettings}
/>
)}
{isBtnSortEnabled && (
<SortButton
viewType={viewType}
libraryViewSettings={libraryViewSettings}
setLibraryViewSettings={setLibraryViewSettings}
/>
)}
{isBtnFilterEnabled && (
<FilterButton
parentId={parentId}
itemType={itemType}
viewType={viewType}
hasFilters={hasFilters}
libraryViewSettings={libraryViewSettings}
setLibraryViewSettings={setLibraryViewSettings}
/>
)}
{isBtnNewCollectionEnabled && <NewCollectionButton />}
{isBtnGridListEnabled && (
<GridListViewButton
viewType={viewType}
libraryViewSettings={libraryViewSettings}
setLibraryViewSettings={setLibraryViewSettings}
/>
)}
</Box>
{isAlphabetPickerEnabled && hasSortName && (
<AlphabetPicker
libraryViewSettings={libraryViewSettings}
setLibraryViewSettings={setLibraryViewSettings}
/>
)}
{isLoading ? (
<Loading />
) : (
<ItemsContainer
libraryViewSettings={libraryViewSettings}
getItemsHtml={getItemsHtml}
/>
)}
<Box className='flex align-items-center justify-content-center flex-wrap-wrap padded-top padded-left padded-right padded-bottom focuscontainer-x'>
<Pagination
totalRecordCount={totalRecordCount}
libraryViewSettings={libraryViewSettings}
isPreviousData={isPreviousData}
setLibraryViewSettings={setLibraryViewSettings}
/>
</Box>
</Box>
);
};
export default ItemsView;

View file

@ -13,15 +13,17 @@ interface PaginationProps {
libraryViewSettings: LibraryViewSettings; libraryViewSettings: LibraryViewSettings;
setLibraryViewSettings: React.Dispatch<React.SetStateAction<LibraryViewSettings>>; setLibraryViewSettings: React.Dispatch<React.SetStateAction<LibraryViewSettings>>;
totalRecordCount: number; totalRecordCount: number;
isPreviousData: boolean
} }
const Pagination: FC<PaginationProps> = ({ const Pagination: FC<PaginationProps> = ({
libraryViewSettings, libraryViewSettings,
setLibraryViewSettings, setLibraryViewSettings,
totalRecordCount totalRecordCount,
isPreviousData
}) => { }) => {
const limit = userSettings.libraryPageSize(undefined); const limit = userSettings.libraryPageSize(undefined);
const startIndex = libraryViewSettings.StartIndex || 0; const startIndex = libraryViewSettings.StartIndex ?? 0;
const recordsStart = totalRecordCount ? startIndex + 1 : 0; const recordsStart = totalRecordCount ? startIndex + 1 : 0;
const recordsEnd = limit ? const recordsEnd = limit ?
Math.min(startIndex + limit, totalRecordCount) : Math.min(startIndex + limit, totalRecordCount) :
@ -29,23 +31,19 @@ const Pagination: FC<PaginationProps> = ({
const showControls = limit > 0 && limit < totalRecordCount; const showControls = limit > 0 && limit < totalRecordCount;
const onNextPageClick = useCallback(() => { const onNextPageClick = useCallback(() => {
if (limit > 0) { const newIndex = startIndex + limit;
const newIndex = startIndex + limit; setLibraryViewSettings((prevState) => ({
setLibraryViewSettings((prevState) => ({ ...prevState,
...prevState, StartIndex: newIndex
StartIndex: newIndex }));
}));
}
}, [limit, setLibraryViewSettings, startIndex]); }, [limit, setLibraryViewSettings, startIndex]);
const onPreviousPageClick = useCallback(() => { const onPreviousPageClick = useCallback(() => {
if (limit > 0) { const newIndex = Math.max(0, startIndex - limit);
const newIndex = Math.max(0, startIndex - limit); setLibraryViewSettings((prevState) => ({
setLibraryViewSettings((prevState) => ({ ...prevState,
...prevState, StartIndex: newIndex
StartIndex: newIndex }));
}));
}
}, [limit, setLibraryViewSettings, startIndex]); }, [limit, setLibraryViewSettings, startIndex]);
return ( return (
@ -67,7 +65,7 @@ const Pagination: FC<PaginationProps> = ({
<IconButton <IconButton
title={globalize.translate('Previous')} title={globalize.translate('Previous')}
className='paper-icon-button-light btnPreviousPage autoSize' className='paper-icon-button-light btnPreviousPage autoSize'
disabled={startIndex == 0} disabled={startIndex == 0 || isPreviousData}
onClick={onPreviousPageClick} onClick={onPreviousPageClick}
> >
<ArrowBackIcon /> <ArrowBackIcon />
@ -76,7 +74,7 @@ const Pagination: FC<PaginationProps> = ({
<IconButton <IconButton
title={globalize.translate('Next')} title={globalize.translate('Next')}
className='paper-icon-button-light btnNextPage autoSize' className='paper-icon-button-light btnNextPage autoSize'
disabled={startIndex + limit >= totalRecordCount } disabled={startIndex + limit >= totalRecordCount || isPreviousData }
onClick={onNextPageClick} onClick={onNextPageClick}
> >
<ArrowForwardIcon /> <ArrowForwardIcon />

View file

@ -49,7 +49,7 @@ const RecommendationContainer: FC<RecommendationContainerProps> = ({
return ( return (
<SectionContainer <SectionContainer
sectionTitle={escapeHTML(title)} sectionTitle={escapeHTML(title)}
items={recommendation.Items || []} items={recommendation.Items ?? []}
cardOptions={{ cardOptions={{
shape: 'overflowPortrait', shape: 'overflowPortrait',
showYear: true, showYear: true,

View file

@ -5,6 +5,7 @@ import React, { FC } from 'react';
import * as userSettings from 'scripts/settings/userSettings'; import * as userSettings from 'scripts/settings/userSettings';
import SuggestionsSectionContainer from './SuggestionsSectionContainer'; import SuggestionsSectionContainer from './SuggestionsSectionContainer';
import { Sections, SectionsView, SectionsViewType } from 'types/suggestionsSections'; import { Sections, SectionsView, SectionsViewType } from 'types/suggestionsSections';
import { ParentId } from 'types/library';
const getSuggestionsSections = (): Sections[] => { const getSuggestionsSections = (): Sections[] => {
return [ return [
@ -178,7 +179,7 @@ const getSuggestionsSections = (): Sections[] => {
}; };
interface SuggestionsItemsContainerProps { interface SuggestionsItemsContainerProps {
parentId?: string | null; parentId: ParentId;
sectionsView: SectionsView[]; sectionsView: SectionsView[];
} }

View file

@ -7,9 +7,10 @@ import { appRouter } from 'components/router/appRouter';
import SectionContainer from './SectionContainer'; import SectionContainer from './SectionContainer';
import { Sections } from 'types/suggestionsSections'; import { Sections } from 'types/suggestionsSections';
import { ParentId } from 'types/library';
interface SuggestionsSectionContainerProps { interface SuggestionsSectionContainerProps {
parentId?: string | null; parentId: ParentId;
section: Sections; section: Sections;
} }
@ -37,7 +38,7 @@ const SuggestionsSectionContainer: FC<SuggestionsSectionContainerProps> = ({
return ( return (
<SectionContainer <SectionContainer
sectionTitle={globalize.translate(section.name)} sectionTitle={globalize.translate(section.name)}
items={items || []} items={items ?? []}
url={getRouteUrl()} url={getRouteUrl()}
cardOptions={{ cardOptions={{
...section.cardOptions ...section.cardOptions

View file

@ -28,7 +28,7 @@ import FiltersTags from './FiltersTags';
import FiltersVideoTypes from './FiltersVideoTypes'; import FiltersVideoTypes from './FiltersVideoTypes';
import FiltersYears from './FiltersYears'; import FiltersYears from './FiltersYears';
import { LibraryViewSettings } from 'types/library'; import { LibraryViewSettings, ParentId } from 'types/library';
import { LibraryTab } from 'types/libraryTab'; import { LibraryTab } from 'types/libraryTab';
const Accordion = styled((props: AccordionProps) => ( const Accordion = styled((props: AccordionProps) => (
@ -73,9 +73,10 @@ const AccordionDetails = styled(MuiAccordionDetails)(({ theme }) => ({
})); }));
interface FilterButtonProps { interface FilterButtonProps {
parentId: string | null | undefined; parentId: ParentId;
itemType: BaseItemKind; itemType: BaseItemKind[];
viewType: LibraryTab; viewType: LibraryTab;
hasFilters: boolean;
libraryViewSettings: LibraryViewSettings; libraryViewSettings: LibraryViewSettings;
setLibraryViewSettings: React.Dispatch< setLibraryViewSettings: React.Dispatch<
React.SetStateAction<LibraryViewSettings> React.SetStateAction<LibraryViewSettings>
@ -86,6 +87,7 @@ const FilterButton: FC<FilterButtonProps> = ({
parentId, parentId,
itemType, itemType,
viewType, viewType,
hasFilters,
libraryViewSettings, libraryViewSettings,
setLibraryViewSettings setLibraryViewSettings
}) => { }) => {
@ -153,16 +155,13 @@ const FilterButton: FC<FilterButtonProps> = ({
return viewType === LibraryTab.Episodes; return viewType === LibraryTab.Episodes;
}; };
const hasFilters =
Object.values(libraryViewSettings.Filters || {}).some((filter) => !!filter);
return ( return (
<Box> <Box>
<IconButton <IconButton
title={globalize.translate('Filter')} title={globalize.translate('Filter')}
sx={{ ml: 2 }} sx={{ ml: 2 }}
aria-describedby={id} aria-describedby={id}
className='paper-icon-button-light btnShuffle autoSize' className='paper-icon-button-light btnFilter autoSize'
onClick={handleClick} onClick={handleClick}
> >
<Badge color='info' variant='dot' invisible={!hasFilters}> <Badge color='info' variant='dot' invisible={!hasFilters}>

View file

@ -27,7 +27,7 @@ type ControllerProps = {
const Home: FunctionComponent = () => { const Home: FunctionComponent = () => {
const [ searchParams ] = useSearchParams(); const [ searchParams ] = useSearchParams();
const initialTabIndex = parseInt(searchParams.get('tab') || '0', 10); const initialTabIndex = parseInt(searchParams.get('tab') ?? '0', 10);
const tabController = useRef<ControllerProps | null>(); const tabController = useRef<ControllerProps | null>();
const tabControllers = useMemo<ControllerProps[]>(() => [], []); const tabControllers = useMemo<ControllerProps[]>(() => [], []);

View file

@ -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 { LibraryViewProps } from 'types/library';
import { CollectionType } from 'types/collectionType';
import { LibraryTab } from 'types/libraryTab';
const CollectionsView: FC<LibraryViewProps> = ({ parentId }) => { const CollectionsView: FC<LibraryViewProps> = ({ parentId }) => {
const getBasekey = useCallback(() => {
return 'collections';
}, []);
const getItemTypes = useCallback(() => {
return ['BoxSet'];
}, []);
const getNoItemsMessage = useCallback(() => {
return 'MessageNoCollectionsAvailable';
}, []);
return ( return (
<ViewItemsContainer <ItemsView
topParentId={parentId} viewType={LibraryTab.Collections}
parentId={parentId}
collectionType={CollectionType.Movies}
isBtnFilterEnabled={false} isBtnFilterEnabled={false}
isBtnNewCollectionEnabled={true} isBtnNewCollectionEnabled={true}
isAlphaPickerEnabled={false} isAlphabetPickerEnabled={false}
getBasekey={getBasekey} itemType={[BaseItemKind.BoxSet]}
getItemTypes={getItemTypes} noItemsMessage='MessageNoCollectionsAvailable'
getNoItemsMessage={getNoItemsMessage}
/> />
); );
}; };

View file

@ -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 { LibraryViewProps } from 'types/library';
import { LibraryTab } from 'types/libraryTab';
const FavoritesView: FC<LibraryViewProps> = ({ parentId }) => { const FavoritesView: FC<LibraryViewProps> = ({ parentId }) => {
const getBasekey = useCallback(() => {
return 'favorites';
}, []);
const getItemTypes = useCallback(() => {
return ['Movie'];
}, []);
const getNoItemsMessage = useCallback(() => {
return 'MessageNoFavoritesAvailable';
}, []);
return ( return (
<ViewItemsContainer <ItemsView
topParentId={parentId} viewType={LibraryTab.Favorites}
getBasekey={getBasekey} parentId={parentId}
getItemTypes={getItemTypes} itemType={[BaseItemKind.Movie]}
getNoItemsMessage={getNoItemsMessage} noItemsMessage='MessageNoFavoritesAvailable'
/> />
); );
}; };

View file

@ -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 { LibraryViewProps } from 'types/library';
import { CollectionType } from 'types/collectionType';
import { LibraryTab } from 'types/libraryTab';
const MoviesView: FC<LibraryViewProps> = ({ parentId }) => { const MoviesView: FC<LibraryViewProps> = ({ parentId }) => {
const getBasekey = useCallback(() => {
return 'movies';
}, []);
const getItemTypes = useCallback(() => {
return ['Movie'];
}, []);
const getNoItemsMessage = useCallback(() => {
return 'MessageNoItemsAvailable';
}, []);
return ( return (
<ViewItemsContainer <ItemsView
topParentId={parentId} viewType={LibraryTab.Movies}
parentId={parentId}
collectionType={CollectionType.Movies}
isBtnShuffleEnabled={true} isBtnShuffleEnabled={true}
getBasekey={getBasekey} itemType={[BaseItemKind.Movie]}
getItemTypes={getItemTypes} noItemsMessage='MessageNoItemsAvailable'
getNoItemsMessage={getNoItemsMessage}
/> />
); );
}; };

View file

@ -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 ItemsView from '../../components/library/ItemsView';
import ViewItemsContainer from 'components/common/ViewItemsContainer';
import { LibraryViewProps } from 'types/library'; import { LibraryViewProps } from 'types/library';
import { LibraryTab } from 'types/libraryTab';
const TrailersView: FC<LibraryViewProps> = ({ parentId }) => { const TrailersView: FC<LibraryViewProps> = ({ parentId }) => {
const getBasekey = useCallback(() => {
return 'trailers';
}, []);
const getItemTypes = useCallback(() => {
return ['Trailer'];
}, []);
const getNoItemsMessage = useCallback(() => {
return 'MessageNoTrailersFound';
}, []);
return ( return (
<ViewItemsContainer <ItemsView
topParentId={parentId} viewType={LibraryTab.Trailers}
getBasekey={getBasekey} parentId={parentId}
getItemTypes={getItemTypes} itemType={[BaseItemKind.Trailer]}
getNoItemsMessage={getNoItemsMessage} noItemsMessage='MessageNoTrailersFound'
/> />
); );
}; };

View file

@ -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 React, { FC } from 'react';
import { useLocation, useSearchParams } from 'react-router-dom'; import { useLocation, useSearchParams } from 'react-router-dom';
import Page from 'components/Page';
import { getDefaultTabIndex } from '../../components/tabs/tabRoutes'; import { getDefaultTabIndex } from '../../components/tabs/tabRoutes';
import Page from 'components/Page';
import CollectionsView from './CollectionsView'; import CollectionsView from './CollectionsView';
import FavoritesView from './FavoritesView'; import FavoritesView from './FavoritesView';
import GenresView from './GenresView'; import GenresView from './GenresView';

View file

@ -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<React.SetStateAction<ViewQuerySettings>>;
}
const AlphaPickerContainer: FC<AlphaPickerContainerProps> = ({ viewQuerySettings, setViewQuerySettings }) => {
const [ alphaPicker, setAlphaPicker ] = useState<AlphaPicker>();
const element = useRef<HTMLDivElement>(null);
alphaPicker?.updateControls(viewQuerySettings);
const onAlphaPickerChange = useCallback((e) => {
const newValue = (e as CustomEvent).detail.value;
let updatedValue: React.SetStateAction<ViewQuerySettings>;
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 (
<div ref={element} className='alphaPicker alphaPicker-fixed alphaPicker-fixed-right alphaPicker-vertical alphabetPicker-right' />
);
};
export default AlphaPickerContainer;

View file

@ -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<string, never>;
getVisibleFilters: () => string[];
viewQuerySettings: ViewQuerySettings;
setViewQuerySettings: React.Dispatch<React.SetStateAction<ViewQuerySettings>>;
}
const Filter: FC<FilterProps> = ({
topParentId,
getItemTypes,
getVisibleFilters,
getFilterMenuOptions,
viewQuerySettings,
setViewQuerySettings
}) => {
const element = useRef<HTMLDivElement>(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 (
<div ref={element}>
<IconButtonElement
is='paper-icon-button-light'
className='btnFilter autoSize'
title='Filter'
icon='material-icons filter_list'
/>
</div>
);
};
export default Filter;

View file

@ -1,42 +0,0 @@
import React, { FC, useCallback, useEffect, useRef } from 'react';
import IconButtonElement from '../../elements/IconButtonElement';
const NewCollection: FC = () => {
const element = useRef<HTMLDivElement>(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 (
<div ref={element}>
<IconButtonElement
is='paper-icon-button-light'
className='btnNewCollection autoSize'
title='Add'
icon='material-icons add'
/>
</div>
);
};
export default NewCollection;

View file

@ -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<React.SetStateAction<ViewQuerySettings>>;
itemsResult?: BaseItemDtoQueryResult;
}
const Pagination: FC<PaginationProps> = ({ 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<HTMLDivElement>(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 (
<div ref={element}>
<div className='paging'>
<div className='listPaging' style={{ display: 'flex', alignItems: 'center' }}>
<span>
{globalize.translate('ListPaging', recordsStart, recordsEnd, totalRecordCount)}
</span>
{showControls && (
<>
<IconButtonElement
is='paper-icon-button-light'
className='btnPreviousPage autoSize'
icon='material-icons arrow_back'
/>
<IconButtonElement
is='paper-icon-button-light'
className='btnNextPage autoSize'
icon='material-icons arrow_forward'
/>
</>
)}
</div>
</div>
</div>
);
};
export default Pagination;

View file

@ -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<React.SetStateAction<ViewQuerySettings>>;
}
const SelectView: FC<SelectViewProps> = ({
getVisibleViewSettings,
viewQuerySettings,
setViewQuerySettings
}) => {
const element = useRef<HTMLDivElement>(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 (
<div ref={element}>
<IconButtonElement
is='paper-icon-button-light'
className='btnSelectView autoSize'
title='ButtonSelectView'
icon='material-icons view_comfy'
/>
</div>
);
};
export default SelectView;

View file

@ -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<ShuffleProps> = ({ itemsResult = {}, topParentId }) => {
const element = useRef<HTMLDivElement>(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 (
<div ref={element}>
<IconButtonElement
is='paper-icon-button-light'
className='btnShuffle autoSize'
title='Shuffle'
icon='material-icons shuffle'
/>
</div>
);
};
export default Shuffle;

View file

@ -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<React.SetStateAction<ViewQuerySettings>>;
}
const Sort: FC<SortProps> = ({
getSortMenuOptions,
viewQuerySettings,
setViewQuerySettings
}) => {
const element = useRef<HTMLDivElement>(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 (
<div ref={element}>
<IconButtonElement
is='paper-icon-button-light'
className='btnSort autoSize'
title='Sort'
icon='material-icons sort_by_alpha'
/>
</div>
);
};
export default Sort;

View file

@ -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<ViewItemsContainerProps> = ({
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>(
`viewQuerySettings - ${getSettingsKey()}`,
defaultViewQuerySettings
);
const [ itemsResult, setItemsResult ] = useState<BaseItemDtoQueryResult>({});
const element = useRef<HTMLDivElement>(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 += '<div class="noItemsMessage centerMessage">';
html += '<h1>' + globalize.translate('MessageNothingHere') + '</h1>';
html += '<p>' + globalize.translate(getNoItemsMessage()) + '</p>';
html += '</div>';
}
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 (
<div ref={element}>
<div className='flex align-items-center justify-content-center flex-wrap-wrap padded-top padded-left padded-right padded-bottom focuscontainer-x'>
<Pagination
itemsResult= {itemsResult}
viewQuerySettings={viewQuerySettings}
setViewQuerySettings={setViewQuerySettings}
/>
{isBtnShuffleEnabled && <Shuffle itemsResult={itemsResult} topParentId={topParentId} />}
<SelectView
getVisibleViewSettings={getVisibleViewSettings}
viewQuerySettings={viewQuerySettings}
setViewQuerySettings={setViewQuerySettings}
/>
<Sort
getSortMenuOptions={getSortMenuOptions}
viewQuerySettings={viewQuerySettings}
setViewQuerySettings={setViewQuerySettings}
/>
{isBtnFilterEnabled && <Filter
topParentId={topParentId}
getItemTypes={getItemTypes}
getVisibleFilters={getVisibleFilters}
getFilterMenuOptions={getFilterMenuOptions}
viewQuerySettings={viewQuerySettings}
setViewQuerySettings={setViewQuerySettings}
/>}
{isBtnNewCollectionEnabled && <NewCollection />}
</div>
{isAlphaPickerEnabled && <AlphaPickerContainer
viewQuerySettings={viewQuerySettings}
setViewQuerySettings={setViewQuerySettings}
/>}
{isLoading && <ItemsContainer
viewQuerySettings={viewQuerySettings}
getItemsHtml={getItemsHtml}
/>}
<div className='flex align-items-center justify-content-center flex-wrap-wrap padded-top padded-left padded-right padded-bottom focuscontainer-x'>
<Pagination
itemsResult= {itemsResult}
viewQuerySettings={viewQuerySettings}
setViewQuerySettings={setViewQuerySettings}
/>
</div>
</div>
);
};
export default ViewItemsContainer;

View file

@ -1,12 +1,11 @@
import type { ItemsApiGetItemsRequest } from '@jellyfin/sdk/lib/generated-client'; import type { ItemsApiGetItemsRequest } from '@jellyfin/sdk/lib/generated-client';
import { AxiosRequestConfig } from 'axios'; import type { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind';
import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind';
import { ImageType } from '@jellyfin/sdk/lib/generated-client/models/image-type'; import { ImageType } from '@jellyfin/sdk/lib/generated-client/models/image-type';
import { ItemFields } from '@jellyfin/sdk/lib/generated-client/models/item-fields'; import { ItemFields } from '@jellyfin/sdk/lib/generated-client/models/item-fields';
import { ItemFilter } from '@jellyfin/sdk/lib/generated-client/models/item-filter'; import { ItemFilter } from '@jellyfin/sdk/lib/generated-client/models/item-filter';
import { SortOrder } from '@jellyfin/sdk/lib/generated-client/models/sort-order'; import { SortOrder } from '@jellyfin/sdk/lib/generated-client/models/sort-order';
import { ItemSortBy } from '@jellyfin/sdk/lib/models/api/item-sort-by'; 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 { getFilterApi } from '@jellyfin/sdk/lib/utils/api/filter-api';
import { getGenresApi } from '@jellyfin/sdk/lib/utils/api/genres-api'; import { getGenresApi } from '@jellyfin/sdk/lib/utils/api/genres-api';
import { getItemsApi } from '@jellyfin/sdk/lib/utils/api/items-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 { getStudiosApi } from '@jellyfin/sdk/lib/utils/api/studios-api';
import { getTvShowsApi } from '@jellyfin/sdk/lib/utils/api/tv-shows-api'; import { getTvShowsApi } from '@jellyfin/sdk/lib/utils/api/tv-shows-api';
import { getUserLibraryApi } from '@jellyfin/sdk/lib/utils/api/user-library-api'; import { getUserLibraryApi } from '@jellyfin/sdk/lib/utils/api/user-library-api';
import { AxiosRequestConfig } from 'axios';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { JellyfinApiContext, useApi } from './useApi'; import { JellyfinApiContext, useApi } from './useApi';
import { getAlphaPickerQuery, getFieldsQuery, getFiltersQuery, getLimitQuery } from 'utils/items';
import { Sections, SectionsViewType } from 'types/suggestionsSections'; 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 ( const fetchGetItem = async (
currentApi: JellyfinApiContext, currentApi: JellyfinApiContext,
@ -291,7 +293,7 @@ export const useGetGenres = (itemType: BaseItemKind, parentId: ParentId) => {
const fetchGetStudios = async ( const fetchGetStudios = async (
currentApi: JellyfinApiContext, currentApi: JellyfinApiContext,
parentId: ParentId, parentId: ParentId,
itemType: BaseItemKind, itemType: BaseItemKind[],
options?: AxiosRequestConfig options?: AxiosRequestConfig
) => { ) => {
const { api, user } = currentApi; const { api, user } = currentApi;
@ -299,7 +301,7 @@ const fetchGetStudios = async (
const response = await getStudiosApi(api).getStudios( const response = await getStudiosApi(api).getStudios(
{ {
userId: user.Id, userId: user.Id,
includeItemTypes: [itemType], includeItemTypes: itemType,
fields: [ fields: [
ItemFields.DateCreated, ItemFields.DateCreated,
ItemFields.PrimaryImageAspectRatio 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(); const currentApi = useApi();
return useQuery({ return useQuery({
queryKey: ['Studios', parentId, itemType], queryKey: ['Studios', parentId, itemType],
@ -329,7 +331,7 @@ export const useGetStudios = (parentId: ParentId, itemType: BaseItemKind) => {
const fetchGetQueryFiltersLegacy = async ( const fetchGetQueryFiltersLegacy = async (
currentApi: JellyfinApiContext, currentApi: JellyfinApiContext,
parentId: ParentId, parentId: ParentId,
itemType: BaseItemKind, itemType: BaseItemKind[],
options?: AxiosRequestConfig options?: AxiosRequestConfig
) => { ) => {
const { api, user } = currentApi; const { api, user } = currentApi;
@ -338,7 +340,7 @@ const fetchGetQueryFiltersLegacy = async (
{ {
userId: user.Id, userId: user.Id,
parentId: parentId ?? undefined, parentId: parentId ?? undefined,
includeItemTypes: [itemType] includeItemTypes: itemType
}, },
{ {
signal: options?.signal signal: options?.signal
@ -350,7 +352,7 @@ const fetchGetQueryFiltersLegacy = async (
export const useGetQueryFiltersLegacy = ( export const useGetQueryFiltersLegacy = (
parentId: ParentId, parentId: ParentId,
itemType: BaseItemKind itemType: BaseItemKind[]
) => { ) => {
const currentApi = useApi(); const currentApi = useApi();
return useQuery({ return useQuery({
@ -362,3 +364,148 @@ export const useGetQueryFiltersLegacy = (
enabled: !!parentId 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
});
};

View file

@ -1,4 +1,5 @@
import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client'; import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client';
import { CollectionType } from './collectionType';
export interface CardOptions { export interface CardOptions {
itemsContainer?: HTMLElement | null; itemsContainer?: HTMLElement | null;
@ -32,7 +33,7 @@ export interface CardOptions {
showUnplayedIndicator?: boolean; showUnplayedIndicator?: boolean;
showChildCountIndicator?: boolean; showChildCountIndicator?: boolean;
lines?: number; lines?: number;
context?: string | null; context?: CollectionType;
action?: string | null; action?: string | null;
defaultShape?: string; defaultShape?: string;
indexBy?: string; indexBy?: string;

View file

@ -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;
}

View file

@ -8,7 +8,7 @@ import { ItemSortBy } from '@jellyfin/sdk/lib/models/api/item-sort-by';
export type ParentId = string | null | undefined; export type ParentId = string | null | undefined;
export interface LibraryViewProps { export interface LibraryViewProps {
parentId: string | null; parentId: ParentId;
} }
export enum FeatureFilters { export enum FeatureFilters {

View file

@ -107,7 +107,7 @@ export const getFieldsQuery = (
export const getLimitQuery = () => { export const getLimitQuery = () => {
return { return {
limit: userSettings.libraryPageSize(undefined) || undefined limit: userSettings.libraryPageSize(undefined) ?? undefined
}; };
}; };
@ -144,12 +144,12 @@ export const getSettingsKey = (viewType: LibraryTab, parentId: ParentId) => {
return `${viewType} - ${parentId}`; return `${viewType} - ${parentId}`;
}; };
export const getDefaultLibraryViewSettings = (): LibraryViewSettings => { export const getDefaultLibraryViewSettings = (viewType: LibraryTab): LibraryViewSettings => {
return { return {
ShowTitle: true, ShowTitle: true,
ShowYear: false, ShowYear: false,
ViewMode: ViewMode.GridView, ViewMode: viewType === LibraryTab.Songs ? ViewMode.ListView : ViewMode.GridView,
ImageType: ImageType.Primary, ImageType: viewType === LibraryTab.Networks ? ImageType.Thumb : ImageType.Primary,
CardLayout: false, CardLayout: false,
SortBy: ItemSortBy.SortName, SortBy: ItemSortBy.SortName,
SortOrder: SortOrder.Ascending, SortOrder: SortOrder.Ascending,