Merge branch 'develop' into sonarqube

This commit is contained in:
Gauvain
2025-09-29 22:32:37 +02:00
committed by GitHub
14 changed files with 788 additions and 145 deletions

View File

@@ -7,9 +7,10 @@ import {
import { useQuery } from "@tanstack/react-query";
import { Image } from "expo-image";
import { LinearGradient } from "expo-linear-gradient";
import { useRouter } from "expo-router";
import { useAtomValue } from "jotai";
import { useCallback, useEffect, useMemo, useState } from "react";
import { Dimensions, Pressable, View } from "react-native";
import { Dimensions, Pressable, TouchableOpacity, View } from "react-native";
import { Gesture, GestureDetector } from "react-native-gesture-handler";
import Animated, {
Easing,
@@ -25,6 +26,7 @@ import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { useSettings } from "@/utils/atoms/settings";
import { getLogoImageUrlById } from "@/utils/jellyfin/image/getLogoImageUrlById";
import { ItemImage } from "./common/ItemImage";
import { getItemNavigation } from "./common/TouchableItemRouter";
import type { SelectedOptions } from "./ItemContent";
import { PlayButton } from "./PlayButton";
import { PlayedStatus } from "./PlayedStatus";
@@ -150,6 +152,7 @@ export const AppleTVCarousel: React.FC<AppleTVCarouselProps> = ({
const api = useAtomValue(apiAtom);
const user = useAtomValue(userAtom);
const { isConnected, serverConnected } = useNetworkStatus();
const router = useRouter();
const [currentIndex, setCurrentIndex] = useState(initialIndex);
const translateX = useSharedValue(-currentIndex * screenWidth);
@@ -301,6 +304,14 @@ export const AppleTVCarousel: React.FC<AppleTVCarouselProps> = ({
[hasItems, items, onItemChange, translateX],
);
const navigateToItem = useCallback(
(item: BaseItemDto) => {
const navigation = getItemNavigation(item, "(home)");
router.push(navigation as any);
},
[router],
);
const panGesture = Gesture.Pan()
.activeOffsetX([-PAN_ACTIVE_OFFSET, PAN_ACTIVE_OFFSET])
.onUpdate((event) => {
@@ -591,7 +602,8 @@ export const AppleTVCarousel: React.FC<AppleTVCarouselProps> = ({
{/* Logo Section */}
{itemLogoUrl && (
<View
<TouchableOpacity
onPress={() => navigateToItem(item)}
style={{
position: "absolute",
bottom: LOGO_BOTTOM_POSITION,
@@ -611,7 +623,7 @@ export const AppleTVCarousel: React.FC<AppleTVCarouselProps> = ({
}}
contentFit='contain'
/>
</View>
</TouchableOpacity>
)}
{/* Type and Genres Section */}
@@ -625,41 +637,56 @@ export const AppleTVCarousel: React.FC<AppleTVCarouselProps> = ({
alignItems: "center",
}}
>
<Animated.Text
style={{
color: `rgba(255, 255, 255, ${TEXT_OPACITY})`,
fontSize: GENRES_FONT_SIZE,
fontWeight: "500",
textAlign: "center",
textShadowColor: TEXT_SHADOW_COLOR,
textShadowOffset: { width: 0, height: 1 },
textShadowRadius: TEXT_SHADOW_RADIUS,
}}
>
{(() => {
const typeLabel =
item.Type === "Series"
? "TV Show"
: item.Type === "Movie"
? "Movie"
: item.Type || "";
<TouchableOpacity onPress={() => navigateToItem(item)}>
<Animated.Text
style={{
color: `rgba(255, 255, 255, ${TEXT_OPACITY})`,
fontSize: GENRES_FONT_SIZE,
fontWeight: "500",
textAlign: "center",
textShadowColor: TEXT_SHADOW_COLOR,
textShadowOffset: { width: 0, height: 1 },
textShadowRadius: TEXT_SHADOW_RADIUS,
}}
>
{(() => {
let typeLabel = "";
const genres =
item.Genres && item.Genres.length > 0
? item.Genres.slice(0, MAX_GENRES_COUNT).join(" • ")
: "";
if (item.Type === "Episode") {
// For episodes, show season and episode number
const season = item.ParentIndexNumber;
const episode = item.IndexNumber;
if (season && episode) {
typeLabel = `S${season} • E${episode}`;
} else {
typeLabel = "Episode";
}
} else {
typeLabel =
item.Type === "Series"
? "TV Show"
: item.Type === "Movie"
? "Movie"
: item.Type || "";
}
if (typeLabel && genres) {
return `${typeLabel}${genres}`;
} else if (typeLabel) {
return typeLabel;
} else if (genres) {
return genres;
} else {
return "";
}
})()}
</Animated.Text>
const genres =
item.Genres && item.Genres.length > 0
? item.Genres.slice(0, MAX_GENRES_COUNT).join(" • ")
: "";
if (typeLabel && genres) {
return `${typeLabel}${genres}`;
} else if (typeLabel) {
return typeLabel;
} else if (genres) {
return genres;
} else {
return "";
}
})()}
</Animated.Text>
</TouchableOpacity>
</View>
{/* Controls Section */}

View File

@@ -225,7 +225,7 @@ export const DownloadItems: React.FC<DownloadProps> = ({
if (!mediaSource) {
console.error(`Could not get download URL for ${item.Name}`);
toast.error(
t("Could not get download URL for {{itemName}}", {
t("home.downloads.toasts.could_not_get_download_url_for_item", {
itemName: item.Name,
}),
);

View File

@@ -6,6 +6,7 @@ import { t } from "i18next";
import { useMemo } from "react";
import {
ActivityIndicator,
Platform,
TouchableOpacity,
type TouchableOpacityProps,
View,
@@ -109,9 +110,9 @@ export const DownloadCard = ({ process, ...props }: DownloadCardProps) => {
/>
)}
{/* Action buttons in top right corner */}
<View className='absolute top-2 right-2 flex flex-row items-center space-x-2 z-10'>
{process.status === "downloading" && (
{/* Action buttons in bottom right corner */}
<View className='absolute bottom-2 right-2 flex flex-row items-center space-x-2 z-10'>
{process.status === "downloading" && Platform.OS !== "ios" && (
<TouchableOpacity
onPress={() => handlePause(process.id)}
className='p-1'
@@ -119,7 +120,7 @@ export const DownloadCard = ({ process, ...props }: DownloadCardProps) => {
<Ionicons name='pause' size={20} color='white' />
</TouchableOpacity>
)}
{process.status === "paused" && (
{process.status === "paused" && Platform.OS !== "ios" && (
<TouchableOpacity
onPress={() => handleResume(process.id)}
className='p-1'

View File

@@ -1,6 +1,7 @@
import { Ionicons } from "@expo/vector-icons";
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client";
import { useCallback, useMemo } from "react";
import { useTranslation } from "react-i18next";
import {
Alert,
Linking,
@@ -16,6 +17,7 @@ interface Props extends ViewProps {
}
export const ItemActions = ({ item, ...props }: Props) => {
const { t } = useTranslation();
const trailerLink = useMemo(() => {
if ("RemoteTrailers" in item && item.RemoteTrailers?.[0]?.Url) {
return item.RemoteTrailers[0].Url;
@@ -30,7 +32,7 @@ export const ItemActions = ({ item, ...props }: Props) => {
const openTrailer = useCallback(async () => {
if (!trailerLink) {
Alert.alert("No trailer available");
Alert.alert(t("common.no_trailer_available"));
return;
}
@@ -39,7 +41,7 @@ export const ItemActions = ({ item, ...props }: Props) => {
} catch (err) {
console.error("Failed to open trailer link:", err);
}
}, [trailerLink]);
}, [trailerLink, t]);
return (
<View className='' {...props}>

View File

@@ -41,10 +41,10 @@ export const OtherSettings: React.FC = () => {
if (settings?.autoDownload === true && !registered) {
registerBackgroundFetchAsync();
toast.success("Background downloads enabled");
toast.success(t("home.settings.toasts.background_downloads_enabled"));
} else if (settings?.autoDownload === false && registered) {
unregisterBackgroundFetchAsync();
toast.info("Background downloads disabled");
toast.info(t("home.settings.toasts.background_downloads_disabled"));
} else if (settings?.autoDownload === true && registered) {
// Don't to anything
} else if (settings?.autoDownload === false && !registered) {