diff --git a/front/apps/mobile/package.json b/front/apps/mobile/package.json index 6de8d41f..e3daf666 100644 --- a/front/apps/mobile/package.json +++ b/front/apps/mobile/package.json @@ -35,7 +35,7 @@ "react-native-safe-area-context": "4.4.1", "react-native-screens": "~3.18.0", "react-native-svg": "13.4.0", - "yoshiki": "0.3.1" + "yoshiki": "0.3.2" }, "devDependencies": { "@babel/core": "^7.19.3", diff --git a/front/apps/web/package.json b/front/apps/web/package.json index cab06450..7e801e89 100644 --- a/front/apps/web/package.json +++ b/front/apps/web/package.json @@ -42,7 +42,7 @@ "react-native-web": "^0.18.10", "solito": "^2.0.5", "superjson": "^1.11.0", - "yoshiki": "0.3.1", + "yoshiki": "0.3.2", "zod": "^3.19.1" }, "devDependencies": { diff --git a/front/packages/primitives/src/icons.tsx b/front/packages/primitives/src/icons.tsx index 094daa84..98a1f7f5 100644 --- a/front/packages/primitives/src/icons.tsx +++ b/front/packages/primitives/src/icons.tsx @@ -19,10 +19,10 @@ */ import { ComponentProps, ComponentType } from "react"; -import { Platform, PressableProps, ViewStyle } from "react-native"; +import { Pressable, Platform, PressableProps, ViewStyle } from "react-native"; import { SvgProps } from "react-native-svg"; import { YoshikiStyle } from "yoshiki/dist/type"; -import { Pressable, px, useYoshiki } from "yoshiki/native"; +import { px, useYoshiki } from "yoshiki/native"; import { ts } from "./utils"; type IconProps = { diff --git a/front/packages/primitives/src/slider.tsx b/front/packages/primitives/src/slider.tsx index fc7d2c49..2b67cc3b 100644 --- a/front/packages/primitives/src/slider.tsx +++ b/front/packages/primitives/src/slider.tsx @@ -18,60 +18,92 @@ * along with Kyoo. If not, see . */ -import { useState } from "react"; -import { Platform, Pressable, View } from "react-native"; +import { useRef, useState } from "react"; +import { Platform, View } from "react-native"; import { percent, Stylable, useYoshiki } from "yoshiki/native"; import { ts } from "./utils"; -const calc = - Platform.OS === "web" - ? (first: number, operator: "+" | "-" | "*" | "/", second: number): number => - `calc(${first} ${operator} ${second})` as unknown as number - : (first: number, operator: "+" | "-" | "*" | "/", second: number): number => { - switch (operator) { - case "+": - return first + second; - case "-": - return first - second; - case "*": - return first * second; - case "/": - return first / second; - } - }; - export const Slider = ({ progress, subtleProgress, max = 100, markers, + setProgress, + startSeek, + endSeek, ...props -}: { progress: number; max?: number; subtleProgress?: number; markers?: number[] } & Stylable) => { +}: { + progress: number; + max?: number; + subtleProgress?: number; + markers?: number[]; + setProgress: (progress: number) => void; + startSeek?: () => void; + endSeek?: () => void; +} & Stylable) => { const { css } = useYoshiki(); + const ref = useRef(null); + const [layout, setLayout] = useState({ x: 0, width: 0 }); const [isSeeking, setSeek] = useState(false); + const [isHover, setHover] = useState(false); + const [isFocus, setFocus] = useState(false); + const smallBar = !(isSeeking || isHover || isFocus); + // TODO keyboard handling (left, right, up, down) return ( - { - // // prevent drag and drop of the UI. - // event.preventDefault(); + setHover(true)} + // @ts-ignore Web only + onMouseLeave={() => setHover(false)} + // TODO: This does not work + tabindex={0} + onFocus={() => setFocus(true)} + onBlur={() => setFocus(false)} + onStartShouldSetResponder={() => true} + onResponderGrant={() => { setSeek(true); + startSeek?.call(null); }} + onResponderRelease={() => { + setSeek(false); + endSeek?.call(null); + }} + onResponderMove={(event) => { + event.preventDefault(); + const locationX = Platform.select({ + android: event.nativeEvent.pageX - layout.x, + default: event.nativeEvent.locationX, + }); + setProgress(Math.max(0, Math.min(locationX / layout.width, 100)) * max); + }} + onLayout={() => + ref.current?.measure((_, __, width, ___, pageX) => + setLayout({ width: width, x: pageX }), + ) + } {...css( { paddingVertical: ts(1), + focus: { + shadowRadius: 0, + }, }, props, )} > theme.overlay0, - })} + {...css([ + { + width: percent(100), + height: ts(1), + bg: (theme) => theme.overlay0, + }, + smallBar && { transform: [{ scaleY: 0.4 }] }, + ])} > - {subtleProgress && ( + {subtleProgress !== undefined && ( theme.overlay1, @@ -84,14 +116,21 @@ export const Slider = ({ /> )} theme.accent, - position: "absolute", - top: 0, - bottom: 0, - left: 0, - width: percent((progress / max) * 100), - })} + {...css( + { + bg: (theme) => theme.accent, + position: "absolute", + top: 0, + bottom: 0, + left: 0, + }, + { + // In an inline style because yoshiki's insertion can not catch up with the constant redraw + style: { + width: percent((progress / max) * 100), + }, + }, + )} /> {markers?.map((x) => ( theme.accent, - width: ts(1), + width: ts(0.5), height: ts(1), - borderRadius: ts(0.5), })} /> ))} theme.accent, - width: ts(2), - height: ts(2), - borderRadius: ts(1), - })} + {...css( + [ + { + position: "absolute", + top: 0, + bottom: 0, + marginY: ts(.5), + bg: (theme) => theme.accent, + width: ts(2), + height: ts(2), + borderRadius: ts(1), + }, + smallBar && { opacity: 0 }, + ], + { + style: { + left: percent((progress / max) * 100), + }, + }, + )} /> - + ); }; diff --git a/front/packages/ui/src/player/components/left-buttons.tsx b/front/packages/ui/src/player/components/left-buttons.tsx index 0c898566..005e495d 100644 --- a/front/packages/ui/src/player/components/left-buttons.tsx +++ b/front/packages/ui/src/player/components/left-buttons.tsx @@ -142,6 +142,6 @@ const ProgressText = () => { const toTimerString = (timer?: number, duration?: number) => { if (timer === undefined) return "??:??"; if (!duration) duration = timer; - if (duration >= 3600) return new Date(timer).toISOString().substring(11, 19); + if (duration >= 3600_000) return new Date(timer).toISOString().substring(11, 19); return new Date(timer).toISOString().substring(14, 19); }; diff --git a/front/packages/ui/src/player/components/progress-bar.tsx b/front/packages/ui/src/player/components/progress-bar.tsx index 6cd6c38a..3fbdf05b 100644 --- a/front/packages/ui/src/player/components/progress-bar.tsx +++ b/front/packages/ui/src/player/components/progress-bar.tsx @@ -20,20 +20,23 @@ import { Chapter } from "@kyoo/models"; import { ts, Slider } from "@kyoo/primitives"; -import { useAtom, useAtomValue } from "jotai"; +import { useAtom, useAtomValue, useSetAtom } from "jotai"; import { useEffect, useRef, useState } from "react"; import { NativeTouchEvent, Pressable, View } from "react-native"; import { useYoshiki, px, percent } from "yoshiki/native"; -import { bufferedAtom, durationAtom, progressAtom } from "../state"; +import { bufferedAtom, durationAtom, playAtom, progressAtom } from "../state"; export const ProgressBar = ({ chapters }: { chapters?: Chapter[] }) => { const [progress, setProgress] = useAtom(progressAtom); const buffered = useAtomValue(bufferedAtom); const duration = useAtomValue(durationAtom); + const setPlay = useSetAtom(playAtom); return ( setPlay(false)} + endSeek={() => setPlay(true)} setProgress={setProgress} subtleProgress={buffered} max={duration} diff --git a/front/yarn.lock b/front/yarn.lock index c9161b13..469a631b 100644 --- a/front/yarn.lock +++ b/front/yarn.lock @@ -9992,7 +9992,7 @@ __metadata: react-native-svg: 13.4.0 react-native-svg-transformer: ^1.0.0 typescript: ^4.6.3 - yoshiki: 0.3.1 + yoshiki: 0.3.2 languageName: unknown linkType: soft @@ -13677,7 +13677,7 @@ __metadata: superjson: ^1.11.0 typescript: ^4.9.3 webpack: ^5.75.0 - yoshiki: 0.3.1 + yoshiki: 0.3.2 zod: ^3.19.1 languageName: unknown linkType: soft @@ -14002,9 +14002,9 @@ __metadata: languageName: node linkType: hard -"yoshiki@npm:0.3.1": - version: 0.3.1 - resolution: "yoshiki@npm:0.3.1" +"yoshiki@npm:0.3.2": + version: 0.3.2 + resolution: "yoshiki@npm:0.3.2" dependencies: "@types/node": 18.x.x "@types/react": 18.x.x @@ -14019,7 +14019,7 @@ __metadata: optional: true react-native-web: optional: true - checksum: 9448b628b61bbcc4485af7aed667a1c0f8490a2066fa35953b4a02126f1d31d94f90e27a592797f8ecedc0ce2220976a7651ba989f4ff3c68513496b2f9fdd0b + checksum: a723473e5593e9871d4903cfc9186240c6745cd97ca45ba9f1f0233e7717afefb908c08a0a6e2dc37ac2bbb0c9df269364767db6c885a8f2e19748d295c42a57 languageName: node linkType: hard