mirror of
https://github.com/sussy-code/smov.git
synced 2024-12-20 14:37:43 +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"
|
||||
>
|
||||
<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>
|
||||
);
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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":
|
||||
|
|
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";
|
||||
|
||||
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;
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in a new issue