mirror of
https://github.com/sussy-code/smov.git
synced 2024-12-30 16:17:41 +01:00
Floating popout router
Co-authored-by: mrjvs <mistrjvs@gmail.com>
This commit is contained in:
parent
b9a9db348b
commit
f72d6db253
9 changed files with 157 additions and 27 deletions
|
@ -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}`,
|
||||
|
|
|
@ -54,7 +54,7 @@ function CardBase(props: { children: ReactNode }) {
|
|||
observer.observe(ref.current, {
|
||||
attributes: false,
|
||||
childList: true,
|
||||
subtree: true,
|
||||
subtree: false,
|
||||
});
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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,
|
||||
|
|
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 { 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>
|
||||
);
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in a new issue