movie-web/src/components/player/atoms/NextEpisodeButton.tsx

201 lines
6.0 KiB
TypeScript

import classNames from "classnames";
import { useCallback } 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 { Icon, Icons } from "@/components/Icon";
import { usePlayerMeta } from "@/components/player/hooks/usePlayerMeta";
import { Transition } from "@/components/utils/Transition";
import { PlayerMeta } from "@/stores/player/slices/source";
import { usePlayerStore } from "@/stores/player/store";
import { useProgressStore } from "@/stores/progress";
import { hasAired } from "../utils/aired";
function shouldShowNextEpisodeButton(
time: number,
duration: number,
): "always" | "hover" | "none" {
const percentage = time / duration;
const secondsFromEnd = duration - time;
if (secondsFromEnd <= 30) return "always";
if (percentage >= 0.9) return "hover";
return "none";
}
function Button(props: {
className: string;
onClick?: () => void;
children: React.ReactNode;
}) {
return (
<button
className={classNames(
"font-bold rounded h-10 w-40 scale-95 hover:scale-100 transition-all duration-200",
props.className,
)}
type="button"
onClick={props.onClick}
>
{props.children}
</button>
);
}
function useSeasons(mediaId: string, isLastEpisode: boolean = false) {
const state = useAsync(async () => {
if (isLastEpisode) {
const data = await getMetaFromId(MWMediaType.SERIES, mediaId ?? "");
if (data?.meta.type !== MWMediaType.SERIES) return null;
return data.meta.seasons;
}
}, [mediaId, isLastEpisode]);
return state;
}
function useNextSeasonEpisode(
nextSeason: MWSeasonMeta | undefined,
mediaId: string,
) {
const state = useAsync(async () => {
if (nextSeason) {
const data = await getMetaFromId(
MWMediaType.SERIES,
mediaId ?? "",
nextSeason?.id,
);
if (data?.meta.type !== MWMediaType.SERIES) return null;
const nextSeasonEpisodes = data?.meta?.seasonData?.episodes
.filter((episode) => hasAired(episode.air_date))
.map((episode) => ({
number: episode.number,
title: episode.title,
tmdbId: episode.id,
}));
if (nextSeasonEpisodes.length > 0) return nextSeasonEpisodes[0];
}
}, [mediaId, nextSeason?.id]);
return state;
}
export function NextEpisodeButton(props: {
controlsShowing: boolean;
onChange?: (meta: PlayerMeta) => void;
}) {
const { t } = useTranslation();
const duration = usePlayerStore((s) => s.progress.duration);
const isHidden = usePlayerStore((s) => s.interface.hideNextEpisodeBtn);
const meta = usePlayerStore((s) => s.meta);
const { setDirectMeta } = usePlayerMeta();
const hideNextEpisodeButton = usePlayerStore((s) => s.hideNextEpisodeButton);
const metaType = usePlayerStore((s) => s.meta?.type);
const time = usePlayerStore((s) => s.progress.time);
const showingState = shouldShowNextEpisodeButton(time, duration);
const status = usePlayerStore((s) => s.status);
const setShouldStartFromBeginning = usePlayerStore(
(s) => s.setShouldStartFromBeginning,
);
const updateItem = useProgressStore((s) => s.updateItem);
const isLastEpisode =
meta?.episode?.number === meta?.episodes?.at(-1)?.number;
const seasons = useSeasons(meta?.tmdbId ?? "", isLastEpisode);
const nextSeason = seasons.value?.find(
(season) => season.number === (meta?.season?.number ?? 0) + 1,
);
const nextSeasonEpisode = useNextSeasonEpisode(
nextSeason,
meta?.tmdbId ?? "",
);
let show = false;
if (showingState === "always") show = true;
else if (showingState === "hover" && props.controlsShowing) show = true;
if (isHidden || status !== "playing" || duration === 0) show = false;
const animation = showingState === "hover" ? "slide-up" : "fade";
let bottom = "bottom-[calc(6rem+env(safe-area-inset-bottom))]";
if (showingState === "always")
bottom = props.controlsShowing
? bottom
: "bottom-[calc(3rem+env(safe-area-inset-bottom))]";
const nextEp = isLastEpisode
? nextSeasonEpisode.value
: meta?.episodes?.find(
(v) => v.number === (meta?.episode?.number ?? 0) + 1,
);
const loadNextEpisode = useCallback(() => {
if (!meta || !nextEp) return;
const metaCopy = { ...meta };
metaCopy.episode = nextEp;
metaCopy.season =
isLastEpisode && nextSeason
? {
...nextSeason,
tmdbId: nextSeason.id,
}
: metaCopy.season;
setShouldStartFromBeginning(true);
setDirectMeta(metaCopy);
props.onChange?.(metaCopy);
const defaultProgress = { duration: 0, watched: 0 };
updateItem({
meta: metaCopy,
progress: defaultProgress,
});
}, [
setDirectMeta,
nextEp,
meta,
props,
setShouldStartFromBeginning,
updateItem,
isLastEpisode,
nextSeason,
]);
if (!meta?.episode || !nextEp) return null;
if (metaType !== "show") return null;
return (
<Transition
animation={animation}
show={show}
className="absolute right-[calc(3rem+env(safe-area-inset-right))] bottom-0"
>
<div
className={classNames([
"absolute bottom-0 right-0 transition-[bottom] duration-200 flex items-center space-x-3",
bottom,
])}
>
<Button
className="py-px box-content bg-buttons-secondary hover:bg-buttons-secondaryHover bg-opacity-90 text-buttons-secondaryText"
onClick={hideNextEpisodeButton}
>
{t("player.nextEpisode.cancel")}
</Button>
<Button
onClick={() => loadNextEpisode()}
className="bg-buttons-primary hover:bg-buttons-primaryHover text-buttons-primaryText flex justify-center items-center"
>
<Icon className="text-xl mr-1" icon={Icons.SKIP_EPISODE} />
{isLastEpisode && nextEp
? t("player.nextEpisode.nextSeason")
: t("player.nextEpisode.next")}
</Button>
</div>
</Transition>
);
}