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