diff --git a/src/components/Icon.tsx b/src/components/Icon.tsx index 5fa79bd6..9987c549 100644 --- a/src/components/Icon.tsx +++ b/src/components/Icon.tsx @@ -31,6 +31,7 @@ export enum Icons { SKIP_FORWARD = "skip_forward", SKIP_BACKWARD = "skip_backward", FILE = "file", + CAPTIONS = "captions", } export interface IconProps { @@ -65,10 +66,11 @@ const iconList: Record = { edit: ``, bookmark_outline: ``, airplay: ``, - episodes: ``, - skip_forward: ``, - skip_backward: ``, - file: ``, + episodes: ``, + skip_forward: ``, + skip_backward: ``, + file: ``, + captions: ``, }; export const Icon = memo((props: IconProps) => { diff --git a/src/video/components/VideoPlayer.tsx b/src/video/components/VideoPlayer.tsx index 5a975308..7efca5fa 100644 --- a/src/video/components/VideoPlayer.tsx +++ b/src/video/components/VideoPlayer.tsx @@ -12,6 +12,7 @@ import { PauseAction } from "@/video/components/actions/PauseAction"; import { ProgressAction } from "@/video/components/actions/ProgressAction"; import { QualityDisplayAction } from "@/video/components/actions/QualityDisplayAction"; import { SeriesSelectionAction } from "@/video/components/actions/SeriesSelectionAction"; +import { CaptionsSelectionAction } from "@/video/components/actions/CaptionsSelectionAction"; import { ShowTitleAction } from "@/video/components/actions/ShowTitleAction"; import { KeyboardShortcutsAction } from "@/video/components/actions/KeyboardShortcutsAction"; import { SkipTimeAction } from "@/video/components/actions/SkipTimeAction"; @@ -135,6 +136,7 @@ export function VideoPlayer(props: Props) {
+ {/* */}
@@ -147,8 +149,10 @@ export function VideoPlayer(props: Props) { {/* */} - +
{/* */} + + )} diff --git a/src/video/components/actions/BackdropAction.tsx b/src/video/components/actions/BackdropAction.tsx index 0ef94bb8..dcceff60 100644 --- a/src/video/components/actions/BackdropAction.tsx +++ b/src/video/components/actions/BackdropAction.tsx @@ -9,7 +9,6 @@ interface BackdropActionProps { onBackdropChange?: (showing: boolean) => void; } -// TODO tap on mobile should remove backdrop instead of pausing export function BackdropAction(props: BackdropActionProps) { const descriptor = useVideoPlayerDescriptor(); const controls = useControls(descriptor); @@ -33,16 +32,32 @@ export function BackdropAction(props: BackdropActionProps) { setMoved(false); }, [setMoved]); + const [lastTouchEnd, setLastTouchEnd] = useState(0); + const handleClick = useCallback( - (e: React.MouseEvent) => { + ( + e: React.MouseEvent | React.TouchEvent + ) => { if (!clickareaRef.current || clickareaRef.current !== e.target) return; if (videoInterface.popout !== null) return; - if (mediaPlaying.isPlaying) controls.pause(); - else controls.play(); + if ((e as React.TouchEvent).type === "touchend") { + setLastTouchEnd(Date.now()); + return; + } + + setTimeout(() => { + if (Date.now() - lastTouchEnd < 200) { + setMoved(!moved); + return; + } + + if (mediaPlaying.isPlaying) controls.pause(); + else controls.play(); + }, 20); }, - [controls, mediaPlaying, videoInterface] + [controls, mediaPlaying, videoInterface, lastTouchEnd, moved] ); const handleDoubleClick = useCallback( (e: React.MouseEvent) => { @@ -56,14 +71,14 @@ export function BackdropAction(props: BackdropActionProps) { const lastBackdropValue = useRef(null); useEffect(() => { - const currentValue = moved || mediaPlaying.isPaused; + const currentValue = + moved || mediaPlaying.isPaused || !!videoInterface.popout; if (currentValue !== lastBackdropValue.current) { lastBackdropValue.current = currentValue; - if (!currentValue) controls.closePopout(); props.onBackdropChange?.(currentValue); } - }, [controls, moved, mediaPlaying, props]); - const showUI = moved || mediaPlaying.isPaused; + }, [moved, mediaPlaying, props, videoInterface]); + const showUI = moved || mediaPlaying.isPaused || !!videoInterface.popout; return (
+
+ + controls.openPopout("captions")} + icon={Icons.CAPTIONS} + /> + +
+
+ ); +} diff --git a/src/video/components/actions/SeriesSelectionAction.tsx b/src/video/components/actions/SeriesSelectionAction.tsx index 6840584f..6c68d8ae 100644 --- a/src/video/components/actions/SeriesSelectionAction.tsx +++ b/src/video/components/actions/SeriesSelectionAction.tsx @@ -1,17 +1,9 @@ -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 { Icons } from "@/components/Icon"; +import { MWMediaType } from "@/backend/metadata/types"; import { useVideoPlayerDescriptor } from "@/video/state/hooks"; import { useMeta } from "@/video/state/logic/meta"; import { VideoPlayerIconButton } from "@/video/components/parts/VideoPlayerIconButton"; import { useControls } from "@/video/state/logic/controls"; -import { VideoPopout } from "@/video/components/parts/VideoPopout"; import { PopoutAnchor } from "@/video/components/popouts/PopoutAnchor"; import { useInterface } from "@/video/state/logic/interface"; @@ -19,163 +11,6 @@ interface Props { className?: string; } -function PopupSection(props: { - children?: React.ReactNode; - className?: string; -}) { - return ( -
- {props.children} -
- ); -} - -function PopupEpisodeSelect() { - const params = useParams<{ - media: string; - }>(); - const descriptor = useVideoPlayerDescriptor(); - const meta = useMeta(descriptor); - const controls = useControls(descriptor); - - const [isPickingSeason, setIsPickingSeason] = useState(false); - 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 ?? meta?.episode?.seasonId; - - const setCurrent = useCallback( - (seasonId: string, episodeId: string) => { - controls.setCurrentEpisode(seasonId, episodeId); - }, - [controls] - ); - - const currentSeasonInfo = useMemo(() => { - return meta?.seasons?.find((season) => season.id === currentSeasonId); - }, [meta, currentSeasonId]); - - const currentSeasonEpisodes = useMemo(() => { - if (currentVisibleSeason?.season) { - return currentVisibleSeason?.season?.episodes; - } - return meta?.seasons?.find?.( - (season) => season && season.id === currentSeasonId - )?.episodes; - }, [meta, currentSeasonId, currentVisibleSeason]); - - const toggleIsPickingSeason = () => { - setIsPickingSeason(!isPickingSeason); - }; - - const setSeason = (id: string) => { - requestSeason(id); - setCurrentVisibleSeason({ seasonId: id }); - }; - - if (isPickingSeason) - return ( - <> - - Pick a season - - -
- {currentSeasonInfo - ? meta?.seasons?.map?.((season) => ( -
setSeason(season.id)} - > - {season.title} -
- )) - : "No season"} -
-
- - ); - - return ( - <> - - - {currentSeasonInfo?.title || ""} - - - {loading ? ( -
- -
- ) : error ? ( -
-
- -

- Something went wrong loading the episodes for{" "} - {currentSeasonInfo?.title?.toLowerCase()} -

-
-
- ) : ( -
- {currentSeasonEpisodes && currentSeasonInfo - ? currentSeasonEpisodes.map((e) => ( -
setCurrent(currentSeasonInfo.id, e.id)} - key={e.id} - > - {e.number}. {e.title} -
- )) - : "No episodes"} -
- )} -
- - ); -} - export function SeriesSelectionAction(props: Props) { const descriptor = useVideoPlayerDescriptor(); const meta = useMeta(descriptor); @@ -196,12 +31,6 @@ export function SeriesSelectionAction(props: Props) { onClick={() => controls.openPopout("episodes")} /> - {/* - - */}
); diff --git a/src/video/components/controllers/SeriesController.tsx b/src/video/components/controllers/SeriesController.tsx index f38701d5..d676e15b 100644 --- a/src/video/components/controllers/SeriesController.tsx +++ b/src/video/components/controllers/SeriesController.tsx @@ -21,6 +21,7 @@ export function SeriesController(props: SeriesControllerProps) { seasonId: meta?.episode?.seasonId, }; if (lastState.current === null) { + if (!meta) return; lastState.current = currentState; return; } diff --git a/src/video/components/hooks/useSyncPopouts.ts b/src/video/components/hooks/useSyncPopouts.ts index 57466b2e..a7edee77 100644 --- a/src/video/components/hooks/useSyncPopouts.ts +++ b/src/video/components/hooks/useSyncPopouts.ts @@ -1,4 +1,3 @@ -import { useInitialized } from "@/video/components/hooks/useInitialized"; import { ControlMethods, useControls } from "@/video/state/logic/controls"; import { useInterface } from "@/video/state/logic/interface"; import { useEffect, useRef } from "react"; @@ -14,13 +13,11 @@ function syncRouteToPopout( else controls.closePopout(); } -// TODO make closing a popout go backwords in history -// TODO fix first event breaking (clicking on page somehow resolves it) +// TODO when opening with an open modal url, closing popout will close tab export function useSyncPopouts(descriptor: string) { const history = useHistory(); const videoInterface = useInterface(descriptor); const controls = useControls(descriptor); - const intialized = useInitialized(descriptor); const loc = useLocation(); const lastKnownValue = useRef(null); @@ -44,15 +41,20 @@ export function useSyncPopouts(descriptor: string) { state: "popout", }); } else { - history.push({ - search: "", - state: "popout", - }); + // dont do anything if no modal is even open + if (!new URLSearchParams(history.location.search).has("modal")) return; + if (history.length > 0) history.goBack(); + else + history.replace({ + search: "", + state: "popout", + }); } }, [videoInterface, history]); // sync router to popout state (but only if its not done by block of code above) useEffect(() => { + // if location update a push from the block above if (loc.state === "popout") return; // sync popout state @@ -63,8 +65,7 @@ export function useSyncPopouts(descriptor: string) { const routerInitialized = useRef(false); useEffect(() => { if (routerInitialized.current) return; - if (!intialized) return; syncRouteToPopout(loc, controlsRef.current); routerInitialized.current = true; - }, [loc, intialized]); + }, [loc]); } diff --git a/src/video/components/popouts/CaptionSelectionPopout.tsx b/src/video/components/popouts/CaptionSelectionPopout.tsx new file mode 100644 index 00000000..99e784c3 --- /dev/null +++ b/src/video/components/popouts/CaptionSelectionPopout.tsx @@ -0,0 +1,14 @@ +import { PopoutSection } from "./PopoutUtils"; + +export function CaptionSelectionPopout() { + return ( + <> + +
Captions
+
+ +
Hi Jeebies
+
+ + ); +} diff --git a/src/video/components/popouts/EpisodeSelectionPopout.tsx b/src/video/components/popouts/EpisodeSelectionPopout.tsx index c72c712e..e60ddd99 100644 --- a/src/video/components/popouts/EpisodeSelectionPopout.tsx +++ b/src/video/components/popouts/EpisodeSelectionPopout.tsx @@ -11,68 +11,7 @@ import { useVideoPlayerDescriptor } from "@/video/state/hooks"; import { useMeta } from "@/video/state/logic/meta"; import { useControls } from "@/video/state/logic/controls"; import { useWatchedContext } from "@/state/watched"; -import { ProgressRing } from "@/components/layout/ProgressRing"; - -function PopupSection(props: { - children?: React.ReactNode; - className?: string; -}) { - return ( -
- {props.children} -
- ); -} - -interface PopoutListEntryTypes { - active?: boolean; - children: React.ReactNode; - onClick?: () => void; - isOnDarkBackground?: boolean; - percentageCompleted?: number; -} - -function PopoutListEntry(props: PopoutListEntryTypes) { - const bg = props.isOnDarkBackground ? "bg-ash-200" : "bg-ash-400"; - const hover = props.isOnDarkBackground - ? "hover:bg-ash-200" - : "hover:bg-ash-400"; - - return ( -
- {props.active && ( -
- )} - {props.children} -
- - {props.percentageCompleted ? ( - 90 ? 100 : props.percentageCompleted - } - /> - ) : ( - "" - )} -
-
- ); -} +import { PopoutListEntry, PopoutSection } from "./PopoutUtils"; export function EpisodeSelectionPopout() { const params = useParams<{ @@ -154,7 +93,7 @@ export function EpisodeSelectionPopout() { return ( <> - +
-
+
- )) : "No season"} - - + + {loading ? (
@@ -225,13 +164,17 @@ export function EpisodeSelectionPopout() {
) : ( -
+
{currentSeasonEpisodes && currentSeasonInfo ? currentSeasonEpisodes.map((e) => ( setCurrent(currentSeasonInfo.id, e.id)} + onClick={() => { + if (e.id === meta?.episode?.episodeId) + controls.closePopout(); + else setCurrent(currentSeasonInfo.id, e.id); + }} percentageCompleted={ watched.items.find( (item) => @@ -247,7 +190,7 @@ export function EpisodeSelectionPopout() { : "No episodes"}
)} - +
); diff --git a/src/video/components/popouts/PopoutProviderAction.tsx b/src/video/components/popouts/PopoutProviderAction.tsx index 76384732..470f309e 100644 --- a/src/video/components/popouts/PopoutProviderAction.tsx +++ b/src/video/components/popouts/PopoutProviderAction.tsx @@ -1,6 +1,7 @@ import { Transition } from "@/components/Transition"; import { useSyncPopouts } from "@/video/components/hooks/useSyncPopouts"; import { EpisodeSelectionPopout } from "@/video/components/popouts/EpisodeSelectionPopout"; +import { CaptionSelectionPopout } from "@/video/components/popouts/CaptionSelectionPopout"; import { useVideoPlayerDescriptor } from "@/video/state/hooks"; import { useControls } from "@/video/state/logic/controls"; import { useInterface } from "@/video/state/logic/interface"; @@ -17,10 +18,11 @@ function ShowPopout(props: { popoutId: string | null }) { }, [props]); if (popoutId === "episodes") return ; + if (popoutId === "captions") return ; return null; } -// TODO bug: first load ref is null +// TODO bug: coords are sometimes completely broken export function PopoutProviderAction() { const ref = useRef(null); const descriptor = useVideoPlayerDescriptor(); diff --git a/src/video/components/popouts/PopoutUtils.tsx b/src/video/components/popouts/PopoutUtils.tsx new file mode 100644 index 00000000..83a88453 --- /dev/null +++ b/src/video/components/popouts/PopoutUtils.tsx @@ -0,0 +1,63 @@ +import { Icon, Icons } from "@/components/Icon"; +import { ProgressRing } from "@/components/layout/ProgressRing"; + +interface PopoutListEntryTypes { + active?: boolean; + children: React.ReactNode; + onClick?: () => void; + isOnDarkBackground?: boolean; + percentageCompleted?: number; +} + +export function PopoutSection(props: { + children?: React.ReactNode; + className?: string; +}) { + return ( +
+ {props.children} +
+ ); +} + +export function PopoutListEntry(props: PopoutListEntryTypes) { + const bg = props.isOnDarkBackground ? "bg-ash-200" : "bg-ash-400"; + const hover = props.isOnDarkBackground + ? "hover:bg-ash-200" + : "hover:bg-ash-400"; + + return ( +
+ {props.active && ( +
+ )} + {props.children} +
+ + {props.percentageCompleted ? ( + 90 ? 100 : props.percentageCompleted + } + /> + ) : ( + "" + )} +
+
+ ); +}