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

View File

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

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