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:
Jelle van Snik 2023-01-23 01:55:57 +01:00
parent 1f7e8abda5
commit b8e49850f4
9 changed files with 344 additions and 30 deletions

View File

@ -8,25 +8,25 @@ const a11yOff = Object.keys(require("eslint-plugin-jsx-a11y").rules).reduce(
module.exports = { module.exports = {
env: { env: {
browser: true, browser: true
}, },
extends: [ extends: [
"airbnb", "airbnb",
"airbnb/hooks", "airbnb/hooks",
"plugin:@typescript-eslint/recommended", "plugin:@typescript-eslint/recommended",
"prettier", "prettier",
"plugin:prettier/recommended", "plugin:prettier/recommended"
], ],
ignorePatterns: ["public/*", "dist/*", "/*.js", "/*.ts"], ignorePatterns: ["public/*", "dist/*", "/*.js", "/*.ts"],
parser: "@typescript-eslint/parser", parser: "@typescript-eslint/parser",
parserOptions: { parserOptions: {
project: "./tsconfig.json", project: "./tsconfig.json",
tsconfigRootDir: "./", tsconfigRootDir: "./"
}, },
settings: { settings: {
"import/resolver": { "import/resolver": {
typescript: {}, typescript: {}
}, }
}, },
plugins: ["@typescript-eslint", "import"], plugins: ["@typescript-eslint", "import"],
rules: { rules: {
@ -48,18 +48,19 @@ module.exports = {
"no-continue": "off", "no-continue": "off",
"no-eval": "off", "no-eval": "off",
"no-await-in-loop": "off", "no-await-in-loop": "off",
"no-nested-ternary": "off",
"react/jsx-filename-extension": [ "react/jsx-filename-extension": [
"error", "error",
{ extensions: [".js", ".tsx", ".jsx"] }, { extensions: [".js", ".tsx", ".jsx"] }
], ],
"import/extensions": [ "import/extensions": [
"error", "error",
"ignorePackages", "ignorePackages",
{ {
ts: "never", ts: "never",
tsx: "never", tsx: "never"
}, }
], ],
...a11yOff, ...a11yOff
}, }
}; };

View File

@ -10,6 +10,7 @@ export enum Icons {
ARROW_RIGHT = "arrowRight", ARROW_RIGHT = "arrowRight",
CHEVRON_DOWN = "chevronDown", CHEVRON_DOWN = "chevronDown",
CHEVRON_RIGHT = "chevronRight", CHEVRON_RIGHT = "chevronRight",
CHEVRON_LEFT = "chevronLeft",
CLAPPER_BOARD = "clapperBoard", CLAPPER_BOARD = "clapperBoard",
FILM = "film", FILM = "film",
DRAGON = "dragon", DRAGON = "dragon",
@ -26,6 +27,7 @@ export enum Icons {
X = "x", X = "x",
EDIT = "edit", EDIT = "edit",
AIRPLAY = "airplay", AIRPLAY = "airplay",
EPISODES = "episodes",
} }
export interface IconProps { 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>`, 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>`, 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>`, 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>`, 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>`, 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>`, 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>`, 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>`, 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>`, 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) => { export const Icon = memo((props: IconProps) => {

View File

@ -9,6 +9,7 @@ import { LoadingControl } from "./controls/LoadingControl";
import { MiddlePauseControl } from "./controls/MiddlePauseControl"; import { MiddlePauseControl } from "./controls/MiddlePauseControl";
import { PauseControl } from "./controls/PauseControl"; import { PauseControl } from "./controls/PauseControl";
import { ProgressControl } from "./controls/ProgressControl"; import { ProgressControl } from "./controls/ProgressControl";
import { SeriesSelectionControl } from "./controls/SeriesSelectionControl";
import { ShowTitleControl } from "./controls/ShowTitleControl"; import { ShowTitleControl } from "./controls/ShowTitleControl";
import { TimeControl } from "./controls/TimeControl"; import { TimeControl } from "./controls/TimeControl";
import { VolumeControl } from "./controls/VolumeControl"; import { VolumeControl } from "./controls/VolumeControl";
@ -93,6 +94,7 @@ export function DecoratedVideoPlayer(
<div className="flex items-center"> <div className="flex items-center">
<LeftSideControls /> <LeftSideControls />
<div className="flex-1" /> <div className="flex-1" />
<SeriesSelectionControl />
<AirplayControl /> <AirplayControl />
<ChromeCastControl /> <ChromeCastControl />
<FullscreenControl /> <FullscreenControl />

View 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>
);
}

View File

@ -1,4 +1,9 @@
import {
MWSeasonMeta,
MWSeasonWithEpisodeMeta,
} from "@/backend/metadata/types";
import { useEffect, useRef } from "react"; import { useEffect, useRef } from "react";
import { PlayerContext } from "../hooks/useVideoPlayer";
import { useVideoPlayerState } from "../VideoContext"; import { useVideoPlayerState } from "../VideoContext";
interface ShowControlProps { interface ShowControlProps {
@ -6,9 +11,28 @@ interface ShowControlProps {
episodeId: string; episodeId: string;
seasonId: string; seasonId: string;
}; };
seasons: MWSeasonMeta[];
seasonData: MWSeasonWithEpisodeMeta;
onSelect?: (state: { episodeId?: string; seasonId?: string }) => void; 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) { export function ShowControl(props: ShowControlProps) {
const { videoState } = useVideoPlayerState(); const { videoState } = useVideoPlayerState();
const lastState = useRef<{ const lastState = useRef<{
@ -19,14 +43,13 @@ export function ShowControl(props: ShowControlProps) {
seasonId: props.series?.seasonId, seasonId: props.series?.seasonId,
}); });
const hasInitialized = useRef(false);
useEffect(() => { useEffect(() => {
videoState.setShowData({ if (hasInitialized.current) return;
current: props.series, if (!videoState.hasInitialized) return;
isSeries: !!props.series, setVideoShowState(videoState, props);
}); hasInitialized.current = true;
// we only want it to run when props change, not when videoState changes }, [props, videoState]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [props]);
useEffect(() => { useEffect(() => {
const currentState = { const currentState = {

View File

@ -1,19 +1,30 @@
import { useMemo } from "react";
import { useVideoPlayerState } from "../VideoContext"; import { useVideoPlayerState } from "../VideoContext";
export function ShowTitleControl() { export function ShowTitleControl() {
const { videoState } = useVideoPlayerState(); const { videoState } = useVideoPlayerState();
if (!videoState.seasonData.isSeries) return null; const { current, seasons } = videoState.seasonData;
if (!videoState.seasonData.title || !videoState.seasonData.current)
return null;
const cur = videoState.seasonData.current; const currentSeasonInfo = useMemo(() => {
const selectedText = `S${cur.season} E${cur.episode}`; 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 ( 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>{selectedText}</span>
<span className="opacity-50">{videoState.seasonData.title}</span> <span className="opacity-50">{currentEpisodeInfo?.title}</span>
</p> </p>
); );
} }

View File

@ -17,6 +17,16 @@ interface ShowData {
seasonId: string; seasonId: string;
}; };
isSeries: boolean; isSeries: boolean;
seasons?: {
id: string;
number: number;
title: string;
episodes?: {
id: string;
number: number;
title: string;
}[];
}[];
} }
export interface PlayerControls { export interface PlayerControls {
@ -30,6 +40,7 @@ export interface PlayerControls {
setLeftControlsHover(hovering: boolean): void; setLeftControlsHover(hovering: boolean): void;
initPlayer(sourceUrl: string, sourceType: MWStreamType): void; initPlayer(sourceUrl: string, sourceType: MWStreamType): void;
setShowData(data: ShowData): void; setShowData(data: ShowData): void;
setCurrentEpisode(sId: string, eId: string): void;
startAirplay(): void; startAirplay(): void;
} }
@ -45,6 +56,7 @@ export const initialControls: PlayerControls = {
initPlayer: () => null, initPlayer: () => null,
setShowData: () => null, setShowData: () => null,
startAirplay: () => null, startAirplay: () => null,
setCurrentEpisode: () => null,
}; };
export function populateControls( export function populateControls(
@ -120,6 +132,18 @@ export function populateControls(
setShowData(data) { setShowData(data) {
update((s) => ({ ...s, seasonData: data })); update((s) => ({ ...s, seasonData: data }));
}, },
setCurrentEpisode(sId: string, eId: string) {
update((s) => ({
...s,
seasonData: {
...s.seasonData,
current: {
seasonId: sId,
episodeId: eId,
},
},
}));
},
startAirplay() { startAirplay() {
const videoPlayer = player as any; const videoPlayer = player as any;
if (videoPlayer.webkitShowPlaybackTargetPicker) if (videoPlayer.webkitShowPlaybackTargetPicker)

View File

@ -29,6 +29,12 @@ export type PlayerState = {
episodeId: string; episodeId: string;
seasonId: string; seasonId: string;
}; };
seasons?: {
id: string;
number: number;
title: string;
episodes?: { id: string; number: number; title: string }[];
}[];
}; };
error: null | { error: null | {
name: string; name: string;

View File

@ -93,6 +93,7 @@ interface MediaViewPlayerProps {
meta: DetailedMeta; meta: DetailedMeta;
stream: MWStream; stream: MWStream;
selected: SelectedMediaData; selected: SelectedMediaData;
onChangeStream: (sId: string, eId: string) => void;
} }
export function MediaViewPlayer(props: MediaViewPlayerProps) { export function MediaViewPlayer(props: MediaViewPlayerProps) {
const goBack = useGoBack(); const goBack = useGoBack();
@ -120,13 +121,20 @@ export function MediaViewPlayer(props: MediaViewPlayerProps) {
startAt={firstStartTime.current} startAt={firstStartTime.current}
onProgress={updateProgress} onProgress={updateProgress}
/> />
{props.selected.type === MWMediaType.SERIES ? ( {props.selected.type === MWMediaType.SERIES &&
props.meta.meta.type === MWMediaType.SERIES ? (
<ShowControl <ShowControl
series={{ series={{
seasonId: props.selected.season, seasonId: props.selected.season,
episodeId: props.selected.episode, 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} ) : null}
</DecoratedVideoPlayer> </DecoratedVideoPlayer>
@ -154,9 +162,25 @@ export function MediaView() {
); );
const [stream, setStream] = useState<MWStream | null>(null); const [stream, setStream] = useState<MWStream | null>(null);
const lastSearchValue = useRef<(string | undefined)[] | null>(null);
useEffect(() => { 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) => { exec(params.media, params.season).then((v) => {
setMeta(v ?? null); setMeta(v ?? null);
setStream(null);
if (v) { if (v) {
if (v.meta.type !== MWMediaType.SERIES) { if (v.meta.type !== MWMediaType.SERIES) {
setSelected({ setSelected({
@ -181,9 +205,7 @@ export function MediaView() {
} }
} else setSelected(null); } else setSelected(null);
}); });
// dont rerender when params changes }, [exec, history, params]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [exec, history]);
if (loading) return <MediaViewLoading onGoBack={goBack} />; if (loading) return <MediaViewLoading onGoBack={goBack} />;
if (error) return <MediaFetchErrorView />; if (error) return <MediaFetchErrorView />;
@ -206,5 +228,18 @@ export function MediaView() {
); );
// show stream once we have a stream // 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)}`
);
}}
/>
);
} }