mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-05-24 02:02:36 -04:00
Add web scrubber on hover
This commit is contained in:
parent
ec90862262
commit
7803f8f11a
@ -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<ViewProps>) => {
|
||||
const { css } = useYoshiki();
|
||||
const ref = useRef<View>(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) {
|
||||
|
@ -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;
|
||||
|
@ -42,3 +42,5 @@ export const WebTooltip = ({ theme }: { theme: Theme }) => {
|
||||
`}</style>
|
||||
);
|
||||
};
|
||||
|
||||
export { Tooltip } from "react-tooltip";
|
||||
|
@ -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 = ({
|
||||
<H2 numberOfLines={1} {...css({ paddingBottom: ts(1) })}>
|
||||
{isLoading ? <Skeleton {...css({ width: rem(15), height: rem(2) })} /> : name}
|
||||
</H2>
|
||||
<ProgressBar chapters={chapters} />
|
||||
<BottomScrubber url={url}/>
|
||||
<ProgressBar url={url} chapters={chapters} />
|
||||
<BottomScrubber url={url} />
|
||||
<View
|
||||
{...css({
|
||||
flexDirection: "row",
|
||||
@ -309,22 +310,42 @@ export const HoverTouch = ({ children, ...props }: { children: ReactNode }) => {
|
||||
);
|
||||
};
|
||||
|
||||
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<number | null>(null);
|
||||
const [layout, setLayout] = useState({ x: 0, y: 0, width: 0, height: 0 });
|
||||
|
||||
return (
|
||||
<Slider
|
||||
progress={progress}
|
||||
startSeek={() => setPlay(false)}
|
||||
endSeek={() => setTimeout(() => setPlay(true), 10)}
|
||||
setProgress={setProgress}
|
||||
subtleProgress={buffered}
|
||||
max={duration}
|
||||
markers={chapters?.map((x) => x.startTime)}
|
||||
/>
|
||||
<>
|
||||
<Slider
|
||||
progress={progress}
|
||||
startSeek={() => 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" }}
|
||||
/>
|
||||
<Tooltip
|
||||
id={"progress-scrubber"}
|
||||
isOpen={hoverProgress !== null}
|
||||
imperativeModeOnly
|
||||
position={{ x: layout.x + (layout.width * hoverProgress!) / (duration ?? 1), y: layout.y }}
|
||||
render={() =>
|
||||
hoverProgress ? (
|
||||
<ScrubberTooltip seconds={hoverProgress} chapters={chapters} url={url} />
|
||||
) : null
|
||||
}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -18,10 +18,10 @@
|
||||
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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<string> => ({
|
||||
},
|
||||
});
|
||||
|
||||
export const ScrubberTooltip = ({
|
||||
url,
|
||||
chapters,
|
||||
seconds,
|
||||
}: {
|
||||
url: string;
|
||||
chapters?: Chapter[];
|
||||
seconds: number;
|
||||
}) => {
|
||||
const { info, error, stats } = useScrubber(url);
|
||||
const { css } = useYoshiki();
|
||||
|
||||
if (error) return <ErrorView error={error} />;
|
||||
|
||||
const current = info.findLast((x) => x.to < seconds * 1000);
|
||||
const chapter = chapters?.findLast((x) => x.endTime < seconds);
|
||||
|
||||
return (
|
||||
<View {...css({ justifyContent: "center" })}>
|
||||
{current && (
|
||||
<FastImage
|
||||
src={current.url}
|
||||
alt={""}
|
||||
width={current.width}
|
||||
height={current.height}
|
||||
x={current.x}
|
||||
y={current.y}
|
||||
columns={stats!.columns}
|
||||
rows={stats!.rows}
|
||||
/>
|
||||
)}
|
||||
<P>{toTimerString(seconds)}</P>
|
||||
{chapter && <P>{chapter.name}</P>}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
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}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
@ -153,7 +190,7 @@ export const BottomScrubber = ({ url }: { url: string }) => {
|
||||
<P
|
||||
{...css({
|
||||
textAlign: "center",
|
||||
color: (theme) => theme.colors.white,
|
||||
color: (theme: Theme) => theme.colors.white,
|
||||
bg: (theme) => theme.darkOverlay,
|
||||
padding: ts(0.5),
|
||||
borderRadius: imageBorderRadius,
|
||||
|
Loading…
x
Reference in New Issue
Block a user