diff --git a/src/components/layout/Stepper.tsx b/src/components/layout/Stepper.tsx
new file mode 100644
index 00000000..d0c9c499
--- /dev/null
+++ b/src/components/layout/Stepper.tsx
@@ -0,0 +1,25 @@
+export interface StepperProps {
+ current: number;
+ steps: number;
+ className?: string;
+}
+
+export function Stepper(props: StepperProps) {
+ const percentage = (props.current / (props.steps + 1)) * 100;
+
+ return (
+
+
+ {props.current}/{props.steps}
+
+
+
+ );
+}
diff --git a/src/components/layout/ThinContainer.tsx b/src/components/layout/ThinContainer.tsx
index f7f90acb..345afa5c 100644
--- a/src/components/layout/ThinContainer.tsx
+++ b/src/components/layout/ThinContainer.tsx
@@ -1,3 +1,4 @@
+import classNames from "classnames";
import { ReactNode } from "react";
interface ThinContainerProps {
@@ -16,3 +17,16 @@ export function ThinContainer(props: ThinContainerProps) {
);
}
+
+export function CenterContainer(props: ThinContainerProps) {
+ return (
+
+ );
+}
diff --git a/src/pages/PlayerView.tsx b/src/pages/PlayerView.tsx
index aa73bd83..5282d9b3 100644
--- a/src/pages/PlayerView.tsx
+++ b/src/pages/PlayerView.tsx
@@ -1,6 +1,12 @@
import { RunOutput } from "@movie-web/providers";
import { useCallback, useEffect, useState } from "react";
-import { useNavigate, useParams } from "react-router-dom";
+import {
+ Navigate,
+ useLocation,
+ useNavigate,
+ useParams,
+} from "react-router-dom";
+import { useAsync } from "react-use";
import { usePlayer } from "@/components/player/hooks/usePlayer";
import { usePlayerMeta } from "@/components/player/hooks/usePlayerMeta";
@@ -15,9 +21,10 @@ import { ScrapeErrorPart } from "@/pages/parts/player/ScrapeErrorPart";
import { ScrapingPart } from "@/pages/parts/player/ScrapingPart";
import { useLastNonPlayerLink } from "@/stores/history";
import { PlayerMeta, playerStatus } from "@/stores/player/slices/source";
+import { needsOnboarding } from "@/utils/onboarding";
import { parseTimestamp } from "@/utils/timestamp";
-export function PlayerView() {
+export function RealPlayerView() {
const navigate = useNavigate();
const params = useParams<{
media: string;
@@ -109,4 +116,25 @@ export function PlayerView() {
);
}
+export function PlayerView() {
+ const loc = useLocation();
+ const { loading, error, value } = useAsync(() => {
+ return needsOnboarding();
+ });
+
+ if (error) throw new Error("Failed to detect onboarding");
+ if (loading) return null;
+ if (value)
+ return (
+
+ );
+ return ;
+}
+
export default PlayerView;
diff --git a/src/pages/layouts/MinimalPageLayout.tsx b/src/pages/layouts/MinimalPageLayout.tsx
new file mode 100644
index 00000000..6d2cf34d
--- /dev/null
+++ b/src/pages/layouts/MinimalPageLayout.tsx
@@ -0,0 +1,28 @@
+import { Link } from "react-router-dom";
+
+import { BrandPill } from "@/components/layout/BrandPill";
+import { BlurEllipsis } from "@/pages/layouts/SubPageLayout";
+
+export function MinimalPageLayout(props: { children: React.ReactNode }) {
+ return (
+
+
+ {/* Main page */}
+
+
+
+
+
+
{props.children}
+
+ );
+}
diff --git a/src/pages/onboarding/Onboarding.tsx b/src/pages/onboarding/Onboarding.tsx
new file mode 100644
index 00000000..439cc87d
--- /dev/null
+++ b/src/pages/onboarding/Onboarding.tsx
@@ -0,0 +1,48 @@
+import { useNavigate } from "react-router-dom";
+
+import { Button } from "@/components/buttons/Button";
+import { Stepper } from "@/components/layout/Stepper";
+import { CenterContainer } from "@/components/layout/ThinContainer";
+import { Modal, ModalCard, useModal } from "@/components/overlays/Modal";
+import { Heading2, Paragraph } from "@/components/utils/Text";
+import { MinimalPageLayout } from "@/pages/layouts/MinimalPageLayout";
+import { useRedirectBack } from "@/pages/onboarding/onboardingHooks";
+import { PageTitle } from "@/pages/parts/util/PageTitle";
+
+export function OnboardingPage() {
+ const navigate = useNavigate();
+ const skipModal = useModal("skip");
+ const { skipAndRedirect } = useRedirectBack();
+
+ return (
+
+
+
+
+
+ Lorem ipsum
+ Lorem ipsum Lorem ipsum Lorem ipsum
+
+
+
+
+
+
+
+ Lorem ipsum
+ Lorem ipsum Lorem ipsum Lorem ipsum Lorem ipsum
+
+
+
+
+
+ );
+}
diff --git a/src/pages/onboarding/OnboardingExtension.tsx b/src/pages/onboarding/OnboardingExtension.tsx
new file mode 100644
index 00000000..8ee85be7
--- /dev/null
+++ b/src/pages/onboarding/OnboardingExtension.tsx
@@ -0,0 +1,27 @@
+import { useNavigate } from "react-router-dom";
+
+import { Button } from "@/components/buttons/Button";
+import { Stepper } from "@/components/layout/Stepper";
+import { CenterContainer } from "@/components/layout/ThinContainer";
+import { Heading2, Paragraph } from "@/components/utils/Text";
+import { MinimalPageLayout } from "@/pages/layouts/MinimalPageLayout";
+import { PageTitle } from "@/pages/parts/util/PageTitle";
+
+export function OnboardingExtensionPage() {
+ const navigate = useNavigate();
+
+ return (
+
+
+
+
+ Lorem ipsum
+ Lorem ipsum Lorem ipsum Lorem ipsum Lorem ipsum
+
+
+
+
+ );
+}
diff --git a/src/pages/onboarding/OnboardingProxy.tsx b/src/pages/onboarding/OnboardingProxy.tsx
new file mode 100644
index 00000000..45d22aea
--- /dev/null
+++ b/src/pages/onboarding/OnboardingProxy.tsx
@@ -0,0 +1,27 @@
+import { useNavigate } from "react-router-dom";
+
+import { Button } from "@/components/buttons/Button";
+import { Stepper } from "@/components/layout/Stepper";
+import { CenterContainer } from "@/components/layout/ThinContainer";
+import { Heading2, Paragraph } from "@/components/utils/Text";
+import { MinimalPageLayout } from "@/pages/layouts/MinimalPageLayout";
+import { PageTitle } from "@/pages/parts/util/PageTitle";
+
+export function OnboardingProxyPage() {
+ const navigate = useNavigate();
+
+ return (
+
+
+
+
+ Lorem ipsum
+ Lorem ipsum Lorem ipsum Lorem ipsum Lorem ipsum
+
+
+
+
+ );
+}
diff --git a/src/pages/onboarding/onboardingHooks.ts b/src/pages/onboarding/onboardingHooks.ts
new file mode 100644
index 00000000..e80234e7
--- /dev/null
+++ b/src/pages/onboarding/onboardingHooks.ts
@@ -0,0 +1,22 @@
+import { useCallback } from "react";
+import { useNavigate } from "react-router-dom";
+
+import { useQueryParam } from "@/hooks/useQueryParams";
+import { useOnboardingStore } from "@/stores/onboarding";
+
+export function useRedirectBack() {
+ const [url] = useQueryParam("redirect");
+ const navigate = useNavigate();
+ const setSkipped = useOnboardingStore((s) => s.setSkipped);
+
+ const redirectBack = useCallback(() => {
+ navigate(url ?? "/");
+ }, [navigate, url]);
+
+ const skipAndRedirect = useCallback(() => {
+ setSkipped(true);
+ redirectBack();
+ }, [redirectBack, setSkipped]);
+
+ return { redirectBack, skipAndRedirect };
+}
diff --git a/src/setup/App.tsx b/src/setup/App.tsx
index afa5f8ba..37c55e0f 100644
--- a/src/setup/App.tsx
+++ b/src/setup/App.tsx
@@ -19,6 +19,9 @@ import { DmcaPage, shouldHaveDmcaPage } from "@/pages/Dmca";
import { NotFoundPage } from "@/pages/errors/NotFoundPage";
import { HomePage } from "@/pages/HomePage";
import { LoginPage } from "@/pages/Login";
+import { OnboardingPage } from "@/pages/onboarding/Onboarding";
+import { OnboardingExtensionPage } from "@/pages/onboarding/OnboardingExtension";
+import { OnboardingProxyPage } from "@/pages/onboarding/OnboardingProxy";
import { RegisterPage } from "@/pages/Register";
import { Layout } from "@/setup/Layout";
import { useHistoryListener } from "@/stores/history";
@@ -119,6 +122,12 @@ function App() {
} />
} />
} />
+ } />
+ }
+ />
+ } />
{shouldHaveDmcaPage() ? (
} />
diff --git a/src/setup/config.ts b/src/setup/config.ts
index 2e1634a4..d6aca1cb 100644
--- a/src/setup/config.ts
+++ b/src/setup/config.ts
@@ -19,6 +19,7 @@ interface Config {
DISALLOWED_IDS: string;
TURNSTILE_KEY: string;
CDN_REPLACEMENTS: string;
+ HAS_ONBOARDING: string;
}
export interface RuntimeConfig {
@@ -34,6 +35,7 @@ export interface RuntimeConfig {
DISALLOWED_IDS: string[];
TURNSTILE_KEY: string | null;
CDN_REPLACEMENTS: Array;
+ HAS_ONBOARDING: boolean;
}
const env: Record = {
@@ -49,6 +51,7 @@ const env: Record = {
DISALLOWED_IDS: import.meta.env.VITE_DISALLOWED_IDS,
TURNSTILE_KEY: import.meta.env.VITE_TURNSTILE_KEY,
CDN_REPLACEMENTS: import.meta.env.VITE_CDN_REPLACEMENTS,
+ HAS_ONBOARDING: import.meta.env.VITE_HAS_ONBOARDING,
};
// loads from different locations, in order: environment (VITE_{KEY}), window (public/config.js)
@@ -82,6 +85,7 @@ export function conf(): RuntimeConfig {
.split(",")
.map((v) => v.trim()),
NORMAL_ROUTER: getKey("NORMAL_ROUTER", "false") === "true",
+ HAS_ONBOARDING: getKey("HAS_ONBOARDING", "false") === "true",
TURNSTILE_KEY: turnstileKey.length > 0 ? turnstileKey : null,
DISALLOWED_IDS: getKey("DISALLOWED_IDS", "")
.split(",")
diff --git a/src/stores/history/index.ts b/src/stores/history/index.ts
index c90a7309..c1f507ee 100644
--- a/src/stores/history/index.ts
+++ b/src/stores/history/index.ts
@@ -46,7 +46,8 @@ export function useLastNonPlayerLink() {
(v) =>
!v.path.startsWith("/media") && // cannot be a player link
location.pathname !== v.path && // cannot be current link
- !v.path.startsWith("/s/"), // cannot be a quick search link
+ !v.path.startsWith("/s/") && // cannot be a quick search link
+ !v.path.startsWith("/onboarding"), // cannot be an onboarding link
);
return route?.path ?? "/";
}, [routes, location]);
diff --git a/src/stores/onboarding/index.tsx b/src/stores/onboarding/index.tsx
new file mode 100644
index 00000000..17d1965d
--- /dev/null
+++ b/src/stores/onboarding/index.tsx
@@ -0,0 +1,22 @@
+import { create } from "zustand";
+import { persist } from "zustand/middleware";
+import { immer } from "zustand/middleware/immer";
+
+export interface OnboardingStore {
+ skipped: boolean;
+ setSkipped(v: boolean): void;
+}
+
+export const useOnboardingStore = create(
+ persist(
+ immer((set) => ({
+ skipped: false,
+ setSkipped(v) {
+ set((s) => {
+ s.skipped = v;
+ });
+ },
+ })),
+ { name: "__MW::onboarding" },
+ ),
+);
diff --git a/src/utils/onboarding.ts b/src/utils/onboarding.ts
new file mode 100644
index 00000000..7289fd1c
--- /dev/null
+++ b/src/utils/onboarding.ts
@@ -0,0 +1,23 @@
+import { isExtensionActive } from "@/backend/extension/messaging";
+import { conf } from "@/setup/config";
+import { useAuthStore } from "@/stores/auth";
+import { useOnboardingStore } from "@/stores/onboarding";
+
+export async function needsOnboarding(): Promise {
+ // if onboarding is dislabed, no onboarding needed
+ if (!conf().HAS_ONBOARDING) return false;
+
+ // if extension is active and working, no onboarding needed
+ const extensionActive = await isExtensionActive();
+ if (extensionActive) return false;
+
+ // if there is any custom proxy urls, no onboarding needed
+ const proxyUrls = useAuthStore.getState().proxySet;
+ if (proxyUrls) return false;
+
+ // if onboarding has been skipped, no onboarding needed
+ const skipped = useOnboardingStore.getState().skipped;
+ if (skipped) return false;
+
+ return true;
+}