mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-05-30 19:54:16 -04:00
Add load and error state
This commit is contained in:
parent
c79a991024
commit
2c16fdad19
@ -119,7 +119,7 @@ const WatchMovieP = z.preprocess(
|
|||||||
/**
|
/**
|
||||||
* The title of this episode.
|
* The title of this episode.
|
||||||
*/
|
*/
|
||||||
name: z.string(),
|
name: z.string().nullable(),
|
||||||
/**
|
/**
|
||||||
* The sumarry of this episode.
|
* The sumarry of this episode.
|
||||||
*/
|
*/
|
||||||
@ -179,15 +179,15 @@ const WatchEpisodeP = WatchMovieP.and(
|
|||||||
/**
|
/**
|
||||||
* The season in witch this episode is in.
|
* The season in witch this episode is in.
|
||||||
*/
|
*/
|
||||||
seasonNumber: z.number(),
|
seasonNumber: z.number().nullable(),
|
||||||
/**
|
/**
|
||||||
* The number of this episode is it's season.
|
* The number of this episode is it's season.
|
||||||
*/
|
*/
|
||||||
episodeNumber: z.number(),
|
episodeNumber: z.number().nullable(),
|
||||||
/**
|
/**
|
||||||
* The absolute number of this episode. It's an episode number that is not reset to 1 after a new season.
|
* The absolute number of this episode. It's an episode number that is not reset to 1 after a new season.
|
||||||
*/
|
*/
|
||||||
absoluteNumber: z.number(),
|
absoluteNumber: z.number().nullable(),
|
||||||
/**
|
/**
|
||||||
* The episode that come before this one if you follow usual watch orders. If this is the first
|
* The episode that come before this one if you follow usual watch orders. If this is the first
|
||||||
* episode or this is a movie, it will be null.
|
* episode or this is a movie, it will be null.
|
||||||
|
@ -18,19 +18,21 @@
|
|||||||
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Platform, View, ViewProps } from "react-native";
|
import { ActivityIndicator, Platform, View } from "react-native";
|
||||||
import { Circle, Svg } from "react-native-svg";
|
import { Circle, Svg } from "react-native-svg";
|
||||||
import { px, useYoshiki } from "yoshiki/native";
|
import { px, Stylable, useYoshiki } from "yoshiki/native";
|
||||||
|
|
||||||
// TODO: Use moti on native
|
|
||||||
export const CircularProgress = ({
|
export const CircularProgress = ({
|
||||||
size = 48,
|
size = 48,
|
||||||
tickness = 5,
|
tickness = 5,
|
||||||
color,
|
color,
|
||||||
...props
|
...props
|
||||||
}: { size?: number; tickness?: number; color?: string } & ViewProps) => {
|
}: { size?: number; tickness?: number; color?: string } & Stylable) => {
|
||||||
const { css, theme } = useYoshiki();
|
const { css, theme } = useYoshiki();
|
||||||
|
|
||||||
|
if (Platform.OS !== "web")
|
||||||
|
return <ActivityIndicator size={size} color={color ?? theme.accent} {...props} />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View {...css({ width: size, height: size, overflow: "hidden" }, props)}>
|
<View {...css({ width: size, height: size, overflow: "hidden" }, props)}>
|
||||||
<style jsx global>{`
|
<style jsx global>{`
|
||||||
@ -60,6 +62,7 @@ export const CircularProgress = ({
|
|||||||
<Svg
|
<Svg
|
||||||
viewBox={`${size / 2} ${size / 2} ${size} ${size}`}
|
viewBox={`${size / 2} ${size / 2} ${size} ${size}`}
|
||||||
{...css(
|
{...css(
|
||||||
|
// @ts-ignore Web only
|
||||||
Platform.OS === "web" && { animation: "circularProgress-svg 1.4s ease-in-out infinite" },
|
Platform.OS === "web" && { animation: "circularProgress-svg 1.4s ease-in-out infinite" },
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@ -73,6 +76,7 @@ export const CircularProgress = ({
|
|||||||
strokeDasharray={[px(80), px(200)]}
|
strokeDasharray={[px(80), px(200)]}
|
||||||
{...css(
|
{...css(
|
||||||
Platform.OS === "web" && {
|
Platform.OS === "web" && {
|
||||||
|
// @ts-ignore Web only
|
||||||
animation: "circularProgress-circle 1.4s ease-in-out infinite",
|
animation: "circularProgress-circle 1.4s ease-in-out infinite",
|
||||||
},
|
},
|
||||||
)}
|
)}
|
||||||
|
@ -65,7 +65,8 @@ export const ErrorView = ({ error }: { error: KyooErrors }) => {
|
|||||||
<View
|
<View
|
||||||
{...css({
|
{...css({
|
||||||
backgroundColor: (theme) => theme.colors.red,
|
backgroundColor: (theme) => theme.colors.red,
|
||||||
flex: 1,
|
flexGrow: 1,
|
||||||
|
flexShrink: 1,
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
})}
|
})}
|
||||||
@ -85,7 +86,8 @@ export const EmptyView = ({ message }: { message: string }) => {
|
|||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
{...css({
|
{...css({
|
||||||
flex: 1,
|
flexGrow: 1,
|
||||||
|
flexShrink: 1,
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
})}
|
})}
|
||||||
|
29
front/packages/ui/src/i18n.d.ts
vendored
Normal file
29
front/packages/ui/src/i18n.d.ts
vendored
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
/*
|
||||||
|
* 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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { resources, defaultNS } from "./i18n";
|
||||||
|
import en from "../../../translations/en.json";
|
||||||
|
|
||||||
|
declare module "i18next" {
|
||||||
|
interface CustomTypeOptions {
|
||||||
|
returnNull: false;
|
||||||
|
resources: { translations: typeof en };
|
||||||
|
}
|
||||||
|
}
|
@ -32,7 +32,8 @@ import {
|
|||||||
} from "@kyoo/primitives";
|
} from "@kyoo/primitives";
|
||||||
import { Chapter, Font, Track } from "@kyoo/models";
|
import { Chapter, Font, Track } from "@kyoo/models";
|
||||||
import { useAtomValue } from "jotai";
|
import { useAtomValue } from "jotai";
|
||||||
import { View, ViewProps } from "react-native";
|
import { Pressable, View, ViewProps } from "react-native";
|
||||||
|
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 } from "./left-buttons";
|
import { LeftButtons } from "./left-buttons";
|
||||||
import { RightButtons } from "./right-buttons";
|
import { RightButtons } from "./right-buttons";
|
||||||
@ -42,6 +43,7 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { percent, rem, useYoshiki } from "yoshiki/native";
|
import { percent, rem, useYoshiki } from "yoshiki/native";
|
||||||
|
|
||||||
export const Hover = ({
|
export const Hover = ({
|
||||||
|
isLoading,
|
||||||
name,
|
name,
|
||||||
showName,
|
showName,
|
||||||
href,
|
href,
|
||||||
@ -53,8 +55,11 @@ export const Hover = ({
|
|||||||
nextSlug,
|
nextSlug,
|
||||||
onMenuOpen,
|
onMenuOpen,
|
||||||
onMenuClose,
|
onMenuClose,
|
||||||
|
show,
|
||||||
|
...props
|
||||||
}: {
|
}: {
|
||||||
name?: string;
|
isLoading: boolean;
|
||||||
|
name?: string | null;
|
||||||
showName?: string;
|
showName?: string;
|
||||||
href?: string;
|
href?: string;
|
||||||
poster?: string | null;
|
poster?: string | null;
|
||||||
@ -65,22 +70,27 @@ export const Hover = ({
|
|||||||
nextSlug?: string | null;
|
nextSlug?: string | null;
|
||||||
onMenuOpen: () => void;
|
onMenuOpen: () => void;
|
||||||
onMenuClose: () => void;
|
onMenuClose: () => void;
|
||||||
}) => {
|
show: boolean;
|
||||||
|
} & ViewProps) => {
|
||||||
|
// TODO animate show
|
||||||
return (
|
return (
|
||||||
<ContrastArea mode="dark">
|
<ContrastArea mode="dark">
|
||||||
{({ css }) => (
|
{({ css }) => (
|
||||||
<>
|
<View {...css([{ flexGrow: 1 }, !show && { opacity: 0 }])}>
|
||||||
<Back name={showName} href={href} />
|
<Back isLoading={isLoading} name={showName} href={href} {...props} />
|
||||||
<View
|
<View
|
||||||
{...css({
|
{...css(
|
||||||
position: "absolute",
|
{
|
||||||
bottom: 0,
|
position: "absolute",
|
||||||
left: 0,
|
bottom: 0,
|
||||||
right: 0,
|
left: 0,
|
||||||
bg: "rgba(0, 0, 0, 0.6)",
|
right: 0,
|
||||||
flexDirection: "row",
|
bg: "rgba(0, 0, 0, 0.6)",
|
||||||
padding: percent(1),
|
flexDirection: "row",
|
||||||
})}
|
padding: percent(1),
|
||||||
|
},
|
||||||
|
props,
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
<VideoPoster poster={poster} />
|
<VideoPoster poster={poster} />
|
||||||
<View
|
<View
|
||||||
@ -90,7 +100,9 @@ export const Hover = ({
|
|||||||
flexGrow: 1,
|
flexGrow: 1,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<H2 {...css({ paddingBottom: ts(1) })}>{name ?? <Skeleton variant="fill" />}</H2>
|
<H2 {...css({ paddingBottom: ts(1) })}>
|
||||||
|
{isLoading ? <Skeleton {...css({ width: rem(15), height: rem(2) })} /> : name}
|
||||||
|
</H2>
|
||||||
<ProgressBar chapters={chapters} />
|
<ProgressBar chapters={chapters} />
|
||||||
<View
|
<View
|
||||||
{...css({ flexDirection: "row", flexGrow: 1, justifyContent: "space-between" })}
|
{...css({ flexDirection: "row", flexGrow: 1, justifyContent: "space-between" })}
|
||||||
@ -105,33 +117,48 @@ export const Hover = ({
|
|||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</>
|
</View>
|
||||||
)}
|
)}
|
||||||
</ContrastArea>
|
</ContrastArea>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
export const Back = ({ name, href }: { name?: string; href?: string }) => {
|
export const Back = ({
|
||||||
|
isLoading,
|
||||||
|
name,
|
||||||
|
href,
|
||||||
|
...props
|
||||||
|
}: { isLoading: boolean; name?: string; href?: string } & ViewProps) => {
|
||||||
const { css } = useYoshiki();
|
const { css } = useYoshiki();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
{...css({
|
{...css(
|
||||||
position: "absolute",
|
{
|
||||||
top: 0,
|
position: "absolute",
|
||||||
left: 0,
|
top: 0,
|
||||||
right: 0,
|
left: 0,
|
||||||
bg: "rgba(0, 0, 0, 0.6)",
|
right: 0,
|
||||||
display: "flex",
|
bg: "rgba(0, 0, 0, 0.6)",
|
||||||
flexDirection: "row",
|
display: "flex",
|
||||||
alignItems: "center",
|
flexDirection: "row",
|
||||||
padding: percent(0.33),
|
alignItems: "center",
|
||||||
color: "white",
|
padding: percent(0.33),
|
||||||
})}
|
color: "white",
|
||||||
|
},
|
||||||
|
props,
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
<IconButton icon={ArrowBack} as={Link} href={href ?? ""} {...tooltip(t("back"))} />
|
<IconButton
|
||||||
|
icon={ArrowBack}
|
||||||
|
{...(href ? { as: Link as any, href: href } : { as: Pressable, onPress: router.back })}
|
||||||
|
{...tooltip(t("player.back"))}
|
||||||
|
/>
|
||||||
<Skeleton>
|
<Skeleton>
|
||||||
{name ? (
|
{isLoading ? (
|
||||||
|
<Skeleton {...css({ width: rem(5), marginBottom: 0 })} />
|
||||||
|
) : (
|
||||||
<H1
|
<H1
|
||||||
{...css({
|
{...css({
|
||||||
alignSelf: "center",
|
alignSelf: "center",
|
||||||
@ -142,8 +169,6 @@ export const Back = ({ name, href }: { name?: string; href?: string }) => {
|
|||||||
>
|
>
|
||||||
{name}
|
{name}
|
||||||
</H1>
|
</H1>
|
||||||
) : (
|
|
||||||
<Skeleton {...css({ width: rem(5), marginBottom: 0 })} />
|
|
||||||
)}
|
)}
|
||||||
</Skeleton>
|
</Skeleton>
|
||||||
</View>
|
</View>
|
||||||
@ -185,7 +210,6 @@ export const LoadingIndicator = () => {
|
|||||||
left: 0,
|
left: 0,
|
||||||
right: 0,
|
right: 0,
|
||||||
bg: "rgba(0, 0, 0, 0.3)",
|
bg: "rgba(0, 0, 0, 0.3)",
|
||||||
display: "flex",
|
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
|
@ -68,7 +68,7 @@ export const LeftButtons = ({
|
|||||||
icon={SkipNext}
|
icon={SkipNext}
|
||||||
as={Link}
|
as={Link}
|
||||||
href={nextSlug}
|
href={nextSlug}
|
||||||
{...tooltip(t("next"))}
|
{...tooltip(t("player.next"))}
|
||||||
{...spacing}
|
{...spacing}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
@ -22,7 +22,7 @@ import { Chapter } from "@kyoo/models";
|
|||||||
import { ts, Slider } from "@kyoo/primitives";
|
import { ts, Slider } from "@kyoo/primitives";
|
||||||
import { useAtom, useAtomValue } from "jotai";
|
import { useAtom, useAtomValue } from "jotai";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { NativeTouchEvent, Pressable, Touchable, View } from "react-native";
|
import { NativeTouchEvent, Pressable, View } from "react-native";
|
||||||
import { useYoshiki, px, percent } from "yoshiki/native";
|
import { useYoshiki, px, percent } from "yoshiki/native";
|
||||||
import { bufferedAtom, durationAtom, progressAtom } from "../state";
|
import { bufferedAtom, durationAtom, progressAtom } from "../state";
|
||||||
|
|
||||||
@ -34,9 +34,10 @@ export const ProgressBar = ({ chapters }: { chapters?: Chapter[] }) => {
|
|||||||
return (
|
return (
|
||||||
<Slider
|
<Slider
|
||||||
progress={progress}
|
progress={progress}
|
||||||
|
setProgress={setProgress}
|
||||||
subtleProgress={buffered}
|
subtleProgress={buffered}
|
||||||
max={duration}
|
max={duration}
|
||||||
markers={chapters?.map((x) => x.startTime)}
|
markers={chapters?.map((x) => x.startTime * 1000)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
const { css } = useYoshiki();
|
const { css } = useYoshiki();
|
||||||
|
@ -75,7 +75,7 @@ export const RightButtons = ({
|
|||||||
<IconButton
|
<IconButton
|
||||||
icon={isFullscreen ? FullscreenExit : Fullscreen}
|
icon={isFullscreen ? FullscreenExit : Fullscreen}
|
||||||
onClick={() => setFullscreen(!isFullscreen)}
|
onClick={() => setFullscreen(!isFullscreen)}
|
||||||
{...tooltip(t("fullscreen"))}
|
{...tooltip(t("player.fullscreen"))}
|
||||||
sx={{ color: "white" }}
|
sx={{ color: "white" }}
|
||||||
/>
|
/>
|
||||||
{/* {subtitleAnchor && ( */}
|
{/* {subtitleAnchor && ( */}
|
||||||
|
@ -21,16 +21,17 @@
|
|||||||
import { QueryIdentifier, QueryPage, WatchItem, WatchItemP, useFetch } from "@kyoo/models";
|
import { QueryIdentifier, QueryPage, WatchItem, WatchItemP, useFetch } from "@kyoo/models";
|
||||||
import { Head } from "@kyoo/primitives";
|
import { Head } from "@kyoo/primitives";
|
||||||
import { useState, useEffect, PointerEvent as ReactPointerEvent, ComponentProps } from "react";
|
import { useState, useEffect, PointerEvent as ReactPointerEvent, ComponentProps } from "react";
|
||||||
import { PointerEvent, StyleSheet, View } from "react-native";
|
import { Platform, Pressable, StyleSheet, View } from "react-native";
|
||||||
import { useAtom, useAtomValue, useSetAtom } from "jotai";
|
import { useAtom, useAtomValue, useSetAtom } from "jotai";
|
||||||
import { useRouter } from "solito/router";
|
import { useRouter } from "solito/router";
|
||||||
import { percent, useYoshiki } from "yoshiki/native";
|
import { percent, useYoshiki } from "yoshiki/native";
|
||||||
import { Hover, LoadingIndicator } from "./components/hover";
|
import { Back, Hover, LoadingIndicator } from "./components/hover";
|
||||||
import { fullscreenAtom, playAtom, Video } from "./state";
|
import { fullscreenAtom, playAtom, 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";
|
||||||
import { ErrorView } from "../fetch";
|
import { ErrorView } from "../fetch";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
// Callback used to hide the controls when the mouse goes iddle. This is stored globally to clear the old timeout
|
// 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)
|
// if the mouse moves again (if this is stored as a state, the whole page is redrawn on mouse move)
|
||||||
@ -46,10 +47,11 @@ const mapData = (
|
|||||||
previousSlug?: string,
|
previousSlug?: string,
|
||||||
nextSlug?: string,
|
nextSlug?: string,
|
||||||
): Partial<ComponentProps<typeof Hover>> => {
|
): Partial<ComponentProps<typeof Hover>> => {
|
||||||
if (!data) return {};
|
if (!data) return { isLoading: true };
|
||||||
return {
|
return {
|
||||||
|
isLoading: false,
|
||||||
name: data.isMovie ? data.name : `${episodeDisplayNumber(data, "")} ${data.name}`,
|
name: data.isMovie ? data.name : `${episodeDisplayNumber(data, "")} ${data.name}`,
|
||||||
showName: data.isMovie ? data.name : data.showTitle,
|
showName: data.isMovie ? data.name! : data.showTitle,
|
||||||
href: data ? (data.isMovie ? `/movie/${data.slug}` : `/show/${data.showSlug}`) : "#",
|
href: data ? (data.isMovie ? `/movie/${data.slug}` : `/show/${data.showSlug}`) : "#",
|
||||||
poster: data.poster,
|
poster: data.poster,
|
||||||
subtitles: data.subtitles,
|
subtitles: data.subtitles,
|
||||||
@ -60,10 +62,11 @@ const mapData = (
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
export const Player: QueryPage<{ slug: string }> = ({ slug }) => {
|
export const Player: QueryPage<{ slug: string }> = ({ slug }) => {
|
||||||
const { css } = useYoshiki();
|
const { css } = useYoshiki();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const [playbackError, setPlaybackError] = useState<string | undefined>(undefined);
|
||||||
const { data, error } = useFetch(query(slug));
|
const { data, error } = useFetch(query(slug));
|
||||||
const previous =
|
const previous =
|
||||||
data && !data.isMovie && data.previousEpisode
|
data && !data.isMovie && data.previousEpisode
|
||||||
@ -84,33 +87,40 @@ export const Player: QueryPage<{ slug: string }> = ({ slug }) => {
|
|||||||
const [menuOpenned, setMenuOpen] = useState(false);
|
const [menuOpenned, setMenuOpen] = useState(false);
|
||||||
|
|
||||||
const displayControls = showHover || !isPlaying || mouseMoved || menuOpenned;
|
const displayControls = showHover || !isPlaying || mouseMoved || menuOpenned;
|
||||||
// const mouseHasMoved = () => {
|
const show = () => {
|
||||||
// setMouseMoved(true);
|
setMouseMoved(true);
|
||||||
// if (mouseCallback) clearTimeout(mouseCallback);
|
if (mouseCallback) clearTimeout(mouseCallback);
|
||||||
// mouseCallback = setTimeout(() => {
|
mouseCallback = setTimeout(() => {
|
||||||
// setMouseMoved(false);
|
setMouseMoved(false);
|
||||||
// }, 2500);
|
}, 2500);
|
||||||
// };
|
};
|
||||||
// useEffect(() => {
|
useEffect(() => {
|
||||||
// const handler = (e: PointerEvent) => {
|
if (Platform.OS !== "web") return;
|
||||||
// if (e.pointerType !== "mouse") return;
|
const handler = (e: PointerEvent) => {
|
||||||
// mouseHasMoved();
|
if (e.pointerType !== "mouse") return;
|
||||||
// };
|
show();
|
||||||
|
};
|
||||||
|
|
||||||
// document.addEventListener("pointermove", handler);
|
document.addEventListener("pointermove", handler);
|
||||||
// return () => document.removeEventListener("pointermove", handler);
|
return () => document.removeEventListener("pointermove", handler);
|
||||||
// });
|
});
|
||||||
|
|
||||||
// useEffect(() => {
|
// useEffect(() => {
|
||||||
// setPlay(true);
|
// setPlay(true);
|
||||||
// }, [slug, setPlay]);
|
// }, [slug, setPlay]);
|
||||||
// useEffect(() => {
|
useEffect(() => {
|
||||||
// if (!/Mobi/i.test(window.navigator.userAgent)) return;
|
if (Platform.OS !== "web" || !/Mobi/i.test(window.navigator.userAgent)) return;
|
||||||
// setFullscreen(true);
|
setFullscreen(true);
|
||||||
// return () => setFullscreen(false);
|
return () => setFullscreen(false);
|
||||||
// }, [setFullscreen]);
|
}, [setFullscreen]);
|
||||||
|
|
||||||
if (error) return <ErrorView error={error} />;
|
if (error || playbackError)
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Back isLoading={false} {...css({ position: "relative", bg: (theme) => theme.appbar })} />
|
||||||
|
<ErrorView error={error ?? { errors: [playbackError!] }} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -131,7 +141,7 @@ export const Player: QueryPage<{ slug: string }> = ({ slug }) => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<MediaSessionManager
|
<MediaSessionManager
|
||||||
title={data?.name}
|
title={data?.name ?? t("show.episodeNoMetadata")}
|
||||||
image={data?.thumbnail}
|
image={data?.thumbnail}
|
||||||
next={next}
|
next={next}
|
||||||
previous={previous}
|
previous={previous}
|
||||||
@ -142,29 +152,20 @@ export const Player: QueryPage<{ slug: string }> = ({ slug }) => {
|
|||||||
{/* text-shadow: -1px -1px 0 #000, 1px -1px 0 #000, -1px 1px 0 #000, 1px 1px 0 #000; */}
|
{/* text-shadow: -1px -1px 0 #000, 1px -1px 0 #000, -1px 1px 0 #000, 1px 1px 0 #000; */}
|
||||||
{/* } */}
|
{/* } */}
|
||||||
{/* `}</style> */}
|
{/* `}</style> */}
|
||||||
<View
|
<Pressable
|
||||||
// onMouseLeave={() => setMouseMoved(false)}
|
onHoverOut={() => setMouseMoved(false)}
|
||||||
|
onPress={Platform.OS === "web" ? () => setPlay(!isPlaying) : show}
|
||||||
{...css({
|
{...css({
|
||||||
flexGrow: 1,
|
flexGrow: 1,
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
// cursor: displayControls ? "unset" : "none",
|
|
||||||
bg: "black",
|
bg: "black",
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<Video
|
<Video
|
||||||
links={data?.link}
|
links={data?.link}
|
||||||
videoStyle={{ width: percent(100), height: percent(100) }}
|
videoStyle={{ width: percent(100), height: percent(100) }}
|
||||||
|
setError={setPlaybackError}
|
||||||
{...css(StyleSheet.absoluteFillObject)}
|
{...css(StyleSheet.absoluteFillObject)}
|
||||||
// onClick={onVideoClick}
|
|
||||||
// onPointerDown={(e: PointerEvent) => {
|
|
||||||
// if (e.type === "mouse") {
|
|
||||||
// onVideoClick();
|
|
||||||
// } else if (mouseMoved) {
|
|
||||||
// setMouseMoved(false);
|
|
||||||
// } else {
|
|
||||||
// // mouseHasMoved();
|
|
||||||
// }
|
|
||||||
// }}
|
|
||||||
// onEnded={() => {
|
// onEnded={() => {
|
||||||
// if (!data) return;
|
// if (!data) return;
|
||||||
// if (data.isMovie) router.push(`/movie/${data.slug}`);
|
// if (data.isMovie) router.push(`/movie/${data.slug}`);
|
||||||
@ -174,34 +175,22 @@ export const Player: QueryPage<{ slug: string }> = ({ slug }) => {
|
|||||||
// );
|
// );
|
||||||
// }}
|
// }}
|
||||||
/>
|
/>
|
||||||
{/* <LoadingIndicator /> */}
|
<LoadingIndicator />
|
||||||
<Hover
|
<Hover
|
||||||
{...mapData(data, previous, next)}
|
{...mapData(data, previous, next)}
|
||||||
// onPointerOver={(e: ReactPointerEvent<HTMLElement>) => {
|
// @ts-ignore Web only types
|
||||||
// if (e.pointerType === "mouse") setHover(true);
|
onMouseEnter={() => setHover(true)}
|
||||||
// }}
|
// @ts-ignore Web only types
|
||||||
// onPointerOut={() => setHover(false)}
|
onMouseLeave={() => setHover(false)}
|
||||||
onMenuOpen={() => setMenuOpen(true)}
|
onMenuOpen={() => setMenuOpen(true)}
|
||||||
onMenuClose={() => {
|
onMenuClose={() => {
|
||||||
// Disable hover since the menu overlay makes the mouseout unreliable.
|
// Disable hover since the menu overlay makes the mouseout unreliable.
|
||||||
setHover(false);
|
setHover(false);
|
||||||
setMenuOpen(false);
|
setMenuOpen(false);
|
||||||
}}
|
}}
|
||||||
// sx={
|
show={displayControls}
|
||||||
// displayControls
|
|
||||||
// ? {
|
|
||||||
// visibility: "visible",
|
|
||||||
// opacity: 1,
|
|
||||||
// transition: "opacity .2s ease-in",
|
|
||||||
// }
|
|
||||||
// : {
|
|
||||||
// visibility: "hidden",
|
|
||||||
// opacity: 0,
|
|
||||||
// transition: "opacity .4s ease-out, visibility 0s .4s",
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
/>
|
/>
|
||||||
</View>
|
</Pressable>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -20,7 +20,7 @@
|
|||||||
|
|
||||||
import { Font, Track, WatchItem } from "@kyoo/models";
|
import { Font, Track, WatchItem } from "@kyoo/models";
|
||||||
import { atom, useAtom, useSetAtom } from "jotai";
|
import { atom, useAtom, useSetAtom } from "jotai";
|
||||||
import { RefObject, useEffect, useRef, useState } from "react";
|
import { RefObject, useEffect, useLayoutEffect, useRef, useState } from "react";
|
||||||
import { createParam } from "solito";
|
import { createParam } from "solito";
|
||||||
import { ResizeMode, Video as NativeVideo, VideoProps } from "expo-av";
|
import { ResizeMode, Video as NativeVideo, VideoProps } from "expo-av";
|
||||||
import SubtitleOctopus from "libass-wasm";
|
import SubtitleOctopus from "libass-wasm";
|
||||||
@ -34,8 +34,7 @@ enum PlayMode {
|
|||||||
|
|
||||||
const playModeAtom = atom<PlayMode>(PlayMode.Direct);
|
const playModeAtom = atom<PlayMode>(PlayMode.Direct);
|
||||||
|
|
||||||
export const playAtom = atom<boolean>(true);
|
export const playAtom = atom(true);
|
||||||
|
|
||||||
export const loadAtom = atom(false);
|
export const loadAtom = atom(false);
|
||||||
export const progressAtom = atom(0);
|
export const progressAtom = atom(0);
|
||||||
export const bufferedAtom = atom(0);
|
export const bufferedAtom = atom(0);
|
||||||
@ -69,10 +68,13 @@ export const [_, fullscreenAtom] = bakedAtom(false, async (_, set, value, baker)
|
|||||||
|
|
||||||
let hls: Hls | null = null;
|
let hls: Hls | null = null;
|
||||||
|
|
||||||
export const Video = ({ links, ...props }: { links?: WatchItem["link"] } & VideoProps) => {
|
export const Video = ({
|
||||||
|
links,
|
||||||
|
setError,
|
||||||
|
...props
|
||||||
|
}: { links?: WatchItem["link"]; setError: (error: string | undefined) => void } & VideoProps) => {
|
||||||
// const player = useRef<HTMLVideoElement>(null);
|
// const player = useRef<HTMLVideoElement>(null);
|
||||||
// const setPlayer = useSetAtom(playerAtom);
|
// const setPlayer = useSetAtom(playerAtom);
|
||||||
// const setLoad = useSetAtom(loadAtom);
|
|
||||||
// const setVolume = useSetAtom(_volumeAtom);
|
// const setVolume = useSetAtom(_volumeAtom);
|
||||||
// const setMuted = useSetAtom(_mutedAtom);
|
// const setMuted = useSetAtom(_mutedAtom);
|
||||||
// const setFullscreen = useSetAtom(fullscreenAtom);
|
// const setFullscreen = useSetAtom(fullscreenAtom);
|
||||||
@ -80,6 +82,12 @@ export const Video = ({ links, ...props }: { links?: WatchItem["link"] } & Video
|
|||||||
|
|
||||||
const ref = useRef<NativeVideo | null>(null);
|
const ref = useRef<NativeVideo | null>(null);
|
||||||
const [isPlaying, setPlay] = useAtom(playAtom);
|
const [isPlaying, setPlay] = useAtom(playAtom);
|
||||||
|
const setLoad = useSetAtom(loadAtom);
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
setLoad(true);
|
||||||
|
}, [])
|
||||||
|
|
||||||
const [progress, setProgress] = useAtom(progressAtom);
|
const [progress, setProgress] = useAtom(progressAtom);
|
||||||
const [buffered, setBuffered] = useAtom(bufferedAtom);
|
const [buffered, setBuffered] = useAtom(bufferedAtom);
|
||||||
const [duration, setDuration] = useAtom(durationAtom);
|
const [duration, setDuration] = useAtom(durationAtom);
|
||||||
@ -134,9 +142,13 @@ export const Video = ({ links, ...props }: { links?: WatchItem["link"] } & Video
|
|||||||
source={links ? { uri: links.direct } : undefined}
|
source={links ? { uri: links.direct } : undefined}
|
||||||
shouldPlay={isPlaying}
|
shouldPlay={isPlaying}
|
||||||
onPlaybackStatusUpdate={(status) => {
|
onPlaybackStatusUpdate={(status) => {
|
||||||
// TODO: Handle error state
|
if (!status.isLoaded) {
|
||||||
if (!status.isLoaded) return;
|
setLoad(true);
|
||||||
|
if (status.error) setError(status.error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoad(status.isPlaying !== status.shouldPlay);
|
||||||
setPlay(status.shouldPlay);
|
setPlay(status.shouldPlay);
|
||||||
setProgress(status.positionMillis);
|
setProgress(status.positionMillis);
|
||||||
setBuffered(status.playableDurationMillis ?? 0);
|
setBuffered(status.playableDurationMillis ?? 0);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user