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 +}