1
0
Fork 0
mirror of https://github.com/sussy-code/smov.git synced 2024-12-20 14:37:43 +01:00

add episode selector, fix bug where video doesnt unload properly, move to react helmet async to fix react warning

Co-authored-by: Jip Frijlink <JipFr@users.noreply.github.com>
This commit is contained in:
mrjvs 2023-10-14 19:28:27 +02:00
parent f2266bff6b
commit 3c5fb66073
23 changed files with 391 additions and 110 deletions

View file

@ -29,7 +29,7 @@
"react": "^17.0.2", "react": "^17.0.2",
"react-dom": "^17.0.2", "react-dom": "^17.0.2",
"react-ga4": "^2.0.0", "react-ga4": "^2.0.0",
"react-helmet": "^6.1.0", "react-helmet-async": "^1.3.0",
"react-i18next": "^12.1.1", "react-i18next": "^12.1.1",
"react-router-dom": "^5.2.0", "react-router-dom": "^5.2.0",
"react-stickynode": "^4.1.0", "react-stickynode": "^4.1.0",

View file

@ -80,9 +80,9 @@ dependencies:
react-ga4: react-ga4:
specifier: ^2.0.0 specifier: ^2.0.0
version: 2.1.0 version: 2.1.0
react-helmet: react-helmet-async:
specifier: ^6.1.0 specifier: ^1.3.0
version: 6.1.0(react@17.0.2) version: 1.3.0(react-dom@17.0.2)(react@17.0.2)
react-i18next: react-i18next:
specifier: ^12.1.1 specifier: ^12.1.1
version: 12.3.1(i18next@22.5.1)(react-dom@17.0.2)(react@17.0.2) version: 12.3.1(i18next@22.5.1)(react-dom@17.0.2)(react@17.0.2)
@ -4191,6 +4191,12 @@ packages:
side-channel: 1.0.4 side-channel: 1.0.4
dev: true dev: true
/invariant@2.2.4:
resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==}
dependencies:
loose-envify: 1.4.0
dev: false
/is-array-buffer@3.0.2: /is-array-buffer@3.0.2:
resolution: {integrity: sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==} resolution: {integrity: sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==}
dependencies: dependencies:
@ -5219,16 +5225,19 @@ packages:
resolution: {integrity: sha512-ZKS7PGNFqqMd3PJ6+C2Jtz/o1iU9ggiy8Y8nUeksgVuvNISbmrQtJiZNvC/TjDsqD0QlU5Wkgs7i+w9+OjHhhQ==} resolution: {integrity: sha512-ZKS7PGNFqqMd3PJ6+C2Jtz/o1iU9ggiy8Y8nUeksgVuvNISbmrQtJiZNvC/TjDsqD0QlU5Wkgs7i+w9+OjHhhQ==}
dev: false dev: false
/react-helmet@6.1.0(react@17.0.2): /react-helmet-async@1.3.0(react-dom@17.0.2)(react@17.0.2):
resolution: {integrity: sha512-4uMzEY9nlDlgxr61NL3XbKRy1hEkXmKNXhjbAIOVw5vcFrsdYbH2FEwcNyWvWinl103nXgzYNlns9ca+8kFiWw==} resolution: {integrity: sha512-9jZ57/dAn9t3q6hneQS0wukqC2ENOBgMNVEhb/ZG9ZSxUetzVIw4iAmEU38IaVg3QGYauQPhSeUTuIUtFglWpg==}
peerDependencies: peerDependencies:
react: '>=16.3.0' react: ^16.6.0 || ^17.0.0 || ^18.0.0
react-dom: ^16.6.0 || ^17.0.0 || ^18.0.0
dependencies: dependencies:
object-assign: 4.1.1 '@babel/runtime': 7.22.11
invariant: 2.2.4
prop-types: 15.8.1 prop-types: 15.8.1
react: 17.0.2 react: 17.0.2
react-dom: 17.0.2(react@17.0.2)
react-fast-compare: 3.2.2 react-fast-compare: 3.2.2
react-side-effect: 2.1.2(react@17.0.2) shallowequal: 1.1.0
dev: false dev: false
/react-i18next@12.3.1(i18next@22.5.1)(react-dom@17.0.2)(react@17.0.2): /react-i18next@12.3.1(i18next@22.5.1)(react-dom@17.0.2)(react@17.0.2):
@ -5295,14 +5304,6 @@ packages:
tiny-warning: 1.0.3 tiny-warning: 1.0.3
dev: false dev: false
/react-side-effect@2.1.2(react@17.0.2):
resolution: {integrity: sha512-PVjOcvVOyIILrYoyGEpDN3vmYNLdy1CajSFNt4TDsVQC5KpTijDvWVoR+/7Rz2xT978D8/ZtFceXxzsPwZEDvw==}
peerDependencies:
react: ^16.3.0 || ^17.0.0 || ^18.0.0
dependencies:
react: 17.0.2
dev: false
/react-stickynode@4.1.0(react-dom@17.0.2)(react@17.0.2): /react-stickynode@4.1.0(react-dom@17.0.2)(react@17.0.2):
resolution: {integrity: sha512-zylWgfad75jLfh/gYIayDcDWIDwO4weZrsZqDpjZ/axhF06zRjdCWFBgUr33Pvv2+htKWqPSFksWTyB6aMQ1ZQ==} resolution: {integrity: sha512-zylWgfad75jLfh/gYIayDcDWIDwO4weZrsZqDpjZ/axhF06zRjdCWFBgUr33Pvv2+htKWqPSFksWTyB6aMQ1ZQ==}
peerDependencies: peerDependencies:

