Skip to content

Commit

Permalink
Merge pull request #90 from mykhailodanilenko/bug-fixes
Browse files Browse the repository at this point in the history
Fixes for issues #86, #87, #88
  • Loading branch information
mykhailodanilenko authored Jul 2, 2024
2 parents 8724fd7 + f21b4ec commit 26bc5df
Show file tree
Hide file tree
Showing 8 changed files with 147 additions and 56 deletions.
10 changes: 1 addition & 9 deletions OwnTube.tv/app/(home)/video.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,13 @@
import { VideoScreen } from "../../screens";
import Head from "expo-router/head";
import { useGetVideoQuery } from "../../api";
import { useLocalSearchParams, useNavigation } from "expo-router";
import { useEffect } from "react";
import { useLocalSearchParams } from "expo-router";
import { RootStackParams } from "../_layout";

export default function video() {
const navigation = useNavigation();
const { id } = useLocalSearchParams<RootStackParams["video"]>();
const { data: title } = useGetVideoQuery(id, (data) => data.name);

useEffect(() => {
if (title) {
navigation.setOptions({ title });
}
}, [title]);

return (
<>
<Head>
Expand Down
7 changes: 2 additions & 5 deletions OwnTube.tv/app/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
import { useFonts } from "expo-font";
import Toast from "react-native-toast-message";
import { BuildInfoToast, ClickableHeaderText, DeviceCapabilitiesModal } from "../components";
import { BuildInfoToast, ClickableHeaderText } from "../components";

const RootStack = () => {
const { backend } = useLocalSearchParams();
Expand Down Expand Up @@ -43,10 +43,7 @@ const RootStack = () => {
}}
name={`(home)/${ROUTES.SETTINGS}`}
/>
<Stack.Screen
options={{ title: "Video", headerRight: () => <DeviceCapabilitiesModal /> }}
name={`(home)/video`}
/>
<Stack.Screen options={{ title: "Video", headerShown: false }} name={`(home)/video`} />
</Stack>
<Toast config={{ buildInfo: () => <BuildInfoToast /> }} />
</ThemeProvider>
Expand Down
77 changes: 65 additions & 12 deletions OwnTube.tv/components/VideoControlsOverlay/VideoControlsOverlay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,16 @@ import { ScrubBar } from "./components/ScrubBar";
import { Typography } from "../Typography";
import { getHumanReadableDuration } from "../../utils";
import { typography } from "../../theme";
import { useNavigation } from "expo-router";
import { DeviceCapabilitiesModal } from "../DeviceCapabilitiesModal";

interface VideoControlsOverlayProps {
isVisible: boolean;
onOverlayPress: () => void;
isPlaying?: boolean;
handlePlayPause: () => void;
handleRW: () => void;
handleFF: () => void;
handleRW: (s: number) => void;
handleFF: (s: number) => void;
duration?: number;
availableDuration?: number;
position?: number;
Expand All @@ -22,6 +24,7 @@ interface VideoControlsOverlayProps {
shouldReplay?: boolean;
handleReplay: () => void;
handleJumpTo: (position: number) => void;
title?: string;
}

export const VideoControlsOverlay = ({
Expand All @@ -40,8 +43,10 @@ export const VideoControlsOverlay = ({
shouldReplay,
handleReplay,
handleJumpTo,
title,
}: PropsWithChildren<VideoControlsOverlayProps>) => {
const { colors } = useTheme();
const navigation = useNavigation();

const uiScale = useMemo(() => {
const { width, height } = Dimensions.get("window");
Expand All @@ -66,20 +71,47 @@ export const VideoControlsOverlay = ({
{isVisible ? (
<View style={styles.contentContainer}>
<View style={styles.topControlsContainer}>
<Pressable onPress={toggleMute}>
<Ionicons name={`volume-${isMute ? "mute" : "high"}`} size={48 * uiScale} color={colors.primary} />
</Pressable>
<View style={styles.topLeftControls}>
<Pressable onPress={navigation.goBack} style={styles.goBackContainer}>
<Ionicons name={"arrow-back"} size={48 * uiScale} color={colors.primary} />
</Pressable>
<Typography
numberOfLines={1}
ellipsizeMode="tail"
color={colors.primary}
fontSize={48 * uiScale}
style={styles.title}
>
{title}
</Typography>
</View>
<View style={styles.topRightControls}>
<Pressable onPress={toggleMute}>
<Ionicons name={`volume-${isMute ? "mute" : "high"}`} size={48 * uiScale} color={colors.primary} />
</Pressable>
<DeviceCapabilitiesModal />
</View>
</View>
<View style={styles.playbackControlsContainer}>
<View style={{ flexDirection: "row", gap: 48 * uiScale }}>
<Pressable onPress={handleRW}>
<Ionicons name={"play-back"} size={96 * uiScale} color={colors.primary} />
<View style={{ flexDirection: "row", gap: 32 * uiScale }}>
<Pressable onPress={() => handleRW(15)}>
<View style={styles.skipTextContainer}>
<Typography color={colors.primary} fontSize={32 * uiScale} style={styles.skipText}>
{15}
</Typography>
</View>
<Ionicons name={"reload"} size={96 * uiScale} color={colors.primary} style={styles.skipLeft} />
</Pressable>
<Pressable onPress={shouldReplay ? handleReplay : handlePlayPause}>
<Ionicons name={centralIconName} size={96 * uiScale} color={colors.primary} />
</Pressable>
<Pressable onPress={handleFF}>
<Ionicons name={"play-forward"} size={96 * uiScale} color={colors.primary} />
<Pressable onPress={() => handleFF(30)}>
<View style={styles.skipTextContainer}>
<Typography color={colors.primary} fontSize={32 * uiScale} style={styles.skipText}>
{30}
</Typography>
</View>
<Ionicons name={"reload"} size={96 * uiScale} color={colors.primary} style={styles.skipRight} />
</Pressable>
</View>
</View>
Expand Down Expand Up @@ -116,6 +148,7 @@ const styles = StyleSheet.create({
width: "100%",
},
contentContainer: { flex: 1, height: "100%", left: 0, position: "absolute", top: 0, width: "100%", zIndex: 1 },
goBackContainer: { justifyContent: "center" },
overlay: {
alignItems: "center",
alignSelf: "center",
Expand All @@ -131,6 +164,23 @@ const styles = StyleSheet.create({
justifyContent: "center",
width: "100%",
},
skipLeft: {
transform: [{ rotateZ: "45deg" }, { scaleX: -1 }],
},
skipRight: {
transform: [{ rotateZ: "-45deg" }],
},
skipText: { fontWeight: "bold", marginTop: 4, userSelect: "none" },
skipTextContainer: {
alignItems: "center",
bottom: 0,
justifyContent: "center",
left: 0,
position: "absolute",
right: 0,
top: 0,
zIndex: 1,
},
timeBlockLeft: {
left: "3%",
position: "absolute",
Expand All @@ -141,16 +191,19 @@ const styles = StyleSheet.create({
right: "3%",
userSelect: "none",
},
title: { fontWeight: "bold" },
topControlsContainer: {
alignItems: "center",
flexDirection: "row",
height: "20%",
justifyContent: "flex-end",
justifyContent: "space-between",
left: 0,
paddingRight: "5%",
paddingHorizontal: "5%",
position: "absolute",
top: 0,
width: "100%",
zIndex: 1,
},
topLeftControls: { flexDirection: "row", gap: 24, width: "80%" },
topRightControls: { alignItems: "center", flexDirection: "row", gap: 24, width: "10%" },
});
57 changes: 40 additions & 17 deletions OwnTube.tv/components/VideoControlsOverlay/components/ScrubBar.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
import { StyleSheet, View } from "react-native";
import { useTheme } from "@react-navigation/native";
import {
Gesture,
GestureDetector,
GestureHandlerRootView,
PanGestureHandlerEventPayload,
} from "react-native-gesture-handler";
import { useEffect, useState } from "react";
import { Gesture, GestureDetector, GestureHandlerRootView } from "react-native-gesture-handler";
import { useEffect, useMemo, useState } from "react";
import { Typography } from "../../Typography";
import { getHumanReadableDuration } from "../../../utils";

interface ScrubBarProps {
percentageAvailable: number;
Expand All @@ -23,35 +20,42 @@ export const ScrubBar = ({ percentageAvailable, percentagePosition, onDrag, dura
const [isDragging, setIsDragging] = useState(false);
const [scrubberPosition, setScrubberPosition] = useState(0);

const handlePan = (event: PanGestureHandlerEventPayload) => {
if (visibleWidth < event.x || event.x < 0) {
const handleTapOrPan = (x: number) => {
if (visibleWidth < x || x < 0) {
return;
}

setScrubberPosition(event.x - INDICATOR_SIZE / 2);
setScrubberPosition(x - INDICATOR_SIZE / 2);
};

const setPosition = (event: PanGestureHandlerEventPayload) => {
const newX = visibleWidth <= event.x ? visibleWidth : event.x < 0 ? 0 : event.x;
const setPosition = (x: number) => {
const newX = visibleWidth <= x ? visibleWidth : x < 0 ? 0 : x;

const newPositionRelation = (newX - INDICATOR_SIZE / 2) / visibleWidth;

onDrag(Math.floor(newPositionRelation * duration));
};

const pan = Gesture.Pan()
.onUpdate(handlePan)
.onUpdate(({ x }) => {
handleTapOrPan(x);
})
.onStart(() => setIsDragging(true))
.onEnd((event) => {
setPosition(event);
.onEnd(({ x }) => {
setPosition(x);
setIsDragging(false);
});
})
.minDistance(0);

useEffect(() => {
if (!isDragging) {
setScrubberPosition((visibleWidth / 100) * percentagePosition);
}
}, [percentagePosition, isDragging, visibleWidth]);
}, [percentagePosition, visibleWidth]);

const seekHint = useMemo(() => {
return getHumanReadableDuration(duration * (scrubberPosition / visibleWidth));
}, [scrubberPosition, visibleWidth, duration]);

return (
<GestureHandlerRootView style={styles.gestureHandlerContainer}>
Expand All @@ -67,6 +71,19 @@ export const ScrubBar = ({ percentageAvailable, percentagePosition, onDrag, dura
]}
onLayout={(event) => setVisibleWidth(event.nativeEvent.layout.width)}
>
{isDragging && (
<View
style={[
styles.seekTime,
{
backgroundColor: colors.card,
left: scrubberPosition,
},
]}
>
<Typography>{seekHint}</Typography>
</View>
)}
<View
style={[
styles.indicator,
Expand Down Expand Up @@ -145,4 +162,10 @@ const styles = StyleSheet.create({
justifyContent: "center",
width: "100%",
},
seekTime: {
borderRadius: 8,
padding: 8,
position: "absolute",
top: -50,
},
});
12 changes: 7 additions & 5 deletions OwnTube.tv/components/VideoView/VideoView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@ export interface VideoViewProps {
testID: string;
handleSetTimeStamp: (timestamp: number) => void;
timestamp?: string;
title?: string;
}

const VideoView = ({ uri, testID, handleSetTimeStamp, timestamp }: VideoViewProps) => {
const VideoView = ({ uri, testID, handleSetTimeStamp, timestamp, title }: VideoViewProps) => {
const videoRef = useRef<Video>(null);
const [isControlsVisible, setIsControlsVisible] = useState(false);
const [playbackStatus, setPlaybackStatus] = useState<AVPlaybackStatusSuccess | null>(null);
Expand All @@ -38,12 +39,12 @@ const VideoView = ({ uri, testID, handleSetTimeStamp, timestamp }: VideoViewProp
}
};

const handleRW = () => {
videoRef.current?.setPositionAsync((playbackStatus?.positionMillis ?? 0) - 10_000);
const handleRW = (seconds: number) => {
videoRef.current?.setPositionAsync((playbackStatus?.positionMillis ?? 0) - seconds * 1000);
};

const handleFF = () => {
videoRef.current?.setPositionAsync((playbackStatus?.positionMillis ?? 0) + 10_000);
const handleFF = (seconds: number) => {
videoRef.current?.setPositionAsync((playbackStatus?.positionMillis ?? 0) + seconds * 1000);
};

const toggleMute = () => {
Expand Down Expand Up @@ -90,6 +91,7 @@ const VideoView = ({ uri, testID, handleSetTimeStamp, timestamp }: VideoViewProp
shouldReplay={playbackStatus?.didJustFinish}
handleReplay={handleReplay}
handleJumpTo={handleJumpTo}
title={title}
>
<Video
testID={`${testID}-video-playback`}
Expand Down
32 changes: 27 additions & 5 deletions OwnTube.tv/components/VideoView/VideoView.web.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ declare const window: {
videojs: typeof videojs;
} & Window;

const VideoView = ({ uri, testID, handleSetTimeStamp, timestamp }: VideoViewProps) => {
const VideoView = ({ uri, testID, handleSetTimeStamp, timestamp, title }: VideoViewProps) => {
const { videojs } = window;
const videoRef = useRef<HTMLDivElement>(null);
const playerRef = useRef<Player | null>(null);
Expand Down Expand Up @@ -41,12 +41,12 @@ const VideoView = ({ uri, testID, handleSetTimeStamp, timestamp }: VideoViewProp
}
};

const handleRW = () => {
playerRef.current?.currentTime(playbackStatus.position / 1000 - 10);
const handleRW = (seconds: number) => {
playerRef.current?.currentTime(playbackStatus.position / 1000 - seconds);
};

const handleFF = () => {
playerRef.current?.currentTime(playbackStatus.position / 1000 + 10);
const handleFF = (seconds: number) => {
playerRef.current?.currentTime(playbackStatus.position / 1000 + seconds);
};

const toggleMute = () => {
Expand Down Expand Up @@ -153,6 +153,27 @@ const VideoView = ({ uri, testID, handleSetTimeStamp, timestamp }: VideoViewProp
handleSetTimeStamp(positionFormatted);
}, [playbackStatus.position]);

useEffect(() => {
const handleKeyboard = (e: KeyboardEvent) => {
if (e.code === "Space") {
handlePlayPause();
}
if (e.code === "ArrowRight") {
handleFF(30);
}
if (e.code === "ArrowLeft") {
handleRW(15);
}
if (e.code === "KeyM") {
toggleMute();
}
};

window.addEventListener("keydown", handleKeyboard);

return () => window.removeEventListener("keydown", handleKeyboard);
}, [handleFF, handleRW, handlePlayPause, toggleMute]);

return (
<View style={styles.container}>
<VideoControlsOverlay
Expand All @@ -170,6 +191,7 @@ const VideoView = ({ uri, testID, handleSetTimeStamp, timestamp }: VideoViewProp
shouldReplay={playbackStatus?.didJustFinish}
handleReplay={handleReplay}
handleJumpTo={handleJumpTo}
title={title}
>
<div style={{ position: "relative" }} ref={videoRef} data-testid={`${testID}-video-playback`} />
</VideoControlsOverlay>
Expand Down
Loading

0 comments on commit 26bc5df

Please sign in to comment.