diff --git a/front/apps/mobile/app/watch/[slug].tsx b/front/apps/mobile/app/watch/[slug].tsx
index 213b397c..ca211966 100644
--- a/front/apps/mobile/app/watch/[slug].tsx
+++ b/front/apps/mobile/app/watch/[slug].tsx
@@ -21,4 +21,10 @@
import { Player } from "@kyoo/ui";
import { withRoute } from "../../utils";
-export default withRoute(Player);
+export default withRoute(Player, {
+ options: {
+ headerShown: false,
+ },
+ statusBar: { hidden: true },
+ fullscreen: true,
+});
diff --git a/front/apps/mobile/package.json b/front/apps/mobile/package.json
index 7c4edb93..d46a9112 100644
--- a/front/apps/mobile/package.json
+++ b/front/apps/mobile/package.json
@@ -15,11 +15,14 @@
"@tanstack/react-query": "^4.19.1",
"babel-plugin-transform-inline-environment-variables": "^0.4.4",
"expo": "^47.0.0",
+ "expo-av": "~13.0.2",
"expo-constants": "~14.0.2",
"expo-linear-gradient": "~12.0.1",
"expo-linking": "~3.2.3",
"expo-localization": "~14.0.0",
+ "expo-navigation-bar": "~2.0.1",
"expo-router": "^0.0.36",
+ "expo-screen-orientation": "~5.0.1",
"expo-status-bar": "~1.4.2",
"i18next": "^22.0.6",
"intl-pluralrules": "^1.3.1",
diff --git a/front/apps/mobile/utils.tsx b/front/apps/mobile/utils.tsx
index a13560c1..408e0067 100644
--- a/front/apps/mobile/utils.tsx
+++ b/front/apps/mobile/utils.tsx
@@ -19,19 +19,37 @@
*/
import { Stack } from "expo-router";
-import { ComponentType } from "react";
+import { ComponentType, useEffect } from "react";
import { StatusBar, StatusBarProps } from "react-native";
+import * as ScreenOrientation from "expo-screen-orientation";
+import * as NavigationBar from "expo-navigation-bar";
+
+const FullscreenProvider = () => {
+ useEffect(() => {
+ ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.LANDSCAPE);
+ NavigationBar.setVisibilityAsync("hidden");
+ return () => {
+ ScreenOrientation.unlockAsync();
+ NavigationBar.setVisibilityAsync("visible");
+ };
+ }, []);
+ return null;
+};
export const withRoute = (
Component: ComponentType,
- options?: Parameters[0] & { statusBar?: StatusBarProps },
+ options?: Parameters[0] & {
+ statusBar?: StatusBarProps;
+ fullscreen?: boolean;
+ },
) => {
- const { statusBar, ...routeOptions } = options ?? {};
+ const { statusBar, fullscreen, ...routeOptions } = options ?? {};
const WithUseRoute = ({ route, ...props }: Props & { route: any }) => {
return (
<>
{routeOptions && }
{statusBar && }
+ {fullscreen && }
>
);
diff --git a/front/apps/web/next.config.js b/front/apps/web/next.config.js
index 24d9b6f0..e60bb5c8 100755
--- a/front/apps/web/next.config.js
+++ b/front/apps/web/next.config.js
@@ -23,7 +23,7 @@ const CopyPlugin = require("copy-webpack-plugin");
const DefinePlugin = require("webpack").DefinePlugin;
const withFont = require("next-fonts");
-const suboctopus = path.dirname(require.resolve("@jellyfin/libass-wasm"));
+const suboctopus = path.dirname(require.resolve("libass-wasm"));
/**
* @type {import("next").NextConfig}
@@ -115,6 +115,7 @@ const nextConfig = {
"@expo/html-elements",
"expo-font",
"expo-asset",
+ "expo-av",
"expo-modules-core",
"expo-linear-gradient",
],
diff --git a/front/apps/web/package.json b/front/apps/web/package.json
index 9cc36393..ae1ecfbb 100644
--- a/front/apps/web/package.json
+++ b/front/apps/web/package.json
@@ -23,6 +23,7 @@
"@tanstack/react-query": "^4.19.1",
"clsx": "^1.2.1",
"csstype": "^3.1.1",
+ "expo-av": "^13.0.2",
"expo-linear-gradient": "^12.0.1",
"hls.js": "^1.2.8",
"i18next": "^22.0.6",
diff --git a/front/apps/web/src/pages/_document.tsx b/front/apps/web/src/pages/_document.tsx
index f721de59..68ef3fc9 100644
--- a/front/apps/web/src/pages/_document.tsx
+++ b/front/apps/web/src/pages/_document.tsx
@@ -43,6 +43,7 @@ html, body, #__next {
flex-grow: 1;
display: flex;
flex: 1;
+ overflow: hidden;
}
html {
scroll-behavior: smooth;
diff --git a/front/apps/web/src/utils/jotai-utils.tsx b/front/packages/ui/src/jotai-utils.tsx
similarity index 100%
rename from front/apps/web/src/utils/jotai-utils.tsx
rename to front/packages/ui/src/jotai-utils.tsx
diff --git a/front/packages/ui/src/player/components/hover.tsx b/front/packages/ui/src/player/components/hover.tsx
index f4d999ea..8002ed23 100644
--- a/front/packages/ui/src/player/components/hover.tsx
+++ b/front/packages/ui/src/player/components/hover.tsx
@@ -18,144 +18,178 @@
* along with Kyoo. If not, see .
*/
-import { ArrowBack } from "@mui/icons-material";
import {
- Box,
- BoxProps,
CircularProgress,
+ ContrastArea,
+ H1,
+ H2,
IconButton,
+ Link,
+ Poster,
Skeleton,
- Tooltip,
- Typography,
-} from "@mui/material";
-import useTranslation from "next-translate/useTranslation";
-import NextLink from "next/link";
-import { Poster } from "~/components/poster";
-import { WatchItem } from "~/models/resources/watch-item";
-import { loadAtom } from "../state";
-import { episodeDisplayNumber } from "~/components/episode";
+ tooltip,
+ ts,
+} from "@kyoo/primitives";
+import { Chapter, Font, Track } from "@kyoo/models";
+import { useAtomValue } from "jotai";
+import { View, ViewProps } from "react-native";
+import ArrowBack from "@material-symbols/svg-400/rounded/arrow_back-fill.svg";
import { LeftButtons } from "./left-buttons";
import { RightButtons } from "./right-buttons";
import { ProgressBar } from "./progress-bar";
-import { useAtomValue } from "jotai";
+import { loadAtom } from "../state";
+import { useTranslation } from "react-i18next";
+import { percent, rem, useYoshiki } from "yoshiki/native";
export const Hover = ({
- data,
+ name,
+ showName,
+ href,
+ poster,
+ chapters,
+ subtitles,
+ fonts,
+ previousSlug,
+ nextSlug,
onMenuOpen,
onMenuClose,
- ...props
-}: { data?: WatchItem; onMenuOpen: () => void; onMenuClose: () => void } & BoxProps) => {
- const name = data
- ? data.isMovie
- ? data.name
- : `${episodeDisplayNumber(data, "")} ${data.name}`
- : undefined;
-
+}: {
+ name?: string;
+ showName?: string;
+ href?: string;
+ poster?: string | null;
+ chapters?: Chapter[];
+ subtitles?: Track[];
+ fonts?: Font[];
+ previousSlug?: string | null;
+ nextSlug?: string | null;
+ onMenuOpen: () => void;
+ onMenuClose: () => void;
+}) => {
return (
-
-
-
-
-
-
- {name ?? }
-
-
-
-
-
-
-
-
-
-
-
+
+ {({ css }) => (
+ <>
+
+
+
+
+ {name ?? }
+
+
+
+
+
+
+
+ >
+ )}
+
);
};
-export const Back = ({ name, href }: { name?: string; href: string }) => {
- const { t } = useTranslation("player");
+export const Back = ({ name, href }: { name?: string; href?: string }) => {
+ const { css } = useYoshiki();
+ const { t } = useTranslation();
return (
-
-
-
-
-
-
-
-
-
- {name ? name : }
-
-
+
+
+ {name ? (
+
+ {name}
+
+ ) : (
+
+ )}
+
+
);
};
const VideoPoster = ({ poster }: { poster?: string | null }) => {
+ const { css } = useYoshiki();
+
return (
-
-
-
+
+
);
};
export const LoadingIndicator = () => {
const isLoading = useAtomValue(loadAtom);
+ const { css } = useYoshiki();
+
if (!isLoading) return null;
+
return (
-
-
-
+
+
);
};
diff --git a/front/packages/ui/src/player/components/left-buttons.tsx b/front/packages/ui/src/player/components/left-buttons.tsx
index 10ada8d3..a687826c 100644
--- a/front/packages/ui/src/player/components/left-buttons.tsx
+++ b/front/packages/ui/src/player/components/left-buttons.tsx
@@ -18,109 +18,95 @@
* along with Kyoo. If not, see .
*/
-import { Box, IconButton, Slider, Tooltip, Typography } from "@mui/material";
+import { IconButton, Link, P, tooltip, ts } from "@kyoo/primitives";
import { useAtom, useAtomValue } from "jotai";
-import useTranslation from "next-translate/useTranslation";
-import { useRouter } from "next/router";
+import { useTranslation } from "react-i18next";
+import { View } from "react-native";
+import SkipPrevious from "@material-symbols/svg-400/rounded/skip_previous-fill.svg";
+import SkipNext from "@material-symbols/svg-400/rounded/skip_next-fill.svg";
+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 VolumeUp from "@material-symbols/svg-400/rounded/volume_up-fill.svg";
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 { useYoshiki } from "yoshiki/native";
export const LeftButtons = ({
previousSlug,
nextSlug,
}: {
- previousSlug?: string;
- nextSlug?: string;
+ previousSlug?: string | null;
+ nextSlug?: string | null;
}) => {
- const { t } = useTranslation("player");
- const router = useRouter();
+ const { css } = useYoshiki();
+ const { t } = useTranslation();
const [isPlaying, setPlay] = useAtom(playAtom);
+ const spacing = css({ marginHorizontal: ts(1) });
+
return (
- *": {
- mx: { xs: "2px !important", sm: "8px !important" },
- p: { xs: "4px !important", sm: "8px !important" },
- },
- }}
- >
+
{previousSlug && (
-
-
-
-
-
-
-
- )}
-
setPlay(!isPlaying)}
- aria-label={isPlaying ? t("pause") : t("play")}
- sx={{ color: "white" }}
- >
- {isPlaying ? : }
-
-
+ icon={SkipPrevious}
+ as={Link}
+ href={previousSlug}
+ {...tooltip(t("player.previous"))}
+ {...spacing}
+ />
+ )}
+ setPlay(!isPlaying)}
+ {...tooltip(isPlaying ? t("player.pause") : t("player.play"))}
+ {...spacing}
+ />
{nextSlug && (
-
-
-
-
-
-
-
+
)}
-
+
);
};
const VolumeSlider = () => {
const [volume, setVolume] = useAtom(volumeAtom);
const [isMuted, setMuted] = useAtom(mutedAtom);
- const { t } = useTranslation("player");
+ const { css } = useYoshiki();
+ const { t } = useTranslation();
+ return null;
return (
-
-
- setMuted(!isMuted)}
- aria-label={t("mute")}
- sx={{ color: "white" }}
- >
- {isMuted || volume == 0 ? (
-
- ) : volume < 25 ? (
-
- ) : volume < 65 ? (
-
- ) : (
-
- )}
-
-
- setMuted(!isMuted)}
+ {...tooltip(t("mute"))}
+ />
+ {
aria-label={t("volume")}
sx={{ alignSelf: "center" }}
/>
-
-
+
+
);
};
const ProgressText = () => {
const progress = useAtomValue(progressAtom);
const duration = useAtomValue(durationAtom);
+ const { css } = useYoshiki();
return (
-
+
{toTimerString(progress, duration)} : {toTimerString(duration)}
-
+
);
};
diff --git a/front/packages/ui/src/player/components/progress-bar.tsx b/front/packages/ui/src/player/components/progress-bar.tsx
index a3866661..b5bb7c24 100644
--- a/front/packages/ui/src/player/components/progress-bar.tsx
+++ b/front/packages/ui/src/player/components/progress-bar.tsx
@@ -18,20 +18,24 @@
* along with Kyoo. If not, see .
*/
-import { Box } from "@mui/material";
+import { Chapter } from "@kyoo/models";
+import { ts } from "@kyoo/primitives";
import { useAtom, useAtomValue } from "jotai";
import { useEffect, useRef, useState } from "react";
-import { Chapter } from "~/models/resources/watch-item";
+import { NativeTouchEvent, Pressable, Touchable, View } from "react-native";
+import { useYoshiki, px, percent } from "yoshiki/native";
import { bufferedAtom, durationAtom, progressAtom } from "../state";
export const ProgressBar = ({ chapters }: { chapters?: Chapter[] }) => {
- const ref = useRef(null);
+ return null;
+ const { css } = useYoshiki();
+ const ref = useRef(null);
const [isSeeking, setSeek] = useState(false);
const [progress, setProgress] = useAtom(progressAtom);
const buffered = useAtomValue(bufferedAtom);
const duration = useAtomValue(durationAtom);
- const updateProgress = (event: MouseEvent | TouchEvent, skipSeek?: boolean) => {
+ const updateProgress = (event: NativeTouchEvent, skipSeek?: boolean) => {
if (!(isSeeking || skipSeek) || !ref?.current) return;
const pageX: number = "pageX" in event ? event.pageX : event.changedTouches[0].pageX;
const value: number = (pageX - ref.current.offsetLeft) / ref.current.clientWidth;
@@ -58,26 +62,25 @@ export const ProgressBar = ({ chapters }: { chapters?: Chapter[] }) => {
});
return (
- {
+ {
// prevent drag and drop of the UI.
event.preventDefault();
setSeek(true);
}}
- onTouchStart={() => setSeek(true)}
- onClick={(event) => updateProgress(event.nativeEvent, true)}
- sx={{
- width: "100%",
- py: 1,
+ onPress={(event) => updateProgress(event.nativeEvent, true)}
+ {...css({
+ width: percent(100),
+ paddingVertical: ts(1),
cursor: "pointer",
WebkitTapHighlightColor: "transparent",
"body.hoverEnabled &:hover": {
".thumb": { opacity: 1 },
".bar": { transform: "unset" },
},
- }}
+ })}
>
- {
position: "relative",
}}
>
- {
background: "rgba(255, 255, 255, 0.5)",
}}
/>
- {
background: (theme) => theme.palette.primary.main,
}}
/>
- {
/>
{chapters?.map((x) => (
- theme.palette.primary.dark,
- }}
+ bg: (theme) => theme.accent,
+ })}
/>
))}
-
-
+
+
);
};
diff --git a/front/packages/ui/src/player/components/right-buttons.tsx b/front/packages/ui/src/player/components/right-buttons.tsx
index f2c93908..6305bde8 100644
--- a/front/packages/ui/src/player/components/right-buttons.tsx
+++ b/front/packages/ui/src/player/components/right-buttons.tsx
@@ -18,14 +18,15 @@
* along with Kyoo. If not, see .
*/
-import { ClosedCaption, Fullscreen, FullscreenExit } from "@mui/icons-material";
-import { Box, IconButton, ListItemText, Menu, MenuItem, Tooltip } from "@mui/material";
+import { Font, Track } from "@kyoo/models";
+import { IconButton, tooltip } from "@kyoo/primitives";
import { useAtom } from "jotai";
-import useTranslation from "next-translate/useTranslation";
-import { useRouter } from "next/router";
+import { useRouter } from "solito/router";
import { useState } from "react";
-import { Font, Track } from "~/models/resources/watch-item";
-import { Link } from "~/utils/link";
+import { View } from "react-native";
+import { useTranslation } from "react-i18next";
+import Fullscreen from "@material-symbols/svg-400/rounded/fullscreen-fill.svg";
+import FullscreenExit from "@material-symbols/svg-400/rounded/fullscreen_exit-fill.svg";
import { fullscreenAtom, subtitleAtom } from "../state";
export const RightButtons = ({
@@ -39,59 +40,56 @@ export const RightButtons = ({
onMenuOpen: () => void;
onMenuClose: () => void;
}) => {
- const { t } = useTranslation("player");
+ const { t } = useTranslation();
const [subtitleAnchor, setSubtitleAnchor] = useState(null);
const [isFullscreen, setFullscreen] = useAtom(fullscreenAtom);
return (
- *": {
- m: { xs: "4px !important", sm: "8px !important" },
- p: { xs: "4px !important", sm: "8px !important" },
- },
- }}
+ *": {
+ // m: { xs: "4px !important", sm: "8px !important" },
+ // p: { xs: "4px !important", sm: "8px !important" },
+ // },
+ // }}
>
- {subtitles && (
-
- {
- setSubtitleAnchor(event.currentTarget);
- onMenuOpen();
- }}
- sx={{ color: "white" }}
- >
-
-
-
- )}
-
- setFullscreen(!isFullscreen)}
- aria-label={t("fullscreen")}
- sx={{ color: "white" }}
- >
- {isFullscreen ? : }
-
-
- {subtitleAnchor && (
- {
- setSubtitleAnchor(null);
- onMenuClose();
- }}
- />
- )}
-
+ {/* {subtitles && ( */}
+ {/* */}
+ {/* { */}
+ {/* setSubtitleAnchor(event.currentTarget); */}
+ {/* onMenuOpen(); */}
+ {/* }} */}
+ {/* sx={{ color: "white" }} */}
+ {/* > */}
+ {/* */}
+ {/* */}
+ {/* */}
+ {/* )} */}
+ setFullscreen(!isFullscreen)}
+ {...tooltip(t("fullscreen"))}
+ sx={{ color: "white" }}
+ />
+ {/* {subtitleAnchor && ( */}
+ {/* { */}
+ {/* setSubtitleAnchor(null); */}
+ {/* onMenuClose(); */}
+ {/* }} */}
+ {/* /> */}
+ {/* )} */}
+
);
};
diff --git a/front/packages/ui/src/player/index.tsx b/front/packages/ui/src/player/index.tsx
new file mode 100644
index 00000000..614c9f3d
--- /dev/null
+++ b/front/packages/ui/src/player/index.tsx
@@ -0,0 +1,209 @@
+/*
+ * Kyoo - A portable and vast media library solution.
+ * Copyright (c) Kyoo.
+ *
+ * See AUTHORS.md and LICENSE file in the project root for full license information.
+ *
+ * Kyoo is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * any later version.
+ *
+ * Kyoo is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Kyoo. If not, see .
+ */
+
+import { QueryIdentifier, QueryPage, WatchItem, WatchItemP, useFetch } from "@kyoo/models";
+import { Head } from "@kyoo/primitives";
+import { useState, useEffect, PointerEvent as ReactPointerEvent, ComponentProps } from "react";
+import { StyleSheet, View } from "react-native";
+import { useAtom, useAtomValue, useSetAtom } from "jotai";
+import { useRouter } from "solito/router";
+import { Video } from "expo-av";
+import { percent, useYoshiki } from "yoshiki/native";
+import { Hover, LoadingIndicator } from "./components/hover";
+import { fullscreenAtom, playAtom, useSubtitleController, useVideoController } from "./state";
+import { episodeDisplayNumber } from "../details/episode";
+import { useVideoKeyboard } from "./keyboard";
+import { MediaSessionManager } from "./media-session";
+import { ErrorView } from "../fetch";
+
+// 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;
+
+const query = (slug: string): QueryIdentifier => ({
+ path: ["watch", slug],
+ parser: WatchItemP,
+});
+
+const mapData = (
+ data: WatchItem | undefined,
+ previousSlug: string,
+ nextSlug?: string,
+): Partial> => {
+ if (!data) return {};
+ return {
+ name: data.isMovie ? data.name : `${episodeDisplayNumber(data, "")} ${data.name}`,
+ showName: data.isMovie ? data.name : data.showTitle,
+ href: data ? (data.isMovie ? `/movie/${data.slug}` : `/show/${data.showSlug}`) : "#",
+ poster: data.poster,
+ subtitles: data.subtitles,
+ chapters: data.chapters,
+ fonts: data.fonts,
+ previousSlug,
+ nextSlug,
+ };
+};
+
+export const Player: QueryPage<{ slug: string }> = ({ slug }) => {
+ const { css } = useYoshiki();
+
+ const { data, error } = useFetch(query(slug));
+ const previous =
+ data && !data.isMovie && data.previousEpisode
+ ? `/watch/${data.previousEpisode.slug}`
+ : undefined;
+ const next =
+ data && !data.isMovie && data.nextEpisode ? `/watch/${data.nextEpisode.slug}` : undefined;
+
+ // const { playerRef, videoProps, onVideoClick } = useVideoController(data?.link);
+ // useSubtitleController(playerRef, data?.subtitles, data?.fonts);
+ // useVideoKeyboard(data?.subtitles, data?.fonts, previous, next);
+
+ const router = useRouter();
+ 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 mouseHasMoved = () => {
+ // setMouseMoved(true);
+ // if (mouseCallback) clearTimeout(mouseCallback);
+ // mouseCallback = setTimeout(() => {
+ // setMouseMoved(false);
+ // }, 2500);
+ // };
+ // useEffect(() => {
+ // const handler = (e: PointerEvent) => {
+ // if (e.pointerType !== "mouse") return;
+ // mouseHasMoved();
+ // };
+
+ // document.addEventListener("pointermove", handler);
+ // return () => document.removeEventListener("pointermove", handler);
+ // });
+
+ // useEffect(() => {
+ // setPlay(true);
+ // }, [slug, setPlay]);
+ // useEffect(() => {
+ // if (!/Mobi/i.test(window.navigator.userAgent)) return;
+ // setFullscreen(true);
+ // return () => setFullscreen(false);
+ // }, [setFullscreen]);
+
+ if (error) return ;
+
+ return (
+ <>
+ {data && (
+
+ )}
+
+ {/* */}
+ setMouseMoved(false)}
+ {...css({
+ flexGrow: 1,
+ // @ts-ignore
+ // cursor: displayControls ? "unset" : "none",
+ bg: "black",
+ })}
+ >
+
+ >
+ );
+};
+
+// Player.getFetchUrls = ({ slug }) => [query(slug)];
diff --git a/front/packages/ui/src/player/keyboard.tsx b/front/packages/ui/src/player/keyboard.tsx
index f26bb526..705dc128 100644
--- a/front/packages/ui/src/player/keyboard.tsx
+++ b/front/packages/ui/src/player/keyboard.tsx
@@ -18,10 +18,10 @@
* along with Kyoo. If not, see .
*/
+import { Font, Track } from "@kyoo/models";
import { atom, useSetAtom } from "jotai";
-import { useRouter } from "next/router";
+import { useRouter } from "solito/router";
import { useEffect } from "react";
-import { Font, Track } from "~/models/resources/watch-item";
import {
durationAtom,
fullscreenAtom,
diff --git a/front/packages/ui/src/player/media-session.tsx b/front/packages/ui/src/player/media-session.tsx
index 5f1bc02d..ee0d18fc 100644
--- a/front/packages/ui/src/player/media-session.tsx
+++ b/front/packages/ui/src/player/media-session.tsx
@@ -19,7 +19,7 @@
*/
import { useAtom, useAtomValue, useSetAtom } from "jotai";
-import { useRouter } from "next/router";
+import { useRouter } from "solito/router";
import { useEffect } from "react";
import { reducerAtom } from "./keyboard";
import { durationAtom, playAtom, progressAtom } from "./state";
diff --git a/front/packages/ui/src/player/player.tsx b/front/packages/ui/src/player/player.tsx
deleted file mode 100644
index cbfbc473..00000000
--- a/front/packages/ui/src/player/player.tsx
+++ /dev/null
@@ -1,201 +0,0 @@
-/*
- * Kyoo - A portable and vast media library solution.
- * Copyright (c) Kyoo.
- *
- * See AUTHORS.md and LICENSE file in the project root for full license information.
- *
- * Kyoo is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * any later version.
- *
- * Kyoo is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with Kyoo. If not, see .
- */
-
-import { QueryIdentifier, QueryPage } from "~/utils/query";
-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, PointerEvent as ReactPointerEvent } from "react";
-import { Box } from "@mui/material";
-import { useAtom, useAtomValue, useSetAtom } from "jotai";
-import { Hover, LoadingIndicator } from "./components/hover";
-import { fullscreenAtom, playAtom, useSubtitleController, useVideoController } from "./state";
-import { useRouter } from "next/router";
-import Head from "next/head";
-import { makeTitle } from "~/utils/utils";
-import { episodeDisplayNumber } from "~/components/episode";
-import { useVideoKeyboard } from "./keyboard";
-import { MediaSessionManager } from "./media-session";
-
-// 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;
-
-const query = (slug: string): QueryIdentifier => ({
- path: ["watch", slug],
- parser: WatchItemP,
-});
-
-const Player: QueryPage<{ slug: string }> = ({ slug }) => {
- const { data, error } = useFetch(query(slug));
- const { playerRef, videoProps, onVideoClick } = useVideoController(data?.link);
- const setFullscreen = useSetAtom(fullscreenAtom);
- const router = useRouter();
-
- 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 previous =
- data && !data.isMovie && data.previousEpisode
- ? `/watch/${data.previousEpisode.slug}`
- : undefined;
- const next =
- data && !data.isMovie && data.nextEpisode ? `/watch/${data.nextEpisode.slug}` : undefined;
-
- const mouseHasMoved = () => {
- setMouseMoved(true);
- if (mouseCallback) clearTimeout(mouseCallback);
- mouseCallback = setTimeout(() => {
- setMouseMoved(false);
- }, 2500);
- };
-
- useEffect(() => {
- const handler = (e: PointerEvent) => {
- if (e.pointerType !== "mouse") return;
- mouseHasMoved();
- };
-
- document.addEventListener("pointermove", handler);
- return () => document.removeEventListener("pointermove", handler);
- });
-
- useEffect(() => {
- setPlay(true);
- }, [slug, setPlay]);
-
- useEffect(() => {
- if (!/Mobi/i.test(window.navigator.userAgent)) return;
- setFullscreen(true);
- return () => setFullscreen(false);
- }, [setFullscreen]);
-
- useSubtitleController(playerRef, data?.subtitles, data?.fonts);
- useVideoKeyboard(data?.subtitles, data?.fonts, previous, next);
-
- if (error) return ;
-
- return (
- <>
- {data && (
-
-
- {makeTitle(
- data.isMovie
- ? data.name
- : data.showTitle +
- " " +
- episodeDisplayNumber({
- seasonNumber: data.seasonNumber,
- episodeNumber: data.episodeNumber,
- absoluteNumber: data.absoluteNumber,
- }),
- )}
-
-
-
- )}
-
-
- setMouseMoved(false)}
- sx={{ cursor: displayControls ? "unset" : "none" }}
- >
- ) => {
- if (e.pointerType === "mouse") {
- onVideoClick();
- } else if (mouseMoved) {
- setMouseMoved(false);
- } else {
- mouseHasMoved();
- }
- }}
- onEnded={() => {
- if (!data) return;
- if (data.isMovie) router.push(`/movie/${data.slug}`);
- else
- router.push(
- data.nextEpisode ? `/watch/${data.nextEpisode.slug}` : `/show/${data.showSlug}`,
- );
- }}
- sx={{
- position: "fixed",
- top: 0,
- bottom: 0,
- left: 0,
- right: 0,
- width: "100%",
- height: "100%",
- objectFit: "contain",
- background: "black",
- }}
- />
-
- ) => {
- 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",
- }
- }
- />
-
- >
- );
-};
-
-Player.getFetchUrls = ({ slug }) => [query(slug)];
-
-export default withRoute(Player);
diff --git a/front/packages/ui/src/player/state.tsx b/front/packages/ui/src/player/state.tsx
index 2d0a0b43..6262e7f0 100644
--- a/front/packages/ui/src/player/state.tsx
+++ b/front/packages/ui/src/player/state.tsx
@@ -18,15 +18,14 @@
* along with Kyoo. If not, see .
*/
-import { BoxProps } from "@mui/material";
+import { Font, Track } from "@kyoo/models";
import { atom, useAtom, useSetAtom } from "jotai";
-import { useRouter } from "next/router";
import { RefObject, useEffect, useRef } from "react";
-import { Font, Track } from "~/models/resources/watch-item";
-import { bakedAtom } from "~/utils/jotai-utils";
-// @ts-ignore
-import SubtitleOctopus from "@jellyfin/libass-wasm/dist/js/subtitles-octopus";
+import { createParam } from "solito";
+import { ResizeMode, VideoProps } from "expo-av";
+import SubtitleOctopus from "libass-wasm";
import Hls from "hls.js";
+import { bakedAtom } from "../jotai-utils";
enum PlayMode {
Direct,
@@ -104,69 +103,70 @@ export const useVideoController = (links?: { direct: string; transmux: string })
setPlayer(player);
- useEffect(() => {
- if (!player.current) return;
- setPlay(!player.current.paused);
- }, [setPlay]);
+ // useEffect(() => {
+ // if (!player.current) return;
+ // setPlay(!player.current.paused);
+ // }, [setPlay]);
- useEffect(() => {
- setPlayMode(PlayMode.Direct);
- }, [links, setPlayMode]);
+ // useEffect(() => {
+ // setPlayMode(PlayMode.Direct);
+ // }, [links, setPlayMode]);
- useEffect(() => {
- const src = playMode === PlayMode.Direct ? links?.direct : links?.transmux;
+ // useEffect(() => {
+ // const src = playMode === PlayMode.Direct ? links?.direct : links?.transmux;
- if (!player?.current || !src) return;
- if (
- playMode == PlayMode.Direct ||
- player.current.canPlayType("application/vnd.apple.mpegurl")
- ) {
- player.current.src = src;
- } else {
- if (hls === null) hls = new Hls();
- hls.loadSource(src);
- hls.attachMedia(player.current);
- hls.on(Hls.Events.MANIFEST_LOADED, async () => {
- try {
- await player.current?.play();
- } catch {}
- });
- }
- }, [playMode, links, player]);
+ // if (!player?.current || !src) return;
+ // if (
+ // playMode == PlayMode.Direct ||
+ // player.current.canPlayType("application/vnd.apple.mpegurl")
+ // ) {
+ // player.current.src = src;
+ // } else {
+ // if (hls === null) hls = new Hls();
+ // hls.loadSource(src);
+ // hls.attachMedia(player.current);
+ // hls.on(Hls.Events.MANIFEST_LOADED, async () => {
+ // try {
+ // await player.current?.play();
+ // } catch {}
+ // });
+ // }
+ // }, [playMode, links, player]);
- useEffect(() => {
- if (!player?.current?.duration) return;
- setDuration(player.current.duration);
- }, [player, setDuration]);
+ // useEffect(() => {
+ // if (!player?.current?.duration) return;
+ // setDuration(player.current.duration);
+ // }, [player, setDuration]);
- const videoProps: BoxProps<"video"> = {
- ref: player,
- onDoubleClick: () => {
- setFullscreen(!document.fullscreenElement);
- },
- onPlay: () => setPlay(true),
- onPause: () => setPlay(false),
- onWaiting: () => setLoad(true),
- onCanPlay: () => setLoad(false),
+ const videoProps: VideoProps = {
+ // ref: player,
+ // shouldPlay: isPlaying,
+ // onDoubleClick: () => {
+ // setFullscreen(!document.fullscreenElement);
+ // },
+ // onPlay: () => setPlay(true),
+ // onPause: () => setPlay(false),
+ // onWaiting: () => setLoad(true),
+ // onCanPlay: () => setLoad(false),
onError: () => {
if (player?.current?.error?.code === MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED)
setPlayMode(PlayMode.Transmux);
},
- onTimeUpdate: () => setProgress(player?.current?.currentTime ?? 0),
- onDurationChange: () => setDuration(player?.current?.duration ?? 0),
- onProgress: () =>
- setBuffered(
- player?.current?.buffered.length
- ? player.current.buffered.end(player.current.buffered.length - 1)
- : 0,
- ),
- onVolumeChange: () => {
- if (!player.current) return;
- setVolume(player.current.volume * 100);
- setMuted(player?.current.muted);
- },
- autoPlay: true,
- controls: false,
+ // onTimeUpdate: () => setProgress(player?.current?.currentTime ?? 0),
+ // onDurationChange: () => setDuration(player?.current?.duration ?? 0),
+ // onProgress: () =>
+ // setBuffered(
+ // player?.current?.buffered.length
+ // ? player.current.buffered.end(player.current.buffered.length - 1)
+ // : 0,
+ // ),
+ // onVolumeChange: () => {
+ // if (!player.current) return;
+ // setVolume(player.current.volume * 100);
+ // setMuted(player?.current.muted);
+ // },
+ resizeMode: ResizeMode.CONTAIN,
+ useNativeControls: false,
};
return {
playerRef: player,
@@ -239,14 +239,14 @@ export const [_subtitleAtom, subtitleAtom] = bakedAtom<
}
});
+const { useParam } = createParam<{ subtitle: string }>();
+
export const useSubtitleController = (
player: RefObject,
subtitles?: Track[],
fonts?: Font[],
) => {
- const {
- query: { subtitle },
- } = useRouter();
+ const [subtitle] = useParam("subtitle");
const selectSubtitle = useSetAtom(subtitleAtom);
const newSub = subtitles?.find((x) => x.language === subtitle);
diff --git a/front/yarn.lock b/front/yarn.lock
index 172fa1d3..afbed535 100644
--- a/front/yarn.lock
+++ b/front/yarn.lock
@@ -6592,6 +6592,15 @@ __metadata:
languageName: node
linkType: hard
+"expo-av@npm:^13.0.2, expo-av@npm:~13.0.2":
+ version: 13.0.2
+ resolution: "expo-av@npm:13.0.2"
+ peerDependencies:
+ expo: "*"
+ checksum: ed929b4dce9ea2d70997fe33a2c5502850d58fed23c30548f99b7256d174d129eb61b193351e4948965f607c264fd229a4eb6cc3527a1a3d3f644593115f12fe
+ languageName: node
+ linkType: hard
+
"expo-constants@npm:~14.0.0, expo-constants@npm:~14.0.2":
version: 14.0.2
resolution: "expo-constants@npm:14.0.2"
@@ -6702,6 +6711,18 @@ __metadata:
languageName: node
linkType: hard
+"expo-navigation-bar@npm:~2.0.1":
+ version: 2.0.1
+ resolution: "expo-navigation-bar@npm:2.0.1"
+ dependencies:
+ "@react-native/normalize-color": ^2.0.0
+ debug: ^4.3.2
+ peerDependencies:
+ expo: "*"
+ checksum: 147daf412dba4df90b47d7b9dfbf323e5c2c7a08c1f2fba69a8d2b56cd98310b74316e8ae0fc28ea378f997a5c188001a624f2939fcd471c4df6c3fe051b7430
+ languageName: node
+ linkType: hard
+
"expo-router@npm:^0.0.36":
version: 0.0.36
resolution: "expo-router@npm:0.0.36"
@@ -6735,6 +6756,15 @@ __metadata:
languageName: node
linkType: hard
+"expo-screen-orientation@npm:~5.0.1":
+ version: 5.0.1
+ resolution: "expo-screen-orientation@npm:5.0.1"
+ peerDependencies:
+ expo: "*"
+ checksum: 7ede30533a8c492f82b58c3b8be110b6373ffcc2cbe273299d9f15d9aa943d678d8aaffb3d2565780b45d1d5a2a1ddea54d813fc84c06e30e3cfd59abbd8e30e
+ languageName: node
+ linkType: hard
+
"expo-splash-screen@npm:*":
version: 0.17.5
resolution: "expo-splash-screen@npm:0.17.5"
@@ -9962,11 +9992,14 @@ __metadata:
"@types/react-native": ~0.70.6
babel-plugin-transform-inline-environment-variables: ^0.4.4
expo: ^47.0.0
+ expo-av: ~13.0.2
expo-constants: ~14.0.2
expo-linear-gradient: ~12.0.1
expo-linking: ~3.2.3
expo-localization: ~14.0.0
+ expo-navigation-bar: ~2.0.1
expo-router: ^0.0.36
+ expo-screen-orientation: ~5.0.1
expo-status-bar: ~1.4.2
i18next: ^22.0.6
intl-pluralrules: ^1.3.1
@@ -13625,6 +13658,7 @@ __metadata:
csstype: ^3.1.1
eslint: ^8.28.0
eslint-config-next: 13.0.5
+ expo-av: ^13.0.2
expo-linear-gradient: ^12.0.1
hls.js: ^1.2.8
i18next: ^22.0.6