mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-07-09 03:04:20 -04:00
Support mobile devices in the player
This commit is contained in:
parent
6e4f5feb7c
commit
6534f3bd25
@ -28,6 +28,7 @@ import { createQueryClient, fetchQuery, QueryIdentifier, QueryPage } from "~/uti
|
||||
import { defaultTheme } from "~/utils/themes/default-theme";
|
||||
import superjson from "superjson";
|
||||
import Head from "next/head";
|
||||
import { useMobileHover } from "~/utils/utils";
|
||||
|
||||
// Simply silence a SSR warning (see https://github.com/facebook/react/issues/14927 for more details)
|
||||
if (typeof window === "undefined") {
|
||||
@ -38,6 +39,8 @@ const App = ({ Component, pageProps }: AppProps) => {
|
||||
const [queryClient] = useState(() => createQueryClient());
|
||||
const { queryState, ...props } = superjson.deserialize<any>(pageProps ?? {});
|
||||
const getLayout = (Component as QueryPage).getLayout ?? ((page) => page);
|
||||
|
||||
useMobileHover();
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -30,7 +30,7 @@ const Document = () => {
|
||||
<link rel="icon" type="image/png" sizes="128x128" href="/icon-128x128.png" />
|
||||
<link rel="icon" type="image/png" sizes="256x256" href="/icon-256x256.png" />
|
||||
</Head>
|
||||
<body>
|
||||
<body className="hoverEnabled">
|
||||
<Main />
|
||||
<NextScript />
|
||||
</body>
|
||||
|
@ -39,7 +39,12 @@ import { RightButtons } from "./right-buttons";
|
||||
import { ProgressBar } from "./progress-bar";
|
||||
import { useAtomValue } from "jotai";
|
||||
|
||||
export const Hover = ({ data, ...props }: { data?: WatchItem } & BoxProps) => {
|
||||
export const Hover = ({
|
||||
data,
|
||||
onMenuOpen,
|
||||
onMenuClose,
|
||||
...props
|
||||
}: { data?: WatchItem; onMenuOpen: () => void; onMenuClose: () => void } & BoxProps) => {
|
||||
const name = data
|
||||
? data.isMovie
|
||||
? data.name
|
||||
@ -64,7 +69,7 @@ export const Hover = ({ data, ...props }: { data?: WatchItem } & BoxProps) => {
|
||||
}}
|
||||
>
|
||||
<VideoPoster poster={data?.poster} />
|
||||
<Box sx={{ width: "100%", ml: 3, display: "flex", flexDirection: "column" }}>
|
||||
<Box sx={{ width: "100%", ml: { xs: 0.5, sm: 3 }, display: "flex", flexDirection: "column" }}>
|
||||
<Typography variant="h4" component="h2" color="white" sx={{ pb: 1 }}>
|
||||
{name ?? <Skeleton />}
|
||||
</Typography>
|
||||
@ -76,7 +81,7 @@ export const Hover = ({ data, ...props }: { data?: WatchItem } & BoxProps) => {
|
||||
previousSlug={data && !data.isMovie ? data.previousEpisode?.slug : undefined}
|
||||
nextSlug={data && !data.isMovie ? data.nextEpisode?.slug : undefined}
|
||||
/>
|
||||
<RightButtons subtitles={data?.subtitles} fonts={data?.fonts} />
|
||||
<RightButtons subtitles={data?.subtitles} fonts={data?.fonts} onMenuOpen={onMenuOpen} onMenuClose={onMenuClose} />
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
|
@ -24,15 +24,38 @@ import useTranslation from "next-translate/useTranslation";
|
||||
import { useRouter } from "next/router";
|
||||
import { durationAtom, mutedAtom, playAtom, progressAtom, volumeAtom } from "../state";
|
||||
import NextLink from "next/link";
|
||||
import { Pause, PlayArrow, SkipNext, SkipPrevious, VolumeDown, VolumeMute, VolumeOff, VolumeUp } from "@mui/icons-material";
|
||||
import {
|
||||
Pause,
|
||||
PlayArrow,
|
||||
SkipNext,
|
||||
SkipPrevious,
|
||||
VolumeDown,
|
||||
VolumeMute,
|
||||
VolumeOff,
|
||||
VolumeUp,
|
||||
} from "@mui/icons-material";
|
||||
|
||||
export const LeftButtons = ({ previousSlug, nextSlug }: { previousSlug?: string; nextSlug?: string }) => {
|
||||
export const LeftButtons = ({
|
||||
previousSlug,
|
||||
nextSlug,
|
||||
}: {
|
||||
previousSlug?: string;
|
||||
nextSlug?: string;
|
||||
}) => {
|
||||
const { t } = useTranslation("player");
|
||||
const router = useRouter();
|
||||
const [isPlaying, setPlay] = useAtom(playAtom);
|
||||
|
||||
return (
|
||||
<Box sx={{ display: "flex", "> *": { mx: "8px !important" } }}>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
"> *": {
|
||||
mx: { xs: "2px !important", sm: "8px !important" },
|
||||
p: { xs: "4px !important", sm: "8px !important" },
|
||||
},
|
||||
}}
|
||||
>
|
||||
{previousSlug && (
|
||||
<Tooltip title={t("previous")}>
|
||||
<NextLink href={{ query: { ...router.query, slug: previousSlug } }} passHref>
|
||||
@ -74,10 +97,10 @@ const VolumeSlider = () => {
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
display: { xs: "none", sm: "flex" },
|
||||
m: "0 !important",
|
||||
p: "8px",
|
||||
"&:hover .slider": { width: "100px", px: "16px" },
|
||||
"body.hoverEnabled &:hover .slider": { width: "100px", px: "16px" },
|
||||
}}
|
||||
>
|
||||
<Tooltip title={t("mute")}>
|
||||
|
@ -31,9 +31,10 @@ export const ProgressBar = ({ chapters }: { chapters?: Chapter[] }) => {
|
||||
const buffered = useAtomValue(bufferedAtom);
|
||||
const duration = useAtomValue(durationAtom);
|
||||
|
||||
const updateProgress = (event: MouseEvent, skipSeek?: boolean) => {
|
||||
const updateProgress = (event: MouseEvent | TouchEvent, skipSeek?: boolean) => {
|
||||
if (!(isSeeking || skipSeek) || !ref?.current) return;
|
||||
const value: number = (event.pageX - ref.current.offsetLeft) / ref.current.clientWidth;
|
||||
const pageX: number = "pageX" in event ? event.pageX : event.changedTouches[0].pageX;
|
||||
const value: number = (pageX - ref.current.offsetLeft) / ref.current.clientWidth;
|
||||
setProgress(Math.max(0, Math.min(value, 1)) * duration);
|
||||
};
|
||||
|
||||
@ -41,16 +42,25 @@ export const ProgressBar = ({ chapters }: { chapters?: Chapter[] }) => {
|
||||
const handler = () => setSeek(false);
|
||||
|
||||
document.addEventListener("mouseup", handler);
|
||||
return () => document.removeEventListener("mouseup", handler);
|
||||
document.addEventListener("touchend", handler);
|
||||
return () => {
|
||||
document.removeEventListener("mouseup", handler);
|
||||
document.removeEventListener("touchend", handler);
|
||||
};
|
||||
});
|
||||
useEffect(() => {
|
||||
document.addEventListener("mousemove", updateProgress);
|
||||
return () => document.removeEventListener("mousemove", updateProgress);
|
||||
document.addEventListener("touchmove", updateProgress);
|
||||
return () => {
|
||||
document.removeEventListener("mousemove", updateProgress);
|
||||
document.removeEventListener("touchmove", updateProgress);
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<Box
|
||||
onMouseDown={(event) => {
|
||||
// prevent drag and drop of the UI.
|
||||
event.preventDefault();
|
||||
setSeek(true);
|
||||
}}
|
||||
@ -60,7 +70,8 @@ export const ProgressBar = ({ chapters }: { chapters?: Chapter[] }) => {
|
||||
width: "100%",
|
||||
py: 1,
|
||||
cursor: "pointer",
|
||||
"&:hover": {
|
||||
WebkitTapHighlightColor: "transparent",
|
||||
"body.hoverEnabled &:hover": {
|
||||
".thumb": { opacity: 1 },
|
||||
".bar": { transform: "unset" },
|
||||
},
|
||||
@ -130,4 +141,3 @@ export const ProgressBar = ({ chapters }: { chapters?: Chapter[] }) => {
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -31,16 +31,28 @@ import { fullscreenAtom, subtitleAtom } from "../state";
|
||||
export const RightButtons = ({
|
||||
subtitles,
|
||||
fonts,
|
||||
onMenuOpen,
|
||||
onMenuClose,
|
||||
}: {
|
||||
subtitles?: Track[];
|
||||
fonts?: Font[];
|
||||
onMenuOpen: () => void;
|
||||
onMenuClose: () => void;
|
||||
}) => {
|
||||
const { t } = useTranslation("player");
|
||||
const [subtitleAnchor, setSubtitleAnchor] = useState<HTMLButtonElement | null>(null);
|
||||
const [isFullscreen, setFullscreen] = useAtom(fullscreenAtom);
|
||||
|
||||
return (
|
||||
<Box sx={{ "> *": { m: "8px !important" } }}>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
"> *": {
|
||||
m: { xs: "4px !important", sm: "8px !important" },
|
||||
p: { xs: "4px !important", sm: "8px !important" },
|
||||
},
|
||||
}}
|
||||
>
|
||||
{subtitles && (
|
||||
<Tooltip title={t("subtitles")}>
|
||||
<IconButton
|
||||
@ -49,7 +61,10 @@ export const RightButtons = ({
|
||||
aria-controls={subtitleAnchor ? "subtitle-menu" : undefined}
|
||||
aria-haspopup="true"
|
||||
aria-expanded={subtitleAnchor ? "true" : undefined}
|
||||
onClick={(event) => setSubtitleAnchor(event.currentTarget)}
|
||||
onClick={(event) => {
|
||||
setSubtitleAnchor(event.currentTarget);
|
||||
onMenuOpen();
|
||||
}}
|
||||
sx={{ color: "white" }}
|
||||
>
|
||||
<ClosedCaption />
|
||||
@ -70,7 +85,10 @@ export const RightButtons = ({
|
||||
subtitles={subtitles!}
|
||||
fonts={fonts!}
|
||||
anchor={subtitleAnchor}
|
||||
onClose={() => setSubtitleAnchor(null)}
|
||||
onClose={() => {
|
||||
setSubtitleAnchor(null);
|
||||
onMenuClose();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
@ -84,7 +102,7 @@ const SubtitleMenu = ({
|
||||
onClose,
|
||||
}: {
|
||||
subtitles: Track[];
|
||||
fonts: Font[],
|
||||
fonts: Font[];
|
||||
anchor: HTMLElement;
|
||||
onClose: () => void;
|
||||
}) => {
|
||||
@ -129,7 +147,7 @@ const SubtitleMenu = ({
|
||||
key={sub.id}
|
||||
selected={selectedSubtitle?.id === sub.id}
|
||||
onClick={() => {
|
||||
setSubtitle({track: sub, fonts});
|
||||
setSubtitle({ track: sub, fonts });
|
||||
onClose();
|
||||
}}
|
||||
component={Link}
|
||||
@ -143,4 +161,3 @@ const SubtitleMenu = ({
|
||||
</Menu>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -23,11 +23,11 @@ import { withRoute } from "~/utils/router";
|
||||
import { WatchItem, WatchItemP } from "~/models/resources/watch-item";
|
||||
import { useFetch } from "~/utils/query";
|
||||
import { ErrorPage } from "~/components/errors";
|
||||
import { useState, useEffect } from "react";
|
||||
import { useState, useEffect, PointerEvent as ReactPointerEvent } from "react";
|
||||
import { Box } from "@mui/material";
|
||||
import { useAtomValue } from "jotai";
|
||||
import { useAtomValue, useSetAtom } from "jotai";
|
||||
import { Hover, LoadingIndicator } from "./components/hover";
|
||||
import { playAtom, useSubtitleController, useVideoController } from "./state";
|
||||
import { fullscreenAtom, playAtom, useSubtitleController, useVideoController } from "./state";
|
||||
|
||||
// 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)
|
||||
@ -40,71 +40,109 @@ const query = (slug: string): QueryIdentifier<WatchItem> => ({
|
||||
|
||||
const Player: QueryPage<{ slug: string }> = ({ slug }) => {
|
||||
const { data, error } = useFetch(query(slug));
|
||||
const { playerRef, videoProps } = useVideoController();
|
||||
const { playerRef, videoProps, onVideoClick } = useVideoController();
|
||||
const setFullscreen = useSetAtom(fullscreenAtom);
|
||||
|
||||
const isPlaying = useAtomValue(playAtom);
|
||||
const [showHover, setHover] = useState(false);
|
||||
const [mouseMoved, setMouseMoved] = useState(false);
|
||||
const displayControls = showHover || !isPlaying || mouseMoved;
|
||||
const [menuOpenned, setMenuOpen] = useState(false);
|
||||
const displayControls = showHover || !isPlaying || mouseMoved || menuOpenned;
|
||||
|
||||
const mouseHasMoved = () => {
|
||||
setMouseMoved(true);
|
||||
if (mouseCallback) clearTimeout(mouseCallback);
|
||||
mouseCallback = setTimeout(() => {
|
||||
setMouseMoved(false);
|
||||
}, 2500);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const handler = () => {
|
||||
setMouseMoved(true);
|
||||
if (mouseCallback) clearTimeout(mouseCallback);
|
||||
mouseCallback = setTimeout(() => {
|
||||
setMouseMoved(false);
|
||||
}, 2500);
|
||||
const handler = (e: PointerEvent) => {
|
||||
if (e.pointerType !== "mouse") return;
|
||||
mouseHasMoved();
|
||||
};
|
||||
|
||||
document.addEventListener("mousemove", handler);
|
||||
return () => document.removeEventListener("mousemove", handler);
|
||||
document.addEventListener("pointermove", handler);
|
||||
return () => document.removeEventListener("pointermove", handler);
|
||||
});
|
||||
|
||||
useSubtitleController(playerRef, data?.subtitles, data?.fonts);
|
||||
|
||||
useEffect(() => {
|
||||
if (!/Mobi/i.test(window.navigator.userAgent)) return;
|
||||
setFullscreen(true);
|
||||
return () => setFullscreen(false);
|
||||
}, [setFullscreen]);
|
||||
|
||||
if (error) return <ErrorPage {...error} />;
|
||||
|
||||
return (
|
||||
<Box
|
||||
onMouseLeave={() => setMouseMoved(false)}
|
||||
sx={{ cursor: displayControls ? "unset" : "none" }}
|
||||
>
|
||||
<Box
|
||||
component="video"
|
||||
src={data?.link.direct}
|
||||
{...videoProps}
|
||||
sx={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
objectFit: "contain",
|
||||
background: "black",
|
||||
}}
|
||||
/>
|
||||
<LoadingIndicator />
|
||||
<Hover
|
||||
data={data}
|
||||
onMouseEnter={() => setHover(true)}
|
||||
onMouseLeave={() => setHover(false)}
|
||||
sx={
|
||||
displayControls
|
||||
? {
|
||||
visibility: "visible",
|
||||
opacity: 1,
|
||||
transition: "opacity .2s ease-in",
|
||||
}
|
||||
: {
|
||||
visibility: "hidden",
|
||||
opacity: 0,
|
||||
transition: "opacity .4s ease-out, visibility 0s .4s",
|
||||
}
|
||||
<>
|
||||
<style jsx global>{`
|
||||
::cue {
|
||||
background-color: transparent;
|
||||
text-shadow: -1px -1px 0 #000, 1px -1px 0 #000, -1px 1px 0 #000, 1px 1px 0 #000;
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
`}</style>
|
||||
<Box
|
||||
onMouseLeave={() => setMouseMoved(false)}
|
||||
sx={{ cursor: displayControls ? "unset" : "none" }}
|
||||
>
|
||||
<Box
|
||||
component="video"
|
||||
src={data?.link.direct}
|
||||
{...videoProps}
|
||||
onPointerDown={(e: ReactPointerEvent<HTMLVideoElement>) => {
|
||||
if (e.pointerType === "mouse") {
|
||||
onVideoClick();
|
||||
} else if (mouseMoved) {
|
||||
setMouseMoved(false);
|
||||
} else {
|
||||
mouseHasMoved();
|
||||
}
|
||||
}}
|
||||
sx={{
|
||||
position: "fixed",
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
objectFit: "contain",
|
||||
background: "black",
|
||||
}}
|
||||
/>
|
||||
<LoadingIndicator />
|
||||
<Hover
|
||||
data={data}
|
||||
onPointerOver={(e: ReactPointerEvent<HTMLElement>) => {
|
||||
if (e.pointerType === "mouse") setHover(true);
|
||||
}}
|
||||
onPointerOut={() => setHover(false)}
|
||||
onMenuOpen={() => setMenuOpen(true)}
|
||||
onMenuClose={() => {
|
||||
// Disable hover since the menu overlay makes the mouseout unreliable.
|
||||
setHover(false);
|
||||
setMenuOpen(false);
|
||||
}}
|
||||
sx={
|
||||
displayControls
|
||||
? {
|
||||
visibility: "visible",
|
||||
opacity: 1,
|
||||
transition: "opacity .2s ease-in",
|
||||
}
|
||||
: {
|
||||
visibility: "hidden",
|
||||
opacity: 0,
|
||||
transition: "opacity .4s ease-out, visibility 0s .4s",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -58,13 +58,17 @@ export const [_mutedAtom, mutedAtom] = bakedAtom(false, (get, set, value, baker)
|
||||
set(baker, value);
|
||||
if (player.current) player.current.muted = value;
|
||||
});
|
||||
export const [_, fullscreenAtom] = bakedAtom(false, (_, set, value, baker) => {
|
||||
set(baker, value);
|
||||
if (value) {
|
||||
document.body.requestFullscreen();
|
||||
} else {
|
||||
document.exitFullscreen();
|
||||
}
|
||||
export const [_, fullscreenAtom] = bakedAtom(false, async (_, set, value, baker) => {
|
||||
try {
|
||||
if (value) {
|
||||
await document.body.requestFullscreen();
|
||||
await screen.orientation.lock("landscape");
|
||||
} else {
|
||||
await document.exitFullscreen();
|
||||
screen.orientation.unlock();
|
||||
}
|
||||
set(baker, value);
|
||||
} catch {}
|
||||
});
|
||||
|
||||
export const useVideoController = () => {
|
||||
@ -94,14 +98,6 @@ export const useVideoController = () => {
|
||||
|
||||
const videoProps: BoxProps<"video"> = {
|
||||
ref: player,
|
||||
onClick: () => {
|
||||
if (!player.current) return;
|
||||
if (player.current.paused) {
|
||||
player.current.play();
|
||||
} else {
|
||||
player.current.pause();
|
||||
}
|
||||
},
|
||||
onDoubleClick: () => {
|
||||
if (document.fullscreenElement) {
|
||||
setFullscreen(false);
|
||||
@ -132,69 +128,77 @@ export const useVideoController = () => {
|
||||
return {
|
||||
playerRef: player,
|
||||
videoProps,
|
||||
onVideoClick: () => {
|
||||
if (!player.current) return;
|
||||
if (player.current.paused) {
|
||||
player.current.play();
|
||||
} else {
|
||||
player.current.pause();
|
||||
}
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const htmlTrackAtom = atom<HTMLTrackElement | null>(null);
|
||||
const suboctoAtom = atom<SubtitleOctopus | null>(null);
|
||||
export const [_subtitleAtom, subtitleAtom] = bakedAtom<Track | null, { track: Track, fonts: Font[] } | null>(
|
||||
null,
|
||||
(get, set, value, baked) => {
|
||||
const removeHtmlSubtitle = () => {
|
||||
const htmlTrack = get(htmlTrackAtom);
|
||||
if (htmlTrack) htmlTrack.remove();
|
||||
set(htmlTrackAtom, null);
|
||||
};
|
||||
const removeOctoSub = () => {
|
||||
const subocto = get(suboctoAtom);
|
||||
if (subocto) {
|
||||
subocto.freeTrack();
|
||||
subocto.dispose();
|
||||
}
|
||||
set(suboctoAtom, null);
|
||||
};
|
||||
|
||||
const player = get(playerAtom);
|
||||
if (!player?.current) return;
|
||||
|
||||
if (get(baked)?.id === value?.track.id) return;
|
||||
|
||||
set(baked, value?.track ?? null);
|
||||
if (!value) {
|
||||
removeHtmlSubtitle();
|
||||
removeOctoSub();
|
||||
} else if (value.track.codec === "vtt" || value.track.codec === "subrip") {
|
||||
removeOctoSub();
|
||||
if (player.current.textTracks.length > 0) player.current.textTracks[0].mode = "hidden";
|
||||
const track: HTMLTrackElement = get(htmlTrackAtom) ?? document.createElement("track");
|
||||
track.kind = "subtitles";
|
||||
track.label = value.track.displayName;
|
||||
if (value.track.language) track.srclang = value.track.language;
|
||||
track.src = value.track.link! + ".vtt";
|
||||
track.className = "subtitle_container";
|
||||
track.default = true;
|
||||
track.onload = () => {
|
||||
if (player.current) player.current.textTracks[0].mode = "showing";
|
||||
};
|
||||
if (!get(htmlTrackAtom)) player.current.appendChild(track);
|
||||
set(htmlTrackAtom, track);
|
||||
} else if (value.track.codec === "ass") {
|
||||
removeHtmlSubtitle();
|
||||
removeOctoSub();
|
||||
set(
|
||||
suboctoAtom,
|
||||
new SubtitleOctopus({
|
||||
video: player.current,
|
||||
subUrl: value.track.link!,
|
||||
workerUrl: "/_next/static/chunks/subtitles-octopus-worker.js",
|
||||
legacyWorkerUrl: "/_next/static/chunks/subtitles-octopus-worker-legacy.js",
|
||||
fonts: value.fonts?.map((x) => x.link),
|
||||
renderMode: "wasm-blend",
|
||||
}),
|
||||
);
|
||||
export const [_subtitleAtom, subtitleAtom] = bakedAtom<
|
||||
Track | null,
|
||||
{ track: Track; fonts: Font[] } | null
|
||||
>(null, (get, set, value, baked) => {
|
||||
const removeHtmlSubtitle = () => {
|
||||
const htmlTrack = get(htmlTrackAtom);
|
||||
if (htmlTrack) htmlTrack.remove();
|
||||
set(htmlTrackAtom, null);
|
||||
};
|
||||
const removeOctoSub = () => {
|
||||
const subocto = get(suboctoAtom);
|
||||
if (subocto) {
|
||||
subocto.freeTrack();
|
||||
subocto.dispose();
|
||||
}
|
||||
},
|
||||
);
|
||||
set(suboctoAtom, null);
|
||||
};
|
||||
|
||||
const player = get(playerAtom);
|
||||
if (!player?.current) return;
|
||||
|
||||
if (get(baked)?.id === value?.track.id) return;
|
||||
|
||||
set(baked, value?.track ?? null);
|
||||
if (!value) {
|
||||
removeHtmlSubtitle();
|
||||
removeOctoSub();
|
||||
} else if (value.track.codec === "vtt" || value.track.codec === "subrip") {
|
||||
removeOctoSub();
|
||||
if (player.current.textTracks.length > 0) player.current.textTracks[0].mode = "hidden";
|
||||
const track: HTMLTrackElement = get(htmlTrackAtom) ?? document.createElement("track");
|
||||
track.kind = "subtitles";
|
||||
track.label = value.track.displayName;
|
||||
if (value.track.language) track.srclang = value.track.language;
|
||||
track.src = value.track.link! + ".vtt";
|
||||
track.className = "subtitle_container";
|
||||
track.default = true;
|
||||
track.onload = () => {
|
||||
if (player.current) player.current.textTracks[0].mode = "showing";
|
||||
};
|
||||
if (!get(htmlTrackAtom)) player.current.appendChild(track);
|
||||
set(htmlTrackAtom, track);
|
||||
} else if (value.track.codec === "ass") {
|
||||
removeHtmlSubtitle();
|
||||
removeOctoSub();
|
||||
set(
|
||||
suboctoAtom,
|
||||
new SubtitleOctopus({
|
||||
video: player.current,
|
||||
subUrl: value.track.link!,
|
||||
workerUrl: "/_next/static/chunks/subtitles-octopus-worker.js",
|
||||
legacyWorkerUrl: "/_next/static/chunks/subtitles-octopus-worker-legacy.js",
|
||||
fonts: value.fonts?.map((x) => x.link),
|
||||
renderMode: "wasm-blend",
|
||||
}),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
export const useSubtitleController = (
|
||||
player: RefObject<HTMLVideoElement>,
|
||||
@ -209,6 +213,6 @@ export const useSubtitleController = (
|
||||
const newSub = subtitles?.find((x) => x.language === subtitle);
|
||||
useEffect(() => {
|
||||
if (newSub === undefined) return;
|
||||
selectSubtitle({track: newSub, fonts: fonts ?? []});
|
||||
selectSubtitle({ track: newSub, fonts: fonts ?? [] });
|
||||
}, [player.current?.src, newSub, fonts, selectSubtitle]);
|
||||
};
|
||||
|
@ -18,6 +18,34 @@
|
||||
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { useEffect } from "react";
|
||||
|
||||
export const makeTitle = (title?: string) => {
|
||||
return title ? `${title} - Kyoo` : "Kyoo";
|
||||
};
|
||||
|
||||
let preventHover: boolean = false;
|
||||
let hoverTimeout: NodeJS.Timeout;
|
||||
|
||||
export const useMobileHover = () => {
|
||||
useEffect(() => {
|
||||
const enableHover = () => {
|
||||
if (preventHover) return;
|
||||
document.body.classList.add("hoverEnabled");
|
||||
}
|
||||
|
||||
const disableHover = () => {
|
||||
if (hoverTimeout) clearTimeout(hoverTimeout);
|
||||
preventHover = true;
|
||||
hoverTimeout = setTimeout(() => preventHover = false, 500);
|
||||
document.body.classList.remove("hoverEnabled");
|
||||
}
|
||||
|
||||
document.addEventListener("touchstart", disableHover, true);
|
||||
document.addEventListener("mousemove", enableHover, true);
|
||||
return () => {
|
||||
document.removeEventListener("touchstart", disableHover);
|
||||
document.removeEventListener("mousemove", enableHover);
|
||||
};
|
||||
}, []);
|
||||
};
|
||||
|
Loading…
x
Reference in New Issue
Block a user