Merge branch 'develop' into feat/refresh-metadata

This commit is contained in:
Fredrik Burmester
2025-11-15 09:08:48 +01:00
committed by GitHub
214 changed files with 9832 additions and 4765 deletions

67
hooks/useInView.ts Normal file
View 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,
};
};

View File

@@ -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 };
};

View File

@@ -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 {

View File

@@ -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";

View File

@@ -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 };
};