mirror of
https://github.com/sussy-code/smov.git
synced 2024-12-29 16:07:40 +01:00
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:
parent
0a3155d399
commit
a05191e1c4
11 changed files with 314 additions and 42 deletions
47
src/components/overlays/OverlayAnchor.tsx
Normal file
47
src/components/overlays/OverlayAnchor.tsx
Normal 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>;
|
||||||
|
}
|
79
src/components/overlays/OverlayDisplay.tsx
Normal file
79
src/components/overlays/OverlayDisplay.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
42
src/components/overlays/OverlayPage.tsx
Normal file
42
src/components/overlays/OverlayPage.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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"
|
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} />
|
<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>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { ReactNode, RefObject, useEffect, useRef } from "react";
|
import { ReactNode, RefObject, useEffect, useRef } from "react";
|
||||||
|
|
||||||
|
import { OverlayDisplay } from "@/components/overlays/OverlayDisplay";
|
||||||
import { HeadUpdater } from "@/components/player/internals/HeadUpdater";
|
import { HeadUpdater } from "@/components/player/internals/HeadUpdater";
|
||||||
import { VideoClickTarget } from "@/components/player/internals/VideoClickTarget";
|
import { VideoClickTarget } from "@/components/player/internals/VideoClickTarget";
|
||||||
import { VideoContainer } from "@/components/player/internals/VideoContainer";
|
import { VideoContainer } from "@/components/player/internals/VideoContainer";
|
||||||
|
@ -61,11 +62,12 @@ function BaseContainer(props: { children?: ReactNode }) {
|
||||||
}, [display, containerEl]);
|
}, [display, containerEl]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div ref={containerEl}>
|
||||||
className="relative overflow-hidden h-screen select-none"
|
<OverlayDisplay>
|
||||||
ref={containerEl}
|
<div className="relative overflow-hidden h-screen select-none">
|
||||||
>
|
{props.children}
|
||||||
{props.children}
|
</div>
|
||||||
|
</OverlayDisplay>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,15 +35,19 @@ export function ScrapeItem(props: ScrapeItemProps) {
|
||||||
const status = statusMap[props.status];
|
const status = statusMap[props.status];
|
||||||
|
|
||||||
return (
|
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} />
|
<StatusCircle type={status} percentage={props.percentage ?? 0} />
|
||||||
<div>
|
<div>
|
||||||
<p className="font-bold text-white">{props.name}</p>
|
<p
|
||||||
<div className="h-4">
|
className={
|
||||||
<Transition animation="fade" show={!!text}>
|
status === "loading" ? "text-white" : "text-type-secondary"
|
||||||
<p>{text}</p>
|
}
|
||||||
</Transition>
|
>
|
||||||
</div>
|
{props.name}
|
||||||
|
</p>
|
||||||
|
<Transition animation="fade" show={!!text}>
|
||||||
|
<p className="text-[15px] mt-1">{text}</p>
|
||||||
|
</Transition>
|
||||||
{props.children}
|
{props.children}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -52,14 +56,15 @@ export function ScrapeItem(props: ScrapeItemProps) {
|
||||||
|
|
||||||
export function ScrapeCard(props: ScrapeCardProps) {
|
export function ScrapeCard(props: ScrapeCardProps) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div data-source-id={props.id} className="w-80 mb-6">
|
||||||
data-source-id={props.id}
|
<div
|
||||||
className={classNames({
|
className={classNames({
|
||||||
"!bg-opacity-100": props.hasChildren,
|
"!bg-opacity-100 py-6": props.hasChildren,
|
||||||
"w-72 rounded-md p-6 bg-video-scraping-card bg-opacity-0": true,
|
"w-80 rounded-md px-6 bg-video-scraping-card bg-opacity-0": true,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<ScrapeItem {...props} />
|
<ScrapeItem {...props} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,7 +31,7 @@ export function StatusCircle(props: StatusCircle | StatusCircleLoading) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={classNames({
|
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,
|
true,
|
||||||
"text-video-scraping-loading": props.type === "loading",
|
"text-video-scraping-loading": props.type === "loading",
|
||||||
"text-video-scraping-noresult text-opacity-50":
|
"text-video-scraping-noresult text-opacity-50":
|
||||||
|
|
55
src/hooks/useOverlayRouter.ts
Normal file
55
src/hooks/useOverlayRouter.ts
Normal 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,
|
||||||
|
};
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
import { useMemo } from "react";
|
import { useCallback, useMemo } from "react";
|
||||||
import { useLocation } from "react-router-dom";
|
import { useLocation } from "react-router-dom";
|
||||||
|
|
||||||
export function useQueryParams() {
|
export function useQueryParams() {
|
||||||
|
@ -15,3 +15,21 @@ export function useQueryParams() {
|
||||||
|
|
||||||
return queryParams;
|
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;
|
||||||
|
}
|
||||||
|
|
|
@ -53,6 +53,7 @@ export function PlayerView() {
|
||||||
) as (keyof typeof out.stream.qualities)[];
|
) as (keyof typeof out.stream.qualities)[];
|
||||||
const file = out.stream.qualities[qualities[0]];
|
const file = out.stream.qualities[qualities[0]];
|
||||||
if (!file) return;
|
if (!file) return;
|
||||||
|
|
||||||
playMedia({
|
playMedia({
|
||||||
type: MWStreamType.MP4,
|
type: MWStreamType.MP4,
|
||||||
url: file.url,
|
url: file.url,
|
||||||
|
|
|
@ -38,6 +38,13 @@ export function ScrapingPart(props: ScrapingProps) {
|
||||||
})();
|
})();
|
||||||
}, [startScraping, props, playMedia]);
|
}, [startScraping, props, playMedia]);
|
||||||
|
|
||||||
|
const currentProvider = sourceOrder.find(
|
||||||
|
(s) => sources[s.id].status === "pending"
|
||||||
|
);
|
||||||
|
const currentProviderIndex = sourceOrder.findIndex(
|
||||||
|
(provider) => currentProvider?.id === provider.id
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full w-full relative" ref={containerRef}>
|
<div className="h-full w-full relative" ref={containerRef}>
|
||||||
<div
|
<div
|
||||||
|
@ -49,28 +56,43 @@ export function ScrapingPart(props: ScrapingProps) {
|
||||||
>
|
>
|
||||||
{sourceOrder.map((order) => {
|
{sourceOrder.map((order) => {
|
||||||
const source = sources[order.id];
|
const source = sources[order.id];
|
||||||
|
const distance = Math.abs(
|
||||||
|
sourceOrder.findIndex((t) => t.id === order.id) -
|
||||||
|
currentProviderIndex
|
||||||
|
);
|
||||||
return (
|
return (
|
||||||
<ScrapeCard
|
<div
|
||||||
id={order.id}
|
className="transition-opacity duration-100"
|
||||||
name={source.name}
|
style={{ opacity: Math.max(0, 1 - distance * 0.3) }}
|
||||||
status={source.status}
|
|
||||||
hasChildren={order.children.length > 0}
|
|
||||||
percentage={source.percentage}
|
|
||||||
key={order.id}
|
key={order.id}
|
||||||
>
|
>
|
||||||
{order.children.map((embedId) => {
|
<ScrapeCard
|
||||||
const embed = sources[embedId];
|
id={order.id}
|
||||||
return (
|
name={source.name}
|
||||||
<ScrapeItem
|
status={source.status}
|
||||||
id={embedId}
|
hasChildren={order.children.length > 0}
|
||||||
name={embed.name}
|
percentage={source.percentage}
|
||||||
status={source.status}
|
>
|
||||||
percentage={embed.percentage}
|
<div
|
||||||
key={embedId}
|
className={classNames({
|
||||||
/>
|
"space-y-6 mt-8": order.children.length > 0,
|
||||||
);
|
})}
|
||||||
})}
|
>
|
||||||
</ScrapeCard>
|
{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>
|
</div>
|
||||||
|
|
Loading…
Reference in a new issue