diff --git a/index.html b/index.html index 7580549c..26bfc261 100644 --- a/index.html +++ b/index.html @@ -35,7 +35,7 @@ diff --git a/src/components/layout/ProgressRing.tsx b/src/components/layout/ProgressRing.tsx new file mode 100644 index 00000000..6e3f93ac --- /dev/null +++ b/src/components/layout/ProgressRing.tsx @@ -0,0 +1,39 @@ +interface Props { + className?: string; + radius?: number; + percentage: number; + backingRingClassname?: string; +} + +export function ProgressRing(props: Props) { + const radius = props.radius ?? 40; + + return ( + + + + + ); +} diff --git a/src/setup/index.css b/src/setup/index.css index 663b4fe2..cd69edd9 100644 --- a/src/setup/index.css +++ b/src/setup/index.css @@ -36,3 +36,11 @@ body[data-no-select] { transform: rotate(360deg); } } + +.line-clamp { + overflow: hidden; + display: -webkit-box; + -webkit-line-clamp: 1; + -webkit-box-orient: vertical; + overflow: hidden; +} diff --git a/src/video/components/actions/ProgressAction.tsx b/src/video/components/actions/ProgressAction.tsx index 096985ae..376235ba 100644 --- a/src/video/components/actions/ProgressAction.tsx +++ b/src/video/components/actions/ProgressAction.tsx @@ -26,13 +26,18 @@ export function ProgressAction() { commitTime ); - // TODO make dragging update timer useEffect(() => { if (dragRef.current === dragging) return; dragRef.current = dragging; controls.setSeeking(dragging); }, [dragRef, dragging, controls]); + useEffect(() => { + if (dragging) { + controls.setDraggingTime(videoTime.duration * (dragPercentage / 100)); + } + }, [videoTime, dragging, dragPercentage, controls]); + let watchProgress = makePercentageString( makePercentage((videoTime.time / videoTime.duration) * 100) ); diff --git a/src/video/components/actions/TimeAction.tsx b/src/video/components/actions/TimeAction.tsx index cd1caadc..e9969224 100644 --- a/src/video/components/actions/TimeAction.tsx +++ b/src/video/components/actions/TimeAction.tsx @@ -1,4 +1,5 @@ import { useVideoPlayerDescriptor } from "@/video/state/hooks"; +import { useMediaPlaying } from "@/video/state/logic/mediaplaying"; import { useProgress } from "@/video/state/logic/progress"; function durationExceedsHour(secs: number): boolean { @@ -35,9 +36,13 @@ interface Props { export function TimeAction(props: Props) { const descriptor = useVideoPlayerDescriptor(); const videoTime = useProgress(descriptor); + const mediaPlaying = useMediaPlaying(descriptor); const hasHours = durationExceedsHour(videoTime.duration); - const time = formatSeconds(videoTime.time, hasHours); + const time = formatSeconds( + mediaPlaying.isSeeking ? videoTime.draggingTime : videoTime.time, + hasHours + ); const duration = formatSeconds(videoTime.duration, hasHours); return ( diff --git a/src/video/components/hooks/useSyncPopouts.ts b/src/video/components/hooks/useSyncPopouts.ts new file mode 100644 index 00000000..57466b2e --- /dev/null +++ b/src/video/components/hooks/useSyncPopouts.ts @@ -0,0 +1,70 @@ +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"; +import { useHistory, useLocation } from "react-router-dom"; + +function syncRouteToPopout( + location: ReturnType, + controls: ControlMethods +) { + const parsed = new URLSearchParams(location.search); + const value = parsed.get("modal"); + if (value) controls.openPopout(value); + else controls.closePopout(); +} + +// TODO make closing a popout go backwords in history +// TODO fix first event breaking (clicking on page somehow resolves it) +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); + + const controlsRef = useRef(controls); + useEffect(() => { + controlsRef.current = controls; + }, [controls]); + + // sync current popout to router + useEffect(() => { + const popoutId = videoInterface.popout; + if (lastKnownValue.current === popoutId) return; + lastKnownValue.current = popoutId; + // rest only triggers with changes + + if (popoutId) { + const params = new URLSearchParams([["modal", popoutId]]).toString(); + history.push({ + search: params, + state: "popout", + }); + } else { + history.push({ + search: "", + state: "popout", + }); + } + }, [videoInterface, history]); + + // sync router to popout state (but only if its not done by block of code above) + useEffect(() => { + if (loc.state === "popout") return; + + // sync popout state + syncRouteToPopout(loc, controlsRef.current); + }, [loc]); + + // mount hook + const routerInitialized = useRef(false); + useEffect(() => { + if (routerInitialized.current) return; + if (!intialized) return; + syncRouteToPopout(loc, controlsRef.current); + routerInitialized.current = true; + }, [loc, intialized]); +} diff --git a/src/video/components/popouts/EpisodeSelectionPopout.tsx b/src/video/components/popouts/EpisodeSelectionPopout.tsx index 63cc3021..c72c712e 100644 --- a/src/video/components/popouts/EpisodeSelectionPopout.tsx +++ b/src/video/components/popouts/EpisodeSelectionPopout.tsx @@ -10,18 +10,70 @@ import { IconPatch } from "@/components/buttons/IconPatch"; 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 + } + /> + ) : ( + "" + )} +
+
+ ); +} + export function EpisodeSelectionPopout() { const params = useParams<{ media: string; @@ -90,80 +142,113 @@ export function EpisodeSelectionPopout() { setCurrentVisibleSeason({ seasonId: id }); }; - if (isPickingSeason) - return ( - <> - - Pick a season - - -
- {currentSeasonInfo - ? meta?.seasons?.map?.((season) => ( -
setSeason(season.id)} - > - {season.title} -
- )) - : "No season"} -
-
- - ); + const { watched } = useWatchedContext(); + + const titlePositionClass = useMemo(() => { + const offset = isPickingSeason ? "left-0" : "left-10"; + return [ + "absolute w-full transition-[left,opacity] duration-200", + offset, + ].join(" "); + }, [isPickingSeason]); return ( <> - - + + {currentSeasonInfo?.title || ""} + + + Seasons + +
+ +
+ - - - {currentSeasonInfo?.title || ""} - - - {loading ? ( -
- -
- ) : error ? ( -
-
- -

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

+ {currentSeasonInfo + ? meta?.seasons?.map?.((season) => ( + setSeason(season.id)} + isOnDarkBackground + > + {season.title} + + )) + : "No season"} + + + {loading ? ( +
+
-
- ) : ( -
- {currentSeasonEpisodes && currentSeasonInfo - ? currentSeasonEpisodes.map((e) => ( -
setCurrent(currentSeasonInfo.id, e.id)} - key={e.id} - > - {e.number}. {e.title} -
- )) - : "No episodes"} -
- )} - + ) : error ? ( +
+
+ +

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

+
+
+ ) : ( +
+ {currentSeasonEpisodes && currentSeasonInfo + ? currentSeasonEpisodes.map((e) => ( + setCurrent(currentSeasonInfo.id, e.id)} + percentageCompleted={ + watched.items.find( + (item) => + item.item?.series?.seasonId === + currentSeasonInfo.id && + item.item?.series?.episodeId === e.id + )?.percentage + } + > + E{e.number} - {e.title} + + )) + : "No episodes"} +
+ )} + +
); } diff --git a/src/video/components/popouts/PopoutProviderAction.tsx b/src/video/components/popouts/PopoutProviderAction.tsx index 0296f416..df71beb4 100644 --- a/src/video/components/popouts/PopoutProviderAction.tsx +++ b/src/video/components/popouts/PopoutProviderAction.tsx @@ -1,4 +1,5 @@ import { Transition } from "@/components/Transition"; +import { useSyncPopouts } from "@/video/components/hooks/useSyncPopouts"; import { EpisodeSelectionPopout } from "@/video/components/popouts/EpisodeSelectionPopout"; import { useVideoPlayerDescriptor } from "@/video/state/hooks"; import { useControls } from "@/video/state/logic/controls"; @@ -19,13 +20,12 @@ function ShowPopout(props: { popoutId: string | null }) { return null; } -// TODO use new design for popouts // TODO improve anti offscreen math -// TODO attach router history to popout state, so you can use back button to remove popout export function PopoutProviderAction() { const descriptor = useVideoPlayerDescriptor(); const videoInterface = useInterface(descriptor); const controls = useControls(descriptor); + useSyncPopouts(descriptor); const handleClick = useCallback(() => { controls.closePopout(); @@ -40,12 +40,12 @@ export function PopoutProviderAction() { 30 )}px` : "30px"; - }, [videoInterface]); + }, [videoInterface.popoutBounds]); const distanceFromBottom = useMemo(() => { return videoInterface.popoutBounds ? `${videoInterface.popoutBounds.height + 30}px` : "30px"; - }, [videoInterface]); + }, [videoInterface.popoutBounds]); return (
{!event.errored ? ( - - - - + ) : ( )} diff --git a/tailwind.config.js b/tailwind.config.js index eb70d4d8..e22b3b3a 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -18,7 +18,13 @@ module.exports = { "denim-400": "#2B263D", "denim-500": "#38334A", "denim-600": "#504B64", - "denim-700": "#7A758F" + "denim-700": "#7A758F", + "ash-600": "#817998", + "ash-500": "#9C93B5", + "ash-400": "#3D394D", + "ash-300": "#2C293A", + "ash-200": "#2B2836", + "ash-100": "#1E1C26" }, /* fonts */