Floating popout router

Co-authored-by: mrjvs <mistrjvs@gmail.com>
This commit is contained in:
Jip Fr 2023-02-28 23:36:46 +01:00
parent b9a9db348b
commit f72d6db253
9 changed files with 157 additions and 27 deletions

View File

@ -4,7 +4,13 @@ import {
TransitionClasses,
} from "@headlessui/react";
type TransitionAnimations = "slide-down" | "slide-up" | "fade" | "none";
type TransitionAnimations =
| "slide-down"
| "slide-full-left"
| "slide-full-right"
| "slide-up"
| "fade"
| "none";
interface Props {
show?: boolean;
@ -41,6 +47,28 @@ function getClasses(
};
}
if (animation === "slide-full-left") {
return {
leave: `transition-[transform] ${duration}`,
leaveFrom: "translate-x-0",
leaveTo: "-translate-x-full",
enter: `transition-[transform] ${duration}`,
enterFrom: "-translate-x-full",
enterTo: "translate-x-0",
};
}
if (animation === "slide-full-right") {
return {
leave: `transition-[transform] ${duration}`,
leaveFrom: "translate-x-0",
leaveTo: "translate-x-full",
enter: `transition-[transform] ${duration}`,
enterFrom: "translate-x-full",
enterTo: "translate-x-0",
};
}
if (animation === "fade") {
return {
leave: `transition-[transform,opacity] ${duration}`,

View File

@ -54,7 +54,7 @@ function CardBase(props: { children: ReactNode }) {
observer.observe(ref.current, {
attributes: false,
childList: true,
subtree: true,
subtree: false,
});
return () => {
observer.disconnect();

View File

@ -36,7 +36,7 @@ export function FloatingContainer(props: Props) {
return createPortal(
<Transition show={props.show} animation="none">
<div className="popout-wrapper pointer-events-auto fixed inset-0 select-none">
<div className="popout-wrapper pointer-events-auto fixed inset-0 z-[999] select-none">
<Transition animation="fade" isChild>
<div
onClick={click}

View File

@ -8,16 +8,22 @@ interface Props {
className?: string;
height?: number;
width?: number;
active?: boolean; // true if a child view is loaded
}
export function FloatingView(props: Props) {
const { isMobile } = useIsMobile();
const width = !isMobile ? `${props.width}px` : "100%";
return (
<Transition animation="slide-up" show={props.show}>
<Transition
animation={props.active ? "slide-full-left" : "slide-full-right"}
className="absolute inset-0"
durationClass="duration-[400ms]"
show={props.show}
>
<div
className={[props.className ?? "", "absolute left-0 top-0"].join(" ")}
data-floating-page="true"
className={[props.className ?? ""].join(" ")}
data-floating-page={props.show ? "true" : undefined}
style={{
height: props.height ? `${props.height}px` : undefined,
width: props.width ? width : undefined,

View File

@ -0,0 +1,60 @@
import { useLayoutEffect, useState } from "react";
export function useFloatingRouter(initial = "/") {
const [route, setRoute] = useState<string[]>(
initial.split("/").filter((v) => v.length > 0)
);
const [previousRoute, setPreviousRoute] = useState(route);
const currentPage = route[route.length - 1] ?? "/";
useLayoutEffect(() => {
if (previousRoute.length === route.length) return;
// when navigating backwards, we delay the updating by a bit so transitions can be applied correctly
setTimeout(() => {
setPreviousRoute(route);
}, 20);
}, [route, previousRoute]);
function navigate(path: string) {
const newRoute = path.split("/").filter((v) => v.length > 0);
if (newRoute.length > previousRoute.length) setPreviousRoute(newRoute);
setRoute(newRoute);
}
function isActive(page: string) {
if (page === "/") return true;
const index = previousRoute.indexOf(page);
if (index === -1) return false; // not active
if (index === previousRoute.length - 1) return false; // active but latest route so shouldnt be counted as active
return true;
}
function isCurrentPage(page: string) {
return page === currentPage;
}
function isLoaded(page: string) {
if (page === "/") return true;
return route.includes(page);
}
function pageProps(page: string) {
return {
show: isCurrentPage(page),
active: isActive(page),
};
}
function reset() {
navigate("/");
}
return {
navigate,
reset,
isLoaded,
isCurrentPage,
pageProps,
isActive,
};
}

View File

@ -2,10 +2,10 @@ import { Icons } from "@/components/Icon";
import { useVideoPlayerDescriptor } from "@/video/state/hooks";
import { VideoPlayerIconButton } from "@/video/components/parts/VideoPlayerIconButton";
import { useControls } from "@/video/state/logic/controls";
import { PopoutAnchor } from "@/video/components/popouts/PopoutAnchor";
import { useInterface } from "@/video/state/logic/interface";
import { useIsMobile } from "@/hooks/useIsMobile";
import { useTranslation } from "react-i18next";
import { FloatingAnchor } from "@/components/popout/FloatingAnchor";
interface Props {
className?: string;
@ -21,7 +21,7 @@ export function SettingsAction(props: Props) {
return (
<div className={props.className}>
<div className="relative">
<PopoutAnchor for="settings">
<FloatingAnchor id="settings">
<VideoPlayerIconButton
active={videoInterface.popout === "settings"}
className={props.className}
@ -33,7 +33,7 @@ export function SettingsAction(props: Props) {
}
icon={Icons.GEAR}
/>
</PopoutAnchor>
</FloatingAnchor>
</div>
</div>
);

View File

@ -100,7 +100,7 @@ export function EpisodeSelectionPopout() {
}, [isPickingSeason]);
return (
<FloatingView show height={300} width={500}>
<FloatingView show height={500} width={320}>
<div className="grid h-full grid-rows-[auto,minmax(0,1fr)]">
<PopoutSection className="bg-ash-100 font-bold text-white">
<div className="relative flex items-center">

View File

@ -1,22 +1,48 @@
import { FloatingView } from "@/components/popout/FloatingView";
import { useFloatingRouter } from "@/hooks/useFloatingRouter";
import { DownloadAction } from "@/video/components/actions/list-entries/DownloadAction";
import { useState } from "react";
import { CaptionsSelectionAction } from "../actions/CaptionsSelectionAction";
import { SourceSelectionAction } from "../actions/SourceSelectionAction";
import { CaptionSelectionPopout } from "./CaptionSelectionPopout";
import { PopoutSection } from "./PopoutUtils";
import { SourceSelectionPopout } from "./SourceSelectionPopout";
export function SettingsPopout() {
const [popoutId, setPopoutId] = useState("");
if (popoutId === "source") return <SourceSelectionPopout />;
if (popoutId === "captions") return <CaptionSelectionPopout />;
function TestPopout(props: { router: ReturnType<typeof useFloatingRouter> }) {
const isCollapsed = props.router.isLoaded("embed");
return (
<PopoutSection>
<DownloadAction />
<SourceSelectionAction onClick={() => setPopoutId("source")} />
<CaptionsSelectionAction onClick={() => setPopoutId("captions")} />
</PopoutSection>
<div>
<p onClick={() => props.router.navigate("/")}>go back</p>
<p>{isCollapsed ? "opened" : "closed"}</p>
<p onClick={() => props.router.navigate("/source/embed")}>Open</p>
</div>
);
}
export function SettingsPopout() {
const floatingRouter = useFloatingRouter();
const { pageProps, navigate, isLoaded, isActive } = floatingRouter;
return (
<>
<FloatingView {...pageProps("/")} width={320}>
<PopoutSection>
<DownloadAction />
<SourceSelectionAction onClick={() => navigate("/source")} />
<CaptionsSelectionAction onClick={() => navigate("/captions")} />
</PopoutSection>
</FloatingView>
<FloatingView
active={isActive("source")}
show={isLoaded("source")}
height={500}
width={320}
>
<TestPopout router={floatingRouter} />
{/* <SourceSelectionPopout /> */}
</FloatingView>
<FloatingView {...pageProps("captions")} height={500} width={320}>
<CaptionSelectionPopout />
</FloatingView>
</>
);
}

View File

@ -3,12 +3,13 @@ import { FloatingAnchor } from "@/components/popout/FloatingAnchor";
import { PopoutFloatingCard } from "@/components/popout/FloatingCard";
import { FloatingContainer } from "@/components/popout/FloatingContainer";
import { FloatingView } from "@/components/popout/FloatingView";
import { useFloatingRouter } from "@/hooks/useFloatingRouter";
import { useEffect, useRef, useState } from "react";
// simple empty view, perfect for putting in tests
export function TestView() {
const [show, setShow] = useState(false);
const [page, setPage] = useState("main");
const { pageProps, navigate } = useFloatingRouter();
const [left, setLeft] = useState(600);
const direction = useRef(1);
@ -34,21 +35,30 @@ export function TestView() {
<FloatingContainer show={show} onClose={() => setShow(false)}>
<PopoutFloatingCard for="test" onClose={() => setShow(false)}>
<FloatingView
show={page === "main"}
{...pageProps("/")}
height={400}
width={400}
className="bg-ash-200"
>
<p>Hello world</p>
<Button onClick={() => setPage("second")}>Next</Button>
<Button onClick={() => navigate("/second")}>Next</Button>
</FloatingView>
<FloatingView
show={page === "second"}
{...pageProps("second")}
height={300}
width={500}
className="bg-ash-200"
>
<Button onClick={() => setPage("main")}>Previous</Button>
<Button onClick={() => navigate("/")}>Previous</Button>
<Button onClick={() => navigate("/second/third")}>Next</Button>
</FloatingView>
<FloatingView
{...pageProps("third")}
height={300}
width={500}
className="bg-ash-200"
>
<Button onClick={() => navigate("/second")}>Previous</Button>
</FloatingView>
</PopoutFloatingCard>
</FloatingContainer>