Add progress change listneer to update the video

This commit is contained in:
Zoe Roux 2022-12-21 11:24:23 +09:00
parent 856eaffda6
commit b1b8772717
5 changed files with 63 additions and 190 deletions

View File

@ -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 (
<View
@ -70,18 +79,10 @@ export const Slider = ({
setSeek(false);
endSeek?.call(null);
}}
onResponderMove={(event) => {
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),

View File

@ -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 = ({
</ContrastArea>
);
};
export const ProgressBar = ({ chapters }: { chapters?: Chapter[] }) => {
const [progress, setProgress] = useAtom(progressAtom);
const buffered = useAtomValue(bufferedAtom);
const duration = useAtomValue(durationAtom);
const setPlay = useSetAtom(playAtom);
return (
<Slider
progress={progress}
startSeek={() => 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",
})}
>

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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 (
<Slider
progress={progress}
startSeek={() => setPlay(false)}
endSeek={() => setPlay(true)}
setProgress={setProgress}
subtleProgress={buffered}
max={duration}
markers={chapters?.map((x) => x.startTime * 1000)}
/>
);
const { css } = useYoshiki();
const ref = useRef<View>(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 (
<Pressable
onPointerDown={(event) => {
// 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" },
},
})}
>
<View
ref={ref}
className="bar"
sx={{
width: "100%",
height: "4px",
background: "rgba(255, 255, 255, 0.2)",
transform: isSeeking ? "unset" : "scaleY(.6)",
position: "relative",
}}
>
<View
sx={{
width: `${(buffered / duration) * 100}%`,
position: "absolute",
top: 0,
bottom: 0,
left: 0,
background: "rgba(255, 255, 255, 0.5)",
}}
/>
<View
sx={{
width: `${(progress / duration) * 100}%`,
position: "absolute",
top: 0,
bottom: 0,
left: 0,
background: (theme) => theme.palette.primary.main,
}}
/>
<View
className="thumb"
sx={{
position: "absolute",
left: `calc(${(progress / duration) * 100}% - 6px)`,
top: 0,
bottom: 0,
margin: "auto",
opacity: +isSeeking,
width: "12px",
height: "12px",
borderRadius: "6px",
background: (theme) => theme.palette.primary.main,
}}
/>
{chapters?.map((x) => (
<View
key={x.startTime}
{...css({
position: "absolute",
width: px(4),
top: 0,
bottom: 0,
left: `${Math.min(100, (x.startTime / duration) * 100)}%`,
bg: (theme) => theme.accent,
})}
/>
))}
</View>
</Pressable>
);
};

View File

@ -195,4 +195,4 @@ export const Player: QueryPage<{ slug: string }> = ({ slug }) => {
);
};
// Player.getFetchUrls = ({ slug }) => [query(slug)];
Player.getFetchUrls = ({ slug }) => [query(slug)];

View File

@ -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>(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<number | undefined>(undefined);
export const progressAtom = atom<number, number>(
(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);
}}