mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-06-05 21:48:31 +01:00
feat: go to next episode countdown
This commit is contained in:
@@ -3,7 +3,8 @@ import React, { PropsWithChildren, ReactNode, useMemo } from "react";
|
|||||||
import { Text, TouchableOpacity, View } from "react-native";
|
import { Text, TouchableOpacity, View } from "react-native";
|
||||||
import { Loader } from "./Loader";
|
import { Loader } from "./Loader";
|
||||||
|
|
||||||
interface ButtonProps extends React.ComponentProps<typeof TouchableOpacity> {
|
export interface ButtonProps
|
||||||
|
extends React.ComponentProps<typeof TouchableOpacity> {
|
||||||
onPress?: () => void;
|
onPress?: () => void;
|
||||||
className?: string;
|
className?: string;
|
||||||
textClassName?: string;
|
textClassName?: string;
|
||||||
|
|||||||
@@ -8,8 +8,10 @@ import {
|
|||||||
TrackInfo,
|
TrackInfo,
|
||||||
VlcPlayerViewRef,
|
VlcPlayerViewRef,
|
||||||
} from "@/modules/vlc-player/src/VlcPlayer.types";
|
} from "@/modules/vlc-player/src/VlcPlayer.types";
|
||||||
|
import { apiAtom } from "@/providers/JellyfinProvider";
|
||||||
import { useSettings } from "@/utils/atoms/settings";
|
import { useSettings } from "@/utils/atoms/settings";
|
||||||
import { getDefaultPlaySettings } from "@/utils/jellyfin/getDefaultPlaySettings";
|
import { getDefaultPlaySettings } from "@/utils/jellyfin/getDefaultPlaySettings";
|
||||||
|
import { getItemById } from "@/utils/jellyfin/user-library/getItemById";
|
||||||
import { writeToLog } from "@/utils/log";
|
import { writeToLog } from "@/utils/log";
|
||||||
import {
|
import {
|
||||||
formatTimeString,
|
formatTimeString,
|
||||||
@@ -23,8 +25,11 @@ import {
|
|||||||
BaseItemDto,
|
BaseItemDto,
|
||||||
MediaSourceInfo,
|
MediaSourceInfo,
|
||||||
} from "@jellyfin/sdk/lib/generated-client";
|
} from "@jellyfin/sdk/lib/generated-client";
|
||||||
|
import * as Haptics from "expo-haptics";
|
||||||
import { Image } from "expo-image";
|
import { Image } from "expo-image";
|
||||||
import { useLocalSearchParams, useRouter } from "expo-router";
|
import { useLocalSearchParams, useRouter } from "expo-router";
|
||||||
|
import { useAtom } from "jotai";
|
||||||
|
import { debounce } from "lodash";
|
||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import { Dimensions, Pressable, TouchableOpacity, View } from "react-native";
|
import { Dimensions, Pressable, TouchableOpacity, View } from "react-native";
|
||||||
import { Slider } from "react-native-awesome-slider";
|
import { Slider } from "react-native-awesome-slider";
|
||||||
@@ -36,19 +41,15 @@ import {
|
|||||||
} from "react-native-reanimated";
|
} from "react-native-reanimated";
|
||||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||||
import { VideoRef } from "react-native-video";
|
import { VideoRef } from "react-native-video";
|
||||||
|
import AudioSlider from "./AudioSlider";
|
||||||
|
import BrightnessSlider from "./BrightnessSlider";
|
||||||
import { ControlProvider } from "./contexts/ControlContext";
|
import { ControlProvider } from "./contexts/ControlContext";
|
||||||
import { VideoProvider } from "./contexts/VideoContext";
|
import { VideoProvider } from "./contexts/VideoContext";
|
||||||
import * as Haptics from "expo-haptics";
|
|
||||||
import DropdownViewDirect from "./dropdown/DropdownViewDirect";
|
import DropdownViewDirect from "./dropdown/DropdownViewDirect";
|
||||||
import DropdownViewTranscoding from "./dropdown/DropdownViewTranscoding";
|
import DropdownViewTranscoding from "./dropdown/DropdownViewTranscoding";
|
||||||
import BrightnessSlider from "./BrightnessSlider";
|
|
||||||
import SkipButton from "./SkipButton";
|
|
||||||
import { debounce } from "lodash";
|
|
||||||
import { EpisodeList } from "./EpisodeList";
|
import { EpisodeList } from "./EpisodeList";
|
||||||
import { getItemById } from "@/utils/jellyfin/user-library/getItemById";
|
import NextEpisodeCountDownButton from "./NextEpisodeCountDownButton";
|
||||||
import { useAtom } from "jotai";
|
import SkipButton from "./SkipButton";
|
||||||
import { apiAtom } from "@/providers/JellyfinProvider";
|
|
||||||
import AudioSlider from "./AudioSlider";
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
item: BaseItemDto;
|
item: BaseItemDto;
|
||||||
@@ -120,7 +121,7 @@ export const Controls: React.FC<Props> = ({
|
|||||||
} = useTrickplay(item, !offline && enableTrickplay);
|
} = useTrickplay(item, !offline && enableTrickplay);
|
||||||
|
|
||||||
const [currentTime, setCurrentTime] = useState(0);
|
const [currentTime, setCurrentTime] = useState(0);
|
||||||
const [remainingTime, setRemainingTime] = useState(0);
|
const [remainingTime, setRemainingTime] = useState(Infinity);
|
||||||
|
|
||||||
const min = useSharedValue(0);
|
const min = useSharedValue(0);
|
||||||
const max = useSharedValue(item.RunTimeTicks || 0);
|
const max = useSharedValue(item.RunTimeTicks || 0);
|
||||||
@@ -209,15 +210,10 @@ export const Controls: React.FC<Props> = ({
|
|||||||
? maxValue - currentProgress
|
? maxValue - currentProgress
|
||||||
: ticksToSeconds(maxValue - currentProgress);
|
: ticksToSeconds(maxValue - currentProgress);
|
||||||
|
|
||||||
|
console.log("remaining: ", remaining);
|
||||||
|
|
||||||
setCurrentTime(current);
|
setCurrentTime(current);
|
||||||
setRemainingTime(remaining);
|
setRemainingTime(remaining);
|
||||||
|
|
||||||
// Currently doesm't work in VLC because of some corrupted timestamps, will need to find a workaround.
|
|
||||||
if (currentProgress === maxValue) {
|
|
||||||
setShowControls(true);
|
|
||||||
// Automatically play the next item if it exists
|
|
||||||
goToNextItem();
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
[goToNextItem, isVlc]
|
[goToNextItem, isVlc]
|
||||||
);
|
);
|
||||||
@@ -229,7 +225,6 @@ export const Controls: React.FC<Props> = ({
|
|||||||
isSeeking: isSeeking.value,
|
isSeeking: isSeeking.value,
|
||||||
}),
|
}),
|
||||||
(result) => {
|
(result) => {
|
||||||
// console.log("Progress changed", result);
|
|
||||||
if (result.isSeeking === false) {
|
if (result.isSeeking === false) {
|
||||||
runOnJS(updateTimes)(result.progress, result.max);
|
runOnJS(updateTimes)(result.progress, result.max);
|
||||||
}
|
}
|
||||||
@@ -290,7 +285,6 @@ export const Controls: React.FC<Props> = ({
|
|||||||
const handleSliderChange = useCallback(
|
const handleSliderChange = useCallback(
|
||||||
debounce((value: number) => {
|
debounce((value: number) => {
|
||||||
const progressInTicks = isVlc ? msToTicks(value) : value;
|
const progressInTicks = isVlc ? msToTicks(value) : value;
|
||||||
console.log("Progress in ticks", progressInTicks);
|
|
||||||
calculateTrickplayUrl(progressInTicks);
|
calculateTrickplayUrl(progressInTicks);
|
||||||
const progressInSeconds = Math.floor(ticksToSeconds(progressInTicks));
|
const progressInSeconds = Math.floor(ticksToSeconds(progressInTicks));
|
||||||
const hours = Math.floor(progressInSeconds / 3600);
|
const hours = Math.floor(progressInSeconds / 3600);
|
||||||
@@ -663,10 +657,8 @@ export const Controls: React.FC<Props> = ({
|
|||||||
right: 0,
|
right: 0,
|
||||||
left: 0,
|
left: 0,
|
||||||
bottom: 0,
|
bottom: 0,
|
||||||
opacity: showControls ? 1 : 0,
|
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
pointerEvents={showControls ? "box-none" : "none"}
|
|
||||||
className={`flex flex-col p-4`}
|
className={`flex flex-col p-4`}
|
||||||
>
|
>
|
||||||
<View
|
<View
|
||||||
@@ -680,7 +672,9 @@ export const Controls: React.FC<Props> = ({
|
|||||||
style={{
|
style={{
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
alignSelf: "flex-end", // Shrink height based on content
|
alignSelf: "flex-end", // Shrink height based on content
|
||||||
|
opacity: showControls ? 1 : 0,
|
||||||
}}
|
}}
|
||||||
|
pointerEvents={showControls ? "box-none" : "none"}
|
||||||
>
|
>
|
||||||
<Text className="font-bold">{item?.Name}</Text>
|
<Text className="font-bold">{item?.Name}</Text>
|
||||||
{item?.Type === "Episode" && (
|
{item?.Type === "Episode" && (
|
||||||
@@ -699,7 +693,6 @@ export const Controls: React.FC<Props> = ({
|
|||||||
style={{
|
style={{
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
alignSelf: "flex-end",
|
alignSelf: "flex-end",
|
||||||
marginRight: insets.right,
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SkipButton
|
<SkipButton
|
||||||
@@ -712,10 +705,19 @@ export const Controls: React.FC<Props> = ({
|
|||||||
onPress={skipCredit}
|
onPress={skipCredit}
|
||||||
buttonText="Skip Credits"
|
buttonText="Skip Credits"
|
||||||
/>
|
/>
|
||||||
|
<NextEpisodeCountDownButton
|
||||||
|
show={isVlc ? remainingTime < 10000 : remainingTime < 10}
|
||||||
|
onFinish={goToNextItem}
|
||||||
|
onPress={goToNextItem}
|
||||||
|
/>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
<View
|
<View
|
||||||
className={`flex flex-col-reverse py-4 pb-1 px-4 rounded-lg items-center bg-neutral-800`}
|
className={`flex flex-col-reverse py-4 pb-1 px-4 rounded-lg items-center bg-neutral-800`}
|
||||||
|
style={{
|
||||||
|
opacity: showControls ? 1 : 0,
|
||||||
|
}}
|
||||||
|
pointerEvents={showControls ? "box-none" : "none"}
|
||||||
>
|
>
|
||||||
<View className={`flex flex-col w-full shrink`}>
|
<View className={`flex flex-col w-full shrink`}>
|
||||||
<Slider
|
<Slider
|
||||||
|
|||||||
@@ -0,0 +1,81 @@
|
|||||||
|
import React, { useEffect } from "react";
|
||||||
|
import { TouchableOpacity, TouchableOpacityProps, View } from "react-native";
|
||||||
|
import { Text } from "@/components/common/Text";
|
||||||
|
import Animated, {
|
||||||
|
useAnimatedStyle,
|
||||||
|
useSharedValue,
|
||||||
|
withTiming,
|
||||||
|
Easing,
|
||||||
|
runOnJS,
|
||||||
|
} from "react-native-reanimated";
|
||||||
|
import { Colors } from "@/constants/Colors";
|
||||||
|
|
||||||
|
interface NextEpisodeCountDownButtonProps extends TouchableOpacityProps {
|
||||||
|
onFinish?: () => void;
|
||||||
|
onPress?: () => void;
|
||||||
|
show: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const NextEpisodeCountDownButton: React.FC<NextEpisodeCountDownButtonProps> = ({
|
||||||
|
onFinish,
|
||||||
|
onPress,
|
||||||
|
show,
|
||||||
|
...props
|
||||||
|
}) => {
|
||||||
|
const progress = useSharedValue(0);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (show) {
|
||||||
|
progress.value = 0;
|
||||||
|
progress.value = withTiming(
|
||||||
|
1,
|
||||||
|
{
|
||||||
|
duration: 10000, // 10 seconds
|
||||||
|
easing: Easing.linear,
|
||||||
|
},
|
||||||
|
(finished) => {
|
||||||
|
if (finished && onFinish) {
|
||||||
|
console.log("finish");
|
||||||
|
runOnJS(onFinish)();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, [show, onFinish]);
|
||||||
|
|
||||||
|
const animatedStyle = useAnimatedStyle(() => {
|
||||||
|
return {
|
||||||
|
position: "absolute",
|
||||||
|
left: 0,
|
||||||
|
top: 0,
|
||||||
|
bottom: 0,
|
||||||
|
width: `${progress.value * 100}%`,
|
||||||
|
backgroundColor: Colors.primary,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const handlePress = () => {
|
||||||
|
if (onPress) {
|
||||||
|
onPress();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!show) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TouchableOpacity
|
||||||
|
className="w-32 overflow-hidden rounded-md bg-neutral-900"
|
||||||
|
{...props}
|
||||||
|
onPress={handlePress}
|
||||||
|
>
|
||||||
|
<Animated.View style={animatedStyle} />
|
||||||
|
<View className="px-2 py-3">
|
||||||
|
<Text className="text-center font-bold">Next Episode</Text>
|
||||||
|
</View>
|
||||||
|
</TouchableOpacity>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NextEpisodeCountDownButton;
|
||||||
Reference in New Issue
Block a user