From b9a9db348b103a201344af753fcc66ad9b07bf58 Mon Sep 17 00:00:00 2001 From: Jip Fr Date: Tue, 28 Feb 2023 21:32:03 +0100 Subject: [PATCH] Move episodes over into new popout Co-authored-by: mrjvs --- src/components/popout/FloatingAnchor.tsx | 6 +- src/components/popout/FloatingCard.tsx | 15 +- src/components/popout/FloatingContainer.tsx | 2 +- src/components/popout/FloatingView.tsx | 27 +-- .../positions/FloatingCardMobilePosition.tsx | 4 +- .../actions/SeriesSelectionAction.tsx | 6 +- .../popouts/EpisodeSelectionPopout.tsx | 207 +++++++++--------- .../popouts/PopoutProviderAction.tsx | 169 +++----------- src/video/components/popouts/PopoutUtils.tsx | 2 +- src/views/developer/TestView.tsx | 4 +- 10 files changed, 164 insertions(+), 278 deletions(-) diff --git a/src/components/popout/FloatingAnchor.tsx b/src/components/popout/FloatingAnchor.tsx index 1a66af55..3d492957 100644 --- a/src/components/popout/FloatingAnchor.tsx +++ b/src/components/popout/FloatingAnchor.tsx @@ -5,7 +5,7 @@ export function createFloatingAnchorEvent(id: string): string { } interface Props { - for: string; + id: string; children?: ReactNode; } @@ -26,9 +26,9 @@ export function FloatingAnchor(props: Props) { const newerStr = JSON.stringify(newer); if (current !== newerStr) { old.current = newerStr; - const evtStr = createFloatingAnchorEvent(props.for); + const evtStr = createFloatingAnchorEvent(props.id); (window as any)[evtStr] = newer; - const evObj = new CustomEvent(createFloatingAnchorEvent(props.for), { + const evObj = new CustomEvent(createFloatingAnchorEvent(props.id), { detail: newer, }); document.dispatchEvent(evObj); diff --git a/src/components/popout/FloatingCard.tsx b/src/components/popout/FloatingCard.tsx index bb8a3fb3..6d9a95c5 100644 --- a/src/components/popout/FloatingCard.tsx +++ b/src/components/popout/FloatingCard.tsx @@ -7,7 +7,7 @@ import { ReactNode, useCallback, useEffect, useRef } from "react"; interface FloatingCardProps { children?: ReactNode; onClose?: () => void; - id: string; + for: string; } interface RootFloatingCardProps extends FloatingCardProps { @@ -27,7 +27,7 @@ function CardBase(props: { children: ReactNode }) { const getNewHeight = useCallback(() => { if (!ref.current) return; const children = ref.current.querySelectorAll( - ":scope > *[data-floating-page='true']" + ":scope *[data-floating-page='true']" ); if (children.length === 0) { height.start(0); @@ -54,7 +54,7 @@ function CardBase(props: { children: ReactNode }) { observer.observe(ref.current, { attributes: false, childList: true, - subtree: false, + subtree: true, }); return () => { observer.disconnect(); @@ -90,12 +90,17 @@ export function FloatingCard(props: RootFloatingCardProps) { ); return ( - + {content} ); } export function PopoutFloatingCard(props: FloatingCardProps) { - return ; + return ( + + ); } diff --git a/src/components/popout/FloatingContainer.tsx b/src/components/popout/FloatingContainer.tsx index 0bab1ac0..eefeb56c 100644 --- a/src/components/popout/FloatingContainer.tsx +++ b/src/components/popout/FloatingContainer.tsx @@ -36,7 +36,7 @@ export function FloatingContainer(props: Props) { return createPortal( -
+
- {props.children} -
- ); + const width = !isMobile ? `${props.width}px` : "100%"; return ( -
+
{props.children}
diff --git a/src/components/popout/positions/FloatingCardMobilePosition.tsx b/src/components/popout/positions/FloatingCardMobilePosition.tsx index ed8905f7..ff0ee347 100644 --- a/src/components/popout/positions/FloatingCardMobilePosition.tsx +++ b/src/components/popout/positions/FloatingCardMobilePosition.tsx @@ -10,7 +10,6 @@ interface MobilePositionProps { export function FloatingCardMobilePosition(props: MobilePositionProps) { const ref = useRef(null); - const height = 500; const closing = useRef(false); const [cardRect, setCardRect] = useState(null); const [{ y }, api] = useSpring(() => ({ @@ -24,6 +23,7 @@ export function FloatingCardMobilePosition(props: MobilePositionProps) { const bind = useDrag( ({ last, velocity: [, vy], direction: [, dy], movement: [, my] }) => { if (closing.current) return; + const height = cardRect?.height ?? 0; if (last) { // if past half height downwards // 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()} > -
+
{props.children}
diff --git a/src/video/components/actions/SeriesSelectionAction.tsx b/src/video/components/actions/SeriesSelectionAction.tsx index 2a6b2b35..4595eabe 100644 --- a/src/video/components/actions/SeriesSelectionAction.tsx +++ b/src/video/components/actions/SeriesSelectionAction.tsx @@ -4,9 +4,9 @@ import { useVideoPlayerDescriptor } from "@/video/state/hooks"; import { useMeta } from "@/video/state/logic/meta"; 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 { useTranslation } from "react-i18next"; +import { FloatingAnchor } from "@/components/popout/FloatingAnchor"; interface Props { className?: string; @@ -24,7 +24,7 @@ export function SeriesSelectionAction(props: Props) { return (
- + controls.openPopout("episodes")} /> - +
); diff --git a/src/video/components/popouts/EpisodeSelectionPopout.tsx b/src/video/components/popouts/EpisodeSelectionPopout.tsx index 1f167731..fceb1ffe 100644 --- a/src/video/components/popouts/EpisodeSelectionPopout.tsx +++ b/src/video/components/popouts/EpisodeSelectionPopout.tsx @@ -12,6 +12,7 @@ import { useMeta } from "@/video/state/logic/meta"; import { useControls } from "@/video/state/logic/controls"; import { useWatchedContext } from "@/state/watched"; import { useTranslation } from "react-i18next"; +import { FloatingView } from "@/components/popout/FloatingView"; import { PopoutListEntry, PopoutSection } from "./PopoutUtils"; export function EpisodeSelectionPopout() { @@ -99,110 +100,112 @@ export function EpisodeSelectionPopout() { }, [isPickingSeason]); return ( - <> - -
- - - {currentSeasonInfo?.title || ""} - - - {t("videoPlayer.popouts.seasons")} - -
-
-
- - {currentSeasonInfo - ? meta?.seasons?.map?.((season) => ( - setSeason(season.id)} - isOnDarkBackground - > - {season.title} - - )) - : "No season"} + +
+ +
+ + + {currentSeasonInfo?.title || ""} + + + {t("videoPlayer.popouts.seasons")} + +
- - {loading ? ( -
- -
- ) : error ? ( -
-
- -

- {t("videoPLayer.popouts.errors.loadingWentWrong", { - seasonTitle: currentSeasonInfo?.title?.toLowerCase(), - })} -

+
+ + {currentSeasonInfo + ? meta?.seasons?.map?.((season) => ( + setSeason(season.id)} + isOnDarkBackground + > + {season.title} + + )) + : "No season"} + + + {loading ? ( +
+
-
- ) : ( -
- {currentSeasonEpisodes && currentSeasonInfo - ? currentSeasonEpisodes.map((e) => ( - { - if (e.id === meta?.episode?.episodeId) - controls.closePopout(); - else setCurrent(currentSeasonInfo.id, e.id); - }} - percentageCompleted={ - watched.items.find( - (item) => - item.item?.series?.seasonId === - currentSeasonInfo.id && - item.item?.series?.episodeId === e.id - )?.percentage - } - > - {t("videoPlayer.popouts.episode", { - index: e.number, - title: e.title, - })} - - )) - : "No episodes"} -
- )} - + ) : error ? ( +
+
+ +

+ {t("videoPLayer.popouts.errors.loadingWentWrong", { + seasonTitle: currentSeasonInfo?.title?.toLowerCase(), + })} +

+
+
+ ) : ( +
+ {currentSeasonEpisodes && currentSeasonInfo + ? currentSeasonEpisodes.map((e) => ( + { + if (e.id === meta?.episode?.episodeId) + controls.closePopout(); + else setCurrent(currentSeasonInfo.id, e.id); + }} + percentageCompleted={ + watched.items.find( + (item) => + item.item?.series?.seasonId === + currentSeasonInfo.id && + item.item?.series?.episodeId === e.id + )?.percentage + } + > + {t("videoPlayer.popouts.episode", { + index: e.number, + title: e.title, + })} + + )) + : "No episodes"} +
+ )} + +
- + ); } diff --git a/src/video/components/popouts/PopoutProviderAction.tsx b/src/video/components/popouts/PopoutProviderAction.tsx index 3965f67f..104b0703 100644 --- a/src/video/components/popouts/PopoutProviderAction.tsx +++ b/src/video/components/popouts/PopoutProviderAction.tsx @@ -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 { EpisodeSelectionPopout } from "@/video/components/popouts/EpisodeSelectionPopout"; 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 { useVideoPlayerDescriptor } from "@/video/state/hooks"; import { useControls } from "@/video/state/logic/controls"; -import { useIsMobile } from "@/hooks/useIsMobile"; -import { - useInterface, - VideoInterfaceEvent, -} from "@/video/state/logic/interface"; -import { useCallback, useEffect, useRef, useState } from "react"; +import { useInterface } from "@/video/state/logic/interface"; +import { useCallback } from "react"; +import { PopoutFloatingCard } from "@/components/popout/FloatingCard"; +import { FloatingContainer } from "@/components/popout/FloatingContainer"; import "./Popouts.css"; -function ShowPopout(props: { popoutId: string | null }) { - // only updates popout id when a new one is set, so transitions look good - const [popoutId, setPopoutId] = useState(props.popoutId); - useEffect(() => { - if (!props.popoutId) return; - setPopoutId(props.popoutId); - }, [props]); - - if (popoutId === "episodes") return ; - if (popoutId === "source") return ; - if (popoutId === "captions") return ; - if (popoutId === "settings") return ; - return ( -
- Unknown popout -
- ); -} - -function MobilePopoutContainer(props: { - videoInterface: VideoInterfaceEvent; - onClose: () => void; -}) { - const ref = useRef(null); - const height = 500; - const closing = useRef(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, - } - ); +function ShowPopout(props: { popoutId: string | null; onClose: () => void }) { + const popoutMap = { + source: , + captions: , + settings: , + episodes: , + }; return ( - -
- - - ); -} - -function DesktopPopoutContainer(props: { - videoInterface: VideoInterfaceEvent; -}) { - const ref = useRef(null); - const [right, setRight] = useState(0); - const [bottom, setBottom] = useState(0); - const [width, setWidth] = useState(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 ( - - - + <> + {Object.entries(popoutMap).map(([id, el]) => ( + + + {el} + + + ))} + ); } @@ -139,30 +41,11 @@ export function PopoutProviderAction() { const descriptor = useVideoPlayerDescriptor(); const videoInterface = useInterface(descriptor); const controls = useControls(descriptor); - const { isMobile } = useIsMobile(false); useSyncPopouts(descriptor); - const handleClick = useCallback(() => { + const onClose = useCallback(() => { controls.closePopout(); }, [controls]); - return ( - -
-
- {isMobile ? ( - - ) : ( - - )} -
- - ); + return ; } diff --git a/src/video/components/popouts/PopoutUtils.tsx b/src/video/components/popouts/PopoutUtils.tsx index 0478db20..3573a86f 100644 --- a/src/video/components/popouts/PopoutUtils.tsx +++ b/src/video/components/popouts/PopoutUtils.tsx @@ -143,7 +143,7 @@ export function PopoutListEntry(props: PopoutListEntryTypes) { isOnDarkBackground={props.isOnDarkBackground} active={props.active} onClick={props.onClick} - noChevron={!props.loading && !props.errored} + noChevron={props.loading || props.errored} right={ <> {props.errored && ( diff --git a/src/views/developer/TestView.tsx b/src/views/developer/TestView.tsx index 88e47c47..0c634f2c 100644 --- a/src/views/developer/TestView.tsx +++ b/src/views/developer/TestView.tsx @@ -32,7 +32,7 @@ export function TestView() { return (
setShow(false)}> - setShow(false)}> + setShow(false)}> - +
setShow((v) => !v)}