volume storage fixed, title cleanup, settings cog start, touch controls start

Co-authored-by: Jip Frijlink <JipFr@users.noreply.github.com>
This commit is contained in:
mrjvs 2023-10-11 23:04:41 +02:00
parent 7b3452c535
commit f3084d37a8
17 changed files with 233 additions and 34 deletions

View File

@ -1,10 +1,20 @@
import classNames from "classnames";
import { ReactNode } from "react";
interface Props {
id: string;
children?: ReactNode;
className?: string;
}
export function OverlayAnchor(props: Props) {
return <div id={`__overlayRouter::${props.id}`}>{props.children}</div>;
return (
<div className={classNames("relative", props.className)}>
<div
id={`__overlayRouter::${props.id}`}
className="absolute inset-0 -z-10"
/>
{props.children}
</div>
);
}

View File

@ -1,16 +1,67 @@
import classNames from "classnames";
import { ReactNode } from "react";
import { ReactNode, useCallback, useEffect, useRef, useState } from "react";
import { useOverlayStore } from "@/stores/overlay/store";
interface AnchorPositionProps {
children?: ReactNode;
className?: string;
}
function useCalculatePositions() {
const anchorPoint = useOverlayStore((s) => s.anchorPoint);
const ref = useRef<HTMLDivElement>(null);
const [left, setLeft] = useState<number>(0);
const [top, setTop] = useState<number>(0);
const [cardRect, setCardRect] = useState<DOMRect | null>(null);
const calculateAndSetCoords = useCallback(
(anchor: typeof anchorPoint, card: DOMRect) => {
if (!anchor) return;
const buttonCenter = anchor.x + anchor.w / 2;
const bottomReal = window.innerHeight - (anchor.y + anchor.h);
setTop(window.innerHeight - bottomReal - anchor.h - card.height - 30);
setLeft(
Math.min(
buttonCenter - card.width / 2,
window.innerWidth - card.width - 30
)
);
},
[]
);
useEffect(() => {
if (!anchorPoint || !cardRect) return;
calculateAndSetCoords(anchorPoint, cardRect);
}, [anchorPoint, calculateAndSetCoords, cardRect]);
useEffect(() => {
if (!ref.current) return;
function checkBox() {
const divRect = ref.current?.getBoundingClientRect();
setCardRect(divRect ?? null);
}
checkBox();
const observer = new ResizeObserver(checkBox);
observer.observe(ref.current);
return () => {
observer.disconnect();
};
}, []);
return [ref, left, top] as const;
}
export function OverlayAnchorPosition(props: AnchorPositionProps) {
const [ref, left, top] = useCalculatePositions();
return (
<div
ref={ref}
style={{
transform: `translateX(0px) translateY(0px)`,
transform: `translateX(${left}px) translateY(${top}px)`,
}}
className={classNames([
"pointer-events-auto z-10 inline-block origin-top-left touch-none",

View File

@ -6,4 +6,5 @@ export * from "./base/BottomControls";
export * from "./base/BlackOverlay";
export * from "./base/BackLink";
export * from "./base/LeftSideControls";
export * from "./base/CenterMobileControls";
export * from "./internals/BookmarkButton";

View File

@ -2,7 +2,7 @@ import { Icons } from "@/components/Icon";
import { VideoPlayerButton } from "@/components/player/internals/Button";
import { usePlayerStore } from "@/stores/player/store";
export function Pause() {
export function Pause(props: { iconSizeClass?: string }) {
const display = usePlayerStore((s) => s.display);
const { isPaused } = usePlayerStore((s) => s.mediaPlaying);
@ -13,6 +13,7 @@ export function Pause() {
return (
<VideoPlayerButton
iconSizeClass={props.iconSizeClass}
onClick={toggle}
icon={isPaused ? Icons.PLAY : Icons.PAUSE}
/>

View File

@ -0,0 +1,38 @@
import { useEffect } from "react";
import { Icons } from "@/components/Icon";
import { OverlayAnchor } from "@/components/overlays/OverlayAnchor";
import { Overlay } from "@/components/overlays/OverlayDisplay";
import { OverlayPage } from "@/components/overlays/OverlayPage";
import { OverlayRouter } from "@/components/overlays/OverlayRouter";
import { VideoPlayerButton } from "@/components/player/internals/Button";
import { useOverlayRouter } from "@/hooks/useOverlayRouter";
import { usePlayerStore } from "@/stores/player/store";
function SettingsOverlay({ id }: { id: string }) {
return (
<Overlay id={id}>
<OverlayRouter id={id}>
<OverlayPage id={id} path="/" width={400} height={400}>
<p>This is settings menu, welcome!</p>
</OverlayPage>
</OverlayRouter>
</Overlay>
);
}
export function Settings() {
const router = useOverlayRouter("settings");
const setHasOpenOverlay = usePlayerStore((s) => s.setHasOpenOverlay);
useEffect(() => {
setHasOpenOverlay(router.isRouterActive);
}, [setHasOpenOverlay, router.isRouterActive]);
return (
<OverlayAnchor id={router.id}>
<VideoPlayerButton onClick={() => router.open()} icon={Icons.GEAR} />
<SettingsOverlay id={router.id} />
</OverlayAnchor>
);
}

View File

@ -4,7 +4,7 @@ import { Icons } from "@/components/Icon";
import { VideoPlayerButton } from "@/components/player/internals/Button";
import { usePlayerStore } from "@/stores/player/store";
export function SkipForward() {
export function SkipForward(props: { iconSizeClass?: string }) {
const display = usePlayerStore((s) => s.display);
const time = usePlayerStore((s) => s.progress.time);
@ -12,10 +12,16 @@ export function SkipForward() {
display?.setTime(time + 10);
}, [display, time]);
return <VideoPlayerButton onClick={commit} icon={Icons.SKIP_FORWARD} />;
return (
<VideoPlayerButton
iconSizeClass={props.iconSizeClass || ""}
onClick={commit}
icon={Icons.SKIP_FORWARD}
/>
);
}
export function SkipBackward() {
export function SkipBackward(props: { iconSizeClass?: string }) {
const display = usePlayerStore((s) => s.display);
const time = usePlayerStore((s) => s.progress.time);
@ -23,5 +29,11 @@ export function SkipBackward() {
display?.setTime(time - 10);
}, [display, time]);
return <VideoPlayerButton onClick={commit} icon={Icons.SKIP_BACKWARD} />;
return (
<VideoPlayerButton
iconSizeClass={props.iconSizeClass || ""}
onClick={commit}
icon={Icons.SKIP_BACKWARD}
/>
);
}

View File

@ -2,5 +2,5 @@ import { usePlayerStore } from "@/stores/player/store";
export function Title() {
const title = usePlayerStore((s) => s.meta?.title);
return <p>{title || "Beep beep, Richie!"}</p>;
return <p>{title}</p>;
}

View File

@ -24,7 +24,6 @@ export function Volume(props: Props) {
const commitVolume = useCallback(
(percentage) => {
console.log("setting", percentage);
setVolume(percentage);
},
[setVolume]

View File

@ -8,3 +8,4 @@ export * from "./AutoPlayStart";
export * from "./Volume";
export * from "./Title";
export * from "./EpisodeTitle";
export * from "./Settings";

View File

@ -0,0 +1,26 @@
import classNames from "classnames";
import { Transition } from "@/components/Transition";
export function CenterMobileControls(props: {
children: React.ReactNode;
show: boolean;
className?: string;
}) {
return (
<Transition
animation="fade"
show={props.show}
className="pointer-events-none"
>
<div
className={classNames([
"absolute inset-0 flex space-x-6 items-center justify-center pointer-events-none [&>*]:pointer-events-auto",
props.className,
])}
>
{props.children}
</div>
</Transition>
);
}

View File

@ -1,8 +1,12 @@
import classNames from "classnames";
import { useCallback, useEffect } from "react";
import { usePlayerStore } from "@/stores/player/store";
export function LeftSideControls(props: { children: React.ReactNode }) {
export function LeftSideControls(props: {
children: React.ReactNode;
className?: string;
}) {
const setHoveringLeftControls = usePlayerStore(
(s) => s.setHoveringLeftControls
);
@ -18,7 +22,10 @@ export function LeftSideControls(props: { children: React.ReactNode }) {
}, [setHoveringLeftControls]);
return (
<div className="flex space-x-3 items-center" onMouseLeave={mouseLeave}>
<div
className={classNames(["flex space-x-3 items-center", props.className])}
onMouseLeave={mouseLeave}
>
{props.children}
</div>
);

View File

@ -1,9 +1,20 @@
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);
export function useShouldShowControls(opts?: { touchOnly: boolean }) {
const hovering = usePlayerStore((s) => s.interface.hovering);
const lastHoveringState = usePlayerStore(
(s) => s.interface.lastHoveringState
);
const isPaused = usePlayerStore((s) => s.mediaPlaying.isPaused);
const hasOpenOverlay = usePlayerStore((s) => s.interface.hasOpenOverlay);
return hovering !== PlayerHoverState.NOT_HOVERING || isPaused;
const showTouchControls =
lastHoveringState === PlayerHoverState.MOBILE_TAPPED;
const notNotHovering = hovering !== PlayerHoverState.NOT_HOVERING;
if (opts?.touchOnly)
return (showTouchControls && notNotHovering) || isPaused || hasOpenOverlay;
return notNotHovering || isPaused || hasOpenOverlay;
}

View File

@ -1,25 +1,26 @@
import {
getStoredVolume,
setStoredVolume,
} from "@/_oldvideo/components/hooks/volumeStore";
import { setStoredVolume } from "@/_oldvideo/components/hooks/volumeStore";
import { usePlayerStore } from "@/stores/player/store";
// TODO use new stored volume
export function useVolume() {
const volume = usePlayerStore((s) => s.mediaPlaying.volume);
const lastVolume = usePlayerStore((s) => s.interface.lastVolume);
const setLastVolume = usePlayerStore((s) => s.setLastVolume);
const display = usePlayerStore((s) => s.display);
const toggleVolume = (_isKeyboardEvent = false) => {
// TODO use keyboard event
let newVolume = 0;
if (volume > 0) {
setStoredVolume(volume);
display?.setVolume(0);
} else {
const storedVolume = getStoredVolume();
if (storedVolume > 0) display?.setVolume(storedVolume);
else display?.setVolume(1);
}
newVolume = 0;
setLastVolume(volume);
} else if (lastVolume > 0) newVolume = lastVolume;
else newVolume = 1;
display?.setVolume(newVolume);
setStoredVolume(newVolume);
};
return {
@ -28,6 +29,7 @@ export function useVolume() {
},
setVolume(vol: number) {
setStoredVolume(vol);
setLastVolume(vol);
display?.setVolume(vol);
},
};

View File

@ -94,6 +94,7 @@ export function useOverlayRouter(id: string) {
const router = useInternalOverlayRouter(id);
return {
id,
isRouterActive: router.isOverlayActive(),
open: router.open,
close: router.close,
navigate: router.navigate,

View File

@ -7,15 +7,21 @@ import { AutoPlayStart } from "@/components/player/atoms";
import { usePlayer } from "@/components/player/hooks/usePlayer";
import { useShouldShowControls } from "@/components/player/hooks/useShouldShowControls";
import { ScrapingPart } from "@/pages/parts/player/ScrapingPart";
import { PlayerHoverState } from "@/stores/player/slices/interface";
import {
PlayerMeta,
metaToScrapeMedia,
playerStatus,
} from "@/stores/player/slices/source";
import { usePlayerStore } from "@/stores/player/store";
export function PlayerView() {
const { status, setScrapeStatus, playMedia, setMeta } = usePlayer();
const { lastHoveringState } = usePlayerStore((s) => s.interface);
const desktopControlsVisible = useShouldShowControls();
const touchControlsVisible = useShouldShowControls({ touchOnly: true });
const meta = useMemo<PlayerMeta>(
() => ({
type: "show",
@ -71,6 +77,15 @@ export function PlayerView() {
<AutoPlayStart />
</Player.CenterControls>
<Player.CenterMobileControls
className="text-white"
show={touchControlsVisible}
>
<Player.SkipBackward iconSizeClass="text-3xl" />
<Player.Pause iconSizeClass="text-5xl" />
<Player.SkipForward iconSizeClass="text-3xl" />
</Player.CenterMobileControls>
<Player.TopControls show={desktopControlsVisible}>
<div className="grid grid-cols-[1fr,auto] xl:grid-cols-3 items-center">
<div className="flex space-x-3 items-center">
@ -91,14 +106,19 @@ export function PlayerView() {
<Player.BottomControls show={desktopControlsVisible}>
<Player.ProgressBar />
<div className="flex justify-between">
<Player.LeftSideControls>
<Player.LeftSideControls className="hidden lg:flex">
<Player.Pause />
<Player.SkipBackward />
<Player.SkipForward />
<Player.Volume />
<Player.Time />
</Player.LeftSideControls>
<div>
<Player.LeftSideControls className="flex lg:hidden">
{/* Do mobile controls here :) */}
<Player.Time />
</Player.LeftSideControls>
<div className="flex items-center">
<Player.Settings />
<Player.Fullscreen />
</div>
</div>

View File

@ -1,5 +1,3 @@
import { useEffect, useRef } from "react";
import { OverlayAnchor } from "@/components/overlays/OverlayAnchor";
import { Overlay, OverlayDisplay } from "@/components/overlays/OverlayDisplay";
import { OverlayPage } from "@/components/overlays/OverlayPage";
@ -21,9 +19,10 @@ export default function TestView() {
>
Open
</button>
<OverlayAnchor id={router.id}>
<div className="h-20 w-20 hover:w-24 mt-[50rem] bg-white" />
</OverlayAnchor>
<OverlayAnchor
id={router.id}
className="h-20 w-20 hover:w-24 mt-[50rem] bg-white"
/>
<Overlay id={router.id}>
<OverlayRouter id={router.id}>
<OverlayPage id={router.id} path="/" width={400} height={400}>

View File

@ -15,7 +15,10 @@ export interface InterfaceSlice {
interface: {
isFullscreen: boolean;
isSeeking: boolean;
lastVolume: number;
hasOpenOverlay: boolean;
hovering: PlayerHoverState;
lastHoveringState: PlayerHoverState;
volumeChangedWithKeybind: boolean; // has the volume recently been adjusted with the up/down arrows recently?
volumeChangedWithKeybindDebounce: NodeJS.Timeout | null; // debounce for the duration of the "volume changed thingamajig"
@ -27,19 +30,34 @@ export interface InterfaceSlice {
setSeeking(seeking: boolean): void;
setTimeFormat(format: VideoPlayerTimeFormat): void;
setHoveringLeftControls(state: boolean): void;
setHasOpenOverlay(state: boolean): void;
setLastVolume(state: number): void;
}
export const createInterfaceSlice: MakeSlice<InterfaceSlice> = (set, get) => ({
interface: {
hasOpenOverlay: false,
isFullscreen: false,
isSeeking: false,
lastVolume: 0,
leftControlHovering: false,
hovering: PlayerHoverState.NOT_HOVERING,
lastHoveringState: PlayerHoverState.NOT_HOVERING,
volumeChangedWithKeybind: false,
volumeChangedWithKeybindDebounce: null,
timeFormat: VideoPlayerTimeFormat.REGULAR,
},
setLastVolume(state) {
set((s) => {
s.interface.lastVolume = state;
});
},
setHasOpenOverlay(state) {
set((s) => {
s.interface.hasOpenOverlay = state;
});
},
setTimeFormat(format) {
set((s) => {
s.interface.timeFormat = format;
@ -47,6 +65,8 @@ export const createInterfaceSlice: MakeSlice<InterfaceSlice> = (set, get) => ({
},
updateInterfaceHovering(newState: PlayerHoverState) {
set((s) => {
if (newState !== PlayerHoverState.NOT_HOVERING)
s.interface.lastHoveringState = newState;
s.interface.hovering = newState;
});
},