mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-05-24 02:02:36 -04:00
Add progress change listneer to update the video
This commit is contained in:
parent
856eaffda6
commit
b1b8772717
@ -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),
|
||||
|
@ -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",
|
||||
})}
|
||||
>
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
@ -195,4 +195,4 @@ export const Player: QueryPage<{ slug: string }> = ({ slug }) => {
|
||||
);
|
||||
};
|
||||
|
||||
// Player.getFetchUrls = ({ slug }) => [query(slug)];
|
||||
Player.getFetchUrls = ({ slug }) => [query(slug)];
|
||||
|
@ -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);
|
||||
}}
|
||||
|
Loading…
x
Reference in New Issue
Block a user