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