fix: resolve type issues and improve component reliability (#1078)
Some checks failed
🤖 Android APK Build (Phone + TV) / 🏗️ Build Android APK (phone) (push) Has been cancelled
🤖 Android APK Build (Phone + TV) / 🏗️ Build Android APK (tv) (push) Has been cancelled
🤖 iOS IPA Build (Phone + TV) / 🏗️ Build iOS IPA (phone) (push) Has been cancelled
🔒 Lockfile Consistency Check / 🔍 Check bun.lock and package.json consistency (push) Has been cancelled
🛡️ CodeQL Analysis / 🔎 Analyze with CodeQL (actions) (push) Has been cancelled
🛡️ CodeQL Analysis / 🔎 Analyze with CodeQL (javascript-typescript) (push) Has been cancelled
🏷️🔀Merge Conflict Labeler / 🏷️ Labeling Merge Conflicts (push) Has been cancelled
🚦 Security & Quality Gate / 📝 Validate PR Title (push) Has been cancelled
🚦 Security & Quality Gate / 🔍 Vulnerable Dependencies (push) Has been cancelled
🚦 Security & Quality Gate / 🚑 Expo Doctor Check (push) Has been cancelled
🚦 Security & Quality Gate / 🔍 Lint & Test (check) (push) Has been cancelled
🚦 Security & Quality Gate / 🔍 Lint & Test (format) (push) Has been cancelled
🚦 Security & Quality Gate / 🔍 Lint & Test (lint) (push) Has been cancelled
🚦 Security & Quality Gate / 🔍 Lint & Test (typecheck) (push) Has been cancelled
🕒 Handle Stale Issues / 🗑️ Cleanup Stale Issues (push) Has been cancelled

This commit is contained in:
Gauvain
2025-09-25 22:47:49 +02:00
committed by GitHub
parent 2a195d9ba5
commit ead37aa806
19 changed files with 147 additions and 61 deletions

View File

@@ -139,7 +139,15 @@ const Page: React.FC = () => {
} }
requestMedia(mediaTitle, body, refetch); requestMedia(mediaTitle, body, refetch);
}, [details, result, requestMedia, hasAdvancedRequestPermission]); }, [
details,
result,
requestMedia,
hasAdvancedRequestPermission,
mediaTitle,
refetch,
mediaType,
]);
const isAnime = useMemo( const isAnime = useMemo(
() => () =>
@@ -277,12 +285,16 @@ const Page: React.FC = () => {
<Button <Button
className='flex-1 bg-purple-600/50 border-purple-400 ring-purple-400 text-purple-100' className='flex-1 bg-purple-600/50 border-purple-400 ring-purple-400 text-purple-100'
onPress={() => { onPress={() => {
const url = router.push({
mediaType === MediaType.MOVIE pathname:
? `/(auth)/(tabs)/(search)/items/page?id=${details?.mediaInfo.jellyfinMediaId}` mediaType === MediaType.MOVIE
: `/(auth)/(tabs)/(search)/series/${details?.mediaInfo.jellyfinMediaId}`; ? "/(auth)/(tabs)/(search)/items/page"
// @ts-expect-error : "/(auth)/(tabs)/(search)/series/[id]",
router.push(url); params:
mediaType === MediaType.MOVIE
? { id: details?.mediaInfo.jellyfinMediaId }
: { id: details?.mediaInfo.jellyfinMediaId },
});
}} }}
iconLeft={ iconLeft={
<Ionicons name='play-outline' size={20} color='white' /> <Ionicons name='play-outline' size={20} color='white' />

View File

@@ -40,7 +40,7 @@ export const TouchableJellyseerrRouter: React.FC<PropsWithChildren<Props>> = ({
const segments = useSegments(); const segments = useSegments();
const { jellyseerrApi, jellyseerrUser, requestMedia } = useJellyseerr(); const { jellyseerrApi, jellyseerrUser, requestMedia } = useJellyseerr();
const from = segments[2] || "(home)"; const from = (segments as string[])[2] || "(home)";
const autoApprove = useMemo(() => { const autoApprove = useMemo(() => {
return ( return (

View File

@@ -43,6 +43,48 @@ export const itemRouter = (item: BaseItemDto, from: string) => {
return `/(auth)/(tabs)/${from}/items/page?id=${item.Id}`; return `/(auth)/(tabs)/${from}/items/page?id=${item.Id}`;
}; };
export const getItemNavigation = (item: BaseItemDto, _from: string) => {
if ("CollectionType" in item && item.CollectionType === "livetv") {
return {
pathname: "/livetv" as const,
};
}
if (item.Type === "Series") {
return {
pathname: "/series/[id]" as const,
params: { id: item.Id! },
};
}
if (item.Type === "Person") {
return {
pathname: "/persons/[personId]" as const,
params: { personId: item.Id! },
};
}
if (item.Type === "BoxSet" || item.Type === "UserView") {
return {
pathname: "/collections/[collectionId]" as const,
params: { collectionId: item.Id! },
};
}
if (item.Type === "CollectionFolder" || item.Type === "Playlist") {
return {
pathname: "/[libraryId]" as const,
params: { libraryId: item.Id! },
};
}
// Default case - items page
return {
pathname: "/items/page" as const,
params: { id: item.Id! },
};
};
export const TouchableItemRouter: React.FC<PropsWithChildren<Props>> = ({ export const TouchableItemRouter: React.FC<PropsWithChildren<Props>> = ({
item, item,
isOffline = false, isOffline = false,
@@ -55,7 +97,7 @@ export const TouchableItemRouter: React.FC<PropsWithChildren<Props>> = ({
const markAsPlayedStatus = useMarkAsPlayed([item]); const markAsPlayedStatus = useMarkAsPlayed([item]);
const { isFavorite, toggleFavorite } = useFavorite(item); const { isFavorite, toggleFavorite } = useFavorite(item);
const from = segments[2] || "(home)"; const from = (segments as string[])[2] || "(home)";
const showActionSheet = useCallback(() => { const showActionSheet = useCallback(() => {
if ( if (
@@ -101,12 +143,15 @@ export const TouchableItemRouter: React.FC<PropsWithChildren<Props>> = ({
<TouchableOpacity <TouchableOpacity
onLongPress={showActionSheet} onLongPress={showActionSheet}
onPress={() => { onPress={() => {
let url = itemRouter(item, from);
if (isOffline) { if (isOffline) {
url += `&offline=true`; // For offline mode, we still need to use query params
const url = `${itemRouter(item, from)}&offline=true`;
router.push(url as any);
return;
} }
// @ts-expect-error
router.push(url); const navigation = getItemNavigation(item, from);
router.push(navigation as any);
}} }}
{...props} {...props}
> >

View File

@@ -21,7 +21,7 @@ import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { useSettings } from "@/utils/atoms/settings"; import { useSettings } from "@/utils/atoms/settings";
import { getBackdropUrl } from "@/utils/jellyfin/image/getBackdropUrl"; import { getBackdropUrl } from "@/utils/jellyfin/image/getBackdropUrl";
import { getLogoImageUrlById } from "@/utils/jellyfin/image/getLogoImageUrlById"; import { getLogoImageUrlById } from "@/utils/jellyfin/image/getLogoImageUrlById";
import { itemRouter } from "../common/TouchableItemRouter"; import { getItemNavigation } from "../common/TouchableItemRouter";
interface Props extends ViewProps {} interface Props extends ViewProps {}
@@ -146,16 +146,15 @@ const RenderItem: React.FC<{ item: BaseItemDto }> = ({ item }) => {
}, [item]); }, [item]);
const segments = useSegments(); const segments = useSegments();
const from = segments[2] || "(home)"; const from = (segments as string[])[2] || "(home)";
const opacity = useSharedValue(1); const opacity = useSharedValue(1);
const handleRoute = useCallback(() => { const handleRoute = useCallback(() => {
if (!from) return; if (!from) return;
const url = itemRouter(item, from);
lightHapticFeedback(); lightHapticFeedback();
// @ts-expect-error const navigation = getItemNavigation(item, from);
if (url) router.push(url); router.push(navigation as any);
}, [item, from]); }, [item, from]);
const tap = Gesture.Tap() const tap = Gesture.Tap()

View File

@@ -1,12 +1,21 @@
import { BottomSheetTextInput } from "@gorhom/bottom-sheet"; import { BottomSheetTextInput } from "@gorhom/bottom-sheet";
import React, { useCallback, useImperativeHandle, useRef } from "react"; import React, { useCallback, useImperativeHandle, useRef } from "react";
import { StyleSheet, Text, type TextInputProps, View } from "react-native"; import {
type StyleProp,
StyleSheet,
Text,
type TextInputProps,
View,
type ViewStyle,
} from "react-native";
interface PinInputProps extends Omit<TextInputProps, "value" | "onChangeText"> { interface PinInputProps
extends Omit<TextInputProps, "value" | "onChangeText" | "style"> {
value: string; value: string;
onChangeText: (text: string) => void; onChangeText: (text: string) => void;
length?: number; length?: number;
autoFocus?: boolean; autoFocus?: boolean;
style?: StyleProp<ViewStyle>;
} }
export interface PinInputRef { export interface PinInputRef {
@@ -118,6 +127,5 @@ const styles = StyleSheet.create({
width: 2, width: 2,
height: 24, height: 24,
backgroundColor: "#6366F1", backgroundColor: "#6366F1",
animation: "blink 1s infinite",
}, },
}); });

View File

@@ -22,7 +22,7 @@ const PersonPoster: React.FC<Props & ViewProps> = ({
const { jellyseerrApi } = useJellyseerr(); const { jellyseerrApi } = useJellyseerr();
const router = useRouter(); const router = useRouter();
const segments = useSegments(); const segments = useSegments();
const from = segments[2] || "(home)"; const from = (segments as string[])[2] || "(home)";
if (from === "(home)" || from === "(search)" || from === "(libraries)") if (from === "(home)" || from === "(search)" || from === "(libraries)")
return ( return (

View File

@@ -16,13 +16,12 @@ const CompanySlide: React.FC<
> = ({ slide, data, ...props }) => { > = ({ slide, data, ...props }) => {
const segments = useSegments(); const segments = useSegments();
const { jellyseerrApi } = useJellyseerr(); const { jellyseerrApi } = useJellyseerr();
const from = segments[2] || "(home)"; const from = (segments as string[])[2] || "(home)";
const navigate = useCallback( const navigate = useCallback(
({ id, image, name }: Network | Studio) => ({ id, image, name }: Network | Studio) =>
router.push({ router.push({
// @ts-expect-error - Dynamic pathname for jellyseerr routing pathname: `/(auth)/(tabs)/${from}/jellyseerr/company/${id}` as any,
pathname: `/(auth)/(tabs)/${from}/jellyseerr/company/${id}`,
params: { id, image, name, type: slide.type }, params: { id, image, name, type: slide.type },
}), }),
[slide], [slide],

View File

@@ -13,13 +13,12 @@ import { genreColorMap } from "@/utils/jellyseerr/src/components/Discover/consta
const GenreSlide: React.FC<SlideProps & ViewProps> = ({ slide, ...props }) => { const GenreSlide: React.FC<SlideProps & ViewProps> = ({ slide, ...props }) => {
const segments = useSegments(); const segments = useSegments();
const { jellyseerrApi } = useJellyseerr(); const { jellyseerrApi } = useJellyseerr();
const from = segments[2] || "(home)"; const from = (segments as string[])[2] || "(home)";
const navigate = useCallback( const navigate = useCallback(
(genre: GenreSliderItem) => (genre: GenreSliderItem) =>
router.push({ router.push({
// @ts-expect-error - Dynamic pathname for jellyseerr routing pathname: `/(auth)/(tabs)/${from}/jellyseerr/genre/${genre.id}` as any,
pathname: `/(auth)/(tabs)/${from}/jellyseerr/genre/${genre.id}`,
params: { type: slide.type, name: genre.name }, params: { type: slide.type, name: genre.name },
}), }),
[slide], [slide],

View File

@@ -8,7 +8,14 @@ import { MediaType } from "@/utils/jellyseerr/server/constants/media";
import type MediaRequest from "@/utils/jellyseerr/server/entity/MediaRequest"; import type MediaRequest from "@/utils/jellyseerr/server/entity/MediaRequest";
import type { NonFunctionProperties } from "@/utils/jellyseerr/server/interfaces/api/common"; import type { NonFunctionProperties } from "@/utils/jellyseerr/server/interfaces/api/common";
const RequestCard: React.FC<{ request: MediaRequest }> = ({ request }) => { type ExtendedMediaRequest = NonFunctionProperties<MediaRequest> & {
profileName: string;
canRemove: boolean;
};
const RequestCard: React.FC<{ request: ExtendedMediaRequest }> = ({
request,
}) => {
const { jellyseerrApi } = useJellyseerr(); const { jellyseerrApi } = useJellyseerr();
const { data: details } = useQuery({ const { data: details } = useQuery({
@@ -67,9 +74,15 @@ const RecentRequestsSlide: React.FC<SlideProps & ViewProps> = ({
<Slide <Slide
{...props} {...props}
slide={slide} slide={slide}
data={requests.results} data={
requests.results.map((item) => ({
...item,
profileName: item.profileName ?? "Unknown",
canRemove: Boolean(item.canRemove),
})) as ExtendedMediaRequest[]
}
keyExtractor={(item) => item.id.toString()} keyExtractor={(item) => item.id.toString()}
renderItem={(item: NonFunctionProperties<MediaRequest>) => ( renderItem={(item: ExtendedMediaRequest) => (
<RequestCard request={item} /> <RequestCard request={item} />
)} )}
/> />

View File

@@ -12,7 +12,6 @@ import { apiAtom } from "@/providers/JellyfinProvider";
import { getPrimaryImageUrl } from "@/utils/jellyfin/image/getPrimaryImageUrl"; import { getPrimaryImageUrl } from "@/utils/jellyfin/image/getPrimaryImageUrl";
import { HorizontalScroll } from "../common/HorizontalScroll"; import { HorizontalScroll } from "../common/HorizontalScroll";
import { Text } from "../common/Text"; import { Text } from "../common/Text";
import { itemRouter } from "../common/TouchableItemRouter";
import Poster from "../posters/Poster"; import Poster from "../posters/Poster";
interface Props extends ViewProps { interface Props extends ViewProps {
@@ -24,7 +23,7 @@ export const CastAndCrew: React.FC<Props> = ({ item, loading, ...props }) => {
const [api] = useAtom(apiAtom); const [api] = useAtom(apiAtom);
const segments = useSegments(); const segments = useSegments();
const { t } = useTranslation(); const { t } = useTranslation();
const from = segments[2]; const from = (segments as string[])[2];
const destinctPeople = useMemo(() => { const destinctPeople = useMemo(() => {
const people: Record<string, BaseItemPerson> = {}; const people: Record<string, BaseItemPerson> = {};
@@ -56,15 +55,12 @@ export const CastAndCrew: React.FC<Props> = ({ item, loading, ...props }) => {
renderItem={(i) => ( renderItem={(i) => (
<TouchableOpacity <TouchableOpacity
onPress={() => { onPress={() => {
const url = itemRouter( if (i.Id) {
{ router.push({
Id: i.Id, pathname: "/persons/[personId]",
Type: "Person", params: { personId: i.Id },
}, });
from, }
);
// @ts-expect-error
router.push(url);
}} }}
className='flex flex-col w-28' className='flex flex-col w-28'
> >

View File

@@ -1,13 +1,12 @@
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
import { getTvShowsApi } from "@jellyfin/sdk/lib/utils/api"; import { getTvShowsApi } from "@jellyfin/sdk/lib/utils/api";
import { useQuery, useQueryClient } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { router } from "expo-router"; import { router } from "expo-router";
import { useAtom } from "jotai"; import { useAtom } from "jotai";
import { useEffect, useMemo, useRef } from "react"; import { useEffect, useMemo, useRef } from "react";
import { TouchableOpacity, type ViewProps } from "react-native"; import { TouchableOpacity, type ViewProps } from "react-native";
import { useDownload } from "@/providers/DownloadProvider"; import { useDownload } from "@/providers/DownloadProvider";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { getUserItemData } from "@/utils/jellyfin/user-library/getUserItemData";
import ContinueWatchingPoster from "../ContinueWatchingPoster"; import ContinueWatchingPoster from "../ContinueWatchingPoster";
import { import {
HorizontalScroll, HorizontalScroll,

View File

@@ -1,7 +1,7 @@
import { Ionicons } from "@expo/vector-icons"; import { Ionicons } from "@expo/vector-icons";
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
import { getTvShowsApi } from "@jellyfin/sdk/lib/utils/api"; import { getTvShowsApi } from "@jellyfin/sdk/lib/utils/api";
import { useQuery, useQueryClient } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { atom, useAtom } from "jotai"; import { atom, useAtom } from "jotai";
import { useEffect, useMemo, useState } from "react"; import { useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
@@ -11,7 +11,6 @@ import {
type SeasonIndexState, type SeasonIndexState,
} from "@/components/series/SeasonDropdown"; } from "@/components/series/SeasonDropdown";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { getUserItemData } from "@/utils/jellyfin/user-library/getUserItemData";
import { runtimeTicksToSeconds } from "@/utils/time"; import { runtimeTicksToSeconds } from "@/utils/time";
import ContinueWatchingPoster from "../ContinueWatchingPoster"; import ContinueWatchingPoster from "../ContinueWatchingPoster";
import { Text } from "../common/Text"; import { Text } from "../common/Text";
@@ -101,8 +100,6 @@ export const SeasonPicker: React.FC<Props> = ({ item }) => {
enabled: !!api && !!user?.Id && !!item.Id && !!selectedSeasonId, enabled: !!api && !!user?.Id && !!item.Id && !!selectedSeasonId,
}); });
const queryClient = useQueryClient();
// Used for height calculation // Used for height calculation
const [nrOfEpisodes, setNrOfEpisodes] = useState(0); const [nrOfEpisodes, setNrOfEpisodes] = useState(0);
useEffect(() => { useEffect(() => {

View File

@@ -125,7 +125,7 @@ export const HomeIndex = () => {
const segments = useSegments(); const segments = useSegments();
useEffect(() => { useEffect(() => {
const unsubscribe = eventBus.on("scrollToTop", () => { const unsubscribe = eventBus.on("scrollToTop", () => {
if (segments[2] === "(home)") if ((segments as string[])[2] === "(home)")
scrollViewRef.current?.scrollTo({ y: -152, animated: true }); scrollViewRef.current?.scrollTo({ y: -152, animated: true });
}); });
@@ -316,7 +316,7 @@ export const HomeIndex = () => {
const id = section.title || `section-${index}`; const id = section.title || `section-${index}`;
ss.push({ ss.push({
title: t(`${id}`), title: t(`${id}`),
queryKey: ["home", id], queryKey: ["home", "custom", String(index), section.title ?? null],
queryFn: async () => { queryFn: async () => {
if (section.items) { if (section.items) {
const response = await getItemsApi(api).getItems({ const response = await getItemsApi(api).getItems({
@@ -364,8 +364,8 @@ export const HomeIndex = () => {
const sections = settings?.home?.sections ? customSections : defaultSections; const sections = settings?.home?.sections ? customSections : defaultSections;
if (!isConnected || serverConnected !== true) { if (!isConnected || serverConnected !== true) {
let title: string; let title = "";
let subtitle: string; let subtitle = "";
if (!isConnected) { if (!isConnected) {
// No network connection // No network connection

View File

@@ -379,8 +379,7 @@ export const Controls: FC<Props> = ({
console.log("queryParams", queryParams); console.log("queryParams", queryParams);
// @ts-expect-error router.replace(`player/direct-player?${queryParams}` as any);
router.replace(`player/direct-player?${queryParams}`);
}, },
[settings, subtitleIndex, audioIndex, mediaSource, bitrateValue, router], [settings, subtitleIndex, audioIndex, mediaSource, bitrateValue, router],
); );

View File

@@ -95,8 +95,7 @@ export const VideoProvider: React.FC<VideoProviderProps> = ({
playbackPosition: playbackPosition, playbackPosition: playbackPosition,
}).toString(); }).toString();
//@ts-expect-error router.replace(`player/direct-player?${queryParams}` as any);
router.replace(`player/direct-player?${queryParams}`);
}; };
const setTrackParams = ( const setTrackParams = (

View File

@@ -51,8 +51,7 @@ const DropdownView = () => {
bitrateValue: bitrate.toString(), bitrateValue: bitrate.toString(),
playbackPosition: playbackPosition, playbackPosition: playbackPosition,
}).toString(); }).toString();
// @ts-expect-error router.replace(`player/direct-player?${queryParams}` as any);
router.replace(`player/direct-player?${queryParams}`);
}, },
[item, mediaSource, subtitleIndex, audioIndex, playbackPosition], [item, mediaSource, subtitleIndex, audioIndex, playbackPosition],
); );

View File

@@ -387,7 +387,7 @@ export class JellyseerrApi {
`Jellyseerr response error\nerror: ${error.toString()}\nurl: ${error?.config?.url}`, `Jellyseerr response error\nerror: ${error.toString()}\nurl: ${error?.config?.url}`,
error.response?.data, error.response?.data,
); );
if (error.status === 403) { if (error.response?.status === 403) {
clearJellyseerrStorageData(); clearJellyseerrStorageData();
} }
return Promise.reject(error); return Promise.reject(error);
@@ -512,7 +512,7 @@ export const useJellyseerr = () => {
}; };
const jellyseerrRegion = useMemo( const jellyseerrRegion = useMemo(
() => jellyseerrUser?.settings?.region || "US", () => jellyseerrUser?.settings?.discoverRegion || "US",
[jellyseerrUser], [jellyseerrUser],
); );

View File

@@ -284,7 +284,28 @@
"collections": "Collections", "collections": "Collections",
"actors": "Actors", "actors": "Actors",
"request_movies": "Request Movies", "request_movies": "Request Movies",
"request_series": "Request Series" "request_series": "Request Series",
"recently_added": "Recently Added",
"recent_requests": "Recent Requests",
"plex_watchlist": "Plex Watchlist",
"trending": "Trending",
"popular_movies": "Popular Movies",
"movie_genres": "Movie Genres",
"upcoming_movies": "Upcoming Movies",
"studios": "Studios",
"popular_tv": "Popular TV",
"tv_genres": "TV Genres",
"upcoming_tv": "Upcoming TV",
"networks": "Networks",
"tmdb_movie_keyword": "TMDB Movie Keyword",
"tmdb_movie_genre": "TMDB Movie Genre",
"tmdb_tv_keyword": "TMDB TV Keyword",
"tmdb_tv_genre": "TMDB TV Genre",
"tmdb_search": "TMDB Search",
"tmdb_studio": "TMDB Studio",
"tmdb_network": "TMDB Network",
"tmdb_movie_streaming_services": "TMDB Movie Streaming Services",
"tmdb_tv_streaming_services": "TMDB TV Streaming Services"
}, },
"library": { "library": {
"no_results": "No Results", "no_results": "No Results",

View File

@@ -88,6 +88,7 @@ export type Home = {
}; };
export type HomeSection = { export type HomeSection = {
title?: string;
orientation?: "horizontal" | "vertical"; orientation?: "horizontal" | "vertical";
items?: HomeSectionItemResolver; items?: HomeSectionItemResolver;
nextUp?: HomeSectionNextUpResolver; nextUp?: HomeSectionNextUpResolver;