settings menu styling + fix shows + fix back link and double redirects

Co-authored-by: Jip Frijlink <JipFr@users.noreply.github.com>
This commit is contained in:
mrjvs 2023-10-12 23:05:47 +02:00
parent 562ab8fa49
commit 5c1807c8f4
14 changed files with 200 additions and 42 deletions

View File

@ -19,7 +19,7 @@
<link rel="preconnect" href="https://fonts.googleapis.com" /> <link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link <link
href="https://fonts.googleapis.com/css2?family=Open+Sans:wght@400;600;700&display=swap" href="https://fonts.googleapis.com/css2?family=Open+Sans:wght@400;500;600;700&display=swap"
rel="stylesheet" rel="stylesheet"
/> />

View File

@ -44,10 +44,7 @@ export function OverlayPage(props: Props) {
show={show} show={show}
> >
<div <div
className={classNames([ className={classNames([props.className, ""])}
props.className,
"grid grid-rows-[auto,minmax(0,1fr)]",
])}
style={{ style={{
height: props.height ? `${props.height}px` : undefined, height: props.height ? `${props.height}px` : undefined,
maxHeight: "70vh", maxHeight: "70vh",

View File

@ -3,6 +3,7 @@ import { ReactNode, useEffect, useMemo, useRef } from "react";
import { OverlayAnchorPosition } from "@/components/overlays/positions/OverlayAnchorPosition"; import { OverlayAnchorPosition } from "@/components/overlays/positions/OverlayAnchorPosition";
import { OverlayMobilePosition } from "@/components/overlays/positions/OverlayMobilePosition"; import { OverlayMobilePosition } from "@/components/overlays/positions/OverlayMobilePosition";
import { Flare } from "@/components/utils/Flare";
import { useIsMobile } from "@/hooks/useIsMobile"; import { useIsMobile } from "@/hooks/useIsMobile";
import { useInternalOverlayRouter } from "@/hooks/useOverlayRouter"; import { useInternalOverlayRouter } from "@/hooks/useOverlayRouter";
import { useOverlayStore } from "@/stores/overlay/store"; import { useOverlayStore } from "@/stores/overlay/store";
@ -71,12 +72,18 @@ function RouterBase(props: { id: string; children: ReactNode }) {
}, [routeMeta?.height, routeMeta?.width, isMobile, api]); }, [routeMeta?.height, routeMeta?.width, isMobile, api]);
return ( return (
<a.div <a.div ref={ref} style={dimensions} className="overflow-hidden">
ref={ref} <Flare.Base className="group w-full h-full rounded-xl transition-colors duration-100 text-video-context-type-main">
style={dimensions} <Flare.Light
className="relative flex items-center justify-center overflow-hidden bg-red-500" flareSize={400}
> cssColorVar="--colors-video-context-light"
backgroundClass="bg-video-context-background duration-100"
className="rounded-xl opacity-100"
/>
<Flare.Child className="pointer-events-auto relative transition-transform duration-100">
{props.children} {props.children}
</Flare.Child>
</Flare.Base>
</a.div> </a.div>
); );
} }

View File

