diff --git a/components/FullScreenVideoPlayer.tsx b/components/FullScreenVideoPlayer.tsx
index 10e79eb4..d9faf3f4 100644
--- a/components/FullScreenVideoPlayer.tsx
+++ b/components/FullScreenVideoPlayer.tsx
@@ -1,54 +1,49 @@
-import { useAdjacentEpisodes } from "@/hooks/useAdjacentEpisodes";
-import { useControlsVisibility } from "@/hooks/useControlsVisibility";
-import { useTrickplay } from "@/hooks/useTrickplay";
-import { apiAtom } from "@/providers/JellyfinProvider";
+import React, { useCallback, useEffect, useMemo, useState } from "react";
+import {
+ View,
+ TouchableOpacity,
+ Alert,
+ Dimensions,
+ BackHandler,
+ Pressable,
+ Touchable,
+} from "react-native";
+import Video, { OnProgressData } from "react-native-video";
+import { Slider } from "react-native-awesome-slider";
+import { Ionicons } from "@expo/vector-icons";
import { usePlayback } from "@/providers/PlaybackProvider";
import { useSettings } from "@/utils/atoms/settings";
-import { getBackdropUrl } from "@/utils/jellyfin/image/getBackdropUrl";
-import { getAuthHeaders } from "@/utils/jellyfin/jellyfin";
-import { writeToLog } from "@/utils/log";
-import orientationToOrientationLock from "@/utils/OrientationLockConverter";
-import { secondsToTicks } from "@/utils/secondsToTicks";
-import { runtimeTicksToSeconds } from "@/utils/time";
-import { Ionicons } from "@expo/vector-icons";
-import { useQuery } from "@tanstack/react-query";
-import { Image } from "expo-image";
-import { useRouter, useSegments } from "expo-router";
-import * as ScreenOrientation from "expo-screen-orientation";
-import { setStatusBarHidden, StatusBar } from "expo-status-bar";
-import { useAtom } from "jotai";
-import { useCallback, useEffect, useMemo, useRef, useState } from "react";
-import {
- Alert,
- AppState,
- AppStateStatus,
- BackHandler,
- Dimensions,
- TouchableOpacity,
- View,
-} from "react-native";
-import { Slider } from "react-native-awesome-slider";
-import "react-native-gesture-handler";
-import { Gesture, GestureDetector } from "react-native-gesture-handler";
-import Animated, {
- runOnJS,
- useAnimatedStyle,
- useSharedValue,
- withTiming,
-} from "react-native-reanimated";
-import { useSafeAreaInsets } from "react-native-safe-area-context";
-import Video, { OnProgressData } from "react-native-video";
+import { useAdjacentEpisodes } from "@/hooks/useAdjacentEpisodes";
+import { useTrickplay } from "@/hooks/useTrickplay";
import { Text } from "./common/Text";
-import { itemRouter } from "./common/TouchableItemRouter";
import { Loader } from "./Loader";
-
-async function lockOrientation(orientation: ScreenOrientation.OrientationLock) {
- await ScreenOrientation.lockAsync(orientation);
-}
-
-async function resetOrientation() {
- await ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.DEFAULT);
-}
+import { writeToLog } from "@/utils/log";
+import { useRouter, useSegments } from "expo-router";
+import { itemRouter } from "./common/TouchableItemRouter";
+import { Image } from "expo-image";
+import { StatusBar } from "expo-status-bar";
+import * as ScreenOrientation from "expo-screen-orientation";
+import { useAtom } from "jotai";
+import { apiAtom } from "@/providers/JellyfinProvider";
+import { useQuery } from "@tanstack/react-query";
+import {
+ runOnJS,
+ useAnimatedReaction,
+ useSharedValue,
+} from "react-native-reanimated";
+import { secondsToTicks } from "@/utils/secondsToTicks";
+import { getAuthHeaders } from "@/utils/jellyfin/jellyfin";
+import { getBackdropUrl } from "@/utils/jellyfin/image/getBackdropUrl";
+import {
+ useSafeAreaFrame,
+ useSafeAreaInsets,
+} from "react-native-safe-area-context";
+import orientationToOrientationLock from "@/utils/OrientationLockConverter";
+import {
+ formatTimeString,
+ runtimeTicksToSeconds,
+ ticksToSeconds,
+} from "@/utils/time";
export const FullScreenVideoPlayer: React.FC = () => {
const {
@@ -61,51 +56,67 @@ export const FullScreenVideoPlayer: React.FC = () => {
isPlaying,
videoRef,
onProgress,
- isBuffering: _isBuffering,
setIsBuffering,
} = usePlayback();
const [settings] = useSettings();
const [api] = useAtom(apiAtom);
- const insets = useSafeAreaInsets();
- const segments = useSegments();
const router = useRouter();
+ const segments = useSegments();
+ const insets = useSafeAreaInsets();
+ const { previousItem, nextItem } = useAdjacentEpisodes({ currentlyPlaying });
const { trickPlayUrl, calculateTrickplayUrl, trickplayInfo } =
useTrickplay(currentlyPlaying);
- const { previousItem, nextItem } = useAdjacentEpisodes({ currentlyPlaying });
- const [orientation, setOrientation] = useState(
- ScreenOrientation.OrientationLock.UNKNOWN
- );
-
- const opacity = useSharedValue(1);
+ const [showControls, setShowControls] = useState(true);
+ const [isBuffering, setIsBufferingState] = useState(true);
const [ignoreSafeArea, setIgnoreSafeArea] = useState(false);
- const from = useMemo(() => segments[2], [segments]);
+ const [isStatusBarHidden, setIsStatusBarHidden] = useState(false);
+ // Seconds
+ const [currentTime, setCurrentTime] = useState(0);
+ const [remainingTime, setRemainingTime] = useState(0);
+
+ const isSeeking = useSharedValue(false);
+
+ const cacheProgress = useSharedValue(0);
const progress = useSharedValue(0);
const min = useSharedValue(0);
const max = useSharedValue(currentlyPlaying?.item.RunTimeTicks || 0);
- const sliding = useRef(false);
- const localIsBuffering = useSharedValue(true);
- const cacheProgress = useSharedValue(0);
- const [isStatusBarHidden, setIsStatusBarHidden] = useState(false);
- const hideControls = useCallback(() => {
- "worklet";
- opacity.value = 0;
- }, [opacity]);
+ const { width: screenWidth, height: screenHeight } = Dimensions.get("window");
- const showControls = useCallback(() => {
- "worklet";
- opacity.value = 1;
- }, [opacity]);
+ const from = useMemo(() => segments[2], [segments]);
+
+ const updateTimes = useCallback(
+ (currentProgress: number, maxValue: number) => {
+ const current = ticksToSeconds(currentProgress);
+ const remaining = ticksToSeconds(maxValue - current);
+
+ setCurrentTime(current);
+ setRemainingTime(remaining);
+ },
+ []
+ );
+
+ useAnimatedReaction(
+ () => ({
+ progress: progress.value,
+ max: max.value,
+ isSeeking: isSeeking.value,
+ }),
+ (result) => {
+ if (result.isSeeking === false) {
+ runOnJS(updateTimes)(result.progress, result.max);
+ }
+ },
+ [updateTimes]
+ );
useEffect(() => {
const backAction = () => {
if (currentlyPlaying) {
- // Your custom back action here
- console.log("onback");
Alert.alert("Hold on!", "Are you sure you want to exit?", [
{
text: "Cancel",
@@ -114,7 +125,6 @@ export const FullScreenVideoPlayer: React.FC = () => {
},
{ text: "Yes", onPress: () => stopPlayback() },
]);
-
return true;
}
return false;
@@ -126,9 +136,35 @@ export const FullScreenVideoPlayer: React.FC = () => {
);
return () => backHandler.remove();
- }, [currentlyPlaying]);
+ }, [currentlyPlaying, stopPlayback]);
- const { width: screenWidth, height: screenHeight } = Dimensions.get("window");
+ const [orientation, setOrientation] = useState(
+ ScreenOrientation.OrientationLock.UNKNOWN
+ );
+
+ /**
+ * Event listener for orientation
+ */
+ useEffect(() => {
+ const subscription = ScreenOrientation.addOrientationChangeListener(
+ (event) => {
+ setOrientation(
+ orientationToOrientationLock(event.orientationInfo.orientation)
+ );
+ }
+ );
+
+ return () => {
+ subscription.remove();
+ };
+ }, []);
+
+ const isLandscape = useMemo(() => {
+ return orientation === ScreenOrientation.OrientationLock.LANDSCAPE_LEFT ||
+ orientation === ScreenOrientation.OrientationLock.LANDSCAPE_RIGHT
+ ? true
+ : false;
+ }, [orientation]);
const poster = useMemo(() => {
if (!currentlyPlaying?.item || !api) return "";
@@ -157,82 +193,118 @@ export const FullScreenVideoPlayer: React.FC = () => {
title: currentlyPlaying.item?.Name || "Unknown",
description: currentlyPlaying.item?.Overview ?? undefined,
imageUri: poster,
- subtitle: currentlyPlaying.item?.Album ?? undefined, // Change here
+ subtitle: currentlyPlaying.item?.Album ?? undefined,
},
};
}, [currentlyPlaying, api, poster]);
- useEffect(() => {
- max.value = currentlyPlaying?.item.RunTimeTicks || 0;
- }, [currentlyPlaying?.item.RunTimeTicks]);
-
useEffect(() => {
if (!currentlyPlaying) {
- resetOrientation();
+ ScreenOrientation.unlockAsync();
progress.value = 0;
- min.value = 0;
max.value = 0;
- cacheProgress.value = 0;
- sliding.current = false;
- hideControls();
- setStatusBarHidden(false);
- // NavigationBar.setVisibilityAsync("visible")
+ setShowControls(true);
+ setIsStatusBarHidden(false);
+ isSeeking.value = false;
} else {
- setStatusBarHidden(true);
- // NavigationBar.setVisibilityAsync("hidden")
- lockOrientation(
+ setIsStatusBarHidden(true);
+ ScreenOrientation.lockAsync(
settings?.defaultVideoOrientation ||
ScreenOrientation.OrientationLock.DEFAULT
);
progress.value =
currentlyPlaying.item?.UserData?.PlaybackPositionTicks || 0;
max.value = currentlyPlaying.item.RunTimeTicks || 0;
- showControls();
+ setShowControls(true);
}
}, [currentlyPlaying, settings]);
- /**
- * Event listener for orientation
- */
- useEffect(() => {
- const subscription = ScreenOrientation.addOrientationChangeListener(
- (event) => {
- setOrientation(
- orientationToOrientationLock(event.orientationInfo.orientation)
- );
- }
- );
+ const toggleControls = () => setShowControls(!showControls);
- return () => {
- subscription.remove();
- };
+ const handleVideoProgress = useCallback(
+ (data: OnProgressData) => {
+ if (isSeeking.value === true) return;
+ progress.value = secondsToTicks(data.currentTime);
+ cacheProgress.value = secondsToTicks(data.playableDuration);
+ setIsBufferingState(data.playableDuration === 0);
+ setIsBuffering(data.playableDuration === 0);
+ onProgress(data);
+ },
+ [onProgress, setIsBuffering, isSeeking]
+ );
+
+ const handleVideoError = useCallback(
+ (e: any) => {
+ console.log(e);
+ writeToLog("ERROR", "Video playback error: " + JSON.stringify(e));
+ Alert.alert("Error", "Cannot play this video file.");
+ setIsPlaying(false);
+ },
+ [setIsPlaying]
+ );
+
+ const handlePlayPause = () => {
+ if (isPlaying) pauseVideo();
+ else playVideo();
+ };
+
+ const handleSliderComplete = (value: number) => {
+ progress.value = value;
+ isSeeking.value = false;
+ videoRef.current?.seek(value / 10000000);
+ };
+
+ const handleSliderChange = (value: number) => {
+ calculateTrickplayUrl(value);
+ };
+
+ const handleSliderStart = useCallback(() => {
+ if (showControls === false) return;
+ isSeeking.value = true;
}, []);
- const isLandscape = useMemo(() => {
- return orientation === ScreenOrientation.OrientationLock.LANDSCAPE_LEFT ||
- orientation === ScreenOrientation.OrientationLock.LANDSCAPE_RIGHT
- ? true
- : false;
- }, [orientation]);
+ const handleSkipBackward = useCallback(async () => {
+ if (!settings) return;
+ try {
+ const curr = await videoRef.current?.getCurrentPosition();
+ if (curr !== undefined) {
+ videoRef.current?.seek(Math.max(0, curr - settings.rewindSkipTime));
+ }
+ } catch (error) {
+ writeToLog("ERROR", "Error seeking video backwards", error);
+ }
+ }, [settings]);
- const animatedStyles = {
- controls: useAnimatedStyle(() => ({
- opacity: withTiming(opacity.value, { duration: 300 }),
- })),
- videoContainer: useAnimatedStyle(() => ({
- opacity: withTiming(
- opacity.value === 1 || localIsBuffering.value ? 0.5 : 1,
- {
- duration: 300,
- }
- ),
- })),
- loader: useAnimatedStyle(() => ({
- opacity: withTiming(
- localIsBuffering.value === true || progress.value === 0 ? 1 : 0,
- { duration: 300 }
- ),
- })),
+ const handleSkipForward = useCallback(async () => {
+ if (!settings) return;
+ try {
+ const curr = await videoRef.current?.getCurrentPosition();
+ if (curr !== undefined) {
+ videoRef.current?.seek(Math.max(0, curr + settings.forwardSkipTime));
+ }
+ } catch (error) {
+ writeToLog("ERROR", "Error seeking video forwards", error);
+ }
+ }, [settings]);
+
+ const handleGoToPreviousItem = () => {
+ if (!previousItem || !from) return;
+ const url = itemRouter(previousItem, from);
+ stopPlayback();
+ // @ts-ignore
+ router.push(url);
+ };
+
+ const handleGoToNextItem = () => {
+ if (!nextItem || !from) return;
+ const url = itemRouter(nextItem, from);
+ stopPlayback();
+ // @ts-ignore
+ router.push(url);
+ };
+
+ const toggleIgnoreSafeArea = () => {
+ setIgnoreSafeArea(!ignoreSafeArea);
};
const { data: introTimestamps } = useQuery({
@@ -266,201 +338,16 @@ export const FullScreenVideoPlayer: React.FC = () => {
enabled: !!currentlyPlaying?.item.Id,
});
- const animatedIntroSkipperStyle = useAnimatedStyle(() => {
- const showButtonAt = secondsToTicks(introTimestamps?.ShowSkipPromptAt || 0);
- const hideButtonAt = secondsToTicks(introTimestamps?.HideSkipPromptAt || 0);
- const showButton =
- progress.value > showButtonAt && progress.value < hideButtonAt;
- return {
- opacity: withTiming(
- localIsBuffering.value === false && showButton && progress.value !== 0
- ? 1
- : 0,
- {
- duration: 300,
- }
- ),
- bottom: withTiming(
- opacity.value === 0 ? insets.bottom + 8 : isLandscape ? 85 : 140,
- {
- duration: 300,
- }
- ),
- };
- });
-
- const toggleIgnoreSafeArea = useCallback(() => {
- setIgnoreSafeArea((prev) => !prev);
- }, []);
-
- const handleToggleControlsPress = useCallback(() => {
- if (opacity.value === 1) {
- hideControls();
- } else {
- showControls();
- }
- }, [opacity.value, hideControls, showControls]);
-
- const skipIntro = useCallback(async () => {
+ const skipIntro = async () => {
if (!introTimestamps || !videoRef.current) return;
try {
videoRef.current.seek(introTimestamps.IntroEnd);
} catch (error) {
writeToLog("ERROR", "Error skipping intro", error);
}
- }, [introTimestamps]);
+ };
- const handleVideoProgress = useCallback(
- (e: OnProgressData) => {
- if (e.playableDuration === 0) {
- setIsBuffering(true);
- localIsBuffering.value = true;
- } else {
- setIsBuffering(false);
- localIsBuffering.value = false;
- }
-
- if (sliding.current) return;
- onProgress(e);
- progress.value = secondsToTicks(e.currentTime);
- cacheProgress.value = secondsToTicks(e.playableDuration);
- },
- [onProgress, setIsBuffering]
- );
-
- const handleVideoError = useCallback(
- (e: any) => {
- console.log(e);
- writeToLog("ERROR", "Video playback error: " + JSON.stringify(e));
- Alert.alert("Error", "Cannot play this video file.");
- setIsPlaying(false);
- },
- [setIsPlaying]
- );
-
- const handleSkipBackward = useCallback(async () => {
- try {
- const curr = await videoRef.current?.getCurrentPosition();
- if (curr !== undefined) {
- videoRef.current?.seek(Math.max(0, curr - 15));
- showControls();
- }
- } catch (error) {
- writeToLog("ERROR", "Error seeking video backwards", error);
- }
- }, [videoRef, showControls]);
-
- const handleSkipForward = useCallback(async () => {
- try {
- const curr = await videoRef.current?.getCurrentPosition();
- if (curr !== undefined) {
- videoRef.current?.seek(Math.max(0, curr + 15));
- showControls();
- }
- } catch (error) {
- writeToLog("ERROR", "Error seeking video forwards", error);
- }
- }, [videoRef, showControls]);
-
- const handlePlayPause = useCallback(() => {
- console.log("handlePlayPause");
- if (isPlaying) pauseVideo();
- else playVideo();
- showControls();
- }, [isPlaying, pauseVideo, playVideo, showControls]);
-
- const handleSliderStart = useCallback(() => {
- if (opacity.value === 0) return;
- sliding.current = true;
- }, []);
-
- const handleSliderComplete = useCallback(
- (val: number) => {
- if (opacity.value === 0) return;
- const tick = Math.floor(val);
- videoRef.current?.seek(tick / 10000000);
- sliding.current = false;
- },
- [videoRef]
- );
-
- const handleSliderChange = useCallback(
- (val: number) => {
- if (opacity.value === 0) return;
- const tick = Math.floor(val);
- progress.value = tick;
- calculateTrickplayUrl(progress);
- showControls();
- },
- [progress, calculateTrickplayUrl, showControls]
- );
-
- const handleGoToPreviousItem = useCallback(() => {
- if (!previousItem || !from) return;
- const url = itemRouter(previousItem, from);
- stopPlayback();
- // @ts-ignore
- router.push(url);
- }, [previousItem, from, stopPlayback, router]);
-
- const handleGoToNextItem = useCallback(() => {
- if (!nextItem || !from) return;
- const url = itemRouter(nextItem, from);
- stopPlayback();
- // @ts-ignore
- router.push(url);
- }, [nextItem, from, stopPlayback, router]);
-
- const videoTap = Gesture.Tap().onBegin(() => {
- runOnJS(handleToggleControlsPress)();
- });
-
- const toggleIgnoreSafeAreaGesture = Gesture.Tap()
- .enabled(opacity.value !== 0)
- .onStart(() => {
- runOnJS(toggleIgnoreSafeArea)();
- });
-
- const playPauseGesture = Gesture.Tap()
- .onBegin(() => {
- console.log("playPauseGesture ~", opacity.value);
- })
- .onStart(() => {
- runOnJS(handlePlayPause)();
- })
- .onFinalize(() => {
- if (opacity.value === 0) opacity.value = 1;
- });
-
- const goToPreviouItemGesture = Gesture.Tap()
- .enabled(opacity.value !== 0)
- .onStart(() => {
- runOnJS(handleGoToPreviousItem)();
- });
-
- const goToNextItemGesture = Gesture.Tap()
- .enabled(opacity.value !== 0)
- .onStart(() => {
- runOnJS(handleGoToNextItem)();
- });
-
- const skipBackwardGesture = Gesture.Tap()
- .enabled(opacity.value !== 0)
- .onStart(() => {
- runOnJS(handleSkipBackward)();
- });
-
- const skipForwardGesture = Gesture.Tap()
- .enabled(opacity.value !== 0)
- .onStart(() => {
- runOnJS(handleSkipForward)();
- });
-
- const skipIntroGesture = Gesture.Tap().onStart(() => {
- runOnJS(skipIntro)();
- });
-
- if (!api || !currentlyPlaying) return null;
+ if (!currentlyPlaying) return null;
return (
{
}}
>
-
-
-
- {videoSource && (
-
-
-
-
- {
width: ignoreSafeArea
? screenWidth
: screenWidth - (insets.left + insets.right),
- justifyContent: "center",
- alignItems: "center",
},
- animatedStyles.loader,
]}
>
-
-
+ {videoSource && (
+
);
};
diff --git a/components/settings/MediaToggles.tsx b/components/settings/MediaToggles.tsx
index 5c724b5c..1fb0f720 100644
--- a/components/settings/MediaToggles.tsx
+++ b/components/settings/MediaToggles.tsx
@@ -9,6 +9,8 @@ interface Props extends ViewProps {}
export const MediaToggles: React.FC = ({ ...props }) => {
const [settings, updateSettings] = useSettings();
+ if (!settings) return null;
+
return (
Media
@@ -119,6 +121,82 @@ export const MediaToggles: React.FC = ({ ...props }) => {
+
+
+
+ Forward skip length
+
+ Choose length in seconds when skipping in video playback.
+
+
+
+
+ updateSettings({
+ forwardSkipTime: Math.max(0, settings.forwardSkipTime - 5),
+ })
+ }
+ className="w-8 h-8 bg-neutral-800 rounded-l-lg flex items-center justify-center"
+ >
+ -
+
+
+ {settings.forwardSkipTime}s
+
+
+ updateSettings({
+ forwardSkipTime: Math.min(60, settings.forwardSkipTime + 5),
+ })
+ }
+ >
+ +
+
+
+
+
+
+
+ Rewind length
+
+ Choose length in seconds when skipping in video playback.
+
+
+
+
+ updateSettings({
+ rewindSkipTime: Math.max(0, settings.rewindSkipTime - 5),
+ })
+ }
+ className="w-8 h-8 bg-neutral-800 rounded-l-lg flex items-center justify-center"
+ >
+ -
+
+
+ {settings.rewindSkipTime}s
+
+
+ updateSettings({
+ rewindSkipTime: Math.min(60, settings.rewindSkipTime + 5),
+ })
+ }
+ >
+ +
+
+
+
);
diff --git a/hooks/useTrickplay.ts b/hooks/useTrickplay.ts
index 502faefa..bf51709b 100644
--- a/hooks/useTrickplay.ts
+++ b/hooks/useTrickplay.ts
@@ -34,7 +34,7 @@ export const useTrickplay = (
const [api] = useAtom(apiAtom);
const [trickPlayUrl, setTrickPlayUrl] = useState(null);
const lastCalculationTime = useRef(0);
- const throttleDelay = 100; // 200ms throttle
+ const throttleDelay = 200; // 200ms throttle
const trickplayInfo = useMemo(() => {
if (!currentlyPlaying?.item.Id || !currentlyPlaying?.item.Trickplay) {
@@ -62,7 +62,7 @@ export const useTrickplay = (
}, [currentlyPlaying]);
const calculateTrickplayUrl = useCallback(
- (progress: SharedValue) => {
+ (progress: number) => {
const now = Date.now();
if (now - lastCalculationTime.current < throttleDelay) {
return null;
@@ -80,7 +80,7 @@ export const useTrickplay = (
throw new Error("Invalid trickplay data");
}
- const currentSecond = Math.max(0, Math.floor(progress.value / 10000000));
+ const currentSecond = Math.max(0, Math.floor(progress / 10000000));
const cols = TileWidth;
const rows = TileHeight;
diff --git a/utils/atoms/settings.ts b/utils/atoms/settings.ts
index 2a09ca93..571ec2ba 100644
--- a/utils/atoms/settings.ts
+++ b/utils/atoms/settings.ts
@@ -70,6 +70,8 @@ type Settings = {
defaultAudioLanguage: DefaultLanguageOption | null;
showHomeTitles: boolean;
defaultVideoOrientation: ScreenOrientation.OrientationLock;
+ forwardSkipTime: number;
+ rewindSkipTime: number;
};
/**
@@ -103,6 +105,8 @@ const loadSettings = async (): Promise => {
defaultSubtitleLanguage: null,
showHomeTitles: true,
defaultVideoOrientation: ScreenOrientation.OrientationLock.DEFAULT,
+ forwardSkipTime: 30,
+ rewindSkipTime: 10,
};
try {
diff --git a/utils/secondsToTicks.ts b/utils/secondsToTicks.ts
index 8d8d38db..df13813e 100644
--- a/utils/secondsToTicks.ts
+++ b/utils/secondsToTicks.ts
@@ -1,6 +1,5 @@
// seconds to ticks util
export function secondsToTicks(seconds: number): number {
- "worklet";
return seconds * 10000000;
}
diff --git a/utils/time.ts b/utils/time.ts
index c12d8da8..80b44cf0 100644
--- a/utils/time.ts
+++ b/utils/time.ts
@@ -6,7 +6,7 @@
* @returns A string formatted as "Xh Ym" where X is hours and Y is minutes.
*/
export const runtimeTicksToMinutes = (
- ticks: number | null | undefined,
+ ticks: number | null | undefined
): string => {
if (!ticks) return "0h 0m";
@@ -20,7 +20,7 @@ export const runtimeTicksToMinutes = (
};
export const runtimeTicksToSeconds = (
- ticks: number | null | undefined,
+ ticks: number | null | undefined
): string => {
if (!ticks) return "0h 0m";
@@ -34,3 +34,37 @@ export const runtimeTicksToSeconds = (
if (hours > 0) return `${hours}h ${minutes}m ${seconds}s`;
else return `${minutes}m ${seconds}s`;
};
+
+export const formatTimeString = (
+ t: number | null | undefined,
+ tick = false
+): string => {
+ if (t === null || t === undefined) return "0:00";
+
+ let seconds = t;
+ if (tick) {
+ seconds = Math.floor(t / 10000000); // Convert ticks to seconds
+ }
+
+ if (seconds < 0) return "0:00";
+
+ const hours = Math.floor(seconds / 3600);
+ const minutes = Math.floor((seconds % 3600) / 60);
+ const remainingSeconds = Math.floor(seconds % 60);
+
+ if (hours > 0) {
+ return `${hours}h ${minutes}m ${remainingSeconds}s`;
+ } else {
+ return `${minutes}m ${remainingSeconds}s`;
+ }
+};
+
+export const secondsToTicks = (seconds?: number | undefined) => {
+ if (!seconds) return 0;
+ return seconds * 10000000;
+};
+
+export const ticksToSeconds = (ticks?: number | undefined) => {
+ if (!ticks) return 0;
+ return ticks / 10000000;
+};