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