mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-05-24 02:02:36 -04:00
Rewrite progress slider (part 1)
This commit is contained in:
parent
7b8d916685
commit
c79a991024
@ -15,7 +15,7 @@
|
||||
"@tanstack/react-query": "^4.19.1",
|
||||
"babel-plugin-transform-inline-environment-variables": "^0.4.4",
|
||||
"expo": "^47.0.0",
|
||||
"expo-av": "~13.0.2",
|
||||
"expo-av": "file:///home/anonymus-raccoon/projects/expo/packages/expo-av/",
|
||||
"expo-constants": "~14.0.2",
|
||||
"expo-linear-gradient": "~12.0.1",
|
||||
"expo-linking": "~3.2.3",
|
||||
|
@ -23,7 +23,7 @@
|
||||
"@tanstack/react-query": "^4.19.1",
|
||||
"clsx": "^1.2.1",
|
||||
"csstype": "^3.1.1",
|
||||
"expo-av": "^13.0.2",
|
||||
"expo-av": "file:///home/anonymus-raccoon/projects/expo/packages/expo-av/",
|
||||
"expo-linear-gradient": "^12.0.1",
|
||||
"hls.js": "^1.2.8",
|
||||
"i18next": "^22.0.6",
|
||||
|
@ -30,6 +30,7 @@ export * from "./tooltip";
|
||||
export * from "./container";
|
||||
export * from "./divider";
|
||||
export * from "./progress";
|
||||
export * from "./slider";
|
||||
|
||||
export * from "./animated";
|
||||
export * from "./utils";
|
||||
|
127
front/packages/primitives/src/slider.tsx
Normal file
127
front/packages/primitives/src/slider.tsx
Normal file
@ -0,0 +1,127 @@
|
||||
/*
|
||||
* 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 { useState } from "react";
|
||||
import { Platform, Pressable, View } from "react-native";
|
||||
import { percent, Stylable, useYoshiki } from "yoshiki/native";
|
||||
import { ts } from "./utils";
|
||||
|
||||
const calc =
|
||||
Platform.OS === "web"
|
||||
? (first: number, operator: "+" | "-" | "*" | "/", second: number): number =>
|
||||
`calc(${first} ${operator} ${second})` as unknown as number
|
||||
: (first: number, operator: "+" | "-" | "*" | "/", second: number): number => {
|
||||
switch (operator) {
|
||||
case "+":
|
||||
return first + second;
|
||||
case "-":
|
||||
return first - second;
|
||||
case "*":
|
||||
return first * second;
|
||||
case "/":
|
||||
return first / second;
|
||||
}
|
||||
};
|
||||
|
||||
export const Slider = ({
|
||||
progress,
|
||||
subtleProgress,
|
||||
max = 100,
|
||||
markers,
|
||||
...props
|
||||
}: { progress: number; max?: number; subtleProgress?: number; markers?: number[] } & Stylable) => {
|
||||
const { css } = useYoshiki();
|
||||
const [isSeeking, setSeek] = useState(false);
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
onTouchStart={(event) => {
|
||||
// // prevent drag and drop of the UI.
|
||||
// event.preventDefault();
|
||||
setSeek(true);
|
||||
}}
|
||||
{...css(
|
||||
{
|
||||
paddingVertical: ts(1),
|
||||
},
|
||||
props,
|
||||
)}
|
||||
>
|
||||
<View
|
||||
{...css({
|
||||
width: percent(100),
|
||||
height: ts(1),
|
||||
bg: (theme) => theme.overlay0,
|
||||
})}
|
||||
>
|
||||
{subtleProgress && (
|
||||
<View
|
||||
{...css({
|
||||
bg: (theme) => theme.overlay1,
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
width: percent((subtleProgress / max) * 100),
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
<View
|
||||
{...css({
|
||||
bg: (theme) => theme.accent,
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
width: percent((progress / max) * 100),
|
||||
})}
|
||||
/>
|
||||
{markers?.map((x) => (
|
||||
<View
|
||||
key={x}
|
||||
{...css({
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
left: percent(Math.min(100, (x / max) * 100)),
|
||||
bg: (theme) => theme.accent,
|
||||
width: ts(1),
|
||||
height: ts(1),
|
||||
borderRadius: ts(0.5),
|
||||
})}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
<View
|
||||
{...css({
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
margin: "auto",
|
||||
left: calc(percent((progress / max) * 100), "-", ts(1)),
|
||||
bg: (theme) => theme.accent,
|
||||
width: ts(2),
|
||||
height: ts(2),
|
||||
borderRadius: ts(1),
|
||||
})}
|
||||
/>
|
||||
</Pressable>
|
||||
);
|
||||
};
|
@ -59,7 +59,7 @@ export const LeftButtons = ({
|
||||
)}
|
||||
<IconButton
|
||||
icon={isPlaying ? Pause : PlayArrow}
|
||||
onClick={() => setPlay(!isPlaying)}
|
||||
onPress={() => setPlay(!isPlaying)}
|
||||
{...tooltip(isPlaying ? t("player.pause") : t("player.play"))}
|
||||
{...spacing}
|
||||
/>
|
||||
@ -139,8 +139,9 @@ const ProgressText = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const toTimerString = (timer: number, duration?: number) => {
|
||||
const toTimerString = (timer?: number, duration?: number) => {
|
||||
if (timer === undefined) return "??:??";
|
||||
if (!duration) duration = timer;
|
||||
if (duration >= 3600) return new Date(timer * 1000).toISOString().substring(11, 19);
|
||||
return new Date(timer * 1000).toISOString().substring(14, 19);
|
||||
if (duration >= 3600) return new Date(timer).toISOString().substring(11, 19);
|
||||
return new Date(timer).toISOString().substring(14, 19);
|
||||
};
|
||||
|
@ -19,7 +19,7 @@
|
||||
*/
|
||||
|
||||
import { Chapter } from "@kyoo/models";
|
||||
import { ts } from "@kyoo/primitives";
|
||||
import { ts, Slider } from "@kyoo/primitives";
|
||||
import { useAtom, useAtomValue } from "jotai";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { NativeTouchEvent, Pressable, Touchable, View } from "react-native";
|
||||
@ -27,14 +27,22 @@ import { useYoshiki, px, percent } from "yoshiki/native";
|
||||
import { bufferedAtom, durationAtom, progressAtom } from "../state";
|
||||
|
||||
export const ProgressBar = ({ chapters }: { chapters?: Chapter[] }) => {
|
||||
return null;
|
||||
const { css } = useYoshiki();
|
||||
const ref = useRef<View>(null);
|
||||
const [isSeeking, setSeek] = useState(false);
|
||||
const [progress, setProgress] = useAtom(progressAtom);
|
||||
const buffered = useAtomValue(bufferedAtom);
|
||||
const duration = useAtomValue(durationAtom);
|
||||
|
||||
return (
|
||||
<Slider
|
||||
progress={progress}
|
||||
subtleProgress={buffered}
|
||||
max={duration}
|
||||
markers={chapters?.map((x) => x.startTime)}
|
||||
/>
|
||||
);
|
||||
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;
|
||||
|
@ -21,13 +21,12 @@
|
||||
import { QueryIdentifier, QueryPage, WatchItem, WatchItemP, useFetch } from "@kyoo/models";
|
||||
import { Head } from "@kyoo/primitives";
|
||||
import { useState, useEffect, PointerEvent as ReactPointerEvent, ComponentProps } from "react";
|
||||
import { StyleSheet, View } from "react-native";
|
||||
import { PointerEvent, StyleSheet, View } from "react-native";
|
||||
import { useAtom, useAtomValue, useSetAtom } from "jotai";
|
||||
import { useRouter } from "solito/router";
|
||||
import { Video } from "expo-av";
|
||||
import { percent, useYoshiki } from "yoshiki/native";
|
||||
import { Hover, LoadingIndicator } from "./components/hover";
|
||||
import { fullscreenAtom, playAtom, useSubtitleController, useVideoController } from "./state";
|
||||
import { fullscreenAtom, playAtom, Video } from "./state";
|
||||
import { episodeDisplayNumber } from "../details/episode";
|
||||
import { useVideoKeyboard } from "./keyboard";
|
||||
import { MediaSessionManager } from "./media-session";
|
||||
@ -44,7 +43,7 @@ const query = (slug: string): QueryIdentifier<WatchItem> => ({
|
||||
|
||||
const mapData = (
|
||||
data: WatchItem | undefined,
|
||||
previousSlug: string,
|
||||
previousSlug?: string,
|
||||
nextSlug?: string,
|
||||
): Partial<ComponentProps<typeof Hover>> => {
|
||||
if (!data) return {};
|
||||
@ -61,6 +60,7 @@ const mapData = (
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
export const Player: QueryPage<{ slug: string }> = ({ slug }) => {
|
||||
const { css } = useYoshiki();
|
||||
|
||||
@ -152,17 +152,17 @@ export const Player: QueryPage<{ slug: string }> = ({ slug }) => {
|
||||
})}
|
||||
>
|
||||
<Video
|
||||
source={{ uri: data?.link.direct }}
|
||||
videoStyle={{ margin: "auto" }}
|
||||
links={data?.link}
|
||||
videoStyle={{ width: percent(100), height: percent(100) }}
|
||||
{...css(StyleSheet.absoluteFillObject)}
|
||||
/* {...videoProps} */
|
||||
// onPointerDown={(e: ReactPointerEvent<HTMLVideoElement>) => {
|
||||
// if (e.pointerType === "mouse") {
|
||||
// onClick={onVideoClick}
|
||||
// onPointerDown={(e: PointerEvent) => {
|
||||
// if (e.type === "mouse") {
|
||||
// onVideoClick();
|
||||
// } else if (mouseMoved) {
|
||||
// setMouseMoved(false);
|
||||
// } else {
|
||||
// mouseHasMoved();
|
||||
// // mouseHasMoved();
|
||||
// }
|
||||
// }}
|
||||
// onEnded={() => {
|
||||
|
@ -84,7 +84,7 @@ export const MediaSessionManager = ({
|
||||
navigator.mediaSession.playbackState = isPlaying ? "playing" : "paused";
|
||||
}, [isPlaying]);
|
||||
useEffect(() => {
|
||||
if (!("mediaSession" in navigator)) return;
|
||||
if (!("mediaSession" in navigator) || !duration) return;
|
||||
navigator.mediaSession.setPositionState({ position: progress, duration, playbackRate: 1 });
|
||||
}, [progress, duration]);
|
||||
|
||||
|
@ -18,11 +18,11 @@
|
||||
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Font, Track } from "@kyoo/models";
|
||||
import { Font, Track, WatchItem } from "@kyoo/models";
|
||||
import { atom, useAtom, useSetAtom } from "jotai";
|
||||
import { RefObject, useEffect, useRef } from "react";
|
||||
import { RefObject, useEffect, useRef, useState } from "react";
|
||||
import { createParam } from "solito";
|
||||
import { ResizeMode, VideoProps } from "expo-av";
|
||||
import { ResizeMode, Video as NativeVideo, VideoProps } from "expo-av";
|
||||
import SubtitleOctopus from "libass-wasm";
|
||||
import Hls from "hls.js";
|
||||
import { bakedAtom } from "../jotai-utils";
|
||||
@ -34,31 +34,13 @@ enum PlayMode {
|
||||
|
||||
const playModeAtom = atom<PlayMode>(PlayMode.Direct);
|
||||
|
||||
export const playerAtom = atom<RefObject<HTMLVideoElement> | null>(null);
|
||||
export const [_playAtom, playAtom] = bakedAtom(true, async (get, set, value) => {
|
||||
const player = get(playerAtom);
|
||||
if (!player?.current) return;
|
||||
if (value) {
|
||||
try {
|
||||
await player.current.play();
|
||||
} catch (e) {
|
||||
if (e instanceof DOMException && e.name === "NotSupportedError")
|
||||
set(playModeAtom, PlayMode.Transmux);
|
||||
else if (!(e instanceof DOMException && e.name === "NotAllowedError")) console.log(e);
|
||||
}
|
||||
} else {
|
||||
player.current.pause();
|
||||
}
|
||||
});
|
||||
export const playAtom = atom<boolean>(true);
|
||||
|
||||
export const loadAtom = atom(false);
|
||||
export const [_progressAtom, progressAtom] = bakedAtom(0, (get, set, value, baker) => {
|
||||
const player = get(playerAtom);
|
||||
if (!player?.current) return;
|
||||
set(baker, value);
|
||||
player.current.currentTime = value;
|
||||
});
|
||||
export const progressAtom = atom(0);
|
||||
export const bufferedAtom = atom(0);
|
||||
export const durationAtom = atom(1);
|
||||
export const durationAtom = atom<number | undefined>(undefined);
|
||||
|
||||
export const [_volumeAtom, volumeAtom] = bakedAtom(100, (get, set, value, baker) => {
|
||||
const player = get(playerAtom);
|
||||
if (!player?.current) return;
|
||||
@ -87,21 +69,27 @@ export const [_, fullscreenAtom] = bakedAtom(false, async (_, set, value, baker)
|
||||
|
||||
let hls: Hls | null = null;
|
||||
|
||||
export const useVideoController = (links?: { direct: string; transmux: string }) => {
|
||||
const player = useRef<HTMLVideoElement>(null);
|
||||
const setPlayer = useSetAtom(playerAtom);
|
||||
const setPlay = useSetAtom(_playAtom);
|
||||
const setPPlay = useSetAtom(playAtom);
|
||||
const setLoad = useSetAtom(loadAtom);
|
||||
const setProgress = useSetAtom(_progressAtom);
|
||||
const setBuffered = useSetAtom(bufferedAtom);
|
||||
const setDuration = useSetAtom(durationAtom);
|
||||
const setVolume = useSetAtom(_volumeAtom);
|
||||
const setMuted = useSetAtom(_mutedAtom);
|
||||
const setFullscreen = useSetAtom(fullscreenAtom);
|
||||
const [playMode, setPlayMode] = useAtom(playModeAtom);
|
||||
export const Video = ({ links, ...props }: { links?: WatchItem["link"] } & VideoProps) => {
|
||||
// const player = useRef<HTMLVideoElement>(null);
|
||||
// const setPlayer = useSetAtom(playerAtom);
|
||||
// const setLoad = useSetAtom(loadAtom);
|
||||
// const setVolume = useSetAtom(_volumeAtom);
|
||||
// const setMuted = useSetAtom(_mutedAtom);
|
||||
// const setFullscreen = useSetAtom(fullscreenAtom);
|
||||
// const [playMode, setPlayMode] = useAtom(playModeAtom);
|
||||
|
||||
setPlayer(player);
|
||||
const ref = useRef<NativeVideo | null>(null);
|
||||
const [isPlaying, setPlay] = useAtom(playAtom);
|
||||
const [progress, setProgress] = useAtom(progressAtom);
|
||||
const [buffered, setBuffered] = useAtom(bufferedAtom);
|
||||
const [duration, setDuration] = useAtom(durationAtom);
|
||||
|
||||
useEffect(() => {
|
||||
// I think this will trigger an infinite refresh loop
|
||||
// ref.current?.setStatusAsync({ positionMillis: progress });
|
||||
}, [progress]);
|
||||
|
||||
// setPlayer(player);
|
||||
|
||||
// useEffect(() => {
|
||||
// if (!player.current) return;
|
||||
@ -137,45 +125,53 @@ export const useVideoController = (links?: { direct: string; transmux: string })
|
||||
// if (!player?.current?.duration) return;
|
||||
// setDuration(player.current.duration);
|
||||
// }, [player, setDuration]);
|
||||
//
|
||||
|
||||
const videoProps: VideoProps = {
|
||||
// ref: player,
|
||||
// shouldPlay: isPlaying,
|
||||
// onDoubleClick: () => {
|
||||
// setFullscreen(!document.fullscreenElement);
|
||||
// },
|
||||
// onPlay: () => setPlay(true),
|
||||
// onPause: () => setPlay(false),
|
||||
// onWaiting: () => setLoad(true),
|
||||
// onCanPlay: () => setLoad(false),
|
||||
onError: () => {
|
||||
if (player?.current?.error?.code === MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED)
|
||||
setPlayMode(PlayMode.Transmux);
|
||||
},
|
||||
// onTimeUpdate: () => setProgress(player?.current?.currentTime ?? 0),
|
||||
// onDurationChange: () => setDuration(player?.current?.duration ?? 0),
|
||||
// onProgress: () =>
|
||||
// setBuffered(
|
||||
// player?.current?.buffered.length
|
||||
// ? player.current.buffered.end(player.current.buffered.length - 1)
|
||||
// : 0,
|
||||
// ),
|
||||
// onVolumeChange: () => {
|
||||
// if (!player.current) return;
|
||||
// setVolume(player.current.volume * 100);
|
||||
// setMuted(player?.current.muted);
|
||||
// },
|
||||
resizeMode: ResizeMode.CONTAIN,
|
||||
useNativeControls: false,
|
||||
};
|
||||
return {
|
||||
playerRef: player,
|
||||
videoProps,
|
||||
onVideoClick: () => {
|
||||
if (!player.current) return;
|
||||
setPPlay(player.current.paused);
|
||||
},
|
||||
};
|
||||
return (
|
||||
<NativeVideo
|
||||
ref={ref}
|
||||
{...props}
|
||||
source={links ? { uri: links.direct } : undefined}
|
||||
shouldPlay={isPlaying}
|
||||
onPlaybackStatusUpdate={(status) => {
|
||||
// TODO: Handle error state
|
||||
if (!status.isLoaded) return;
|
||||
|
||||
setPlay(status.shouldPlay);
|
||||
setProgress(status.positionMillis);
|
||||
setBuffered(status.playableDurationMillis ?? 0);
|
||||
setDuration(status.durationMillis);
|
||||
}}
|
||||
// ref: player,
|
||||
// shouldPlay: isPlaying,
|
||||
// onDoubleClick: () => {
|
||||
// setFullscreen(!document.fullscreenElement);
|
||||
// },
|
||||
// onPlay: () => setPlay(true),
|
||||
// onPause: () => setPlay(false),
|
||||
// onWaiting: () => setLoad(true),
|
||||
// onCanPlay: () => setLoad(false),
|
||||
// onError: () => {
|
||||
// if (player?.current?.error?.code === MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED)
|
||||
// setPlayMode(PlayMode.Transmux);
|
||||
// },
|
||||
// onTimeUpdate: () => setProgress(player?.current?.currentTime ?? 0),
|
||||
// onDurationChange: () => setDuration(player?.current?.duration ?? 0),
|
||||
// onProgress: () =>
|
||||
// setBuffered(
|
||||
// player?.current?.buffered.length
|
||||
// ? player.current.buffered.end(player.current.buffered.length - 1)
|
||||
// : 0,
|
||||
// ),
|
||||
// onVolumeChange: () => {
|
||||
// if (!player.current) return;
|
||||
// setVolume(player.current.volume * 100);
|
||||
// setMuted(player?.current.muted);
|
||||
// },
|
||||
resizeMode={ResizeMode.CONTAIN}
|
||||
useNativeControls={false}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const htmlTrackAtom = atom<HTMLTrackElement | null>(null);
|
||||
|
618
front/yarn.lock
618
front/yarn.lock
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user