mirror of
https://github.com/movie-web/movie-web.git
synced 2024-12-24 04:31:51 +01:00
styling of video player controls
This commit is contained in:
parent
098f6af0ae
commit
024325f640
@ -14,6 +14,12 @@ export enum Icons {
|
||||
MOVIE_WEB = "movieWeb",
|
||||
DISCORD = "discord",
|
||||
GITHUB = "github",
|
||||
PLAY = "play",
|
||||
PAUSE = "pause",
|
||||
EXPAND = "expand",
|
||||
COMPRESS = "compress",
|
||||
VOLUME = "volume",
|
||||
VOLUME_X = "volume_x",
|
||||
}
|
||||
|
||||
export interface IconProps {
|
||||
@ -37,6 +43,12 @@ const iconList: Record<Icons, string> = {
|
||||
movieWeb: `<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 20.927 20.927"><path d="M18.186,4.5V6.241H16.445V4.5H9.482V6.241H7.741V4.5H6V20.168H7.741V18.427H9.482v1.741h6.964V18.427h1.741v1.741h1.741V4.5Zm-8.7,12.186H7.741V14.945H9.482Zm0-3.482H7.741V11.464H9.482Zm0-3.482H7.741V7.982H9.482Zm8.7,6.964H16.445V14.945h1.741Zm0-3.482H16.445V11.464h1.741Zm0-3.482H16.445V7.982h1.741Z" transform="translate(10.018 -7.425) rotate(45)" fill="currentColor"/></svg>`,
|
||||
discord: `<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 640 512"><!--! Font Awesome Pro 6.0.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --><path fill="currentColor" d="M524.531,69.836a1.5,1.5,0,0,0-.764-.7A485.065,485.065,0,0,0,404.081,32.03a1.816,1.816,0,0,0-1.923.91,337.461,337.461,0,0,0-14.9,30.6,447.848,447.848,0,0,0-134.426,0,309.541,309.541,0,0,0-15.135-30.6,1.89,1.89,0,0,0-1.924-.91A483.689,483.689,0,0,0,116.085,69.137a1.712,1.712,0,0,0-.788.676C39.068,183.651,18.186,294.69,28.43,404.354a2.016,2.016,0,0,0,.765,1.375A487.666,487.666,0,0,0,176.02,479.918a1.9,1.9,0,0,0,2.063-.676A348.2,348.2,0,0,0,208.12,430.4a1.86,1.86,0,0,0-1.019-2.588,321.173,321.173,0,0,1-45.868-21.853,1.885,1.885,0,0,1-.185-3.126c3.082-2.309,6.166-4.711,9.109-7.137a1.819,1.819,0,0,1,1.9-.256c96.229,43.917,200.41,43.917,295.5,0a1.812,1.812,0,0,1,1.924.233c2.944,2.426,6.027,4.851,9.132,7.16a1.884,1.884,0,0,1-.162,3.126,301.407,301.407,0,0,1-45.89,21.83,1.875,1.875,0,0,0-1,2.611,391.055,391.055,0,0,0,30.014,48.815,1.864,1.864,0,0,0,2.063.7A486.048,486.048,0,0,0,610.7,405.729a1.882,1.882,0,0,0,.765-1.352C623.729,277.594,590.933,167.465,524.531,69.836ZM222.491,337.58c-28.972,0-52.844-26.587-52.844-59.239S193.056,219.1,222.491,219.1c29.665,0,53.306,26.82,52.843,59.239C275.334,310.993,251.924,337.58,222.491,337.58Zm195.38,0c-28.971,0-52.843-26.587-52.843-59.239S388.437,219.1,417.871,219.1c29.667,0,53.307,26.82,52.844,59.239C470.715,310.993,447.538,337.58,417.871,337.58Z"/></svg>`,
|
||||
github: `<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 496 512"><!--! Font Awesome Pro 6.0.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --><path fill="currentColor" d="M165.9 397.4c0 2-2.3 3.6-5.2 3.6-3.3.3-5.6-1.3-5.6-3.6 0-2 2.3-3.6 5.2-3.6 3-.3 5.6 1.3 5.6 3.6zm-31.1-4.5c-.7 2 1.3 4.3 4.3 4.9 2.6 1 5.6 0 6.2-2s-1.3-4.3-4.3-5.2c-2.6-.7-5.5.3-6.2 2.3zm44.2-1.7c-2.9.7-4.9 2.6-4.6 4.9.3 2 2.9 3.3 5.9 2.6 2.9-.7 4.9-2.6 4.6-4.6-.3-1.9-3-3.2-5.9-2.9zM244.8 8C106.1 8 0 113.3 0 252c0 110.9 69.8 205.8 169.5 239.2 12.8 2.3 17.3-5.6 17.3-12.1 0-6.2-.3-40.4-.3-61.4 0 0-70 15-84.7-29.8 0 0-11.4-29.1-27.8-36.6 0 0-22.9-15.7 1.6-15.4 0 0 24.9 2 38.6 25.8 21.9 38.6 58.6 27.5 72.9 20.9 2.3-16 8.8-27.1 16-33.7-55.9-6.2-112.3-14.3-112.3-110.5 0-27.5 7.6-41.3 23.6-58.9-2.6-6.5-11.1-33.3 2.6-67.9 20.9-6.5 69 27 69 27 20-5.6 41.5-8.5 62.8-8.5s42.8 2.9 62.8 8.5c0 0 48.1-33.6 69-27 13.7 34.7 5.2 61.4 2.6 67.9 16 17.7 25.8 31.5 25.8 58.9 0 96.5-58.9 104.2-114.8 110.5 9.2 7.9 17 22.9 17 46.4 0 33.7-.3 75.4-.3 83.6 0 6.5 4.6 14.4 17.3 12.1C428.2 457.8 496 362.9 496 252 496 113.3 383.5 8 244.8 8zM97.2 352.9c-1.3 1-1 3.3.7 5.2 1.6 1.6 3.9 2.3 5.2 1 1.3-1 1-3.3-.7-5.2-1.6-1.6-3.9-2.3-5.2-1zm-10.8-8.1c-.7 1.3.3 2.9 2.3 3.9 1.6 1 3.6.7 4.3-.7.7-1.3-.3-2.9-2.3-3.9-2-.6-3.6-.3-4.3.7zm32.4 35.6c-1.6 1.3-1 4.3 1.3 6.2 2.3 2.3 5.2 2.6 6.5 1 1.3-1.3.7-4.3-1.3-6.2-2.2-2.3-5.2-2.6-6.5-1zm-11.4-14.7c-1.6 1-1.6 3.6 0 5.9 1.6 2.3 4.3 3.3 5.6 2.3 1.6-1.3 1.6-3.9 0-6.2-1.4-2.3-4-3.3-5.6-2z"/></svg>`,
|
||||
play: `<svg xmlns="http://www.w3.org/2000/svg" width="1em" style="transform: translateX(5%)" height="1em" viewBox="0 0 384 512"><!--! Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --><path fill="currentColor" d="M73 39c-14.8-9.1-33.4-9.4-48.5-.9S0 62.6 0 80V432c0 17.4 9.4 33.4 24.5 41.9s33.7 8.1 48.5-.9L361 297c14.3-8.7 23-24.2 23-41s-8.7-32.2-23-41L73 39z"/></svg>`,
|
||||
pause: `<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 320 512"><!--! Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --><path fill="currentColor" d="M48 64C21.5 64 0 85.5 0 112V400c0 26.5 21.5 48 48 48H80c26.5 0 48-21.5 48-48V112c0-26.5-21.5-48-48-48H48zm192 0c-26.5 0-48 21.5-48 48V400c0 26.5 21.5 48 48 48h32c26.5 0 48-21.5 48-48V112c0-26.5-21.5-48-48-48H240z"/></svg>`,
|
||||
expand: `<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 448 512"><!--! Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --><path fill="currentColor" d="M32 32C14.3 32 0 46.3 0 64v96c0 17.7 14.3 32 32 32s32-14.3 32-32V96h64c17.7 0 32-14.3 32-32s-14.3-32-32-32H32zM64 352c0-17.7-14.3-32-32-32s-32 14.3-32 32v96c0 17.7 14.3 32 32 32h96c17.7 0 32-14.3 32-32s-14.3-32-32-32H64V352zM320 32c-17.7 0-32 14.3-32 32s14.3 32 32 32h64v64c0 17.7 14.3 32 32 32s32-14.3 32-32V64c0-17.7-14.3-32-32-32H320zM448 352c0-17.7-14.3-32-32-32s-32 14.3-32 32v64H320c-17.7 0-32 14.3-32 32s14.3 32 32 32h96c17.7 0 32-14.3 32-32V352z"/></svg>`,
|
||||
compress: `<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 448 512"><!--! Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --><path fill="currentColor" d="M160 64c0-17.7-14.3-32-32-32s-32 14.3-32 32v64H32c-17.7 0-32 14.3-32 32s14.3 32 32 32h96c17.7 0 32-14.3 32-32V64zM32 320c-17.7 0-32 14.3-32 32s14.3 32 32 32H96v64c0 17.7 14.3 32 32 32s32-14.3 32-32V352c0-17.7-14.3-32-32-32H32zM352 64c0-17.7-14.3-32-32-32s-32 14.3-32 32v96c0 17.7 14.3 32 32 32h96c17.7 0 32-14.3 32-32s-14.3-32-32-32H352V64zM320 320c-17.7 0-32 14.3-32 32v96c0 17.7 14.3 32 32 32s32-14.3 32-32V384h64c17.7 0 32-14.3 32-32s-14.3-32-32-32H320z"/></svg>`,
|
||||
volume: `<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 640 512"><!--! Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --><path fill="currentColor" d="M533.6 32.5C598.5 85.3 640 165.8 640 256s-41.5 170.8-106.4 223.5c-10.3 8.4-25.4 6.8-33.8-3.5s-6.8-25.4 3.5-33.8C557.5 398.2 592 331.2 592 256s-34.5-142.2-88.7-186.3c-10.3-8.4-11.8-23.5-3.5-33.8s23.5-11.8 33.8-3.5zM473.1 107c43.2 35.2 70.9 88.9 70.9 149s-27.7 113.8-70.9 149c-10.3 8.4-25.4 6.8-33.8-3.5s-6.8-25.4 3.5-33.8C475.3 341.3 496 301.1 496 256s-20.7-85.3-53.2-111.8c-10.3-8.4-11.8-23.5-3.5-33.8s23.5-11.8 33.8-3.5zm-60.5 74.5C434.1 199.1 448 225.9 448 256s-13.9 56.9-35.4 74.5c-10.3 8.4-25.4 6.8-33.8-3.5s-6.8-25.4 3.5-33.8C393.1 284.4 400 271 400 256s-6.9-28.4-17.7-37.3c-10.3-8.4-11.8-23.5-3.5-33.8s23.5-11.8 33.8-3.5zM301.1 34.8C312.6 40 320 51.4 320 64V448c0 12.6-7.4 24-18.9 29.2s-25 3.1-34.4-5.3L131.8 352H64c-35.3 0-64-28.7-64-64V224c0-35.3 28.7-64 64-64h67.8L266.7 40.1c9.4-8.4 22.9-10.4 34.4-5.3z"/></svg>`,
|
||||
volume_x: `<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 576 512"><!--! Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --><path fill="currentColor" d="M301.1 34.8C312.6 40 320 51.4 320 64V448c0 12.6-7.4 24-18.9 29.2s-25 3.1-34.4-5.3L131.8 352H64c-35.3 0-64-28.7-64-64V224c0-35.3 28.7-64 64-64h67.8L266.7 40.1c9.4-8.4 22.9-10.4 34.4-5.3zM425 167l55 55 55-55c9.4-9.4 24.6-9.4 33.9 0s9.4 24.6 0 33.9l-55 55 55 55c9.4 9.4 9.4 24.6 0 33.9s-24.6 9.4-33.9 0l-55-55-55 55c-9.4 9.4-24.6 9.4-33.9 0s-9.4-24.6 0-33.9l55-55-55-55c-9.4-9.4-9.4-24.6 0-33.9s24.6-9.4 33.9 0z"/></svg>`,
|
||||
};
|
||||
|
||||
export function Icon(props: IconProps) {
|
||||
|
@ -11,12 +11,19 @@ export function DecoratedVideoPlayer(props: VideoPlayerProps) {
|
||||
return (
|
||||
<VideoPlayer autoPlay={props.autoPlay}>
|
||||
<BackdropControl>
|
||||
<PauseControl />
|
||||
<FullscreenControl />
|
||||
<ProgressControl />
|
||||
<VolumeControl />
|
||||
<LoadingControl />
|
||||
<TimeControl />
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<LoadingControl />
|
||||
</div>
|
||||
<div className="pointer-events-auto absolute inset-x-0 bottom-0 mb-4 flex flex-col px-6">
|
||||
<ProgressControl />
|
||||
<div className="flex items-center">
|
||||
<PauseControl />
|
||||
<VolumeControl className="mr-2" />
|
||||
<TimeControl />
|
||||
<div className="flex-1" />
|
||||
<FullscreenControl />
|
||||
</div>
|
||||
</div>
|
||||
</BackdropControl>
|
||||
{props.children}
|
||||
</VideoPlayer>
|
||||
|
@ -31,7 +31,7 @@ export function VideoPlayer(props: VideoPlayerProps) {
|
||||
return (
|
||||
<VideoPlayerContextProvider player={playerRef} wrapper={playerWrapperRef}>
|
||||
<div
|
||||
className="relative aspect-video w-full bg-black"
|
||||
className="relative aspect-video w-full select-none bg-black"
|
||||
ref={playerWrapperRef}
|
||||
>
|
||||
<VideoPlayerInternals
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { useCallback, useRef, useState } from "react";
|
||||
import React, { useCallback, useRef, useState } from "react";
|
||||
import { useVideoPlayerState } from "../VideoContext";
|
||||
|
||||
interface BackdropControlProps {
|
||||
@ -9,6 +9,7 @@ export function BackdropControl(props: BackdropControlProps) {
|
||||
const { videoState } = useVideoPlayerState();
|
||||
const [moved, setMoved] = useState(false);
|
||||
const timeout = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const clickareaRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const handleMouseMove = useCallback(() => {
|
||||
setMoved(true);
|
||||
@ -19,10 +20,19 @@ export function BackdropControl(props: BackdropControlProps) {
|
||||
}, 3000);
|
||||
}, [timeout, setMoved]);
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
if (videoState.isPlaying) videoState.pause();
|
||||
else videoState.play();
|
||||
}, [videoState]);
|
||||
const handleMouseLeave = useCallback(() => {
|
||||
setMoved(false);
|
||||
}, [setMoved]);
|
||||
|
||||
const handleClick = useCallback(
|
||||
(e: React.MouseEvent<HTMLDivElement>) => {
|
||||
if (!clickareaRef.current || clickareaRef.current !== e.target) return;
|
||||
|
||||
if (videoState.isPlaying) videoState.pause();
|
||||
else videoState.play();
|
||||
},
|
||||
[videoState, clickareaRef]
|
||||
);
|
||||
|
||||
const showUI = moved || videoState.isPaused;
|
||||
|
||||
@ -30,24 +40,28 @@ export function BackdropControl(props: BackdropControlProps) {
|
||||
<div
|
||||
className={`absolute inset-0 ${!showUI ? "cursor-none" : ""}`}
|
||||
onMouseMove={handleMouseMove}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
ref={clickareaRef}
|
||||
onClick={handleClick}
|
||||
>
|
||||
<div
|
||||
className={`absolute inset-0 bg-black bg-opacity-20 transition-opacity duration-200 ${
|
||||
className={`pointer-events-none absolute inset-0 bg-black bg-opacity-20 transition-opacity duration-200 ${
|
||||
!showUI ? "!opacity-0" : ""
|
||||
}`}
|
||||
/>
|
||||
<div
|
||||
className={`absolute inset-x-0 bottom-0 h-[30%] bg-gradient-to-t from-black to-transparent opacity-75 transition-opacity duration-200 ${
|
||||
className={`pointer-events-none absolute inset-x-0 bottom-0 h-[30%] bg-gradient-to-t from-black to-transparent opacity-75 transition-opacity duration-200 ${
|
||||
!showUI ? "!opacity-0" : ""
|
||||
}`}
|
||||
/>
|
||||
<div
|
||||
className={`absolute inset-x-0 top-0 h-[30%] bg-gradient-to-b from-black to-transparent opacity-75 transition-opacity duration-200 ${
|
||||
className={`pointer-events-none absolute inset-x-0 top-0 h-[30%] bg-gradient-to-b from-black to-transparent opacity-75 transition-opacity duration-200 ${
|
||||
!showUI ? "!opacity-0" : ""
|
||||
}`}
|
||||
/>
|
||||
<div className="absolute inset-0">{showUI ? props.children : null}</div>
|
||||
<div className="pointer-events-none absolute inset-0">
|
||||
{showUI ? props.children : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -1,9 +1,15 @@
|
||||
import { Icons } from "@/components/Icon";
|
||||
import { useCallback } from "react";
|
||||
import { VideoPlayerIconButton } from "../parts/VideoPlayerIconButton";
|
||||
import { useVideoPlayerState } from "../VideoContext";
|
||||
|
||||
const canFullscreen = document.fullscreenEnabled;
|
||||
|
||||
export function FullscreenControl() {
|
||||
interface Props {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function FullscreenControl(props: Props) {
|
||||
const { videoState } = useVideoPlayerState();
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
@ -13,16 +19,11 @@ export function FullscreenControl() {
|
||||
|
||||
if (!canFullscreen) return null;
|
||||
|
||||
let text = "not fullscreen";
|
||||
if (videoState.isFullscreen) text = "in fullscreen";
|
||||
|
||||
return (
|
||||
<button
|
||||
className="m-1 rounded bg-denim-100 p-1 text-white hover:opacity-75"
|
||||
type="button"
|
||||
<VideoPlayerIconButton
|
||||
className={props.className}
|
||||
onClick={handleClick}
|
||||
>
|
||||
{text}
|
||||
</button>
|
||||
icon={videoState.isFullscreen ? Icons.COMPRESS : Icons.EXPAND}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -1,7 +1,13 @@
|
||||
import { Icons } from "@/components/Icon";
|
||||
import { useCallback } from "react";
|
||||
import { VideoPlayerIconButton } from "../parts/VideoPlayerIconButton";
|
||||
import { useVideoPlayerState } from "../VideoContext";
|
||||
|
||||
export function PauseControl() {
|
||||
interface Props {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function PauseControl(props: Props) {
|
||||
const { videoState } = useVideoPlayerState();
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
@ -9,16 +15,14 @@ export function PauseControl() {
|
||||
else videoState.play();
|
||||
}, [videoState]);
|
||||
|
||||
const text =
|
||||
videoState.isPlaying || videoState.isSeeking ? "playing" : "paused";
|
||||
const icon =
|
||||
videoState.isPlaying || videoState.isSeeking ? Icons.PAUSE : Icons.PLAY;
|
||||
|
||||
return (
|
||||
<button
|
||||
className="m-1 rounded bg-denim-100 p-1 text-white hover:opacity-75"
|
||||
type="button"
|
||||
<VideoPlayerIconButton
|
||||
className={props.className}
|
||||
icon={icon}
|
||||
onClick={handleClick}
|
||||
>
|
||||
{text}
|
||||
</button>
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -1,74 +1,68 @@
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import {
|
||||
makePercentage,
|
||||
makePercentageString,
|
||||
useProgressBar,
|
||||
} from "@/hooks/useProgressBar";
|
||||
import { useCallback, useRef } from "react";
|
||||
import { useVideoPlayerState } from "../VideoContext";
|
||||
|
||||
export function ProgressControl() {
|
||||
const { videoState } = useVideoPlayerState();
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const [mouseDown, setMouseDown] = useState<boolean>(false);
|
||||
const [progress, setProgress] = useState<number>(0);
|
||||
|
||||
let watchProgress = `${(
|
||||
(videoState.time / videoState.duration) *
|
||||
100
|
||||
).toFixed(2)}%`;
|
||||
if (mouseDown) watchProgress = `${progress}%`;
|
||||
const commitTime = useCallback(
|
||||
(percentage) => {
|
||||
videoState.setTime(percentage * videoState.duration);
|
||||
},
|
||||
[videoState]
|
||||
);
|
||||
const { dragging, dragPercentage, dragMouseDown } = useProgressBar(
|
||||
ref,
|
||||
commitTime
|
||||
);
|
||||
|
||||
const bufferProgress = `${(
|
||||
(videoState.buffered / videoState.duration) *
|
||||
100
|
||||
).toFixed(2)}%`;
|
||||
let watchProgress = makePercentageString(
|
||||
makePercentage((videoState.time / videoState.duration) * 100)
|
||||
);
|
||||
if (dragging)
|
||||
watchProgress = makePercentageString(makePercentage(dragPercentage));
|
||||
|
||||
useEffect(() => {
|
||||
function mouseMove(ev: MouseEvent) {
|
||||
if (!mouseDown || !ref.current) return;
|
||||
const rect = ref.current.getBoundingClientRect();
|
||||
const pos = ((ev.pageX - rect.left) / ref.current.offsetWidth) * 100;
|
||||
setProgress(pos);
|
||||
}
|
||||
|
||||
function mouseUp(ev: MouseEvent) {
|
||||
if (!mouseDown) return;
|
||||
setMouseDown(false);
|
||||
document.body.removeAttribute("data-no-select");
|
||||
|
||||
if (!ref.current) return;
|
||||
const rect = ref.current.getBoundingClientRect();
|
||||
const pos = (ev.pageX - rect.left) / ref.current.offsetWidth;
|
||||
videoState.setTime(pos * videoState.duration);
|
||||
}
|
||||
|
||||
document.addEventListener("mousemove", mouseMove);
|
||||
document.addEventListener("mouseup", mouseUp);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("mousemove", mouseMove);
|
||||
document.removeEventListener("mouseup", mouseUp);
|
||||
};
|
||||
}, [mouseDown, videoState]);
|
||||
|
||||
const handleMouseDown = useCallback(() => {
|
||||
setMouseDown(true);
|
||||
document.body.setAttribute("data-no-select", "true");
|
||||
}, []);
|
||||
const bufferProgress = makePercentageString(
|
||||
makePercentage((videoState.buffered / videoState.duration) * 100)
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className="relative m-1 my-4 h-4 w-48 overflow-hidden rounded-full border border-white bg-denim-100"
|
||||
onMouseDown={handleMouseDown}
|
||||
>
|
||||
<div className="group pointer-events-auto cursor-pointer rounded-full px-2">
|
||||
<div
|
||||
className="absolute inset-y-0 left-0 bg-denim-700 opacity-50"
|
||||
style={{
|
||||
width: bufferProgress,
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className="absolute inset-y-0 left-0 bg-denim-700"
|
||||
style={{
|
||||
width: watchProgress,
|
||||
}}
|
||||
/>
|
||||
ref={ref}
|
||||
className="-my-3 flex h-8 items-center"
|
||||
onMouseDown={dragMouseDown}
|
||||
>
|
||||
<div
|
||||
className={`relative h-1 flex-1 rounded-full bg-gray-500 bg-opacity-50 transition-[height] duration-100 group-hover:h-2 ${
|
||||
dragging ? "!h-2" : ""
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className="absolute inset-y-0 left-0 flex items-center justify-end rounded-full bg-gray-300 bg-opacity-50"
|
||||
style={{
|
||||
width: bufferProgress,
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className="absolute inset-y-0 left-0 flex items-center justify-end rounded-full bg-bink-500"
|
||||
style={{
|
||||
width: watchProgress,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={`absolute h-1 w-1 translate-x-1/2 rounded-full bg-white opacity-0 transition-[transform,opacity] group-hover:scale-[400%] group-hover:opacity-100 ${
|
||||
dragging ? "!scale-[400%] !opacity-100" : ""
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -22,15 +22,21 @@ function formatSeconds(secs: number, showHours = false): string {
|
||||
return `${Math.round(hours).toString()}:${minuteString}`;
|
||||
}
|
||||
|
||||
export function TimeControl() {
|
||||
interface Props {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function TimeControl(props: Props) {
|
||||
const { videoState } = useVideoPlayerState();
|
||||
const hasHours = durationExceedsHour(videoState.duration);
|
||||
const time = formatSeconds(videoState.time, hasHours);
|
||||
const duration = formatSeconds(videoState.duration, hasHours);
|
||||
|
||||
return (
|
||||
<p>
|
||||
{time} / {duration}
|
||||
</p>
|
||||
<div className={props.className}>
|
||||
<p className="select-none text-white">
|
||||
{time} / {duration}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -1,34 +1,86 @@
|
||||
import { useCallback, useRef } from "react";
|
||||
import { Icon, Icons } from "@/components/Icon";
|
||||
import {
|
||||
makePercentage,
|
||||
makePercentageString,
|
||||
useProgressBar,
|
||||
} from "@/hooks/useProgressBar";
|
||||
import { useCallback, useRef, useState } from "react";
|
||||
import { useVideoPlayerState } from "../VideoContext";
|
||||
|
||||
export function VolumeControl() {
|
||||
interface Props {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
// TODO make hoveredOnce false when control bar appears
|
||||
|
||||
export function VolumeControl(props: Props) {
|
||||
const { videoState } = useVideoPlayerState();
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const [storedVolume, setStoredVolume] = useState(1);
|
||||
const [hoveredOnce, setHoveredOnce] = useState(false);
|
||||
|
||||
const percentage = `${(videoState.volume * 100).toFixed(2)}%`;
|
||||
|
||||
const handleClick = useCallback(
|
||||
(e: React.MouseEvent<HTMLElement>) => {
|
||||
if (!ref.current) return;
|
||||
const rect = ref.current.getBoundingClientRect();
|
||||
const pos = (e.pageX - rect.left) / ref.current.offsetWidth;
|
||||
videoState.setVolume(pos);
|
||||
const commitVolume = useCallback(
|
||||
(percentage) => {
|
||||
videoState.setVolume(percentage);
|
||||
setStoredVolume(percentage);
|
||||
},
|
||||
[videoState, ref]
|
||||
[videoState, setStoredVolume]
|
||||
);
|
||||
const { dragging, dragPercentage, dragMouseDown } = useProgressBar(
|
||||
ref,
|
||||
commitVolume,
|
||||
true
|
||||
);
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
if (videoState.volume > 0) {
|
||||
videoState.setVolume(0);
|
||||
setStoredVolume(videoState.volume);
|
||||
} else {
|
||||
videoState.setVolume(storedVolume > 0 ? storedVolume : 1);
|
||||
}
|
||||
}, [videoState, setStoredVolume, storedVolume]);
|
||||
|
||||
const handleMouseEnter = useCallback(() => {
|
||||
setHoveredOnce(true);
|
||||
}, [setHoveredOnce]);
|
||||
|
||||
let percentage = makePercentage(videoState.volume * 100);
|
||||
if (dragging) percentage = makePercentage(dragPercentage);
|
||||
const percentageString = makePercentageString(percentage);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className="relative m-1 my-4 h-4 w-48 overflow-hidden rounded-full border border-white bg-bink-300"
|
||||
onClick={handleClick}
|
||||
>
|
||||
<div className={props.className}>
|
||||
<div
|
||||
className="absolute inset-y-0 left-0 bg-bink-700"
|
||||
style={{
|
||||
width: percentage,
|
||||
}}
|
||||
/>
|
||||
className="pointer-events-auto flex cursor-pointer items-center"
|
||||
onMouseEnter={handleMouseEnter}
|
||||
>
|
||||
<div className="px-4 text-2xl text-white" onClick={handleClick}>
|
||||
<Icon icon={percentage > 0 ? Icons.VOLUME : Icons.VOLUME_X} />
|
||||
</div>
|
||||
<div
|
||||
className={`-ml-2 w-0 overflow-hidden transition-[width,opacity] duration-300 ease-in ${
|
||||
hoveredOnce ? "!w-20 opacity-100" : "w-4 opacity-0"
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
ref={ref}
|
||||
className="flex h-10 w-16 items-center px-2"
|
||||
onMouseDown={dragMouseDown}
|
||||
>
|
||||
<div className="relative h-1 flex-1 rounded-full bg-gray-500 bg-opacity-50">
|
||||
<div
|
||||
className="absolute inset-y-0 left-0 flex items-center justify-end rounded-full bg-bink-500"
|
||||
style={{
|
||||
width: percentageString,
|
||||
}}
|
||||
>
|
||||
<div className="absolute h-3 w-3 translate-x-1/2 rounded-full bg-white" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
26
src/components/video/parts/VideoPlayerIconButton.tsx
Normal file
26
src/components/video/parts/VideoPlayerIconButton.tsx
Normal file
@ -0,0 +1,26 @@
|
||||
import { Icon, Icons } from "@/components/Icon";
|
||||
import React from "react";
|
||||
|
||||
export interface VideoPlayerIconButtonProps {
|
||||
onClick?: (e: React.MouseEvent<HTMLButtonElement>) => void;
|
||||
icon: Icons;
|
||||
text?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function VideoPlayerIconButton(props: VideoPlayerIconButtonProps) {
|
||||
return (
|
||||
<div className={props.className}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={props.onClick}
|
||||
className="group pointer-events-auto p-2 text-white transition-transform duration-100 active:scale-110"
|
||||
>
|
||||
<div className="flex items-center justify-center rounded-full bg-white bg-opacity-0 p-2 transition-colors duration-100 group-hover:bg-opacity-20">
|
||||
<Icon icon={props.icon} className="text-2xl" />
|
||||
{props.text ? <span className="ml-2">{props.text}</span> : null}
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
66
src/hooks/useProgressBar.ts
Normal file
66
src/hooks/useProgressBar.ts
Normal file
@ -0,0 +1,66 @@
|
||||
import React, { RefObject, useCallback, useEffect, useState } from "react";
|
||||
|
||||
export function makePercentageString(num: number) {
|
||||
return `${num.toFixed(2)}%`;
|
||||
}
|
||||
|
||||
export function makePercentage(num: number) {
|
||||
return Number(Math.max(0, Math.min(num, 100)).toFixed(2));
|
||||
}
|
||||
|
||||
export function useProgressBar(
|
||||
barRef: RefObject<HTMLElement>,
|
||||
commit: (percentage: number) => void,
|
||||
commitImmediately = false
|
||||
) {
|
||||
const [mouseDown, setMouseDown] = useState<boolean>(false);
|
||||
const [progress, setProgress] = useState<number>(0);
|
||||
|
||||
useEffect(() => {
|
||||
function mouseMove(ev: MouseEvent) {
|
||||
if (!mouseDown || !barRef.current) return;
|
||||
const rect = barRef.current.getBoundingClientRect();
|
||||
const pos = ((ev.pageX - rect.left) / barRef.current.offsetWidth) * 100;
|
||||
setProgress(pos);
|
||||
if (commitImmediately) commit(pos);
|
||||
}
|
||||
|
||||
function mouseUp(ev: MouseEvent) {
|
||||
if (!mouseDown) return;
|
||||
setMouseDown(false);
|
||||
document.body.removeAttribute("data-no-select");
|
||||
|
||||
if (!barRef.current) return;
|
||||
const rect = barRef.current.getBoundingClientRect();
|
||||
const pos = (ev.pageX - rect.left) / barRef.current.offsetWidth;
|
||||
commit(pos);
|
||||
}
|
||||
|
||||
document.addEventListener("mousemove", mouseMove);
|
||||
document.addEventListener("mouseup", mouseUp);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("mousemove", mouseMove);
|
||||
document.removeEventListener("mouseup", mouseUp);
|
||||
};
|
||||
}, [mouseDown, barRef, commit, commitImmediately]);
|
||||
|
||||
const dragMouseDown = useCallback(
|
||||
(ev: React.MouseEvent<HTMLElement>) => {
|
||||
setMouseDown(true);
|
||||
document.body.setAttribute("data-no-select", "true");
|
||||
|
||||
if (!barRef.current) return;
|
||||
const rect = barRef.current.getBoundingClientRect();
|
||||
const pos = ((ev.pageX - rect.left) / barRef.current.offsetWidth) * 100;
|
||||
setProgress(pos);
|
||||
},
|
||||
[setProgress, barRef]
|
||||
);
|
||||
|
||||
return {
|
||||
dragging: mouseDown,
|
||||
dragPercentage: progress,
|
||||
dragMouseDown,
|
||||
};
|
||||
}
|
Loading…
Reference in New Issue
Block a user