From ee32ecd52717cfdad3d4ab5d348be1c0550e1892 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Sun, 20 Jul 2025 15:13:07 +0200 Subject: [PATCH] Implement player's `enEnd` & loading indicator --- front/src/primitives/index.ts | 8 +- front/src/primitives/progress.tsx | 84 +------------- front/src/primitives/select.tsx | 20 ---- front/src/primitives/slider.tsx | 28 +---- front/src/ui/player/components/hover.tsx | 136 ++++++++++++----------- front/src/ui/player/index.tsx | 30 ++++- 6 files changed, 112 insertions(+), 194 deletions(-) diff --git a/front/src/primitives/index.ts b/front/src/primitives/index.ts index e81398a9..63355757 100644 --- a/front/src/primitives/index.ts +++ b/front/src/primitives/index.ts @@ -7,16 +7,16 @@ export * from "./divider"; export * from "./icons"; export * from "./image"; export * from "./image-background"; -// export * from "./popup"; -// export * from "./select"; export * from "./input"; export * from "./links"; -// export * from "./progress"; -// export * from "./slider"; // export * from "./snackbar"; // export * from "./alert"; export * from "./menu"; +export * from "./progress"; +// export * from "./popup"; +export * from "./select"; export * from "./skeleton"; +export * from "./slider"; export * from "./text"; export * from "./theme"; export * from "./tooltip"; diff --git a/front/src/primitives/progress.tsx b/front/src/primitives/progress.tsx index f2325e0d..060a207f 100644 --- a/front/src/primitives/progress.tsx +++ b/front/src/primitives/progress.tsx @@ -1,26 +1,5 @@ -/* - * 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 { ActivityIndicator, Platform, View } from "react-native"; -import { Circle, Svg } from "react-native-svg"; -import { px, type Stylable, useYoshiki } from "yoshiki/native"; +import { ActivityIndicator } from "react-native"; +import { type Stylable, useYoshiki } from "yoshiki/native"; export const CircularProgress = ({ size = 48, @@ -28,64 +7,9 @@ export const CircularProgress = ({ color, ...props }: { size?: number; tickness?: number; color?: string } & Stylable) => { - const { css, theme } = useYoshiki(); - - if (Platform.OS !== "web") - return ( - - ); + const { theme } = useYoshiki(); return ( - - - - - - + ); }; diff --git a/front/src/primitives/select.tsx b/front/src/primitives/select.tsx index 541ad060..a9ffbdfb 100644 --- a/front/src/primitives/select.tsx +++ b/front/src/primitives/select.tsx @@ -1,23 +1,3 @@ -/* - * 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 ExpandMore from "@material-symbols/svg-400/rounded/keyboard_arrow_down-fill.svg"; import { Button } from "./button"; import { Icon } from "./icons"; diff --git a/front/src/primitives/slider.tsx b/front/src/primitives/slider.tsx index 4c71ed2f..0b6c864a 100644 --- a/front/src/primitives/slider.tsx +++ b/front/src/primitives/slider.tsx @@ -1,26 +1,10 @@ -/* - * 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 { useRef, useState } from "react"; -import { type GestureResponderEvent, Platform, View } from "react-native"; -import type { ViewProps } from "react-native-svg/lib/typescript/fabric/utils"; +import { + type GestureResponderEvent, + Platform, + View, + type ViewProps, +} from "react-native"; import { percent, px, useYoshiki } from "yoshiki/native"; import { focusReset } from "./utils"; diff --git a/front/src/ui/player/components/hover.tsx b/front/src/ui/player/components/hover.tsx index b4170e6f..29ab5799 100644 --- a/front/src/ui/player/components/hover.tsx +++ b/front/src/ui/player/components/hover.tsx @@ -1,25 +1,24 @@ -/* - * 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 type { Audio, Chapter, KyooImage, Subtitle } from "@kyoo/models"; +import ArrowBack from "@material-symbols/svg-400/rounded/arrow_back-fill.svg"; import { + type ReactNode, + useCallback, + useEffect, + useRef, + useState, +} from "react"; +import { useTranslation } from "react-i18next"; +import { + type ImageStyle, + Platform, + Pressable, + View, + type ViewProps, +} from "react-native"; +import { useEvent, type VideoPlayer } from "react-native-video"; +import { percent, rem, useYoshiki } from "yoshiki/native"; +import type { AudioTrack, Chapter, KImage, Subtitle } from "~/models"; +import { + alpha, CircularProgress, ContrastArea, H1, @@ -30,43 +29,14 @@ import { Skeleton, Slider, Tooltip, - alpha, - imageBorderRadius, tooltip, ts, useIsTouch, -} from "@kyoo/primitives"; -import ArrowBack from "@material-symbols/svg-400/rounded/arrow_back-fill.svg"; -import { useAtom, useAtomValue, useSetAtom } from "jotai"; -import { atom } from "jotai"; -import { type ReactNode, useCallback, useEffect, useRef, useState } from "react"; -import { useTranslation } from "react-i18next"; -import { type ImageStyle, Platform, Pressable, View, type ViewProps } from "react-native"; -import { useRouter } from "solito/router"; -import { percent, rem, useYoshiki } from "yoshiki/native"; -import { - bufferedAtom, - durationAtom, - fullscreenAtom, - loadAtom, - playAtom, - progressAtom, -} from "../state"; +} from "~/primitives"; import { LeftButtons, TouchControls } from "./left-buttons"; import { RightButtons } from "./right-buttons"; import { BottomScrubber, ScrubberTooltip } from "./scrubber"; -const hoverReasonAtom = atom({ - mouseMoved: false, - mouseHover: false, - menuOpened: false, -}); -export const hoverAtom = atom((get) => - [!get(playAtom), ...Object.values(get(hoverReasonAtom))].includes(true), -); -export const seekingAtom = atom(false); -export const seekProgressAtom = atom(null); - export const Hover = ({ isLoading, url, @@ -145,7 +115,11 @@ export const Hover = ({ padding: percent(1), })} > - + {!showBottomSeeker && (

