15 changed files with 196 additions and 51 deletions
@ -0,0 +1,47 @@ |
|||||||
|
import { ChangeEventHandler, useEffect, useRef } from "react"; |
||||||
|
|
||||||
|
export type SliderProps = { |
||||||
|
label?: string; |
||||||
|
min: number; |
||||||
|
max: number; |
||||||
|
step: number; |
||||||
|
value?: number; |
||||||
|
valueDisplay?: string; |
||||||
|
onChange: ChangeEventHandler<HTMLInputElement>; |
||||||
|
}; |
||||||
|
|
||||||
|
export function Slider(props: SliderProps) { |
||||||
|
const ref = useRef<HTMLInputElement>(null); |
||||||
|
useEffect(() => { |
||||||
|
const e = ref.current as HTMLInputElement; |
||||||
|
e.style.setProperty("--value", e.value); |
||||||
|
e.style.setProperty("--min", e.min === "" ? "0" : e.min); |
||||||
|
e.style.setProperty("--max", e.max === "" ? "100" : e.max); |
||||||
|
e.addEventListener("input", () => e.style.setProperty("--value", e.value)); |
||||||
|
}, [ref]); |
||||||
|
|
||||||
|
return ( |
||||||
|
<div className="mb-6 flex flex-row gap-4"> |
||||||
|
<div className="flex w-full flex-col gap-2"> |
||||||
|
{props.label ? ( |
||||||
|
<label className="font-bold">{props.label}</label> |
||||||
|
) : null} |
||||||
|
<input |
||||||
|
type="range" |
||||||
|
ref={ref} |
||||||
|
className="styled-slider slider-progress mt-[20px]" |
||||||
|
onChange={props.onChange} |
||||||
|
value={props.value} |
||||||
|
max={props.max} |
||||||
|
min={props.min} |
||||||
|
step={props.step} |
||||||
|
/> |
||||||
|
</div> |
||||||
|
<div className="mt-1 aspect-[2/1] h-8 rounded-sm bg-[#1C161B] pt-1"> |
||||||
|
<div className="text-center font-bold text-white"> |
||||||
|
{props.valueDisplay ?? props.value} |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
); |
||||||
|
} |
||||||
@ -0,0 +1,17 @@ |
|||||||
|
import { Icons } from "@/components/Icon"; |
||||||
|
import { useTranslation } from "react-i18next"; |
||||||
|
import { PopoutListAction } from "../../popouts/PopoutUtils"; |
||||||
|
|
||||||
|
interface Props { |
||||||
|
onClick: () => any; |
||||||
|
} |
||||||
|
|
||||||
|
export function PlaybackSpeedSelectionAction(props: Props) { |
||||||
|
const { t } = useTranslation(); |
||||||
|
|
||||||
|
return ( |
||||||
|
<PopoutListAction icon={Icons.TACHOMETER} onClick={props.onClick}> |
||||||
|
{t("videoPlayer.buttons.playbackSpeed")} |
||||||
|
</PopoutListAction> |
||||||
|
); |
||||||
|
} |
||||||
@ -0,0 +1,73 @@ |
|||||||
|
import { Icon, Icons } from "@/components/Icon"; |
||||||
|
import { FloatingCardView } from "@/components/popout/FloatingCard"; |
||||||
|
import { FloatingView } from "@/components/popout/FloatingView"; |
||||||
|
import { useFloatingRouter } from "@/hooks/useFloatingRouter"; |
||||||
|
import { useVideoPlayerDescriptor } from "@/video/state/hooks"; |
||||||
|
import { useControls } from "@/video/state/logic/controls"; |
||||||
|
import { useTranslation } from "react-i18next"; |
||||||
|
import { useMediaPlaying } from "@/video/state/logic/mediaplaying"; |
||||||
|
import { Slider } from "@/components/Slider"; |
||||||
|
import { PopoutListEntry, PopoutSection } from "./PopoutUtils"; |
||||||
|
|
||||||
|
const speedSelectionOptions = [0.25, 0.5, 0.75, 1, 1.25, 1.5, 2]; |
||||||
|
|
||||||
|
export function PlaybackSpeedPopout(props: { |
||||||
|
router: ReturnType<typeof useFloatingRouter>; |
||||||
|
prefix: string; |
||||||
|
}) { |
||||||
|
const { t } = useTranslation(); |
||||||
|
|
||||||
|
const descriptor = useVideoPlayerDescriptor(); |
||||||
|
const controls = useControls(descriptor); |
||||||
|
const mediaPlaying = useMediaPlaying(descriptor); |
||||||
|
|
||||||
|
return ( |
||||||
|
<FloatingView |
||||||
|
{...props.router.pageProps(props.prefix)} |
||||||
|
width={320} |
||||||
|
height={500} |
||||||
|
> |
||||||
|
<FloatingCardView.Header |
||||||
|
title={t("videoPlayer.popouts.playbackSpeed")} |
||||||
|
description={t("videoPlayer.popouts.descriptions.playbackSpeed")} |
||||||
|
goBack={() => props.router.navigate("/")} |
||||||
|
/> |
||||||
|
<FloatingCardView.Content noSection> |
||||||
|
<PopoutSection> |
||||||
|
{speedSelectionOptions.map((speed) => ( |
||||||
|
<PopoutListEntry |
||||||
|
key={speed} |
||||||
|
active={mediaPlaying.playbackSpeed === speed} |
||||||
|
onClick={() => { |
||||||
|
controls.setPlaybackSpeed(speed); |
||||||
|
controls.closePopout(); |
||||||
|
}} |
||||||
|
> |
||||||
|
{speed}x |
||||||
|
</PopoutListEntry> |
||||||
|
))} |
||||||
|
</PopoutSection> |
||||||
|
|
||||||
|
<p className="sticky top-0 z-10 flex items-center space-x-1 bg-ash-300 px-5 py-3 text-xs font-bold uppercase"> |
||||||
|
<Icon className="text-base" icon={Icons.TACHOMETER} /> |
||||||
|
<span>{t("videoPlayer.popouts.customPlaybackSpeed")}</span> |
||||||
|
</p> |
||||||
|
|
||||||
|
<PopoutSection className="pt-0"> |
||||||
|
<div> |
||||||
|
<Slider |
||||||
|
min={0.1} |
||||||
|
max={10} |
||||||
|
step={0.1} |
||||||
|
value={mediaPlaying.playbackSpeed} |
||||||
|
valueDisplay={`${mediaPlaying.playbackSpeed}x`} |
||||||
|
onChange={(e: { target: { valueAsNumber: number } }) => |
||||||
|
controls.setPlaybackSpeed(e.target.valueAsNumber) |
||||||
|
} |
||||||
|
/> |
||||||
|
</div> |
||||||
|
</PopoutSection> |
||||||
|
</FloatingCardView.Content> |
||||||
|
</FloatingView> |
||||||
|
); |
||||||
|
} |
||||||
Loading…
Reference in new issue