diff --git a/OwnTube.tv/app/(home)/video.tsx b/OwnTube.tv/app/(home)/video.tsx index 35cf17f..53ee856 100644 --- a/OwnTube.tv/app/(home)/video.tsx +++ b/OwnTube.tv/app/(home)/video.tsx @@ -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(); const { data: title } = useGetVideoQuery(id, (data) => data.name); - useEffect(() => { - if (title) { - navigation.setOptions({ title }); - } - }, [title]); - return ( <> diff --git a/OwnTube.tv/app/_layout.tsx b/OwnTube.tv/app/_layout.tsx index 67dad7d..d71900a 100644 --- a/OwnTube.tv/app/_layout.tsx +++ b/OwnTube.tv/app/_layout.tsx @@ -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(); @@ -43,10 +43,7 @@ const RootStack = () => { }} name={`(home)/${ROUTES.SETTINGS}`} /> - }} - name={`(home)/video`} - /> + }} /> diff --git a/OwnTube.tv/components/VideoControlsOverlay/VideoControlsOverlay.tsx b/OwnTube.tv/components/VideoControlsOverlay/VideoControlsOverlay.tsx index 5c52dc2..7ce4a2a 100644 --- a/OwnTube.tv/components/VideoControlsOverlay/VideoControlsOverlay.tsx +++ b/OwnTube.tv/components/VideoControlsOverlay/VideoControlsOverlay.tsx @@ -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; @@ -22,6 +24,7 @@ interface VideoControlsOverlayProps { shouldReplay?: boolean; handleReplay: () => void; handleJumpTo: (position: number) => void; + title?: string; } export const VideoControlsOverlay = ({ @@ -40,8 +43,10 @@ export const VideoControlsOverlay = ({ shouldReplay, handleReplay, handleJumpTo, + title, }: PropsWithChildren) => { const { colors } = useTheme(); + const navigation = useNavigation(); const uiScale = useMemo(() => { const { width, height } = Dimensions.get("window"); @@ -66,20 +71,47 @@ export const VideoControlsOverlay = ({ {isVisible ? ( - - - + + + + + + {title} + + + + + + + + - - - + + handleRW(15)}> + + + {15} + + + - - + handleFF(30)}> + + + {30} + + + @@ -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", @@ -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", @@ -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%" }, }); diff --git a/OwnTube.tv/components/VideoControlsOverlay/components/ScrubBar.tsx b/OwnTube.tv/components/VideoControlsOverlay/components/ScrubBar.tsx index 947945e..5b64b37 100644 --- a/OwnTube.tv/components/VideoControlsOverlay/components/ScrubBar.tsx +++ b/OwnTube.tv/components/VideoControlsOverlay/components/ScrubBar.tsx @@ -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; @@ -23,16 +20,16 @@ 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; @@ -40,18 +37,25 @@ export const ScrubBar = ({ percentageAvailable, percentagePosition, onDrag, dura }; 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 ( @@ -67,6 +71,19 @@ export const ScrubBar = ({ percentageAvailable, percentagePosition, onDrag, dura ]} onLayout={(event) => setVisibleWidth(event.nativeEvent.layout.width)} > + {isDragging && ( + + {seekHint} + + )} void; timestamp?: string; + title?: string; } -const VideoView = ({ uri, testID, handleSetTimeStamp, timestamp }: VideoViewProps) => { +const VideoView = ({ uri, testID, handleSetTimeStamp, timestamp, title }: VideoViewProps) => { const videoRef = useRef