From 7803f8f11a3617d575f2adce53eecd924931dc21 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Thu, 25 Jan 2024 14:24:43 +0100 Subject: [PATCH] Add web scrubber on hover --- front/packages/primitives/src/slider.tsx | 23 +++++++-- front/packages/primitives/src/tooltip.ts | 3 ++ front/packages/primitives/src/tooltip.web.tsx | 2 + .../ui/src/player/components/hover.tsx | 49 +++++++++++++------ .../ui/src/player/components/scrubber.tsx | 49 ++++++++++++++++--- 5 files changed, 102 insertions(+), 24 deletions(-) diff --git a/front/packages/primitives/src/slider.tsx b/front/packages/primitives/src/slider.tsx index a9f264e7..15965eb2 100644 --- a/front/packages/primitives/src/slider.tsx +++ b/front/packages/primitives/src/slider.tsx @@ -22,6 +22,7 @@ import { useRef, useState } from "react"; import { GestureResponderEvent, Platform, View } from "react-native"; import { px, percent, Stylable, useYoshiki } from "yoshiki/native"; import { focusReset } from "./utils"; +import { ViewProps } from "react-native-svg/lib/typescript/fabric/utils"; export const Slider = ({ progress, @@ -31,6 +32,7 @@ export const Slider = ({ setProgress, startSeek, endSeek, + onHover, size = 6, ...props }: { @@ -41,11 +43,15 @@ export const Slider = ({ setProgress: (progress: number) => void; startSeek?: () => void; endSeek?: () => void; + onHover?: ( + position: number | null, + layout: { x: number; y: number; width: number; height: number }, + ) => void; size?: number; -} & Stylable) => { +} & Partial) => { const { css } = useYoshiki(); const ref = useRef(null); - const [layout, setLayout] = useState({ x: 0, width: 0 }); + const [layout, setLayout] = useState({ x: 0, y: 0, width: 0, height: 0 }); const [isSeeking, setSeek] = useState(false); const [isHover, setHover] = useState(false); const [isFocus, setFocus] = useState(false); @@ -68,7 +74,14 @@ export const Slider = ({ // @ts-ignore Web only onMouseEnter={() => setHover(true)} // @ts-ignore Web only - onMouseLeave={() => setHover(false)} + onMouseLeave={() => { + setHover(false); + onHover?.(null, layout); + }} + // @ts-ignore Web only + onMouseMove={(e) => + onHover?.(Math.max(0, Math.min((e.clientX - layout.x) / layout.width, 1) * max), layout) + } tabIndex={0} onFocus={() => setFocus(true)} onBlur={() => setFocus(false)} @@ -84,7 +97,9 @@ export const Slider = ({ onResponderStart={change} onResponderMove={change} onLayout={() => - ref.current?.measure((_, __, width, ___, pageX) => setLayout({ width: width, x: pageX })) + ref.current?.measure((_, __, width, height, pageX, pageY) => + setLayout({ width, height, x: pageX, y: pageY }), + ) } onKeyDown={(e: KeyboardEvent) => { switch (e.code) { diff --git a/front/packages/primitives/src/tooltip.ts b/front/packages/primitives/src/tooltip.ts index 48780562..0fe1693c 100644 --- a/front/packages/primitives/src/tooltip.ts +++ b/front/packages/primitives/src/tooltip.ts @@ -25,3 +25,6 @@ export const tooltip = (tooltip: string, up?: boolean) => ({ ToastAndroid.show(tooltip, ToastAndroid.SHORT); }, }); + +import type { Tooltip as RTooltip } from "react-tooltip"; +export declare const Tooltip: RTooltip; diff --git a/front/packages/primitives/src/tooltip.web.tsx b/front/packages/primitives/src/tooltip.web.tsx index 23074f38..f43f219f 100644 --- a/front/packages/primitives/src/tooltip.web.tsx +++ b/front/packages/primitives/src/tooltip.web.tsx @@ -42,3 +42,5 @@ export const WebTooltip = ({ theme }: { theme: Theme }) => { `} ); }; + +export { Tooltip } from "react-tooltip"; diff --git a/front/packages/ui/src/player/components/hover.tsx b/front/packages/ui/src/player/components/hover.tsx index 8d7fa827..382e8063 100644 --- a/front/packages/ui/src/player/components/hover.tsx +++ b/front/packages/ui/src/player/components/hover.tsx @@ -29,6 +29,7 @@ import { PressableFeedback, Skeleton, Slider, + Tooltip, tooltip, ts, } from "@kyoo/primitives"; @@ -49,9 +50,9 @@ import { playAtom, progressAtom, } from "../state"; -import { ReactNode, useCallback, useEffect, useRef } from "react"; +import { ReactNode, useCallback, useEffect, useRef, useState } from "react"; import { atom } from "jotai"; -import { BottomScrubber } from "./scrubber"; +import { BottomScrubber, ScrubberTooltip } from "./scrubber"; const hoverReasonAtom = atom({ mouseMoved: false, @@ -151,8 +152,8 @@ export const Hover = ({

{isLoading ? : name}

- - + + { ); }; -const ProgressBar = ({ chapters }: { chapters?: Chapter[] }) => { +const ProgressBar = ({ url, chapters }: { url: string; chapters?: Chapter[] }) => { const [progress, setProgress] = useAtom(progressAtom); const buffered = useAtomValue(bufferedAtom); const duration = useAtomValue(durationAtom); const setPlay = useSetAtom(playAtom); + const [hoverProgress, setHoverProgress] = useState(null); + const [layout, setLayout] = useState({ x: 0, y: 0, width: 0, height: 0 }); return ( - setPlay(false)} - endSeek={() => setTimeout(() => setPlay(true), 10)} - setProgress={setProgress} - subtleProgress={buffered} - max={duration} - markers={chapters?.map((x) => x.startTime)} - /> + <> + setPlay(false)} + endSeek={() => setTimeout(() => setPlay(true), 10)} + onHover={(progress, layout) => { + setHoverProgress(progress); + setLayout(layout); + }} + setProgress={setProgress} + subtleProgress={buffered} + max={duration} + markers={chapters?.map((x) => x.startTime)} + dataSet={{ tooltipId: "progress-scrubber" }} + /> + + hoverProgress ? ( + + ) : null + } + /> + ); }; diff --git a/front/packages/ui/src/player/components/scrubber.tsx b/front/packages/ui/src/player/components/scrubber.tsx index e2357636..62bf1e80 100644 --- a/front/packages/ui/src/player/components/scrubber.tsx +++ b/front/packages/ui/src/player/components/scrubber.tsx @@ -18,10 +18,10 @@ * along with Kyoo. If not, see . */ -import { useFetch, QueryIdentifier, imageFn } from "@kyoo/models"; -import { FastImage, P, imageBorderRadius, tooltip, ts } from "@kyoo/primitives"; +import { useFetch, QueryIdentifier, imageFn, Chapter } from "@kyoo/models"; +import { FastImage, P, Tooltip, imageBorderRadius, tooltip, ts } from "@kyoo/primitives"; import { Platform, View } from "react-native"; -import { percent, useYoshiki, px, vh } from "yoshiki/native"; +import { percent, useYoshiki, px, vh, Theme } from "yoshiki/native"; import { ErrorView } from "../../fetch"; import { ComponentProps, useEffect, useMemo } from "react"; import { CssObject } from "yoshiki/src/web/generator"; @@ -91,6 +91,43 @@ useScrubber.query = (url: string): QueryIdentifier => ({ }, }); +export const ScrubberTooltip = ({ + url, + chapters, + seconds, +}: { + url: string; + chapters?: Chapter[]; + seconds: number; +}) => { + const { info, error, stats } = useScrubber(url); + const { css } = useYoshiki(); + + if (error) return ; + + const current = info.findLast((x) => x.to < seconds * 1000); + const chapter = chapters?.findLast((x) => x.endTime < seconds); + + return ( + + {current && ( + + )} +

{toTimerString(seconds)}

+ {chapter &&

{chapter.name}

} +
+ ); +}; + export const BottomScrubber = ({ url }: { url: string }) => { const { css } = useYoshiki(); const { info, error, stats } = useScrubber(url); @@ -124,8 +161,8 @@ export const BottomScrubber = ({ url }: { url: string }) => { height={thumb.height} x={thumb.x} y={thumb.y} - columns={stats?.columns} - rows={stats?.rows} + columns={stats!.columns} + rows={stats!.rows} /> ))}
@@ -153,7 +190,7 @@ export const BottomScrubber = ({ url }: { url: string }) => {

theme.colors.white, + color: (theme: Theme) => theme.colors.white, bg: (theme) => theme.darkOverlay, padding: ts(0.5), borderRadius: imageBorderRadius,