mirror of
https://github.com/sussy-code/smov.git
synced 2024-12-20 14:37:43 +01:00
scraping, topbar, fix timestuff, darkened overlay, fix click targets
Co-authored-by: Jip Frijlink <JipFr@users.noreply.github.com>
This commit is contained in:
parent
fa0ac293b4
commit
3b7df601af
19 changed files with 256 additions and 115 deletions
|
@ -1,3 +1,7 @@
|
||||||
export * from "./atoms";
|
export * from "./atoms";
|
||||||
export * from "./base/Container";
|
export * from "./base/Container";
|
||||||
|
export * from "./base/TopControls";
|
||||||
export * from "./base/BottomControls";
|
export * from "./base/BottomControls";
|
||||||
|
export * from "./base/BlackOverlay";
|
||||||
|
export * from "./base/BackLink";
|
||||||
|
export * from "./internals/BookmarkButton";
|
||||||
|
|
|
@ -34,7 +34,7 @@ export function ProgressBar() {
|
||||||
return (
|
return (
|
||||||
<div ref={ref}>
|
<div ref={ref}>
|
||||||
<div
|
<div
|
||||||
className="group w-full h-8 flex items-center"
|
className="group w-full h-8 flex items-center cursor-pointer"
|
||||||
onMouseDown={dragMouseDown}
|
onMouseDown={dragMouseDown}
|
||||||
onTouchStart={dragMouseDown}
|
onTouchStart={dragMouseDown}
|
||||||
>
|
>
|
||||||
|
|
|
@ -1,19 +1,29 @@
|
||||||
import { useState } from "react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import { VideoPlayerButton } from "@/components/player/internals/Button";
|
import { VideoPlayerButton } from "@/components/player/internals/Button";
|
||||||
|
import { VideoPlayerTimeFormat } from "@/stores/player/slices/interface";
|
||||||
import { usePlayerStore } from "@/stores/player/store";
|
import { usePlayerStore } from "@/stores/player/store";
|
||||||
import { formatSeconds } from "@/utils/formatSeconds";
|
import { formatSeconds } from "@/utils/formatSeconds";
|
||||||
|
|
||||||
|
function durationExceedsHour(secs: number): boolean {
|
||||||
|
return secs > 60 * 60;
|
||||||
|
}
|
||||||
|
|
||||||
export function Time() {
|
export function Time() {
|
||||||
const [timeMode, setTimeMode] = useState(true);
|
const timeFormat = usePlayerStore((s) => s.interface.timeFormat);
|
||||||
|
const setTimeFormat = usePlayerStore((s) => s.setTimeFormat);
|
||||||
|
|
||||||
const { duration, time, draggingTime } = usePlayerStore((s) => s.progress);
|
const { duration, time, draggingTime } = usePlayerStore((s) => s.progress);
|
||||||
const { isSeeking } = usePlayerStore((s) => s.interface);
|
const { isSeeking } = usePlayerStore((s) => s.interface);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const hasHours = durationExceedsHour(duration);
|
||||||
|
|
||||||
function toggleMode() {
|
function toggleMode() {
|
||||||
setTimeMode(!timeMode);
|
setTimeFormat(
|
||||||
|
timeFormat === VideoPlayerTimeFormat.REGULAR
|
||||||
|
? VideoPlayerTimeFormat.REMAINING
|
||||||
|
: VideoPlayerTimeFormat.REGULAR
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentTime = Math.min(
|
const currentTime = Math.min(
|
||||||
|
@ -30,13 +40,20 @@ export function Time() {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const child = timeMode ? (
|
const child =
|
||||||
|
timeFormat === VideoPlayerTimeFormat.REGULAR ? (
|
||||||
<>
|
<>
|
||||||
{formatSeconds(currentTime)} <span>/ {formatSeconds(duration)}</span>
|
{formatSeconds(currentTime, hasHours)}{" "}
|
||||||
|
<span>/ {formatSeconds(duration, hasHours)}</span>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{t("videoPlayer.timeLeft", { timeLeft: formatSeconds(secondsRemaining) })}{" "}
|
{t("videoPlayer.timeLeft", {
|
||||||
|
timeLeft: formatSeconds(
|
||||||
|
secondsRemaining,
|
||||||
|
durationExceedsHour(secondsRemaining)
|
||||||
|
),
|
||||||
|
})}{" "}
|
||||||
• {formattedTimeFinished}
|
• {formattedTimeFinished}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
23
src/components/player/base/BackLink.tsx
Normal file
23
src/components/player/base/BackLink.tsx
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
import { Icon, Icons } from "@/components/Icon";
|
||||||
|
import { useGoBack } from "@/hooks/useGoBack";
|
||||||
|
|
||||||
|
export function BackLink() {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const goBack = useGoBack();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center">
|
||||||
|
<span
|
||||||
|
onClick={() => goBack()}
|
||||||
|
className="flex items-center cursor-pointer text-type-secondary hover:text-white transition-colors duration-200 font-medium"
|
||||||
|
>
|
||||||
|
<Icon className="mr-2" icon={Icons.ARROW_LEFT} />
|
||||||
|
<span>{t("videoPlayer.backToHomeShort")}</span>
|
||||||
|
</span>
|
||||||
|
<span className="text mx-3 text-type-secondary">/</span>
|
||||||
|
<span>Mr Jeebaloo's Big Ocean Adventure</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
11
src/components/player/base/BlackOverlay.tsx
Normal file
11
src/components/player/base/BlackOverlay.tsx
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
import { Transition } from "@/components/Transition";
|
||||||
|
|
||||||
|
export function BlackOverlay(props: { show?: boolean }) {
|
||||||
|
return (
|
||||||
|
<Transition
|
||||||
|
animation="fade"
|
||||||
|
show={props.show}
|
||||||
|
className="absolute inset-0 w-full h-full bg-black bg-opacity-20 pointer-events-none"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,25 +1,19 @@
|
||||||
import { Transition } from "@/components/Transition";
|
import { Transition } from "@/components/Transition";
|
||||||
import { PlayerHoverState } from "@/stores/player/slices/interface";
|
|
||||||
import { usePlayerStore } from "@/stores/player/store";
|
|
||||||
|
|
||||||
export function BottomControls(props: {
|
export function BottomControls(props: {
|
||||||
show?: boolean;
|
show?: boolean;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
const { hovering } = usePlayerStore((s) => s.interface);
|
|
||||||
const visible =
|
|
||||||
(hovering !== PlayerHoverState.NOT_HOVERING || props.show) ?? false;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full text-white">
|
<div className="w-full text-white">
|
||||||
<Transition
|
<Transition
|
||||||
animation="fade"
|
animation="fade"
|
||||||
show={visible}
|
show={props.show}
|
||||||
className="pointer-events-none flex justify-end pt-32 bg-gradient-to-t from-black to-transparent [margin-bottom:env(safe-area-inset-bottom)] transition-opacity duration-200 absolute bottom-0 w-full"
|
className="pointer-events-none flex justify-end pt-32 bg-gradient-to-t from-black to-transparent [margin-bottom:env(safe-area-inset-bottom)] transition-opacity duration-200 absolute bottom-0 w-full"
|
||||||
/>
|
/>
|
||||||
<Transition
|
<Transition
|
||||||
animation="slide-up"
|
animation="slide-up"
|
||||||
show={visible}
|
show={props.show}
|
||||||
className="pointer-events-auto px-4 pb-3 absolute bottom-0 w-full"
|
className="pointer-events-auto px-4 pb-3 absolute bottom-0 w-full"
|
||||||
>
|
>
|
||||||
{props.children}
|
{props.children}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { ReactNode, RefObject, useEffect, useRef } from "react";
|
import { ReactNode, RefObject, useEffect, useRef } from "react";
|
||||||
|
|
||||||
|
import { VideoClickTarget } from "@/components/player/internals/VideoClickTarget";
|
||||||
import { VideoContainer } from "@/components/player/internals/VideoContainer";
|
import { VideoContainer } from "@/components/player/internals/VideoContainer";
|
||||||
import { PlayerHoverState } from "@/stores/player/slices/interface";
|
import { PlayerHoverState } from "@/stores/player/slices/interface";
|
||||||
import { usePlayerStore } from "@/stores/player/store";
|
import { usePlayerStore } from "@/stores/player/store";
|
||||||
|
@ -36,22 +37,12 @@ function useHovering(containerEl: RefObject<HTMLDivElement>) {
|
||||||
if (timeoutRef.current) clearTimeout(timeoutRef.current);
|
if (timeoutRef.current) clearTimeout(timeoutRef.current);
|
||||||
}
|
}
|
||||||
|
|
||||||
function pointerUp(e: PointerEvent) {
|
|
||||||
if (e.pointerType === "mouse") return;
|
|
||||||
if (timeoutRef.current) clearTimeout(timeoutRef.current);
|
|
||||||
if (hovering !== PlayerHoverState.MOBILE_TAPPED)
|
|
||||||
updateInterfaceHovering(PlayerHoverState.MOBILE_TAPPED);
|
|
||||||
else updateInterfaceHovering(PlayerHoverState.NOT_HOVERING);
|
|
||||||
}
|
|
||||||
|
|
||||||
el.addEventListener("pointermove", pointerMove);
|
el.addEventListener("pointermove", pointerMove);
|
||||||
el.addEventListener("pointerleave", pointerLeave);
|
el.addEventListener("pointerleave", pointerLeave);
|
||||||
el.addEventListener("pointerup", pointerUp);
|
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
el.removeEventListener("pointermove", pointerMove);
|
el.removeEventListener("pointermove", pointerMove);
|
||||||
el.removeEventListener("pointerleave", pointerLeave);
|
el.removeEventListener("pointerleave", pointerLeave);
|
||||||
el.removeEventListener("pointerup", pointerUp);
|
|
||||||
};
|
};
|
||||||
}, [containerEl, hovering, updateInterfaceHovering]);
|
}, [containerEl, hovering, updateInterfaceHovering]);
|
||||||
}
|
}
|
||||||
|
@ -69,7 +60,10 @@ function BaseContainer(props: { children?: ReactNode }) {
|
||||||
}, [display, containerEl]);
|
}, [display, containerEl]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative overflow-hidden h-screen" ref={containerEl}>
|
<div
|
||||||
|
className="relative overflow-hidden h-screen select-none"
|
||||||
|
ref={containerEl}
|
||||||
|
>
|
||||||
{props.children}
|
{props.children}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -84,6 +78,7 @@ export function Container(props: PlayerProps) {
|
||||||
return (
|
return (
|
||||||
<BaseContainer>
|
<BaseContainer>
|
||||||
<VideoContainer />
|
<VideoContainer />
|
||||||
|
<VideoClickTarget />
|
||||||
{props.children}
|
{props.children}
|
||||||
</BaseContainer>
|
</BaseContainer>
|
||||||
);
|
);
|
||||||
|
|
23
src/components/player/base/TopControls.tsx
Normal file
23
src/components/player/base/TopControls.tsx
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
import { Transition } from "@/components/Transition";
|
||||||
|
|
||||||
|
export function TopControls(props: {
|
||||||
|
show?: boolean;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className="w-full text-white">
|
||||||
|
<Transition
|
||||||
|
animation="fade"
|
||||||
|
show={props.show}
|
||||||
|
className="pointer-events-none flex justify-end pb-32 bg-gradient-to-b from-black to-transparent [margin-bottom:env(safe-area-inset-bottom)] transition-opacity duration-200 absolute top-0 w-full"
|
||||||
|
/>
|
||||||
|
<Transition
|
||||||
|
animation="slide-down"
|
||||||
|
show={props.show}
|
||||||
|
className="pointer-events-auto px-4 pt-6 absolute top-0 w-full text-white"
|
||||||
|
>
|
||||||
|
{props.children}
|
||||||
|
</Transition>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -21,6 +21,7 @@ export function makeVideoElementDisplayInterface(): DisplayInterface {
|
||||||
let containerElement: HTMLElement | null = null;
|
let containerElement: HTMLElement | null = null;
|
||||||
let isFullscreen = false;
|
let isFullscreen = false;
|
||||||
let isPausedBeforeSeeking = false;
|
let isPausedBeforeSeeking = false;
|
||||||
|
let isSeeking = false;
|
||||||
|
|
||||||
function setSource() {
|
function setSource() {
|
||||||
if (!videoElement || !source) return;
|
if (!videoElement || !source) return;
|
||||||
|
@ -78,6 +79,9 @@ export function makeVideoElementDisplayInterface(): DisplayInterface {
|
||||||
videoElement?.play();
|
videoElement?.play();
|
||||||
},
|
},
|
||||||
setSeeking(active) {
|
setSeeking(active) {
|
||||||
|
if (active === isSeeking) return;
|
||||||
|
isSeeking = active;
|
||||||
|
|
||||||
// if it was playing when starting to seek, play again
|
// if it was playing when starting to seek, play again
|
||||||
if (!active) {
|
if (!active) {
|
||||||
if (!isPausedBeforeSeeking) this.play();
|
if (!isPausedBeforeSeeking) this.play();
|
||||||
|
|
9
src/components/player/hooks/useShouldShowControls.tsx
Normal file
9
src/components/player/hooks/useShouldShowControls.tsx
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
import { PlayerHoverState } from "@/stores/player/slices/interface";
|
||||||
|
import { usePlayerStore } from "@/stores/player/store";
|
||||||
|
|
||||||
|
export function useShouldShowControls() {
|
||||||
|
const { hovering } = usePlayerStore((s) => s.interface);
|
||||||
|
const { isPaused } = usePlayerStore((s) => s.mediaPlaying);
|
||||||
|
|
||||||
|
return hovering !== PlayerHoverState.NOT_HOVERING || isPaused;
|
||||||
|
}
|
14
src/components/player/internals/BookmarkButton.tsx
Normal file
14
src/components/player/internals/BookmarkButton.tsx
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
import { Icons } from "@/components/Icon";
|
||||||
|
|
||||||
|
import { VideoPlayerButton } from "./Button";
|
||||||
|
|
||||||
|
export function BookmarkButton() {
|
||||||
|
return (
|
||||||
|
<VideoPlayerButton
|
||||||
|
onClick={() => window.open("https://youtu.be/TENzstSjsus", "_blank")}
|
||||||
|
icon={Icons.BOOKMARK_OUTLINE}
|
||||||
|
iconSizeClass="text-base"
|
||||||
|
className="p-3"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
|
@ -4,14 +4,21 @@ export function VideoPlayerButton(props: {
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
icon?: Icons;
|
icon?: Icons;
|
||||||
|
iconSizeClass?: string;
|
||||||
|
className?: string;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={props.onClick}
|
onClick={props.onClick}
|
||||||
className="p-2 rounded-full hover:bg-video-buttonBackground hover:bg-opacity-75 transition-transform duration-100 active:scale-110 active:bg-opacity-100 active:text-white"
|
className={[
|
||||||
|
"p-2 rounded-full hover:bg-video-buttonBackground hover:bg-opacity-75 transition-transform duration-100 active:scale-110 active:bg-opacity-100 active:text-white",
|
||||||
|
props.className ?? "",
|
||||||
|
].join(" ")}
|
||||||
>
|
>
|
||||||
{props.icon && <Icon className="text-2xl" icon={props.icon} />}
|
{props.icon && (
|
||||||
|
<Icon className={props.iconSizeClass || "text-2xl"} icon={props.icon} />
|
||||||
|
)}
|
||||||
{props.children}
|
{props.children}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
|
|
47
src/components/player/internals/VideoClickTarget.tsx
Normal file
47
src/components/player/internals/VideoClickTarget.tsx
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
import { PointerEvent, useCallback } from "react";
|
||||||
|
|
||||||
|
import { useShouldShowVideoElement } from "@/components/player/internals/VideoContainer";
|
||||||
|
import { PlayerHoverState } from "@/stores/player/slices/interface";
|
||||||
|
import { usePlayerStore } from "@/stores/player/store";
|
||||||
|
|
||||||
|
export function VideoClickTarget() {
|
||||||
|
const show = useShouldShowVideoElement();
|
||||||
|
const display = usePlayerStore((s) => s.display);
|
||||||
|
const isPaused = usePlayerStore((s) => s.mediaPlaying.isPaused);
|
||||||
|
const updateInterfaceHovering = usePlayerStore(
|
||||||
|
(s) => s.updateInterfaceHovering
|
||||||
|
);
|
||||||
|
const hovering = usePlayerStore((s) => s.interface.hovering);
|
||||||
|
|
||||||
|
const toggleFullscreen = useCallback(() => {
|
||||||
|
display?.toggleFullscreen();
|
||||||
|
}, [display]);
|
||||||
|
|
||||||
|
const togglePause = useCallback(
|
||||||
|
(e: PointerEvent<HTMLDivElement>) => {
|
||||||
|
// pause on mouse click
|
||||||
|
if (e.pointerType === "mouse") {
|
||||||
|
if (e.button !== 0) return;
|
||||||
|
if (isPaused) display?.play();
|
||||||
|
else display?.pause();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// toggle on other types of clicks
|
||||||
|
if (hovering !== PlayerHoverState.MOBILE_TAPPED)
|
||||||
|
updateInterfaceHovering(PlayerHoverState.MOBILE_TAPPED);
|
||||||
|
else updateInterfaceHovering(PlayerHoverState.NOT_HOVERING);
|
||||||
|
},
|
||||||
|
[display, isPaused, hovering, updateInterfaceHovering]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!show) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="absolute inset-0"
|
||||||
|
onDoubleClick={toggleFullscreen}
|
||||||
|
onPointerUp={togglePause}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
import { PointerEvent, useCallback, useEffect, useRef } from "react";
|
import { useEffect, useRef } from "react";
|
||||||
|
|
||||||
import { makeVideoElementDisplayInterface } from "@/components/player/display/base";
|
import { makeVideoElementDisplayInterface } from "@/components/player/display/base";
|
||||||
import { playerStatus } from "@/stores/player/slices/source";
|
import { playerStatus } from "@/stores/player/slices/source";
|
||||||
|
@ -16,7 +16,7 @@ function useDisplayInterface() {
|
||||||
}, [display, setDisplay]);
|
}, [display, setDisplay]);
|
||||||
}
|
}
|
||||||
|
|
||||||
function useShouldShowVideoElement() {
|
export function useShouldShowVideoElement() {
|
||||||
const status = usePlayerStore((s) => s.status);
|
const status = usePlayerStore((s) => s.status);
|
||||||
|
|
||||||
if (status !== playerStatus.PLAYING) return false;
|
if (status !== playerStatus.PLAYING) return false;
|
||||||
|
@ -26,20 +26,6 @@ function useShouldShowVideoElement() {
|
||||||
function VideoElement() {
|
function VideoElement() {
|
||||||
const videoEl = useRef<HTMLVideoElement>(null);
|
const videoEl = useRef<HTMLVideoElement>(null);
|
||||||
const display = usePlayerStore((s) => s.display);
|
const display = usePlayerStore((s) => s.display);
|
||||||
const isPaused = usePlayerStore((s) => s.mediaPlaying.isPaused);
|
|
||||||
|
|
||||||
const toggleFullscreen = useCallback(() => {
|
|
||||||
display?.toggleFullscreen();
|
|
||||||
}, [display]);
|
|
||||||
|
|
||||||
const togglePause = useCallback(
|
|
||||||
(e: PointerEvent<HTMLVideoElement>) => {
|
|
||||||
if (e.pointerType !== "mouse") return;
|
|
||||||
if (isPaused) display?.play();
|
|
||||||
else display?.pause();
|
|
||||||
},
|
|
||||||
[display, isPaused]
|
|
||||||
);
|
|
||||||
|
|
||||||
// report video element to display interface
|
// report video element to display interface
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -48,15 +34,7 @@ function VideoElement() {
|
||||||
}
|
}
|
||||||
}, [display, videoEl]);
|
}, [display, videoEl]);
|
||||||
|
|
||||||
return (
|
return <video className="w-full h-screen bg-black" autoPlay ref={videoEl} />;
|
||||||
<video
|
|
||||||
className="w-full h-screen bg-black"
|
|
||||||
autoPlay
|
|
||||||
ref={videoEl}
|
|
||||||
onDoubleClick={toggleFullscreen}
|
|
||||||
onPointerUp={togglePause}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function VideoContainer() {
|
export function VideoContainer() {
|
||||||
|
|
|
@ -1,26 +1,46 @@
|
||||||
import { useCallback } from "react";
|
import { BrandPill } from "@/components/layout/BrandPill";
|
||||||
|
|
||||||
import { MWStreamType } from "@/backend/helpers/streams";
|
|
||||||
import { Player } from "@/components/player";
|
import { Player } from "@/components/player";
|
||||||
import { usePlayer } from "@/components/player/hooks/usePlayer";
|
import { usePlayer } from "@/components/player/hooks/usePlayer";
|
||||||
|
import { useShouldShowControls } from "@/components/player/hooks/useShouldShowControls";
|
||||||
import { ScrapingPart } from "@/pages/parts/player/ScrapingPart";
|
import { ScrapingPart } from "@/pages/parts/player/ScrapingPart";
|
||||||
import { playerStatus } from "@/stores/player/slices/source";
|
import { playerStatus } from "@/stores/player/slices/source";
|
||||||
|
|
||||||
export function PlayerView() {
|
export function PlayerView() {
|
||||||
const { status, playMedia, setScrapeStatus } = usePlayer();
|
const { status, setScrapeStatus } = usePlayer();
|
||||||
|
const desktopControlsVisible = useShouldShowControls();
|
||||||
const startStream = useCallback(() => {
|
|
||||||
playMedia({
|
|
||||||
type: MWStreamType.MP4,
|
|
||||||
// url: "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4",
|
|
||||||
// url: "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/WhatCarCanYouGetForAGrand.mp4",
|
|
||||||
url: "http://95.111.247.180/frog.mp4",
|
|
||||||
});
|
|
||||||
}, [playMedia]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Player.Container onLoad={setScrapeStatus}>
|
<Player.Container onLoad={setScrapeStatus}>
|
||||||
<Player.BottomControls>
|
{status === playerStatus.SCRAPING ? (
|
||||||
|
<ScrapingPart
|
||||||
|
media={{
|
||||||
|
type: "movie",
|
||||||
|
title: "Everything Everywhere All At Once",
|
||||||
|
tmdbId: "545611",
|
||||||
|
releaseYear: 2022,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<Player.BlackOverlay show={desktopControlsVisible} />
|
||||||
|
<Player.TopControls show={desktopControlsVisible}>
|
||||||
|
<div className="grid grid-cols-[1fr,auto] xl:grid-cols-3 items-center">
|
||||||
|
<div className="flex space-x-3 items-center">
|
||||||
|
<Player.BackLink />
|
||||||
|
<Player.BookmarkButton />
|
||||||
|
</div>
|
||||||
|
<div className="text-center hidden xl:flex justify-center items-center">
|
||||||
|
<span className="text-white font-medium mr-3">S1 E5</span>
|
||||||
|
<span className="text-type-secondary font-medium">
|
||||||
|
Mr. Jeebaloo discovers Atlantis
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-end">
|
||||||
|
<BrandPill />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Player.TopControls>
|
||||||
|
<Player.BottomControls show={desktopControlsVisible}>
|
||||||
<Player.ProgressBar />
|
<Player.ProgressBar />
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
<div className="flex space-x-3 items-center">
|
<div className="flex space-x-3 items-center">
|
||||||
|
@ -34,18 +54,6 @@ export function PlayerView() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Player.BottomControls>
|
</Player.BottomControls>
|
||||||
|
|
||||||
{status === playerStatus.SCRAPING ? (
|
|
||||||
<ScrapingPart
|
|
||||||
onGetStream={startStream}
|
|
||||||
media={{
|
|
||||||
type: "movie",
|
|
||||||
title: "Hamilton",
|
|
||||||
tmdbId: "556574",
|
|
||||||
releaseYear: 2020,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
</Player.Container>
|
</Player.Container>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,13 @@
|
||||||
import { ScrapeMedia } from "@movie-web/providers";
|
import { ScrapeMedia } from "@movie-web/providers";
|
||||||
import { useCallback, useState } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
|
import { MWStreamType } from "@/backend/helpers/streams";
|
||||||
|
import { usePlayer } from "@/components/player/hooks/usePlayer";
|
||||||
import { providers } from "@/utils/providers";
|
import { providers } from "@/utils/providers";
|
||||||
|
|
||||||
export interface ScrapingProps {
|
export interface ScrapingProps {
|
||||||
media: ScrapeMedia;
|
media: ScrapeMedia;
|
||||||
onGetStream?: () => void;
|
// onGetStream?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ScrapingSegment {
|
export interface ScrapingSegment {
|
||||||
|
@ -32,7 +34,6 @@ function useScrape() {
|
||||||
media,
|
media,
|
||||||
events: {
|
events: {
|
||||||
init(evt) {
|
init(evt) {
|
||||||
console.log("init", evt);
|
|
||||||
setSources(
|
setSources(
|
||||||
evt.sourceIds
|
evt.sourceIds
|
||||||
.map((v) => {
|
.map((v) => {
|
||||||
|
@ -54,14 +55,12 @@ function useScrape() {
|
||||||
setSourceOrder(evt.sourceIds.map((v) => ({ id: v, children: [] })));
|
setSourceOrder(evt.sourceIds.map((v) => ({ id: v, children: [] })));
|
||||||
},
|
},
|
||||||
start(id) {
|
start(id) {
|
||||||
console.log("start", id);
|
|
||||||
setSources((s) => {
|
setSources((s) => {
|
||||||
if (s[id]) s[id].status = "pending";
|
if (s[id]) s[id].status = "pending";
|
||||||
return { ...s };
|
return { ...s };
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
update(evt) {
|
update(evt) {
|
||||||
console.log("update", evt);
|
|
||||||
setSources((s) => {
|
setSources((s) => {
|
||||||
if (s[evt.id]) {
|
if (s[evt.id]) {
|
||||||
s[evt.id].status = evt.status;
|
s[evt.id].status = evt.status;
|
||||||
|
@ -72,7 +71,6 @@ function useScrape() {
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
discoverEmbeds(evt) {
|
discoverEmbeds(evt) {
|
||||||
console.log("discoverEmbeds", evt);
|
|
||||||
setSources((s) => {
|
setSources((s) => {
|
||||||
evt.embeds.forEach((v) => {
|
evt.embeds.forEach((v) => {
|
||||||
const source = providers.getMetadata(v.embedScraperId);
|
const source = providers.getMetadata(v.embedScraperId);
|
||||||
|
@ -97,7 +95,6 @@ function useScrape() {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(output);
|
|
||||||
return output;
|
return output;
|
||||||
},
|
},
|
||||||
[setSourceOrder, setSources]
|
[setSourceOrder, setSources]
|
||||||
|
@ -111,10 +108,26 @@ function useScrape() {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ScrapingPart(props: ScrapingProps) {
|
export function ScrapingPart(props: ScrapingProps) {
|
||||||
|
const { playMedia } = usePlayer();
|
||||||
const { startScraping, sourceOrder, sources } = useScrape();
|
const { startScraping, sourceOrder, sources } = useScrape();
|
||||||
|
|
||||||
|
const started = useRef(false);
|
||||||
|
useEffect(() => {
|
||||||
|
if (started.current) return;
|
||||||
|
started.current = true;
|
||||||
|
(async () => {
|
||||||
|
const output = await startScraping(props.media);
|
||||||
|
if (output?.stream.type !== "file") return;
|
||||||
|
const firstFile = Object.values(output.stream.qualities)[0];
|
||||||
|
playMedia({
|
||||||
|
type: MWStreamType.MP4,
|
||||||
|
url: firstFile.url,
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
}, [startScraping, props, playMedia]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="h-full w-full flex items-center justify-center flex-col">
|
||||||
{sourceOrder.map((order) => {
|
{sourceOrder.map((order) => {
|
||||||
const source = sources[order.id];
|
const source = sources[order.id];
|
||||||
if (!source) return null;
|
if (!source) return null;
|
||||||
|
@ -141,20 +154,6 @@ export function ScrapingPart(props: ScrapingProps) {
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => startScraping(props.media)}
|
|
||||||
className="block"
|
|
||||||
>
|
|
||||||
Start scraping
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => props.onGetStream?.()}
|
|
||||||
className="block"
|
|
||||||
>
|
|
||||||
Finish scraping
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,6 +25,7 @@ export interface InterfaceSlice {
|
||||||
};
|
};
|
||||||
updateInterfaceHovering(newState: PlayerHoverState): void;
|
updateInterfaceHovering(newState: PlayerHoverState): void;
|
||||||
setSeeking(seeking: boolean): void;
|
setSeeking(seeking: boolean): void;
|
||||||
|
setTimeFormat(format: VideoPlayerTimeFormat): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const createInterfaceSlice: MakeSlice<InterfaceSlice> = (set, get) => ({
|
export const createInterfaceSlice: MakeSlice<InterfaceSlice> = (set, get) => ({
|
||||||
|
@ -38,6 +39,11 @@ export const createInterfaceSlice: MakeSlice<InterfaceSlice> = (set, get) => ({
|
||||||
timeFormat: VideoPlayerTimeFormat.REGULAR,
|
timeFormat: VideoPlayerTimeFormat.REGULAR,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
setTimeFormat(format) {
|
||||||
|
set((s) => {
|
||||||
|
s.interface.timeFormat = format;
|
||||||
|
});
|
||||||
|
},
|
||||||
updateInterfaceHovering(newState: PlayerHoverState) {
|
updateInterfaceHovering(newState: PlayerHoverState) {
|
||||||
set((s) => {
|
set((s) => {
|
||||||
s.interface.hovering = newState;
|
s.interface.hovering = newState;
|
||||||
|
|
|
@ -78,7 +78,8 @@ module.exports = {
|
||||||
emphasis: "#FFFFFF",
|
emphasis: "#FFFFFF",
|
||||||
text: "#73739D",
|
text: "#73739D",
|
||||||
dimmed: "#926CAD",
|
dimmed: "#926CAD",
|
||||||
divider: "#262632"
|
divider: "#262632",
|
||||||
|
secondary: "#64647B"
|
||||||
},
|
},
|
||||||
|
|
||||||
// search bar
|
// search bar
|
||||||
|
|
13
v4-todo.md
13
v4-todo.md
|
@ -1,7 +1,7 @@
|
||||||
player itself:
|
player itself:
|
||||||
- [ ] BUG Pause should keep controls visible
|
- [x] BUG Pause should keep controls visible
|
||||||
- [ ] BUG Touch on bottoms shouldn't toggle UI
|
- [x] BUG Touch on bottoms shouldn't toggle UI
|
||||||
- [ ] BUG unpause when controls hover
|
- [x] BUG unpause when controls hover
|
||||||
- [ ] keyboard controls
|
- [ ] keyboard controls
|
||||||
- [ ] fullscreen
|
- [ ] fullscreen
|
||||||
- [ ] barrel roll
|
- [ ] barrel roll
|
||||||
|
@ -9,15 +9,16 @@ player itself:
|
||||||
- [ ] skip forward/backward
|
- [ ] skip forward/backward
|
||||||
- [ ] pause
|
- [ ] pause
|
||||||
- [ ] volume ui
|
- [ ] volume ui
|
||||||
- [ ] header (back, title, logo)
|
- [x] header (back, title, logo)
|
||||||
- [ ] touch middle controls (forward, backward, pause)
|
- [ ] touch middle controls (forward, backward, pause)
|
||||||
- [ ] volume
|
|
||||||
- [ ] bookmark in header
|
- [ ] bookmark in header
|
||||||
- [ ] airplay
|
- [ ] airplay
|
||||||
- [ ] responsiveness
|
- [ ] responsiveness
|
||||||
- [ ] chromecast
|
- [ ] chromecast
|
||||||
- [ ] thumbnails
|
- [ ] thumbnails
|
||||||
- [ ] hover darken overlay (10% black)
|
- [x] hover darken overlay (20% black)
|
||||||
|
- [ ] autoplay not working
|
||||||
|
- [ ] play button in middle if cant autoplay
|
||||||
|
|
||||||
player views:
|
player views:
|
||||||
- [ ] scraping view
|
- [ ] scraping view
|
||||||
|
|
Loading…
Reference in a new issue