mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-04-20 16:22:04 +00:00
removed dead code
This commit is contained in:
parent
8588aca948
commit
39498f78b7
13 changed files with 15 additions and 805 deletions
1
libmpv-android
Submodule
1
libmpv-android
Submodule
|
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit 8c4778b5aad441bb0449a7f9b3d6d827fd3d6a2a
|
||||||
1
mpv-android
Submodule
1
mpv-android
Submodule
|
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit 118cd1ed3d498265e44230e5dbb015bdd59f9dad
|
||||||
|
|
@ -81,22 +81,29 @@ export const usePlayerSetup = (config: PlayerSetupConfig) => {
|
||||||
};
|
};
|
||||||
}, [isOpeningAnimationComplete]);
|
}, [isOpeningAnimationComplete]);
|
||||||
|
|
||||||
// Handle Orientation (Lock to Landscape after opening)
|
const orientationLocked = useRef(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isOpeningAnimationComplete) {
|
if (isOpeningAnimationComplete && !orientationLocked.current) {
|
||||||
const task = InteractionManager.runAfterInteractions(() => {
|
const task = InteractionManager.runAfterInteractions(() => {
|
||||||
ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.LANDSCAPE)
|
ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.LANDSCAPE)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
if (__DEV__) logger.log('[VideoPlayer] Locked to landscape orientation');
|
orientationLocked.current = true;
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch(() => { });
|
||||||
logger.warn('[VideoPlayer] Failed to lock orientation:', error);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
return () => task.cancel();
|
return () => task.cancel();
|
||||||
}
|
}
|
||||||
}, [isOpeningAnimationComplete]);
|
}, [isOpeningAnimationComplete]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.DEFAULT)
|
||||||
|
.then(() => ScreenOrientation.unlockAsync())
|
||||||
|
.catch(() => { });
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
// Handle App State
|
// Handle App State
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const onAppStateChange = (state: string) => {
|
const onAppStateChange = (state: string) => {
|
||||||
|
|
|
||||||
|
|
@ -1,58 +0,0 @@
|
||||||
import { useState } from 'react';
|
|
||||||
import {
|
|
||||||
DEFAULT_SUBTITLE_SIZE,
|
|
||||||
SubtitleCue,
|
|
||||||
SubtitleSegment,
|
|
||||||
WyzieSubtitle
|
|
||||||
} from '../../utils/playerTypes';
|
|
||||||
|
|
||||||
export const useCustomSubtitles = () => {
|
|
||||||
// Data State
|
|
||||||
const [customSubtitles, setCustomSubtitles] = useState<SubtitleCue[]>([]);
|
|
||||||
const [currentSubtitle, setCurrentSubtitle] = useState<string>('');
|
|
||||||
const [currentFormattedSegments, setCurrentFormattedSegments] = useState<SubtitleSegment[][]>([]);
|
|
||||||
const [availableSubtitles, setAvailableSubtitles] = useState<WyzieSubtitle[]>([]);
|
|
||||||
const [useCustomSubtitles, setUseCustomSubtitles] = useState<boolean>(false);
|
|
||||||
|
|
||||||
// Loading State
|
|
||||||
const [isLoadingSubtitles, setIsLoadingSubtitles] = useState<boolean>(false);
|
|
||||||
const [isLoadingSubtitleList, setIsLoadingSubtitleList] = useState<boolean>(false);
|
|
||||||
|
|
||||||
// Styling State
|
|
||||||
const [subtitleSize, setSubtitleSize] = useState<number>(DEFAULT_SUBTITLE_SIZE);
|
|
||||||
const [subtitleBackground, setSubtitleBackground] = useState<boolean>(false);
|
|
||||||
const [subtitleTextColor, setSubtitleTextColor] = useState<string>('#FFFFFF');
|
|
||||||
const [subtitleBgOpacity, setSubtitleBgOpacity] = useState<number>(0.7);
|
|
||||||
const [subtitleTextShadow, setSubtitleTextShadow] = useState<boolean>(true);
|
|
||||||
const [subtitleOutline, setSubtitleOutline] = useState<boolean>(true);
|
|
||||||
const [subtitleOutlineColor, setSubtitleOutlineColor] = useState<string>('#000000');
|
|
||||||
const [subtitleOutlineWidth, setSubtitleOutlineWidth] = useState<number>(4);
|
|
||||||
const [subtitleAlign, setSubtitleAlign] = useState<'center' | 'left' | 'right'>('center');
|
|
||||||
const [subtitleBottomOffset, setSubtitleBottomOffset] = useState<number>(10);
|
|
||||||
const [subtitleLetterSpacing, setSubtitleLetterSpacing] = useState<number>(0);
|
|
||||||
const [subtitleLineHeightMultiplier, setSubtitleLineHeightMultiplier] = useState<number>(1.2);
|
|
||||||
const [subtitleOffsetSec, setSubtitleOffsetSec] = useState<number>(0);
|
|
||||||
|
|
||||||
return {
|
|
||||||
customSubtitles, setCustomSubtitles,
|
|
||||||
currentSubtitle, setCurrentSubtitle,
|
|
||||||
currentFormattedSegments, setCurrentFormattedSegments,
|
|
||||||
availableSubtitles, setAvailableSubtitles,
|
|
||||||
useCustomSubtitles, setUseCustomSubtitles,
|
|
||||||
isLoadingSubtitles, setIsLoadingSubtitles,
|
|
||||||
isLoadingSubtitleList, setIsLoadingSubtitleList,
|
|
||||||
subtitleSize, setSubtitleSize,
|
|
||||||
subtitleBackground, setSubtitleBackground,
|
|
||||||
subtitleTextColor, setSubtitleTextColor,
|
|
||||||
subtitleBgOpacity, setSubtitleBgOpacity,
|
|
||||||
subtitleTextShadow, setSubtitleTextShadow,
|
|
||||||
subtitleOutline, setSubtitleOutline,
|
|
||||||
subtitleOutlineColor, setSubtitleOutlineColor,
|
|
||||||
subtitleOutlineWidth, setSubtitleOutlineWidth,
|
|
||||||
subtitleAlign, setSubtitleAlign,
|
|
||||||
subtitleBottomOffset, setSubtitleBottomOffset,
|
|
||||||
subtitleLetterSpacing, setSubtitleLetterSpacing,
|
|
||||||
subtitleLineHeightMultiplier, setSubtitleLineHeightMultiplier,
|
|
||||||
subtitleOffsetSec, setSubtitleOffsetSec
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
@ -1,58 +0,0 @@
|
||||||
import { useMemo } from 'react';
|
|
||||||
|
|
||||||
export const useNextEpisode = (
|
|
||||||
type: string | undefined,
|
|
||||||
season: number | undefined,
|
|
||||||
episode: number | undefined,
|
|
||||||
groupedEpisodes: any,
|
|
||||||
metadataGroupedEpisodes: any,
|
|
||||||
episodeId: string | undefined
|
|
||||||
) => {
|
|
||||||
// Current description
|
|
||||||
const currentEpisodeDescription = useMemo(() => {
|
|
||||||
try {
|
|
||||||
if (type !== 'series') return '';
|
|
||||||
const allEpisodes = Object.values(groupedEpisodes || {}).flat() as any[];
|
|
||||||
if (!allEpisodes || allEpisodes.length === 0) return '';
|
|
||||||
|
|
||||||
let match: any | null = null;
|
|
||||||
if (episodeId) {
|
|
||||||
match = allEpisodes.find(ep => ep?.stremioId === episodeId || String(ep?.id) === String(episodeId));
|
|
||||||
}
|
|
||||||
if (!match && season && episode) {
|
|
||||||
match = allEpisodes.find(ep => ep?.season_number === season && ep?.episode_number === episode);
|
|
||||||
}
|
|
||||||
return (match?.overview || '').trim();
|
|
||||||
} catch {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
}, [type, groupedEpisodes, episodeId, season, episode]);
|
|
||||||
|
|
||||||
// Next Episode
|
|
||||||
const nextEpisode = useMemo(() => {
|
|
||||||
try {
|
|
||||||
if (type !== 'series' || !season || !episode) return null;
|
|
||||||
const sourceGroups = groupedEpisodes && Object.keys(groupedEpisodes || {}).length > 0
|
|
||||||
? groupedEpisodes
|
|
||||||
: (metadataGroupedEpisodes || {});
|
|
||||||
|
|
||||||
const allEpisodes = Object.values(sourceGroups || {}).flat() as any[];
|
|
||||||
if (!allEpisodes || allEpisodes.length === 0) return null;
|
|
||||||
|
|
||||||
let nextEp = allEpisodes.find((ep: any) =>
|
|
||||||
ep.season_number === season && ep.episode_number === episode + 1
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!nextEp) {
|
|
||||||
nextEp = allEpisodes.find((ep: any) =>
|
|
||||||
ep.season_number === season + 1 && ep.episode_number === 1
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return nextEp;
|
|
||||||
} catch {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}, [type, season, episode, groupedEpisodes, metadataGroupedEpisodes]);
|
|
||||||
|
|
||||||
return { currentEpisodeDescription, nextEpisode };
|
|
||||||
};
|
|
||||||
|
|
@ -1,149 +0,0 @@
|
||||||
import { useRef, useState, useEffect } from 'react';
|
|
||||||
import { Animated, InteractionManager } from 'react-native';
|
|
||||||
import FastImage from '@d11/react-native-fast-image';
|
|
||||||
import { logger } from '../../../../utils/logger';
|
|
||||||
|
|
||||||
export const useOpeningAnimation = (backdrop: string | undefined, metadata: any) => {
|
|
||||||
// Animation Values
|
|
||||||
const fadeAnim = useRef(new Animated.Value(1)).current;
|
|
||||||
const openingFadeAnim = useRef(new Animated.Value(0)).current;
|
|
||||||
const openingScaleAnim = useRef(new Animated.Value(0.8)).current;
|
|
||||||
const backgroundFadeAnim = useRef(new Animated.Value(1)).current;
|
|
||||||
const backdropImageOpacityAnim = useRef(new Animated.Value(0)).current;
|
|
||||||
const logoScaleAnim = useRef(new Animated.Value(0.8)).current;
|
|
||||||
const logoOpacityAnim = useRef(new Animated.Value(0)).current;
|
|
||||||
const pulseAnim = useRef(new Animated.Value(1)).current;
|
|
||||||
|
|
||||||
const [isOpeningAnimationComplete, setIsOpeningAnimationComplete] = useState(false);
|
|
||||||
const [shouldHideOpeningOverlay, setShouldHideOpeningOverlay] = useState(false);
|
|
||||||
const [isBackdropLoaded, setIsBackdropLoaded] = useState(false);
|
|
||||||
|
|
||||||
// Prefetch Background
|
|
||||||
useEffect(() => {
|
|
||||||
const task = InteractionManager.runAfterInteractions(() => {
|
|
||||||
if (backdrop && typeof backdrop === 'string') {
|
|
||||||
setIsBackdropLoaded(false);
|
|
||||||
backdropImageOpacityAnim.setValue(0);
|
|
||||||
try {
|
|
||||||
FastImage.preload([{ uri: backdrop }]);
|
|
||||||
setIsBackdropLoaded(true);
|
|
||||||
Animated.timing(backdropImageOpacityAnim, {
|
|
||||||
toValue: 1,
|
|
||||||
duration: 400,
|
|
||||||
useNativeDriver: true,
|
|
||||||
}).start();
|
|
||||||
} catch (error) {
|
|
||||||
setIsBackdropLoaded(true);
|
|
||||||
backdropImageOpacityAnim.setValue(1);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
setIsBackdropLoaded(true);
|
|
||||||
backdropImageOpacityAnim.setValue(0);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return () => task.cancel();
|
|
||||||
}, [backdrop]);
|
|
||||||
|
|
||||||
// Prefetch Logo
|
|
||||||
useEffect(() => {
|
|
||||||
const task = InteractionManager.runAfterInteractions(() => {
|
|
||||||
const logoUrl = metadata?.logo;
|
|
||||||
if (logoUrl && typeof logoUrl === 'string') {
|
|
||||||
try {
|
|
||||||
FastImage.preload([{ uri: logoUrl }]);
|
|
||||||
} catch (error) { }
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return () => task.cancel();
|
|
||||||
}, [metadata]);
|
|
||||||
|
|
||||||
const startOpeningAnimation = () => {
|
|
||||||
Animated.parallel([
|
|
||||||
Animated.timing(logoOpacityAnim, {
|
|
||||||
toValue: 1,
|
|
||||||
duration: 300,
|
|
||||||
useNativeDriver: true,
|
|
||||||
}),
|
|
||||||
Animated.spring(logoScaleAnim, {
|
|
||||||
toValue: 1,
|
|
||||||
tension: 80,
|
|
||||||
friction: 8,
|
|
||||||
useNativeDriver: true,
|
|
||||||
}),
|
|
||||||
]).start();
|
|
||||||
|
|
||||||
const createPulseAnimation = () => {
|
|
||||||
return Animated.sequence([
|
|
||||||
Animated.timing(pulseAnim, {
|
|
||||||
toValue: 1.05,
|
|
||||||
duration: 800,
|
|
||||||
useNativeDriver: true,
|
|
||||||
}),
|
|
||||||
Animated.timing(pulseAnim, {
|
|
||||||
toValue: 1,
|
|
||||||
duration: 800,
|
|
||||||
useNativeDriver: true,
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
};
|
|
||||||
|
|
||||||
const loopPulse = () => {
|
|
||||||
createPulseAnimation().start(() => {
|
|
||||||
if (!isOpeningAnimationComplete) {
|
|
||||||
loopPulse();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
loopPulse();
|
|
||||||
};
|
|
||||||
|
|
||||||
const completeOpeningAnimation = () => {
|
|
||||||
pulseAnim.stopAnimation();
|
|
||||||
|
|
||||||
Animated.parallel([
|
|
||||||
Animated.timing(openingFadeAnim, {
|
|
||||||
toValue: 1,
|
|
||||||
duration: 300,
|
|
||||||
useNativeDriver: true,
|
|
||||||
}),
|
|
||||||
Animated.timing(openingScaleAnim, {
|
|
||||||
toValue: 1,
|
|
||||||
duration: 350,
|
|
||||||
useNativeDriver: true,
|
|
||||||
}),
|
|
||||||
Animated.timing(backgroundFadeAnim, {
|
|
||||||
toValue: 0,
|
|
||||||
duration: 400,
|
|
||||||
useNativeDriver: true,
|
|
||||||
}),
|
|
||||||
]).start(() => {
|
|
||||||
setIsOpeningAnimationComplete(true);
|
|
||||||
setTimeout(() => {
|
|
||||||
setShouldHideOpeningOverlay(true);
|
|
||||||
}, 450);
|
|
||||||
});
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
if (!isOpeningAnimationComplete) {
|
|
||||||
// logger.warn('[VideoPlayer] Opening animation fallback triggered');
|
|
||||||
setIsOpeningAnimationComplete(true);
|
|
||||||
}
|
|
||||||
}, 1000);
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
fadeAnim,
|
|
||||||
openingFadeAnim,
|
|
||||||
openingScaleAnim,
|
|
||||||
backgroundFadeAnim,
|
|
||||||
backdropImageOpacityAnim,
|
|
||||||
logoScaleAnim,
|
|
||||||
logoOpacityAnim,
|
|
||||||
pulseAnim,
|
|
||||||
isOpeningAnimationComplete,
|
|
||||||
shouldHideOpeningOverlay,
|
|
||||||
isBackdropLoaded,
|
|
||||||
startOpeningAnimation,
|
|
||||||
completeOpeningAnimation
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
@ -1,63 +0,0 @@
|
||||||
import { useRef, useCallback } from 'react';
|
|
||||||
import { Platform } from 'react-native';
|
|
||||||
import { logger } from '../../../../utils/logger';
|
|
||||||
|
|
||||||
const DEBUG_MODE = false;
|
|
||||||
const END_EPSILON = 0.3;
|
|
||||||
|
|
||||||
export const usePlayerControls = (
|
|
||||||
ksPlayerRef: any,
|
|
||||||
paused: boolean,
|
|
||||||
setPaused: (paused: boolean) => void,
|
|
||||||
currentTime: number,
|
|
||||||
duration: number,
|
|
||||||
isSeeking: React.MutableRefObject<boolean>,
|
|
||||||
isMounted: React.MutableRefObject<boolean>
|
|
||||||
) => {
|
|
||||||
// iOS seeking helpers
|
|
||||||
const iosWasPausedDuringSeekRef = useRef<boolean | null>(null);
|
|
||||||
|
|
||||||
const togglePlayback = useCallback(() => {
|
|
||||||
setPaused(!paused);
|
|
||||||
}, [paused, setPaused]);
|
|
||||||
|
|
||||||
const seekToTime = useCallback((rawSeconds: number) => {
|
|
||||||
const timeInSeconds = Math.max(0, Math.min(rawSeconds, duration > 0 ? duration - END_EPSILON : rawSeconds));
|
|
||||||
|
|
||||||
if (ksPlayerRef.current && duration > 0 && !isSeeking.current) {
|
|
||||||
if (DEBUG_MODE) logger.log(`[usePlayerControls] Seeking to ${timeInSeconds}`);
|
|
||||||
|
|
||||||
isSeeking.current = true;
|
|
||||||
|
|
||||||
// iOS optimization: pause while seeking for smoother experience
|
|
||||||
iosWasPausedDuringSeekRef.current = paused;
|
|
||||||
if (!paused) setPaused(true);
|
|
||||||
|
|
||||||
// Actually perform the seek
|
|
||||||
ksPlayerRef.current.seek(timeInSeconds);
|
|
||||||
|
|
||||||
// Debounce the seeking state reset
|
|
||||||
setTimeout(() => {
|
|
||||||
if (isMounted.current && isSeeking.current) {
|
|
||||||
isSeeking.current = false;
|
|
||||||
// Resume if it was playing
|
|
||||||
if (iosWasPausedDuringSeekRef.current === false) {
|
|
||||||
setPaused(false);
|
|
||||||
iosWasPausedDuringSeekRef.current = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, 500);
|
|
||||||
}
|
|
||||||
}, [duration, paused, setPaused, ksPlayerRef, isSeeking, isMounted]);
|
|
||||||
|
|
||||||
const skip = useCallback((seconds: number) => {
|
|
||||||
seekToTime(currentTime + seconds);
|
|
||||||
}, [currentTime, seekToTime]);
|
|
||||||
|
|
||||||
return {
|
|
||||||
togglePlayback,
|
|
||||||
seekToTime,
|
|
||||||
skip,
|
|
||||||
iosWasPausedDuringSeekRef
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
@ -1,34 +0,0 @@
|
||||||
import { useState } from 'react';
|
|
||||||
import { Episode } from '../../../../types/metadata';
|
|
||||||
|
|
||||||
export const usePlayerModals = () => {
|
|
||||||
const [showAudioModal, setShowAudioModal] = useState(false);
|
|
||||||
const [showSubtitleModal, setShowSubtitleModal] = useState(false);
|
|
||||||
const [showSpeedModal, setShowSpeedModal] = useState(false);
|
|
||||||
const [showSourcesModal, setShowSourcesModal] = useState(false);
|
|
||||||
const [showEpisodesModal, setShowEpisodesModal] = useState(false);
|
|
||||||
const [showEpisodeStreamsModal, setShowEpisodeStreamsModal] = useState(false);
|
|
||||||
const [showErrorModal, setShowErrorModal] = useState(false);
|
|
||||||
const [showSubtitleLanguageModal, setShowSubtitleLanguageModal] = useState(false);
|
|
||||||
const [showCastDetails, setShowCastDetails] = useState(false);
|
|
||||||
|
|
||||||
// Some modals have associated data
|
|
||||||
const [selectedEpisodeForStreams, setSelectedEpisodeForStreams] = useState<Episode | null>(null);
|
|
||||||
const [errorDetails, setErrorDetails] = useState<string>('');
|
|
||||||
const [selectedCastMember, setSelectedCastMember] = useState<any>(null);
|
|
||||||
|
|
||||||
return {
|
|
||||||
showAudioModal, setShowAudioModal,
|
|
||||||
showSubtitleModal, setShowSubtitleModal,
|
|
||||||
showSpeedModal, setShowSpeedModal,
|
|
||||||
showSourcesModal, setShowSourcesModal,
|
|
||||||
showEpisodesModal, setShowEpisodesModal,
|
|
||||||
showEpisodeStreamsModal, setShowEpisodeStreamsModal,
|
|
||||||
showErrorModal, setShowErrorModal,
|
|
||||||
showSubtitleLanguageModal, setShowSubtitleLanguageModal,
|
|
||||||
showCastDetails, setShowCastDetails,
|
|
||||||
selectedEpisodeForStreams, setSelectedEpisodeForStreams,
|
|
||||||
errorDetails, setErrorDetails,
|
|
||||||
selectedCastMember, setSelectedCastMember
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
@ -1,103 +0,0 @@
|
||||||
import { useEffect, useRef, useCallback } from 'react';
|
|
||||||
import { StatusBar, Dimensions, AppState, InteractionManager } from 'react-native';
|
|
||||||
import * as Brightness from 'expo-brightness';
|
|
||||||
import * as ScreenOrientation from 'expo-screen-orientation';
|
|
||||||
import { logger } from '../../../../utils/logger';
|
|
||||||
import { useFocusEffect } from '@react-navigation/native';
|
|
||||||
|
|
||||||
export const usePlayerSetup = (
|
|
||||||
setScreenDimensions: (dim: any) => void,
|
|
||||||
setVolume: (vol: number) => void,
|
|
||||||
setBrightness: (bri: number) => void,
|
|
||||||
isOpeningAnimationComplete: boolean
|
|
||||||
) => {
|
|
||||||
const isAppBackgrounded = useRef(false);
|
|
||||||
|
|
||||||
const enableImmersiveMode = () => {
|
|
||||||
StatusBar.setHidden(true, 'none');
|
|
||||||
};
|
|
||||||
|
|
||||||
const disableImmersiveMode = () => {
|
|
||||||
StatusBar.setHidden(false, 'fade');
|
|
||||||
};
|
|
||||||
|
|
||||||
useFocusEffect(
|
|
||||||
useCallback(() => {
|
|
||||||
if (isOpeningAnimationComplete) {
|
|
||||||
enableImmersiveMode();
|
|
||||||
}
|
|
||||||
return () => { };
|
|
||||||
}, [isOpeningAnimationComplete])
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// Initial Setup
|
|
||||||
const subscription = Dimensions.addEventListener('change', ({ screen }) => {
|
|
||||||
setScreenDimensions(screen);
|
|
||||||
if (isOpeningAnimationComplete) {
|
|
||||||
enableImmersiveMode();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
StatusBar.setHidden(true, 'none');
|
|
||||||
if (isOpeningAnimationComplete) {
|
|
||||||
enableImmersiveMode();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize volume (KSPlayer uses 0-100)
|
|
||||||
setVolume(100);
|
|
||||||
|
|
||||||
// Initialize Brightness
|
|
||||||
const initBrightness = () => {
|
|
||||||
InteractionManager.runAfterInteractions(async () => {
|
|
||||||
try {
|
|
||||||
const currentBrightness = await Brightness.getBrightnessAsync();
|
|
||||||
setBrightness(currentBrightness);
|
|
||||||
} catch (error) {
|
|
||||||
logger.warn('[usePlayerSetup] Error getting initial brightness:', error);
|
|
||||||
setBrightness(1.0);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
initBrightness();
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
subscription?.remove();
|
|
||||||
disableImmersiveMode();
|
|
||||||
};
|
|
||||||
}, [isOpeningAnimationComplete]);
|
|
||||||
|
|
||||||
// Handle Orientation (Lock to Landscape after opening)
|
|
||||||
useEffect(() => {
|
|
||||||
if (isOpeningAnimationComplete) {
|
|
||||||
const task = InteractionManager.runAfterInteractions(() => {
|
|
||||||
ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.LANDSCAPE)
|
|
||||||
.then(() => {
|
|
||||||
if (__DEV__) logger.log('[VideoPlayer] Locked to landscape orientation');
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
logger.warn('[VideoPlayer] Failed to lock orientation:', error);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
return () => task.cancel();
|
|
||||||
}
|
|
||||||
}, [isOpeningAnimationComplete]);
|
|
||||||
|
|
||||||
// Handle App State
|
|
||||||
useEffect(() => {
|
|
||||||
const onAppStateChange = (state: string) => {
|
|
||||||
if (state === 'active') {
|
|
||||||
isAppBackgrounded.current = false;
|
|
||||||
if (isOpeningAnimationComplete) {
|
|
||||||
enableImmersiveMode();
|
|
||||||
}
|
|
||||||
} else if (state === 'background' || state === 'inactive') {
|
|
||||||
isAppBackgrounded.current = true;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const sub = AppState.addEventListener('change', onAppStateChange);
|
|
||||||
return () => sub.remove();
|
|
||||||
}, [isOpeningAnimationComplete]);
|
|
||||||
|
|
||||||
return { isAppBackgrounded };
|
|
||||||
};
|
|
||||||
|
|
@ -1,83 +0,0 @@
|
||||||
import { useState, useRef } from 'react';
|
|
||||||
import { Dimensions, Platform } from 'react-native';
|
|
||||||
|
|
||||||
// Use a specific type for resizeMode that matches what KSPlayerComponent supports
|
|
||||||
type PlayerResizeMode = 'contain' | 'cover' | 'stretch';
|
|
||||||
|
|
||||||
export const usePlayerState = () => {
|
|
||||||
// Playback State
|
|
||||||
const [paused, setPaused] = useState(false);
|
|
||||||
const [currentTime, setCurrentTime] = useState(0);
|
|
||||||
const [duration, setDuration] = useState(0);
|
|
||||||
const [buffered, setBuffered] = useState(0);
|
|
||||||
const [isBuffering, setIsBuffering] = useState(false);
|
|
||||||
const [isVideoLoaded, setIsVideoLoaded] = useState(false);
|
|
||||||
const [isPlayerReady, setIsPlayerReady] = useState(false);
|
|
||||||
|
|
||||||
// UI State
|
|
||||||
const [showControls, setShowControls] = useState(true);
|
|
||||||
const [resizeMode, setResizeMode] = useState<PlayerResizeMode>('contain');
|
|
||||||
const [videoAspectRatio, setVideoAspectRatio] = useState<number | null>(null);
|
|
||||||
const [is16by9Content, setIs16by9Content] = useState(false);
|
|
||||||
const screenData = Dimensions.get('screen');
|
|
||||||
const [screenDimensions, setScreenDimensions] = useState(screenData);
|
|
||||||
|
|
||||||
// Zoom State
|
|
||||||
const [zoomScale, setZoomScale] = useState(1);
|
|
||||||
const [zoomTranslateX, setZoomTranslateX] = useState(0);
|
|
||||||
const [zoomTranslateY, setZoomTranslateY] = useState(0);
|
|
||||||
const [lastZoomScale, setLastZoomScale] = useState(1);
|
|
||||||
const [lastTranslateX, setLastTranslateX] = useState(0);
|
|
||||||
const [lastTranslateY, setLastTranslateY] = useState(0);
|
|
||||||
|
|
||||||
// AirPlay State
|
|
||||||
const [isAirPlayActive, setIsAirPlayActive] = useState<boolean>(false);
|
|
||||||
const [allowsAirPlay, setAllowsAirPlay] = useState<boolean>(true);
|
|
||||||
|
|
||||||
// Logic State
|
|
||||||
const isSeeking = useRef(false);
|
|
||||||
const isDragging = useRef(false);
|
|
||||||
const isMounted = useRef(true);
|
|
||||||
const seekDebounceTimer = useRef<NodeJS.Timeout | null>(null);
|
|
||||||
const pendingSeekValue = useRef<number | null>(null);
|
|
||||||
const lastSeekTime = useRef<number>(0);
|
|
||||||
const wasPlayingBeforeDragRef = useRef<boolean>(false);
|
|
||||||
|
|
||||||
// Helper for iPad/macOS fullscreen
|
|
||||||
const isIPad = Platform.OS === 'ios' && (screenData.width > 1000 || screenData.height > 1000);
|
|
||||||
const isMacOS = Platform.OS === 'ios' && Platform.isPad === true;
|
|
||||||
const shouldUseFullscreen = isIPad || isMacOS;
|
|
||||||
const windowData = Dimensions.get('window');
|
|
||||||
const effectiveDimensions = shouldUseFullscreen ? windowData : screenDimensions;
|
|
||||||
|
|
||||||
return {
|
|
||||||
paused, setPaused,
|
|
||||||
currentTime, setCurrentTime,
|
|
||||||
duration, setDuration,
|
|
||||||
buffered, setBuffered,
|
|
||||||
isBuffering, setIsBuffering,
|
|
||||||
isVideoLoaded, setIsVideoLoaded,
|
|
||||||
isPlayerReady, setIsPlayerReady,
|
|
||||||
showControls, setShowControls,
|
|
||||||
resizeMode, setResizeMode,
|
|
||||||
videoAspectRatio, setVideoAspectRatio,
|
|
||||||
is16by9Content, setIs16by9Content,
|
|
||||||
screenDimensions, setScreenDimensions,
|
|
||||||
zoomScale, setZoomScale,
|
|
||||||
zoomTranslateX, setZoomTranslateX,
|
|
||||||
zoomTranslateY, setZoomTranslateY,
|
|
||||||
lastZoomScale, setLastZoomScale,
|
|
||||||
lastTranslateX, setLastTranslateX,
|
|
||||||
lastTranslateY, setLastTranslateY,
|
|
||||||
isAirPlayActive, setIsAirPlayActive,
|
|
||||||
allowsAirPlay, setAllowsAirPlay,
|
|
||||||
isSeeking,
|
|
||||||
isDragging,
|
|
||||||
isMounted,
|
|
||||||
seekDebounceTimer,
|
|
||||||
pendingSeekValue,
|
|
||||||
lastSeekTime,
|
|
||||||
wasPlayingBeforeDragRef,
|
|
||||||
effectiveDimensions
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
@ -1,38 +0,0 @@
|
||||||
import { useState, useMemo, useCallback } from 'react';
|
|
||||||
import { AudioTrack, TextTrack } from '../../utils/playerTypes';
|
|
||||||
|
|
||||||
export const usePlayerTracks = () => {
|
|
||||||
const [audioTracks, setAudioTracks] = useState<AudioTrack[]>([]);
|
|
||||||
const [selectedAudioTrack, setSelectedAudioTrack] = useState<number | null>(null);
|
|
||||||
const [textTracks, setTextTracks] = useState<TextTrack[]>([]);
|
|
||||||
const [selectedTextTrack, setSelectedTextTrack] = useState<number>(-1);
|
|
||||||
|
|
||||||
const [ksAudioTracks, setKsAudioTracks] = useState<Array<{ id: number, name: string, language?: string }>>([]);
|
|
||||||
const [ksTextTracks, setKsTextTracks] = useState<Array<{ id: number, name: string, language?: string }>>([]);
|
|
||||||
|
|
||||||
// Derived states or logic
|
|
||||||
const hasAudioTracks = audioTracks.length > 0;
|
|
||||||
const hasTextTracks = textTracks.length > 0;
|
|
||||||
|
|
||||||
// Track selection functions
|
|
||||||
const selectAudioTrack = useCallback((trackId: number) => {
|
|
||||||
setSelectedAudioTrack(trackId);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const selectTextTrack = useCallback((trackId: number) => {
|
|
||||||
setSelectedTextTrack(trackId);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return {
|
|
||||||
audioTracks, setAudioTracks,
|
|
||||||
selectedAudioTrack, setSelectedAudioTrack,
|
|
||||||
textTracks, setTextTracks,
|
|
||||||
selectedTextTrack, setSelectedTextTrack,
|
|
||||||
ksAudioTracks, setKsAudioTracks,
|
|
||||||
ksTextTracks, setKsTextTracks,
|
|
||||||
hasAudioTracks,
|
|
||||||
hasTextTracks,
|
|
||||||
selectAudioTrack,
|
|
||||||
selectTextTrack
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
@ -1,93 +0,0 @@
|
||||||
import { useState, useRef, useCallback, useEffect } from 'react';
|
|
||||||
import { Animated } from 'react-native';
|
|
||||||
import { mmkvStorage } from '../../../../services/mmkvStorage';
|
|
||||||
import { logger } from '../../../../utils/logger';
|
|
||||||
|
|
||||||
const SPEED_SETTINGS_KEY = '@nuvio_speed_settings';
|
|
||||||
|
|
||||||
export const useSpeedControl = (initialSpeed: number = 1.0) => {
|
|
||||||
const [playbackSpeed, setPlaybackSpeed] = useState<number>(initialSpeed);
|
|
||||||
const [holdToSpeedEnabled, setHoldToSpeedEnabled] = useState(true);
|
|
||||||
const [holdToSpeedValue, setHoldToSpeedValue] = useState(2.0);
|
|
||||||
const [isSpeedBoosted, setIsSpeedBoosted] = useState(false);
|
|
||||||
const [originalSpeed, setOriginalSpeed] = useState<number>(initialSpeed);
|
|
||||||
const [showSpeedActivatedOverlay, setShowSpeedActivatedOverlay] = useState(false);
|
|
||||||
|
|
||||||
const speedActivatedOverlayOpacity = useRef(new Animated.Value(0)).current;
|
|
||||||
|
|
||||||
// Load Settings
|
|
||||||
useEffect(() => {
|
|
||||||
const loadSettings = async () => {
|
|
||||||
try {
|
|
||||||
const saved = await mmkvStorage.getItem(SPEED_SETTINGS_KEY);
|
|
||||||
if (saved) {
|
|
||||||
const settings = JSON.parse(saved);
|
|
||||||
if (typeof settings.holdToSpeedEnabled === 'boolean') setHoldToSpeedEnabled(settings.holdToSpeedEnabled);
|
|
||||||
if (typeof settings.holdToSpeedValue === 'number') setHoldToSpeedValue(settings.holdToSpeedValue);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
logger.warn('[useSpeedControl] Error loading settings', e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
loadSettings();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Save Settings
|
|
||||||
useEffect(() => {
|
|
||||||
const saveSettings = async () => {
|
|
||||||
try {
|
|
||||||
await mmkvStorage.setItem(SPEED_SETTINGS_KEY, JSON.stringify({
|
|
||||||
holdToSpeedEnabled,
|
|
||||||
holdToSpeedValue
|
|
||||||
}));
|
|
||||||
} catch (e) { }
|
|
||||||
};
|
|
||||||
saveSettings();
|
|
||||||
}, [holdToSpeedEnabled, holdToSpeedValue]);
|
|
||||||
|
|
||||||
const activateSpeedBoost = useCallback(() => {
|
|
||||||
if (!holdToSpeedEnabled || isSpeedBoosted || playbackSpeed === holdToSpeedValue) return;
|
|
||||||
|
|
||||||
setOriginalSpeed(playbackSpeed);
|
|
||||||
setPlaybackSpeed(holdToSpeedValue);
|
|
||||||
setIsSpeedBoosted(true);
|
|
||||||
setShowSpeedActivatedOverlay(true);
|
|
||||||
|
|
||||||
Animated.timing(speedActivatedOverlayOpacity, {
|
|
||||||
toValue: 1,
|
|
||||||
duration: 200,
|
|
||||||
useNativeDriver: true
|
|
||||||
}).start();
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
Animated.timing(speedActivatedOverlayOpacity, {
|
|
||||||
toValue: 0,
|
|
||||||
duration: 300,
|
|
||||||
useNativeDriver: true
|
|
||||||
}).start(() => setShowSpeedActivatedOverlay(false));
|
|
||||||
}, 2000);
|
|
||||||
|
|
||||||
}, [holdToSpeedEnabled, isSpeedBoosted, playbackSpeed, holdToSpeedValue]);
|
|
||||||
|
|
||||||
const deactivateSpeedBoost = useCallback(() => {
|
|
||||||
if (isSpeedBoosted) {
|
|
||||||
setPlaybackSpeed(originalSpeed);
|
|
||||||
setIsSpeedBoosted(false);
|
|
||||||
Animated.timing(speedActivatedOverlayOpacity, { toValue: 0, duration: 100, useNativeDriver: true }).start();
|
|
||||||
}
|
|
||||||
}, [isSpeedBoosted, originalSpeed]);
|
|
||||||
|
|
||||||
return {
|
|
||||||
playbackSpeed,
|
|
||||||
setPlaybackSpeed,
|
|
||||||
holdToSpeedEnabled,
|
|
||||||
setHoldToSpeedEnabled,
|
|
||||||
holdToSpeedValue,
|
|
||||||
setHoldToSpeedValue,
|
|
||||||
isSpeedBoosted,
|
|
||||||
activateSpeedBoost,
|
|
||||||
deactivateSpeedBoost,
|
|
||||||
showSpeedActivatedOverlay,
|
|
||||||
speedActivatedOverlayOpacity
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
@ -1,120 +0,0 @@
|
||||||
import { useState, useEffect, useRef } from 'react';
|
|
||||||
import { storageService } from '../../../../services/storageService';
|
|
||||||
import { logger } from '../../../../utils/logger';
|
|
||||||
import { useSettings } from '../../../../hooks/useSettings';
|
|
||||||
|
|
||||||
export const useWatchProgress = (
|
|
||||||
id: string | undefined,
|
|
||||||
type: string | undefined,
|
|
||||||
episodeId: string | undefined,
|
|
||||||
currentTime: number,
|
|
||||||
duration: number,
|
|
||||||
paused: boolean,
|
|
||||||
traktAutosync: any,
|
|
||||||
seekToTime: (time: number) => void
|
|
||||||
) => {
|
|
||||||
const [resumePosition, setResumePosition] = useState<number | null>(null);
|
|
||||||
const [savedDuration, setSavedDuration] = useState<number | null>(null);
|
|
||||||
const [initialPosition, setInitialPosition] = useState<number | null>(null);
|
|
||||||
const [showResumeOverlay, setShowResumeOverlay] = useState(false);
|
|
||||||
const [progressSaveInterval, setProgressSaveInterval] = useState<NodeJS.Timeout | null>(null);
|
|
||||||
|
|
||||||
const { settings: appSettings } = useSettings();
|
|
||||||
const initialSeekTargetRef = useRef<number | null>(null);
|
|
||||||
|
|
||||||
// Values refs for unmount cleanup
|
|
||||||
const currentTimeRef = useRef(currentTime);
|
|
||||||
const durationRef = useRef(duration);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
currentTimeRef.current = currentTime;
|
|
||||||
}, [currentTime]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
durationRef.current = duration;
|
|
||||||
}, [duration]);
|
|
||||||
|
|
||||||
// Load Watch Progress
|
|
||||||
useEffect(() => {
|
|
||||||
const loadWatchProgress = async () => {
|
|
||||||
if (id && type) {
|
|
||||||
try {
|
|
||||||
const savedProgress = await storageService.getWatchProgress(id, type, episodeId);
|
|
||||||
if (savedProgress) {
|
|
||||||
const progressPercent = (savedProgress.currentTime / savedProgress.duration) * 100;
|
|
||||||
|
|
||||||
if (progressPercent < 85) {
|
|
||||||
setResumePosition(savedProgress.currentTime);
|
|
||||||
setSavedDuration(savedProgress.duration);
|
|
||||||
|
|
||||||
if (appSettings.alwaysResume) {
|
|
||||||
setInitialPosition(savedProgress.currentTime);
|
|
||||||
initialSeekTargetRef.current = savedProgress.currentTime;
|
|
||||||
seekToTime(savedProgress.currentTime);
|
|
||||||
} else {
|
|
||||||
setShowResumeOverlay(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('[useWatchProgress] Error loading watch progress:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
loadWatchProgress();
|
|
||||||
}, [id, type, episodeId, appSettings.alwaysResume]);
|
|
||||||
|
|
||||||
const saveWatchProgress = async () => {
|
|
||||||
if (id && type && currentTimeRef.current > 0 && durationRef.current > 0) {
|
|
||||||
const progress = {
|
|
||||||
currentTime: currentTimeRef.current,
|
|
||||||
duration: durationRef.current,
|
|
||||||
lastUpdated: Date.now()
|
|
||||||
};
|
|
||||||
try {
|
|
||||||
await storageService.setWatchProgress(id, type, progress, episodeId);
|
|
||||||
await traktAutosync.handleProgressUpdate(currentTimeRef.current, durationRef.current);
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('[useWatchProgress] Error saving watch progress:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Save Interval
|
|
||||||
useEffect(() => {
|
|
||||||
if (id && type && !paused && duration > 0) {
|
|
||||||
if (progressSaveInterval) clearInterval(progressSaveInterval);
|
|
||||||
|
|
||||||
const interval = setInterval(() => {
|
|
||||||
saveWatchProgress();
|
|
||||||
}, 10000);
|
|
||||||
|
|
||||||
setProgressSaveInterval(interval);
|
|
||||||
return () => {
|
|
||||||
clearInterval(interval);
|
|
||||||
setProgressSaveInterval(null);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}, [id, type, paused, currentTime, duration]);
|
|
||||||
|
|
||||||
// Unmount Save
|
|
||||||
useEffect(() => {
|
|
||||||
return () => {
|
|
||||||
if (id && type && durationRef.current > 0) {
|
|
||||||
saveWatchProgress();
|
|
||||||
traktAutosync.handlePlaybackEnd(currentTimeRef.current, durationRef.current, 'unmount');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}, [id, type]);
|
|
||||||
|
|
||||||
return {
|
|
||||||
resumePosition,
|
|
||||||
savedDuration,
|
|
||||||
initialPosition,
|
|
||||||
setInitialPosition,
|
|
||||||
showResumeOverlay,
|
|
||||||
setShowResumeOverlay,
|
|
||||||
saveWatchProgress,
|
|
||||||
initialSeekTargetRef
|
|
||||||
};
|
|
||||||
};
|
|
||||||
Loading…
Reference in a new issue