mirror of
https://github.com/zimplexing/OrionTV.git
synced 2026-02-04 11:44:44 +08:00
Enhance video playback features by adding playTime and initialPosition handling, and update PlayerControls for better focus management
This commit is contained in:
@@ -76,6 +76,7 @@ export default function HomeScreen() {
|
||||
year={item.year}
|
||||
rate={item.rate}
|
||||
progress={item.progress}
|
||||
playTime={item.play_time}
|
||||
episodeIndex={item.episodeIndex}
|
||||
sourceName={item.sourceName}
|
||||
totalEpisodes={item.totalEpisodes}
|
||||
|
||||
26
app/play.tsx
26
app/play.tsx
@@ -14,7 +14,12 @@ import { useTVRemoteHandler } from '@/hooks/useTVRemoteHandler';
|
||||
export default function PlayScreen() {
|
||||
const videoRef = useRef<Video>(null);
|
||||
useKeepAwake();
|
||||
const { source, id, episodeIndex } = useLocalSearchParams<{ source: string; id: string; episodeIndex: string }>();
|
||||
const { source, id, episodeIndex, position } = useLocalSearchParams<{
|
||||
source: string;
|
||||
id: string;
|
||||
episodeIndex: string;
|
||||
position: string;
|
||||
}>();
|
||||
|
||||
const {
|
||||
detail,
|
||||
@@ -24,6 +29,7 @@ export default function PlayScreen() {
|
||||
showControls,
|
||||
showEpisodeModal,
|
||||
showNextEpisodeOverlay,
|
||||
initialPosition,
|
||||
setVideoRef,
|
||||
loadVideo,
|
||||
playEpisode,
|
||||
@@ -39,14 +45,14 @@ export default function PlayScreen() {
|
||||
useEffect(() => {
|
||||
setVideoRef(videoRef);
|
||||
if (source && id) {
|
||||
loadVideo(source, id, parseInt(episodeIndex || '0', 10));
|
||||
loadVideo(source, id, parseInt(episodeIndex || '0', 10), parseInt(position || '0', 10));
|
||||
}
|
||||
return () => {
|
||||
reset(); // Reset state when component unmounts
|
||||
};
|
||||
}, [source, id, episodeIndex, setVideoRef, loadVideo, reset]);
|
||||
}, [source, id, episodeIndex, position, setVideoRef, loadVideo, reset]);
|
||||
|
||||
const { setCurrentFocus } = useTVRemoteHandler({
|
||||
const { currentFocus, setCurrentFocus } = useTVRemoteHandler({
|
||||
showControls,
|
||||
setShowControls,
|
||||
showEpisodeModal,
|
||||
@@ -56,6 +62,7 @@ export default function PlayScreen() {
|
||||
onPlayNextEpisode: () => {
|
||||
if (currentEpisodeIndex < episodes.length - 1) {
|
||||
playEpisode(currentEpisodeIndex + 1);
|
||||
setShowControls(false);
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -71,7 +78,7 @@ export default function PlayScreen() {
|
||||
const currentEpisode = episodes[currentEpisodeIndex];
|
||||
|
||||
return (
|
||||
<ThemedView style={styles.container}>
|
||||
<ThemedView focusable style={styles.container}>
|
||||
<TouchableOpacity
|
||||
activeOpacity={1}
|
||||
style={styles.videoContainer}
|
||||
@@ -86,13 +93,18 @@ export default function PlayScreen() {
|
||||
source={{ uri: currentEpisode?.url }}
|
||||
resizeMode={ResizeMode.CONTAIN}
|
||||
onPlaybackStatusUpdate={handlePlaybackStatusUpdate}
|
||||
onLoad={() => usePlayerStore.setState({ isLoading: false })}
|
||||
onLoad={() => {
|
||||
if (initialPosition > 0) {
|
||||
videoRef.current?.setPositionAsync(initialPosition);
|
||||
}
|
||||
usePlayerStore.setState({ isLoading: false });
|
||||
}}
|
||||
onLoadStart={() => usePlayerStore.setState({ isLoading: true })}
|
||||
useNativeControls={false}
|
||||
shouldPlay
|
||||
/>
|
||||
|
||||
{showControls && <PlayerControls />}
|
||||
{showControls && <PlayerControls currentFocus={currentFocus} setShowControls={setShowControls} />}
|
||||
|
||||
<LoadingOverlay visible={isLoading} />
|
||||
|
||||
|
||||
@@ -8,9 +8,12 @@ import { MediaButton } from '@/components/MediaButton';
|
||||
|
||||
import usePlayerStore from '@/stores/playerStore';
|
||||
|
||||
interface PlayerControlsProps {}
|
||||
interface PlayerControlsProps {
|
||||
currentFocus: string | null;
|
||||
setShowControls: (show: boolean) => void;
|
||||
}
|
||||
|
||||
export const PlayerControls: React.FC<PlayerControlsProps> = () => {
|
||||
export const PlayerControls: React.FC<PlayerControlsProps> = ({ currentFocus, setShowControls }) => {
|
||||
const router = useRouter();
|
||||
const {
|
||||
detail,
|
||||
@@ -41,6 +44,7 @@ export const PlayerControls: React.FC<PlayerControlsProps> = () => {
|
||||
const onPlayNextEpisode = () => {
|
||||
if (hasNextEpisode) {
|
||||
playEpisode(currentEpisodeIndex + 1);
|
||||
setShowControls(false);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -77,11 +81,11 @@ export const PlayerControls: React.FC<PlayerControlsProps> = () => {
|
||||
</ThemedText>
|
||||
|
||||
<View style={styles.bottomControls}>
|
||||
<MediaButton onPress={() => seek(false)}>
|
||||
<MediaButton onPress={() => seek(false)} isFocused={currentFocus === 'skipBack'}>
|
||||
<ChevronsLeft color="white" size={24} />
|
||||
</MediaButton>
|
||||
|
||||
<MediaButton onPress={togglePlayPause}>
|
||||
<MediaButton onPress={togglePlayPause} isFocused={currentFocus === 'playPause'}>
|
||||
{status?.isLoaded && status.isPlaying ? (
|
||||
<Pause color="white" size={24} />
|
||||
) : (
|
||||
@@ -89,15 +93,19 @@ export const PlayerControls: React.FC<PlayerControlsProps> = () => {
|
||||
)}
|
||||
</MediaButton>
|
||||
|
||||
<MediaButton onPress={onPlayNextEpisode} isDisabled={!hasNextEpisode}>
|
||||
<MediaButton
|
||||
onPress={onPlayNextEpisode}
|
||||
isDisabled={!hasNextEpisode}
|
||||
isFocused={currentFocus === 'nextEpisode'}
|
||||
>
|
||||
<SkipForward color={hasNextEpisode ? 'white' : '#666'} size={24} />
|
||||
</MediaButton>
|
||||
|
||||
<MediaButton onPress={() => seek(true)}>
|
||||
<MediaButton onPress={() => seek(true)} isFocused={currentFocus === 'skipForward'}>
|
||||
<ChevronsRight color="white" size={24} />
|
||||
</MediaButton>
|
||||
|
||||
<MediaButton onPress={() => setShowEpisodeModal(true)}>
|
||||
<MediaButton onPress={() => setShowEpisodeModal(true)} isFocused={currentFocus === 'episodes'}>
|
||||
<List color="white" size={24} />
|
||||
</MediaButton>
|
||||
</View>
|
||||
|
||||
@@ -16,6 +16,7 @@ interface VideoCardProps {
|
||||
rate?: string;
|
||||
sourceName?: string;
|
||||
progress?: number; // 播放进度,0-1之间的小数
|
||||
playTime?: number; // 播放时间 in ms
|
||||
episodeIndex?: number; // 剧集索引
|
||||
totalEpisodes?: number; // 总集数
|
||||
onFocus?: () => void;
|
||||
@@ -37,6 +38,7 @@ export default function VideoCard({
|
||||
onFocus,
|
||||
onRecordDeleted,
|
||||
api,
|
||||
playTime,
|
||||
}: VideoCardProps) {
|
||||
const router = useRouter();
|
||||
const [isFocused, setIsFocused] = useState(false);
|
||||
@@ -60,7 +62,7 @@ export default function VideoCard({
|
||||
if (progress !== undefined && episodeIndex !== undefined) {
|
||||
router.push({
|
||||
pathname: '/play',
|
||||
params: { source, id, episodeIndex },
|
||||
params: { source, id, episodeIndex, position: playTime },
|
||||
});
|
||||
} else {
|
||||
router.push({
|
||||
|
||||
@@ -46,7 +46,7 @@ export const useTVRemoteHandler = ({
|
||||
}
|
||||
|
||||
// Only set a timer to hide controls if they are shown AND no element is focused.
|
||||
if (showControls && currentFocus === null) {
|
||||
if (showControls) {
|
||||
controlsTimer.current = setTimeout(() => {
|
||||
setShowControls(false);
|
||||
}, 5000);
|
||||
@@ -64,9 +64,13 @@ export const useTVRemoteHandler = ({
|
||||
return;
|
||||
}
|
||||
|
||||
// If controls are hidden, the first interaction will just show them.
|
||||
// If controls are hidden, 'select' should toggle play/pause immediately
|
||||
// and other interactions will just show the controls.
|
||||
if (!showControls) {
|
||||
if (["up", "down", "left", "right", "select"].includes(event.eventType)) {
|
||||
if (event.eventType === "select") {
|
||||
onPlayPause();
|
||||
setShowControls(true);
|
||||
} else if (["up", "down", "left", "right"].includes(event.eventType)) {
|
||||
setShowControls(true);
|
||||
}
|
||||
return;
|
||||
|
||||
@@ -8,6 +8,7 @@ export type RowItem = (SearchResult | PlayRecord) & {
|
||||
title: string;
|
||||
poster: string;
|
||||
progress?: number;
|
||||
play_time?: number;
|
||||
lastPlayed?: number;
|
||||
episodeIndex?: number;
|
||||
sourceName?: string;
|
||||
@@ -61,48 +62,48 @@ const useHomeStore = create<HomeState>((set, get) => ({
|
||||
error: null,
|
||||
|
||||
fetchInitialData: async () => {
|
||||
const { selectedCategory } = get();
|
||||
set({ loading: true, contentData: [], pageStart: 0, hasMore: true, error: null });
|
||||
await get().loadMoreData();
|
||||
set({ loading: false });
|
||||
},
|
||||
|
||||
loadMoreData: async () => {
|
||||
const { selectedCategory, pageStart, loading, loadingMore, hasMore } = get();
|
||||
if (loading || loadingMore || !hasMore) return;
|
||||
const { selectedCategory, pageStart, loadingMore, hasMore } = get();
|
||||
if (loadingMore || !hasMore) return;
|
||||
|
||||
if (selectedCategory.type === 'record') {
|
||||
const records = await PlayRecordManager.getAll();
|
||||
const rowItems = Object.entries(records)
|
||||
.map(([key, record]) => {
|
||||
const [source, id] = key.split('+');
|
||||
return { ...record, id, source, progress: record.play_time / record.total_time, poster: record.cover, sourceName: record.source_name, episodeIndex: record.index, totalEpisodes: record.total_episodes, lastPlayed: record.save_time };
|
||||
})
|
||||
.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 });
|
||||
return;
|
||||
if (pageStart > 0) {
|
||||
set({ loadingMore: true });
|
||||
}
|
||||
|
||||
if (!selectedCategory.type || !selectedCategory.tag) return;
|
||||
|
||||
set({ loadingMore: true });
|
||||
try {
|
||||
const result = await api.getDoubanData(selectedCategory.type, selectedCategory.tag, 20, pageStart);
|
||||
if (result.list.length === 0) {
|
||||
set({ hasMore: false });
|
||||
if (selectedCategory.type === 'record') {
|
||||
const records = await PlayRecordManager.getAll();
|
||||
const rowItems = Object.entries(records)
|
||||
.map(([key, record]) => {
|
||||
const [source, id] = key.split('+');
|
||||
return { ...record, id, source, progress: record.play_time / record.total_time, poster: record.cover, sourceName: record.source_name, episodeIndex: record.index, totalEpisodes: record.total_episodes, lastPlayed: record.save_time, play_time: record.play_time };
|
||||
})
|
||||
.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 });
|
||||
} else if (selectedCategory.type && selectedCategory.tag) {
|
||||
const result = await api.getDoubanData(selectedCategory.type, selectedCategory.tag, 20, pageStart);
|
||||
if (result.list.length === 0) {
|
||||
set({ hasMore: false });
|
||||
} else {
|
||||
const newItems = result.list.map(item => ({
|
||||
...item,
|
||||
id: item.title,
|
||||
source: 'douban',
|
||||
})) as RowItem[];
|
||||
set(state => ({
|
||||
contentData: pageStart === 0 ? newItems : [...state.contentData, ...newItems],
|
||||
pageStart: state.pageStart + result.list.length,
|
||||
hasMore: true,
|
||||
}));
|
||||
}
|
||||
} else {
|
||||
const newItems = result.list.map(item => ({
|
||||
...item,
|
||||
id: item.title,
|
||||
source: 'douban',
|
||||
})) as RowItem[];
|
||||
set(state => ({
|
||||
contentData: pageStart === 0 ? newItems : [...state.contentData, ...newItems],
|
||||
pageStart: state.pageStart + result.list.length,
|
||||
hasMore: true,
|
||||
}));
|
||||
set({ hasMore: false });
|
||||
}
|
||||
} catch (err: any) {
|
||||
if (err.message === 'API_URL_NOT_SET') {
|
||||
@@ -111,7 +112,7 @@ const useHomeStore = create<HomeState>((set, get) => ({
|
||||
set({ error: '加载失败,请重试' });
|
||||
}
|
||||
} finally {
|
||||
set({ loadingMore: false });
|
||||
set({ loading: false, loadingMore: false });
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -27,8 +27,9 @@ interface PlayerState {
|
||||
isSeeking: boolean;
|
||||
seekPosition: number;
|
||||
progressPosition: number;
|
||||
initialPosition: number;
|
||||
setVideoRef: (ref: RefObject<Video>) => void;
|
||||
loadVideo: (source: string, id: string, episodeIndex: number) => Promise<void>;
|
||||
loadVideo: (source: string, id: string, episodeIndex: number, position?: number) => Promise<void>;
|
||||
playEpisode: (index: number) => void;
|
||||
togglePlayPause: () => void;
|
||||
seek: (forward: boolean) => void;
|
||||
@@ -53,11 +54,18 @@ const usePlayerStore = create<PlayerState>((set, get) => ({
|
||||
isSeeking: false,
|
||||
seekPosition: 0,
|
||||
progressPosition: 0,
|
||||
initialPosition: 0,
|
||||
|
||||
setVideoRef: (ref) => set({ videoRef: ref }),
|
||||
|
||||
loadVideo: async (source, id, episodeIndex) => {
|
||||
set({ isLoading: true, detail: null, episodes: [], currentEpisodeIndex: 0 });
|
||||
loadVideo: async (source, id, episodeIndex, position) => {
|
||||
set({
|
||||
isLoading: true,
|
||||
detail: null,
|
||||
episodes: [],
|
||||
currentEpisodeIndex: 0,
|
||||
initialPosition: position || 0,
|
||||
});
|
||||
try {
|
||||
const videoDetail = await api.getVideoDetail(source, id);
|
||||
const episodes = videoDetail.episodes.map((ep, index) => ({ url: ep, title: `第 ${index + 1} 集` }));
|
||||
@@ -76,7 +84,7 @@ const usePlayerStore = create<PlayerState>((set, get) => ({
|
||||
playEpisode: (index) => {
|
||||
const { episodes, videoRef } = get();
|
||||
if (index >= 0 && index < episodes.length) {
|
||||
set({ currentEpisodeIndex: index, showNextEpisodeOverlay: false });
|
||||
set({ currentEpisodeIndex: index, showNextEpisodeOverlay: false, initialPosition: 0 });
|
||||
videoRef?.current?.replayAsync();
|
||||
}
|
||||
},
|
||||
@@ -156,6 +164,7 @@ const usePlayerStore = create<PlayerState>((set, get) => ({
|
||||
showControls: true,
|
||||
showEpisodeModal: false,
|
||||
showNextEpisodeOverlay: false,
|
||||
initialPosition: 0,
|
||||
});
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { create } from 'zustand';
|
||||
import { SettingsManager } from '@/services/storage';
|
||||
import { api } from '@/services/api';
|
||||
import useHomeStore from './homeStore';
|
||||
|
||||
interface SettingsState {
|
||||
apiBaseUrl: string;
|
||||
@@ -26,6 +27,7 @@ export const useSettingsStore = create<SettingsState>((set, get) => ({
|
||||
await SettingsManager.save({ apiBaseUrl });
|
||||
api.setBaseUrl(apiBaseUrl);
|
||||
set({ isModalVisible: false });
|
||||
useHomeStore.getState().fetchInitialData();
|
||||
},
|
||||
showModal: () => set({ isModalVisible: true }),
|
||||
hideModal: () => set({ isModalVisible: false }),
|
||||
|
||||
Reference in New Issue
Block a user