From 9efc71fa3b2c8fea9fba98e8203b56b50fbccd4d Mon Sep 17 00:00:00 2001 From: grafixeyehero Date: Wed, 31 Jan 2024 04:20:42 +0300 Subject: [PATCH] Convert ListView to react --- src/components/listview/List/List.tsx | 32 ++++ src/components/listview/List/ListContent.tsx | 106 +++++++++++ .../listview/List/ListContentWrapper.tsx | 34 ++++ .../listview/List/ListGroupHeaderWrapper.tsx | 30 +++ .../listview/List/ListImageContainer.tsx | 103 +++++++++++ src/components/listview/List/ListItemBody.tsx | 65 +++++++ .../listview/List/ListTextWrapper.tsx | 30 +++ .../listview/List/ListViewUserDataButtons.tsx | 87 +++++++++ src/components/listview/List/ListWrapper.tsx | 49 +++++ src/components/listview/List/Lists.tsx | 57 ++++++ src/components/listview/List/listHelper.ts | 171 ++++++++++++++++++ src/components/listview/List/useList.ts | 77 ++++++++ .../listview/List/useListTextlines.tsx | 167 +++++++++++++++++ src/components/listview/listview.scss | 1 + 14 files changed, 1009 insertions(+) create mode 100644 src/components/listview/List/List.tsx create mode 100644 src/components/listview/List/ListContent.tsx create mode 100644 src/components/listview/List/ListContentWrapper.tsx create mode 100644 src/components/listview/List/ListGroupHeaderWrapper.tsx create mode 100644 src/components/listview/List/ListImageContainer.tsx create mode 100644 src/components/listview/List/ListItemBody.tsx create mode 100644 src/components/listview/List/ListTextWrapper.tsx create mode 100644 src/components/listview/List/ListViewUserDataButtons.tsx create mode 100644 src/components/listview/List/ListWrapper.tsx create mode 100644 src/components/listview/List/Lists.tsx create mode 100644 src/components/listview/List/listHelper.ts create mode 100644 src/components/listview/List/useList.ts create mode 100644 src/components/listview/List/useListTextlines.tsx diff --git a/src/components/listview/List/List.tsx b/src/components/listview/List/List.tsx new file mode 100644 index 0000000000..995c057526 --- /dev/null +++ b/src/components/listview/List/List.tsx @@ -0,0 +1,32 @@ +import React, { FC } from 'react'; +import useList from './useList'; +import ListContent from './ListContent'; +import ListWrapper from './ListWrapper'; +import type { ItemDto } from 'types/itemDto'; +import type { ListOptions } from 'types/listOptions'; +import '../../mediainfo/mediainfo.scss'; +import '../../guide/programs.scss'; + +interface ListProps { + index: number; + item: ItemDto; + listOptions?: ListOptions; +} + +const List: FC = ({ index, item, listOptions = {} }) => { + const { getListdWrapperProps, getListContentProps } = useList({ item, listOptions } ); + const listWrapperProps = getListdWrapperProps(); + const listContentProps = getListContentProps(); + + return ( + + + + ); +}; + +export default List; diff --git a/src/components/listview/List/ListContent.tsx b/src/components/listview/List/ListContent.tsx new file mode 100644 index 0000000000..045c003f73 --- /dev/null +++ b/src/components/listview/List/ListContent.tsx @@ -0,0 +1,106 @@ +import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client'; +import React, { FC } from 'react'; +import DragHandleIcon from '@mui/icons-material/DragHandle'; +import Box from '@mui/material/Box'; + +import useIndicator from 'components/indicators/useIndicator'; +import PrimaryMediaInfo from '../../mediainfo/PrimaryMediaInfo'; +import ListContentWrapper from './ListContentWrapper'; +import ListItemBody from './ListItemBody'; +import ListImageContainer from './ListImageContainer'; +import ListViewUserDataButtons from './ListViewUserDataButtons'; + +import type { ItemDto } from 'types/itemDto'; +import type { ListOptions } from 'types/listOptions'; + +interface ListContentProps { + item: ItemDto; + listOptions: ListOptions; + enableContentWrapper?: boolean; + enableOverview?: boolean; + enableSideMediaInfo?: boolean; + clickEntireItem?: boolean; + action?: string; + isLargeStyle: boolean; + downloadWidth?: number; +} + +const ListContent: FC = ({ + item, + listOptions, + enableContentWrapper, + enableOverview, + enableSideMediaInfo, + clickEntireItem, + action, + isLargeStyle, + downloadWidth +}) => { + const indicator = useIndicator(item); + return ( + + + {!clickEntireItem && listOptions.dragHandle && ( + + )} + + {listOptions.image !== false && ( + + )} + + {listOptions.showIndexNumberLeft && ( + + {item.IndexNumber ??  } + + )} + + + + {listOptions.mediaInfo !== false && enableSideMediaInfo && ( + + )} + + {listOptions.recordButton + && (item.Type === 'Timer' || item.Type === BaseItemKind.Program) && ( + indicator.getTimerIndicator('listItemAside') + )} + + {!clickEntireItem && ( + + )} + + ); +}; + +export default ListContent; diff --git a/src/components/listview/List/ListContentWrapper.tsx b/src/components/listview/List/ListContentWrapper.tsx new file mode 100644 index 0000000000..1b0678ad50 --- /dev/null +++ b/src/components/listview/List/ListContentWrapper.tsx @@ -0,0 +1,34 @@ +import React, { FC } from 'react'; +import Box from '@mui/material/Box'; + +interface ListContentWrapperProps { + itemOverview: string | null | undefined; + enableContentWrapper?: boolean; + enableOverview?: boolean; +} + +const ListContentWrapper: FC = ({ + itemOverview, + enableContentWrapper, + enableOverview, + children +}) => { + if (enableContentWrapper) { + return ( + <> + {children} + + {enableOverview && itemOverview && ( + + {itemOverview} + + )} + + ); + } else { + // eslint-disable-next-line react/jsx-no-useless-fragment + return <>{children}; + } +}; + +export default ListContentWrapper; diff --git a/src/components/listview/List/ListGroupHeaderWrapper.tsx b/src/components/listview/List/ListGroupHeaderWrapper.tsx new file mode 100644 index 0000000000..f2a131e324 --- /dev/null +++ b/src/components/listview/List/ListGroupHeaderWrapper.tsx @@ -0,0 +1,30 @@ +import React, { FC } from 'react'; +import Typography from '@mui/material/Typography'; + +interface ListGroupHeaderWrapperProps { + index?: number; +} + +const ListGroupHeaderWrapper: FC = ({ + index, + children +}) => { + if (index === 0) { + return ( + + {children} + + ); + } else { + return ( + + {children} + + ); + } +}; + +export default ListGroupHeaderWrapper; diff --git a/src/components/listview/List/ListImageContainer.tsx b/src/components/listview/List/ListImageContainer.tsx new file mode 100644 index 0000000000..fe77707750 --- /dev/null +++ b/src/components/listview/List/ListImageContainer.tsx @@ -0,0 +1,103 @@ +import React, { FC } from 'react'; +import classNames from 'classnames'; +import Box from '@mui/material/Box'; +import { useApi } from 'hooks/useApi'; +import useIndicator from '../../indicators/useIndicator'; +import layoutManager from '../../layoutManager'; +import { getDefaultBackgroundClass } from '../../cardbuilder/cardBuilderUtils'; +import { + canResume, + getChannelImageUrl, + getImageUrl +} from './listHelper'; + +import Media from 'components/common/Media'; +import PlayArrowIconButton from 'components/common/PlayArrowIconButton'; +import type { ItemDto } from 'types/itemDto'; +import type { ListOptions } from 'types/listOptions'; + +interface ListImageContainerProps { + item: ItemDto; + listOptions: ListOptions; + action?: string | null; + isLargeStyle: boolean; + clickEntireItem?: boolean; + downloadWidth?: number; +} + +const ListImageContainer: FC = ({ + item = {}, + listOptions, + action, + isLargeStyle, + clickEntireItem, + downloadWidth +}) => { + const { api } = useApi(); + const { getMediaSourceIndicator, getProgressBar, getPlayedIndicator } = useIndicator(item); + const imgInfo = listOptions.imageSource === 'channel' ? + getChannelImageUrl(item, api, downloadWidth) : + getImageUrl(item, api, downloadWidth); + + const defaultCardImageIcon = listOptions.defaultCardImageIcon; + const disableIndicators = listOptions.disableIndicators; + const imgUrl = imgInfo?.imgUrl; + const blurhash = imgInfo.blurhash; + + const imageClass = classNames( + 'listItemImage', + { 'listItemImage-large': isLargeStyle }, + { 'listItemImage-channel': listOptions.imageSource === 'channel' }, + { 'listItemImage-large-tv': isLargeStyle && layoutManager.tv }, + { itemAction: !clickEntireItem }, + { [getDefaultBackgroundClass(item.Name)]: !imgUrl } + ); + + const playOnImageClick = listOptions.imagePlayButton && !layoutManager.tv; + + const imageAction = playOnImageClick ? 'link' : action; + + const btnCssClass = + 'paper-icon-button-light listItemImageButton itemAction'; + + const mediaSourceIndicator = getMediaSourceIndicator(); + const playedIndicator = getPlayedIndicator(); + const progressBar = getProgressBar(); + const playbackPositionTicks = item?.UserData?.PlaybackPositionTicks; + + return ( + + + + + {disableIndicators !== true && mediaSourceIndicator} + + {playedIndicator && ( + + {playedIndicator} + + )} + + {playOnImageClick && ( + + )} + + {progressBar} + + ); +}; + +export default ListImageContainer; diff --git a/src/components/listview/List/ListItemBody.tsx b/src/components/listview/List/ListItemBody.tsx new file mode 100644 index 0000000000..7d033c4f5d --- /dev/null +++ b/src/components/listview/List/ListItemBody.tsx @@ -0,0 +1,65 @@ +import React, { FC } from 'react'; +import classNames from 'classnames'; +import Box from '@mui/material/Box'; +import useListTextlines from './useListTextlines'; +import PrimaryMediaInfo from '../../mediainfo/PrimaryMediaInfo'; + +import type { ItemDto } from 'types/itemDto'; +import type { ListOptions } from 'types/listOptions'; + +interface ListItemBodyProps { + item: ItemDto; + listOptions: ListOptions; + action?: string | null; + isLargeStyle?: boolean; + clickEntireItem?: boolean; + enableContentWrapper?: boolean; + enableOverview?: boolean; + enableSideMediaInfo?: boolean; + getMissingIndicator: () => React.JSX.Element | null +} + +const ListItemBody: FC = ({ + item = {}, + listOptions = {}, + action, + isLargeStyle, + clickEntireItem, + enableContentWrapper, + enableOverview, + enableSideMediaInfo, + getMissingIndicator +}) => { + const { listTextLines } = useListTextlines({ item, listOptions, isLargeStyle }); + const cssClass = classNames( + 'listItemBody', + { 'itemAction': !clickEntireItem }, + { 'listItemBody-noleftpadding': listOptions.image === false } + ); + + return ( + + + {listTextLines} + + {listOptions.mediaInfo !== false && !enableSideMediaInfo && ( + + )} + + {!enableContentWrapper && enableOverview && item.Overview && ( + + {item.Overview} + + )} + + ); +}; + +export default ListItemBody; diff --git a/src/components/listview/List/ListTextWrapper.tsx b/src/components/listview/List/ListTextWrapper.tsx new file mode 100644 index 0000000000..c2139742ae --- /dev/null +++ b/src/components/listview/List/ListTextWrapper.tsx @@ -0,0 +1,30 @@ +import React, { FC } from 'react'; +import Box from '@mui/material/Box'; +import Typography from '@mui/material/Typography'; + +interface ListTextWrapperProps { + index?: number; + isLargeStyle?: boolean; +} + +const ListTextWrapper: FC = ({ + index, + isLargeStyle, + children +}) => { + if (index === 0) { + if (isLargeStyle) { + return ( + + {children} + + ); + } else { + return {children}; + } + } else { + return {children}; + } +}; + +export default ListTextWrapper; diff --git a/src/components/listview/List/ListViewUserDataButtons.tsx b/src/components/listview/List/ListViewUserDataButtons.tsx new file mode 100644 index 0000000000..f3ad43ed9e --- /dev/null +++ b/src/components/listview/List/ListViewUserDataButtons.tsx @@ -0,0 +1,87 @@ +import React, { FC } from 'react'; +import { Box } from '@mui/material'; +import itemHelper from '../../itemHelper'; +import PlayedButton from 'elements/emby-playstatebutton/PlayedButton'; +import FavoriteButton from 'elements/emby-ratingbutton/FavoriteButton'; +import PlaylistAddIconButton from '../../common/PlaylistAddIconButton'; +import InfoIconButton from '../../common/InfoIconButton'; +import RightIconButtons from '../../common/RightIconButtons'; +import MoreVertIconButton from '../../common/MoreVertIconButton'; + +import type { ItemDto } from 'types/itemDto'; +import type { ListOptions } from 'types/listOptions'; + +interface ListViewUserDataButtonsProps { + item: ItemDto; + listOptions: ListOptions; +} + +const ListViewUserDataButtons: FC = ({ + item = {}, + listOptions +}) => { + const { IsFavorite, Played } = item.UserData ?? {}; + + const renderRightButtons = () => { + return listOptions.rightButtons?.map((button, index) => ( + + )); + }; + + return ( + + {listOptions.addToListButton && ( + + + )} + {listOptions.infoButton && ( + + + ) } + + {listOptions.rightButtons && renderRightButtons()} + + {listOptions.enableUserDataButtons !== false && ( + <> + {itemHelper.canMarkPlayed(item) + && listOptions.enablePlayedButton !== false && ( + + )} + + {itemHelper.canRate(item) + && listOptions.enableRatingButton !== false && ( + + )} + + )} + + {listOptions.moreButton !== false && ( + + )} + + ); +}; + +export default ListViewUserDataButtons; diff --git a/src/components/listview/List/ListWrapper.tsx b/src/components/listview/List/ListWrapper.tsx new file mode 100644 index 0000000000..a6d4ab292e --- /dev/null +++ b/src/components/listview/List/ListWrapper.tsx @@ -0,0 +1,49 @@ +import classNames from 'classnames'; +import escapeHTML from 'escape-html'; +import React, { FC } from 'react'; +import Box from '@mui/material/Box'; +import Button from '@mui/material/Button'; +import layoutManager from '../../layoutManager'; +import type { DataAttributes } from 'types/dataAttributes'; + +interface ListWrapperProps { + index: number | undefined; + title?: string | null; + action?: string | null; + dataAttributes?: DataAttributes; + className?: string; +} + +const ListWrapper: FC = ({ + index, + action, + title, + className, + dataAttributes, + children +}) => { + if (layoutManager.tv) { + return ( + + ); + } else { + return ( + + {children} + + ); + } +}; + +export default ListWrapper; diff --git a/src/components/listview/List/Lists.tsx b/src/components/listview/List/Lists.tsx new file mode 100644 index 0000000000..ce90622c1f --- /dev/null +++ b/src/components/listview/List/Lists.tsx @@ -0,0 +1,57 @@ +import React, { FC } from 'react'; +import escapeHTML from 'escape-html'; +import { groupBy } from 'lodash-es'; +import Box from '@mui/material/Box'; +import { getIndex } from './listHelper'; +import ListGroupHeaderWrapper from './ListGroupHeaderWrapper'; +import List from './List'; + +import type { ItemDto } from 'types/itemDto'; +import type { ListOptions } from 'types/listOptions'; +import '../listview.scss'; + +interface ListsProps { + items: ItemDto[]; + listOptions?: ListOptions; +} + +const Lists: FC = ({ items = [], listOptions = {} }) => { + const groupedData = groupBy(items, (item) => { + if (listOptions.showIndex) { + return getIndex(item, listOptions); + } + return ''; + }); + + const renderListItem = (item: ItemDto, index: number) => { + return ( + + ); + }; + + return ( + <> + {Object.entries(groupedData).map( + ([itemGroupTitle, getItems], index) => ( + // eslint-disable-next-line react/no-array-index-key + + {itemGroupTitle && ( + + {escapeHTML(itemGroupTitle)} + + )} + {getItems.map((item) => renderListItem(item, index))} + + ) + )} + + ); +}; + +export default Lists; diff --git a/src/components/listview/List/listHelper.ts b/src/components/listview/List/listHelper.ts new file mode 100644 index 0000000000..d2ab794ea4 --- /dev/null +++ b/src/components/listview/List/listHelper.ts @@ -0,0 +1,171 @@ +import { Api } from '@jellyfin/sdk'; +import { BaseItemKind, ImageType } from '@jellyfin/sdk/lib/generated-client'; +import globalize from 'scripts/globalize'; + +import type { ItemDto } from 'types/itemDto'; +import type { ListOptions } from 'types/listOptions'; + +const sortBySortName = (item: ItemDto): string => { + if (item.Type === BaseItemKind.Episode) { + return ''; + } + + // SortName + const name = (item.SortName ?? item.Name ?? '?')[0].toUpperCase(); + + const code = name.charCodeAt(0); + if (code < 65 || code > 90) { + return '#'; + } + + return name.toUpperCase(); +}; + +const sortByOfficialrating = (item: ItemDto): string => { + return item.OfficialRating ?? globalize.translate('Unrated'); +}; + +const sortByCommunityRating = (item: ItemDto): string => { + if (item.CommunityRating == null) { + return globalize.translate('Unrated'); + } + + return String(Math.floor(item.CommunityRating)); +}; + +const sortByCriticRating = (item: ItemDto): string => { + if (item.CriticRating == null) { + return globalize.translate('Unrated'); + } + + return String(Math.floor(item.CriticRating)); +}; + +const sortByAlbumArtist = (item: ItemDto): string => { + // SortName + if (!item.AlbumArtist) { + return ''; + } + + const name = item.AlbumArtist[0].toUpperCase(); + + const code = name.charCodeAt(0); + if (code < 65 || code > 90) { + return '#'; + } + + return name.toUpperCase(); +}; + +export function getIndex(item: ItemDto, listOptions: ListOptions): string { + if (listOptions.index === 'disc') { + return item.ParentIndexNumber == null ? + '' : + globalize.translate('ValueDiscNumber', item.ParentIndexNumber); + } + + const sortBy = (listOptions.sortBy ?? '').toLowerCase(); + + if (sortBy.startsWith('sortname')) { + return sortBySortName(item); + } + if (sortBy.startsWith('officialrating')) { + return sortByOfficialrating(item); + } + if (sortBy.startsWith('communityrating')) { + return sortByCommunityRating(item); + } + if (sortBy.startsWith('criticrating')) { + return sortByCriticRating(item); + } + if (sortBy.startsWith('albumartist')) { + return sortByAlbumArtist(item); + } + return ''; +} + +export function getImageUrl( + item: ItemDto, + api: Api | undefined, + size: number | undefined +) { + let imgTag; + let itemId; + const fillWidth = size; + const fillHeight = size; + const imgType = ImageType.Primary; + + if (item.ImageTags?.Primary) { + imgTag = item.ImageTags.Primary; + itemId = item.Id; + } else if (item.AlbumId && item.AlbumPrimaryImageTag) { + imgTag = item.AlbumPrimaryImageTag; + itemId = item.AlbumId; + } else if (item.SeriesId && item.SeriesPrimaryImageTag) { + imgTag = item.SeriesPrimaryImageTag; + itemId = item.SeriesId; + } else if (item.ParentPrimaryImageTag) { + imgTag = item.ParentPrimaryImageTag; + itemId = item.ParentPrimaryImageItemId; + } + + if (api && imgTag && imgType && itemId) { + const response = api.getItemImageUrl(itemId, imgType, { + fillWidth: fillWidth, + fillHeight: fillHeight, + tag: imgTag + }); + + return { + imgUrl: response, + blurhash: item.ImageBlurHashes?.[imgType]?.[imgTag] + }; + } + + return { + imgUrl: undefined, + blurhash: undefined + }; +} + +export function getChannelImageUrl( + item: ItemDto, + api: Api | undefined, + size: number | undefined +) { + let imgTag; + let itemId; + const fillWidth = size; + const fillHeight = size; + const imgType = ImageType.Primary; + + if (item.ChannelId && item.ChannelPrimaryImageTag) { + imgTag = item.ChannelPrimaryImageTag; + itemId = item.ChannelId; + } + + if (api && imgTag && imgType && itemId) { + const response = api.getItemImageUrl(itemId, imgType, { + fillWidth: fillWidth, + fillHeight: fillHeight, + tag: imgTag + }); + + return { + imgUrl: response, + blurhash: item.ImageBlurHashes?.[imgType]?.[imgTag] + }; + } + + return { + imgUrl: undefined, + blurhash: undefined + }; +} + +export function canResume(PlaybackPositionTicks: number | undefined): boolean { + return Boolean( + PlaybackPositionTicks + && PlaybackPositionTicks > 0 + ); +} diff --git a/src/components/listview/List/useList.ts b/src/components/listview/List/useList.ts new file mode 100644 index 0000000000..75a60c6b54 --- /dev/null +++ b/src/components/listview/List/useList.ts @@ -0,0 +1,77 @@ +import classNames from 'classnames'; +import { getDataAttributes } from 'utils/items'; +import layoutManager from 'components/layoutManager'; + +import type { ItemDto } from 'types/itemDto'; +import type { ListOptions } from 'types/listOptions'; + +interface UseListProps { + item: ItemDto; + listOptions: ListOptions; +} + +function useList({ item, listOptions }: UseListProps) { + const action = listOptions.action ?? 'link'; + const isLargeStyle = listOptions.imageSize === 'large'; + const enableOverview = listOptions.enableOverview; + const clickEntireItem = !!layoutManager.tv; + const enableSideMediaInfo = listOptions.enableSideMediaInfo ?? true; + const enableContentWrapper = + listOptions.enableOverview && !layoutManager.tv; + const downloadWidth = isLargeStyle ? 500 : 80; + + const dataAttributes = getDataAttributes( + { + action, + itemServerId: item.ServerId, + itemId: item.Id, + collectionId: listOptions.collectionId, + playlistId: listOptions.playlistId, + itemChannelId: item.ChannelId, + itemType: item.Type, + itemMediaType: item.MediaType, + itemCollectionType: item.CollectionType, + itemIsFolder: item.IsFolder, + itemPlaylistItemId: item.PlaylistItemId + } + ); + + const listWrapperClass = classNames( + 'listItem', + { + 'listItem-border': + listOptions.border + ?? (listOptions.highlight !== false && !layoutManager.tv) + }, + { 'itemAction listItem-button': clickEntireItem }, + { 'listItem-focusscale': layoutManager.tv }, + { 'listItem-largeImage': isLargeStyle }, + { 'listItem-withContentWrapper': enableContentWrapper } + ); + + const getListdWrapperProps = () => ({ + className: listWrapperClass, + title: item.Name, + action, + dataAttributes + }); + + const getListContentProps = () => ({ + item, + listOptions, + enableContentWrapper, + enableOverview, + enableSideMediaInfo, + clickEntireItem, + action, + isLargeStyle, + downloadWidth + }); + + return { + getListdWrapperProps, + getListContentProps + }; +} + +export default useList; diff --git a/src/components/listview/List/useListTextlines.tsx b/src/components/listview/List/useListTextlines.tsx new file mode 100644 index 0000000000..cb5f7ceeb8 --- /dev/null +++ b/src/components/listview/List/useListTextlines.tsx @@ -0,0 +1,167 @@ +import React from 'react'; +import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client'; +import itemHelper from '../../itemHelper'; +import datetime from 'scripts/datetime'; +import ListTextWrapper from './ListTextWrapper'; +import type { ItemDto } from 'types/itemDto'; +import type { ListOptions } from 'types/listOptions'; + +function getParentTitle( + showParentTitle: boolean | undefined, + item: ItemDto, + parentTitleWithTitle: boolean | undefined, + displayName: string | null | undefined +) { + let parentTitle = null; + if (showParentTitle) { + if (item.Type === BaseItemKind.Episode) { + parentTitle = item.SeriesName; + } else if (item.IsSeries || (item.EpisodeTitle && item.Name)) { + parentTitle = item.Name; + } + } + if (showParentTitle && parentTitleWithTitle) { + if (displayName) { + parentTitle += ' - '; + } + parentTitle = (parentTitle ?? '') + displayName; + } + return parentTitle; +} + +function getNameOrIndexWithName( + item: ItemDto, + listOptions: ListOptions, + showIndexNumber: boolean | undefined +) { + let displayName = itemHelper.getDisplayName(item, { + includeParentInfo: listOptions.includeParentInfoInTitle + }); + + if (showIndexNumber && item.IndexNumber != null) { + displayName = `${item.IndexNumber}. ${displayName}`; + } + return displayName; +} + +interface UseListTextlinesProps { + item: ItemDto; + listOptions?: ListOptions; + isLargeStyle?: boolean; +} + +function useListTextlines({ item = {}, listOptions = {}, isLargeStyle }: UseListTextlinesProps) { + const { + showProgramDateTime, + showProgramTime, + showChannel, + showParentTitle, + showIndexNumber, + parentTitleWithTitle, + artist + } = listOptions; + const textLines: string[] = []; + + const addTextLine = (text: string | null) => { + if (text) { + textLines.push(text); + } + }; + + const addProgramDateTime = () => { + if (showProgramDateTime) { + const programDateTime = datetime.toLocaleString( + datetime.parseISO8601Date(item.StartDate), + { + weekday: 'long', + month: 'short', + day: 'numeric', + hour: 'numeric', + minute: '2-digit' + } + ); + addTextLine(programDateTime); + } + }; + + const addProgramTime = () => { + if (showProgramTime) { + const programTime = datetime.getDisplayTime( + datetime.parseISO8601Date(item.StartDate) + ); + addTextLine(programTime); + } + }; + + const addChannelName = () => { + if (showChannel && item.ChannelName) { + addTextLine(item.ChannelName); + } + }; + + const displayName = getNameOrIndexWithName(item, listOptions, showIndexNumber); + + const parentTitle = getParentTitle(showParentTitle, item, parentTitleWithTitle, displayName ); + + const addParentTitle = () => { + addTextLine(parentTitle ?? ''); + }; + + const addDisplayName = () => { + if (displayName && !parentTitleWithTitle) { + addTextLine(displayName); + } + }; + + const addAlbumArtistOrArtists = () => { + if (item.IsFolder && artist !== false) { + if (item.AlbumArtist && item.Type === BaseItemKind.MusicAlbum) { + addTextLine(item.AlbumArtist); + } + } else if (artist) { + const artistItems = item.ArtistItems; + if (artistItems && item.Type !== BaseItemKind.MusicAlbum) { + const artists = artistItems.map((a) => a.Name).join(', '); + addTextLine(artists); + } + } + }; + + const addCurrentProgram = () => { + if (item.Type === BaseItemKind.TvChannel && item.CurrentProgram) { + const currentProgram = itemHelper.getDisplayName( + item.CurrentProgram + ); + addTextLine(currentProgram); + } + }; + + addProgramDateTime(); + addProgramTime(); + addChannelName(); + addParentTitle(); + addDisplayName(); + addAlbumArtistOrArtists(); + addCurrentProgram(); + + const renderTextlines = (text: string, index: number) => { + return ( + + {text} + + ); + }; + + const listTextLines = textLines?.map((text, index) => renderTextlines(text, index)); + + return { + listTextLines + }; +} + +export default useListTextlines; diff --git a/src/components/listview/listview.scss b/src/components/listview/listview.scss index 2aafd936a2..ea829154bb 100644 --- a/src/components/listview/listview.scss +++ b/src/components/listview/listview.scss @@ -183,6 +183,7 @@ } .listItemImage .cardImageIcon { + margin: auto; font-size: 3em; }