mirror of
https://github.com/movie-web/movie-web.git
synced 2024-12-25 06:41:49 +01:00
add better popout system
Co-authored-by: Jip Frijlink <JipFr@users.noreply.github.com>
This commit is contained in:
parent
76e4bc5851
commit
5d5a727663
@ -2,11 +2,11 @@ import { ReactNode, useRef } from "react";
|
|||||||
import { CSSTransition } from "react-transition-group";
|
import { CSSTransition } from "react-transition-group";
|
||||||
import { CSSTransitionClassNames } from "react-transition-group/CSSTransition";
|
import { CSSTransitionClassNames } from "react-transition-group/CSSTransition";
|
||||||
|
|
||||||
type TransitionAnimations = "slide-down" | "slide-up" | "fade";
|
type TransitionAnimations = "slide-down" | "slide-up" | "fade" | "fade-inverse";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
show: boolean;
|
show: boolean;
|
||||||
duration?: number;
|
durationClass?: string;
|
||||||
animation: TransitionAnimations;
|
animation: TransitionAnimations;
|
||||||
className?: string;
|
className?: string;
|
||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
@ -46,21 +46,37 @@ function getClasses(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (animation === "fade-inverse") {
|
||||||
|
return {
|
||||||
|
enter: `transition-[transform,opacity] duration-${duration} opacity-100`,
|
||||||
|
enterActive: "!opacity-0",
|
||||||
|
exit: `transition-[transform,opacity] duration-${duration} opacity-0`,
|
||||||
|
exitActive: "!opacity-100",
|
||||||
|
enterDone: "hidden",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Transition(props: Props) {
|
export function Transition(props: Props) {
|
||||||
const ref = useRef<HTMLDivElement>(null);
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
const duration = props.duration ?? 200;
|
const duration = props.durationClass
|
||||||
|
? parseInt(props.durationClass.split("-")[1], 10)
|
||||||
|
: 200;
|
||||||
|
const classes = getClasses(props.animation, duration);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CSSTransition
|
<CSSTransition
|
||||||
nodeRef={ref}
|
nodeRef={ref}
|
||||||
in={props.show}
|
in={props.show}
|
||||||
timeout={200}
|
timeout={duration}
|
||||||
classNames={getClasses(props.animation, duration)}
|
classNames={classes}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={[props.className ?? "", classes.enter ?? ""].join(" ")}
|
||||||
>
|
>
|
||||||
<div ref={ref} className={props.className}>
|
|
||||||
{props.children}
|
{props.children}
|
||||||
</div>
|
</div>
|
||||||
</CSSTransition>
|
</CSSTransition>
|
||||||
|
@ -25,6 +25,7 @@ import {
|
|||||||
import { useVideoPlayerDescriptor } from "@/video/state/hooks";
|
import { useVideoPlayerDescriptor } from "@/video/state/hooks";
|
||||||
import { useControls } from "@/video/state/logic/controls";
|
import { useControls } from "@/video/state/logic/controls";
|
||||||
import { ReactNode, useCallback, useState } from "react";
|
import { ReactNode, useCallback, useState } from "react";
|
||||||
|
import { PopoutProviderAction } from "@/video/components/popouts/PopoutProviderAction";
|
||||||
|
|
||||||
type Props = VideoPlayerBaseProps;
|
type Props = VideoPlayerBaseProps;
|
||||||
|
|
||||||
@ -153,6 +154,7 @@ export function VideoPlayer(props: Props) {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Transition>
|
</Transition>
|
||||||
|
{show ? <PopoutProviderAction /> : null}
|
||||||
</BackdropAction>
|
</BackdropAction>
|
||||||
{props.children}
|
{props.children}
|
||||||
</VideoPlayerError>
|
</VideoPlayerError>
|
||||||
|
@ -12,6 +12,8 @@ import { useMeta } from "@/video/state/logic/meta";
|
|||||||
import { VideoPlayerIconButton } from "@/video/components/parts/VideoPlayerIconButton";
|
import { VideoPlayerIconButton } from "@/video/components/parts/VideoPlayerIconButton";
|
||||||
import { useControls } from "@/video/state/logic/controls";
|
import { useControls } from "@/video/state/logic/controls";
|
||||||
import { VideoPopout } from "@/video/components/parts/VideoPopout";
|
import { VideoPopout } from "@/video/components/parts/VideoPopout";
|
||||||
|
import { PopoutAnchor } from "@/video/components/popouts/PopoutAnchor";
|
||||||
|
import { useInterface } from "@/video/state/logic/interface";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
className?: string;
|
className?: string;
|
||||||
@ -177,6 +179,7 @@ function PopupEpisodeSelect() {
|
|||||||
export function SeriesSelectionAction(props: Props) {
|
export function SeriesSelectionAction(props: Props) {
|
||||||
const descriptor = useVideoPlayerDescriptor();
|
const descriptor = useVideoPlayerDescriptor();
|
||||||
const meta = useMeta(descriptor);
|
const meta = useMeta(descriptor);
|
||||||
|
const videoInterface = useInterface(descriptor);
|
||||||
const controls = useControls(descriptor);
|
const controls = useControls(descriptor);
|
||||||
|
|
||||||
if (meta?.meta.type !== MWMediaType.SERIES) return null;
|
if (meta?.meta.type !== MWMediaType.SERIES) return null;
|
||||||
@ -184,17 +187,21 @@ export function SeriesSelectionAction(props: Props) {
|
|||||||
return (
|
return (
|
||||||
<div className={props.className}>
|
<div className={props.className}>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<VideoPopout
|
<PopoutAnchor for="episodes">
|
||||||
|
<VideoPlayerIconButton
|
||||||
|
active={videoInterface.popout === "episodes"}
|
||||||
|
icon={Icons.EPISODES}
|
||||||
|
text="Episodes"
|
||||||
|
wide
|
||||||
|
onClick={() => controls.openPopout("episodes")}
|
||||||
|
/>
|
||||||
|
</PopoutAnchor>
|
||||||
|
{/* <VideoPopout
|
||||||
id="episodes"
|
id="episodes"
|
||||||
className="grid grid-rows-[auto,minmax(0,1fr)]"
|
className="grid grid-rows-[auto,minmax(0,1fr)]"
|
||||||
>
|
>
|
||||||
<PopupEpisodeSelect />
|
<PopupEpisodeSelect />
|
||||||
</VideoPopout>
|
</VideoPopout> */}
|
||||||
<VideoPlayerIconButton
|
|
||||||
icon={Icons.EPISODES}
|
|
||||||
text="Episodes"
|
|
||||||
onClick={() => controls.openPopout("episodes")}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -7,6 +7,8 @@ export interface VideoPlayerIconButtonProps {
|
|||||||
text?: string;
|
text?: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
iconSize?: string;
|
iconSize?: string;
|
||||||
|
active?: boolean;
|
||||||
|
wide?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function VideoPlayerIconButton(props: VideoPlayerIconButtonProps) {
|
export function VideoPlayerIconButton(props: VideoPlayerIconButtonProps) {
|
||||||
@ -17,7 +19,13 @@ export function VideoPlayerIconButton(props: VideoPlayerIconButtonProps) {
|
|||||||
onClick={props.onClick}
|
onClick={props.onClick}
|
||||||
className="group pointer-events-auto p-2 text-white transition-transform duration-100 active:scale-110"
|
className="group pointer-events-auto p-2 text-white transition-transform duration-100 active:scale-110"
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-center rounded-full bg-white bg-opacity-0 p-2 transition-colors duration-100 group-hover:bg-opacity-20">
|
<div
|
||||||
|
className={[
|
||||||
|
"flex items-center justify-center rounded-full bg-denim-600 bg-opacity-0 transition-colors duration-100 group-hover:bg-opacity-50 group-active:bg-denim-500 group-active:bg-opacity-100",
|
||||||
|
props.active ? "!bg-denim-500 !bg-opacity-100" : "",
|
||||||
|
props.wide ? "py-2 px-4" : "p-2",
|
||||||
|
].join(" ")}
|
||||||
|
>
|
||||||
<Icon icon={props.icon} className={props.iconSize ?? "text-2xl"} />
|
<Icon icon={props.icon} className={props.iconSize ?? "text-2xl"} />
|
||||||
{props.text ? <span className="ml-2">{props.text}</span> : null}
|
{props.text ? <span className="ml-2">{props.text}</span> : null}
|
||||||
</div>
|
</div>
|
||||||
|
@ -9,8 +9,6 @@ interface Props {
|
|||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO store popout in router history so you can press back to yeet
|
|
||||||
// TODO add transition
|
|
||||||
export function VideoPopout(props: Props) {
|
export function VideoPopout(props: Props) {
|
||||||
const descriptor = useVideoPlayerDescriptor();
|
const descriptor = useVideoPlayerDescriptor();
|
||||||
const videoInterface = useInterface(descriptor);
|
const videoInterface = useInterface(descriptor);
|
||||||
|
169
src/video/components/popouts/EpisodeSelectionPopout.tsx
Normal file
169
src/video/components/popouts/EpisodeSelectionPopout.tsx
Normal file
@ -0,0 +1,169 @@
|
|||||||
|
import React, { useCallback, useMemo, useState } from "react";
|
||||||
|
import { useParams } from "react-router-dom";
|
||||||
|
import { Icon, Icons } from "@/components/Icon";
|
||||||
|
import { useLoading } from "@/hooks/useLoading";
|
||||||
|
import { MWMediaType, MWSeasonWithEpisodeMeta } from "@/backend/metadata/types";
|
||||||
|
import { getMetaFromId } from "@/backend/metadata/getmeta";
|
||||||
|
import { decodeJWId } from "@/backend/metadata/justwatch";
|
||||||
|
import { Loading } from "@/components/layout/Loading";
|
||||||
|
import { IconPatch } from "@/components/buttons/IconPatch";
|
||||||
|
import { useVideoPlayerDescriptor } from "@/video/state/hooks";
|
||||||
|
import { useMeta } from "@/video/state/logic/meta";
|
||||||
|
import { useControls } from "@/video/state/logic/controls";
|
||||||
|
|
||||||
|
function PopupSection(props: {
|
||||||
|
children?: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className={["p-4", props.className || ""].join(" ")}>
|
||||||
|
{props.children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function EpisodeSelectionPopout() {
|
||||||
|
const params = useParams<{
|
||||||
|
media: string;
|
||||||
|
}>();
|
||||||
|
const descriptor = useVideoPlayerDescriptor();
|
||||||
|
const meta = useMeta(descriptor);
|
||||||
|
const controls = useControls(descriptor);
|
||||||
|
|
||||||
|
const [isPickingSeason, setIsPickingSeason] = useState<boolean>(false);
|
||||||
|
const [currentVisibleSeason, setCurrentVisibleSeason] = useState<{
|
||||||
|
seasonId: string;
|
||||||
|
season?: MWSeasonWithEpisodeMeta;
|
||||||
|
} | null>(null);
|
||||||
|
const [reqSeasonMeta, loading, error] = useLoading(
|
||||||
|
(id: string, seasonId: string) => {
|
||||||
|
return getMetaFromId(MWMediaType.SERIES, id, seasonId);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const requestSeason = useCallback(
|
||||||
|
(sId: string) => {
|
||||||
|
setCurrentVisibleSeason({
|
||||||
|
seasonId: sId,
|
||||||
|
season: undefined,
|
||||||
|
});
|
||||||
|
setIsPickingSeason(false);
|
||||||
|
reqSeasonMeta(decodeJWId(params.media)?.id as string, sId).then((v) => {
|
||||||
|
if (v?.meta.type !== MWMediaType.SERIES) return;
|
||||||
|
setCurrentVisibleSeason({
|
||||||
|
seasonId: sId,
|
||||||
|
season: v?.meta.seasonData,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[reqSeasonMeta, params.media]
|
||||||
|
);
|
||||||
|
|
||||||
|
const currentSeasonId =
|
||||||
|
currentVisibleSeason?.seasonId ?? meta?.episode?.seasonId;
|
||||||
|
|
||||||
|
const setCurrent = useCallback(
|
||||||
|
(seasonId: string, episodeId: string) => {
|
||||||
|
controls.setCurrentEpisode(seasonId, episodeId);
|
||||||
|
},
|
||||||
|
[controls]
|
||||||
|
);
|
||||||
|
|
||||||
|
const currentSeasonInfo = useMemo(() => {
|
||||||
|
return meta?.seasons?.find((season) => season.id === currentSeasonId);
|
||||||
|
}, [meta, currentSeasonId]);
|
||||||
|
|
||||||
|
const currentSeasonEpisodes = useMemo(() => {
|
||||||
|
if (currentVisibleSeason?.season) {
|
||||||
|
return currentVisibleSeason?.season?.episodes;
|
||||||
|
}
|
||||||
|
return meta?.seasons?.find?.(
|
||||||
|
(season) => season && season.id === currentSeasonId
|
||||||
|
)?.episodes;
|
||||||
|
}, [meta, currentSeasonId, currentVisibleSeason]);
|
||||||
|
|
||||||
|
const toggleIsPickingSeason = () => {
|
||||||
|
setIsPickingSeason(!isPickingSeason);
|
||||||
|
};
|
||||||
|
|
||||||
|
const setSeason = (id: string) => {
|
||||||
|
requestSeason(id);
|
||||||
|
setCurrentVisibleSeason({ seasonId: id });
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isPickingSeason)
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<PopupSection className="flex items-center space-x-3 border-b border-denim-500 font-bold text-white">
|
||||||
|
Pick a season
|
||||||
|
</PopupSection>
|
||||||
|
<PopupSection className="overflow-y-auto">
|
||||||
|
<div className="space-y-1">
|
||||||
|
{currentSeasonInfo
|
||||||
|
? meta?.seasons?.map?.((season) => (
|
||||||
|
<div
|
||||||
|
className="text-denim-800 -mx-2 flex items-center space-x-1 rounded p-2 text-white hover:bg-denim-600"
|
||||||
|
key={season.id}
|
||||||
|
onClick={() => setSeason(season.id)}
|
||||||
|
>
|
||||||
|
{season.title}
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
: "No season"}
|
||||||
|
</div>
|
||||||
|
</PopupSection>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<PopupSection className="flex items-center space-x-3 border-b border-denim-500 font-bold text-white">
|
||||||
|
<button
|
||||||
|
className="-m-1.5 rounded p-1.5 hover:bg-denim-600"
|
||||||
|
onClick={toggleIsPickingSeason}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<Icon icon={Icons.CHEVRON_LEFT} />
|
||||||
|
</button>
|
||||||
|
<span>{currentSeasonInfo?.title || ""}</span>
|
||||||
|
</PopupSection>
|
||||||
|
<PopupSection className="h-full overflow-y-auto">
|
||||||
|
{loading ? (
|
||||||
|
<div className="flex h-full w-full items-center justify-center">
|
||||||
|
<Loading />
|
||||||
|
</div>
|
||||||
|
) : error ? (
|
||||||
|
<div className="flex h-full w-full items-center justify-center">
|
||||||
|
<div className="flex flex-col flex-wrap items-center text-slate-400">
|
||||||
|
<IconPatch
|
||||||
|
icon={Icons.EYE_SLASH}
|
||||||
|
className="text-xl text-bink-600"
|
||||||
|
/>
|
||||||
|
<p className="mt-6 w-full text-center">
|
||||||
|
Something went wrong loading the episodes for{" "}
|
||||||
|
{currentSeasonInfo?.title?.toLowerCase()}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-1">
|
||||||
|
{currentSeasonEpisodes && currentSeasonInfo
|
||||||
|
? currentSeasonEpisodes.map((e) => (
|
||||||
|
<div
|
||||||
|
className={[
|
||||||
|
"text-denim-800 -mx-2 flex items-center space-x-1 rounded p-2 text-white hover:bg-denim-600",
|
||||||
|
meta?.episode?.episodeId === e.id &&
|
||||||
|
"outline outline-2 outline-denim-700",
|
||||||
|
].join(" ")}
|
||||||
|
onClick={() => setCurrent(currentSeasonInfo.id, e.id)}
|
||||||
|
key={e.id}
|
||||||
|
>
|
||||||
|
{e.number}. {e.title}
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
: "No episodes"}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</PopupSection>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
41
src/video/components/popouts/PopoutAnchor.tsx
Normal file
41
src/video/components/popouts/PopoutAnchor.tsx
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import { getPlayerState } from "@/video/state/cache";
|
||||||
|
import { useVideoPlayerDescriptor } from "@/video/state/hooks";
|
||||||
|
import { updateInterface } from "@/video/state/logic/interface";
|
||||||
|
import { ReactNode, useEffect, useRef } from "react";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
for: string;
|
||||||
|
children?: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PopoutAnchor(props: Props) {
|
||||||
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
|
const descriptor = useVideoPlayerDescriptor();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!ref.current) return;
|
||||||
|
const state = getPlayerState(descriptor);
|
||||||
|
|
||||||
|
if (state.interface.popout !== props.for) return;
|
||||||
|
|
||||||
|
let handle = -1;
|
||||||
|
function render() {
|
||||||
|
if (ref.current) {
|
||||||
|
const current = JSON.stringify(state.interface.popoutBounds);
|
||||||
|
const newer = ref.current.getBoundingClientRect();
|
||||||
|
if (current !== JSON.stringify(newer)) {
|
||||||
|
state.interface.popoutBounds = newer;
|
||||||
|
updateInterface(descriptor, state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
handle = window.requestAnimationFrame(render);
|
||||||
|
}
|
||||||
|
|
||||||
|
handle = window.requestAnimationFrame(render);
|
||||||
|
return () => {
|
||||||
|
window.cancelAnimationFrame(handle);
|
||||||
|
};
|
||||||
|
}, [descriptor, props]);
|
||||||
|
|
||||||
|
return <div ref={ref}>{props.children}</div>;
|
||||||
|
}
|
70
src/video/components/popouts/PopoutProviderAction.tsx
Normal file
70
src/video/components/popouts/PopoutProviderAction.tsx
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
import { EpisodeSelectionPopout } from "@/video/components/popouts/EpisodeSelectionPopout";
|
||||||
|
import { useVideoPlayerDescriptor } from "@/video/state/hooks";
|
||||||
|
import { useControls } from "@/video/state/logic/controls";
|
||||||
|
import { useInterface } from "@/video/state/logic/interface";
|
||||||
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
|
|
||||||
|
import "./Popouts.css";
|
||||||
|
|
||||||
|
function ShowPopout(props: { popoutId: string }) {
|
||||||
|
// only updates popout id when a new one is set, so transitions look good
|
||||||
|
const [popoutId, setPopoutId] = useState<string | null>(props.popoutId);
|
||||||
|
useEffect(() => {
|
||||||
|
if (!props.popoutId) return;
|
||||||
|
setPopoutId(props.popoutId);
|
||||||
|
}, [props]);
|
||||||
|
|
||||||
|
if (popoutId === "episodes") return <EpisodeSelectionPopout />;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO use new design for popouts
|
||||||
|
// TODO improve anti offscreen math
|
||||||
|
// TODO in and out transition
|
||||||
|
// TODO attach router history to popout state, so you can use back button to remove popout
|
||||||
|
export function PopoutProviderAction() {
|
||||||
|
const descriptor = useVideoPlayerDescriptor();
|
||||||
|
const videoInterface = useInterface(descriptor);
|
||||||
|
const controls = useControls(descriptor);
|
||||||
|
|
||||||
|
const handleClick = useCallback(() => {
|
||||||
|
controls.closePopout();
|
||||||
|
}, [controls]);
|
||||||
|
|
||||||
|
const distanceFromRight = useMemo(() => {
|
||||||
|
return videoInterface.popoutBounds
|
||||||
|
? `${Math.max(
|
||||||
|
window.innerWidth -
|
||||||
|
videoInterface.popoutBounds.right -
|
||||||
|
videoInterface.popoutBounds.width / 2,
|
||||||
|
30
|
||||||
|
)}px`
|
||||||
|
: "30px";
|
||||||
|
}, [videoInterface]);
|
||||||
|
const distanceFromBottom = useMemo(() => {
|
||||||
|
return videoInterface.popoutBounds
|
||||||
|
? `${Math.max(
|
||||||
|
videoInterface.popoutBounds.bottom -
|
||||||
|
videoInterface.popoutBounds.top +
|
||||||
|
videoInterface.popoutBounds.height
|
||||||
|
)}px`
|
||||||
|
: "30px";
|
||||||
|
}, [videoInterface]);
|
||||||
|
|
||||||
|
if (!videoInterface.popout) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="popout-wrapper pointer-events-auto absolute inset-0">
|
||||||
|
<div onClick={handleClick} className="absolute inset-0" />
|
||||||
|
<div
|
||||||
|
className="grid-template-rows-[auto,minmax(0px,1fr)] absolute z-10 grid h-[500px] w-72 rounded-lg bg-denim-300"
|
||||||
|
style={{
|
||||||
|
right: distanceFromRight,
|
||||||
|
bottom: distanceFromBottom,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ShowPopout popoutId={videoInterface.popout ?? ""} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
15
src/video/components/popouts/Popouts.css
Normal file
15
src/video/components/popouts/Popouts.css
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
.popout-wrapper ::-webkit-scrollbar-track {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popout-wrapper ::-webkit-scrollbar-thumb {
|
||||||
|
background-color: theme("colors.denim-500");
|
||||||
|
border: 5px solid transparent;
|
||||||
|
border-left: 0;
|
||||||
|
background-clip: content-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popout-wrapper ::-webkit-scrollbar {
|
||||||
|
/* For some reason the styles don't get applied without the width */
|
||||||
|
width: 13px;
|
||||||
|
}
|
@ -59,6 +59,7 @@ export function useControls(
|
|||||||
},
|
},
|
||||||
closePopout() {
|
closePopout() {
|
||||||
state.interface.popout = null;
|
state.interface.popout = null;
|
||||||
|
state.interface.popoutBounds = null;
|
||||||
updateInterface(descriptor, state);
|
updateInterface(descriptor, state);
|
||||||
},
|
},
|
||||||
setFocused(focused) {
|
setFocused(focused) {
|
||||||
|
@ -8,6 +8,7 @@ export type VideoInterfaceEvent = {
|
|||||||
leftControlHovering: boolean;
|
leftControlHovering: boolean;
|
||||||
isFocused: boolean;
|
isFocused: boolean;
|
||||||
isFullscreen: boolean;
|
isFullscreen: boolean;
|
||||||
|
popoutBounds: null | DOMRect;
|
||||||
};
|
};
|
||||||
|
|
||||||
function getInterfaceFromState(state: VideoPlayerState): VideoInterfaceEvent {
|
function getInterfaceFromState(state: VideoPlayerState): VideoInterfaceEvent {
|
||||||
@ -16,6 +17,7 @@ function getInterfaceFromState(state: VideoPlayerState): VideoInterfaceEvent {
|
|||||||
leftControlHovering: state.interface.leftControlHovering,
|
leftControlHovering: state.interface.leftControlHovering,
|
||||||
isFocused: state.interface.isFocused,
|
isFocused: state.interface.isFocused,
|
||||||
isFullscreen: state.interface.isFullscreen,
|
isFullscreen: state.interface.isFullscreen,
|
||||||
|
popoutBounds: state.interface.popoutBounds,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -23,6 +23,7 @@ export type VideoPlayerState = {
|
|||||||
popout: string | null; // id of current popout (eg source select, episode select)
|
popout: string | null; // id of current popout (eg source select, episode select)
|
||||||
isFocused: boolean; // is the video player the users focus? (shortcuts only works when its focused)
|
isFocused: boolean; // is the video player the users focus? (shortcuts only works when its focused)
|
||||||
leftControlHovering: boolean; // is the cursor hovered over the left side of player controls
|
leftControlHovering: boolean; // is the cursor hovered over the left side of player controls
|
||||||
|
popoutBounds: null | DOMRect; // bounding box of current popout
|
||||||
};
|
};
|
||||||
|
|
||||||
// state related to the playing state of the media
|
// state related to the playing state of the media
|
||||||
|
Loading…
Reference in New Issue
Block a user