mirror of
https://github.com/zimplexing/OrionTV.git
synced 2026-02-15 12:24:44 +08:00
feat: enhance PlayScreen and VideoCard with improved video loading and app state handling; update player store for better episode management
This commit is contained in:
35
app/play.tsx
35
app/play.tsx
@@ -1,5 +1,5 @@
|
||||
import React, { useEffect, useRef } from "react";
|
||||
import { StyleSheet, TouchableOpacity, ActivityIndicator, BackHandler } from "react-native";
|
||||
import { StyleSheet, TouchableOpacity, ActivityIndicator, BackHandler, AppState, AppStateStatus } from "react-native";
|
||||
import { useLocalSearchParams, useRouter } from "expo-router";
|
||||
import { Video, ResizeMode } from "expo-av";
|
||||
import { useKeepAwake } from "expo-keep-awake";
|
||||
@@ -13,7 +13,7 @@ import { LoadingOverlay } from "@/components/LoadingOverlay";
|
||||
import useDetailStore from "@/stores/detailStore";
|
||||
import { useTVRemoteHandler } from "@/hooks/useTVRemoteHandler";
|
||||
import Toast from "react-native-toast-message";
|
||||
import usePlayerStore from "@/stores/playerStore";
|
||||
import usePlayerStore, { selectCurrentEpisode } from "@/stores/playerStore";
|
||||
|
||||
export default function PlayScreen() {
|
||||
const videoRef = useRef<Video>(null);
|
||||
@@ -23,16 +23,22 @@ export default function PlayScreen() {
|
||||
episodeIndex: episodeIndexStr,
|
||||
position: positionStr,
|
||||
source: sourceStr,
|
||||
id: videoId,
|
||||
title: videoTitle,
|
||||
} = useLocalSearchParams<{
|
||||
episodeIndex: string;
|
||||
position?: string;
|
||||
source?: string;
|
||||
id?: string;
|
||||
title?: string;
|
||||
}>();
|
||||
const episodeIndex = parseInt(episodeIndexStr || "0", 10);
|
||||
const position = positionStr ? parseInt(positionStr, 10) : undefined;
|
||||
|
||||
const { detail } = useDetailStore();
|
||||
const source = sourceStr || detail?.source;
|
||||
const id = videoId || detail?.id.toString();
|
||||
const title = videoTitle || detail?.title;
|
||||
const {
|
||||
isLoading,
|
||||
showControls,
|
||||
@@ -46,17 +52,32 @@ export default function PlayScreen() {
|
||||
reset,
|
||||
loadVideo,
|
||||
} = usePlayerStore();
|
||||
const currentEpisode = usePlayerStore(selectCurrentEpisode);
|
||||
|
||||
useEffect(() => {
|
||||
setVideoRef(videoRef);
|
||||
if (source) {
|
||||
loadVideo(source, episodeIndex, position);
|
||||
if (source && id && title) {
|
||||
loadVideo({ source, id, episodeIndex, position, title });
|
||||
}
|
||||
|
||||
return () => {
|
||||
reset(); // Reset state when component unmounts
|
||||
};
|
||||
}, [episodeIndex, source, position, setVideoRef, reset, loadVideo]);
|
||||
}, [episodeIndex, source, position, setVideoRef, reset, loadVideo, id, title]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleAppStateChange = (nextAppState: AppStateStatus) => {
|
||||
if (nextAppState === "background" || nextAppState === "inactive") {
|
||||
videoRef.current?.pauseAsync();
|
||||
}
|
||||
};
|
||||
|
||||
const subscription = AppState.addEventListener("change", handleAppStateChange);
|
||||
|
||||
return () => {
|
||||
subscription.remove();
|
||||
};
|
||||
}, []);
|
||||
|
||||
const { onScreenPress } = useTVRemoteHandler();
|
||||
|
||||
@@ -83,15 +104,13 @@ export default function PlayScreen() {
|
||||
);
|
||||
}
|
||||
|
||||
const currentEpisode = detail.episodes[episodeIndex];
|
||||
|
||||
return (
|
||||
<ThemedView focusable style={styles.container}>
|
||||
<TouchableOpacity activeOpacity={1} style={styles.videoContainer} onPress={onScreenPress}>
|
||||
<Video
|
||||
ref={videoRef}
|
||||
style={styles.videoPlayer}
|
||||
source={{ uri: currentEpisode }}
|
||||
source={{ uri: currentEpisode?.url || "" }}
|
||||
usePoster
|
||||
posterSource={{ uri: detail?.poster ?? "" }}
|
||||
resizeMode={ResizeMode.CONTAIN}
|
||||
|
||||
@@ -34,11 +34,10 @@ export default function VideoCard({
|
||||
sourceName,
|
||||
progress,
|
||||
episodeIndex,
|
||||
totalEpisodes,
|
||||
onFocus,
|
||||
onRecordDeleted,
|
||||
api,
|
||||
playTime,
|
||||
playTime = 0,
|
||||
}: VideoCardProps) {
|
||||
const router = useRouter();
|
||||
const [isFocused, setIsFocused] = useState(false);
|
||||
@@ -62,7 +61,7 @@ export default function VideoCard({
|
||||
if (progress !== undefined && episodeIndex !== undefined) {
|
||||
router.push({
|
||||
pathname: "/play",
|
||||
params: { source, id, episodeIndex, position: playTime },
|
||||
params: { source, id, episodeIndex: episodeIndex - 1, title, position: playTime * 1000 },
|
||||
});
|
||||
} else {
|
||||
router.push({
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { create } from "zustand";
|
||||
import Cookies from "@react-native-cookies/cookies";
|
||||
import { api, ServerConfig } from "@/services/api";
|
||||
import { api } from "@/services/api";
|
||||
|
||||
interface AuthState {
|
||||
isLoggedIn: boolean;
|
||||
|
||||
@@ -17,7 +17,7 @@ interface DetailState {
|
||||
controller: AbortController | null;
|
||||
isFavorited: boolean;
|
||||
|
||||
init: (q: string, preferredSource?: string, id?: string) => void;
|
||||
init: (q: string, preferredSource?: string, id?: string) => Promise<void>;
|
||||
setDetail: (detail: SearchResultWithResolution) => void;
|
||||
abort: () => void;
|
||||
toggleFavorite: () => Promise<void>;
|
||||
|
||||
@@ -119,7 +119,7 @@ const useHomeStore = create<HomeState>((set, get) => ({
|
||||
play_time: record.play_time,
|
||||
};
|
||||
})
|
||||
.filter((record) => record.progress !== undefined && record.progress > 0 && record.progress < 1)
|
||||
// .filter((record) => record.progress !== undefined && record.progress > 0 && record.progress < 1)
|
||||
.sort((a, b) => (b.lastPlayed || 0) - (a.lastPlayed || 0));
|
||||
|
||||
set({ contentData: rowItems, hasMore: false });
|
||||
|
||||
@@ -27,7 +27,7 @@ interface PlayerState {
|
||||
introEndTime?: number;
|
||||
outroStartTime?: number;
|
||||
setVideoRef: (ref: RefObject<Video>) => void;
|
||||
loadVideo: (source: string, episodeIndex: number, position?: number) => Promise<void>;
|
||||
loadVideo: (options: {source: string, id: string, title: string; episodeIndex: number, position?: number}) => Promise<void>;
|
||||
playEpisode: (index: number) => void;
|
||||
togglePlayPause: () => void;
|
||||
seek: (duration: number) => void;
|
||||
@@ -48,7 +48,7 @@ interface PlayerState {
|
||||
const usePlayerStore = create<PlayerState>((set, get) => ({
|
||||
videoRef: null,
|
||||
episodes: [],
|
||||
currentEpisodeIndex: 0,
|
||||
currentEpisodeIndex: -1,
|
||||
status: null,
|
||||
isLoading: true,
|
||||
showControls: false,
|
||||
@@ -65,22 +65,31 @@ const usePlayerStore = create<PlayerState>((set, get) => ({
|
||||
|
||||
setVideoRef: (ref) => set({ videoRef: ref }),
|
||||
|
||||
loadVideo: async (source, episodeIndex, position) => {
|
||||
const detail = useDetailStore.getState().detail;
|
||||
const episodes = episodesSelectorBySource(source)(useDetailStore.getState());
|
||||
|
||||
if (!detail || !episodes || episodes.length === 0) return;
|
||||
loadVideo: async ({ source, id, episodeIndex, position, title }) => {
|
||||
let detail = useDetailStore.getState().detail;
|
||||
let episodes = episodesSelectorBySource(source)(useDetailStore.getState());
|
||||
|
||||
set({
|
||||
isLoading: true,
|
||||
});
|
||||
|
||||
if (!detail || !episodes || episodes.length === 0 || detail.title !== title) {
|
||||
await useDetailStore.getState().init(title, source, id);
|
||||
detail = useDetailStore.getState().detail;
|
||||
episodes = episodesSelectorBySource(source)(useDetailStore.getState());
|
||||
if (!detail) {
|
||||
console.info("Detail not found after initialization");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
const playRecord = await PlayRecordManager.get(detail.source, detail.id.toString());
|
||||
const initialPositionFromRecord = playRecord?.play_time ? playRecord.play_time * 1000 : 0;
|
||||
set({
|
||||
isLoading: false,
|
||||
currentEpisodeIndex: episodeIndex,
|
||||
initialPosition: position || 0,
|
||||
initialPosition: position || initialPositionFromRecord,
|
||||
episodes: episodes.map((ep, index) => ({
|
||||
url: ep,
|
||||
title: `第 ${index + 1} 集`,
|
||||
@@ -94,7 +103,7 @@ const usePlayerStore = create<PlayerState>((set, get) => ({
|
||||
}
|
||||
},
|
||||
|
||||
playEpisode: (index) => {
|
||||
playEpisode: async (index) => {
|
||||
const { episodes, videoRef } = get();
|
||||
if (index >= 0 && index < episodes.length) {
|
||||
set({
|
||||
@@ -104,27 +113,42 @@ const usePlayerStore = create<PlayerState>((set, get) => ({
|
||||
progressPosition: 0,
|
||||
seekPosition: 0,
|
||||
});
|
||||
videoRef?.current?.replayAsync();
|
||||
}
|
||||
},
|
||||
|
||||
togglePlayPause: () => {
|
||||
const { status, videoRef } = get();
|
||||
if (status?.isLoaded) {
|
||||
if (status.isPlaying) {
|
||||
videoRef?.current?.pauseAsync();
|
||||
} else {
|
||||
videoRef?.current?.playAsync();
|
||||
try {
|
||||
await videoRef?.current?.replayAsync();
|
||||
} catch (error) {
|
||||
console.error("Failed to replay video:", error);
|
||||
Toast.show({ type: "error", text1: "播放失败" });
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
seek: (duration) => {
|
||||
togglePlayPause: async () => {
|
||||
const { status, videoRef } = get();
|
||||
if (status?.isLoaded) {
|
||||
try {
|
||||
if (status.isPlaying) {
|
||||
await videoRef?.current?.pauseAsync();
|
||||
} else {
|
||||
await videoRef?.current?.playAsync();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to toggle play/pause:", error);
|
||||
Toast.show({ type: "error", text1: "操作失败" });
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
seek: async (duration) => {
|
||||
const { status, videoRef } = get();
|
||||
if (!status?.isLoaded || !status.durationMillis) return;
|
||||
|
||||
const newPosition = Math.max(0, Math.min(status.positionMillis + duration, status.durationMillis));
|
||||
videoRef?.current?.setPositionAsync(newPosition);
|
||||
try {
|
||||
await videoRef?.current?.setPositionAsync(newPosition);
|
||||
} catch (error) {
|
||||
console.error("Failed to seek video:", error);
|
||||
Toast.show({ type: "error", text1: "快进/快退失败" });
|
||||
}
|
||||
|
||||
set({
|
||||
isSeeking: true,
|
||||
@@ -202,10 +226,10 @@ const usePlayerStore = create<PlayerState>((set, get) => ({
|
||||
PlayRecordManager.save(detail.source, detail.id.toString(), {
|
||||
title: detail.title,
|
||||
cover: detail.poster || "",
|
||||
index: currentEpisodeIndex,
|
||||
index: currentEpisodeIndex + 1,
|
||||
total_episodes: episodes.length,
|
||||
play_time: status.positionMillis,
|
||||
total_time: status.durationMillis || 0,
|
||||
play_time: Math.floor(status.positionMillis / 1000),
|
||||
total_time: status.durationMillis ? Math.floor(status.durationMillis / 1000) : 0,
|
||||
source_name: detail.source_name,
|
||||
year: detail.year || "",
|
||||
...existingRecord,
|
||||
@@ -282,3 +306,9 @@ const usePlayerStore = create<PlayerState>((set, get) => ({
|
||||
}));
|
||||
|
||||
export default usePlayerStore;
|
||||
|
||||
export const selectCurrentEpisode = (state: PlayerState) => {
|
||||
if (state.episodes.length > state.currentEpisodeIndex) {
|
||||
return state.episodes[state.currentEpisodeIndex];
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2,7 +2,6 @@ import { create } from "zustand";
|
||||
import { SettingsManager } from "@/services/storage";
|
||||
import { api, ServerConfig } from "@/services/api";
|
||||
import { storageConfig } from "@/services/storageConfig";
|
||||
// import useHomeStore from './homeStore';
|
||||
|
||||
interface SettingsState {
|
||||
apiBaseUrl: string;
|
||||
@@ -77,7 +76,7 @@ export const useSettingsStore = create<SettingsState>((set, get) => ({
|
||||
});
|
||||
api.setBaseUrl(apiBaseUrl);
|
||||
set({ isModalVisible: false });
|
||||
// useHomeStore.getState().fetchInitialData();
|
||||
await get().fetchServerConfig();
|
||||
},
|
||||
showModal: () => set({ isModalVisible: true }),
|
||||
hideModal: () => set({ isModalVisible: false }),
|
||||
|
||||
Reference in New Issue
Block a user