mirror of
https://github.com/sussy-code/smov.git
synced 2024-12-30 16:17:41 +01:00
Move episodes over into new popout
Co-authored-by: mrjvs <mistrjvs@gmail.com>
This commit is contained in:
parent
cc51559c29
commit
b9a9db348b
10 changed files with 164 additions and 278 deletions
|
@ -5,7 +5,7 @@ export function createFloatingAnchorEvent(id: string): string {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
for: string;
|
id: string;
|
||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -26,9 +26,9 @@ export function FloatingAnchor(props: Props) {
|
||||||
const newerStr = JSON.stringify(newer);
|
const newerStr = JSON.stringify(newer);
|
||||||
if (current !== newerStr) {
|
if (current !== newerStr) {
|
||||||
old.current = newerStr;
|
old.current = newerStr;
|
||||||
const evtStr = createFloatingAnchorEvent(props.for);
|
const evtStr = createFloatingAnchorEvent(props.id);
|
||||||
(window as any)[evtStr] = newer;
|
(window as any)[evtStr] = newer;
|
||||||
const evObj = new CustomEvent(createFloatingAnchorEvent(props.for), {
|
const evObj = new CustomEvent(createFloatingAnchorEvent(props.id), {
|
||||||
detail: newer,
|
detail: newer,
|
||||||
});
|
});
|
||||||
document.dispatchEvent(evObj);
|
document.dispatchEvent(evObj);
|
||||||
|
|
|
@ -7,7 +7,7 @@ import { ReactNode, useCallback, useEffect, useRef } from "react";
|
||||||
interface FloatingCardProps {
|
interface FloatingCardProps {
|
||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
onClose?: () => void;
|
onClose?: () => void;
|
||||||
id: string;
|
for: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface RootFloatingCardProps extends FloatingCardProps {
|
interface RootFloatingCardProps extends FloatingCardProps {
|
||||||
|
@ -27,7 +27,7 @@ function CardBase(props: { children: ReactNode }) {
|
||||||
const getNewHeight = useCallback(() => {
|
const getNewHeight = useCallback(() => {
|
||||||
if (!ref.current) return;
|
if (!ref.current) return;
|
||||||
const children = ref.current.querySelectorAll(
|
const children = ref.current.querySelectorAll(
|
||||||
":scope > *[data-floating-page='true']"
|
":scope *[data-floating-page='true']"
|
||||||
);
|
);
|
||||||
if (children.length === 0) {
|
if (children.length === 0) {
|
||||||
height.start(0);
|
height.start(0);
|
||||||
|
@ -54,7 +54,7 @@ function CardBase(props: { children: ReactNode }) {
|
||||||
observer.observe(ref.current, {
|
observer.observe(ref.current, {
|
||||||
attributes: false,
|
attributes: false,
|
||||||
childList: true,
|
childList: true,
|
||||||
subtree: false,
|
subtree: true,
|
||||||
});
|
});
|
||||||
return () => {
|
return () => {
|
||||||
observer.disconnect();
|
observer.disconnect();
|
||||||
|
@ -90,12 +90,17 @@ export function FloatingCard(props: RootFloatingCardProps) {
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FloatingCardAnchorPosition id={props.id} className={props.className}>
|
<FloatingCardAnchorPosition id={props.for} className={props.className}>
|
||||||
{content}
|
{content}
|
||||||
</FloatingCardAnchorPosition>
|
</FloatingCardAnchorPosition>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function PopoutFloatingCard(props: FloatingCardProps) {
|
export function PopoutFloatingCard(props: FloatingCardProps) {
|
||||||
return <FloatingCard className="rounded-md bg-ash-400 p-2" {...props} />;
|
return (
|
||||||
|
<FloatingCard
|
||||||
|
className="overflow-hidden rounded-md bg-ash-300"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,7 +36,7 @@ export function FloatingContainer(props: Props) {
|
||||||
|
|
||||||
return createPortal(
|
return createPortal(
|
||||||
<Transition show={props.show} animation="none">
|
<Transition show={props.show} animation="none">
|
||||||
<div className="pointer-events-auto fixed inset-0">
|
<div className="popout-wrapper pointer-events-auto fixed inset-0 select-none">
|
||||||
<Transition animation="fade" isChild>
|
<Transition animation="fade" isChild>
|
||||||
<div
|
<div
|
||||||
onClick={click}
|
onClick={click}
|
||||||
|
|
|
@ -6,28 +6,23 @@ interface Props {
|
||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
show?: boolean;
|
show?: boolean;
|
||||||
className?: string;
|
className?: string;
|
||||||
height: number;
|
height?: number;
|
||||||
width: number;
|
width?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function FloatingView(props: Props) {
|
export function FloatingView(props: Props) {
|
||||||
const { isMobile } = useIsMobile();
|
const { isMobile } = useIsMobile();
|
||||||
if (!props.show) return null;
|
const width = !isMobile ? `${props.width}px` : "100%";
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={[props.className ?? "", "absolute"].join(" ")}
|
|
||||||
data-floating-page="true"
|
|
||||||
style={{
|
|
||||||
height: `${props.height}px`,
|
|
||||||
width: !isMobile ? `${props.width}px` : "100%",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{props.children}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
return (
|
return (
|
||||||
<Transition animation="slide-up" show={props.show}>
|
<Transition animation="slide-up" show={props.show}>
|
||||||
<div data-floating-page="true" className={props.className}>
|
<div
|
||||||
|
className={[props.className ?? "", "absolute left-0 top-0"].join(" ")}
|
||||||
|
data-floating-page="true"
|
||||||
|
style={{
|
||||||
|
height: props.height ? `${props.height}px` : undefined,
|
||||||
|
width: props.width ? width : undefined,
|
||||||
|
}}
|
||||||
|
>
|
||||||
{props.children}
|
{props.children}
|
||||||
</div>
|
</div>
|
||||||
</Transition>
|
</Transition>
|
||||||
|
|
|
@ -10,7 +10,6 @@ interface MobilePositionProps {
|
||||||
|
|
||||||
export function FloatingCardMobilePosition(props: MobilePositionProps) {
|
export function FloatingCardMobilePosition(props: MobilePositionProps) {
|
||||||
const ref = useRef<HTMLDivElement>(null);
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
const height = 500;
|
|
||||||
const closing = useRef<boolean>(false);
|
const closing = useRef<boolean>(false);
|
||||||
const [cardRect, setCardRect] = useState<DOMRect | null>(null);
|
const [cardRect, setCardRect] = useState<DOMRect | null>(null);
|
||||||
const [{ y }, api] = useSpring(() => ({
|
const [{ y }, api] = useSpring(() => ({
|
||||||
|
@ -24,6 +23,7 @@ export function FloatingCardMobilePosition(props: MobilePositionProps) {
|
||||||
const bind = useDrag(
|
const bind = useDrag(
|
||||||
({ last, velocity: [, vy], direction: [, dy], movement: [, my] }) => {
|
({ last, velocity: [, vy], direction: [, dy], movement: [, my] }) => {
|
||||||
if (closing.current) return;
|
if (closing.current) return;
|
||||||
|
const height = cardRect?.height ?? 0;
|
||||||
if (last) {
|
if (last) {
|
||||||
// if past half height downwards
|
// if past half height downwards
|
||||||
// OR Y velocity is past 0.5 AND going down AND 20 pixels below start position
|
// OR Y velocity is past 0.5 AND going down AND 20 pixels below start position
|
||||||
|
@ -84,7 +84,7 @@ export function FloatingCardMobilePosition(props: MobilePositionProps) {
|
||||||
}}
|
}}
|
||||||
{...bind()}
|
{...bind()}
|
||||||
>
|
>
|
||||||
<div className="mx-auto my-2 mb-4 h-1 w-12 rounded-full bg-ash-500 bg-opacity-30" />
|
<div className="mx-auto my-2 mb-2 h-1 w-12 rounded-full bg-ash-500 bg-opacity-30" />
|
||||||
{props.children}
|
{props.children}
|
||||||
<div className="h-[200px]" />
|
<div className="h-[200px]" />
|
||||||
</animated.div>
|
</animated.div>
|
||||||
|
|
|
@ -4,9 +4,9 @@ import { useVideoPlayerDescriptor } from "@/video/state/hooks";
|
||||||
import { useMeta } from "@/video/state/logic/meta";
|
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 { PopoutAnchor } from "@/video/components/popouts/PopoutAnchor";
|
|
||||||
import { useInterface } from "@/video/state/logic/interface";
|
import { useInterface } from "@/video/state/logic/interface";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { FloatingAnchor } from "@/components/popout/FloatingAnchor";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
className?: string;
|
className?: string;
|
||||||
|
@ -24,7 +24,7 @@ export function SeriesSelectionAction(props: Props) {
|
||||||
return (
|
return (
|
||||||
<div className={props.className}>
|
<div className={props.className}>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<PopoutAnchor for="episodes">
|
<FloatingAnchor id="episodes">
|
||||||
<VideoPlayerIconButton
|
<VideoPlayerIconButton
|
||||||
active={videoInterface.popout === "episodes"}
|
active={videoInterface.popout === "episodes"}
|
||||||
icon={Icons.EPISODES}
|
icon={Icons.EPISODES}
|
||||||
|
@ -32,7 +32,7 @@ export function SeriesSelectionAction(props: Props) {
|
||||||
wide
|
wide
|
||||||
onClick={() => controls.openPopout("episodes")}
|
onClick={() => controls.openPopout("episodes")}
|
||||||
/>
|
/>
|
||||||
</PopoutAnchor>
|
</FloatingAnchor>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -12,6 +12,7 @@ import { useMeta } from "@/video/state/logic/meta";
|
||||||
import { useControls } from "@/video/state/logic/controls";
|
import { useControls } from "@/video/state/logic/controls";
|
||||||
import { useWatchedContext } from "@/state/watched";
|
import { useWatchedContext } from "@/state/watched";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { FloatingView } from "@/components/popout/FloatingView";
|
||||||
import { PopoutListEntry, PopoutSection } from "./PopoutUtils";
|
import { PopoutListEntry, PopoutSection } from "./PopoutUtils";
|
||||||
|
|
||||||
export function EpisodeSelectionPopout() {
|
export function EpisodeSelectionPopout() {
|
||||||
|
@ -99,110 +100,112 @@ export function EpisodeSelectionPopout() {
|
||||||
}, [isPickingSeason]);
|
}, [isPickingSeason]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<FloatingView show height={300} width={500}>
|
||||||
<PopoutSection className="bg-ash-100 font-bold text-white">
|
<div className="grid h-full grid-rows-[auto,minmax(0,1fr)]">
|
||||||
<div className="relative flex items-center">
|
<PopoutSection className="bg-ash-100 font-bold text-white">
|
||||||
<button
|
<div className="relative flex items-center">
|
||||||
className={[
|
<button
|
||||||
"-m-1.5 rounded-lg p-1.5 transition-opacity duration-100 hover:bg-ash-200",
|
className={[
|
||||||
isPickingSeason ? "pointer-events-none opacity-0" : "opacity-1",
|
"-m-1.5 rounded-lg p-1.5 transition-opacity duration-100 hover:bg-ash-200",
|
||||||
].join(" ")}
|
isPickingSeason ? "pointer-events-none opacity-0" : "opacity-1",
|
||||||
onClick={toggleIsPickingSeason}
|
].join(" ")}
|
||||||
type="button"
|
onClick={toggleIsPickingSeason}
|
||||||
>
|
type="button"
|
||||||
<Icon icon={Icons.CHEVRON_LEFT} />
|
>
|
||||||
</button>
|
<Icon icon={Icons.CHEVRON_LEFT} />
|
||||||
<span
|
</button>
|
||||||
className={[
|
<span
|
||||||
titlePositionClass,
|
className={[
|
||||||
!isPickingSeason ? "opacity-1" : "opacity-0",
|
titlePositionClass,
|
||||||
].join(" ")}
|
!isPickingSeason ? "opacity-1" : "opacity-0",
|
||||||
>
|
].join(" ")}
|
||||||
{currentSeasonInfo?.title || ""}
|
>
|
||||||
</span>
|
{currentSeasonInfo?.title || ""}
|
||||||
<span
|
</span>
|
||||||
className={[
|
<span
|
||||||
titlePositionClass,
|
className={[
|
||||||
isPickingSeason ? "opacity-1" : "opacity-0",
|
titlePositionClass,
|
||||||
].join(" ")}
|
isPickingSeason ? "opacity-1" : "opacity-0",
|
||||||
>
|
].join(" ")}
|
||||||
{t("videoPlayer.popouts.seasons")}
|
>
|
||||||
</span>
|
{t("videoPlayer.popouts.seasons")}
|
||||||
</div>
|
</span>
|
||||||
</PopoutSection>
|
</div>
|
||||||
<div className="relative grid h-full grid-rows-[minmax(1px,1fr)]">
|
|
||||||
<PopoutSection
|
|
||||||
className={[
|
|
||||||
"absolute inset-0 z-30 overflow-y-auto border-ash-400 bg-ash-100 transition-[max-height,padding] duration-200",
|
|
||||||
isPickingSeason
|
|
||||||
? "max-h-full border-t"
|
|
||||||
: "max-h-0 overflow-hidden py-0",
|
|
||||||
].join(" ")}
|
|
||||||
>
|
|
||||||
{currentSeasonInfo
|
|
||||||
? meta?.seasons?.map?.((season) => (
|
|
||||||
<PopoutListEntry
|
|
||||||
key={season.id}
|
|
||||||
active={meta?.episode?.seasonId === season.id}
|
|
||||||
onClick={() => setSeason(season.id)}
|
|
||||||
isOnDarkBackground
|
|
||||||
>
|
|
||||||
{season.title}
|
|
||||||
</PopoutListEntry>
|
|
||||||
))
|
|
||||||
: "No season"}
|
|
||||||
</PopoutSection>
|
</PopoutSection>
|
||||||
<PopoutSection className="relative h-full overflow-y-auto">
|
<div className="relative grid h-full grid-rows-[minmax(1px,1fr)]">
|
||||||
{loading ? (
|
<PopoutSection
|
||||||
<div className="flex h-full w-full items-center justify-center">
|
className={[
|
||||||
<Loading />
|
"absolute inset-0 z-30 overflow-y-auto border-ash-400 bg-ash-100 transition-[max-height,padding] duration-200",
|
||||||
</div>
|
isPickingSeason
|
||||||
) : error ? (
|
? "max-h-full border-t"
|
||||||
<div className="flex h-full w-full items-center justify-center">
|
: "max-h-0 overflow-hidden py-0",
|
||||||
<div className="flex flex-col flex-wrap items-center text-slate-400">
|
].join(" ")}
|
||||||
<IconPatch
|
>
|
||||||
icon={Icons.EYE_SLASH}
|
{currentSeasonInfo
|
||||||
className="text-xl text-bink-600"
|
? meta?.seasons?.map?.((season) => (
|
||||||
/>
|
<PopoutListEntry
|
||||||
<p className="mt-6 w-full text-center">
|
key={season.id}
|
||||||
{t("videoPLayer.popouts.errors.loadingWentWrong", {
|
active={meta?.episode?.seasonId === season.id}
|
||||||
seasonTitle: currentSeasonInfo?.title?.toLowerCase(),
|
onClick={() => setSeason(season.id)}
|
||||||
})}
|
isOnDarkBackground
|
||||||
</p>
|
>
|
||||||
|
{season.title}
|
||||||
|
</PopoutListEntry>
|
||||||
|
))
|
||||||
|
: "No season"}
|
||||||
|
</PopoutSection>
|
||||||
|
<PopoutSection className="relative h-full overflow-y-auto">
|
||||||
|
{loading ? (
|
||||||
|
<div className="flex h-full w-full items-center justify-center">
|
||||||
|
<Loading />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
) : error ? (
|
||||||
) : (
|
<div className="flex h-full w-full items-center justify-center">
|
||||||
<div>
|
<div className="flex flex-col flex-wrap items-center text-slate-400">
|
||||||
{currentSeasonEpisodes && currentSeasonInfo
|
<IconPatch
|
||||||
? currentSeasonEpisodes.map((e) => (
|
icon={Icons.EYE_SLASH}
|
||||||
<PopoutListEntry
|
className="text-xl text-bink-600"
|
||||||
key={e.id}
|
/>
|
||||||
active={e.id === meta?.episode?.episodeId}
|
<p className="mt-6 w-full text-center">
|
||||||
onClick={() => {
|
{t("videoPLayer.popouts.errors.loadingWentWrong", {
|
||||||
if (e.id === meta?.episode?.episodeId)
|
seasonTitle: currentSeasonInfo?.title?.toLowerCase(),
|
||||||
controls.closePopout();
|
})}
|
||||||
else setCurrent(currentSeasonInfo.id, e.id);
|
</p>
|
||||||
}}
|
</div>
|
||||||
percentageCompleted={
|
</div>
|
||||||
watched.items.find(
|
) : (
|
||||||
(item) =>
|
<div>
|
||||||
item.item?.series?.seasonId ===
|
{currentSeasonEpisodes && currentSeasonInfo
|
||||||
currentSeasonInfo.id &&
|
? currentSeasonEpisodes.map((e) => (
|
||||||
item.item?.series?.episodeId === e.id
|
<PopoutListEntry
|
||||||
)?.percentage
|
key={e.id}
|
||||||
}
|
active={e.id === meta?.episode?.episodeId}
|
||||||
>
|
onClick={() => {
|
||||||
{t("videoPlayer.popouts.episode", {
|
if (e.id === meta?.episode?.episodeId)
|
||||||
index: e.number,
|
controls.closePopout();
|
||||||
title: e.title,
|
else setCurrent(currentSeasonInfo.id, e.id);
|
||||||
})}
|
}}
|
||||||
</PopoutListEntry>
|
percentageCompleted={
|
||||||
))
|
watched.items.find(
|
||||||
: "No episodes"}
|
(item) =>
|
||||||
</div>
|
item.item?.series?.seasonId ===
|
||||||
)}
|
currentSeasonInfo.id &&
|
||||||
</PopoutSection>
|
item.item?.series?.episodeId === e.id
|
||||||
|
)?.percentage
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{t("videoPlayer.popouts.episode", {
|
||||||
|
index: e.number,
|
||||||
|
title: e.title,
|
||||||
|
})}
|
||||||
|
</PopoutListEntry>
|
||||||
|
))
|
||||||
|
: "No episodes"}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</PopoutSection>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</FloatingView>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,3 @@
|
||||||
import { useDrag } from "@use-gesture/react";
|
|
||||||
import { a, useSpring, config, easings } from "@react-spring/web";
|
|
||||||
import { Transition } from "@/components/Transition";
|
|
||||||
import { useSyncPopouts } from "@/video/components/hooks/useSyncPopouts";
|
import { useSyncPopouts } from "@/video/components/hooks/useSyncPopouts";
|
||||||
import { EpisodeSelectionPopout } from "@/video/components/popouts/EpisodeSelectionPopout";
|
import { EpisodeSelectionPopout } from "@/video/components/popouts/EpisodeSelectionPopout";
|
||||||
import { SourceSelectionPopout } from "@/video/components/popouts/SourceSelectionPopout";
|
import { SourceSelectionPopout } from "@/video/components/popouts/SourceSelectionPopout";
|
||||||
|
@ -8,130 +5,35 @@ import { CaptionSelectionPopout } from "@/video/components/popouts/CaptionSelect
|
||||||
import { SettingsPopout } from "@/video/components/popouts/SettingsPopout";
|
import { SettingsPopout } from "@/video/components/popouts/SettingsPopout";
|
||||||
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 { useIsMobile } from "@/hooks/useIsMobile";
|
import { useInterface } from "@/video/state/logic/interface";
|
||||||
import {
|
import { useCallback } from "react";
|
||||||
useInterface,
|
import { PopoutFloatingCard } from "@/components/popout/FloatingCard";
|
||||||
VideoInterfaceEvent,
|
import { FloatingContainer } from "@/components/popout/FloatingContainer";
|
||||||
} from "@/video/state/logic/interface";
|
|
||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
|
||||||
|
|
||||||
import "./Popouts.css";
|
import "./Popouts.css";
|
||||||
|
|
||||||
function ShowPopout(props: { popoutId: string | null }) {
|
function ShowPopout(props: { popoutId: string | null; onClose: () => void }) {
|
||||||
// only updates popout id when a new one is set, so transitions look good
|
const popoutMap = {
|
||||||
const [popoutId, setPopoutId] = useState<string | null>(props.popoutId);
|
source: <SourceSelectionPopout />,
|
||||||
useEffect(() => {
|
captions: <CaptionSelectionPopout />,
|
||||||
if (!props.popoutId) return;
|
settings: <SettingsPopout />,
|
||||||
setPopoutId(props.popoutId);
|
episodes: <EpisodeSelectionPopout />,
|
||||||
}, [props]);
|
};
|
||||||
|
|
||||||
if (popoutId === "episodes") return <EpisodeSelectionPopout />;
|
|
||||||
if (popoutId === "source") return <SourceSelectionPopout />;
|
|
||||||
if (popoutId === "captions") return <CaptionSelectionPopout />;
|
|
||||||
if (popoutId === "settings") return <SettingsPopout />;
|
|
||||||
return (
|
|
||||||
<div className="flex w-full items-center justify-center p-10">
|
|
||||||
Unknown popout
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function MobilePopoutContainer(props: {
|
|
||||||
videoInterface: VideoInterfaceEvent;
|
|
||||||
onClose: () => void;
|
|
||||||
}) {
|
|
||||||
const ref = useRef<HTMLDivElement>(null);
|
|
||||||
const height = 500;
|
|
||||||
const closing = useRef<boolean>(false);
|
|
||||||
const [{ y }, api] = useSpring(() => ({
|
|
||||||
y: 0,
|
|
||||||
onRest() {
|
|
||||||
if (!closing.current) return;
|
|
||||||
props.onClose();
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
const bind = useDrag(
|
|
||||||
({ last, velocity: [, vy], direction: [, dy], movement: [, my] }) => {
|
|
||||||
if (closing.current) return;
|
|
||||||
if (last) {
|
|
||||||
if (my > height * 0.5 || (vy > 0.5 && dy > 0)) {
|
|
||||||
api.start({
|
|
||||||
y: height * 1.2,
|
|
||||||
immediate: false,
|
|
||||||
config: { ...config.wobbly, velocity: vy, clamp: true },
|
|
||||||
});
|
|
||||||
closing.current = true;
|
|
||||||
} else {
|
|
||||||
api.start({
|
|
||||||
y: 0,
|
|
||||||
immediate: false,
|
|
||||||
config: config.wobbly,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
api.start({ y: my, immediate: true });
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
from: () => [0, y.get()],
|
|
||||||
filterTaps: true,
|
|
||||||
bounds: { top: 0 },
|
|
||||||
rubberband: true,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<a.div
|
<>
|
||||||
ref={ref}
|
{Object.entries(popoutMap).map(([id, el]) => (
|
||||||
className="absolute inset-x-0 -bottom-[200px] z-10 mx-auto grid h-[700px] max-w-[400px] touch-none grid-rows-[auto,minmax(0,1fr)] overflow-hidden rounded-t-lg bg-ash-200"
|
<FloatingContainer
|
||||||
style={{
|
key={id}
|
||||||
y,
|
show={props.popoutId === id}
|
||||||
}}
|
onClose={props.onClose}
|
||||||
{...bind()}
|
>
|
||||||
>
|
<PopoutFloatingCard for={id} onClose={props.onClose}>
|
||||||
<div className="mx-auto mt-3 -mb-3 h-1 w-12 rounded-full bg-ash-500 bg-opacity-30" />
|
{el}
|
||||||
<ShowPopout popoutId={props.videoInterface.popout} />
|
</PopoutFloatingCard>
|
||||||
</a.div>
|
</FloatingContainer>
|
||||||
);
|
))}
|
||||||
}
|
</>
|
||||||
|
|
||||||
function DesktopPopoutContainer(props: {
|
|
||||||
videoInterface: VideoInterfaceEvent;
|
|
||||||
}) {
|
|
||||||
const ref = useRef<HTMLDivElement>(null);
|
|
||||||
const [right, setRight] = useState<number>(0);
|
|
||||||
const [bottom, setBottom] = useState<number>(0);
|
|
||||||
const [width, setWidth] = useState<number>(0);
|
|
||||||
|
|
||||||
const calculateAndSetCoords = useCallback((rect: DOMRect, w: number) => {
|
|
||||||
const buttonCenter = rect.left + rect.width / 2;
|
|
||||||
|
|
||||||
setBottom(rect ? rect.height + 30 : 30);
|
|
||||||
setRight(Math.max(window.innerWidth - buttonCenter - w / 2, 30));
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!props.videoInterface.popoutBounds) return;
|
|
||||||
calculateAndSetCoords(props.videoInterface.popoutBounds, width);
|
|
||||||
}, [props.videoInterface.popoutBounds, calculateAndSetCoords, width]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const rect = ref.current?.getBoundingClientRect();
|
|
||||||
setWidth(rect?.width ?? 0);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<a.div
|
|
||||||
ref={ref}
|
|
||||||
className="absolute z-10 grid h-[500px] w-80 touch-none grid-rows-[auto,minmax(0,1fr)] overflow-hidden rounded-lg bg-ash-200"
|
|
||||||
style={{
|
|
||||||
right: `${right}px`,
|
|
||||||
bottom: `${bottom}px`,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ShowPopout popoutId={props.videoInterface.popout} />
|
|
||||||
</a.div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -139,30 +41,11 @@ export function PopoutProviderAction() {
|
||||||
const descriptor = useVideoPlayerDescriptor();
|
const descriptor = useVideoPlayerDescriptor();
|
||||||
const videoInterface = useInterface(descriptor);
|
const videoInterface = useInterface(descriptor);
|
||||||
const controls = useControls(descriptor);
|
const controls = useControls(descriptor);
|
||||||
const { isMobile } = useIsMobile(false);
|
|
||||||
useSyncPopouts(descriptor);
|
useSyncPopouts(descriptor);
|
||||||
|
|
||||||
const handleClick = useCallback(() => {
|
const onClose = useCallback(() => {
|
||||||
controls.closePopout();
|
controls.closePopout();
|
||||||
}, [controls]);
|
}, [controls]);
|
||||||
|
|
||||||
return (
|
return <ShowPopout popoutId={videoInterface.popout} onClose={onClose} />;
|
||||||
<Transition
|
|
||||||
show={!!videoInterface.popout}
|
|
||||||
animation="slide-up"
|
|
||||||
className="h-full"
|
|
||||||
>
|
|
||||||
<div className="popout-wrapper pointer-events-auto absolute inset-0">
|
|
||||||
<div onClick={handleClick} className="absolute inset-0" />
|
|
||||||
{isMobile ? (
|
|
||||||
<MobilePopoutContainer
|
|
||||||
videoInterface={videoInterface}
|
|
||||||
onClose={handleClick}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<DesktopPopoutContainer videoInterface={videoInterface} />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</Transition>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -143,7 +143,7 @@ export function PopoutListEntry(props: PopoutListEntryTypes) {
|
||||||
isOnDarkBackground={props.isOnDarkBackground}
|
isOnDarkBackground={props.isOnDarkBackground}
|
||||||
active={props.active}
|
active={props.active}
|
||||||
onClick={props.onClick}
|
onClick={props.onClick}
|
||||||
noChevron={!props.loading && !props.errored}
|
noChevron={props.loading || props.errored}
|
||||||
right={
|
right={
|
||||||
<>
|
<>
|
||||||
{props.errored && (
|
{props.errored && (
|
||||||
|
|
|
@ -32,7 +32,7 @@ export function TestView() {
|
||||||
return (
|
return (
|
||||||
<div className="relative h-[800px] w-full rounded border border-white">
|
<div className="relative h-[800px] w-full rounded border border-white">
|
||||||
<FloatingContainer show={show} onClose={() => setShow(false)}>
|
<FloatingContainer show={show} onClose={() => setShow(false)}>
|
||||||
<PopoutFloatingCard id="test" onClose={() => setShow(false)}>
|
<PopoutFloatingCard for="test" onClose={() => setShow(false)}>
|
||||||
<FloatingView
|
<FloatingView
|
||||||
show={page === "main"}
|
show={page === "main"}
|
||||||
height={400}
|
height={400}
|
||||||
|
@ -58,7 +58,7 @@ export function TestView() {
|
||||||
left: `${left}px`,
|
left: `${left}px`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<FloatingAnchor for="test">
|
<FloatingAnchor id="test">
|
||||||
<div
|
<div
|
||||||
className="h-8 w-8 bg-white"
|
className="h-8 w-8 bg-white"
|
||||||
onClick={() => setShow((v) => !v)}
|
onClick={() => setShow((v) => !v)}
|
||||||
|
|
Loading…
Reference in a new issue