mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-01-15 23:59:08 +00:00
chore: refactor
This commit is contained in:
@@ -36,11 +36,11 @@ import { useAtom } from "jotai";
|
||||
import { debounce } from "lodash";
|
||||
import React, { useCallback, useEffect, useRef, useState } from "react";
|
||||
import {
|
||||
GestureResponderEvent,
|
||||
Pressable,
|
||||
TouchableOpacity,
|
||||
useWindowDimensions,
|
||||
View,
|
||||
GestureResponderEvent,
|
||||
} from "react-native";
|
||||
import { Slider } from "react-native-awesome-slider";
|
||||
import {
|
||||
@@ -60,6 +60,9 @@ import DropdownViewTranscoding from "./dropdown/DropdownViewTranscoding";
|
||||
import { EpisodeList } from "./EpisodeList";
|
||||
import NextEpisodeCountDownButton from "./NextEpisodeCountDownButton";
|
||||
import SkipButton from "./SkipButton";
|
||||
import { useTapDetection } from "./useTapDetection";
|
||||
import { VideoTouchOverlay } from "./VideoTouchOverlay";
|
||||
import { useControlsTimeout } from "./useControlsTimeout";
|
||||
|
||||
interface Props {
|
||||
item: BaseItemDto;
|
||||
@@ -90,6 +93,8 @@ interface Props {
|
||||
isVlc?: boolean;
|
||||
}
|
||||
|
||||
const CONTROLS_TIMEOUT = 4000;
|
||||
|
||||
export const Controls: React.FC<Props> = ({
|
||||
item,
|
||||
seek,
|
||||
@@ -122,6 +127,12 @@ export const Controls: React.FC<Props> = ({
|
||||
const insets = useSafeAreaInsets();
|
||||
const [api] = useAtom(apiAtom);
|
||||
|
||||
const [episodeView, setEpisodeView] = useState(false);
|
||||
const [isSliding, setIsSliding] = useState(false);
|
||||
|
||||
// Used when user changes audio through audio button on device.
|
||||
const [showAudioSlider, setShowAudioSlider] = useState(false);
|
||||
|
||||
const { height: screenHeight, width: screenWidth } = useWindowDimensions();
|
||||
const { previousItem, nextItem } = useAdjacentItems({ item });
|
||||
const {
|
||||
@@ -140,6 +151,23 @@ export const Controls: React.FC<Props> = ({
|
||||
const wasPlayingRef = useRef(false);
|
||||
const lastProgressRef = useRef<number>(0);
|
||||
|
||||
const lightHapticFeedback = useHaptic("light");
|
||||
|
||||
useEffect(() => {
|
||||
prefetchAllTrickplayImages();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (item) {
|
||||
progress.value = isVlc
|
||||
? ticksToMs(item?.UserData?.PlaybackPositionTicks)
|
||||
: item?.UserData?.PlaybackPositionTicks || 0;
|
||||
max.value = isVlc
|
||||
? ticksToMs(item.RunTimeTicks || 0)
|
||||
: item.RunTimeTicks || 0;
|
||||
}
|
||||
}, [item, isVlc]);
|
||||
|
||||
const { bitrateValue, subtitleIndex, audioIndex } = useLocalSearchParams<{
|
||||
bitrateValue: string;
|
||||
audioIndex: string;
|
||||
@@ -162,8 +190,6 @@ export const Controls: React.FC<Props> = ({
|
||||
isVlc
|
||||
);
|
||||
|
||||
const lightHapticFeedback = useHaptic("light");
|
||||
|
||||
const goToPreviousItem = useCallback(() => {
|
||||
if (!previousItem || !settings) return;
|
||||
|
||||
@@ -267,52 +293,21 @@ export const Controls: React.FC<Props> = ({
|
||||
[updateTimes]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (item) {
|
||||
progress.value = isVlc
|
||||
? ticksToMs(item?.UserData?.PlaybackPositionTicks)
|
||||
: item?.UserData?.PlaybackPositionTicks || 0;
|
||||
max.value = isVlc
|
||||
? ticksToMs(item.RunTimeTicks || 0)
|
||||
: item.RunTimeTicks || 0;
|
||||
}
|
||||
}, [item, isVlc]);
|
||||
|
||||
useEffect(() => {
|
||||
prefetchAllTrickplayImages();
|
||||
const hideControls = useCallback(() => {
|
||||
setShowControls(false);
|
||||
setShowAudioSlider(false);
|
||||
}, []);
|
||||
|
||||
const CONTROLS_TIMEOUT = 5000;
|
||||
const controlsTimeoutRef = useRef<NodeJS.Timeout>();
|
||||
|
||||
useEffect(() => {
|
||||
const resetControlsTimeout = () => {
|
||||
if (controlsTimeoutRef.current) {
|
||||
clearTimeout(controlsTimeoutRef.current);
|
||||
}
|
||||
|
||||
if (showControls && !isSliding && !EpisodeView) {
|
||||
controlsTimeoutRef.current = setTimeout(() => {
|
||||
setShowControls(false);
|
||||
setShowAudioSlider(false);
|
||||
}, CONTROLS_TIMEOUT);
|
||||
}
|
||||
};
|
||||
|
||||
resetControlsTimeout();
|
||||
|
||||
return () => {
|
||||
if (controlsTimeoutRef.current) {
|
||||
clearTimeout(controlsTimeoutRef.current);
|
||||
}
|
||||
};
|
||||
}, [showControls, isSliding, EpisodeView]);
|
||||
const { handleControlsInteraction } = useControlsTimeout({
|
||||
showControls,
|
||||
isSliding,
|
||||
episodeView,
|
||||
onHideControls: hideControls,
|
||||
timeout: CONTROLS_TIMEOUT,
|
||||
});
|
||||
|
||||
const toggleControls = () => {
|
||||
if (showControls) {
|
||||
if (controlsTimeoutRef.current) {
|
||||
clearTimeout(controlsTimeoutRef.current);
|
||||
}
|
||||
setShowAudioSlider(false);
|
||||
setShowControls(false);
|
||||
} else {
|
||||
@@ -320,18 +315,6 @@ export const Controls: React.FC<Props> = ({
|
||||
}
|
||||
};
|
||||
|
||||
const handleControlsInteraction = () => {
|
||||
if (showControls) {
|
||||
if (controlsTimeoutRef.current) {
|
||||
clearTimeout(controlsTimeoutRef.current);
|
||||
}
|
||||
controlsTimeoutRef.current = setTimeout(() => {
|
||||
setShowControls(false);
|
||||
setShowAudioSlider(false);
|
||||
}, CONTROLS_TIMEOUT);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSliderStart = useCallback(() => {
|
||||
if (showControls === false) return;
|
||||
|
||||
@@ -343,16 +326,13 @@ export const Controls: React.FC<Props> = ({
|
||||
isSeeking.value = true;
|
||||
}, [showControls, isPlaying]);
|
||||
|
||||
const [isSliding, setIsSliding] = useState(false);
|
||||
const handleSliderComplete = useCallback(
|
||||
async (value: number) => {
|
||||
isSeeking.value = false;
|
||||
progress.value = value;
|
||||
setIsSliding(false);
|
||||
|
||||
await seek(
|
||||
Math.max(0, Math.floor(isVlc ? value : ticksToSeconds(value)))
|
||||
);
|
||||
seek(Math.max(0, Math.floor(isVlc ? value : ticksToSeconds(value))));
|
||||
if (wasPlayingRef.current === true) play();
|
||||
},
|
||||
[isVlc]
|
||||
@@ -382,7 +362,7 @@ export const Controls: React.FC<Props> = ({
|
||||
const newTime = isVlc
|
||||
? Math.max(0, curr - secondsToMs(settings.rewindSkipTime))
|
||||
: Math.max(0, ticksToSeconds(curr) - settings.rewindSkipTime);
|
||||
await seek(newTime);
|
||||
seek(newTime);
|
||||
if (wasPlayingRef.current === true) play();
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -400,7 +380,7 @@ export const Controls: React.FC<Props> = ({
|
||||
const newTime = isVlc
|
||||
? curr + secondsToMs(settings.forwardSkipTime)
|
||||
: ticksToSeconds(curr) + settings.forwardSkipTime;
|
||||
await seek(Math.max(0, newTime));
|
||||
seek(Math.max(0, newTime));
|
||||
if (wasPlayingRef.current === true) play();
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -408,11 +388,62 @@ export const Controls: React.FC<Props> = ({
|
||||
}
|
||||
}, [settings, isPlaying, isVlc]);
|
||||
|
||||
const goToItem = useCallback(
|
||||
async (itemId: string) => {
|
||||
try {
|
||||
const gotoItem = await getItemById(api, itemId);
|
||||
if (!settings || !gotoItem) return;
|
||||
|
||||
lightHapticFeedback();
|
||||
|
||||
const previousIndexes: previousIndexes = {
|
||||
subtitleIndex: subtitleIndex ? parseInt(subtitleIndex) : undefined,
|
||||
audioIndex: audioIndex ? parseInt(audioIndex) : undefined,
|
||||
};
|
||||
|
||||
const {
|
||||
mediaSource: newMediaSource,
|
||||
audioIndex: defaultAudioIndex,
|
||||
subtitleIndex: defaultSubtitleIndex,
|
||||
} = getDefaultPlaySettings(
|
||||
gotoItem,
|
||||
settings,
|
||||
previousIndexes,
|
||||
mediaSource ?? undefined
|
||||
);
|
||||
|
||||
const queryParams = new URLSearchParams({
|
||||
itemId: gotoItem.Id ?? "", // Ensure itemId is a string
|
||||
audioIndex: defaultAudioIndex?.toString() ?? "",
|
||||
subtitleIndex: defaultSubtitleIndex?.toString() ?? "",
|
||||
mediaSourceId: newMediaSource?.Id ?? "", // Ensure mediaSourceId is a string
|
||||
bitrateValue: bitrateValue.toString(),
|
||||
}).toString();
|
||||
|
||||
if (!bitrateValue) {
|
||||
// @ts-expect-error
|
||||
router.replace(`player/direct-player?${queryParams}`);
|
||||
return;
|
||||
}
|
||||
// @ts-expect-error
|
||||
router.replace(`player/transcoding-player?${queryParams}`);
|
||||
} catch (error) {
|
||||
console.error("Error in gotoEpisode:", error);
|
||||
}
|
||||
},
|
||||
[settings, subtitleIndex, audioIndex]
|
||||
);
|
||||
|
||||
const toggleIgnoreSafeAreas = useCallback(() => {
|
||||
setIgnoreSafeAreas((prev) => !prev);
|
||||
lightHapticFeedback();
|
||||
}, []);
|
||||
|
||||
const switchOnEpisodeMode = useCallback(() => {
|
||||
setEpisodeView(true);
|
||||
if (isPlaying) togglePlay();
|
||||
}, [isPlaying, togglePlay]);
|
||||
|
||||
const memoizedRenderBubble = useCallback(() => {
|
||||
if (!trickPlayUrl || !trickplayInfo) {
|
||||
return null;
|
||||
@@ -476,99 +507,13 @@ export const Controls: React.FC<Props> = ({
|
||||
);
|
||||
}, [trickPlayUrl, trickplayInfo, time]);
|
||||
|
||||
const [EpisodeView, setEpisodeView] = useState(false);
|
||||
|
||||
const switchOnEpisodeMode = () => {
|
||||
setEpisodeView(true);
|
||||
if (isPlaying) togglePlay();
|
||||
};
|
||||
|
||||
const goToItem = useCallback(
|
||||
async (itemId: string) => {
|
||||
try {
|
||||
const gotoItem = await getItemById(api, itemId);
|
||||
if (!settings || !gotoItem) return;
|
||||
|
||||
lightHapticFeedback();
|
||||
|
||||
const previousIndexes: previousIndexes = {
|
||||
subtitleIndex: subtitleIndex ? parseInt(subtitleIndex) : undefined,
|
||||
audioIndex: audioIndex ? parseInt(audioIndex) : undefined,
|
||||
};
|
||||
|
||||
const {
|
||||
mediaSource: newMediaSource,
|
||||
audioIndex: defaultAudioIndex,
|
||||
subtitleIndex: defaultSubtitleIndex,
|
||||
} = getDefaultPlaySettings(
|
||||
gotoItem,
|
||||
settings,
|
||||
previousIndexes,
|
||||
mediaSource ?? undefined
|
||||
);
|
||||
|
||||
const queryParams = new URLSearchParams({
|
||||
itemId: gotoItem.Id ?? "", // Ensure itemId is a string
|
||||
audioIndex: defaultAudioIndex?.toString() ?? "",
|
||||
subtitleIndex: defaultSubtitleIndex?.toString() ?? "",
|
||||
mediaSourceId: newMediaSource?.Id ?? "", // Ensure mediaSourceId is a string
|
||||
bitrateValue: bitrateValue.toString(),
|
||||
}).toString();
|
||||
|
||||
if (!bitrateValue) {
|
||||
// @ts-expect-error
|
||||
router.replace(`player/direct-player?${queryParams}`);
|
||||
return;
|
||||
}
|
||||
// @ts-expect-error
|
||||
router.replace(`player/transcoding-player?${queryParams}`);
|
||||
} catch (error) {
|
||||
console.error("Error in gotoEpisode:", error);
|
||||
}
|
||||
},
|
||||
[settings, subtitleIndex, audioIndex]
|
||||
);
|
||||
|
||||
// Used when user changes audio through audio button on device.
|
||||
const [showAudioSlider, setShowAudioSlider] = useState(false);
|
||||
|
||||
// Prevent opening controls when user swipes on the screen.
|
||||
const touchStartTime = useRef(0);
|
||||
const touchStartPosition = useRef({ x: 0, y: 0 });
|
||||
|
||||
const handleTouchStart = (event: GestureResponderEvent) => {
|
||||
touchStartTime.current = Date.now();
|
||||
touchStartPosition.current = {
|
||||
x: event.nativeEvent.pageX,
|
||||
y: event.nativeEvent.pageY,
|
||||
};
|
||||
};
|
||||
|
||||
const handleTouchEnd = (event: GestureResponderEvent) => {
|
||||
const touchEndTime = Date.now();
|
||||
const touchEndPosition = {
|
||||
x: event.nativeEvent.pageX,
|
||||
y: event.nativeEvent.pageY,
|
||||
};
|
||||
|
||||
const touchDuration = touchEndTime - touchStartTime.current;
|
||||
const touchDistance = Math.sqrt(
|
||||
Math.pow(touchEndPosition.x - touchStartPosition.current.x, 2) +
|
||||
Math.pow(touchEndPosition.y - touchStartPosition.current.y, 2)
|
||||
);
|
||||
|
||||
if (touchDuration < 200 && touchDistance < 10) {
|
||||
toggleControls();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<ControlProvider
|
||||
item={item}
|
||||
mediaSource={mediaSource}
|
||||
isVideoLoaded={isVideoLoaded}
|
||||
>
|
||||
{EpisodeView ? (
|
||||
{episodeView ? (
|
||||
<EpisodeList
|
||||
item={item}
|
||||
close={() => setEpisodeView(false)}
|
||||
@@ -576,22 +521,12 @@ export const Controls: React.FC<Props> = ({
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<Pressable
|
||||
onTouchStart={handleTouchStart}
|
||||
onTouchEnd={handleTouchEnd}
|
||||
style={{
|
||||
position: "absolute",
|
||||
width: screenWidth,
|
||||
height: screenHeight,
|
||||
backgroundColor: "black",
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
opacity: showControls ? 0.5 : 0,
|
||||
}}
|
||||
<VideoTouchOverlay
|
||||
screenWidth={screenWidth}
|
||||
screenHeight={screenHeight}
|
||||
showControls={showControls}
|
||||
onToggleControls={toggleControls}
|
||||
/>
|
||||
|
||||
<View
|
||||
style={[
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user