mirror of
https://github.com/movie-web/native-app.git
synced 2025-09-13 16:33:26 +00:00
Improve layout, add current time and duration
This commit is contained in:
@@ -13,8 +13,7 @@ import { findHighestQuality } from "@movie-web/provider-utils";
|
|||||||
|
|
||||||
import type { ItemData } from "~/components/item/item";
|
import type { ItemData } from "~/components/item/item";
|
||||||
import type { HeaderData } from "~/components/player/Header";
|
import type { HeaderData } from "~/components/player/Header";
|
||||||
import { Header } from "~/components/player/Header";
|
import { ControlsOverlay } from "~/components/player/ControlsOverlay";
|
||||||
import { MiddleControls } from "~/components/player/MiddleControls";
|
|
||||||
import { usePlayerStore } from "~/stores/player/store";
|
import { usePlayerStore } from "~/stores/player/store";
|
||||||
|
|
||||||
export default function VideoPlayerWrapper() {
|
export default function VideoPlayerWrapper() {
|
||||||
@@ -44,6 +43,7 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({ data }) => {
|
|||||||
const scale = useSharedValue(1);
|
const scale = useSharedValue(1);
|
||||||
const setVideoRef = usePlayerStore((state) => state.setVideoRef);
|
const setVideoRef = usePlayerStore((state) => state.setVideoRef);
|
||||||
const setStatus = usePlayerStore((state) => state.setStatus);
|
const setStatus = usePlayerStore((state) => state.setStatus);
|
||||||
|
const isIdle = usePlayerStore((state) => state.interface.isIdle);
|
||||||
const setIsIdle = usePlayerStore((state) => state.setIsIdle);
|
const setIsIdle = usePlayerStore((state) => state.setIsIdle);
|
||||||
const presentFullscreenPlayer = usePlayerStore(
|
const presentFullscreenPlayer = usePlayerStore(
|
||||||
(state) => state.presentFullscreenPlayer,
|
(state) => state.presentFullscreenPlayer,
|
||||||
@@ -67,6 +67,11 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({ data }) => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const initializePlayer = async () => {
|
const initializePlayer = async () => {
|
||||||
|
if (!data) {
|
||||||
|
await dismissFullscreenPlayer();
|
||||||
|
return router.push("/(tabs)");
|
||||||
|
}
|
||||||
|
|
||||||
StatusBar.setStatusBarHidden(true);
|
StatusBar.setStatusBarHidden(true);
|
||||||
|
|
||||||
if (Platform.OS === "android") {
|
if (Platform.OS === "android") {
|
||||||
@@ -74,11 +79,6 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({ data }) => {
|
|||||||
}
|
}
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
|
||||||
if (!data) {
|
|
||||||
await dismissFullscreenPlayer();
|
|
||||||
return router.push("/(tabs)");
|
|
||||||
}
|
|
||||||
|
|
||||||
const { item, stream, media } = data;
|
const { item, stream, media } = data;
|
||||||
|
|
||||||
setHeaderData({
|
setHeaderData({
|
||||||
@@ -145,16 +145,17 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({ data }) => {
|
|||||||
ref={setVideoRef}
|
ref={setVideoRef}
|
||||||
source={videoSrc}
|
source={videoSrc}
|
||||||
shouldPlay
|
shouldPlay
|
||||||
resizeMode={resizeMode}
|
resizeMode={ResizeMode.CONTAIN}
|
||||||
onLoadStart={onVideoLoadStart}
|
onLoadStart={onVideoLoadStart}
|
||||||
onReadyForDisplay={onReadyForDisplay}
|
onReadyForDisplay={onReadyForDisplay}
|
||||||
onPlaybackStatusUpdate={setStatus}
|
onPlaybackStatusUpdate={setStatus}
|
||||||
style={styles.video}
|
style={styles.video}
|
||||||
onTouchStart={() => setIsIdle(false)}
|
onTouchStart={() => setIsIdle(!isIdle)}
|
||||||
/>
|
/>
|
||||||
{isLoading && <ActivityIndicator size="large" color="#0000ff" />}
|
{isLoading && <ActivityIndicator size="large" color="#0000ff" />}
|
||||||
{!isLoading && data && <Header data={headerData!} />}
|
{!isLoading && headerData && (
|
||||||
{!isLoading && <MiddleControls />}
|
<ControlsOverlay headerData={headerData} />
|
||||||
|
)}
|
||||||
</View>
|
</View>
|
||||||
</GestureDetector>
|
</GestureDetector>
|
||||||
);
|
);
|
||||||
|
28
apps/expo/src/components/player/BottomControls.tsx
Normal file
28
apps/expo/src/components/player/BottomControls.tsx
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import { View } from "react-native";
|
||||||
|
|
||||||
|
import { usePlayerStore } from "~/stores/player/store";
|
||||||
|
import { Text } from "../ui/Text";
|
||||||
|
import { Controls } from "./Controls";
|
||||||
|
import { mapMillisecondsToTime } from "./utils";
|
||||||
|
|
||||||
|
export const BottomControls = () => {
|
||||||
|
const status = usePlayerStore((state) => state.status);
|
||||||
|
status?.isLoaded;
|
||||||
|
|
||||||
|
if (status?.isLoaded) {
|
||||||
|
return (
|
||||||
|
<Controls>
|
||||||
|
<View className="flex h-16 w-full flex-row items-center justify-center">
|
||||||
|
<View className="flex flex-row items-center justify-center gap-2 px-4 py-2">
|
||||||
|
<Text className="font-bold">
|
||||||
|
{mapMillisecondsToTime(status.positionMillis ?? 0)}
|
||||||
|
</Text>
|
||||||
|
<Text className="font-bold">
|
||||||
|
{mapMillisecondsToTime(status.durationMillis ?? 0)}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</Controls>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
@@ -1,5 +1,6 @@
|
|||||||
|
import type { TouchableOpacity } from "react-native";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { TouchableOpacity } from "react-native";
|
import { TouchableWithoutFeedback } from "react-native-gesture-handler";
|
||||||
|
|
||||||
import { usePlayerStore } from "~/stores/player/store";
|
import { usePlayerStore } from "~/stores/player/store";
|
||||||
|
|
||||||
@@ -9,10 +10,14 @@ interface ControlsProps extends React.ComponentProps<typeof TouchableOpacity> {
|
|||||||
|
|
||||||
export const Controls = ({ children, className }: ControlsProps) => {
|
export const Controls = ({ children, className }: ControlsProps) => {
|
||||||
const idle = usePlayerStore((state) => state.interface.isIdle);
|
const idle = usePlayerStore((state) => state.interface.isIdle);
|
||||||
|
const setIsIdle = usePlayerStore((state) => state.setIsIdle);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TouchableOpacity className={className}>
|
<TouchableWithoutFeedback
|
||||||
|
className={className}
|
||||||
|
onPress={() => setIsIdle(false)}
|
||||||
|
>
|
||||||
{!idle && children}
|
{!idle && children}
|
||||||
</TouchableOpacity>
|
</TouchableWithoutFeedback>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
20
apps/expo/src/components/player/ControlsOverlay.tsx
Normal file
20
apps/expo/src/components/player/ControlsOverlay.tsx
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { View } from "react-native";
|
||||||
|
|
||||||
|
import type { HeaderData } from "./Header";
|
||||||
|
import { BottomControls } from "./BottomControls";
|
||||||
|
import { Header } from "./Header";
|
||||||
|
import { MiddleControls } from "./MiddleControls";
|
||||||
|
|
||||||
|
interface ControlsOverlayProps {
|
||||||
|
headerData: HeaderData;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ControlsOverlay = ({ headerData }: ControlsOverlayProps) => {
|
||||||
|
return (
|
||||||
|
<View className="absolute left-0 top-0 flex h-full w-full flex-1">
|
||||||
|
<Header data={headerData} />
|
||||||
|
<MiddleControls />
|
||||||
|
<BottomControls />
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
@@ -1,5 +1,6 @@
|
|||||||
import { Image, View } from "react-native";
|
import { Image, View } from "react-native";
|
||||||
|
|
||||||
|
import { usePlayerStore } from "~/stores/player/store";
|
||||||
import Icon from "../../../assets/images/icon-transparent.png";
|
import Icon from "../../../assets/images/icon-transparent.png";
|
||||||
import { Text } from "../ui/Text";
|
import { Text } from "../ui/Text";
|
||||||
import { BackButton } from "./BackButton";
|
import { BackButton } from "./BackButton";
|
||||||
@@ -17,18 +18,24 @@ interface HeaderProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const Header = ({ data }: HeaderProps) => {
|
export const Header = ({ data }: HeaderProps) => {
|
||||||
return (
|
const isIdle = usePlayerStore((state) => state.interface.isIdle);
|
||||||
<Controls className="absolute top-0 flex w-full flex-row items-center justify-between px-6 pt-6">
|
|
||||||
<BackButton className="w-36" />
|
if (!isIdle) {
|
||||||
<Text className="font-bold">
|
return (
|
||||||
{data.season !== undefined && data.episode !== undefined
|
<View className="flex h-16 w-full flex-row items-center justify-between px-6 pt-6">
|
||||||
? `${data.title} (${data.year}) S${data.season.toString().padStart(2, "0")}E${data.episode.toString().padStart(2, "0")}`
|
<Controls>
|
||||||
: `${data.title} (${data.year})`}
|
<BackButton className="w-36" />
|
||||||
</Text>
|
</Controls>
|
||||||
<View className="flex w-36 flex-row items-center justify-center gap-2 space-x-2 rounded-full bg-secondary-300 px-4 py-2 opacity-80">
|
<Text className="font-bold">
|
||||||
<Image source={Icon} className="h-6 w-6" />
|
{data.season !== undefined && data.episode !== undefined
|
||||||
<Text className="font-bold">movie-web</Text>
|
? `${data.title} (${data.year}) S${data.season.toString().padStart(2, "0")}E${data.episode.toString().padStart(2, "0")}`
|
||||||
|
: `${data.title} (${data.year})`}
|
||||||
|
</Text>
|
||||||
|
<View className="flex w-36 flex-row items-center justify-center gap-2 space-x-2 rounded-full bg-secondary-300 px-4 py-2 opacity-80">
|
||||||
|
<Image source={Icon} className="h-6 w-6" />
|
||||||
|
<Text className="font-bold">movie-web</Text>
|
||||||
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</Controls>
|
);
|
||||||
);
|
}
|
||||||
};
|
};
|
||||||
|
@@ -1,31 +1,21 @@
|
|||||||
import { TouchableWithoutFeedback, View } from "react-native";
|
import { View } from "react-native";
|
||||||
|
|
||||||
import { usePlayerStore } from "~/stores/player/store";
|
|
||||||
import { Controls } from "./Controls";
|
import { Controls } from "./Controls";
|
||||||
import { PlayButton } from "./PlayButton";
|
import { PlayButton } from "./PlayButton";
|
||||||
import { SeekButton } from "./SeekButton";
|
import { SeekButton } from "./SeekButton";
|
||||||
|
|
||||||
export const MiddleControls = () => {
|
export const MiddleControls = () => {
|
||||||
const idle = usePlayerStore((state) => state.interface.isIdle);
|
|
||||||
const setIsIdle = usePlayerStore((state) => state.setIsIdle);
|
|
||||||
|
|
||||||
const handleTouch = () => {
|
|
||||||
setIsIdle(!idle);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TouchableWithoutFeedback onPress={handleTouch}>
|
<View className="flex flex-1 flex-row items-center justify-center gap-24">
|
||||||
<View className="absolute inset-x-0 bottom-1/3 top-1/3 flex flex-row items-center justify-center gap-24">
|
<Controls>
|
||||||
<Controls>
|
<SeekButton type="backward" />
|
||||||
<SeekButton type="backward" />
|
</Controls>
|
||||||
</Controls>
|
<Controls>
|
||||||
<Controls>
|
<PlayButton />
|
||||||
<PlayButton />
|
</Controls>
|
||||||
</Controls>
|
<Controls>
|
||||||
<Controls>
|
<SeekButton type="forward" />
|
||||||
<SeekButton type="forward" />
|
</Controls>
|
||||||
</Controls>
|
</View>
|
||||||
</View>
|
|
||||||
</TouchableWithoutFeedback>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
18
apps/expo/src/components/player/utils.ts
Normal file
18
apps/expo/src/components/player/utils.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
export const mapMillisecondsToTime = (milliseconds: number): string => {
|
||||||
|
const hours = Math.floor(milliseconds / (1000 * 60 * 60));
|
||||||
|
const minutes = Math.floor((milliseconds % (1000 * 60 * 60)) / (1000 * 60));
|
||||||
|
const seconds = Math.floor((milliseconds % (1000 * 60)) / 1000);
|
||||||
|
|
||||||
|
const components: string[] = [];
|
||||||
|
|
||||||
|
if (hours > 0) {
|
||||||
|
components.push(hours.toString().padStart(2, "0"));
|
||||||
|
}
|
||||||
|
|
||||||
|
components.push(minutes.toString().padStart(2, "0"));
|
||||||
|
components.push(seconds.toString().padStart(2, "0"));
|
||||||
|
|
||||||
|
const formattedTime = components.join(":");
|
||||||
|
|
||||||
|
return formattedTime;
|
||||||
|
};
|
Reference in New Issue
Block a user