feat(player): implement playback speed control with persistent settings

- Add playback rate state and actions to player store
- Create SpeedSelectionModal with 7 speed options (0.5x - 2x)
- Add speed control button with Gauge icon to PlayerControls
- Integrate rate prop with Expo AV Video component
- Extend PlayerSettings storage to persist playback rate per video
- Support speed control across TV, mobile, and tablet platforms
This commit is contained in:
zimplexing
2025-08-14 15:14:37 +08:00
parent 09c3931117
commit 1ef5a6b445
5 changed files with 140 additions and 6 deletions

View File

@@ -1,6 +1,6 @@
import React from "react";
import { View, Text, StyleSheet, Pressable } from "react-native";
import { Pause, Play, SkipForward, List, Tv, ArrowDownToDot, ArrowUpFromDot } from "lucide-react-native";
import { Pause, Play, SkipForward, List, Tv, ArrowDownToDot, ArrowUpFromDot, Gauge } from "lucide-react-native";
import { ThemedText } from "@/components/ThemedText";
import { MediaButton } from "@/components/MediaButton";
@@ -21,10 +21,12 @@ export const PlayerControls: React.FC<PlayerControlsProps> = ({ showControls, se
isSeeking,
seekPosition,
progressPosition,
playbackRate,
togglePlayPause,
playEpisode,
setShowEpisodeModal,
setShowSourceModal,
setShowSpeedModal,
setIntroEndTime,
setOutroStartTime,
introEndTime,
@@ -109,6 +111,10 @@ export const PlayerControls: React.FC<PlayerControlsProps> = ({ showControls, se
<List color="white" size={24} />
</MediaButton>
<MediaButton onPress={() => setShowSpeedModal(true)} timeLabel={playbackRate !== 1.0 ? `${playbackRate}x` : undefined}>
<Gauge color="white" size={24} />
</MediaButton>
<MediaButton onPress={() => setShowSourceModal(true)}>
<Tv color="white" size={24} />
</MediaButton>

View File

@@ -0,0 +1,93 @@
import React from "react";
import { View, Text, StyleSheet, Modal, FlatList } from "react-native";
import { StyledButton } from "./StyledButton";
import usePlayerStore from "@/stores/playerStore";
interface SpeedOption {
rate: number;
label: string;
}
const SPEED_OPTIONS: SpeedOption[] = [
{ rate: 0.5, label: "0.5x" },
{ rate: 0.75, label: "0.75x" },
{ rate: 1.0, label: "1x" },
{ rate: 1.25, label: "1.25x" },
{ rate: 1.5, label: "1.5x" },
{ rate: 1.75, label: "1.75x" },
{ rate: 2.0, label: "2x" },
];
export const SpeedSelectionModal: React.FC = () => {
const { showSpeedModal, setShowSpeedModal, playbackRate, setPlaybackRate } = usePlayerStore();
const onSelectSpeed = (rate: number) => {
setPlaybackRate(rate);
setShowSpeedModal(false);
};
const onClose = () => {
setShowSpeedModal(false);
};
return (
<Modal visible={showSpeedModal} transparent={true} animationType="slide" onRequestClose={onClose}>
<View style={styles.modalContainer}>
<View style={styles.modalContent}>
<Text style={styles.modalTitle}></Text>
<FlatList
data={SPEED_OPTIONS}
numColumns={3}
contentContainerStyle={styles.speedList}
keyExtractor={(item) => `speed-${item.rate}`}
renderItem={({ item }) => (
<StyledButton
text={item.label}
onPress={() => onSelectSpeed(item.rate)}
isSelected={playbackRate === item.rate}
hasTVPreferredFocus={playbackRate === item.rate}
style={styles.speedItem}
textStyle={styles.speedItemText}
/>
)}
/>
</View>
</View>
</Modal>
);
};
const styles = StyleSheet.create({
modalContainer: {
flex: 1,
flexDirection: "row",
justifyContent: "flex-end",
backgroundColor: "transparent",
},
modalContent: {
width: 500,
height: "100%",
backgroundColor: "rgba(0, 0, 0, 0.85)",
padding: 20,
},
modalTitle: {
color: "white",
marginBottom: 12,
textAlign: "center",
fontSize: 18,
fontWeight: "bold",
},
speedList: {
justifyContent: "flex-start",
},
speedItem: {
paddingVertical: 10,
margin: 4,
marginLeft: 10,
marginRight: 8,
width: "30%",
},
speedItemText: {
fontSize: 16,
},
});