diff --git a/app/play.tsx b/app/play.tsx
index 11eb37f..e994a80 100644
--- a/app/play.tsx
+++ b/app/play.tsx
@@ -7,6 +7,7 @@ import { ThemedView } from "@/components/ThemedView";
import { PlayerControls } from "@/components/PlayerControls";
import { EpisodeSelectionModal } from "@/components/EpisodeSelectionModal";
import { SourceSelectionModal } from "@/components/SourceSelectionModal";
+import { SpeedSelectionModal } from "@/components/SpeedSelectionModal";
import { SeekingBar } from "@/components/SeekingBar";
// import { NextEpisodeOverlay } from "@/components/NextEpisodeOverlay";
import VideoLoadingAnimation from "@/components/VideoLoadingAnimation";
@@ -50,6 +51,7 @@ export default function PlayScreen() {
// showNextEpisodeOverlay,
initialPosition,
introEndTime,
+ playbackRate,
setVideoRef,
handlePlaybackStatusUpdate,
setShowControls,
@@ -147,6 +149,7 @@ export default function PlayScreen() {
source={{ uri: currentEpisode?.url || "" }}
posterSource={{ uri: detail?.poster ?? "" }}
resizeMode={ResizeMode.CONTAIN}
+ rate={playbackRate}
onPlaybackStatusUpdate={handlePlaybackStatusUpdate}
onLoad={() => {
const jumpPosition = initialPosition || introEndTime || 0;
@@ -175,6 +178,7 @@ export default function PlayScreen() {
+
);
}
diff --git a/components/PlayerControls.tsx b/components/PlayerControls.tsx
index fcb3c4c..e15e5d7 100644
--- a/components/PlayerControls.tsx
+++ b/components/PlayerControls.tsx
@@ -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 = ({ showControls, se
isSeeking,
seekPosition,
progressPosition,
+ playbackRate,
togglePlayPause,
playEpisode,
setShowEpisodeModal,
setShowSourceModal,
+ setShowSpeedModal,
setIntroEndTime,
setOutroStartTime,
introEndTime,
@@ -109,6 +111,10 @@ export const PlayerControls: React.FC = ({ showControls, se
+ setShowSpeedModal(true)} timeLabel={playbackRate !== 1.0 ? `${playbackRate}x` : undefined}>
+
+
+
setShowSourceModal(true)}>
diff --git a/components/SpeedSelectionModal.tsx b/components/SpeedSelectionModal.tsx
new file mode 100644
index 0000000..4dbd3e9
--- /dev/null
+++ b/components/SpeedSelectionModal.tsx
@@ -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 (
+
+
+
+ 播放速度
+ `speed-${item.rate}`}
+ renderItem={({ item }) => (
+ onSelectSpeed(item.rate)}
+ isSelected={playbackRate === item.rate}
+ hasTVPreferredFocus={playbackRate === item.rate}
+ style={styles.speedItem}
+ textStyle={styles.speedItemText}
+ />
+ )}
+ />
+
+
+
+ );
+};
+
+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,
+ },
+});
\ No newline at end of file
diff --git a/services/storage.ts b/services/storage.ts
index 46442dd..1fd7028 100644
--- a/services/storage.ts
+++ b/services/storage.ts
@@ -22,6 +22,7 @@ export type Favorite = ApiFavorite;
export interface PlayerSettings {
introEndTime?: number;
outroStartTime?: number;
+ playbackRate?: number;
}
export interface AppSettings {
@@ -60,10 +61,10 @@ export class PlayerSettingsManager {
const allSettings = await this.getAll();
const key = generateKey(source, id);
// Only save if there are actual values to save
- if (settings.introEndTime !== undefined || settings.outroStartTime !== undefined) {
+ if (settings.introEndTime !== undefined || settings.outroStartTime !== undefined || settings.playbackRate !== undefined) {
allSettings[key] = { ...allSettings[key], ...settings };
} else {
- // If both are undefined, remove the key
+ // If all are undefined, remove the key
delete allSettings[key];
}
await AsyncStorage.setItem(STORAGE_KEYS.PLAYER_SETTINGS, JSON.stringify(allSettings));
diff --git a/stores/playerStore.ts b/stores/playerStore.ts
index 76f9d5f..c60d93e 100644
--- a/stores/playerStore.ts
+++ b/stores/playerStore.ts
@@ -2,7 +2,7 @@ import { create } from "zustand";
import Toast from "react-native-toast-message";
import { AVPlaybackStatus, Video } from "expo-av";
import { RefObject } from "react";
-import { PlayRecord, PlayRecordManager } from "@/services/storage";
+import { PlayRecord, PlayRecordManager, PlayerSettingsManager } from "@/services/storage";
import useDetailStore, { episodesSelectorBySource } from "./detailStore";
interface Episode {
@@ -19,11 +19,13 @@ interface PlayerState {
showControls: boolean;
showEpisodeModal: boolean;
showSourceModal: boolean;
+ showSpeedModal: boolean;
showNextEpisodeOverlay: boolean;
isSeeking: boolean;
seekPosition: number;
progressPosition: number;
initialPosition: number;
+ playbackRate: number;
introEndTime?: number;
outroStartTime?: number;
setVideoRef: (ref: RefObject