mirror of
https://github.com/sussy-code/smov.git
synced 2024-12-29 16:07:40 +01:00
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:
parent
562ab8fa49
commit
5c1807c8f4
14 changed files with 200 additions and 42 deletions
|
@ -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"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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"
|
||||||
{props.children}
|
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}
|
||||||
|
</Flare.Child>
|
||||||
|
</Flare.Base>
|
||||||
</a.div>
|
</a.div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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} />
|
||||||
|
|
|
@ -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);
|
||||||
|
|
56
src/components/player/internals/ContextUtils.tsx
Normal file
56
src/components/player/internals/ContextUtils.tsx
Normal 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,
|
||||||
|
};
|
|
@ -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}`);
|
||||||
|
|
|
@ -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} />
|
||||||
|
|
54
src/pages/parts/player/MetaPart.tsx
Normal file
54
src/pages/parts/player/MetaPart.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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 />
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
});
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -123,6 +123,17 @@ module.exports = {
|
||||||
|
|
||||||
audio: {
|
audio: {
|
||||||
set: "#A75FC9"
|
set: "#A75FC9"
|
||||||
|
},
|
||||||
|
|
||||||
|
context: {
|
||||||
|
background: "#0C1216",
|
||||||
|
light: "#4D79A8",
|
||||||
|
border: "#4F5C66",
|
||||||
|
|
||||||
|
type: {
|
||||||
|
main: "#617A8A",
|
||||||
|
secondary: "#374A56"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue