mirror of
https://github.com/zimplexing/OrionTV.git
synced 2026-05-18 06:47:29 +08:00
Update
This commit is contained in:
194
app/play.tsx
Normal file
194
app/play.tsx
Normal file
@@ -0,0 +1,194 @@
|
||||
import React, { useState, useRef } from "react";
|
||||
import {
|
||||
View,
|
||||
StyleSheet,
|
||||
TouchableOpacity,
|
||||
ActivityIndicator,
|
||||
} from "react-native";
|
||||
import { useRouter } from "expo-router";
|
||||
import { Video, ResizeMode } from "expo-av";
|
||||
import { useKeepAwake } from "expo-keep-awake";
|
||||
import { ThemedView } from "@/components/ThemedView";
|
||||
import { PlayerControls } from "@/components/PlayerControls";
|
||||
import { EpisodeSelectionModal } from "@/components/EpisodeSelectionModal";
|
||||
import { NextEpisodeOverlay } from "@/components/NextEpisodeOverlay";
|
||||
import { LoadingOverlay } from "@/components/LoadingOverlay";
|
||||
import { usePlaybackManager } from "@/hooks/usePlaybackManager";
|
||||
import { useTVRemoteHandler } from "@/hooks/useTVRemoteHandler";
|
||||
|
||||
export default function PlayScreen() {
|
||||
const router = useRouter();
|
||||
const videoRef = useRef<Video>(null);
|
||||
useKeepAwake();
|
||||
|
||||
const {
|
||||
detail,
|
||||
episodes,
|
||||
currentEpisodeIndex,
|
||||
status,
|
||||
isLoading,
|
||||
setIsLoading,
|
||||
showNextEpisodeOverlay,
|
||||
playEpisode,
|
||||
togglePlayPause,
|
||||
seek,
|
||||
handlePlaybackStatusUpdate,
|
||||
setShowNextEpisodeOverlay,
|
||||
} = usePlaybackManager(videoRef);
|
||||
|
||||
const [showControls, setShowControls] = useState(true);
|
||||
const [showEpisodeModal, setShowEpisodeModal] = useState(false);
|
||||
const [episodeGroupSize] = useState(30);
|
||||
const [selectedEpisodeGroup, setSelectedEpisodeGroup] = useState(
|
||||
Math.floor(currentEpisodeIndex / episodeGroupSize)
|
||||
);
|
||||
|
||||
const { currentFocus, setCurrentFocus } = useTVRemoteHandler({
|
||||
showControls,
|
||||
setShowControls,
|
||||
showEpisodeModal,
|
||||
onPlayPause: togglePlayPause,
|
||||
onSeek: seek,
|
||||
onShowEpisodes: () => setShowEpisodeModal(true),
|
||||
onPlayNextEpisode: () => {
|
||||
if (currentEpisodeIndex < episodes.length - 1) {
|
||||
playEpisode(currentEpisodeIndex + 1);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const [isSeeking, setIsSeeking] = useState(false);
|
||||
const [seekPosition, setSeekPosition] = useState(0);
|
||||
const [progressPosition, setProgressPosition] = useState(0);
|
||||
|
||||
const formatTime = (milliseconds: number) => {
|
||||
if (!milliseconds) return "00:00";
|
||||
const totalSeconds = Math.floor(milliseconds / 1000);
|
||||
const minutes = Math.floor(totalSeconds / 60);
|
||||
const seconds = totalSeconds % 60;
|
||||
return `${minutes.toString().padStart(2, "0")}:${seconds
|
||||
.toString()
|
||||
.padStart(2, "0")}`;
|
||||
};
|
||||
|
||||
const handleSeekStart = () => setIsSeeking(true);
|
||||
|
||||
const handleSeekMove = (event: { nativeEvent: { locationX: number } }) => {
|
||||
if (!status?.isLoaded || !status.durationMillis) return;
|
||||
const { locationX } = event.nativeEvent;
|
||||
const progressBarWidth = 300;
|
||||
const progress = Math.max(0, Math.min(locationX / progressBarWidth, 1));
|
||||
setSeekPosition(progress);
|
||||
};
|
||||
|
||||
const handleSeekRelease = (event: { nativeEvent: { locationX: number } }) => {
|
||||
if (!videoRef.current || !status?.isLoaded || !status.durationMillis)
|
||||
return;
|
||||
const wasPlaying = status.isPlaying;
|
||||
const { locationX } = event.nativeEvent;
|
||||
const progressBarWidth = 300;
|
||||
const progress = Math.max(0, Math.min(locationX / progressBarWidth, 1));
|
||||
const newPosition = progress * status.durationMillis;
|
||||
videoRef.current.setPositionAsync(newPosition).then(() => {
|
||||
if (wasPlaying) {
|
||||
videoRef.current?.playAsync();
|
||||
}
|
||||
});
|
||||
setIsSeeking(false);
|
||||
};
|
||||
|
||||
if (!detail && isLoading) {
|
||||
return (
|
||||
<ThemedView style={[styles.container, styles.centered]}>
|
||||
<ActivityIndicator size="large" color="#fff" />
|
||||
</ThemedView>
|
||||
);
|
||||
}
|
||||
|
||||
const currentEpisode = episodes[currentEpisodeIndex];
|
||||
const videoTitle = detail?.videoInfo?.title || "";
|
||||
const hasNextEpisode = currentEpisodeIndex < episodes.length - 1;
|
||||
|
||||
return (
|
||||
<ThemedView style={styles.container}>
|
||||
<TouchableOpacity
|
||||
activeOpacity={1}
|
||||
style={styles.videoContainer}
|
||||
onPress={() => {
|
||||
setShowControls(!showControls);
|
||||
setCurrentFocus(null);
|
||||
}}
|
||||
>
|
||||
<Video
|
||||
ref={videoRef}
|
||||
style={styles.videoPlayer}
|
||||
source={{ uri: currentEpisode?.url }}
|
||||
resizeMode={ResizeMode.CONTAIN}
|
||||
onPlaybackStatusUpdate={(s) => {
|
||||
handlePlaybackStatusUpdate(s);
|
||||
if (s.isLoaded && !isSeeking) {
|
||||
setProgressPosition(s.positionMillis / (s.durationMillis || 1));
|
||||
}
|
||||
}}
|
||||
onLoad={() => setIsLoading(false)}
|
||||
onLoadStart={() => setIsLoading(true)}
|
||||
useNativeControls={false}
|
||||
shouldPlay
|
||||
/>
|
||||
|
||||
{showControls && (
|
||||
<PlayerControls
|
||||
videoTitle={videoTitle}
|
||||
currentEpisodeTitle={currentEpisode?.title}
|
||||
status={status}
|
||||
isSeeking={isSeeking}
|
||||
seekPosition={seekPosition}
|
||||
progressPosition={progressPosition}
|
||||
currentFocus={currentFocus}
|
||||
hasNextEpisode={hasNextEpisode}
|
||||
onSeekStart={handleSeekStart}
|
||||
onSeekMove={handleSeekMove}
|
||||
onSeekRelease={handleSeekRelease}
|
||||
onSeek={seek}
|
||||
onTogglePlayPause={togglePlayPause}
|
||||
onPlayNextEpisode={() => playEpisode(currentEpisodeIndex + 1)}
|
||||
onShowEpisodes={() => setShowEpisodeModal(true)}
|
||||
formatTime={formatTime}
|
||||
/>
|
||||
)}
|
||||
|
||||
<LoadingOverlay visible={isLoading} />
|
||||
|
||||
<NextEpisodeOverlay
|
||||
visible={showNextEpisodeOverlay}
|
||||
onCancel={() => setShowNextEpisodeOverlay(false)}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
|
||||
<EpisodeSelectionModal
|
||||
visible={showEpisodeModal}
|
||||
episodes={episodes}
|
||||
currentEpisodeIndex={currentEpisodeIndex}
|
||||
episodeGroupSize={episodeGroupSize}
|
||||
selectedEpisodeGroup={selectedEpisodeGroup}
|
||||
setSelectedEpisodeGroup={setSelectedEpisodeGroup}
|
||||
onSelectEpisode={(index) => {
|
||||
playEpisode(index);
|
||||
setShowEpisodeModal(false);
|
||||
}}
|
||||
onClose={() => setShowEpisodeModal(false)}
|
||||
/>
|
||||
</ThemedView>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: { flex: 1, backgroundColor: "black" },
|
||||
centered: { flex: 1, justifyContent: "center", alignItems: "center" },
|
||||
videoContainer: {
|
||||
...StyleSheet.absoluteFillObject,
|
||||
},
|
||||
videoPlayer: {
|
||||
...StyleSheet.absoluteFillObject,
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user