/* * Kyoo - A portable and vast media library solution. * Copyright (c) Kyoo. * * See AUTHORS.md and LICENSE file in the project root for full license information. * * Kyoo is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * any later version. * * Kyoo is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Kyoo. If not, see . */ 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, subtleProgress, max = 100, markers, setProgress, startSeek, endSeek, onHover, size = 6, ...props }: { progress: number; max?: number; subtleProgress?: number; markers?: number[]; setProgress: (progress: number) => void; startSeek?: () => void; endSeek?: () => void; onHover?: ( position: number | null, layout: { x: number; y: number; width: number; height: number }, ) => void; size?: number; } & Partial) => { const { css } = useYoshiki(); const ref = useRef(null); 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); const smallBar = !(isSeeking || isHover || isFocus); const ts = (value: number) => px(value * size); const change = (event: GestureResponderEvent) => { 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, 1)) * max); }; return ( setHover(true)} // @ts-ignore Web only 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)} onStartShouldSetResponder={() => true} onResponderGrant={() => { setSeek(true); startSeek?.call(null); }} onResponderRelease={() => { setSeek(false); endSeek?.call(null); }} onResponderStart={change} onResponderMove={change} onLayout={() => ref.current?.measure((_, __, width, height, pageX, pageY) => setLayout({ width, height, x: pageX, y: pageY }), ) } onKeyDown={(e: KeyboardEvent) => { switch (e.code) { case "ArrowLeft": setProgress(Math.max(progress - 0.05 * max, 0)); break; case "ArrowRight": setProgress(Math.min(progress + 0.05 * max, max)); break; case "ArrowDown": setProgress(Math.max(progress - 0.1 * max, 0)); break; case "ArrowUp": setProgress(Math.min(progress + 0.1 * max, max)); break; } }} {...css( { paddingVertical: ts(1), // @ts-ignore Web only cursor: "pointer", ...focusReset, }, props, )} > theme.overlay0, }, smallBar && { transform: "scaleY(0.4)" as any }, ])} > {subtleProgress !== undefined && ( theme.overlay1, position: "absolute", top: 0, bottom: 0, left: 0, }, { style: { width: percent((subtleProgress / max) * 100), }, }, )} /> )} 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(0.5), height: ts(1), })} /> ))} theme.accent, width: ts(2), height: ts(2), borderRadius: ts(1), marginLeft: ts(-1), }, smallBar && { opacity: 0 }, ], { style: { left: percent((progress / max) * 100), }, }, )} /> ); };