diff --git a/src/components/player/Player.tsx b/src/components/player/Player.tsx index 71552757..74407269 100644 --- a/src/components/player/Player.tsx +++ b/src/components/player/Player.tsx @@ -1,3 +1,7 @@ export * from "./atoms"; export * from "./base/Container"; +export * from "./base/TopControls"; export * from "./base/BottomControls"; +export * from "./base/BlackOverlay"; +export * from "./base/BackLink"; +export * from "./internals/BookmarkButton"; diff --git a/src/components/player/atoms/ProgressBar.tsx b/src/components/player/atoms/ProgressBar.tsx index f84843ab..e036b439 100644 --- a/src/components/player/atoms/ProgressBar.tsx +++ b/src/components/player/atoms/ProgressBar.tsx @@ -34,7 +34,7 @@ export function ProgressBar() { return (
diff --git a/src/components/player/atoms/Time.tsx b/src/components/player/atoms/Time.tsx index ac266bb1..2ba0a75f 100644 --- a/src/components/player/atoms/Time.tsx +++ b/src/components/player/atoms/Time.tsx @@ -1,19 +1,29 @@ -import { useState } from "react"; import { useTranslation } from "react-i18next"; import { VideoPlayerButton } from "@/components/player/internals/Button"; +import { VideoPlayerTimeFormat } from "@/stores/player/slices/interface"; import { usePlayerStore } from "@/stores/player/store"; import { formatSeconds } from "@/utils/formatSeconds"; +function durationExceedsHour(secs: number): boolean { + return secs > 60 * 60; +} + 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 { isSeeking } = usePlayerStore((s) => s.interface); const { t } = useTranslation(); + const hasHours = durationExceedsHour(duration); function toggleMode() { - setTimeMode(!timeMode); + setTimeFormat( + timeFormat === VideoPlayerTimeFormat.REGULAR + ? VideoPlayerTimeFormat.REMAINING + : VideoPlayerTimeFormat.REGULAR + ); } const currentTime = Math.min( @@ -30,16 +40,23 @@ export function Time() { }, }); - const child = timeMode ? ( - <> - {formatSeconds(currentTime)} / {formatSeconds(duration)} - - ) : ( - <> - {t("videoPlayer.timeLeft", { timeLeft: formatSeconds(secondsRemaining) })}{" "} - • {formattedTimeFinished} - - ); + const child = + timeFormat === VideoPlayerTimeFormat.REGULAR ? ( + <> + {formatSeconds(currentTime, hasHours)}{" "} + / {formatSeconds(duration, hasHours)} + + ) : ( + <> + {t("videoPlayer.timeLeft", { + timeLeft: formatSeconds( + secondsRemaining, + durationExceedsHour(secondsRemaining) + ), + })}{" "} + • {formattedTimeFinished} + + ); return ( toggleMode()}>{child} diff --git a/src/components/player/base/BackLink.tsx b/src/components/player/base/BackLink.tsx new file mode 100644 index 00000000..d4a3db62 --- /dev/null +++ b/src/components/player/base/BackLink.tsx @@ -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 ( +
+ goBack()} + className="flex items-center cursor-pointer text-type-secondary hover:text-white transition-colors duration-200 font-medium" + > + + {t("videoPlayer.backToHomeShort")} + + / + Mr Jeebaloo's Big Ocean Adventure +
+ ); +} diff --git a/src/components/player/base/BlackOverlay.tsx b/src/components/player/base/BlackOverlay.tsx new file mode 100644 index 00000000..b6231fa3 --- /dev/null +++ b/src/components/player/base/BlackOverlay.tsx @@ -0,0 +1,11 @@ +import { Transition } from "@/components/Transition"; + +export function BlackOverlay(props: { show?: boolean }) { + return ( + + ); +} diff --git a/src/components/player/base/BottomControls.tsx b/src/components/player/base/BottomControls.tsx index 2a0430f7..34054478 100644 --- a/src/components/player/base/BottomControls.tsx +++ b/src/components/player/base/BottomControls.tsx @@ -1,25 +1,19 @@ import { Transition } from "@/components/Transition"; -import { PlayerHoverState } from "@/stores/player/slices/interface"; -import { usePlayerStore } from "@/stores/player/store"; export function BottomControls(props: { show?: boolean; children: React.ReactNode; }) { - const { hovering } = usePlayerStore((s) => s.interface); - const visible = - (hovering !== PlayerHoverState.NOT_HOVERING || props.show) ?? false; - return (
{props.children} diff --git a/src/components/player/base/Container.tsx b/src/components/player/base/Container.tsx index d00612f2..63f25efb 100644 --- a/src/components/player/base/Container.tsx +++ b/src/components/player/base/Container.tsx @@ -1,5 +1,6 @@ import { ReactNode, RefObject, useEffect, useRef } from "react"; +import { VideoClickTarget } from "@/components/player/internals/VideoClickTarget"; import { VideoContainer } from "@/components/player/internals/VideoContainer"; import { PlayerHoverState } from "@/stores/player/slices/interface"; import { usePlayerStore } from "@/stores/player/store"; @@ -36,22 +37,12 @@ function useHovering(containerEl: RefObject) { 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("pointerleave", pointerLeave); - el.addEventListener("pointerup", pointerUp); return () => { el.removeEventListener("pointermove", pointerMove); el.removeEventListener("pointerleave", pointerLeave); - el.removeEventListener("pointerup", pointerUp); }; }, [containerEl, hovering, updateInterfaceHovering]); } @@ -69,7 +60,10 @@ function BaseContainer(props: { children?: ReactNode }) { }, [display, containerEl]); return ( -
+
{props.children}
); @@ -84,6 +78,7 @@ export function Container(props: PlayerProps) { return ( + {props.children} ); diff --git a/src/components/player/base/TopControls.tsx b/src/components/player/base/TopControls.tsx new file mode 100644 index 00000000..cc3d4ccb --- /dev/null +++ b/src/components/player/base/TopControls.tsx @@ -0,0 +1,23 @@ +import { Transition } from "@/components/Transition"; + +export function TopControls(props: { + show?: boolean; + children: React.ReactNode; +}) { + return ( +
+ + + {props.children} + +
+ ); +} diff --git a/src/components/player/display/base.ts b/src/components/player/display/base.ts index 90371493..f92bdeee 100644 --- a/src/components/player/display/base.ts +++ b/src/components/player/display/base.ts @@ -21,6 +21,7 @@ export function makeVideoElementDisplayInterface(): DisplayInterface { let containerElement: HTMLElement | null = null; let isFullscreen = false; let isPausedBeforeSeeking = false; + let isSeeking = false; function setSource() { if (!videoElement || !source) return; @@ -78,6 +79,9 @@ export function makeVideoElementDisplayInterface(): DisplayInterface { videoElement?.play(); }, setSeeking(active) { + if (active === isSeeking) return; + isSeeking = active; + // if it was playing when starting to seek, play again if (!active) { if (!isPausedBeforeSeeking) this.play(); diff --git a/src/components/player/hooks/useShouldShowControls.tsx b/src/components/player/hooks/useShouldShowControls.tsx new file mode 100644 index 00000000..c0f3af9f --- /dev/null +++ b/src/components/player/hooks/useShouldShowControls.tsx @@ -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; +} diff --git a/src/components/player/internals/BookmarkButton.tsx b/src/components/player/internals/BookmarkButton.tsx new file mode 100644 index 00000000..5a618b6a --- /dev/null +++ b/src/components/player/internals/BookmarkButton.tsx @@ -0,0 +1,14 @@ +import { Icons } from "@/components/Icon"; + +import { VideoPlayerButton } from "./Button"; + +export function BookmarkButton() { + return ( + window.open("https://youtu.be/TENzstSjsus", "_blank")} + icon={Icons.BOOKMARK_OUTLINE} + iconSizeClass="text-base" + className="p-3" + /> + ); +} diff --git a/src/components/player/internals/Button.tsx b/src/components/player/internals/Button.tsx index 420ff70b..4a9412a4 100644 --- a/src/components/player/internals/Button.tsx +++ b/src/components/player/internals/Button.tsx @@ -4,14 +4,21 @@ export function VideoPlayerButton(props: { children?: React.ReactNode; onClick: () => void; icon?: Icons; + iconSizeClass?: string; + className?: string; }) { return ( ); diff --git a/src/components/player/internals/VideoClickTarget.tsx b/src/components/player/internals/VideoClickTarget.tsx new file mode 100644 index 00000000..d8dcd4aa --- /dev/null +++ b/src/components/player/internals/VideoClickTarget.tsx @@ -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) => { + // 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 ( +
+ ); +} diff --git a/src/components/player/internals/VideoContainer.tsx b/src/components/player/internals/VideoContainer.tsx index 358a1c1d..9b6dbe2c 100644 --- a/src/components/player/internals/VideoContainer.tsx +++ b/src/components/player/internals/VideoContainer.tsx @@ -1,4 +1,4 @@ -import { PointerEvent, useCallback, useEffect, useRef } from "react"; +import { useEffect, useRef } from "react"; import { makeVideoElementDisplayInterface } from "@/components/player/display/base"; import { playerStatus } from "@/stores/player/slices/source"; @@ -16,7 +16,7 @@ function useDisplayInterface() { }, [display, setDisplay]); } -function useShouldShowVideoElement() { +export function useShouldShowVideoElement() { const status = usePlayerStore((s) => s.status); if (status !== playerStatus.PLAYING) return false; @@ -26,20 +26,6 @@ function useShouldShowVideoElement() { function VideoElement() { const videoEl = useRef(null); const display = usePlayerStore((s) => s.display); - const isPaused = usePlayerStore((s) => s.mediaPlaying.isPaused); - - const toggleFullscreen = useCallback(() => { - display?.toggleFullscreen(); - }, [display]); - - const togglePause = useCallback( - (e: PointerEvent) => { - if (e.pointerType !== "mouse") return; - if (isPaused) display?.play(); - else display?.pause(); - }, - [display, isPaused] - ); // report video element to display interface useEffect(() => { @@ -48,15 +34,7 @@ function VideoElement() { } }, [display, videoEl]); - return ( -