diff --git a/src/components/Banner.tsx b/src/components/Banner.tsx deleted file mode 100644 index 044bc524..00000000 --- a/src/components/Banner.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { Icon, Icons } from "@/components/Icon"; -import { useBanner } from "@/hooks/useBanner"; - -export function Banner(props: { children: React.ReactNode; type: "error" }) { - const [ref] = useBanner("internet"); - const styles = { - error: "bg-[#C93957] text-white", - }; - const icons = { - error: Icons.CIRCLE_EXCLAMATION, - }; - - return ( -
-
-
- -
{props.children}
-
-
-
- ); -} diff --git a/src/components/Transition.tsx b/src/components/Transition.tsx index c29846c3..4bf79b4e 100644 --- a/src/components/Transition.tsx +++ b/src/components/Transition.tsx @@ -2,7 +2,7 @@ import { Transition as HeadlessTransition, TransitionClasses, } from "@headlessui/react"; -import { Fragment, ReactNode } from "react"; +import { CSSProperties, Fragment, ReactNode } from "react"; export type TransitionAnimations = | "slide-down" @@ -19,6 +19,7 @@ interface Props { className?: string; children?: ReactNode; isChild?: boolean; + style?: CSSProperties; } function getClasses( @@ -90,14 +91,18 @@ export function Transition(props: Props) { if (props.isChild) { return ( -
{props.children}
+
+ {props.children} +
); } return ( -
{props.children}
+
+ {props.children} +
); } diff --git a/src/components/layout/Navigation.tsx b/src/components/layout/Navigation.tsx index 8285c15c..632ba92a 100644 --- a/src/components/layout/Navigation.tsx +++ b/src/components/layout/Navigation.tsx @@ -4,8 +4,8 @@ import { Link } from "react-router-dom"; import { IconPatch } from "@/components/buttons/IconPatch"; import { Icons } from "@/components/Icon"; import { Lightbar } from "@/components/utils/Lightbar"; -import { useBannerSize } from "@/hooks/useBanner"; import { conf } from "@/setup/config"; +import { useBannerSize } from "@/stores/banner"; import { BrandPill } from "./BrandPill"; @@ -20,27 +20,32 @@ export function Navigation(props: NavigationProps) { return ( <> {!props.noLightbar ? ( -
+
) : null}
-
+
-
+
-
+
diff --git a/src/components/player/base/TopControls.tsx b/src/components/player/base/TopControls.tsx index 03369b75..acb1c7dc 100644 --- a/src/components/player/base/TopControls.tsx +++ b/src/components/player/base/TopControls.tsx @@ -1,12 +1,15 @@ import { useEffect } from "react"; import { Transition } from "@/components/Transition"; +import { useBannerSize } from "@/stores/banner"; +import { BannerLocation } from "@/stores/banner/BannerLocation"; import { usePlayerStore } from "@/stores/player/store"; export function TopControls(props: { show?: boolean; children: React.ReactNode; }) { + const bannerSize = useBannerSize("player"); const setHoveringAnyControls = usePlayerStore( (s) => s.setHoveringAnyControls ); @@ -22,12 +25,21 @@ export function TopControls(props: { +
+ +
setHoveringAnyControls(true)} onMouseOut={() => setHoveringAnyControls(false)} className="pointer-events-auto pl-[calc(2rem+env(safe-area-inset-left))] pr-[calc(2rem+env(safe-area-inset-right))] pt-6 absolute top-0 w-full" + style={{ + top: `${bannerSize}px`, + }} > >] ->(null as any); - -export function BannerContextProvider(props: { children: ReactNode }) { - const [state, setState] = useState([]); - const memod = useMemo< - [BannerInstance[], Dispatch>] - >(() => [state, setState], [state]); - - return ( - - {props.children} - - ); -} - -export function useBanner(id: string) { - const [ref, { height }] = useMeasure(); - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const [_, set] = useContext(BannerContext); - - useEffect(() => { - set((v) => [...v, { id, height: 0 }]); - set((value) => { - const v = value.find((item) => item.id === id); - if (v) { - v.height = height; - } - return value; - }); - return () => { - set((v) => v.filter((item) => item.id !== id)); - }; - }, [height, id, set]); - - return [ref]; -} - -export function useBannerSize() { - const [val] = useContext(BannerContext); - - return val.reduce((a, v) => a + v.height, 0); -} diff --git a/src/hooks/useGoBack.ts b/src/hooks/useGoBack.ts deleted file mode 100644 index 3ecc29a6..00000000 --- a/src/hooks/useGoBack.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { useCallback } from "react"; -import { useHistory } from "react-router-dom"; - -export function useGoBack() { - const reactHistory = useHistory(); - - const goBack = useCallback(() => { - if (reactHistory.action !== "POP") reactHistory.goBack(); - else reactHistory.push("/"); - }, [reactHistory]); - return goBack; -} diff --git a/src/hooks/usePing.ts b/src/hooks/usePing.ts index cba29c93..ce7f7c3d 100644 --- a/src/hooks/usePing.ts +++ b/src/hooks/usePing.ts @@ -1,7 +1,9 @@ -import { useEffect, useRef, useState } from "react"; +import { useEffect, useRef } from "react"; -export function useIsOnline() { - const [online, setOnline] = useState(true); +import { useBannerStore } from "@/stores/banner"; + +export function useOnlineListener() { + const updateOnline = useBannerStore((s) => s.updateOnline); const ref = useRef(true); useEffect(() => { @@ -21,12 +23,12 @@ export function useIsOnline() { const signal = abort.signal; fetch("/ping.txt", { signal }) .then(() => { - setOnline(true); + updateOnline(true); ref.current = true; }) .catch((err) => { if (err.name === "AbortError") return; - setOnline(false); + updateOnline(false); ref.current = false; }); }, 5000); @@ -35,7 +37,5 @@ export function useIsOnline() { clearInterval(interval); if (abort) abort.abort(); }; - }, []); - - return online; + }, [updateOnline]); } diff --git a/src/index.tsx b/src/index.tsx index 8e56c24f..b96fc3a2 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -12,7 +12,6 @@ import { assertConfig, conf } from "@/setup/config"; import i18n from "@/setup/i18n"; import "@/setup/ga"; -import "@/setup/sentry"; import "@/setup/index.css"; import { initializeChromecast } from "./setup/chromecast"; import { SettingsStore } from "./state/settings/store"; diff --git a/src/pages/HomePage.tsx b/src/pages/HomePage.tsx index c2a6f6e5..c51cbf32 100644 --- a/src/pages/HomePage.tsx +++ b/src/pages/HomePage.tsx @@ -40,7 +40,7 @@ export function HomePage() { return ( -
+
{t("global.name")} diff --git a/src/pages/parts/home/HeroPart.tsx b/src/pages/parts/home/HeroPart.tsx index 6e924957..9540669e 100644 --- a/src/pages/parts/home/HeroPart.tsx +++ b/src/pages/parts/home/HeroPart.tsx @@ -5,8 +5,8 @@ import Sticky from "react-stickynode"; import { ThinContainer } from "@/components/layout/ThinContainer"; import { SearchBarInput } from "@/components/SearchBar"; import { Title } from "@/components/text/Title"; -import { useBannerSize } from "@/hooks/useBanner"; import { useSearchQuery } from "@/hooks/useSearchQuery"; +import { useBannerSize } from "@/stores/banner"; export interface HeroPartProps { setIsSticky: (val: boolean) => void; diff --git a/src/setup/App.tsx b/src/setup/App.tsx index 10c46f84..2400aae0 100644 --- a/src/setup/App.tsx +++ b/src/setup/App.tsx @@ -10,7 +10,7 @@ import { import { convertLegacyUrl, isLegacyUrl } from "@/backend/metadata/getmeta"; import { generateQuickSearchMediaUrl } from "@/backend/metadata/tmdb"; -import { BannerContextProvider } from "@/hooks/useBanner"; +import { useOnlineListener } from "@/hooks/usePing"; import { AboutPage } from "@/pages/About"; import { DmcaPage } from "@/pages/Dmca"; import { NotFoundPage } from "@/pages/errors/NotFoundPage"; @@ -57,76 +57,75 @@ function QuickSearch() { function App() { useHistoryListener(); + useOnlineListener(); return ( - - - - {/* functional routes */} - - - - - - - - {({ match }) => { - if (match?.params.query) - return ( - - ); - return ; - }} - + + + {/* functional routes */} + + + + + + + + {({ match }) => { + if (match?.params.query) + return ( + + ); + return ; + }} + - {/* pages */} - - - - - - - - + {/* pages */} + + + + + + + + - {/* other */} + {/* other */} + import("@/pages/DeveloperPage"))} + /> + import("@/pages/developer/VideoTesterView") + )} + /> + {/* developer routes that can abuse workers are disabled in production */} + {process.env.NODE_ENV === "development" ? ( import("@/pages/DeveloperPage"))} + path="/dev/test" + component={lazy(() => import("@/pages/developer/TestView"))} /> - import("@/pages/developer/VideoTesterView") - )} - /> - {/* developer routes that can abuse workers are disabled in production */} - {process.env.NODE_ENV === "development" ? ( - import("@/pages/developer/TestView"))} - /> - ) : null} - - - - + ) : null} + + + diff --git a/src/setup/Layout.tsx b/src/setup/Layout.tsx index 23348dfe..3227ecbc 100644 --- a/src/setup/Layout.tsx +++ b/src/setup/Layout.tsx @@ -1,23 +1,20 @@ import { ReactNode } from "react"; -import { useTranslation } from "react-i18next"; -import { Banner } from "@/components/Banner"; -import { useBannerSize } from "@/hooks/useBanner"; -import { useIsOnline } from "@/hooks/usePing"; +import { useBannerSize, useBannerStore } from "@/stores/banner"; +import { BannerLocation } from "@/stores/banner/BannerLocation"; export function Layout(props: { children: ReactNode }) { - const { t } = useTranslation(); - const isOnline = useIsOnline(); const bannerSize = useBannerSize(); + const location = useBannerStore((s) => s.location); return (
- {!isOnline ? {t("errors.offline")} : null} +
diff --git a/src/setup/sentry.tsx b/src/setup/sentry.tsx deleted file mode 100644 index 8dae0b5a..00000000 --- a/src/setup/sentry.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { CaptureConsole, HttpClient } from "@sentry/integrations"; -import * as Sentry from "@sentry/react"; - -import { conf } from "@/setup/config"; -import { SENTRY_DSN } from "@/setup/constants"; - -if (process.env.NODE_ENV !== "development") - Sentry.init({ - dsn: SENTRY_DSN, - release: `movie-web@${conf().APP_VERSION}`, - sampleRate: 0.5, - integrations: [ - new Sentry.BrowserTracing(), - new CaptureConsole(), - new HttpClient(), - ], - }); diff --git a/src/stores/banner/BannerLocation.tsx b/src/stores/banner/BannerLocation.tsx new file mode 100644 index 00000000..df2fbbf8 --- /dev/null +++ b/src/stores/banner/BannerLocation.tsx @@ -0,0 +1,63 @@ +import { useEffect } from "react"; +import { useTranslation } from "react-i18next"; + +import { Icon, Icons } from "@/components/Icon"; +import { useBannerStore, useRegisterBanner } from "@/stores/banner"; + +export function Banner(props: { + children: React.ReactNode; + type: "error"; + id: string; +}) { + const [ref] = useRegisterBanner(props.id); + const styles = { + error: "bg-[#C93957] text-white", + }; + const icons = { + error: Icons.CIRCLE_EXCLAMATION, + }; + + return ( +
+
+
+ +
{props.children}
+
+
+
+ ); +} + +export function BannerLocation(props: { location?: string }) { + const { t } = useTranslation(); + const isOnline = useBannerStore((s) => s.isOnline); + const setLocation = useBannerStore((s) => s.setLocation); + const currentLocation = useBannerStore((s) => s.location); + const loc = props.location ?? null; + + useEffect(() => { + if (!loc) return; + setLocation(loc); + return () => { + setLocation(null); + }; + }, [setLocation, loc]); + + if (currentLocation !== loc) return null; + + return ( +
+ {!isOnline ? ( + + {t("errors.offline")} + + ) : null} +
+ ); +} diff --git a/src/stores/banner/index.ts b/src/stores/banner/index.ts new file mode 100644 index 00000000..f993215e --- /dev/null +++ b/src/stores/banner/index.ts @@ -0,0 +1,88 @@ +import { useEffect } from "react"; +import { useMeasure } from "react-use"; +import { create } from "zustand"; +import { immer } from "zustand/middleware/immer"; + +interface BannerInstance { + id: string; + height: number; +} + +interface BannerStore { + banners: BannerInstance[]; + isOnline: boolean; + location: string | null; + updateHeight(id: string, height: number): void; + showBanner(id: string): void; + hideBanner(id: string): void; + setLocation(loc: string | null): void; + updateOnline(isOnline: boolean): void; +} + +export const useBannerStore = create( + immer((set) => ({ + banners: [], + isOnline: true, + location: null, + updateOnline(isOnline) { + set((s) => { + s.isOnline = isOnline; + }); + }, + setLocation(loc) { + set((s) => { + s.location = loc; + }); + }, + showBanner(id) { + set((s) => { + if (s.banners.find((v) => v.id === id)) return; + s.banners.push({ + id, + height: 0, + }); + }); + }, + hideBanner(id) { + set((s) => { + s.banners = s.banners.filter((v) => v.id !== id); + }); + }, + updateHeight(id, height) { + set((s) => { + const found = s.banners.find((v) => v.id === id); + if (found) found.height = height; + }); + }, + })) +); + +export function useBannerSize(location?: string) { + const loc = location ?? null; + const banners = useBannerStore((s) => s.banners); + const currentLocation = useBannerStore((s) => s.location); + + const size = banners.reduce((a, v) => a + v.height, 0); + if (loc !== currentLocation) return 0; + return size; +} + +export function useRegisterBanner(id: string) { + const [ref, { height }] = useMeasure(); + const updateHeight = useBannerStore((s) => s.updateHeight); + const showBanner = useBannerStore((s) => s.showBanner); + const hideBanner = useBannerStore((s) => s.hideBanner); + + useEffect(() => { + showBanner(id); + return () => { + hideBanner(id); + }; + }, [showBanner, hideBanner, id]); + + useEffect(() => { + updateHeight(id, height); + }, [height, id, updateHeight]); + + return [ref]; +}