15 changed files with 276 additions and 24 deletions
@ -0,0 +1,92 @@ |
|||||||
|
import { Icon, Icons } from "@/components/Icon"; |
||||||
|
import { |
||||||
|
makePercentage, |
||||||
|
makePercentageString, |
||||||
|
useProgressBar, |
||||||
|
} from "@/hooks/useProgressBar"; |
||||||
|
import { useVolumeControl } from "@/hooks/useVolumeToggle"; |
||||||
|
import { canChangeVolume } from "@/utils/detectFeatures"; |
||||||
|
import { useVideoPlayerDescriptor } from "@/video/state/hooks"; |
||||||
|
import { useControls } from "@/video/state/logic/controls"; |
||||||
|
import { useInterface } from "@/video/state/logic/interface"; |
||||||
|
import { useMediaPlaying } from "@/video/state/logic/mediaplaying"; |
||||||
|
import { useCallback, useEffect, useRef, useState } from "react"; |
||||||
|
|
||||||
|
interface Props { |
||||||
|
className?: string; |
||||||
|
} |
||||||
|
|
||||||
|
export function VolumeAction(props: Props) { |
||||||
|
const descriptor = useVideoPlayerDescriptor(); |
||||||
|
const controls = useControls(descriptor); |
||||||
|
const mediaPlaying = useMediaPlaying(descriptor); |
||||||
|
const videoInterface = useInterface(descriptor); |
||||||
|
const { setStoredVolume, toggleVolume } = useVolumeControl(descriptor); |
||||||
|
const ref = useRef<HTMLDivElement>(null); |
||||||
|
const [hoveredOnce, setHoveredOnce] = useState(false); |
||||||
|
|
||||||
|
const commitVolume = useCallback( |
||||||
|
(percentage) => { |
||||||
|
controls.setVolume(percentage); |
||||||
|
setStoredVolume(percentage); |
||||||
|
}, |
||||||
|
[controls, setStoredVolume] |
||||||
|
); |
||||||
|
const { dragging, dragPercentage, dragMouseDown } = useProgressBar( |
||||||
|
ref, |
||||||
|
commitVolume, |
||||||
|
true |
||||||
|
); |
||||||
|
|
||||||
|
useEffect(() => { |
||||||
|
if (!videoInterface.leftControlHovering) setHoveredOnce(false); |
||||||
|
}, [videoInterface]); |
||||||
|
|
||||||
|
const handleClick = useCallback(() => { |
||||||
|
toggleVolume(); |
||||||
|
}, [toggleVolume]); |
||||||
|
|
||||||
|
const handleMouseEnter = useCallback(async () => { |
||||||
|
if (await canChangeVolume()) setHoveredOnce(true); |
||||||
|
}, [setHoveredOnce]); |
||||||
|
|
||||||
|
let percentage = makePercentage(mediaPlaying.volume * 100); |
||||||
|
if (dragging) percentage = makePercentage(dragPercentage); |
||||||
|
const percentageString = makePercentageString(percentage); |
||||||
|
|
||||||
|
return ( |
||||||
|
<div className={props.className}> |
||||||
|
<div |
||||||
|
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={`linear -ml-2 w-0 overflow-hidden transition-[width,opacity] duration-300 ${ |
||||||
|
hoveredOnce || dragging ? "!w-24 opacity-100" : "w-4 opacity-0" |
||||||
|
}`}
|
||||||
|
> |
||||||
|
<div |
||||||
|
ref={ref} |
||||||
|
className="flex h-10 w-20 items-center px-2" |
||||||
|
onMouseDown={dragMouseDown} |
||||||
|
onTouchStart={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> |
||||||
|
); |
||||||
|
} |
||||||
@ -0,0 +1,47 @@ |
|||||||
|
import { useEffect, useMemo, useRef } from "react"; |
||||||
|
import throttle from "lodash.throttle"; |
||||||
|
import { useVideoPlayerDescriptor } from "@/video/state/hooks"; |
||||||
|
import { useMediaPlaying } from "@/video/state/logic/mediaplaying"; |
||||||
|
import { useProgress } from "@/video/state/logic/progress"; |
||||||
|
import { useControls } from "@/video/state/logic/controls"; |
||||||
|
|
||||||
|
interface Props { |
||||||
|
startAt?: number; |
||||||
|
onProgress?: (time: number, duration: number) => void; |
||||||
|
} |
||||||
|
|
||||||
|
export function ProgressListenerController(props: Props) { |
||||||
|
const descriptor = useVideoPlayerDescriptor(); |
||||||
|
const mediaPlaying = useMediaPlaying(descriptor); |
||||||
|
const progress = useProgress(descriptor); |
||||||
|
const controls = useControls(descriptor); |
||||||
|
const didInitialize = useRef<true | null>(null); |
||||||
|
|
||||||
|
// time updates (throttled)
|
||||||
|
const updateTime = useMemo( |
||||||
|
() => throttle((a: number, b: number) => props.onProgress?.(a, b), 1000), |
||||||
|
[props] |
||||||
|
); |
||||||
|
useEffect(() => { |
||||||
|
if (!mediaPlaying.isPlaying) return; |
||||||
|
if (progress.duration === 0 || progress.time === 0) return; |
||||||
|
updateTime(progress.time, progress.duration); |
||||||
|
}, [progress, mediaPlaying, updateTime]); |
||||||
|
useEffect(() => { |
||||||
|
return () => { |
||||||
|
updateTime.cancel(); |
||||||
|
}; |
||||||
|
}, [updateTime]); |
||||||
|
|
||||||
|
// initialize
|
||||||
|
useEffect(() => { |
||||||
|
if (didInitialize.current) return; |
||||||
|
if (mediaPlaying.isFirstLoading || Number.isNaN(progress.duration)) return; |
||||||
|
if (props.startAt !== undefined) { |
||||||
|
controls.setTime(props.startAt); |
||||||
|
} |
||||||
|
didInitialize.current = true; |
||||||
|
}, [didInitialize, props, progress, mediaPlaying, controls]); |
||||||
|
|
||||||
|
return null; |
||||||
|
} |
||||||
@ -0,0 +1,25 @@ |
|||||||
|
import { versionedStoreBuilder } from "@/utils/storage"; |
||||||
|
|
||||||
|
export const volumeStore = versionedStoreBuilder() |
||||||
|
.setKey("mw-volume") |
||||||
|
.addVersion({ |
||||||
|
version: 0, |
||||||
|
create() { |
||||||
|
return { |
||||||
|
volume: 1, |
||||||
|
}; |
||||||
|
}, |
||||||
|
}) |
||||||
|
.build(); |
||||||
|
|
||||||
|
export function getStoredVolume(): number { |
||||||
|
const store = volumeStore.get(); |
||||||
|
return store.volume; |
||||||
|
} |
||||||
|
|
||||||
|
export function setStoredVolume(volume: number) { |
||||||
|
const store = volumeStore.get(); |
||||||
|
store.save({ |
||||||
|
volume, |
||||||
|
}); |
||||||
|
} |
||||||
Loading…
Reference in new issue