mirror of
https://github.com/sussy-code/smov.git
synced 2024-12-20 14:37:43 +01:00
fix recursive rendering + show meta in player
This commit is contained in:
parent
b6a23aa0b7
commit
5a01a68ce4
11 changed files with 147 additions and 45 deletions
|
@ -7,6 +7,7 @@ import { LoadingControl } from "./controls/LoadingControl";
|
||||||
import { MiddlePauseControl } from "./controls/MiddlePauseControl";
|
import { MiddlePauseControl } from "./controls/MiddlePauseControl";
|
||||||
import { PauseControl } from "./controls/PauseControl";
|
import { PauseControl } from "./controls/PauseControl";
|
||||||
import { ProgressControl } from "./controls/ProgressControl";
|
import { ProgressControl } from "./controls/ProgressControl";
|
||||||
|
import { ShowTitleControl } from "./controls/ShowTitleControl";
|
||||||
import { TimeControl } from "./controls/TimeControl";
|
import { TimeControl } from "./controls/TimeControl";
|
||||||
import { VolumeControl } from "./controls/VolumeControl";
|
import { VolumeControl } from "./controls/VolumeControl";
|
||||||
import { VideoPlayerError } from "./parts/VideoPlayerError";
|
import { VideoPlayerError } from "./parts/VideoPlayerError";
|
||||||
|
@ -30,15 +31,18 @@ function LeftSideControls() {
|
||||||
}, [videoState]);
|
}, [videoState]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<>
|
||||||
className="flex items-center px-2"
|
<div
|
||||||
onMouseLeave={handleMouseLeave}
|
className="flex items-center px-2"
|
||||||
onMouseEnter={handleMouseEnter}
|
onMouseLeave={handleMouseLeave}
|
||||||
>
|
onMouseEnter={handleMouseEnter}
|
||||||
<PauseControl />
|
>
|
||||||
<VolumeControl className="mr-2" />
|
<PauseControl />
|
||||||
<TimeControl />
|
<VolumeControl className="mr-2" />
|
||||||
</div>
|
<TimeControl />
|
||||||
|
</div>
|
||||||
|
<ShowTitleControl />
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -12,15 +12,14 @@ export function BackdropControl(props: BackdropControlProps) {
|
||||||
const timeout = useRef<ReturnType<typeof setTimeout> | null>(null);
|
const timeout = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
const clickareaRef = useRef<HTMLDivElement>(null);
|
const clickareaRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
// TODO fix infinite loop
|
|
||||||
const handleMouseMove = useCallback(() => {
|
const handleMouseMove = useCallback(() => {
|
||||||
setMoved(true);
|
if (!moved) setMoved(true);
|
||||||
if (timeout.current) clearTimeout(timeout.current);
|
if (timeout.current) clearTimeout(timeout.current);
|
||||||
timeout.current = setTimeout(() => {
|
timeout.current = setTimeout(() => {
|
||||||
setMoved(false);
|
if (moved) setMoved(false);
|
||||||
timeout.current = null;
|
timeout.current = null;
|
||||||
}, 3000);
|
}, 3000);
|
||||||
}, [timeout, setMoved]);
|
}, [setMoved, moved]);
|
||||||
|
|
||||||
const handleMouseLeave = useCallback(() => {
|
const handleMouseLeave = useCallback(() => {
|
||||||
setMoved(false);
|
setMoved(false);
|
||||||
|
@ -45,8 +44,13 @@ export function BackdropControl(props: BackdropControlProps) {
|
||||||
[videoState, clickareaRef]
|
[videoState, clickareaRef]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const lastBackdropValue = useRef<boolean | null>(null);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
props.onBackdropChange?.(moved || videoState.isPaused);
|
const currentValue = moved || videoState.isPaused;
|
||||||
|
if (currentValue !== lastBackdropValue.current) {
|
||||||
|
lastBackdropValue.current = currentValue;
|
||||||
|
props.onBackdropChange?.(currentValue);
|
||||||
|
}
|
||||||
}, [videoState, moved, props]);
|
}, [videoState, moved, props]);
|
||||||
const showUI = moved || videoState.isPaused;
|
const showUI = moved || videoState.isPaused;
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,6 @@ interface Props {
|
||||||
onProgress?: (time: number, duration: number) => void;
|
onProgress?: (time: number, duration: number) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO fix infinite loops
|
|
||||||
export function ProgressListenerControl(props: Props) {
|
export function ProgressListenerControl(props: Props) {
|
||||||
const { videoState } = useVideoPlayerState();
|
const { videoState } = useVideoPlayerState();
|
||||||
const didInitialize = useRef<true | null>(null);
|
const didInitialize = useRef<true | null>(null);
|
||||||
|
|
26
src/components/video/controls/ShowControl.tsx
Normal file
26
src/components/video/controls/ShowControl.tsx
Normal file
|
@ -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;
|
||||||
|
}
|
19
src/components/video/controls/ShowTitleControl.tsx
Normal file
19
src/components/video/controls/ShowTitleControl.tsx
Normal file
|
@ -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 (
|
||||||
|
<p className="ml-8 select-none space-x-2 font-bold text-white">
|
||||||
|
<span>{selectedText}</span>
|
||||||
|
<span className="opacity-50">{videoState.seasonData.title}</span>
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
}
|
|
@ -11,6 +11,15 @@ import React, { RefObject } from "react";
|
||||||
import { PlayerState } from "./useVideoPlayer";
|
import { PlayerState } from "./useVideoPlayer";
|
||||||
import { getStoredVolume, setStoredVolume } from "./volumeStore";
|
import { getStoredVolume, setStoredVolume } from "./volumeStore";
|
||||||
|
|
||||||
|
interface ShowData {
|
||||||
|
current?: {
|
||||||
|
episode: number;
|
||||||
|
season: number;
|
||||||
|
};
|
||||||
|
isSeries: boolean;
|
||||||
|
title?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface PlayerControls {
|
export interface PlayerControls {
|
||||||
play(): void;
|
play(): void;
|
||||||
pause(): void;
|
pause(): void;
|
||||||
|
@ -21,6 +30,7 @@ export interface PlayerControls {
|
||||||
setSeeking(active: boolean): void;
|
setSeeking(active: boolean): void;
|
||||||
setLeftControlsHover(hovering: boolean): void;
|
setLeftControlsHover(hovering: boolean): void;
|
||||||
initPlayer(sourceUrl: string, sourceType: MWStreamType): void;
|
initPlayer(sourceUrl: string, sourceType: MWStreamType): void;
|
||||||
|
setShowData(data: ShowData): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const initialControls: PlayerControls = {
|
export const initialControls: PlayerControls = {
|
||||||
|
@ -33,6 +43,7 @@ export const initialControls: PlayerControls = {
|
||||||
setSeeking: () => null,
|
setSeeking: () => null,
|
||||||
setLeftControlsHover: () => null,
|
setLeftControlsHover: () => null,
|
||||||
initPlayer: () => null,
|
initPlayer: () => null,
|
||||||
|
setShowData: () => null,
|
||||||
};
|
};
|
||||||
|
|
||||||
export function populateControls(
|
export function populateControls(
|
||||||
|
@ -105,6 +116,9 @@ export function populateControls(
|
||||||
setLeftControlsHover(hovering) {
|
setLeftControlsHover(hovering) {
|
||||||
update((s) => ({ ...s, leftControlHovering: hovering }));
|
update((s) => ({ ...s, leftControlHovering: hovering }));
|
||||||
},
|
},
|
||||||
|
setShowData(data) {
|
||||||
|
update((s) => ({ ...s, seasonData: data }));
|
||||||
|
},
|
||||||
initPlayer(sourceUrl: string, sourceType: MWStreamType) {
|
initPlayer(sourceUrl: string, sourceType: MWStreamType) {
|
||||||
this.setVolume(getStoredVolume());
|
this.setVolume(getStoredVolume());
|
||||||
|
|
||||||
|
|
|
@ -23,6 +23,14 @@ export type PlayerState = {
|
||||||
hasInitialized: boolean;
|
hasInitialized: boolean;
|
||||||
leftControlHovering: boolean;
|
leftControlHovering: boolean;
|
||||||
hasPlayedOnce: boolean;
|
hasPlayedOnce: boolean;
|
||||||
|
seasonData: {
|
||||||
|
isSeries: boolean;
|
||||||
|
current?: {
|
||||||
|
episode: number;
|
||||||
|
season: number;
|
||||||
|
};
|
||||||
|
title?: string;
|
||||||
|
};
|
||||||
error: null | {
|
error: null | {
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
|
@ -47,6 +55,9 @@ export const initialPlayerState: PlayerContext = {
|
||||||
leftControlHovering: false,
|
leftControlHovering: false,
|
||||||
hasPlayedOnce: false,
|
hasPlayedOnce: false,
|
||||||
error: null,
|
error: null,
|
||||||
|
seasonData: {
|
||||||
|
isSeries: false,
|
||||||
|
},
|
||||||
...initialControls,
|
...initialControls,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -35,19 +35,22 @@ export function VideoPlayerHeader(props: VideoPlayerHeaderProps) {
|
||||||
<span className="mx-4 h-6 w-[1.5px] rotate-[30deg] bg-white opacity-50" />
|
<span className="mx-4 h-6 w-[1.5px] rotate-[30deg] bg-white opacity-50" />
|
||||||
) : null}
|
) : null}
|
||||||
{props.media ? (
|
{props.media ? (
|
||||||
<span className="flex items-center space-x-2 text-white">
|
<span className="flex items-center text-white">
|
||||||
<span>{props.media.title}</span>
|
<span>{props.media.title}</span>
|
||||||
<IconPatch
|
|
||||||
clickable
|
|
||||||
transparent
|
|
||||||
icon={isBookmarked ? Icons.BOOKMARK : Icons.BOOKMARK_OUTLINE}
|
|
||||||
onClick={() =>
|
|
||||||
props.media && setItemBookmark(props.media, !isBookmarked)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</span>
|
</span>
|
||||||
) : null}
|
) : null}
|
||||||
</p>
|
</p>
|
||||||
|
{props.media ? (
|
||||||
|
<IconPatch
|
||||||
|
clickable
|
||||||
|
transparent
|
||||||
|
icon={isBookmarked ? Icons.BOOKMARK : Icons.BOOKMARK_OUTLINE}
|
||||||
|
className="ml-2 text-white"
|
||||||
|
onClick={() =>
|
||||||
|
props.media && setItemBookmark(props.media, !isBookmarked)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
<BrandPill />
|
<BrandPill />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -26,10 +26,6 @@ if (key) {
|
||||||
// - safari fullscreen will make video overlap player controls
|
// - safari fullscreen will make video overlap player controls
|
||||||
// - safari progress bar is fucked (video doesnt change time but video.currentTime does change)
|
// - 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:
|
// TODO stuff to test:
|
||||||
// - browser: firefox, chrome, edge, safari desktop
|
// - browser: firefox, chrome, edge, safari desktop
|
||||||
// - phones: android firefox, android chrome, iphone safari
|
// - phones: android firefox, android chrome, iphone safari
|
||||||
|
|
|
@ -6,6 +6,7 @@ import {
|
||||||
useCallback,
|
useCallback,
|
||||||
useContext,
|
useContext,
|
||||||
useMemo,
|
useMemo,
|
||||||
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
} from "react";
|
} from "react";
|
||||||
import { VideoProgressStore } from "./store";
|
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.items.find((v) => meta && v.item.meta.id === meta?.meta.id),
|
||||||
[watched, meta]
|
[watched, meta]
|
||||||
);
|
);
|
||||||
|
const lastCommitedTime = useRef([0, 0]);
|
||||||
|
|
||||||
const callback = useCallback(
|
const callback = useCallback(
|
||||||
(progress: number, total: number) => {
|
(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.meta }, progress, total);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[updateProgress, meta]
|
[meta, updateProgress]
|
||||||
);
|
);
|
||||||
|
|
||||||
return { updateProgress: callback, watchedItem: item };
|
return { updateProgress: callback, watchedItem: item };
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { useParams } from "react-router-dom";
|
import { useParams } from "react-router-dom";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { DecoratedVideoPlayer } from "@/components/video/DecoratedVideoPlayer";
|
import { DecoratedVideoPlayer } from "@/components/video/DecoratedVideoPlayer";
|
||||||
import { MWStream } from "@/backend/helpers/streams";
|
import { MWStream } from "@/backend/helpers/streams";
|
||||||
import { SelectedMediaData, useScrape } from "@/hooks/useScrape";
|
import { SelectedMediaData, useScrape } from "@/hooks/useScrape";
|
||||||
|
@ -15,6 +15,7 @@ import { IconPatch } from "@/components/buttons/IconPatch";
|
||||||
import { Icons } from "@/components/Icon";
|
import { Icons } from "@/components/Icon";
|
||||||
import { useWatchedItem } from "@/state/watched";
|
import { useWatchedItem } from "@/state/watched";
|
||||||
import { ProgressListenerControl } from "@/components/video/controls/ProgressListenerControl";
|
import { ProgressListenerControl } from "@/components/video/controls/ProgressListenerControl";
|
||||||
|
import { ShowControl } from "@/components/video/controls/ShowControl";
|
||||||
import { MediaFetchErrorView } from "./MediaErrorView";
|
import { MediaFetchErrorView } from "./MediaErrorView";
|
||||||
import { MediaScrapeLog } from "./MediaScrapeLog";
|
import { MediaScrapeLog } from "./MediaScrapeLog";
|
||||||
import { NotFoundMedia, NotFoundWrapper } from "../notfound/NotFoundView";
|
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 (
|
||||||
|
<div className="h-screen w-screen">
|
||||||
|
<DecoratedVideoPlayer media={props.meta.meta} onGoBack={goBack} autoPlay>
|
||||||
|
<SourceControl
|
||||||
|
source={props.stream.streamUrl}
|
||||||
|
type={props.stream.type}
|
||||||
|
/>
|
||||||
|
<ProgressListenerControl
|
||||||
|
startAt={firstStartTime.current}
|
||||||
|
onProgress={updateProgress}
|
||||||
|
/>
|
||||||
|
<ShowControl series={{ episode: 5, season: 2 }} title="hello world" />
|
||||||
|
</DecoratedVideoPlayer>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function MediaView() {
|
export function MediaView() {
|
||||||
const params = useParams<{ media: string }>();
|
const params = useParams<{ media: string }>();
|
||||||
const goBack = useGoBack();
|
const goBack = useGoBack();
|
||||||
|
@ -101,8 +133,6 @@ export function MediaView() {
|
||||||
});
|
});
|
||||||
const [stream, setStream] = useState<MWStream | null>(null);
|
const [stream, setStream] = useState<MWStream | null>(null);
|
||||||
|
|
||||||
const { updateProgress, watchedItem } = useWatchedItem(meta);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
exec(params.media).then((v) => {
|
exec(params.media).then((v) => {
|
||||||
setMeta(v ?? null);
|
setMeta(v ?? null);
|
||||||
|
@ -137,15 +167,5 @@ export function MediaView() {
|
||||||
);
|
);
|
||||||
|
|
||||||
// show stream once we have a stream
|
// show stream once we have a stream
|
||||||
return (
|
return <MediaViewPlayer meta={meta} stream={stream} />;
|
||||||
<div className="h-screen w-screen">
|
|
||||||
<DecoratedVideoPlayer media={meta.meta} onGoBack={goBack} autoPlay>
|
|
||||||
<SourceControl source={stream.streamUrl} type={stream.type} />
|
|
||||||
<ProgressListenerControl
|
|
||||||
startAt={watchedItem?.progress}
|
|
||||||
onProgress={updateProgress}
|
|
||||||
/>
|
|
||||||
</DecoratedVideoPlayer>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue