mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-06-03 13:44:33 -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,
|
Skeleton,
|
||||||
Slider,
|
Slider,
|
||||||
tooltip,
|
tooltip,
|
||||||
touchOnly,
|
|
||||||
ts,
|
ts,
|
||||||
} from "@kyoo/primitives";
|
} from "@kyoo/primitives";
|
||||||
import { Chapter, KyooImage, Subtitle, Audio } from "@kyoo/models";
|
import { Chapter, KyooImage, Subtitle, Audio } from "@kyoo/models";
|
||||||
import { useAtomValue, useSetAtom, useAtom } from "jotai";
|
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 { useTranslation } from "react-i18next";
|
||||||
import { percent, rem, useYoshiki } from "yoshiki/native";
|
import { percent, rem, useYoshiki } from "yoshiki/native";
|
||||||
import { useRouter } from "solito/router";
|
import { useRouter } from "solito/router";
|
||||||
import ArrowBack from "@material-symbols/svg-400/rounded/arrow_back-fill.svg";
|
import ArrowBack from "@material-symbols/svg-400/rounded/arrow_back-fill.svg";
|
||||||
import { LeftButtons, TouchControls } from "./left-buttons";
|
import { LeftButtons, TouchControls } from "./left-buttons";
|
||||||
import { RightButtons } from "./right-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 = ({
|
export const Hover = ({
|
||||||
isLoading,
|
isLoading,
|
||||||
@ -57,11 +81,6 @@ export const Hover = ({
|
|||||||
fonts,
|
fonts,
|
||||||
previousSlug,
|
previousSlug,
|
||||||
nextSlug,
|
nextSlug,
|
||||||
onMenuOpen,
|
|
||||||
onMenuClose,
|
|
||||||
show,
|
|
||||||
onPointerDown,
|
|
||||||
...props
|
|
||||||
}: {
|
}: {
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
name?: string | null;
|
name?: string | null;
|
||||||
@ -74,78 +93,207 @@ export const Hover = ({
|
|||||||
fonts?: string[];
|
fonts?: string[];
|
||||||
previousSlug?: string | null;
|
previousSlug?: string | null;
|
||||||
nextSlug?: string | null;
|
nextSlug?: string | null;
|
||||||
onMenuOpen: () => void;
|
}) => {
|
||||||
onMenuClose: () => void;
|
const show = useAtomValue(hoverAtom);
|
||||||
show: boolean;
|
const setHover = useSetAtom(hoverReasonAtom);
|
||||||
} & ViewProps) => {
|
|
||||||
// TODO: animate show
|
|
||||||
const opacity = !show && (Platform.OS === "web" ? { opacity: 0 } : { display: "none" as const });
|
|
||||||
return (
|
return (
|
||||||
<ContrastArea mode="dark">
|
<ContrastArea mode="dark">
|
||||||
{({ css }) => (
|
{({ css }) => (
|
||||||
<>
|
<>
|
||||||
<Back isLoading={isLoading} name={showName} href={href} {...css(opacity, props)} />
|
|
||||||
<TouchControls previousSlug={previousSlug} nextSlug={nextSlug} />
|
<TouchControls previousSlug={previousSlug} nextSlug={nextSlug} />
|
||||||
<Pressable
|
<View
|
||||||
tabIndex={-1}
|
onPointerEnter={(e) => {
|
||||||
onPointerDown={onPointerDown}
|
if (e.nativeEvent.pointerType === "mouse")
|
||||||
onPress={Platform.OS !== "web" ? () => onPointerDown?.({} as any) : undefined}
|
setHover((x) => ({ ...x, mouseHover: true }));
|
||||||
{...css(
|
}}
|
||||||
[
|
onPointerLeave={(e) => {
|
||||||
{
|
if (e.nativeEvent.pointerType === "mouse")
|
||||||
// Fixed is used because firefox android make the hover disapear under the navigation bar in absolute
|
setHover((x) => ({ ...x, mouseHover: false }));
|
||||||
position: Platform.OS === "web" ? ("fixed" as any) : "absolute",
|
}}
|
||||||
bottom: 0,
|
pointerEvents="none"
|
||||||
left: 0,
|
{...css({
|
||||||
right: 0,
|
// TODO: animate show
|
||||||
bg: (theme) => theme.darkOverlay,
|
display: !show ? "none" : "flex",
|
||||||
flexDirection: "row",
|
position: "absolute",
|
||||||
padding: percent(1),
|
top: 0,
|
||||||
},
|
left: 0,
|
||||||
opacity,
|
bottom: 0,
|
||||||
],
|
right: 0,
|
||||||
props,
|
})}
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
<VideoPoster poster={poster} alt={showName} isLoading={isLoading} />
|
<Back isLoading={isLoading} name={showName} href={href} pointerEvents="auto" />
|
||||||
<View
|
<View
|
||||||
|
pointerEvents="auto"
|
||||||
{...css({
|
{...css({
|
||||||
marginLeft: { xs: ts(0.5), sm: ts(3) },
|
// Fixed is used because firefox android make the hover disapear under the navigation bar in absolute
|
||||||
flexDirection: "column",
|
position: Platform.OS === "web" ? ("fixed" as any) : "absolute",
|
||||||
flexGrow: 1,
|
bottom: 0,
|
||||||
flexShrink: 1,
|
left: 0,
|
||||||
maxWidth: percent(100),
|
right: 0,
|
||||||
|
bg: (theme) => theme.darkOverlay,
|
||||||
|
flexDirection: "row",
|
||||||
|
padding: percent(1),
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<H2 {...css({ paddingBottom: ts(1) })}>
|
<VideoPoster poster={poster} alt={showName} isLoading={isLoading} />
|
||||||
{isLoading ? <Skeleton {...css({ width: rem(15), height: rem(2) })} /> : name}
|
|
||||||
</H2>
|
|
||||||
<ProgressBar chapters={chapters} />
|
|
||||||
<View
|
<View
|
||||||
{...css({
|
{...css({
|
||||||
flexDirection: "row",
|
marginLeft: { xs: ts(0.5), sm: ts(3) },
|
||||||
|
flexDirection: "column",
|
||||||
flexGrow: 1,
|
flexGrow: 1,
|
||||||
justifyContent: "space-between",
|
flexShrink: 1,
|
||||||
flexWrap: "wrap",
|
maxWidth: percent(100),
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<LeftButtons previousSlug={previousSlug} nextSlug={nextSlug} />
|
<H2 numberOfLines={1} {...css({ paddingBottom: ts(1) })}>
|
||||||
<RightButtons
|
{isLoading ? <Skeleton {...css({ width: rem(15), height: rem(2) })} /> : name}
|
||||||
subtitles={subtitles}
|
</H2>
|
||||||
audios={audios}
|
<ProgressBar chapters={chapters} />
|
||||||
fonts={fonts}
|
<View
|
||||||
onMenuOpen={onMenuOpen}
|
{...css({
|
||||||
onMenuClose={onMenuClose}
|
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>
|
||||||
</View>
|
</View>
|
||||||
</Pressable>
|
</View>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</ContrastArea>
|
</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 ProgressBar = ({ chapters }: { chapters?: Chapter[] }) => {
|
||||||
const [progress, setProgress] = useAtom(progressAtom);
|
const [progress, setProgress] = useAtom(progressAtom);
|
||||||
const buffered = useAtomValue(bufferedAtom);
|
const buffered = useAtomValue(bufferedAtom);
|
||||||
@ -165,7 +313,7 @@ const ProgressBar = ({ chapters }: { chapters?: Chapter[] }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const Back = ({
|
export const Back = ({
|
||||||
isLoading,
|
isLoading,
|
||||||
name,
|
name,
|
||||||
href,
|
href,
|
||||||
|
@ -18,17 +18,7 @@
|
|||||||
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {
|
import { IconButton, Link, P, Slider, noTouch, tooltip, touchOnly, ts } from "@kyoo/primitives";
|
||||||
IconButton,
|
|
||||||
Link,
|
|
||||||
NoTouch,
|
|
||||||
P,
|
|
||||||
Slider,
|
|
||||||
noTouch,
|
|
||||||
tooltip,
|
|
||||||
touchOnly,
|
|
||||||
ts,
|
|
||||||
} from "@kyoo/primitives";
|
|
||||||
import { useAtom, useAtomValue } from "jotai";
|
import { useAtom, useAtomValue } from "jotai";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Platform, View } from "react-native";
|
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 Pause from "@material-symbols/svg-400/rounded/pause-fill.svg";
|
||||||
import VolumeOff from "@material-symbols/svg-400/rounded/volume_off-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 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 VolumeUp from "@material-symbols/svg-400/rounded/volume_up-fill.svg";
|
||||||
import { durationAtom, mutedAtom, playAtom, progressAtom, volumeAtom } from "../state";
|
import { durationAtom, mutedAtom, playAtom, progressAtom, volumeAtom } from "../state";
|
||||||
import { Stylable, px, useYoshiki } from "yoshiki/native";
|
import { Stylable, px, useYoshiki } from "yoshiki/native";
|
||||||
import { Component, ComponentProps } from "react";
|
import { HoverTouch, hoverAtom } from "./hover";
|
||||||
|
|
||||||
export const LeftButtons = ({
|
export const LeftButtons = ({
|
||||||
previousSlug,
|
previousSlug,
|
||||||
@ -96,18 +86,30 @@ export const LeftButtons = ({
|
|||||||
export const TouchControls = ({
|
export const TouchControls = ({
|
||||||
previousSlug,
|
previousSlug,
|
||||||
nextSlug,
|
nextSlug,
|
||||||
|
...props
|
||||||
}: {
|
}: {
|
||||||
previousSlug?: string | null;
|
previousSlug?: string | null;
|
||||||
nextSlug?: string | null;
|
nextSlug?: string | null;
|
||||||
}) => {
|
}) => {
|
||||||
const { css } = useYoshiki();
|
const { css } = useYoshiki();
|
||||||
const { t } = useTranslation();
|
|
||||||
const [isPlaying, setPlay] = useAtom(playAtom);
|
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 (
|
return (
|
||||||
<View
|
<HoverTouch
|
||||||
{...css(
|
{...css(
|
||||||
{
|
{
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
@ -119,7 +121,7 @@ export const TouchControls = ({
|
|||||||
right: 0,
|
right: 0,
|
||||||
bottom: 0,
|
bottom: 0,
|
||||||
},
|
},
|
||||||
touchOnly,
|
props,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{previousSlug && (
|
{previousSlug && (
|
||||||
@ -129,31 +131,22 @@ export const TouchControls = ({
|
|||||||
href={previousSlug}
|
href={previousSlug}
|
||||||
replace
|
replace
|
||||||
size={ts(4)}
|
size={ts(4)}
|
||||||
{...tooltip(t("player.previous"), true)}
|
{...common}
|
||||||
{...spacing}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<IconButton
|
<IconButton
|
||||||
icon={isPlaying ? Pause : PlayArrow}
|
icon={isPlaying ? Pause : PlayArrow}
|
||||||
onPress={() => setPlay(!isPlaying)}
|
onPress={() => setPlay(!isPlaying)}
|
||||||
size={ts(8)}
|
size={ts(8)}
|
||||||
{...tooltip(isPlaying ? t("player.pause") : t("player.play"), true)}
|
{...common}
|
||||||
{...spacing}
|
|
||||||
/>
|
/>
|
||||||
{nextSlug && (
|
{nextSlug && (
|
||||||
<IconButton
|
<IconButton icon={SkipNext} as={Link} href={nextSlug} replace size={ts(4)} {...common} />
|
||||||
icon={SkipNext}
|
|
||||||
as={Link}
|
|
||||||
href={nextSlug}
|
|
||||||
replace
|
|
||||||
size={ts(4)}
|
|
||||||
{...tooltip(t("player.next"), true)}
|
|
||||||
{...spacing}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
</View>
|
</HoverTouch>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const VolumeSlider = () => {
|
const VolumeSlider = () => {
|
||||||
const [volume, setVolume] = useAtom(volumeAtom);
|
const [volume, setVolume] = useAtom(volumeAtom);
|
||||||
const [isMuted, setMuted] = useAtom(mutedAtom);
|
const [isMuted, setMuted] = useAtom(mutedAtom);
|
||||||
|
@ -31,13 +31,13 @@ import {
|
|||||||
} from "@kyoo/models";
|
} from "@kyoo/models";
|
||||||
import { Head } from "@kyoo/primitives";
|
import { Head } from "@kyoo/primitives";
|
||||||
import { useState, useEffect, ComponentProps } from "react";
|
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 { useTranslation } from "react-i18next";
|
||||||
import { useRouter } from "solito/router";
|
import { useRouter } from "solito/router";
|
||||||
import { useAtom } from "jotai";
|
import { useSetAtom } from "jotai";
|
||||||
import { useYoshiki } from "yoshiki/native";
|
import { useYoshiki } from "yoshiki/native";
|
||||||
import { Back, Hover, LoadingIndicator } from "./components/hover";
|
import { Back, Hover, LoadingIndicator } from "./components/hover";
|
||||||
import { fullscreenAtom, playAtom, Video } from "./state";
|
import { fullscreenAtom, Video } from "./state";
|
||||||
import { episodeDisplayNumber } from "../details/episode";
|
import { episodeDisplayNumber } from "../details/episode";
|
||||||
import { useVideoKeyboard } from "./keyboard";
|
import { useVideoKeyboard } from "./keyboard";
|
||||||
import { MediaSessionManager } from "./media-session";
|
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 }) => {
|
export const Player: QueryPage<{ slug: string; type: "episode" | "movie" }> = ({ slug, type }) => {
|
||||||
const { css } = useYoshiki();
|
const { css } = useYoshiki();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@ -113,65 +105,15 @@ export const Player: QueryPage<{ slug: string; type: "episode" | "movie" }> = ({
|
|||||||
|
|
||||||
useVideoKeyboard(info?.subtitles, info?.fonts, previous, next);
|
useVideoKeyboard(info?.subtitles, info?.fonts, previous, next);
|
||||||
|
|
||||||
const [isFullscreen, setFullscreen] = useAtom(fullscreenAtom);
|
const setFullscreen = useSetAtom(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);
|
|
||||||
};
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (Platform.OS !== "web") return;
|
if (Platform.OS !== "web") return;
|
||||||
const handler = (e: PointerEvent) => {
|
if (!/Mobi/i.test(window.navigator.userAgent)) setFullscreen(true);
|
||||||
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);
|
|
||||||
return () => {
|
return () => {
|
||||||
setFullscreen(false);
|
setFullscreen(false);
|
||||||
};
|
};
|
||||||
}, [setFullscreen]);
|
}, [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)
|
if (error || infoError || playbackError)
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -206,15 +148,10 @@ export const Player: QueryPage<{ slug: string; type: "episode" | "movie" }> = ({
|
|||||||
/>
|
/>
|
||||||
{data && <WatchStatusObserver type={type} slug={data.slug} />}
|
{data && <WatchStatusObserver type={type} slug={data.slug} />}
|
||||||
<View
|
<View
|
||||||
onPointerLeave={(e) => {
|
|
||||||
if (e.nativeEvent.pointerType === "mouse") setMouseMoved(false);
|
|
||||||
}}
|
|
||||||
{...css({
|
{...css({
|
||||||
flexGrow: 1,
|
flexGrow: 1,
|
||||||
flexShrink: 1,
|
flexShrink: 1,
|
||||||
bg: "black",
|
bg: "black",
|
||||||
// @ts-ignore Web only
|
|
||||||
cursor: displayControls ? "unset" : "none",
|
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<Video
|
<Video
|
||||||
@ -222,7 +159,6 @@ export const Player: QueryPage<{ slug: string; type: "episode" | "movie" }> = ({
|
|||||||
subtitles={info?.subtitles}
|
subtitles={info?.subtitles}
|
||||||
setError={setPlaybackError}
|
setError={setPlaybackError}
|
||||||
fonts={info?.fonts}
|
fonts={info?.fonts}
|
||||||
onPointerDown={(e) => onPointerDown(e)}
|
|
||||||
onEnd={() => {
|
onEnd={() => {
|
||||||
if (!data) return;
|
if (!data) return;
|
||||||
if (data.type === "movie")
|
if (data.type === "movie")
|
||||||
@ -239,32 +175,7 @@ export const Player: QueryPage<{ slug: string; type: "episode" | "movie" }> = ({
|
|||||||
{...css(StyleSheet.absoluteFillObject)}
|
{...css(StyleSheet.absoluteFillObject)}
|
||||||
/>
|
/>
|
||||||
<LoadingIndicator />
|
<LoadingIndicator />
|
||||||
<Hover
|
<Hover {...mapData(data, info, previous, next)} />
|
||||||
{...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",
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
</View>
|
</View>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -39,9 +39,13 @@ export const durationAtom = atom<number | undefined>(undefined);
|
|||||||
|
|
||||||
export const progressAtom = atom(
|
export const progressAtom = atom(
|
||||||
(get) => get(privateProgressAtom),
|
(get) => get(privateProgressAtom),
|
||||||
(_, set, value: number) => {
|
(get, set, update: number | ((value: number) => number)) => {
|
||||||
set(privateProgressAtom, value);
|
const run = (value: number) => {
|
||||||
set(publicProgressAtom, value);
|
set(privateProgressAtom, value);
|
||||||
|
set(publicProgressAtom, value);
|
||||||
|
};
|
||||||
|
if (typeof update === "function") run(update(get(privateProgressAtom)));
|
||||||
|
else run(update);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
const privateProgressAtom = atom(0);
|
const privateProgressAtom = atom(0);
|
||||||
@ -52,23 +56,27 @@ export const mutedAtom = atom(false);
|
|||||||
|
|
||||||
export const fullscreenAtom = atom(
|
export const fullscreenAtom = atom(
|
||||||
(get) => get(privateFullscreen),
|
(get) => get(privateFullscreen),
|
||||||
async (_, set, value: boolean) => {
|
(get, set, update: boolean | ((value: boolean) => boolean)) => {
|
||||||
try {
|
const run = async (value: boolean) => {
|
||||||
if (value) {
|
try {
|
||||||
await document.body.requestFullscreen({
|
if (value) {
|
||||||
navigationUI: "hide",
|
await document.body.requestFullscreen({
|
||||||
});
|
navigationUI: "hide",
|
||||||
set(privateFullscreen, true);
|
});
|
||||||
// @ts-expect-error Firefox does not support this so ts complains
|
set(privateFullscreen, true);
|
||||||
await screen.orientation.lock("landscape");
|
// @ts-expect-error Firefox does not support this so ts complains
|
||||||
} else {
|
await screen.orientation.lock("landscape");
|
||||||
await document.exitFullscreen();
|
} else {
|
||||||
set(privateFullscreen, false);
|
if (document.fullscreenElement) await document.exitFullscreen();
|
||||||
screen.orientation.unlock();
|
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);
|
const privateFullscreen = atom(false);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user