mirror of
https://github.com/movie-web/movie-web.git
synced 2024-12-28 23:06:06 +00:00
episode selection
Co-authored-by: James Hawkins <jhawki2005@gmail.com> Co-authored-by: Jip Frijlink <JipFr@users.noreply.github.com>
This commit is contained in:
parent
1f7e8abda5
commit
b8e49850f4
21
.eslintrc.js
21
.eslintrc.js
|
@ -8,25 +8,25 @@ const a11yOff = Object.keys(require("eslint-plugin-jsx-a11y").rules).reduce(
|
|||
|
||||
module.exports = {
|
||||
env: {
|
||||
browser: true,
|
||||
browser: true
|
||||
},
|
||||
extends: [
|
||||
"airbnb",
|
||||
"airbnb/hooks",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"prettier",
|
||||
"plugin:prettier/recommended",
|
||||
"plugin:prettier/recommended"
|
||||
],
|
||||
ignorePatterns: ["public/*", "dist/*", "/*.js", "/*.ts"],
|
||||
parser: "@typescript-eslint/parser",
|
||||
parserOptions: {
|
||||
project: "./tsconfig.json",
|
||||
tsconfigRootDir: "./",
|
||||
tsconfigRootDir: "./"
|
||||
},
|
||||
settings: {
|
||||
"import/resolver": {
|
||||
typescript: {},
|
||||
},
|
||||
typescript: {}
|
||||
}
|
||||
},
|
||||
plugins: ["@typescript-eslint", "import"],
|
||||
rules: {
|
||||
|
@ -48,18 +48,19 @@ module.exports = {
|
|||
"no-continue": "off",
|
||||
"no-eval": "off",
|
||||
"no-await-in-loop": "off",
|
||||
"no-nested-ternary": "off",
|
||||
"react/jsx-filename-extension": [
|
||||
"error",
|
||||
{ extensions: [".js", ".tsx", ".jsx"] },
|
||||
{ extensions: [".js", ".tsx", ".jsx"] }
|
||||
],
|
||||
"import/extensions": [
|
||||
"error",
|
||||
"ignorePackages",
|
||||
{
|
||||
ts: "never",
|
||||
tsx: "never",
|
||||
},
|
||||
tsx: "never"
|
||||
}
|
||||
],
|
||||
...a11yOff,
|
||||
},
|
||||
...a11yOff
|
||||
}
|
||||
};
|
||||
|
|
|
@ -10,6 +10,7 @@ export enum Icons {
|
|||
ARROW_RIGHT = "arrowRight",
|
||||
CHEVRON_DOWN = "chevronDown",
|
||||
CHEVRON_RIGHT = "chevronRight",
|
||||
CHEVRON_LEFT = "chevronLeft",
|
||||
CLAPPER_BOARD = "clapperBoard",
|
||||
FILM = "film",
|
||||
DRAGON = "dragon",
|
||||
|
@ -26,6 +27,7 @@ export enum Icons {
|
|||
X = "x",
|
||||
EDIT = "edit",
|
||||
AIRPLAY = "airplay",
|
||||
EPISODES = "episodes",
|
||||
}
|
||||
|
||||
export interface IconProps {
|
||||
|
@ -41,6 +43,7 @@ const iconList: Record<Icons, string> = {
|
|||
arrowLeft: `<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" class="feather feather-arrow-left"><line x1="19" y1="12" x2="5" y2="12"></line><polyline points="12 19 5 12 12 5"></polyline></svg>`,
|
||||
chevronDown: `<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-chevron-down"><polyline points="6 9 12 15 18 9"></polyline></svg>`,
|
||||
chevronRight: `<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-chevron-right"><polyline points="9 18 15 12 9 6"></polyline></svg>`,
|
||||
chevronLeft: `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-chevron-left"><polyline points="15 18 9 12 15 6"></polyline></svg>`,
|
||||
clapperBoard: `<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 512 512"><!--! Font Awesome Pro 6.0.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --><path fill="currentColor" d="M326.1 160l127.4-127.4C451.7 32.39 449.9 32 448 32h-86.06l-128 128H326.1zM166.1 160l128-128H201.9l-128 128H166.1zM497.7 56.19L393.9 160H512V96C512 80.87 506.5 67.15 497.7 56.19zM134.1 32H64C28.65 32 0 60.65 0 96v64h6.062L134.1 32zM0 416c0 35.35 28.65 64 64 64h384c35.35 0 64-28.65 64-64V192H0V416z"/></svg>`,
|
||||
film: `<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 512 512"><!--! Font Awesome Pro 6.0.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --><path fill="currentColor" d="M463.1 32h-416C21.49 32-.0001 53.49-.0001 80v352c0 26.51 21.49 48 47.1 48h416c26.51 0 48-21.49 48-48v-352C511.1 53.49 490.5 32 463.1 32zM111.1 408c0 4.418-3.582 8-8 8H55.1c-4.418 0-8-3.582-8-8v-48c0-4.418 3.582-8 8-8h47.1c4.418 0 8 3.582 8 8L111.1 408zM111.1 280c0 4.418-3.582 8-8 8H55.1c-4.418 0-8-3.582-8-8v-48c0-4.418 3.582-8 8-8h47.1c4.418 0 8 3.582 8 8V280zM111.1 152c0 4.418-3.582 8-8 8H55.1c-4.418 0-8-3.582-8-8v-48c0-4.418 3.582-8 8-8h47.1c4.418 0 8 3.582 8 8L111.1 152zM351.1 400c0 8.836-7.164 16-16 16H175.1c-8.836 0-16-7.164-16-16v-96c0-8.838 7.164-16 16-16h160c8.836 0 16 7.162 16 16V400zM351.1 208c0 8.836-7.164 16-16 16H175.1c-8.836 0-16-7.164-16-16v-96c0-8.838 7.164-16 16-16h160c8.836 0 16 7.162 16 16V208zM463.1 408c0 4.418-3.582 8-8 8h-47.1c-4.418 0-7.1-3.582-7.1-8l0-48c0-4.418 3.582-8 8-8h47.1c4.418 0 8 3.582 8 8V408zM463.1 280c0 4.418-3.582 8-8 8h-47.1c-4.418 0-8-3.582-8-8v-48c0-4.418 3.582-8 8-8h47.1c4.418 0 8 3.582 8 8V280zM463.1 152c0 4.418-3.582 8-8 8h-47.1c-4.418 0-8-3.582-8-8l0-48c0-4.418 3.582-8 7.1-8h47.1c4.418 0 8 3.582 8 8V152z"/></svg>`,
|
||||
dragon: `<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 640 512"><!--! Font Awesome Pro 6.0.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --><path fill="currentColor" d="M18.43 255.8L192 224L100.8 292.6C90.67 302.8 97.8 320 112 320h222.7c-9.499-26.5-14.75-54.5-14.75-83.38V194.2L200.3 106.8C176.5 90.88 145 92.75 123.3 111.2l-117.5 116.4C-6.562 238 2.436 258 18.43 255.8zM575.2 289.9l-100.7-50.25c-16.25-8.125-26.5-24.75-26.5-43V160h63.99l28.12 22.62C546.1 188.6 554.2 192 562.7 192h30.1c11.1 0 23.12-6.875 28.5-17.75l14.37-28.62c5.374-10.87 4.25-23.75-2.999-33.5l-74.49-99.37C552.1 4.75 543.5 0 533.5 0H296C288.9 0 285.4 8.625 290.4 13.62L351.1 64L292.4 88.75c-5.874 3-5.874 11.37 0 14.37L351.1 128l-.0011 108.6c0 72 35.99 139.4 95.99 179.4c-195.6 6.75-344.4 41-434.1 60.88c-8.124 1.75-13.87 9-13.87 17.38C.0463 504 8.045 512 17.79 512h499.1c63.24 0 119.6-47.5 122.1-110.8C642.3 354 617.1 310.9 575.2 289.9zM489.1 66.25l45.74 11.38c-2.75 11-12.5 18.88-24.12 18.25C497.7 95.25 484.8 83.38 489.1 66.25z"/></svg>`,
|
||||
|
@ -59,6 +62,7 @@ const iconList: Record<Icons, string> = {
|
|||
edit: `<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 512 512"><!--! Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --><path fill="currentColor" d="M362.7 19.3L314.3 67.7 444.3 197.7l48.4-48.4c25-25 25-65.5 0-90.5L453.3 19.3c-25-25-65.5-25-90.5 0zm-71 71L58.6 323.5c-10.4 10.4-18 23.3-22.2 37.4L1 481.2C-1.5 489.7 .8 498.8 7 505s15.3 8.5 23.7 6.1l120.3-35.4c14.1-4.2 27-11.8 37.4-22.2L421.7 220.3 291.7 90.3z"/></svg>`,
|
||||
bookmark_outline: `<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 384 512"><!--! Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --><path fill="currentColor" d="M336 0h-288C21.49 0 0 21.49 0 48v431.9c0 24.7 26.79 40.08 48.12 27.64L192 423.6l143.9 83.93C357.2 519.1 384 504.6 384 479.9V48C384 21.49 362.5 0 336 0zM336 452L192 368l-144 84V54C48 50.63 50.63 48 53.1 48h276C333.4 48 336 50.63 336 54V452z"/></svg>`,
|
||||
airplay: `<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-airplay"><path d="M5 17H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2h-1"></path><polygon fill="currentColor" points="12 15 17 21 7 21 12 15"></polygon></svg>`,
|
||||
episodes: `<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M3 4C1.34315 4 0 5.34314 0 7V13.9496C0 15.6065 1.34315 16.9496 3 16.9496H5.86645V14.9496H3C2.44772 14.9496 2 14.5019 2 13.9496V7C2 6.44771 2.44771 6 3 6H16.0327C16.585 6 17.0327 6.44772 17.0327 7V9.86645H19.0327V7C19.0327 5.34315 17.6896 4 16.0327 4H3Z" fill="currentColor"/><rect x="5.89929" y="10.5444" width="17" height="10" rx="2" stroke="currentColor" stroke-width="2"/></svg>`,
|
||||
};
|
||||
|
||||
export const Icon = memo((props: IconProps) => {
|
||||
|
|
|
@ -9,6 +9,7 @@ import { LoadingControl } from "./controls/LoadingControl";
|
|||
import { MiddlePauseControl } from "./controls/MiddlePauseControl";
|
||||
import { PauseControl } from "./controls/PauseControl";
|
||||
import { ProgressControl } from "./controls/ProgressControl";
|
||||
import { SeriesSelectionControl } from "./controls/SeriesSelectionControl";
|
||||
import { ShowTitleControl } from "./controls/ShowTitleControl";
|
||||
import { TimeControl } from "./controls/TimeControl";
|
||||
import { VolumeControl } from "./controls/VolumeControl";
|
||||
|
@ -93,6 +94,7 @@ export function DecoratedVideoPlayer(
|
|||
<div className="flex items-center">
|
||||
<LeftSideControls />
|
||||
<div className="flex-1" />
|
||||
<SeriesSelectionControl />
|
||||
<AirplayControl />
|
||||
<ChromeCastControl />
|
||||
<FullscreenControl />
|
||||
|
|
208
src/components/video/controls/SeriesSelectionControl.tsx
Normal file
208
src/components/video/controls/SeriesSelectionControl.tsx
Normal file
|
@ -0,0 +1,208 @@
|
|||
import React, { useCallback, useMemo, useState } from "react";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { Icon, Icons } from "@/components/Icon";
|
||||
import { useLoading } from "@/hooks/useLoading";
|
||||
import { MWMediaType, MWSeasonWithEpisodeMeta } from "@/backend/metadata/types";
|
||||
import { getMetaFromId } from "@/backend/metadata/getmeta";
|
||||
import { decodeJWId } from "@/backend/metadata/justwatch";
|
||||
import { Loading } from "@/components/layout/Loading";
|
||||
import { IconPatch } from "@/components/buttons/IconPatch";
|
||||
import { useVideoPlayerState } from "../VideoContext";
|
||||
import { VideoPlayerIconButton } from "../parts/VideoPlayerIconButton";
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function PopupThingy(props: {
|
||||
children?: React.ReactNode;
|
||||
containerClassName?: string;
|
||||
}) {
|
||||
return (
|
||||
<div className="absolute inset-x-0 h-0">
|
||||
<div className="absolute bottom-10 right-0 h-96 w-72 rounded-lg bg-denim-400">
|
||||
<div className={["h-full w-full", props.containerClassName].join(" ")}>
|
||||
{props.children}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function PopupSection(props: {
|
||||
children?: React.ReactNode;
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<div className={["p-4", props.className || ""].join(" ")}>
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function PopupEpisodeSelect() {
|
||||
const params = useParams<{
|
||||
media: string;
|
||||
}>();
|
||||
const { videoState } = useVideoPlayerState();
|
||||
const [isPickingSeason, setIsPickingSeason] = useState<boolean>(false);
|
||||
const { current, seasons } = videoState.seasonData;
|
||||
const [currentVisibleSeason, setCurrentVisibleSeason] = useState<{
|
||||
seasonId: string;
|
||||
season?: MWSeasonWithEpisodeMeta;
|
||||
} | null>(null);
|
||||
const [reqSeasonMeta, loading, error] = useLoading(
|
||||
(id: string, seasonId: string) => {
|
||||
return getMetaFromId(MWMediaType.SERIES, id, seasonId);
|
||||
}
|
||||
);
|
||||
const requestSeason = useCallback(
|
||||
(sId: string) => {
|
||||
setCurrentVisibleSeason({
|
||||
seasonId: sId,
|
||||
season: undefined,
|
||||
});
|
||||
setIsPickingSeason(false);
|
||||
reqSeasonMeta(decodeJWId(params.media)?.id as string, sId).then((v) => {
|
||||
if (v?.meta.type !== MWMediaType.SERIES) return;
|
||||
setCurrentVisibleSeason({
|
||||
seasonId: sId,
|
||||
season: v?.meta.seasonData,
|
||||
});
|
||||
});
|
||||
},
|
||||
[reqSeasonMeta, params.media]
|
||||
);
|
||||
|
||||
const currentSeasonId = currentVisibleSeason?.seasonId ?? current?.seasonId;
|
||||
|
||||
const setCurrent = useCallback(
|
||||
(seasonId: string, episodeId: string) => {
|
||||
videoState.setCurrentEpisode(seasonId, episodeId);
|
||||
},
|
||||
[videoState]
|
||||
);
|
||||
|
||||
const currentSeasonInfo = useMemo(() => {
|
||||
return seasons?.find((season) => season.id === currentSeasonId);
|
||||
}, [seasons, currentSeasonId]);
|
||||
|
||||
const currentSeasonEpisodes = useMemo(() => {
|
||||
if (currentVisibleSeason?.season) {
|
||||
return currentVisibleSeason?.season?.episodes;
|
||||
}
|
||||
return videoState?.seasonData.seasons?.find?.(
|
||||
(season) => season && season.id === currentSeasonId
|
||||
)?.episodes;
|
||||
}, [videoState, currentSeasonId, currentVisibleSeason]);
|
||||
|
||||
const toggleIsPickingSeason = () => {
|
||||
setIsPickingSeason(!isPickingSeason);
|
||||
};
|
||||
|
||||
const setSeason = (id: string) => {
|
||||
requestSeason(id);
|
||||
setCurrentVisibleSeason({ seasonId: id });
|
||||
};
|
||||
|
||||
if (isPickingSeason)
|
||||
return (
|
||||
<>
|
||||
<PopupSection className="flex items-center space-x-3 border-b border-denim-500 font-bold text-white">
|
||||
Pick a season
|
||||
</PopupSection>
|
||||
<PopupSection className="overflow-y-auto">
|
||||
<div className="space-y-1">
|
||||
{currentSeasonInfo
|
||||
? videoState?.seasonData?.seasons?.map?.((season) => (
|
||||
<div
|
||||
className="text-denim-800 -mx-2 flex items-center space-x-1 rounded p-2 text-white hover:bg-denim-600"
|
||||
key={season.id}
|
||||
onClick={() => setSeason(season.id)}
|
||||
>
|
||||
{season.title}
|
||||
</div>
|
||||
))
|
||||
: "No season"}
|
||||
</div>
|
||||
</PopupSection>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<PopupSection className="flex items-center space-x-3 border-b border-denim-600 font-bold text-white">
|
||||
<button
|
||||
className="-m-1.5 rounded p-1.5 hover:bg-denim-600"
|
||||
onClick={toggleIsPickingSeason}
|
||||
type="button"
|
||||
>
|
||||
<Icon icon={Icons.CHEVRON_LEFT} />
|
||||
</button>
|
||||
<span>{currentSeasonInfo?.title || ""}</span>
|
||||
</PopupSection>
|
||||
<PopupSection className="overflow-y-auto">
|
||||
{loading ? (
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<Loading />
|
||||
</div>
|
||||
) : error ? (
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<div className="flex flex-col flex-wrap items-center text-slate-400">
|
||||
<IconPatch
|
||||
icon={Icons.EYE_SLASH}
|
||||
className="text-xl text-bink-600"
|
||||
/>
|
||||
<p className="mt-6 w-full text-center">
|
||||
Something went wrong loading the episodes for{" "}
|
||||
{currentSeasonInfo?.title?.toLowerCase()}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-1">
|
||||
{currentSeasonEpisodes && currentSeasonInfo
|
||||
? currentSeasonEpisodes.map((e) => (
|
||||
<div
|
||||
className={[
|
||||
"text-denim-800 -mx-2 flex items-center space-x-1 rounded p-2 text-white hover:bg-denim-600",
|
||||
current?.episodeId === e.id &&
|
||||
"outline outline-2 outline-denim-700",
|
||||
].join(" ")}
|
||||
onClick={() => setCurrent(currentSeasonInfo.id, e.id)}
|
||||
key={e.id}
|
||||
>
|
||||
{e.number}. {e.title}
|
||||
</div>
|
||||
))
|
||||
: "No episodes"}
|
||||
</div>
|
||||
)}
|
||||
</PopupSection>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function SeriesSelectionControl(props: Props) {
|
||||
const { videoState } = useVideoPlayerState();
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
if (!videoState.seasonData.isSeries) return null;
|
||||
|
||||
return (
|
||||
<div className={props.className}>
|
||||
<div className="relative">
|
||||
{open ? (
|
||||
<PopupThingy containerClassName="grid grid-rows-[auto,minmax(0,1fr)]">
|
||||
<PopupEpisodeSelect />
|
||||
</PopupThingy>
|
||||
) : null}
|
||||
<VideoPlayerIconButton
|
||||
icon={Icons.EPISODES}
|
||||
text="Episodes"
|
||||
onClick={() => setOpen((s) => !s)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -1,4 +1,9 @@
|
|||
import {
|
||||
MWSeasonMeta,
|
||||
MWSeasonWithEpisodeMeta,
|
||||
} from "@/backend/metadata/types";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { PlayerContext } from "../hooks/useVideoPlayer";
|
||||
import { useVideoPlayerState } from "../VideoContext";
|
||||
|
||||
interface ShowControlProps {
|
||||
|
@ -6,9 +11,28 @@ interface ShowControlProps {
|
|||
episodeId: string;
|
||||
seasonId: string;
|
||||
};
|
||||
seasons: MWSeasonMeta[];
|
||||
seasonData: MWSeasonWithEpisodeMeta;
|
||||
onSelect?: (state: { episodeId?: string; seasonId?: string }) => void;
|
||||
}
|
||||
|
||||
function setVideoShowState(videoState: PlayerContext, props: ShowControlProps) {
|
||||
const seasonsWithEpisodes = props.seasons.map((v) => {
|
||||
if (v.id === props.seasonData.id)
|
||||
return {
|
||||
...v,
|
||||
episodes: props.seasonData.episodes,
|
||||
};
|
||||
return v;
|
||||
});
|
||||
|
||||
videoState.setShowData({
|
||||
current: props.series,
|
||||
isSeries: !!props.series,
|
||||
seasons: seasonsWithEpisodes,
|
||||
});
|
||||
}
|
||||
|
||||
export function ShowControl(props: ShowControlProps) {
|
||||
const { videoState } = useVideoPlayerState();
|
||||
const lastState = useRef<{
|
||||
|
@ -19,14 +43,13 @@ export function ShowControl(props: ShowControlProps) {
|
|||
seasonId: props.series?.seasonId,
|
||||
});
|
||||
|
||||
const hasInitialized = useRef(false);
|
||||
useEffect(() => {
|
||||
videoState.setShowData({
|
||||
current: props.series,
|
||||
isSeries: !!props.series,
|
||||
});
|
||||
// we only want it to run when props change, not when videoState changes
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [props]);
|
||||
if (hasInitialized.current) return;
|
||||
if (!videoState.hasInitialized) return;
|
||||
setVideoShowState(videoState, props);
|
||||
hasInitialized.current = true;
|
||||
}, [props, videoState]);
|
||||
|
||||
useEffect(() => {
|
||||
const currentState = {
|
||||
|
|
|
@ -1,19 +1,30 @@
|
|||
import { useMemo } from "react";
|
||||
import { useVideoPlayerState } from "../VideoContext";
|
||||
|
||||
export function ShowTitleControl() {
|
||||
const { videoState } = useVideoPlayerState();
|
||||
|
||||
if (!videoState.seasonData.isSeries) return null;
|
||||
if (!videoState.seasonData.title || !videoState.seasonData.current)
|
||||
return null;
|
||||
const { current, seasons } = videoState.seasonData;
|
||||
|
||||
const cur = videoState.seasonData.current;
|
||||
const selectedText = `S${cur.season} E${cur.episode}`;
|
||||
const currentSeasonInfo = useMemo(() => {
|
||||
return seasons?.find((season) => season.id === current?.seasonId);
|
||||
}, [seasons, current]);
|
||||
|
||||
const currentEpisodeInfo = useMemo(() => {
|
||||
return currentSeasonInfo?.episodes?.find(
|
||||
(episode) => episode.id === current?.episodeId
|
||||
);
|
||||
}, [currentSeasonInfo, current]);
|
||||
|
||||
if (!videoState.seasonData.isSeries) return null;
|
||||
if (!videoState.seasonData.current) return null;
|
||||
|
||||
const selectedText = `S${currentSeasonInfo?.number} E${currentEpisodeInfo?.number}`;
|
||||
|
||||
return (
|
||||
<p className="ml-8 select-none space-x-2 font-bold text-white">
|
||||
<p className="ml-8 select-none space-x-2 text-white">
|
||||
<span>{selectedText}</span>
|
||||
<span className="opacity-50">{videoState.seasonData.title}</span>
|
||||
<span className="opacity-50">{currentEpisodeInfo?.title}</span>
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -17,6 +17,16 @@ interface ShowData {
|
|||
seasonId: string;
|
||||
};
|
||||
isSeries: boolean;
|
||||
seasons?: {
|
||||
id: string;
|
||||
number: number;
|
||||
title: string;
|
||||
episodes?: {
|
||||
id: string;
|
||||
number: number;
|
||||
title: string;
|
||||
}[];
|
||||
}[];
|
||||
}
|
||||
|
||||
export interface PlayerControls {
|
||||
|
@ -30,6 +40,7 @@ export interface PlayerControls {
|
|||
setLeftControlsHover(hovering: boolean): void;
|
||||
initPlayer(sourceUrl: string, sourceType: MWStreamType): void;
|
||||
setShowData(data: ShowData): void;
|
||||
setCurrentEpisode(sId: string, eId: string): void;
|
||||
startAirplay(): void;
|
||||
}
|
||||
|
||||
|
@ -45,6 +56,7 @@ export const initialControls: PlayerControls = {
|
|||
initPlayer: () => null,
|
||||
setShowData: () => null,
|
||||
startAirplay: () => null,
|
||||
setCurrentEpisode: () => null,
|
||||
};
|
||||
|
||||
export function populateControls(
|
||||
|
@ -120,6 +132,18 @@ export function populateControls(
|
|||
setShowData(data) {
|
||||
update((s) => ({ ...s, seasonData: data }));
|
||||
},
|
||||
setCurrentEpisode(sId: string, eId: string) {
|
||||
update((s) => ({
|
||||
...s,
|
||||
seasonData: {
|
||||
...s.seasonData,
|
||||
current: {
|
||||
seasonId: sId,
|
||||
episodeId: eId,
|
||||
},
|
||||
},
|
||||
}));
|
||||
},
|
||||
startAirplay() {
|
||||
const videoPlayer = player as any;
|
||||
if (videoPlayer.webkitShowPlaybackTargetPicker)
|
||||
|
|
|
@ -29,6 +29,12 @@ export type PlayerState = {
|
|||
episodeId: string;
|
||||
seasonId: string;
|
||||
};
|
||||
seasons?: {
|
||||
id: string;
|
||||
number: number;
|
||||
title: string;
|
||||
episodes?: { id: string; number: number; title: string }[];
|
||||
}[];
|
||||
};
|
||||
error: null | {
|
||||
name: string;
|
||||
|
|
|
@ -93,6 +93,7 @@ interface MediaViewPlayerProps {
|
|||
meta: DetailedMeta;
|
||||
stream: MWStream;
|
||||
selected: SelectedMediaData;
|
||||
onChangeStream: (sId: string, eId: string) => void;
|
||||
}
|
||||
export function MediaViewPlayer(props: MediaViewPlayerProps) {
|
||||
const goBack = useGoBack();
|
||||
|
@ -120,13 +121,20 @@ export function MediaViewPlayer(props: MediaViewPlayerProps) {
|
|||
startAt={firstStartTime.current}
|
||||
onProgress={updateProgress}
|
||||
/>
|
||||
{props.selected.type === MWMediaType.SERIES ? (
|
||||
{props.selected.type === MWMediaType.SERIES &&
|
||||
props.meta.meta.type === MWMediaType.SERIES ? (
|
||||
<ShowControl
|
||||
series={{
|
||||
seasonId: props.selected.season,
|
||||
episodeId: props.selected.episode,
|
||||
}}
|
||||
onSelect={(d) => console.log("selected stuff", d)}
|
||||
onSelect={(d) =>
|
||||
d.seasonId &&
|
||||
d.episodeId &&
|
||||
props.onChangeStream?.(d.seasonId, d.episodeId)
|
||||
}
|
||||
seasonData={props.meta.meta.seasonData}
|
||||
seasons={props.meta.meta.seasons}
|
||||
/>
|
||||
) : null}
|
||||
</DecoratedVideoPlayer>
|
||||
|
@ -154,9 +162,25 @@ export function MediaView() {
|
|||
);
|
||||
const [stream, setStream] = useState<MWStream | null>(null);
|
||||
|
||||
const lastSearchValue = useRef<(string | undefined)[] | null>(null);
|
||||
useEffect(() => {
|
||||
const newValue = [params.media, params.season, params.episode];
|
||||
const lastVal = lastSearchValue.current;
|
||||
|
||||
const isSame =
|
||||
lastVal?.[0] === newValue[0] &&
|
||||
(lastVal?.[1] === newValue[1] || !lastVal?.[1]) &&
|
||||
(lastVal?.[2] === newValue[2] || !lastVal?.[2]);
|
||||
|
||||
lastSearchValue.current = newValue;
|
||||
if (isSame && lastVal !== null) return;
|
||||
|
||||
setMeta(null);
|
||||
setStream(null);
|
||||
setSelected(null);
|
||||
exec(params.media, params.season).then((v) => {
|
||||
setMeta(v ?? null);
|
||||
setStream(null);
|
||||
if (v) {
|
||||
if (v.meta.type !== MWMediaType.SERIES) {
|
||||
setSelected({
|
||||
|
@ -181,9 +205,7 @@ export function MediaView() {
|
|||
}
|
||||
} else setSelected(null);
|
||||
});
|
||||
// dont rerender when params changes
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [exec, history]);
|
||||
}, [exec, history, params]);
|
||||
|
||||
if (loading) return <MediaViewLoading onGoBack={goBack} />;
|
||||
if (error) return <MediaFetchErrorView />;
|
||||
|
@ -206,5 +228,18 @@ export function MediaView() {
|
|||
);
|
||||
|
||||
// show stream once we have a stream
|
||||
return <MediaViewPlayer meta={meta} stream={stream} selected={selected} />;
|
||||
return (
|
||||
<MediaViewPlayer
|
||||
meta={meta}
|
||||
stream={stream}
|
||||
selected={selected}
|
||||
onChangeStream={(sId, eId) => {
|
||||
history.replace(
|
||||
`/media/${encodeURIComponent(params.media)}/${encodeURIComponent(
|
||||
sId
|
||||
)}/${encodeURIComponent(eId)}`
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue