diff --git a/src/components/media/WatchedMediaCard.tsx b/src/components/media/WatchedMediaCard.tsx index 4d799809..68e43618 100644 --- a/src/components/media/WatchedMediaCard.tsx +++ b/src/components/media/WatchedMediaCard.tsx @@ -1,6 +1,6 @@ import { useMemo } from "react"; -import { useProgressStore } from "@/stores/progress"; +import { getProgressPercentage, useProgressStore } from "@/stores/progress"; import { ShowProgressResult, shouldShowProgress, @@ -36,7 +36,10 @@ export function WatchedMediaCard(props: WatchedMediaCardProps) { [item], ); const percentage = itemToDisplay?.show - ? (itemToDisplay.progress.watched / itemToDisplay.progress.duration) * 100 + ? getProgressPercentage( + itemToDisplay.progress.watched, + itemToDisplay.progress.duration, + ) : undefined; return ( diff --git a/src/components/overlays/detailsModal/components/carousels/EpisodeCarousel.tsx b/src/components/overlays/detailsModal/components/carousels/EpisodeCarousel.tsx index 7c0f1d2a..b6eeacb6 100644 --- a/src/components/overlays/detailsModal/components/carousels/EpisodeCarousel.tsx +++ b/src/components/overlays/detailsModal/components/carousels/EpisodeCarousel.tsx @@ -9,7 +9,7 @@ import { Icon, Icons } from "@/components/Icon"; import { Modal, ModalCard, useModal } from "@/components/overlays/Modal"; import { hasAired } from "@/components/player/utils/aired"; import { useBookmarkStore } from "@/stores/bookmarks"; -import { useProgressStore } from "@/stores/progress"; +import { getProgressPercentage, useProgressStore } from "@/stores/progress"; import { EpisodeCarouselProps } from "../../types"; @@ -170,9 +170,10 @@ export function EpisodeCarousel({ const episodeProgress = progress[mediaId.toString()]?.episodes?.[episodeId]; const percentage = episodeProgress - ? (episodeProgress.progress.watched / - episodeProgress.progress.duration) * - 100 + ? getProgressPercentage( + episodeProgress.progress.watched, + episodeProgress.progress.duration, + ) : 0; // If watched (>90%), reset to 0%, otherwise set to 100% @@ -282,9 +283,10 @@ export function EpisodeCarousel({ const episodeProgress = progress[mediaId?.toString() ?? ""]?.episodes?.[episode.id]; const percentage = episodeProgress - ? (episodeProgress.progress.watched / - episodeProgress.progress.duration) * - 100 + ? getProgressPercentage( + episodeProgress.progress.watched, + episodeProgress.progress.duration, + ) : 0; const isAired = hasAired(episode.air_date); const isWatched = percentage > 90; @@ -299,9 +301,10 @@ export function EpisodeCarousel({ const episodeProgress = progress[mediaId?.toString() ?? ""]?.episodes?.[episode.id]; const percentage = episodeProgress - ? (episodeProgress.progress.watched / - episodeProgress.progress.duration) * - 100 + ? getProgressPercentage( + episodeProgress.progress.watched, + episodeProgress.progress.duration, + ) : 0; const isAired = hasAired(episode.air_date); const isWatched = percentage > 90; @@ -378,9 +381,10 @@ export function EpisodeCarousel({ const episodeProgress = progress[mediaId?.toString() ?? ""]?.episodes?.[episode.id]; const percentage = episodeProgress - ? (episodeProgress.progress.watched / - episodeProgress.progress.duration) * - 100 + ? getProgressPercentage( + episodeProgress.progress.watched, + episodeProgress.progress.duration, + ) : 0; const isAired = hasAired(episode.air_date); const isWatched = percentage > 90; @@ -565,9 +569,10 @@ export function EpisodeCarousel({ const episodeProgress = progress[mediaId?.toString() ?? ""]?.episodes?.[episode.id]; const percentage = episodeProgress - ? (episodeProgress.progress.watched / - episodeProgress.progress.duration) * - 100 + ? getProgressPercentage( + episodeProgress.progress.watched, + episodeProgress.progress.duration, + ) : 0; const isAired = hasAired(episode.air_date); const isExpanded = expandedEpisodes[episode.id]; diff --git a/src/components/player/atoms/WatchPartyStatus.tsx b/src/components/player/atoms/WatchPartyStatus.tsx index a4bf3840..5f04ed68 100644 --- a/src/components/player/atoms/WatchPartyStatus.tsx +++ b/src/components/player/atoms/WatchPartyStatus.tsx @@ -4,6 +4,7 @@ import { useTranslation } from "react-i18next"; import { Button } from "@/components/buttons/Button"; import { Icon, Icons } from "@/components/Icon"; import { useWatchPartySync } from "@/hooks/useWatchPartySync"; +import { getProgressPercentage } from "@/stores/progress"; import { useWatchPartyStore } from "@/stores/watchParty"; export function WatchPartyStatus() { @@ -110,7 +111,7 @@ export function WatchPartyStatus() { {user.player.duration > 0 - ? `${Math.floor((user.player.time / user.player.duration) * 100)}%` + ? `${Math.floor(getProgressPercentage(user.player.time, user.player.duration))}%` : `${Math.floor(user.player.time)}s`} diff --git a/src/components/player/atoms/settings/WatchPartyView.tsx b/src/components/player/atoms/settings/WatchPartyView.tsx index 7434ad83..d10b0eab 100644 --- a/src/components/player/atoms/settings/WatchPartyView.tsx +++ b/src/components/player/atoms/settings/WatchPartyView.tsx @@ -13,6 +13,7 @@ import { useBackendUrl } from "@/hooks/auth/useBackendUrl"; import { useOverlayRouter } from "@/hooks/useOverlayRouter"; import { useWatchPartySync } from "@/hooks/useWatchPartySync"; import { useAuthStore } from "@/stores/auth"; +import { getProgressPercentage } from "@/stores/progress"; import { useWatchPartyStore } from "@/stores/watchParty"; import { useDownloadLink } from "./Downloads"; @@ -326,7 +327,7 @@ export function WatchPartyView({ id }: { id: string }) { {user.player.duration > 0 - ? `${Math.floor((user.player.time / user.player.duration) * 100)}%` + ? `${Math.floor(getProgressPercentage(user.player.time, user.player.duration))}%` : `${Math.floor(user.player.time)}s`} diff --git a/src/pages/PlayerView.tsx b/src/pages/PlayerView.tsx index b08d7154..4dc69a71 100644 --- a/src/pages/PlayerView.tsx +++ b/src/pages/PlayerView.tsx @@ -26,7 +26,7 @@ import { SourceSelectPart } from "@/pages/parts/player/SourceSelectPart"; import { useLastNonPlayerLink } from "@/stores/history"; import { PlayerMeta, playerStatus } from "@/stores/player/slices/source"; import { usePreferencesStore } from "@/stores/preferences"; -import { useProgressStore } from "@/stores/progress"; +import { getProgressPercentage, useProgressStore } from "@/stores/progress"; import { needsOnboarding } from "@/utils/onboarding"; import { parseTimestamp } from "@/utils/timestamp"; @@ -107,16 +107,20 @@ export function RealPlayerView() { if (meta.type === "movie") { if (!item.progress) return false; - const percentage = - (item.progress.watched / item.progress.duration) * 100; + const percentage = getProgressPercentage( + item.progress.watched, + item.progress.duration, + ); return percentage > 80; } if (meta.type === "show" && meta.episode?.tmdbId) { const episode = item.episodes?.[meta.episode.tmdbId]; if (!episode) return false; - const percentage = - (episode.progress.watched / episode.progress.duration) * 100; + const percentage = getProgressPercentage( + episode.progress.watched, + episode.progress.duration, + ); return percentage > 80; } diff --git a/src/pages/parts/player/ResumePart.tsx b/src/pages/parts/player/ResumePart.tsx index 98272352..98191d13 100644 --- a/src/pages/parts/player/ResumePart.tsx +++ b/src/pages/parts/player/ResumePart.tsx @@ -8,7 +8,7 @@ import { Title } from "@/components/text/Title"; import { ErrorContainer, ErrorLayout } from "@/pages/layouts/ErrorLayout"; import { PlayerMeta } from "@/stores/player/slices/source"; import { usePlayerStore } from "@/stores/player/store"; -import { useProgressStore } from "@/stores/progress"; +import { getProgressPercentage, useProgressStore } from "@/stores/progress"; export interface ResumePartProps { onResume: () => void; @@ -30,13 +30,19 @@ export function ResumePart(props: ResumePartProps) { if (meta.type === "movie") { if (!item.progress) return 0; - return (item.progress.watched / item.progress.duration) * 100; + return getProgressPercentage( + item.progress.watched, + item.progress.duration, + ); } if (meta.type === "show" && meta.episode?.tmdbId) { const episode = item.episodes?.[meta.episode.tmdbId]; if (!episode) return 0; - return (episode.progress.watched / episode.progress.duration) * 100; + return getProgressPercentage( + episode.progress.watched, + episode.progress.duration, + ); } return 0; diff --git a/src/stores/progress/index.ts b/src/stores/progress/index.ts index a209658f..8ff3d292 100644 --- a/src/stores/progress/index.ts +++ b/src/stores/progress/index.ts @@ -4,6 +4,8 @@ import { immer } from "zustand/middleware/immer"; import { PlayerMeta } from "@/stores/player/slices/source"; +export { getProgressPercentage } from "./utils"; + export interface ProgressItem { watched: number; duration: number; diff --git a/src/stores/progress/utils.ts b/src/stores/progress/utils.ts index 6ea09462..eebe9e67 100644 --- a/src/stores/progress/utils.ts +++ b/src/stores/progress/utils.ts @@ -55,6 +55,19 @@ function isFirstEpisodeOfShow( return season.number === 1 && episode.number === 1; } +export function getProgressPercentage( + watched: number, + duration: number, +): number { + // Handle edge cases to prevent infinity or invalid percentages + if (!duration || duration <= 0) return 0; + if (!watched || watched < 0) return 0; + + // Cap percentage at 100% to prevent >100% values + const percentage = Math.min((watched / duration) * 100, 100); + return percentage; +} + export function shouldShowProgress( item: ProgressMediaItem, ): ShowProgressResult {