fix: bump biome and fix error (#864)
Some checks failed
🤖 Android APK Build / 🏗️ Build Android APK (push) Has been cancelled
🤖 iOS IPA Build / 🏗️ Build iOS IPA (push) Has been cancelled
🔒 Lockfile Consistency Check / 🔍 Check bun.lock and package.json consistency (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
🕒 Handle Stale Issues / 🗑️ Cleanup Stale Issues (push) Has been cancelled

This commit is contained in:
Gauvain
2025-07-21 09:44:24 +02:00
committed by GitHub
parent 3b2a6bd40a
commit 5f39622ad6
202 changed files with 1858 additions and 1954 deletions

View File

@@ -1,8 +1,8 @@
import { RoundButton } from "@/components/RoundButton";
import { useFavorite } from "@/hooks/useFavorite";
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client";
import type { FC } from "react";
import { View, type ViewProps } from "react-native";
import { RoundButton } from "@/components/RoundButton";
import { useFavorite } from "@/hooks/useFavorite";
interface Props extends ViewProps {
item: BaseItemDto;

View File

@@ -1,7 +1,9 @@
import type { MediaSourceInfo } from "@jellyfin/sdk/lib/generated-client/models";
import { useMemo } from "react";
import { Platform, TouchableOpacity, View } from "react-native";
const DropdownMenu = !Platform.isTV ? require("zeego/dropdown-menu") : null;
import { useTranslation } from "react-i18next";
import { Text } from "./common/Text";
@@ -17,7 +19,8 @@ export const AudioTrackSelector: React.FC<Props> = ({
selected,
...props
}) => {
if (Platform.isTV) return null;
const isTv = Platform.isTV;
const audioStreams = useMemo(
() => source?.MediaStreams?.filter((x) => x.Type === "Audio"),
[source],
@@ -30,6 +33,8 @@ export const AudioTrackSelector: React.FC<Props> = ({
const { t } = useTranslation();
if (isTv) return null;
return (
<View
className='flex shrink'

View File

@@ -1,5 +1,7 @@
import { Platform, TouchableOpacity, View } from "react-native";
const DropdownMenu = !Platform.isTV ? require("zeego/dropdown-menu") : null;
import { useMemo } from "react";
import { useTranslation } from "react-i18next";
import { Text } from "./common/Text";
@@ -58,23 +60,26 @@ export const BitrateSelector: React.FC<Props> = ({
inverted,
...props
}) => {
if (Platform.isTV) return null;
const isTv = Platform.isTV;
const sorted = useMemo(() => {
if (inverted)
return BITRATES.sort(
return BITRATES.slice().sort(
(a, b) =>
(a.value || Number.POSITIVE_INFINITY) -
(b.value || Number.POSITIVE_INFINITY),
);
return BITRATES.sort(
return BITRATES.slice().sort(
(a, b) =>
(b.value || Number.POSITIVE_INFINITY) -
(a.value || Number.POSITIVE_INFINITY),
);
}, []);
}, [inverted]);
const { t } = useTranslation();
if (isTv) return null;
return (
<View
className='flex shrink'

View File

@@ -1,7 +1,7 @@
import { useHaptic } from "@/hooks/useHaptic";
import type React from "react";
import { type PropsWithChildren, type ReactNode, useMemo } from "react";
import { Platform, Text, TouchableOpacity, View } from "react-native";
import { Text, TouchableOpacity, View } from "react-native";
import { useHaptic } from "@/hooks/useHaptic";
import { Loader } from "./Loader";
export interface ButtonProps

View File

@@ -1,6 +1,6 @@
import { Feather } from "@expo/vector-icons";
import React, { useCallback, useEffect } from "react";
import { Platform, TouchableOpacity, type ViewProps } from "react-native";
import { useCallback, useEffect } from "react";
import { Platform, type ViewProps } from "react-native";
import GoogleCast, {
CastButton,
CastContext,
@@ -44,11 +44,7 @@ export function Chromecast({
// Android requires the cast button to be present for startDiscovery to work
const AndroidCastButton = useCallback(
() =>
Platform.OS === "android" ? (
<CastButton tintColor='transparent' />
) : (
<></>
),
Platform.OS === "android" ? <CastButton tintColor='transparent' /> : null,
[Platform.OS],
);

View File

@@ -1,11 +1,11 @@
import { apiAtom } from "@/providers/JellyfinProvider";
import { Ionicons } from "@expo/vector-icons";
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
import { Image } from "expo-image";
import { useAtomValue } from "jotai";
import { useMemo } from "react";
import type React from "react";
import { useMemo } from "react";
import { View } from "react-native";
import { apiAtom } from "@/providers/JellyfinProvider";
import { WatchedIndicator } from "./WatchedIndicator";
type ContinueWatchingPosterProps = {

View File

@@ -1,11 +1,3 @@
import { useDownload } from "@/providers/DownloadProvider";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { queueActions, queueAtom } from "@/utils/atoms/queue";
import { DownloadMethod, useSettings } from "@/utils/atoms/settings";
import { getDefaultPlaySettings } from "@/utils/jellyfin/getDefaultPlaySettings";
import { getStreamUrl } from "@/utils/jellyfin/media/getStreamUrl";
import { saveDownloadItemInfoToDiskTmp } from "@/utils/optimize-server";
import download from "@/utils/profiles/download";
import Ionicons from "@expo/vector-icons/Ionicons";
import {
BottomSheetBackdrop,
@@ -24,15 +16,23 @@ import type React from "react";
import { useCallback, useMemo, useRef, useState } from "react";
import { Alert, Platform, View, type ViewProps } from "react-native";
import { toast } from "sonner-native";
import { useDownload } from "@/providers/DownloadProvider";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { queueAtom } from "@/utils/atoms/queue";
import { DownloadMethod, useSettings } from "@/utils/atoms/settings";
import { getDefaultPlaySettings } from "@/utils/jellyfin/getDefaultPlaySettings";
import { getStreamUrl } from "@/utils/jellyfin/media/getStreamUrl";
import { saveDownloadItemInfoToDiskTmp } from "@/utils/optimize-server";
import download from "@/utils/profiles/download";
import { AudioTrackSelector } from "./AudioTrackSelector";
import { type Bitrate, BitrateSelector } from "./BitrateSelector";
import { Button } from "./Button";
import { Text } from "./common/Text";
import { Loader } from "./Loader";
import { MediaSourceSelector } from "./MediaSourceSelector";
import ProgressCircle from "./ProgressCircle";
import { RoundButton } from "./RoundButton";
import { SubtitleTrackSelector } from "./SubtitleTrackSelector";
import { Text } from "./common/Text";
interface DownloadProps extends ViewProps {
items: BaseItemDto[];
@@ -88,7 +88,7 @@ export const DownloadItems: React.FC<DownloadProps> = ({
bottomSheetModalRef.current?.present();
}, []);
const handleSheetChanges = useCallback((index: number) => {}, []);
const handleSheetChanges = useCallback((_index: number) => {}, []);
const closeModal = useCallback(() => {
bottomSheetModalRef.current?.dismiss();

View File

@@ -1,4 +1,3 @@
import { tc } from "@/utils/textTools";
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
import type React from "react";
import { View } from "react-native";

View File

@@ -1,25 +1,3 @@
import { AudioTrackSelector } from "@/components/AudioTrackSelector";
import { type Bitrate, BitrateSelector } from "@/components/BitrateSelector";
import { DownloadSingleItem } from "@/components/DownloadItem";
import { OverviewText } from "@/components/OverviewText";
import { ParallaxScrollView } from "@/components/ParallaxPage";
// const PlayButton = !Platform.isTV ? require("@/components/PlayButton") : null;
import { PlayButton } from "@/components/PlayButton";
import { PlayedStatus } from "@/components/PlayedStatus";
import { SimilarItems } from "@/components/SimilarItems";
import { SubtitleTrackSelector } from "@/components/SubtitleTrackSelector";
import { ItemImage } from "@/components/common/ItemImage";
import { CastAndCrew } from "@/components/series/CastAndCrew";
import { CurrentSeries } from "@/components/series/CurrentSeries";
import { SeasonEpisodesCarousel } from "@/components/series/SeasonEpisodesCarousel";
import useDefaultPlaySettings from "@/hooks/useDefaultPlaySettings";
import { useImageColors } from "@/hooks/useImageColors";
import { useOrientation } from "@/hooks/useOrientation";
import * as ScreenOrientation from "@/packages/expo-screen-orientation";
import { apiAtom } from "@/providers/JellyfinProvider";
import { userAtom } from "@/providers/JellyfinProvider";
import { useSettings } from "@/utils/atoms/settings";
import { getLogoImageUrlById } from "@/utils/jellyfin/image/getLogoImageUrlById";
import type {
BaseItemDto,
MediaSourceInfo,
@@ -30,12 +8,34 @@ import { useAtom } from "jotai";
import React, { useEffect, useMemo, useState } from "react";
import { Platform, View } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { AudioTrackSelector } from "@/components/AudioTrackSelector";
import { type Bitrate, BitrateSelector } from "@/components/BitrateSelector";
import { ItemImage } from "@/components/common/ItemImage";
import { DownloadSingleItem } from "@/components/DownloadItem";
import { OverviewText } from "@/components/OverviewText";
import { ParallaxScrollView } from "@/components/ParallaxPage";
// const PlayButton = !Platform.isTV ? require("@/components/PlayButton") : null;
import { PlayButton } from "@/components/PlayButton";
import { PlayedStatus } from "@/components/PlayedStatus";
import { SimilarItems } from "@/components/SimilarItems";
import { SubtitleTrackSelector } from "@/components/SubtitleTrackSelector";
import { CastAndCrew } from "@/components/series/CastAndCrew";
import { CurrentSeries } from "@/components/series/CurrentSeries";
import { SeasonEpisodesCarousel } from "@/components/series/SeasonEpisodesCarousel";
import useDefaultPlaySettings from "@/hooks/useDefaultPlaySettings";
import { useImageColors } from "@/hooks/useImageColors";
import { useOrientation } from "@/hooks/useOrientation";
import * as ScreenOrientation from "@/packages/expo-screen-orientation";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { useSettings } from "@/utils/atoms/settings";
import { getLogoImageUrlById } from "@/utils/jellyfin/image/getLogoImageUrlById";
import { AddToFavorites } from "./AddToFavorites";
import { ItemHeader } from "./ItemHeader";
import { ItemTechnicalDetails } from "./ItemTechnicalDetails";
import { MediaSourceSelector } from "./MediaSourceSelector";
import { MoreMoviesWithActor } from "./MoreMoviesWithActor";
import { PlayInRemoteSessionButton } from "./PlayInRemoteSession";
const Chromecast = !Platform.isTV ? require("./Chromecast") : null;
export type SelectedOptions = {
@@ -85,8 +85,8 @@ export const ItemContent: React.FC<{ item: BaseItemDto }> = React.memo(
defaultMediaSource,
]);
if (!Platform.isTV) {
useEffect(() => {
useEffect(() => {
if (!Platform.isTV) {
navigation.setOptions({
headerRight: () =>
item && (
@@ -112,8 +112,8 @@ export const ItemContent: React.FC<{ item: BaseItemDto }> = React.memo(
</View>
),
});
}, [item]);
}
}
}, [item, navigation, user]);
useEffect(() => {
if (orientation !== ScreenOrientation.OrientationLock.PORTRAIT_UP)
@@ -122,12 +122,16 @@ export const ItemContent: React.FC<{ item: BaseItemDto }> = React.memo(
else setHeaderHeight(350);
}, [item.Type, orientation]);
const logoUrl = useMemo(() => getLogoImageUrlById({ api, item }), [item]);
const logoUrl = useMemo(
() => getLogoImageUrlById({ api, item }),
[api, item],
);
const loading = useMemo(() => {
return Boolean(logoUrl && loadingLogo);
}, [loadingLogo, logoUrl]);
if (!selectedOptions) return null;
if (!selectedOptions) return <View />;
return (
<View
@@ -168,7 +172,9 @@ export const ItemContent: React.FC<{ item: BaseItemDto }> = React.memo(
onLoad={() => setLoadingLogo(false)}
onError={() => setLoadingLogo(false)}
/>
) : null
) : (
<View />
)
}
>
<View className='flex flex-col bg-transparent shrink'>

View File

@@ -2,8 +2,8 @@ import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
import type React from "react";
import { View, type ViewProps } from "react-native";
import { GenreTags } from "./GenreTags";
import { Ratings } from "./Ratings";
import { MoviesTitleHeader } from "./movies/MoviesTitleHeader";
import { Ratings } from "./Ratings";
import { EpisodeTitleHeader } from "./series/EpisodeTitleHeader";
import { ItemActions } from "./series/SeriesActions";

View File

@@ -1,11 +1,9 @@
import { formatBitrate } from "@/utils/bitrate";
import { Ionicons } from "@expo/vector-icons";
import {
BottomSheetBackdrop,
type BottomSheetBackdropProps,
BottomSheetModal,
BottomSheetScrollView,
BottomSheetView,
} from "@gorhom/bottom-sheet";
import type {
MediaSourceInfo,
@@ -15,15 +13,15 @@ import type React from "react";
import { useMemo, useRef } from "react";
import { useTranslation } from "react-i18next";
import { TouchableOpacity, View } from "react-native";
import { formatBitrate } from "@/utils/bitrate";
import { Badge } from "./Badge";
import { Button } from "./Button";
import { Text } from "./common/Text";
interface Props {
source?: MediaSourceInfo;
}
export const ItemTechnicalDetails: React.FC<Props> = ({ source, ...props }) => {
export const ItemTechnicalDetails: React.FC<Props> = ({ source }) => {
const bottomSheetModalRef = useRef<BottomSheetModal>(null);
const { t } = useTranslation();
@@ -55,7 +53,7 @@ export const ItemTechnicalDetails: React.FC<Props> = ({ source, ...props }) => {
>
<BottomSheetScrollView>
<View className='flex flex-col space-y-2 p-4 mb-4'>
<View className=''>
<View>
<Text className='text-lg font-bold mb-4'>
{t("item_card.video")}
</Text>
@@ -64,7 +62,7 @@ export const ItemTechnicalDetails: React.FC<Props> = ({ source, ...props }) => {
</View>
</View>
<View className=''>
<View>
<Text className='text-lg font-bold mb-2'>
{t("item_card.audio")}
</Text>
@@ -77,7 +75,7 @@ export const ItemTechnicalDetails: React.FC<Props> = ({ source, ...props }) => {
/>
</View>
<View className=''>
<View>
<Text className='text-lg font-bold mb-2'>
{t("item_card.subtitles")}
</Text>
@@ -103,7 +101,7 @@ const SubtitleStreamInfo = ({
}) => {
return (
<View className='flex flex-col'>
{subtitleStreams.map((stream, index) => (
{subtitleStreams.map((stream, _index) => (
<View key={stream.Index} className='flex flex-col'>
<Text className='text-xs mb-3 text-neutral-400'>
{stream.DisplayTitle}
@@ -177,15 +175,13 @@ const AudioStreamInfo = ({ audioStreams }: { audioStreams: MediaStream[] }) => {
};
const VideoStreamInfo = ({ source }: { source?: MediaSourceInfo }) => {
if (!source) return null;
const videoStream = useMemo(() => {
return source.MediaStreams?.find(
(stream) => stream.Type === "Video",
) as MediaStream;
}, [source.MediaStreams]);
return source?.MediaStreams?.find((stream) => stream.Type === "Video") as
| MediaStream
| undefined;
}, [source?.MediaStreams]);
if (!videoStream) return null;
if (!source || !videoStream) return null;
return (
<View className='flex-row flex-wrap gap-2'>
@@ -223,7 +219,11 @@ const VideoStreamInfo = ({ source }: { source?: MediaSourceInfo }) => {
<Badge
variant='gray'
iconLeft={<Ionicons name='play-outline' size={16} color='white' />}
text={`${videoStream.AverageFrameRate?.toFixed(0)} fps`}
text={
videoStream.AverageFrameRate != null
? `${videoStream.AverageFrameRate.toFixed(0)} fps`
: ""
}
/>
</View>
);

View File

@@ -1,7 +1,7 @@
import { useJellyfinDiscovery } from "@/hooks/useJellyfinDiscovery";
import type React from "react";
import { useTranslation } from "react-i18next";
import { Text, TouchableOpacity, View } from "react-native";
import { Text, View } from "react-native";
import { useJellyfinDiscovery } from "@/hooks/useJellyfinDiscovery";
import { Button } from "./Button";
import { ListGroup } from "./list/ListGroup";
import { ListItem } from "./list/ListItem";

View File

@@ -2,7 +2,6 @@ import {
ActivityIndicator,
type ActivityIndicatorProps,
Platform,
View,
} from "react-native";
interface Props extends ActivityIndicatorProps {}

View File

@@ -4,7 +4,9 @@ import type {
} from "@jellyfin/sdk/lib/generated-client/models";
import { useMemo } from "react";
import { Platform, TouchableOpacity, View } from "react-native";
const DropdownMenu = !Platform.isTV ? require("zeego/dropdown-menu") : null;
import { useTranslation } from "react-i18next";
import { Text } from "./common/Text";
@@ -20,7 +22,8 @@ export const MediaSourceSelector: React.FC<Props> = ({
selected,
...props
}) => {
if (Platform.isTV) return null;
const isTv = Platform.isTV;
const selectedName = useMemo(
() =>
item.MediaSources?.find((x) => x.Id === selected?.Id)?.MediaStreams?.find(
@@ -52,6 +55,8 @@ export const MediaSourceSelector: React.FC<Props> = ({
return name?.replace(commonPrefix, "").toLowerCase();
};
if (isTv) return null;
return (
<View
className='flex shrink'

View File

@@ -1,10 +1,3 @@
import { ItemCardText } from "@/components/ItemCardText";
import { HorizontalScroll } from "@/components/common/HorrizontalScroll";
import { Text } from "@/components/common/Text";
import { TouchableItemRouter } from "@/components/common/TouchableItemRouter";
import MoviePoster from "@/components/posters/MoviePoster";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { getUserItemData } from "@/utils/jellyfin/user-library/getUserItemData";
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
import { getItemsApi } from "@jellyfin/sdk/lib/utils/api";
import { useQuery } from "@tanstack/react-query";
@@ -12,6 +5,13 @@ import { useAtom } from "jotai";
import type React from "react";
import { useTranslation } from "react-i18next";
import { View, type ViewProps } from "react-native";
import { HorizontalScroll } from "@/components/common/HorrizontalScroll";
import { Text } from "@/components/common/Text";
import { TouchableItemRouter } from "@/components/common/TouchableItemRouter";
import { ItemCardText } from "@/components/ItemCardText";
import MoviePoster from "@/components/posters/MoviePoster";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { getUserItemData } from "@/utils/jellyfin/user-library/getUserItemData";
interface Props extends ViewProps {
actorId: string;

View File

@@ -1,8 +1,8 @@
import { Text } from "@/components/common/Text";
import { tc } from "@/utils/textTools";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { TouchableOpacity, View, type ViewProps } from "react-native";
import { Text } from "@/components/common/Text";
import { tc } from "@/utils/textTools";
interface Props extends ViewProps {
text?: string | null;

View File

@@ -1,11 +1,6 @@
import { LinearGradient } from "expo-linear-gradient";
import type { PropsWithChildren, ReactElement } from "react";
import {
type NativeScrollEvent,
NativeSyntheticEvent,
View,
type ViewProps,
} from "react-native";
import { type NativeScrollEvent, View, type ViewProps } from "react-native";
import Animated, {
interpolate,
useAnimatedRef,

View File

@@ -1,6 +1,7 @@
import { BlurView } from "expo-blur";
import type React from "react";
import { Platform, View, type ViewProps } from "react-native";
interface Props extends ViewProps {
blurAmount?: number;
blurType?: "light" | "dark" | "xlight";

View File

@@ -1,13 +1,3 @@
import { useHaptic } from "@/hooks/useHaptic";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { itemThemeColorAtom } from "@/utils/atoms/primaryColor";
import { useSettings } from "@/utils/atoms/settings";
import { getParentBackdropImageUrl } from "@/utils/jellyfin/image/getParentBackdropImageUrl";
import { getPrimaryImageUrl } from "@/utils/jellyfin/image/getPrimaryImageUrl";
import { getStreamUrl } from "@/utils/jellyfin/media/getStreamUrl";
import { chromecast } from "@/utils/profiles/chromecast";
import { chromecasth265 } from "@/utils/profiles/chromecasth265";
import { runtimeTicksToMinutes } from "@/utils/time";
import { useActionSheet } from "@expo/react-native-action-sheet";
import { Feather, Ionicons, MaterialCommunityIcons } from "@expo/vector-icons";
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client";
@@ -15,7 +5,6 @@ import { useRouter } from "expo-router";
import { useAtom, useAtomValue } from "jotai";
import { useCallback, useEffect } from "react";
import { useTranslation } from "react-i18next";
import { Platform, Pressable } from "react-native";
import { Alert, TouchableOpacity, View } from "react-native";
import CastContext, {
CastButton,
@@ -33,6 +22,16 @@ import Animated, {
useSharedValue,
withTiming,
} from "react-native-reanimated";
import { useHaptic } from "@/hooks/useHaptic";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { itemThemeColorAtom } from "@/utils/atoms/primaryColor";
import { useSettings } from "@/utils/atoms/settings";
import { getParentBackdropImageUrl } from "@/utils/jellyfin/image/getParentBackdropImageUrl";
import { getPrimaryImageUrl } from "@/utils/jellyfin/image/getPrimaryImageUrl";
import { getStreamUrl } from "@/utils/jellyfin/media/getStreamUrl";
import { chromecast } from "@/utils/profiles/chromecast";
import { chromecasth265 } from "@/utils/profiles/chromecasth265";
import { runtimeTicksToMinutes } from "@/utils/time";
import type { Button } from "./Button";
import type { SelectedOptions } from "./ItemContent";

View File

@@ -1,17 +1,11 @@
import { useHaptic } from "@/hooks/useHaptic";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { itemThemeColorAtom } from "@/utils/atoms/primaryColor";
import { useSettings } from "@/utils/atoms/settings";
import { runtimeTicksToMinutes } from "@/utils/time";
import { useActionSheet } from "@expo/react-native-action-sheet";
import { Feather, Ionicons, MaterialCommunityIcons } from "@expo/vector-icons";
import { Ionicons, MaterialCommunityIcons } from "@expo/vector-icons";
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client";
import { useRouter } from "expo-router";
import { useAtom, useAtomValue } from "jotai";
import { useCallback, useEffect } from "react";
import { useTranslation } from "react-i18next";
import { Platform } from "react-native";
import { Alert, TouchableOpacity, View } from "react-native";
import { TouchableOpacity, View } from "react-native";
import Animated, {
Easing,
interpolate,
@@ -22,6 +16,11 @@ import Animated, {
useSharedValue,
withTiming,
} from "react-native-reanimated";
import { useHaptic } from "@/hooks/useHaptic";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { itemThemeColorAtom } from "@/utils/atoms/primaryColor";
import { useSettings } from "@/utils/atoms/settings";
import { runtimeTicksToMinutes } from "@/utils/time";
import type { Button } from "./Button";
import type { SelectedOptions } from "./ItemContent";
@@ -42,8 +41,8 @@ export const PlayButton: React.FC<Props> = ({
const { t } = useTranslation();
const [colorAtom] = useAtom(itemThemeColorAtom);
const api = useAtomValue(apiAtom);
const user = useAtomValue(userAtom);
const _api = useAtomValue(apiAtom);
const _user = useAtomValue(userAtom);
const router = useRouter();

View File

@@ -1,5 +1,3 @@
import { useAllSessions, type useSessionsProps } from "@/hooks/useSessions";
import { apiAtom } from "@/providers/JellyfinProvider";
import { Ionicons } from "@expo/vector-icons";
import {
type BaseItemDto,
@@ -15,9 +13,11 @@ import {
TouchableOpacity,
View,
} from "react-native";
import { useAllSessions, type useSessionsProps } from "@/hooks/useSessions";
import { apiAtom } from "@/providers/JellyfinProvider";
import { Text } from "./common/Text";
import { Loader } from "./Loader";
import { RoundButton } from "./RoundButton";
import { Text } from "./common/Text";
interface Props extends React.ComponentProps<typeof View> {
item: BaseItemDto;

View File

@@ -1,8 +1,8 @@
import { useMarkAsPlayed } from "@/hooks/useMarkAsPlayed";
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
import { useQueryClient } from "@tanstack/react-query";
import type React from "react";
import { View, type ViewProps } from "react-native";
import { useMarkAsPlayed } from "@/hooks/useMarkAsPlayed";
import { RoundButton } from "./RoundButton";
interface Props extends ViewProps {
@@ -13,7 +13,7 @@ interface Props extends ViewProps {
export const PlayedStatus: React.FC<Props> = ({ items, ...props }) => {
const queryClient = useQueryClient();
const invalidateQueries = () => {
const _invalidateQueries = () => {
items.forEach((item) => {
queryClient.invalidateQueries({
queryKey: ["item", item.Id],

View File

@@ -1,5 +1,4 @@
import type React from "react";
import { StyleSheet, View } from "react-native";
import { AnimatedCircularProgress } from "react-native-circular-progress";
type ProgressCircleProps = {

View File

@@ -1,3 +1,9 @@
import { Ionicons } from "@expo/vector-icons";
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
import { useQuery } from "@tanstack/react-query";
import { Image } from "expo-image";
import { useMemo } from "react";
import { View, type ViewProps } from "react-native";
import { useJellyseerr } from "@/hooks/useJellyseerr";
import { MediaType } from "@/utils/jellyseerr/server/constants/media";
import type { MovieDetails } from "@/utils/jellyseerr/server/models/Movie";
@@ -6,12 +12,6 @@ import type {
TvResult,
} from "@/utils/jellyseerr/server/models/Search";
import type { TvDetails } from "@/utils/jellyseerr/server/models/Tv";
import { Ionicons } from "@expo/vector-icons";
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
import { useQuery } from "@tanstack/react-query";
import { Image } from "expo-image";
import { useMemo } from "react";
import { View, type ViewProps } from "react-native";
import { Badge } from "./Badge";
interface Props extends ViewProps {

View File

@@ -1,4 +1,3 @@
import { useHaptic } from "@/hooks/useHaptic";
import { Ionicons } from "@expo/vector-icons";
import { BlurView } from "expo-blur";
import type { PropsWithChildren } from "react";
@@ -7,6 +6,7 @@ import {
TouchableOpacity,
type TouchableOpacityProps,
} from "react-native";
import { useHaptic } from "@/hooks/useHaptic";
interface Props extends TouchableOpacityProps {
onPress?: () => void;

View File

@@ -1,23 +1,16 @@
import MoviePoster from "@/components/posters/MoviePoster";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
import { getLibraryApi } from "@jellyfin/sdk/lib/utils/api";
import { useQuery } from "@tanstack/react-query";
import { router } from "expo-router";
import { useAtom } from "jotai";
import { useMemo } from "react";
import { useTranslation } from "react-i18next";
import {
ScrollView,
TouchableOpacity,
View,
type ViewProps,
} from "react-native";
import { ItemCardText } from "./ItemCardText";
import { Loader } from "./Loader";
import { View, type ViewProps } from "react-native";
import MoviePoster from "@/components/posters/MoviePoster";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { HorizontalScroll } from "./common/HorrizontalScroll";
import { Text } from "./common/Text";
import { TouchableItemRouter } from "./common/TouchableItemRouter";
import { ItemCardText } from "./ItemCardText";
interface SimilarItemsProps extends ViewProps {
itemId?: string | null;

View File

@@ -1,8 +1,10 @@
import { tc } from "@/utils/textTools";
import type { MediaSourceInfo } from "@jellyfin/sdk/lib/generated-client/models";
import { useMemo } from "react";
import { Platform, TouchableOpacity, View } from "react-native";
import { tc } from "@/utils/textTools";
const DropdownMenu = !Platform.isTV ? require("zeego/dropdown-menu") : null;
import { useTranslation } from "react-i18next";
import { Text } from "./common/Text";
@@ -18,7 +20,8 @@ export const SubtitleTrackSelector: React.FC<Props> = ({
selected,
...props
}) => {
if (Platform.isTV) return null;
const isTv = Platform.isTV;
const subtitleStreams = useMemo(() => {
return source?.MediaStreams?.filter((x) => x.Type === "Subtitle");
}, [source]);
@@ -28,10 +31,11 @@ export const SubtitleTrackSelector: React.FC<Props> = ({
[subtitleStreams, selected],
);
if (subtitleStreams?.length === 0) return null;
const { t } = useTranslation();
if (isTv) return null;
if (subtitleStreams?.length === 0) return null;
return (
<View
className='flex col shrink justify-start place-self-start items-start'

View File

@@ -1,4 +1,3 @@
import * as React from "react";
import renderer from "react-test-renderer";
import { ThemedText } from "../ThemedText";

View File

@@ -1,5 +1,5 @@
import { Text } from "@/components/common/Text";
import { View, type ViewProps } from "react-native";
import { Text } from "@/components/common/Text";
interface Props extends ViewProps {}

View File

@@ -1,7 +1,6 @@
import { useMemo } from "react";
import { StyleSheet, View, type ViewProps } from "react-native";
import { View, type ViewProps } from "react-native";
const getItemStyle = (index: number, numColumns: number) => {
const _getItemStyle = (index: number, numColumns: number) => {
const alignItems = (() => {
if (numColumns < 2 || index % numColumns === 0) return "flex-start";
if ((index + 1) % numColumns === 0) return "flex-end";

View File

@@ -1,13 +1,14 @@
const DropdownMenu = !Platform.isTV ? require("zeego/dropdown-menu") : null;
import { Text } from "@/components/common/Text";
import DisabledSetting from "@/components/settings/DisabledSetting";
import React, {
import {
type PropsWithChildren,
type ReactNode,
useEffect,
useState,
} from "react";
import { Platform, TouchableOpacity, View, type ViewProps } from "react-native";
import { Text } from "@/components/common/Text";
import DisabledSetting from "@/components/settings/DisabledSetting";
interface Props<T> {
data: T[];
@@ -33,14 +34,17 @@ const Dropdown = <T,>({
multiple = false,
...props
}: PropsWithChildren<Props<T> & ViewProps>) => {
if (Platform.isTV) return null;
const isTv = Platform.isTV;
const [selected, setSelected] = useState<T[]>();
useEffect(() => {
if (selected !== undefined) {
onSelected(...selected);
}
}, [selected]);
}, [selected, onSelected]);
if (isTv) return null;
return (
<DisabledSetting disabled={disabled === true} showText={false} {...props}>
@@ -58,7 +62,7 @@ const Dropdown = <T,>({
</TouchableOpacity>
</View>
) : (
<>{title}</>
title
)}
</DropdownMenu.Trigger>
<DropdownMenu.Content
@@ -71,7 +75,7 @@ const Dropdown = <T,>({
sideOffset={0}
>
<DropdownMenu.Label>{label}</DropdownMenu.Label>
{data.map((item, idx) =>
{data.map((item, _idx) =>
multiple ? (
<DropdownMenu.CheckboxItem
value={
@@ -80,7 +84,10 @@ const Dropdown = <T,>({
: "off"
}
key={keyExtractor(item)}
onValueChange={(next: "on" | "off", previous: "on" | "off") => {
onValueChange={(
next: "on" | "off",
_previous: "on" | "off",
) => {
setSelected((p) => {
const prev = p || [];
if (next === "on") {

View File

@@ -1,4 +1,3 @@
import { Text } from "@/components/common/Text";
import { Ionicons } from "@expo/vector-icons";
import { BlurView, type BlurViewProps } from "expo-blur";
import { useRouter } from "expo-router";
@@ -6,8 +5,6 @@ import {
Platform,
TouchableOpacity,
type TouchableOpacityProps,
View,
ViewProps,
} from "react-native";
interface Props extends BlurViewProps {

View File

@@ -1,4 +1,3 @@
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import type {
BaseItemDto,
BaseItemDtoQueryResult,
@@ -14,6 +13,7 @@ import Animated, {
useSharedValue,
withTiming,
} from "react-native-reanimated";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { Loader } from "../Loader";
import { Text } from "./Text";

View File

@@ -1,11 +1,11 @@
import { apiAtom } from "@/providers/JellyfinProvider";
import { getItemImage } from "@/utils/getItemImage";
import { Ionicons } from "@expo/vector-icons";
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
import { Image, type ImageProps } from "expo-image";
import { useAtom } from "jotai";
import { type FC, useMemo } from "react";
import { View, type ViewProps } from "react-native";
import { apiAtom } from "@/providers/JellyfinProvider";
import { getItemImage } from "@/utils/getItemImage";
interface Props extends ImageProps {
item: BaseItemDto;

View File

@@ -1,9 +1,13 @@
import { useRouter, useSegments } from "expo-router";
import type React from "react";
import { type PropsWithChildren, useCallback, useMemo } from "react";
import { TouchableOpacity, type TouchableOpacityProps } from "react-native";
import * as ContextMenu from "@/components/ContextMenu";
import { useJellyseerr } from "@/hooks/useJellyseerr";
import { MediaType } from "@/utils/jellyseerr/server/constants/media";
import {
Permission,
hasPermission,
Permission,
} from "@/utils/jellyseerr/server/lib/permissions";
import type { MovieDetails } from "@/utils/jellyseerr/server/models/Movie";
import type {
@@ -11,10 +15,6 @@ import type {
TvResult,
} from "@/utils/jellyseerr/server/models/Search";
import type { TvDetails } from "@/utils/jellyseerr/server/models/Tv";
import { useRouter, useSegments } from "expo-router";
import type React from "react";
import { type PropsWithChildren, useCallback, useMemo } from "react";
import { TouchableOpacity, type TouchableOpacityProps } from "react-native";
interface Props extends TouchableOpacityProps {
result?: MovieResult | TvResult | MovieDetails | TvDetails;
@@ -60,69 +60,67 @@ export const TouchableJellyseerrRouter: React.FC<PropsWithChildren<Props>> = ({
if (from === "(home)" || from === "(search)" || from === "(libraries)")
return (
<>
<ContextMenu.Root>
<ContextMenu.Trigger>
<TouchableOpacity
onPress={() => {
if (!result) return;
<ContextMenu.Root>
<ContextMenu.Trigger>
<TouchableOpacity
onPress={() => {
if (!result) return;
// @ts-ignore
router.push({
pathname: `/(auth)/(tabs)/${from}/jellyseerr/page`,
params: {
...result,
mediaTitle,
releaseYear,
canRequest,
posterSrc,
mediaType,
},
});
}}
{...props}
>
{children}
</TouchableOpacity>
</ContextMenu.Trigger>
<ContextMenu.Content
avoidCollisions
alignOffset={0}
collisionPadding={0}
loop={false}
key={"content"}
// @ts-ignore
router.push({
pathname: `/(auth)/(tabs)/${from}/jellyseerr/page`,
params: {
...result,
mediaTitle,
releaseYear,
canRequest,
posterSrc,
mediaType,
},
});
}}
{...props}
>
<ContextMenu.Label key='label-1'>Actions</ContextMenu.Label>
{canRequest && mediaType === MediaType.MOVIE && (
<ContextMenu.Item
key='item-1'
onSelect={() => {
if (autoApprove) {
request();
}
{children}
</TouchableOpacity>
</ContextMenu.Trigger>
<ContextMenu.Content
avoidCollisions
alignOffset={0}
collisionPadding={0}
loop={false}
key={"content"}
>
<ContextMenu.Label key='label-1'>Actions</ContextMenu.Label>
{canRequest && mediaType === MediaType.MOVIE && (
<ContextMenu.Item
key='item-1'
onSelect={() => {
if (autoApprove) {
request();
}
}}
shouldDismissMenuOnSelect
>
<ContextMenu.ItemTitle key='item-1-title'>
Request
</ContextMenu.ItemTitle>
<ContextMenu.ItemIcon
ios={{
name: "arrow.down.to.line",
pointSize: 18,
weight: "semibold",
scale: "medium",
hierarchicalColor: {
dark: "purple",
light: "purple",
},
}}
shouldDismissMenuOnSelect
>
<ContextMenu.ItemTitle key='item-1-title'>
Request
</ContextMenu.ItemTitle>
<ContextMenu.ItemIcon
ios={{
name: "arrow.down.to.line",
pointSize: 18,
weight: "semibold",
scale: "medium",
hierarchicalColor: {
dark: "purple",
light: "purple",
},
}}
androidIconName='download'
/>
</ContextMenu.Item>
)}
</ContextMenu.Content>
</ContextMenu.Root>
</>
androidIconName='download'
/>
</ContextMenu.Item>
)}
</ContextMenu.Content>
</ContextMenu.Root>
);
};

View File

@@ -1,6 +1,4 @@
import React from "react";
import { Platform, type TextProps } from "react-native";
import { Text as RNText } from "react-native";
import { Platform, Text as RNText, type TextProps } from "react-native";
import { UITextView } from "react-native-uitextview";
export function Text(
props: TextProps & {

View File

@@ -1,5 +1,3 @@
import { useFavorite } from "@/hooks/useFavorite";
import { useMarkAsPlayed } from "@/hooks/useMarkAsPlayed";
import { useActionSheet } from "@expo/react-native-action-sheet";
import type {
BaseItemDto,
@@ -8,6 +6,8 @@ import type {
import { useRouter, useSegments } from "expo-router";
import { type PropsWithChildren, useCallback } from "react";
import { TouchableOpacity, type TouchableOpacityProps } from "react-native";
import { useFavorite } from "@/hooks/useFavorite";
import { useMarkAsPlayed } from "@/hooks/useMarkAsPlayed";
interface Props extends TouchableOpacityProps {
item: BaseItemDto;

View File

@@ -1,4 +1,3 @@
import { Text } from "@/components/common/Text";
import { View, type ViewProps } from "react-native";
interface Props extends ViewProps {

View File

@@ -1,9 +1,3 @@
import { Text } from "@/components/common/Text";
import { useDownload } from "@/providers/DownloadProvider";
import { DownloadMethod, useSettings } from "@/utils/atoms/settings";
import { storage } from "@/utils/mmkv";
import type { JobStatus } from "@/utils/optimize-server";
import { formatTimeString } from "@/utils/time";
import { Ionicons } from "@expo/vector-icons";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { Image } from "expo-image";
@@ -19,7 +13,14 @@ import {
type ViewProps,
} from "react-native";
import { toast } from "sonner-native";
import { Text } from "@/components/common/Text";
import { useDownload } from "@/providers/DownloadProvider";
import { DownloadMethod, useSettings } from "@/utils/atoms/settings";
import { storage } from "@/utils/mmkv";
import type { JobStatus } from "@/utils/optimize-server";
import { formatTimeString } from "@/utils/time";
import { Button } from "../Button";
const BackGroundDownloader = !Platform.isTV
? require("@kesha-antonov/react-native-background-downloader")
: null;

View File

@@ -1,9 +1,9 @@
import { Text } from "@/components/common/Text";
import { useDownload } from "@/providers/DownloadProvider";
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
import type React from "react";
import { useEffect, useMemo, useState } from "react";
import type { TextProps } from "react-native";
import { Text } from "@/components/common/Text";
import { useDownload } from "@/providers/DownloadProvider";
interface DownloadSizeProps extends TextProps {
items: BaseItemDto[];
@@ -39,10 +39,8 @@ export const DownloadSize: React.FC<DownloadSizeProps> = ({
}, [size]);
return (
<>
<Text className='text-xs text-neutral-500' {...props}>
{sizeText}
</Text>
</>
<Text className='text-xs text-neutral-500' {...props}>
{sizeText}
</Text>
);
};

View File

@@ -1,4 +1,3 @@
import { useHaptic } from "@/hooks/useHaptic";
import {
ActionSheetProvider,
useActionSheet,
@@ -11,17 +10,14 @@ import {
type TouchableOpacityProps,
View,
} from "react-native";
import { Text } from "@/components/common/Text";
import { DownloadSize } from "@/components/downloads/DownloadSize";
import { useDownloadedFileOpener } from "@/hooks/useDownloadedFileOpener";
import { useHaptic } from "@/hooks/useHaptic";
import { useDownload } from "@/providers/DownloadProvider";
import { storage } from "@/utils/mmkv";
import { runtimeTicksToSeconds } from "@/utils/time";
import { Ionicons } from "@expo/vector-icons";
import { Image } from "expo-image";
import ContinueWatchingPoster from "../ContinueWatchingPoster";
import { TouchableItemRouter } from "../common/TouchableItemRouter";
interface EpisodeCardProps extends TouchableOpacityProps {
item: BaseItemDto;
@@ -33,7 +29,7 @@ export const EpisodeCard: React.FC<EpisodeCardProps> = ({ item, ...props }) => {
const { showActionSheetWithOptions } = useActionSheet();
const successHapticFeedback = useHaptic("success");
const base64Image = useMemo(() => {
const _base64Image = useMemo(() => {
return storage.getString(item.Id!);
}, [item]);

View File

@@ -1,19 +1,18 @@
import { useHaptic } from "@/hooks/useHaptic";
import {
ActionSheetProvider,
useActionSheet,
} from "@expo/react-native-action-sheet";
import { Ionicons } from "@expo/vector-icons";
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
import { Image } from "expo-image";
import type React from "react";
import { useCallback, useMemo } from "react";
import { TouchableOpacity, View } from "react-native";
import { DownloadSize } from "@/components/downloads/DownloadSize";
import { useDownloadedFileOpener } from "@/hooks/useDownloadedFileOpener";
import { useHaptic } from "@/hooks/useHaptic";
import { useDownload } from "@/providers/DownloadProvider";
import { storage } from "@/utils/mmkv";
import { Ionicons } from "@expo/vector-icons";
import { Image } from "expo-image";
import { ItemCardText } from "../ItemCardText";
interface MovieCardProps {

View File

@@ -1,6 +1,3 @@
import { DownloadSize } from "@/components/downloads/DownloadSize";
import { useDownload } from "@/providers/DownloadProvider";
import { storage } from "@/utils/mmkv";
import { useActionSheet } from "@expo/react-native-action-sheet";
import { Ionicons } from "@expo/vector-icons";
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
@@ -9,6 +6,9 @@ import { router } from "expo-router";
import type React from "react";
import { useCallback, useMemo } from "react";
import { TouchableOpacity, View } from "react-native";
import { DownloadSize } from "@/components/downloads/DownloadSize";
import { useDownload } from "@/providers/DownloadProvider";
import { storage } from "@/utils/mmkv";
import { Text } from "../common/Text";
export const SeriesCard: React.FC<{ items: BaseItemDto[] }> = ({ items }) => {

View File

@@ -1,8 +1,8 @@
import { Text } from "@/components/common/Text";
import { FontAwesome, Ionicons } from "@expo/vector-icons";
import { useQuery } from "@tanstack/react-query";
import { useState } from "react";
import { TouchableOpacity, View, type ViewProps } from "react-native";
import { Text } from "@/components/common/Text";
import { FilterSheet } from "./FilterSheet";
interface FilterButtonProps<T> extends ViewProps {

View File

@@ -1,16 +1,12 @@
import { Ionicons } from "@expo/vector-icons";
import {
BottomSheetBackdrop,
type BottomSheetBackdropProps,
BottomSheetFlatList,
BottomSheetModal,
BottomSheetScrollView,
BottomSheetView,
} from "@gorhom/bottom-sheet";
import type React from "react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { Text } from "@/components/common/Text";
import { Ionicons } from "@expo/vector-icons";
import { useTranslation } from "react-i18next";
import {
StyleSheet,
@@ -18,6 +14,7 @@ import {
View,
type ViewProps,
} from "react-native";
import { Text } from "@/components/common/Text";
import { Button } from "../Button";
import { Input } from "../common/Input";

View File

@@ -1,11 +1,11 @@
import { Ionicons } from "@expo/vector-icons";
import { useAtom } from "jotai";
import { TouchableOpacity, type TouchableOpacityProps } from "react-native";
import {
genreFilterAtom,
tagsFilterAtom,
yearFilterAtom,
} from "@/utils/atoms/filters";
import { Ionicons } from "@expo/vector-icons";
import { useAtom } from "jotai";
import { TouchableOpacity, type TouchableOpacityProps } from "react-native";
interface Props extends TouchableOpacityProps {}

View File

@@ -1,5 +1,3 @@
import { Colors } from "@/constants/Colors";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import type { Api } from "@jellyfin/sdk";
import type { BaseItemKind } from "@jellyfin/sdk/lib/generated-client";
import { getItemsApi } from "@jellyfin/sdk/lib/utils/api";
@@ -7,10 +5,11 @@ import { t } from "i18next";
import { useAtom } from "jotai";
import { useCallback, useEffect, useState } from "react";
import { Image, Text, View } from "react-native";
import { ScrollingCollectionList } from "./ScrollingCollectionList";
// PNG ASSET
import heart from "@/assets/icons/heart.fill.png";
import { Colors } from "@/constants/Colors";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { ScrollingCollectionList } from "./ScrollingCollectionList";
type FavoriteTypes =
| "Series"

View File

@@ -1,8 +1,3 @@
import { useHaptic } from "@/hooks/useHaptic";
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 type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
import { getItemsApi } from "@jellyfin/sdk/lib/utils/api";
import { useQuery } from "@tanstack/react-query";
@@ -21,6 +16,11 @@ import Carousel, {
type ICarouselInstance,
Pagination,
} from "react-native-reanimated-carousel";
import { useHaptic } from "@/hooks/useHaptic";
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";
interface Props extends ViewProps {}

View File

@@ -1,5 +1,3 @@
import { Text } from "@/components/common/Text";
import MoviePoster from "@/components/posters/MoviePoster";
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
import {
type QueryFunction,
@@ -8,9 +6,11 @@ import {
} from "@tanstack/react-query";
import { useTranslation } from "react-i18next";
import { ScrollView, View, type ViewProps } from "react-native";
import { Text } from "@/components/common/Text";
import MoviePoster from "@/components/posters/MoviePoster";
import ContinueWatchingPoster from "../ContinueWatchingPoster";
import { ItemCardText } from "../ItemCardText";
import { TouchableItemRouter } from "../common/TouchableItemRouter";
import { ItemCardText } from "../ItemCardText";
import SeriesPoster from "../posters/SeriesPoster";
interface Props extends ViewProps {

View File

@@ -1,6 +1,6 @@
import { TouchableOpacity } from "react-native";
import { Text } from "@/components/common/Text";
import DisabledSetting from "@/components/settings/DisabledSetting";
import { TouchableOpacity, View } from "react-native";
interface StepperProps {
value: number;

View File

@@ -1,11 +1,11 @@
import { Text } from "@/components/common/Text";
import PersonPoster from "@/components/jellyseerr/PersonPoster";
import type { MovieDetails } from "@/utils/jellyseerr/server/models/Movie";
import type { TvDetails } from "@/utils/jellyseerr/server/models/Tv";
import { FlashList } from "@shopify/flash-list";
import type React from "react";
import { useTranslation } from "react-i18next";
import { View, type ViewProps } from "react-native";
import { Text } from "@/components/common/Text";
import PersonPoster from "@/components/jellyseerr/PersonPoster";
import type { MovieDetails } from "@/utils/jellyseerr/server/models/Movie";
import type { TvDetails } from "@/utils/jellyseerr/server/models/Tv";
const CastSlide: React.FC<
{ details?: MovieDetails | TvDetails } & ViewProps

View File

@@ -1,15 +1,15 @@
import { Text } from "@/components/common/Text";
import { useJellyseerr } from "@/hooks/useJellyseerr";
import { ANIME_KEYWORD_ID } from "@/utils/jellyseerr/server/api/themoviedb/constants";
import type { TmdbRelease } from "@/utils/jellyseerr/server/api/themoviedb/interfaces";
import type { MovieDetails } from "@/utils/jellyseerr/server/models/Movie";
import type { TvDetails } from "@/utils/jellyseerr/server/models/Tv";
import { Ionicons, MaterialCommunityIcons } from "@expo/vector-icons";
import { uniqBy } from "lodash";
import { useMemo } from "react";
import { useTranslation } from "react-i18next";
import { View, type ViewProps } from "react-native";
import CountryFlag from "react-native-country-flag";
import { Text } from "@/components/common/Text";
import { useJellyseerr } from "@/hooks/useJellyseerr";
import { ANIME_KEYWORD_ID } from "@/utils/jellyseerr/server/api/themoviedb/constants";
import type { TmdbRelease } from "@/utils/jellyseerr/server/api/themoviedb/interfaces";
import type { MovieDetails } from "@/utils/jellyseerr/server/models/Movie";
import type { TvDetails } from "@/utils/jellyseerr/server/models/Tv";
interface Release {
certification: string;

View File

@@ -1,3 +1,13 @@
import { orderBy, uniqBy } from "lodash";
import type React from "react";
import { useMemo } from "react";
import { useTranslation } from "react-i18next";
import { View, type ViewProps } from "react-native";
import {
useAnimatedReaction,
useSharedValue,
withTiming,
} from "react-native-reanimated";
import Discover from "@/components/jellyseerr/discover/Discover";
import { useJellyseerr } from "@/hooks/useJellyseerr";
import { MediaType } from "@/utils/jellyseerr/server/constants/media";
@@ -7,17 +17,6 @@ import type {
TvResult,
} from "@/utils/jellyseerr/server/models/Search";
import { useReactNavigationQuery } from "@/utils/useReactNavigationQuery";
import { orderBy, uniqBy } from "lodash";
import type React from "react";
import { useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { View, type ViewProps } from "react-native";
import {
useAnimatedReaction,
useAnimatedStyle,
useSharedValue,
withTiming,
} from "react-native-reanimated";
import { Text } from "../common/Text";
import JellyseerrPoster from "../posters/JellyseerrPoster";
import { LoadingSkeleton } from "../search/LoadingSkeleton";

View File

@@ -1,7 +1,7 @@
import { MediaType } from "@/utils/jellyseerr/server/constants/media";
import { Feather, MaterialCommunityIcons } from "@expo/vector-icons";
import { useMemo } from "react";
import { View, type ViewProps } from "react-native";
import { MediaType } from "@/utils/jellyseerr/server/constants/media";
const JellyseerrMediaIcon: React.FC<
{ mediaType: "tv" | "movie" } & ViewProps

View File

@@ -1,7 +1,7 @@
import { MediaStatus } from "@/utils/jellyseerr/server/constants/media";
import { MaterialCommunityIcons } from "@expo/vector-icons";
import { useEffect, useState } from "react";
import { TouchableOpacity, View, type ViewProps } from "react-native";
import { MediaStatus } from "@/utils/jellyseerr/server/constants/media";
interface Props {
mediaStatus?: MediaStatus;

View File

@@ -1,7 +1,4 @@
import { ParallaxScrollView } from "@/components/ParallaxPage";
import { Text } from "@/components/common/Text";
import { FlashList } from "@shopify/flash-list";
import { useFocusEffect } from "expo-router";
import type React from "react";
import {
type PropsWithChildren,
@@ -10,9 +7,10 @@ import {
useRef,
useState,
} from "react";
import { Dimensions, View, type ViewProps } from "react-native";
import { Animated } from "react-native";
import { Animated, View, type ViewProps } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { Text } from "@/components/common/Text";
import { ParallaxScrollView } from "@/components/ParallaxPage";
const ANIMATION_ENTER = 250;
const ANIMATION_EXIT = 250;

View File

@@ -1,9 +1,9 @@
import { Text } from "@/components/common/Text";
import Poster from "@/components/posters/Poster";
import { useJellyseerr } from "@/hooks/useJellyseerr";
import { useRouter, useSegments } from "expo-router";
import type React from "react";
import { TouchableOpacity, View, type ViewProps } from "react-native";
import { Text } from "@/components/common/Text";
import Poster from "@/components/posters/Poster";
import { useJellyseerr } from "@/hooks/useJellyseerr";
interface Props {
id: string;

View File

@@ -1,3 +1,14 @@
import {
BottomSheetBackdrop,
type BottomSheetBackdropProps,
BottomSheetModal,
BottomSheetView,
} from "@gorhom/bottom-sheet";
import type { BottomSheetModalMethods } from "@gorhom/bottom-sheet/lib/typescript/types";
import { useQuery } from "@tanstack/react-query";
import { forwardRef, useCallback, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { View, type ViewProps } from "react-native";
import { Button } from "@/components/Button";
import Dropdown from "@/components/common/Dropdown";
import { Text } from "@/components/common/Text";
@@ -10,17 +21,6 @@ import type {
import type { MediaType } from "@/utils/jellyseerr/server/constants/media";
import type { MediaRequestBody } from "@/utils/jellyseerr/server/interfaces/api/requestInterfaces";
import { writeDebugLog } from "@/utils/log";
import {
BottomSheetBackdrop,
type BottomSheetBackdropProps,
BottomSheetModal,
BottomSheetView,
} from "@gorhom/bottom-sheet";
import type { BottomSheetModalMethods } from "@gorhom/bottom-sheet/lib/typescript/types";
import { useQuery } from "@tanstack/react-query";
import React, { forwardRef, useCallback, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { View, type ViewProps } from "react-native";
interface Props {
id: number;

View File

@@ -1,3 +1,7 @@
import { router, useSegments } from "expo-router";
import type React from "react";
import { useCallback } from "react";
import { TouchableOpacity, type ViewProps } from "react-native";
import GenericSlideCard from "@/components/jellyseerr/discover/GenericSlideCard";
import Slide, { type SlideProps } from "@/components/jellyseerr/discover/Slide";
import { useJellyseerr } from "@/hooks/useJellyseerr";
@@ -6,10 +10,6 @@ import {
type Network,
} from "@/utils/jellyseerr/src/components/Discover/NetworkSlider";
import type { Studio } from "@/utils/jellyseerr/src/components/Discover/StudioSlider";
import { router, useSegments } from "expo-router";
import type React from "react";
import { useCallback } from "react";
import { TouchableOpacity, type ViewProps } from "react-native";
const CompanySlide: React.FC<
{ data: Network[] | Studio[] } & SlideProps & ViewProps
@@ -33,7 +33,7 @@ const CompanySlide: React.FC<
slide={slide}
data={data}
keyExtractor={(item) => item.id.toString()}
renderItem={(item, index) => (
renderItem={(item, _index) => (
<TouchableOpacity className='mr-2' onPress={() => navigate(item)}>
<GenericSlideCard
className='w-28 rounded-lg overflow-hidden border border-neutral-900 p-4'

View File

@@ -1,3 +1,7 @@
import { sortBy } from "lodash";
import type React from "react";
import { useMemo } from "react";
import { View } from "react-native";
import CompanySlide from "@/components/jellyseerr/discover/CompanySlide";
import GenreSlide from "@/components/jellyseerr/discover/GenreSlide";
import MovieTvSlide from "@/components/jellyseerr/discover/MovieTvSlide";
@@ -6,27 +10,26 @@ import { DiscoverSliderType } from "@/utils/jellyseerr/server/constants/discover
import type DiscoverSlider from "@/utils/jellyseerr/server/entity/DiscoverSlider";
import { networks } from "@/utils/jellyseerr/src/components/Discover/NetworkSlider";
import { studios } from "@/utils/jellyseerr/src/components/Discover/StudioSlider";
import { sortBy } from "lodash";
import type React from "react";
import { useMemo } from "react";
import { View } from "react-native";
interface Props {
sliders?: DiscoverSlider[];
}
const Discover: React.FC<Props> = ({ sliders }) => {
if (!sliders) return;
const hasSliders = !!sliders;
const sortedSliders = useMemo(
() =>
sortBy(
sliders.filter((s) => s.enabled),
(sliders ?? []).filter((s) => s.enabled),
"order",
"asc",
),
[sliders],
);
if (!hasSliders) return null;
return (
<View className='flex flex-col space-y-4 mb-8'>
{sortedSliders.map((slide) => {
@@ -60,6 +63,8 @@ const Discover: React.FC<Props> = ({ sliders }) => {
contentContainerStyle={{ paddingBottom: 16 }}
/>
);
default:
return null;
}
})}
</View>

View File

@@ -1,8 +1,8 @@
import { Text } from "@/components/common/Text";
import { Image, type ImageContentFit } from "expo-image";
import { LinearGradient } from "expo-linear-gradient";
import type React from "react";
import { StyleSheet, View, type ViewProps } from "react-native";
import { Text } from "@/components/common/Text";
export const textShadowStyle = StyleSheet.create({
shadow: {

View File

@@ -1,14 +1,14 @@
import { useQuery } from "@tanstack/react-query";
import { router, useSegments } from "expo-router";
import type React from "react";
import { useCallback } from "react";
import { TouchableOpacity, type ViewProps } from "react-native";
import GenericSlideCard from "@/components/jellyseerr/discover/GenericSlideCard";
import Slide, { type SlideProps } from "@/components/jellyseerr/discover/Slide";
import { Endpoints, useJellyseerr } from "@/hooks/useJellyseerr";
import { DiscoverSliderType } from "@/utils/jellyseerr/server/constants/discover";
import type { GenreSliderItem } from "@/utils/jellyseerr/server/interfaces/api/discoverInterfaces";
import { genreColorMap } from "@/utils/jellyseerr/src/components/Discover/constants";
import { useQuery } from "@tanstack/react-query";
import { router, useSegments } from "expo-router";
import type React from "react";
import { useCallback } from "react";
import { TouchableOpacity, type ViewProps } from "react-native";
const GenreSlide: React.FC<SlideProps & ViewProps> = ({ slide, ...props }) => {
const segments = useSegments();
@@ -43,7 +43,7 @@ const GenreSlide: React.FC<SlideProps & ViewProps> = ({ slide, ...props }) => {
slide={slide}
data={data}
keyExtractor={(item) => item.id.toString()}
renderItem={(item, index) => (
renderItem={(item, _index) => (
<TouchableOpacity className='mr-2' onPress={() => navigate(item)}>
<GenericSlideCard
className='w-28 rounded-lg overflow-hidden border border-neutral-900'

View File

@@ -1,3 +1,8 @@
import { useInfiniteQuery } from "@tanstack/react-query";
import { uniqBy } from "lodash";
import type React from "react";
import { useMemo } from "react";
import type { ViewProps } from "react-native";
import Slide, { type SlideProps } from "@/components/jellyseerr/discover/Slide";
import JellyseerrPoster from "@/components/posters/JellyseerrPoster";
import {
@@ -10,11 +15,6 @@ import type {
MovieResult,
TvResult,
} from "@/utils/jellyseerr/server/models/Search";
import { useInfiniteQuery } from "@tanstack/react-query";
import { uniqBy } from "lodash";
import type React from "react";
import { useMemo } from "react";
import type { ViewProps } from "react-native";
const MovieTvSlide: React.FC<SlideProps & ViewProps> = ({
slide,
@@ -25,7 +25,7 @@ const MovieTvSlide: React.FC<SlideProps & ViewProps> = ({
const { data, fetchNextPage, hasNextPage } = useInfiniteQuery({
queryKey: ["jellyseerr", "discover", slide.id],
queryFn: async ({ pageParam }) => {
let endpoint: DiscoverEndpoint | undefined = undefined;
let endpoint: DiscoverEndpoint | undefined;
let params: any = {
page: Number(pageParam),
};

View File

@@ -1,12 +1,12 @@
import { useQuery } from "@tanstack/react-query";
import type React from "react";
import type { ViewProps } from "react-native";
import Slide, { type SlideProps } from "@/components/jellyseerr/discover/Slide";
import JellyseerrPoster from "@/components/posters/JellyseerrPoster";
import { useJellyseerr } from "@/hooks/useJellyseerr";
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";
import { useQuery } from "@tanstack/react-query";
import type React from "react";
import type { ViewProps } from "react-native";
const RequestCard: React.FC<{ request: MediaRequest }> = ({ request }) => {
const { jellyseerrApi } = useJellyseerr();

View File

@@ -1,12 +1,12 @@
import { Text } from "@/components/common/Text";
import { DiscoverSliderType } from "@/utils/jellyseerr/server/constants/discover";
import type DiscoverSlider from "@/utils/jellyseerr/server/entity/DiscoverSlider";
import { FlashList } from "@shopify/flash-list";
import type { ContentStyle } from "@shopify/flash-list/src/FlashListProps";
import { t } from "i18next";
import type React from "react";
import type { PropsWithChildren } from "react";
import { View, type ViewProps } from "react-native";
import { Text } from "@/components/common/Text";
import { DiscoverSliderType } from "@/utils/jellyseerr/server/constants/discover";
import type DiscoverSlider from "@/utils/jellyseerr/server/entity/DiscoverSlider";
export interface SlideProps {
slide: DiscoverSlider;
@@ -51,7 +51,7 @@ const Slide = <T,>({
onEndReached={onEndReached}
//@ts-ignore
renderItem={({ item, index }) =>
item ? renderItem(item, index) : <></>
item ? renderItem(item, index) : null
}
/>
</View>

View File

@@ -1,7 +1,3 @@
import { Text } from "@/components/common/Text";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { useSettings } from "@/utils/atoms/settings";
import { getPrimaryImageUrl } from "@/utils/jellyfin/image/getPrimaryImageUrl";
import { Ionicons } from "@expo/vector-icons";
import type {
BaseItemDto,
@@ -15,6 +11,10 @@ import { useAtom } from "jotai";
import { useMemo } from "react";
import { useTranslation } from "react-i18next";
import { type TouchableOpacityProps, View } from "react-native";
import { Text } from "@/components/common/Text";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { useSettings } from "@/utils/atoms/settings";
import { getPrimaryImageUrl } from "@/utils/jellyfin/image/getPrimaryImageUrl";
import { TouchableItemRouter } from "../common/TouchableItemRouter";
interface Props extends TouchableOpacityProps {

View File

@@ -1,13 +1,12 @@
import {
Children,
type PropsWithChildren,
type ReactElement,
cloneElement,
isValidElement,
type PropsWithChildren,
type ReactElement,
} from "react";
import { StyleSheet, View, type ViewProps, type ViewStyle } from "react-native";
import { Text } from "../common/Text";
import { ListItem } from "./ListItem";
interface Props extends ViewProps {
title?: string | null | undefined;

View File

@@ -1,4 +1,3 @@
import React from "react";
import { View } from "react-native";
import { Text } from "../common/Text";

View File

@@ -15,7 +15,7 @@ export const LiveTVGuideRow = ({
scrollX?: number;
isVisible?: boolean;
}) => {
const positionRefs = useRef<{ [key: string]: number }>({});
const _positionRefs = useRef<{ [key: string]: number }>({});
const screenWidth = Dimensions.get("window").width;
const calculateWidth = (s?: string | null, e?: string | null) => {

View File

@@ -1,4 +1,3 @@
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import type {
BaseItemDto,
BaseItemDtoQueryResult,
@@ -12,10 +11,11 @@ import {
import { useAtom } from "jotai";
import { useCallback } from "react";
import { View, type ViewProps } from "react-native";
import { ItemCardText } from "../ItemCardText";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { InfiniteHorizontalScroll } from "../common/InfiniteHorrizontalScroll";
import { Text } from "../common/Text";
import { TouchableItemRouter } from "../common/TouchableItemRouter";
import { ItemCardText } from "../ItemCardText";
import MoviePoster from "../posters/MoviePoster";
interface Props extends ViewProps {

View File

@@ -1,6 +1,6 @@
import { Text } from "@/components/common/Text";
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
import { View, type ViewProps } from "react-native";
import { Text } from "@/components/common/Text";
interface Props extends ViewProps {
item: BaseItemDto;

View File

@@ -1,7 +1,7 @@
// You can explore the built-in icon families and icons on the web at https://icons.expo.fyi/
import Ionicons from "@expo/vector-icons/Ionicons";
import type { IconProps } from "@expo/vector-icons/build/createIconSet";
import Ionicons from "@expo/vector-icons/Ionicons";
import type { ComponentProps } from "react";
export function TabBarIcon({

View File

@@ -1,11 +1,10 @@
import { WatchedIndicator } from "@/components/WatchedIndicator";
import { apiAtom } from "@/providers/JellyfinProvider";
import { getPrimaryImageUrl } from "@/utils/jellyfin/image/getPrimaryImageUrl";
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
import { Image } from "expo-image";
import { useAtom } from "jotai";
import { useMemo, useState } from "react";
import { View } from "react-native";
import { WatchedIndicator } from "@/components/WatchedIndicator";
import { apiAtom } from "@/providers/JellyfinProvider";
type MoviePosterProps = {
item: BaseItemDto;
@@ -24,7 +23,7 @@ export const EpisodePoster: React.FC<MoviePosterProps> = ({
}
}, [item]);
const [progress, setProgress] = useState(
const [progress, _setProgress] = useState(
item.UserData?.PlayedPercentage || 0,
);

View File

@@ -1,12 +1,8 @@
import { Text } from "@/components/common/Text";
import {
type BaseItemDto,
BaseItemKind,
} from "@jellyfin/sdk/lib/generated-client/models";
import { type BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
import { useState } from "react";
import { View, type ViewProps } from "react-native";
import { WatchedIndicator } from "../WatchedIndicator";
import { ItemImage } from "../common/ItemImage";
import { WatchedIndicator } from "../WatchedIndicator";
interface Props extends ViewProps {
item: BaseItemDto;
@@ -18,7 +14,7 @@ export const ItemPoster: React.FC<Props> = ({
showProgress,
...props
}) => {
const [progress, setProgress] = useState(
const [progress, _setProgress] = useState(
item.UserData?.PlayedPercentage || 0,
);

View File

@@ -1,9 +1,18 @@
import { Tag, Tags } from "@/components/GenreTags";
import { Image } from "expo-image";
import { useMemo } from "react";
import { useTranslation } from "react-i18next";
import { View, type ViewProps } from "react-native";
import Animated, {
useAnimatedStyle,
useSharedValue,
withTiming,
} from "react-native-reanimated";
import { TouchableJellyseerrRouter } from "@/components/common/JellyseerrItemRouter";
import { Text } from "@/components/common/Text";
import { Tag, Tags } from "@/components/GenreTags";
import { textShadowStyle } from "@/components/jellyseerr/discover/GenericSlideCard";
import JellyseerrMediaIcon from "@/components/jellyseerr/JellyseerrMediaIcon";
import JellyseerrStatusIcon from "@/components/jellyseerr/JellyseerrStatusIcon";
import { textShadowStyle } from "@/components/jellyseerr/discover/GenericSlideCard";
import { Colors } from "@/constants/Colors";
import { useJellyseerr } from "@/hooks/useJellyseerr";
import { useJellyseerrCanRequest } from "@/utils/_jellyseerr/useJellyseerrCanRequest";
@@ -16,15 +25,6 @@ import type {
TvResult,
} from "@/utils/jellyseerr/server/models/Search";
import type { TvDetails } from "@/utils/jellyseerr/server/models/Tv";
import { Image } from "expo-image";
import { useMemo } from "react";
import { useTranslation } from "react-i18next";
import { View, type ViewProps } from "react-native";
import Animated, {
useAnimatedStyle,
useSharedValue,
withTiming,
} from "react-native-reanimated";
interface Props extends ViewProps {
item?: MovieResult | TvResult | MovieDetails | TvDetails;

View File

@@ -1,11 +1,11 @@
import { WatchedIndicator } from "@/components/WatchedIndicator";
import { apiAtom } from "@/providers/JellyfinProvider";
import { getPrimaryImageUrl } from "@/utils/jellyfin/image/getPrimaryImageUrl";
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
import { Image } from "expo-image";
import { useAtom } from "jotai";
import { useMemo, useState } from "react";
import { View } from "react-native";
import { WatchedIndicator } from "@/components/WatchedIndicator";
import { apiAtom } from "@/providers/JellyfinProvider";
import { getPrimaryImageUrl } from "@/utils/jellyfin/image/getPrimaryImageUrl";
type MoviePosterProps = {
item: BaseItemDto;
@@ -26,7 +26,7 @@ const MoviePoster: React.FC<MoviePosterProps> = ({
});
}, [item]);
const [progress, setProgress] = useState(
const [progress, _setProgress] = useState(
item.UserData?.PlayedPercentage || 0,
);

View File

@@ -1,8 +1,8 @@
import { apiAtom } from "@/providers/JellyfinProvider";
import { Image } from "expo-image";
import { useAtom } from "jotai";
import { useMemo } from "react";
import { View } from "react-native";
import { apiAtom } from "@/providers/JellyfinProvider";
type PosterProps = {
id?: string;

View File

@@ -1,11 +1,10 @@
import { WatchedIndicator } from "@/components/WatchedIndicator";
import { apiAtom } from "@/providers/JellyfinProvider";
import { getPrimaryImageUrl } from "@/utils/jellyfin/image/getPrimaryImageUrl";
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
import { Image } from "expo-image";
import { useAtom } from "jotai";
import { useMemo, useState } from "react";
import { useMemo } from "react";
import { View } from "react-native";
import { apiAtom } from "@/providers/JellyfinProvider";
import { getPrimaryImageUrl } from "@/utils/jellyfin/image/getPrimaryImageUrl";
type MoviePosterProps = {
item: BaseItemDto;

View File

@@ -1,7 +1,7 @@
import { View } from "react-native";
import Animated, {
useAnimatedStyle,
useAnimatedReaction,
useAnimatedStyle,
useSharedValue,
withTiming,
} from "react-native-reanimated";

View File

@@ -1,11 +1,8 @@
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { getUserItemData } from "@/utils/jellyfin/user-library/getUserItemData";
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client";
import { FlashList } from "@shopify/flash-list";
import { useQuery } from "@tanstack/react-query";
import { useAtom } from "jotai";
import type React from "react";
import type { PropsWithChildren } from "react";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { Text } from "../common/Text";
type SearchItemWrapperProps<T> = {
@@ -21,8 +18,8 @@ export const SearchItemWrapper = <T,>({
header,
onEndReached,
}: PropsWithChildren<SearchItemWrapperProps<T>>) => {
const [api] = useAtom(apiAtom);
const [user] = useAtom(userAtom);
const [_api] = useAtom(apiAtom);
const [_user] = useAtom(userAtom);
if (!items || items.length === 0) return null;

View File

@@ -1,5 +1,3 @@
import { apiAtom } from "@/providers/JellyfinProvider";
import { getPrimaryImageUrl } from "@/utils/jellyfin/image/getPrimaryImageUrl";
import type {
BaseItemDto,
BaseItemPerson,
@@ -10,6 +8,8 @@ import type React from "react";
import { useMemo } from "react";
import { useTranslation } from "react-i18next";
import { TouchableOpacity, View, type ViewProps } from "react-native";
import { apiAtom } from "@/providers/JellyfinProvider";
import { getPrimaryImageUrl } from "@/utils/jellyfin/image/getPrimaryImageUrl";
import { HorizontalScroll } from "../common/HorrizontalScroll";
import { Text } from "../common/Text";
import { itemRouter } from "../common/TouchableItemRouter";
@@ -48,7 +48,7 @@ export const CastAndCrew: React.FC<Props> = ({ item, loading, ...props }) => {
</Text>
<HorizontalScroll
loading={loading}
keyExtractor={(i, idx) => i.Id.toString()}
keyExtractor={(i, _idx) => i.Id.toString()}
height={247}
data={destinctPeople}
renderItem={(i) => (

View File

@@ -1,11 +1,11 @@
import { apiAtom } from "@/providers/JellyfinProvider";
import { getPrimaryImageUrlById } from "@/utils/jellyfin/image/getPrimaryImageUrlById";
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
import { router } from "expo-router";
import { useAtom } from "jotai";
import type React from "react";
import { useTranslation } from "react-i18next";
import { TouchableOpacity, View, type ViewProps } from "react-native";
import { apiAtom } from "@/providers/JellyfinProvider";
import { getPrimaryImageUrlById } from "@/utils/jellyfin/image/getPrimaryImageUrlById";
import { HorizontalScroll } from "../common/HorrizontalScroll";
import { Text } from "../common/Text";
import Poster from "../posters/Poster";
@@ -26,7 +26,7 @@ export const CurrentSeries: React.FC<Props> = ({ item, ...props }) => {
<HorizontalScroll
data={[item]}
height={247}
renderItem={(item, index) => (
renderItem={(item, _index) => (
<TouchableOpacity
key={item.Id}
onPress={() => router.push(`/series/${item.SeriesId}`)}

View File

@@ -1,7 +1,7 @@
import { Text } from "@/components/common/Text";
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
import { useRouter } from "expo-router";
import { TouchableOpacity, View, type ViewProps } from "react-native";
import { Text } from "@/components/common/Text";
interface Props extends ViewProps {
item: BaseItemDto;

View File

@@ -1,21 +1,3 @@
import { Tags } from "@/components/GenreTags";
import { RoundButton } from "@/components/RoundButton";
import { HorizontalScroll } from "@/components/common/HorrizontalScroll";
import { Text } from "@/components/common/Text";
import { dateOpts } from "@/components/jellyseerr/DetailFacts";
import JellyseerrStatusIcon from "@/components/jellyseerr/JellyseerrStatusIcon";
import { textShadowStyle } from "@/components/jellyseerr/discover/GenericSlideCard";
import { useJellyseerr } from "@/hooks/useJellyseerr";
import {
MediaStatus,
MediaType,
} from "@/utils/jellyseerr/server/constants/media";
import type MediaRequest from "@/utils/jellyseerr/server/entity/MediaRequest";
import type Season from "@/utils/jellyseerr/server/entity/Season";
import type { MediaRequestBody } from "@/utils/jellyseerr/server/interfaces/api/requestInterfaces";
import type { MovieDetails } from "@/utils/jellyseerr/server/models/Movie";
import { TvResult } from "@/utils/jellyseerr/server/models/Search";
import type { TvDetails } from "@/utils/jellyseerr/server/models/Tv";
import { Ionicons } from "@expo/vector-icons";
import { FlashList } from "@shopify/flash-list";
import {
@@ -29,6 +11,23 @@ import { orderBy } from "lodash";
import type React from "react";
import { useCallback, useMemo, useState } from "react";
import { Alert, TouchableOpacity, View } from "react-native";
import { HorizontalScroll } from "@/components/common/HorrizontalScroll";
import { Text } from "@/components/common/Text";
import { Tags } from "@/components/GenreTags";
import { dateOpts } from "@/components/jellyseerr/DetailFacts";
import { textShadowStyle } from "@/components/jellyseerr/discover/GenericSlideCard";
import JellyseerrStatusIcon from "@/components/jellyseerr/JellyseerrStatusIcon";
import { RoundButton } from "@/components/RoundButton";
import { useJellyseerr } from "@/hooks/useJellyseerr";
import {
MediaStatus,
MediaType,
} from "@/utils/jellyseerr/server/constants/media";
import type MediaRequest from "@/utils/jellyseerr/server/entity/MediaRequest";
import type Season from "@/utils/jellyseerr/server/entity/Season";
import type { MediaRequestBody } from "@/utils/jellyseerr/server/interfaces/api/requestInterfaces";
import type { MovieDetails } from "@/utils/jellyseerr/server/models/Movie";
import type { TvDetails } from "@/utils/jellyseerr/server/models/Tv";
import { Loader } from "../Loader";
const JellyseerrSeasonEpisodes: React.FC<{
@@ -70,12 +69,11 @@ const RenderItem = ({ item, index }: any) => {
const airDate = item.airDate;
if (airDate) {
const airDateObj = new Date(airDate);
if (new Date() < airDateObj) {
return airDateObj.toLocaleDateString(`${locale}-${region}`, dateOpts);
}
}
}, [item]);
}, [item, locale, region]);
return (
<View className='flex flex-col w-44 mt-2'>
@@ -91,7 +89,7 @@ const RenderItem = ({ item, index }: any) => {
cachePolicy={"memory-disk"}
contentFit='cover'
className='w-full h-full'
onError={(e) => {
onError={(_e) => {
setImageError(true);
}}
/>
@@ -127,7 +125,6 @@ const RenderItem = ({ item, index }: any) => {
{`S${item.seasonNumber}:E${item.episodeNumber}`}
</Text>
</View>
<Text numberOfLines={3} className='text-xs text-neutral-500 shrink'>
{item.overview}
</Text>
@@ -152,40 +149,35 @@ const JellyseerrSeasons: React.FC<{
hasAdvancedRequest,
onAdvancedRequest,
}) => {
if (!details) return null;
const { jellyseerrApi, requestMedia } = useJellyseerr();
const [seasonStates, setSeasonStates] = useState<{
[key: number]: boolean;
}>();
const [seasonStates, setSeasonStates] = useState<{ [key: number]: boolean }>(
{},
);
const seasons = useMemo(() => {
const mediaInfoSeasons = details?.mediaInfo?.seasons?.filter(
if (!details) return [];
const mediaInfoSeasons = details.mediaInfo?.seasons?.filter(
(s: Season) => s.seasonNumber !== 0,
);
const requestedSeasons = details?.mediaInfo?.requests?.flatMap(
(r: MediaRequest) => r.seasons,
);
return details.seasons?.map((season) => {
return {
const requestedSeasons =
details.mediaInfo?.requests?.flatMap((r: MediaRequest) => r.seasons) ??
[];
return (
details.seasons?.map((season) => ({
...season,
status:
// What our library status is
mediaInfoSeasons?.find(
(mediaSeason: Season) =>
mediaSeason.seasonNumber === season.seasonNumber,
)?.status ??
// What our request status is
requestedSeasons?.find(
(s: Season) => s.seasonNumber === season.seasonNumber,
)?.status ??
// Otherwise set it as unknown
MediaStatus.UNKNOWN,
};
});
})) ?? []
);
}, [details]);
const allSeasonsAvailable = useMemo(
() => seasons?.every((season) => season.status === MediaStatus.AVAILABLE),
() => seasons.every((season) => season.status === MediaStatus.AVAILABLE),
[seasons],
);
@@ -201,14 +193,20 @@ const JellyseerrSeasons: React.FC<{
)
.map((s) => s.seasonNumber),
};
if (hasAdvancedRequest) {
return onAdvancedRequest?.(body);
}
requestMedia(details.name, body, refetch);
}
}, [jellyseerrApi, seasons, details, hasAdvancedRequest, onAdvancedRequest]);
}, [
jellyseerrApi,
seasons,
details,
hasAdvancedRequest,
onAdvancedRequest,
requestMedia,
refetch,
]);
const promptRequestAll = useCallback(
() =>
@@ -231,24 +229,24 @@ const JellyseerrSeasons: React.FC<{
const requestSeason = useCallback(
async (canRequest: boolean, seasonNumber: number) => {
if (canRequest) {
if (canRequest && details) {
const body: MediaRequestBody = {
mediaId: details.id,
mediaType: MediaType.TV,
tvdbId: details.externalIds?.tvdbId,
seasons: [seasonNumber],
};
if (hasAdvancedRequest) {
return onAdvancedRequest?.(body);
}
requestMedia(`${details.name}, Season ${seasonNumber}`, body, refetch);
}
},
[requestMedia, hasAdvancedRequest, onAdvancedRequest],
[requestMedia, hasAdvancedRequest, onAdvancedRequest, refetch, details],
);
if (!details) return null;
if (isLoading)
return (
<View>
@@ -269,7 +267,7 @@ const JellyseerrSeasons: React.FC<{
return (
<FlashList
data={orderBy(
details.seasons.filter((s) => s.seasonNumber !== 0),
seasons.filter((s) => s.seasonNumber !== 0),
"seasonNumber",
"desc",
)}
@@ -314,9 +312,7 @@ const JellyseerrSeasons: React.FC<{
]}
/>
{[0].map(() => {
const canRequest =
seasons?.find((s) => s.seasonNumber === season.seasonNumber)
?.status === MediaStatus.UNKNOWN;
const canRequest = season.status === MediaStatus.UNKNOWN;
return (
<JellyseerrStatusIcon
key={0}
@@ -324,11 +320,7 @@ const JellyseerrSeasons: React.FC<{
requestSeason(canRequest, season.seasonNumber)
}
className={canRequest ? "bg-gray-700/40" : undefined}
mediaStatus={
seasons?.find(
(s) => s.seasonNumber === season.seasonNumber,
)?.status
}
mediaStatus={season.status}
showRequestIcon={canRequest}
/>
);

View File

@@ -1,4 +1,3 @@
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { Ionicons } from "@expo/vector-icons";
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
import { getItemsApi } from "@jellyfin/sdk/lib/utils/api";
@@ -6,6 +5,7 @@ import { useQuery } from "@tanstack/react-query";
import { useRouter } from "expo-router";
import { useAtom } from "jotai";
import { useMemo } from "react";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { Button } from "../Button";
interface Props extends React.ComponentProps<typeof Button> {

View File

@@ -1,18 +1,15 @@
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
import { getTvShowsApi } from "@jellyfin/sdk/lib/utils/api";
import { FlashList } from "@shopify/flash-list";
import { useQuery } from "@tanstack/react-query";
import { router } from "expo-router";
import { useAtom } from "jotai";
import type React from "react";
import { useTranslation } from "react-i18next";
import { TouchableOpacity, View } from "react-native";
import { View } from "react-native";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import ContinueWatchingPoster from "../ContinueWatchingPoster";
import { ItemCardText } from "../ItemCardText";
import { HorizontalScroll } from "../common/HorrizontalScroll";
import { Text } from "../common/Text";
import { TouchableItemRouter } from "../common/TouchableItemRouter";
import { ItemCardText } from "../ItemCardText";
export const NextUp: React.FC<{ seriesId: string }> = ({ seriesId }) => {
const [user] = useAtom(userAtom);

View File

@@ -1,7 +1,9 @@
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
import { useEffect, useMemo } from "react";
import { Platform, TouchableOpacity, View } from "react-native";
const DropdownMenu = !Platform.isTV ? require("zeego/dropdown-menu") : null;
import { t } from "i18next";
import { Text } from "../common/Text";
@@ -30,7 +32,7 @@ export const SeasonDropdown: React.FC<Props> = ({
state,
onSelect,
}) => {
if (Platform.isTV) return null;
const isTv = Platform.isTV;
const keys = useMemo<SeasonKeys>(
() =>
@@ -50,10 +52,11 @@ export const SeasonDropdown: React.FC<Props> = ({
const seasonIndex = useMemo(
() => state[(item[keys.id] as string) ?? ""],
[state],
[state, item, keys],
);
useEffect(() => {
if (isTv) return;
if (seasons && seasons.length > 0 && seasonIndex === undefined) {
let initialIndex: number | undefined;
@@ -79,16 +82,26 @@ export const SeasonDropdown: React.FC<Props> = ({
const initialSeason = seasons.find(
(season: any) => season[keys.index] === initialIndex,
);
if (initialSeason) onSelect(initialSeason!);
else throw Error("Initial index could not be found!");
}
}
}, [seasons, seasonIndex, item[keys.id], initialSeasonIndex]);
}, [
isTv,
seasons,
seasonIndex,
item,
item[keys.id],
initialSeasonIndex,
keys,
onSelect,
]);
const sortByIndex = (a: BaseItemDto, b: BaseItemDto) =>
Number(a[keys.index]) - Number(b[keys.index]);
if (isTv) return null;
return (
<DropdownMenu.Root>
<DropdownMenu.Trigger>

View File

@@ -1,17 +1,17 @@
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { getUserItemData } from "@/utils/jellyfin/user-library/getUserItemData";
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { router } from "expo-router";
import { useAtom } from "jotai";
import { useEffect, useMemo, useRef } from "react";
import { TouchableOpacity, View, type ViewProps } from "react-native";
import { TouchableOpacity, type ViewProps } from "react-native";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { getUserItemData } from "@/utils/jellyfin/user-library/getUserItemData";
import ContinueWatchingPoster from "../ContinueWatchingPoster";
import { ItemCardText } from "../ItemCardText";
import {
HorizontalScroll,
type HorizontalScrollRef,
} from "../common/HorrizontalScroll";
import { ItemCardText } from "../ItemCardText";
interface Props extends ViewProps {
item?: BaseItemDto | null;
@@ -123,7 +123,7 @@ export const SeasonEpisodesCarousel: React.FC<Props> = ({
data={episodes}
extraData={item}
loading={loading || isLoading || isFetching}
renderItem={(_item, idx) => (
renderItem={(_item, _idx) => (
<TouchableOpacity
key={_item.Id}
onPress={() => {

View File

@@ -1,11 +1,4 @@
import {
SeasonDropdown,
type SeasonIndexState,
} from "@/components/series/SeasonDropdown";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { getUserItemData } from "@/utils/jellyfin/user-library/getUserItemData";
import { runtimeTicksToSeconds } from "@/utils/time";
import { Ionicons, MaterialCommunityIcons } from "@expo/vector-icons";
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";
@@ -13,12 +6,19 @@ import { atom, useAtom } from "jotai";
import { useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { View } from "react-native";
import {
SeasonDropdown,
type SeasonIndexState,
} from "@/components/series/SeasonDropdown";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { getUserItemData } from "@/utils/jellyfin/user-library/getUserItemData";
import { runtimeTicksToSeconds } from "@/utils/time";
import ContinueWatchingPoster from "../ContinueWatchingPoster";
import { Text } from "../common/Text";
import { TouchableItemRouter } from "../common/TouchableItemRouter";
import { DownloadItems, DownloadSingleItem } from "../DownloadItem";
import { Loader } from "../Loader";
import { PlayedStatus } from "../PlayedStatus";
import { Text } from "../common/Text";
import { TouchableItemRouter } from "../common/TouchableItemRouter";
type Props = {
item: BaseItemDto;

View File

@@ -1,5 +1,3 @@
import type { MovieDetails } from "@/utils/jellyseerr/server/models/Movie";
import type { TvDetails } from "@/utils/jellyseerr/server/models/Tv";
import { Ionicons } from "@expo/vector-icons";
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client";
import { useCallback, useMemo } from "react";
@@ -10,6 +8,8 @@ import {
View,
type ViewProps,
} from "react-native";
import type { MovieDetails } from "@/utils/jellyseerr/server/models/Movie";
import type { TvDetails } from "@/utils/jellyseerr/server/models/Tv";
interface Props extends ViewProps {
item: BaseItemDto | MovieDetails | TvDetails;

View File

@@ -1,8 +1,8 @@
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client";
import { useMemo } from "react";
import { View } from "react-native";
import { Ratings } from "../Ratings";
import { Text } from "../common/Text";
import { Ratings } from "../Ratings";
import { ItemActions } from "./SeriesActions";
interface Props {

View File

@@ -1,8 +1,9 @@
const DropdownMenu = !Platform.isTV ? require("zeego/dropdown-menu") : null;
import { APP_LANGUAGES } from "@/i18n";
import { useSettings } from "@/utils/atoms/settings";
import { useTranslation } from "react-i18next";
import { Platform, TouchableOpacity, View, type ViewProps } from "react-native";
import { APP_LANGUAGES } from "@/i18n";
import { useSettings } from "@/utils/atoms/settings";
import { Text } from "../common/Text";
import { ListGroup } from "../list/ListGroup";
import { ListItem } from "../list/ListItem";
@@ -10,10 +11,11 @@ import { ListItem } from "../list/ListItem";
interface Props extends ViewProps {}
export const AppLanguageSelector: React.FC<Props> = ({ ...props }) => {
if (Platform.isTV) return null;
const isTv = Platform.isTV;
const [settings, updateSettings] = useSettings();
const { t } = useTranslation();
if (isTv) return null;
if (!settings) return null;
return (

View File

@@ -1,9 +1,11 @@
import { Platform, TouchableOpacity, View, type ViewProps } from "react-native";
const DropdownMenu = !Platform.isTV ? require("zeego/dropdown-menu") : null;
import { useSettings } from "@/utils/atoms/settings";
import { Ionicons } from "@expo/vector-icons";
import { useTranslation } from "react-i18next";
import { Switch } from "react-native-gesture-handler";
import { useSettings } from "@/utils/atoms/settings";
import { Text } from "../common/Text";
import { ListGroup } from "../list/ListGroup";
import { ListItem } from "../list/ListItem";
@@ -12,13 +14,15 @@ import { useMedia } from "./MediaContext";
interface Props extends ViewProps {}
export const AudioToggles: React.FC<Props> = ({ ...props }) => {
if (Platform.isTV) return null;
const isTv = Platform.isTV;
const media = useMedia();
const [_, __, pluginSettings] = useSettings();
const { settings, updateSettings } = media;
const cultures = media.cultures;
const { t } = useTranslation();
if (isTv) return null;
if (!settings) return null;
return (

View File

@@ -1,5 +1,5 @@
import { useSettings } from "@/utils/atoms/settings";
import { Switch, View } from "react-native";
import { useSettings } from "@/utils/atoms/settings";
import { ListGroup } from "../list/ListGroup";
import { ListItem } from "../list/ListItem";

View File

@@ -1,14 +1,13 @@
import { useSessions, type useSessionsProps } from "@/hooks/useSessions";
import { useSettings } from "@/utils/atoms/settings";
import { useRouter } from "expo-router";
import React from "react";
import { useTranslation } from "react-i18next";
import { View } from "react-native";
import { useSessions, type useSessionsProps } from "@/hooks/useSessions";
import { useSettings } from "@/utils/atoms/settings";
import { ListGroup } from "../list/ListGroup";
import { ListItem } from "../list/ListItem";
export const Dashboard = () => {
const [settings, updateSettings] = useSettings();
const [settings, _updateSettings] = useSettings();
const { sessions = [], isLoading } = useSessions({} as useSessionsProps);
const router = useRouter();

View File

@@ -1,5 +1,5 @@
import { Text } from "@/components/common/Text";
import { View, type ViewProps } from "react-native";
import { Text } from "@/components/common/Text";
const DisabledSetting: React.FC<
{ disabled: boolean; showText?: boolean; text?: string } & ViewProps

View File

@@ -1,3 +1,8 @@
import { Ionicons } from "@expo/vector-icons";
import { useQueryClient } from "@tanstack/react-query";
import { useRouter } from "expo-router";
import { useMemo } from "react";
import { Platform, Switch, TouchableOpacity } from "react-native";
import { Stepper } from "@/components/inputs/Stepper";
import { useDownload } from "@/providers/DownloadProvider";
import {
@@ -5,14 +10,11 @@ import {
type Settings,
useSettings,
} from "@/utils/atoms/settings";
import { Ionicons } from "@expo/vector-icons";
import { useQueryClient } from "@tanstack/react-query";
import { useRouter } from "expo-router";
import React, { useMemo } from "react";
import { Platform, Switch, TouchableOpacity } from "react-native";
const DropdownMenu = !Platform.isTV ? require("zeego/dropdown-menu") : null;
import DisabledSetting from "@/components/settings/DisabledSetting";
import { useTranslation } from "react-i18next";
import DisabledSetting from "@/components/settings/DisabledSetting";
import { Text } from "../common/Text";
import { ListGroup } from "../list/ListGroup";
import { ListItem } from "../list/ListItem";

View File

@@ -1,5 +1,3 @@
import React from "react";
export default function DownloadSettings({ ...props }) {
return <></>;
return null;
}

View File

@@ -1,15 +1,3 @@
import { Button } from "@/components/Button";
import { Loader } from "@/components/Loader";
import { Text } from "@/components/common/Text";
import { LargeMovieCarousel } from "@/components/home/LargeMovieCarousel";
import { ScrollingCollectionList } from "@/components/home/ScrollingCollectionList";
import { MediaListSection } from "@/components/medialists/MediaListSection";
import { Colors } from "@/constants/Colors";
import { useInvalidatePlaybackProgressCache } from "@/hooks/useRevalidatePlaybackProgressCache";
import { useDownload } from "@/providers/DownloadProvider";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { useSettings } from "@/utils/atoms/settings";
import { eventBus } from "@/utils/eventBus";
import { Feather, Ionicons } from "@expo/vector-icons";
import type { Api } from "@jellyfin/sdk";
import type {
@@ -25,12 +13,7 @@ import {
} from "@jellyfin/sdk/lib/utils/api";
import NetInfo from "@react-native-community/netinfo";
import { type QueryFunction, useQuery } from "@tanstack/react-query";
import {
useNavigation,
usePathname,
useRouter,
useSegments,
} from "expo-router";
import { useNavigation, useRouter, useSegments } from "expo-router";
import { useAtomValue } from "jotai";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
@@ -43,6 +26,18 @@ import {
View,
} from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { Button } from "@/components/Button";
import { Text } from "@/components/common/Text";
import { LargeMovieCarousel } from "@/components/home/LargeMovieCarousel";
import { ScrollingCollectionList } from "@/components/home/ScrollingCollectionList";
import { Loader } from "@/components/Loader";
import { MediaListSection } from "@/components/medialists/MediaListSection";
import { Colors } from "@/constants/Colors";
import { useInvalidatePlaybackProgressCache } from "@/hooks/useRevalidatePlaybackProgressCache";
import { useDownload } from "@/providers/DownloadProvider";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { useSettings } from "@/utils/atoms/settings";
import { eventBus } from "@/utils/eventBus";
type ScrollingCollectionListSection = {
type: "ScrollingCollectionList";
@@ -71,9 +66,9 @@ export const HomeIndex = () => {
const [loading, setLoading] = useState(false);
const [
settings,
updateSettings,
pluginSettings,
setPluginSettings,
_updateSettings,
_pluginSettings,
_setPluginSettings,
refreshStreamyfinPluginSettings,
] = useSettings();
@@ -114,7 +109,7 @@ export const HomeIndex = () => {
}, [downloadedFiles, navigation, router]);
useEffect(() => {
cleanCacheDirectory().catch((e) =>
cleanCacheDirectory().catch((_e) =>
console.error("Something went wrong cleaning cache directory"),
);
}, []);
@@ -232,166 +227,164 @@ export const HomeIndex = () => {
[api, user?.Id],
);
let sections: Section[] = [];
if (!settings?.home || !settings?.home?.sections) {
sections = useMemo(() => {
if (!api || !user?.Id) return [];
// Always call useMemo() at the top-level, using computed dependencies for both "default"/custom sections
const defaultSections = useMemo(() => {
if (!api || !user?.Id) return [];
const latestMediaViews = collections.map((c) => {
const includeItemTypes: BaseItemKind[] =
c.CollectionType === "tvshows" ? ["Series"] : ["Movie"];
const title = t("home.recently_added_in", { libraryName: c.Name });
const queryKey = [
"home",
`recentlyAddedIn${c.CollectionType}`,
user?.Id!,
c.Id!,
];
return createCollectionConfig(
title || "",
queryKey,
includeItemTypes,
c.Id,
);
});
const ss: Section[] = [
{
title: t("home.continue_watching"),
queryKey: ["home", "resumeItems"],
queryFn: async () =>
(
await getItemsApi(api).getResumeItems({
userId: user.Id,
enableImageTypes: ["Primary", "Backdrop", "Thumb"],
includeItemTypes: ["Movie", "Series", "Episode"],
})
).data.Items || [],
type: "ScrollingCollectionList",
orientation: "horizontal",
},
{
title: t("home.next_up"),
queryKey: ["home", "nextUp-all"],
queryFn: async () =>
(
await getTvShowsApi(api).getNextUp({
userId: user?.Id,
fields: ["MediaSourceCount"],
limit: 20,
enableImageTypes: ["Primary", "Backdrop", "Thumb"],
enableResumable: false,
})
).data.Items || [],
type: "ScrollingCollectionList",
orientation: "horizontal",
},
...latestMediaViews,
// ...(mediaListCollections?.map(
// (ml) =>
// ({
// title: ml.Name,
// queryKey: ["home", "mediaList", ml.Id!],
// queryFn: async () => ml,
// type: "MediaListSection",
// orientation: "vertical",
// } as Section)
// ) || []),
{
title: t("home.suggested_movies"),
queryKey: ["home", "suggestedMovies", user?.Id],
queryFn: async () =>
(
await getSuggestionsApi(api).getSuggestions({
userId: user?.Id,
limit: 10,
mediaType: ["Video"],
type: ["Movie"],
})
).data.Items || [],
type: "ScrollingCollectionList",
orientation: "vertical",
},
{
title: t("home.suggested_episodes"),
queryKey: ["home", "suggestedEpisodes", user?.Id],
queryFn: async () => {
try {
const suggestions = await getSuggestions(api, user.Id);
const nextUpPromises = suggestions.map((series) =>
getNextUp(api, user.Id, series.Id),
);
const nextUpResults = await Promise.all(nextUpPromises);
return nextUpResults.filter((item) => item !== null) || [];
} catch (error) {
console.error("Error fetching data:", error);
return [];
}
},
type: "ScrollingCollectionList",
orientation: "horizontal",
},
const latestMediaViews = collections.map((c) => {
const includeItemTypes: BaseItemKind[] =
c.CollectionType === "tvshows" ? ["Series"] : ["Movie"];
const title = t("home.recently_added_in", { libraryName: c.Name });
const queryKey = [
"home",
`recentlyAddedIn${c.CollectionType}`,
user?.Id!,
c.Id!,
];
return ss;
}, [api, user?.Id, collections]);
} else {
sections = useMemo(() => {
if (!api || !user?.Id) return [];
const ss: Section[] = [];
return createCollectionConfig(
title || "",
queryKey,
includeItemTypes,
c.Id,
);
});
for (const key in settings.home?.sections) {
// @ts-expect-error
const section = settings.home?.sections[key];
const id = section.title || key;
ss.push({
title: id,
queryKey: ["home", id],
queryFn: async () => {
if (section.items) {
const response = await getItemsApi(api).getItems({
userId: user?.Id,
limit: section.items?.limit || 25,
recursive: true,
includeItemTypes: section.items?.includeItemTypes,
sortBy: section.items?.sortBy,
sortOrder: section.items?.sortOrder,
filters: section.items?.filters,
parentId: section.items?.parentId,
});
return response.data.Items || [];
}
if (section.nextUp) {
const response = await getTvShowsApi(api).getNextUp({
userId: user?.Id,
fields: ["MediaSourceCount"],
limit: section.items?.limit || 25,
enableImageTypes: ["Primary", "Backdrop", "Thumb"],
enableResumable: section.items?.enableResumable,
enableRewatching: section.items?.enableRewatching,
});
return response.data.Items || [];
}
const ss: Section[] = [
{
title: t("home.continue_watching"),
queryKey: ["home", "resumeItems"],
queryFn: async () =>
(
await getItemsApi(api).getResumeItems({
userId: user.Id,
enableImageTypes: ["Primary", "Backdrop", "Thumb"],
includeItemTypes: ["Movie", "Series", "Episode"],
})
).data.Items || [],
type: "ScrollingCollectionList",
orientation: "horizontal",
},
{
title: t("home.next_up"),
queryKey: ["home", "nextUp-all"],
queryFn: async () =>
(
await getTvShowsApi(api).getNextUp({
userId: user?.Id,
fields: ["MediaSourceCount"],
limit: 20,
enableImageTypes: ["Primary", "Backdrop", "Thumb"],
enableResumable: false,
})
).data.Items || [],
type: "ScrollingCollectionList",
orientation: "horizontal",
},
...latestMediaViews,
// ...(mediaListCollections?.map(
// (ml) =>
// ({
// title: ml.Name,
// queryKey: ["home", "mediaList", ml.Id!],
// queryFn: async () => ml,
// type: "MediaListSection",
// orientation: "vertical",
// } as Section)
// ) || []),
{
title: t("home.suggested_movies"),
queryKey: ["home", "suggestedMovies", user?.Id],
queryFn: async () =>
(
await getSuggestionsApi(api).getSuggestions({
userId: user?.Id,
limit: 10,
mediaType: ["Video"],
type: ["Movie"],
})
).data.Items || [],
type: "ScrollingCollectionList",
orientation: "vertical",
},
{
title: t("home.suggested_episodes"),
queryKey: ["home", "suggestedEpisodes", user?.Id],
queryFn: async () => {
try {
const suggestions = await getSuggestions(api, user.Id);
const nextUpPromises = suggestions.map((series) =>
getNextUp(api, user.Id, series.Id),
);
const nextUpResults = await Promise.all(nextUpPromises);
if (section.latest) {
const response = await getUserLibraryApi(api).getLatestMedia({
userId: user?.Id,
includeItemTypes: section.latest?.includeItemTypes,
limit: section.latest?.limit || 25,
isPlayed: section.latest?.isPlayed,
groupItems: section.latest?.groupItems,
});
return response.data || [];
}
return nextUpResults.filter((item) => item !== null) || [];
} catch (error) {
console.error("Error fetching data:", error);
return [];
},
type: "ScrollingCollectionList",
orientation: section?.orientation || "vertical",
});
}
return ss;
}, [api, user?.Id, settings.home?.sections]);
}
}
},
type: "ScrollingCollectionList",
orientation: "horizontal",
},
];
return ss;
}, [api, user?.Id, collections, t, createCollectionConfig]);
const customSections = useMemo(() => {
if (!api || !user?.Id || !settings?.home?.sections) return [];
const ss: Section[] = [];
for (const key in settings.home?.sections) {
// @ts-expect-error
const section = settings.home?.sections[key];
const id = section.title || key;
ss.push({
title: id,
queryKey: ["home", id],
queryFn: async () => {
if (section.items) {
const response = await getItemsApi(api).getItems({
userId: user?.Id,
limit: section.items?.limit || 25,
recursive: true,
includeItemTypes: section.items?.includeItemTypes,
sortBy: section.items?.sortBy,
sortOrder: section.items?.sortOrder,
filters: section.items?.filters,
parentId: section.items?.parentId,
});
return response.data.Items || [];
}
if (section.nextUp) {
const response = await getTvShowsApi(api).getNextUp({
userId: user?.Id,
fields: ["MediaSourceCount"],
limit: section.items?.limit || 25,
enableImageTypes: ["Primary", "Backdrop", "Thumb"],
enableResumable: section.items?.enableResumable,
enableRewatching: section.items?.enableRewatching,
});
return response.data.Items || [];
}
if (section.latest) {
const response = await getUserLibraryApi(api).getLatestMedia({
userId: user?.Id,
includeItemTypes: section.latest?.includeItemTypes,
limit: section.latest?.limit || 25,
isPlayed: section.latest?.isPlayed,
groupItems: section.latest?.groupItems,
});
return response.data || [];
}
return [];
},
type: "ScrollingCollectionList",
orientation: section?.orientation || "vertical",
});
}
return ss;
}, [api, user?.Id, settings?.home?.sections]);
const sections = settings?.home?.sections ? customSections : defaultSections;
if (isConnected === false) {
return (

View File

@@ -1,12 +1,12 @@
import { JellyseerrApi, useJellyseerr } from "@/hooks/useJellyseerr";
import { userAtom } from "@/providers/JellyfinProvider";
import { useSettings } from "@/utils/atoms/settings";
import { useMutation } from "@tanstack/react-query";
import { useAtom } from "jotai";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { View } from "react-native";
import { toast } from "sonner-native";
import { JellyseerrApi, useJellyseerr } from "@/hooks/useJellyseerr";
import { userAtom } from "@/providers/JellyfinProvider";
import { useSettings } from "@/utils/atoms/settings";
import { Button } from "../Button";
import { Input } from "../common/Input";
import { Text } from "../common/Text";
@@ -24,7 +24,7 @@ export const JellyseerrSettings = () => {
const { t } = useTranslation();
const [user] = useAtom(userAtom);
const [settings, updateSettings, pluginSettings] = useSettings();
const [settings, updateSettings, _pluginSettings] = useSettings();
const [jellyseerrPassword, setJellyseerrPassword] = useState<
string | undefined

Some files were not shown because too many files have changed in this diff Show More