Merge branch 'develop' into build-performance

This commit is contained in:
Gauvain
2025-09-25 22:48:39 +02:00
committed by GitHub
21 changed files with 151 additions and 63 deletions

View File

@@ -39,7 +39,7 @@ jobs:
bun-version: latest
- name: 💾 Cache Bun dependencies
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: ~/.bun/install/cache
key: ${{ runner.os }}-${{ runner.arch }}-bun-develop-${{ hashFiles('bun.lock') }}
@@ -53,7 +53,7 @@ jobs:
bun run submodule-reload
- name: 💾 Cache Gradle global
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: |
~/.gradle/caches
@@ -70,7 +70,7 @@ jobs:
fi
- name: 💾 Cache project Gradle (.gradle)
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: android/.gradle
key: ${{ runner.os }}-android-gradle-develop-${{ hashFiles('android/**/build.gradle', 'android/gradle/wrapper/gradle-wrapper.properties') }}

View File

@@ -44,7 +44,7 @@ jobs:
bun-version: latest
- name: 💾 Cache Bun dependencies
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: ~/.bun/install/cache
key: ${{ runner.os }}-bun-cache-${{ hashFiles('bun.lock') }}

View File

@@ -32,7 +32,7 @@ jobs:
bun-version: latest
- name: 💾 Cache Bun dependencies
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: |
~/.bun/install/cache

View File

@@ -139,7 +139,15 @@ const Page: React.FC = () => {
}
requestMedia(mediaTitle, body, refetch);
}, [details, result, requestMedia, hasAdvancedRequestPermission]);
}, [
details,
result,
requestMedia,
hasAdvancedRequestPermission,
mediaTitle,
refetch,
mediaType,
]);
const isAnime = useMemo(
() =>
@@ -277,12 +285,16 @@ const Page: React.FC = () => {
<Button
className='flex-1 bg-purple-600/50 border-purple-400 ring-purple-400 text-purple-100'
onPress={() => {
const url =
mediaType === MediaType.MOVIE
? `/(auth)/(tabs)/(search)/items/page?id=${details?.mediaInfo.jellyfinMediaId}`
: `/(auth)/(tabs)/(search)/series/${details?.mediaInfo.jellyfinMediaId}`;
// @ts-expect-error
router.push(url);
router.push({
pathname:
mediaType === MediaType.MOVIE
? "/(auth)/(tabs)/(search)/items/page"
: "/(auth)/(tabs)/(search)/series/[id]",
params:
mediaType === MediaType.MOVIE
? { id: details?.mediaInfo.jellyfinMediaId }
: { id: details?.mediaInfo.jellyfinMediaId },
});
}}
iconLeft={
<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 { jellyseerrApi, jellyseerrUser, requestMedia } = useJellyseerr();
const from = segments[2] || "(home)";
const from = (segments as string[])[2] || "(home)";
const autoApprove = useMemo(() => {
return (

View File

@@ -43,6 +43,48 @@ export const itemRouter = (item: BaseItemDto, from: string) => {
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>> = ({
item,
isOffline = false,
@@ -55,7 +97,7 @@ export const TouchableItemRouter: React.FC<PropsWithChildren<Props>> = ({
const markAsPlayedStatus = useMarkAsPlayed([item]);
const { isFavorite, toggleFavorite } = useFavorite(item);
const from = segments[2] || "(home)";
const from = (segments as string[])[2] || "(home)";
const showActionSheet = useCallback(() => {
if (
@@ -101,12 +143,15 @@ export const TouchableItemRouter: React.FC<PropsWithChildren<Props>> = ({
<TouchableOpacity
onLongPress={showActionSheet}
onPress={() => {
let url = itemRouter(item, from);
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}
>

View File

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

View File

@@ -1,12 +1,21 @@
import { BottomSheetTextInput } from "@gorhom/bottom-sheet";
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;
onChangeText: (text: string) => void;
length?: number;
autoFocus?: boolean;
style?: StyleProp<ViewStyle>;
}
export interface PinInputRef {
@@ -118,6 +127,5 @@ const styles = StyleSheet.create({
width: 2,
height: 24,
backgroundColor: "#6366F1",
animation: "blink 1s infinite",
},
});

View File

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

View File

@@ -16,13 +16,12 @@ const CompanySlide: React.FC<
> = ({ slide, data, ...props }) => {
const segments = useSegments();
const { jellyseerrApi } = useJellyseerr();
const from = segments[2] || "(home)";
const from = (segments as string[])[2] || "(home)";
const navigate = useCallback(
({ id, image, name }: Network | Studio) =>
router.push({
// @ts-expect-error - Dynamic pathname for jellyseerr routing
pathname: `/(auth)/(tabs)/${from}/jellyseerr/company/${id}`,
pathname: `/(auth)/(tabs)/${from}/jellyseerr/company/${id}` as any,
params: { id, image, name, type: slide.type },
}),
[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 segments = useSegments();
const { jellyseerrApi } = useJellyseerr();
const from = segments[2] || "(home)";
const from = (segments as string[])[2] || "(home)";
const navigate = useCallback(
(genre: GenreSliderItem) =>
router.push({
// @ts-expect-error - Dynamic pathname for jellyseerr routing
pathname: `/(auth)/(tabs)/${from}/jellyseerr/genre/${genre.id}`,
pathname: `/(auth)/(tabs)/${from}/jellyseerr/genre/${genre.id}` as any,
params: { type: slide.type, name: genre.name },
}),
[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 { 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 { data: details } = useQuery({
@@ -67,9 +74,15 @@ const RecentRequestsSlide: React.FC<SlideProps & ViewProps> = ({
<Slide
{...props}
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()}
renderItem={(item: NonFunctionProperties<MediaRequest>) => (
renderItem={(item: ExtendedMediaRequest) => (
<RequestCard request={item} />
)}
/>

View File

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

View File

@@ -1,7 +1,7 @@
import { Ionicons } from "@expo/vector-icons";
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
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 { useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
@@ -100,8 +100,6 @@ export const SeasonPicker: React.FC<Props> = ({ item }) => {
enabled: !!api && !!user?.Id && !!item.Id && !!selectedSeasonId,
});
const _queryClient = useQueryClient();
// Used for height calculation
const [nrOfEpisodes, setNrOfEpisodes] = useState(0);
useEffect(() => {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -284,7 +284,28 @@
"collections": "Collections",
"actors": "Actors",
"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": {
"no_results": "No Results",

View File

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