Add cool new popout stuff

Co-authored-by: mrjvs <mistrjvs@gmail.com>
This commit is contained in:
Jip Fr 2023-03-12 21:49:58 +01:00
parent 89f77debca
commit c0867182d7
12 changed files with 308 additions and 239 deletions

View File

@ -116,7 +116,12 @@ export function FloatingCard(props: RootFloatingCardProps) {
} }
export function PopoutFloatingCard(props: FloatingCardProps) { export function PopoutFloatingCard(props: FloatingCardProps) {
return <FloatingCard className="overflow-hidden rounded-md" {...props} />; return (
<FloatingCard
className="overflow-hidden rounded-md bg-ash-300"
{...props}
/>
);
} }
export const FloatingCardView = { export const FloatingCardView = {
@ -149,7 +154,7 @@ export const FloatingCardView = {
); );
return ( return (
<div className="mb-[-1px] flex flex-col bg-[#1C161B] bg-opacity-80 backdrop-blur-xl"> <div className="flex flex-col bg-[#1C161B]">
<FloatingDragHandle /> <FloatingDragHandle />
<PopoutSection> <PopoutSection>
<div className="flex justify-between"> <div className="flex justify-between">
@ -165,12 +170,20 @@ export const FloatingCardView = {
</div> </div>
); );
}, },
Content(props: { children: React.ReactNode }) { Content(props: { children: React.ReactNode; noSection?: boolean }) {
return ( return (
<PopoutSection className="bg-ash-300"> <div className="grid h-full grid-rows-[auto,minmax(0,1fr)]">
{props.children} {props.noSection ? (
<div className="relative h-full overflow-y-auto bg-ash-300">
{props.children}
</div>
) : (
<PopoutSection className="relative h-full overflow-y-auto bg-ash-300">
{props.children}
</PopoutSection>
)}
<MobilePopoutSpacer /> <MobilePopoutSpacer />
</PopoutSection> </div>
); );
}, },
}; };

View File

@ -6,7 +6,7 @@ export function FloatingDragHandle() {
if (!isMobile) return null; if (!isMobile) return null;
return ( return (
<div className="mx-auto my-3 -mb-3 h-1 w-12 rounded-full bg-ash-500 bg-opacity-30" /> <div className="relative z-50 mx-auto my-3 -mb-3 h-1 w-12 rounded-full bg-ash-500 bg-opacity-30" />
); );
} }

View File

