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:
parent
f2266bff6b
commit
3c5fb66073
23 changed files with 391 additions and 110 deletions
|
@ -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",
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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";
|
||||||
|
|
||||||
|
|
|
@ -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";
|
||||||
|
|
||||||
|
|
203
src/components/player/atoms/Episodes.tsx
Normal file
203
src/components/player/atoms/Episodes.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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>
|
||||||
|
|
|
@ -40,22 +40,22 @@ export function Time() {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const timeString = `${formatSeconds(currentTime, hasHours)} / ${formatSeconds(
|
||||||
|
duration,
|
||||||
|
hasHours
|
||||||
|
)}`;
|
||||||
|
const timeFinishedString = `${t("videoPlayer.timeLeft", {
|
||||||
|
timeLeft: formatSeconds(
|
||||||
|
secondsRemaining,
|
||||||
|
durationExceedsHour(secondsRemaining)
|
||||||
|
),
|
||||||
|
})} • ${formattedTimeFinished}`;
|
||||||
|
|
||||||
const child =
|
const child =
|
||||||
timeFormat === VideoPlayerTimeFormat.REGULAR ? (
|
timeFormat === VideoPlayerTimeFormat.REGULAR ? (
|
||||||
<>
|
<span>{timeString}</span>
|
||||||
{formatSeconds(currentTime, hasHours)}{" "}
|
|
||||||
<span>/ {formatSeconds(duration, hasHours)}</span>
|
|
||||||
</>
|
|
||||||
) : (
|
) : (
|
||||||
<>
|
<span>{timeFinishedString}</span>
|
||||||
{t("videoPlayer.timeLeft", {
|
|
||||||
timeLeft: formatSeconds(
|
|
||||||
secondsRemaining,
|
|
||||||
durationExceedsHour(secondsRemaining)
|
|
||||||
),
|
|
||||||
})}{" "}
|
|
||||||
• {formattedTimeFinished}
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -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";
|
||||||
|
|
|
@ -29,15 +29,17 @@ 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");
|
||||||
|
|
||||||
hls = new Hls({ enableWorker: false });
|
if (!hls) {
|
||||||
hls.on(Hls.Events.ERROR, (event, data) => {
|
hls = new Hls({ enableWorker: false });
|
||||||
console.error("HLS error", data);
|
hls.on(Hls.Events.ERROR, (event, data) => {
|
||||||
if (data.fatal) {
|
console.error("HLS error", data);
|
||||||
throw new Error(
|
if (data.fatal) {
|
||||||
`HLS ERROR:${data.error?.message ?? "Something went wrong"}`
|
throw new Error(
|
||||||
);
|
`HLS ERROR:${data.error?.message ?? "Something went wrong"}`
|
||||||
}
|
);
|
||||||
});
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
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();
|
||||||
},
|
},
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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,25 +82,36 @@ 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"
|
onClick={props.onClick}
|
||||||
onClick={props.onClick}
|
>
|
||||||
>
|
<Icon className="text-xl" icon={Icons.ARROW_LEFT} />
|
||||||
<Icon className="text-xl" icon={Icons.ARROW_LEFT} />
|
</button>
|
||||||
</button>
|
<span className="line-clamp-1 break-all">{props.children}</span>
|
||||||
<span>{props.children}</span>
|
</Title>
|
||||||
</div>
|
|
||||||
<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,
|
||||||
|
|
|
@ -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";
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -60,23 +60,26 @@ export function useInternalOverlayRouter(id: string) {
|
||||||
setTransition(null);
|
setTransition(null);
|
||||||
}, [setRoute, route, setTransition]);
|
}, [setRoute, route, setTransition]);
|
||||||
|
|
||||||
const open = useCallback(() => {
|
const open = useCallback(
|
||||||
const anchor = document.getElementById(`__overlayRouter::${id}`);
|
(defaultRoute = "/") => {
|
||||||
if (anchor) {
|
const anchor = document.getElementById(`__overlayRouter::${id}`);
|
||||||
const rect = anchor.getBoundingClientRect();
|
if (anchor) {
|
||||||
setAnchorPoint({
|
const rect = anchor.getBoundingClientRect();
|
||||||
h: rect.height,
|
setAnchorPoint({
|
||||||
w: rect.width,
|
h: rect.height,
|
||||||
x: rect.x,
|
w: rect.width,
|
||||||
y: rect.y,
|
x: rect.x,
|
||||||
});
|
y: rect.y,
|
||||||
} else {
|
});
|
||||||
setAnchorPoint(null);
|
} else {
|
||||||
}
|
setAnchorPoint(null);
|
||||||
|
}
|
||||||
|
|
||||||
setTransition(null);
|
setTransition(null);
|
||||||
setRoute(`/${id}`);
|
setRoute(joinPath(splitPath(defaultRoute, id)));
|
||||||
}, [id, setRoute, setTransition, setAnchorPoint]);
|
},
|
||||||
|
[id, setRoute, setTransition, setAnchorPoint]
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
showBackwardsTransition,
|
showBackwardsTransition,
|
||||||
|
|
|
@ -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>
|
||||||
<TheRouter>
|
<HelmetProvider>
|
||||||
<Suspense fallback="">
|
<TheRouter>
|
||||||
<LazyLoadedApp />
|
<Suspense fallback="">
|
||||||
</Suspense>
|
<LazyLoadedApp />
|
||||||
</TheRouter>
|
</Suspense>
|
||||||
|
</TheRouter>
|
||||||
|
</HelmetProvider>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
</React.StrictMode>,
|
</React.StrictMode>,
|
||||||
document.getElementById("root")
|
document.getElementById("root")
|
||||||
|
|
|
@ -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";
|
||||||
|
|
|
@ -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) => {
|
||||||
|
|
|
@ -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";
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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,
|
||||||
|
|
Loading…
Reference in a new issue