From f72d6db253f281f4019009fb78dc77ff7261a58b Mon Sep 17 00:00:00 2001 From: Jip Fr Date: Tue, 28 Feb 2023 23:36:46 +0100 Subject: [PATCH] Floating popout router Co-authored-by: mrjvs --- src/components/Transition.tsx | 30 +++++++++- src/components/popout/FloatingCard.tsx | 2 +- src/components/popout/FloatingContainer.tsx | 2 +- src/components/popout/FloatingView.tsx | 12 +++- src/hooks/useFloatingRouter.ts | 60 +++++++++++++++++++ .../components/actions/SettingsAction.tsx | 6 +- .../popouts/EpisodeSelectionPopout.tsx | 2 +- .../components/popouts/SettingsPopout.tsx | 50 ++++++++++++---- src/views/developer/TestView.tsx | 20 +++++-- 9 files changed, 157 insertions(+), 27 deletions(-) create mode 100644 src/hooks/useFloatingRouter.ts diff --git a/src/components/Transition.tsx b/src/components/Transition.tsx index cda3b945..f7d0d533 100644 --- a/src/components/Transition.tsx +++ b/src/components/Transition.tsx @@ -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}`, diff --git a/src/components/popout/FloatingCard.tsx b/src/components/popout/FloatingCard.tsx index 6d9a95c5..4e326d51 100644 --- a/src/components/popout/FloatingCard.tsx +++ b/src/components/popout/FloatingCard.tsx @@ -54,7 +54,7 @@ function CardBase(props: { children: ReactNode }) { observer.observe(ref.current, { attributes: false, childList: true, - subtree: true, + subtree: false, }); return () => { observer.disconnect(); diff --git a/src/components/popout/FloatingContainer.tsx b/src/components/popout/FloatingContainer.tsx index eefeb56c..48e4f5cc 100644 --- a/src/components/popout/FloatingContainer.tsx +++ b/src/components/popout/FloatingContainer.tsx @@ -36,7 +36,7 @@ export function FloatingContainer(props: Props) { return createPortal( -
+
+
( + 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, + }; +} diff --git a/src/video/components/actions/SettingsAction.tsx b/src/video/components/actions/SettingsAction.tsx index 34792379..b012639a 100644 --- a/src/video/components/actions/SettingsAction.tsx +++ b/src/video/components/actions/SettingsAction.tsx @@ -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 (
- + - +
); diff --git a/src/video/components/popouts/EpisodeSelectionPopout.tsx b/src/video/components/popouts/EpisodeSelectionPopout.tsx index fceb1ffe..b5dbd4f0 100644 --- a/src/video/components/popouts/EpisodeSelectionPopout.tsx +++ b/src/video/components/popouts/EpisodeSelectionPopout.tsx @@ -100,7 +100,7 @@ export function EpisodeSelectionPopout() { }, [isPickingSeason]); return ( - +
diff --git a/src/video/components/popouts/SettingsPopout.tsx b/src/video/components/popouts/SettingsPopout.tsx index 119db857..76b0afdc 100644 --- a/src/video/components/popouts/SettingsPopout.tsx +++ b/src/video/components/popouts/SettingsPopout.tsx @@ -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 ; - if (popoutId === "captions") return ; +function TestPopout(props: { router: ReturnType }) { + const isCollapsed = props.router.isLoaded("embed"); return ( - - - setPopoutId("source")} /> - setPopoutId("captions")} /> - +
+

props.router.navigate("/")}>go back

+

{isCollapsed ? "opened" : "closed"}

+

props.router.navigate("/source/embed")}>Open

+
+ ); +} + +export function SettingsPopout() { + const floatingRouter = useFloatingRouter(); + const { pageProps, navigate, isLoaded, isActive } = floatingRouter; + + return ( + <> + + + + navigate("/source")} /> + navigate("/captions")} /> + + + + + {/* */} + + + + + ); } diff --git a/src/views/developer/TestView.tsx b/src/views/developer/TestView.tsx index 0c634f2c..c760e8d4 100644 --- a/src/views/developer/TestView.tsx +++ b/src/views/developer/TestView.tsx @@ -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() { setShow(false)}> setShow(false)}>

Hello world

- +
- + + + + +