View file

@ -1,4 +1,4 @@
import { Helmet } from "react-helmet"; import { Helmet } from "react-helmet-async";
import { useVideoPlayerDescriptor } from "@/_oldvideo/state/hooks"; import { useVideoPlayerDescriptor } from "@/_oldvideo/state/hooks";

View file

@ -1,4 +1,4 @@
import { Helmet } from "react-helmet"; import { Helmet } from "react-helmet-async";
import { Transition } from "@/components/Transition"; import { Transition } from "@/components/Transition";

View file

@ -0,0 +1,203 @@
import { ReactNode, useCallback, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { useAsync } from "react-use";
import { getMetaFromId } from "@/backend/metadata/getmeta";
import { MWMediaType, MWSeasonMeta } from "@/backend/metadata/types/mw";
import { Icons } from "@/components/Icon";
import { OverlayAnchor } from "@/components/overlays/OverlayAnchor";
import { Overlay } from "@/components/overlays/OverlayDisplay";
import { OverlayPage } from "@/components/overlays/OverlayPage";
import { OverlayRouter } from "@/components/overlays/OverlayRouter";
import { usePlayerMeta } from "@/components/player/hooks/usePlayerMeta";
import { VideoPlayerButton } from "@/components/player/internals/Button";
import { Context } from "@/components/player/internals/ContextUtils";
import { useOverlayRouter } from "@/hooks/useOverlayRouter";
import { usePlayerStore } from "@/stores/player/store";
function CenteredText(props: { children: React.ReactNode }) {
return (
<div className="h-full w-full flex justify-center items-center p-8 text-center">
{props.children}
</div>
);
}
function useSeasonData(mediaId: string, seasonId: string) {
const [seasons, setSeason] = useState<MWSeasonMeta[] | null>(null);
const state = useAsync(async () => {
const data = await getMetaFromId(MWMediaType.SERIES, mediaId, seasonId);
if (data?.meta.type !== MWMediaType.SERIES) return null;
setSeason(data.meta.seasons);
return {
season: data.meta.seasonData,
fullData: data,
};
}, [mediaId, seasonId]);
return [state, seasons] as const;
}
function SeasonsView({
selectedSeason,
setSeason,
}: {
selectedSeason: string;
setSeason: (id: string) => void;
}) {
const meta = usePlayerStore((s) => s.meta);
const [loadingState, seasons] = useSeasonData(
meta?.tmdbId ?? "",
selectedSeason
);
let content: ReactNode = null;
if (seasons) {
content = (
<Context.Section className="pb-6">
{seasons?.map((season) => {
return (
<Context.Link key={season.id} onClick={() => setSeason(season.id)}>
<Context.LinkTitle>{season.title}</Context.LinkTitle>
<Context.LinkChevron />
</Context.Link>
);
})}
</Context.Section>
);
} else if (loadingState.error)
content = <CenteredText>Error loading season</CenteredText>;
else if (loadingState.loading)
content = <CenteredText>Loading...</CenteredText>;
return (
<Context.CardWithScrollable>
<Context.Title>{meta?.title}</Context.Title>
{content}
</Context.CardWithScrollable>
);
}
function EpisodesView({
id,
selectedSeason,
goBack,
}: {
id: string;
selectedSeason: string;
goBack?: () => void;
}) {
const { t } = useTranslation();
const router = useOverlayRouter(id);
const { setPlayerMeta } = usePlayerMeta();
const meta = usePlayerStore((s) => s.meta);
const [loadingState] = useSeasonData(meta?.tmdbId ?? "", selectedSeason);
const playEpisode = useCallback(
(episodeId: string) => {
if (loadingState.value)
setPlayerMeta(loadingState.value.fullData, episodeId);
router.close();
},
[setPlayerMeta, loadingState, router]
);
let content: ReactNode = null;
if (loadingState.error)
content = <CenteredText>Error loading season</CenteredText>;
else if (loadingState.loading)
content = <CenteredText>Loading...</CenteredText>;
else if (loadingState.value) {
content = (
<Context.Section className="pb-6">
{loadingState.value.season.episodes.map((ep) => {
return (
<Context.Link
key={ep.id}
onClick={() => playEpisode(ep.id)}
active={ep.id === meta?.episode?.tmdbId}
>
<Context.LinkTitle>
<div className="text-left flex items-center space-x-3">
<span className="p-0.5 px-2 rounded inline bg-video-context-border bg-opacity-10">
E{ep.number}
</span>
<span className="line-clamp-1 break-all">{ep.title}</span>
</div>
</Context.LinkTitle>
<Context.LinkChevron />
</Context.Link>
);
})}
</Context.Section>
);
}
return (
<Context.CardWithScrollable>
<Context.BackLink onClick={goBack}>
{loadingState?.value?.season.title || t("videoPlayer.loading")}
</Context.BackLink>
{content}
</Context.CardWithScrollable>
);
}
function EpisodesOverlay({ id }: { id: string }) {
const router = useOverlayRouter(id);
const meta = usePlayerStore((s) => s.meta);
const [selectedSeason, setSelectedSeason] = useState(
meta?.season?.tmdbId ?? ""
);
const setSeason = useCallback(
(seasonId: string) => {
setSelectedSeason(seasonId);
router.navigate("/episodes");
},
[router]
);
return (
<Overlay id={id}>
<OverlayRouter id={id}>
<OverlayPage id={id} path="/" width={343} height={431}>
<SeasonsView setSeason={setSeason} selectedSeason={selectedSeason} />
</OverlayPage>
<OverlayPage id={id} path="/episodes" width={343} height={431}>
<EpisodesView
selectedSeason={selectedSeason}
id={id}
goBack={() => router.navigate("/")}
/>
</OverlayPage>
</OverlayRouter>
</Overlay>
);
}
export function Episodes() {
const { t } = useTranslation();
const router = useOverlayRouter("episodes");
const setHasOpenOverlay = usePlayerStore((s) => s.setHasOpenOverlay);
const type = usePlayerStore((s) => s.meta?.type);
useEffect(() => {
setHasOpenOverlay(router.isRouterActive);
}, [setHasOpenOverlay, router.isRouterActive]);
if (type !== "show") return null;
return (
<OverlayAnchor id={router.id}>
<VideoPlayerButton
onClick={() => router.open("/episodes")}
icon={Icons.EPISODES}
>
{t("videoPlayer.buttons.episodes")}
</VideoPlayerButton>
<EpisodesOverlay id={router.id} />
</OverlayAnchor>
);
}

View file

@ -101,7 +101,7 @@ function SettingsOverlay({ id }: { id: string }) {
<OverlayRouter id={id}> <OverlayRouter id={id}>
<OverlayPage id={id} path="/" width={343} height={431}> <OverlayPage id={id} path="/" width={343} height={431}>
<Context.Card> <Context.Card>
<Context.Title>Video settings</Context.Title> <Context.SectionTitle>Video settings</Context.SectionTitle>
<Context.Section> <Context.Section>
<Context.Link onClick={() => router.navigate("/quality")}> <Context.Link onClick={() => router.navigate("/quality")}>
<Context.LinkTitle>Quality</Context.LinkTitle> <Context.LinkTitle>Quality</Context.LinkTitle>
@ -119,11 +119,11 @@ function SettingsOverlay({ id }: { id: string }) {
</Context.Link> </Context.Link>
</Context.Section> </Context.Section>
<Context.Title>Viewing Experience</Context.Title> <Context.SectionTitle>Viewing Experience</Context.SectionTitle>
<Context.Section> <Context.Section>
<Context.Link onClick={() => router.navigate("/quality")}> <Context.Link onClick={() => router.navigate("/quality")}>
<Context.LinkTitle>Enable Captions</Context.LinkTitle> <Context.LinkTitle>Enable Captions</Context.LinkTitle>
<Context.IconButton icon={Icons.CHEVRON_DOWN} /> <Context.LinkChevron />
</Context.Link> </Context.Link>
<Context.Link> <Context.Link>
<Context.LinkTitle>Caption settings</Context.LinkTitle> <Context.LinkTitle>Caption settings</Context.LinkTitle>

View file

@ -40,22 +40,22 @@ export function Time() {
}, },
}); });
const child = const timeString = `${formatSeconds(currentTime, hasHours)} / ${formatSeconds(
timeFormat === VideoPlayerTimeFormat.REGULAR ? ( duration,
<> hasHours
{formatSeconds(currentTime, hasHours)}{" "} )}`;
<span>/ {formatSeconds(duration, hasHours)}</span> const timeFinishedString = `${t("videoPlayer.timeLeft", {
</>
) : (
<>
{t("videoPlayer.timeLeft", {
timeLeft: formatSeconds( timeLeft: formatSeconds(
secondsRemaining, secondsRemaining,
durationExceedsHour(secondsRemaining) durationExceedsHour(secondsRemaining)
), ),
})}{" "} })} ${formattedTimeFinished}`;
{formattedTimeFinished}
</> const child =
timeFormat === VideoPlayerTimeFormat.REGULAR ? (
<span>{timeString}</span>
) : (
<span>{timeFinishedString}</span>
); );
return ( return (

View file

@ -9,3 +9,4 @@ export * from "./Volume";
export * from "./Title"; export * from "./Title";
export * from "./EpisodeTitle"; export * from "./EpisodeTitle";
export * from "./Settings"; export * from "./Settings";
export * from "./Episodes";

View file

@ -29,6 +29,7 @@ export function makeVideoElementDisplayInterface(): DisplayInterface {
if (src.type === "hls") { if (src.type === "hls") {
if (!Hls.isSupported()) throw new Error("HLS not supported"); if (!Hls.isSupported()) throw new Error("HLS not supported");
if (!hls) {
hls = new Hls({ enableWorker: false }); hls = new Hls({ enableWorker: false });
hls.on(Hls.Events.ERROR, (event, data) => { hls.on(Hls.Events.ERROR, (event, data) => {
console.error("HLS error", data); console.error("HLS error", data);
@ -38,6 +39,7 @@ export function makeVideoElementDisplayInterface(): DisplayInterface {
); );
} }
}); });
}
hls.attachMedia(vid); hls.attachMedia(vid);
hls.loadSource(src.url); hls.loadSource(src.url);
@ -77,6 +79,21 @@ export function makeVideoElementDisplayInterface(): DisplayInterface {
}); });
} }
function unloadSource() {
if (videoElement) videoElement.removeAttribute("src");
if (hls) {
hls.destroy();
hls = null;
}
}
function destroyVideoElement() {
unloadSource();
if (videoElement) {
videoElement = null;
}
}
function fullscreenChange() { function fullscreenChange() {
isFullscreen = isFullscreen =
!!document.fullscreenElement || // other browsers !!document.fullscreenElement || // other browsers
@ -88,20 +105,18 @@ export function makeVideoElementDisplayInterface(): DisplayInterface {
on, on,
off, off,
destroy: () => { destroy: () => {
if (hls) hls.destroy(); destroyVideoElement();
if (videoElement) {
videoElement.src = "";
videoElement.remove();
}
fscreen.removeEventListener("fullscreenchange", fullscreenChange); fscreen.removeEventListener("fullscreenchange", fullscreenChange);
}, },
load(newSource) { load(newSource) {
if (!newSource) unloadSource();
source = newSource; source = newSource;
emit("loading", true); emit("loading", true);
setSource(); setSource();
}, },
processVideoElement(video) { processVideoElement(video) {
destroyVideoElement();
videoElement = video; videoElement = video;
setSource(); setSource();
}, },

View file

@ -17,7 +17,7 @@ export type DisplayInterfaceEvents = {
export interface DisplayInterface extends Listener<DisplayInterfaceEvents> { export interface DisplayInterface extends Listener<DisplayInterfaceEvents> {
play(): void; play(): void;
pause(): void; pause(): void;
load(source: LoadableSource): void; load(source: LoadableSource | null): void;
processVideoElement(video: HTMLVideoElement): void; processVideoElement(video: HTMLVideoElement): void;
processContainerElement(container: HTMLElement): void; processContainerElement(container: HTMLElement): void;
toggleFullscreen(): void; toggleFullscreen(): void;

View file

@ -1,3 +1,5 @@
import classNames from "classnames";
import { Icon, Icons } from "@/components/Icon"; import { Icon, Icons } from "@/components/Icon";
export function VideoPlayerButton(props: { export function VideoPlayerButton(props: {
@ -12,15 +14,21 @@ export function VideoPlayerButton(props: {
<button <button
type="button" type="button"
onClick={props.onClick} onClick={props.onClick}
className={[ className={classNames([
"p-2 rounded-full hover:bg-video-buttonBackground hover:bg-opacity-75 transition-transform duration-100", "p-2 rounded-full hover:bg-video-buttonBackground hover:bg-opacity-50 transition-transform duration-100 flex items-center",
props.activeClass ?? props.activeClass ??
"active:scale-110 active:bg-opacity-100 active:text-white", "active:scale-110 active:bg-opacity-75 active:text-white",
props.className ?? "", props.className ?? "",
].join(" ")} ])}
> >
{props.icon && ( {props.icon && (
<Icon className={props.iconSizeClass || "text-2xl"} icon={props.icon} /> <Icon
className={classNames(
props.iconSizeClass || "text-2xl",
props.children ? "mr-3" : ""
)}
icon={props.icon}
/>
)} )}
{props.children} {props.children}
</button> </button>

View file

@ -3,12 +3,26 @@ import classNames from "classnames";
import { Icon, Icons } from "@/components/Icon"; import { Icon, Icons } from "@/components/Icon";
function Card(props: { children: React.ReactNode }) { function Card(props: { children: React.ReactNode }) {
return <div className="px-6 py-0">{props.children}</div>; return (
<div className="h-full grid grid-rows-[1fr]">
<div className="px-6 h-full overflow-y-auto overflow-x-hidden">
{props.children}
</div>
</div>
);
} }
function Title(props: { children: React.ReactNode }) { function CardWithScrollable(props: { children: React.ReactNode }) {
return ( return (
<h3 className="uppercase mt-8 font-bold text-video-context-type-secondary text-sm pl-1 pb-2.5 border-b border-opacity-25 border-video-context-border mb-6"> <div className="[&>*]:px-6 h-full grid grid-rows-[auto,1fr] [&>*:nth-child(2)]:overflow-y-auto [&>*:nth-child(2)]:overflow-x-hidden">
{props.children}
</div>
);
}
function SectionTitle(props: { children: React.ReactNode }) {
return (
<h3 className="uppercase font-bold text-video-context-type-secondary text-sm pt-8 pl-1 pb-2.5 border-b border-opacity-25 border-video-context-border">
{props.children} {props.children}
</h3> </h3>
); );
@ -18,7 +32,7 @@ function LinkTitle(props: { children: React.ReactNode; textClass?: string }) {
return ( return (
<span <span
className={classNames([ className={classNames([
"font-medium", "font-medium text-left",
props.textClass || "text-video-context-type-main", props.textClass || "text-video-context-type-main",
])} ])}
> >
@ -27,16 +41,23 @@ function LinkTitle(props: { children: React.ReactNode; textClass?: string }) {
); );
} }
function Section(props: { children: React.ReactNode }) { function Section(props: { children: React.ReactNode; className?: string }) {
return <div className="my-5">{props.children}</div>; return (
<div className={classNames("pt-5", props.className)}>{props.children}</div>
);
} }
function Link(props: { onClick?: () => void; children: React.ReactNode }) { function Link(props: {
onClick?: () => void;
children: React.ReactNode;
active?: boolean;
}) {
const classes = classNames( const classes = classNames(
"flex justify-between items-center py-2 pl-3 pr-3 -ml-3 rounded w-full", "flex justify-between items-center py-2 pl-3 pr-3 -ml-3 rounded w-full",
{ {
"cursor-default": !props.onClick, "cursor-default": !props.onClick,
"hover:bg-video-context-border hover:bg-opacity-10": !!props.onClick, "hover:bg-video-context-border hover:bg-opacity-10": !!props.onClick,
"bg-video-context-border bg-opacity-10": props.active,
} }
); );
const styles = { width: "calc(100% + 1.5rem)" }; const styles = { width: "calc(100% + 1.5rem)" };
@ -61,14 +82,27 @@ function Link(props: { onClick?: () => void; children: React.ReactNode }) {
); );
} }
function Title(props: {
children: React.ReactNode;
rightSide?: React.ReactNode;
}) {
return (
<div>
<h3 className="font-bold text-video-context-type-main pb-3 pt-5 border-b border-opacity-25 border-video-context-border flex justify-between items-center">
<div className="flex items-center space-x-3">{props.children}</div>
<div>{props.rightSide}</div>
</h3>
</div>
);
}
function BackLink(props: { function BackLink(props: {
onClick?: () => void; onClick?: () => void;
children: React.ReactNode; children: React.ReactNode;
rightSide?: React.ReactNode; rightSide?: React.ReactNode;
}) { }) {
return ( return (
<h3 className="font-bold text-video-context-type-main pb-3 pt-5 border-b border-opacity-25 border-video-context-border mb-6 flex justify-between items-center"> <Title rightSide={props.rightSide}>
<div className="flex items-center space-x-3">
<button <button
type="button" type="button"
className="-ml-2 p-2 rounded hover:bg-video-context-light hover:bg-opacity-10" className="-ml-2 p-2 rounded hover:bg-video-context-light hover:bg-opacity-10"
@ -76,10 +110,8 @@ function BackLink(props: {
> >
<Icon className="text-xl" icon={Icons.ARROW_LEFT} /> <Icon className="text-xl" icon={Icons.ARROW_LEFT} />
</button> </button>
<span>{props.children}</span> <span className="line-clamp-1 break-all">{props.children}</span>
</div> </Title>
<div>{props.rightSide}</div>
</h3>
); );
} }
@ -124,7 +156,9 @@ function Anchor(props: { children: React.ReactNode; onClick: () => void }) {
export const Context = { export const Context = {
Card, Card,
CardWithScrollable,
Title, Title,
SectionTitle,
BackLink, BackLink,
Section, Section,
Link, Link,

View file

@ -1,4 +1,4 @@
import { Helmet } from "react-helmet"; import { Helmet } from "react-helmet-async";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { usePlayerStore } from "@/stores/player/store"; import { usePlayerStore } from "@/stores/player/store";

View file

@ -9,14 +9,24 @@ function useDisplayInterface() {
const display = usePlayerStore((s) => s.display); const display = usePlayerStore((s) => s.display);
const setDisplay = usePlayerStore((s) => s.setDisplay); const setDisplay = usePlayerStore((s) => s.setDisplay);
const displayRef = useRef(display);
useEffect(() => { useEffect(() => {
if (!display) { displayRef.current = display;
setDisplay(makeVideoElementDisplayInterface()); }, [display]);
useEffect(() => {
if (!displayRef.current) {
const newDisplay = makeVideoElementDisplayInterface();
displayRef.current = newDisplay;
setDisplay(newDisplay);
} }
return () => { return () => {
if (display) setDisplay(null); if (displayRef.current) {
displayRef.current = null;
setDisplay(null);
}
}; };
}, [display, setDisplay]); }, [setDisplay]);
} }
export function useShouldShowVideoElement() { export function useShouldShowVideoElement() {

View file

@ -60,7 +60,8 @@ export function useInternalOverlayRouter(id: string) {
setTransition(null); setTransition(null);
}, [setRoute, route, setTransition]); }, [setRoute, route, setTransition]);
const open = useCallback(() => { const open = useCallback(
(defaultRoute = "/") => {
const anchor = document.getElementById(`__overlayRouter::${id}`); const anchor = document.getElementById(`__overlayRouter::${id}`);
if (anchor) { if (anchor) {
const rect = anchor.getBoundingClientRect(); const rect = anchor.getBoundingClientRect();
@ -75,8 +76,10 @@ export function useInternalOverlayRouter(id: string) {
} }
setTransition(null); setTransition(null);
setRoute(`/${id}`); setRoute(joinPath(splitPath(defaultRoute, id)));
}, [id, setRoute, setTransition, setAnchorPoint]); },
[id, setRoute, setTransition, setAnchorPoint]
);
return { return {
showBackwardsTransition, showBackwardsTransition,

View file

@ -2,6 +2,7 @@ import "core-js/stable";
import React, { Suspense } from "react"; import React, { Suspense } from "react";
import type { ReactNode } from "react"; import type { ReactNode } from "react";
import ReactDOM from "react-dom"; import ReactDOM from "react-dom";
import { HelmetProvider } from "react-helmet-async";
import { BrowserRouter, HashRouter } from "react-router-dom"; import { BrowserRouter, HashRouter } from "react-router-dom";
import { registerSW } from "virtual:pwa-register"; import { registerSW } from "virtual:pwa-register";
@ -48,11 +49,13 @@ function TheRouter(props: { children: ReactNode }) {
ReactDOM.render( ReactDOM.render(
<React.StrictMode> <React.StrictMode>
<ErrorBoundary> <ErrorBoundary>
<HelmetProvider>
<TheRouter> <TheRouter>
<Suspense fallback=""> <Suspense fallback="">
<LazyLoadedApp /> <LazyLoadedApp />
</Suspense> </Suspense>
</TheRouter> </TheRouter>
</HelmetProvider>
</ErrorBoundary> </ErrorBoundary>
</React.StrictMode>, </React.StrictMode>,
document.getElementById("root") document.getElementById("root")

View file

@ -1,5 +1,5 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { Helmet } from "react-helmet"; import { Helmet } from "react-helmet-async";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { WideContainer } from "@/components/layout/WideContainer"; import { WideContainer } from "@/components/layout/WideContainer";

View file

@ -1,8 +1,7 @@
import { RunOutput } from "@movie-web/providers"; import { RunOutput } from "@movie-web/providers";
import { useCallback, useEffect, useState } from "react"; import { useCallback, useEffect, useRef, useState } from "react";
import { useParams } from "react-router-dom"; import { useParams } from "react-router-dom";
import { MWStreamType } from "@/backend/helpers/streams";
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 { convertRunoutputToSource } from "@/components/player/utils/convertRunoutputToSource"; import { convertRunoutputToSource } from "@/components/player/utils/convertRunoutputToSource";
@ -21,9 +20,13 @@ export function PlayerView() {
const { setPlayerMeta, scrapeMedia } = usePlayerMeta(); const { setPlayerMeta, scrapeMedia } = usePlayerMeta();
const [backUrl] = useState("/"); // TODO redirect to search when needed const [backUrl] = useState("/"); // TODO redirect to search when needed
const lastMedia = useRef(params.media);
useEffect(() => { useEffect(() => {
if (params.media === lastMedia.current) return;
lastMedia.current = params.media;
console.log("resetting");
reset(); reset();
}, [params.media, reset]); }, [params, reset]);
const playAfterScrape = useCallback( const playAfterScrape = useCallback(
(out: RunOutput | null) => { (out: RunOutput | null) => {

View file

@ -1,5 +1,5 @@
import { ReactNode } from "react"; import { ReactNode } from "react";
import { Helmet } from "react-helmet"; import { Helmet } from "react-helmet-async";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { VideoPlayerHeader } from "@/_oldvideo/components/parts/VideoPlayerHeader"; import { VideoPlayerHeader } from "@/_oldvideo/components/parts/VideoPlayerHeader";

View file

@ -63,7 +63,8 @@ export function PlayerPart(props: PlayerPartProps) {
{/* Do mobile controls here :) */} {/* Do mobile controls here :) */}
<Player.Time /> <Player.Time />
</Player.LeftSideControls> </Player.LeftSideControls>
<div className="flex items-center"> <div className="flex items-center space-x-3">
<Player.Episodes />
<Player.Settings /> <Player.Settings />
<Player.Fullscreen /> <Player.Fullscreen />
</div> </div>

View file

@ -200,7 +200,7 @@ input[type=range].styled-slider.slider-progress::-ms-fill-lower {
} }
::-webkit-scrollbar-thumb { ::-webkit-scrollbar-thumb {
background-color: theme("colors.denim-500"); background-color: theme("colors.video.context.border");
border: 5px solid transparent; border: 5px solid transparent;
border-left: 0; border-left: 0;
background-clip: content-box; background-clip: content-box;

View file

@ -81,7 +81,7 @@ export const createDisplaySlice: MakeSlice<DisplaySlice> = (set, get) => ({
}); });
}, },
reset() { reset() {
get().display?.destroy(); get().display?.load(null);
set((s) => { set((s) => {
s.status = playerStatus.IDLE; s.status = playerStatus.IDLE;
s.meta = null; s.meta = null;

View file

@ -2,7 +2,6 @@ import { ScrapeMedia } from "@movie-web/providers";
import { MakeSlice } from "@/stores/player/slices/types"; import { MakeSlice } from "@/stores/player/slices/types";
import { import {
LoadableSource,
SourceQuality, SourceQuality,
SourceSliceSource, SourceSliceSource,
selectQuality, selectQuality,