diff --git a/src/components/overlays/OverlayAnchor.tsx b/src/components/overlays/OverlayAnchor.tsx new file mode 100644 index 00000000..1282f9ee --- /dev/null +++ b/src/components/overlays/OverlayAnchor.tsx @@ -0,0 +1,47 @@ +import { ReactNode, useEffect, useRef } from "react"; + +export function createOverlayAnchorEvent(id: string): string { + return `__overlay::anchor::${id}`; +} + +interface Props { + id: string; + children?: ReactNode; +} + +export function OverlayAnchor(props: Props) { + const ref = useRef(null); + const old = useRef(null); + + useEffect(() => { + if (!ref.current) return; + + let cancelled = false; + function render() { + if (cancelled) return; + + if (ref.current) { + const current = old.current; + const newer = ref.current.getBoundingClientRect(); + const newerStr = JSON.stringify(newer); + if (current !== newerStr) { + old.current = newerStr; + const evtStr = createOverlayAnchorEvent(props.id); + (window as any)[evtStr] = newer; + const evObj = new CustomEvent(createOverlayAnchorEvent(props.id), { + detail: newer, + }); + document.dispatchEvent(evObj); + } + } + window.requestAnimationFrame(render); + } + + window.requestAnimationFrame(render); + return () => { + cancelled = true; + }; + }, [props]); + + return
{props.children}
; +} diff --git a/src/components/overlays/OverlayDisplay.tsx b/src/components/overlays/OverlayDisplay.tsx new file mode 100644 index 00000000..a470a9ce --- /dev/null +++ b/src/components/overlays/OverlayDisplay.tsx @@ -0,0 +1,79 @@ +import classNames from "classnames"; +import { ReactNode, useCallback, useEffect, useRef, useState } from "react"; +import { createPortal } from "react-dom"; + +import { Transition } from "@/components/Transition"; + +export interface OverlayProps { + children?: ReactNode; + onClose?: () => void; + show?: boolean; + darken?: boolean; +} + +export function OverlayDisplay(props: { children: ReactNode }) { + return
{props.children}
; +} + +export function Overlay(props: OverlayProps) { + const [portalElement, setPortalElement] = useState(null); + const ref = useRef(null); + const target = useRef(null); + + useEffect(() => { + function listen(e: MouseEvent) { + target.current = e.target as Element; + } + document.addEventListener("mousedown", listen); + return () => { + document.removeEventListener("mousedown", listen); + }; + }); + + const click = useCallback( + (e: React.MouseEvent) => { + const startedTarget = target.current; + target.current = null; + if (e.currentTarget !== e.target) return; + if (!startedTarget) return; + if (!startedTarget.isEqualNode(e.currentTarget as Element)) return; + if (props.onClose) props.onClose(); + }, + [props] + ); + + useEffect(() => { + const element = ref.current?.closest(".popout-location"); + setPortalElement(element ?? document.body); + }, []); + + const backdrop = ( + +
+ + ); + + return ( +
+ {portalElement + ? createPortal( + +
+ {backdrop} + + {props.children} + +
+
, + portalElement + ) + : null} +
+ ); +} diff --git a/src/components/overlays/OverlayPage.tsx b/src/components/overlays/OverlayPage.tsx new file mode 100644 index 00000000..bc8e0bfd --- /dev/null +++ b/src/components/overlays/OverlayPage.tsx @@ -0,0 +1,42 @@ +import classNames from "classnames"; +import { ReactNode } from "react"; + +import { Transition } from "@/components/Transition"; +import { useIsMobile } from "@/hooks/useIsMobile"; + +interface Props { + children?: ReactNode; + show?: boolean; + 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 ( + +
+ {props.children} +
+
+ ); +} diff --git a/src/components/player/base/BackLink.tsx b/src/components/player/base/BackLink.tsx index 31f4791d..56f97044 100644 --- a/src/components/player/base/BackLink.tsx +++ b/src/components/player/base/BackLink.tsx @@ -14,7 +14,8 @@ export function BackLink() { className="flex items-center cursor-pointer text-type-secondary hover:text-white transition-colors duration-200 font-medium" > - {t("videoPlayer.backToHomeShort")} + {t("videoPlayer.backToHomeShort")} + {t("videoPlayer.backToHome")}
); diff --git a/src/components/player/base/Container.tsx b/src/components/player/base/Container.tsx index 472c9d0b..180f36b6 100644 --- a/src/components/player/base/Container.tsx +++ b/src/components/player/base/Container.tsx @@ -1,5 +1,6 @@ import { ReactNode, RefObject, useEffect, useRef } from "react"; +import { OverlayDisplay } from "@/components/overlays/OverlayDisplay"; import { HeadUpdater } from "@/components/player/internals/HeadUpdater"; import { VideoClickTarget } from "@/components/player/internals/VideoClickTarget"; import { VideoContainer } from "@/components/player/internals/VideoContainer"; @@ -61,11 +62,12 @@ function BaseContainer(props: { children?: ReactNode }) { }, [display, containerEl]); return ( -
- {props.children} +
+ +
+ {props.children} +
+
); } diff --git a/src/components/player/internals/ScrapeCard.tsx b/src/components/player/internals/ScrapeCard.tsx index 91d2d997..b1e98b61 100644 --- a/src/components/player/internals/ScrapeCard.tsx +++ b/src/components/player/internals/ScrapeCard.tsx @@ -35,15 +35,19 @@ export function ScrapeItem(props: ScrapeItemProps) { const status = statusMap[props.status]; return ( -
+
-

{props.name}

-
- -

{text}

-
-
+

+ {props.name} +

+ +

{text}

+
{props.children}
@@ -52,14 +56,15 @@ export function ScrapeItem(props: ScrapeItemProps) { export function ScrapeCard(props: ScrapeCardProps) { return ( -
- +
+
+ +
); } diff --git a/src/components/player/internals/StatusCircle.tsx b/src/components/player/internals/StatusCircle.tsx index 961347f3..8276b764 100644 --- a/src/components/player/internals/StatusCircle.tsx +++ b/src/components/player/internals/StatusCircle.tsx @@ -31,7 +31,7 @@ export function StatusCircle(props: StatusCircle | StatusCircleLoading) { return (
v.length > 0); + const routerActive = routeParts.length > 0 && routeParts[0] === id; + const currentPage = routeParts[routeParts.length - 1] ?? "/"; + + function navigate(path: string) { + const newRoute = [id, ...path.split("/").filter((v) => v.length > 0)]; + setRoute(newRoute.join("/")); + } + + function isActive(page: string) { + if (page === "/") return true; + const index = routeParts.indexOf(page); + if (index === -1) return false; // not active + if (index === routeParts.length - 1) return false; // active but latest route so shouldnt be counted as active + return true; + } + + function isCurrentPage(page: string) { + return routerActive && page === currentPage; + } + + function isLoaded(page: string) { + if (page === "/") return true; + return route.includes(page); + } + + function isOverlayActive() { + return routerActive; + } + + function pageProps(page: string) { + return { + show: isCurrentPage(page), + active: isActive(page), + }; + } + + function close() { + navigate("/"); + } + + return { + isOverlayActive, + navigate, + close, + isLoaded, + isCurrentPage, + pageProps, + isActive, + }; +} diff --git a/src/hooks/useQueryParams.ts b/src/hooks/useQueryParams.ts index be8c3c86..6de52f1c 100644 --- a/src/hooks/useQueryParams.ts +++ b/src/hooks/useQueryParams.ts @@ -1,4 +1,4 @@ -import { useMemo } from "react"; +import { useCallback, useMemo } from "react"; import { useLocation } from "react-router-dom"; export function useQueryParams() { @@ -15,3 +15,21 @@ export function useQueryParams() { return queryParams; } + +export function useQueryParam(param: string) { + const params = useQueryParams(); + const location = useLocation(); + const currentValue = params[param]; + + const set = useCallback( + (value: string | null) => { + const parsed = new URLSearchParams(location.search); + if (value) parsed.set(param, value); + else parsed.delete(param); + location.search = parsed.toString(); + }, + [param, location] + ); + + return [currentValue, set] as const; +} diff --git a/src/pages/PlayerView.tsx b/src/pages/PlayerView.tsx index 459d2c6a..ab25411a 100644 --- a/src/pages/PlayerView.tsx +++ b/src/pages/PlayerView.tsx @@ -53,6 +53,7 @@ export function PlayerView() { ) as (keyof typeof out.stream.qualities)[]; const file = out.stream.qualities[qualities[0]]; if (!file) return; + playMedia({ type: MWStreamType.MP4, url: file.url, diff --git a/src/pages/parts/player/ScrapingPart.tsx b/src/pages/parts/player/ScrapingPart.tsx index 9d52706f..681a58dd 100644 --- a/src/pages/parts/player/ScrapingPart.tsx +++ b/src/pages/parts/player/ScrapingPart.tsx @@ -38,6 +38,13 @@ export function ScrapingPart(props: ScrapingProps) { })(); }, [startScraping, props, playMedia]); + const currentProvider = sourceOrder.find( + (s) => sources[s.id].status === "pending" + ); + const currentProviderIndex = sourceOrder.findIndex( + (provider) => currentProvider?.id === provider.id + ); + return (
{sourceOrder.map((order) => { const source = sources[order.id]; + const distance = Math.abs( + sourceOrder.findIndex((t) => t.id === order.id) - + currentProviderIndex + ); return ( - 0} - percentage={source.percentage} +
- {order.children.map((embedId) => { - const embed = sources[embedId]; - return ( - - ); - })} - + 0} + percentage={source.percentage} + > +
0, + })} + > + {order.children.map((embedId) => { + const embed = sources[embedId]; + return ( + + ); + })} +
+
+
); })}