From 22bce0cd940452f35b3ff330ed64814122f3f54c Mon Sep 17 00:00:00 2001 From: grafixeyehero Date: Wed, 31 Jan 2024 01:19:35 +0300 Subject: [PATCH 01/24] Add shared itemDto type --- src/types/itemDto.ts | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 src/types/itemDto.ts diff --git a/src/types/itemDto.ts b/src/types/itemDto.ts new file mode 100644 index 0000000000..37fc15fd8e --- /dev/null +++ b/src/types/itemDto.ts @@ -0,0 +1,26 @@ +import type { BaseItemDto, BaseItemKind, CollectionTypeOptions, RecordingStatus, SearchHint, SeriesTimerInfoDto, TimerInfoDto, UserItemDataDto, VirtualFolderInfo } from '@jellyfin/sdk/lib/generated-client'; + +type BaseItem = Omit; +type TimerInfo = Omit; +type SeriesTimerInfo = Omit; +type SearchHintItem = Omit; +type UserItem = Omit; +type VirtualFolder = Omit; + +export interface ItemDto extends BaseItem, TimerInfo, SeriesTimerInfo, SearchHintItem, UserItem, VirtualFolder { + 'ChannelId'?: string | null; + 'EndDate'?: string | null; + 'Id'?: string | null; + 'StartDate'?: string | null; + 'Type'?: BaseItemKind | string | null; + 'Status'?: RecordingStatus | string | null; + 'CollectionType'?: CollectionTypeOptions | string | null; + 'Artists'?: Array | null; + 'MediaType'?: string | null; + 'Name'?: string | null; + 'ItemId'?: string | null; +} + +export type NullableString = string | null | undefined; +export type NullableNumber = number | null | undefined; +export type NullableBoolean = boolean | null | undefined; From 090e2991cb20832ffab4e00bf2a65429b5dc5a88 Mon Sep 17 00:00:00 2001 From: grafixeyehero Date: Wed, 31 Jan 2024 01:20:35 +0300 Subject: [PATCH 02/24] Add RefreshIndicator --- .../RefreshIndicator.tsx | 92 +++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 src/elements/emby-itemrefreshindicator/RefreshIndicator.tsx diff --git a/src/elements/emby-itemrefreshindicator/RefreshIndicator.tsx b/src/elements/emby-itemrefreshindicator/RefreshIndicator.tsx new file mode 100644 index 0000000000..aabc709201 --- /dev/null +++ b/src/elements/emby-itemrefreshindicator/RefreshIndicator.tsx @@ -0,0 +1,92 @@ +import React, { FC, useCallback, useEffect, useState } from 'react'; +import Events, { Event } from 'utils/events'; +import serverNotifications from 'scripts/serverNotifications'; +import classNames from 'classnames'; + +import CircularProgress, { + CircularProgressProps +} from '@mui/material/CircularProgress'; +import Typography from '@mui/material/Typography'; +import Box from '@mui/material/Box'; +import { toPercent } from 'utils/number'; +import { getCurrentDateTimeLocale } from 'scripts/globalize'; +import type { ItemDto } from 'types/itemDto'; + +function CircularProgressWithLabel( + props: CircularProgressProps & { value: number } +) { + return ( + + + + + {toPercent(props.value / 100, getCurrentDateTimeLocale())} + + + + ); +} + +interface RefreshIndicatorProps { + item: ItemDto; + className?: string; +} + +const RefreshIndicator: FC = ({ item, className }) => { + const [progress, setProgress] = useState(item.RefreshProgress || 0); + + const onRefreshProgress = useCallback((_e: Event, apiClient, info) => { + if (info.ItemId === item?.Id) { + setProgress(parseFloat(info.Progress)); + } + }, [item?.Id]); + + const unbindEvents = useCallback(() => { + Events.off(serverNotifications, 'RefreshProgress', onRefreshProgress); + }, [onRefreshProgress]); + + const bindEvents = useCallback(() => { + unbindEvents(); + + if (item?.Id) { + Events.on(serverNotifications, 'RefreshProgress', onRefreshProgress); + } + }, [item?.Id, onRefreshProgress, unbindEvents]); + + useEffect(() => { + bindEvents(); + + return () => { + unbindEvents(); + }; + }, [bindEvents, item.Id, unbindEvents]); + + const progressringClass = classNames( + 'progressring', + className, + { 'hide': !progress || progress >= 100 } + ); + + return ( +
+ +
+ ); +}; + +export default RefreshIndicator; From 2e90f669e5c1c5c709608c7a39911ca7d65cd0f1 Mon Sep 17 00:00:00 2001 From: grafixeyehero Date: Wed, 31 Jan 2024 02:59:45 +0300 Subject: [PATCH 03/24] Migrate Indicator to react --- src/components/indicators/indicators.scss | 8 + src/components/indicators/useIndicator.tsx | 260 ++++++++++++++++++ .../emby-progressbar/AutoTimeProgressBar.tsx | 77 ++++++ src/types/progressOptions.ts | 8 + 4 files changed, 353 insertions(+) create mode 100644 src/components/indicators/useIndicator.tsx create mode 100644 src/elements/emby-progressbar/AutoTimeProgressBar.tsx create mode 100644 src/types/progressOptions.ts diff --git a/src/components/indicators/indicators.scss b/src/components/indicators/indicators.scss index 29137a5df5..6e99a1c3c9 100644 --- a/src/components/indicators/indicators.scss +++ b/src/components/indicators/indicators.scss @@ -5,6 +5,14 @@ height: 0.28em; } +.itemLinearProgress { + width: 100%; + position: absolute; + left: 0; + bottom: 0; + border-radius: 100px; +} + .itemProgressBarForeground { position: absolute; top: 0; diff --git a/src/components/indicators/useIndicator.tsx b/src/components/indicators/useIndicator.tsx new file mode 100644 index 0000000000..3015094b14 --- /dev/null +++ b/src/components/indicators/useIndicator.tsx @@ -0,0 +1,260 @@ +import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind'; +import { LocationType } from '@jellyfin/sdk/lib/generated-client'; +import React from 'react'; +import Box from '@mui/material/Box'; +import LinearProgress, { + linearProgressClasses +} from '@mui/material/LinearProgress'; +import FiberSmartRecordIcon from '@mui/icons-material/FiberSmartRecord'; +import FiberManualRecordIcon from '@mui/icons-material/FiberManualRecord'; +import CheckIcon from '@mui/icons-material/Check'; +import VideocamIcon from '@mui/icons-material/Videocam'; +import FolderIcon from '@mui/icons-material/Folder'; +import PhotoAlbumIcon from '@mui/icons-material/PhotoAlbum'; +import PhotoIcon from '@mui/icons-material/Photo'; +import classNames from 'classnames'; +import datetime from 'scripts/datetime'; +import itemHelper from 'components/itemHelper'; +import AutoTimeProgressBar from 'elements/emby-progressbar/AutoTimeProgressBar'; +import type { ItemDto, NullableString } from 'types/itemDto'; +import type { ProgressOptions } from 'types/progressOptions'; + +const TypeIcon = { + Video: , + Folder: , + PhotoAlbum: , + Photo: +}; + +const getTypeIcon = (itemType: NullableString) => { + return TypeIcon[itemType as keyof typeof TypeIcon]; +}; + +const enableProgressIndicator = ( + itemType: NullableString, + itemMediaType: NullableString +) => { + return ( + (itemMediaType === 'Video' && itemType !== BaseItemKind.TvChannel) + || itemType === BaseItemKind.AudioBook + || itemType === 'AudioPodcast' + ); +}; + +const enableAutoTimeProgressIndicator = ( + itemType: NullableString, + itemStartDate: NullableString, + itemEndDate: NullableString +) => { + return ( + (itemType === BaseItemKind.Program + || itemType === 'Timer' + || itemType === BaseItemKind.Recording) + && Boolean(itemStartDate) + && Boolean(itemEndDate) + ); +}; + +const enablePlayedIndicator = (item: ItemDto) => { + return itemHelper.canMarkPlayed(item); +}; + +const useIndicator = (item: ItemDto) => { + const getMediaSourceIndicator = () => { + const mediaSourceCount = item.MediaSourceCount ?? 0; + if (mediaSourceCount > 1) { + return mediaSourceCount; + } + + return null; + }; + + const getMissingIndicator = () => { + if ( + item.Type === BaseItemKind.Episode + && item.LocationType === LocationType.Virtual + ) { + if (item.PremiereDate) { + try { + const premiereDate = datetime + .parseISO8601Date(item.PremiereDate) + .getTime(); + if (premiereDate > new Date().getTime()) { + return Unaired; + } + } catch (err) { + console.error(err); + } + } + return Missing; + } + + return null; + }; + + const getTimerIndicator = (className?: string) => { + const indicatorIconClass = classNames('timerIndicator', className); + + let status; + + if (item.Type === 'SeriesTimer') { + return ; + } else if (item.TimerId || item.SeriesTimerId) { + status = item.Status || 'Cancelled'; + } else if (item.Type === 'Timer') { + status = item.Status; + } else { + return null; + } + + if (item.SeriesTimerId) { + return ( + + ); + } + + return ; + }; + + const getTypeIndicator = () => { + const icon = getTypeIcon(item.Type); + if (icon) { + return {icon}; + } + return null; + }; + + const getChildCountIndicator = () => { + const childCount = item.ChildCount ?? 0; + + if (childCount > 1) { + return ( + + {datetime.toLocaleString(item.ChildCount)} + + ); + } + + return null; + }; + + const getPlayedIndicator = () => { + if (enablePlayedIndicator(item)) { + const userData = item.UserData || {}; + if (userData.UnplayedItemCount) { + return ( + + {datetime.toLocaleString(userData.UnplayedItemCount)} + + ); + } + + if ( + (userData.PlayedPercentage + && userData.PlayedPercentage >= 100) + || userData.Played + ) { + return ( + + + + ); + } + } + + return null; + }; + + const getProgress = (pct: number, progressOptions?: ProgressOptions) => { + const progressBarClass = classNames( + 'itemLinearProgress', + progressOptions?.containerClass + ); + + return ( + + ); + }; + + const getProgressBar = (progressOptions?: ProgressOptions) => { + if ( + enableProgressIndicator(item.Type, item.MediaType) + && item.Type !== 'Recording' + ) { + const playedPercentage = progressOptions?.userData?.PlayedPercentage ? + progressOptions.userData.PlayedPercentage : + item?.UserData?.PlayedPercentage; + if (playedPercentage && playedPercentage < 100) { + return getProgress(playedPercentage); + } + } + + if ( + enableAutoTimeProgressIndicator( + item.Type, + item.StartDate, + item.EndDate + ) + ) { + let startDate = 0; + let endDate = 1; + + try { + startDate = datetime.parseISO8601Date(item.StartDate).getTime(); + endDate = datetime.parseISO8601Date(item.EndDate).getTime(); + } catch (err) { + console.error(err); + } + + const now = new Date().getTime(); + const total = endDate - startDate; + const pct = 100 * ((now - startDate) / total); + + if (pct > 0 && pct < 100) { + const isRecording = + item.Type === 'Timer' + || item.Type === BaseItemKind.Recording + || Boolean(item.TimerId); + return ( + + ); + } + } + + return null; + }; + + return { + getProgress, + getProgressBar, + getMediaSourceIndicator, + getMissingIndicator, + getTimerIndicator, + getTypeIndicator, + getChildCountIndicator, + getPlayedIndicator + }; +}; + +export default useIndicator; diff --git a/src/elements/emby-progressbar/AutoTimeProgressBar.tsx b/src/elements/emby-progressbar/AutoTimeProgressBar.tsx new file mode 100644 index 0000000000..05b4e6de4a --- /dev/null +++ b/src/elements/emby-progressbar/AutoTimeProgressBar.tsx @@ -0,0 +1,77 @@ +import React, { FC, useCallback, useEffect, useRef, useState } from 'react'; +import { ProgressOptions } from 'types/progressOptions'; +import LinearProgress, { linearProgressClasses } from '@mui/material/LinearProgress'; +import classNames from 'classnames'; + +interface AutoTimeProgressBarProps { + pct: number; + starTtime: number; + endTtime: number; + isRecording: boolean; + dataAutoMode?: string; + progressOptions?: ProgressOptions; +} + +const AutoTimeProgressBar: FC = ({ + pct, + dataAutoMode, + isRecording, + starTtime, + endTtime, + progressOptions +}) => { + const [progress, setProgress] = useState(pct); + const timerRef = useRef | null>(null); + + const onAutoTimeProgress = useCallback(() => { + const start = parseInt(starTtime.toString(), 10); + const end = parseInt(endTtime.toString(), 10); + + const now = new Date().getTime(); + const total = end - start; + let percentage = 100 * ((now - start) / total); + + percentage = Math.min(100, percentage); + percentage = Math.max(0, percentage); + + setProgress(percentage); + }, [endTtime, starTtime]); + + useEffect(() => { + if (timerRef.current) { + clearInterval(timerRef.current); + } + + if (dataAutoMode === 'time') { + timerRef.current = setInterval(onAutoTimeProgress, 60000); + } + + return () => { + if (timerRef.current) { + clearInterval(timerRef.current); + timerRef.current = null; + } + }; + }, [dataAutoMode, onAutoTimeProgress]); + + const progressBarClass = classNames( + 'itemLinearProgress', + progressOptions?.containerClass + ); + + return ( + + ); +}; + +export default AutoTimeProgressBar; diff --git a/src/types/progressOptions.ts b/src/types/progressOptions.ts new file mode 100644 index 0000000000..ae043f2066 --- /dev/null +++ b/src/types/progressOptions.ts @@ -0,0 +1,8 @@ +import { UserItemDataDto } from '@jellyfin/sdk/lib/generated-client'; + +export interface ProgressOptions { + containerClass: string, + type?: string | null, + userData?: UserItemDataDto, + mediaType?: string +} From c3b5d503131572f43d28c5b45b5cd76ddf14c450 Mon Sep 17 00:00:00 2001 From: grafixeyehero Date: Wed, 31 Jan 2024 03:01:58 +0300 Subject: [PATCH 04/24] Convert mediainfo PrimaryMediaInfo to react --- src/components/mediainfo/CaptionMediaInfo.tsx | 25 + .../mediainfo/CriticRatingMediaInfo.tsx | 25 + src/components/mediainfo/EndsAt.tsx | 31 ++ src/components/mediainfo/MediaInfoItem.tsx | 27 + src/components/mediainfo/PrimaryMediaInfo.tsx | 103 ++++ src/components/mediainfo/StarIcons.tsx | 29 + .../mediainfo/usePrimaryMediaInfo.tsx | 522 ++++++++++++++++++ src/types/mediaInfoItem.ts | 4 + 8 files changed, 766 insertions(+) create mode 100644 src/components/mediainfo/CaptionMediaInfo.tsx create mode 100644 src/components/mediainfo/CriticRatingMediaInfo.tsx create mode 100644 src/components/mediainfo/EndsAt.tsx create mode 100644 src/components/mediainfo/MediaInfoItem.tsx create mode 100644 src/components/mediainfo/PrimaryMediaInfo.tsx create mode 100644 src/components/mediainfo/StarIcons.tsx create mode 100644 src/components/mediainfo/usePrimaryMediaInfo.tsx create mode 100644 src/types/mediaInfoItem.ts diff --git a/src/components/mediainfo/CaptionMediaInfo.tsx b/src/components/mediainfo/CaptionMediaInfo.tsx new file mode 100644 index 0000000000..58a6f49af5 --- /dev/null +++ b/src/components/mediainfo/CaptionMediaInfo.tsx @@ -0,0 +1,25 @@ +import React, { FC } from 'react'; +import classNames from 'classnames'; +import ClosedCaptionIcon from '@mui/icons-material/ClosedCaption'; +import Box from '@mui/material/Box'; + +interface CaptionMediaInfoProps { + className?: string; +} + +const CaptionMediaInfo: FC = ({ className }) => { + const cssClass = classNames( + 'mediaInfoItem', + 'mediaInfoText', + 'closedCaptionMediaInfoText', + className + ); + + return ( + + + + ); +}; + +export default CaptionMediaInfo; diff --git a/src/components/mediainfo/CriticRatingMediaInfo.tsx b/src/components/mediainfo/CriticRatingMediaInfo.tsx new file mode 100644 index 0000000000..080aef78fa --- /dev/null +++ b/src/components/mediainfo/CriticRatingMediaInfo.tsx @@ -0,0 +1,25 @@ +import React, { FC } from 'react'; +import classNames from 'classnames'; +import Box from '@mui/material/Box'; + +interface CriticRatingMediaInfoProps { + className?: string; + criticRating: number; +} + +const CriticRatingMediaInfo: FC = ({ + className, + criticRating +}) => { + const cssClass = classNames( + 'mediaInfoCriticRating', + 'mediaInfoItem', + criticRating >= 60 ? + 'mediaInfoCriticRatingFresh' : + 'mediaInfoCriticRatingRotten', + className + ); + return {criticRating}; +}; + +export default CriticRatingMediaInfo; diff --git a/src/components/mediainfo/EndsAt.tsx b/src/components/mediainfo/EndsAt.tsx new file mode 100644 index 0000000000..693f949f8b --- /dev/null +++ b/src/components/mediainfo/EndsAt.tsx @@ -0,0 +1,31 @@ +import React, { FC } from 'react'; +import classNames from 'classnames'; +import Box from '@mui/material/Box'; +import datetime from 'scripts/datetime'; +import globalize from 'scripts/globalize'; + +interface EndsAtProps { + className?: string; + runTimeTicks: number +} + +const EndsAt: FC = ({ runTimeTicks, className }) => { + const cssClass = classNames( + 'mediaInfoItem', + 'mediaInfoText', + 'endsAt', + className + ); + + const endTime = new Date().getTime() + (runTimeTicks / 10000); + const endDate = new Date(endTime); + const displayTime = datetime.getDisplayTime(endDate); + + return ( + + {globalize.translate('EndsAtValue', displayTime)} + + ); +}; + +export default EndsAt; diff --git a/src/components/mediainfo/MediaInfoItem.tsx b/src/components/mediainfo/MediaInfoItem.tsx new file mode 100644 index 0000000000..b832e02e45 --- /dev/null +++ b/src/components/mediainfo/MediaInfoItem.tsx @@ -0,0 +1,27 @@ +import React, { FC } from 'react'; +import Box from '@mui/material/Box'; +import classNames from 'classnames'; +import type { MiscInfo } from 'types/mediaInfoItem'; + +interface MediaInfoItemProps { + className?: string; + miscInfo?: MiscInfo ; + +} + +const MediaInfoItem: FC = ({ className, miscInfo }) => { + const cssClass = classNames( + 'mediaInfoItem', + 'mediaInfoText', + className, + miscInfo?.cssClass + ); + + return ( + + {miscInfo?.text} + + ); +}; + +export default MediaInfoItem; diff --git a/src/components/mediainfo/PrimaryMediaInfo.tsx b/src/components/mediainfo/PrimaryMediaInfo.tsx new file mode 100644 index 0000000000..90b640054a --- /dev/null +++ b/src/components/mediainfo/PrimaryMediaInfo.tsx @@ -0,0 +1,103 @@ +import React, { FC } from 'react'; +import classNames from 'classnames'; +import Box from '@mui/material/Box'; +import usePrimaryMediaInfo from './usePrimaryMediaInfo'; + +import MediaInfoItem from './MediaInfoItem'; +import StarIcons from './StarIcons'; +import CaptionMediaInfo from './CaptionMediaInfo'; +import CriticRatingMediaInfo from './CriticRatingMediaInfo'; +import EndsAt from './EndsAt'; +import type { ItemDto } from 'types/itemDto'; +import type { MiscInfo } from 'types/mediaInfoItem'; + +interface PrimaryMediaInfoProps { + className?: string; + item: ItemDto; + isYearEnabled?: boolean; + isContainerEnabled?: boolean; + isEpisodeTitleEnabled?: boolean; + isCriticRatingEnabled?: boolean; + isEndsAtEnabled?: boolean; + isOriginalAirDateEnabled?: boolean; + isRuntimeEnabled?: boolean; + isProgramIndicatorEnabled?: boolean; + isEpisodeTitleIndexNumberEnabled?: boolean; + isOfficialRatingEnabled?: boolean; + isStarRatingEnabled?: boolean; + isCaptionIndicatorEnabled?: boolean; + isMissingIndicatorEnabled?: boolean; + getMissingIndicator: () => React.JSX.Element | null +} + +const PrimaryMediaInfo: FC = ({ + className, + item, + isYearEnabled = false, + isContainerEnabled = false, + isEpisodeTitleEnabled = false, + isCriticRatingEnabled = false, + isEndsAtEnabled = false, + isOriginalAirDateEnabled = false, + isRuntimeEnabled = false, + isProgramIndicatorEnabled = false, + isEpisodeTitleIndexNumberEnabled = false, + isOfficialRatingEnabled = false, + isStarRatingEnabled = false, + isCaptionIndicatorEnabled = false, + isMissingIndicatorEnabled = false, + getMissingIndicator +}) => { + const miscInfo = usePrimaryMediaInfo({ + item, + isYearEnabled, + isContainerEnabled, + isEpisodeTitleEnabled, + isOriginalAirDateEnabled, + isRuntimeEnabled, + isProgramIndicatorEnabled, + isEpisodeTitleIndexNumberEnabled, + isOfficialRatingEnabled + }); + const { + StartDate, + HasSubtitles, + MediaType, + RunTimeTicks, + CommunityRating, + CriticRating + } = item; + + const cssClass = classNames(className); + + const renderMediaInfo = (info: MiscInfo | undefined, index: number) => ( + + ); + + return ( + + {miscInfo.map((info, index) => renderMediaInfo(info, index))} + + {isStarRatingEnabled && CommunityRating && ( + + )} + + {HasSubtitles && isCaptionIndicatorEnabled && } + + {CriticRating && isCriticRatingEnabled && ( + + )} + + {isEndsAtEnabled + && MediaType === 'Video' + && RunTimeTicks + && !StartDate && } + + {isMissingIndicatorEnabled && ( + getMissingIndicator() + )} + + ); +}; + +export default PrimaryMediaInfo; diff --git a/src/components/mediainfo/StarIcons.tsx b/src/components/mediainfo/StarIcons.tsx new file mode 100644 index 0000000000..d253a2db3d --- /dev/null +++ b/src/components/mediainfo/StarIcons.tsx @@ -0,0 +1,29 @@ +import React, { FC } from 'react'; +import classNames from 'classnames'; +import StarIcon from '@mui/icons-material/Star'; +import Box from '@mui/material/Box'; + +interface StarIconsProps { + className?: string; + communityRating: number; +} + +const StarIcons: FC = ({ className, communityRating }) => { + const cssClass = classNames( + 'mediaInfoItem', + 'mediaInfoText', + 'starRatingContainer', + className + ); + + return ( + + + {communityRating.toFixed(1)} + + ); +}; + +export default StarIcons; diff --git a/src/components/mediainfo/usePrimaryMediaInfo.tsx b/src/components/mediainfo/usePrimaryMediaInfo.tsx new file mode 100644 index 0000000000..480f31dbbc --- /dev/null +++ b/src/components/mediainfo/usePrimaryMediaInfo.tsx @@ -0,0 +1,522 @@ +import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client'; +import * as userSettings from 'scripts/settings/userSettings'; +import datetime from 'scripts/datetime'; +import globalize from 'scripts/globalize'; +import itemHelper from '../itemHelper'; +import type { ItemDto, NullableNumber, NullableString } from 'types/itemDto'; +import type { MiscInfo } from 'types/mediaInfoItem'; + +function shouldShowFolderRuntime( + itemType: NullableString, + itemMediaType: NullableString +): boolean { + return ( + itemType === BaseItemKind.MusicAlbum + || itemMediaType === 'MusicArtist' + || itemType === BaseItemKind.Playlist + || itemMediaType === 'Playlist' + || itemMediaType === 'MusicGenre' + ); +} + +function addTrackCountOrItemCount( + showFolderRuntime: boolean, + itemSongCount: NullableNumber, + itemChildCount: NullableNumber, + itemRunTimeTicks: NullableNumber, + itemType: NullableString, + addMiscInfo: (val: MiscInfo) => void +): void { + if (showFolderRuntime) { + const count = itemSongCount ?? itemChildCount; + if (count) { + addMiscInfo({ text: globalize.translate('TrackCount', count) }); + } + + if (itemRunTimeTicks) { + addMiscInfo({ text: datetime.getDisplayDuration(itemRunTimeTicks) }); + } + } else if (itemType === BaseItemKind.PhotoAlbum || itemType === BaseItemKind.BoxSet) { + const count = itemChildCount; + if (count) { + addMiscInfo({ text: globalize.translate('ItemCount', count) }); + } + } +} + +function addOriginalAirDateInfo( + itemType: NullableString, + itemMediaType: NullableString, + isOriginalAirDateEnabled: boolean, + itemPremiereDate: NullableString, + addMiscInfo: (val: MiscInfo) => void +): void { + if ( + itemPremiereDate + && (itemType === BaseItemKind.Episode || itemMediaType === 'Photo') + && isOriginalAirDateEnabled + ) { + try { + //don't modify date to locale if episode. Only Dates (not times) are stored, or editable in the edit metadata dialog + const date = datetime.parseISO8601Date( + itemPremiereDate, + itemType !== BaseItemKind.Episode + ); + addMiscInfo({ text: datetime.toLocaleDateString(date) }); + } catch (e) { + console.error('error parsing date:', itemPremiereDate); + } + } +} + +function addSeriesTimerInfo( + itemType: NullableString, + itemRecordAnyTime: boolean | undefined, + itemStartDate: NullableString, + itemRecordAnyChannel: boolean | undefined, + itemChannelName: NullableString, + addMiscInfo: (val: MiscInfo) => void +): void { + if (itemType === 'SeriesTimer') { + if (itemRecordAnyTime) { + addMiscInfo({ text: globalize.translate('Anytime') }); + } else { + addMiscInfo({ text: datetime.getDisplayTime(itemStartDate) }); + } + + if (itemRecordAnyChannel) { + addMiscInfo({ text: globalize.translate('AllChannels') }); + } else { + addMiscInfo({ + text: itemChannelName ?? globalize.translate('OneChannel') + }); + } + } +} + +function addProgramIndicatorInfo( + program: ItemDto | undefined, + addMiscInfo: (val: MiscInfo) => void +): void { + if ( + program?.IsLive + && userSettings.get('guide-indicator-live', false) === 'true' + ) { + addMiscInfo({ + text: globalize.translate('Live'), + cssClass: 'mediaInfoProgramAttribute liveTvProgram' + }); + } else if ( + program?.IsPremiere + && userSettings.get('guide-indicator-premiere', false) === 'true' + ) { + addMiscInfo({ + text: globalize.translate('Premiere'), + cssClass: 'mediaInfoProgramAttribute premiereTvProgram' + }); + } else if ( + program?.IsSeries + && !program?.IsRepeat + && userSettings.get('guide-indicator-new', false) === 'true' + ) { + addMiscInfo({ + text: globalize.translate('New'), + cssClass: 'mediaInfoProgramAttribute newTvProgram' + }); + } else if ( + program?.IsSeries + && program?.IsRepeat + && userSettings.get('guide-indicator-repeat', false) === 'true' + ) { + addMiscInfo({ + text: globalize.translate('Repeat'), + cssClass: 'mediaInfoProgramAttribute repeatTvProgram' + }); + } +} + +function addProgramIndicators( + item: ItemDto, + isYearEnabled: boolean, + isEpisodeTitleEnabled: boolean, + isOriginalAirDateEnabled: boolean, + isProgramIndicatorEnabled: boolean, + isEpisodeTitleIndexNumberEnabled: boolean, + addMiscInfo: (val: MiscInfo) => void +): void { + if (item.Type === BaseItemKind.Program || item.Type === 'Timer') { + let program = item; + if (item.Type === 'Timer' && item.ProgramInfo) { + program = item.ProgramInfo; + } + + if (isProgramIndicatorEnabled !== false) { + addProgramIndicatorInfo(program, addMiscInfo); + } + + addProgramTextInfo( + program, + isEpisodeTitleEnabled, + isEpisodeTitleIndexNumberEnabled, + isOriginalAirDateEnabled, + isYearEnabled, + addMiscInfo + ); + } +} + +function addProgramTextInfo( + program: ItemDto, + isEpisodeTitleEnabled: boolean, + isEpisodeTitleIndexNumberEnabled: boolean, + isOriginalAirDateEnabled: boolean, + isYearEnabled: boolean, + addMiscInfo: (val: MiscInfo) => void +): void { + if ((program?.IsSeries || program?.EpisodeTitle) + && isEpisodeTitleEnabled !== false) { + const text = itemHelper.getDisplayName(program, { + includeIndexNumber: isEpisodeTitleIndexNumberEnabled + }); + + if (text) { + addMiscInfo({ text: text }); + } + } else if ( + program?.ProductionYear + && ((program?.IsMovie && isOriginalAirDateEnabled !== false) + || isYearEnabled !== false) + ) { + addMiscInfo({ text: program.ProductionYear }); + } else if (program?.PremiereDate && isOriginalAirDateEnabled !== false) { + try { + const date = datetime.parseISO8601Date(program.PremiereDate); + const text = globalize.translate( + 'OriginalAirDateValue', + datetime.toLocaleDateString(date) + ); + addMiscInfo({ text: text }); + } catch (e) { + console.error('error parsing date:', program.PremiereDate); + } + } +} + +function addStartDateInfo( + itemStartDate: NullableString, + itemType: NullableString, + addMiscInfo: (val: MiscInfo) => void +): void { + if ( + itemStartDate + && itemType !== BaseItemKind.Program + && itemType !== 'SeriesTimer' + && itemType !== 'Timer' + ) { + try { + const date = datetime.parseISO8601Date(itemStartDate); + addMiscInfo({ text: datetime.toLocaleDateString(date) }); + + if (itemType !== BaseItemKind.Recording) { + addMiscInfo({ text: datetime.getDisplayTime(date) }); + } + } catch (e) { + console.error('error parsing date:', itemStartDate); + } + } +} + +function addSeriesProductionYearInfo( + itemProductionYear: NullableNumber, + itemType: NullableString, + isYearEnabled: boolean, + itemStatus: NullableString, + itemEndDate: NullableString, + addMiscInfo: (val: MiscInfo) => void +): void { + if (itemProductionYear && isYearEnabled && itemType === BaseItemKind.Series) { + if (itemStatus === 'Continuing') { + addMiscInfo({ + text: globalize.translate( + 'SeriesYearToPresent', + datetime.toLocaleString(itemProductionYear, { + useGrouping: false + }) + ) + }); + } else { + addproductionYearWithEndDate(itemProductionYear, itemEndDate, addMiscInfo); + } + } +} + +function addproductionYearWithEndDate( + itemProductionYear: number, + itemEndDate: NullableString, + addMiscInfo: (val: MiscInfo) => void +): void { + let productionYear = datetime.toLocaleString(itemProductionYear, { + useGrouping: false + }); + + if (itemEndDate) { + try { + const endYear = datetime.toLocaleString( + datetime.parseISO8601Date(itemEndDate).getFullYear(), + { useGrouping: false } + ); + /* At this point, text will contain only the start year */ + if (endYear !== itemProductionYear) { + productionYear += `-${endYear}`; + } + } catch (e) { + console.error('error parsing date:', itemEndDate); + } + } + addMiscInfo({ text: productionYear }); +} + +function addYearInfo( + isYearEnabled: boolean, + itemType: NullableString, + itemMediaType: NullableString, + itemProductionYear: NullableNumber, + itemPremiereDate: NullableString, + addMiscInfo: (val: MiscInfo) => void +): void { + if ( + isYearEnabled + && itemType !== BaseItemKind.Series + && itemType !== BaseItemKind.Episode + && itemType !== BaseItemKind.Person + && itemMediaType !== 'Photo' + && itemType !== BaseItemKind.Program + && itemType !== BaseItemKind.Season + ) { + if (itemProductionYear) { + addMiscInfo({ text: itemProductionYear }); + } else if (itemPremiereDate) { + try { + const text = datetime.toLocaleString( + datetime.parseISO8601Date(itemPremiereDate).getFullYear(), + { useGrouping: false } + ); + addMiscInfo({ text: text }); + } catch (e) { + console.error('error parsing date:', itemPremiereDate); + } + } + } +} + +function addVideo3DFormat( + itemVideo3DFormat: NullableString, + addMiscInfo: (val: MiscInfo) => void +): void { + if (itemVideo3DFormat) { + addMiscInfo({ text: '3D' }); + } +} + +function addRunTimeInfo( + itemRunTimeTicks: NullableNumber, + itemType: NullableString, + showFolderRuntime: boolean, + isRuntimeEnabled: boolean, + addMiscInfo: (val: MiscInfo) => void +): void { + if ( + itemRunTimeTicks + && itemType !== BaseItemKind.Series + && itemType !== BaseItemKind.Program + && itemType !== 'Timer' + && itemType !== BaseItemKind.Book + && !showFolderRuntime + && isRuntimeEnabled + ) { + if (itemType === BaseItemKind.Audio) { + addMiscInfo({ text: datetime.getDisplayRunningTime(itemRunTimeTicks) }); + } else { + addMiscInfo({ text: datetime.getDisplayDuration(itemRunTimeTicks) }); + } + } +} + +function addOfficialRatingInfo( + itemOfficialRating: NullableString, + itemType: NullableString, + isOfficialRatingEnabled: boolean, + addMiscInfo: (val: MiscInfo) => void +): void { + if ( + itemOfficialRating + && isOfficialRatingEnabled + && itemType !== BaseItemKind.Season + && itemType !== BaseItemKind.Episode + ) { + addMiscInfo({ + text: itemOfficialRating, + cssClass: 'mediaInfoOfficialRating' + }); + } +} + +function addAudioContainer( + itemContainer: NullableString, + isContainerEnabled: boolean, + itemType: NullableString, + addMiscInfo: (val: MiscInfo) => void +): void { + if (itemContainer && isContainerEnabled && itemType === BaseItemKind.Audio) { + addMiscInfo({ text: itemContainer }); + } +} + +function addPhotoSize( + itemMediaType: NullableString, + itemWidth: NullableNumber, + itemHeight: NullableNumber, + addMiscInfo: (val: MiscInfo) => void +): void { + if (itemMediaType === 'Photo' && itemWidth && itemHeight) { + const size = `${itemWidth}x${itemHeight}`; + + addMiscInfo({ text: size }); + } +} + +interface UsePrimaryMediaInfoProps { + item: ItemDto; + isYearEnabled: boolean; + isContainerEnabled: boolean; + isEpisodeTitleEnabled: boolean; + isOriginalAirDateEnabled: boolean; + isRuntimeEnabled: boolean; + isProgramIndicatorEnabled: boolean; + isEpisodeTitleIndexNumberEnabled: boolean; + isOfficialRatingEnabled: boolean; +} + +function usePrimaryMediaInfo({ + item, + isYearEnabled = false, + isContainerEnabled = false, + isEpisodeTitleEnabled = false, + isOriginalAirDateEnabled = false, + isRuntimeEnabled = false, + isProgramIndicatorEnabled = false, + isEpisodeTitleIndexNumberEnabled = false, + isOfficialRatingEnabled = false +}: UsePrimaryMediaInfoProps) { + const { + EndDate, + Status, + StartDate, + ProductionYear, + Video3DFormat, + Type, + Width, + Height, + MediaType, + SongCount, + RecordAnyTime, + RecordAnyChannel, + ChannelName, + ChildCount, + RunTimeTicks, + PremiereDate, + OfficialRating, + Container + } = item; + + const miscInfo: MiscInfo[] = []; + + const addMiscInfo = (val: MiscInfo) => { + if (val) { + miscInfo.push(val); + } + }; + + const showFolderRuntime = shouldShowFolderRuntime(Type, MediaType); + + addTrackCountOrItemCount( + showFolderRuntime, + SongCount, + ChildCount, + RunTimeTicks, + Type, + addMiscInfo + ); + + addOriginalAirDateInfo( + Type, + MediaType, + isOriginalAirDateEnabled, + PremiereDate, + addMiscInfo + ); + + addSeriesTimerInfo( + Type, + RecordAnyTime, + StartDate, + RecordAnyChannel, + ChannelName, + addMiscInfo + ); + + addStartDateInfo(StartDate, Type, addMiscInfo); + + addSeriesProductionYearInfo( + ProductionYear, + Type, + isYearEnabled, + Status, + EndDate, + addMiscInfo + ); + + addProgramIndicators( + item, + isProgramIndicatorEnabled, + isEpisodeTitleEnabled, + isEpisodeTitleIndexNumberEnabled, + isOriginalAirDateEnabled, + isYearEnabled, + addMiscInfo + ); + + addYearInfo( + isYearEnabled, + Type, + MediaType, + ProductionYear, + PremiereDate, + addMiscInfo + ); + + addRunTimeInfo( + RunTimeTicks, + Type, + showFolderRuntime, + isRuntimeEnabled, + addMiscInfo + ); + + addOfficialRatingInfo( + OfficialRating, + Type, + isOfficialRatingEnabled, + addMiscInfo + ); + + addVideo3DFormat(Video3DFormat, addMiscInfo); + + addPhotoSize(MediaType, Width, Height, addMiscInfo); + + addAudioContainer(Container, isContainerEnabled, Type, addMiscInfo); + + return miscInfo; +} + +export default usePrimaryMediaInfo; diff --git a/src/types/mediaInfoItem.ts b/src/types/mediaInfoItem.ts new file mode 100644 index 0000000000..3620fad620 --- /dev/null +++ b/src/types/mediaInfoItem.ts @@ -0,0 +1,4 @@ +export interface MiscInfo { + text?: string | number; + cssClass?: string; +} From cc87ba38593791e89dc39fc6deae256149c073a0 Mon Sep 17 00:00:00 2001 From: grafixeyehero Date: Wed, 31 Jan 2024 04:18:12 +0300 Subject: [PATCH 05/24] Add reusable component --- package-lock.json | 74 ++++++++++++++++++- package.json | 3 + src/components/common/DefaultIconText.tsx | 56 ++++++++++++++ src/components/common/DefaultName.tsx | 23 ++++++ src/components/common/Image.tsx | 67 +++++++++++++++++ src/components/common/InfoIconButton.tsx | 22 ++++++ src/components/common/Media.tsx | 36 +++++++++ src/components/common/MoreVertIconButton.tsx | 23 ++++++ src/components/common/NoItemsMessage.tsx | 25 +++++++ src/components/common/PlayArrowIconButton.tsx | 25 +++++++ .../common/PlaylistAddIconButton.tsx | 22 ++++++ src/components/common/RightIconButtons.tsx | 24 ++++++ src/types/dataAttributes.ts | 48 ++++++++++++ src/utils/image.ts | 37 +++++++++- src/utils/items.ts | 30 ++++++++ webpack.common.js | 2 + 16 files changed, 512 insertions(+), 5 deletions(-) create mode 100644 src/components/common/DefaultIconText.tsx create mode 100644 src/components/common/DefaultName.tsx create mode 100644 src/components/common/Image.tsx create mode 100644 src/components/common/InfoIconButton.tsx create mode 100644 src/components/common/Media.tsx create mode 100644 src/components/common/MoreVertIconButton.tsx create mode 100644 src/components/common/NoItemsMessage.tsx create mode 100644 src/components/common/PlayArrowIconButton.tsx create mode 100644 src/components/common/PlaylistAddIconButton.tsx create mode 100644 src/components/common/RightIconButtons.tsx create mode 100644 src/types/dataAttributes.ts diff --git a/package-lock.json b/package-lock.json index 18853390b0..559b3e61d4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,6 +25,7 @@ "@react-hook/resize-observer": "1.2.6", "@tanstack/react-query": "4.36.1", "@tanstack/react-query-devtools": "4.36.1", + "@types/react-lazy-load-image-component": "1.6.3", "abortcontroller-polyfill": "1.7.5", "blurhash": "2.0.5", "classlist.js": "https://github.com/eligrey/classList.js/archive/1.2.20180112.tar.gz", @@ -52,7 +53,9 @@ "native-promise-only": "0.8.1", "pdfjs-dist": "3.11.174", "react": "17.0.2", + "react-blurhash": "0.3.0", "react-dom": "17.0.2", + "react-lazy-load-image-component": "1.6.0", "react-router-dom": "6.21.3", "resize-observer-polyfill": "1.5.1", "screenfull": "6.0.2", @@ -4705,6 +4708,15 @@ "@types/react": "^17" } }, + "node_modules/@types/react-lazy-load-image-component": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/@types/react-lazy-load-image-component/-/react-lazy-load-image-component-1.6.3.tgz", + "integrity": "sha512-HsIsYz7yWWTh/bftdzGnijKD26JyofLRqM/RM80sxs7Gk13G83ew8R/ra2XzXuiZfjNEjAq/Va+NBHFF9ciwxA==", + "dependencies": { + "@types/react": "*", + "csstype": "^3.0.2" + } + }, "node_modules/@types/react-transition-group": { "version": "4.4.10", "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.10.tgz", @@ -12671,8 +12683,7 @@ "node_modules/lodash.debounce": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", - "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", - "dev": true + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==" }, "node_modules/lodash.memoize": { "version": "4.1.2", @@ -12686,6 +12697,11 @@ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, + "node_modules/lodash.throttle": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz", + "integrity": "sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==" + }, "node_modules/lodash.truncate": { "version": "4.4.2", "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz", @@ -16202,6 +16218,15 @@ "node": ">=0.10.0" } }, + "node_modules/react-blurhash": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/react-blurhash/-/react-blurhash-0.3.0.tgz", + "integrity": "sha512-XlKr4Ns1iYFRnk6DkAblNbAwN/bTJvxTVoxMvmTcURdc5oLoXZwqAF9N3LZUh/HT+QFlq5n6IS6VsDGsviYAiQ==", + "peerDependencies": { + "blurhash": "^2.0.3", + "react": ">=15" + } + }, "node_modules/react-dom": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz", @@ -16220,6 +16245,19 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, + "node_modules/react-lazy-load-image-component": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/react-lazy-load-image-component/-/react-lazy-load-image-component-1.6.0.tgz", + "integrity": "sha512-8KFkDTgjh+0+PVbH+cx0AgxLGbdTsxWMnxXzU5HEUztqewk9ufQAu8cstjZhyvtMIPsdMcPZfA0WAa7HtjQbBQ==", + "dependencies": { + "lodash.debounce": "^4.0.8", + "lodash.throttle": "^4.1.1" + }, + "peerDependencies": { + "react": "^15.x.x || ^16.x.x || ^17.x.x || ^18.x.x", + "react-dom": "^15.x.x || ^16.x.x || ^17.x.x || ^18.x.x" + } + }, "node_modules/react-router": { "version": "6.21.3", "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.21.3.tgz", @@ -25943,6 +25981,15 @@ "@types/react": "^17" } }, + "@types/react-lazy-load-image-component": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/@types/react-lazy-load-image-component/-/react-lazy-load-image-component-1.6.3.tgz", + "integrity": "sha512-HsIsYz7yWWTh/bftdzGnijKD26JyofLRqM/RM80sxs7Gk13G83ew8R/ra2XzXuiZfjNEjAq/Va+NBHFF9ciwxA==", + "requires": { + "@types/react": "*", + "csstype": "^3.0.2" + } + }, "@types/react-transition-group": { "version": "4.4.10", "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.10.tgz", @@ -31866,8 +31913,7 @@ "lodash.debounce": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", - "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", - "dev": true + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==" }, "lodash.memoize": { "version": "4.1.2", @@ -31881,6 +31927,11 @@ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, + "lodash.throttle": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz", + "integrity": "sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==" + }, "lodash.truncate": { "version": "4.4.2", "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz", @@ -34295,6 +34346,12 @@ "object-assign": "^4.1.1" } }, + "react-blurhash": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/react-blurhash/-/react-blurhash-0.3.0.tgz", + "integrity": "sha512-XlKr4Ns1iYFRnk6DkAblNbAwN/bTJvxTVoxMvmTcURdc5oLoXZwqAF9N3LZUh/HT+QFlq5n6IS6VsDGsviYAiQ==", + "requires": {} + }, "react-dom": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz", @@ -34310,6 +34367,15 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, + "react-lazy-load-image-component": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/react-lazy-load-image-component/-/react-lazy-load-image-component-1.6.0.tgz", + "integrity": "sha512-8KFkDTgjh+0+PVbH+cx0AgxLGbdTsxWMnxXzU5HEUztqewk9ufQAu8cstjZhyvtMIPsdMcPZfA0WAa7HtjQbBQ==", + "requires": { + "lodash.debounce": "^4.0.8", + "lodash.throttle": "^4.1.1" + } + }, "react-router": { "version": "6.21.3", "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.21.3.tgz", diff --git a/package.json b/package.json index 9975e3f49f..aa6830dc2f 100644 --- a/package.json +++ b/package.json @@ -86,6 +86,7 @@ "@react-hook/resize-observer": "1.2.6", "@tanstack/react-query": "4.36.1", "@tanstack/react-query-devtools": "4.36.1", + "@types/react-lazy-load-image-component": "1.6.3", "abortcontroller-polyfill": "1.7.5", "blurhash": "2.0.5", "classlist.js": "https://github.com/eligrey/classList.js/archive/1.2.20180112.tar.gz", @@ -113,7 +114,9 @@ "native-promise-only": "0.8.1", "pdfjs-dist": "3.11.174", "react": "17.0.2", + "react-blurhash": "0.3.0", "react-dom": "17.0.2", + "react-lazy-load-image-component": "1.6.0", "react-router-dom": "6.21.3", "resize-observer-polyfill": "1.5.1", "screenfull": "6.0.2", diff --git a/src/components/common/DefaultIconText.tsx b/src/components/common/DefaultIconText.tsx new file mode 100644 index 0000000000..41f0014cb0 --- /dev/null +++ b/src/components/common/DefaultIconText.tsx @@ -0,0 +1,56 @@ +import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client'; +import React, { FC } from 'react'; +import Icon from '@mui/material/Icon'; +import imageHelper from 'utils/image'; +import DefaultName from './DefaultName'; +import type { ItemDto } from 'types/itemDto'; + +interface DefaultIconTextProps { + item: ItemDto; + defaultCardImageIcon?: string; +} + +const DefaultIconText: FC = ({ + item, + defaultCardImageIcon +}) => { + if (item.CollectionType) { + return ( + + ); + } + + if (item.Type && !(item.Type === BaseItemKind.TvChannel || item.Type === BaseItemKind.Studio )) { + return ( + + ); + } + + if (defaultCardImageIcon) { + return ( + + ); + } + + return ; +}; + +export default DefaultIconText; diff --git a/src/components/common/DefaultName.tsx b/src/components/common/DefaultName.tsx new file mode 100644 index 0000000000..5946fe27b5 --- /dev/null +++ b/src/components/common/DefaultName.tsx @@ -0,0 +1,23 @@ +import React, { FC } from 'react'; +import Box from '@mui/material/Box'; +import escapeHTML from 'escape-html'; +import itemHelper from 'components/itemHelper'; +import { isUsingLiveTvNaming } from '../cardbuilder/cardBuilderUtils'; +import type { ItemDto } from 'types/itemDto'; + +interface DefaultNameProps { + item: ItemDto; +} + +const DefaultName: FC = ({ item }) => { + const defaultName = isUsingLiveTvNaming(item.Type) ? + item.Name : + itemHelper.getDisplayName(item); + return ( + + {escapeHTML(defaultName)} + + ); +}; + +export default DefaultName; diff --git a/src/components/common/Image.tsx b/src/components/common/Image.tsx new file mode 100644 index 0000000000..14df552660 --- /dev/null +++ b/src/components/common/Image.tsx @@ -0,0 +1,67 @@ +import React, { FC, useCallback, useState } from 'react'; +import { BlurhashCanvas } from 'react-blurhash'; +import { LazyLoadImage } from 'react-lazy-load-image-component'; + +const imageStyle: React.CSSProperties = { + position: 'absolute', + top: 0, + bottom: 0, + left: 0, + right: 0, + width: '100%', + height: '100%', + zIndex: 0 +}; + +interface ImageProps { + imgUrl: string; + blurhash?: string; + containImage: boolean; +} + +const Image: FC = ({ + imgUrl, + blurhash, + containImage +}) => { + const [isLoaded, setIsLoaded] = useState(false); + const [isLoadStarted, setIsLoadStarted] = useState(false); + const handleLoad = useCallback(() => { + setIsLoaded(true); + }, []); + + const handleLoadStarted = useCallback(() => { + setIsLoadStarted(true); + }, []); + + return ( +
+ {!isLoaded && isLoadStarted && blurhash && ( + + )} + + +
+ ); +}; + +export default Image; diff --git a/src/components/common/InfoIconButton.tsx b/src/components/common/InfoIconButton.tsx new file mode 100644 index 0000000000..69c602e327 --- /dev/null +++ b/src/components/common/InfoIconButton.tsx @@ -0,0 +1,22 @@ +import React, { FC } from 'react'; +import IconButton from '@mui/material/IconButton'; +import InfoIcon from '@mui/icons-material/Info'; +import globalize from 'scripts/globalize'; + +interface InfoIconButtonProps { + className?: string; +} + +const InfoIconButton: FC = ({ className }) => { + return ( + + + + ); +}; + +export default InfoIconButton; diff --git a/src/components/common/Media.tsx b/src/components/common/Media.tsx new file mode 100644 index 0000000000..170208416f --- /dev/null +++ b/src/components/common/Media.tsx @@ -0,0 +1,36 @@ +import { BaseItemKind, ImageType } from '@jellyfin/sdk/lib/generated-client'; +import React, { FC } from 'react'; +import Image from './Image'; +import DefaultIconText from './DefaultIconText'; +import type { ItemDto } from 'types/itemDto'; + +interface MediaProps { + item: ItemDto; + imgUrl: string | undefined; + blurhash: string | undefined; + imageType?: ImageType + defaultCardImageIcon?: string +} + +const Media: FC = ({ + item, + imgUrl, + blurhash, + imageType, + defaultCardImageIcon +}) => { + return imgUrl ? ( + + ) : ( + + ); +}; + +export default Media; diff --git a/src/components/common/MoreVertIconButton.tsx b/src/components/common/MoreVertIconButton.tsx new file mode 100644 index 0000000000..231a2afed1 --- /dev/null +++ b/src/components/common/MoreVertIconButton.tsx @@ -0,0 +1,23 @@ +import React, { FC } from 'react'; +import IconButton from '@mui/material/IconButton'; +import MoreVertIcon from '@mui/icons-material/MoreVert'; +import globalize from 'scripts/globalize'; + +interface MoreVertIconButtonProps { + className?: string; + iconClassName?: string; +} + +const MoreVertIconButton: FC = ({ className, iconClassName }) => { + return ( + + + + ); +}; + +export default MoreVertIconButton; diff --git a/src/components/common/NoItemsMessage.tsx b/src/components/common/NoItemsMessage.tsx new file mode 100644 index 0000000000..2c59b0ed6b --- /dev/null +++ b/src/components/common/NoItemsMessage.tsx @@ -0,0 +1,25 @@ +import React, { FC } from 'react'; +import Box from '@mui/material/Box'; +import Typography from '@mui/material/Typography'; +import globalize from 'scripts/globalize'; + +interface NoItemsMessageProps { + noItemsMessage?: string; +} + +const NoItemsMessage: FC = ({ + noItemsMessage = 'MessageNoItemsAvailable' +}) => { + return ( + + + {globalize.translate('MessageNothingHere')} + + + {globalize.translate(noItemsMessage)} + + + ); +}; + +export default NoItemsMessage; diff --git a/src/components/common/PlayArrowIconButton.tsx b/src/components/common/PlayArrowIconButton.tsx new file mode 100644 index 0000000000..b64fd9bd05 --- /dev/null +++ b/src/components/common/PlayArrowIconButton.tsx @@ -0,0 +1,25 @@ +import React, { FC } from 'react'; +import IconButton from '@mui/material/IconButton'; +import PlayArrowIcon from '@mui/icons-material/PlayArrow'; +import globalize from 'scripts/globalize'; + +interface PlayArrowIconButtonProps { + className: string; + action: string; + title: string; + iconClassName?: string; +} + +const PlayArrowIconButton: FC = ({ className, action, title, iconClassName }) => { + return ( + + + + ); +}; + +export default PlayArrowIconButton; diff --git a/src/components/common/PlaylistAddIconButton.tsx b/src/components/common/PlaylistAddIconButton.tsx new file mode 100644 index 0000000000..19469e0fe3 --- /dev/null +++ b/src/components/common/PlaylistAddIconButton.tsx @@ -0,0 +1,22 @@ +import React, { FC } from 'react'; +import IconButton from '@mui/material/IconButton'; +import PlaylistAddIcon from '@mui/icons-material/PlaylistAdd'; +import globalize from 'scripts/globalize'; + +interface PlaylistAddIconButtonProps { + className?: string; +} + +const PlaylistAddIconButton: FC = ({ className }) => { + return ( + + + + ); +}; + +export default PlaylistAddIconButton; diff --git a/src/components/common/RightIconButtons.tsx b/src/components/common/RightIconButtons.tsx new file mode 100644 index 0000000000..2787a1856c --- /dev/null +++ b/src/components/common/RightIconButtons.tsx @@ -0,0 +1,24 @@ +import React, { FC } from 'react'; +import IconButton from '@mui/material/IconButton'; + +interface RightIconButtonsProps { + className?: string; + id: string; + icon: string; + title: string; +} + +const RightIconButtons: FC = ({ className, id, title, icon }) => { + return ( + + {icon} + + ); +}; + +export default RightIconButtons; diff --git a/src/types/dataAttributes.ts b/src/types/dataAttributes.ts new file mode 100644 index 0000000000..6e91b125cd --- /dev/null +++ b/src/types/dataAttributes.ts @@ -0,0 +1,48 @@ +import type { CollectionType, UserItemDataDto } from '@jellyfin/sdk/lib/generated-client'; +import type { NullableBoolean, NullableNumber, NullableString } from './itemDto'; + +export type AttributesOpts = { + context?: CollectionType | undefined, + parentId?: NullableString, + collectionId?: NullableString, + playlistId?: NullableString, + prefix?: NullableString, + action?: NullableString, + itemServerId?: NullableString, + itemId?: NullableString, + itemTimerId?: NullableString, + itemSeriesTimerId?: NullableString, + itemChannelId?: NullableString, + itemPlaylistItemId?: NullableString, + itemType?: NullableString, + itemMediaType?: NullableString, + itemCollectionType?: NullableString, + itemIsFolder?: NullableBoolean, + itemPath?: NullableString, + itemStartDate?: NullableString, + itemEndDate?: NullableString, + itemUserData?: UserItemDataDto +}; + +export type DataAttributes = { + 'data-playlistitemid'?: NullableString; + 'data-timerid'?: NullableString; + 'data-seriestimerid'?: NullableString; + 'data-serverid'?: NullableString; + 'data-id'?: NullableString; + 'data-type'?: NullableString; + 'data-collectionid'?: NullableString; + 'data-playlistid'?: NullableString; + 'data-mediatype'?: NullableString; + 'data-channelid'?: NullableString; + 'data-path'?: NullableString; + 'data-collectiontype'?: NullableString; + 'data-context'?: NullableString; + 'data-parentid'?: NullableString; + 'data-startdate'?: NullableString; + 'data-enddate'?: NullableString; + 'data-prefix'?: NullableString; + 'data-action'?: NullableString; + 'data-positionticks'?: NullableNumber; + 'data-isfolder'?: NullableBoolean; +}; diff --git a/src/utils/image.ts b/src/utils/image.ts index 7420fad41c..3819f865df 100644 --- a/src/utils/image.ts +++ b/src/utils/image.ts @@ -1,3 +1,4 @@ +import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client'; import type { DeviceInfo } from '@jellyfin/sdk/lib/generated-client/models/device-info'; import type { SessionInfo } from '@jellyfin/sdk/lib/generated-client/models/session-info'; @@ -103,7 +104,41 @@ export function getLibraryIcon(library: string | null | undefined) { } } +export function getItemTypeIcon(itemType: BaseItemKind | string) { + switch (itemType) { + case BaseItemKind.MusicAlbum: + return 'album'; + case BaseItemKind.MusicArtist: + case BaseItemKind.Person: + return 'person'; + case BaseItemKind.Audio: + return 'audiotrack'; + case BaseItemKind.Movie: + return 'movie'; + case BaseItemKind.Episode: + case BaseItemKind.Series: + return 'tv'; + case BaseItemKind.Program: + return 'live_tv'; + case BaseItemKind.Book: + return 'book'; + case BaseItemKind.Folder: + return 'folder'; + case BaseItemKind.BoxSet: + return 'collections'; + case BaseItemKind.Playlist: + return 'view_list'; + case BaseItemKind.Photo: + return 'photo'; + case BaseItemKind.PhotoAlbum: + return 'photo_album'; + default: + return 'folder'; + } +} + export default { getDeviceIcon, - getLibraryIcon + getLibraryIcon, + getItemTypeIcon }; diff --git a/src/utils/items.ts b/src/utils/items.ts index 752226db26..6a5dcd0985 100644 --- a/src/utils/items.ts +++ b/src/utils/items.ts @@ -3,8 +3,10 @@ import { ImageType } from '@jellyfin/sdk/lib/generated-client/models/image-type' import { ItemSortBy } from '@jellyfin/sdk/lib/models/api/item-sort-by'; import { SortOrder } from '@jellyfin/sdk/lib/generated-client/models/sort-order'; import * as userSettings from 'scripts/settings/userSettings'; +import layoutManager from 'components/layoutManager'; import { EpisodeFilter, FeatureFilters, LibraryViewSettings, ParentId, VideoBasicFilter, ViewMode } from '../types/library'; import { LibraryTab } from 'types/libraryTab'; +import type { AttributesOpts, DataAttributes } from 'types/dataAttributes'; export const getVideoBasicFilter = (libraryViewSettings: LibraryViewSettings) => { let isHd; @@ -164,3 +166,31 @@ export const getDefaultLibraryViewSettings = (viewType: LibraryTab): LibraryView StartIndex: 0 }; }; + +export function getDataAttributes( + opts: AttributesOpts +): DataAttributes { + return { + 'data-context': opts.context, + 'data-collectionid': opts.collectionId, + 'data-playlistid': opts.playlistId, + 'data-parentid': opts.parentId, + 'data-playlistitemid': opts.itemPlaylistItemId, + 'data-action': layoutManager.tv ? opts.action : null, + 'data-serverid': opts.itemServerId, + 'data-id': opts.itemId, + 'data-timerid': opts.itemTimerId, + 'data-seriestimerid': opts.itemSeriesTimerId, + 'data-channelid': opts.itemChannelId, + 'data-type': opts.itemType, + 'data-mediatype': opts.itemMediaType, + 'data-collectiontype': opts.itemCollectionType, + 'data-isfolder': opts.itemIsFolder, + 'data-path': opts.itemPath, + 'data-prefix': opts.prefix, + 'data-positionticks': opts.itemUserData?.PlaybackPositionTicks, + 'data-startdate': opts.itemStartDate?.toString(), + 'data-enddate': opts.itemEndDate?.toString() + }; +} + diff --git a/webpack.common.js b/webpack.common.js index dd85d03183..1c4587e87b 100644 --- a/webpack.common.js +++ b/webpack.common.js @@ -200,6 +200,8 @@ const config = { path.resolve(__dirname, 'node_modules/markdown-it'), path.resolve(__dirname, 'node_modules/mdurl'), path.resolve(__dirname, 'node_modules/punycode'), + path.resolve(__dirname, 'node_modules/react-blurhash'), + path.resolve(__dirname, 'node_modules/react-lazy-load-image-component'), path.resolve(__dirname, 'node_modules/react-router'), path.resolve(__dirname, 'node_modules/screenfull'), path.resolve(__dirname, 'node_modules/ssr-window'), From 9efc71fa3b2c8fea9fba98e8203b56b50fbccd4d Mon Sep 17 00:00:00 2001 From: grafixeyehero Date: Wed, 31 Jan 2024 04:20:42 +0300 Subject: [PATCH 06/24] 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; } From 97472ac8bb4d54b4c6323cf7c8d79288d8342297 Mon Sep 17 00:00:00 2001 From: grafixeyehero Date: Wed, 31 Jan 2024 04:25:14 +0300 Subject: [PATCH 07/24] Convert CardView to react --- src/components/cardbuilder/Card/Card.tsx | 25 + src/components/cardbuilder/Card/CardBox.tsx | 79 ++ .../cardbuilder/Card/CardContent.tsx | 50 ++ .../cardbuilder/Card/CardFooterText.tsx | 81 ++ .../cardbuilder/Card/CardHoverMenu.tsx | 83 ++ .../cardbuilder/Card/CardImageContainer.tsx | 82 ++ .../cardbuilder/Card/CardInnerFooter.tsx | 42 + .../cardbuilder/Card/CardOuterFooter.tsx | 45 ++ .../cardbuilder/Card/CardOverlayButtons.tsx | 96 +++ src/components/cardbuilder/Card/CardText.tsx | 33 + .../cardbuilder/Card/CardWrapper.tsx | 30 + src/components/cardbuilder/Card/Cards.tsx | 32 + src/components/cardbuilder/Card/cardHelper.ts | 721 ++++++++++++++++++ src/components/cardbuilder/Card/useCard.ts | 121 +++ .../cardbuilder/Card/useCardImageUrl.ts | 301 ++++++++ .../cardbuilder/Card/useCardText.tsx | 113 +++ src/components/cardbuilder/card.scss | 7 +- src/components/cardbuilder/cardBuilder.js | 2 +- .../cardbuilder/cardBuilderUtils.ts | 4 +- src/types/cardOptions.ts | 57 +- 20 files changed, 1993 insertions(+), 11 deletions(-) create mode 100644 src/components/cardbuilder/Card/Card.tsx create mode 100644 src/components/cardbuilder/Card/CardBox.tsx create mode 100644 src/components/cardbuilder/Card/CardContent.tsx create mode 100644 src/components/cardbuilder/Card/CardFooterText.tsx create mode 100644 src/components/cardbuilder/Card/CardHoverMenu.tsx create mode 100644 src/components/cardbuilder/Card/CardImageContainer.tsx create mode 100644 src/components/cardbuilder/Card/CardInnerFooter.tsx create mode 100644 src/components/cardbuilder/Card/CardOuterFooter.tsx create mode 100644 src/components/cardbuilder/Card/CardOverlayButtons.tsx create mode 100644 src/components/cardbuilder/Card/CardText.tsx create mode 100644 src/components/cardbuilder/Card/CardWrapper.tsx create mode 100644 src/components/cardbuilder/Card/Cards.tsx create mode 100644 src/components/cardbuilder/Card/cardHelper.ts create mode 100644 src/components/cardbuilder/Card/useCard.ts create mode 100644 src/components/cardbuilder/Card/useCardImageUrl.ts create mode 100644 src/components/cardbuilder/Card/useCardText.tsx diff --git a/src/components/cardbuilder/Card/Card.tsx b/src/components/cardbuilder/Card/Card.tsx new file mode 100644 index 0000000000..2173e0301b --- /dev/null +++ b/src/components/cardbuilder/Card/Card.tsx @@ -0,0 +1,25 @@ +import React, { FC } from 'react'; +import useCard from './useCard'; +import CardWrapper from './CardWrapper'; +import CardBox from './CardBox'; + +import type { CardOptions } from 'types/cardOptions'; +import type { ItemDto } from 'types/itemDto'; + +interface CardProps { + item?: ItemDto; + cardOptions: CardOptions; +} + +const Card: FC = ({ item = {}, cardOptions }) => { + const { getCardWrapperProps, getCardBoxProps } = useCard({ item, cardOptions } ); + const cardWrapperProps = getCardWrapperProps(); + const cardBoxProps = getCardBoxProps(); + return ( + + + + ); +}; + +export default Card; diff --git a/src/components/cardbuilder/Card/CardBox.tsx b/src/components/cardbuilder/Card/CardBox.tsx new file mode 100644 index 0000000000..430c27b444 --- /dev/null +++ b/src/components/cardbuilder/Card/CardBox.tsx @@ -0,0 +1,79 @@ + +import React, { FC } from 'react'; +import layoutManager from 'components/layoutManager'; + +import CardOverlayButtons from './CardOverlayButtons'; +import CardHoverMenu from './CardHoverMenu'; +import CardOuterFooter from './CardOuterFooter'; +import CardContent from './CardContent'; + +import type { ItemDto } from 'types/itemDto'; +import type { CardOptions } from 'types/cardOptions'; + +interface CardBoxProps { + item: ItemDto; + cardOptions: CardOptions; + className: string; + shape: string | null | undefined; + imgUrl: string | undefined; + blurhash: string | undefined; + forceName: boolean; + coveredImage: boolean; + overlayText: boolean | undefined; +} + +const CardBox: FC = ({ + item, + cardOptions, + className, + shape, + imgUrl, + blurhash, + forceName, + coveredImage, + overlayText +}) => { + return ( +
+
+
+ + {layoutManager.mobile && ( + + )} + + {layoutManager.desktop + && !cardOptions.disableHoverMenu && ( + + )} +
+ {!overlayText && ( + + )} +
+ ); +}; + +export default CardBox; + diff --git a/src/components/cardbuilder/Card/CardContent.tsx b/src/components/cardbuilder/Card/CardContent.tsx new file mode 100644 index 0000000000..3d846758a4 --- /dev/null +++ b/src/components/cardbuilder/Card/CardContent.tsx @@ -0,0 +1,50 @@ +import React, { FC } from 'react'; +import classNames from 'classnames'; +import { getDefaultBackgroundClass } from '../cardBuilderUtils'; +import CardImageContainer from './CardImageContainer'; + +import type { ItemDto } from 'types/itemDto'; +import type { CardOptions } from 'types/cardOptions'; + +interface CardContentProps { + item: ItemDto; + cardOptions: CardOptions; + coveredImage: boolean; + overlayText: boolean | undefined; + imgUrl: string | undefined; + blurhash: string | undefined; + forceName: boolean; +} + +const CardContent: FC = ({ + item, + cardOptions, + coveredImage, + overlayText, + imgUrl, + blurhash, + forceName +}) => { + const cardContentClass = classNames( + 'cardContent', + { [getDefaultBackgroundClass(item.Name)]: !imgUrl } + ); + + return ( +
+ +
+ ); +}; + +export default CardContent; diff --git a/src/components/cardbuilder/Card/CardFooterText.tsx b/src/components/cardbuilder/Card/CardFooterText.tsx new file mode 100644 index 0000000000..87ba3b22ea --- /dev/null +++ b/src/components/cardbuilder/Card/CardFooterText.tsx @@ -0,0 +1,81 @@ +import React, { FC } from 'react'; +import Box from '@mui/material/Box'; +import useCardText from './useCardText'; +import layoutManager from 'components/layoutManager'; +import MoreVertIconButton from '../../common/MoreVertIconButton'; +import type { ItemDto } from 'types/itemDto'; +import type { CardOptions } from 'types/cardOptions'; + +const shouldShowDetailsMenu = ( + cardOptions: CardOptions, + isOuterFooter: boolean +) => { + return ( + cardOptions.showDetailsMenu + && isOuterFooter + && cardOptions.cardLayout + && layoutManager.mobile + && cardOptions.cardFooterAside !== 'none' + ); +}; + +interface LogoComponentProps { + logoUrl: string; +} + +const LogoComponent: FC = ({ logoUrl }) => { + return ; +}; + +interface CardFooterTextProps { + item: ItemDto; + cardOptions: CardOptions; + forceName: boolean; + overlayText: boolean | undefined; + imgUrl: string | undefined; + footerClass: string | undefined; + progressBar?: React.JSX.Element | null; + logoUrl?: string; + isOuterFooter: boolean; +} + +const CardFooterText: FC = ({ + item, + cardOptions, + forceName, + imgUrl, + footerClass, + overlayText, + progressBar, + logoUrl, + isOuterFooter +}) => { + const { cardTextLines } = useCardText({ + item, + cardOptions, + forceName, + imgUrl, + overlayText, + isOuterFooter, + cssClass: cardOptions.centerText ? + 'cardText cardTextCentered' : + 'cardText', + forceLines: !cardOptions.overlayText, + maxLines: cardOptions.lines + }); + + return ( + + {logoUrl && } + {shouldShowDetailsMenu(cardOptions, isOuterFooter) && ( + + )} + + {cardTextLines} + + {progressBar} + + ); +}; + +export default CardFooterText; diff --git a/src/components/cardbuilder/Card/CardHoverMenu.tsx b/src/components/cardbuilder/Card/CardHoverMenu.tsx new file mode 100644 index 0000000000..e135d1bd82 --- /dev/null +++ b/src/components/cardbuilder/Card/CardHoverMenu.tsx @@ -0,0 +1,83 @@ +import React, { FC } from 'react'; +import Box from '@mui/material/Box'; +import ButtonGroup from '@mui/material/ButtonGroup'; +import classNames from 'classnames'; +import escapeHTML from 'escape-html'; +import { appRouter } from 'components/router/appRouter'; +import itemHelper from 'components/itemHelper'; +import { playbackManager } from 'components/playback/playbackmanager'; + +import PlayedButton from 'elements/emby-playstatebutton/PlayedButton'; +import FavoriteButton from 'elements/emby-ratingbutton/FavoriteButton'; +import PlayArrowIconButton from '../../common/PlayArrowIconButton'; +import MoreVertIconButton from '../../common/MoreVertIconButton'; + +import type { ItemDto } from 'types/itemDto'; +import type { CardOptions } from 'types/cardOptions'; + +interface CardHoverMenuProps { + item: ItemDto; + cardOptions: CardOptions; +} + +const CardHoverMenu: FC = ({ + item, + cardOptions +}) => { + const url = appRouter.getRouteUrl(item, { + parentId: cardOptions.parentId + }); + const btnCssClass = + 'paper-icon-button-light cardOverlayButton cardOverlayButton-hover itemAction'; + + const centerPlayButtonClass = classNames( + btnCssClass, + 'cardOverlayFab-primary' + ); + const { IsFavorite, Played } = item.UserData ?? {}; + + return ( + + + + {playbackManager.canPlay(item) && ( + + )} + + + {itemHelper.canMarkPlayed(item) && cardOptions.enablePlayedButton !== false && ( + + )} + + {itemHelper.canRate(item) && cardOptions.enableRatingButton !== false && ( + + )} + + + + + ); +}; + +export default CardHoverMenu; diff --git a/src/components/cardbuilder/Card/CardImageContainer.tsx b/src/components/cardbuilder/Card/CardImageContainer.tsx new file mode 100644 index 0000000000..3b66048e9e --- /dev/null +++ b/src/components/cardbuilder/Card/CardImageContainer.tsx @@ -0,0 +1,82 @@ +import React, { FC } from 'react'; +import Box from '@mui/material/Box'; +import classNames from 'classnames'; +import useIndicator from 'components/indicators/useIndicator'; +import RefreshIndicator from 'elements/emby-itemrefreshindicator/RefreshIndicator'; +import Media from '../../common/Media'; +import CardInnerFooter from './CardInnerFooter'; + +import type { ItemDto } from 'types/itemDto'; +import type { CardOptions } from 'types/cardOptions'; + +interface CardImageContainerProps { + item: ItemDto; + cardOptions: CardOptions; + coveredImage: boolean; + overlayText: boolean | undefined; + imgUrl: string | undefined; + blurhash: string | undefined; + forceName: boolean; +} + +const CardImageContainer: FC = ({ + item, + cardOptions, + coveredImage, + overlayText, + imgUrl, + blurhash, + forceName +}) => { + const indicator = useIndicator(item); + const cardImageClass = classNames( + 'cardImageContainer', + { coveredImage: coveredImage }, + { 'coveredImage-contain': coveredImage && item.Type === 'TvChannel' } + ); + + return ( +
+ {cardOptions.disableIndicators !== true && ( + + {indicator.getMediaSourceIndicator()} + + + {cardOptions.missingIndicator !== false + && indicator.getMissingIndicator()} + + {indicator.getTimerIndicator()} + {indicator.getTypeIndicator()} + + {cardOptions.showGroupCount ? + indicator.getChildCountIndicator() : + indicator.getPlayedIndicator()} + + {(item.Type === 'CollectionFolder' + || item.CollectionType) + && item.RefreshProgress && ( + + )} + + + )} + + + + {overlayText && ( + + )} + + {!overlayText && indicator.getProgressBar()} +
+ ); +}; + +export default CardImageContainer; diff --git a/src/components/cardbuilder/Card/CardInnerFooter.tsx b/src/components/cardbuilder/Card/CardInnerFooter.tsx new file mode 100644 index 0000000000..d6edf853c0 --- /dev/null +++ b/src/components/cardbuilder/Card/CardInnerFooter.tsx @@ -0,0 +1,42 @@ +import React, { FC } from 'react'; +import classNames from 'classnames'; +import CardFooterText from './CardFooterText'; +import type { ItemDto } from 'types/itemDto'; +import type { CardOptions } from 'types/cardOptions'; + +interface CardInnerFooterProps { + item: ItemDto; + cardOptions: CardOptions; + imgUrl: string | undefined; + progressBar?: React.JSX.Element | null; + forceName: boolean; + overlayText: boolean | undefined; +} + +const CardInnerFooter: FC = ({ + item, + cardOptions, + imgUrl, + overlayText, + progressBar, + forceName +}) => { + const footerClass = classNames('innerCardFooter', { + fullInnerCardFooter: progressBar + }); + + return ( + + ); +}; + +export default CardInnerFooter; diff --git a/src/components/cardbuilder/Card/CardOuterFooter.tsx b/src/components/cardbuilder/Card/CardOuterFooter.tsx new file mode 100644 index 0000000000..020a64d584 --- /dev/null +++ b/src/components/cardbuilder/Card/CardOuterFooter.tsx @@ -0,0 +1,45 @@ +import React, { FC } from 'react'; +import classNames from 'classnames'; +import { useApi } from 'hooks/useApi'; +import { getCardLogoUrl } from './cardHelper'; +import CardFooterText from './CardFooterText'; + +import type { ItemDto } from 'types/itemDto'; +import type { CardOptions } from 'types/cardOptions'; + +interface CardOuterFooterProps { + item: ItemDto + cardOptions: CardOptions; + imgUrl: string | undefined; + forceName: boolean; + overlayText: boolean | undefined +} + +const CardOuterFooter: FC = ({ item, cardOptions, overlayText, imgUrl, forceName }) => { + const { api } = useApi(); + const logoInfo = getCardLogoUrl(item, api, cardOptions); + const logoUrl = logoInfo.logoUrl; + + const footerClass = classNames( + 'cardFooter', + { 'cardFooter-transparent': cardOptions.cardLayout }, + { 'cardFooter-withlogo': logoUrl } + ); + + return ( + + + ); +}; + +export default CardOuterFooter; diff --git a/src/components/cardbuilder/Card/CardOverlayButtons.tsx b/src/components/cardbuilder/Card/CardOverlayButtons.tsx new file mode 100644 index 0000000000..0ac8463969 --- /dev/null +++ b/src/components/cardbuilder/Card/CardOverlayButtons.tsx @@ -0,0 +1,96 @@ +import React, { FC } from 'react'; +import Box from '@mui/material/Box'; +import ButtonGroup from '@mui/material/ButtonGroup'; +import classNames from 'classnames'; + +import PlayArrowIconButton from '../../common/PlayArrowIconButton'; +import MoreVertIconButton from '../../common/MoreVertIconButton'; + +import type { ItemDto } from 'types/itemDto'; +import type { CardOptions } from 'types/cardOptions'; + +const sholudShowOverlayPlayButton = ( + overlayPlayButton: boolean | undefined, + item: ItemDto +) => { + return ( + overlayPlayButton + && !item.IsPlaceHolder + && (item.LocationType !== 'Virtual' + || !item.MediaType + || item.Type === 'Program') + && item.Type !== 'Person' + ); +}; + +interface CardOverlayButtonsProps { + item: ItemDto; + cardOptions: CardOptions; +} + +const CardOverlayButtons: FC = ({ + item, + cardOptions +}) => { + let overlayPlayButton = cardOptions.overlayPlayButton; + + if ( + overlayPlayButton == null + && !cardOptions.overlayMoreButton + && !cardOptions.overlayInfoButton + && !cardOptions.cardLayout + ) { + overlayPlayButton = item.MediaType === 'Video'; + } + + const btnCssClass = classNames( + 'paper-icon-button-light', + 'cardOverlayButton', + 'itemAction' + ); + + const centerPlayButtonClass = classNames( + btnCssClass, + 'cardOverlayButton-centered' + ); + + return ( + + {cardOptions.centerPlayButton && ( + + )} + + + {sholudShowOverlayPlayButton(overlayPlayButton, item) && ( + + )} + + {cardOptions.overlayMoreButton && ( + + )} + + + ); +}; + +export default CardOverlayButtons; diff --git a/src/components/cardbuilder/Card/CardText.tsx b/src/components/cardbuilder/Card/CardText.tsx new file mode 100644 index 0000000000..be6d0b049c --- /dev/null +++ b/src/components/cardbuilder/Card/CardText.tsx @@ -0,0 +1,33 @@ +import React, { FC } from 'react'; +import Box from '@mui/material/Box'; +import escapeHTML from 'escape-html'; +import type { TextLine } from './cardHelper'; + +interface CardTextProps { + className?: string; + textLine: TextLine; +} + +const CardText: FC = ({ className, textLine }) => { + const { title, titleAction } = textLine; + const renderCardText = () => { + if (titleAction) { + return ( + + {escapeHTML(titleAction.title)} + + ); + } else { + return title; + } + }; + + return {renderCardText()}; +}; + +export default CardText; diff --git a/src/components/cardbuilder/Card/CardWrapper.tsx b/src/components/cardbuilder/Card/CardWrapper.tsx new file mode 100644 index 0000000000..01d6446a91 --- /dev/null +++ b/src/components/cardbuilder/Card/CardWrapper.tsx @@ -0,0 +1,30 @@ +import React, { FC } from 'react'; +import layoutManager from 'components/layoutManager'; +import type { DataAttributes } from 'types/dataAttributes'; + +interface CardWrapperProps { + className: string; + dataAttributes: DataAttributes; +} + +const CardWrapper: FC = ({ + className, + dataAttributes, + children +}) => { + if (layoutManager.tv) { + return ( + + ); + } else { + return ( +
+ {children} +
+ ); + } +}; + +export default CardWrapper; diff --git a/src/components/cardbuilder/Card/Cards.tsx b/src/components/cardbuilder/Card/Cards.tsx new file mode 100644 index 0000000000..fcf2454a57 --- /dev/null +++ b/src/components/cardbuilder/Card/Cards.tsx @@ -0,0 +1,32 @@ +import React, { FC } from 'react'; +import { setCardData } from '../cardBuilder'; +import Card from './Card'; +import type { ItemDto } from 'types/itemDto'; +import type { CardOptions } from 'types/cardOptions'; +import '../card.scss'; + +interface CardsProps { + items: ItemDto[]; + cardOptions: CardOptions; +} + +const Cards: FC = ({ + items = [], + cardOptions +}) => { + setCardData(items, cardOptions); + return ( + // eslint-disable-next-line react/jsx-no-useless-fragment + <> + {items?.map((item) => ( + + ))} + + ); +}; + +export default Cards; diff --git a/src/components/cardbuilder/Card/cardHelper.ts b/src/components/cardbuilder/Card/cardHelper.ts new file mode 100644 index 0000000000..b6f8c37ab1 --- /dev/null +++ b/src/components/cardbuilder/Card/cardHelper.ts @@ -0,0 +1,721 @@ +import { + BaseItemDto, + BaseItemKind, + BaseItemPerson, + ImageType +} from '@jellyfin/sdk/lib/generated-client'; +import { Api } from '@jellyfin/sdk'; +import escapeHTML from 'escape-html'; + +import { appRouter } from 'components/router/appRouter'; +import layoutManager from 'components/layoutManager'; +import itemHelper from 'components/itemHelper'; +import globalize from 'scripts/globalize'; +import datetime from 'scripts/datetime'; + +import { isUsingLiveTvNaming } from '../cardBuilderUtils'; + +import type { ItemDto, NullableNumber, NullableString } from 'types/itemDto'; +import type { CardOptions } from 'types/cardOptions'; +import type { DataAttributes } from 'types/dataAttributes'; +import { getDataAttributes } from 'utils/items'; + +export function getCardLogoUrl( + item: ItemDto, + api: Api | undefined, + cardOptions: CardOptions +) { + let imgType; + let imgTag; + let itemId; + const logoHeight = 40; + + if (cardOptions.showChannelLogo && item.ChannelPrimaryImageTag) { + imgType = ImageType.Primary; + imgTag = item.ChannelPrimaryImageTag; + itemId = item.ChannelId; + } else if (cardOptions.showLogo && item.ParentLogoImageTag) { + imgType = ImageType.Logo; + imgTag = item.ParentLogoImageTag; + itemId = item.ParentLogoItemId; + } + + if (!itemId) { + itemId = item.Id; + } + + if (api && imgTag && imgType && itemId) { + const response = api.getItemImageUrl(itemId, imgType, { + height: logoHeight, + tag: imgTag + }); + + return { + logoUrl: response + }; + } + + return { + logoUrl: undefined + }; +} + +interface TextAction { + url: string; + title: string; + dataAttributes: DataAttributes +} + +export interface TextLine { + title?: NullableString; + titleAction?: TextAction; +} + +export function getTextActionButton( + item: ItemDto, + text?: NullableString, + serverId?: NullableString +): TextLine { + if (!text) { + text = itemHelper.getDisplayName(item); + } + + text = escapeHTML(text); + + if (layoutManager.tv) { + return { + title: text + }; + } + + const url = appRouter.getRouteUrl(item); + + const dataAttributes = getDataAttributes( + { + action: 'link', + itemServerId: serverId ?? item.ServerId, + itemId: item.Id, + itemChannelId: item.ChannelId, + itemType: item.Type, + itemMediaType: item.MediaType, + itemCollectionType: item.CollectionType, + itemIsFolder: item.IsFolder + } + ); + + return { + titleAction: { + url, + title: text, + dataAttributes + } + }; +} + +export function getAirTimeText( + item: ItemDto, + showAirDateTime: boolean | undefined, + showAirEndTime: boolean | undefined +) { + let airTimeText = ''; + + if (item.StartDate) { + try { + let date = datetime.parseISO8601Date(item.StartDate); + + if (showAirDateTime) { + airTimeText + += datetime.toLocaleDateString(date, { + weekday: 'short', + month: 'short', + day: 'numeric' + }) + ' '; + } + + airTimeText += datetime.getDisplayTime(date); + + if (item.EndDate && showAirEndTime) { + date = datetime.parseISO8601Date(item.EndDate); + airTimeText += ' - ' + datetime.getDisplayTime(date); + } + } catch (e) { + console.error('error parsing date: ' + item.StartDate); + } + } + return airTimeText; +} + +function isGenreOrStudio(itemType: NullableString) { + return itemType === BaseItemKind.Genre || itemType === BaseItemKind.Studio; +} + +function isMusicGenreOrMusicArtist( + itemType: NullableString, + context: NullableString +) { + return itemType === BaseItemKind.MusicGenre || context === 'MusicArtist'; +} + +function getMovieCount(itemMovieCount: NullableNumber) { + if (itemMovieCount) { + return itemMovieCount === 1 ? + globalize.translate('ValueOneMovie') : + globalize.translate('ValueMovieCount', itemMovieCount); + } +} + +function getSeriesCount(itemSeriesCount: NullableNumber) { + if (itemSeriesCount) { + return itemSeriesCount === 1 ? + globalize.translate('ValueOneSeries') : + globalize.translate('ValueSeriesCount', itemSeriesCount); + } +} + +function getEpisodeCount(itemEpisodeCount: NullableNumber) { + if (itemEpisodeCount) { + return itemEpisodeCount === 1 ? + globalize.translate('ValueOneEpisode') : + globalize.translate('ValueEpisodeCount', itemEpisodeCount); + } +} + +function getAlbumCount(itemAlbumCount: NullableNumber) { + if (itemAlbumCount) { + return itemAlbumCount === 1 ? + globalize.translate('ValueOneAlbum') : + globalize.translate('ValueAlbumCount', itemAlbumCount); + } +} + +function getSongCount(itemSongCount: NullableNumber) { + if (itemSongCount) { + return itemSongCount === 1 ? + globalize.translate('ValueOneSong') : + globalize.translate('ValueSongCount', itemSongCount); + } +} + +function getMusicVideoCount(itemMusicVideoCount: NullableNumber) { + if (itemMusicVideoCount) { + return itemMusicVideoCount === 1 ? + globalize.translate('ValueOneMusicVideo') : + globalize.translate('ValueMusicVideoCount', itemMusicVideoCount); + } +} + +function getRecursiveItemCount(itemRecursiveItemCount: NullableNumber) { + return itemRecursiveItemCount === 1 ? + globalize.translate('ValueOneEpisode') : + globalize.translate('ValueEpisodeCount', itemRecursiveItemCount); +} + +function getParentTitle( + isOuterFooter: boolean, + serverId: NullableString, + item: ItemDto +) { + if (isOuterFooter && item.AlbumArtists && item.AlbumArtists.length) { + (item.AlbumArtists[0] as BaseItemDto).Type = BaseItemKind.MusicArtist; + (item.AlbumArtists[0] as BaseItemDto).IsFolder = true; + return getTextActionButton(item.AlbumArtists[0], null, serverId); + } else { + return { + title: isUsingLiveTvNaming(item.Type) ? + item.Name : + item.SeriesName + || item.Series + || item.Album + || item.AlbumArtist + || '' + }; + } +} + +function getRunTimeTicks(itemRunTimeTicks: NullableNumber) { + if (itemRunTimeTicks) { + let minutes = itemRunTimeTicks / 600000000; + + minutes = minutes || 1; + + return globalize.translate('ValueMinutes', Math.round(minutes)); + } else { + return globalize.translate('ValueMinutes', 0); + } +} + +export function getItemCounts(cardOptions: CardOptions, item: ItemDto) { + const counts: string[] = []; + + const addCount = (text: NullableString) => { + if (text) { + counts.push(text); + } + }; + + if (item.Type === BaseItemKind.Playlist) { + const runTimeTicksText = getRunTimeTicks(item.RunTimeTicks); + addCount(runTimeTicksText); + } else if (isGenreOrStudio(item.Type)) { + const movieCountText = getMovieCount(item.MovieCount); + addCount(movieCountText); + + const seriesCountText = getSeriesCount(item.SeriesCount); + addCount(seriesCountText); + + const episodeCountText = getEpisodeCount(item.EpisodeCount); + addCount(episodeCountText); + } else if (isMusicGenreOrMusicArtist(item.Type, cardOptions.context)) { + const albumCountText = getAlbumCount(item.AlbumCount); + addCount(albumCountText); + + const songCountText = getSongCount(item.SongCount); + addCount(songCountText); + + const musicVideoCountText = getMusicVideoCount(item.MusicVideoCount); + addCount(musicVideoCountText); + } else if (item.Type === BaseItemKind.Series) { + const recursiveItemCountText = getRecursiveItemCount( + item.RecursiveItemCount + ); + addCount(recursiveItemCountText); + } + + return counts.join(', '); +} + +export function shouldShowTitle( + showTitle: boolean | string | undefined, + itemType: NullableString +) { + return ( + Boolean(showTitle) + || itemType === BaseItemKind.PhotoAlbum + || itemType === BaseItemKind.Folder + ); +} + +export function shouldShowOtherText( + isOuterFooter: boolean, + overlayText: boolean | undefined +) { + return isOuterFooter ? !overlayText : overlayText; +} + +export function shouldShowParentTitleUnderneath( + itemType: NullableString +) { + return ( + itemType === BaseItemKind.MusicAlbum + || itemType === BaseItemKind.Audio + || itemType === BaseItemKind.MusicVideo + ); +} + +function shouldShowMediaTitle( + titleAdded: boolean, + showTitle: boolean, + forceName: boolean, + cardOptions: CardOptions, + textLines: TextLine[] +) { + let showMediaTitle = + (showTitle && !titleAdded) + || (cardOptions.showParentTitleOrTitle && !textLines.length); + if (!showMediaTitle && !titleAdded && (showTitle || forceName)) { + showMediaTitle = true; + } + return showMediaTitle; +} + +function shouldShowExtraType(itemExtraType: NullableString) { + return itemExtraType && itemExtraType !== 'Unknown'; +} + +function shouldShowSeriesYearOrYear( + showYear: string | boolean | undefined, + showSeriesYear: boolean | undefined +) { + return Boolean(showYear) || showSeriesYear; +} + +function shouldShowCurrentProgram( + showCurrentProgram: boolean | undefined, + itemType: NullableString +) { + return showCurrentProgram && itemType === BaseItemKind.TvChannel; +} + +function shouldShowCurrentProgramTime( + showCurrentProgramTime: boolean | undefined, + itemType: NullableString +) { + return showCurrentProgramTime && itemType === BaseItemKind.TvChannel; +} + +function shouldShowPersonRoleOrType( + showPersonRoleOrType: boolean | undefined, + item: ItemDto +) { + return showPersonRoleOrType && (item as BaseItemPerson).Role; +} + +function shouldShowParentTitle( + showParentTitle: boolean | undefined, + parentTitleUnderneath: boolean +) { + return showParentTitle && parentTitleUnderneath; +} + +function addOtherText( + cardOptions: CardOptions, + parentTitleUnderneath: boolean, + isOuterFooter: boolean, + item: ItemDto, + addTextLine: (val: TextLine) => void, + serverId: NullableString +) { + if ( + shouldShowParentTitle( + cardOptions.showParentTitle, + parentTitleUnderneath + ) + ) { + addTextLine(getParentTitle(isOuterFooter, serverId, item)); + } + + if (shouldShowExtraType(item.ExtraType)) { + addTextLine({ title: globalize.translate(item.ExtraType) }); + } + + if (cardOptions.showItemCounts) { + addTextLine({ title: getItemCounts(cardOptions, item) }); + } + + if (cardOptions.textLines) { + addTextLine({ title: getAdditionalLines(cardOptions.textLines, item) }); + } + + if (cardOptions.showSongCount) { + addTextLine({ title: getSongCount(item.SongCount) }); + } + + if (cardOptions.showPremiereDate) { + addTextLine({ title: getPremiereDate(item.PremiereDate) }); + } + + if ( + shouldShowSeriesYearOrYear( + cardOptions.showYear, + cardOptions.showSeriesYear + ) + ) { + addTextLine({ title: getProductionYear(item) }); + } + + if (cardOptions.showRuntime) { + addTextLine({ title: getRunTime(item.RunTimeTicks) }); + } + + if (cardOptions.showAirTime) { + addTextLine({ + title: getAirTimeText( + item, + cardOptions.showAirDateTime, + cardOptions.showAirEndTime + ) + }); + } + + if (cardOptions.showChannelName) { + addTextLine(getChannelName(item)); + } + + if (shouldShowCurrentProgram(cardOptions.showCurrentProgram, item.Type)) { + addTextLine({ title: getCurrentProgramName(item.CurrentProgram) }); + } + + if ( + shouldShowCurrentProgramTime( + cardOptions.showCurrentProgramTime, + item.Type + ) + ) { + addTextLine({ title: getCurrentProgramTime(item.CurrentProgram) }); + } + + if (cardOptions.showSeriesTimerTime) { + addTextLine({ title: getSeriesTimerTime(item) }); + } + + if (cardOptions.showSeriesTimerChannel) { + addTextLine({ title: getSeriesTimerChannel(item) }); + } + + if (shouldShowPersonRoleOrType(cardOptions.showCurrentProgramTime, item)) { + addTextLine({ + title: globalize.translate( + 'PersonRole', + (item as BaseItemPerson).Role + ) + }); + } +} + +function getSeriesTimerChannel(item: ItemDto) { + if (item.RecordAnyChannel) { + return globalize.translate('AllChannels'); + } else { + return item.ChannelName || '' || globalize.translate('OneChannel'); + } +} + +function getSeriesTimerTime(item: ItemDto) { + if (item.RecordAnyTime) { + return globalize.translate('Anytime'); + } else { + return datetime.getDisplayTime(item.StartDate); + } +} + +function getCurrentProgramTime(CurrentProgram: BaseItemDto | undefined) { + if (CurrentProgram) { + return getAirTimeText(CurrentProgram, false, true) || ''; + } else { + return ''; + } +} + +function getCurrentProgramName(CurrentProgram: BaseItemDto | undefined) { + if (CurrentProgram) { + return CurrentProgram.Name; + } else { + return ''; + } +} + +function getChannelName(item: ItemDto) { + if (item.ChannelId) { + return getTextActionButton( + { + Id: item.ChannelId, + ServerId: item.ServerId, + Name: item.ChannelName, + Type: BaseItemKind.TvChannel, + MediaType: item.MediaType, + IsFolder: false + }, + item.ChannelName + ); + } else { + return { title: item.ChannelName || '' || ' ' }; + } +} + +function getRunTime(itemRunTimeTicks: NullableNumber) { + if (itemRunTimeTicks) { + return datetime.getDisplayRunningTime(itemRunTimeTicks); + } else { + return ''; + } +} + +function getPremiereDate(PremiereDate: string | null | undefined) { + if (PremiereDate) { + try { + return datetime.toLocaleDateString( + datetime.parseISO8601Date(PremiereDate), + { weekday: 'long', month: 'long', day: 'numeric' } + ); + } catch (err) { + return ''; + } + } else { + return ''; + } +} + +function getAdditionalLines( + textLines: (item: ItemDto) => (string | undefined)[], + item: ItemDto +) { + const additionalLines = textLines(item); + for (const additionalLine of additionalLines) { + return additionalLine; + } +} + +function getProductionYear(item: ItemDto) { + const productionYear = + item.ProductionYear + && datetime.toLocaleString(item.ProductionYear, { + useGrouping: false + }); + if (item.Type === BaseItemKind.Series) { + if (item.Status === 'Continuing') { + return globalize.translate( + 'SeriesYearToPresent', + productionYear || '' + ); + } else if (item.EndDate && item.ProductionYear) { + const endYear = datetime.toLocaleString( + datetime.parseISO8601Date(item.EndDate).getFullYear(), + { useGrouping: false } + ); + return ( + productionYear + + (endYear === productionYear ? '' : ' - ' + endYear) + ); + } else { + return productionYear || ''; + } + } else { + return productionYear || ''; + } +} + +function getMediaTitle(cardOptions: CardOptions, item: ItemDto): TextLine { + const name = + cardOptions.showTitle === 'auto' + && !item.IsFolder + && item.MediaType === 'Photo' ? + '' : + itemHelper.getDisplayName(item, { + includeParentInfo: cardOptions.includeParentInfoInTitle + }); + + return getTextActionButton({ + Id: item.Id, + ServerId: item.ServerId, + Name: name, + Type: item.Type, + CollectionType: item.CollectionType, + IsFolder: item.IsFolder + }); +} + +function getParentTitleOrTitle( + isOuterFooter: boolean, + item: ItemDto, + setTitleAdded: (val: boolean) => void, + showTitle: boolean +): TextLine { + if ( + isOuterFooter + && item.Type === BaseItemKind.Episode + && item.SeriesName + ) { + if (item.SeriesId) { + return getTextActionButton({ + Id: item.SeriesId, + ServerId: item.ServerId, + Name: item.SeriesName, + Type: BaseItemKind.Series, + IsFolder: true + }); + } else { + return { title: item.SeriesName }; + } + } else if (isUsingLiveTvNaming(item.Type)) { + if (!item.EpisodeTitle && !item.IndexNumber) { + setTitleAdded(true); + } + return { title: item.Name }; + } else { + const parentTitle = + item.SeriesName + || item.Series + || item.Album + || item.AlbumArtist + || ''; + + if (parentTitle || showTitle) { + return { title: parentTitle }; + } + + return { title: '' }; + } +} + +interface TextLinesOpts { + isOuterFooter: boolean; + overlayText: boolean | undefined; + forceName: boolean; + item: ItemDto; + cardOptions: CardOptions; + imgUrl: string | undefined; +} + +export function getCardTextLines({ + isOuterFooter, + overlayText, + forceName, + item, + cardOptions, + imgUrl +}: TextLinesOpts) { + const showTitle = shouldShowTitle(cardOptions.showTitle, item.Type); + const showOtherText = shouldShowOtherText(isOuterFooter, overlayText); + const serverId = item.ServerId || cardOptions.serverId; + let textLines: TextLine[] = []; + const parentTitleUnderneath = shouldShowParentTitleUnderneath(item.Type); + + let titleAdded = false; + const addTextLine = (val: TextLine) => { + textLines.push(val); + }; + + const setTitleAdded = (val: boolean) => { + titleAdded = val; + }; + + if ( + showOtherText + && (cardOptions.showParentTitle || cardOptions.showParentTitleOrTitle) + && !parentTitleUnderneath + ) { + addTextLine( + getParentTitleOrTitle(isOuterFooter, item, setTitleAdded, showTitle) + ); + } + + const showMediaTitle = shouldShowMediaTitle( + titleAdded, + showTitle, + forceName, + cardOptions, + textLines + ); + + if (showMediaTitle) { + addTextLine(getMediaTitle(cardOptions, item)); + } + + if (showOtherText) { + addOtherText( + cardOptions, + parentTitleUnderneath, + isOuterFooter, + item, + addTextLine, + serverId + ); + } + + if ( + (showTitle || !imgUrl) + && forceName + && overlayText + && textLines.length === 1 + ) { + textLines = []; + } + + if (overlayText && showTitle) { + textLines = [{ title: item.Name }]; + } + + return { + textLines + }; +} diff --git a/src/components/cardbuilder/Card/useCard.ts b/src/components/cardbuilder/Card/useCard.ts new file mode 100644 index 0000000000..5751471801 --- /dev/null +++ b/src/components/cardbuilder/Card/useCard.ts @@ -0,0 +1,121 @@ +import classNames from 'classnames'; +import useCardImageUrl from './useCardImageUrl'; +import { + resolveAction, + resolveMixedShapeByAspectRatio +} from '../cardBuilderUtils'; +import { getDataAttributes } from 'utils/items'; +import layoutManager from 'components/layoutManager'; + +import type { ItemDto } from 'types/itemDto'; +import type { CardOptions } from 'types/cardOptions'; + +interface UseCardProps { + item: ItemDto; + cardOptions: CardOptions; +} + +function useCard({ item, cardOptions }: UseCardProps) { + const action = resolveAction({ + defaultAction: cardOptions.action ?? 'link', + isFolder: item.IsFolder ?? false, + isPhoto: item.MediaType === 'Photo' + }); + + let shape = cardOptions.shape; + + if (shape === 'mixed') { + shape = resolveMixedShapeByAspectRatio(item.PrimaryImageAspectRatio); + } + + const imgInfo = useCardImageUrl({ + item: item.ProgramInfo ?? item, + cardOptions, + shape + }); + const imgUrl = imgInfo.imgUrl; + const blurhash = imgInfo.blurhash; + const forceName = imgInfo.forceName; + const coveredImage = cardOptions.coverImage ?? imgInfo.coverImage; + const overlayText = cardOptions.overlayText; + + const nameWithPrefix = item.SortName ?? item.Name ?? ''; + let prefix = nameWithPrefix.substring( + 0, + Math.min(3, nameWithPrefix.length) + ); + + if (prefix) { + prefix = prefix.toUpperCase(); + } + + const dataAttributes = getDataAttributes( + { + action, + itemServerId: item.ServerId ?? cardOptions.serverId, + context: cardOptions.context, + parentId: cardOptions.parentId, + collectionId: cardOptions.collectionId, + playlistId: cardOptions.playlistId, + itemId: item.Id, + itemTimerId: item.TimerId, + itemSeriesTimerId: item.SeriesTimerId, + itemChannelId: item.ChannelId, + itemType: item.Type, + itemMediaType: item.MediaType, + itemCollectionType: item.CollectionType, + itemIsFolder: item.IsFolder, + itemPath: item.Path, + itemStartDate: item.StartDate, + itemEndDate: item.EndDate, + itemUserData: item.UserData, + prefix + } + ); + + const cardClass = classNames( + 'card', + { [`${shape}Card`]: shape }, + cardOptions.cardCssClass, + cardOptions.cardClass, + { 'card-hoverable': layoutManager.desktop }, + { groupedCard: cardOptions.showChildCountIndicator && item.ChildCount }, + { + 'card-withuserdata': + item.Type !== 'MusicAlbum' + && item.Type !== 'MusicArtist' + && item.Type !== 'Audio' + }, + { itemAction: layoutManager.tv } + ); + + const cardBoxClass = classNames( + 'cardBox', + { visualCardBox: cardOptions.cardLayout }, + { 'cardBox-bottompadded': !cardOptions.cardLayout } + ); + + const getCardWrapperProps = () => ({ + className: cardClass, + dataAttributes + }); + + const getCardBoxProps = () => ({ + item, + cardOptions, + className: cardBoxClass, + shape, + imgUrl, + blurhash, + forceName, + coveredImage, + overlayText + }); + + return { + getCardWrapperProps, + getCardBoxProps + }; +} + +export default useCard; diff --git a/src/components/cardbuilder/Card/useCardImageUrl.ts b/src/components/cardbuilder/Card/useCardImageUrl.ts new file mode 100644 index 0000000000..ef1cb6ab61 --- /dev/null +++ b/src/components/cardbuilder/Card/useCardImageUrl.ts @@ -0,0 +1,301 @@ +import { BaseItemKind, ImageType } from '@jellyfin/sdk/lib/generated-client'; +import { useApi } from 'hooks/useApi'; +import { getDesiredAspect } from '../cardBuilderUtils'; + +import type { ItemDto } from 'types/itemDto'; +import type { CardOptions } from 'types/cardOptions'; + +type ShapeType = string | null | undefined; + +function getPreferThumbInfo(item: ItemDto, cardOptions: CardOptions) { + let imgType; + let itemId; + let imgTag; + let forceName = false; + + if (item.ImageTags?.Thumb) { + imgType = ImageType.Thumb; + imgTag = item.ImageTags.Thumb; + itemId = item.Id; + } else if (item.SeriesThumbImageTag && cardOptions.inheritThumb !== false) { + imgType = ImageType.Thumb; + imgTag = item.SeriesThumbImageTag; + itemId = item.SeriesId; + } else if ( + item.ParentThumbItemId + && cardOptions.inheritThumb !== false + && item.MediaType !== 'Photo' + ) { + imgType = ImageType.Thumb; + imgTag = item.ParentThumbImageTag; + itemId = item.ParentThumbItemId; + } else if (item.BackdropImageTags?.length) { + imgType = ImageType.Backdrop; + imgTag = item.BackdropImageTags[0]; + itemId = item.Id; + forceName = true; + } else if ( + item.ParentBackdropImageTags?.length + && cardOptions.inheritThumb !== false + && item.Type === BaseItemKind.Episode + ) { + imgType = ImageType.Backdrop; + imgTag = item.ParentBackdropImageTags[0]; + itemId = item.ParentBackdropItemId; + } + return { + itemId: itemId, + imgTag: imgTag, + imgType: imgType, + forceName: forceName + }; +} + +function getPreferLogoInfo(item: ItemDto) { + let imgType; + let itemId; + let imgTag; + + if (item.ImageTags?.Logo) { + imgType = ImageType.Logo; + imgTag = item.ImageTags.Logo; + itemId = item.Id; + } else if (item.ParentLogoImageTag && item.ParentLogoItemId) { + imgType = ImageType.Logo; + imgTag = item.ParentLogoImageTag; + itemId = item.ParentLogoItemId; + } + return { + itemId: itemId, + imgTag: imgTag, + imgType: imgType + }; +} + +function getCalculatedHeight( + width: number | undefined, + primaryImageAspectRatio: number | null | undefined +) { + if (width && primaryImageAspectRatio) { + return Math.round(width / primaryImageAspectRatio); + } +} + +function isForceName(cardOptions: CardOptions) { + return !!(cardOptions.preferThumb && cardOptions.showTitle !== false); +} + +function isCoverImage( + primaryImageAspectRatio: number | null | undefined, + uiAspect: number | null +) { + if (primaryImageAspectRatio && uiAspect) { + return Math.abs(primaryImageAspectRatio - uiAspect) / uiAspect <= 0.2; + } + + return false; +} + +function shouldShowPreferBanner( + imageTagsBanner: string | undefined, + cardOptions: CardOptions, + shape: ShapeType +): boolean { + return ( + (cardOptions.preferBanner || shape === 'banner') + && Boolean(imageTagsBanner) + ); +} + +function shouldShowPreferDisc( + imageTagsDisc: string | undefined, + cardOptions: CardOptions +): boolean { + return cardOptions.preferDisc === true && Boolean(imageTagsDisc); +} + +function shouldShowImageTagsPrimary(item: ItemDto): boolean { + return ( + Boolean(item.ImageTags?.Primary) && (item.Type !== BaseItemKind.Episode || item.ChildCount !== 0) + ); +} + +function shouldShowImageTagsThumb(item: ItemDto): boolean { + return item.Type === BaseItemKind.Season && Boolean(item.ImageTags?.Thumb); +} + +function shouldShowSeriesThumbImageTag( + item: ItemDto, + cardOptions: CardOptions +): boolean { + return ( + Boolean(item.SeriesThumbImageTag) && cardOptions.inheritThumb !== false + ); +} + +function shouldShowParentThumbImageTag( + item: ItemDto, + cardOptions: CardOptions +): boolean { + return ( + Boolean(item.ParentThumbItemId) && cardOptions.inheritThumb !== false + ); +} + +function shouldShowParentBackdropImageTags(item: ItemDto): boolean { + return Boolean(item.AlbumId) && Boolean(item.AlbumPrimaryImageTag); +} + +function shouldShowPreferThumb(type: string | null | undefined, cardOptions: CardOptions): boolean { + return Boolean(cardOptions.preferThumb) && !(type === BaseItemKind.Program || type === BaseItemKind.Episode); +} + +function getCardImageInfo( + item: ItemDto, + cardOptions: CardOptions, + shape: ShapeType +) { + const width = cardOptions.width; + let height; + const primaryImageAspectRatio = item.PrimaryImageAspectRatio; + let forceName = false; + let imgTag; + let coverImage = false; + const uiAspect = getDesiredAspect(shape); + let imgType; + let itemId; + + if (shouldShowPreferThumb(item.Type, cardOptions)) { + const preferThumbInfo = getPreferThumbInfo(item, cardOptions); + imgType = preferThumbInfo.imgType; + imgTag = preferThumbInfo.imgTag; + itemId = preferThumbInfo.itemId; + forceName = preferThumbInfo.forceName; + } else if (shouldShowPreferBanner(item.ImageTags?.Banner, cardOptions, shape)) { + imgType = ImageType.Banner; + imgTag = item.ImageTags?.Banner; + itemId = item.Id; + } else if (shouldShowPreferDisc(item.ImageTags?.Disc, cardOptions)) { + imgType = ImageType.Disc; + imgTag = item.ImageTags?.Disc; + itemId = item.Id; + } else if (cardOptions.preferLogo) { + const preferLogoInfo = getPreferLogoInfo(item); + imgType = preferLogoInfo.imgType; + imgTag = preferLogoInfo.imgType; + itemId = preferLogoInfo.itemId; + } else if (shouldShowImageTagsPrimary(item)) { + imgType = ImageType.Primary; + imgTag = item.ImageTags?.Primary; + itemId = item.Id; + height = getCalculatedHeight(width, primaryImageAspectRatio); + forceName = isForceName(cardOptions); + coverImage = isCoverImage(primaryImageAspectRatio, uiAspect); + } else if (item.SeriesPrimaryImageTag) { + imgType = ImageType.Primary; + imgTag = item.SeriesPrimaryImageTag; + itemId = item.SeriesId; + } else if (item.PrimaryImageTag) { + imgType = ImageType.Primary; + imgTag = item.PrimaryImageTag; + itemId = item.PrimaryImageItemId; + height = getCalculatedHeight(width, primaryImageAspectRatio); + forceName = isForceName(cardOptions); + coverImage = isCoverImage(primaryImageAspectRatio, uiAspect); + } else if (item.ParentPrimaryImageTag) { + imgType = ImageType.Primary; + imgTag = item.ParentPrimaryImageTag; + itemId = item.ParentPrimaryImageItemId; + } else if (shouldShowParentBackdropImageTags(item)) { + imgType = ImageType.Primary; + imgTag = item.AlbumPrimaryImageTag; + itemId = item.AlbumId; + height = getCalculatedHeight(width, primaryImageAspectRatio); + forceName = isForceName(cardOptions); + coverImage = isCoverImage(primaryImageAspectRatio, uiAspect); + } else if (shouldShowImageTagsThumb(item)) { + imgType = ImageType.Thumb; + imgTag = item.ImageTags?.Thumb; + itemId = item.Id; + } else if (item.BackdropImageTags?.length) { + imgType = ImageType.Backdrop; + imgTag = item.BackdropImageTags[0]; + itemId = item.Id; + /*} else if (item.ImageTags?.Thumb) { + imgType = ImageType.Thumb; + imgTag = item.ImageTags.Thumb; + itemId = item.Id;*/ + } else if (shouldShowSeriesThumbImageTag(item, cardOptions)) { + imgType = ImageType.Thumb; + imgTag = item.SeriesThumbImageTag; + itemId = item.SeriesId; + } else if (shouldShowParentThumbImageTag(item, cardOptions)) { + imgType = ImageType.Thumb; + imgTag = item.ParentThumbImageTag; + itemId = item.ParentThumbItemId; + } else if ( + item.ParentBackdropImageTags?.length + && cardOptions.inheritThumb !== false + ) { + imgType = ImageType.Backdrop; + imgTag = item.ParentBackdropImageTags[0]; + itemId = item.ParentBackdropItemId; + } + + return { + imgType, + imgTag, + itemId, + width, + height, + forceName, + coverImage + }; +} + +interface UseCardImageUrlProps { + item: ItemDto; + cardOptions: CardOptions; + shape: ShapeType; +} + +function useCardImageUrl({ item, cardOptions, shape }: UseCardImageUrlProps) { + const { api } = useApi(); + const imgInfo = getCardImageInfo(item, cardOptions, shape); + + let width = imgInfo.width; + let height = imgInfo.height; + const imgTag = imgInfo.imgTag; + const imgType = imgInfo.imgType; + const itemId = imgInfo.itemId; + const ratio = window.devicePixelRatio || 1; + let imgUrl; + let blurhash; + + if (api && imgTag && imgType && itemId) { + if (width) { + width = Math.round(width * ratio); + } + + if (height) { + height = Math.round(height * ratio); + } + imgUrl = api?.getItemImageUrl(itemId, imgType, { + quality: 96, + fillWidth: width, + fillHeight: height, + tag: imgTag + }); + + blurhash = item?.ImageBlurHashes?.[imgType]?.[imgTag]; + } + + return { + imgUrl: imgUrl, + blurhash: blurhash, + forceName: imgInfo.forceName, + coverImage: imgInfo.coverImage + }; +} + +export default useCardImageUrl; diff --git a/src/components/cardbuilder/Card/useCardText.tsx b/src/components/cardbuilder/Card/useCardText.tsx new file mode 100644 index 0000000000..904777fe85 --- /dev/null +++ b/src/components/cardbuilder/Card/useCardText.tsx @@ -0,0 +1,113 @@ +import React from 'react'; +import Box from '@mui/material/Box'; +import classNames from 'classnames'; +import layoutManager from 'components/layoutManager'; +import CardText from './CardText'; +import { getCardTextLines } from './cardHelper'; + +import type { ItemDto } from 'types/itemDto'; +import type { CardOptions } from 'types/cardOptions'; + +const enableRightMargin = ( + isOuterFooter: boolean, + cardLayout: boolean | null | undefined, + centerText: boolean | undefined, + cardFooterAside: string | undefined +) => { + return ( + isOuterFooter + && cardLayout + && !centerText + && cardFooterAside !== 'none' + && layoutManager.mobile + ); +}; + +interface UseCardTextProps { + item: ItemDto; + cardOptions: CardOptions; + forceName: boolean; + overlayText: boolean | undefined; + imgUrl: string | undefined; + isOuterFooter: boolean; + cssClass: string; + forceLines: boolean; + maxLines: number | undefined; +} + +function useCardText({ + item, + cardOptions, + forceName, + imgUrl, + overlayText, + isOuterFooter, + cssClass, + forceLines, + maxLines +}: UseCardTextProps) { + const { textLines } = getCardTextLines({ + isOuterFooter, + overlayText, + forceName, + item, + cardOptions, + imgUrl + }); + + const addRightMargin = enableRightMargin( + isOuterFooter, + cardOptions.cardLayout, + cardOptions.centerText, + cardOptions.cardFooterAside + ); + + const renderCardTextLines = () => { + const components: React.ReactNode[] = []; + let valid = 0; + for (const textLine of textLines) { + const currentCssClass = classNames( + cssClass, + { + 'cardText-secondary': + valid > 0 && isOuterFooter + }, + { 'cardText-first': valid === 0 && isOuterFooter }, + { 'cardText-rightmargin': addRightMargin } + ); + + if (textLine) { + components.push( + + ); + + valid++; + if (maxLines && valid >= maxLines) { + break; + } + } + } + + if (forceLines) { + const linesLength = maxLines ?? Math.min(textLines.length, maxLines ?? textLines.length); + while (valid < linesLength) { + components.push( + +   + + ); + valid++; + } + } + + return components; + }; + + const cardTextLines = renderCardTextLines(); + + return { + cardTextLines + }; +} + +export default useCardText; diff --git a/src/components/cardbuilder/card.scss b/src/components/cardbuilder/card.scss index 28f55abe2d..731ef32a80 100644 --- a/src/components/cardbuilder/card.scss +++ b/src/components/cardbuilder/card.scss @@ -378,7 +378,7 @@ button::-moz-focus-inner { margin-right: 2em; } -.cardDefaultText { +.cardImageContainer > .cardDefaultText { white-space: normal; text-align: center; font-size: 2em; @@ -408,6 +408,7 @@ button::-moz-focus-inner { display: flex; align-items: center; contain: layout style; + z-index: 1; [dir="ltr"] & { right: 0.225em; @@ -852,7 +853,7 @@ button::-moz-focus-inner { opacity: 1; } -.cardOverlayFab-primary { +.cardOverlayContainer > .cardOverlayFab-primary { background-color: rgba(0, 0, 0, 0.7); font-size: 130%; padding: 0; @@ -865,7 +866,7 @@ button::-moz-focus-inner { left: 50%; } -.cardOverlayFab-primary:hover { +.cardOverlayContainer > .cardOverlayFab-primary:hover { transform: scale(1.4, 1.4); transition: 0.2s; } diff --git a/src/components/cardbuilder/cardBuilder.js b/src/components/cardbuilder/cardBuilder.js index 811bac1851..acbe05c270 100644 --- a/src/components/cardbuilder/cardBuilder.js +++ b/src/components/cardbuilder/cardBuilder.js @@ -73,7 +73,7 @@ function getImageWidth(shape, screenWidth, isOrientationLandscape) { * @param {Object} items - A set of items. * @param {Object} options - Options for handling the items. */ -function setCardData(items, options) { +export function setCardData(items, options) { options.shape = options.shape || 'auto'; const primaryImageAspectRatio = imageLoader.getPrimaryImageAspectRatio(items); diff --git a/src/components/cardbuilder/cardBuilderUtils.ts b/src/components/cardbuilder/cardBuilderUtils.ts index d7215b190c..3ac471ccb1 100644 --- a/src/components/cardbuilder/cardBuilderUtils.ts +++ b/src/components/cardbuilder/cardBuilderUtils.ts @@ -10,10 +10,10 @@ const ASPECT_RATIOS = { /** * Determines if the item is live TV. - * @param {string} itemType - Item type to use for the check. + * @param {string | null | undefined} itemType - Item type to use for the check. * @returns {boolean} Flag showing if the item is live TV. */ -export const isUsingLiveTvNaming = (itemType: string): boolean => itemType === 'Program' || itemType === 'Timer' || itemType === 'Recording'; +export const isUsingLiveTvNaming = (itemType: string | null | undefined): boolean => itemType === 'Program' || itemType === 'Timer' || itemType === 'Recording'; /** * Resolves Card action to display diff --git a/src/types/cardOptions.ts b/src/types/cardOptions.ts index cb4f49c0af..3745d804a4 100644 --- a/src/types/cardOptions.ts +++ b/src/types/cardOptions.ts @@ -1,10 +1,12 @@ -import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client'; +import type { BaseItemDtoImageBlurHashes, BaseItemKind, ImageType, UserItemDataDto } from '@jellyfin/sdk/lib/generated-client'; import { CollectionType } from '@jellyfin/sdk/lib/generated-client/models/collection-type'; +import type { ItemDto, NullableString } from './itemDto'; +import { ParentId } from './library'; export interface CardOptions { itemsContainer?: HTMLElement | null; parentContainer?: HTMLElement | null; - items?: BaseItemDto[] | null; + items?: ItemDto[] | null; allowBottomPadding?: boolean; centerText?: boolean; coverImage?: boolean; @@ -12,13 +14,15 @@ export interface CardOptions { overlayMoreButton?: boolean; overlayPlayButton?: boolean; overlayText?: boolean; + imageBlurhashes?: BaseItemDtoImageBlurHashes | null; + preferBanner?: boolean; preferThumb?: boolean | string | null; preferDisc?: boolean; preferLogo?: boolean; scalable?: boolean; shape?: string | null; lazy?: boolean; - cardLayout?: boolean | string; + cardLayout?: boolean | null; showParentTitle?: boolean; showParentTitleOrTitle?: boolean; showAirTime?: boolean; @@ -37,7 +41,7 @@ export interface CardOptions { action?: string | null; defaultShape?: string; indexBy?: string; - parentId?: string | null; + parentId?: ParentId; showMenu?: boolean; cardCssClass?: string | null; cardClass?: string | null; @@ -61,9 +65,10 @@ export interface CardOptions { showSeriesTimerChannel?: boolean; showSongCount?: boolean; width?: number; + widths?: any; showChannelLogo?: boolean; showLogo?: boolean; - serverId?: string; + serverId?: NullableString; collectionId?: string | null; playlistId?: string | null; defaultCardImageIcon?: string; @@ -72,4 +77,46 @@ export interface CardOptions { showGroupCount?: boolean; containerClass?: string; noItemsMessage?: string; + showIndex?: boolean; + index?: string; + showIndexNumber?: boolean; + enableContentWrapper?: boolean; + enableOverview?: boolean; + enablePlayedButton?: boolean; + infoButton?: boolean; + imageSize?: string; + enableSideMediaInfo?: boolean; + imagePlayButton?: boolean; + border?: boolean; + highlight?: boolean; + smallIcon?: boolean; + artist?: boolean; + addToListButton?: boolean; + enableUserDataButtons?: boolean; + enableRatingButton?: boolean; + image?: boolean; + imageSource?: string; + showProgramDateTime?: boolean; + showChannel?: boolean; + mediaInfo?: boolean; + moreButton?: boolean; + recordButton?: boolean; + dragHandle?: boolean; + showProgramTime?: boolean; + parentTitleWithTitle?: boolean; + showIndexNumberLeft?: boolean; + sortBy?: string; + textLines?: (item: ItemDto) => (BaseItemKind | string | undefined)[]; + userData?: UserItemDataDto; + rightButtons?: { + icon: string; + title: string; + id: string; + }[]; + uiAspect?: number | null; + primaryImageAspectRatio?: number | null; + rows?: number | null; + imageType?: ImageType; + queryKey?: string[] } + From 31a77c25f3b8e793f7a7a02ad27427c35d4161e3 Mon Sep 17 00:00:00 2001 From: grafixeyehero Date: Wed, 31 Jan 2024 04:32:54 +0300 Subject: [PATCH 08/24] Update favorite and played state to use Query Invalidation --- .../library/ProgramsSectionView.tsx | 7 ++- .../library/SuggestionsSectionView.tsx | 2 + .../emby-playstatebutton/PlayedButton.tsx | 35 +++++++---- .../emby-ratingbutton/FavoriteButton.tsx | 31 +++++---- src/hooks/useFetchItems.ts | 63 +++++++++---------- 5 files changed, 79 insertions(+), 59 deletions(-) diff --git a/src/apps/experimental/components/library/ProgramsSectionView.tsx b/src/apps/experimental/components/library/ProgramsSectionView.tsx index ac39d899b7..b15f319789 100644 --- a/src/apps/experimental/components/library/ProgramsSectionView.tsx +++ b/src/apps/experimental/components/library/ProgramsSectionView.tsx @@ -18,7 +18,7 @@ const ProgramsSectionView: FC = ({ sectionType, isUpcomingRecordingsEnabled = false }) => { - const { isLoading, data: sectionsWithItems } = useGetProgramsSectionsWithItems(parentId, sectionType); + const { isLoading, data: sectionsWithItems, refetch } = useGetProgramsSectionsWithItems(parentId, sectionType); const { isLoading: isUpcomingRecordingsLoading, data: upcomingRecordings @@ -60,8 +60,10 @@ const ProgramsSectionView: FC = ({ sectionTitle={globalize.translate(section.name)} items={items ?? []} url={getRouteUrl(section)} + reloadItems={refetch} cardOptions={{ - ...section.cardOptions + ...section.cardOptions, + queryKey: ['ProgramSectionWithItems'] }} /> @@ -73,6 +75,7 @@ const ProgramsSectionView: FC = ({ sectionTitle={group.name} items={group.timerInfo ?? []} cardOptions={{ + queryKey: ['Timers'], shape: 'overflowBackdrop', showTitle: true, showParentTitleOrTitle: true, diff --git a/src/apps/experimental/components/library/SuggestionsSectionView.tsx b/src/apps/experimental/components/library/SuggestionsSectionView.tsx index 039f49e4c6..d41270e4a6 100644 --- a/src/apps/experimental/components/library/SuggestionsSectionView.tsx +++ b/src/apps/experimental/components/library/SuggestionsSectionView.tsx @@ -102,6 +102,7 @@ const SuggestionsSectionView: FC = ({ url={getRouteUrl(section)} cardOptions={{ ...section.cardOptions, + queryKey: ['SuggestionSectionWithItems'], showTitle: true, centerText: true, cardLayout: false, @@ -117,6 +118,7 @@ const SuggestionsSectionView: FC = ({ sectionTitle={getRecommendationTittle(recommendation)} items={recommendation.Items ?? []} cardOptions={{ + queryKey: ['MovieRecommendations'], shape: 'overflowPortrait', showYear: true, scalable: true, diff --git a/src/elements/emby-playstatebutton/PlayedButton.tsx b/src/elements/emby-playstatebutton/PlayedButton.tsx index 89f4052b11..687953fb91 100644 --- a/src/elements/emby-playstatebutton/PlayedButton.tsx +++ b/src/elements/emby-playstatebutton/PlayedButton.tsx @@ -1,4 +1,5 @@ import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client'; +import { useQueryClient } from '@tanstack/react-query'; import React, { FC, useCallback } from 'react'; import CheckIcon from '@mui/icons-material/Check'; import { IconButton } from '@mui/material'; @@ -10,28 +11,30 @@ interface PlayedButtonProps { className?: string; isPlayed : boolean | undefined; itemId: string | null | undefined; - itemType: string | null | undefined + itemType: string | null | undefined, + queryKey?: string[] } const PlayedButton: FC = ({ className, isPlayed = false, itemId, - itemType + itemType, + queryKey }) => { + const queryClient = useQueryClient(); const { mutateAsync: togglePlayedMutation } = useTogglePlayedMutation(); - const [playedState, setPlayedState] = React.useState(isPlayed); const getTitle = useCallback(() => { let buttonTitle; if (itemType !== BaseItemKind.AudioBook) { - buttonTitle = playedState ? globalize.translate('Watched') : globalize.translate('MarkPlayed'); + buttonTitle = isPlayed ? globalize.translate('Watched') : globalize.translate('MarkPlayed'); } else { - buttonTitle = playedState ? globalize.translate('Played') : globalize.translate('MarkPlayed'); + buttonTitle = isPlayed ? globalize.translate('Played') : globalize.translate('MarkPlayed'); } return buttonTitle; - }, [playedState, itemType]); + }, [itemType, isPlayed]); const onClick = useCallback(async () => { try { @@ -39,23 +42,29 @@ const PlayedButton: FC = ({ throw new Error('Item has no Id'); } - const _isPlayed = await togglePlayedMutation({ + await togglePlayedMutation({ itemId, - playedState - }); - setPlayedState(!!_isPlayed); + isPlayed + }, + { onSuccess: async() => { + await queryClient.invalidateQueries({ + queryKey: queryKey, + type: 'all', + refetchType: 'active' + }); + } }); } catch (e) { console.error(e); } - }, [playedState, itemId, togglePlayedMutation]); + }, [itemId, togglePlayedMutation, isPlayed, queryClient, queryKey]); const btnClass = classNames( className, - { 'playstatebutton-played': playedState } + { 'playstatebutton-played': isPlayed } ); const iconClass = classNames( - { 'playstatebutton-icon-played': playedState } + { 'playstatebutton-icon-played': isPlayed } ); return ( = ({ className, isFavorite = false, - itemId + itemId, + queryKey }) => { + const queryClient = useQueryClient(); const { mutateAsync: toggleFavoriteMutation } = useToggleFavoriteMutation(); - const [favoriteState, setFavoriteState] = React.useState(isFavorite); const onClick = useCallback(async () => { try { @@ -25,28 +28,34 @@ const FavoriteButton: FC = ({ throw new Error('Item has no Id'); } - const _isFavorite = await toggleFavoriteMutation({ + await toggleFavoriteMutation({ itemId, - favoriteState - }); - setFavoriteState(!!_isFavorite); + isFavorite + }, + { onSuccess: async() => { + await queryClient.invalidateQueries({ + queryKey: queryKey, + type: 'all', + refetchType: 'active' + }); + } }); } catch (e) { console.error(e); } - }, [favoriteState, itemId, toggleFavoriteMutation]); + }, [isFavorite, itemId, queryClient, queryKey, toggleFavoriteMutation]); const btnClass = classNames( className, - { 'ratingbutton-withrating': favoriteState } + { 'ratingbutton-withrating': isFavorite } ); const iconClass = classNames( - { 'ratingbutton-icon-withrating': favoriteState } + { 'ratingbutton-icon-withrating': isFavorite } ); return ( fetchGetItemsViewByType( @@ -526,17 +528,17 @@ export const useGetGroupsUpcomingEpisodes = (parentId: ParentId) => { interface ToggleFavoriteMutationProp { itemId: string; - favoriteState: boolean + isFavorite: boolean } const fetchUpdateFavoriteStatus = async ( currentApi: JellyfinApiContext, itemId: string, - favoriteState: boolean + isFavorite: boolean ) => { const { api, user } = currentApi; if (api && user?.Id) { - if (favoriteState) { + if (isFavorite) { const response = await getUserLibraryApi(api).unmarkFavoriteItem({ userId: user.Id, itemId: itemId @@ -555,24 +557,24 @@ const fetchUpdateFavoriteStatus = async ( export const useToggleFavoriteMutation = () => { const currentApi = useApi(); return useMutation({ - mutationFn: ({ itemId, favoriteState }: ToggleFavoriteMutationProp) => - fetchUpdateFavoriteStatus(currentApi, itemId, favoriteState ) + mutationFn: ({ itemId, isFavorite }: ToggleFavoriteMutationProp) => + fetchUpdateFavoriteStatus(currentApi, itemId, isFavorite ) }); }; interface TogglePlayedMutationProp { itemId: string; - playedState: boolean + isPlayed: boolean } const fetchUpdatePlayedState = async ( currentApi: JellyfinApiContext, itemId: string, - playedState: boolean + isPlayed: boolean ) => { const { api, user } = currentApi; if (api && user?.Id) { - if (playedState) { + if (isPlayed) { const response = await getPlaystateApi(api).markUnplayedItem({ userId: user.Id, itemId: itemId @@ -591,8 +593,8 @@ const fetchUpdatePlayedState = async ( export const useTogglePlayedMutation = () => { const currentApi = useApi(); return useMutation({ - mutationFn: ({ itemId, playedState }: TogglePlayedMutationProp) => - fetchUpdatePlayedState(currentApi, itemId, playedState ) + mutationFn: ({ itemId, isPlayed }: TogglePlayedMutationProp) => + fetchUpdatePlayedState(currentApi, itemId, isPlayed ) }); }; @@ -676,7 +678,7 @@ const fetchGetTimers = async ( export const useGetTimers = (isUpcomingRecordingsEnabled: boolean, indexByDate?: boolean) => { const currentApi = useApi(); return useQuery({ - queryKey: ['Timers', isUpcomingRecordingsEnabled, indexByDate], + queryKey: ['Timers', { isUpcomingRecordingsEnabled, indexByDate }], queryFn: ({ signal }) => isUpcomingRecordingsEnabled ? fetchGetTimers(currentApi, indexByDate, { signal }) : [] }); @@ -830,7 +832,7 @@ const fetchGetSectionItems = async ( ], parentId: parentId ?? undefined, imageTypeLimit: 1, - enableImageTypes: [ImageType.Primary], + enableImageTypes: [ImageType.Primary, ImageType.Thumb], ...section.parametersOptions }, { @@ -882,19 +884,15 @@ const getSectionsWithItems = async ( const updatedSectionWithItems: SectionWithItems[] = []; for (const section of sections) { - try { - const items = await fetchGetSectionItems( - currentApi, parentId, section, options - ); + const items = await fetchGetSectionItems( + currentApi, parentId, section, options + ); - if (items && items.length > 0) { - updatedSectionWithItems.push({ - section, - items - }); - } - } catch (error) { - console.error(`Error occurred for section ${section.type}: ${error}`); + if (items && items.length > 0) { + updatedSectionWithItems.push({ + section, + items + }); } } @@ -908,7 +906,7 @@ export const useGetSuggestionSectionsWithItems = ( const currentApi = useApi(); const sections = getSuggestionSections(); return useQuery({ - queryKey: ['SuggestionSectionWithItems', suggestionSectionType], + queryKey: ['SuggestionSectionWithItems', { suggestionSectionType }], queryFn: ({ signal }) => getSectionsWithItems(currentApi, parentId, sections, suggestionSectionType, { signal }), enabled: !!parentId @@ -922,9 +920,8 @@ export const useGetProgramsSectionsWithItems = ( const currentApi = useApi(); const sections = getProgramSections(); return useQuery({ - queryKey: ['ProgramSectionWithItems', programSectionType], - queryFn: ({ signal }) => - getSectionsWithItems(currentApi, parentId, sections, programSectionType, { signal }) + queryKey: ['ProgramSectionWithItems', { programSectionType }], + queryFn: ({ signal }) => getSectionsWithItems(currentApi, parentId, sections, programSectionType, { signal }) + }); }; - From 4a12d5b2c659f12b0a3f1cfd805eac2eae96125d Mon Sep 17 00:00:00 2001 From: grafixeyehero Date: Wed, 31 Jan 2024 04:33:19 +0300 Subject: [PATCH 09/24] Replace card and list component in itemsView --- .../components/library/ItemsView.tsx | 73 +++++++++++-------- 1 file changed, 43 insertions(+), 30 deletions(-) diff --git a/src/apps/experimental/components/library/ItemsView.tsx b/src/apps/experimental/components/library/ItemsView.tsx index 65b26ffcdc..b13ab14165 100644 --- a/src/apps/experimental/components/library/ItemsView.tsx +++ b/src/apps/experimental/components/library/ItemsView.tsx @@ -8,10 +8,7 @@ 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 ItemsContainer from 'elements/emby-itemscontainer/ItemsContainer'; import AlphabetPicker from './AlphabetPicker'; import FilterButton from './filter/FilterButton'; @@ -22,12 +19,15 @@ import QueueButton from './QueueButton'; import ShuffleButton from './ShuffleButton'; import SortButton from './SortButton'; import GridListViewButton from './GridListViewButton'; -import { LibraryViewSettings, ParentId, ViewMode } from 'types/library'; +import NoItemsMessage from 'components/common/NoItemsMessage'; +import Lists from 'components/listview/List/Lists'; +import Cards from 'components/cardbuilder/Card/Cards'; +import { type LibraryViewSettings, type ParentId, ViewMode } from 'types/library'; import { CollectionType } from '@jellyfin/sdk/lib/generated-client/models/collection-type'; import { LibraryTab } from 'types/libraryTab'; -import { CardOptions } from 'types/cardOptions'; -import { ListOptions } from 'types/listOptions'; +import type { CardOptions } from 'types/cardOptions'; +import type { ListOptions } from 'types/listOptions'; interface ItemsViewProps { viewType: LibraryTab; @@ -135,9 +135,9 @@ const ItemsView: FC = ({ preferThumb: preferThumb, preferDisc: preferDisc, preferLogo: preferLogo, - overlayPlayButton: false, - overlayMoreButton: true, - overlayText: !libraryViewSettings.ShowTitle + overlayText: !libraryViewSettings.ShowTitle, + imageType: libraryViewSettings.ImageType, + queryKey: ['ItemsViewByType'] }; if ( @@ -146,20 +146,26 @@ const ItemsView: FC = ({ || viewType === LibraryTab.Episodes ) { cardOptions.showParentTitle = libraryViewSettings.ShowTitle; + cardOptions.overlayPlayButton = true; } else if (viewType === LibraryTab.Artists) { cardOptions.lines = 1; cardOptions.showYear = false; + cardOptions.overlayPlayButton = true; } else if (viewType === LibraryTab.Channels) { cardOptions.shape = 'square'; cardOptions.showDetailsMenu = true; cardOptions.showCurrentProgram = true; cardOptions.showCurrentProgramTime = true; } else if (viewType === LibraryTab.SeriesTimers) { - cardOptions.defaultShape = 'portrait'; - cardOptions.preferThumb = 'auto'; + cardOptions.shape = 'backdrop'; cardOptions.showSeriesTimerTime = true; cardOptions.showSeriesTimerChannel = true; + cardOptions.overlayMoreButton = true; cardOptions.lines = 3; + } else if (viewType === LibraryTab.Movies) { + cardOptions.overlayPlayButton = true; + } else if (viewType === LibraryTab.Series || viewType === LibraryTab.Networks) { + cardOptions.overlayMoreButton = true; } return cardOptions; @@ -172,27 +178,32 @@ const ItemsView: FC = ({ viewType ]); - const getItemsHtml = useCallback(() => { - let html = ''; + const getItems = useCallback(() => { + if (!itemsResult?.Items?.length) { + return ; + } if (libraryViewSettings.ViewMode === ViewMode.ListView) { - html = listview.getListViewHtml(getListOptions()); - } else { - html = cardBuilder.getCardsHtml( - itemsResult?.Items ?? [], - getCardOptions() + return ( + ); } - - if (!itemsResult?.Items?.length) { - html += '
'; - html += '

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

'; - html += '

' + globalize.translate(noItemsMessage) + '

'; - html += '
'; - } - - return html; - }, [libraryViewSettings.ViewMode, itemsResult?.Items, getListOptions, getCardOptions, noItemsMessage]); + return ( + + ); + }, [ + libraryViewSettings.ViewMode, + itemsResult?.Items, + getListOptions, + getCardOptions, + noItemsMessage + ]); const totalRecordCount = itemsResult?.TotalRecordCount ?? 0; const items = itemsResult?.Items ?? []; @@ -289,8 +300,10 @@ const ItemsView: FC = ({ className={itemsContainerClass} parentId={parentId} reloadItems={refetch} - getItemsHtml={getItemsHtml} - /> + queryKey={['ItemsViewByType']} + > + {getItems()} + )} {isPaginationEnabled && ( From 42b4d08e55ece6b068539a2d689ff40039e41f06 Mon Sep 17 00:00:00 2001 From: grafixeyehero Date: Wed, 31 Jan 2024 04:34:00 +0300 Subject: [PATCH 10/24] Replace card component in SectionContainer --- .../components/library/SectionContainer.tsx | 34 +++++++------------ 1 file changed, 12 insertions(+), 22 deletions(-) diff --git a/src/apps/experimental/components/library/SectionContainer.tsx b/src/apps/experimental/components/library/SectionContainer.tsx index 18669452a0..8eb64c4e6f 100644 --- a/src/apps/experimental/components/library/SectionContainer.tsx +++ b/src/apps/experimental/components/library/SectionContainer.tsx @@ -1,43 +1,29 @@ import type { BaseItemDto, TimerInfoDto } from '@jellyfin/sdk/lib/generated-client'; -import React, { FC, useEffect, useRef } from 'react'; +import React, { FC } from 'react'; -import cardBuilder from 'components/cardbuilder/cardBuilder'; import ItemsContainer from 'elements/emby-itemscontainer/ItemsContainer'; import Scroller from 'elements/emby-scroller/Scroller'; import LinkButton from 'elements/emby-button/LinkButton'; -import imageLoader from 'components/images/imageLoader'; - -import { CardOptions } from 'types/cardOptions'; +import Cards from 'components/cardbuilder/Card/Cards'; +import type { CardOptions } from 'types/cardOptions'; interface SectionContainerProps { url?: string; sectionTitle: string; items: BaseItemDto[] | TimerInfoDto[]; cardOptions: CardOptions; + reloadItems?: () => void; } const SectionContainer: FC = ({ sectionTitle, url, items, - cardOptions + cardOptions, + reloadItems }) => { - const element = useRef(null); - - useEffect(() => { - const itemsContainer = element.current?.querySelector('.itemsContainer'); - cardBuilder.buildCards(items, { - itemsContainer: itemsContainer, - parentContainer: element.current, - - ...cardOptions - }); - - imageLoader.lazyChildren(itemsContainer); - }, [cardOptions, items]); - return ( -
+
{url && items.length > 5 ? ( = ({ > + reloadItems={reloadItems} + queryKey={cardOptions.queryKey} + > + +
); From e3b618f2fb7113ed3c4f49bac29607bc1e9897ce Mon Sep 17 00:00:00 2001 From: grafixeyehero Date: Wed, 31 Jan 2024 04:36:00 +0300 Subject: [PATCH 11/24] Refactor ItemsContainer invalidate Queries --- .../emby-itemscontainer/ItemsContainer.tsx | 187 ++++-------------- 1 file changed, 43 insertions(+), 144 deletions(-) diff --git a/src/elements/emby-itemscontainer/ItemsContainer.tsx b/src/elements/emby-itemscontainer/ItemsContainer.tsx index bcbda52841..f817221b0c 100644 --- a/src/elements/emby-itemscontainer/ItemsContainer.tsx +++ b/src/elements/emby-itemscontainer/ItemsContainer.tsx @@ -1,13 +1,11 @@ import type { - LibraryUpdateInfo, - SeriesTimerInfoDto, - TimerInfoDto, - UserItemDataDto + LibraryUpdateInfo } from '@jellyfin/sdk/lib/generated-client'; import React, { FC, useCallback, useEffect, useRef } from 'react'; import classNames from 'classnames'; -import { Box } from '@mui/material'; +import Box from '@mui/material/Box'; import Sortable from 'sortablejs'; +import { useQueryClient } from '@tanstack/react-query'; import { usePlaylistsMoveItemMutation } from 'hooks/useFetchItems'; import Events, { Event } from 'utils/events'; import serverNotifications from 'scripts/serverNotifications'; @@ -40,11 +38,11 @@ interface ItemsContainerProps { isContextMenuEnabled?: boolean; isMultiSelectEnabled?: boolean; isDragreOrderEnabled?: boolean; - dataMonitor?: string; + eventsToMonitor?: string[]; parentId?: ParentId; reloadItems?: () => void; getItemsHtml?: () => string; - children?: React.ReactNode; + queryKey?: string[] } const ItemsContainer: FC = ({ @@ -52,12 +50,14 @@ const ItemsContainer: FC = ({ isContextMenuEnabled, isMultiSelectEnabled, isDragreOrderEnabled, - dataMonitor, + eventsToMonitor = [], parentId, + queryKey, reloadItems, getItemsHtml, children }) => { + const queryClient = useQueryClient(); const { mutateAsync: playlistsMoveItemMutation } = usePlaylistsMoveItemMutation(); const itemsContainerRef = useRef(null); const multiSelectref = useRef(null); @@ -172,6 +172,14 @@ const ItemsContainer: FC = ({ } }, []); + const invalidateQueries = useCallback(async () => { + await queryClient.invalidateQueries({ + queryKey: queryKey, + type: 'all', + refetchType: 'active' + }); + }, [queryClient, queryKey]); + const notifyRefreshNeeded = useCallback( (isInForeground: boolean) => { if (!reloadItems) return; @@ -184,144 +192,37 @@ const ItemsContainer: FC = ({ [reloadItems] ); - const getEventsToMonitor = useCallback(() => { - const monitor = dataMonitor; - if (monitor) { - return monitor.split(','); - } - - return []; - }, [dataMonitor]); - - const onUserDataChanged = useCallback( - (_e: Event, userData: UserItemDataDto) => { - const itemsContainer = itemsContainerRef.current as HTMLDivElement; - - import('../../components/cardbuilder/cardBuilder') - .then((cardBuilder) => { - cardBuilder.onUserDataChanged(userData, itemsContainer); - }) - .catch((err) => { - console.error( - '[onUserDataChanged] failed to load onUserData Changed', - err - ); - }); - - const eventsToMonitor = getEventsToMonitor(); - if ( - eventsToMonitor.indexOf('markfavorite') !== -1 - || eventsToMonitor.indexOf('markplayed') !== -1 - ) { - notifyRefreshNeeded(false); - } - }, - [getEventsToMonitor, notifyRefreshNeeded] + const onUserDataChanged = useCallback(async () => { + await invalidateQueries(); + }, + [invalidateQueries] ); - const onTimerCreated = useCallback( - (_e: Event, data: TimerInfoDto) => { - const itemsContainer = itemsContainerRef.current as HTMLDivElement; - const eventsToMonitor = getEventsToMonitor(); - if (eventsToMonitor.indexOf('timers') !== -1) { - notifyRefreshNeeded(false); - return; - } - - const programId = data.ProgramId; - // This could be null, not supported by all tv providers - const newTimerId = data.Id; - if (programId && newTimerId) { - import('../../components/cardbuilder/cardBuilder') - .then((cardBuilder) => { - cardBuilder.onTimerCreated( - programId, - newTimerId, - itemsContainer - ); - }) - .catch((err) => { - console.error( - '[onTimerCreated] failed to load onTimer Created', - err - ); - }); - } - }, - [getEventsToMonitor, notifyRefreshNeeded] + const onTimerCreated = useCallback(async () => { + await invalidateQueries(); + }, + [invalidateQueries] ); - const onSeriesTimerCreated = useCallback(() => { - const eventsToMonitor = getEventsToMonitor(); - if (eventsToMonitor.indexOf('seriestimers') !== -1) { - notifyRefreshNeeded(false); - } - }, [getEventsToMonitor, notifyRefreshNeeded]); + const onSeriesTimerCreated = useCallback(async () => { + await invalidateQueries(); + }, [invalidateQueries]); - const onTimerCancelled = useCallback( - (_e: Event, data: TimerInfoDto) => { - const itemsContainer = itemsContainerRef.current as HTMLDivElement; - const eventsToMonitor = getEventsToMonitor(); - if (eventsToMonitor.indexOf('timers') !== -1) { - notifyRefreshNeeded(false); - return; - } - - const timerId = data.Id; - - if (timerId) { - import('../../components/cardbuilder/cardBuilder') - .then((cardBuilder) => { - cardBuilder.onTimerCancelled(timerId, itemsContainer); - }) - .catch((err) => { - console.error( - '[onTimerCancelled] failed to load onTimer Cancelled', - err - ); - }); - } - }, - [getEventsToMonitor, notifyRefreshNeeded] + const onTimerCancelled = useCallback(async () => { + await invalidateQueries(); + }, + [invalidateQueries] ); - const onSeriesTimerCancelled = useCallback( - (_e: Event, data: SeriesTimerInfoDto) => { - const itemsContainer = itemsContainerRef.current as HTMLDivElement; - const eventsToMonitor = getEventsToMonitor(); - if (eventsToMonitor.indexOf('seriestimers') !== -1) { - notifyRefreshNeeded(false); - return; - } - - const cancelledTimerId = data.Id; - - if (cancelledTimerId) { - import('../../components/cardbuilder/cardBuilder') - .then((cardBuilder) => { - cardBuilder.onSeriesTimerCancelled( - cancelledTimerId, - itemsContainer - ); - }) - .catch((err) => { - console.error( - '[onSeriesTimerCancelled] failed to load onSeriesTimer Cancelled', - err - ); - }); - } - }, - [getEventsToMonitor, notifyRefreshNeeded] + const onSeriesTimerCancelled = useCallback(async () => { + await invalidateQueries(); + }, + [invalidateQueries] ); const onLibraryChanged = useCallback( - (_e: Event, data: LibraryUpdateInfo) => { - const eventsToMonitor = getEventsToMonitor(); - if ( - eventsToMonitor.indexOf('seriestimers') !== -1 - || eventsToMonitor.indexOf('timers') !== -1 - ) { + (_e: Event, apiClient, data: LibraryUpdateInfo) => { + if (eventsToMonitor.includes('seriestimers') || eventsToMonitor.includes('timers')) { // yes this is an assumption return; } @@ -348,32 +249,31 @@ const ItemsContainer: FC = ({ notifyRefreshNeeded(false); }, - [getEventsToMonitor, notifyRefreshNeeded, parentId] + [eventsToMonitor, notifyRefreshNeeded, parentId] ); const onPlaybackStopped = useCallback( - (_e: Event, stopInfo) => { + (_e: Event, apiClient, stopInfo) => { const state = stopInfo.state; - const eventsToMonitor = getEventsToMonitor(); if ( state.NowPlayingItem && state.NowPlayingItem.MediaType === 'Video' ) { - if (eventsToMonitor.indexOf('videoplayback') !== -1) { + if (eventsToMonitor.includes('videoplayback')) { notifyRefreshNeeded(true); return; } } else if ( state.NowPlayingItem && state.NowPlayingItem.MediaType === 'Audio' - && eventsToMonitor.indexOf('audioplayback') !== -1 + && eventsToMonitor.includes('videoplayback') ) { notifyRefreshNeeded(true); return; } }, - [getEventsToMonitor, notifyRefreshNeeded] + [eventsToMonitor, notifyRefreshNeeded] ); const setFocus = useCallback( @@ -418,10 +318,9 @@ const ItemsContainer: FC = ({ if (getItemsHtml) { itemsContainer.innerHTML = getItemsHtml(); + imageLoader.lazyChildren(itemsContainer); } - imageLoader.lazyChildren(itemsContainer); - if (hasActiveElement) { setFocus(itemsContainer, focusId); } From 876fbee53edeb9dab2dc3684da0289ad87fc055b Mon Sep 17 00:00:00 2001 From: grafixeyehero Date: Wed, 31 Jan 2024 05:25:38 +0300 Subject: [PATCH 12/24] Fix navigation for mobile layout --- .../cardbuilder/Card/CardOverlayButtons.tsx | 17 ++++++++++++----- src/types/dataAttributes.ts | 2 +- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/src/components/cardbuilder/Card/CardOverlayButtons.tsx b/src/components/cardbuilder/Card/CardOverlayButtons.tsx index 0ac8463969..c2938c1088 100644 --- a/src/components/cardbuilder/Card/CardOverlayButtons.tsx +++ b/src/components/cardbuilder/Card/CardOverlayButtons.tsx @@ -1,8 +1,8 @@ import React, { FC } from 'react'; -import Box from '@mui/material/Box'; import ButtonGroup from '@mui/material/ButtonGroup'; import classNames from 'classnames'; - +import { appRouter } from 'components/router/appRouter'; +import escapeHTML from 'escape-html'; import PlayArrowIconButton from '../../common/PlayArrowIconButton'; import MoreVertIconButton from '../../common/MoreVertIconButton'; @@ -43,6 +43,10 @@ const CardOverlayButtons: FC = ({ overlayPlayButton = item.MediaType === 'Video'; } + const url = appRouter.getRouteUrl(item, { + parentId: cardOptions.parentId + }); + const btnCssClass = classNames( 'paper-icon-button-light', 'cardOverlayButton', @@ -55,8 +59,10 @@ const CardOverlayButtons: FC = ({ ); return ( - = ({ borderRadius: '0.2em' }} > + {cardOptions.centerPlayButton && ( = ({ /> )} - + ); }; diff --git a/src/types/dataAttributes.ts b/src/types/dataAttributes.ts index 6e91b125cd..1258d61cd4 100644 --- a/src/types/dataAttributes.ts +++ b/src/types/dataAttributes.ts @@ -2,7 +2,7 @@ import type { CollectionType, UserItemDataDto } from '@jellyfin/sdk/lib/generate import type { NullableBoolean, NullableNumber, NullableString } from './itemDto'; export type AttributesOpts = { - context?: CollectionType | undefined, + context?: CollectionType, parentId?: NullableString, collectionId?: NullableString, playlistId?: NullableString, From ed46ee5254bc57f96e837d9dc0c7324a98f8c360 Mon Sep 17 00:00:00 2001 From: grafixeyehero Date: Thu, 1 Feb 2024 19:41:08 +0300 Subject: [PATCH 13/24] Replace deprecated getItemImageUrl with imageUrlsApi.getItemImageUrlById --- .../cardbuilder/Card/CardContent.tsx | 2 +- src/components/cardbuilder/Card/cardHelper.ts | 3 +- .../cardbuilder/Card/useCardImageUrl.ts | 51 +++++++++---------- src/components/listview/List/listHelper.ts | 3 +- 4 files changed, 28 insertions(+), 31 deletions(-) diff --git a/src/components/cardbuilder/Card/CardContent.tsx b/src/components/cardbuilder/Card/CardContent.tsx index 3d846758a4..11a443fb90 100644 --- a/src/components/cardbuilder/Card/CardContent.tsx +++ b/src/components/cardbuilder/Card/CardContent.tsx @@ -32,7 +32,7 @@ const CardContent: FC = ({ return (
Date: Mon, 19 Feb 2024 05:59:54 +0300 Subject: [PATCH 14/24] Fix mediaSourceCount display --- src/components/indicators/useIndicator.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/indicators/useIndicator.tsx b/src/components/indicators/useIndicator.tsx index 3015094b14..214c736267 100644 --- a/src/components/indicators/useIndicator.tsx +++ b/src/components/indicators/useIndicator.tsx @@ -63,7 +63,7 @@ const useIndicator = (item: ItemDto) => { const getMediaSourceIndicator = () => { const mediaSourceCount = item.MediaSourceCount ?? 0; if (mediaSourceCount > 1) { - return mediaSourceCount; + return {mediaSourceCount}; } return null; From 533ae17767cd7c4fba85501f17af1f1910e24bd3 Mon Sep 17 00:00:00 2001 From: grafixeyehero Date: Wed, 28 Feb 2024 21:02:05 +0300 Subject: [PATCH 15/24] Use type import for react FC Co-authored-by: Bill Thornton --- src/components/cardbuilder/Card/Card.tsx | 2 +- src/components/cardbuilder/Card/CardBox.tsx | 3 +-- src/components/cardbuilder/Card/CardContent.tsx | 2 +- src/components/cardbuilder/Card/CardFooterText.tsx | 2 +- src/components/cardbuilder/Card/CardHoverMenu.tsx | 2 +- src/components/cardbuilder/Card/CardImageContainer.tsx | 2 +- src/components/cardbuilder/Card/CardInnerFooter.tsx | 2 +- src/components/cardbuilder/Card/CardOuterFooter.tsx | 2 +- src/components/cardbuilder/Card/CardOverlayButtons.tsx | 2 +- src/components/cardbuilder/Card/CardText.tsx | 2 +- src/components/cardbuilder/Card/CardWrapper.tsx | 2 +- src/components/cardbuilder/Card/Cards.tsx | 2 +- src/components/common/DefaultIconText.tsx | 2 +- src/components/common/DefaultName.tsx | 2 +- src/components/common/Image.tsx | 2 +- src/components/common/InfoIconButton.tsx | 2 +- src/components/common/Media.tsx | 2 +- src/components/common/MoreVertIconButton.tsx | 2 +- src/components/common/NoItemsMessage.tsx | 2 +- src/components/common/PlayArrowIconButton.tsx | 2 +- src/components/common/PlaylistAddIconButton.tsx | 2 +- src/components/common/RightIconButtons.tsx | 2 +- src/components/listview/List/List.tsx | 2 +- src/components/listview/List/ListContent.tsx | 2 +- src/components/listview/List/ListContentWrapper.tsx | 2 +- src/components/listview/List/ListGroupHeaderWrapper.tsx | 2 +- src/components/listview/List/ListImageContainer.tsx | 2 +- src/components/listview/List/ListItemBody.tsx | 2 +- src/components/listview/List/ListTextWrapper.tsx | 2 +- src/components/listview/List/ListViewUserDataButtons.tsx | 2 +- src/components/listview/List/ListWrapper.tsx | 2 +- src/components/listview/List/Lists.tsx | 2 +- src/components/mediainfo/CaptionMediaInfo.tsx | 2 +- src/components/mediainfo/CriticRatingMediaInfo.tsx | 2 +- src/components/mediainfo/EndsAt.tsx | 2 +- src/components/mediainfo/MediaInfoItem.tsx | 2 +- src/components/mediainfo/PrimaryMediaInfo.tsx | 2 +- src/components/mediainfo/StarIcons.tsx | 2 +- src/elements/emby-itemrefreshindicator/RefreshIndicator.tsx | 2 +- src/elements/emby-itemscontainer/ItemsContainer.tsx | 4 ++-- src/elements/emby-playstatebutton/PlayedButton.tsx | 2 +- src/elements/emby-progressbar/AutoTimeProgressBar.tsx | 4 ++-- src/elements/emby-ratingbutton/FavoriteButton.tsx | 2 +- src/elements/emby-scrollbuttons/ScrollButtons.tsx | 2 +- src/elements/emby-scroller/Scroller.tsx | 2 +- 45 files changed, 47 insertions(+), 48 deletions(-) diff --git a/src/components/cardbuilder/Card/Card.tsx b/src/components/cardbuilder/Card/Card.tsx index 2173e0301b..e1718e6459 100644 --- a/src/components/cardbuilder/Card/Card.tsx +++ b/src/components/cardbuilder/Card/Card.tsx @@ -1,4 +1,4 @@ -import React, { FC } from 'react'; +import React, { type FC } from 'react'; import useCard from './useCard'; import CardWrapper from './CardWrapper'; import CardBox from './CardBox'; diff --git a/src/components/cardbuilder/Card/CardBox.tsx b/src/components/cardbuilder/Card/CardBox.tsx index 430c27b444..34e5044bec 100644 --- a/src/components/cardbuilder/Card/CardBox.tsx +++ b/src/components/cardbuilder/Card/CardBox.tsx @@ -1,5 +1,4 @@ - -import React, { FC } from 'react'; +import React, { type FC } from 'react'; import layoutManager from 'components/layoutManager'; import CardOverlayButtons from './CardOverlayButtons'; diff --git a/src/components/cardbuilder/Card/CardContent.tsx b/src/components/cardbuilder/Card/CardContent.tsx index 11a443fb90..8ebeb0cb87 100644 --- a/src/components/cardbuilder/Card/CardContent.tsx +++ b/src/components/cardbuilder/Card/CardContent.tsx @@ -1,4 +1,4 @@ -import React, { FC } from 'react'; +import React, { type FC } from 'react'; import classNames from 'classnames'; import { getDefaultBackgroundClass } from '../cardBuilderUtils'; import CardImageContainer from './CardImageContainer'; diff --git a/src/components/cardbuilder/Card/CardFooterText.tsx b/src/components/cardbuilder/Card/CardFooterText.tsx index 87ba3b22ea..b9bf7bbafd 100644 --- a/src/components/cardbuilder/Card/CardFooterText.tsx +++ b/src/components/cardbuilder/Card/CardFooterText.tsx @@ -1,4 +1,4 @@ -import React, { FC } from 'react'; +import React, { type FC } from 'react'; import Box from '@mui/material/Box'; import useCardText from './useCardText'; import layoutManager from 'components/layoutManager'; diff --git a/src/components/cardbuilder/Card/CardHoverMenu.tsx b/src/components/cardbuilder/Card/CardHoverMenu.tsx index e135d1bd82..b4f56f7188 100644 --- a/src/components/cardbuilder/Card/CardHoverMenu.tsx +++ b/src/components/cardbuilder/Card/CardHoverMenu.tsx @@ -1,4 +1,4 @@ -import React, { FC } from 'react'; +import React, { type FC } from 'react'; import Box from '@mui/material/Box'; import ButtonGroup from '@mui/material/ButtonGroup'; import classNames from 'classnames'; diff --git a/src/components/cardbuilder/Card/CardImageContainer.tsx b/src/components/cardbuilder/Card/CardImageContainer.tsx index 3b66048e9e..69eb47c66b 100644 --- a/src/components/cardbuilder/Card/CardImageContainer.tsx +++ b/src/components/cardbuilder/Card/CardImageContainer.tsx @@ -1,4 +1,4 @@ -import React, { FC } from 'react'; +import React, { type FC } from 'react'; import Box from '@mui/material/Box'; import classNames from 'classnames'; import useIndicator from 'components/indicators/useIndicator'; diff --git a/src/components/cardbuilder/Card/CardInnerFooter.tsx b/src/components/cardbuilder/Card/CardInnerFooter.tsx index d6edf853c0..33534e8a9a 100644 --- a/src/components/cardbuilder/Card/CardInnerFooter.tsx +++ b/src/components/cardbuilder/Card/CardInnerFooter.tsx @@ -1,4 +1,4 @@ -import React, { FC } from 'react'; +import React, { type FC } from 'react'; import classNames from 'classnames'; import CardFooterText from './CardFooterText'; import type { ItemDto } from 'types/itemDto'; diff --git a/src/components/cardbuilder/Card/CardOuterFooter.tsx b/src/components/cardbuilder/Card/CardOuterFooter.tsx index 020a64d584..f03dcb8703 100644 --- a/src/components/cardbuilder/Card/CardOuterFooter.tsx +++ b/src/components/cardbuilder/Card/CardOuterFooter.tsx @@ -1,4 +1,4 @@ -import React, { FC } from 'react'; +import React, { type FC } from 'react'; import classNames from 'classnames'; import { useApi } from 'hooks/useApi'; import { getCardLogoUrl } from './cardHelper'; diff --git a/src/components/cardbuilder/Card/CardOverlayButtons.tsx b/src/components/cardbuilder/Card/CardOverlayButtons.tsx index c2938c1088..482a14a816 100644 --- a/src/components/cardbuilder/Card/CardOverlayButtons.tsx +++ b/src/components/cardbuilder/Card/CardOverlayButtons.tsx @@ -1,4 +1,4 @@ -import React, { FC } from 'react'; +import React, { type FC } from 'react'; import ButtonGroup from '@mui/material/ButtonGroup'; import classNames from 'classnames'; import { appRouter } from 'components/router/appRouter'; diff --git a/src/components/cardbuilder/Card/CardText.tsx b/src/components/cardbuilder/Card/CardText.tsx index be6d0b049c..dc64dc61ba 100644 --- a/src/components/cardbuilder/Card/CardText.tsx +++ b/src/components/cardbuilder/Card/CardText.tsx @@ -1,4 +1,4 @@ -import React, { FC } from 'react'; +import React, { type FC } from 'react'; import Box from '@mui/material/Box'; import escapeHTML from 'escape-html'; import type { TextLine } from './cardHelper'; diff --git a/src/components/cardbuilder/Card/CardWrapper.tsx b/src/components/cardbuilder/Card/CardWrapper.tsx index 01d6446a91..4c8ec854ea 100644 --- a/src/components/cardbuilder/Card/CardWrapper.tsx +++ b/src/components/cardbuilder/Card/CardWrapper.tsx @@ -1,4 +1,4 @@ -import React, { FC } from 'react'; +import React, { type FC } from 'react'; import layoutManager from 'components/layoutManager'; import type { DataAttributes } from 'types/dataAttributes'; diff --git a/src/components/cardbuilder/Card/Cards.tsx b/src/components/cardbuilder/Card/Cards.tsx index fcf2454a57..82b67cbdfc 100644 --- a/src/components/cardbuilder/Card/Cards.tsx +++ b/src/components/cardbuilder/Card/Cards.tsx @@ -1,4 +1,4 @@ -import React, { FC } from 'react'; +import React, { type FC } from 'react'; import { setCardData } from '../cardBuilder'; import Card from './Card'; import type { ItemDto } from 'types/itemDto'; diff --git a/src/components/common/DefaultIconText.tsx b/src/components/common/DefaultIconText.tsx index 41f0014cb0..60b1aa3fb9 100644 --- a/src/components/common/DefaultIconText.tsx +++ b/src/components/common/DefaultIconText.tsx @@ -1,5 +1,5 @@ import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client'; -import React, { FC } from 'react'; +import React, { type FC } from 'react'; import Icon from '@mui/material/Icon'; import imageHelper from 'utils/image'; import DefaultName from './DefaultName'; diff --git a/src/components/common/DefaultName.tsx b/src/components/common/DefaultName.tsx index 5946fe27b5..0ead8876a3 100644 --- a/src/components/common/DefaultName.tsx +++ b/src/components/common/DefaultName.tsx @@ -1,4 +1,4 @@ -import React, { FC } from 'react'; +import React, { type FC } from 'react'; import Box from '@mui/material/Box'; import escapeHTML from 'escape-html'; import itemHelper from 'components/itemHelper'; diff --git a/src/components/common/Image.tsx b/src/components/common/Image.tsx index 14df552660..8e26e78b24 100644 --- a/src/components/common/Image.tsx +++ b/src/components/common/Image.tsx @@ -1,4 +1,4 @@ -import React, { FC, useCallback, useState } from 'react'; +import React, { type FC, useCallback, useState } from 'react'; import { BlurhashCanvas } from 'react-blurhash'; import { LazyLoadImage } from 'react-lazy-load-image-component'; diff --git a/src/components/common/InfoIconButton.tsx b/src/components/common/InfoIconButton.tsx index 69c602e327..deefa0628b 100644 --- a/src/components/common/InfoIconButton.tsx +++ b/src/components/common/InfoIconButton.tsx @@ -1,4 +1,4 @@ -import React, { FC } from 'react'; +import React, { type FC } from 'react'; import IconButton from '@mui/material/IconButton'; import InfoIcon from '@mui/icons-material/Info'; import globalize from 'scripts/globalize'; diff --git a/src/components/common/Media.tsx b/src/components/common/Media.tsx index 170208416f..598c9ec7a5 100644 --- a/src/components/common/Media.tsx +++ b/src/components/common/Media.tsx @@ -1,5 +1,5 @@ import { BaseItemKind, ImageType } from '@jellyfin/sdk/lib/generated-client'; -import React, { FC } from 'react'; +import React, { type FC } from 'react'; import Image from './Image'; import DefaultIconText from './DefaultIconText'; import type { ItemDto } from 'types/itemDto'; diff --git a/src/components/common/MoreVertIconButton.tsx b/src/components/common/MoreVertIconButton.tsx index 231a2afed1..c0a77088c5 100644 --- a/src/components/common/MoreVertIconButton.tsx +++ b/src/components/common/MoreVertIconButton.tsx @@ -1,4 +1,4 @@ -import React, { FC } from 'react'; +import React, { type FC } from 'react'; import IconButton from '@mui/material/IconButton'; import MoreVertIcon from '@mui/icons-material/MoreVert'; import globalize from 'scripts/globalize'; diff --git a/src/components/common/NoItemsMessage.tsx b/src/components/common/NoItemsMessage.tsx index 2c59b0ed6b..88f288c144 100644 --- a/src/components/common/NoItemsMessage.tsx +++ b/src/components/common/NoItemsMessage.tsx @@ -1,4 +1,4 @@ -import React, { FC } from 'react'; +import React, { type FC } from 'react'; import Box from '@mui/material/Box'; import Typography from '@mui/material/Typography'; import globalize from 'scripts/globalize'; diff --git a/src/components/common/PlayArrowIconButton.tsx b/src/components/common/PlayArrowIconButton.tsx index b64fd9bd05..d7ca732966 100644 --- a/src/components/common/PlayArrowIconButton.tsx +++ b/src/components/common/PlayArrowIconButton.tsx @@ -1,4 +1,4 @@ -import React, { FC } from 'react'; +import React, { type FC } from 'react'; import IconButton from '@mui/material/IconButton'; import PlayArrowIcon from '@mui/icons-material/PlayArrow'; import globalize from 'scripts/globalize'; diff --git a/src/components/common/PlaylistAddIconButton.tsx b/src/components/common/PlaylistAddIconButton.tsx index 19469e0fe3..14fb2a83cd 100644 --- a/src/components/common/PlaylistAddIconButton.tsx +++ b/src/components/common/PlaylistAddIconButton.tsx @@ -1,4 +1,4 @@ -import React, { FC } from 'react'; +import React, { type FC } from 'react'; import IconButton from '@mui/material/IconButton'; import PlaylistAddIcon from '@mui/icons-material/PlaylistAdd'; import globalize from 'scripts/globalize'; diff --git a/src/components/common/RightIconButtons.tsx b/src/components/common/RightIconButtons.tsx index 2787a1856c..cfe65e451c 100644 --- a/src/components/common/RightIconButtons.tsx +++ b/src/components/common/RightIconButtons.tsx @@ -1,4 +1,4 @@ -import React, { FC } from 'react'; +import React, { type FC } from 'react'; import IconButton from '@mui/material/IconButton'; interface RightIconButtonsProps { diff --git a/src/components/listview/List/List.tsx b/src/components/listview/List/List.tsx index 995c057526..feafd5a04d 100644 --- a/src/components/listview/List/List.tsx +++ b/src/components/listview/List/List.tsx @@ -1,4 +1,4 @@ -import React, { FC } from 'react'; +import React, { type FC } from 'react'; import useList from './useList'; import ListContent from './ListContent'; import ListWrapper from './ListWrapper'; diff --git a/src/components/listview/List/ListContent.tsx b/src/components/listview/List/ListContent.tsx index 045c003f73..f9081f0b8a 100644 --- a/src/components/listview/List/ListContent.tsx +++ b/src/components/listview/List/ListContent.tsx @@ -1,5 +1,5 @@ import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client'; -import React, { FC } from 'react'; +import React, { type FC } from 'react'; import DragHandleIcon from '@mui/icons-material/DragHandle'; import Box from '@mui/material/Box'; diff --git a/src/components/listview/List/ListContentWrapper.tsx b/src/components/listview/List/ListContentWrapper.tsx index 1b0678ad50..59323dec73 100644 --- a/src/components/listview/List/ListContentWrapper.tsx +++ b/src/components/listview/List/ListContentWrapper.tsx @@ -1,4 +1,4 @@ -import React, { FC } from 'react'; +import React, { type FC } from 'react'; import Box from '@mui/material/Box'; interface ListContentWrapperProps { diff --git a/src/components/listview/List/ListGroupHeaderWrapper.tsx b/src/components/listview/List/ListGroupHeaderWrapper.tsx index f2a131e324..fd17d83120 100644 --- a/src/components/listview/List/ListGroupHeaderWrapper.tsx +++ b/src/components/listview/List/ListGroupHeaderWrapper.tsx @@ -1,4 +1,4 @@ -import React, { FC } from 'react'; +import React, { type FC } from 'react'; import Typography from '@mui/material/Typography'; interface ListGroupHeaderWrapperProps { diff --git a/src/components/listview/List/ListImageContainer.tsx b/src/components/listview/List/ListImageContainer.tsx index fe77707750..b447b2a701 100644 --- a/src/components/listview/List/ListImageContainer.tsx +++ b/src/components/listview/List/ListImageContainer.tsx @@ -1,4 +1,4 @@ -import React, { FC } from 'react'; +import React, { type FC } from 'react'; import classNames from 'classnames'; import Box from '@mui/material/Box'; import { useApi } from 'hooks/useApi'; diff --git a/src/components/listview/List/ListItemBody.tsx b/src/components/listview/List/ListItemBody.tsx index 7d033c4f5d..5152040585 100644 --- a/src/components/listview/List/ListItemBody.tsx +++ b/src/components/listview/List/ListItemBody.tsx @@ -1,4 +1,4 @@ -import React, { FC } from 'react'; +import React, { type FC } from 'react'; import classNames from 'classnames'; import Box from '@mui/material/Box'; import useListTextlines from './useListTextlines'; diff --git a/src/components/listview/List/ListTextWrapper.tsx b/src/components/listview/List/ListTextWrapper.tsx index c2139742ae..675ebe99d4 100644 --- a/src/components/listview/List/ListTextWrapper.tsx +++ b/src/components/listview/List/ListTextWrapper.tsx @@ -1,4 +1,4 @@ -import React, { FC } from 'react'; +import React, { type FC } from 'react'; import Box from '@mui/material/Box'; import Typography from '@mui/material/Typography'; diff --git a/src/components/listview/List/ListViewUserDataButtons.tsx b/src/components/listview/List/ListViewUserDataButtons.tsx index f3ad43ed9e..97668ed999 100644 --- a/src/components/listview/List/ListViewUserDataButtons.tsx +++ b/src/components/listview/List/ListViewUserDataButtons.tsx @@ -1,4 +1,4 @@ -import React, { FC } from 'react'; +import React, { type FC } from 'react'; import { Box } from '@mui/material'; import itemHelper from '../../itemHelper'; import PlayedButton from 'elements/emby-playstatebutton/PlayedButton'; diff --git a/src/components/listview/List/ListWrapper.tsx b/src/components/listview/List/ListWrapper.tsx index a6d4ab292e..76303a0f2b 100644 --- a/src/components/listview/List/ListWrapper.tsx +++ b/src/components/listview/List/ListWrapper.tsx @@ -1,6 +1,6 @@ import classNames from 'classnames'; import escapeHTML from 'escape-html'; -import React, { FC } from 'react'; +import React, { type FC } from 'react'; import Box from '@mui/material/Box'; import Button from '@mui/material/Button'; import layoutManager from '../../layoutManager'; diff --git a/src/components/listview/List/Lists.tsx b/src/components/listview/List/Lists.tsx index ce90622c1f..51f5612bba 100644 --- a/src/components/listview/List/Lists.tsx +++ b/src/components/listview/List/Lists.tsx @@ -1,4 +1,4 @@ -import React, { FC } from 'react'; +import React, { type FC } from 'react'; import escapeHTML from 'escape-html'; import { groupBy } from 'lodash-es'; import Box from '@mui/material/Box'; diff --git a/src/components/mediainfo/CaptionMediaInfo.tsx b/src/components/mediainfo/CaptionMediaInfo.tsx index 58a6f49af5..497f9fae59 100644 --- a/src/components/mediainfo/CaptionMediaInfo.tsx +++ b/src/components/mediainfo/CaptionMediaInfo.tsx @@ -1,4 +1,4 @@ -import React, { FC } from 'react'; +import React, { type FC } from 'react'; import classNames from 'classnames'; import ClosedCaptionIcon from '@mui/icons-material/ClosedCaption'; import Box from '@mui/material/Box'; diff --git a/src/components/mediainfo/CriticRatingMediaInfo.tsx b/src/components/mediainfo/CriticRatingMediaInfo.tsx index 080aef78fa..8046c2a931 100644 --- a/src/components/mediainfo/CriticRatingMediaInfo.tsx +++ b/src/components/mediainfo/CriticRatingMediaInfo.tsx @@ -1,4 +1,4 @@ -import React, { FC } from 'react'; +import React, { type FC } from 'react'; import classNames from 'classnames'; import Box from '@mui/material/Box'; diff --git a/src/components/mediainfo/EndsAt.tsx b/src/components/mediainfo/EndsAt.tsx index 693f949f8b..373e9817d7 100644 --- a/src/components/mediainfo/EndsAt.tsx +++ b/src/components/mediainfo/EndsAt.tsx @@ -1,4 +1,4 @@ -import React, { FC } from 'react'; +import React, { type FC } from 'react'; import classNames from 'classnames'; import Box from '@mui/material/Box'; import datetime from 'scripts/datetime'; diff --git a/src/components/mediainfo/MediaInfoItem.tsx b/src/components/mediainfo/MediaInfoItem.tsx index b832e02e45..d38635ac2d 100644 --- a/src/components/mediainfo/MediaInfoItem.tsx +++ b/src/components/mediainfo/MediaInfoItem.tsx @@ -1,4 +1,4 @@ -import React, { FC } from 'react'; +import React, { type FC } from 'react'; import Box from '@mui/material/Box'; import classNames from 'classnames'; import type { MiscInfo } from 'types/mediaInfoItem'; diff --git a/src/components/mediainfo/PrimaryMediaInfo.tsx b/src/components/mediainfo/PrimaryMediaInfo.tsx index 90b640054a..2978a41683 100644 --- a/src/components/mediainfo/PrimaryMediaInfo.tsx +++ b/src/components/mediainfo/PrimaryMediaInfo.tsx @@ -1,4 +1,4 @@ -import React, { FC } from 'react'; +import React, { type FC } from 'react'; import classNames from 'classnames'; import Box from '@mui/material/Box'; import usePrimaryMediaInfo from './usePrimaryMediaInfo'; diff --git a/src/components/mediainfo/StarIcons.tsx b/src/components/mediainfo/StarIcons.tsx index d253a2db3d..faa09ade3a 100644 --- a/src/components/mediainfo/StarIcons.tsx +++ b/src/components/mediainfo/StarIcons.tsx @@ -1,4 +1,4 @@ -import React, { FC } from 'react'; +import React, { type FC } from 'react'; import classNames from 'classnames'; import StarIcon from '@mui/icons-material/Star'; import Box from '@mui/material/Box'; diff --git a/src/elements/emby-itemrefreshindicator/RefreshIndicator.tsx b/src/elements/emby-itemrefreshindicator/RefreshIndicator.tsx index aabc709201..67a65703dc 100644 --- a/src/elements/emby-itemrefreshindicator/RefreshIndicator.tsx +++ b/src/elements/emby-itemrefreshindicator/RefreshIndicator.tsx @@ -1,4 +1,4 @@ -import React, { FC, useCallback, useEffect, useState } from 'react'; +import React, { type FC, useCallback, useEffect, useState } from 'react'; import Events, { Event } from 'utils/events'; import serverNotifications from 'scripts/serverNotifications'; import classNames from 'classnames'; diff --git a/src/elements/emby-itemscontainer/ItemsContainer.tsx b/src/elements/emby-itemscontainer/ItemsContainer.tsx index f817221b0c..75f0df99ed 100644 --- a/src/elements/emby-itemscontainer/ItemsContainer.tsx +++ b/src/elements/emby-itemscontainer/ItemsContainer.tsx @@ -1,7 +1,7 @@ import type { LibraryUpdateInfo } from '@jellyfin/sdk/lib/generated-client'; -import React, { FC, useCallback, useEffect, useRef } from 'react'; +import React, { type FC, useCallback, useEffect, useRef } from 'react'; import classNames from 'classnames'; import Box from '@mui/material/Box'; import Sortable from 'sortablejs'; @@ -19,7 +19,7 @@ import itemShortcuts from 'components/shortcuts'; import MultiSelect from 'components/multiSelect/multiSelect'; import loading from 'components/loading/loading'; import focusManager from 'components/focusManager'; -import { ParentId } from 'types/library'; +import type { ParentId } from 'types/library'; function disableEvent(e: MouseEvent) { e.preventDefault(); diff --git a/src/elements/emby-playstatebutton/PlayedButton.tsx b/src/elements/emby-playstatebutton/PlayedButton.tsx index 687953fb91..25434a0912 100644 --- a/src/elements/emby-playstatebutton/PlayedButton.tsx +++ b/src/elements/emby-playstatebutton/PlayedButton.tsx @@ -1,6 +1,6 @@ import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client'; import { useQueryClient } from '@tanstack/react-query'; -import React, { FC, useCallback } from 'react'; +import React, { type FC, useCallback } from 'react'; import CheckIcon from '@mui/icons-material/Check'; import { IconButton } from '@mui/material'; import classNames from 'classnames'; diff --git a/src/elements/emby-progressbar/AutoTimeProgressBar.tsx b/src/elements/emby-progressbar/AutoTimeProgressBar.tsx index 05b4e6de4a..5dcca778f8 100644 --- a/src/elements/emby-progressbar/AutoTimeProgressBar.tsx +++ b/src/elements/emby-progressbar/AutoTimeProgressBar.tsx @@ -1,7 +1,7 @@ -import React, { FC, useCallback, useEffect, useRef, useState } from 'react'; -import { ProgressOptions } from 'types/progressOptions'; +import React, { type FC, useCallback, useEffect, useRef, useState } from 'react'; import LinearProgress, { linearProgressClasses } from '@mui/material/LinearProgress'; import classNames from 'classnames'; +import type { ProgressOptions } from 'types/progressOptions'; interface AutoTimeProgressBarProps { pct: number; diff --git a/src/elements/emby-ratingbutton/FavoriteButton.tsx b/src/elements/emby-ratingbutton/FavoriteButton.tsx index 673f51c336..2d97fc2747 100644 --- a/src/elements/emby-ratingbutton/FavoriteButton.tsx +++ b/src/elements/emby-ratingbutton/FavoriteButton.tsx @@ -1,4 +1,4 @@ -import React, { FC, useCallback } from 'react'; +import React, { type FC, useCallback } from 'react'; import { useQueryClient } from '@tanstack/react-query'; import FavoriteIcon from '@mui/icons-material/Favorite'; import { IconButton } from '@mui/material'; diff --git a/src/elements/emby-scrollbuttons/ScrollButtons.tsx b/src/elements/emby-scrollbuttons/ScrollButtons.tsx index 80e8a705c7..050354dfba 100644 --- a/src/elements/emby-scrollbuttons/ScrollButtons.tsx +++ b/src/elements/emby-scrollbuttons/ScrollButtons.tsx @@ -1,4 +1,4 @@ -import React, { FC, useCallback, useEffect, useRef, useState } from 'react'; +import React, { type FC, useCallback, useEffect, useRef, useState } from 'react'; import scrollerFactory from '../../libraries/scroller'; import globalize from '../../scripts/globalize'; import IconButton from '../emby-button/IconButton'; diff --git a/src/elements/emby-scroller/Scroller.tsx b/src/elements/emby-scroller/Scroller.tsx index cb3d5b75b4..1a31101928 100644 --- a/src/elements/emby-scroller/Scroller.tsx +++ b/src/elements/emby-scroller/Scroller.tsx @@ -1,4 +1,4 @@ -import React, { FC, useCallback, useEffect, useRef, useState } from 'react'; +import React, { type FC, useCallback, useEffect, useRef, useState } from 'react'; import classNames from 'classnames'; import useElementSize from 'hooks/useElementSize'; import layoutManager from '../../components/layoutManager'; From 11d013b07ee8523f9829386ef2bd7002eb802989 Mon Sep 17 00:00:00 2001 From: grafixeyehero Date: Wed, 28 Feb 2024 21:18:37 +0300 Subject: [PATCH 16/24] Remove escapeHTML Co-authored-by: Bill Thornton --- src/components/cardbuilder/Card/CardHoverMenu.tsx | 3 +-- src/components/cardbuilder/Card/CardOverlayButtons.tsx | 3 +-- src/components/cardbuilder/Card/CardText.tsx | 3 +-- src/components/cardbuilder/Card/cardHelper.ts | 2 +- src/components/common/DefaultName.tsx | 3 +-- src/components/listview/List/ListWrapper.tsx | 3 +-- src/components/listview/List/Lists.tsx | 3 +-- 7 files changed, 7 insertions(+), 13 deletions(-) diff --git a/src/components/cardbuilder/Card/CardHoverMenu.tsx b/src/components/cardbuilder/Card/CardHoverMenu.tsx index b4f56f7188..c09e2bad83 100644 --- a/src/components/cardbuilder/Card/CardHoverMenu.tsx +++ b/src/components/cardbuilder/Card/CardHoverMenu.tsx @@ -2,7 +2,6 @@ import React, { type FC } from 'react'; import Box from '@mui/material/Box'; import ButtonGroup from '@mui/material/ButtonGroup'; import classNames from 'classnames'; -import escapeHTML from 'escape-html'; import { appRouter } from 'components/router/appRouter'; import itemHelper from 'components/itemHelper'; import { playbackManager } from 'components/playback/playbackmanager'; @@ -42,7 +41,7 @@ const CardHoverMenu: FC = ({ > diff --git a/src/components/cardbuilder/Card/CardOverlayButtons.tsx b/src/components/cardbuilder/Card/CardOverlayButtons.tsx index 482a14a816..f3b1b34749 100644 --- a/src/components/cardbuilder/Card/CardOverlayButtons.tsx +++ b/src/components/cardbuilder/Card/CardOverlayButtons.tsx @@ -2,7 +2,6 @@ import React, { type FC } from 'react'; import ButtonGroup from '@mui/material/ButtonGroup'; import classNames from 'classnames'; import { appRouter } from 'components/router/appRouter'; -import escapeHTML from 'escape-html'; import PlayArrowIconButton from '../../common/PlayArrowIconButton'; import MoreVertIconButton from '../../common/MoreVertIconButton'; @@ -61,7 +60,7 @@ const CardOverlayButtons: FC = ({ return ( = ({ className, textLine }) => { title={titleAction.title} {...titleAction.dataAttributes} > - {escapeHTML(titleAction.title)} + {titleAction.title} ); } else { diff --git a/src/components/cardbuilder/Card/cardHelper.ts b/src/components/cardbuilder/Card/cardHelper.ts index b07289dfc2..4ce3247379 100644 --- a/src/components/cardbuilder/Card/cardHelper.ts +++ b/src/components/cardbuilder/Card/cardHelper.ts @@ -216,7 +216,7 @@ function getParentTitle( serverId: NullableString, item: ItemDto ) { - if (isOuterFooter && item.AlbumArtists && item.AlbumArtists.length) { + if (isOuterFooter && item.AlbumArtists?.length) { (item.AlbumArtists[0] as BaseItemDto).Type = BaseItemKind.MusicArtist; (item.AlbumArtists[0] as BaseItemDto).IsFolder = true; return getTextActionButton(item.AlbumArtists[0], null, serverId); diff --git a/src/components/common/DefaultName.tsx b/src/components/common/DefaultName.tsx index 0ead8876a3..ba782e1162 100644 --- a/src/components/common/DefaultName.tsx +++ b/src/components/common/DefaultName.tsx @@ -1,6 +1,5 @@ import React, { type FC } from 'react'; import Box from '@mui/material/Box'; -import escapeHTML from 'escape-html'; import itemHelper from 'components/itemHelper'; import { isUsingLiveTvNaming } from '../cardbuilder/cardBuilderUtils'; import type { ItemDto } from 'types/itemDto'; @@ -15,7 +14,7 @@ const DefaultName: FC = ({ item }) => { itemHelper.getDisplayName(item); return ( - {escapeHTML(defaultName)} + {defaultName} ); }; diff --git a/src/components/listview/List/ListWrapper.tsx b/src/components/listview/List/ListWrapper.tsx index 76303a0f2b..fb03919191 100644 --- a/src/components/listview/List/ListWrapper.tsx +++ b/src/components/listview/List/ListWrapper.tsx @@ -1,5 +1,4 @@ import classNames from 'classnames'; -import escapeHTML from 'escape-html'; import React, { type FC } from 'react'; import Box from '@mui/material/Box'; import Button from '@mui/material/Button'; @@ -31,7 +30,7 @@ const ListWrapper: FC = ({ 'itemAction listItem-button listItem-focusscale' )} data-action={action} - aria-label={escapeHTML(title)} + aria-label={title} {...dataAttributes} > {children} diff --git a/src/components/listview/List/Lists.tsx b/src/components/listview/List/Lists.tsx index 51f5612bba..1516140635 100644 --- a/src/components/listview/List/Lists.tsx +++ b/src/components/listview/List/Lists.tsx @@ -1,5 +1,4 @@ import React, { type FC } from 'react'; -import escapeHTML from 'escape-html'; import { groupBy } from 'lodash-es'; import Box from '@mui/material/Box'; import { getIndex } from './listHelper'; @@ -43,7 +42,7 @@ const Lists: FC = ({ items = [], listOptions = {} }) => { {itemGroupTitle && ( - {escapeHTML(itemGroupTitle)} + {itemGroupTitle} )} {getItems.map((item) => renderListItem(item, index))} From 8cbddba8fd8cccf2bb5222d65110a94119d17343 Mon Sep 17 00:00:00 2001 From: grafixeyehero Date: Wed, 28 Feb 2024 22:47:36 +0300 Subject: [PATCH 17/24] Use enums CardShape & BaseItemKind, Use type import for react FC, Remove escapeHTML Co-authored-by: Bill Thornton --- .../library/GenresSectionContainer.tsx | 13 ++++---- .../components/library/ItemsView.tsx | 22 ++++++------- .../library/ProgramsSectionView.tsx | 9 ++--- .../library/SuggestionsSectionView.tsx | 14 ++++---- .../components/library/UpcomingView.tsx | 7 ++-- src/components/cardbuilder/Card/CardBox.tsx | 4 +-- .../cardbuilder/Card/CardImageContainer.tsx | 5 +-- .../cardbuilder/Card/CardOverlayButtons.tsx | 8 +++-- src/components/cardbuilder/Card/useCard.ts | 10 +++--- .../cardbuilder/Card/useCardImageUrl.ts | 13 ++++---- .../cardbuilder/cardBuilderUtils.ts | 9 ++--- src/components/indicators/useIndicator.tsx | 2 +- src/components/listview/List/ListWrapper.tsx | 2 +- src/types/cardOptions.ts | 14 +++++--- src/types/listOptions.ts | 7 ++-- src/types/progressOptions.ts | 2 +- src/utils/card.ts | 10 +++++- src/utils/sections.ts | 33 ++++++++++--------- 18 files changed, 102 insertions(+), 82 deletions(-) diff --git a/src/apps/experimental/components/library/GenresSectionContainer.tsx b/src/apps/experimental/components/library/GenresSectionContainer.tsx index 13ba08ced6..39e81052eb 100644 --- a/src/apps/experimental/components/library/GenresSectionContainer.tsx +++ b/src/apps/experimental/components/library/GenresSectionContainer.tsx @@ -1,18 +1,17 @@ import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client'; +import { CollectionType } from '@jellyfin/sdk/lib/generated-client/models/collection-type'; import { ItemFields } from '@jellyfin/sdk/lib/generated-client/models/item-fields'; import { ImageType } from '@jellyfin/sdk/lib/generated-client/models/image-type'; import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind'; import { ItemSortBy } from '@jellyfin/sdk/lib/models/api/item-sort-by'; import { SortOrder } from '@jellyfin/sdk/lib/generated-client/models/sort-order'; -import escapeHTML from 'escape-html'; -import React, { FC } from 'react'; - +import React, { type FC } from 'react'; import { useGetItems } from 'hooks/useFetchItems'; import Loading from 'components/loading/LoadingComponent'; import { appRouter } from 'components/router/appRouter'; import SectionContainer from './SectionContainer'; -import { CollectionType } from '@jellyfin/sdk/lib/generated-client/models/collection-type'; -import { ParentId } from 'types/library'; +import { CardShape } from 'utils/card'; +import type { ParentId } from 'types/library'; interface GenresSectionContainerProps { parentId: ParentId; @@ -60,7 +59,7 @@ const GenresSectionContainer: FC = ({ } return = ({ showTitle: true, centerText: true, cardLayout: false, - shape: collectionType === CollectionType.Music ? 'overflowSquare' : 'overflowPortrait', + shape: collectionType === CollectionType.Music ? CardShape.SquareOverflow : CardShape.PortraitOverflow, showParentTitle: collectionType === CollectionType.Music, showYear: collectionType !== CollectionType.Music }} diff --git a/src/apps/experimental/components/library/ItemsView.tsx b/src/apps/experimental/components/library/ItemsView.tsx index b13ab14165..93be9d1348 100644 --- a/src/apps/experimental/components/library/ItemsView.tsx +++ b/src/apps/experimental/components/library/ItemsView.tsx @@ -1,12 +1,14 @@ import type { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind'; +import { CollectionType } from '@jellyfin/sdk/lib/generated-client/models/collection-type'; 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 React, { type FC, useCallback } from 'react'; import Box from '@mui/material/Box'; import classNames from 'classnames'; import { useLocalStorage } from 'hooks/useLocalStorage'; import { useGetItem, useGetItemsViewByType } from 'hooks/useFetchItems'; import { getDefaultLibraryViewSettings, getSettingsKey } from 'utils/items'; +import { CardShape } from 'utils/card'; import Loading from 'components/loading/LoadingComponent'; import { playbackManager } from 'components/playback/playbackmanager'; import ItemsContainer from 'elements/emby-itemscontainer/ItemsContainer'; @@ -22,10 +24,8 @@ import GridListViewButton from './GridListViewButton'; import NoItemsMessage from 'components/common/NoItemsMessage'; import Lists from 'components/listview/List/Lists'; import Cards from 'components/cardbuilder/Card/Cards'; -import { type LibraryViewSettings, type ParentId, ViewMode } from 'types/library'; -import { CollectionType } from '@jellyfin/sdk/lib/generated-client/models/collection-type'; import { LibraryTab } from 'types/libraryTab'; - +import { type LibraryViewSettings, type ParentId, ViewMode } from 'types/library'; import type { CardOptions } from 'types/cardOptions'; import type { ListOptions } from 'types/listOptions'; @@ -110,18 +110,18 @@ const ItemsView: FC = ({ let preferLogo; if (libraryViewSettings.ImageType === ImageType.Banner) { - shape = 'banner'; + shape = CardShape.Banner; } else if (libraryViewSettings.ImageType === ImageType.Disc) { - shape = 'square'; + shape = CardShape.Square; preferDisc = true; } else if (libraryViewSettings.ImageType === ImageType.Logo) { - shape = 'backdrop'; + shape = CardShape.Backdrop; preferLogo = true; } else if (libraryViewSettings.ImageType === ImageType.Thumb) { - shape = 'backdrop'; + shape = CardShape.Backdrop; preferThumb = true; } else { - shape = 'auto'; + shape = CardShape.Auto; } const cardOptions: CardOptions = { @@ -152,12 +152,12 @@ const ItemsView: FC = ({ cardOptions.showYear = false; cardOptions.overlayPlayButton = true; } else if (viewType === LibraryTab.Channels) { - cardOptions.shape = 'square'; + cardOptions.shape = CardShape.Square; cardOptions.showDetailsMenu = true; cardOptions.showCurrentProgram = true; cardOptions.showCurrentProgramTime = true; } else if (viewType === LibraryTab.SeriesTimers) { - cardOptions.shape = 'backdrop'; + cardOptions.shape = CardShape.Backdrop; cardOptions.showSeriesTimerTime = true; cardOptions.showSeriesTimerChannel = true; cardOptions.overlayMoreButton = true; diff --git a/src/apps/experimental/components/library/ProgramsSectionView.tsx b/src/apps/experimental/components/library/ProgramsSectionView.tsx index b15f319789..960ba1e96f 100644 --- a/src/apps/experimental/components/library/ProgramsSectionView.tsx +++ b/src/apps/experimental/components/library/ProgramsSectionView.tsx @@ -1,11 +1,12 @@ -import React, { FC } from 'react'; +import React, { type FC } from 'react'; import { useGetProgramsSectionsWithItems, useGetTimers } from 'hooks/useFetchItems'; import { appRouter } from 'components/router/appRouter'; import globalize from 'scripts/globalize'; import Loading from 'components/loading/LoadingComponent'; import SectionContainer from './SectionContainer'; -import { ParentId } from 'types/library'; -import { Section, SectionType } from 'types/sections'; +import { CardShape } from 'utils/card'; +import type { ParentId } from 'types/library'; +import type { Section, SectionType } from 'types/sections'; interface ProgramsSectionViewProps { parentId: ParentId; @@ -76,7 +77,7 @@ const ProgramsSectionView: FC = ({ items={group.timerInfo ?? []} cardOptions={{ queryKey: ['Timers'], - shape: 'overflowBackdrop', + shape: CardShape.BackdropOverflow, showTitle: true, showParentTitleOrTitle: true, showAirTime: true, diff --git a/src/apps/experimental/components/library/SuggestionsSectionView.tsx b/src/apps/experimental/components/library/SuggestionsSectionView.tsx index d41270e4a6..ca3631e67d 100644 --- a/src/apps/experimental/components/library/SuggestionsSectionView.tsx +++ b/src/apps/experimental/components/library/SuggestionsSectionView.tsx @@ -1,9 +1,8 @@ import { - RecommendationDto, + type RecommendationDto, RecommendationType } from '@jellyfin/sdk/lib/generated-client'; -import React, { FC } from 'react'; -import escapeHTML from 'escape-html'; +import React, { type FC } from 'react'; import { useGetMovieRecommendations, useGetSuggestionSectionsWithItems @@ -12,8 +11,9 @@ import { appRouter } from 'components/router/appRouter'; import globalize from 'scripts/globalize'; import Loading from 'components/loading/LoadingComponent'; import SectionContainer from './SectionContainer'; -import { ParentId } from 'types/library'; -import { Section, SectionType } from 'types/sections'; +import { CardShape } from 'utils/card'; +import type { ParentId } from 'types/library'; +import type { Section, SectionType } from 'types/sections'; interface SuggestionsSectionViewProps { parentId: ParentId; @@ -89,7 +89,7 @@ const SuggestionsSectionView: FC = ({ ); break; } - return escapeHTML(title); + return title; }; return ( @@ -119,7 +119,7 @@ const SuggestionsSectionView: FC = ({ items={recommendation.Items ?? []} cardOptions={{ queryKey: ['MovieRecommendations'], - shape: 'overflowPortrait', + shape: CardShape.PortraitOverflow, showYear: true, scalable: true, overlayPlayButton: true, diff --git a/src/apps/experimental/components/library/UpcomingView.tsx b/src/apps/experimental/components/library/UpcomingView.tsx index bf6a6b0ace..874382d9e5 100644 --- a/src/apps/experimental/components/library/UpcomingView.tsx +++ b/src/apps/experimental/components/library/UpcomingView.tsx @@ -1,10 +1,11 @@ -import React, { FC } from 'react'; +import React, { type FC } from 'react'; import Box from '@mui/material/Box'; import { useGetGroupsUpcomingEpisodes } from 'hooks/useFetchItems'; import Loading from 'components/loading/LoadingComponent'; import globalize from 'scripts/globalize'; import SectionContainer from './SectionContainer'; -import { LibraryViewProps } from 'types/library'; +import { CardShape } from 'utils/card'; +import type { LibraryViewProps } from 'types/library'; const UpcomingView: FC = ({ parentId }) => { const { isLoading, data: groupsUpcomingEpisodes } = useGetGroupsUpcomingEpisodes(parentId); @@ -29,7 +30,7 @@ const UpcomingView: FC = ({ parentId }) => { sectionTitle={group.name} items={group.items ?? []} cardOptions={{ - shape: 'overflowBackdrop', + shape: CardShape.BackdropOverflow, showLocationTypeIndicator: false, showParentTitle: true, preferThumb: true, diff --git a/src/components/cardbuilder/Card/CardBox.tsx b/src/components/cardbuilder/Card/CardBox.tsx index 34e5044bec..07c8bc2d67 100644 --- a/src/components/cardbuilder/Card/CardBox.tsx +++ b/src/components/cardbuilder/Card/CardBox.tsx @@ -5,7 +5,7 @@ import CardOverlayButtons from './CardOverlayButtons'; import CardHoverMenu from './CardHoverMenu'; import CardOuterFooter from './CardOuterFooter'; import CardContent from './CardContent'; - +import { CardShape } from 'utils/card'; import type { ItemDto } from 'types/itemDto'; import type { CardOptions } from 'types/cardOptions'; @@ -13,7 +13,7 @@ interface CardBoxProps { item: ItemDto; cardOptions: CardOptions; className: string; - shape: string | null | undefined; + shape: CardShape | undefined; imgUrl: string | undefined; blurhash: string | undefined; forceName: boolean; diff --git a/src/components/cardbuilder/Card/CardImageContainer.tsx b/src/components/cardbuilder/Card/CardImageContainer.tsx index 69eb47c66b..8a10b6b330 100644 --- a/src/components/cardbuilder/Card/CardImageContainer.tsx +++ b/src/components/cardbuilder/Card/CardImageContainer.tsx @@ -1,3 +1,4 @@ +import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind'; import React, { type FC } from 'react'; import Box from '@mui/material/Box'; import classNames from 'classnames'; @@ -32,7 +33,7 @@ const CardImageContainer: FC = ({ const cardImageClass = classNames( 'cardImageContainer', { coveredImage: coveredImage }, - { 'coveredImage-contain': coveredImage && item.Type === 'TvChannel' } + { 'coveredImage-contain': coveredImage && item.Type === BaseItemKind.TvChannel } ); return ( @@ -52,7 +53,7 @@ const CardImageContainer: FC = ({ indicator.getChildCountIndicator() : indicator.getPlayedIndicator()} - {(item.Type === 'CollectionFolder' + {(item.Type === BaseItemKind.CollectionFolder || item.CollectionType) && item.RefreshProgress && ( diff --git a/src/components/cardbuilder/Card/CardOverlayButtons.tsx b/src/components/cardbuilder/Card/CardOverlayButtons.tsx index f3b1b34749..66abd459e3 100644 --- a/src/components/cardbuilder/Card/CardOverlayButtons.tsx +++ b/src/components/cardbuilder/Card/CardOverlayButtons.tsx @@ -1,3 +1,5 @@ +import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind'; +import { LocationType } from '@jellyfin/sdk/lib/generated-client/models/location-type'; import React, { type FC } from 'react'; import ButtonGroup from '@mui/material/ButtonGroup'; import classNames from 'classnames'; @@ -15,10 +17,10 @@ const sholudShowOverlayPlayButton = ( return ( overlayPlayButton && !item.IsPlaceHolder - && (item.LocationType !== 'Virtual' + && (item.LocationType !== LocationType.Virtual || !item.MediaType - || item.Type === 'Program') - && item.Type !== 'Person' + || item.Type === BaseItemKind.Program) + && item.Type !== BaseItemKind.Person ); }; diff --git a/src/components/cardbuilder/Card/useCard.ts b/src/components/cardbuilder/Card/useCard.ts index 5751471801..80c5dd69b7 100644 --- a/src/components/cardbuilder/Card/useCard.ts +++ b/src/components/cardbuilder/Card/useCard.ts @@ -1,3 +1,4 @@ +import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind'; import classNames from 'classnames'; import useCardImageUrl from './useCardImageUrl'; import { @@ -5,6 +6,7 @@ import { resolveMixedShapeByAspectRatio } from '../cardBuilderUtils'; import { getDataAttributes } from 'utils/items'; +import { CardShape } from 'utils/card'; import layoutManager from 'components/layoutManager'; import type { ItemDto } from 'types/itemDto'; @@ -24,7 +26,7 @@ function useCard({ item, cardOptions }: UseCardProps) { let shape = cardOptions.shape; - if (shape === 'mixed') { + if (shape === CardShape.Mixed) { shape = resolveMixedShapeByAspectRatio(item.PrimaryImageAspectRatio); } @@ -82,9 +84,9 @@ function useCard({ item, cardOptions }: UseCardProps) { { groupedCard: cardOptions.showChildCountIndicator && item.ChildCount }, { 'card-withuserdata': - item.Type !== 'MusicAlbum' - && item.Type !== 'MusicArtist' - && item.Type !== 'Audio' + item.Type !== BaseItemKind.MusicAlbum + && item.Type !== BaseItemKind.MusicArtist + && item.Type !== BaseItemKind.Audio }, { itemAction: layoutManager.tv } ); diff --git a/src/components/cardbuilder/Card/useCardImageUrl.ts b/src/components/cardbuilder/Card/useCardImageUrl.ts index 3e1997d542..1d1fca2dc7 100644 --- a/src/components/cardbuilder/Card/useCardImageUrl.ts +++ b/src/components/cardbuilder/Card/useCardImageUrl.ts @@ -1,8 +1,9 @@ -import { BaseItemKind, ImageType } from '@jellyfin/sdk/lib/generated-client'; +import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind'; +import { ImageType } from '@jellyfin/sdk/lib/generated-client/models/image-type'; import { getImageApi } from '@jellyfin/sdk/lib/utils/api/image-api'; import { useApi } from 'hooks/useApi'; import { getDesiredAspect } from '../cardBuilderUtils'; - +import { CardShape } from 'utils/card'; import type { ItemDto, NullableNumber, NullableString } from 'types/itemDto'; import type { CardOptions } from 'types/cardOptions'; @@ -98,10 +99,10 @@ function isCoverImage( function shouldShowPreferBanner( imageTagsBanner: NullableString, cardOptions: CardOptions, - shape: NullableString + shape: CardShape | undefined ): boolean { return ( - (cardOptions.preferBanner || shape === 'banner') + (cardOptions.preferBanner || shape === CardShape.Banner) && Boolean(imageTagsBanner) ); } @@ -152,7 +153,7 @@ function shouldShowPreferThumb(itemType: NullableString, cardOptions: CardOption function getCardImageInfo( item: ItemDto, cardOptions: CardOptions, - shape: NullableString + shape: CardShape | undefined ) { const width = cardOptions.width; let height; @@ -251,7 +252,7 @@ function getCardImageInfo( interface UseCardImageUrlProps { item: ItemDto; cardOptions: CardOptions; - shape: NullableString; + shape: CardShape | undefined; } function useCardImageUrl({ item, cardOptions, shape }: UseCardImageUrlProps) { diff --git a/src/components/cardbuilder/cardBuilderUtils.ts b/src/components/cardbuilder/cardBuilderUtils.ts index 3ac471ccb1..ecd1d375b5 100644 --- a/src/components/cardbuilder/cardBuilderUtils.ts +++ b/src/components/cardbuilder/cardBuilderUtils.ts @@ -1,3 +1,4 @@ +import { CardShape } from 'utils/card'; import { randomInt } from '../../utils/number'; import classNames from 'classnames'; @@ -54,15 +55,15 @@ export const isResizable = (windowWidth: number): boolean => { */ export const resolveMixedShapeByAspectRatio = (primaryImageAspectRatio: number | null | undefined) => { if (primaryImageAspectRatio === undefined || primaryImageAspectRatio === null) { - return 'mixedSquare'; + return CardShape.MixedSquare; } if (primaryImageAspectRatio >= 1.33) { - return 'mixedBackdrop'; + return CardShape.MixedBackdrop; } else if (primaryImageAspectRatio > 0.71) { - return 'mixedSquare'; + return CardShape.MixedSquare; } else { - return 'mixedPortrait'; + return CardShape.MixedPortrait; } }; diff --git a/src/components/indicators/useIndicator.tsx b/src/components/indicators/useIndicator.tsx index 214c736267..d6e3df180e 100644 --- a/src/components/indicators/useIndicator.tsx +++ b/src/components/indicators/useIndicator.tsx @@ -193,7 +193,7 @@ const useIndicator = (item: ItemDto) => { const getProgressBar = (progressOptions?: ProgressOptions) => { if ( enableProgressIndicator(item.Type, item.MediaType) - && item.Type !== 'Recording' + && item.Type !== BaseItemKind.Recording ) { const playedPercentage = progressOptions?.userData?.PlayedPercentage ? progressOptions.userData.PlayedPercentage : diff --git a/src/components/listview/List/ListWrapper.tsx b/src/components/listview/List/ListWrapper.tsx index fb03919191..9b394f9839 100644 --- a/src/components/listview/List/ListWrapper.tsx +++ b/src/components/listview/List/ListWrapper.tsx @@ -30,7 +30,7 @@ const ListWrapper: FC = ({ 'itemAction listItem-button listItem-focusscale' )} data-action={action} - aria-label={title} + aria-label={title || ''} {...dataAttributes} > {children} diff --git a/src/types/cardOptions.ts b/src/types/cardOptions.ts index 3745d804a4..19cea6a272 100644 --- a/src/types/cardOptions.ts +++ b/src/types/cardOptions.ts @@ -1,7 +1,11 @@ -import type { BaseItemDtoImageBlurHashes, BaseItemKind, ImageType, UserItemDataDto } from '@jellyfin/sdk/lib/generated-client'; -import { CollectionType } from '@jellyfin/sdk/lib/generated-client/models/collection-type'; +import type { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind'; +import type { ImageType } from '@jellyfin/sdk/lib/generated-client/models/image-type'; +import type { UserItemDataDto } from '@jellyfin/sdk/lib/generated-client/models/user-item-data-dto'; +import type { BaseItemDtoImageBlurHashes } from '@jellyfin/sdk/lib/generated-client/models/base-item-dto-image-blur-hashes'; +import type { CollectionType } from '@jellyfin/sdk/lib/generated-client/models/collection-type'; +import { CardShape } from 'utils/card'; import type { ItemDto, NullableString } from './itemDto'; -import { ParentId } from './library'; +import type { ParentId } from './library'; export interface CardOptions { itemsContainer?: HTMLElement | null; @@ -20,7 +24,8 @@ export interface CardOptions { preferDisc?: boolean; preferLogo?: boolean; scalable?: boolean; - shape?: string | null; + shape?: CardShape; + defaultShape?: CardShape; lazy?: boolean; cardLayout?: boolean | null; showParentTitle?: boolean; @@ -39,7 +44,6 @@ export interface CardOptions { lines?: number; context?: CollectionType; action?: string | null; - defaultShape?: string; indexBy?: string; parentId?: ParentId; showMenu?: boolean; diff --git a/src/types/listOptions.ts b/src/types/listOptions.ts index 7df383a48b..b9b5eea80a 100644 --- a/src/types/listOptions.ts +++ b/src/types/listOptions.ts @@ -1,9 +1,8 @@ -import { BaseItemDto, SeriesTimerInfoDto } from '@jellyfin/sdk/lib/generated-client'; import { ItemSortBy } from '@jellyfin/sdk/lib/models/api/item-sort-by'; -import { CollectionType } from '@jellyfin/sdk/lib/generated-client/models/collection-type'; - +import type { CollectionType } from '@jellyfin/sdk/lib/generated-client/models/collection-type'; +import type { ItemDto } from './itemDto'; export interface ListOptions { - items?: BaseItemDto[] | SeriesTimerInfoDto[] | null; + items?: ItemDto[] | null; index?: string; showIndex?: boolean; action?: string | null; diff --git a/src/types/progressOptions.ts b/src/types/progressOptions.ts index ae043f2066..fd5ed7e468 100644 --- a/src/types/progressOptions.ts +++ b/src/types/progressOptions.ts @@ -1,4 +1,4 @@ -import { UserItemDataDto } from '@jellyfin/sdk/lib/generated-client'; +import type { UserItemDataDto } from '@jellyfin/sdk/lib/generated-client/models/user-item-data-dto'; export interface ProgressOptions { containerClass: string, diff --git a/src/utils/card.ts b/src/utils/card.ts index bdb68e7243..950964054a 100644 --- a/src/utils/card.ts +++ b/src/utils/card.ts @@ -5,7 +5,15 @@ export enum CardShape { Portrait = 'portrait', PortraitOverflow = 'overflowPortrait', Square = 'square', - SquareOverflow = 'overflowSquare' + SquareOverflow = 'overflowSquare', + Auto = 'auto', + AutoHome = 'autohome', + AutoOverflow = 'autooverflow', + AutoVertical = 'autoVertical', + Mixed = 'mixed', + MixedSquare = 'mixedSquare', + MixedBackdrop = 'mixedBackdrop', + MixedPortrait = 'mixedPortrait', } export function getSquareShape(enableOverflow = true) { diff --git a/src/utils/sections.ts b/src/utils/sections.ts index 4617ea7f14..ce78417da8 100644 --- a/src/utils/sections.ts +++ b/src/utils/sections.ts @@ -3,6 +3,7 @@ import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-ite import { ItemSortBy } from '@jellyfin/sdk/lib/models/api/item-sort-by'; import { SortOrder } from '@jellyfin/sdk/lib/generated-client/models/sort-order'; import * as userSettings from 'scripts/settings/userSettings'; +import { CardShape } from 'utils/card'; import { Section, SectionType, SectionApiMethod } from 'types/sections'; export const getSuggestionSections = (): Section[] => { @@ -29,7 +30,7 @@ export const getSuggestionSections = (): Section[] => { cardOptions: { overlayPlayButton: true, preferThumb: true, - shape: 'overflowBackdrop', + shape: CardShape.BackdropOverflow, showYear: true } }, @@ -43,7 +44,7 @@ export const getSuggestionSections = (): Section[] => { }, cardOptions: { overlayPlayButton: true, - shape: 'overflowPortrait', + shape: CardShape.PortraitOverflow, showYear: true } }, @@ -57,7 +58,7 @@ export const getSuggestionSections = (): Section[] => { }, cardOptions: { overlayPlayButton: true, - shape: 'overflowBackdrop', + shape: CardShape.BackdropOverflow, preferThumb: true, inheritThumb: !userSettings.useEpisodeImagesInNextUpAndResume(undefined), @@ -74,7 +75,7 @@ export const getSuggestionSections = (): Section[] => { }, cardOptions: { overlayPlayButton: true, - shape: 'overflowBackdrop', + shape: CardShape.BackdropOverflow, preferThumb: true, showSeriesYear: true, showParentTitle: true, @@ -90,7 +91,7 @@ export const getSuggestionSections = (): Section[] => { type: SectionType.NextUp, cardOptions: { overlayPlayButton: true, - shape: 'overflowBackdrop', + shape: CardShape.BackdropOverflow, preferThumb: true, inheritThumb: !userSettings.useEpisodeImagesInNextUpAndResume(undefined), @@ -107,7 +108,7 @@ export const getSuggestionSections = (): Section[] => { }, cardOptions: { showUnplayedIndicator: false, - shape: 'overflowSquare', + shape: CardShape.SquareOverflow, showParentTitle: true, overlayPlayButton: true, coverImage: true @@ -125,7 +126,7 @@ export const getSuggestionSections = (): Section[] => { }, cardOptions: { showUnplayedIndicator: false, - shape: 'overflowSquare', + shape: CardShape.SquareOverflow, showParentTitle: true, action: 'instantmix', overlayMoreButton: true, @@ -144,7 +145,7 @@ export const getSuggestionSections = (): Section[] => { }, cardOptions: { showUnplayedIndicator: false, - shape: 'overflowSquare', + shape: CardShape.SquareOverflow, showParentTitle: true, action: 'instantmix', overlayMoreButton: true, @@ -157,8 +158,8 @@ export const getSuggestionSections = (): Section[] => { export const getProgramSections = (): Section[] => { const cardOptions = { inheritThumb: false, - shape: 'autooverflow', - defaultShape: 'overflowBackdrop', + shape: CardShape.AutoOverflow, + defaultShape: CardShape.BackdropOverflow, centerText: true, coverImage: true, overlayText: false, @@ -309,8 +310,8 @@ export const getProgramSections = (): Section[] => { cardOptions: { showYear: true, lines: 2, - shape: 'autooverflow', - defaultShape: 'overflowBackdrop', + shape: CardShape.AutoOverflow, + defaultShape: CardShape.BackdropOverflow, showTitle: true, showParentTitle: true, coverImage: true, @@ -328,8 +329,8 @@ export const getProgramSections = (): Section[] => { cardOptions: { showYear: false, showParentTitle: false, - shape: 'autooverflow', - defaultShape: 'overflowBackdrop', + shape: CardShape.AutoOverflow, + defaultShape: CardShape.BackdropOverflow, showTitle: true, coverImage: true, cardLayout: false, @@ -347,8 +348,8 @@ export const getProgramSections = (): Section[] => { isInProgress: true }, cardOptions: { - shape: 'autooverflow', - defaultShape: 'backdrop', + shape: CardShape.AutoOverflow, + defaultShape: CardShape.Backdrop, showParentTitle: false, showParentTitleOrTitle: true, showTitle: true, From 511f8340ef4315abfeb61a0ce84e799f5b1461b0 Mon Sep 17 00:00:00 2001 From: grafixeyehero Date: Wed, 28 Feb 2024 23:10:31 +0300 Subject: [PATCH 18/24] Replace jellyfin sdk generated-client models wiyh full path Co-authored-by: Bill Thornton --- .../components/library/GenresItemsContainer.tsx | 6 +++--- src/apps/experimental/components/library/GenresView.tsx | 6 +++--- .../experimental/components/library/PageTabContent.tsx | 8 ++++---- src/components/common/DefaultIconText.tsx | 2 +- src/components/indicators/useIndicator.tsx | 2 +- src/components/listview/List/ListContent.tsx | 2 +- src/components/listview/List/useListTextlines.tsx | 2 +- src/components/mediainfo/usePrimaryMediaInfo.tsx | 2 +- src/elements/emby-itemscontainer/ItemsContainer.tsx | 2 +- src/elements/emby-playstatebutton/PlayedButton.tsx | 4 ++-- src/elements/emby-ratingbutton/FavoriteButton.tsx | 2 +- src/types/libraryTabContent.ts | 8 ++++---- src/utils/image.ts | 2 +- src/utils/sections.ts | 6 ++++-- 14 files changed, 28 insertions(+), 26 deletions(-) diff --git a/src/apps/experimental/components/library/GenresItemsContainer.tsx b/src/apps/experimental/components/library/GenresItemsContainer.tsx index b3f3b5f7bf..a676a0c78b 100644 --- a/src/apps/experimental/components/library/GenresItemsContainer.tsx +++ b/src/apps/experimental/components/library/GenresItemsContainer.tsx @@ -1,11 +1,11 @@ -import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind'; +import type { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind'; +import type { CollectionType } from '@jellyfin/sdk/lib/generated-client/models/collection-type'; import React, { FC } from 'react'; import { useGetGenres } from 'hooks/useFetchItems'; import globalize from 'scripts/globalize'; import Loading from 'components/loading/LoadingComponent'; import GenresSectionContainer from './GenresSectionContainer'; -import { CollectionType } from '@jellyfin/sdk/lib/generated-client/models/collection-type'; -import { ParentId } from 'types/library'; +import type { ParentId } from 'types/library'; interface GenresItemsContainerProps { parentId: ParentId; diff --git a/src/apps/experimental/components/library/GenresView.tsx b/src/apps/experimental/components/library/GenresView.tsx index 9076c28c6d..50d8c68507 100644 --- a/src/apps/experimental/components/library/GenresView.tsx +++ b/src/apps/experimental/components/library/GenresView.tsx @@ -1,8 +1,8 @@ -import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind'; +import type { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind'; +import type { CollectionType } from '@jellyfin/sdk/lib/generated-client/models/collection-type'; import React, { FC } from 'react'; import GenresItemsContainer from './GenresItemsContainer'; -import { ParentId } from 'types/library'; -import { CollectionType } from '@jellyfin/sdk/lib/generated-client/models/collection-type'; +import type { ParentId } from 'types/library'; interface GenresViewProps { parentId: ParentId; diff --git a/src/apps/experimental/components/library/PageTabContent.tsx b/src/apps/experimental/components/library/PageTabContent.tsx index 9726f2ffa5..6cd99c5575 100644 --- a/src/apps/experimental/components/library/PageTabContent.tsx +++ b/src/apps/experimental/components/library/PageTabContent.tsx @@ -1,13 +1,13 @@ -import React, { FC } from 'react'; +import React, { type FC } from 'react'; import SuggestionsSectionView from './SuggestionsSectionView'; import UpcomingView from './UpcomingView'; import GenresView from './GenresView'; import ItemsView from './ItemsView'; -import { LibraryTab } from 'types/libraryTab'; -import { ParentId } from 'types/library'; -import { LibraryTabContent } from 'types/libraryTabContent'; import GuideView from './GuideView'; import ProgramsSectionView from './ProgramsSectionView'; +import { LibraryTab } from 'types/libraryTab'; +import type { ParentId } from 'types/library'; +import type { LibraryTabContent } from 'types/libraryTabContent'; interface PageTabContentProps { parentId: ParentId; diff --git a/src/components/common/DefaultIconText.tsx b/src/components/common/DefaultIconText.tsx index 60b1aa3fb9..f6af526c07 100644 --- a/src/components/common/DefaultIconText.tsx +++ b/src/components/common/DefaultIconText.tsx @@ -1,4 +1,4 @@ -import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client'; +import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind'; import React, { type FC } from 'react'; import Icon from '@mui/material/Icon'; import imageHelper from 'utils/image'; diff --git a/src/components/indicators/useIndicator.tsx b/src/components/indicators/useIndicator.tsx index d6e3df180e..f54d76d9cd 100644 --- a/src/components/indicators/useIndicator.tsx +++ b/src/components/indicators/useIndicator.tsx @@ -1,5 +1,5 @@ import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind'; -import { LocationType } from '@jellyfin/sdk/lib/generated-client'; +import { LocationType } from '@jellyfin/sdk/lib/generated-client/models/location-type'; import React from 'react'; import Box from '@mui/material/Box'; import LinearProgress, { diff --git a/src/components/listview/List/ListContent.tsx b/src/components/listview/List/ListContent.tsx index f9081f0b8a..0cf2160ca6 100644 --- a/src/components/listview/List/ListContent.tsx +++ b/src/components/listview/List/ListContent.tsx @@ -1,4 +1,4 @@ -import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client'; +import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind'; import React, { type FC } from 'react'; import DragHandleIcon from '@mui/icons-material/DragHandle'; import Box from '@mui/material/Box'; diff --git a/src/components/listview/List/useListTextlines.tsx b/src/components/listview/List/useListTextlines.tsx index cb5f7ceeb8..da66673a5a 100644 --- a/src/components/listview/List/useListTextlines.tsx +++ b/src/components/listview/List/useListTextlines.tsx @@ -1,5 +1,5 @@ +import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind'; 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'; diff --git a/src/components/mediainfo/usePrimaryMediaInfo.tsx b/src/components/mediainfo/usePrimaryMediaInfo.tsx index 480f31dbbc..b702bc42ce 100644 --- a/src/components/mediainfo/usePrimaryMediaInfo.tsx +++ b/src/components/mediainfo/usePrimaryMediaInfo.tsx @@ -1,4 +1,4 @@ -import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client'; +import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind'; import * as userSettings from 'scripts/settings/userSettings'; import datetime from 'scripts/datetime'; import globalize from 'scripts/globalize'; diff --git a/src/elements/emby-itemscontainer/ItemsContainer.tsx b/src/elements/emby-itemscontainer/ItemsContainer.tsx index 75f0df99ed..779e718271 100644 --- a/src/elements/emby-itemscontainer/ItemsContainer.tsx +++ b/src/elements/emby-itemscontainer/ItemsContainer.tsx @@ -174,7 +174,7 @@ const ItemsContainer: FC = ({ const invalidateQueries = useCallback(async () => { await queryClient.invalidateQueries({ - queryKey: queryKey, + queryKey, type: 'all', refetchType: 'active' }); diff --git a/src/elements/emby-playstatebutton/PlayedButton.tsx b/src/elements/emby-playstatebutton/PlayedButton.tsx index 25434a0912..6a15940686 100644 --- a/src/elements/emby-playstatebutton/PlayedButton.tsx +++ b/src/elements/emby-playstatebutton/PlayedButton.tsx @@ -1,4 +1,4 @@ -import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client'; +import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind'; import { useQueryClient } from '@tanstack/react-query'; import React, { type FC, useCallback } from 'react'; import CheckIcon from '@mui/icons-material/Check'; @@ -48,7 +48,7 @@ const PlayedButton: FC = ({ }, { onSuccess: async() => { await queryClient.invalidateQueries({ - queryKey: queryKey, + queryKey, type: 'all', refetchType: 'active' }); diff --git a/src/elements/emby-ratingbutton/FavoriteButton.tsx b/src/elements/emby-ratingbutton/FavoriteButton.tsx index 2d97fc2747..2bbdeef4ab 100644 --- a/src/elements/emby-ratingbutton/FavoriteButton.tsx +++ b/src/elements/emby-ratingbutton/FavoriteButton.tsx @@ -34,7 +34,7 @@ const FavoriteButton: FC = ({ }, { onSuccess: async() => { await queryClient.invalidateQueries({ - queryKey: queryKey, + queryKey, type: 'all', refetchType: 'active' }); diff --git a/src/types/libraryTabContent.ts b/src/types/libraryTabContent.ts index 433744cd22..547d025134 100644 --- a/src/types/libraryTabContent.ts +++ b/src/types/libraryTabContent.ts @@ -1,7 +1,7 @@ -import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client'; -import { LibraryTab } from './libraryTab'; -import { CollectionType } from '@jellyfin/sdk/lib/generated-client/models/collection-type'; -import { SectionType } from './sections'; +import type { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind'; +import type { CollectionType } from '@jellyfin/sdk/lib/generated-client/models/collection-type'; +import type { LibraryTab } from './libraryTab'; +import type { SectionType } from './sections'; export interface SectionsView { suggestionSections?: SectionType[]; diff --git a/src/utils/image.ts b/src/utils/image.ts index 3819f865df..dc32e87973 100644 --- a/src/utils/image.ts +++ b/src/utils/image.ts @@ -1,4 +1,4 @@ -import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client'; +import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind'; import type { DeviceInfo } from '@jellyfin/sdk/lib/generated-client/models/device-info'; import type { SessionInfo } from '@jellyfin/sdk/lib/generated-client/models/session-info'; diff --git a/src/utils/sections.ts b/src/utils/sections.ts index ce78417da8..70c74b598e 100644 --- a/src/utils/sections.ts +++ b/src/utils/sections.ts @@ -1,10 +1,12 @@ -import { ImageType, ItemFields, ItemFilter } from '@jellyfin/sdk/lib/generated-client'; import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind'; +import { ItemFields } from '@jellyfin/sdk/lib/generated-client/models/item-fields'; +import { ItemFilter } from '@jellyfin/sdk/lib/generated-client/models/item-filter'; +import { ImageType } from '@jellyfin/sdk/lib/generated-client/models/image-type'; import { ItemSortBy } from '@jellyfin/sdk/lib/models/api/item-sort-by'; import { SortOrder } from '@jellyfin/sdk/lib/generated-client/models/sort-order'; import * as userSettings from 'scripts/settings/userSettings'; import { CardShape } from 'utils/card'; -import { Section, SectionType, SectionApiMethod } from 'types/sections'; +import { type Section, SectionType, SectionApiMethod } from 'types/sections'; export const getSuggestionSections = (): Section[] => { const parametersOptions = { From bbc1860bdec1c5b71d6e778d7fe1fc2c84a65332 Mon Sep 17 00:00:00 2001 From: grafixeyehero Date: Thu, 29 Feb 2024 04:21:24 +0300 Subject: [PATCH 19/24] Replace hardcoded color with theme reference Co-authored-by: Bill Thornton --- src/components/mediainfo/StarIcons.tsx | 4 +++- .../emby-progressbar/AutoTimeProgressBar.tsx | 6 ++++-- src/themes/theme.ts | 16 ++++++++++++++++ 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/src/components/mediainfo/StarIcons.tsx b/src/components/mediainfo/StarIcons.tsx index faa09ade3a..0d38453d7e 100644 --- a/src/components/mediainfo/StarIcons.tsx +++ b/src/components/mediainfo/StarIcons.tsx @@ -2,6 +2,7 @@ import React, { type FC } from 'react'; import classNames from 'classnames'; import StarIcon from '@mui/icons-material/Star'; import Box from '@mui/material/Box'; +import { useTheme } from '@mui/material/styles'; interface StarIconsProps { className?: string; @@ -9,6 +10,7 @@ interface StarIconsProps { } const StarIcons: FC = ({ className, communityRating }) => { + const theme = useTheme(); const cssClass = classNames( 'mediaInfoItem', 'mediaInfoText', @@ -19,7 +21,7 @@ const StarIcons: FC = ({ className, communityRating }) => { return ( {communityRating.toFixed(1)} diff --git a/src/elements/emby-progressbar/AutoTimeProgressBar.tsx b/src/elements/emby-progressbar/AutoTimeProgressBar.tsx index 5dcca778f8..b6e5910cb6 100644 --- a/src/elements/emby-progressbar/AutoTimeProgressBar.tsx +++ b/src/elements/emby-progressbar/AutoTimeProgressBar.tsx @@ -1,6 +1,7 @@ import React, { type FC, useCallback, useEffect, useRef, useState } from 'react'; -import LinearProgress, { linearProgressClasses } from '@mui/material/LinearProgress'; import classNames from 'classnames'; +import LinearProgress, { linearProgressClasses } from '@mui/material/LinearProgress'; +import { useTheme } from '@mui/material/styles'; import type { ProgressOptions } from 'types/progressOptions'; interface AutoTimeProgressBarProps { @@ -22,6 +23,7 @@ const AutoTimeProgressBar: FC = ({ }) => { const [progress, setProgress] = useState(pct); const timerRef = useRef | null>(null); + const theme = useTheme(); const onAutoTimeProgress = useCallback(() => { const start = parseInt(starTtime.toString(), 10); @@ -67,7 +69,7 @@ const AutoTimeProgressBar: FC = ({ sx={{ [`& .${linearProgressClasses.bar}`]: { borderRadius: 5, - backgroundColor: isRecording ? '#cb272a' : '#00a4dc' + backgroundColor: isRecording ? theme.palette.error.main : theme.palette.primary.main } }} /> diff --git a/src/themes/theme.ts b/src/themes/theme.ts index a5230ebafd..70e060dd74 100644 --- a/src/themes/theme.ts +++ b/src/themes/theme.ts @@ -1,5 +1,15 @@ import { createTheme } from '@mui/material/styles'; +declare module '@mui/material/styles' { + interface Palette { + starIcon: Palette['primary']; + } + + interface PaletteOptions { + starIcon?: PaletteOptions['primary']; + } +} + const LIST_ICON_WIDTH = 36; /** The default Jellyfin app theme for mui */ @@ -18,6 +28,12 @@ const theme = createTheme({ }, action: { selectedOpacity: 0.2 + }, + starIcon: { + main: '#f2b01e' // Yellow color + }, + error: { + main: '#cb272a' // Red color } }, typography: { From 90a1d065579a5aea5dd17f5d301ebdcafacd5475 Mon Sep 17 00:00:00 2001 From: grafixeyehero Date: Thu, 29 Feb 2024 04:22:13 +0300 Subject: [PATCH 20/24] separate shared types Co-authored-by: Bill Thornton --- src/components/cardbuilder/Card/cardHelper.ts | 3 ++- src/components/cardbuilder/Card/useCardImageUrl.ts | 3 ++- src/components/cardbuilder/cardBuilderUtils.ts | 2 +- src/components/indicators/useIndicator.tsx | 3 ++- src/components/mediainfo/usePrimaryMediaInfo.tsx | 3 ++- src/types/base/common/shared/types.ts | 3 +++ src/types/cardOptions.ts | 3 ++- src/types/dataAttributes.ts | 5 +++-- src/types/itemDto.ts | 4 ---- 9 files changed, 17 insertions(+), 12 deletions(-) create mode 100644 src/types/base/common/shared/types.ts diff --git a/src/components/cardbuilder/Card/cardHelper.ts b/src/components/cardbuilder/Card/cardHelper.ts index 4ce3247379..ae4ef06f18 100644 --- a/src/components/cardbuilder/Card/cardHelper.ts +++ b/src/components/cardbuilder/Card/cardHelper.ts @@ -16,7 +16,8 @@ import datetime from 'scripts/datetime'; import { isUsingLiveTvNaming } from '../cardBuilderUtils'; -import type { ItemDto, NullableNumber, NullableString } from 'types/itemDto'; +import type { NullableNumber, NullableString } from 'types/base/common/shared/types'; +import type { ItemDto } from 'types/itemDto'; import type { CardOptions } from 'types/cardOptions'; import type { DataAttributes } from 'types/dataAttributes'; import { getDataAttributes } from 'utils/items'; diff --git a/src/components/cardbuilder/Card/useCardImageUrl.ts b/src/components/cardbuilder/Card/useCardImageUrl.ts index 1d1fca2dc7..8afec0cab3 100644 --- a/src/components/cardbuilder/Card/useCardImageUrl.ts +++ b/src/components/cardbuilder/Card/useCardImageUrl.ts @@ -4,7 +4,8 @@ import { getImageApi } from '@jellyfin/sdk/lib/utils/api/image-api'; import { useApi } from 'hooks/useApi'; import { getDesiredAspect } from '../cardBuilderUtils'; import { CardShape } from 'utils/card'; -import type { ItemDto, NullableNumber, NullableString } from 'types/itemDto'; +import type { NullableNumber, NullableString } from 'types/base/common/shared/types'; +import type { ItemDto } from 'types/itemDto'; import type { CardOptions } from 'types/cardOptions'; function getPreferThumbInfo(item: ItemDto, cardOptions: CardOptions) { diff --git a/src/components/cardbuilder/cardBuilderUtils.ts b/src/components/cardbuilder/cardBuilderUtils.ts index ecd1d375b5..489d6c8abd 100644 --- a/src/components/cardbuilder/cardBuilderUtils.ts +++ b/src/components/cardbuilder/cardBuilderUtils.ts @@ -1,4 +1,4 @@ -import { CardShape } from 'utils/card'; +import { CardShape } from '../../utils/card'; import { randomInt } from '../../utils/number'; import classNames from 'classnames'; diff --git a/src/components/indicators/useIndicator.tsx b/src/components/indicators/useIndicator.tsx index f54d76d9cd..da5ea537bf 100644 --- a/src/components/indicators/useIndicator.tsx +++ b/src/components/indicators/useIndicator.tsx @@ -16,7 +16,8 @@ import classNames from 'classnames'; import datetime from 'scripts/datetime'; import itemHelper from 'components/itemHelper'; import AutoTimeProgressBar from 'elements/emby-progressbar/AutoTimeProgressBar'; -import type { ItemDto, NullableString } from 'types/itemDto'; +import type { NullableString } from 'types/base/common/shared/types'; +import type { ItemDto } from 'types/itemDto'; import type { ProgressOptions } from 'types/progressOptions'; const TypeIcon = { diff --git a/src/components/mediainfo/usePrimaryMediaInfo.tsx b/src/components/mediainfo/usePrimaryMediaInfo.tsx index b702bc42ce..6c58609152 100644 --- a/src/components/mediainfo/usePrimaryMediaInfo.tsx +++ b/src/components/mediainfo/usePrimaryMediaInfo.tsx @@ -3,7 +3,8 @@ import * as userSettings from 'scripts/settings/userSettings'; import datetime from 'scripts/datetime'; import globalize from 'scripts/globalize'; import itemHelper from '../itemHelper'; -import type { ItemDto, NullableNumber, NullableString } from 'types/itemDto'; +import type { NullableNumber, NullableString } from 'types/base/common/shared/types'; +import type { ItemDto } from 'types/itemDto'; import type { MiscInfo } from 'types/mediaInfoItem'; function shouldShowFolderRuntime( diff --git a/src/types/base/common/shared/types.ts b/src/types/base/common/shared/types.ts new file mode 100644 index 0000000000..f89240dc23 --- /dev/null +++ b/src/types/base/common/shared/types.ts @@ -0,0 +1,3 @@ +export type NullableString = string | null | undefined; +export type NullableNumber = number | null | undefined; +export type NullableBoolean = boolean | null | undefined; diff --git a/src/types/cardOptions.ts b/src/types/cardOptions.ts index 19cea6a272..b18906c0c4 100644 --- a/src/types/cardOptions.ts +++ b/src/types/cardOptions.ts @@ -4,7 +4,8 @@ import type { UserItemDataDto } from '@jellyfin/sdk/lib/generated-client/models/ import type { BaseItemDtoImageBlurHashes } from '@jellyfin/sdk/lib/generated-client/models/base-item-dto-image-blur-hashes'; import type { CollectionType } from '@jellyfin/sdk/lib/generated-client/models/collection-type'; import { CardShape } from 'utils/card'; -import type { ItemDto, NullableString } from './itemDto'; +import type { NullableString } from './base/common/shared/types'; +import type { ItemDto } from './itemDto'; import type { ParentId } from './library'; export interface CardOptions { diff --git a/src/types/dataAttributes.ts b/src/types/dataAttributes.ts index 1258d61cd4..b9ae880f7a 100644 --- a/src/types/dataAttributes.ts +++ b/src/types/dataAttributes.ts @@ -1,5 +1,6 @@ -import type { CollectionType, UserItemDataDto } from '@jellyfin/sdk/lib/generated-client'; -import type { NullableBoolean, NullableNumber, NullableString } from './itemDto'; +import type { CollectionType } from '@jellyfin/sdk/lib/generated-client/models/collection-type'; +import type { UserItemDataDto } from '@jellyfin/sdk/lib/generated-client/models/user-item-data-dto'; +import type { NullableBoolean, NullableNumber, NullableString } from './base/common/shared/types'; export type AttributesOpts = { context?: CollectionType, diff --git a/src/types/itemDto.ts b/src/types/itemDto.ts index 37fc15fd8e..a3fcedeaee 100644 --- a/src/types/itemDto.ts +++ b/src/types/itemDto.ts @@ -20,7 +20,3 @@ export interface ItemDto extends BaseItem, TimerInfo, SeriesTimerInfo, SearchHin 'Name'?: string | null; 'ItemId'?: string | null; } - -export type NullableString = string | null | undefined; -export type NullableNumber = number | null | undefined; -export type NullableBoolean = boolean | null | undefined; From efe5d0b84de612e4928012d16c38030719faaad6 Mon Sep 17 00:00:00 2001 From: grafixeyehero Date: Fri, 1 Mar 2024 21:15:52 +0300 Subject: [PATCH 21/24] Remove disabled jsx-no-useless-fragment commit Co-authored-by: Bill Thornton --- src/components/cardbuilder/Card/Cards.tsx | 24 ++++++++--------------- 1 file changed, 8 insertions(+), 16 deletions(-) diff --git a/src/components/cardbuilder/Card/Cards.tsx b/src/components/cardbuilder/Card/Cards.tsx index 82b67cbdfc..ca5a52f76e 100644 --- a/src/components/cardbuilder/Card/Cards.tsx +++ b/src/components/cardbuilder/Card/Cards.tsx @@ -10,23 +10,15 @@ interface CardsProps { cardOptions: CardOptions; } -const Cards: FC = ({ - items = [], - cardOptions -}) => { +const Cards: FC = ({ items, cardOptions }) => { setCardData(items, cardOptions); - return ( - // eslint-disable-next-line react/jsx-no-useless-fragment - <> - {items?.map((item) => ( - - ))} - - ); + + const renderCards = () => + items.map((item) => ( + + )); + + return <>{renderCards()}; }; export default Cards; From d5a775502b7e05bb81893c0f102d50cb4961ca02 Mon Sep 17 00:00:00 2001 From: grafixeyehero Date: Sun, 3 Mar 2024 01:31:35 +0300 Subject: [PATCH 22/24] Move itemdto to base models folder --- src/components/cardbuilder/Card/Card.tsx | 2 +- src/components/cardbuilder/Card/CardBox.tsx | 2 +- src/components/cardbuilder/Card/CardContent.tsx | 2 +- src/components/cardbuilder/Card/CardFooterText.tsx | 2 +- src/components/cardbuilder/Card/CardHoverMenu.tsx | 2 +- src/components/cardbuilder/Card/CardImageContainer.tsx | 2 +- src/components/cardbuilder/Card/CardInnerFooter.tsx | 2 +- src/components/cardbuilder/Card/CardOuterFooter.tsx | 2 +- src/components/cardbuilder/Card/CardOverlayButtons.tsx | 2 +- src/components/cardbuilder/Card/Cards.tsx | 2 +- src/components/cardbuilder/Card/cardHelper.ts | 2 +- src/components/cardbuilder/Card/useCard.ts | 2 +- src/components/cardbuilder/Card/useCardImageUrl.ts | 2 +- src/components/cardbuilder/Card/useCardText.tsx | 2 +- src/components/common/DefaultIconText.tsx | 2 +- src/components/common/DefaultName.tsx | 2 +- src/components/common/Media.tsx | 2 +- src/components/indicators/useIndicator.tsx | 2 +- src/components/listview/List/List.tsx | 2 +- src/components/listview/List/ListContent.tsx | 2 +- src/components/listview/List/ListImageContainer.tsx | 2 +- src/components/listview/List/ListItemBody.tsx | 2 +- src/components/listview/List/ListViewUserDataButtons.tsx | 2 +- src/components/listview/List/Lists.tsx | 2 +- src/components/listview/List/listHelper.ts | 2 +- src/components/listview/List/useList.ts | 2 +- src/components/listview/List/useListTextlines.tsx | 2 +- src/components/mediainfo/PrimaryMediaInfo.tsx | 2 +- src/components/mediainfo/usePrimaryMediaInfo.tsx | 2 +- src/elements/emby-itemrefreshindicator/RefreshIndicator.tsx | 2 +- src/types/{itemDto.ts => base/models/item-dto.ts} | 0 src/types/cardOptions.ts | 2 +- src/types/listOptions.ts | 2 +- 33 files changed, 32 insertions(+), 32 deletions(-) rename src/types/{itemDto.ts => base/models/item-dto.ts} (100%) diff --git a/src/components/cardbuilder/Card/Card.tsx b/src/components/cardbuilder/Card/Card.tsx index e1718e6459..2b5314d4cc 100644 --- a/src/components/cardbuilder/Card/Card.tsx +++ b/src/components/cardbuilder/Card/Card.tsx @@ -4,7 +4,7 @@ import CardWrapper from './CardWrapper'; import CardBox from './CardBox'; import type { CardOptions } from 'types/cardOptions'; -import type { ItemDto } from 'types/itemDto'; +import type { ItemDto } from 'types/base/models/item-dto'; interface CardProps { item?: ItemDto; diff --git a/src/components/cardbuilder/Card/CardBox.tsx b/src/components/cardbuilder/Card/CardBox.tsx index 07c8bc2d67..a7fd41c0cd 100644 --- a/src/components/cardbuilder/Card/CardBox.tsx +++ b/src/components/cardbuilder/Card/CardBox.tsx @@ -6,7 +6,7 @@ import CardHoverMenu from './CardHoverMenu'; import CardOuterFooter from './CardOuterFooter'; import CardContent from './CardContent'; import { CardShape } from 'utils/card'; -import type { ItemDto } from 'types/itemDto'; +import type { ItemDto } from 'types/base/models/item-dto'; import type { CardOptions } from 'types/cardOptions'; interface CardBoxProps { diff --git a/src/components/cardbuilder/Card/CardContent.tsx b/src/components/cardbuilder/Card/CardContent.tsx index 8ebeb0cb87..eb8ee8a2eb 100644 --- a/src/components/cardbuilder/Card/CardContent.tsx +++ b/src/components/cardbuilder/Card/CardContent.tsx @@ -3,7 +3,7 @@ import classNames from 'classnames'; import { getDefaultBackgroundClass } from '../cardBuilderUtils'; import CardImageContainer from './CardImageContainer'; -import type { ItemDto } from 'types/itemDto'; +import type { ItemDto } from 'types/base/models/item-dto'; import type { CardOptions } from 'types/cardOptions'; interface CardContentProps { diff --git a/src/components/cardbuilder/Card/CardFooterText.tsx b/src/components/cardbuilder/Card/CardFooterText.tsx index b9bf7bbafd..9dae59b8f6 100644 --- a/src/components/cardbuilder/Card/CardFooterText.tsx +++ b/src/components/cardbuilder/Card/CardFooterText.tsx @@ -3,7 +3,7 @@ import Box from '@mui/material/Box'; import useCardText from './useCardText'; import layoutManager from 'components/layoutManager'; import MoreVertIconButton from '../../common/MoreVertIconButton'; -import type { ItemDto } from 'types/itemDto'; +import type { ItemDto } from 'types/base/models/item-dto'; import type { CardOptions } from 'types/cardOptions'; const shouldShowDetailsMenu = ( diff --git a/src/components/cardbuilder/Card/CardHoverMenu.tsx b/src/components/cardbuilder/Card/CardHoverMenu.tsx index c09e2bad83..b79747034b 100644 --- a/src/components/cardbuilder/Card/CardHoverMenu.tsx +++ b/src/components/cardbuilder/Card/CardHoverMenu.tsx @@ -11,7 +11,7 @@ import FavoriteButton from 'elements/emby-ratingbutton/FavoriteButton'; import PlayArrowIconButton from '../../common/PlayArrowIconButton'; import MoreVertIconButton from '../../common/MoreVertIconButton'; -import type { ItemDto } from 'types/itemDto'; +import type { ItemDto } from 'types/base/models/item-dto'; import type { CardOptions } from 'types/cardOptions'; interface CardHoverMenuProps { diff --git a/src/components/cardbuilder/Card/CardImageContainer.tsx b/src/components/cardbuilder/Card/CardImageContainer.tsx index 8a10b6b330..db609f21e6 100644 --- a/src/components/cardbuilder/Card/CardImageContainer.tsx +++ b/src/components/cardbuilder/Card/CardImageContainer.tsx @@ -7,7 +7,7 @@ import RefreshIndicator from 'elements/emby-itemrefreshindicator/RefreshIndicato import Media from '../../common/Media'; import CardInnerFooter from './CardInnerFooter'; -import type { ItemDto } from 'types/itemDto'; +import type { ItemDto } from 'types/base/models/item-dto'; import type { CardOptions } from 'types/cardOptions'; interface CardImageContainerProps { diff --git a/src/components/cardbuilder/Card/CardInnerFooter.tsx b/src/components/cardbuilder/Card/CardInnerFooter.tsx index 33534e8a9a..e5908adc27 100644 --- a/src/components/cardbuilder/Card/CardInnerFooter.tsx +++ b/src/components/cardbuilder/Card/CardInnerFooter.tsx @@ -1,7 +1,7 @@ import React, { type FC } from 'react'; import classNames from 'classnames'; import CardFooterText from './CardFooterText'; -import type { ItemDto } from 'types/itemDto'; +import type { ItemDto } from 'types/base/models/item-dto'; import type { CardOptions } from 'types/cardOptions'; interface CardInnerFooterProps { diff --git a/src/components/cardbuilder/Card/CardOuterFooter.tsx b/src/components/cardbuilder/Card/CardOuterFooter.tsx index f03dcb8703..3f6380aa9e 100644 --- a/src/components/cardbuilder/Card/CardOuterFooter.tsx +++ b/src/components/cardbuilder/Card/CardOuterFooter.tsx @@ -4,7 +4,7 @@ import { useApi } from 'hooks/useApi'; import { getCardLogoUrl } from './cardHelper'; import CardFooterText from './CardFooterText'; -import type { ItemDto } from 'types/itemDto'; +import type { ItemDto } from 'types/base/models/item-dto'; import type { CardOptions } from 'types/cardOptions'; interface CardOuterFooterProps { diff --git a/src/components/cardbuilder/Card/CardOverlayButtons.tsx b/src/components/cardbuilder/Card/CardOverlayButtons.tsx index 66abd459e3..a00b194587 100644 --- a/src/components/cardbuilder/Card/CardOverlayButtons.tsx +++ b/src/components/cardbuilder/Card/CardOverlayButtons.tsx @@ -7,7 +7,7 @@ import { appRouter } from 'components/router/appRouter'; import PlayArrowIconButton from '../../common/PlayArrowIconButton'; import MoreVertIconButton from '../../common/MoreVertIconButton'; -import type { ItemDto } from 'types/itemDto'; +import type { ItemDto } from 'types/base/models/item-dto'; import type { CardOptions } from 'types/cardOptions'; const sholudShowOverlayPlayButton = ( diff --git a/src/components/cardbuilder/Card/Cards.tsx b/src/components/cardbuilder/Card/Cards.tsx index ca5a52f76e..2ea6863954 100644 --- a/src/components/cardbuilder/Card/Cards.tsx +++ b/src/components/cardbuilder/Card/Cards.tsx @@ -1,7 +1,7 @@ import React, { type FC } from 'react'; import { setCardData } from '../cardBuilder'; import Card from './Card'; -import type { ItemDto } from 'types/itemDto'; +import type { ItemDto } from 'types/base/models/item-dto'; import type { CardOptions } from 'types/cardOptions'; import '../card.scss'; diff --git a/src/components/cardbuilder/Card/cardHelper.ts b/src/components/cardbuilder/Card/cardHelper.ts index ae4ef06f18..ab40841073 100644 --- a/src/components/cardbuilder/Card/cardHelper.ts +++ b/src/components/cardbuilder/Card/cardHelper.ts @@ -17,7 +17,7 @@ import datetime from 'scripts/datetime'; import { isUsingLiveTvNaming } from '../cardBuilderUtils'; import type { NullableNumber, NullableString } from 'types/base/common/shared/types'; -import type { ItemDto } from 'types/itemDto'; +import type { ItemDto } from 'types/base/models/item-dto'; import type { CardOptions } from 'types/cardOptions'; import type { DataAttributes } from 'types/dataAttributes'; import { getDataAttributes } from 'utils/items'; diff --git a/src/components/cardbuilder/Card/useCard.ts b/src/components/cardbuilder/Card/useCard.ts index 80c5dd69b7..6c031afa8d 100644 --- a/src/components/cardbuilder/Card/useCard.ts +++ b/src/components/cardbuilder/Card/useCard.ts @@ -9,7 +9,7 @@ import { getDataAttributes } from 'utils/items'; import { CardShape } from 'utils/card'; import layoutManager from 'components/layoutManager'; -import type { ItemDto } from 'types/itemDto'; +import type { ItemDto } from 'types/base/models/item-dto'; import type { CardOptions } from 'types/cardOptions'; interface UseCardProps { diff --git a/src/components/cardbuilder/Card/useCardImageUrl.ts b/src/components/cardbuilder/Card/useCardImageUrl.ts index 8afec0cab3..4675c1d80b 100644 --- a/src/components/cardbuilder/Card/useCardImageUrl.ts +++ b/src/components/cardbuilder/Card/useCardImageUrl.ts @@ -5,7 +5,7 @@ import { useApi } from 'hooks/useApi'; import { getDesiredAspect } from '../cardBuilderUtils'; import { CardShape } from 'utils/card'; import type { NullableNumber, NullableString } from 'types/base/common/shared/types'; -import type { ItemDto } from 'types/itemDto'; +import type { ItemDto } from 'types/base/models/item-dto'; import type { CardOptions } from 'types/cardOptions'; function getPreferThumbInfo(item: ItemDto, cardOptions: CardOptions) { diff --git a/src/components/cardbuilder/Card/useCardText.tsx b/src/components/cardbuilder/Card/useCardText.tsx index 904777fe85..ff93662841 100644 --- a/src/components/cardbuilder/Card/useCardText.tsx +++ b/src/components/cardbuilder/Card/useCardText.tsx @@ -5,7 +5,7 @@ import layoutManager from 'components/layoutManager'; import CardText from './CardText'; import { getCardTextLines } from './cardHelper'; -import type { ItemDto } from 'types/itemDto'; +import type { ItemDto } from 'types/base/models/item-dto'; import type { CardOptions } from 'types/cardOptions'; const enableRightMargin = ( diff --git a/src/components/common/DefaultIconText.tsx b/src/components/common/DefaultIconText.tsx index f6af526c07..c6ea81b18e 100644 --- a/src/components/common/DefaultIconText.tsx +++ b/src/components/common/DefaultIconText.tsx @@ -3,7 +3,7 @@ import React, { type FC } from 'react'; import Icon from '@mui/material/Icon'; import imageHelper from 'utils/image'; import DefaultName from './DefaultName'; -import type { ItemDto } from 'types/itemDto'; +import type { ItemDto } from 'types/base/models/item-dto'; interface DefaultIconTextProps { item: ItemDto; diff --git a/src/components/common/DefaultName.tsx b/src/components/common/DefaultName.tsx index ba782e1162..44a7829531 100644 --- a/src/components/common/DefaultName.tsx +++ b/src/components/common/DefaultName.tsx @@ -2,7 +2,7 @@ import React, { type FC } from 'react'; import Box from '@mui/material/Box'; import itemHelper from 'components/itemHelper'; import { isUsingLiveTvNaming } from '../cardbuilder/cardBuilderUtils'; -import type { ItemDto } from 'types/itemDto'; +import type { ItemDto } from 'types/base/models/item-dto'; interface DefaultNameProps { item: ItemDto; diff --git a/src/components/common/Media.tsx b/src/components/common/Media.tsx index 598c9ec7a5..99858356c0 100644 --- a/src/components/common/Media.tsx +++ b/src/components/common/Media.tsx @@ -2,7 +2,7 @@ import { BaseItemKind, ImageType } from '@jellyfin/sdk/lib/generated-client'; import React, { type FC } from 'react'; import Image from './Image'; import DefaultIconText from './DefaultIconText'; -import type { ItemDto } from 'types/itemDto'; +import type { ItemDto } from 'types/base/models/item-dto'; interface MediaProps { item: ItemDto; diff --git a/src/components/indicators/useIndicator.tsx b/src/components/indicators/useIndicator.tsx index da5ea537bf..a9ecc12bc8 100644 --- a/src/components/indicators/useIndicator.tsx +++ b/src/components/indicators/useIndicator.tsx @@ -17,7 +17,7 @@ import datetime from 'scripts/datetime'; import itemHelper from 'components/itemHelper'; import AutoTimeProgressBar from 'elements/emby-progressbar/AutoTimeProgressBar'; import type { NullableString } from 'types/base/common/shared/types'; -import type { ItemDto } from 'types/itemDto'; +import type { ItemDto } from 'types/base/models/item-dto'; import type { ProgressOptions } from 'types/progressOptions'; const TypeIcon = { diff --git a/src/components/listview/List/List.tsx b/src/components/listview/List/List.tsx index feafd5a04d..8afe3503ba 100644 --- a/src/components/listview/List/List.tsx +++ b/src/components/listview/List/List.tsx @@ -2,7 +2,7 @@ import React, { type FC } from 'react'; import useList from './useList'; import ListContent from './ListContent'; import ListWrapper from './ListWrapper'; -import type { ItemDto } from 'types/itemDto'; +import type { ItemDto } from 'types/base/models/item-dto'; import type { ListOptions } from 'types/listOptions'; import '../../mediainfo/mediainfo.scss'; import '../../guide/programs.scss'; diff --git a/src/components/listview/List/ListContent.tsx b/src/components/listview/List/ListContent.tsx index 0cf2160ca6..6dba901dbf 100644 --- a/src/components/listview/List/ListContent.tsx +++ b/src/components/listview/List/ListContent.tsx @@ -10,7 +10,7 @@ import ListItemBody from './ListItemBody'; import ListImageContainer from './ListImageContainer'; import ListViewUserDataButtons from './ListViewUserDataButtons'; -import type { ItemDto } from 'types/itemDto'; +import type { ItemDto } from 'types/base/models/item-dto'; import type { ListOptions } from 'types/listOptions'; interface ListContentProps { diff --git a/src/components/listview/List/ListImageContainer.tsx b/src/components/listview/List/ListImageContainer.tsx index b447b2a701..bebe97cb12 100644 --- a/src/components/listview/List/ListImageContainer.tsx +++ b/src/components/listview/List/ListImageContainer.tsx @@ -13,7 +13,7 @@ import { import Media from 'components/common/Media'; import PlayArrowIconButton from 'components/common/PlayArrowIconButton'; -import type { ItemDto } from 'types/itemDto'; +import type { ItemDto } from 'types/base/models/item-dto'; import type { ListOptions } from 'types/listOptions'; interface ListImageContainerProps { diff --git a/src/components/listview/List/ListItemBody.tsx b/src/components/listview/List/ListItemBody.tsx index 5152040585..847d46b4de 100644 --- a/src/components/listview/List/ListItemBody.tsx +++ b/src/components/listview/List/ListItemBody.tsx @@ -4,7 +4,7 @@ import Box from '@mui/material/Box'; import useListTextlines from './useListTextlines'; import PrimaryMediaInfo from '../../mediainfo/PrimaryMediaInfo'; -import type { ItemDto } from 'types/itemDto'; +import type { ItemDto } from 'types/base/models/item-dto'; import type { ListOptions } from 'types/listOptions'; interface ListItemBodyProps { diff --git a/src/components/listview/List/ListViewUserDataButtons.tsx b/src/components/listview/List/ListViewUserDataButtons.tsx index 97668ed999..8a8b4ce901 100644 --- a/src/components/listview/List/ListViewUserDataButtons.tsx +++ b/src/components/listview/List/ListViewUserDataButtons.tsx @@ -8,7 +8,7 @@ import InfoIconButton from '../../common/InfoIconButton'; import RightIconButtons from '../../common/RightIconButtons'; import MoreVertIconButton from '../../common/MoreVertIconButton'; -import type { ItemDto } from 'types/itemDto'; +import type { ItemDto } from 'types/base/models/item-dto'; import type { ListOptions } from 'types/listOptions'; interface ListViewUserDataButtonsProps { diff --git a/src/components/listview/List/Lists.tsx b/src/components/listview/List/Lists.tsx index 1516140635..1215851cc5 100644 --- a/src/components/listview/List/Lists.tsx +++ b/src/components/listview/List/Lists.tsx @@ -5,7 +5,7 @@ import { getIndex } from './listHelper'; import ListGroupHeaderWrapper from './ListGroupHeaderWrapper'; import List from './List'; -import type { ItemDto } from 'types/itemDto'; +import type { ItemDto } from 'types/base/models/item-dto'; import type { ListOptions } from 'types/listOptions'; import '../listview.scss'; diff --git a/src/components/listview/List/listHelper.ts b/src/components/listview/List/listHelper.ts index 8386ff3273..d909feb568 100644 --- a/src/components/listview/List/listHelper.ts +++ b/src/components/listview/List/listHelper.ts @@ -3,7 +3,7 @@ import { BaseItemKind, ImageType } from '@jellyfin/sdk/lib/generated-client'; import { getImageApi } from '@jellyfin/sdk/lib/utils/api/image-api'; import globalize from 'scripts/globalize'; -import type { ItemDto } from 'types/itemDto'; +import type { ItemDto } from 'types/base/models/item-dto'; import type { ListOptions } from 'types/listOptions'; const sortBySortName = (item: ItemDto): string => { diff --git a/src/components/listview/List/useList.ts b/src/components/listview/List/useList.ts index 75a60c6b54..196721a0dc 100644 --- a/src/components/listview/List/useList.ts +++ b/src/components/listview/List/useList.ts @@ -2,7 +2,7 @@ import classNames from 'classnames'; import { getDataAttributes } from 'utils/items'; import layoutManager from 'components/layoutManager'; -import type { ItemDto } from 'types/itemDto'; +import type { ItemDto } from 'types/base/models/item-dto'; import type { ListOptions } from 'types/listOptions'; interface UseListProps { diff --git a/src/components/listview/List/useListTextlines.tsx b/src/components/listview/List/useListTextlines.tsx index da66673a5a..490f94634d 100644 --- a/src/components/listview/List/useListTextlines.tsx +++ b/src/components/listview/List/useListTextlines.tsx @@ -3,7 +3,7 @@ import React from 'react'; import itemHelper from '../../itemHelper'; import datetime from 'scripts/datetime'; import ListTextWrapper from './ListTextWrapper'; -import type { ItemDto } from 'types/itemDto'; +import type { ItemDto } from 'types/base/models/item-dto'; import type { ListOptions } from 'types/listOptions'; function getParentTitle( diff --git a/src/components/mediainfo/PrimaryMediaInfo.tsx b/src/components/mediainfo/PrimaryMediaInfo.tsx index 2978a41683..c68f823b9b 100644 --- a/src/components/mediainfo/PrimaryMediaInfo.tsx +++ b/src/components/mediainfo/PrimaryMediaInfo.tsx @@ -8,7 +8,7 @@ import StarIcons from './StarIcons'; import CaptionMediaInfo from './CaptionMediaInfo'; import CriticRatingMediaInfo from './CriticRatingMediaInfo'; import EndsAt from './EndsAt'; -import type { ItemDto } from 'types/itemDto'; +import type { ItemDto } from 'types/base/models/item-dto'; import type { MiscInfo } from 'types/mediaInfoItem'; interface PrimaryMediaInfoProps { diff --git a/src/components/mediainfo/usePrimaryMediaInfo.tsx b/src/components/mediainfo/usePrimaryMediaInfo.tsx index 6c58609152..d41eea0ec3 100644 --- a/src/components/mediainfo/usePrimaryMediaInfo.tsx +++ b/src/components/mediainfo/usePrimaryMediaInfo.tsx @@ -4,7 +4,7 @@ import datetime from 'scripts/datetime'; import globalize from 'scripts/globalize'; import itemHelper from '../itemHelper'; import type { NullableNumber, NullableString } from 'types/base/common/shared/types'; -import type { ItemDto } from 'types/itemDto'; +import type { ItemDto } from 'types/base/models/item-dto'; import type { MiscInfo } from 'types/mediaInfoItem'; function shouldShowFolderRuntime( diff --git a/src/elements/emby-itemrefreshindicator/RefreshIndicator.tsx b/src/elements/emby-itemrefreshindicator/RefreshIndicator.tsx index 67a65703dc..8d2541f9e3 100644 --- a/src/elements/emby-itemrefreshindicator/RefreshIndicator.tsx +++ b/src/elements/emby-itemrefreshindicator/RefreshIndicator.tsx @@ -10,7 +10,7 @@ import Typography from '@mui/material/Typography'; import Box from '@mui/material/Box'; import { toPercent } from 'utils/number'; import { getCurrentDateTimeLocale } from 'scripts/globalize'; -import type { ItemDto } from 'types/itemDto'; +import type { ItemDto } from 'types/base/models/item-dto'; function CircularProgressWithLabel( props: CircularProgressProps & { value: number } diff --git a/src/types/itemDto.ts b/src/types/base/models/item-dto.ts similarity index 100% rename from src/types/itemDto.ts rename to src/types/base/models/item-dto.ts diff --git a/src/types/cardOptions.ts b/src/types/cardOptions.ts index b18906c0c4..02d10f965f 100644 --- a/src/types/cardOptions.ts +++ b/src/types/cardOptions.ts @@ -5,7 +5,7 @@ import type { BaseItemDtoImageBlurHashes } from '@jellyfin/sdk/lib/generated-cli import type { CollectionType } from '@jellyfin/sdk/lib/generated-client/models/collection-type'; import { CardShape } from 'utils/card'; import type { NullableString } from './base/common/shared/types'; -import type { ItemDto } from './itemDto'; +import type { ItemDto } from './base/models/item-dto'; import type { ParentId } from './library'; export interface CardOptions { diff --git a/src/types/listOptions.ts b/src/types/listOptions.ts index b9b5eea80a..e34999e758 100644 --- a/src/types/listOptions.ts +++ b/src/types/listOptions.ts @@ -1,6 +1,6 @@ import { ItemSortBy } from '@jellyfin/sdk/lib/models/api/item-sort-by'; import type { CollectionType } from '@jellyfin/sdk/lib/generated-client/models/collection-type'; -import type { ItemDto } from './itemDto'; +import type { ItemDto } from './base/models/item-dto'; export interface ListOptions { items?: ItemDto[] | null; index?: string; From 064cebd41f58b3752b7cddef14e5438d63a245e3 Mon Sep 17 00:00:00 2001 From: grafixeyehero Date: Sun, 3 Mar 2024 01:33:51 +0300 Subject: [PATCH 23/24] Add shared ItemKind Type --- src/types/base/models/item-kind.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 src/types/base/models/item-kind.ts diff --git a/src/types/base/models/item-kind.ts b/src/types/base/models/item-kind.ts new file mode 100644 index 0000000000..d89f8cbeef --- /dev/null +++ b/src/types/base/models/item-kind.ts @@ -0,0 +1,12 @@ +import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind'; + +export const ItemKind = { + ...BaseItemKind, + Timer: 'Timer', + SeriesTimer: 'SeriesTimer' +} as const; + +// eslint-disable-next-line @typescript-eslint/no-redeclare +export type ItemKind = keyof typeof ItemKind; + +export type ItemType = ItemKind | null | undefined; From 28bf7d4dfbd56e7301829276eba47ada845250ec Mon Sep 17 00:00:00 2001 From: grafixeyehero Date: Sun, 3 Mar 2024 01:36:21 +0300 Subject: [PATCH 24/24] Add shared ItemMediaKind Type --- src/types/base/models/item-media-kind.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 src/types/base/models/item-media-kind.ts diff --git a/src/types/base/models/item-media-kind.ts b/src/types/base/models/item-media-kind.ts new file mode 100644 index 0000000000..14fbe6a137 --- /dev/null +++ b/src/types/base/models/item-media-kind.ts @@ -0,0 +1,15 @@ +export const ItemMediaKind = { + MusicArtist: 'MusicArtist', + Playlist: 'Playlist', + MusicGenre: 'MusicGenre', + Photo: 'Photo', + Audio: 'Audio', + Video: 'Video', + Book: 'Book', + Recording: 'Recording' +} as const; + +// eslint-disable-next-line @typescript-eslint/no-redeclare +export type ItemMediaKind = keyof typeof ItemMediaKind; + +export type ItemMediaType = ItemMediaKind | null | undefined;