- {isLoading ? : name} + {isLoading ? ( + + ) : ( + name + )}

)} @@ -172,15 +150,24 @@ export const Hover = ({ flexWrap: "wrap", })} > - + setHover((x) => ({ ...x, menuOpened: true }))} + onMenuOpen={() => + setHover((x) => ({ ...x, menuOpened: true })) + } onMenuClose={() => { // Disable hover since the menu overlay makes the mouseout unreliable. - setHover((x) => ({ ...x, menuOpened: false, mouseHover: false })); + setHover((x) => ({ + ...x, + menuOpened: false, + mouseHover: false, + })); }} />
@@ -198,7 +185,9 @@ export const HoverTouch = ({ children, ...props }: { children: ReactNode }) => { const hover = useAtomValue(hoverAtom); const setHover = useSetAtom(hoverReasonAtom); const mouseCallback = useRef(null); - const touch = useRef<{ count: number; timeout?: NodeJS.Timeout }>({ count: 0 }); + const touch = useRef<{ count: number; timeout?: NodeJS.Timeout }>({ + count: 0, + }); const playerWidth = useRef(null); const isTouch = useIsTouch(); @@ -226,7 +215,8 @@ export const HoverTouch = ({ children, ...props }: { children: ReactNode }) => { // It also serves to hide the tooltip. useEffect(() => { if (Platform.OS !== "web") return; - if (!hover && document.activeElement instanceof HTMLElement) document.activeElement.blur(); + if (!hover && document.activeElement instanceof HTMLElement) + document.activeElement.blur(); }, [hover]); const { css } = useYoshiki(); @@ -282,7 +272,8 @@ export const HoverTouch = ({ children, ...props }: { children: ReactNode }) => { { - if (e.nativeEvent.pointerType === "mouse") setHover((x) => ({ ...x, mouseMoved: false })); + if (e.nativeEvent.pointerType === "mouse") + setHover((x) => ({ ...x, mouseMoved: false })); }} onPress={(e) => { e.preventDefault(); @@ -315,7 +306,13 @@ export const HoverTouch = ({ children, ...props }: { children: ReactNode }) => { ); }; -const ProgressBar = ({ url, chapters }: { url: string; chapters?: Chapter[] }) => { +const ProgressBar = ({ + url, + chapters, +}: { + url: string; + chapters?: Chapter[]; +}) => { const [progress, setProgress] = useAtom(progressAtom); const buffered = useAtomValue(bufferedAtom); const duration = useAtomValue(durationAtom); @@ -353,10 +350,17 @@ const ProgressBar = ({ url, chapters }: { url: string; chapters?: Chapter[] }) = id={"progress-scrubber"} isOpen={hoverProgress !== null} place="top" - position={{ x: layout.x + (layout.width * hoverProgress!) / (duration ?? 1), y: layout.y }} + position={{ + x: layout.x + (layout.width * hoverProgress!) / (duration ?? 1), + y: layout.y, + }} render={() => hoverProgress ? ( - + ) : null } opacity={1} @@ -449,9 +453,13 @@ const VideoPoster = ({ ); }; -export const LoadingIndicator = () => { - const isLoading = useAtomValue(loadAtom); +export const LoadingIndicator = ({ player }: { player: VideoPlayer }) => { const { css } = useYoshiki(); + const [isLoading, setLoading] = useState(false); + + useEvent(player, "onStatusChange", (status) => { + setLoading(status === "loading"); + }); if (!isLoading) return null; diff --git a/front/src/ui/player/index.tsx b/front/src/ui/player/index.tsx index c2b2e0ae..cb947041 100644 --- a/front/src/ui/player/index.tsx +++ b/front/src/ui/player/index.tsx @@ -1,7 +1,12 @@ -import { Stack } from "expo-router"; +import { Stack, useRouter } from "expo-router"; import { useEffect, useRef } from "react"; import { StyleSheet, View } from "react-native"; -import { useVideoPlayer, VideoView, VideoViewRef } from "react-native-video"; +import { + useEvent, + useVideoPlayer, + VideoView, + VideoViewRef, +} from "react-native-video"; import { entryDisplayNumber } from "~/components/entries"; import { FullVideo, VideoInfo } from "~/models"; import { Head } from "~/primitives"; @@ -9,6 +14,7 @@ import { useToken } from "~/providers/account-context"; import { useLocalSetting } from "~/providers/settings"; import { type QueryIdentifier, useFetch } from "~/query"; import { useQueryState } from "~/utils"; +import { LoadingIndicator } from "./components/hover"; // import { Hover, LoadingIndicator } from "./components/hover"; // import { useVideoKeyboard } from "./keyboard"; @@ -33,7 +39,8 @@ const mapMetadata = (item: FullVideo | undefined) => { }; export const Player = () => { - const [slug] = useQueryState("slug", undefined!); + const [slug, setSlug] = useQueryState("slug", undefined!); + const [start, setStart] = useQueryState("t", undefined); const { data, error } = useFetch(Player.query(slug)); const { data: info, error: infoError } = useFetch(Player.infoQuery(slug)); @@ -60,10 +67,25 @@ export const Player = () => { (p) => { p.playWhenInactive = true; p.playInBackground = true; + const seek = start ?? data?.progress.time; + // TODO: fix console.error bellow + if (seek) p.seekTo(seek); + else console.error("Player got ready before progress info was loaded."); p.play(); }, ); + const router = useRouter(); + useEvent(player, "onEnd", () => { + if (!data) return; + if (data.next) { + setStart(0); + setSlug(data.next.video); + } else { + router.navigate(data.show!.href); + } + }); + // const [playbackError, setPlaybackError] = useState( // undefined, // ); @@ -120,7 +142,7 @@ export const Player = () => { controls style={StyleSheet.absoluteFillObject} /> - {/* */} + {/* */} );