@ -6,15 +6,41 @@ import { Overlay } from "@/components/overlays/OverlayDisplay";
import { OverlayPage } from "@/components/overlays/OverlayPage"; import { OverlayPage } from "@/components/overlays/OverlayPage";
import { OverlayRouter } from "@/components/overlays/OverlayRouter"; import { OverlayRouter } from "@/components/overlays/OverlayRouter";
import { VideoPlayerButton } from "@/components/player/internals/Button"; import { VideoPlayerButton } from "@/components/player/internals/Button";
import { Context } from "@/components/player/internals/ContextUtils";
import { useOverlayRouter } from "@/hooks/useOverlayRouter"; import { useOverlayRouter } from "@/hooks/useOverlayRouter";
import { usePlayerStore } from "@/stores/player/store"; import { usePlayerStore } from "@/stores/player/store";
function SettingsOverlay({ id }: { id: string }) { function SettingsOverlay({ id }: { id: string }) {
const router = useOverlayRouter("settings");
return ( return (
<Overlay id={id}> <Overlay id={id}>
<OverlayRouter id={id}> <OverlayRouter id={id}>
<OverlayPage id={id} path="/" width={400} height={400}> <OverlayPage id={id} path="/" width={343} height={431}>
<p>This is settings menu, welcome!</p> <Context.Card>
<Context.Title>Ba ba ba ba my title</Context.Title>
<Context.Section>
Hi
<Context.Link onClick={() => router.navigate("/other")}>
<Context.LinkTitle>Go to page 2</Context.LinkTitle>
<Context.LinkChevron />
</Context.Link>
<Context.Link>
<Context.LinkTitle>Video source</Context.LinkTitle>
<Context.LinkChevron>SuperStream</Context.LinkChevron>
</Context.Link>
</Context.Section>
</Context.Card>
</OverlayPage>
<OverlayPage id={id} path="/other" width={343} height={431}>
<Context.Card>
<Context.Title>Some other bit</Context.Title>
<Context.Section>
<button type="button" onClick={() => router.navigate("/")}>
Go BACK PLS
</button>
</Context.Section>
</Context.Card>
</OverlayPage> </OverlayPage>
</OverlayRouter> </OverlayRouter>
</Overlay> </Overlay>

View File

@ -1,16 +1,16 @@
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useHistory } from "react-router-dom";
import { Icon, Icons } from "@/components/Icon"; import { Icon, Icons } from "@/components/Icon";
import { useGoBack } from "@/hooks/useGoBack";
export function BackLink() { export function BackLink(props: { url: string }) {
const { t } = useTranslation(); const { t } = useTranslation();
const goBack = useGoBack(); const history = useHistory();
return ( return (
<div className="flex items-center"> <div className="flex items-center">
<span <span
onClick={() => goBack()} onClick={() => history.push(props.url)}
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} />

View File

@ -13,9 +13,11 @@ export function usePlayer() {
const setMeta = usePlayerStore((s) => s.setMeta); const setMeta = usePlayerStore((s) => s.setMeta);
const status = usePlayerStore((s) => s.status); const status = usePlayerStore((s) => s.status);
const display = usePlayerStore((s) => s.display); const display = usePlayerStore((s) => s.display);
const reset = usePlayerStore((s) => s.reset);
const { init } = useInitializePlayer(); const { init } = useInitializePlayer();
return { return {
reset,
status, status,
setMeta(meta: PlayerMeta) { setMeta(meta: PlayerMeta) {
setMeta(meta); setMeta(meta);

View File

@ -0,0 +1,56 @@
import { Icon, Icons } from "@/components/Icon";
function Card(props: { children: React.ReactNode }) {
return <div className="px-6 py-8">{props.children}</div>;
}
function Title(props: { children: React.ReactNode }) {
return (
<h3 className="uppercase font-bold text-video-context-type-secondary text-sm pl-1 pb-2 border-b border-opacity-25 border-video-context-border mb-6">
{props.children}
</h3>
);
}
function Section(props: { children: React.ReactNode }) {
return <div className="my-5">{props.children}</div>;
}
function Link(props: { onClick?: () => void; children: React.ReactNode }) {
return (
<button
type="button"
className="flex justify-between items-center py-2 pl-3 pr-3 -ml-3 rounded hover:bg-video-context-border hover:bg-opacity-10 w-full"
style={{ width: "calc(100% + 1.5rem)" }}
onClick={props.onClick}
>
{props.children}
</button>
);
}
function LinkTitle(props: { children: React.ReactNode }) {
return (
<span className="text-video-context-type-main font-medium">
{props.children}
</span>
);
}
function LinkChevron(props: { children?: React.ReactNode }) {
return (
<span className="text-white flex items-center font-medium">
{props.children}
<Icon className="text-xl ml-1" icon={Icons.CHEVRON_RIGHT} />
</span>
);
}
export const Context = {
Card,
Title,
Section,
Link,
LinkTitle,
LinkChevron,
};

View File

@ -56,9 +56,9 @@ export function useInternalOverlayRouter(id: string) {
} }
const close = useCallback(() => { const close = useCallback(() => {
if (route) setRoute(null);
setTransition(null); setTransition(null);
setRoute(null); }, [setRoute, route, setTransition]);
}, [setRoute, setTransition]);
const open = useCallback(() => { const open = useCallback(() => {
const anchor = document.getElementById(`__overlayRouter::${id}`); const anchor = document.getElementById(`__overlayRouter::${id}`);

View File

@ -1,13 +1,11 @@
import { RunOutput } from "@movie-web/providers"; import { RunOutput } from "@movie-web/providers";
import { useCallback } from "react"; import { useCallback, useEffect, useState } from "react";
import { useParams } from "react-router-dom"; import { useParams } from "react-router-dom";
import { useAsync } from "react-use";
import { MWStreamType } from "@/backend/helpers/streams"; import { MWStreamType } from "@/backend/helpers/streams";
import { getMetaFromId } from "@/backend/metadata/getmeta";
import { decodeTMDBId } from "@/backend/metadata/tmdb";
import { usePlayer } from "@/components/player/hooks/usePlayer"; import { usePlayer } from "@/components/player/hooks/usePlayer";
import { usePlayerMeta } from "@/components/player/hooks/usePlayerMeta"; import { usePlayerMeta } from "@/components/player/hooks/usePlayerMeta";
import { MetaPart } from "@/pages/parts/player/MetaPart";
import { PlayerPart } from "@/pages/parts/player/PlayerPart"; import { PlayerPart } from "@/pages/parts/player/PlayerPart";
import { ScrapingPart } from "@/pages/parts/player/ScrapingPart"; import { ScrapingPart } from "@/pages/parts/player/ScrapingPart";
import { playerStatus } from "@/stores/player/slices/source"; import { playerStatus } from "@/stores/player/slices/source";
@ -18,18 +16,13 @@ export function PlayerView() {
episode?: string; episode?: string;
season?: string; season?: string;
}>(); }>();
const { status, playMedia } = usePlayer(); const { status, playMedia, reset } = usePlayer();
const { setPlayerMeta, scrapeMedia } = usePlayerMeta(); const { setPlayerMeta, scrapeMedia } = usePlayerMeta();
const [backUrl] = useState("/");
const { loading, error } = useAsync(async () => { useEffect(() => {
const data = decodeTMDBId(params.media); reset();
if (!data) return; }, [params.media, reset]);
const meta = await getMetaFromId(data.type, data.id, params.season);
if (!meta) return;
setPlayerMeta(meta);
}, []);
const playAfterScrape = useCallback( const playAfterScrape = useCallback(
(out: RunOutput | null) => { (out: RunOutput | null) => {
@ -57,12 +50,9 @@ export function PlayerView() {
); );
return ( return (
<PlayerPart> <PlayerPart backUrl={backUrl}>
{status === playerStatus.IDLE ? ( {status === playerStatus.IDLE ? (
<div className="flex items-center justify-center"> <MetaPart onGetMeta={setPlayerMeta} />
{loading ? <p>loading meta...</p> : null}
{error ? <p>failed to load meta!</p> : null}
</div>
) : null} ) : null}
{status === playerStatus.SCRAPING && scrapeMedia ? ( {status === playerStatus.SCRAPING && scrapeMedia ? (
<ScrapingPart media={scrapeMedia} onGetStream={playAfterScrape} /> <ScrapingPart media={scrapeMedia} onGetStream={playAfterScrape} />

View File

@ -0,0 +1,54 @@
import { useHistory, useParams } from "react-router-dom";
import { useAsync } from "react-use";
import { DetailedMeta, getMetaFromId } from "@/backend/metadata/getmeta";
import { decodeTMDBId } from "@/backend/metadata/tmdb";
import { MWMediaType } from "@/backend/metadata/types/mw";
export interface MetaPartProps {
onGetMeta?: (meta: DetailedMeta, episodeId?: string) => void;
}
export function MetaPart(props: MetaPartProps) {
const params = useParams<{
media: string;
episode?: string;
season?: string;
}>();
const history = useHistory();
const { loading, error } = useAsync(async () => {
const data = decodeTMDBId(params.media);
if (!data) return;
const meta = await getMetaFromId(data.type, data.id, params.season);
if (!meta) return;
// replace link with new link if youre not already on the right link
let epId = params.episode;
if (meta.meta.type === MWMediaType.SERIES) {
let ep = meta.meta.seasonData.episodes.find(
(v) => v.id === params.episode
);
if (!ep) ep = meta.meta.seasonData.episodes[0];
epId = ep.id;
if (
params.season !== meta.meta.seasonData.id ||
params.episode !== ep.id
) {
history.replace(
`/media/${params.media}/${meta.meta.seasonData.id}/${ep.id}`
);
}
}
props.onGetMeta?.(meta, epId);
}, []);
return (
<div className="flex items-center justify-center">
{loading ? <p>loading meta...</p> : null}
{error ? <p>failed to load meta!</p> : null}
</div>
);
}

View File

@ -6,6 +6,7 @@ import { useShouldShowControls } from "@/components/player/hooks/useShouldShowCo
export interface PlayerPartProps { export interface PlayerPartProps {
children?: ReactNode; children?: ReactNode;
backUrl: string;
onLoad?: () => void; onLoad?: () => void;
} }
@ -34,7 +35,7 @@ export function PlayerPart(props: PlayerPartProps) {
<Player.TopControls show={showTargets}> <Player.TopControls show={showTargets}>
<div className="grid grid-cols-[1fr,auto] xl:grid-cols-3 items-center"> <div className="grid grid-cols-[1fr,auto] xl:grid-cols-3 items-center">
<div className="flex space-x-3 items-center"> <div className="flex space-x-3 items-center">
<Player.BackLink /> <Player.BackLink url={props.backUrl} />
<span className="text mx-3 text-type-secondary">/</span> <span className="text mx-3 text-type-secondary">/</span>
<Player.Title /> <Player.Title />
<Player.BookmarkButton /> <Player.BookmarkButton />

View File

@ -67,13 +67,18 @@ function App() {
<QuickSearch /> <QuickSearch />
</Route> </Route>
<Route exact path="/search/:type"> <Route exact path="/search/:type">
<Redirect to="/browse" /> <Redirect to="/browse" push={false} />
</Route> </Route>
<Route exact path="/search/:type/:query?"> <Route exact path="/search/:type/:query?">
{({ match }) => { {({ match }) => {
if (match?.params.query) if (match?.params.query)
return <Redirect to={`/browse/${match?.params.query}`} />; return (
return <Redirect to="/browse" />; <Redirect
to={`/browse/${match?.params.query}`}
push={false}
/>
);
return <Redirect to="/browse" push={false} />;
}} }}
</Route> </Route>

View File

@ -1,9 +1,11 @@
import { DisplayInterface } from "@/components/player/display/displayInterface"; import { DisplayInterface } from "@/components/player/display/displayInterface";
import { playerStatus } from "@/stores/player/slices/source";
import { MakeSlice } from "@/stores/player/slices/types"; import { MakeSlice } from "@/stores/player/slices/types";
export interface DisplaySlice { export interface DisplaySlice {
display: DisplayInterface | null; display: DisplayInterface | null;
setDisplay(display: DisplayInterface | null): void; setDisplay(display: DisplayInterface | null): void;
reset(): void;
} }
export const createDisplaySlice: MakeSlice<DisplaySlice> = (set, get) => ({ export const createDisplaySlice: MakeSlice<DisplaySlice> = (set, get) => ({
@ -68,4 +70,11 @@ export const createDisplaySlice: MakeSlice<DisplaySlice> = (set, get) => ({
s.display = newDisplay; s.display = newDisplay;
}); });
}, },
reset() {
get().display?.destroy();
set((s) => {
s.status = playerStatus.IDLE;
s.meta = null;
});
},
}); });

View File

@ -123,6 +123,17 @@ module.exports = {
audio: { audio: {
set: "#A75FC9" set: "#A75FC9"
},
context: {
background: "#0C1216",
light: "#4D79A8",
border: "#4F5C66",
type: {
main: "#617A8A",
secondary: "#374A56"
}
} }
} }
} }