mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-01-15 15:48:05 +00:00
697 lines
21 KiB
TypeScript
697 lines
21 KiB
TypeScript
import { Api } from "@jellyfin/sdk";
|
|
import type {
|
|
BaseItemDto,
|
|
MediaSourceInfo,
|
|
} from "@jellyfin/sdk/lib/generated-client";
|
|
import { useLocalSearchParams } from "expo-router";
|
|
import {
|
|
type FC,
|
|
useCallback,
|
|
useEffect,
|
|
useMemo,
|
|
useRef,
|
|
useState,
|
|
} from "react";
|
|
import { useTranslation } from "react-i18next";
|
|
import { StyleSheet, useWindowDimensions, View } from "react-native";
|
|
import Animated, {
|
|
Easing,
|
|
type SharedValue,
|
|
useAnimatedReaction,
|
|
useAnimatedStyle,
|
|
useSharedValue,
|
|
withTiming,
|
|
} from "react-native-reanimated";
|
|
import ContinueWatchingOverlay from "@/components/video-player/controls/ContinueWatchingOverlay";
|
|
import useRouter from "@/hooks/useAppRouter";
|
|
import { useHaptic } from "@/hooks/useHaptic";
|
|
import { usePlaybackManager } from "@/hooks/usePlaybackManager";
|
|
import { useSegmentSkipper } from "@/hooks/useSegmentSkipper";
|
|
import { useTrickplay } from "@/hooks/useTrickplay";
|
|
import type { TechnicalInfo } from "@/modules/mpv-player";
|
|
import { DownloadedItem } from "@/providers/Downloads/types";
|
|
import { useOfflineMode } from "@/providers/OfflineModeProvider";
|
|
import { useSettings } from "@/utils/atoms/settings";
|
|
import { getDefaultPlaySettings } from "@/utils/jellyfin/getDefaultPlaySettings";
|
|
import { useSegments } from "@/utils/segments";
|
|
import { msToSeconds, ticksToMs } from "@/utils/time";
|
|
import { BottomControls } from "./BottomControls";
|
|
import { CenterControls } from "./CenterControls";
|
|
import { CONTROLS_CONSTANTS } from "./constants";
|
|
import { EpisodeList } from "./EpisodeList";
|
|
import { GestureOverlay } from "./GestureOverlay";
|
|
import { HeaderControls } from "./HeaderControls";
|
|
import { useRemoteControl } from "./hooks/useRemoteControl";
|
|
import { useVideoNavigation } from "./hooks/useVideoNavigation";
|
|
import { useVideoSlider } from "./hooks/useVideoSlider";
|
|
import { useVideoTime } from "./hooks/useVideoTime";
|
|
import { TechnicalInfoOverlay } from "./TechnicalInfoOverlay";
|
|
import { useControlsTimeout } from "./useControlsTimeout";
|
|
import { PlaybackSpeedScope } from "./utils/playback-speed-settings";
|
|
import { type AspectRatio } from "./VideoScalingModeSelector";
|
|
|
|
// No-op function to avoid creating new references on every render
|
|
const noop = () => {};
|
|
|
|
interface Props {
|
|
item: BaseItemDto;
|
|
isPlaying: boolean;
|
|
isSeeking: SharedValue<boolean>;
|
|
cacheProgress: SharedValue<number>;
|
|
progress: SharedValue<number>;
|
|
isBuffering: boolean;
|
|
showControls: boolean;
|
|
enableTrickplay?: boolean;
|
|
togglePlay: () => void;
|
|
setShowControls: (shown: boolean) => void;
|
|
mediaSource?: MediaSourceInfo | null;
|
|
seek: (ticks: number) => void;
|
|
startPictureInPicture?: () => Promise<void>;
|
|
play: () => void;
|
|
pause: () => void;
|
|
aspectRatio?: AspectRatio;
|
|
isZoomedToFill?: boolean;
|
|
onZoomToggle?: () => void;
|
|
api?: Api | null;
|
|
downloadedFiles?: DownloadedItem[];
|
|
// Playback speed props
|
|
playbackSpeed?: number;
|
|
setPlaybackSpeed?: (speed: number, scope: PlaybackSpeedScope) => void;
|
|
// Technical info props
|
|
showTechnicalInfo?: boolean;
|
|
onToggleTechnicalInfo?: () => void;
|
|
getTechnicalInfo?: () => Promise<TechnicalInfo>;
|
|
playMethod?: "DirectPlay" | "DirectStream" | "Transcode";
|
|
transcodeReasons?: string[];
|
|
}
|
|
|
|
export const Controls: FC<Props> = ({
|
|
item,
|
|
seek,
|
|
startPictureInPicture,
|
|
play,
|
|
pause,
|
|
togglePlay,
|
|
isPlaying,
|
|
isSeeking,
|
|
progress,
|
|
isBuffering,
|
|
cacheProgress,
|
|
showControls,
|
|
setShowControls,
|
|
mediaSource,
|
|
aspectRatio = "default",
|
|
isZoomedToFill = false,
|
|
onZoomToggle,
|
|
api = null,
|
|
downloadedFiles = undefined,
|
|
playbackSpeed = 1.0,
|
|
setPlaybackSpeed,
|
|
showTechnicalInfo = false,
|
|
onToggleTechnicalInfo,
|
|
getTechnicalInfo,
|
|
playMethod,
|
|
transcodeReasons,
|
|
}) => {
|
|
const offline = useOfflineMode();
|
|
const { settings, updateSettings } = useSettings();
|
|
const router = useRouter();
|
|
const lightHapticFeedback = useHaptic("light");
|
|
|
|
const [episodeView, setEpisodeView] = useState(false);
|
|
const [showAudioSlider, setShowAudioSlider] = useState(false);
|
|
|
|
// Ref to track pending play timeout for cleanup and cancellation
|
|
const playTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
|
|
// Clean up timeout on unmount
|
|
useEffect(() => {
|
|
return () => {
|
|
if (playTimeoutRef.current) {
|
|
clearTimeout(playTimeoutRef.current);
|
|
}
|
|
};
|
|
}, []);
|
|
|
|
const { height: screenHeight, width: screenWidth } = useWindowDimensions();
|
|
const { previousItem, nextItem } = usePlaybackManager({
|
|
item,
|
|
isOffline: offline,
|
|
});
|
|
|
|
const {
|
|
trickPlayUrl,
|
|
calculateTrickplayUrl,
|
|
trickplayInfo,
|
|
prefetchAllTrickplayImages,
|
|
} = useTrickplay(item);
|
|
|
|
const min = useSharedValue(0);
|
|
// Regular value for use during render (avoids Reanimated warning)
|
|
const maxMs = ticksToMs(item.RunTimeTicks || 0);
|
|
const max = useSharedValue(maxMs);
|
|
|
|
// Animation values for controls
|
|
const controlsOpacity = useSharedValue(showControls ? 1 : 0);
|
|
const headerTranslateY = useSharedValue(showControls ? 0 : -50);
|
|
const bottomTranslateY = useSharedValue(showControls ? 0 : 50);
|
|
|
|
useEffect(() => {
|
|
prefetchAllTrickplayImages();
|
|
}, [prefetchAllTrickplayImages]);
|
|
|
|
// Animate controls visibility
|
|
useEffect(() => {
|
|
const animationConfig = {
|
|
duration: 300,
|
|
easing: Easing.out(Easing.quad),
|
|
};
|
|
|
|
controlsOpacity.value = withTiming(showControls ? 1 : 0, animationConfig);
|
|
headerTranslateY.value = withTiming(
|
|
showControls ? 0 : -10,
|
|
animationConfig,
|
|
);
|
|
bottomTranslateY.value = withTiming(showControls ? 0 : 10, animationConfig);
|
|
}, [showControls, controlsOpacity, headerTranslateY, bottomTranslateY]);
|
|
|
|
// Create animated styles
|
|
const headerAnimatedStyle = useAnimatedStyle(() => ({
|
|
opacity: controlsOpacity.value,
|
|
transform: [{ translateY: headerTranslateY.value }],
|
|
position: "absolute" as const,
|
|
top: 0,
|
|
left: 0,
|
|
right: 0,
|
|
zIndex: 10,
|
|
}));
|
|
|
|
const centerAnimatedStyle = useAnimatedStyle(() => ({
|
|
opacity: controlsOpacity.value,
|
|
position: "absolute" as const,
|
|
top: 0,
|
|
left: 0,
|
|
right: 0,
|
|
bottom: 0,
|
|
zIndex: 5,
|
|
}));
|
|
|
|
const bottomAnimatedStyle = useAnimatedStyle(() => ({
|
|
opacity: controlsOpacity.value,
|
|
transform: [{ translateY: bottomTranslateY.value }],
|
|
position: "absolute" as const,
|
|
bottom: 0,
|
|
left: 0,
|
|
right: 0,
|
|
zIndex: 10,
|
|
}));
|
|
|
|
// Initialize progress values - MPV uses milliseconds
|
|
useEffect(() => {
|
|
if (item) {
|
|
progress.value = ticksToMs(item?.UserData?.PlaybackPositionTicks);
|
|
max.value = ticksToMs(item.RunTimeTicks || 0);
|
|
}
|
|
}, [item, progress, max]);
|
|
|
|
// Navigation hooks
|
|
const {
|
|
handleSeekBackward,
|
|
handleSeekForward,
|
|
handleSkipBackward,
|
|
handleSkipForward,
|
|
} = useVideoNavigation({
|
|
progress,
|
|
isPlaying,
|
|
seek,
|
|
play,
|
|
});
|
|
|
|
// Time management hook
|
|
const { currentTime, remainingTime } = useVideoTime({
|
|
progress,
|
|
max,
|
|
isSeeking,
|
|
});
|
|
|
|
const toggleControls = useCallback(() => {
|
|
if (showControls) {
|
|
setShowAudioSlider(false);
|
|
setShowControls(false);
|
|
} else {
|
|
setShowControls(true);
|
|
}
|
|
}, [showControls, setShowControls]);
|
|
|
|
// Remote control hook
|
|
const {
|
|
remoteScrubProgress,
|
|
isRemoteScrubbing,
|
|
showRemoteBubble,
|
|
isSliding: isRemoteSliding,
|
|
time: remoteTime,
|
|
} = useRemoteControl({
|
|
progress,
|
|
min,
|
|
max,
|
|
showControls,
|
|
isPlaying,
|
|
seek,
|
|
play,
|
|
togglePlay,
|
|
toggleControls,
|
|
calculateTrickplayUrl,
|
|
handleSeekForward,
|
|
handleSeekBackward,
|
|
});
|
|
|
|
// Slider hook
|
|
const {
|
|
isSliding,
|
|
time,
|
|
handleSliderStart,
|
|
handleTouchStart,
|
|
handleTouchEnd,
|
|
handleSliderComplete,
|
|
handleSliderChange,
|
|
} = useVideoSlider({
|
|
progress,
|
|
isSeeking,
|
|
isPlaying,
|
|
seek,
|
|
play,
|
|
pause,
|
|
calculateTrickplayUrl,
|
|
showControls,
|
|
});
|
|
|
|
const effectiveProgress = useSharedValue(0);
|
|
|
|
// Recompute progress whenever remote scrubbing is active or when progress significantly changes
|
|
useAnimatedReaction(
|
|
() => ({
|
|
isScrubbing: isRemoteScrubbing.value,
|
|
scrub: remoteScrubProgress.value,
|
|
actual: progress.value,
|
|
}),
|
|
(current, previous) => {
|
|
// Always update if scrubbing state changed or we're currently scrubbing
|
|
if (
|
|
current.isScrubbing !== previous?.isScrubbing ||
|
|
current.isScrubbing
|
|
) {
|
|
effectiveProgress.value =
|
|
current.isScrubbing && current.scrub != null
|
|
? current.scrub
|
|
: current.actual;
|
|
} else {
|
|
// When not scrubbing, only update if progress changed significantly (1 second)
|
|
// MPV uses milliseconds
|
|
const progressUnit = CONTROLS_CONSTANTS.PROGRESS_UNIT_MS;
|
|
const progressDiff = Math.abs(current.actual - effectiveProgress.value);
|
|
if (progressDiff >= progressUnit) {
|
|
effectiveProgress.value = current.actual;
|
|
}
|
|
}
|
|
},
|
|
[],
|
|
);
|
|
|
|
const { bitrateValue, subtitleIndex, audioIndex } = useLocalSearchParams<{
|
|
bitrateValue: string;
|
|
audioIndex: string;
|
|
subtitleIndex: string;
|
|
}>();
|
|
|
|
// Fetch all segments for the current item
|
|
const { data: segments } = useSegments(
|
|
item.Id ?? "",
|
|
offline,
|
|
downloadedFiles,
|
|
api,
|
|
);
|
|
|
|
// Convert milliseconds to seconds for segment comparison
|
|
const currentTimeSeconds = msToSeconds(currentTime);
|
|
const maxSeconds = maxMs ? msToSeconds(maxMs) : undefined;
|
|
|
|
// Wrapper to convert segment skip from seconds to milliseconds
|
|
// Includes 200ms delay to allow seek operation to complete before resuming playback
|
|
const seekMs = useCallback(
|
|
(timeInSeconds: number) => {
|
|
// Cancel any pending play call to avoid race conditions
|
|
if (playTimeoutRef.current) {
|
|
clearTimeout(playTimeoutRef.current);
|
|
}
|
|
seek(timeInSeconds * 1000);
|
|
// Brief delay ensures the seek operation completes before resuming playback
|
|
// Without this, playback may resume from the old position
|
|
playTimeoutRef.current = setTimeout(() => {
|
|
play();
|
|
playTimeoutRef.current = null;
|
|
}, 200);
|
|
},
|
|
[seek, play],
|
|
);
|
|
|
|
// Use unified segment skipper for all segment types
|
|
const introSkipper = useSegmentSkipper({
|
|
segments: segments?.introSegments || [],
|
|
segmentType: "Intro",
|
|
currentTime: currentTimeSeconds,
|
|
seek: seekMs,
|
|
isPaused: !isPlaying,
|
|
});
|
|
|
|
const outroSkipper = useSegmentSkipper({
|
|
segments: segments?.creditSegments || [],
|
|
segmentType: "Outro",
|
|
currentTime: currentTimeSeconds,
|
|
totalDuration: maxSeconds,
|
|
seek: seekMs,
|
|
isPaused: !isPlaying,
|
|
});
|
|
|
|
const recapSkipper = useSegmentSkipper({
|
|
segments: segments?.recapSegments || [],
|
|
segmentType: "Recap",
|
|
currentTime: currentTimeSeconds,
|
|
seek: seekMs,
|
|
isPaused: !isPlaying,
|
|
});
|
|
|
|
const commercialSkipper = useSegmentSkipper({
|
|
segments: segments?.commercialSegments || [],
|
|
segmentType: "Commercial",
|
|
currentTime: currentTimeSeconds,
|
|
seek: seekMs,
|
|
isPaused: !isPlaying,
|
|
});
|
|
|
|
const previewSkipper = useSegmentSkipper({
|
|
segments: segments?.previewSegments || [],
|
|
segmentType: "Preview",
|
|
currentTime: currentTimeSeconds,
|
|
seek: seekMs,
|
|
isPaused: !isPlaying,
|
|
});
|
|
|
|
// Determine which segment button to show (priority order)
|
|
// Commercial > Recap > Intro > Preview > Outro
|
|
const activeSegment = useMemo(() => {
|
|
if (commercialSkipper.currentSegment)
|
|
return { type: "Commercial", ...commercialSkipper };
|
|
if (recapSkipper.currentSegment) return { type: "Recap", ...recapSkipper };
|
|
if (introSkipper.currentSegment) return { type: "Intro", ...introSkipper };
|
|
if (previewSkipper.currentSegment)
|
|
return { type: "Preview", ...previewSkipper };
|
|
if (outroSkipper.currentSegment) return { type: "Outro", ...outroSkipper };
|
|
return null;
|
|
}, [
|
|
commercialSkipper.currentSegment,
|
|
recapSkipper.currentSegment,
|
|
introSkipper.currentSegment,
|
|
previewSkipper.currentSegment,
|
|
outroSkipper.currentSegment,
|
|
commercialSkipper,
|
|
recapSkipper,
|
|
introSkipper,
|
|
previewSkipper,
|
|
outroSkipper,
|
|
]);
|
|
|
|
// Legacy compatibility: map to old variable names
|
|
const showSkipButton = !!(
|
|
activeSegment &&
|
|
["Intro", "Recap", "Commercial", "Preview"].includes(activeSegment.type)
|
|
);
|
|
const skipIntro = activeSegment?.skipSegment || noop;
|
|
const showSkipCreditButton = activeSegment?.type === "Outro";
|
|
const skipCredit = outroSkipper.skipSegment;
|
|
const hasContentAfterCredits =
|
|
outroSkipper.currentSegment && maxSeconds
|
|
? outroSkipper.currentSegment.endTime < maxSeconds
|
|
: false;
|
|
|
|
// Get button text based on segment type using i18n
|
|
const { t } = useTranslation();
|
|
const skipButtonText = activeSegment
|
|
? t(`player.skip_${activeSegment.type.toLowerCase()}`)
|
|
: t("player.skip_intro");
|
|
const skipCreditButtonText = t("player.skip_outro");
|
|
|
|
const goToItemCommon = useCallback(
|
|
(item: BaseItemDto) => {
|
|
if (!item || !settings) {
|
|
return;
|
|
}
|
|
lightHapticFeedback();
|
|
const previousIndexes = {
|
|
subtitleIndex: subtitleIndex
|
|
? Number.parseInt(subtitleIndex, 10)
|
|
: undefined,
|
|
audioIndex: audioIndex ? Number.parseInt(audioIndex, 10) : undefined,
|
|
};
|
|
|
|
const {
|
|
mediaSource: newMediaSource,
|
|
audioIndex: defaultAudioIndex,
|
|
subtitleIndex: defaultSubtitleIndex,
|
|
} = getDefaultPlaySettings(item, settings, {
|
|
indexes: previousIndexes,
|
|
source: mediaSource ?? undefined,
|
|
});
|
|
|
|
const queryParams = new URLSearchParams({
|
|
...(offline && { offline: "true" }),
|
|
itemId: item.Id ?? "",
|
|
audioIndex: defaultAudioIndex?.toString() ?? "",
|
|
subtitleIndex: defaultSubtitleIndex?.toString() ?? "",
|
|
mediaSourceId: newMediaSource?.Id ?? "",
|
|
bitrateValue: bitrateValue?.toString(),
|
|
playbackPosition:
|
|
item.UserData?.PlaybackPositionTicks?.toString() ?? "",
|
|
}).toString();
|
|
|
|
router.replace(`player/direct-player?${queryParams}` as any);
|
|
},
|
|
[settings, subtitleIndex, audioIndex, mediaSource, bitrateValue, router],
|
|
);
|
|
|
|
const goToPreviousItem = useCallback(() => {
|
|
if (!previousItem) {
|
|
return;
|
|
}
|
|
goToItemCommon(previousItem);
|
|
}, [previousItem, goToItemCommon]);
|
|
|
|
const goToNextItem = useCallback(
|
|
({
|
|
isAutoPlay,
|
|
resetWatchCount,
|
|
}: {
|
|
isAutoPlay?: boolean;
|
|
resetWatchCount?: boolean;
|
|
}) => {
|
|
if (!nextItem) {
|
|
return;
|
|
}
|
|
|
|
if (!isAutoPlay) {
|
|
// if we are not autoplaying, we won't update anything, we just go to the next item
|
|
goToItemCommon(nextItem);
|
|
if (resetWatchCount) {
|
|
updateSettings({
|
|
autoPlayEpisodeCount: 0,
|
|
});
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Skip autoplay logic if maxAutoPlayEpisodeCount is -1
|
|
if (settings.maxAutoPlayEpisodeCount.value === -1) {
|
|
goToItemCommon(nextItem);
|
|
return;
|
|
}
|
|
|
|
if (
|
|
settings.autoPlayEpisodeCount + 1 <
|
|
settings.maxAutoPlayEpisodeCount.value
|
|
) {
|
|
goToItemCommon(nextItem);
|
|
}
|
|
|
|
// Check if the autoPlayEpisodeCount is less than maxAutoPlayEpisodeCount for the autoPlay
|
|
if (
|
|
settings.autoPlayEpisodeCount < settings.maxAutoPlayEpisodeCount.value
|
|
) {
|
|
// update the autoPlayEpisodeCount in settings
|
|
updateSettings({
|
|
autoPlayEpisodeCount: settings.autoPlayEpisodeCount + 1,
|
|
});
|
|
}
|
|
},
|
|
[nextItem, goToItemCommon],
|
|
);
|
|
|
|
// Add a memoized handler for autoplay next episode
|
|
const handleNextEpisodeAutoPlay = useCallback(() => {
|
|
goToNextItem({ isAutoPlay: true });
|
|
}, [goToNextItem]);
|
|
|
|
// Add a memoized handler for manual next episode
|
|
const handleNextEpisodeManual = useCallback(() => {
|
|
goToNextItem({ isAutoPlay: false });
|
|
}, [goToNextItem]);
|
|
|
|
// Add a memoized handler for ContinueWatchingOverlay
|
|
const handleContinueWatching = useCallback(
|
|
(options: { isAutoPlay?: boolean; resetWatchCount?: boolean }) => {
|
|
goToNextItem(options);
|
|
},
|
|
[goToNextItem],
|
|
);
|
|
|
|
const hideControls = useCallback(() => {
|
|
setShowControls(false);
|
|
setShowAudioSlider(false);
|
|
}, [setShowControls]);
|
|
|
|
const { handleControlsInteraction } = useControlsTimeout({
|
|
showControls,
|
|
isSliding: isSliding || isRemoteSliding,
|
|
episodeView,
|
|
onHideControls: hideControls,
|
|
timeout: CONTROLS_CONSTANTS.TIMEOUT,
|
|
disabled: true,
|
|
});
|
|
|
|
const switchOnEpisodeMode = useCallback(() => {
|
|
setEpisodeView(true);
|
|
if (isPlaying) {
|
|
togglePlay();
|
|
}
|
|
}, [isPlaying, togglePlay]);
|
|
|
|
return (
|
|
<View style={styles.controlsContainer} pointerEvents='box-none'>
|
|
{episodeView ? (
|
|
<EpisodeList
|
|
item={item}
|
|
close={() => setEpisodeView(false)}
|
|
goToItem={goToItemCommon}
|
|
/>
|
|
) : (
|
|
<>
|
|
<GestureOverlay
|
|
screenWidth={screenWidth}
|
|
screenHeight={screenHeight}
|
|
showControls={showControls}
|
|
onToggleControls={toggleControls}
|
|
onSkipForward={handleSkipForward}
|
|
onSkipBackward={handleSkipBackward}
|
|
/>
|
|
{/* Technical Info Overlay - rendered outside animated views to stay visible */}
|
|
{getTechnicalInfo && (
|
|
<TechnicalInfoOverlay
|
|
showControls={showControls}
|
|
visible={showTechnicalInfo}
|
|
getTechnicalInfo={getTechnicalInfo}
|
|
playMethod={playMethod}
|
|
transcodeReasons={transcodeReasons}
|
|
/>
|
|
)}
|
|
<Animated.View
|
|
style={headerAnimatedStyle}
|
|
pointerEvents={showControls ? "auto" : "none"}
|
|
>
|
|
<HeaderControls
|
|
item={item}
|
|
showControls={showControls}
|
|
offline={offline}
|
|
mediaSource={mediaSource}
|
|
startPictureInPicture={startPictureInPicture}
|
|
switchOnEpisodeMode={switchOnEpisodeMode}
|
|
goToPreviousItem={goToPreviousItem}
|
|
goToNextItem={goToNextItem}
|
|
previousItem={previousItem}
|
|
nextItem={nextItem}
|
|
aspectRatio={aspectRatio}
|
|
isZoomedToFill={isZoomedToFill}
|
|
onZoomToggle={onZoomToggle}
|
|
playbackSpeed={playbackSpeed}
|
|
setPlaybackSpeed={setPlaybackSpeed}
|
|
showTechnicalInfo={showTechnicalInfo}
|
|
onToggleTechnicalInfo={onToggleTechnicalInfo}
|
|
/>
|
|
</Animated.View>
|
|
<Animated.View
|
|
style={centerAnimatedStyle}
|
|
pointerEvents={showControls ? "box-none" : "none"}
|
|
>
|
|
<CenterControls
|
|
showControls={showControls}
|
|
isPlaying={isPlaying}
|
|
isBuffering={isBuffering}
|
|
showAudioSlider={showAudioSlider}
|
|
setShowAudioSlider={setShowAudioSlider}
|
|
togglePlay={togglePlay}
|
|
handleSkipBackward={handleSkipBackward}
|
|
handleSkipForward={handleSkipForward}
|
|
/>
|
|
</Animated.View>
|
|
<Animated.View
|
|
style={bottomAnimatedStyle}
|
|
pointerEvents={showControls ? "auto" : "none"}
|
|
>
|
|
<BottomControls
|
|
item={item}
|
|
showControls={showControls}
|
|
isSliding={isSliding}
|
|
showRemoteBubble={showRemoteBubble}
|
|
currentTime={currentTime}
|
|
remainingTime={remainingTime}
|
|
showSkipButton={showSkipButton}
|
|
skipButtonText={skipButtonText}
|
|
showSkipCreditButton={showSkipCreditButton}
|
|
skipCreditButtonText={skipCreditButtonText}
|
|
hasContentAfterCredits={hasContentAfterCredits}
|
|
skipIntro={skipIntro}
|
|
skipCredit={skipCredit}
|
|
nextItem={nextItem}
|
|
handleNextEpisodeAutoPlay={handleNextEpisodeAutoPlay}
|
|
handleNextEpisodeManual={handleNextEpisodeManual}
|
|
handleControlsInteraction={handleControlsInteraction}
|
|
min={min}
|
|
max={max}
|
|
effectiveProgress={effectiveProgress}
|
|
cacheProgress={cacheProgress}
|
|
handleSliderStart={handleSliderStart}
|
|
handleSliderComplete={handleSliderComplete}
|
|
handleSliderChange={handleSliderChange}
|
|
handleTouchStart={handleTouchStart}
|
|
handleTouchEnd={handleTouchEnd}
|
|
trickPlayUrl={trickPlayUrl}
|
|
trickplayInfo={trickplayInfo}
|
|
time={isSliding || showRemoteBubble ? time : remoteTime}
|
|
/>
|
|
</Animated.View>
|
|
</>
|
|
)}
|
|
{settings.maxAutoPlayEpisodeCount.value !== -1 && (
|
|
<ContinueWatchingOverlay goToNextItem={handleContinueWatching} />
|
|
)}
|
|
</View>
|
|
);
|
|
};
|
|
|
|
const styles = StyleSheet.create({
|
|
controlsContainer: {
|
|
position: "absolute",
|
|
top: 0,
|
|
left: 0,
|
|
right: 0,
|
|
bottom: 0,
|
|
},
|
|
});
|