@ -22,7 +22,10 @@ export function FloatingView(props: Props) {
show={props.show} show={props.show}
> >
<div <div
className={[props.className ?? ""].join(" ")} className={[
props.className ?? "",
"grid grid-rows-[auto,minmax(0,1fr)]",
].join(" ")}
data-floating-page={props.show ? "true" : undefined} data-floating-page={props.show ? "true" : undefined}
style={{ style={{
height: props.height ? `${props.height}px` : undefined, height: props.height ? `${props.height}px` : undefined,

View File

@ -10,10 +10,7 @@ import { MobileCenterAction } from "@/video/components/actions/MobileCenterActio
import { PageTitleAction } from "@/video/components/actions/PageTitleAction"; import { PageTitleAction } from "@/video/components/actions/PageTitleAction";
import { PauseAction } from "@/video/components/actions/PauseAction"; import { PauseAction } from "@/video/components/actions/PauseAction";
import { ProgressAction } from "@/video/components/actions/ProgressAction"; import { ProgressAction } from "@/video/components/actions/ProgressAction";
import { QualityDisplayAction } from "@/video/components/actions/QualityDisplayAction";
import { SeriesSelectionAction } from "@/video/components/actions/SeriesSelectionAction"; import { SeriesSelectionAction } from "@/video/components/actions/SeriesSelectionAction";
import { SourceSelectionAction } from "@/video/components/actions/SourceSelectionAction";
import { CaptionsSelectionAction } from "@/video/components/actions/CaptionsSelectionAction";
import { ShowTitleAction } from "@/video/components/actions/ShowTitleAction"; import { ShowTitleAction } from "@/video/components/actions/ShowTitleAction";
import { KeyboardShortcutsAction } from "@/video/components/actions/KeyboardShortcutsAction"; import { KeyboardShortcutsAction } from "@/video/components/actions/KeyboardShortcutsAction";
import { SkipTimeAction } from "@/video/components/actions/SkipTimeAction"; import { SkipTimeAction } from "@/video/components/actions/SkipTimeAction";

View File

@ -1,6 +1,6 @@
import { Icons } from "@/components/Icon"; import { Icons } from "@/components/Icon";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { PopoutListAction } from "../popouts/PopoutUtils"; import { PopoutListAction } from "../../popouts/PopoutUtils";
interface Props { interface Props {
onClick: () => any; onClick: () => any;

View File

@ -1,6 +1,6 @@
import { Icon, Icons } from "@/components/Icon"; import { Icon, Icons } from "@/components/Icon";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { PopoutListAction } from "../popouts/PopoutUtils"; import { PopoutListAction } from "../../popouts/PopoutUtils";
import { QualityDisplayAction } from "./QualityDisplayAction"; import { QualityDisplayAction } from "./QualityDisplayAction";
interface Props { interface Props {

View File

@ -1,6 +1,9 @@
import { getCaptionUrl } from "@/backend/helpers/captions"; import { getCaptionUrl } from "@/backend/helpers/captions";
import { MWCaption } from "@/backend/helpers/streams"; import { MWCaption } from "@/backend/helpers/streams";
import { Icon, Icons } from "@/components/Icon"; import { Icon, Icons } from "@/components/Icon";
import { FloatingCardView } from "@/components/popout/FloatingCard";
import { FloatingView } from "@/components/popout/FloatingView";
import { useFloatingRouter } from "@/hooks/useFloatingRouter";
import { useLoading } from "@/hooks/useLoading"; import { useLoading } from "@/hooks/useLoading";
import { useVideoPlayerDescriptor } from "@/video/state/hooks"; import { useVideoPlayerDescriptor } from "@/video/state/hooks";
import { useControls } from "@/video/state/logic/controls"; import { useControls } from "@/video/state/logic/controls";
@ -14,7 +17,10 @@ function makeCaptionId(caption: MWCaption, isLinked: boolean): string {
return isLinked ? `linked-${caption.langIso}` : `external-${caption.langIso}`; return isLinked ? `linked-${caption.langIso}` : `external-${caption.langIso}`;
} }
export function CaptionSelectionPopout() { export function CaptionSelectionPopout(props: {
router: ReturnType<typeof useFloatingRouter>;
prefix: string;
}) {
const { t } = useTranslation(); const { t } = useTranslation();
const descriptor = useVideoPlayerDescriptor(); const descriptor = useVideoPlayerDescriptor();
@ -39,11 +45,17 @@ export function CaptionSelectionPopout() {
const currentCaption = source.source?.caption?.id; const currentCaption = source.source?.caption?.id;
return ( return (
<> <FloatingView
<PopoutSection className="bg-ash-100 font-bold text-white"> {...props.router.pageProps(props.prefix)}
<div>{t("videoPlayer.popouts.captions")}</div> width={320}
</PopoutSection> height={500}
<div className="relative overflow-y-auto"> >
<FloatingCardView.Header
title={t("videoPlayer.popouts.sources")}
description="What provider do you want to use?"
goBack={() => props.router.navigate("/")}
/>
<FloatingCardView.Content noSection>
<PopoutSection> <PopoutSection>
<PopoutListEntry <PopoutListEntry
active={!currentCaption} active={!currentCaption}
@ -56,7 +68,7 @@ export function CaptionSelectionPopout() {
</PopoutListEntry> </PopoutListEntry>
</PopoutSection> </PopoutSection>
<p className="sticky top-0 z-10 flex items-center space-x-1 bg-ash-200 px-5 py-3 text-sm font-bold uppercase"> <p className="sticky top-0 z-10 flex items-center space-x-1 bg-ash-300 px-5 py-3 text-xs font-bold uppercase">
<Icon className="text-base" icon={Icons.LINK} /> <Icon className="text-base" icon={Icons.LINK} />
<span>{t("videoPlayer.popouts.linkedCaptions")}</span> <span>{t("videoPlayer.popouts.linkedCaptions")}</span>
</p> </p>
@ -79,7 +91,7 @@ export function CaptionSelectionPopout() {
))} ))}
</div> </div>
</PopoutSection> </PopoutSection>
</div> </FloatingCardView.Content>
</> </FloatingView>
); );
} }

View File

@ -13,19 +13,21 @@ import { useControls } from "@/video/state/logic/controls";
import { useWatchedContext } from "@/state/watched"; import { useWatchedContext } from "@/state/watched";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { FloatingView } from "@/components/popout/FloatingView"; import { FloatingView } from "@/components/popout/FloatingView";
import { PopoutListEntry, PopoutSection } from "./PopoutUtils"; import { useFloatingRouter } from "@/hooks/useFloatingRouter";
import { FloatingCardView } from "@/components/popout/FloatingCard";
import { PopoutListEntry } from "./PopoutUtils";
export function EpisodeSelectionPopout() { export function EpisodeSelectionPopout() {
const params = useParams<{ const params = useParams<{
media: string; media: string;
}>(); }>();
const { t } = useTranslation(); const { t } = useTranslation();
const { pageProps, navigate } = useFloatingRouter("/season/episodes");
const descriptor = useVideoPlayerDescriptor(); const descriptor = useVideoPlayerDescriptor();
const meta = useMeta(descriptor); const meta = useMeta(descriptor);
const controls = useControls(descriptor); const controls = useControls(descriptor);
const [isPickingSeason, setIsPickingSeason] = useState<boolean>(false);
const [currentVisibleSeason, setCurrentVisibleSeason] = useState<{ const [currentVisibleSeason, setCurrentVisibleSeason] = useState<{
seasonId: string; seasonId: string;
season?: MWSeasonWithEpisodeMeta; season?: MWSeasonWithEpisodeMeta;
@ -41,7 +43,6 @@ export function EpisodeSelectionPopout() {
seasonId: sId, seasonId: sId,
season: undefined, season: undefined,
}); });
setIsPickingSeason(false);
reqSeasonMeta(decodeJWId(params.media)?.id as string, sId).then((v) => { reqSeasonMeta(decodeJWId(params.media)?.id as string, sId).then((v) => {
if (v?.meta.type !== MWMediaType.SERIES) return; if (v?.meta.type !== MWMediaType.SERIES) return;
setCurrentVisibleSeason({ setCurrentVisibleSeason({
@ -80,132 +81,216 @@ export function EpisodeSelectionPopout() {
)?.episodes; )?.episodes;
}, [meta, currentSeasonId, currentVisibleSeason]); }, [meta, currentSeasonId, currentVisibleSeason]);
const toggleIsPickingSeason = () => {
setIsPickingSeason(!isPickingSeason);
};
const setSeason = (id: string) => { const setSeason = (id: string) => {
requestSeason(id); requestSeason(id);
setCurrentVisibleSeason({ seasonId: id }); setCurrentVisibleSeason({ seasonId: id });
navigate("/season");
}; };
const { watched } = useWatchedContext(); const { watched } = useWatchedContext();
const titlePositionClass = useMemo(() => { const closePopout = () => {
const offset = isPickingSeason ? "left-0" : "left-10"; controls.closePopout();
return [ };
"absolute w-full transition-[left,opacity] duration-200",
offset,
].join(" ");
}, [isPickingSeason]);
return ( return (
<FloatingView show height={500} width={320}> <>
<div className="grid h-full grid-rows-[auto,minmax(0,1fr)]"> <FloatingView {...pageProps("episodes")} height={600} width={375}>
<PopoutSection className="bg-ash-100 font-bold text-white"> <FloatingCardView.Header
<div className="relative flex items-center"> title="Seasons"
description="Choose which season you want to watch"
goBack={() => navigate("/season")}
backText={`To ${currentSeasonInfo?.title.toLowerCase()}`}
/>
<FloatingCardView.Content>
{currentSeasonInfo
? meta?.seasons?.map?.((season) => (
<PopoutListEntry
key={season.id}
active={meta?.episode?.seasonId === season.id}
onClick={() => setSeason(season.id)}
>
{season.title}
</PopoutListEntry>
))
: "No season"}
</FloatingCardView.Content>
</FloatingView>
<FloatingView {...pageProps("season")} height={600} width={375}>
<FloatingCardView.Header
title={currentSeasonInfo?.title ?? "Unknown season"}
description="Pick an episode"
goBack={closePopout}
close
action={
<button <button
className={[
"-m-1.5 rounded-lg p-1.5 transition-opacity duration-100 hover:bg-ash-200",
isPickingSeason ? "pointer-events-none opacity-0" : "opacity-1",
].join(" ")}
onClick={toggleIsPickingSeason}
type="button" type="button"
onClick={() => navigate("/season/episodes")}
className="flex cursor-pointer items-center space-x-2 transition-colors duration-200 hover:text-white"
> >
<Icon icon={Icons.CHEVRON_LEFT} /> <span>Other seasons</span>
<Icon icon={Icons.CHEVRON_RIGHT} />
</button> </button>
<span }
className={[ />
titlePositionClass, <FloatingCardView.Content>
!isPickingSeason ? "opacity-1" : "opacity-0", {loading ? (
].join(" ")} <div className="flex h-full w-full items-center justify-center">
> <Loading />
{currentSeasonInfo?.title || ""} </div>
</span> ) : error ? (
<span <div className="flex h-full w-full items-center justify-center">
className={[ <div className="flex flex-col flex-wrap items-center text-slate-400">
titlePositionClass, <IconPatch
isPickingSeason ? "opacity-1" : "opacity-0", icon={Icons.EYE_SLASH}
].join(" ")} className="text-xl text-bink-600"
> />
{t("videoPlayer.popouts.seasons")} <p className="mt-6 w-full text-center">
</span> {t("videoPLayer.popouts.errors.loadingWentWrong", {
</div> seasonTitle: currentSeasonInfo?.title?.toLowerCase(),
</PopoutSection> })}
<div className="relative grid h-full grid-rows-[minmax(1px,1fr)]"> </p>
<PopoutSection
className={[
"absolute inset-0 z-30 overflow-y-auto border-ash-400 bg-ash-100 transition-[max-height,padding] duration-200",
isPickingSeason
? "max-h-full border-t"
: "max-h-0 overflow-hidden py-0",
].join(" ")}
>
{currentSeasonInfo
? meta?.seasons?.map?.((season) => (
<PopoutListEntry
key={season.id}
active={meta?.episode?.seasonId === season.id}
onClick={() => setSeason(season.id)}
isOnDarkBackground
>
{season.title}
</PopoutListEntry>
))
: "No season"}
</PopoutSection>
<PopoutSection className="relative h-full overflow-y-auto">
{loading ? (
<div className="flex h-full w-full items-center justify-center">
<Loading />
</div> </div>
) : error ? ( </div>
<div className="flex h-full w-full items-center justify-center"> ) : (
<div className="flex flex-col flex-wrap items-center text-slate-400"> <div>
<IconPatch {currentSeasonEpisodes && currentSeasonInfo
icon={Icons.EYE_SLASH} ? currentSeasonEpisodes.map((e) => (
className="text-xl text-bink-600" <PopoutListEntry
/> key={e.id}
<p className="mt-6 w-full text-center"> active={e.id === meta?.episode?.episodeId}
{t("videoPLayer.popouts.errors.loadingWentWrong", { onClick={() => {
seasonTitle: currentSeasonInfo?.title?.toLowerCase(), if (e.id === meta?.episode?.episodeId)
})} controls.closePopout();
</p> else setCurrent(currentSeasonInfo.id, e.id);
</div> }}
</div> percentageCompleted={
) : ( watched.items.find(
<div> (item) =>
{currentSeasonEpisodes && currentSeasonInfo item.item?.series?.seasonId ===
? currentSeasonEpisodes.map((e) => ( currentSeasonInfo.id &&
<PopoutListEntry item.item?.series?.episodeId === e.id
key={e.id} )?.percentage
active={e.id === meta?.episode?.episodeId} }
onClick={() => { >
if (e.id === meta?.episode?.episodeId) {t("videoPlayer.popouts.episode", {
controls.closePopout(); index: e.number,
else setCurrent(currentSeasonInfo.id, e.id); title: e.title,
}} })}
percentageCompleted={ </PopoutListEntry>
watched.items.find( ))
(item) => : "No episodes"}
item.item?.series?.seasonId === </div>
currentSeasonInfo.id && )}
item.item?.series?.episodeId === e.id </FloatingCardView.Content>
)?.percentage </FloatingView>
} </>
> // <FloatingView show height={500} width={320}>
{t("videoPlayer.popouts.episode", { // <div className="grid h-full grid-rows-[auto,minmax(0,1fr)]">
index: e.number, // <PopoutSection className="bg-ash-100 font-bold text-white">
title: e.title, // <div className="relative flex items-center">
})} // <button
</PopoutListEntry> // className={[
)) // "-m-1.5 rounded-lg p-1.5 transition-opacity duration-100 hover:bg-ash-200",
: "No episodes"} // isPickingSeason ? "pointer-events-none opacity-0" : "opacity-1",
</div> // ].join(" ")}
)} // onClick={toggleIsPickingSeason}
</PopoutSection> // type="button"
</div> // >
</div> // <Icon icon={Icons.CHEVRON_LEFT} />
</FloatingView> // </button>
// <span
// className={[
// titlePositionClass,
// !isPickingSeason ? "opacity-1" : "opacity-0",
// ].join(" ")}
// >
// {currentSeasonInfo?.title || ""}
// </span>
// <span
// className={[
// titlePositionClass,
// isPickingSeason ? "opacity-1" : "opacity-0",
// ].join(" ")}
// >
// {t("videoPlayer.popouts.seasons")}
// </span>
// </div>
// </PopoutSection>
// <div className="relative grid h-full grid-rows-[minmax(1px,1fr)]">
// <PopoutSection
// className={[
// "absolute inset-0 z-30 overflow-y-auto border-ash-400 bg-ash-100 transition-[max-height,padding] duration-200",
// isPickingSeason
// ? "max-h-full border-t"
// : "max-h-0 overflow-hidden py-0",
// ].join(" ")}
// >
// {currentSeasonInfo
// ? meta?.seasons?.map?.((season) => (
// <PopoutListEntry
// key={season.id}
// active={meta?.episode?.seasonId === season.id}
// onClick={() => setSeason(season.id)}
// isOnDarkBackground
// >
// {season.title}
// </PopoutListEntry>
// ))
// : "No season"}
// </PopoutSection>
// <PopoutSection className="relative h-full 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">
// {t("videoPLayer.popouts.errors.loadingWentWrong", {
// seasonTitle: currentSeasonInfo?.title?.toLowerCase(),
// })}
// </p>
// </div>
// </div>
// ) : (
// <div>
// {currentSeasonEpisodes && currentSeasonInfo
// ? currentSeasonEpisodes.map((e) => (
// <PopoutListEntry
// key={e.id}
// active={e.id === meta?.episode?.episodeId}
// onClick={() => {
// if (e.id === meta?.episode?.episodeId)
// controls.closePopout();
// else setCurrent(currentSeasonInfo.id, e.id);
// }}
// percentageCompleted={
// watched.items.find(
// (item) =>
// item.item?.series?.seasonId ===
// currentSeasonInfo.id &&
// item.item?.series?.episodeId === e.id
// )?.percentage
// }
// >
// {t("videoPlayer.popouts.episode", {
// index: e.number,
// title: e.title,
// })}
// </PopoutListEntry>
// ))
// : "No episodes"}
// </div>
// )}
// </PopoutSection>
// </div>
// </div>
// </FloatingView>
); );
} }

View File

@ -1,7 +1,5 @@
import { useSyncPopouts } from "@/video/components/hooks/useSyncPopouts"; import { useSyncPopouts } from "@/video/components/hooks/useSyncPopouts";
import { EpisodeSelectionPopout } from "@/video/components/popouts/EpisodeSelectionPopout"; import { EpisodeSelectionPopout } from "@/video/components/popouts/EpisodeSelectionPopout";
import { SourceSelectionPopout } from "@/video/components/popouts/SourceSelectionPopout";
import { CaptionSelectionPopout } from "@/video/components/popouts/CaptionSelectionPopout";
import { SettingsPopout } from "@/video/components/popouts/SettingsPopout"; import { SettingsPopout } from "@/video/components/popouts/SettingsPopout";
import { useVideoPlayerDescriptor } from "@/video/state/hooks"; import { useVideoPlayerDescriptor } from "@/video/state/hooks";
import { useControls } from "@/video/state/logic/controls"; import { useControls } from "@/video/state/logic/controls";
@ -14,8 +12,6 @@ import "./Popouts.css";
function ShowPopout(props: { popoutId: string | null; onClose: () => void }) { function ShowPopout(props: { popoutId: string | null; onClose: () => void }) {
const popoutMap = { const popoutMap = {
source: <SourceSelectionPopout />,
captions: <CaptionSelectionPopout />,
settings: <SettingsPopout />, settings: <SettingsPopout />,
episodes: <EpisodeSelectionPopout />, episodes: <EpisodeSelectionPopout />,
}; };

View File

@ -1,49 +1,29 @@
import { FloatingCardView } from "@/components/popout/FloatingCard";
import { FloatingDragHandle } from "@/components/popout/FloatingDragHandle";
import { FloatingView } from "@/components/popout/FloatingView"; import { FloatingView } from "@/components/popout/FloatingView";
import { useFloatingRouter } from "@/hooks/useFloatingRouter"; import { useFloatingRouter } from "@/hooks/useFloatingRouter";
import { DownloadAction } from "@/video/components/actions/list-entries/DownloadAction"; import { DownloadAction } from "@/video/components/actions/list-entries/DownloadAction";
import { CaptionsSelectionAction } from "../actions/CaptionsSelectionAction"; import { CaptionsSelectionAction } from "@/video/components/actions/list-entries/CaptionsSelectionAction";
import { SourceSelectionAction } from "../actions/SourceSelectionAction"; import { SourceSelectionAction } from "@/video/components/actions/list-entries/SourceSelectionAction";
import { CaptionSelectionPopout } from "./CaptionSelectionPopout"; import { CaptionSelectionPopout } from "./CaptionSelectionPopout";
import { PopoutSection } from "./PopoutUtils";
import { SourceSelectionPopout } from "./SourceSelectionPopout"; import { SourceSelectionPopout } from "./SourceSelectionPopout";
function TestPopout(props: { router: ReturnType<typeof useFloatingRouter> }) {
const isCollapsed = props.router.isLoaded("embed");
return (
<div>
<p onClick={() => props.router.navigate("/")}>go back</p>
<p>{isCollapsed ? "opened" : "closed"}</p>
<p onClick={() => props.router.navigate("/source/embed")}>Open</p>
</div>
);
}
export function SettingsPopout() { export function SettingsPopout() {
const floatingRouter = useFloatingRouter(); const floatingRouter = useFloatingRouter();
const { pageProps, navigate, isLoaded, isActive } = floatingRouter; const { pageProps, navigate } = floatingRouter;
return ( return (
<> <>
<FloatingView {...pageProps("/")} width={320}> <FloatingView {...pageProps("/")} width={320}>
<PopoutSection> <FloatingDragHandle />
<FloatingCardView.Content>
<DownloadAction /> <DownloadAction />
<SourceSelectionAction onClick={() => navigate("/source")} /> <SourceSelectionAction onClick={() => navigate("/source")} />
<CaptionsSelectionAction onClick={() => navigate("/captions")} /> <CaptionsSelectionAction onClick={() => navigate("/captions")} />
</PopoutSection> </FloatingCardView.Content>
</FloatingView>
<FloatingView
active={isActive("source")}
show={isLoaded("source")}
height={500}
width={320}
>
{/* <TestPopout router={floatingRouter} /> */}
<SourceSelectionPopout />
</FloatingView>
<FloatingView {...pageProps("captions")} height={500} width={320}>
<CaptionSelectionPopout />
</FloatingView> </FloatingView>
<SourceSelectionPopout router={floatingRouter} prefix="source" />
<CaptionSelectionPopout router={floatingRouter} prefix="captions" />
</> </>
); );
} }

View File

@ -1,5 +1,5 @@
import { useMemo, useRef, useState } from "react"; import { useMemo, useRef, useState } from "react";
import { Icon, Icons } from "@/components/Icon"; import { Icons } from "@/components/Icon";
import { useLoading } from "@/hooks/useLoading"; import { useLoading } from "@/hooks/useLoading";
import { Loading } from "@/components/layout/Loading"; import { Loading } from "@/components/layout/Loading";
import { IconPatch } from "@/components/buttons/IconPatch"; import { IconPatch } from "@/components/buttons/IconPatch";
@ -15,7 +15,10 @@ import { runEmbedScraper, runProvider } from "@/backend/helpers/run";
import { MWProviderScrapeResult } from "@/backend/helpers/provider"; import { MWProviderScrapeResult } from "@/backend/helpers/provider";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { MWEmbed, MWEmbedType } from "@/backend/helpers/embed"; import { MWEmbed, MWEmbedType } from "@/backend/helpers/embed";
import { PopoutListEntry, PopoutSection } from "./PopoutUtils"; import { FloatingCardView } from "@/components/popout/FloatingCard";
import { FloatingView } from "@/components/popout/FloatingView";
import { useFloatingRouter } from "@/hooks/useFloatingRouter";
import { PopoutListEntry } from "./PopoutUtils";
interface EmbedEntryProps { interface EmbedEntryProps {
name: string; name: string;
@ -49,7 +52,10 @@ export function EmbedEntry(props: EmbedEntryProps) {
); );
} }
export function SourceSelectionPopout() { export function SourceSelectionPopout(props: {
router: ReturnType<typeof useFloatingRouter>;
prefix: string;
}) {
const { t } = useTranslation(); const { t } = useTranslation();
const descriptor = useVideoPlayerDescriptor(); const descriptor = useVideoPlayerDescriptor();
@ -66,7 +72,6 @@ export function SourceSelectionPopout() {
const [selectedProvider, setSelectedProvider] = useState<string | null>(null); const [selectedProvider, setSelectedProvider] = useState<string | null>(null);
const [scrapeResult, setScrapeResult] = const [scrapeResult, setScrapeResult] =
useState<MWProviderScrapeResult | null>(null); useState<MWProviderScrapeResult | null>(null);
const showingProvider = !!selectedProvider;
const selectedProviderPopulated = useMemo( const selectedProviderPopulated = useMemo(
() => providers.find((v) => v.id === selectedProvider) ?? null, () => providers.find((v) => v.id === selectedProvider) ?? null,
[providers, selectedProvider] [providers, selectedProvider]
@ -106,6 +111,7 @@ export function SourceSelectionPopout() {
if (!providerId) { if (!providerId) {
providerRef.current = null; providerRef.current = null;
setSelectedProvider(null); setSelectedProvider(null);
props.router.navigate(`/${props.prefix}/source`);
return; return;
} }
@ -135,16 +141,9 @@ export function SourceSelectionPopout() {
}); });
providerRef.current = providerId; providerRef.current = providerId;
setSelectedProvider(providerId); setSelectedProvider(providerId);
props.router.navigate(`/${props.prefix}/source/embeds`);
}; };
const titlePositionClass = useMemo(() => {
const offset = !showingProvider ? "left-0" : "left-10";
return [
"absolute w-full transition-[left,opacity] duration-200",
offset,
].join(" ");
}, [showingProvider]);
const visibleEmbeds = useMemo(() => { const visibleEmbeds = useMemo(() => {
const embeds = scrapeResult?.embeds || []; const embeds = scrapeResult?.embeds || [];
@ -174,45 +173,43 @@ export function SourceSelectionPopout() {
return ( return (
<> <>
<PopoutSection className="bg-ash-100 font-bold text-white"> {/* List providers */}
<div className="relative flex items-center"> <FloatingView
<button {...props.router.pageProps(props.prefix)}
className={[ width={320}
"-m-1.5 rounded-lg p-1.5 transition-opacity duration-100 hover:bg-ash-200", height={500}
!showingProvider ? "pointer-events-none opacity-0" : "opacity-1", >
].join(" ")} <FloatingCardView.Header
onClick={() => selectProvider()} title={t("videoPlayer.popouts.sources")}
type="button" description="What provider do you want to use?"
> goBack={() => props.router.navigate("/")}
<Icon icon={Icons.CHEVRON_LEFT} /> />
</button> <FloatingCardView.Content>
<span {providers.map((v) => (
className={[ <PopoutListEntry
titlePositionClass, key={v.id}
showingProvider ? "opacity-1" : "opacity-0", onClick={() => {
].join(" ")} selectProvider(v.id);
> }}
{selectedProviderPopulated?.displayName ?? ""} >
</span> {v.displayName}
<span </PopoutListEntry>
className={[ ))}
titlePositionClass, </FloatingCardView.Content>
!showingProvider ? "opacity-1" : "opacity-0", </FloatingView>
].join(" ")}
> {/* List embeds */}
{t("videoPlayer.popouts.sources")} <FloatingView
</span> {...props.router.pageProps(`embeds`)}
</div> width={320}
</PopoutSection> height={500}
<div className="relative grid h-full grid-rows-[minmax(1px,1fr)]"> >
<PopoutSection <FloatingCardView.Header
className={[ title={selectedProviderPopulated?.displayName ?? ""}
"absolute inset-0 z-30 overflow-y-auto border-ash-400 bg-ash-100 transition-[max-height,padding] duration-200", description="Choose which video to view"
showingProvider goBack={() => props.router.navigate(`/${props.prefix}`)}
? "max-h-full border-t" />
: "max-h-0 overflow-hidden py-0", <FloatingCardView.Content>
].join(" ")}
>
{loading ? ( {loading ? (
<div className="flex h-full w-full items-center justify-center"> <div className="flex h-full w-full items-center justify-center">
<Loading /> <Loading />
@ -268,22 +265,8 @@ export function SourceSelectionPopout() {
)} )}
</> </>
)} )}
</PopoutSection> </FloatingCardView.Content>
<PopoutSection className="relative h-full overflow-y-auto"> </FloatingView>
<div>
{providers.map((v) => (
<PopoutListEntry
key={v.id}
onClick={() => {
selectProvider(v.id);
}}
>
{v.displayName}
</PopoutListEntry>
))}
</div>
</PopoutSection>
</div>
</> </>
); );
} }