1
0
Fork 0
mirror of https://github.com/sussy-code/smov.git synced 2024-12-29 16:07:40 +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:
mrjvs 2023-10-02 21:04:40 +02:00
parent fa0ac293b4
commit 3b7df601af
19 changed files with 256 additions and 115 deletions

View file

@ -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";

View file

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

View file

@ -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,16 +40,23 @@ 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) })}{" "} ) : (
{formattedTimeFinished} <>
</> {t("videoPlayer.timeLeft", {
); timeLeft: formatSeconds(
secondsRemaining,
durationExceedsHour(secondsRemaining)
),
})}{" "}
{formattedTimeFinished}
</>
);
return ( return (
<VideoPlayerButton onClick={() => toggleMode()}>{child}</VideoPlayerButton> <VideoPlayerButton onClick={() => toggleMode()}>{child}</VideoPlayerButton>

View 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&apos;s Big Ocean Adventure</span>
</div>
);
}

View 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"
/>
);
}

View file

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

View file

@ -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>
); );

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

View file

@ -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();

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

View 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"
/>
);
}

View file

@ -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>
); );

View 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}
/>
);
}

View file

@ -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() {

View file

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

View file

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

View file

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

View file

@ -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

View file

@ -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