mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-05-25 16:26:54 +01:00
Merge branch 'develop' into feat/refresh-metadata
This commit is contained in:
67
hooks/useInView.ts
Normal file
67
hooks/useInView.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { type LayoutRectangle, useWindowDimensions } from "react-native";
|
||||
|
||||
interface UseInViewOptions {
|
||||
threshold?: number; // Distance in pixels before component is considered "in view"
|
||||
enabled?: boolean; // Allow disabling the hook
|
||||
}
|
||||
|
||||
interface UseInViewReturn {
|
||||
ref: (node: any) => void;
|
||||
isInView: boolean;
|
||||
onLayout: () => void;
|
||||
}
|
||||
|
||||
export const useInView = (
|
||||
scrollY: number = 0,
|
||||
options: UseInViewOptions = {},
|
||||
): UseInViewReturn => {
|
||||
const { threshold = 400, enabled = true } = options;
|
||||
const { height: windowHeight } = useWindowDimensions();
|
||||
const [layout, setLayout] = useState<LayoutRectangle | null>(null);
|
||||
const [hasBeenInView, setHasBeenInView] = useState(false);
|
||||
const nodeRef = useRef<any>(null);
|
||||
|
||||
const ref = useCallback((node: any) => {
|
||||
nodeRef.current = node;
|
||||
}, []);
|
||||
|
||||
const onLayout = useCallback(() => {
|
||||
if (!nodeRef.current) return;
|
||||
|
||||
// Use measure to get absolute position
|
||||
nodeRef.current.measure(
|
||||
(
|
||||
_x: number,
|
||||
_y: number,
|
||||
width: number,
|
||||
height: number,
|
||||
pageX: number,
|
||||
pageY: number,
|
||||
) => {
|
||||
setLayout({ x: pageX, y: pageY, width, height });
|
||||
},
|
||||
);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!enabled || hasBeenInView || !layout) return;
|
||||
|
||||
// Calculate if the section is in view or about to be
|
||||
const sectionTop = layout.y;
|
||||
const viewportBottom = scrollY + windowHeight;
|
||||
|
||||
// Check if section is within threshold distance of viewport
|
||||
const isNearView = viewportBottom + threshold >= sectionTop;
|
||||
|
||||
if (isNearView) {
|
||||
setHasBeenInView(true);
|
||||
}
|
||||
}, [scrollY, windowHeight, threshold, layout, hasBeenInView, enabled]);
|
||||
|
||||
return {
|
||||
ref,
|
||||
isInView: hasBeenInView,
|
||||
onLayout,
|
||||
};
|
||||
};
|
||||
@@ -43,26 +43,60 @@ export const useIntroSkipper = (
|
||||
const introTimestamps = segments?.introSegments?.[0];
|
||||
|
||||
useEffect(() => {
|
||||
console.log(`[INTRO_SKIPPER] Hook state:`, {
|
||||
itemId,
|
||||
currentTime,
|
||||
hasSegments: !!segments,
|
||||
segments: segments,
|
||||
introSegmentsCount: segments?.introSegments?.length || 0,
|
||||
introSegments: segments?.introSegments,
|
||||
hasIntroTimestamps: !!introTimestamps,
|
||||
introTimestamps,
|
||||
isVlc,
|
||||
isOffline,
|
||||
});
|
||||
|
||||
if (introTimestamps) {
|
||||
setShowSkipButton(
|
||||
const shouldShow =
|
||||
currentTime > introTimestamps.startTime &&
|
||||
currentTime < introTimestamps.endTime,
|
||||
);
|
||||
currentTime < introTimestamps.endTime;
|
||||
|
||||
console.log(`[INTRO_SKIPPER] Button visibility check:`, {
|
||||
currentTime,
|
||||
introStart: introTimestamps.startTime,
|
||||
introEnd: introTimestamps.endTime,
|
||||
afterStart: currentTime > introTimestamps.startTime,
|
||||
beforeEnd: currentTime < introTimestamps.endTime,
|
||||
shouldShow,
|
||||
});
|
||||
|
||||
setShowSkipButton(shouldShow);
|
||||
} else {
|
||||
if (showSkipButton) {
|
||||
console.log(`[INTRO_SKIPPER] No intro timestamps, hiding button`);
|
||||
setShowSkipButton(false);
|
||||
}
|
||||
}
|
||||
}, [introTimestamps, currentTime]);
|
||||
}, [introTimestamps, currentTime, showSkipButton]);
|
||||
|
||||
const skipIntro = useCallback(() => {
|
||||
if (!introTimestamps) return;
|
||||
try {
|
||||
console.log(
|
||||
`[INTRO_SKIPPER] Skipping intro to:`,
|
||||
introTimestamps.endTime,
|
||||
);
|
||||
lightHapticFeedback();
|
||||
wrappedSeek(introTimestamps.endTime);
|
||||
setTimeout(() => {
|
||||
play();
|
||||
}, 200);
|
||||
} catch (error) {
|
||||
console.error("Error skipping intro", error);
|
||||
console.error("[INTRO_SKIPPER] Error skipping intro", error);
|
||||
}
|
||||
}, [introTimestamps, lightHapticFeedback, wrappedSeek, play]);
|
||||
|
||||
console.log(`[INTRO_SKIPPER] Returning state:`, { showSkipButton });
|
||||
|
||||
return { showSkipButton, skipIntro };
|
||||
};
|
||||
|
||||
@@ -66,8 +66,8 @@ const JELLYSEERR_USER = "JELLYSEERR_USER";
|
||||
const JELLYSEERR_COOKIES = "JELLYSEERR_COOKIES";
|
||||
|
||||
export const clearJellyseerrStorageData = () => {
|
||||
storage.delete(JELLYSEERR_USER);
|
||||
storage.delete(JELLYSEERR_COOKIES);
|
||||
storage.remove(JELLYSEERR_USER);
|
||||
storage.remove(JELLYSEERR_COOKIES);
|
||||
};
|
||||
|
||||
export enum Endpoints {
|
||||
|
||||
@@ -1,58 +1,2 @@
|
||||
import NetInfo from "@react-native-community/netinfo";
|
||||
import { useAtom } from "jotai";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { apiAtom } from "@/providers/JellyfinProvider";
|
||||
|
||||
async function checkApiReachable(basePath?: string): Promise<boolean> {
|
||||
if (!basePath) return false;
|
||||
try {
|
||||
const response = await fetch(basePath, { method: "HEAD" });
|
||||
return response.ok;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function useNetworkStatus() {
|
||||
const [isConnected, setIsConnected] = useState(false);
|
||||
const [serverConnected, setServerConnected] = useState<boolean | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [api] = useAtom(apiAtom);
|
||||
|
||||
const validateConnection = useCallback(async () => {
|
||||
if (!api?.basePath) return false;
|
||||
const reachable = await checkApiReachable(api.basePath);
|
||||
setServerConnected(reachable);
|
||||
return reachable;
|
||||
}, [api?.basePath]);
|
||||
|
||||
const retryCheck = useCallback(async () => {
|
||||
setLoading(true);
|
||||
await validateConnection();
|
||||
setLoading(false);
|
||||
}, [validateConnection]);
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribe = NetInfo.addEventListener(async (state) => {
|
||||
setIsConnected(!!state.isConnected);
|
||||
if (state.isConnected) {
|
||||
await validateConnection();
|
||||
} else {
|
||||
setServerConnected(false);
|
||||
}
|
||||
});
|
||||
|
||||
// Initial check: wait for NetInfo first
|
||||
NetInfo.fetch().then((state) => {
|
||||
if (state.isConnected) {
|
||||
validateConnection();
|
||||
} else {
|
||||
setServerConnected(false);
|
||||
}
|
||||
});
|
||||
|
||||
return () => unsubscribe();
|
||||
}, [validateConnection]);
|
||||
|
||||
return { isConnected, serverConnected, loading, retryCheck };
|
||||
}
|
||||
// Re-export from provider to maintain backward compatibility
|
||||
export { useNetworkStatus } from "@/providers/NetworkStatusProvider";
|
||||
|
||||
@@ -1,7 +1,23 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Platform } from "react-native";
|
||||
import * as ScreenOrientation from "@/packages/expo-screen-orientation";
|
||||
import orientationToOrientationLock from "@/utils/OrientationLockConverter";
|
||||
import { OrientationLock } from "@/packages/expo-screen-orientation";
|
||||
import { Orientation } from "../packages/expo-screen-orientation.tv";
|
||||
|
||||
const orientationToOrientationLock = (
|
||||
orientation: Orientation,
|
||||
): OrientationLock => {
|
||||
switch (orientation) {
|
||||
case Orientation.LANDSCAPE_LEFT:
|
||||
return OrientationLock.LANDSCAPE_LEFT;
|
||||
case Orientation.LANDSCAPE_RIGHT:
|
||||
return OrientationLock.LANDSCAPE_RIGHT;
|
||||
case Orientation.PORTRAIT_UP:
|
||||
return OrientationLock.PORTRAIT_UP;
|
||||
default:
|
||||
return OrientationLock.PORTRAIT_UP;
|
||||
}
|
||||
};
|
||||
|
||||
export const useOrientation = () => {
|
||||
const [orientation, setOrientation] = useState(
|
||||
@@ -29,5 +45,20 @@ export const useOrientation = () => {
|
||||
};
|
||||
}, []);
|
||||
|
||||
return { orientation, setOrientation };
|
||||
const lockOrientation = async (lock: OrientationLock) => {
|
||||
if (Platform.isTV) return;
|
||||
|
||||
if (lock === ScreenOrientation.OrientationLock.DEFAULT) {
|
||||
await ScreenOrientation.unlockAsync();
|
||||
} else {
|
||||
await ScreenOrientation.lockAsync(lock);
|
||||
}
|
||||
};
|
||||
|
||||
const unlockOrientation = async () => {
|
||||
if (Platform.isTV) return;
|
||||
await ScreenOrientation.unlockAsync();
|
||||
};
|
||||
|
||||
return { orientation, setOrientation, lockOrientation, unlockOrientation };
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user