diff --git a/src/components/media/MediaCard.tsx b/src/components/media/MediaCard.tsx index e51db836..1b23b974 100644 --- a/src/components/media/MediaCard.tsx +++ b/src/components/media/MediaCard.tsx @@ -6,9 +6,21 @@ import { mediaTypeToJW } from "@/backend/metadata/justwatch"; export interface MediaCardProps { media: MWMediaMeta; linkable?: boolean; + series?: { + episode: number; + season: number; + }; + percentage?: number; } -function MediaCardContent({ media, linkable }: MediaCardProps) { +function MediaCardContent({ + media, + linkable, + series, + percentage, +}: MediaCardProps) { + const percentageString = `${Math.round(percentage ?? 0).toFixed(0)}%`; + return (
+ > + {series ? ( +
+

+ S{series.season} E{series.episode} +

+
+ ) : null} + + {percentage !== undefined ? ( + <> +
+
+
+
+
+
+
+ + ) : null} +

{media.title}

diff --git a/src/components/media/WatchedMediaCard.tsx b/src/components/media/WatchedMediaCard.tsx index 6dcf7064..1dd11d5e 100644 --- a/src/components/media/WatchedMediaCard.tsx +++ b/src/components/media/WatchedMediaCard.tsx @@ -1,4 +1,6 @@ import { MWMediaMeta } from "@/backend/metadata/types"; +import { useWatchedContext } from "@/state/watched"; +import { useMemo } from "react"; import { MediaCard } from "./MediaCard"; export interface WatchedMediaCardProps { @@ -6,5 +8,17 @@ export interface WatchedMediaCardProps { } export function WatchedMediaCard(props: WatchedMediaCardProps) { - return ; + const { watched } = useWatchedContext(); + const watchedMedia = useMemo(() => { + return watched.items.find((v) => v.item.meta.id === props.media.id); + }, [watched, props.media]); + + return ( + + ); } diff --git a/src/components/video/controls/BackdropControl.tsx b/src/components/video/controls/BackdropControl.tsx index 4c081ba7..2d099627 100644 --- a/src/components/video/controls/BackdropControl.tsx +++ b/src/components/video/controls/BackdropControl.tsx @@ -12,6 +12,7 @@ export function BackdropControl(props: BackdropControlProps) { const timeout = useRef | null>(null); const clickareaRef = useRef(null); + // TODO fix infinite loop const handleMouseMove = useCallback(() => { setMoved(true); if (timeout.current) clearTimeout(timeout.current); diff --git a/src/components/video/controls/ProgressControl.tsx b/src/components/video/controls/ProgressControl.tsx index b8e277a0..eaeed9ee 100644 --- a/src/components/video/controls/ProgressControl.tsx +++ b/src/components/video/controls/ProgressControl.tsx @@ -21,6 +21,8 @@ export function ProgressControl() { ref, commitTime ); + + // TODO make dragging update timer useEffect(() => { if (dragRef.current === dragging) return; dragRef.current = dragging; diff --git a/src/components/video/controls/ProgressListenerControl.tsx b/src/components/video/controls/ProgressListenerControl.tsx index a8f9a80d..b20fc4c8 100644 --- a/src/components/video/controls/ProgressListenerControl.tsx +++ b/src/components/video/controls/ProgressListenerControl.tsx @@ -7,25 +7,7 @@ interface Props { onProgress?: (time: number, duration: number) => void; } -const FIVETEEN_MINUTES = 15 * 60; -const FIVE_MINUTES = 5 * 60; - -function shouldRestoreTime(time: number, duration: number): boolean { - const timeFromEnd = Math.max(0, duration - time); - - // short movie - if (duration < FIVETEEN_MINUTES) { - if (time < 5) return false; - if (timeFromEnd < 60) return false; - return true; - } - - // long movie - if (time < 30) return false; - if (timeFromEnd < FIVE_MINUTES) return false; - return true; -} - +// TODO fix infinite loops export function ProgressListenerControl(props: Props) { const { videoState } = useVideoPlayerState(); const didInitialize = useRef(null); @@ -50,14 +32,11 @@ export function ProgressListenerControl(props: Props) { useEffect(() => { if (didInitialize.current) return; if (!videoState.hasInitialized || Number.isNaN(videoState.duration)) return; - if ( - props.startAt !== undefined && - shouldRestoreTime(props.startAt, videoState.duration) - ) { + if (props.startAt !== undefined) { videoState.setTime(props.startAt); } didInitialize.current = true; - }, [didInitialize, videoState, props]); + }, [didInitialize, props, videoState]); return null; } diff --git a/src/components/video/controls/SourceControl.tsx b/src/components/video/controls/SourceControl.tsx index 0e612a50..9025c404 100644 --- a/src/components/video/controls/SourceControl.tsx +++ b/src/components/video/controls/SourceControl.tsx @@ -1,5 +1,5 @@ import { MWStreamType } from "@/backend/helpers/streams"; -import { useContext, useEffect } from "react"; +import { useContext, useEffect, useRef } from "react"; import { VideoPlayerDispatchContext } from "../VideoContext"; interface SourceControlProps { @@ -9,13 +9,16 @@ interface SourceControlProps { export function SourceControl(props: SourceControlProps) { const dispatch = useContext(VideoPlayerDispatchContext); + const didInitialize = useRef(false); useEffect(() => { + if (didInitialize.current) return; dispatch({ type: "SET_SOURCE", url: props.source, sourceType: props.type, }); + didInitialize.current = true; }, [props, dispatch]); return null; diff --git a/src/index.tsx b/src/index.tsx index f2f33418..5d345107 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -21,6 +21,7 @@ if (key) { // - mobile UI // - season/episode select // - chrome cast support +// - airplay support // - source selection // - safari fullscreen will make video overlap player controls // - safari progress bar is fucked (video doesnt change time but video.currentTime does change) @@ -47,7 +48,6 @@ if (key) { // - localize everything // - add titles to pages // - find place for bookmark button -// - find place for progress bar for "continue watching" section ReactDOM.render( diff --git a/src/state/watched/context.tsx b/src/state/watched/context.tsx index cefec243..dd44ba58 100644 --- a/src/state/watched/context.tsx +++ b/src/state/watched/context.tsx @@ -10,6 +10,25 @@ import { } from "react"; import { VideoProgressStore } from "./store"; +const FIVETEEN_MINUTES = 15 * 60; +const FIVE_MINUTES = 5 * 60; + +function shouldSave(time: number, duration: number): boolean { + const timeFromEnd = Math.max(0, duration - time); + + // short movie + if (duration < FIVETEEN_MINUTES) { + if (time < 5) return false; + if (timeFromEnd < 60) return false; + return true; + } + + // long movie + if (time < 30) return false; + if (timeFromEnd < FIVE_MINUTES) return false; + return true; +} + interface MediaItem { meta: MWMediaMeta; series?: { @@ -66,8 +85,12 @@ export function WatchedContextProvider(props: { children: ReactNode }) { const contextValue = useMemo( () => ({ updateProgress(media: MediaItem, progress: number, total: number): void { + // TODO series support setWatched((data: WatchedStoreData) => { - let item = data.items.find((v) => v.item.meta.id === media.meta.id); + const newData = { ...data }; + let item = newData.items.find( + (v) => v.item.meta.id === media.meta.id + ); if (!item) { item = { item: { @@ -78,12 +101,20 @@ export function WatchedContextProvider(props: { children: ReactNode }) { progress: 0, percentage: 0, }; - data.items.push(item); + newData.items.push(item); } // update actual item item.progress = progress; item.percentage = Math.round((progress / total) * 100); - return data; + + // remove item if shouldnt save + if (!shouldSave(progress, total)) { + newData.items = data.items.filter( + (v) => v.item.meta.id !== media.meta.id + ); + } + + return newData; }); }, getFilteredWatched() {