diff --git a/front/packages/primitives/src/slider.tsx b/front/packages/primitives/src/slider.tsx index 2b67cc3b..cf44f9ec 100644 --- a/front/packages/primitives/src/slider.tsx +++ b/front/packages/primitives/src/slider.tsx @@ -19,7 +19,7 @@ */ import { useRef, useState } from "react"; -import { Platform, View } from "react-native"; +import { GestureResponderEvent, Platform, View } from "react-native"; import { percent, Stylable, useYoshiki } from "yoshiki/native"; import { ts } from "./utils"; @@ -49,6 +49,15 @@ export const Slider = ({ const [isFocus, setFocus] = useState(false); const smallBar = !(isSeeking || isHover || isFocus); + const change = (event: GestureResponderEvent) => { + event.preventDefault(); + const locationX = Platform.select({ + android: event.nativeEvent.pageX - layout.x, + default: event.nativeEvent.locationX, + }); + setProgress(Math.max(0, Math.min(locationX / layout.width, 1)) * max); + }; + // TODO keyboard handling (left, right, up, down) return ( { - event.preventDefault(); - const locationX = Platform.select({ - android: event.nativeEvent.pageX - layout.x, - default: event.nativeEvent.locationX, - }); - setProgress(Math.max(0, Math.min(locationX / layout.width, 100)) * max); - }} + onResponderStart={change} + onResponderMove={change} onLayout={() => - ref.current?.measure((_, __, width, ___, pageX) => - setLayout({ width: width, x: pageX }), - ) + ref.current?.measure((_, __, width, ___, pageX) => setLayout({ width: width, x: pageX })) } {...css( { @@ -154,7 +155,7 @@ export const Slider = ({ position: "absolute", top: 0, bottom: 0, - marginY: ts(.5), + marginY: ts(0.5), bg: (theme) => theme.accent, width: ts(2), height: ts(2), diff --git a/front/packages/ui/src/player/components/hover.tsx b/front/packages/ui/src/player/components/hover.tsx index 8fbdf828..7f1ff040 100644 --- a/front/packages/ui/src/player/components/hover.tsx +++ b/front/packages/ui/src/player/components/hover.tsx @@ -19,6 +19,7 @@ */ import { + alpha, CircularProgress, ContrastArea, H1, @@ -27,20 +28,20 @@ import { Link, Poster, Skeleton, + Slider, tooltip, ts, } from "@kyoo/primitives"; import { Chapter, Font, Track } from "@kyoo/models"; -import { useAtomValue } from "jotai"; +import { useAtomValue, useSetAtom, useAtom } from "jotai"; import { Pressable, View, ViewProps } from "react-native"; +import { useTranslation } from "react-i18next"; +import { percent, rem, useYoshiki } from "yoshiki/native"; import { useRouter } from "solito/router"; 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 { loadAtom } from "../state"; -import { useTranslation } from "react-i18next"; -import { percent, rem, useYoshiki } from "yoshiki/native"; +import { bufferedAtom, durationAtom, loadAtom, playAtom, progressAtom } from "../state"; export const Hover = ({ isLoading, @@ -85,7 +86,7 @@ export const Hover = ({ bottom: 0, left: 0, right: 0, - bg: "rgba(0, 0, 0, 0.6)", + bg: (theme) => alpha(theme.colors.black, 0.6), flexDirection: "row", padding: percent(1), }, @@ -122,6 +123,26 @@ export const Hover = ({ ); }; + +export const ProgressBar = ({ chapters }: { chapters?: Chapter[] }) => { + const [progress, setProgress] = useAtom(progressAtom); + const buffered = useAtomValue(bufferedAtom); + const duration = useAtomValue(durationAtom); + const setPlay = useSetAtom(playAtom); + + return ( + setPlay(false)} + endSeek={() => setTimeout(() => setPlay(true), 10)} + setProgress={setProgress} + subtleProgress={buffered} + max={duration} + markers={chapters?.map((x) => x.startTime * 1000)} + /> + ); +}; + export const Back = ({ isLoading, name, @@ -140,7 +161,7 @@ export const Back = ({ top: 0, left: 0, right: 0, - bg: "rgba(0, 0, 0, 0.6)", + bg: (theme) => alpha(theme.colors.black, 0.6), display: "flex", flexDirection: "row", alignItems: "center", @@ -209,7 +230,7 @@ export const LoadingIndicator = () => { bottom: 0, left: 0, right: 0, - bg: "rgba(0, 0, 0, 0.3)", + bg: (theme) => alpha(theme.colors.black, 0.3), justifyContent: "center", })} > diff --git a/front/packages/ui/src/player/components/progress-bar.tsx b/front/packages/ui/src/player/components/progress-bar.tsx deleted file mode 100644 index 3fbdf05b..00000000 --- a/front/packages/ui/src/player/components/progress-bar.tsx +++ /dev/null @@ -1,158 +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 { Chapter } from "@kyoo/models"; -import { ts, Slider } from "@kyoo/primitives"; -import { useAtom, useAtomValue, useSetAtom } from "jotai"; -import { useEffect, useRef, useState } from "react"; -import { NativeTouchEvent, Pressable, View } from "react-native"; -import { useYoshiki, px, percent } from "yoshiki/native"; -import { bufferedAtom, durationAtom, playAtom, progressAtom } from "../state"; - -export const ProgressBar = ({ chapters }: { chapters?: Chapter[] }) => { - const [progress, setProgress] = useAtom(progressAtom); - const buffered = useAtomValue(bufferedAtom); - const duration = useAtomValue(durationAtom); - const setPlay = useSetAtom(playAtom); - - return ( - setPlay(false)} - endSeek={() => setPlay(true)} - setProgress={setProgress} - subtleProgress={buffered} - max={duration} - markers={chapters?.map((x) => x.startTime * 1000)} - /> - ); - const { css } = useYoshiki(); - const ref = useRef(null); - const [isSeeking, setSeek] = useState(false); - - 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; - setProgress(Math.max(0, Math.min(value, 1)) * duration); - }; - - useEffect(() => { - const handler = () => setSeek(false); - - document.addEventListener("mouseup", handler); - document.addEventListener("touchend", handler); - return () => { - document.removeEventListener("mouseup", handler); - document.removeEventListener("touchend", handler); - }; - }); - useEffect(() => { - document.addEventListener("mousemove", updateProgress); - document.addEventListener("touchmove", updateProgress); - return () => { - document.removeEventListener("mousemove", updateProgress); - document.removeEventListener("touchmove", updateProgress); - }; - }); - - return ( - { - // prevent drag and drop of the UI. - event.preventDefault(); - setSeek(true); - }} - 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" }, - }, - })} - > - - - theme.palette.primary.main, - }} - /> - theme.palette.primary.main, - }} - /> - - {chapters?.map((x) => ( - theme.accent, - })} - /> - ))} - - - ); -}; diff --git a/front/packages/ui/src/player/index.tsx b/front/packages/ui/src/player/index.tsx index 415e79fc..040f7e13 100644 --- a/front/packages/ui/src/player/index.tsx +++ b/front/packages/ui/src/player/index.tsx @@ -195,4 +195,4 @@ export const Player: QueryPage<{ slug: string }> = ({ slug }) => { ); }; -// Player.getFetchUrls = ({ slug }) => [query(slug)]; +Player.getFetchUrls = ({ slug }) => [query(slug)]; diff --git a/front/packages/ui/src/player/state.tsx b/front/packages/ui/src/player/state.tsx index 1683bce7..4756fefe 100644 --- a/front/packages/ui/src/player/state.tsx +++ b/front/packages/ui/src/player/state.tsx @@ -19,7 +19,7 @@ */ import { Font, Track, WatchItem } from "@kyoo/models"; -import { atom, useAtom, useSetAtom } from "jotai"; +import { atom, useAtom, useAtomValue, useSetAtom } from "jotai"; import { RefObject, useEffect, useLayoutEffect, useRef, useState } from "react"; import { createParam } from "solito"; import { ResizeMode, Video as NativeVideo, VideoProps } from "expo-av"; @@ -36,10 +36,19 @@ const playModeAtom = atom(PlayMode.Direct); export const playAtom = atom(true); export const loadAtom = atom(false); -export const progressAtom = atom(0); export const bufferedAtom = atom(0); export const durationAtom = atom(undefined); +export const progressAtom = atom( + (get) => get(privateProgressAtom), + (_, set, value) => { + set(privateProgressAtom, value); + set(publicProgressAtom, value); + }, +); +const privateProgressAtom = atom(0); +const publicProgressAtom = atom(0); + export const [_volumeAtom, volumeAtom] = bakedAtom(100, (get, set, value, baker) => { const player = get(playerAtom); if (!player?.current) return; @@ -86,16 +95,16 @@ export const Video = ({ useLayoutEffect(() => { setLoad(true); - }, []) + }, [setLoad]); - const [progress, setProgress] = useAtom(progressAtom); - const [buffered, setBuffered] = useAtom(bufferedAtom); - const [duration, setDuration] = useAtom(durationAtom); + const publicProgress = useAtomValue(publicProgressAtom); + const setPrivateProgress = useSetAtom(privateProgressAtom); + const setBuffered = useSetAtom(bufferedAtom); + const setDuration = useSetAtom(durationAtom); useEffect(() => { - // I think this will trigger an infinite refresh loop - // ref.current?.setStatusAsync({ positionMillis: progress }); - }, [progress]); + ref.current?.setStatusAsync({ positionMillis: publicProgress }); + }, [publicProgress]); // setPlayer(player); @@ -150,7 +159,7 @@ export const Video = ({ setLoad(status.isPlaying !== status.shouldPlay); setPlay(status.shouldPlay); - setProgress(status.positionMillis); + setPrivateProgress(status.positionMillis); setBuffered(status.playableDurationMillis ?? 0); setDuration(status.durationMillis); }}