chore: update dependencies and refactor config plugin imports (#993)

This commit is contained in:
Gauvain
2025-08-29 22:06:50 +02:00
committed by GitHub
parent f54da12fdb
commit a68d8500a6
35 changed files with 591 additions and 302 deletions

View File

@@ -90,7 +90,12 @@ export const DownloadItems: React.FC<DownloadProps> = ({
bottomSheetModalRef.current?.present();
}, []);
const handleSheetChanges = useCallback((_index: number) => {}, []);
const handleSheetChanges = useCallback((index: number) => {
// Ensure modal is fully dismissed when index is -1
if (index === -1) {
// Modal is fully closed
}
}, []);
const closeModal = useCallback(() => {
bottomSheetModalRef.current?.dismiss();
@@ -245,14 +250,19 @@ export const DownloadItems: React.FC<DownloadProps> = ({
],
);
const acceptDownloadOptions = useCallback(() => {
const acceptDownloadOptions = useCallback(async () => {
if (userCanDownload === true) {
if (itemsToDownload.some((i) => !i.Id)) {
throw new Error("No item id");
}
closeModal();
initiateDownload(...itemsToDownload);
// Ensure modal is dismissed before starting download
await closeModal();
// Small delay to ensure modal is fully dismissed
setTimeout(() => {
initiateDownload(...itemsToDownload);
}, 100);
} else {
toast.error(
t("home.downloads.toasts.you_are_not_allowed_to_download_files"),
@@ -326,7 +336,15 @@ export const DownloadItems: React.FC<DownloadProps> = ({
backgroundColor: "#171717",
}}
onChange={handleSheetChanges}
onDismiss={() => {
// Ensure any pending state is cleared when modal is dismissed
}}
backdropComponent={renderBackdrop}
enablePanDownToClose
enableDismissOnClose
android_keyboardInputMode='adjustResize'
keyboardBehavior='interactive'
keyboardBlurBehavior='restore'
>
<BottomSheetView>
<View className='flex flex-col space-y-4 px-4 pb-8 pt-2'>

View File

@@ -176,8 +176,8 @@ export const ItemContent: React.FC<ItemContentProps> = React.memo(
style={{
height: 130,
width: "100%",
resizeMode: "contain",
}}
contentFit='contain'
onLoad={() => setLoadingLogo(false)}
onError={() => setLoadingLogo(false)}
/>

View File

@@ -125,6 +125,34 @@ export const PlayButton: React.FC<Props> = ({
// Check if user wants H265 for Chromecast
const enableH265 = settings.enableH265ForChromecast;
// Validate required parameters before calling getStreamUrl
if (!api) {
console.warn("API not available for Chromecast streaming");
Alert.alert(
t("player.client_error"),
t("player.missing_parameters"),
);
return;
}
if (!user?.Id) {
console.warn(
"User not authenticated for Chromecast streaming",
);
Alert.alert(
t("player.client_error"),
t("player.missing_parameters"),
);
return;
}
if (!item?.Id) {
console.warn("Item not available for Chromecast streaming");
Alert.alert(
t("player.client_error"),
t("player.missing_parameters"),
);
return;
}
// Get a new URL with the Chromecast device profile
try {
const data = await getStreamUrl({
@@ -132,7 +160,7 @@ export const PlayButton: React.FC<Props> = ({
item,
deviceProfile: enableH265 ? chromecasth265 : chromecast,
startTimeTicks: item?.UserData?.PlaybackPositionTicks!,
userId: user?.Id,
userId: user.Id,
audioStreamIndex: selectedOptions.audioIndex,
maxStreamingBitrate: selectedOptions.bitrate?.value,
mediaSourceId: selectedOptions.mediaSource?.Id,

View File

@@ -1,5 +1,5 @@
import { Ionicons } from "@expo/vector-icons";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { useQueryClient } from "@tanstack/react-query";
import { Image } from "expo-image";
import { useRouter } from "expo-router";
import { t } from "i18next";
@@ -9,7 +9,6 @@ import {
TouchableOpacity,
type TouchableOpacityProps,
View,
type ViewProps,
} from "react-native";
import { toast } from "sonner-native";
import { Text } from "@/components/common/Text";
@@ -19,13 +18,13 @@ import { storage } from "@/utils/mmkv";
import { formatTimeString } from "@/utils/time";
import { Button } from "../Button";
interface Props extends ViewProps {}
const bytesToMB = (bytes: number) => {
return bytes / 1024 / 1024;
};
export const ActiveDownloads: React.FC<Props> = ({ ...props }) => {
interface ActiveDownloadsProps extends TouchableOpacityProps {}
export default function ActiveDownloads({ ...props }: ActiveDownloadsProps) {
const { processes } = useDownload();
if (processes?.length === 0)
return (
@@ -51,31 +50,48 @@ export const ActiveDownloads: React.FC<Props> = ({ ...props }) => {
</View>
</View>
);
};
}
interface DownloadCardProps extends TouchableOpacityProps {
process: JobStatus;
}
const DownloadCard = ({ process, ...props }: DownloadCardProps) => {
const { startDownload, removeProcess } = useDownload();
const { startDownload, pauseDownload, resumeDownload, removeProcess } =
useDownload();
const router = useRouter();
const queryClient = useQueryClient();
const cancelJobMutation = useMutation({
mutationFn: async (id: string) => {
if (!process) throw new Error("No active download");
removeProcess(id);
},
onSuccess: () => {
toast.success(t("home.downloads.toasts.download_cancelled"));
const handlePause = async (id: string) => {
try {
await pauseDownload(id);
toast.success(t("home.downloads.toasts.download_paused"));
} catch (error) {
console.error("Error pausing download:", error);
toast.error(t("home.downloads.toasts.could_not_pause_download"));
}
};
const handleResume = async (id: string) => {
try {
await resumeDownload(id);
toast.success(t("home.downloads.toasts.download_resumed"));
} catch (error) {
console.error("Error resuming download:", error);
toast.error(t("home.downloads.toasts.could_not_resume_download"));
}
};
const handleDelete = async (id: string) => {
try {
await removeProcess(id);
toast.success(t("home.downloads.toasts.download_deleted"));
queryClient.invalidateQueries({ queryKey: ["downloads"] });
},
onError: (e) => {
console.error(e);
toast.error(t("home.downloads.toasts.could_not_cancel_download"));
},
});
} catch (error) {
console.error("Error deleting download:", error);
toast.error(t("home.downloads.toasts.could_not_delete_download"));
}
};
const eta = (p: JobStatus) => {
if (!p.speed || p.speed <= 0 || !p.estimatedTotalSizeBytes) return null;
@@ -121,8 +137,8 @@ const DownloadCard = ({ process, ...props }: DownloadCardProps) => {
style={{
width: "100%",
height: "100%",
resizeMode: "cover",
}}
contentFit='cover'
/>
</View>
)}
@@ -154,17 +170,30 @@ const DownloadCard = ({ process, ...props }: DownloadCardProps) => {
<Text className='text-xs capitalize'>{process.status}</Text>
</View>
</View>
<TouchableOpacity
disabled={cancelJobMutation.isPending}
onPress={() => cancelJobMutation.mutate(process.id)}
className='ml-auto p-2 rounded-full'
>
{cancelJobMutation.isPending ? (
<ActivityIndicator size='small' color='white' />
) : (
<Ionicons name='close' size={24} color='red' />
<View className='ml-auto flex flex-row items-center space-x-2'>
{process.status === "downloading" && (
<TouchableOpacity
onPress={() => handlePause(process.id)}
className='p-2 rounded-full bg-yellow-600'
>
<Ionicons name='pause' size={20} color='white' />
</TouchableOpacity>
)}
</TouchableOpacity>
{process.status === "paused" && (
<TouchableOpacity
onPress={() => handleResume(process.id)}
className='p-2 rounded-full bg-green-600'
>
<Ionicons name='play' size={20} color='white' />
</TouchableOpacity>
)}
<TouchableOpacity
onPress={() => handleDelete(process.id)}
className='p-2 rounded-full bg-red-600'
>
<Ionicons name='close' size={20} color='white' />
</TouchableOpacity>
</View>
</View>
{process.status === "completed" && (
<View className='flex flex-row mt-4 space-x-4'>

View File

@@ -77,8 +77,8 @@ export const MovieCard: React.FC<MovieCardProps> = ({ item }) => {
style={{
width: "100%",
height: "100%",
resizeMode: "cover",
}}
contentFit='cover'
/>
<ProgressBar item={item} />
</View>

View File

@@ -52,8 +52,8 @@ export const SeriesCard: React.FC<{ items: BaseItemDto[] }> = ({ items }) => {
style={{
width: "100%",
height: "100%",
resizeMode: "cover",
}}
contentFit='cover'
/>
<View className='bg-purple-600 rounded-full h-6 w-6 flex items-center justify-center absolute bottom-1 right-1'>
<Text className='text-xs font-bold'>{items.length}</Text>

View File

@@ -1,10 +1,11 @@
import type { Api } from "@jellyfin/sdk";
import type { BaseItemKind } from "@jellyfin/sdk/lib/generated-client";
import { getItemsApi } from "@jellyfin/sdk/lib/utils/api";
import { Image } from "expo-image";
import { t } from "i18next";
import { useAtom } from "jotai";
import { useCallback, useEffect, useState } from "react";
import { Image, Text, View } from "react-native";
import { Text, View } from "react-native";
// PNG ASSET
import heart from "@/assets/icons/heart.fill.png";
import { Colors } from "@/constants/Colors";
@@ -125,7 +126,8 @@ export const Favorites = () => {
<View className='flex-1 items-center justify-center py-12'>
<Image
className={"w-10 h-10 mb-4"}
style={{ tintColor: Colors.primary, resizeMode: "contain" }}
style={{ tintColor: Colors.primary }}
contentFit='contain'
source={heart}
/>
<Text className='text-xl font-semibold text-white mb-2'>

View File

@@ -200,8 +200,8 @@ const RenderItem: React.FC<{ item: BaseItemDto }> = ({ item }) => {
style={{
width: "100%",
height: "100%",
resizeMode: "contain",
}}
contentFit='contain'
/>
</View>
</View>

View File

@@ -1,5 +1,6 @@
import { Ionicons } from "@expo/vector-icons";
import { useRouter } from "expo-router";
import * as TaskManager from "expo-task-manager";
import { TFunction } from "i18next";
import type React from "react";
import { useEffect, useMemo } from "react";
@@ -20,11 +21,6 @@ import { Text } from "../common/Text";
import { ListGroup } from "../list/ListGroup";
import { ListItem } from "../list/ListItem";
const BackgroundFetch = !Platform.isTV
? require("expo-background-fetch")
: null;
const TaskManager = !Platform.isTV ? require("expo-task-manager") : null;
export const OtherSettings: React.FC = () => {
const router = useRouter();
const [settings, updateSettings, pluginSettings] = useSettings();
@@ -35,10 +31,8 @@ export const OtherSettings: React.FC = () => {
* Background task
*******************/
const checkStatusAsync = async () => {
if (Platform.isTV) return;
await BackgroundFetch.getStatusAsync();
return await TaskManager.isTaskRegisteredAsync(BACKGROUND_FETCH_TASK);
if (Platform.isTV) return false;
return TaskManager.isTaskRegisteredAsync(BACKGROUND_FETCH_TASK);
};
useEffect(() => {

View File

@@ -1,3 +1,4 @@
import { Api } from "@jellyfin/sdk";
import type {
BaseItemDto,
MediaSourceInfo,
@@ -28,6 +29,7 @@ import { useIntroSkipper } from "@/hooks/useIntroSkipper";
import { usePlaybackManager } from "@/hooks/usePlaybackManager";
import { useTrickplay } from "@/hooks/useTrickplay";
import type { TrackInfo, VlcPlayerViewRef } from "@/modules/VlcPlayer.types";
import { DownloadedItem } from "@/providers/Downloads/types";
import { useSettings } from "@/utils/atoms/settings";
import { getDefaultPlaySettings } from "@/utils/jellyfin/getDefaultPlaySettings";
import { ticksToMs } from "@/utils/time";
@@ -78,6 +80,8 @@ interface Props {
setAspectRatio?: Dispatch<SetStateAction<AspectRatio>>;
setScaleFactor?: Dispatch<SetStateAction<ScaleFactor>>;
isVlc?: boolean;
api?: Api | null;
downloadedFiles?: DownloadedItem[];
}
export const Controls: FC<Props> = ({
@@ -109,8 +113,10 @@ export const Controls: FC<Props> = ({
setScaleFactor,
offline = false,
isVlc = false,
api = null,
downloadedFiles = undefined,
}) => {
const [settings, updateSettings] = useSettings();
const [settings, updateSettings] = useSettings(api);
const router = useRouter();
const lightHapticFeedback = useHaptic("light");
@@ -321,6 +327,8 @@ export const Controls: FC<Props> = ({
play,
isVlc,
offline,
api,
downloadedFiles,
);
const { showSkipCreditButton, skipCredit } = useCreditSkipper(
@@ -330,6 +338,8 @@ export const Controls: FC<Props> = ({
play,
isVlc,
offline,
api,
downloadedFiles,
);
const goToItemCommon = useCallback(

View File

@@ -71,7 +71,6 @@ export const TrickplayBubble: FC<TrickplayBubbleProps> = ({
{ translateX: -x * tileWidth },
{ translateY: -y * tileHeight },
],
resizeMode: "cover",
}}
source={{ uri: url }}
contentFit='cover'