From 5bb28c02c71061b261ef42a751cb46119df4e54f Mon Sep 17 00:00:00 2001 From: Pas <74743263+Pasithea0@users.noreply.github.com> Date: Wed, 6 Aug 2025 11:42:10 -0600 Subject: [PATCH] add resume action page asks what you want to do if your progress is greater than 80% --- src/assets/locales/en.json | 7 ++ .../player/atoms/NextEpisodeButton.tsx | 28 +++++- src/components/player/hooks/usePlayer.ts | 1 + src/pages/PlayerView.tsx | 61 +++++++++++- src/pages/parts/player/ResumePart.tsx | 92 +++++++++++++++++++ src/stores/player/slices/source.ts | 1 + 6 files changed, 184 insertions(+), 6 deletions(-) create mode 100644 src/pages/parts/player/ResumePart.tsx diff --git a/src/assets/locales/en.json b/src/assets/locales/en.json index 05e62b15..7b5fc8b1 100644 --- a/src/assets/locales/en.json +++ b/src/assets/locales/en.json @@ -740,6 +740,13 @@ "next": "Next episode", "nextSeason": "Next season" }, + "resume": { + "badge": "Resume", + "title": "Continue watching?", + "description": "This episode is {{percentage}}% watched", + "resume": "Resume", + "restart": "Restart" + }, "playbackError": { "badge": "Playback error", "errors": { diff --git a/src/components/player/atoms/NextEpisodeButton.tsx b/src/components/player/atoms/NextEpisodeButton.tsx index 437c0ddf..86e082c5 100644 --- a/src/components/player/atoms/NextEpisodeButton.tsx +++ b/src/components/player/atoms/NextEpisodeButton.tsx @@ -5,6 +5,7 @@ import { useAsync } from "react-use"; import { getMetaFromId } from "@/backend/metadata/getmeta"; import { MWMediaType, MWSeasonMeta } from "@/backend/metadata/types/mw"; +import { Button } from "@/components/buttons/Button"; import { Icon, Icons } from "@/components/Icon"; import { usePlayerMeta } from "@/components/player/hooks/usePlayerMeta"; import { Transition } from "@/components/utils/Transition"; @@ -27,7 +28,7 @@ function shouldShowNextEpisodeButton( return "none"; } -function Button(props: { +function ActionButton(props: { className: string; onClick?: () => void; children: React.ReactNode; @@ -94,6 +95,7 @@ export function NextEpisodeButton(props: { controlsShowing: boolean; onChange?: (meta: PlayerMeta) => void; inControl: boolean; + showAsButton?: boolean; }) { const { t } = useTranslation(); const duration = usePlayerStore((s) => s.progress.duration); @@ -213,6 +215,22 @@ export function NextEpisodeButton(props: { if (!meta?.episode || !nextEp) return null; if (metaType !== "show") return null; + if (props.showAsButton) { + return ( + + ); + } + return ( - - + ); diff --git a/src/components/player/hooks/usePlayer.ts b/src/components/player/hooks/usePlayer.ts index 14f042c9..f531b935 100644 --- a/src/components/player/hooks/usePlayer.ts +++ b/src/components/player/hooks/usePlayer.ts @@ -54,6 +54,7 @@ export function usePlayer() { status, shouldStartFromBeginning, setShouldStartFromBeginning, + setStatus, setMeta(m: PlayerMeta, newStatus?: PlayerStatus) { setMeta(m, newStatus); }, diff --git a/src/pages/PlayerView.tsx b/src/pages/PlayerView.tsx index 5c574b95..609adacd 100644 --- a/src/pages/PlayerView.tsx +++ b/src/pages/PlayerView.tsx @@ -8,6 +8,7 @@ import { } from "react-router-dom"; import { useAsync } from "react-use"; +import { DetailedMeta } from "@/backend/metadata/getmeta"; import { usePlayer } from "@/components/player/hooks/usePlayer"; import { usePlayerMeta } from "@/components/player/hooks/usePlayerMeta"; import { convertProviderCaption } from "@/components/player/utils/captions"; @@ -18,10 +19,12 @@ import { useQueryParam } from "@/hooks/useQueryParams"; import { MetaPart } from "@/pages/parts/player/MetaPart"; import { PlaybackErrorPart } from "@/pages/parts/player/PlaybackErrorPart"; import { PlayerPart } from "@/pages/parts/player/PlayerPart"; +import { ResumePart } from "@/pages/parts/player/ResumePart"; import { ScrapeErrorPart } from "@/pages/parts/player/ScrapeErrorPart"; import { ScrapingPart } from "@/pages/parts/player/ScrapingPart"; import { useLastNonPlayerLink } from "@/stores/history"; import { PlayerMeta, playerStatus } from "@/stores/player/slices/source"; +import { useProgressStore } from "@/stores/progress"; import { needsOnboarding } from "@/utils/onboarding"; import { parseTimestamp } from "@/utils/timestamp"; @@ -44,11 +47,13 @@ export function RealPlayerView() { setScrapeNotFound, shouldStartFromBeginning, setShouldStartFromBeginning, + setStatus, } = usePlayer(); const { setPlayerMeta, scrapeMedia } = usePlayerMeta(); const backUrl = useLastNonPlayerLink(); const router = useOverlayRouter("settings"); const openedWatchPartyRef = useRef(false); + const progressItems = useProgressStore((s) => s.items); const paramsData = JSON.stringify({ media: params.media, @@ -87,6 +92,53 @@ export function RealPlayerView() { [navigate, params], ); + // Check if episode is more than 80% watched + const shouldShowResumeScreen = useCallback( + (meta: PlayerMeta) => { + if (!meta?.tmdbId) return false; + + const item = progressItems[meta.tmdbId]; + if (!item) return false; + + if (meta.type === "movie") { + if (!item.progress) return false; + const percentage = + (item.progress.watched / item.progress.duration) * 100; + 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; + return percentage > 80; + } + + return false; + }, + [progressItems], + ); + + const handleMetaReceived = useCallback( + (detailedMeta: DetailedMeta, episodeId?: string) => { + const playerMeta = setPlayerMeta(detailedMeta, episodeId); + if (playerMeta && shouldShowResumeScreen(playerMeta)) { + setStatus(playerStatus.RESUME); + } + }, + [shouldShowResumeScreen, setStatus, setPlayerMeta], + ); + + const handleResume = useCallback(() => { + setStatus(playerStatus.SCRAPING); + }, [setStatus]); + + const handleRestart = useCallback(() => { + setShouldStartFromBeginning(true); + setStatus(playerStatus.SCRAPING); + }, [setShouldStartFromBeginning, setStatus]); + const playAfterScrape = useCallback( (out: RunOutput | null) => { if (!out) return; @@ -113,7 +165,14 @@ export function RealPlayerView() { return ( {status === playerStatus.IDLE ? ( - + + ) : null} + {status === playerStatus.RESUME ? ( + ) : null} {status === playerStatus.SCRAPING && scrapeMedia ? ( void; + onRestart: () => void; + onMetaChange?: (meta: PlayerMeta) => void; +} + +export function ResumePart(props: ResumePartProps) { + const { t } = useTranslation(); + const meta = usePlayerStore((s) => s.meta); + const progressItems = useProgressStore((s) => s.items); + + // Calculate watch percentage + const watchPercentage = (() => { + if (!meta?.tmdbId) return 0; + + const item = progressItems[meta.tmdbId]; + if (!item) return 0; + + if (meta.type === "movie") { + if (!item.progress) return 0; + return (item.progress.watched / item.progress.duration) * 100; + } + + 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 0; + })(); + + const roundedPercentage = Math.round(watchPercentage); + + return ( + + + {/* {t("player.resume.badge")} */} + {t("player.resume.title")} + + {t("player.resume.description", { percentage: roundedPercentage })} + + +
+ + + + + {meta?.type === "show" && ( +
+ +
+ )} +
+
+
+ ); +} diff --git a/src/stores/player/slices/source.ts b/src/stores/player/slices/source.ts index 8c04dc6b..9ff5162b 100644 --- a/src/stores/player/slices/source.ts +++ b/src/stores/player/slices/source.ts @@ -12,6 +12,7 @@ import { ValuesOf } from "@/utils/typeguard"; export const playerStatus = { IDLE: "idle", + RESUME: "resume", SCRAPING: "scraping", PLAYING: "playing", SCRAPE_NOT_FOUND: "scrapeNotFound",