mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-05-31 12:14:46 -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 { GestureResponderEvent, Platform, View } from "react-native";
|
||||||
import { px, percent, Stylable, useYoshiki } from "yoshiki/native";
|
import { px, percent, Stylable, useYoshiki } from "yoshiki/native";
|
||||||
import { focusReset } from "./utils";
|
import { focusReset } from "./utils";
|
||||||
|
import { ViewProps } from "react-native-svg/lib/typescript/fabric/utils";
|
||||||
|
|
||||||
export const Slider = ({
|
export const Slider = ({
|
||||||
progress,
|
progress,
|
||||||
@ -31,6 +32,7 @@ export const Slider = ({
|
|||||||
setProgress,
|
setProgress,
|
||||||
startSeek,
|
startSeek,
|
||||||
endSeek,
|
endSeek,
|
||||||
|
onHover,
|
||||||
size = 6,
|
size = 6,
|
||||||
...props
|
...props
|
||||||
}: {
|
}: {
|
||||||
@ -41,11 +43,15 @@ export const Slider = ({
|
|||||||
setProgress: (progress: number) => void;
|
setProgress: (progress: number) => void;
|
||||||
startSeek?: () => void;
|
startSeek?: () => void;
|
||||||
endSeek?: () => void;
|
endSeek?: () => void;
|
||||||
|
onHover?: (
|
||||||
|
position: number | null,
|
||||||
|
layout: { x: number; y: number; width: number; height: number },
|
||||||
|
) => void;
|
||||||
size?: number;
|
size?: number;
|
||||||
} & Stylable) => {
|
} & Partial<ViewProps>) => {
|
||||||
const { css } = useYoshiki();
|
const { css } = useYoshiki();
|
||||||
const ref = useRef<View>(null);
|
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 [isSeeking, setSeek] = useState(false);
|
||||||
const [isHover, setHover] = useState(false);
|
const [isHover, setHover] = useState(false);
|
||||||
const [isFocus, setFocus] = useState(false);
|
const [isFocus, setFocus] = useState(false);
|
||||||
@ -68,7 +74,14 @@ export const Slider = ({
|
|||||||
// @ts-ignore Web only
|
// @ts-ignore Web only
|
||||||
onMouseEnter={() => setHover(true)}
|
onMouseEnter={() => setHover(true)}
|
||||||
// @ts-ignore Web only
|
// @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}
|
tabIndex={0}
|
||||||
onFocus={() => setFocus(true)}
|
onFocus={() => setFocus(true)}
|
||||||
onBlur={() => setFocus(false)}
|
onBlur={() => setFocus(false)}
|
||||||
@ -84,7 +97,9 @@ export const Slider = ({
|
|||||||
onResponderStart={change}
|
onResponderStart={change}
|
||||||
onResponderMove={change}
|
onResponderMove={change}
|
||||||
onLayout={() =>
|
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) => {
|
onKeyDown={(e: KeyboardEvent) => {
|
||||||
switch (e.code) {
|
switch (e.code) {
|
||||||
|
@ -25,3 +25,6 @@ export const tooltip = (tooltip: string, up?: boolean) => ({
|
|||||||
ToastAndroid.show(tooltip, ToastAndroid.SHORT);
|
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>
|
`}</style>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export { Tooltip } from "react-tooltip";
|
||||||
|
@ -29,6 +29,7 @@ import {
|
|||||||
PressableFeedback,
|
PressableFeedback,
|
||||||
Skeleton,
|
Skeleton,
|
||||||
Slider,
|
Slider,
|
||||||
|
Tooltip,
|
||||||
tooltip,
|
tooltip,
|
||||||
ts,
|
ts,
|
||||||
} from "@kyoo/primitives";
|
} from "@kyoo/primitives";
|
||||||
@ -49,9 +50,9 @@ import {
|
|||||||
playAtom,
|
playAtom,
|
||||||
progressAtom,
|
progressAtom,
|
||||||
} from "../state";
|
} from "../state";
|
||||||
import { ReactNode, useCallback, useEffect, useRef } from "react";
|
import { ReactNode, useCallback, useEffect, useRef, useState } from "react";
|
||||||
import { atom } from "jotai";
|
import { atom } from "jotai";
|
||||||
import { BottomScrubber } from "./scrubber";
|
import { BottomScrubber, ScrubberTooltip } from "./scrubber";
|
||||||
|
|
||||||
const hoverReasonAtom = atom({
|
const hoverReasonAtom = atom({
|
||||||
mouseMoved: false,
|
mouseMoved: false,
|
||||||
@ -151,8 +152,8 @@ export const Hover = ({
|
|||||||
<H2 numberOfLines={1} {...css({ paddingBottom: ts(1) })}>
|
<H2 numberOfLines={1} {...css({ paddingBottom: ts(1) })}>
|
||||||
{isLoading ? <Skeleton {...css({ width: rem(15), height: rem(2) })} /> : name}
|
{isLoading ? <Skeleton {...css({ width: rem(15), height: rem(2) })} /> : name}
|
||||||
</H2>
|
</H2>
|
||||||
<ProgressBar chapters={chapters} />
|
<ProgressBar url={url} chapters={chapters} />
|
||||||
<BottomScrubber url={url}/>
|
<BottomScrubber url={url} />
|
||||||
<View
|
<View
|
||||||
{...css({
|
{...css({
|
||||||
flexDirection: "row",
|
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 [progress, setProgress] = useAtom(progressAtom);
|
||||||
const buffered = useAtomValue(bufferedAtom);
|
const buffered = useAtomValue(bufferedAtom);
|
||||||
const duration = useAtomValue(durationAtom);
|
const duration = useAtomValue(durationAtom);
|
||||||
const setPlay = useSetAtom(playAtom);
|
const setPlay = useSetAtom(playAtom);
|
||||||
|
const [hoverProgress, setHoverProgress] = useState<number | null>(null);
|
||||||
|
const [layout, setLayout] = useState({ x: 0, y: 0, width: 0, height: 0 });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Slider
|
<>
|
||||||
progress={progress}
|
<Slider
|
||||||
startSeek={() => setPlay(false)}
|
progress={progress}
|
||||||
endSeek={() => setTimeout(() => setPlay(true), 10)}
|
startSeek={() => setPlay(false)}
|
||||||
setProgress={setProgress}
|
endSeek={() => setTimeout(() => setPlay(true), 10)}
|
||||||
subtleProgress={buffered}
|
onHover={(progress, layout) => {
|
||||||
max={duration}
|
setHoverProgress(progress);
|
||||||
markers={chapters?.map((x) => x.startTime)}
|
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/>.
|
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useFetch, QueryIdentifier, imageFn } from "@kyoo/models";
|
import { useFetch, QueryIdentifier, imageFn, Chapter } from "@kyoo/models";
|
||||||
import { FastImage, P, imageBorderRadius, tooltip, ts } from "@kyoo/primitives";
|
import { FastImage, P, Tooltip, imageBorderRadius, tooltip, ts } from "@kyoo/primitives";
|
||||||
import { Platform, View } from "react-native";
|
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 { ErrorView } from "../../fetch";
|
||||||
import { ComponentProps, useEffect, useMemo } from "react";
|
import { ComponentProps, useEffect, useMemo } from "react";
|
||||||
import { CssObject } from "yoshiki/src/web/generator";
|
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 }) => {
|
export const BottomScrubber = ({ url }: { url: string }) => {
|
||||||
const { css } = useYoshiki();
|
const { css } = useYoshiki();
|
||||||
const { info, error, stats } = useScrubber(url);
|
const { info, error, stats } = useScrubber(url);
|
||||||
@ -124,8 +161,8 @@ export const BottomScrubber = ({ url }: { url: string }) => {
|
|||||||
height={thumb.height}
|
height={thumb.height}
|
||||||
x={thumb.x}
|
x={thumb.x}
|
||||||
y={thumb.y}
|
y={thumb.y}
|
||||||
columns={stats?.columns}
|
columns={stats!.columns}
|
||||||
rows={stats?.rows}
|
rows={stats!.rows}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</View>
|
</View>
|
||||||
@ -153,7 +190,7 @@ export const BottomScrubber = ({ url }: { url: string }) => {
|
|||||||
<P
|
<P
|
||||||
{...css({
|
{...css({
|
||||||
textAlign: "center",
|
textAlign: "center",
|
||||||
color: (theme) => theme.colors.white,
|
color: (theme: Theme) => theme.colors.white,
|
||||||
bg: (theme) => theme.darkOverlay,
|
bg: (theme) => theme.darkOverlay,
|
||||||
padding: ts(0.5),
|
padding: ts(0.5),
|
||||||
borderRadius: imageBorderRadius,
|
borderRadius: imageBorderRadius,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user