Add slider touch handlers

This commit is contained in:
Zoe Roux 2022-12-21 00:42:38 +09:00
parent 2c16fdad19
commit 856eaffda6
7 changed files with 115 additions and 65 deletions

View File

@ -35,7 +35,7 @@
"react-native-safe-area-context": "4.4.1", "react-native-safe-area-context": "4.4.1",
"react-native-screens": "~3.18.0", "react-native-screens": "~3.18.0",
"react-native-svg": "13.4.0", "react-native-svg": "13.4.0",
"yoshiki": "0.3.1" "yoshiki": "0.3.2"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.19.3", "@babel/core": "^7.19.3",

View File

@ -42,7 +42,7 @@
"react-native-web": "^0.18.10", "react-native-web": "^0.18.10",
"solito": "^2.0.5", "solito": "^2.0.5",
"superjson": "^1.11.0", "superjson": "^1.11.0",
"yoshiki": "0.3.1", "yoshiki": "0.3.2",
"zod": "^3.19.1" "zod": "^3.19.1"
}, },
"devDependencies": { "devDependencies": {

View File

@ -19,10 +19,10 @@
*/ */
import { ComponentProps, ComponentType } from "react"; 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 { SvgProps } from "react-native-svg";
import { YoshikiStyle } from "yoshiki/dist/type"; import { YoshikiStyle } from "yoshiki/dist/type";
import { Pressable, px, useYoshiki } from "yoshiki/native"; import { px, useYoshiki } from "yoshiki/native";
import { ts } from "./utils"; import { ts } from "./utils";
type IconProps = { type IconProps = {

View File

@ -18,60 +18,92 @@
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>. * along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { useState } from "react"; import { useRef, useState } from "react";
import { Platform, Pressable, View } from "react-native"; import { Platform, View } from "react-native";
import { percent, Stylable, useYoshiki } from "yoshiki/native"; import { percent, Stylable, useYoshiki } from "yoshiki/native";
import { ts } from "./utils"; 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 = ({ export const Slider = ({
progress, progress,
subtleProgress, subtleProgress,
max = 100, max = 100,
markers, markers,
setProgress,
startSeek,
endSeek,
...props ...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 { css } = useYoshiki();
const ref = useRef<View>(null);
const [layout, setLayout] = useState({ x: 0, width: 0 });
const [isSeeking, setSeek] = useState(false); 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 ( return (
<Pressable <View
onTouchStart={(event) => { ref={ref}
// // prevent drag and drop of the UI. // @ts-ignore Web only
// event.preventDefault(); onMouseEnter={() => 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); 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( {...css(
{ {
paddingVertical: ts(1), paddingVertical: ts(1),
focus: {
shadowRadius: 0,
},
}, },
props, props,
)} )}
> >
<View <View
{...css({ {...css([
width: percent(100), {
height: ts(1), width: percent(100),
bg: (theme) => theme.overlay0, height: ts(1),
})} bg: (theme) => theme.overlay0,
},
smallBar && { transform: [{ scaleY: 0.4 }] },
])}
> >
{subtleProgress && ( {subtleProgress !== undefined && (
<View <View
{...css({ {...css({
bg: (theme) => theme.overlay1, bg: (theme) => theme.overlay1,
@ -84,14 +116,21 @@ export const Slider = ({
/> />
)} )}
<View <View
{...css({ {...css(
bg: (theme) => theme.accent, {
position: "absolute", bg: (theme) => theme.accent,
top: 0, position: "absolute",
bottom: 0, top: 0,
left: 0, bottom: 0,
width: percent((progress / max) * 100), 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) => ( {markers?.map((x) => (
<View <View
@ -102,26 +141,34 @@ export const Slider = ({
bottom: 0, bottom: 0,
left: percent(Math.min(100, (x / max) * 100)), left: percent(Math.min(100, (x / max) * 100)),
bg: (theme) => theme.accent, bg: (theme) => theme.accent,
width: ts(1), width: ts(0.5),
height: ts(1), height: ts(1),
borderRadius: ts(0.5),
})} })}
/> />
))} ))}
</View> </View>
<View <View
{...css({ {...css(
position: "absolute", [
top: 0, {
bottom: 0, position: "absolute",
margin: "auto", top: 0,
left: calc(percent((progress / max) * 100), "-", ts(1)), bottom: 0,
bg: (theme) => theme.accent, marginY: ts(.5),
width: ts(2), bg: (theme) => theme.accent,
height: ts(2), width: ts(2),
borderRadius: ts(1), height: ts(2),
})} borderRadius: ts(1),
},
smallBar && { opacity: 0 },
],
{
style: {
left: percent((progress / max) * 100),
},
},
)}
/> />
</Pressable> </View>
); );
}; };

View File

@ -142,6 +142,6 @@ const ProgressText = () => {
const toTimerString = (timer?: number, duration?: number) => { const toTimerString = (timer?: number, duration?: number) => {
if (timer === undefined) return "??:??"; if (timer === undefined) return "??:??";
if (!duration) duration = timer; 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); return new Date(timer).toISOString().substring(14, 19);
}; };

View File

@ -20,20 +20,23 @@
import { Chapter } from "@kyoo/models"; import { Chapter } from "@kyoo/models";
import { ts, Slider } from "@kyoo/primitives"; import { ts, Slider } from "@kyoo/primitives";
import { useAtom, useAtomValue } from "jotai"; import { useAtom, useAtomValue, useSetAtom } from "jotai";
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { NativeTouchEvent, Pressable, View } from "react-native"; import { NativeTouchEvent, Pressable, View } from "react-native";
import { useYoshiki, px, percent } from "yoshiki/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[] }) => { export const ProgressBar = ({ chapters }: { chapters?: Chapter[] }) => {
const [progress, setProgress] = useAtom(progressAtom); const [progress, setProgress] = useAtom(progressAtom);
const buffered = useAtomValue(bufferedAtom); const buffered = useAtomValue(bufferedAtom);
const duration = useAtomValue(durationAtom); const duration = useAtomValue(durationAtom);
const setPlay = useSetAtom(playAtom);
return ( return (
<Slider <Slider
progress={progress} progress={progress}
startSeek={() => setPlay(false)}
endSeek={() => setPlay(true)}
setProgress={setProgress} setProgress={setProgress}
subtleProgress={buffered} subtleProgress={buffered}
max={duration} max={duration}

View File

@ -9992,7 +9992,7 @@ __metadata:
react-native-svg: 13.4.0 react-native-svg: 13.4.0
react-native-svg-transformer: ^1.0.0 react-native-svg-transformer: ^1.0.0
typescript: ^4.6.3 typescript: ^4.6.3
yoshiki: 0.3.1 yoshiki: 0.3.2
languageName: unknown languageName: unknown
linkType: soft linkType: soft
@ -13677,7 +13677,7 @@ __metadata:
superjson: ^1.11.0 superjson: ^1.11.0
typescript: ^4.9.3 typescript: ^4.9.3
webpack: ^5.75.0 webpack: ^5.75.0
yoshiki: 0.3.1 yoshiki: 0.3.2
zod: ^3.19.1 zod: ^3.19.1
languageName: unknown languageName: unknown
linkType: soft linkType: soft
@ -14002,9 +14002,9 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"yoshiki@npm:0.3.1": "yoshiki@npm:0.3.2":
version: 0.3.1 version: 0.3.2
resolution: "yoshiki@npm:0.3.1" resolution: "yoshiki@npm:0.3.2"
dependencies: dependencies:
"@types/node": 18.x.x "@types/node": 18.x.x
"@types/react": 18.x.x "@types/react": 18.x.x
@ -14019,7 +14019,7 @@ __metadata:
optional: true optional: true
react-native-web: react-native-web:
optional: true optional: true
checksum: 9448b628b61bbcc4485af7aed667a1c0f8490a2066fa35953b4a02126f1d31d94f90e27a592797f8ecedc0ce2220976a7651ba989f4ff3c68513496b2f9fdd0b checksum: a723473e5593e9871d4903cfc9186240c6745cd97ca45ba9f1f0233e7717afefb908c08a0a6e2dc37ac2bbb0c9df269364767db6c885a8f2e19748d295c42a57
languageName: node languageName: node
linkType: hard linkType: hard