scraping page refining + bigger back text + start on overlay router

Co-authored-by: Jip Frijlink <JipFr@users.noreply.github.com>
This commit is contained in:
mrjvs 2023-10-08 19:35:11 +02:00
parent 0a3155d399
commit a05191e1c4
11 changed files with 314 additions and 42 deletions

View File

@ -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<HTMLDivElement>(null);
const old = useRef<string | null>(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 <div ref={ref}>{props.children}</div>;
}

View File

@ -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 <div className="popout-location">{props.children}</div>;
}
export function Overlay(props: OverlayProps) {
const [portalElement, setPortalElement] = useState<Element | null>(null);
const ref = useRef<HTMLDivElement>(null);
const target = useRef<Element | null>(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 = (
<Transition animation="fade" isChild>
<div
onClick={click}
className={classNames({
"absolute inset-0": true,
"bg-black opacity-90": props.darken,
})}
/>
</Transition>
);
return (
<div ref={ref}>
{portalElement
? createPortal(
<Transition show={props.show} animation="none">
<div className="popout-wrapper pointer-events-auto fixed inset-0 z-[999] select-none">
{backdrop}
<Transition animation="slide-up" className="h-0" isChild>
{props.children}
</Transition>
</div>
</Transition>,
portalElement
)
: null}
</div>
);
}

View File

@ -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 (
<Transition
animation={props.active ? "slide-full-left" : "slide-full-right"}
className="absolute inset-0"
durationClass="duration-[400ms]"
show={props.show}
>
<div
className={classNames([
props.className,
"grid grid-rows-[auto,minmax(0,1fr)]",
])}
data-floating-page={props.show ? "true" : undefined}
style={{
height: props.height ? `${props.height}px` : undefined,
maxHeight: "70vh",
width: props.width ? width : undefined,
}}
>
{props.children}
</div>
</Transition>
);
}

View File

@ -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"
>
<Icon className="mr-2" icon={Icons.ARROW_LEFT} />
<span>{t("videoPlayer.backToHomeShort")}</span>
<span className="md:hidden">{t("videoPlayer.backToHomeShort")}</span>
<span className="hidden md:block">{t("videoPlayer.backToHome")}</span>
</span>
</div>
);

View File

@ -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 (
<div
className="relative overflow-hidden h-screen select-none"
ref={containerEl}
>
{props.children}
<div ref={containerEl}>
<OverlayDisplay>
<div className="relative overflow-hidden h-screen select-none">
{props.children}
</div>
</OverlayDisplay>
</div>
);
}

View File

@ -35,15 +35,19 @@ export function ScrapeItem(props: ScrapeItemProps) {
const status = statusMap[props.status];
return (
<div className="grid gap-6 grid-cols-[auto,1fr]" data-source-id={props.id}>
<div className="grid gap-4 grid-cols-[auto,1fr]" data-source-id={props.id}>
<StatusCircle type={status} percentage={props.percentage ?? 0} />
<div>
<p className="font-bold text-white">{props.name}</p>
<div className="h-4">
<Transition animation="fade" show={!!text}>
<p>{text}</p>
</Transition>
</div>
<p
className={
status === "loading" ? "text-white" : "text-type-secondary"
}
>
{props.name}
</p>
<Transition animation="fade" show={!!text}>
<p className="text-[15px] mt-1">{text}</p>
</Transition>
{props.children}
</div>
</div>
@ -52,14 +56,15 @@ export function ScrapeItem(props: ScrapeItemProps) {
export function ScrapeCard(props: ScrapeCardProps) {
return (
<div
data-source-id={props.id}
className={classNames({
"!bg-opacity-100": props.hasChildren,
"w-72 rounded-md p-6 bg-video-scraping-card bg-opacity-0": true,
})}
>
<ScrapeItem {...props} />
<div data-source-id={props.id} className="w-80 mb-6">
<div
className={classNames({
"!bg-opacity-100 py-6": props.hasChildren,
"w-80 rounded-md px-6 bg-video-scraping-card bg-opacity-0": true,
})}
>
<ScrapeItem {...props} />
</div>
</div>
);
}

View File

@ -31,7 +31,7 @@ export function StatusCircle(props: StatusCircle | StatusCircleLoading) {
return (
<div
className={classNames({
"p-0.5 border-current border-2 rounded-full h-6 w-6 relative transition-colors":
"p-0.5 border-current border-[3px] rounded-full h-6 w-6 relative transition-colors":
true,
"text-video-scraping-loading": props.type === "loading",
"text-video-scraping-noresult text-opacity-50":

View File

@ -0,0 +1,55 @@
import { useQueryParam } from "@/hooks/useQueryParams";
export function useOverlayRouter(id: string) {
const [route, setRoute] = useQueryParam("r");
const routeParts = (route ?? "").split("/").filter((v) => 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,
};
}

View File

@ -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;
}

View File

@ -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,

View File

@ -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 (
<div className="h-full w-full relative" ref={containerRef}>
<div
@ -49,28 +56,43 @@ export function ScrapingPart(props: ScrapingProps) {
>
{sourceOrder.map((order) => {
const source = sources[order.id];
const distance = Math.abs(
sourceOrder.findIndex((t) => t.id === order.id) -
currentProviderIndex
);
return (
<ScrapeCard
id={order.id}
name={source.name}
status={source.status}
hasChildren={order.children.length > 0}
percentage={source.percentage}
<div
className="transition-opacity duration-100"
style={{ opacity: Math.max(0, 1 - distance * 0.3) }}
key={order.id}
>
{order.children.map((embedId) => {
const embed = sources[embedId];
return (
<ScrapeItem
id={embedId}
name={embed.name}
status={source.status}
percentage={embed.percentage}
key={embedId}
/>
);
})}
</ScrapeCard>
<ScrapeCard
id={order.id}
name={source.name}
status={source.status}
hasChildren={order.children.length > 0}
percentage={source.percentage}
>
<div
className={classNames({
"space-y-6 mt-8": order.children.length > 0,
})}
>
{order.children.map((embedId) => {
const embed = sources[embedId];
return (
<ScrapeItem
id={embedId}
name={embed.name}
status={embed.status}
percentage={embed.percentage}
key={embedId}
/>
);
})}
</div>
</ScrapeCard>
</div>
);
})}
</div>