20 changed files with 384 additions and 29 deletions
@ -0,0 +1,43 @@ |
|||||||
|
import { Icon, Icons } from "@/components/Icon"; |
||||||
|
import { Transition } from "@/components/Transition"; |
||||||
|
import { Flare } from "@/components/utils/Flare"; |
||||||
|
import { usePlayerStore } from "@/stores/player/store"; |
||||||
|
import { useEmpheralVolumeStore } from "@/stores/volume"; |
||||||
|
|
||||||
|
export function VolumeChangedPopout() { |
||||||
|
const empheralVolume = useEmpheralVolumeStore(); |
||||||
|
|
||||||
|
const volume = usePlayerStore((s) => s.mediaPlaying.volume); |
||||||
|
|
||||||
|
return ( |
||||||
|
<Transition |
||||||
|
animation="slide-down" |
||||||
|
show={empheralVolume.showVolume} |
||||||
|
className="absolute inset-x-0 top-4 flex justify-center" |
||||||
|
> |
||||||
|
<Flare.Base className="hover:flare-enabled bg-video-context-background pl-4 pr-6 py-3 group w-72 h-full rounded-lg transition-colors text-video-context-type-main"> |
||||||
|
<Flare.Light |
||||||
|
enabled |
||||||
|
flareSize={200} |
||||||
|
cssColorVar="--colors-video-context-light" |
||||||
|
backgroundClass="bg-video-context-background duration-100" |
||||||
|
className="rounded-lg" |
||||||
|
/> |
||||||
|
<Flare.Child className="grid grid-cols-[auto,1fr] gap-3 pointer-events-auto relative transition-transform"> |
||||||
|
<Icon |
||||||
|
className="text-2xl" |
||||||
|
icon={volume > 0 ? Icons.VOLUME : Icons.VOLUME_X} |
||||||
|
/> |
||||||
|
<div className="w-full flex items-center"> |
||||||
|
<div className="w-full h-1.5 rounded-full bg-video-context-slider bg-opacity-25"> |
||||||
|
<div |
||||||
|
className="h-full bg-video-context-sliderFilled rounded-full transition-[width] duration-100" |
||||||
|
style={{ width: `${volume * 100}%` }} |
||||||
|
/> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</Flare.Child> |
||||||
|
</Flare.Base> |
||||||
|
</Transition> |
||||||
|
); |
||||||
|
} |
@ -0,0 +1,67 @@ |
|||||||
|
import classNames from "classnames"; |
||||||
|
import { useCallback } from "react"; |
||||||
|
|
||||||
|
import { Context } from "@/components/player/internals/ContextUtils"; |
||||||
|
import { useOverlayRouter } from "@/hooks/useOverlayRouter"; |
||||||
|
import { usePlayerStore } from "@/stores/player/store"; |
||||||
|
|
||||||
|
function ButtonList(props: { |
||||||
|
options: number[]; |
||||||
|
selected: number; |
||||||
|
onClick: (v: any) => void; |
||||||
|
}) { |
||||||
|
return ( |
||||||
|
<div className="flex items-center bg-video-context-buttons-list p-1 rounded-lg"> |
||||||
|
{props.options.map((option) => { |
||||||
|
return ( |
||||||
|
<button |
||||||
|
type="button" |
||||||
|
className={classNames( |
||||||
|
"w-full px-2 py-1 rounded-md", |
||||||
|
props.selected === option |
||||||
|
? "bg-video-context-buttons-active text-white" |
||||||
|
: null |
||||||
|
)} |
||||||
|
onClick={() => props.onClick(option)} |
||||||
|
key={option} |
||||||
|
> |
||||||
|
{option}x |
||||||
|
</button> |
||||||
|
); |
||||||
|
})} |
||||||
|
</div> |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
export function PlaybackSettingsView({ id }: { id: string }) { |
||||||
|
const router = useOverlayRouter(id); |
||||||
|
const playbackRate = usePlayerStore((s) => s.mediaPlaying.playbackRate); |
||||||
|
const display = usePlayerStore((s) => s.display); |
||||||
|
|
||||||
|
const setPlaybackRate = useCallback( |
||||||
|
(v: number) => { |
||||||
|
display?.setPlaybackRate(v); |
||||||
|
}, |
||||||
|
[display] |
||||||
|
); |
||||||
|
|
||||||
|
const options = [0.25, 0.5, 1, 1.25, 2]; |
||||||
|
|
||||||
|
return ( |
||||||
|
<> |
||||||
|
<Context.BackLink onClick={() => router.navigate("/")}> |
||||||
|
Playback settings |
||||||
|
</Context.BackLink> |
||||||
|
<Context.Section> |
||||||
|
<div className="space-y-4 mt-3"> |
||||||
|
<Context.FieldTitle>Playback speed</Context.FieldTitle> |
||||||
|
<ButtonList |
||||||
|
options={options} |
||||||
|
selected={playbackRate} |
||||||
|
onClick={setPlaybackRate} |
||||||
|
/> |
||||||
|
</div> |
||||||
|
</Context.Section> |
||||||
|
</> |
||||||
|
); |
||||||
|
} |
@ -0,0 +1,109 @@ |
|||||||
|
import { useEffect, useRef, useState } from "react"; |
||||||
|
|
||||||
|
import { useVolume } from "@/components/player/hooks/useVolume"; |
||||||
|
import { usePlayerStore } from "@/stores/player/store"; |
||||||
|
import { useEmpheralVolumeStore } from "@/stores/volume"; |
||||||
|
|
||||||
|
export function KeyboardEvents() { |
||||||
|
const display = usePlayerStore((s) => s.display); |
||||||
|
const mediaPlaying = usePlayerStore((s) => s.mediaPlaying); |
||||||
|
const time = usePlayerStore((s) => s.progress.time); |
||||||
|
const { setVolume, toggleMute } = useVolume(); |
||||||
|
|
||||||
|
const setShowVolume = useEmpheralVolumeStore((s) => s.setShowVolume); |
||||||
|
|
||||||
|
const [isRolling, setIsRolling] = useState(false); |
||||||
|
const volumeDebounce = useRef<ReturnType<typeof setTimeout> | undefined>(); |
||||||
|
|
||||||
|
const dataRef = useRef({ |
||||||
|
setShowVolume, |
||||||
|
setVolume, |
||||||
|
toggleMute, |
||||||
|
setIsRolling, |
||||||
|
display, |
||||||
|
mediaPlaying, |
||||||
|
isRolling, |
||||||
|
time, |
||||||
|
}); |
||||||
|
useEffect(() => { |
||||||
|
dataRef.current = { |
||||||
|
setShowVolume, |
||||||
|
setVolume, |
||||||
|
toggleMute, |
||||||
|
setIsRolling, |
||||||
|
display, |
||||||
|
mediaPlaying, |
||||||
|
isRolling, |
||||||
|
time, |
||||||
|
}; |
||||||
|
}, [ |
||||||
|
setShowVolume, |
||||||
|
setVolume, |
||||||
|
toggleMute, |
||||||
|
setIsRolling, |
||||||
|
display, |
||||||
|
mediaPlaying, |
||||||
|
isRolling, |
||||||
|
time, |
||||||
|
]); |
||||||
|
|
||||||
|
useEffect(() => { |
||||||
|
const keyEventHandler = (evt: KeyboardEvent) => { |
||||||
|
const k = evt.key; |
||||||
|
|
||||||
|
// Volume
|
||||||
|
if (["ArrowUp", "ArrowDown", "m"].includes(k)) { |
||||||
|
dataRef.current.setShowVolume(true); |
||||||
|
|
||||||
|
if (volumeDebounce.current) clearTimeout(volumeDebounce.current); |
||||||
|
volumeDebounce.current = setTimeout(() => { |
||||||
|
dataRef.current.setShowVolume(false); |
||||||
|
}, 3e3); |
||||||
|
} |
||||||
|
if (k === "ArrowUp") |
||||||
|
dataRef.current.setVolume( |
||||||
|
(dataRef.current.mediaPlaying?.volume || 0) + 0.15 |
||||||
|
); |
||||||
|
if (k === "ArrowDown") |
||||||
|
dataRef.current.setVolume( |
||||||
|
(dataRef.current.mediaPlaying?.volume || 0) - 0.15 |
||||||
|
); |
||||||
|
if (k === "m") dataRef.current.toggleMute(); |
||||||
|
|
||||||
|
// Video progress
|
||||||
|
if (k === "ArrowRight") |
||||||
|
dataRef.current.display?.setTime(dataRef.current.time + 5); |
||||||
|
if (k === "ArrowLeft") |
||||||
|
dataRef.current.display?.setTime(dataRef.current.time - 5); |
||||||
|
|
||||||
|
// Utils
|
||||||
|
if (k === "f") dataRef.current.display?.toggleFullscreen(); |
||||||
|
if (k === " ") |
||||||
|
dataRef.current.display?.[ |
||||||
|
dataRef.current.mediaPlaying.isPaused ? "play" : "pause" |
||||||
|
](); |
||||||
|
|
||||||
|
// Do a barrell roll!
|
||||||
|
if (k === "r") { |
||||||
|
if (dataRef.current.isRolling || evt.ctrlKey || evt.metaKey) return; |
||||||
|
|
||||||
|
dataRef.current.setIsRolling(true); |
||||||
|
document.querySelector(".popout-location")?.classList.add("roll"); |
||||||
|
document.body.setAttribute("data-no-scroll", "true"); |
||||||
|
|
||||||
|
setTimeout(() => { |
||||||
|
document.querySelector(".popout-location")?.classList.remove("roll"); |
||||||
|
document.body.removeAttribute("data-no-scroll"); |
||||||
|
dataRef.current.setIsRolling(false); |
||||||
|
}, 1e3); |
||||||
|
} |
||||||
|
}; |
||||||
|
window.addEventListener("keydown", keyEventHandler); |
||||||
|
|
||||||
|
return () => { |
||||||
|
window.removeEventListener("keydown", keyEventHandler); |
||||||
|
}; |
||||||
|
}, []); |
||||||
|
|
||||||
|
return null; |
||||||
|
} |
Loading…
Reference in new issue