mirror of
https://github.com/sussy-code/smov.git
synced 2024-12-20 14:37:43 +01:00
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:
parent
7b3452c535
commit
f3084d37a8
17 changed files with 233 additions and 34 deletions
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
|
|
38
src/components/player/atoms/Settings.tsx
Normal file
38
src/components/player/atoms/Settings.tsx
Normal 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>
|
||||
);
|
||||
}
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>;
|
||||
}
|
||||
|
|
|
@ -24,7 +24,6 @@ export function Volume(props: Props) {
|
|||
|
||||
const commitVolume = useCallback(
|
||||
(percentage) => {
|
||||
console.log("setting", percentage);
|
||||
setVolume(percentage);
|
||||
},
|
||||
[setVolume]
|
||||
|
|
|
@ -8,3 +8,4 @@ export * from "./AutoPlayStart";
|
|||
export * from "./Volume";
|
||||
export * from "./Title";
|
||||
export * from "./EpisodeTitle";
|
||||
export * from "./Settings";
|
||||
|
|
26
src/components/player/base/CenterMobileControls.tsx
Normal file
26
src/components/player/base/CenterMobileControls.tsx
Normal 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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
},
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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}>
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
},
|
||||
|
|
Loading…
Reference in a new issue