Refactor color scheme handling to use a fixed 'dark' theme and implement SourceSelectionModal for source management in the player

This commit is contained in:
zimplexing
2025-07-08 20:57:38 +08:00
parent b238ffe3ba
commit 5043b33222
13 changed files with 224 additions and 94 deletions

View File

@@ -2,7 +2,7 @@ import React, { useCallback, useState } from "react";
import { View, Text, StyleSheet, TouchableOpacity, Pressable } from "react-native";
import { useRouter } from "expo-router";
import { AVPlaybackStatus } from "expo-av";
import { ArrowLeft, Pause, Play, SkipForward, List, ChevronsRight, ChevronsLeft } from "lucide-react-native";
import { ArrowLeft, Pause, Play, SkipForward, List, ChevronsRight, ChevronsLeft, Tv } from "lucide-react-native";
import { ThemedText } from "@/components/ThemedText";
import { MediaButton } from "@/components/MediaButton";
@@ -18,6 +18,7 @@ export const PlayerControls: React.FC<PlayerControlsProps> = ({ showControls, se
const {
detail,
currentEpisodeIndex,
currentSourceIndex,
status,
isSeeking,
seekPosition,
@@ -26,11 +27,14 @@ export const PlayerControls: React.FC<PlayerControlsProps> = ({ showControls, se
togglePlayPause,
playEpisode,
setShowEpisodeModal,
setShowSourceModal,
} = usePlayerStore();
const videoTitle = detail?.videoInfo?.title || "";
const currentEpisode = detail?.episodes[currentEpisodeIndex];
const currentEpisodeTitle = currentEpisode?.title;
const currentSource = detail?.sources[currentSourceIndex];
const currentSourceName = currentSource?.source_name;
const hasNextEpisode = currentEpisodeIndex < (detail?.episodes.length || 0) - 1;
const formatTime = (milliseconds: number) => {
@@ -51,7 +55,8 @@ export const PlayerControls: React.FC<PlayerControlsProps> = ({ showControls, se
<View style={styles.controlsOverlay}>
<View style={styles.topControls}>
<Text style={styles.controlTitle}>
{videoTitle} {currentEpisodeTitle ? `- ${currentEpisodeTitle}` : ""}
{videoTitle} {currentEpisodeTitle ? `- ${currentEpisodeTitle}` : ""}{" "}
{currentSourceName ? `(${currentSourceName})` : ""}
</Text>
</View>
@@ -99,6 +104,10 @@ export const PlayerControls: React.FC<PlayerControlsProps> = ({ showControls, se
<MediaButton onPress={() => setShowEpisodeModal(true)}>
<List color="white" size={24} />
</MediaButton>
<MediaButton onPress={() => setShowSourceModal(true)}>
<Tv color="white" size={24} />
</MediaButton>
</View>
</View>
</View>

View File

@@ -1,5 +1,5 @@
import React, { useState, useEffect, useRef } from "react";
import { Modal, View, Text, TextInput, StyleSheet, useColorScheme } from "react-native";
import { Modal, View, Text, TextInput, StyleSheet } from "react-native";
import { ThemedText } from "./ThemedText";
import { ThemedView } from "./ThemedView";
import { useSettingsStore } from "@/stores/settingsStore";
@@ -9,7 +9,7 @@ export const SettingsModal: React.FC = () => {
const { isModalVisible, hideModal, apiBaseUrl, setApiBaseUrl, saveSettings, loadSettings } = useSettingsStore();
const [isInputFocused, setIsInputFocused] = useState(false);
const colorScheme = useColorScheme();
const colorScheme = "dark"; // Replace with useColorScheme() if needed
const inputRef = useRef<TextInput>(null);
useEffect(() => {

View File

@@ -0,0 +1,78 @@
import React from "react";
import { View, Text, StyleSheet, Modal, FlatList } from "react-native";
import { StyledButton } from "./StyledButton";
import usePlayerStore from "@/stores/playerStore";
export const SourceSelectionModal: React.FC = () => {
const { showSourceModal, sources, currentSourceIndex, switchSource, setShowSourceModal } = usePlayerStore();
const onSelectSource = (index: number) => {
if (index !== currentSourceIndex) {
switchSource(index);
}
setShowSourceModal(false);
};
const onClose = () => {
setShowSourceModal(false);
};
return (
<Modal visible={showSourceModal} transparent={true} animationType="slide" onRequestClose={onClose}>
<View style={styles.modalContainer}>
<View style={styles.modalContent}>
<Text style={styles.modalTitle}></Text>
<FlatList
data={sources}
numColumns={3}
contentContainerStyle={styles.sourceList}
keyExtractor={(item, index) => `source-${item.source}-${item.id}-${index}`}
renderItem={({ item, index }) => (
<StyledButton
text={item.source_name}
onPress={() => onSelectSource(index)}
isSelected={currentSourceIndex === index}
hasTVPreferredFocus={currentSourceIndex === index}
style={styles.sourceItem}
textStyle={styles.sourceItemText}
/>
)}
/>
</View>
</View>
</Modal>
);
};
const styles = StyleSheet.create({
modalContainer: {
flex: 1,
flexDirection: "row",
justifyContent: "flex-end",
backgroundColor: "transparent",
},
modalContent: {
width: 600,
height: "100%",
backgroundColor: "rgba(0, 0, 0, 0.85)",
padding: 20,
},
modalTitle: {
color: "white",
marginBottom: 12,
textAlign: "center",
fontSize: 18,
fontWeight: "bold",
},
sourceList: {
justifyContent: "flex-start",
},
sourceItem: {
paddingVertical: 2,
margin: 4,
width: "31%",
},
sourceItemText: {
fontSize: 14,
},
});

View File

@@ -1,14 +1,5 @@
import React from "react";
import {
Animated,
Pressable,
StyleSheet,
StyleProp,
ViewStyle,
PressableProps,
TextStyle,
useColorScheme,
} from "react-native";
import { Animated, Pressable, StyleSheet, StyleProp, ViewStyle, PressableProps, TextStyle } from "react-native";
import { ThemedText } from "./ThemedText";
import { Colors } from "@/constants/Colors";
import { useButtonAnimation } from "@/hooks/useButtonAnimation";
@@ -31,7 +22,7 @@ export const StyledButton: React.FC<StyledButtonProps> = ({
textStyle,
...rest
}) => {
const colorScheme = useColorScheme() ?? "dark";
const colorScheme = "dark";
const colors = Colors[colorScheme];
const [isFocused, setIsFocused] = React.useState(false);
const animationStyle = useButtonAnimation(isFocused);

View File

@@ -1,11 +1,11 @@
import React, { useState, useEffect, useCallback, useRef } from 'react';
import { View, Text, Image, StyleSheet, Pressable, TouchableOpacity, Alert } from 'react-native';
import Animated, { useSharedValue, useAnimatedStyle, withSpring } from 'react-native-reanimated';
import { useRouter } from 'expo-router';
import { Heart, Star, Play, Trash2 } from 'lucide-react-native';
import { FavoriteManager, PlayRecordManager } from '@/services/storage';
import { API, api } from '@/services/api';
import { ThemedText } from '@/components/ThemedText';
import React, { useState, useEffect, useCallback, useRef } from "react";
import { View, Text, Image, StyleSheet, Pressable, TouchableOpacity, Alert } from "react-native";
import Animated, { useSharedValue, useAnimatedStyle, withSpring } from "react-native-reanimated";
import { useRouter } from "expo-router";
import { Heart, Star, Play, Trash2 } from "lucide-react-native";
import { FavoriteManager, PlayRecordManager } from "@/services/storage";
import { API, api } from "@/services/api";
import { ThemedText } from "@/components/ThemedText";
interface VideoCardProps {
id: string;
@@ -61,12 +61,12 @@ export default function VideoCard({
// 如果有播放进度,直接转到播放页面
if (progress !== undefined && episodeIndex !== undefined) {
router.push({
pathname: '/play',
pathname: "/play",
params: { source, id, episodeIndex, position: playTime },
});
} else {
router.push({
pathname: '/detail',
pathname: "/detail",
params: { source, q: title },
});
}
@@ -90,14 +90,14 @@ export default function VideoCard({
longPressTriggered.current = true;
// Show confirmation dialog to delete play record
Alert.alert('删除观看记录', `确定要删除"${title}"的观看记录吗?`, [
Alert.alert("删除观看记录", `确定要删除"${title}"的观看记录吗?`, [
{
text: '取消',
style: 'cancel',
text: "取消",
style: "cancel",
},
{
text: '删除',
style: 'destructive',
text: "删除",
style: "destructive",
onPress: async () => {
try {
// Delete from local storage
@@ -109,11 +109,11 @@ export default function VideoCard({
}
// 如果没有回调函数,则使用导航刷新作为备选方案
else if (router.canGoBack()) {
router.replace('/');
router.replace("/");
}
} catch (error) {
console.error('Failed to delete play record:', error);
Alert.alert('错误', '删除观看记录失败,请重试');
console.error("Failed to delete play record:", error);
Alert.alert("错误", "删除观看记录失败,请重试");
}
},
},
@@ -173,7 +173,7 @@ export default function VideoCard({
</View>
<View style={styles.infoContainer}>
<ThemedText numberOfLines={1}>{title}</ThemedText>
{isContinueWatching && !isFocused && (
{isContinueWatching && (
<View style={styles.infoRow}>
<ThemedText style={styles.continueLabel}>
{episodeIndex! + 1} {Math.round((progress || 0) * 100)}%
@@ -194,126 +194,126 @@ const styles = StyleSheet.create({
marginHorizontal: 8,
},
pressable: {
alignItems: 'center',
alignItems: "center",
},
card: {
width: CARD_WIDTH,
height: CARD_HEIGHT,
borderRadius: 8,
backgroundColor: '#222',
overflow: 'hidden',
backgroundColor: "#222",
overflow: "hidden",
},
poster: {
width: '100%',
height: '100%',
width: "100%",
height: "100%",
},
overlay: {
...StyleSheet.absoluteFillObject,
backgroundColor: 'rgba(0,0,0,0.3)',
justifyContent: 'center',
alignItems: 'center',
backgroundColor: "rgba(0,0,0,0.3)",
justifyContent: "center",
alignItems: "center",
},
buttonRow: {
position: 'absolute',
position: "absolute",
top: 8,
left: 8,
flexDirection: 'row',
flexDirection: "row",
gap: 8,
},
iconButton: {
padding: 4,
},
favButton: {
position: 'absolute',
position: "absolute",
top: 8,
left: 8,
},
ratingContainer: {
position: 'absolute',
position: "absolute",
top: 8,
right: 8,
flexDirection: 'row',
alignItems: 'center',
backgroundColor: 'rgba(0, 0, 0, 0.7)',
flexDirection: "row",
alignItems: "center",
backgroundColor: "rgba(0, 0, 0, 0.7)",
borderRadius: 6,
paddingHorizontal: 6,
paddingVertical: 3,
},
ratingText: {
color: '#FFD700',
color: "#FFD700",
fontSize: 12,
fontWeight: 'bold',
fontWeight: "bold",
marginLeft: 4,
},
infoContainer: {
width: CARD_WIDTH,
marginTop: 8,
alignItems: 'flex-start', // Align items to the start
alignItems: "flex-start", // Align items to the start
marginBottom: 16,
paddingHorizontal: 4, // Add some padding
},
infoRow: {
flexDirection: 'row',
justifyContent: 'space-between',
width: '100%',
flexDirection: "row",
justifyContent: "space-between",
width: "100%",
},
title: {
color: 'white',
color: "white",
fontSize: 16,
fontWeight: 'bold',
textAlign: 'center',
fontWeight: "bold",
textAlign: "center",
},
yearBadge: {
position: 'absolute',
position: "absolute",
top: 8,
right: 8,
backgroundColor: 'rgba(0, 0, 0, 0.7)',
backgroundColor: "rgba(0, 0, 0, 0.7)",
borderRadius: 6,
paddingHorizontal: 6,
paddingVertical: 3,
},
sourceNameBadge: {
position: 'absolute',
position: "absolute",
top: 8,
left: 8,
backgroundColor: 'rgba(0, 0, 0, 0.7)',
backgroundColor: "rgba(0, 0, 0, 0.7)",
borderRadius: 6,
paddingHorizontal: 6,
paddingVertical: 3,
},
badgeText: {
color: 'white',
color: "white",
fontSize: 12,
fontWeight: 'bold',
fontWeight: "bold",
},
progressContainer: {
position: 'absolute',
position: "absolute",
bottom: 0,
left: 0,
right: 0,
height: 3,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
backgroundColor: "rgba(0, 0, 0, 0.5)",
},
progressBar: {
height: 3,
backgroundColor: '#ff0000',
backgroundColor: "#ff0000",
},
continueWatchingBadge: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: 'rgba(255, 0, 0, 0.8)',
flexDirection: "row",
alignItems: "center",
backgroundColor: "rgba(255, 0, 0, 0.8)",
paddingHorizontal: 10,
paddingVertical: 5,
borderRadius: 5,
},
continueWatchingText: {
color: 'white',
color: "white",
marginLeft: 5,
fontSize: 12,
fontWeight: 'bold',
fontWeight: "bold",
},
continueLabel: {
color: '#ff5252',
color: "#ff5252",
fontSize: 12,
},
});