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 { 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,6 +31,7 @@ function LeftSideControls() {
|
|||
}, [videoState]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className="flex items-center px-2"
|
||||
onMouseLeave={handleMouseLeave}
|
||||
|
@ -39,6 +41,8 @@ function LeftSideControls() {
|
|||
<VolumeControl className="mr-2" />
|
||||
<TimeControl />
|
||||
</div>
|
||||
<ShowTitleControl />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -12,15 +12,14 @@ export function BackdropControl(props: BackdropControlProps) {
|
|||
const timeout = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const clickareaRef = useRef<HTMLDivElement>(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<boolean | null>(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;
|
||||
|
||||
|
|
|
@ -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<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 { 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());
|
||||
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
||||
|
|
|
@ -35,19 +35,22 @@ export function VideoPlayerHeader(props: VideoPlayerHeaderProps) {
|
|||
<span className="mx-4 h-6 w-[1.5px] rotate-[30deg] bg-white opacity-50" />
|
||||
) : null}
|
||||
{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>
|
||||
) : null}
|
||||
</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)
|
||||
}
|
||||
/>
|
||||
</span>
|
||||
) : null}
|
||||
</p>
|
||||
</div>
|
||||
<BrandPill />
|
||||
</div>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
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 };
|
||||
|
|
|
@ -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 (
|
||||
<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() {
|
||||
const params = useParams<{ media: string }>();
|
||||
const goBack = useGoBack();
|
||||
|
@ -101,8 +133,6 @@ export function MediaView() {
|
|||
});
|
||||
const [stream, setStream] = useState<MWStream | null>(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 (
|
||||
<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>
|
||||
);
|
||||
return <MediaViewPlayer meta={meta} stream={stream} />;
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue