1
0
Fork 0
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:
grafixeyehero 2024-09-23 02:53:44 +03:00 committed by Bill Thornton
parent c3e253d98d
commit 12995545b9
11 changed files with 257 additions and 183 deletions

View file

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

View file

@ -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,

View file

@ -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) {

View file

@ -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,

View file

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

View file

@ -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,

View file

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

View file

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

View 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;

View file

@ -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) {

View file

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