mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-05-24 02:02:36 -04:00
Completly rewrite hover handling and add seek double press
This commit is contained in:
parent
fbc8e14125
commit
57b4463c01
@ -31,19 +31,43 @@ import {
|
||||
Skeleton,
|
||||
Slider,
|
||||
tooltip,
|
||||
touchOnly,
|
||||
ts,
|
||||
} from "@kyoo/primitives";
|
||||
import { Chapter, KyooImage, Subtitle, Audio } from "@kyoo/models";
|
||||
import { useAtomValue, useSetAtom, useAtom } from "jotai";
|
||||
import { ImageStyle, Platform, Pressable, View, ViewProps } from "react-native";
|
||||
import {
|
||||
ImageStyle,
|
||||
Platform,
|
||||
Pressable,
|
||||
View,
|
||||
ViewProps,
|
||||
PointerEvent as NativePointerEvent,
|
||||
} from "react-native";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { percent, rem, useYoshiki } from "yoshiki/native";
|
||||
import { useRouter } from "solito/router";
|
||||
import ArrowBack from "@material-symbols/svg-400/rounded/arrow_back-fill.svg";
|
||||
import { LeftButtons, TouchControls } from "./left-buttons";
|
||||
import { RightButtons } from "./right-buttons";
|
||||
import { bufferedAtom, durationAtom, loadAtom, playAtom, progressAtom } from "../state";
|
||||
import {
|
||||
bufferedAtom,
|
||||
durationAtom,
|
||||
fullscreenAtom,
|
||||
loadAtom,
|
||||
playAtom,
|
||||
progressAtom,
|
||||
} from "../state";
|
||||
import { ReactNode, useCallback, useEffect, useRef } from "react";
|
||||
import { atom } from "jotai";
|
||||
|
||||
const hoverReasonAtom = atom({
|
||||
mouseMoved: false,
|
||||
mouseHover: false,
|
||||
menuOpened: false,
|
||||
});
|
||||
export const hoverAtom = atom((get) =>
|
||||
[!get(playAtom), ...Object.values(get(hoverReasonAtom))].includes(true),
|
||||
);
|
||||
|
||||
export const Hover = ({
|
||||
isLoading,
|
||||
@ -57,11 +81,6 @@ export const Hover = ({
|
||||
fonts,
|
||||
previousSlug,
|
||||
nextSlug,
|
||||
onMenuOpen,
|
||||
onMenuClose,
|
||||
show,
|
||||
onPointerDown,
|
||||
...props
|
||||
}: {
|
||||
isLoading: boolean;
|
||||
name?: string | null;
|
||||
@ -74,78 +93,207 @@ export const Hover = ({
|
||||
fonts?: string[];
|
||||
previousSlug?: string | null;
|
||||
nextSlug?: string | null;
|
||||
onMenuOpen: () => void;
|
||||
onMenuClose: () => void;
|
||||
show: boolean;
|
||||
} & ViewProps) => {
|
||||
// TODO: animate show
|
||||
const opacity = !show && (Platform.OS === "web" ? { opacity: 0 } : { display: "none" as const });
|
||||
}) => {
|
||||
const show = useAtomValue(hoverAtom);
|
||||
const setHover = useSetAtom(hoverReasonAtom);
|
||||
|
||||
return (
|
||||
<ContrastArea mode="dark">
|
||||
{({ css }) => (
|
||||
<>
|
||||
<Back isLoading={isLoading} name={showName} href={href} {...css(opacity, props)} />
|
||||
<TouchControls previousSlug={previousSlug} nextSlug={nextSlug} />
|
||||
<Pressable
|
||||
tabIndex={-1}
|
||||
onPointerDown={onPointerDown}
|
||||
onPress={Platform.OS !== "web" ? () => onPointerDown?.({} as any) : undefined}
|
||||
{...css(
|
||||
[
|
||||
{
|
||||
// Fixed is used because firefox android make the hover disapear under the navigation bar in absolute
|
||||
position: Platform.OS === "web" ? ("fixed" as any) : "absolute",
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bg: (theme) => theme.darkOverlay,
|
||||
flexDirection: "row",
|
||||
padding: percent(1),
|
||||
},
|
||||
opacity,
|
||||
],
|
||||
props,
|
||||
)}
|
||||
<View
|
||||
onPointerEnter={(e) => {
|
||||
if (e.nativeEvent.pointerType === "mouse")
|
||||
setHover((x) => ({ ...x, mouseHover: true }));
|
||||
}}
|
||||
onPointerLeave={(e) => {
|
||||
if (e.nativeEvent.pointerType === "mouse")
|
||||
setHover((x) => ({ ...x, mouseHover: false }));
|
||||
}}
|
||||
pointerEvents="none"
|
||||
{...css({
|
||||
// TODO: animate show
|
||||
display: !show ? "none" : "flex",
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
})}
|
||||
>
|
||||
<VideoPoster poster={poster} alt={showName} isLoading={isLoading} />
|
||||
<Back isLoading={isLoading} name={showName} href={href} pointerEvents="auto" />
|
||||
<View
|
||||
pointerEvents="auto"
|
||||
{...css({
|
||||
marginLeft: { xs: ts(0.5), sm: ts(3) },
|
||||
flexDirection: "column",
|
||||
flexGrow: 1,
|
||||
flexShrink: 1,
|
||||
maxWidth: percent(100),
|
||||
// Fixed is used because firefox android make the hover disapear under the navigation bar in absolute
|
||||
position: Platform.OS === "web" ? ("fixed" as any) : "absolute",
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bg: (theme) => theme.darkOverlay,
|
||||
flexDirection: "row",
|
||||
padding: percent(1),
|
||||
})}
|
||||
>
|
||||
<H2 {...css({ paddingBottom: ts(1) })}>
|
||||
{isLoading ? <Skeleton {...css({ width: rem(15), height: rem(2) })} /> : name}
|
||||
</H2>
|
||||
<ProgressBar chapters={chapters} />
|
||||
<VideoPoster poster={poster} alt={showName} isLoading={isLoading} />
|
||||
<View
|
||||
{...css({
|
||||
flexDirection: "row",
|
||||
marginLeft: { xs: ts(0.5), sm: ts(3) },
|
||||
flexDirection: "column",
|
||||
flexGrow: 1,
|
||||
justifyContent: "space-between",
|
||||
flexWrap: "wrap",
|
||||
flexShrink: 1,
|
||||
maxWidth: percent(100),
|
||||
})}
|
||||
>
|
||||
<LeftButtons previousSlug={previousSlug} nextSlug={nextSlug} />
|
||||
<RightButtons
|
||||
subtitles={subtitles}
|
||||
audios={audios}
|
||||
fonts={fonts}
|
||||
onMenuOpen={onMenuOpen}
|
||||
onMenuClose={onMenuClose}
|
||||
/>
|
||||
<H2 numberOfLines={1} {...css({ paddingBottom: ts(1) })}>
|
||||
{isLoading ? <Skeleton {...css({ width: rem(15), height: rem(2) })} /> : name}
|
||||
</H2>
|
||||
<ProgressBar chapters={chapters} />
|
||||
<View
|
||||
{...css({
|
||||
flexDirection: "row",
|
||||
flexGrow: 1,
|
||||
justifyContent: "space-between",
|
||||
flexWrap: "wrap",
|
||||
})}
|
||||
>
|
||||
<LeftButtons previousSlug={previousSlug} nextSlug={nextSlug} />
|
||||
<RightButtons
|
||||
subtitles={subtitles}
|
||||
audios={audios}
|
||||
fonts={fonts}
|
||||
onMenuOpen={() => setHover((x) => ({ ...x, menuOpened: true }))}
|
||||
onMenuClose={() => {
|
||||
// Disable hover since the menu overlay makes the mouseout unreliable.
|
||||
setHover((x) => ({ ...x, menuOpened: false, mouseHover: false }));
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</Pressable>
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
</ContrastArea>
|
||||
);
|
||||
};
|
||||
|
||||
export const HoverTouch = ({ children, ...props }: { children: ReactNode }) => {
|
||||
const hover = useAtomValue(hoverAtom);
|
||||
const setHover = useSetAtom(hoverReasonAtom);
|
||||
const mouseCallback = useRef<NodeJS.Timeout | null>(null);
|
||||
const touch = useRef<{ count: number; timeout?: NodeJS.Timeout }>({ count: 0 });
|
||||
const playerWidth = useRef<number | null>(null);
|
||||
|
||||
const show = useCallback(() => {
|
||||
setHover((x) => ({ ...x, mouseMoved: true }));
|
||||
if (mouseCallback.current) clearTimeout(mouseCallback.current);
|
||||
mouseCallback.current = setTimeout(() => {
|
||||
setHover((x) => ({ ...x, mouseMoved: false }));
|
||||
}, 2500);
|
||||
}, [setHover]);
|
||||
|
||||
// On mouse move
|
||||
useEffect(() => {
|
||||
if (Platform.OS !== "web") return;
|
||||
const handler = (e: PointerEvent) => {
|
||||
if (e.pointerType !== "mouse") return;
|
||||
show();
|
||||
};
|
||||
|
||||
document.addEventListener("pointermove", handler);
|
||||
return () => document.removeEventListener("pointermove", handler);
|
||||
}, [show]);
|
||||
|
||||
// When the controls hide, remove focus so space can be used to play/pause instead of triggering the button
|
||||
// It also serves to hide the tooltip.
|
||||
useEffect(() => {
|
||||
if (Platform.OS !== "web") return;
|
||||
if (!hover && document.activeElement instanceof HTMLElement) document.activeElement.blur();
|
||||
}, [hover]);
|
||||
|
||||
const { css } = useYoshiki();
|
||||
|
||||
const duration = useAtomValue(durationAtom);
|
||||
const setPlay = useSetAtom(playAtom);
|
||||
const setProgress = useSetAtom(progressAtom);
|
||||
const setFullscreen = useSetAtom(fullscreenAtom);
|
||||
|
||||
const onPress = (e: NativePointerEvent) => {
|
||||
if (Platform.OS === "web" && e.nativeEvent.pointerType === "mouse") {
|
||||
setPlay((x) => !x);
|
||||
return;
|
||||
}
|
||||
if (hover) setHover((x) => ({ ...x, mouseMoved: false }));
|
||||
else show();
|
||||
};
|
||||
const onDoublePress = (e: NativePointerEvent) => {
|
||||
if (Platform.OS === "web" && e.nativeEvent.pointerType === "mouse") {
|
||||
// Only reset touch count for the web, on mobile you can continue to seek by pressing again.
|
||||
touch.current.count = 0;
|
||||
setFullscreen((x) => !x);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!duration || !playerWidth.current) return;
|
||||
|
||||
if (e.nativeEvent.x < playerWidth.current * 0.33) {
|
||||
setProgress((x) => Math.max(x - 10, 0));
|
||||
}
|
||||
if (e.nativeEvent.x > playerWidth.current * 0.66) {
|
||||
setProgress((x) => Math.min(x + 10, duration));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
tabIndex={-1}
|
||||
onPointerLeave={(e) => {
|
||||
if (e.nativeEvent.pointerType === "mouse") setHover((x) => ({ ...x, mouseMoved: false }));
|
||||
}}
|
||||
onPointerDown={(e) => {
|
||||
console.log("down");
|
||||
if (Platform.OS === "web") e.preventDefault();
|
||||
|
||||
touch.current.count++;
|
||||
if (touch.current.count >= 2) {
|
||||
touch.current.count = 0;
|
||||
onDoublePress(e);
|
||||
clearTimeout(touch.current.timeout);
|
||||
} else {
|
||||
onPress(e);
|
||||
}
|
||||
|
||||
touch.current.timeout = setTimeout(() => {
|
||||
touch.current.count = 0;
|
||||
touch.current.timeout = undefined;
|
||||
}, 400);
|
||||
}}
|
||||
onLayout={(e) => {
|
||||
playerWidth.current = e.nativeEvent.layout.width;
|
||||
}}
|
||||
{...css(
|
||||
{
|
||||
flexDirection: "row",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
// @ts-expect-error Web only property
|
||||
cursor: hover ? "unset" : "none",
|
||||
},
|
||||
props,
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</Pressable>
|
||||
);
|
||||
};
|
||||
|
||||
const ProgressBar = ({ chapters }: { chapters?: Chapter[] }) => {
|
||||
const [progress, setProgress] = useAtom(progressAtom);
|
||||
const buffered = useAtomValue(bufferedAtom);
|
||||
@ -165,7 +313,7 @@ const ProgressBar = ({ chapters }: { chapters?: Chapter[] }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const Back = ({
|
||||
export const Back = ({
|
||||
isLoading,
|
||||
name,
|
||||
href,
|
||||
|
@ -18,17 +18,7 @@
|
||||
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {
|
||||
IconButton,
|
||||
Link,
|
||||
NoTouch,
|
||||
P,
|
||||
Slider,
|
||||
noTouch,
|
||||
tooltip,
|
||||
touchOnly,
|
||||
ts,
|
||||
} from "@kyoo/primitives";
|
||||
import { IconButton, Link, P, Slider, noTouch, tooltip, touchOnly, ts } from "@kyoo/primitives";
|
||||
import { useAtom, useAtomValue } from "jotai";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Platform, View } from "react-native";
|
||||
@ -38,11 +28,11 @@ import PlayArrow from "@material-symbols/svg-400/rounded/play_arrow-fill.svg";
|
||||
import Pause from "@material-symbols/svg-400/rounded/pause-fill.svg";
|
||||
import VolumeOff from "@material-symbols/svg-400/rounded/volume_off-fill.svg";
|
||||
import VolumeMute from "@material-symbols/svg-400/rounded/volume_mute-fill.svg";
|
||||
import VolumeDown from "@material-symbols/svg-400/rounded/volume_down-fill.svg";
|
||||
import common from "@material-symbols/svg-400/rounded/volume_down-fill.svg";
|
||||
import VolumeUp from "@material-symbols/svg-400/rounded/volume_up-fill.svg";
|
||||
import { durationAtom, mutedAtom, playAtom, progressAtom, volumeAtom } from "../state";
|
||||
import { Stylable, px, useYoshiki } from "yoshiki/native";
|
||||
import { Component, ComponentProps } from "react";
|
||||
import { HoverTouch, hoverAtom } from "./hover";
|
||||
|
||||
export const LeftButtons = ({
|
||||
previousSlug,
|
||||
@ -96,18 +86,30 @@ export const LeftButtons = ({
|
||||
export const TouchControls = ({
|
||||
previousSlug,
|
||||
nextSlug,
|
||||
...props
|
||||
}: {
|
||||
previousSlug?: string | null;
|
||||
nextSlug?: string | null;
|
||||
}) => {
|
||||
const { css } = useYoshiki();
|
||||
const { t } = useTranslation();
|
||||
const [isPlaying, setPlay] = useAtom(playAtom);
|
||||
const hover = useAtomValue(hoverAtom);
|
||||
|
||||
const spacing = css({ backgroundColor: (theme) => theme.darkOverlay, marginHorizontal: ts(3) });
|
||||
const common = css(
|
||||
[
|
||||
{
|
||||
backgroundColor: (theme) => theme.darkOverlay,
|
||||
marginHorizontal: ts(3),
|
||||
},
|
||||
!hover && {
|
||||
display: "none",
|
||||
},
|
||||
],
|
||||
touchOnly,
|
||||
);
|
||||
|
||||
return (
|
||||
<View
|
||||
<HoverTouch
|
||||
{...css(
|
||||
{
|
||||
flexDirection: "row",
|
||||
@ -119,7 +121,7 @@ export const TouchControls = ({
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
},
|
||||
touchOnly,
|
||||
props,
|
||||
)}
|
||||
>
|
||||
{previousSlug && (
|
||||
@ -129,31 +131,22 @@ export const TouchControls = ({
|
||||
href={previousSlug}
|
||||
replace
|
||||
size={ts(4)}
|
||||
{...tooltip(t("player.previous"), true)}
|
||||
{...spacing}
|
||||
{...common}
|
||||
/>
|
||||
)}
|
||||
<IconButton
|
||||
icon={isPlaying ? Pause : PlayArrow}
|
||||
onPress={() => setPlay(!isPlaying)}
|
||||
size={ts(8)}
|
||||
{...tooltip(isPlaying ? t("player.pause") : t("player.play"), true)}
|
||||
{...spacing}
|
||||
{...common}
|
||||
/>
|
||||
{nextSlug && (
|
||||
<IconButton
|
||||
icon={SkipNext}
|
||||
as={Link}
|
||||
href={nextSlug}
|
||||
replace
|
||||
size={ts(4)}
|
||||
{...tooltip(t("player.next"), true)}
|
||||
{...spacing}
|
||||
/>
|
||||
<IconButton icon={SkipNext} as={Link} href={nextSlug} replace size={ts(4)} {...common} />
|
||||
)}
|
||||
</View>
|
||||
</HoverTouch>
|
||||
);
|
||||
};
|
||||
|
||||
const VolumeSlider = () => {
|
||||
const [volume, setVolume] = useAtom(volumeAtom);
|
||||
const [isMuted, setMuted] = useAtom(mutedAtom);
|
||||
|
@ -31,13 +31,13 @@ import {
|
||||
} from "@kyoo/models";
|
||||
import { Head } from "@kyoo/primitives";
|
||||
import { useState, useEffect, ComponentProps } from "react";
|
||||
import { Platform, StyleSheet, View, PointerEvent as NativePointerEvent } from "react-native";
|
||||
import { Platform, StyleSheet, View } from "react-native";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useRouter } from "solito/router";
|
||||
import { useAtom } from "jotai";
|
||||
import { useSetAtom } from "jotai";
|
||||
import { useYoshiki } from "yoshiki/native";
|
||||
import { Back, Hover, LoadingIndicator } from "./components/hover";
|
||||
import { fullscreenAtom, playAtom, Video } from "./state";
|
||||
import { fullscreenAtom, Video } from "./state";
|
||||
import { episodeDisplayNumber } from "../details/episode";
|
||||
import { useVideoKeyboard } from "./keyboard";
|
||||
import { MediaSessionManager } from "./media-session";
|
||||
@ -86,14 +86,6 @@ const mapData = (
|
||||
};
|
||||
};
|
||||
|
||||
// Callback used to hide the controls when the mouse goes iddle. This is stored globally to clear the old timeout
|
||||
// if the mouse moves again (if this is stored as a state, the whole page is redrawn on mouse move)
|
||||
let mouseCallback: NodeJS.Timeout;
|
||||
// Number of time the video has been pressed. Used to handle double click. Since there is only one player,
|
||||
// this can be global and not in the state.
|
||||
let touchCount = 0;
|
||||
let touchTimeout: NodeJS.Timeout;
|
||||
|
||||
export const Player: QueryPage<{ slug: string; type: "episode" | "movie" }> = ({ slug, type }) => {
|
||||
const { css } = useYoshiki();
|
||||
const { t } = useTranslation();
|
||||
@ -113,65 +105,15 @@ export const Player: QueryPage<{ slug: string; type: "episode" | "movie" }> = ({
|
||||
|
||||
useVideoKeyboard(info?.subtitles, info?.fonts, previous, next);
|
||||
|
||||
const [isFullscreen, setFullscreen] = useAtom(fullscreenAtom);
|
||||
const [isPlaying, setPlay] = useAtom(playAtom);
|
||||
const [showHover, setHover] = useState(false);
|
||||
const [mouseMoved, setMouseMoved] = useState(false);
|
||||
const [menuOpenned, setMenuOpen] = useState(false);
|
||||
|
||||
const displayControls = showHover || !isPlaying || mouseMoved || menuOpenned;
|
||||
const show = () => {
|
||||
setMouseMoved(true);
|
||||
if (mouseCallback) clearTimeout(mouseCallback);
|
||||
mouseCallback = setTimeout(() => {
|
||||
setMouseMoved(false);
|
||||
}, 2500);
|
||||
};
|
||||
const setFullscreen = useSetAtom(fullscreenAtom);
|
||||
useEffect(() => {
|
||||
if (Platform.OS !== "web") return;
|
||||
const handler = (e: PointerEvent) => {
|
||||
if (e.pointerType !== "mouse") return;
|
||||
show();
|
||||
};
|
||||
|
||||
document.addEventListener("pointermove", handler);
|
||||
return () => document.removeEventListener("pointermove", handler);
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (Platform.OS !== "web" || !/Mobi/i.test(window.navigator.userAgent)) return;
|
||||
setFullscreen(true);
|
||||
if (!/Mobi/i.test(window.navigator.userAgent)) setFullscreen(true);
|
||||
return () => {
|
||||
setFullscreen(false);
|
||||
};
|
||||
}, [setFullscreen]);
|
||||
|
||||
const onPointerDown = (e: NativePointerEvent) => {
|
||||
if (Platform.OS === "web") e.preventDefault();
|
||||
if (Platform.OS !== "web" || e.nativeEvent.pointerType !== "mouse") {
|
||||
displayControls ? setMouseMoved(false) : show();
|
||||
return;
|
||||
}
|
||||
touchCount++;
|
||||
if (touchCount == 2) {
|
||||
touchCount = 0;
|
||||
setFullscreen(!isFullscreen);
|
||||
clearTimeout(touchTimeout);
|
||||
} else
|
||||
touchTimeout = setTimeout(() => {
|
||||
touchCount = 0;
|
||||
}, 400);
|
||||
setPlay(!isPlaying);
|
||||
};
|
||||
|
||||
// When the controls hide, remove focus so space can be used to play/pause instead of triggering the button
|
||||
// It also serves to hide the tooltip.
|
||||
useEffect(() => {
|
||||
if (Platform.OS !== "web") return;
|
||||
if (!displayControls && document.activeElement instanceof HTMLElement)
|
||||
document.activeElement.blur();
|
||||
}, [displayControls]);
|
||||
|
||||
if (error || infoError || playbackError)
|
||||
return (
|
||||
<>
|
||||
@ -206,15 +148,10 @@ export const Player: QueryPage<{ slug: string; type: "episode" | "movie" }> = ({
|
||||
/>
|
||||
{data && <WatchStatusObserver type={type} slug={data.slug} />}
|
||||
<View
|
||||
onPointerLeave={(e) => {
|
||||
if (e.nativeEvent.pointerType === "mouse") setMouseMoved(false);
|
||||
}}
|
||||
{...css({
|
||||
flexGrow: 1,
|
||||
flexShrink: 1,
|
||||
bg: "black",
|
||||
// @ts-ignore Web only
|
||||
cursor: displayControls ? "unset" : "none",
|
||||
})}
|
||||
>
|
||||
<Video
|
||||
@ -222,7 +159,6 @@ export const Player: QueryPage<{ slug: string; type: "episode" | "movie" }> = ({
|
||||
subtitles={info?.subtitles}
|
||||
setError={setPlaybackError}
|
||||
fonts={info?.fonts}
|
||||
onPointerDown={(e) => onPointerDown(e)}
|
||||
onEnd={() => {
|
||||
if (!data) return;
|
||||
if (data.type === "movie")
|
||||
@ -239,32 +175,7 @@ export const Player: QueryPage<{ slug: string; type: "episode" | "movie" }> = ({
|
||||
{...css(StyleSheet.absoluteFillObject)}
|
||||
/>
|
||||
<LoadingIndicator />
|
||||
<Hover
|
||||
{...mapData(data, info, previous, next)}
|
||||
onPointerEnter={(e) => {
|
||||
if (Platform.OS !== "web" || e.nativeEvent.pointerType === "mouse") setHover(true);
|
||||
}}
|
||||
onPointerLeave={(e) => {
|
||||
if (e.nativeEvent.pointerType === "mouse") setHover(false);
|
||||
}}
|
||||
onPointerDown={(e) => {
|
||||
if (!displayControls) {
|
||||
onPointerDown(e);
|
||||
if (Platform.OS === "web") e.preventDefault();
|
||||
}
|
||||
}}
|
||||
onMenuOpen={() => setMenuOpen(true)}
|
||||
onMenuClose={() => {
|
||||
// Disable hover since the menu overlay makes the mouseout unreliable.
|
||||
setHover(false);
|
||||
setMenuOpen(false);
|
||||
}}
|
||||
show={displayControls}
|
||||
{...css({
|
||||
// @ts-ignore Web only
|
||||
cursor: "unset",
|
||||
})}
|
||||
/>
|
||||
<Hover {...mapData(data, info, previous, next)} />
|
||||
</View>
|
||||
</>
|
||||
);
|
||||
|
@ -39,9 +39,13 @@ export const durationAtom = atom<number | undefined>(undefined);
|
||||
|
||||
export const progressAtom = atom(
|
||||
(get) => get(privateProgressAtom),
|
||||
(_, set, value: number) => {
|
||||
set(privateProgressAtom, value);
|
||||
set(publicProgressAtom, value);
|
||||
(get, set, update: number | ((value: number) => number)) => {
|
||||
const run = (value: number) => {
|
||||
set(privateProgressAtom, value);
|
||||
set(publicProgressAtom, value);
|
||||
};
|
||||
if (typeof update === "function") run(update(get(privateProgressAtom)));
|
||||
else run(update);
|
||||
},
|
||||
);
|
||||
const privateProgressAtom = atom(0);
|
||||
@ -52,23 +56,27 @@ export const mutedAtom = atom(false);
|
||||
|
||||
export const fullscreenAtom = atom(
|
||||
(get) => get(privateFullscreen),
|
||||
async (_, set, value: boolean) => {
|
||||
try {
|
||||
if (value) {
|
||||
await document.body.requestFullscreen({
|
||||
navigationUI: "hide",
|
||||
});
|
||||
set(privateFullscreen, true);
|
||||
// @ts-expect-error Firefox does not support this so ts complains
|
||||
await screen.orientation.lock("landscape");
|
||||
} else {
|
||||
await document.exitFullscreen();
|
||||
set(privateFullscreen, false);
|
||||
screen.orientation.unlock();
|
||||
(get, set, update: boolean | ((value: boolean) => boolean)) => {
|
||||
const run = async (value: boolean) => {
|
||||
try {
|
||||
if (value) {
|
||||
await document.body.requestFullscreen({
|
||||
navigationUI: "hide",
|
||||
});
|
||||
set(privateFullscreen, true);
|
||||
// @ts-expect-error Firefox does not support this so ts complains
|
||||
await screen.orientation.lock("landscape");
|
||||
} else {
|
||||
if (document.fullscreenElement) await document.exitFullscreen();
|
||||
set(privateFullscreen, false);
|
||||
screen.orientation.unlock();
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
};
|
||||
if (typeof update === "function") run(update(get(privateFullscreen)));
|
||||
else run(update);
|
||||
},
|
||||
);
|
||||
const privateFullscreen = atom(false);
|
||||
|
Loading…
x
Reference in New Issue
Block a user