pstream is dead; long live pstream taciturnaxolotl.github.io/pstream-ng/
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

Basic onboarding structure

mrjvs 925f3dff fa2b610e

+281 -3
+25
src/components/layout/Stepper.tsx
··· 1 + export interface StepperProps { 2 + current: number; 3 + steps: number; 4 + className?: string; 5 + } 6 + 7 + export function Stepper(props: StepperProps) { 8 + const percentage = (props.current / (props.steps + 1)) * 100; 9 + 10 + return ( 11 + <div className={props.className}> 12 + <p className="mb-2"> 13 + {props.current}/{props.steps} 14 + </p> 15 + <div className="max-w-full h-1 w-32 bg-white rounded-full overflow-hidden"> 16 + <div 17 + className="h-full bg-blue-500 transition-[width] rounded-full" 18 + style={{ 19 + width: `${percentage.toFixed(0)}%`, 20 + }} 21 + /> 22 + </div> 23 + </div> 24 + ); 25 + }
+14
src/components/layout/ThinContainer.tsx
··· 1 + import classNames from "classnames"; 1 2 import { ReactNode } from "react"; 2 3 3 4 interface ThinContainerProps { ··· 16 17 </div> 17 18 ); 18 19 } 20 + 21 + export function CenterContainer(props: ThinContainerProps) { 22 + return ( 23 + <div 24 + className={classNames( 25 + "min-h-screen w-full flex justify-center p-8 items-center", 26 + props.classNames, 27 + )} 28 + > 29 + <div className="w-[600px] max-w-full">{props.children}</div> 30 + </div> 31 + ); 32 + }
+30 -2
src/pages/PlayerView.tsx
··· 1 1 import { RunOutput } from "@movie-web/providers"; 2 2 import { useCallback, useEffect, useState } from "react"; 3 - import { useNavigate, useParams } from "react-router-dom"; 3 + import { 4 + Navigate, 5 + useLocation, 6 + useNavigate, 7 + useParams, 8 + } from "react-router-dom"; 9 + import { useAsync } from "react-use"; 4 10 5 11 import { usePlayer } from "@/components/player/hooks/usePlayer"; 6 12 import { usePlayerMeta } from "@/components/player/hooks/usePlayerMeta"; ··· 15 21 import { ScrapingPart } from "@/pages/parts/player/ScrapingPart"; 16 22 import { useLastNonPlayerLink } from "@/stores/history"; 17 23 import { PlayerMeta, playerStatus } from "@/stores/player/slices/source"; 24 + import { needsOnboarding } from "@/utils/onboarding"; 18 25 import { parseTimestamp } from "@/utils/timestamp"; 19 26 20 - export function PlayerView() { 27 + export function RealPlayerView() { 21 28 const navigate = useNavigate(); 22 29 const params = useParams<{ 23 30 media: string; ··· 107 114 {status === playerStatus.PLAYBACK_ERROR ? <PlaybackErrorPart /> : null} 108 115 </PlayerPart> 109 116 ); 117 + } 118 + 119 + export function PlayerView() { 120 + const loc = useLocation(); 121 + const { loading, error, value } = useAsync(() => { 122 + return needsOnboarding(); 123 + }); 124 + 125 + if (error) throw new Error("Failed to detect onboarding"); 126 + if (loading) return null; 127 + if (value) 128 + return ( 129 + <Navigate 130 + replace 131 + to={{ 132 + pathname: "/onboarding", 133 + search: `redirect=${encodeURIComponent(loc.pathname)}`, 134 + }} 135 + /> 136 + ); 137 + return <RealPlayerView />; 110 138 } 111 139 112 140 export default PlayerView;
+28
src/pages/layouts/MinimalPageLayout.tsx
··· 1 + import { Link } from "react-router-dom"; 2 + 3 + import { BrandPill } from "@/components/layout/BrandPill"; 4 + import { BlurEllipsis } from "@/pages/layouts/SubPageLayout"; 5 + 6 + export function MinimalPageLayout(props: { children: React.ReactNode }) { 7 + return ( 8 + <div 9 + className="bg-background-main min-h-screen" 10 + style={{ 11 + backgroundImage: 12 + "linear-gradient(to bottom, var(--tw-gradient-from), var(--tw-gradient-to) 800px)", 13 + }} 14 + > 15 + <BlurEllipsis /> 16 + {/* Main page */} 17 + <div className="fixed px-7 py-5 left-0 top-0"> 18 + <Link 19 + className="block tabbable rounded-full text-xs ssm:text-base" 20 + to="/" 21 + > 22 + <BrandPill clickable /> 23 + </Link> 24 + </div> 25 + <div className="min-h-screen">{props.children}</div> 26 + </div> 27 + ); 28 + }
+48
src/pages/onboarding/Onboarding.tsx
··· 1 + import { useNavigate } from "react-router-dom"; 2 + 3 + import { Button } from "@/components/buttons/Button"; 4 + import { Stepper } from "@/components/layout/Stepper"; 5 + import { CenterContainer } from "@/components/layout/ThinContainer"; 6 + import { Modal, ModalCard, useModal } from "@/components/overlays/Modal"; 7 + import { Heading2, Paragraph } from "@/components/utils/Text"; 8 + import { MinimalPageLayout } from "@/pages/layouts/MinimalPageLayout"; 9 + import { useRedirectBack } from "@/pages/onboarding/onboardingHooks"; 10 + import { PageTitle } from "@/pages/parts/util/PageTitle"; 11 + 12 + export function OnboardingPage() { 13 + const navigate = useNavigate(); 14 + const skipModal = useModal("skip"); 15 + const { skipAndRedirect } = useRedirectBack(); 16 + 17 + return ( 18 + <MinimalPageLayout> 19 + <PageTitle subpage k="global.pages.about" /> 20 + <Modal id={skipModal.id}> 21 + <ModalCard> 22 + <ModalCard> 23 + <Heading2 className="!mt-0">Lorem ipsum</Heading2> 24 + <Paragraph>Lorem ipsum Lorem ipsum Lorem ipsum</Paragraph> 25 + <Button theme="secondary" onClick={skipModal.hide}> 26 + Lorem ipsum 27 + </Button> 28 + <Button theme="danger" onClick={() => skipAndRedirect()}> 29 + Lorem ipsum 30 + </Button> 31 + </ModalCard> 32 + </ModalCard> 33 + </Modal> 34 + <CenterContainer> 35 + <Stepper steps={2} current={1} className="mb-12" /> 36 + <Heading2 className="!mt-0">Lorem ipsum</Heading2> 37 + <Paragraph>Lorem ipsum Lorem ipsum Lorem ipsum Lorem ipsum</Paragraph> 38 + <Button onClick={() => navigate("/onboarding/proxy")}> 39 + Custom proxy 40 + </Button> 41 + <Button onClick={() => navigate("/onboarding/extension")}> 42 + Extension 43 + </Button> 44 + <Button onClick={skipModal.show}>Default</Button> 45 + </CenterContainer> 46 + </MinimalPageLayout> 47 + ); 48 + }
+27
src/pages/onboarding/OnboardingExtension.tsx
··· 1 + import { useNavigate } from "react-router-dom"; 2 + 3 + import { Button } from "@/components/buttons/Button"; 4 + import { Stepper } from "@/components/layout/Stepper"; 5 + import { CenterContainer } from "@/components/layout/ThinContainer"; 6 + import { Heading2, Paragraph } from "@/components/utils/Text"; 7 + import { MinimalPageLayout } from "@/pages/layouts/MinimalPageLayout"; 8 + import { PageTitle } from "@/pages/parts/util/PageTitle"; 9 + 10 + export function OnboardingExtensionPage() { 11 + const navigate = useNavigate(); 12 + 13 + return ( 14 + <MinimalPageLayout> 15 + <PageTitle subpage k="global.pages.about" /> 16 + <CenterContainer> 17 + <Stepper steps={2} current={2} className="mb-12" /> 18 + <Heading2 className="!mt-0">Lorem ipsum</Heading2> 19 + <Paragraph>Lorem ipsum Lorem ipsum Lorem ipsum Lorem ipsum</Paragraph> 20 + <Button onClick={() => navigate("/onboarding")}>Back</Button> 21 + <Button onClick={() => alert("Check extension here or something")}> 22 + Check extension 23 + </Button> 24 + </CenterContainer> 25 + </MinimalPageLayout> 26 + ); 27 + }
+27
src/pages/onboarding/OnboardingProxy.tsx
··· 1 + import { useNavigate } from "react-router-dom"; 2 + 3 + import { Button } from "@/components/buttons/Button"; 4 + import { Stepper } from "@/components/layout/Stepper"; 5 + import { CenterContainer } from "@/components/layout/ThinContainer"; 6 + import { Heading2, Paragraph } from "@/components/utils/Text"; 7 + import { MinimalPageLayout } from "@/pages/layouts/MinimalPageLayout"; 8 + import { PageTitle } from "@/pages/parts/util/PageTitle"; 9 + 10 + export function OnboardingProxyPage() { 11 + const navigate = useNavigate(); 12 + 13 + return ( 14 + <MinimalPageLayout> 15 + <PageTitle subpage k="global.pages.about" /> 16 + <CenterContainer> 17 + <Stepper steps={2} current={2} className="mb-12" /> 18 + <Heading2 className="!mt-0">Lorem ipsum</Heading2> 19 + <Paragraph>Lorem ipsum Lorem ipsum Lorem ipsum Lorem ipsum</Paragraph> 20 + <Button onClick={() => navigate("/onboarding")}>Back</Button> 21 + <Button onClick={() => alert("Check proxy or smth")}> 22 + Check extension 23 + </Button> 24 + </CenterContainer> 25 + </MinimalPageLayout> 26 + ); 27 + }
+22
src/pages/onboarding/onboardingHooks.ts
··· 1 + import { useCallback } from "react"; 2 + import { useNavigate } from "react-router-dom"; 3 + 4 + import { useQueryParam } from "@/hooks/useQueryParams"; 5 + import { useOnboardingStore } from "@/stores/onboarding"; 6 + 7 + export function useRedirectBack() { 8 + const [url] = useQueryParam("redirect"); 9 + const navigate = useNavigate(); 10 + const setSkipped = useOnboardingStore((s) => s.setSkipped); 11 + 12 + const redirectBack = useCallback(() => { 13 + navigate(url ?? "/"); 14 + }, [navigate, url]); 15 + 16 + const skipAndRedirect = useCallback(() => { 17 + setSkipped(true); 18 + redirectBack(); 19 + }, [redirectBack, setSkipped]); 20 + 21 + return { redirectBack, skipAndRedirect }; 22 + }
+9
src/setup/App.tsx
··· 19 19 import { NotFoundPage } from "@/pages/errors/NotFoundPage"; 20 20 import { HomePage } from "@/pages/HomePage"; 21 21 import { LoginPage } from "@/pages/Login"; 22 + import { OnboardingPage } from "@/pages/onboarding/Onboarding"; 23 + import { OnboardingExtensionPage } from "@/pages/onboarding/OnboardingExtension"; 24 + import { OnboardingProxyPage } from "@/pages/onboarding/OnboardingProxy"; 22 25 import { RegisterPage } from "@/pages/Register"; 23 26 import { Layout } from "@/setup/Layout"; 24 27 import { useHistoryListener } from "@/stores/history"; ··· 119 122 <Route path="/register" element={<RegisterPage />} /> 120 123 <Route path="/login" element={<LoginPage />} /> 121 124 <Route path="/about" element={<AboutPage />} /> 125 + <Route path="/onboarding" element={<OnboardingPage />} /> 126 + <Route 127 + path="/onboarding/extension" 128 + element={<OnboardingExtensionPage />} 129 + /> 130 + <Route path="/onboarding/proxy" element={<OnboardingProxyPage />} /> 122 131 123 132 {shouldHaveDmcaPage() ? ( 124 133 <Route path="/dmca" element={<DmcaPage />} />
+4
src/setup/config.ts
··· 19 19 DISALLOWED_IDS: string; 20 20 TURNSTILE_KEY: string; 21 21 CDN_REPLACEMENTS: string; 22 + HAS_ONBOARDING: string; 22 23 } 23 24 24 25 export interface RuntimeConfig { ··· 34 35 DISALLOWED_IDS: string[]; 35 36 TURNSTILE_KEY: string | null; 36 37 CDN_REPLACEMENTS: Array<string[]>; 38 + HAS_ONBOARDING: boolean; 37 39 } 38 40 39 41 const env: Record<keyof Config, undefined | string> = { ··· 49 51 DISALLOWED_IDS: import.meta.env.VITE_DISALLOWED_IDS, 50 52 TURNSTILE_KEY: import.meta.env.VITE_TURNSTILE_KEY, 51 53 CDN_REPLACEMENTS: import.meta.env.VITE_CDN_REPLACEMENTS, 54 + HAS_ONBOARDING: import.meta.env.VITE_HAS_ONBOARDING, 52 55 }; 53 56 54 57 // loads from different locations, in order: environment (VITE_{KEY}), window (public/config.js) ··· 82 85 .split(",") 83 86 .map((v) => v.trim()), 84 87 NORMAL_ROUTER: getKey("NORMAL_ROUTER", "false") === "true", 88 + HAS_ONBOARDING: getKey("HAS_ONBOARDING", "false") === "true", 85 89 TURNSTILE_KEY: turnstileKey.length > 0 ? turnstileKey : null, 86 90 DISALLOWED_IDS: getKey("DISALLOWED_IDS", "") 87 91 .split(",")
+2 -1
src/stores/history/index.ts
··· 46 46 (v) => 47 47 !v.path.startsWith("/media") && // cannot be a player link 48 48 location.pathname !== v.path && // cannot be current link 49 - !v.path.startsWith("/s/"), // cannot be a quick search link 49 + !v.path.startsWith("/s/") && // cannot be a quick search link 50 + !v.path.startsWith("/onboarding"), // cannot be an onboarding link 50 51 ); 51 52 return route?.path ?? "/"; 52 53 }, [routes, location]);
+22
src/stores/onboarding/index.tsx
··· 1 + import { create } from "zustand"; 2 + import { persist } from "zustand/middleware"; 3 + import { immer } from "zustand/middleware/immer"; 4 + 5 + export interface OnboardingStore { 6 + skipped: boolean; 7 + setSkipped(v: boolean): void; 8 + } 9 + 10 + export const useOnboardingStore = create( 11 + persist( 12 + immer<OnboardingStore>((set) => ({ 13 + skipped: false, 14 + setSkipped(v) { 15 + set((s) => { 16 + s.skipped = v; 17 + }); 18 + }, 19 + })), 20 + { name: "__MW::onboarding" }, 21 + ), 22 + );
+23
src/utils/onboarding.ts
··· 1 + import { isExtensionActive } from "@/backend/extension/messaging"; 2 + import { conf } from "@/setup/config"; 3 + import { useAuthStore } from "@/stores/auth"; 4 + import { useOnboardingStore } from "@/stores/onboarding"; 5 + 6 + export async function needsOnboarding(): Promise<boolean> { 7 + // if onboarding is dislabed, no onboarding needed 8 + if (!conf().HAS_ONBOARDING) return false; 9 + 10 + // if extension is active and working, no onboarding needed 11 + const extensionActive = await isExtensionActive(); 12 + if (extensionActive) return false; 13 + 14 + // if there is any custom proxy urls, no onboarding needed 15 + const proxyUrls = useAuthStore.getState().proxySet; 16 + if (proxyUrls) return false; 17 + 18 + // if onboarding has been skipped, no onboarding needed 19 + const skipped = useOnboardingStore.getState().skipped; 20 + if (skipped) return false; 21 + 22 + return true; 23 + }