mirror of
https://github.com/movie-web/movie-web.git
synced 2025-01-27 23:35:28 +01:00
Floating popout router
Co-authored-by: mrjvs <mistrjvs@gmail.com>
This commit is contained in:
parent
b9a9db348b
commit
f72d6db253
@ -4,7 +4,13 @@ import {
|
|||||||
TransitionClasses,
|
TransitionClasses,
|
||||||
} from "@headlessui/react";
|
} 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 {
|
interface Props {
|
||||||
show?: boolean;
|
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") {
|
if (animation === "fade") {
|
||||||
return {
|
return {
|
||||||
leave: `transition-[transform,opacity] ${duration}`,
|
leave: `transition-[transform,opacity] ${duration}`,
|
||||||
|
@ -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: true,
|
subtree: false,
|
||||||
});
|
});
|
||||||
return () => {
|
return () => {
|
||||||
observer.disconnect();
|
observer.disconnect();
|
||||||
|
@ -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="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>
|
<Transition animation="fade" isChild>
|
||||||
<div
|
<div
|
||||||
onClick={click}
|
onClick={click}
|
||||||
|
@ -8,16 +8,22 @@ interface Props {
|
|||||||
className?: string;
|
className?: string;
|
||||||
height?: number;
|
height?: number;
|
||||||
width?: number;
|
width?: number;
|
||||||
|
active?: boolean; // true if a child view is loaded
|
||||||
}
|
}
|
||||||
|
|
||||||
export function FloatingView(props: Props) {
|
export function FloatingView(props: Props) {
|
||||||
const { isMobile } = useIsMobile();
|
const { isMobile } = useIsMobile();
|
||||||
const width = !isMobile ? `${props.width}px` : "100%";
|
const width = !isMobile ? `${props.width}px` : "100%";
|
||||||
return (
|
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
|
<div
|
||||||
className={[props.className ?? "", "absolute left-0 top-0"].join(" ")}
|
className={[props.className ?? ""].join(" ")}
|
||||||
data-floating-page="true"
|
data-floating-page={props.show ? "true" : undefined}
|
||||||
style={{
|
style={{
|
||||||
height: props.height ? `${props.height}px` : undefined,
|
height: props.height ? `${props.height}px` : undefined,
|
||||||
width: props.width ? width : undefined,
|
width: props.width ? width : undefined,
|
||||||
|
60
src/hooks/useFloatingRouter.ts
Normal file
60
src/hooks/useFloatingRouter.ts
Normal 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,
|
||||||
|
};
|
||||||
|
}
|
@ -2,10 +2,10 @@ import { Icons } from "@/components/Icon";
|
|||||||
import { useVideoPlayerDescriptor } from "@/video/state/hooks";
|
import { useVideoPlayerDescriptor } from "@/video/state/hooks";
|
||||||
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 { useIsMobile } from "@/hooks/useIsMobile";
|
import { useIsMobile } from "@/hooks/useIsMobile";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { FloatingAnchor } from "@/components/popout/FloatingAnchor";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
className?: string;
|
className?: string;
|
||||||
@ -21,7 +21,7 @@ export function SettingsAction(props: Props) {
|
|||||||
return (
|
return (
|
||||||
<div className={props.className}>
|
<div className={props.className}>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<PopoutAnchor for="settings">
|
<FloatingAnchor id="settings">
|
||||||
<VideoPlayerIconButton
|
<VideoPlayerIconButton
|
||||||
active={videoInterface.popout === "settings"}
|
active={videoInterface.popout === "settings"}
|
||||||
className={props.className}
|
className={props.className}
|
||||||
@ -33,7 +33,7 @@ export function SettingsAction(props: Props) {
|
|||||||
}
|
}
|
||||||
icon={Icons.GEAR}
|
icon={Icons.GEAR}
|
||||||
/>
|
/>
|
||||||
</PopoutAnchor>
|
</FloatingAnchor>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -100,7 +100,7 @@ export function EpisodeSelectionPopout() {
|
|||||||
}, [isPickingSeason]);
|
}, [isPickingSeason]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FloatingView show height={300} width={500}>
|
<FloatingView show height={500} width={320}>
|
||||||
<div className="grid h-full grid-rows-[auto,minmax(0,1fr)]">
|
<div className="grid h-full grid-rows-[auto,minmax(0,1fr)]">
|
||||||
<PopoutSection className="bg-ash-100 font-bold text-white">
|
<PopoutSection className="bg-ash-100 font-bold text-white">
|
||||||
<div className="relative flex items-center">
|
<div className="relative flex items-center">
|
||||||
|
@ -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 { DownloadAction } from "@/video/components/actions/list-entries/DownloadAction";
|
||||||
import { useState } from "react";
|
|
||||||
import { CaptionsSelectionAction } from "../actions/CaptionsSelectionAction";
|
import { CaptionsSelectionAction } from "../actions/CaptionsSelectionAction";
|
||||||
import { SourceSelectionAction } from "../actions/SourceSelectionAction";
|
import { SourceSelectionAction } from "../actions/SourceSelectionAction";
|
||||||
import { CaptionSelectionPopout } from "./CaptionSelectionPopout";
|
import { CaptionSelectionPopout } from "./CaptionSelectionPopout";
|
||||||
import { PopoutSection } from "./PopoutUtils";
|
import { PopoutSection } from "./PopoutUtils";
|
||||||
import { SourceSelectionPopout } from "./SourceSelectionPopout";
|
|
||||||
|
|
||||||
export function SettingsPopout() {
|
function TestPopout(props: { router: ReturnType<typeof useFloatingRouter> }) {
|
||||||
const [popoutId, setPopoutId] = useState("");
|
const isCollapsed = props.router.isLoaded("embed");
|
||||||
|
|
||||||
if (popoutId === "source") return <SourceSelectionPopout />;
|
|
||||||
if (popoutId === "captions") return <CaptionSelectionPopout />;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PopoutSection>
|
<div>
|
||||||
<DownloadAction />
|
<p onClick={() => props.router.navigate("/")}>go back</p>
|
||||||
<SourceSelectionAction onClick={() => setPopoutId("source")} />
|
<p>{isCollapsed ? "opened" : "closed"}</p>
|
||||||
<CaptionsSelectionAction onClick={() => setPopoutId("captions")} />
|
<p onClick={() => props.router.navigate("/source/embed")}>Open</p>
|
||||||
</PopoutSection>
|
</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>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -3,12 +3,13 @@ import { FloatingAnchor } from "@/components/popout/FloatingAnchor";
|
|||||||
import { PopoutFloatingCard } from "@/components/popout/FloatingCard";
|
import { PopoutFloatingCard } from "@/components/popout/FloatingCard";
|
||||||
import { FloatingContainer } from "@/components/popout/FloatingContainer";
|
import { FloatingContainer } from "@/components/popout/FloatingContainer";
|
||||||
import { FloatingView } from "@/components/popout/FloatingView";
|
import { FloatingView } from "@/components/popout/FloatingView";
|
||||||
|
import { useFloatingRouter } from "@/hooks/useFloatingRouter";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
// simple empty view, perfect for putting in tests
|
// simple empty view, perfect for putting in tests
|
||||||
export function TestView() {
|
export function TestView() {
|
||||||
const [show, setShow] = useState(false);
|
const [show, setShow] = useState(false);
|
||||||
const [page, setPage] = useState("main");
|
const { pageProps, navigate } = useFloatingRouter();
|
||||||
const [left, setLeft] = useState(600);
|
const [left, setLeft] = useState(600);
|
||||||
const direction = useRef(1);
|
const direction = useRef(1);
|
||||||
|
|
||||||
@ -34,21 +35,30 @@ export function TestView() {
|
|||||||
<FloatingContainer show={show} onClose={() => setShow(false)}>
|
<FloatingContainer show={show} onClose={() => setShow(false)}>
|
||||||
<PopoutFloatingCard for="test" onClose={() => setShow(false)}>
|
<PopoutFloatingCard for="test" onClose={() => setShow(false)}>
|
||||||
<FloatingView
|
<FloatingView
|
||||||
show={page === "main"}
|
{...pageProps("/")}
|
||||||
height={400}
|
height={400}
|
||||||
width={400}
|
width={400}
|
||||||
className="bg-ash-200"
|
className="bg-ash-200"
|
||||||
>
|
>
|
||||||
<p>Hello world</p>
|
<p>Hello world</p>
|
||||||
<Button onClick={() => setPage("second")}>Next</Button>
|
<Button onClick={() => navigate("/second")}>Next</Button>
|
||||||
</FloatingView>
|
</FloatingView>
|
||||||
<FloatingView
|
<FloatingView
|
||||||
show={page === "second"}
|
{...pageProps("second")}
|
||||||
height={300}
|
height={300}
|
||||||
width={500}
|
width={500}
|
||||||
className="bg-ash-200"
|
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>
|
</FloatingView>
|
||||||
</PopoutFloatingCard>
|
</PopoutFloatingCard>
|
||||||
</FloatingContainer>
|
</FloatingContainer>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user