mirror of
https://github.com/jellyfin/jellyfin-web
synced 2025-03-30 19:56:21 +00:00
Refactoring Section Container
This commit is contained in:
parent
c3e253d98d
commit
12995545b9
11 changed files with 257 additions and 183 deletions
|
@ -2,7 +2,7 @@ import type { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/bas
|
||||||
import type { CollectionType } from '@jellyfin/sdk/lib/generated-client/models/collection-type';
|
import type { CollectionType } from '@jellyfin/sdk/lib/generated-client/models/collection-type';
|
||||||
import React, { FC } from 'react';
|
import React, { FC } from 'react';
|
||||||
import { useGetGenres } from 'hooks/useFetchItems';
|
import { useGetGenres } from 'hooks/useFetchItems';
|
||||||
import globalize from 'lib/globalize';
|
import NoItemsMessage from 'components/common/NoItemsMessage';
|
||||||
import Loading from 'components/loading/LoadingComponent';
|
import Loading from 'components/loading/LoadingComponent';
|
||||||
import GenresSectionContainer from './GenresSectionContainer';
|
import GenresSectionContainer from './GenresSectionContainer';
|
||||||
import type { ParentId } from 'types/library';
|
import type { ParentId } from 'types/library';
|
||||||
|
@ -25,27 +25,18 @@ const GenresItemsContainer: FC<GenresItemsContainerProps> = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!genresResult?.Items?.length) {
|
if (!genresResult?.Items?.length) {
|
||||||
return (
|
return <NoItemsMessage message='MessageNoGenresAvailable' />;
|
||||||
<div className='noItemsMessage centerMessage'>
|
|
||||||
<h1>{globalize.translate('MessageNothingHere')}</h1>
|
|
||||||
<p>{globalize.translate('MessageNoGenresAvailable')}</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return genresResult.Items.map((genre) => (
|
||||||
<>
|
<GenresSectionContainer
|
||||||
{genresResult.Items.map((genre) => (
|
key={genre.Id}
|
||||||
<GenresSectionContainer
|
collectionType={collectionType}
|
||||||
key={genre.Id}
|
parentId={parentId}
|
||||||
collectionType={collectionType}
|
itemType={itemType}
|
||||||
parentId={parentId}
|
genre={genre}
|
||||||
itemType={itemType}
|
/>
|
||||||
genre={genre}
|
));
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default GenresItemsContainer;
|
export default GenresItemsContainer;
|
||||||
|
|
|
@ -8,7 +8,7 @@ import React, { type FC } from 'react';
|
||||||
import { useGetItems } from 'hooks/useFetchItems';
|
import { useGetItems } from 'hooks/useFetchItems';
|
||||||
import Loading from 'components/loading/LoadingComponent';
|
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 'components/common/SectionContainer';
|
||||||
import { CardShape } from 'utils/card';
|
import { CardShape } from 'utils/card';
|
||||||
import type { ParentId } from 'types/library';
|
import type { ParentId } from 'types/library';
|
||||||
import type { ItemDto } from 'types/base/models/item-dto';
|
import type { ItemDto } from 'types/base/models/item-dto';
|
||||||
|
@ -59,9 +59,12 @@ const GenresSectionContainer: FC<GenresSectionContainerProps> = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
return <SectionContainer
|
return <SectionContainer
|
||||||
sectionTitle={genre.Name || ''}
|
key={genre.Name}
|
||||||
items={itemsResult?.Items || []}
|
sectionHeaderProps={{
|
||||||
url={getRouteUrl(genre)}
|
title: genre.Name || '',
|
||||||
|
url: getRouteUrl(genre)
|
||||||
|
}}
|
||||||
|
items={itemsResult?.Items}
|
||||||
cardOptions={{
|
cardOptions={{
|
||||||
scalable: true,
|
scalable: true,
|
||||||
overlayPlayButton: true,
|
overlayPlayButton: true,
|
||||||
|
|
|
@ -181,7 +181,7 @@ const ItemsView: FC<ItemsViewProps> = ({
|
||||||
|
|
||||||
const getItems = useCallback(() => {
|
const getItems = useCallback(() => {
|
||||||
if (!itemsResult?.Items?.length) {
|
if (!itemsResult?.Items?.length) {
|
||||||
return <NoItemsMessage noItemsMessage={noItemsMessage} />;
|
return <NoItemsMessage message={noItemsMessage} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (libraryViewSettings.ViewMode === ViewMode.ListView) {
|
if (libraryViewSettings.ViewMode === ViewMode.ListView) {
|
||||||
|
|
|
@ -3,7 +3,8 @@ import { useGetProgramsSectionsWithItems, useGetTimers } from 'hooks/useFetchIte
|
||||||
import { appRouter } from 'components/router/appRouter';
|
import { appRouter } from 'components/router/appRouter';
|
||||||
import globalize from 'lib/globalize';
|
import globalize from 'lib/globalize';
|
||||||
import Loading from 'components/loading/LoadingComponent';
|
import Loading from 'components/loading/LoadingComponent';
|
||||||
import SectionContainer from './SectionContainer';
|
import NoItemsMessage from 'components/common/NoItemsMessage';
|
||||||
|
import SectionContainer from 'components/common/SectionContainer';
|
||||||
import { CardShape } from 'utils/card';
|
import { CardShape } from 'utils/card';
|
||||||
import type { ParentId } from 'types/library';
|
import type { ParentId } from 'types/library';
|
||||||
import type { Section, SectionType } from 'types/sections';
|
import type { Section, SectionType } from 'types/sections';
|
||||||
|
@ -30,14 +31,7 @@ const ProgramsSectionView: FC<ProgramsSectionViewProps> = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!sectionsWithItems?.length && !upcomingRecordings?.length) {
|
if (!sectionsWithItems?.length && !upcomingRecordings?.length) {
|
||||||
return (
|
return <NoItemsMessage />;
|
||||||
<div className='noItemsMessage centerMessage'>
|
|
||||||
<h1>{globalize.translate('MessageNothingHere')}</h1>
|
|
||||||
<p>
|
|
||||||
{globalize.translate('MessageNoItemsAvailable')}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const getRouteUrl = (section: Section) => {
|
const getRouteUrl = (section: Section) => {
|
||||||
|
@ -58,23 +52,33 @@ const ProgramsSectionView: FC<ProgramsSectionViewProps> = ({
|
||||||
{sectionsWithItems?.map(({ section, items }) => (
|
{sectionsWithItems?.map(({ section, items }) => (
|
||||||
<SectionContainer
|
<SectionContainer
|
||||||
key={section.type}
|
key={section.type}
|
||||||
sectionTitle={globalize.translate(section.name)}
|
sectionHeaderProps={{
|
||||||
items={items ?? []}
|
title: globalize.translate(section.name),
|
||||||
url={getRouteUrl(section)}
|
url: getRouteUrl(section)
|
||||||
reloadItems={refetch}
|
}}
|
||||||
|
itemsContainerProps={{
|
||||||
|
queryKey: ['ProgramSectionWithItems'],
|
||||||
|
reloadItems: refetch
|
||||||
|
}}
|
||||||
|
items={items}
|
||||||
cardOptions={{
|
cardOptions={{
|
||||||
...section.cardOptions,
|
...section.cardOptions,
|
||||||
queryKey: ['ProgramSectionWithItems']
|
queryKey: ['ProgramSectionWithItems']
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{upcomingRecordings?.map((group) => (
|
{upcomingRecordings?.map((group) => (
|
||||||
<SectionContainer
|
<SectionContainer
|
||||||
key={group.name}
|
key={group.name}
|
||||||
sectionTitle={group.name}
|
sectionHeaderProps={{
|
||||||
items={group.timerInfo ?? []}
|
title: group.name
|
||||||
|
}}
|
||||||
|
itemsContainerProps={{
|
||||||
|
queryKey: ['Timers'],
|
||||||
|
reloadItems: refetch
|
||||||
|
}}
|
||||||
|
items={group.timerInfo }
|
||||||
cardOptions={{
|
cardOptions={{
|
||||||
queryKey: ['Timers'],
|
queryKey: ['Timers'],
|
||||||
shape: CardShape.BackdropOverflow,
|
shape: CardShape.BackdropOverflow,
|
||||||
|
|
|
@ -1,65 +0,0 @@
|
||||||
import React, { FC } from 'react';
|
|
||||||
|
|
||||||
import ItemsContainer from 'elements/emby-itemscontainer/ItemsContainer';
|
|
||||||
import Scroller from 'elements/emby-scroller/Scroller';
|
|
||||||
import LinkButton from 'elements/emby-button/LinkButton';
|
|
||||||
import Cards from 'components/cardbuilder/Card/Cards';
|
|
||||||
import type { CardOptions } from 'types/cardOptions';
|
|
||||||
import type { ItemDto } from 'types/base/models/item-dto';
|
|
||||||
|
|
||||||
interface SectionContainerProps {
|
|
||||||
url?: string;
|
|
||||||
sectionTitle: string;
|
|
||||||
items: ItemDto[];
|
|
||||||
cardOptions: CardOptions;
|
|
||||||
reloadItems?: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const SectionContainer: FC<SectionContainerProps> = ({
|
|
||||||
sectionTitle,
|
|
||||||
url,
|
|
||||||
items,
|
|
||||||
cardOptions,
|
|
||||||
reloadItems
|
|
||||||
}) => {
|
|
||||||
return (
|
|
||||||
<div className='verticalSection'>
|
|
||||||
<div className='sectionTitleContainer sectionTitleContainer-cards padded-left'>
|
|
||||||
{url && items.length > 5 ? (
|
|
||||||
<LinkButton
|
|
||||||
className='more button-flat button-flat-mini sectionTitleTextButton btnMoreFromGenre'
|
|
||||||
href={url}
|
|
||||||
>
|
|
||||||
<h2 className='sectionTitle sectionTitle-cards'>
|
|
||||||
{sectionTitle}
|
|
||||||
</h2>
|
|
||||||
<span
|
|
||||||
className='material-icons chevron_right'
|
|
||||||
aria-hidden='true'
|
|
||||||
></span>
|
|
||||||
</LinkButton>
|
|
||||||
) : (
|
|
||||||
<h2 className='sectionTitle sectionTitle-cards'>
|
|
||||||
{sectionTitle}
|
|
||||||
</h2>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Scroller
|
|
||||||
className='padded-top-focusscale padded-bottom-focusscale'
|
|
||||||
isMouseWheelEnabled={false}
|
|
||||||
isCenterFocusEnabled={true}
|
|
||||||
>
|
|
||||||
<ItemsContainer
|
|
||||||
className='itemsContainer scrollSlider focuscontainer-x'
|
|
||||||
reloadItems={reloadItems}
|
|
||||||
queryKey={cardOptions.queryKey}
|
|
||||||
>
|
|
||||||
<Cards items={items} cardOptions={cardOptions} />
|
|
||||||
</ItemsContainer>
|
|
||||||
</Scroller>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default SectionContainer;
|
|
|
@ -8,7 +8,8 @@ import {
|
||||||
import { appRouter } from 'components/router/appRouter';
|
import { appRouter } from 'components/router/appRouter';
|
||||||
import globalize from 'lib/globalize';
|
import globalize from 'lib/globalize';
|
||||||
import Loading from 'components/loading/LoadingComponent';
|
import Loading from 'components/loading/LoadingComponent';
|
||||||
import SectionContainer from './SectionContainer';
|
import NoItemsMessage from 'components/common/NoItemsMessage';
|
||||||
|
import SectionContainer from '../../../../components/common/SectionContainer';
|
||||||
import { CardShape } from 'utils/card';
|
import { CardShape } from 'utils/card';
|
||||||
import type { ParentId } from 'types/library';
|
import type { ParentId } from 'types/library';
|
||||||
import type { Section, SectionType } from 'types/sections';
|
import type { Section, SectionType } from 'types/sections';
|
||||||
|
@ -38,12 +39,7 @@ const SuggestionsSectionView: FC<SuggestionsSectionViewProps> = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!sectionsWithItems?.length && !movieRecommendationsItems?.length) {
|
if (!sectionsWithItems?.length && !movieRecommendationsItems?.length) {
|
||||||
return (
|
return <NoItemsMessage />;
|
||||||
<div className='noItemsMessage centerMessage'>
|
|
||||||
<h1>{globalize.translate('MessageNothingHere')}</h1>
|
|
||||||
<p>{globalize.translate('MessageNoItemsAvailable')}</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const getRouteUrl = (section: Section) => {
|
const getRouteUrl = (section: Section) => {
|
||||||
|
@ -96,9 +92,14 @@ const SuggestionsSectionView: FC<SuggestionsSectionViewProps> = ({
|
||||||
{sectionsWithItems?.map(({ section, items }) => (
|
{sectionsWithItems?.map(({ section, items }) => (
|
||||||
<SectionContainer
|
<SectionContainer
|
||||||
key={section.type}
|
key={section.type}
|
||||||
sectionTitle={globalize.translate(section.name)}
|
sectionHeaderProps={{
|
||||||
items={items ?? []}
|
title: globalize.translate(section.name),
|
||||||
url={getRouteUrl(section)}
|
url: getRouteUrl(section)
|
||||||
|
}}
|
||||||
|
itemsContainerProps={{
|
||||||
|
queryKey: ['SuggestionSectionWithItems']
|
||||||
|
}}
|
||||||
|
items={items}
|
||||||
cardOptions={{
|
cardOptions={{
|
||||||
...section.cardOptions,
|
...section.cardOptions,
|
||||||
queryKey: ['SuggestionSectionWithItems'],
|
queryKey: ['SuggestionSectionWithItems'],
|
||||||
|
@ -114,8 +115,13 @@ const SuggestionsSectionView: FC<SuggestionsSectionViewProps> = ({
|
||||||
<SectionContainer
|
<SectionContainer
|
||||||
// eslint-disable-next-line react/no-array-index-key
|
// eslint-disable-next-line react/no-array-index-key
|
||||||
key={`${recommendation.CategoryId}-${index}`} // use a unique id return value may have duplicate id
|
key={`${recommendation.CategoryId}-${index}`} // use a unique id return value may have duplicate id
|
||||||
sectionTitle={getRecommendationTittle(recommendation)}
|
sectionHeaderProps={{
|
||||||
items={(recommendation.Items as ItemDto[]) ?? []}
|
title: getRecommendationTittle(recommendation)
|
||||||
|
}}
|
||||||
|
itemsContainerProps={{
|
||||||
|
queryKey: ['MovieRecommendations']
|
||||||
|
}}
|
||||||
|
items={recommendation.Items as ItemDto[]}
|
||||||
cardOptions={{
|
cardOptions={{
|
||||||
queryKey: ['MovieRecommendations'],
|
queryKey: ['MovieRecommendations'],
|
||||||
shape: CardShape.PortraitOverflow,
|
shape: CardShape.PortraitOverflow,
|
||||||
|
|
|
@ -1,49 +1,44 @@
|
||||||
import React, { type FC } from 'react';
|
import React, { type FC } from 'react';
|
||||||
import Box from '@mui/material/Box';
|
|
||||||
import { useGetGroupsUpcomingEpisodes } from 'hooks/useFetchItems';
|
import { useGetGroupsUpcomingEpisodes } from 'hooks/useFetchItems';
|
||||||
import Loading from 'components/loading/LoadingComponent';
|
import Loading from 'components/loading/LoadingComponent';
|
||||||
import globalize from 'lib/globalize';
|
import NoItemsMessage from 'components/common/NoItemsMessage';
|
||||||
import SectionContainer from './SectionContainer';
|
import SectionContainer from 'components/common/SectionContainer';
|
||||||
import { CardShape } from 'utils/card';
|
import { CardShape } from 'utils/card';
|
||||||
import type { LibraryViewProps } from 'types/library';
|
import type { LibraryViewProps } from 'types/library';
|
||||||
|
|
||||||
const UpcomingView: FC<LibraryViewProps> = ({ parentId }) => {
|
const UpcomingView: FC<LibraryViewProps> = ({ parentId }) => {
|
||||||
const { isLoading, data: groupsUpcomingEpisodes } = useGetGroupsUpcomingEpisodes(parentId);
|
const { isLoading, data: groupsUpcomingEpisodes } =
|
||||||
|
useGetGroupsUpcomingEpisodes(parentId);
|
||||||
|
|
||||||
if (isLoading) return <Loading />;
|
if (isLoading) return <Loading />;
|
||||||
|
|
||||||
return (
|
if (!groupsUpcomingEpisodes?.length) {
|
||||||
<Box>
|
return <NoItemsMessage message='MessagePleaseEnsureInternetMetadata' />;
|
||||||
{!groupsUpcomingEpisodes?.length ? (
|
}
|
||||||
<div className='noItemsMessage centerMessage'>
|
|
||||||
<h1>{globalize.translate('MessageNothingHere')}</h1>
|
return groupsUpcomingEpisodes?.map((group) => (
|
||||||
<p>
|
<SectionContainer
|
||||||
{globalize.translate(
|
key={group.name}
|
||||||
'MessagePleaseEnsureInternetMetadata'
|
sectionHeaderProps={{
|
||||||
)}
|
title: group.name
|
||||||
</p>
|
}}
|
||||||
</div>
|
itemsContainerProps={{
|
||||||
) : (
|
queryKey: ['GroupsUpcomingEpisodes']
|
||||||
groupsUpcomingEpisodes?.map((group) => (
|
}}
|
||||||
<SectionContainer
|
items={group.items}
|
||||||
key={group.name}
|
cardOptions={{
|
||||||
sectionTitle={group.name}
|
shape: CardShape.BackdropOverflow,
|
||||||
items={group.items ?? []}
|
showLocationTypeIndicator: false,
|
||||||
cardOptions={{
|
showParentTitle: true,
|
||||||
shape: CardShape.BackdropOverflow,
|
preferThumb: true,
|
||||||
showLocationTypeIndicator: false,
|
lazy: true,
|
||||||
showParentTitle: true,
|
showDetailsMenu: true,
|
||||||
preferThumb: true,
|
missingIndicator: false,
|
||||||
lazy: true,
|
cardLayout: false,
|
||||||
showDetailsMenu: true,
|
queryKey: ['GroupsUpcomingEpisodes']
|
||||||
missingIndicator: false,
|
}}
|
||||||
cardLayout: false
|
/>
|
||||||
}}
|
));
|
||||||
/>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default UpcomingView;
|
export default UpcomingView;
|
||||||
|
|
|
@ -4,19 +4,19 @@ import Typography from '@mui/material/Typography';
|
||||||
import globalize from 'lib/globalize';
|
import globalize from 'lib/globalize';
|
||||||
|
|
||||||
interface NoItemsMessageProps {
|
interface NoItemsMessageProps {
|
||||||
noItemsMessage?: string;
|
message?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const NoItemsMessage: FC<NoItemsMessageProps> = ({
|
const NoItemsMessage: FC<NoItemsMessageProps> = ({
|
||||||
noItemsMessage = 'MessageNoItemsAvailable'
|
message = 'MessageNoItemsAvailable'
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<Box className='noItemsMessage centerMessage'>
|
<Box className='noItemsMessage centerMessage'>
|
||||||
<Typography variant='h2'>
|
<Typography variant='h1'>
|
||||||
{globalize.translate('MessageNothingHere')}
|
{globalize.translate('MessageNothingHere')}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography paragraph variant='h2'>
|
<Typography paragraph>
|
||||||
{globalize.translate(noItemsMessage)}
|
{globalize.translate(message)}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|
145
src/components/common/SectionContainer.tsx
Normal file
145
src/components/common/SectionContainer.tsx
Normal file
|
@ -0,0 +1,145 @@
|
||||||
|
import React, { type FC, type PropsWithChildren } from 'react';
|
||||||
|
import Box from '@mui/material/Box';
|
||||||
|
import Link from '@mui/material/Link';
|
||||||
|
import Typography from '@mui/material/Typography';
|
||||||
|
import ChevronRightIcon from '@mui/icons-material/ChevronRight';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import ItemsContainer, {
|
||||||
|
type ItemsContainerProps
|
||||||
|
} from 'elements/emby-itemscontainer/ItemsContainer';
|
||||||
|
import Scroller, { type ScrollerProps } from 'elements/emby-scroller/Scroller';
|
||||||
|
import Cards from 'components/cardbuilder/Card/Cards';
|
||||||
|
import Lists from 'components/listview/List/Lists';
|
||||||
|
import type { CardOptions } from 'types/cardOptions';
|
||||||
|
import type { ListOptions } from 'types/listOptions';
|
||||||
|
import type { ItemDto } from 'types/base/models/item-dto';
|
||||||
|
|
||||||
|
interface SectionHeaderProps {
|
||||||
|
className?: string;
|
||||||
|
itemsLength?: number;
|
||||||
|
url?: string;
|
||||||
|
title: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SectionHeader: FC<SectionHeaderProps> = ({
|
||||||
|
title,
|
||||||
|
className,
|
||||||
|
itemsLength = 0,
|
||||||
|
url
|
||||||
|
}) => {
|
||||||
|
const sectionHeaderClass = classNames(
|
||||||
|
'sectionTitleContainer sectionTitleContainer-cards',
|
||||||
|
'padded-left',
|
||||||
|
className
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box className={sectionHeaderClass}>
|
||||||
|
{url && itemsLength > 5 ? (
|
||||||
|
<Link
|
||||||
|
className='clearLink button-flat sectionTitleTextButton'
|
||||||
|
underline='none'
|
||||||
|
href={url}
|
||||||
|
>
|
||||||
|
<Typography
|
||||||
|
className='sectionTitle sectionTitle-cards'
|
||||||
|
variant='h2'
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</Typography>
|
||||||
|
<ChevronRightIcon sx={{ pt: '5px' }} />
|
||||||
|
</Link>
|
||||||
|
) : (
|
||||||
|
<Typography
|
||||||
|
className='sectionTitle sectionTitle-cards'
|
||||||
|
variant='h2'
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface SectionContainerProps {
|
||||||
|
className?: string;
|
||||||
|
items?: ItemDto[];
|
||||||
|
sectionHeaderProps?: Omit<SectionHeaderProps, 'itemsLength'>;
|
||||||
|
scrollerProps?: ScrollerProps;
|
||||||
|
itemsContainerProps?: ItemsContainerProps;
|
||||||
|
isListMode?: boolean;
|
||||||
|
isScrollerMode?: boolean;
|
||||||
|
noPadding?: boolean;
|
||||||
|
cardOptions?: CardOptions;
|
||||||
|
listOptions?: ListOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SectionContainer: FC<PropsWithChildren<SectionContainerProps>> = ({
|
||||||
|
className,
|
||||||
|
sectionHeaderProps,
|
||||||
|
scrollerProps,
|
||||||
|
itemsContainerProps,
|
||||||
|
isListMode = false,
|
||||||
|
isScrollerMode = true,
|
||||||
|
noPadding = false,
|
||||||
|
items = [],
|
||||||
|
cardOptions = {},
|
||||||
|
listOptions = {},
|
||||||
|
children
|
||||||
|
}) => {
|
||||||
|
const sectionClass = classNames('verticalSection', className);
|
||||||
|
|
||||||
|
const renderItems = () => {
|
||||||
|
if (React.isValidElement(children)) {
|
||||||
|
return children;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isListMode && !isScrollerMode) {
|
||||||
|
return <Lists items={items} listOptions={listOptions} />;
|
||||||
|
} else {
|
||||||
|
return <Cards items={items} cardOptions={cardOptions} />;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const content = (
|
||||||
|
<ItemsContainer
|
||||||
|
className={classNames(
|
||||||
|
{ scrollSlider: isScrollerMode },
|
||||||
|
itemsContainerProps?.className
|
||||||
|
)}
|
||||||
|
{...itemsContainerProps}
|
||||||
|
>
|
||||||
|
{renderItems()}
|
||||||
|
</ItemsContainer>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box className={sectionClass}>
|
||||||
|
{sectionHeaderProps?.title && (
|
||||||
|
<SectionHeader
|
||||||
|
className={classNames(
|
||||||
|
{ 'no-padding': noPadding },
|
||||||
|
sectionHeaderProps?.className
|
||||||
|
)}
|
||||||
|
itemsLength={items.length}
|
||||||
|
{...sectionHeaderProps}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{isScrollerMode && !isListMode ? (
|
||||||
|
<Scroller
|
||||||
|
className={classNames(
|
||||||
|
{ 'no-padding': noPadding },
|
||||||
|
scrollerProps?.className
|
||||||
|
)}
|
||||||
|
{...scrollerProps}
|
||||||
|
>
|
||||||
|
{content}
|
||||||
|
</Scroller>
|
||||||
|
) : (
|
||||||
|
content
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SectionContainer;
|
|
@ -36,7 +36,7 @@ function getShortcutOptions() {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ItemsContainerProps {
|
export interface ItemsContainerProps {
|
||||||
className?: string;
|
className?: string;
|
||||||
isContextMenuEnabled?: boolean;
|
isContextMenuEnabled?: boolean;
|
||||||
isMultiSelectEnabled?: boolean;
|
isMultiSelectEnabled?: boolean;
|
||||||
|
@ -136,14 +136,13 @@ const ItemsContainer: FC<PropsWithChildren<ItemsContainerProps>> = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!itemId) throw new Error('null itemId');
|
if (!itemId) throw new Error('null itemId');
|
||||||
if (!newIndex) throw new Error('null newIndex');
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
loading.show();
|
loading.show();
|
||||||
await playlistsMoveItemMutation({
|
await playlistsMoveItemMutation({
|
||||||
playlistId,
|
playlistId,
|
||||||
itemId,
|
itemId,
|
||||||
newIndex
|
newIndex: newIndex || 0
|
||||||
});
|
});
|
||||||
loading.hide();
|
loading.hide();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
|
@ -9,7 +9,7 @@ import ScrollerFactory from 'lib/scroller';
|
||||||
import ScrollButtons from '../emby-scrollbuttons/ScrollButtons';
|
import ScrollButtons from '../emby-scrollbuttons/ScrollButtons';
|
||||||
import './emby-scroller.scss';
|
import './emby-scroller.scss';
|
||||||
|
|
||||||
interface ScrollerProps {
|
export interface ScrollerProps {
|
||||||
className?: string;
|
className?: string;
|
||||||
isHorizontalEnabled?: boolean;
|
isHorizontalEnabled?: boolean;
|
||||||
isMouseWheelEnabled?: boolean;
|
isMouseWheelEnabled?: boolean;
|
||||||
|
@ -23,14 +23,14 @@ interface ScrollerProps {
|
||||||
|
|
||||||
const Scroller: FC<PropsWithChildren<ScrollerProps>> = ({
|
const Scroller: FC<PropsWithChildren<ScrollerProps>> = ({
|
||||||
className,
|
className,
|
||||||
isHorizontalEnabled,
|
isHorizontalEnabled = true,
|
||||||
isMouseWheelEnabled,
|
isMouseWheelEnabled = false,
|
||||||
isCenterFocusEnabled,
|
isCenterFocusEnabled = false,
|
||||||
isScrollButtonsEnabled,
|
isScrollButtonsEnabled = true,
|
||||||
isSkipFocusWhenVisibleEnabled,
|
isSkipFocusWhenVisibleEnabled = false,
|
||||||
isScrollEventEnabled,
|
isScrollEventEnabled = false,
|
||||||
isHideScrollbarEnabled,
|
isHideScrollbarEnabled = false,
|
||||||
isAllowNativeSmoothScrollEnabled,
|
isAllowNativeSmoothScrollEnabled = false,
|
||||||
children
|
children
|
||||||
}) => {
|
}) => {
|
||||||
const [scrollRef, size] = useElementSize();
|
const [scrollRef, size] = useElementSize();
|
||||||
|
@ -158,27 +158,23 @@ const Scroller: FC<PropsWithChildren<ScrollerProps>> = ({
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const horizontal = isHorizontalEnabled !== false;
|
const enableScrollButtons = layoutManager.desktop && isHorizontalEnabled && isScrollButtonsEnabled;
|
||||||
const scrollbuttons = isScrollButtonsEnabled !== false;
|
|
||||||
const mousewheel = isMouseWheelEnabled !== false;
|
|
||||||
|
|
||||||
const enableScrollButtons = layoutManager.desktop && horizontal && scrollbuttons;
|
|
||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
horizontal: horizontal,
|
horizontal: isHorizontalEnabled,
|
||||||
mouseDragging: 1,
|
mouseDragging: 1,
|
||||||
mouseWheel: mousewheel,
|
mouseWheel: isMouseWheelEnabled,
|
||||||
touchDragging: 1,
|
touchDragging: 1,
|
||||||
slidee: scrollRef.current?.querySelector('.scrollSlider'),
|
slidee: scrollRef.current?.querySelector('.scrollSlider'),
|
||||||
scrollBy: 200,
|
scrollBy: 200,
|
||||||
speed: horizontal ? 270 : 240,
|
speed: isHorizontalEnabled ? 270 : 240,
|
||||||
elasticBounds: 1,
|
elasticBounds: 1,
|
||||||
dragHandle: 1,
|
dragHandle: 1,
|
||||||
autoImmediate: true,
|
autoImmediate: true,
|
||||||
skipSlideToWhenVisible: isSkipFocusWhenVisibleEnabled === true,
|
skipSlideToWhenVisible: isSkipFocusWhenVisibleEnabled,
|
||||||
dispatchScrollEvent: enableScrollButtons || isScrollEventEnabled === true,
|
dispatchScrollEvent: enableScrollButtons || isScrollEventEnabled,
|
||||||
hideScrollbar: enableScrollButtons || isHideScrollbarEnabled === true,
|
hideScrollbar: enableScrollButtons || isHideScrollbarEnabled,
|
||||||
allowNativeSmoothScroll: isAllowNativeSmoothScrollEnabled === true && !enableScrollButtons,
|
allowNativeSmoothScroll: isAllowNativeSmoothScrollEnabled && !enableScrollButtons,
|
||||||
allowNativeScroll: !enableScrollButtons,
|
allowNativeScroll: !enableScrollButtons,
|
||||||
forceHideScrollbars: enableScrollButtons,
|
forceHideScrollbars: enableScrollButtons,
|
||||||
// In edge, with the native scroll, the content jumps around when hovering over the buttons
|
// In edge, with the native scroll, the content jumps around when hovering over the buttons
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue