diff --git a/src/components/video/DecoratedVideoPlayer.tsx b/src/components/video/DecoratedVideoPlayer.tsx index 7c68bf6f..11b4fd68 100644 --- a/src/components/video/DecoratedVideoPlayer.tsx +++ b/src/components/video/DecoratedVideoPlayer.tsx @@ -7,6 +7,7 @@ import { LoadingControl } from "./controls/LoadingControl"; import { MiddlePauseControl } from "./controls/MiddlePauseControl"; import { PauseControl } from "./controls/PauseControl"; import { ProgressControl } from "./controls/ProgressControl"; +import { ShowTitleControl } from "./controls/ShowTitleControl"; import { TimeControl } from "./controls/TimeControl"; import { VolumeControl } from "./controls/VolumeControl"; import { VideoPlayerError } from "./parts/VideoPlayerError"; @@ -30,15 +31,18 @@ function LeftSideControls() { }, [videoState]); return ( -
- - - -
+ <> +
+ + + +
+ + ); } diff --git a/src/components/video/controls/BackdropControl.tsx b/src/components/video/controls/BackdropControl.tsx index 2d099627..2fba50f3 100644 --- a/src/components/video/controls/BackdropControl.tsx +++ b/src/components/video/controls/BackdropControl.tsx @@ -12,15 +12,14 @@ export function BackdropControl(props: BackdropControlProps) { const timeout = useRef | null>(null); const clickareaRef = useRef(null); - // TODO fix infinite loop const handleMouseMove = useCallback(() => { - setMoved(true); + if (!moved) setMoved(true); if (timeout.current) clearTimeout(timeout.current); timeout.current = setTimeout(() => { - setMoved(false); + if (moved) setMoved(false); timeout.current = null; }, 3000); - }, [timeout, setMoved]); + }, [setMoved, moved]); const handleMouseLeave = useCallback(() => { setMoved(false); @@ -45,8 +44,13 @@ export function BackdropControl(props: BackdropControlProps) { [videoState, clickareaRef] ); + const lastBackdropValue = useRef(null); useEffect(() => { - props.onBackdropChange?.(moved || videoState.isPaused); + const currentValue = moved || videoState.isPaused; + if (currentValue !== lastBackdropValue.current) { + lastBackdropValue.current = currentValue; + props.onBackdropChange?.(currentValue); + } }, [videoState, moved, props]); const showUI = moved || videoState.isPaused; diff --git a/src/components/video/controls/ProgressListenerControl.tsx b/src/components/video/controls/ProgressListenerControl.tsx index b20fc4c8..6c23bb18 100644 --- a/src/components/video/controls/ProgressListenerControl.tsx +++ b/src/components/video/controls/ProgressListenerControl.tsx @@ -7,7 +7,6 @@ interface Props { onProgress?: (time: number, duration: number) => void; } -// TODO fix infinite loops export function ProgressListenerControl(props: Props) { const { videoState } = useVideoPlayerState(); const didInitialize = useRef(null); diff --git a/src/components/video/controls/ShowControl.tsx b/src/components/video/controls/ShowControl.tsx new file mode 100644 index 00000000..5e2467e9 --- /dev/null +++ b/src/components/video/controls/ShowControl.tsx @@ -0,0 +1,26 @@ +import { useEffect } from "react"; +import { useVideoPlayerState } from "../VideoContext"; + +interface ShowControlProps { + series?: { + episode: number; + season: number; + }; + title?: string; +} + +export function ShowControl(props: ShowControlProps) { + const { videoState } = useVideoPlayerState(); + + useEffect(() => { + videoState.setShowData({ + current: props.series, + isSeries: !!props.series, + title: props.title, + }); + // we only want it to run when props change, not when videoState changes + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [props]); + + return null; +} diff --git a/src/components/video/controls/ShowTitleControl.tsx b/src/components/video/controls/ShowTitleControl.tsx new file mode 100644 index 00000000..06cc7f7b --- /dev/null +++ b/src/components/video/controls/ShowTitleControl.tsx @@ -0,0 +1,19 @@ +import { useVideoPlayerState } from "../VideoContext"; + +export function ShowTitleControl() { + const { videoState } = useVideoPlayerState(); + + if (!videoState.seasonData.isSeries) return null; + if (!videoState.seasonData.title || !videoState.seasonData.current) + return null; + + const cur = videoState.seasonData.current; + const selectedText = `S${cur.season} E${cur.episode}`; + + return ( +

+ {selectedText} + {videoState.seasonData.title} +

+ ); +} diff --git a/src/components/video/hooks/controlVideo.ts b/src/components/video/hooks/controlVideo.ts index dc2d1aa1..6a30aa0e 100644 --- a/src/components/video/hooks/controlVideo.ts +++ b/src/components/video/hooks/controlVideo.ts @@ -11,6 +11,15 @@ import React, { RefObject } from "react"; import { PlayerState } from "./useVideoPlayer"; import { getStoredVolume, setStoredVolume } from "./volumeStore"; +interface ShowData { + current?: { + episode: number; + season: number; + }; + isSeries: boolean; + title?: string; +} + export interface PlayerControls { play(): void; pause(): void; @@ -21,6 +30,7 @@ export interface PlayerControls { setSeeking(active: boolean): void; setLeftControlsHover(hovering: boolean): void; initPlayer(sourceUrl: string, sourceType: MWStreamType): void; + setShowData(data: ShowData): void; } export const initialControls: PlayerControls = { @@ -33,6 +43,7 @@ export const initialControls: PlayerControls = { setSeeking: () => null, setLeftControlsHover: () => null, initPlayer: () => null, + setShowData: () => null, }; export function populateControls( @@ -105,6 +116,9 @@ export function populateControls( setLeftControlsHover(hovering) { update((s) => ({ ...s, leftControlHovering: hovering })); }, + setShowData(data) { + update((s) => ({ ...s, seasonData: data })); + }, initPlayer(sourceUrl: string, sourceType: MWStreamType) { this.setVolume(getStoredVolume()); diff --git a/src/components/video/hooks/useVideoPlayer.ts b/src/components/video/hooks/useVideoPlayer.ts index 4c82de6d..9c17a47e 100644 --- a/src/components/video/hooks/useVideoPlayer.ts +++ b/src/components/video/hooks/useVideoPlayer.ts @@ -23,6 +23,14 @@ export type PlayerState = { hasInitialized: boolean; leftControlHovering: boolean; hasPlayedOnce: boolean; + seasonData: { + isSeries: boolean; + current?: { + episode: number; + season: number; + }; + title?: string; + }; error: null | { name: string; description: string; @@ -47,6 +55,9 @@ export const initialPlayerState: PlayerContext = { leftControlHovering: false, hasPlayedOnce: false, error: null, + seasonData: { + isSeries: false, + }, ...initialControls, }; diff --git a/src/components/video/parts/VideoPlayerHeader.tsx b/src/components/video/parts/VideoPlayerHeader.tsx index 0fbde691..3f835645 100644 --- a/src/components/video/parts/VideoPlayerHeader.tsx +++ b/src/components/video/parts/VideoPlayerHeader.tsx @@ -35,19 +35,22 @@ export function VideoPlayerHeader(props: VideoPlayerHeaderProps) { ) : null} {props.media ? ( - + {props.media.title} - - props.media && setItemBookmark(props.media, !isBookmarked) - } - /> ) : null}

+ {props.media ? ( + + props.media && setItemBookmark(props.media, !isBookmarked) + } + /> + ) : null} diff --git a/src/index.tsx b/src/index.tsx index 630068b8..53312ce8 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -26,10 +26,6 @@ if (key) { // - safari fullscreen will make video overlap player controls // - safari progress bar is fucked (video doesnt change time but video.currentTime does change) -// TODO optional todos: -// - shortcuts when player is active -// - improve seekables (if possible) - // TODO stuff to test: // - browser: firefox, chrome, edge, safari desktop // - phones: android firefox, android chrome, iphone safari diff --git a/src/state/watched/context.tsx b/src/state/watched/context.tsx index 00208956..ae6421ae 100644 --- a/src/state/watched/context.tsx +++ b/src/state/watched/context.tsx @@ -6,6 +6,7 @@ import { useCallback, useContext, useMemo, + useRef, useState, } from "react"; import { VideoProgressStore } from "./store"; @@ -180,15 +181,20 @@ export function useWatchedItem(meta: DetailedMeta | null) { () => watched.items.find((v) => meta && v.item.meta.id === meta?.meta.id), [watched, meta] ); + const lastCommitedTime = useRef([0, 0]); const callback = useCallback( (progress: number, total: number) => { - if (meta) { - // TODO add series support + // TODO add series support + const hasChanged = + lastCommitedTime.current[0] !== progress || + lastCommitedTime.current[1] !== total; + if (meta && hasChanged) { + lastCommitedTime.current = [progress, total]; updateProgress({ meta: meta.meta }, progress, total); } }, - [updateProgress, meta] + [meta, updateProgress] ); return { updateProgress: callback, watchedItem: item }; diff --git a/src/views/media/MediaView.tsx b/src/views/media/MediaView.tsx index f6bb4272..fb3583a6 100644 --- a/src/views/media/MediaView.tsx +++ b/src/views/media/MediaView.tsx @@ -1,5 +1,5 @@ import { useParams } from "react-router-dom"; -import { useEffect, useState } from "react"; +import { useEffect, useRef, useState } from "react"; import { DecoratedVideoPlayer } from "@/components/video/DecoratedVideoPlayer"; import { MWStream } from "@/backend/helpers/streams"; import { SelectedMediaData, useScrape } from "@/hooks/useScrape"; @@ -15,6 +15,7 @@ import { IconPatch } from "@/components/buttons/IconPatch"; import { Icons } from "@/components/Icon"; import { useWatchedItem } from "@/state/watched"; import { ProgressListenerControl } from "@/components/video/controls/ProgressListenerControl"; +import { ShowControl } from "@/components/video/controls/ShowControl"; import { MediaFetchErrorView } from "./MediaErrorView"; import { MediaScrapeLog } from "./MediaScrapeLog"; import { NotFoundMedia, NotFoundWrapper } from "../notfound/NotFoundView"; @@ -81,6 +82,37 @@ function MediaViewScraping(props: MediaViewScrapingProps) { ); } +interface MediaViewPlayerProps { + meta: DetailedMeta; + stream: MWStream; +} +export function MediaViewPlayer(props: MediaViewPlayerProps) { + const goBack = useGoBack(); + const { updateProgress, watchedItem } = useWatchedItem(props.meta); + const firstStartTime = useRef(watchedItem?.progress); + useEffect(() => { + firstStartTime.current = watchedItem?.progress; + // only want it to change when stream changes + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [props.stream]); + + return ( +
+ + + + + +
+ ); +} + export function MediaView() { const params = useParams<{ media: string }>(); const goBack = useGoBack(); @@ -101,8 +133,6 @@ export function MediaView() { }); const [stream, setStream] = useState(null); - const { updateProgress, watchedItem } = useWatchedItem(meta); - useEffect(() => { exec(params.media).then((v) => { setMeta(v ?? null); @@ -137,15 +167,5 @@ export function MediaView() { ); // show stream once we have a stream - return ( -
- - - - -
- ); + return ; }