diff --git a/.eslintrc.json b/.eslintrc.json deleted file mode 100644 index 957cd154..00000000 --- a/.eslintrc.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "extends": ["next/core-web-vitals"] -} diff --git a/.gitignore b/.gitignore index 3bb03471..afbe086e 100644 --- a/.gitignore +++ b/.gitignore @@ -10,7 +10,6 @@ npm-debug.* *.orig.* web-build/ modules/vlc-player/android/build -modules/vlc-player/android/.gradle # macOS .DS_Store diff --git a/app.json b/app.json index 441b11cb..4fa7289f 100644 --- a/app.json +++ b/app.json @@ -113,7 +113,6 @@ } } ], - ["react-native-bottom-tabs"], ["./plugins/withChangeNativeAndroidTextToWhite.js"], ["./plugins/withAndroidManifest.js"], ["./plugins/withTrustLocalCerts.js"], diff --git a/app/(auth)/(tabs)/(custom-links)/index.tsx b/app/(auth)/(tabs)/(custom-links)/index.tsx index d6b34fb2..7dad5453 100644 --- a/app/(auth)/(tabs)/(custom-links)/index.tsx +++ b/app/(auth)/(tabs)/(custom-links)/index.tsx @@ -1,13 +1,12 @@ +import Ionicons from "@expo/vector-icons/Ionicons"; +import { useAtom } from "jotai/index"; +import { useCallback, useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { FlatList, Platform, TouchableOpacity, View } from "react-native"; +import { useSafeAreaInsets } from "react-native-safe-area-context"; import { Text } from "@/components/common/Text"; import { ListItem } from "@/components/list/ListItem"; import { apiAtom } from "@/providers/JellyfinProvider"; -import Ionicons from "@expo/vector-icons/Ionicons"; -import { useAtom } from "jotai/index"; -import React, { useCallback, useEffect, useState } from "react"; -import { useTranslation } from "react-i18next"; -import { Platform } from "react-native"; -import { FlatList, TouchableOpacity, View } from "react-native"; -import { useSafeAreaInsets } from "react-native-safe-area-context"; const WebBrowser = !Platform.isTV ? require("expo-web-browser") : null; diff --git a/app/(auth)/(tabs)/(favorites)/_layout.tsx b/app/(auth)/(tabs)/(favorites)/_layout.tsx index 82df4a12..1e1406fd 100644 --- a/app/(auth)/(tabs)/(favorites)/_layout.tsx +++ b/app/(auth)/(tabs)/(favorites)/_layout.tsx @@ -1,7 +1,7 @@ -import { nestedTabPageScreenOptions } from "@/components/stacks/NestedTabPageStack"; import { Stack } from "expo-router"; import { useTranslation } from "react-i18next"; import { Platform } from "react-native"; +import { nestedTabPageScreenOptions } from "@/components/stacks/NestedTabPageStack"; export default function SearchLayout() { const { t } = useTranslation(); diff --git a/app/(auth)/(tabs)/(favorites)/index.tsx b/app/(auth)/(tabs)/(favorites)/index.tsx index 6fa7d63a..abab30a9 100644 --- a/app/(auth)/(tabs)/(favorites)/index.tsx +++ b/app/(auth)/(tabs)/(favorites)/index.tsx @@ -1,8 +1,8 @@ -import { Favorites } from "@/components/home/Favorites"; -import { useInvalidatePlaybackProgressCache } from "@/hooks/useRevalidatePlaybackProgressCache"; -import React, { useCallback, useState } from "react"; +import { useCallback, useState } from "react"; import { RefreshControl, ScrollView, View } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; +import { Favorites } from "@/components/home/Favorites"; +import { useInvalidatePlaybackProgressCache } from "@/hooks/useRevalidatePlaybackProgressCache"; export default function favorites() { const invalidateCache = useInvalidatePlaybackProgressCache(); diff --git a/app/(auth)/(tabs)/(home)/_layout.tsx b/app/(auth)/(tabs)/(home)/_layout.tsx index 86ae2cbe..c3c2f1b3 100644 --- a/app/(auth)/(tabs)/(home)/_layout.tsx +++ b/app/(auth)/(tabs)/(home)/_layout.tsx @@ -1,15 +1,17 @@ -import { nestedTabPageScreenOptions } from "@/components/stacks/NestedTabPageStack"; import { Feather, Ionicons } from "@expo/vector-icons"; import { Stack, useRouter } from "expo-router"; import { useTranslation } from "react-i18next"; import { Platform, TouchableOpacity, View } from "react-native"; +import { nestedTabPageScreenOptions } from "@/components/stacks/NestedTabPageStack"; + const Chromecast = Platform.isTV ? null : require("@/components/Chromecast"); + +import { useAtom } from "jotai"; import { useSessions, type useSessionsProps } from "@/hooks/useSessions"; import { userAtom } from "@/providers/JellyfinProvider"; -import { useAtom } from "jotai"; export default function IndexLayout() { - const router = useRouter(); + const _router = useRouter(); const [user] = useAtom(userAtom); const { t } = useTranslation(); diff --git a/app/(auth)/(tabs)/(home)/downloads/[seriesId].tsx b/app/(auth)/(tabs)/(home)/downloads/[seriesId].tsx index 9e617d67..023846b4 100644 --- a/app/(auth)/(tabs)/(home)/downloads/[seriesId].tsx +++ b/app/(auth)/(tabs)/(home)/downloads/[seriesId].tsx @@ -1,3 +1,8 @@ +import { Ionicons } from "@expo/vector-icons"; +import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; +import { router, useLocalSearchParams, useNavigation } from "expo-router"; +import { useCallback, useEffect, useMemo, useState } from "react"; +import { Alert, ScrollView, TouchableOpacity, View } from "react-native"; import { Text } from "@/components/common/Text"; import { EpisodeCard } from "@/components/downloads/EpisodeCard"; import { @@ -6,11 +11,6 @@ import { } from "@/components/series/SeasonDropdown"; import { useDownload } from "@/providers/DownloadProvider"; import { storage } from "@/utils/mmkv"; -import { Ionicons } from "@expo/vector-icons"; -import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; -import { router, useLocalSearchParams, useNavigation } from "expo-router"; -import React, { useCallback, useEffect, useMemo, useState } from "react"; -import { Alert, ScrollView, TouchableOpacity, View } from "react-native"; export default function page() { const navigation = useNavigation(); diff --git a/app/(auth)/(tabs)/(home)/downloads/index.tsx b/app/(auth)/(tabs)/(home)/downloads/index.tsx index 52c94c06..1f2cedb9 100644 --- a/app/(auth)/(tabs)/(home)/downloads/index.tsx +++ b/app/(auth)/(tabs)/(home)/downloads/index.tsx @@ -1,3 +1,17 @@ +import { Ionicons } from "@expo/vector-icons"; +import { + BottomSheetBackdrop, + type BottomSheetBackdropProps, + BottomSheetModal, + BottomSheetView, +} from "@gorhom/bottom-sheet"; +import { useNavigation, useRouter } from "expo-router"; +import { useAtom } from "jotai"; +import { useEffect, useMemo, useRef, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { Alert, ScrollView, TouchableOpacity, View } from "react-native"; +import { useSafeAreaInsets } from "react-native-safe-area-context"; +import { toast } from "sonner-native"; import { Button } from "@/components/Button"; import { Text } from "@/components/common/Text"; import { ActiveDownloads } from "@/components/downloads/ActiveDownloads"; @@ -8,36 +22,47 @@ import { type DownloadedItem, useDownload } from "@/providers/DownloadProvider"; import { queueAtom } from "@/utils/atoms/queue"; import { DownloadMethod, useSettings } from "@/utils/atoms/settings"; import { writeToLog } from "@/utils/log"; -import { Ionicons } from "@expo/vector-icons"; -import { - BottomSheetBackdrop, - type BottomSheetBackdropProps, - BottomSheetModal, - BottomSheetView, -} from "@gorhom/bottom-sheet"; -import { useNavigation, useRouter } from "expo-router"; -import { t } from "i18next"; -import { useAtom } from "jotai"; -import React, { useEffect, useMemo, useRef } from "react"; -import { useTranslation } from "react-i18next"; -import { Alert, ScrollView, TouchableOpacity, View } from "react-native"; -import { useSafeAreaInsets } from "react-native-safe-area-context"; -import { toast } from "sonner-native"; export default function page() { const navigation = useNavigation(); const { t } = useTranslation(); const [queue, setQueue] = useAtom(queueAtom); - const { removeProcess, downloadedFiles, deleteFileByType } = useDownload(); + const { removeProcess, downloadedFiles, deleteFileByType, deleteAllFiles } = + useDownload(); const router = useRouter(); const [settings] = useSettings(); const bottomSheetModalRef = useRef(null); + const [showMigration, setShowMigration] = useState(false); + + const insets = useSafeAreaInsets(); + + const migration_20241124 = () => { + Alert.alert( + t("home.downloads.new_app_version_requires_re_download"), + t("home.downloads.new_app_version_requires_re_download_description"), + [ + { + text: t("home.downloads.back"), + onPress: () => setShowMigration(false) || router.back(), + }, + { + text: t("home.downloads.delete"), + style: "destructive", + onPress: async () => { + await deleteAllFiles(); + setShowMigration(false); + }, + }, + ], + ); + }; + const movies = useMemo(() => { try { return downloadedFiles?.filter((f) => f.item.Type === "Movie") || []; } catch { - migration_20241124(); + setShowMigration(true); return []; } }, [downloadedFiles]); @@ -54,13 +79,11 @@ export default function page() { }); return Object.values(series); } catch { - migration_20241124(); + setShowMigration(true); return []; } }, [downloadedFiles]); - const insets = useSafeAreaInsets(); - useEffect(() => { navigation.setOptions({ headerRight: () => ( @@ -71,6 +94,12 @@ export default function page() { }); }, [downloadedFiles]); + useEffect(() => { + if (showMigration) { + migration_20241124(); + } + }, [showMigration]); + const deleteMovies = () => deleteFileByType("Movie") .then(() => @@ -249,23 +278,3 @@ export default function page() { ); } - -function migration_20241124() { - const router = useRouter(); - const { deleteAllFiles } = useDownload(); - Alert.alert( - t("home.downloads.new_app_version_requires_re_download"), - t("home.downloads.new_app_version_requires_re_download_description"), - [ - { - text: t("home.downloads.back"), - onPress: () => router.back(), - }, - { - text: t("home.downloads.delete"), - style: "destructive", - onPress: async () => await deleteAllFiles(), - }, - ], - ); -} diff --git a/app/(auth)/(tabs)/(home)/intro/page.tsx b/app/(auth)/(tabs)/(home)/intro/page.tsx index 6b914dd8..790ecd02 100644 --- a/app/(auth)/(tabs)/(home)/intro/page.tsx +++ b/app/(auth)/(tabs)/(home)/intro/page.tsx @@ -1,12 +1,12 @@ -import { Button } from "@/components/Button"; -import { Text } from "@/components/common/Text"; -import { storage } from "@/utils/mmkv"; import { Feather, Ionicons } from "@expo/vector-icons"; import { Image } from "expo-image"; import { useFocusEffect, useRouter } from "expo-router"; import { useCallback } from "react"; import { useTranslation } from "react-i18next"; import { Linking, TouchableOpacity, View } from "react-native"; +import { Button } from "@/components/Button"; +import { Text } from "@/components/common/Text"; +import { storage } from "@/utils/mmkv"; export default function page() { const router = useRouter(); diff --git a/app/(auth)/(tabs)/(home)/sessions/index.tsx b/app/(auth)/(tabs)/(home)/sessions/index.tsx index 012207bc..cdcf058a 100644 --- a/app/(auth)/(tabs)/(home)/sessions/index.tsx +++ b/app/(auth)/(tabs)/(home)/sessions/index.tsx @@ -1,19 +1,4 @@ -import { Badge } from "@/components/Badge"; -import { Loader } from "@/components/Loader"; -import { Text } from "@/components/common/Text"; -import Poster from "@/components/posters/Poster"; -import { useInterval } from "@/hooks/useInterval"; -import { useSessions, type useSessionsProps } from "@/hooks/useSessions"; -import { apiAtom } from "@/providers/JellyfinProvider"; -import { formatBitrate } from "@/utils/bitrate"; -import { getPrimaryImageUrl } from "@/utils/jellyfin/image/getPrimaryImageUrl"; -import { formatTimeString } from "@/utils/time"; -import { - AntDesign, - Entypo, - Ionicons, - MaterialCommunityIcons, -} from "@expo/vector-icons"; +import { Ionicons, MaterialCommunityIcons } from "@expo/vector-icons"; import { HardwareAccelerationType, type SessionInfoDto, @@ -26,10 +11,19 @@ import { getSessionApi } from "@jellyfin/sdk/lib/utils/api/session-api"; import { FlashList } from "@shopify/flash-list"; import { useQuery } from "@tanstack/react-query"; import { useAtomValue } from "jotai"; -import { get } from "lodash"; -import React, { useEffect, useMemo, useState } from "react"; +import { useEffect, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { TouchableOpacity, View } from "react-native"; +import { Badge } from "@/components/Badge"; +import { Text } from "@/components/common/Text"; +import { Loader } from "@/components/Loader"; +import Poster from "@/components/posters/Poster"; +import { useInterval } from "@/hooks/useInterval"; +import { useSessions, type useSessionsProps } from "@/hooks/useSessions"; +import { apiAtom } from "@/providers/JellyfinProvider"; +import { formatBitrate } from "@/utils/bitrate"; +import { getPrimaryImageUrl } from "@/utils/jellyfin/image/getPrimaryImageUrl"; +import { formatTimeString } from "@/utils/time"; export default function page() { const { sessions, isLoading } = useSessions({} as useSessionsProps); @@ -454,20 +448,18 @@ const TranscodingStreamView = ({ {isTranscoding && transcodeProperties ? ( - <> - - - - - - - - - + + + + + + + + ) : null} ); diff --git a/app/(auth)/(tabs)/(home)/settings.tsx b/app/(auth)/(tabs)/(home)/settings.tsx index c7d9618e..3c68024f 100644 --- a/app/(auth)/(tabs)/(home)/settings.tsx +++ b/app/(auth)/(tabs)/(home)/settings.tsx @@ -1,3 +1,9 @@ +import { useNavigation, useRouter } from "expo-router"; +import { t } from "i18next"; +import { useAtom } from "jotai"; +import { useEffect } from "react"; +import { ScrollView, TouchableOpacity, View } from "react-native"; +import { useSafeAreaInsets } from "react-native-safe-area-context"; import { Text } from "@/components/common/Text"; import { ListGroup } from "@/components/list/ListGroup"; import { ListItem } from "@/components/list/ListItem"; @@ -14,21 +20,14 @@ import { StorageSettings } from "@/components/settings/StorageSettings"; import { SubtitleToggles } from "@/components/settings/SubtitleToggles"; import { UserInfo } from "@/components/settings/UserInfo"; import { useHaptic } from "@/hooks/useHaptic"; -import { useJellyfin } from "@/providers/JellyfinProvider"; -import { userAtom } from "@/providers/JellyfinProvider"; +import { useJellyfin, userAtom } from "@/providers/JellyfinProvider"; import { clearLogs } from "@/utils/log"; import { storage } from "@/utils/mmkv"; -import { useNavigation, useRouter } from "expo-router"; -import { t } from "i18next"; -import { useAtom } from "jotai"; -import React, { useEffect } from "react"; -import { ScrollView, Switch, TouchableOpacity, View } from "react-native"; -import { useSafeAreaInsets } from "react-native-safe-area-context"; export default function settings() { const router = useRouter(); const insets = useSafeAreaInsets(); - const [user] = useAtom(userAtom); + const [_user] = useAtom(userAtom); const { logout } = useJellyfin(); const successHapticFeedback = useHaptic("success"); diff --git a/app/(auth)/(tabs)/(home)/settings/hide-libraries/page.tsx b/app/(auth)/(tabs)/(home)/settings/hide-libraries/page.tsx index a90f7ef8..f0c202f4 100644 --- a/app/(auth)/(tabs)/(home)/settings/hide-libraries/page.tsx +++ b/app/(auth)/(tabs)/(home)/settings/hide-libraries/page.tsx @@ -1,15 +1,15 @@ -import { Loader } from "@/components/Loader"; -import { Text } from "@/components/common/Text"; -import { ListGroup } from "@/components/list/ListGroup"; -import { ListItem } from "@/components/list/ListItem"; -import DisabledSetting from "@/components/settings/DisabledSetting"; -import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; -import { useSettings } from "@/utils/atoms/settings"; import { getUserViewsApi } from "@jellyfin/sdk/lib/utils/api"; import { useQuery } from "@tanstack/react-query"; import { useAtomValue } from "jotai"; import { useTranslation } from "react-i18next"; import { Switch, View } from "react-native"; +import { Text } from "@/components/common/Text"; +import { Loader } from "@/components/Loader"; +import { ListGroup } from "@/components/list/ListGroup"; +import { ListItem } from "@/components/list/ListItem"; +import DisabledSetting from "@/components/settings/DisabledSetting"; +import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; +import { useSettings } from "@/utils/atoms/settings"; export default function page() { const [settings, updateSettings, pluginSettings] = useSettings(); diff --git a/app/(auth)/(tabs)/(home)/settings/jellyseerr/page.tsx b/app/(auth)/(tabs)/(home)/settings/jellyseerr/page.tsx index 507d01e2..7364348e 100644 --- a/app/(auth)/(tabs)/(home)/settings/jellyseerr/page.tsx +++ b/app/(auth)/(tabs)/(home)/settings/jellyseerr/page.tsx @@ -3,7 +3,7 @@ import { JellyseerrSettings } from "@/components/settings/Jellyseerr"; import { useSettings } from "@/utils/atoms/settings"; export default function page() { - const [settings, updateSettings, pluginSettings] = useSettings(); + const [_settings, _updateSettings, pluginSettings] = useSettings(); return ( { const local = useLocalSearchParams(); diff --git a/app/(auth)/(tabs)/(home,libraries,search,favorites)/collections/[collectionId].tsx b/app/(auth)/(tabs)/(home,libraries,search,favorites)/collections/[collectionId].tsx index 6add1ede..9260ccc8 100644 --- a/app/(auth)/(tabs)/(home,libraries,search,favorites)/collections/[collectionId].tsx +++ b/app/(auth)/(tabs)/(home,libraries,search,favorites)/collections/[collectionId].tsx @@ -1,22 +1,3 @@ -import { ItemCardText } from "@/components/ItemCardText"; -import { Text } from "@/components/common/Text"; -import { TouchableItemRouter } from "@/components/common/TouchableItemRouter"; -import { FilterButton } from "@/components/filters/FilterButton"; -import { ResetFiltersButton } from "@/components/filters/ResetFiltersButton"; -import { ItemPoster } from "@/components/posters/ItemPoster"; -import * as ScreenOrientation from "@/packages/expo-screen-orientation"; -import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; -import { - SortByOption, - SortOrderOption, - genreFilterAtom, - sortByAtom, - sortOptions, - sortOrderAtom, - sortOrderOptions, - tagsFilterAtom, - yearFilterAtom, -} from "@/utils/atoms/filters"; import type { BaseItemDto, BaseItemDtoQueryResult, @@ -35,6 +16,25 @@ import type React from "react"; import { useCallback, useEffect, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { FlatList, View } from "react-native"; +import { Text } from "@/components/common/Text"; +import { TouchableItemRouter } from "@/components/common/TouchableItemRouter"; +import { FilterButton } from "@/components/filters/FilterButton"; +import { ResetFiltersButton } from "@/components/filters/ResetFiltersButton"; +import { ItemCardText } from "@/components/ItemCardText"; +import { ItemPoster } from "@/components/posters/ItemPoster"; +import * as ScreenOrientation from "@/packages/expo-screen-orientation"; +import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; +import { + genreFilterAtom, + SortByOption, + SortOrderOption, + sortByAtom, + sortOptions, + sortOrderAtom, + sortOrderOptions, + tagsFilterAtom, + yearFilterAtom, +} from "@/utils/atoms/filters"; const page: React.FC = () => { const searchParams = useLocalSearchParams(); @@ -43,7 +43,7 @@ const page: React.FC = () => { const [api] = useAtom(apiAtom); const [user] = useAtom(userAtom); const navigation = useNavigation(); - const [orientation, setOrientation] = useState( + const [orientation, _setOrientation] = useState( ScreenOrientation.Orientation.PORTRAIT_UP, ); diff --git a/app/(auth)/(tabs)/(home,libraries,search,favorites)/items/page.tsx b/app/(auth)/(tabs)/(home,libraries,search,favorites)/items/page.tsx index b7c39f9f..d55c05d4 100644 --- a/app/(auth)/(tabs)/(home,libraries,search,favorites)/items/page.tsx +++ b/app/(auth)/(tabs)/(home,libraries,search,favorites)/items/page.tsx @@ -1,6 +1,3 @@ -import { ItemContent } from "@/components/ItemContent"; -import { Text } from "@/components/common/Text"; -import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; import { getUserLibraryApi } from "@jellyfin/sdk/lib/utils/api"; import { useQuery } from "@tanstack/react-query"; import { useLocalSearchParams } from "expo-router"; @@ -15,6 +12,9 @@ import Animated, { useSharedValue, withTiming, } from "react-native-reanimated"; +import { Text } from "@/components/common/Text"; +import { ItemContent } from "@/components/ItemContent"; +import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; const Page: React.FC = () => { const [api] = useAtom(apiAtom); diff --git a/app/(auth)/(tabs)/(home,libraries,search,favorites)/jellyseerr/company/[companyId].tsx b/app/(auth)/(tabs)/(home,libraries,search,favorites)/jellyseerr/company/[companyId].tsx index 8e744ee0..f71fd8f0 100644 --- a/app/(auth)/(tabs)/(home,libraries,search,favorites)/jellyseerr/company/[companyId].tsx +++ b/app/(auth)/(tabs)/(home,libraries,search,favorites)/jellyseerr/company/[companyId].tsx @@ -1,18 +1,17 @@ +import { useInfiniteQuery } from "@tanstack/react-query"; +import { Image } from "expo-image"; +import { useLocalSearchParams } from "expo-router"; +import { uniqBy } from "lodash"; +import { useMemo } from "react"; import ParallaxSlideShow from "@/components/jellyseerr/ParallaxSlideShow"; import JellyseerrPoster from "@/components/posters/JellyseerrPoster"; import { Endpoints, useJellyseerr } from "@/hooks/useJellyseerr"; import { DiscoverSliderType } from "@/utils/jellyseerr/server/constants/discover"; import { type MovieResult, - Results, type TvResult, } from "@/utils/jellyseerr/server/models/Search"; import { COMPANY_LOGO_IMAGE_FILTER } from "@/utils/jellyseerr/src/components/Discover/NetworkSlider"; -import { useInfiniteQuery } from "@tanstack/react-query"; -import { Image } from "expo-image"; -import { useLocalSearchParams } from "expo-router"; -import { uniqBy } from "lodash"; -import React, { useMemo } from "react"; export default function page() { const local = useLocalSearchParams(); @@ -99,7 +98,7 @@ export default function page() { }} /> } - renderItem={(item, index) => ( + renderItem={(item, _index) => ( )} /> diff --git a/app/(auth)/(tabs)/(home,libraries,search,favorites)/jellyseerr/genre/[genreId].tsx b/app/(auth)/(tabs)/(home,libraries,search,favorites)/jellyseerr/genre/[genreId].tsx index 368bc127..5482c45d 100644 --- a/app/(auth)/(tabs)/(home,libraries,search,favorites)/jellyseerr/genre/[genreId].tsx +++ b/app/(auth)/(tabs)/(home,libraries,search,favorites)/jellyseerr/genre/[genreId].tsx @@ -1,21 +1,17 @@ +import { useInfiniteQuery } from "@tanstack/react-query"; +import { useLocalSearchParams } from "expo-router"; +import { uniqBy } from "lodash"; +import { useMemo } from "react"; import { Text } from "@/components/common/Text"; -import JellyseerrMediaIcon from "@/components/jellyseerr/JellyseerrMediaIcon"; -import ParallaxSlideShow from "@/components/jellyseerr/ParallaxSlideShow"; import { textShadowStyle } from "@/components/jellyseerr/discover/GenericSlideCard"; +import ParallaxSlideShow from "@/components/jellyseerr/ParallaxSlideShow"; import JellyseerrPoster from "@/components/posters/JellyseerrPoster"; -import Poster from "@/components/posters/Poster"; import { Endpoints, useJellyseerr } from "@/hooks/useJellyseerr"; import { DiscoverSliderType } from "@/utils/jellyseerr/server/constants/discover"; import { type MovieResult, - Results, type TvResult, } from "@/utils/jellyseerr/server/models/Search"; -import { useInfiniteQuery } from "@tanstack/react-query"; -import { router, useLocalSearchParams, useSegments } from "expo-router"; -import { uniqBy } from "lodash"; -import React, { useMemo } from "react"; -import { TouchableOpacity } from "react-native"; export default function page() { const local = useLocalSearchParams(); @@ -96,7 +92,7 @@ export default function page() { {name} } - renderItem={(item, index) => ( + renderItem={(item, _index) => ( )} /> diff --git a/app/(auth)/(tabs)/(home,libraries,search,favorites)/jellyseerr/page.tsx b/app/(auth)/(tabs)/(home,libraries,search,favorites)/jellyseerr/page.tsx index f72035a3..57f6aa4b 100644 --- a/app/(auth)/(tabs)/(home,libraries,search,favorites)/jellyseerr/page.tsx +++ b/app/(auth)/(tabs)/(home,libraries,search,favorites)/jellyseerr/page.tsx @@ -1,25 +1,3 @@ -import { Button } from "@/components/Button"; -import { GenreTags } from "@/components/GenreTags"; -import { OverviewText } from "@/components/OverviewText"; -import { ParallaxScrollView } from "@/components/ParallaxPage"; -import { JellyserrRatings } from "@/components/Ratings"; -import { Text } from "@/components/common/Text"; -import Cast from "@/components/jellyseerr/Cast"; -import DetailFacts from "@/components/jellyseerr/DetailFacts"; -import JellyseerrSeasons from "@/components/series/JellyseerrSeasons"; -import { ItemActions } from "@/components/series/SeriesActions"; -import { useJellyseerr } from "@/hooks/useJellyseerr"; -import { useJellyseerrCanRequest } from "@/utils/_jellyseerr/useJellyseerrCanRequest"; -import { - type IssueType, - IssueTypeName, -} from "@/utils/jellyseerr/server/constants/issue"; -import { MediaType } from "@/utils/jellyseerr/server/constants/media"; -import type { - MovieResult, - TvResult, -} from "@/utils/jellyseerr/server/models/Search"; -import type { TvDetails } from "@/utils/jellyseerr/server/models/Tv"; import { Ionicons } from "@expo/vector-icons"; import { BottomSheetBackdrop, @@ -36,7 +14,31 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { Platform, TouchableOpacity, View } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; +import { Button } from "@/components/Button"; +import { Text } from "@/components/common/Text"; +import { GenreTags } from "@/components/GenreTags"; +import Cast from "@/components/jellyseerr/Cast"; +import DetailFacts from "@/components/jellyseerr/DetailFacts"; +import { OverviewText } from "@/components/OverviewText"; +import { ParallaxScrollView } from "@/components/ParallaxPage"; +import { JellyserrRatings } from "@/components/Ratings"; +import JellyseerrSeasons from "@/components/series/JellyseerrSeasons"; +import { ItemActions } from "@/components/series/SeriesActions"; +import { useJellyseerr } from "@/hooks/useJellyseerr"; +import { useJellyseerrCanRequest } from "@/utils/_jellyseerr/useJellyseerrCanRequest"; +import { + type IssueType, + IssueTypeName, +} from "@/utils/jellyseerr/server/constants/issue"; +import { MediaType } from "@/utils/jellyseerr/server/constants/media"; +import type { + MovieResult, + TvResult, +} from "@/utils/jellyseerr/server/models/Search"; +import type { TvDetails } from "@/utils/jellyseerr/server/models/Tv"; + const DropdownMenu = !Platform.isTV ? require("zeego/dropdown-menu") : null; + import RequestModal from "@/components/jellyseerr/RequestModal"; import { ANIME_KEYWORD_ID } from "@/utils/jellyseerr/server/api/themoviedb/constants"; import type { MediaRequestBody } from "@/utils/jellyseerr/server/interfaces/api/requestInterfaces"; @@ -380,7 +382,7 @@ const Page: React.FC = () => { {Object.entries(IssueTypeName) .reverse() - .map(([key, value], idx) => ( + .map(([key, value], _idx) => ( diff --git a/app/(auth)/(tabs)/(home,libraries,search,favorites)/jellyseerr/person/[personId].tsx b/app/(auth)/(tabs)/(home,libraries,search,favorites)/jellyseerr/person/[personId].tsx index bbe9f6cc..7550931f 100644 --- a/app/(auth)/(tabs)/(home,libraries,search,favorites)/jellyseerr/person/[personId].tsx +++ b/app/(auth)/(tabs)/(home,libraries,search,favorites)/jellyseerr/person/[personId].tsx @@ -1,6 +1,12 @@ -import { OverviewText } from "@/components/OverviewText"; +import { useQuery } from "@tanstack/react-query"; +import { Image } from "expo-image"; +import { useLocalSearchParams } from "expo-router"; +import { orderBy, uniqBy } from "lodash"; +import { useMemo } from "react"; +import { useTranslation } from "react-i18next"; import { Text } from "@/components/common/Text"; import ParallaxSlideShow from "@/components/jellyseerr/ParallaxSlideShow"; +import { OverviewText } from "@/components/OverviewText"; import JellyseerrPoster from "@/components/posters/JellyseerrPoster"; import { useJellyseerr } from "@/hooks/useJellyseerr"; import type { PersonCreditCast } from "@/utils/jellyseerr/server/models/Person"; @@ -8,12 +14,6 @@ import type { MovieResult, TvResult, } from "@/utils/jellyseerr/server/models/Search"; -import { useQuery } from "@tanstack/react-query"; -import { Image } from "expo-image"; -import { useLocalSearchParams, useSegments } from "expo-router"; -import { orderBy, uniqBy } from "lodash"; -import React, { useMemo } from "react"; -import { useTranslation } from "react-i18next"; export default function page() { const local = useLocalSearchParams(); @@ -107,7 +107,7 @@ export default function page() { MainContent={() => ( )} - renderItem={(item, index) => ( + renderItem={(item, _index) => ( )} /> diff --git a/app/(auth)/(tabs)/(home,libraries,search,favorites)/livetv/_layout.tsx b/app/(auth)/(tabs)/(home,libraries,search,favorites)/livetv/_layout.tsx index 9c6625e6..eaec0692 100644 --- a/app/(auth)/(tabs)/(home,libraries,search,favorites)/livetv/_layout.tsx +++ b/app/(auth)/(tabs)/(home,libraries,search,favorites)/livetv/_layout.tsx @@ -8,7 +8,6 @@ import type { TabNavigationState, } from "@react-navigation/native"; import { Stack, withLayoutContext } from "expo-router"; -import React from "react"; const { Navigator } = createMaterialTopTabNavigator(); diff --git a/app/(auth)/(tabs)/(home,libraries,search,favorites)/livetv/channels.tsx b/app/(auth)/(tabs)/(home,libraries,search,favorites)/livetv/channels.tsx index eae563fb..2aabbe46 100644 --- a/app/(auth)/(tabs)/(home,libraries,search,favorites)/livetv/channels.tsx +++ b/app/(auth)/(tabs)/(home,libraries,search,favorites)/livetv/channels.tsx @@ -1,18 +1,17 @@ -import { ItemImage } from "@/components/common/ItemImage"; -import { Text } from "@/components/common/Text"; -import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; import { getLiveTvApi } from "@jellyfin/sdk/lib/utils/api"; import { FlashList } from "@shopify/flash-list"; import { useQuery } from "@tanstack/react-query"; import { useAtom } from "jotai"; -import React from "react"; import { View } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; +import { ItemImage } from "@/components/common/ItemImage"; +import { Text } from "@/components/common/Text"; +import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; export default function page() { const [api] = useAtom(apiAtom); const [user] = useAtom(userAtom); - const insets = useSafeAreaInsets(); + const _insets = useSafeAreaInsets(); const { data: channels } = useQuery({ queryKey: ["livetv", "channels"], diff --git a/app/(auth)/(tabs)/(home,libraries,search,favorites)/livetv/guide.tsx b/app/(auth)/(tabs)/(home,libraries,search,favorites)/livetv/guide.tsx index 458ce5be..56ac9d67 100644 --- a/app/(auth)/(tabs)/(home,libraries,search,favorites)/livetv/guide.tsx +++ b/app/(auth)/(tabs)/(home,libraries,search,favorites)/livetv/guide.tsx @@ -1,23 +1,16 @@ -import { ItemImage } from "@/components/common/ItemImage"; -import { Text } from "@/components/common/Text"; -import { HourHeader } from "@/components/livetv/HourHeader"; -import { LiveTVGuideRow } from "@/components/livetv/LiveTVGuideRow"; -import { TAB_HEIGHT } from "@/constants/Values"; -import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; import { Ionicons } from "@expo/vector-icons"; import { getLiveTvApi } from "@jellyfin/sdk/lib/utils/api"; import { useQuery } from "@tanstack/react-query"; import { useAtom } from "jotai"; -import React, { useCallback, useMemo, useState } from "react"; +import React, { useCallback, useState } from "react"; import { useTranslation } from "react-i18next"; -import { - Button, - Dimensions, - ScrollView, - TouchableOpacity, - View, -} from "react-native"; +import { Dimensions, ScrollView, TouchableOpacity, View } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; +import { ItemImage } from "@/components/common/ItemImage"; +import { Text } from "@/components/common/Text"; +import { HourHeader } from "@/components/livetv/HourHeader"; +import { LiveTVGuideRow } from "@/components/livetv/LiveTVGuideRow"; +import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; const HOUR_HEIGHT = 30; const ITEMS_PER_PAGE = 20; @@ -28,7 +21,7 @@ export default function page() { const [api] = useAtom(apiAtom); const [user] = useAtom(userAtom); const insets = useSafeAreaInsets(); - const [date, setDate] = useState(new Date()); + const [date, _setDate] = useState(new Date()); const [currentPage, setCurrentPage] = useState(1); const { data: guideInfo } = useQuery({ @@ -150,7 +143,7 @@ export default function page() { > - {channels?.Items?.map((c, i) => ( + {channels?.Items?.map((c, _i) => ( { const navigation = useNavigation(); diff --git a/app/(auth)/(tabs)/(libraries)/[libraryId].tsx b/app/(auth)/(tabs)/(libraries)/[libraryId].tsx index 63dcf453..6800eb31 100644 --- a/app/(auth)/(tabs)/(libraries)/[libraryId].tsx +++ b/app/(auth)/(tabs)/(libraries)/[libraryId].tsx @@ -1,34 +1,3 @@ -import * as ScreenOrientation from "@/packages/expo-screen-orientation"; -import { useInfiniteQuery, useQuery } from "@tanstack/react-query"; -import { useLocalSearchParams, useNavigation } from "expo-router"; -import { useAtom } from "jotai"; -import React, { useCallback, useEffect, useMemo } from "react"; -import { FlatList, View, useWindowDimensions } from "react-native"; - -import { ItemCardText } from "@/components/ItemCardText"; -import { Loader } from "@/components/Loader"; -import { Text } from "@/components/common/Text"; -import { TouchableItemRouter } from "@/components/common/TouchableItemRouter"; -import { FilterButton } from "@/components/filters/FilterButton"; -import { ResetFiltersButton } from "@/components/filters/ResetFiltersButton"; -import { ItemPoster } from "@/components/posters/ItemPoster"; -import { useOrientation } from "@/hooks/useOrientation"; -import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; -import { - SortByOption, - SortOrderOption, - genreFilterAtom, - getSortByPreference, - getSortOrderPreference, - sortByAtom, - sortByPreferenceAtom, - sortOptions, - sortOrderAtom, - sortOrderOptions, - sortOrderPreferenceAtom, - tagsFilterAtom, - yearFilterAtom, -} from "@/utils/atoms/filters"; import type { BaseItemDto, BaseItemDtoQueryResult, @@ -40,8 +9,38 @@ import { getUserLibraryApi, } from "@jellyfin/sdk/lib/utils/api"; import { FlashList } from "@shopify/flash-list"; +import { useInfiniteQuery, useQuery } from "@tanstack/react-query"; +import { useLocalSearchParams, useNavigation } from "expo-router"; +import { useAtom } from "jotai"; +import React, { useCallback, useEffect, useMemo } from "react"; import { useTranslation } from "react-i18next"; +import { FlatList, useWindowDimensions, View } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; +import { Text } from "@/components/common/Text"; +import { TouchableItemRouter } from "@/components/common/TouchableItemRouter"; +import { FilterButton } from "@/components/filters/FilterButton"; +import { ResetFiltersButton } from "@/components/filters/ResetFiltersButton"; +import { ItemCardText } from "@/components/ItemCardText"; +import { Loader } from "@/components/Loader"; +import { ItemPoster } from "@/components/posters/ItemPoster"; +import { useOrientation } from "@/hooks/useOrientation"; +import * as ScreenOrientation from "@/packages/expo-screen-orientation"; +import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; +import { + genreFilterAtom, + getSortByPreference, + getSortOrderPreference, + SortByOption, + SortOrderOption, + sortByAtom, + sortByPreferenceAtom, + sortOptions, + sortOrderAtom, + sortOrderOptions, + sortOrderPreferenceAtom, + tagsFilterAtom, + yearFilterAtom, +} from "@/utils/atoms/filters"; const Page = () => { const searchParams = useLocalSearchParams(); diff --git a/app/(auth)/(tabs)/(libraries)/_layout.tsx b/app/(auth)/(tabs)/(libraries)/_layout.tsx index 66bdad1f..18b41fcf 100644 --- a/app/(auth)/(tabs)/(libraries)/_layout.tsx +++ b/app/(auth)/(tabs)/(libraries)/_layout.tsx @@ -1,9 +1,11 @@ -import { nestedTabPageScreenOptions } from "@/components/stacks/NestedTabPageStack"; -import { useSettings } from "@/utils/atoms/settings"; import { Ionicons } from "@expo/vector-icons"; import { Stack } from "expo-router"; import { Platform } from "react-native"; +import { nestedTabPageScreenOptions } from "@/components/stacks/NestedTabPageStack"; +import { useSettings } from "@/utils/atoms/settings"; + const DropdownMenu = !Platform.isTV ? require("zeego/dropdown-menu") : null; + import { useTranslation } from "react-i18next"; export default function IndexLayout() { diff --git a/app/(auth)/(tabs)/(libraries)/index.tsx b/app/(auth)/(tabs)/(libraries)/index.tsx index 3b39d527..906f8225 100644 --- a/app/(auth)/(tabs)/(libraries)/index.tsx +++ b/app/(auth)/(tabs)/(libraries)/index.tsx @@ -1,8 +1,3 @@ -import { Loader } from "@/components/Loader"; -import { Text } from "@/components/common/Text"; -import { LibraryItemCard } from "@/components/library/LibraryItemCard"; -import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; -import { useSettings } from "@/utils/atoms/settings"; import { getUserLibraryApi, getUserViewsApi, @@ -14,6 +9,11 @@ import { useEffect, useMemo } from "react"; import { useTranslation } from "react-i18next"; import { StyleSheet, View } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; +import { Text } from "@/components/common/Text"; +import { Loader } from "@/components/Loader"; +import { LibraryItemCard } from "@/components/library/LibraryItemCard"; +import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; +import { useSettings } from "@/utils/atoms/settings"; export default function index() { const [api] = useAtom(apiAtom); diff --git a/app/(auth)/(tabs)/(search)/_layout.tsx b/app/(auth)/(tabs)/(search)/_layout.tsx index 806f192e..63b2e1f2 100644 --- a/app/(auth)/(tabs)/(search)/_layout.tsx +++ b/app/(auth)/(tabs)/(search)/_layout.tsx @@ -1,10 +1,10 @@ +import { Stack } from "expo-router"; +import { useTranslation } from "react-i18next"; +import { Platform } from "react-native"; import { commonScreenOptions, nestedTabPageScreenOptions, } from "@/components/stacks/NestedTabPageStack"; -import { Stack } from "expo-router"; -import { useTranslation } from "react-i18next"; -import { Platform } from "react-native"; export default function SearchLayout() { const { t } = useTranslation(); diff --git a/app/(auth)/(tabs)/(search)/index.tsx b/app/(auth)/(tabs)/(search)/index.tsx index 22f3ea73..78dbcfb0 100644 --- a/app/(auth)/(tabs)/(search)/index.tsx +++ b/app/(auth)/(tabs)/(search)/index.tsx @@ -1,9 +1,30 @@ +import type { + BaseItemDto, + BaseItemKind, +} from "@jellyfin/sdk/lib/generated-client/models"; +import { getItemsApi } from "@jellyfin/sdk/lib/utils/api"; +import { useQuery } from "@tanstack/react-query"; +import axios from "axios"; +import { router, useLocalSearchParams, useNavigation } from "expo-router"; +import { useAtom } from "jotai"; +import { + useCallback, + useEffect, + useLayoutEffect, + useMemo, + useRef, + useState, +} from "react"; +import { useTranslation } from "react-i18next"; +import { Platform, ScrollView, TouchableOpacity, View } from "react-native"; +import { useSafeAreaInsets } from "react-native-safe-area-context"; +import { useDebounce } from "use-debounce"; import ContinueWatchingPoster from "@/components/ContinueWatchingPoster"; -import { Tag } from "@/components/GenreTags"; -import { ItemCardText } from "@/components/ItemCardText"; import { Text } from "@/components/common/Text"; import { TouchableItemRouter } from "@/components/common/TouchableItemRouter"; import { FilterButton } from "@/components/filters/FilterButton"; +import { Tag } from "@/components/GenreTags"; +import { ItemCardText } from "@/components/ItemCardText"; import { JellyseerrSearchSort, JellyserrIndexPage, @@ -16,27 +37,6 @@ import { useJellyseerr } from "@/hooks/useJellyseerr"; import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; import { useSettings } from "@/utils/atoms/settings"; import { eventBus } from "@/utils/eventBus"; -import type { - BaseItemDto, - BaseItemKind, -} from "@jellyfin/sdk/lib/generated-client/models"; -import { getItemsApi, getSearchApi } from "@jellyfin/sdk/lib/utils/api"; -import { useQuery } from "@tanstack/react-query"; -import axios from "axios"; -import { router, useLocalSearchParams, useNavigation } from "expo-router"; -import { useAtom } from "jotai"; -import React, { - useCallback, - useEffect, - useLayoutEffect, - useMemo, - useRef, - useState, -} from "react"; -import { useTranslation } from "react-i18next"; -import { Platform, ScrollView, TouchableOpacity, View } from "react-native"; -import { useSafeAreaInsets } from "react-native-safe-area-context"; -import { useDebounce } from "use-debounce"; type SearchType = "Library" | "Discover"; @@ -249,205 +249,203 @@ export default function search() { }, [l1, l2, l3, l7, l8]); return ( - <> - + - - {jellyseerrApi && ( - - setSearchType("Library")}> - - - setSearchType("Discover")}> - - - {searchType === "Discover" && - !loading && - noResults && - debouncedSearch.length > 0 && ( - - - Object.keys(JellyseerrSearchSort).filter((v) => - Number.isNaN(Number(v)), - ) - } - set={(value) => setJellyseerrOrderBy(value[0])} - values={[jellyseerrOrderBy]} - title={t("library.filters.sort_by")} - renderItemLabel={(item) => - t(`home.settings.plugins.jellyseerr.order_by.${item}`) - } - showSearch={false} - /> - ["asc", "desc"]} - set={(value) => setJellyseerrSortOrder(value[0])} - values={[jellyseerrSortOrder]} - title={t("library.filters.sort_order")} - renderItemLabel={(item) => t(`library.filters.${item}`)} - showSearch={false} - /> - - )} - - )} + {jellyseerrApi && ( + + setSearchType("Library")}> + + + setSearchType("Discover")}> + + + {searchType === "Discover" && + !loading && + noResults && + debouncedSearch.length > 0 && ( + + + Object.keys(JellyseerrSearchSort).filter((v) => + Number.isNaN(Number(v)), + ) + } + set={(value) => setJellyseerrOrderBy(value[0])} + values={[jellyseerrOrderBy]} + title={t("library.filters.sort_by")} + renderItemLabel={(item) => + t(`home.settings.plugins.jellyseerr.order_by.${item}`) + } + showSearch={false} + /> + ["asc", "desc"]} + set={(value) => setJellyseerrSortOrder(value[0])} + values={[jellyseerrSortOrder]} + title={t("library.filters.sort_order")} + renderItemLabel={(item) => t(`library.filters.${item}`)} + showSearch={false} + /> + + )} + + )} - - - - - {searchType === "Library" ? ( - - ( - - - - {item.Name} - - - {item.ProductionYear} - - - )} - /> - ( - - - - {item.Name} - - - {item.ProductionYear} - - - )} - /> - ( - - - - - )} - /> - ( - - - - {item.Name} - - - )} - /> - ( - - - - - )} - /> - - ) : ( - - )} - - {searchType === "Library" && - (!loading && noResults && debouncedSearch.length > 0 ? ( - - - {t("search.no_results_found_for")} - - - "{debouncedSearch}" - - - ) : debouncedSearch.length === 0 ? ( - - {exampleSearches.map((e) => ( - { - setSearch(e); - searchBarRef.current?.setText(e); - }} - key={e} - className='mb-2' - > - {e} - - ))} - - ) : null)} + + - - + + {searchType === "Library" ? ( + + ( + + + + {item.Name} + + + {item.ProductionYear} + + + )} + /> + ( + + + + {item.Name} + + + {item.ProductionYear} + + + )} + /> + ( + + + + + )} + /> + ( + + + + {item.Name} + + + )} + /> + ( + + + + + )} + /> + + ) : ( + + )} + + {searchType === "Library" && + (!loading && noResults && debouncedSearch.length > 0 ? ( + + + {t("search.no_results_found_for")} + + + "{debouncedSearch}" + + + ) : debouncedSearch.length === 0 ? ( + + {exampleSearches.map((e) => ( + { + setSearch(e); + searchBarRef.current?.setText(e); + }} + key={e} + className='mb-2' + > + {e} + + ))} + + ) : null)} + + ); } diff --git a/app/(auth)/(tabs)/_layout.tsx b/app/(auth)/(tabs)/_layout.tsx index d8fd30b6..0aafe593 100644 --- a/app/(auth)/(tabs)/_layout.tsx +++ b/app/(auth)/(tabs)/_layout.tsx @@ -1,32 +1,29 @@ -import React, { useCallback, useRef } from "react"; -import { useTranslation } from "react-i18next"; -import { Platform } from "react-native"; - -import { useFocusEffect, useRouter, withLayoutContext } from "expo-router"; - import { - type NativeBottomTabNavigationEventMap, - createNativeBottomTabNavigator, -} from "@bottom-tabs/react-navigation"; - -const { Navigator } = createNativeBottomTabNavigator(); -import type { BottomTabNavigationOptions } from "@react-navigation/bottom-tabs"; - -import { Colors } from "@/constants/Colors"; -import { useSettings } from "@/utils/atoms/settings"; -import { eventBus } from "@/utils/eventBus"; -import { storage } from "@/utils/mmkv"; + type BottomTabNavigationEventMap, + type BottomTabNavigationOptions, + createBottomTabNavigator, +} from "@react-navigation/bottom-tabs"; import type { ParamListBase, TabNavigationState, } from "@react-navigation/native"; +import { useFocusEffect, useRouter, withLayoutContext } from "expo-router"; +import { useCallback } from "react"; +import { useTranslation } from "react-i18next"; +import { Platform } from "react-native"; import { SystemBars } from "react-native-edge-to-edge"; +import { Colors } from "@/constants/Colors"; +import { useSettings } from "@/utils/atoms/settings"; +import { eventBus } from "@/utils/eventBus"; +import { storage } from "@/utils/mmkv"; + +const { Navigator } = createBottomTabNavigator(); export const NativeTabs = withLayoutContext< BottomTabNavigationOptions, typeof Navigator, TabNavigationState, - NativeBottomTabNavigationEventMap + BottomTabNavigationEventMap >(Navigator); export default function TabLayout() { @@ -64,7 +61,7 @@ export default function TabLayout() { ({ - tabPress: (e) => { + tabPress: (_e) => { eventBus.emit("scrollToTop"); }, })} @@ -83,7 +80,7 @@ export default function TabLayout() { /> ({ - tabPress: (e) => { + tabPress: (_e) => { eventBus.emit("searchTabPressed"); }, })} diff --git a/app/(auth)/player/_layout.tsx b/app/(auth)/player/_layout.tsx index 3c29bff4..97a3f7eb 100644 --- a/app/(auth)/player/_layout.tsx +++ b/app/(auth)/player/_layout.tsx @@ -1,5 +1,4 @@ import { Stack } from "expo-router"; -import React from "react"; import { SystemBars } from "react-native-edge-to-edge"; export default function Layout() { diff --git a/app/_layout.tsx b/app/_layout.tsx index f98fc002..465bf6ee 100644 --- a/app/_layout.tsx +++ b/app/_layout.tsx @@ -1,11 +1,15 @@ import "@/augmentations"; +import { ActionSheetProvider } from "@expo/react-native-action-sheet"; +import { BottomSheetModalProvider } from "@gorhom/bottom-sheet"; +import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client"; +import { Platform } from "react-native"; import i18n from "@/i18n"; import { DownloadProvider } from "@/providers/DownloadProvider"; import { - JellyfinProvider, apiAtom, getOrSetDeviceId, getTokenFromStorage, + JellyfinProvider, } from "@/providers/JellyfinProvider"; import { JobQueueProvider } from "@/providers/JobQueueProvider"; import { PlaySettingsProvider } from "@/providers/PlaySettingsProvider"; @@ -24,35 +28,37 @@ import { } from "@/utils/log"; import { storage } from "@/utils/mmkv"; import { cancelJobById, getAllJobsByDeviceId } from "@/utils/optimize-server"; -import { ActionSheetProvider } from "@expo/react-native-action-sheet"; -import { BottomSheetModalProvider } from "@gorhom/bottom-sheet"; -import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client"; -import { Platform } from "react-native"; + const BackGroundDownloader = !Platform.isTV ? require("@kesha-antonov/react-native-background-downloader") : null; + import { DarkTheme, ThemeProvider } from "@react-navigation/native"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; + const BackgroundFetch = !Platform.isTV ? require("expo-background-fetch") : null; + import * as Device from "expo-device"; import * as FileSystem from "expo-file-system"; + const Notifications = !Platform.isTV ? require("expo-notifications") : null; -import * as ScreenOrientation from "@/packages/expo-screen-orientation"; -import { Stack, router, useSegments } from "expo-router"; + +import { router, Stack, useSegments } from "expo-router"; import * as SplashScreen from "expo-splash-screen"; +import * as ScreenOrientation from "@/packages/expo-screen-orientation"; + const TaskManager = !Platform.isTV ? require("expo-task-manager") : null; + import { getLocales } from "expo-localization"; import { Provider as JotaiProvider } from "jotai"; import { useEffect, useRef, useState } from "react"; import { I18nextProvider } from "react-i18next"; -import { AppState, Appearance } from "react-native"; +import { Appearance, AppState } from "react-native"; import { SystemBars } from "react-native-edge-to-edge"; import { GestureHandlerRootView } from "react-native-gesture-handler"; import "react-native-reanimated"; -import { userAtom } from "@/providers/JellyfinProvider"; -import { store } from "@/utils/store"; import { getSessionApi } from "@jellyfin/sdk/lib/utils/api/session-api"; import type { EventSubscription } from "expo-modules-core"; import type { @@ -62,6 +68,8 @@ import type { import type { ExpoPushToken } from "expo-notifications/build/Tokens.types"; import { useAtom } from "jotai"; import { Toaster } from "sonner-native"; +import { userAtom } from "@/providers/JellyfinProvider"; +import { store } from "@/utils/store"; if (!Platform.isTV) { Notifications.setNotificationHandler({ @@ -83,9 +91,9 @@ SplashScreen.setOptions({ }); function useNotificationObserver() { - if (Platform.isTV) return; - useEffect(() => { + if (Platform.isTV) return; + let isMounted = true; function redirect(notification: typeof Notifications.Notification) { @@ -138,14 +146,12 @@ if (!Platform.isTV) { console.log("TaskManager ~ trigger"); const now = Date.now(); - const settingsData = storage.getString("settings"); if (!settingsData) return BackgroundFetch.BackgroundFetchResult.NoData; const settings: Partial = JSON.parse(settingsData); const url = settings?.optimizedVersionsServerUrl; - if (!settings?.autoDownload || !url) return BackgroundFetch.BackgroundFetchResult.NoData; @@ -223,7 +229,6 @@ if (!Platform.isTV) { } console.log(`Auto download started: ${new Date(now).toISOString()}`); - // Be sure to return the successful result type! return BackgroundFetch.BackgroundFetchResult.NewData; }); @@ -301,51 +306,51 @@ function Layout() { ); }, [settings?.preferedLanguage, i18n]); - if (!Platform.isTV) { - useNotificationObserver(); + useNotificationObserver(); - const [expoPushToken, setExpoPushToken] = useState(); - const notificationListener = useRef(); - const responseListener = useRef(); + const [expoPushToken, setExpoPushToken] = useState(); + const notificationListener = useRef(); + const responseListener = useRef(); - useEffect(() => { - if (expoPushToken && api && user) { - api - ?.post("/Streamyfin/device", { - token: expoPushToken.data, - deviceId: getOrSetDeviceId(), - userId: user.Id, - }) - .then((_) => console.log("Posted expo push token")) - .catch((_) => - writeErrorLog("Failed to push expo push token to plugin"), - ); - } else console.log("No token available"); - }, [api, expoPushToken, user]); + useEffect(() => { + if (!Platform.isTV && expoPushToken && api && user) { + api + ?.post("/Streamyfin/device", { + token: expoPushToken.data, + deviceId: getOrSetDeviceId(), + userId: user.Id, + }) + .then((_) => console.log("Posted expo push token")) + .catch((_) => + writeErrorLog("Failed to push expo push token to plugin"), + ); + } else console.log("No token available"); + }, [api, expoPushToken, user]); - async function registerNotifications() { - if (Platform.OS === "android") { - console.log("Setting android notification channel 'default'"); - await Notifications?.setNotificationChannelAsync("default", { - name: "default", - }); - } - - await checkAndRequestPermissions(); - - if (!Platform.isTV && user && user.Policy?.IsAdministrator) { - await registerBackgroundFetchAsyncSessions(); - } - - // only create push token for real devices (pointless for emulators) - if (Device.isDevice) { - Notifications?.getExpoPushTokenAsync() - .then((token: ExpoPushToken) => token && setExpoPushToken(token)) - .catch((reason: any) => console.log("Failed to get token", reason)); - } + async function registerNotifications() { + if (Platform.OS === "android") { + console.log("Setting android notification channel 'default'"); + await Notifications?.setNotificationChannelAsync("default", { + name: "default", + }); } - useEffect(() => { + await checkAndRequestPermissions(); + + if (!Platform.isTV && user && user.Policy?.IsAdministrator) { + await registerBackgroundFetchAsyncSessions(); + } + + // only create push token for real devices (pointless for emulators) + if (Device.isDevice) { + Notifications?.getExpoPushTokenAsync() + .then((token: ExpoPushToken) => token && setExpoPushToken(token)) + .catch((reason: any) => console.log("Failed to get token", reason)); + } + } + + useEffect(() => { + if (!Platform.isTV) { registerNotifications(); notificationListener.current = @@ -363,12 +368,10 @@ function Layout() { (response: NotificationResponse) => { // Currently the notifications supported by the plugin will send data for deep links. const { title, data } = response.notification.request.content; - writeDebugLog( `Notification ${title} opened`, response.notification.request.content, ); - if (data && Object.keys(data).length > 0) { const type = data?.type?.toLower?.(); const itemId = data?.id; @@ -381,12 +384,10 @@ function Layout() { // We just clicked a notification for an individual episode. if (itemId) { router.push(`/(auth)/(tabs)/home/items/page?id=${itemId}`); - } - // summarized season notification for multiple episodes. Bring them to series season - else { + // summarized season notification for multiple episodes. Bring them to series season + } else { const seriesId = data.seriesId; const seasonIndex = data.seasonIndex; - if (seasonIndex) { router.push( `/(auth)/(tabs)/home/series/${seriesId}?seasonIndex=${seasonIndex}`, @@ -411,56 +412,57 @@ function Layout() { responseListener.current, ); }; - }, []); + } + }, [user, api]); - useEffect(() => { - if (Platform.isTV) { - return; + useEffect(() => { + if (Platform.isTV) { + return; + } + + if (segments.includes("direct-player" as never)) { + if ( + !settings.followDeviceOrientation && + settings.defaultVideoOrientation + ) { + ScreenOrientation.lockAsync(settings.defaultVideoOrientation); } + return; + } - if (segments.includes("direct-player" as never)) { - if ( - !settings.followDeviceOrientation && - settings.defaultVideoOrientation - ) { - ScreenOrientation.lockAsync(settings.defaultVideoOrientation); - } - return; - } - - if (settings.followDeviceOrientation === true) { - ScreenOrientation.unlockAsync(); - } else { - ScreenOrientation.lockAsync( - ScreenOrientation.OrientationLock.PORTRAIT_UP, - ); - } - }, [ - settings.followDeviceOrientation, - settings.defaultVideoOrientation, - segments, - ]); - - useEffect(() => { - const subscription = AppState.addEventListener( - "change", - (nextAppState) => { - if ( - appState.current.match(/inactive|background/) && - nextAppState === "active" - ) { - BackGroundDownloader.checkForExistingDownloads(); - } - }, + if (settings.followDeviceOrientation === true) { + ScreenOrientation.unlockAsync(); + } else { + ScreenOrientation.lockAsync( + ScreenOrientation.OrientationLock.PORTRAIT_UP, ); + } + }, [ + settings.followDeviceOrientation, + settings.defaultVideoOrientation, + segments, + ]); - BackGroundDownloader.checkForExistingDownloads(); + useEffect(() => { + if (Platform.isTV) { + return; + } - return () => { - subscription.remove(); - }; - }, []); - } + const subscription = AppState.addEventListener("change", (nextAppState) => { + if ( + appState.current.match(/inactive|background/) && + nextAppState === "active" + ) { + BackGroundDownloader.checkForExistingDownloads(); + } + }); + + BackGroundDownloader.checkForExistingDownloads(); + + return () => { + subscription.remove(); + }; + }, []); return ( diff --git a/app/login.tsx b/app/login.tsx index e59d1d35..5879fe8a 100644 --- a/app/login.tsx +++ b/app/login.tsx @@ -1,29 +1,29 @@ -import { Button } from "@/components/Button"; -import JellyfinServerDiscovery from "@/components/JellyfinServerDiscovery"; -import { PreviousServersList } from "@/components/PreviousServersList"; -import { Input } from "@/components/common/Input"; -import { Text } from "@/components/common/Text"; -import { Colors } from "@/constants/Colors"; -import { apiAtom, useJellyfin } from "@/providers/JellyfinProvider"; import { Ionicons, MaterialCommunityIcons } from "@expo/vector-icons"; import type { PublicSystemInfo } from "@jellyfin/sdk/lib/generated-client"; import { Image } from "expo-image"; import { useLocalSearchParams, useNavigation } from "expo-router"; +import { t } from "i18next"; import { useAtomValue } from "jotai"; import type React from "react"; import { useCallback, useEffect, useState } from "react"; import { Alert, + Keyboard, KeyboardAvoidingView, Platform, SafeAreaView, TouchableOpacity, View, } from "react-native"; -import { Keyboard } from "react-native"; - -import { t } from "i18next"; import { z } from "zod"; +import { Button } from "@/components/Button"; +import { Input } from "@/components/common/Input"; +import { Text } from "@/components/common/Text"; +import JellyfinServerDiscovery from "@/components/JellyfinServerDiscovery"; +import { PreviousServersList } from "@/components/PreviousServersList"; +import { Colors } from "@/constants/Colors"; +import { apiAtom, useJellyfin } from "@/providers/JellyfinProvider"; + const CredentialsSchema = z.object({ username: z.string().min(1, t("login.username_required")), }); @@ -199,7 +199,7 @@ const Login: React.FC = () => { ], ); } - } catch (error) { + } catch (_error) { Alert.alert( t("login.error_title"), t("login.failed_to_initiate_quick_connect"), @@ -213,133 +213,127 @@ const Login: React.FC = () => { behavior={Platform.OS === "ios" ? "padding" : "height"} > {api?.basePath ? ( - <> - - - - - {serverName ? ( - <> - {`${t("login.login_to_title")} `} - {serverName} - - ) : ( - t("login.login_title") - )} - - - {api.basePath} - - - setCredentials({ ...credentials, username: text }) - } - value={credentials.username} - keyboardType='default' - returnKeyType='done' - autoCapitalize='none' - // Changed from username to oneTimeCode because it is a known issue in RN - // https://github.com/facebook/react-native/issues/47106#issuecomment-2521270037 - textContentType='oneTimeCode' - clearButtonMode='while-editing' - maxLength={500} - /> - - - setCredentials({ ...credentials, password: text }) - } - value={credentials.password} - secureTextEntry - keyboardType='default' - returnKeyType='done' - autoCapitalize='none' - textContentType='password' - clearButtonMode='while-editing' - maxLength={500} - /> - - - - - - - - - - - - - ) : ( - <> - - - - Streamyfin - - {t("server.enter_url_to_jellyfin_server")} + + + + + {serverName ? ( + <> + {`${t("login.login_to_title")} `} + {serverName} + + ) : ( + t("login.login_title") + )} + {api.basePath} + setCredentials({ ...credentials, username: text }) + } + value={credentials.username} + keyboardType='default' returnKeyType='done' autoCapitalize='none' - textContentType='URL' + // Changed from username to oneTimeCode because it is a known issue in RN + // https://github.com/facebook/react-native/issues/47106#issuecomment-2521270037 + textContentType='oneTimeCode' + clearButtonMode='while-editing' maxLength={500} /> - - { - setServerURL(server.address); - if (server.serverName) { - setServerName(server.serverName); - } - await handleConnect(server.address); - }} - /> - { - await handleConnect(s.address); - }} + + + setCredentials({ ...credentials, password: text }) + } + value={credentials.password} + secureTextEntry + keyboardType='default' + returnKeyType='done' + autoCapitalize='none' + textContentType='password' + clearButtonMode='while-editing' + maxLength={500} /> + + + + + + - + + + + ) : ( + + + + Streamyfin + + {t("server.enter_url_to_jellyfin_server")} + + + + { + setServerURL(server.address); + if (server.serverName) { + setServerName(server.serverName); + } + await handleConnect(server.address); + }} + /> + { + await handleConnect(s.address); + }} + /> + + )} diff --git a/augmentations/api.ts b/augmentations/api.ts index b79e341a..0336751c 100644 --- a/augmentations/api.ts +++ b/augmentations/api.ts @@ -1,6 +1,6 @@ -import type { StreamyfinPluginConfig } from "@/utils/atoms/settings"; -import { AUTHORIZATION_HEADER, Api } from "@jellyfin/sdk"; +import { Api, AUTHORIZATION_HEADER } from "@jellyfin/sdk"; import type { AxiosRequestConfig, AxiosResponse } from "axios"; +import type { StreamyfinPluginConfig } from "@/utils/atoms/settings"; declare module "@jellyfin/sdk" { interface Api { diff --git a/biome.json b/biome.json index dc9714d5..3fcc7aa6 100644 --- a/biome.json +++ b/biome.json @@ -1,5 +1,5 @@ { - "$schema": "https://biomejs.dev/schemas/2.0.5/schema.json", + "$schema": "https://biomejs.dev/schemas/2.1.2/schema.json", "files": { "includes": [ "**/*", diff --git a/bun.lock b/bun.lock index 418d279c..9b36b829 100644 --- a/bun.lock +++ b/bun.lock @@ -4,7 +4,6 @@ "": { "name": "streamyfin", "dependencies": { - "@bottom-tabs/react-navigation": "0.8.6", "@expo/config-plugins": "~9.0.15", "@expo/react-native-action-sheet": "^4.1.1", "@expo/vector-icons": "^14.0.4", @@ -14,43 +13,44 @@ "@kesha-antonov/react-native-background-downloader": "3.2.6", "@react-native-community/netinfo": "11.4.1", "@react-native-menu/menu": "^1.2.2", - "@react-navigation/bottom-tabs": "^7.2.0", + "@react-navigation/bottom-tabs": "^7.4.2", "@react-navigation/material-top-tabs": "^7.1.0", "@react-navigation/native": "^7.0.14", "@shopify/flash-list": "1.7.3", "@tanstack/react-query": "^5.66.0", "add": "^2.0.6", "axios": "^1.7.9", - "expo": "~52.0.47", - "expo-asset": "~11.0.5", - "expo-background-fetch": "~13.0.6", + "expo": "~52.0.31", + "expo-asset": "~11.0.3", + "expo-background-fetch": "~13.0.5", "expo-blur": "~14.0.3", "expo-brightness": "~13.0.3", - "expo-build-properties": "~0.13.3", - "expo-constants": "~17.0.8", + "expo-build-properties": "~0.13.2", + "expo-constants": "~17.0.5", "expo-crypto": "~14.0.2", - "expo-dev-client": "~5.0.20", - "expo-device": "~7.0.3", + "expo-dev-client": "~5.0.11", + "expo-device": "~7.0.2", "expo-font": "~13.0.3", "expo-haptics": "~14.0.1", - "expo-image": "~2.0.7", + "expo-image": "~2.0.4", "expo-keep-awake": "~14.0.2", "expo-linear-gradient": "~14.0.2", "expo-linking": "~7.0.5", "expo-localization": "~16.0.1", "expo-network": "~7.0.5", - "expo-notifications": "~0.29.14", - "expo-router": "~4.0.21", + "expo-notifications": "~0.29.13", + "expo-router": "~4.0.17", "expo-screen-orientation": "~8.0.4", "expo-sensors": "~14.0.2", "expo-sharing": "~13.0.1", - "expo-splash-screen": "~0.29.24", + "expo-splash-screen": "~0.29.22", "expo-status-bar": "~2.0.1", - "expo-system-ui": "~4.0.9", - "expo-task-manager": "~12.0.6", - "expo-updates": "~0.27.4", + "expo-system-ui": "~4.0.8", + "expo-task-manager": "~12.0.5", + "expo-updates": "~0.26.17", "expo-web-browser": "~14.0.2", "i18next": "^25.0.0", + "install": "^0.13.0", "jotai": "^2.11.3", "lodash": "^4.17.21", "nativewind": "^2.0.11", @@ -59,14 +59,13 @@ "react-i18next": "^15.4.0", "react-native": "npm:react-native-tvos@~0.77.2-0", "react-native-awesome-slider": "^2.9.0", - "react-native-bottom-tabs": "0.8.6", "react-native-circular-progress": "^1.4.1", "react-native-collapsible": "^1.6.2", "react-native-compressor": "^1.10.3", "react-native-country-flag": "^2.0.2", "react-native-device-info": "^14.0.4", "react-native-edge-to-edge": "^1.4.3", - "react-native-gesture-handler": "~2.20.2", + "react-native-gesture-handler": "2.22.0", "react-native-get-random-values": "^1.11.0", "react-native-google-cast": "^4.8.3", "react-native-image-colors": "^2.4.0", @@ -77,9 +76,9 @@ "react-native-progress": "^5.0.1", "react-native-reanimated": "~3.16.7", "react-native-reanimated-carousel": "3.5.1", - "react-native-safe-area-context": "4.12.0", - "react-native-screens": "~4.4.0", - "react-native-svg": "15.8.0", + "react-native-safe-area-context": "5.5.0", + "react-native-screens": "~4.5.0", + "react-native-svg": "15.11.1", "react-native-tab-view": "^4.0.5", "react-native-udp": "^4.1.7", "react-native-uitextview": "^1.4.0", @@ -88,7 +87,7 @@ "react-native-video": "6.10.0", "react-native-volume-manager": "^2.0.8", "react-native-web": "~0.19.13", - "react-native-webview": "13.12.5", + "react-native-webview": "13.13.2", "sonner-native": "^0.17.0", "tailwindcss": "3.3.2", "use-debounce": "^10.0.4", @@ -98,7 +97,7 @@ }, "devDependencies": { "@babel/core": "^7.26.8", - "@biomejs/biome": "^2.0.0", + "@biomejs/biome": "^2.1.2", "@react-native-community/cli": "18.0.0", "@react-native-tvos/config-tv": "^0.1.1", "@types/jest": "^30.0.0", @@ -116,6 +115,9 @@ }, }, }, + "trustedDependencies": [ + "postinstall-postinstall", + ], "packages": { "@0no-co/graphql.web": ["@0no-co/graphql.web@1.1.1", "", { "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0" }, "optionalPeers": ["graphql"] }, "sha512-F2i3xdycesw78QCOBHmpTn7eaD2iNXGwB2gkfwxcOfBbeauYpr8RBSyJOkDrFtKtVRMclg8Sg3n1ip0ACyUuag=="], @@ -379,25 +381,23 @@ "@babel/types": ["@babel/types@7.26.9", "", { "dependencies": { "@babel/helper-string-parser": "^7.25.9", "@babel/helper-validator-identifier": "^7.25.9" } }, "sha512-Y3IR1cRnOxOCDvMmNiym7XpXQ93iGDDPHx+Zj+NM+rg0fBaShfQLkg+hKPaZCEvg5N/LeCo4+Rj/i3FuJsIQaw=="], - "@biomejs/biome": ["@biomejs/biome@2.0.5", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.0.5", "@biomejs/cli-darwin-x64": "2.0.5", "@biomejs/cli-linux-arm64": "2.0.5", "@biomejs/cli-linux-arm64-musl": "2.0.5", "@biomejs/cli-linux-x64": "2.0.5", "@biomejs/cli-linux-x64-musl": "2.0.5", "@biomejs/cli-win32-arm64": "2.0.5", "@biomejs/cli-win32-x64": "2.0.5" }, "bin": { "biome": "bin/biome" } }, "sha512-MztFGhE6cVjf3QmomWu83GpTFyWY8KIcskgRf2AqVEMSH4qI4rNdBLdpAQ11TNK9pUfLGz3IIOC1ZYwgBePtig=="], + "@biomejs/biome": ["@biomejs/biome@2.1.2", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.1.2", "@biomejs/cli-darwin-x64": "2.1.2", "@biomejs/cli-linux-arm64": "2.1.2", "@biomejs/cli-linux-arm64-musl": "2.1.2", "@biomejs/cli-linux-x64": "2.1.2", "@biomejs/cli-linux-x64-musl": "2.1.2", "@biomejs/cli-win32-arm64": "2.1.2", "@biomejs/cli-win32-x64": "2.1.2" }, "bin": { "biome": "bin/biome" } }, "sha512-yq8ZZuKuBVDgAS76LWCfFKHSYIAgqkxVB3mGVVpOe2vSkUTs7xG46zXZeNPRNVjiJuw0SZ3+J2rXiYx0RUpfGg=="], - "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.0.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-VIIWQv9Rcj9XresjCf3isBFfWjFStsdGZvm8SmwJzKs/22YQj167ge7DkxuaaZbNf2kmYif0AcjAKvtNedEoEw=="], + "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.1.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-leFAks64PEIjc7MY/cLjE8u5OcfBKkcDB0szxsWUB4aDfemBep1WVKt0qrEyqZBOW8LPHzrFMyDl3FhuuA0E7g=="], - "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.0.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-DRpGxBgf5Z7HUFcNUB6n66UiD4VlBlMpngNf32wPraxX8vYU6N9cb3xQWOXIQVBBQ64QfsSLJnjNu79i/LNmSg=="], + "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.1.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-Nmmv7wRX5Nj7lGmz0FjnWdflJg4zii8Ivruas6PBKzw5SJX/q+Zh2RfnO+bBnuKLXpj8kiI2x2X12otpH6a32A=="], - "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.0.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-FQTfDNMXOknf8+g9Eede2daaduRjTC2SNbfWPNFMadN9K3UKjeZ62jwiYxztPaz9zQQsZU8VbddQIaeQY5CmIA=="], + "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.1.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-NWNy2Diocav61HZiv2enTQykbPP/KrA/baS7JsLSojC7Xxh2nl9IczuvE5UID7+ksRy2e7yH7klm/WkA72G1dw=="], - "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.0.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-OpflTCOw/ElEs7QZqN/HFaSViPHjAsAPxFJ22LhWUWvuJgcy/Z8+hRV0/3mk/ZRWy5A6fCDKHZqAxU+xB6W4mA=="], + "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.1.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-qgHvafhjH7Oca114FdOScmIKf1DlXT1LqbOrrbR30kQDLFPEOpBG0uzx6MhmsrmhGiCFCr2obDamu+czk+X0HQ=="], - "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.0.5", "", { "os": "linux", "cpu": "x64" }, "sha512-znpfydUDPuDkyBTulnODrQVK2FaG/4hIOPcQSsF2GeauQOYrBAOplj0etGB0NUrr0dFsvaQ15nzDXYb60ACoiw=="], + "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.1.2", "", { "os": "linux", "cpu": "x64" }, "sha512-Km/UYeVowygTjpX6sGBzlizjakLoMQkxWbruVZSNE6osuSI63i4uCeIL+6q2AJlD3dxoiBJX70dn1enjQnQqwA=="], - "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.0.5", "", { "os": "linux", "cpu": "x64" }, "sha512-9lmjCnajAzpZXbav2P6D87ugkhnaDpJtDvOH5uQbY2RXeW6Rq18uOUltxgacGBP+d8GusTr+s3IFOu7SN0Ok8g=="], + "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.1.2", "", { "os": "linux", "cpu": "x64" }, "sha512-xlB3mU14ZUa3wzLtXfmk2IMOGL+S0aHFhSix/nssWS/2XlD27q+S6f0dlQ8WOCbYoXcuz8BCM7rCn2lxdTrlQA=="], - "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.0.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-CP2wKQB+gh8HdJTFKYRFETqReAjxlcN9AlYDEoye8v2eQp+L9v+PUeDql/wsbaUhSsLR0sjj3PtbBtt+02AN3A=="], + "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.1.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-G8KWZli5ASOXA3yUQgx+M4pZRv3ND16h77UsdunUL17uYpcL/UC7RkWTdkfvMQvogVsAuz5JUcBDjgZHXxlKoA=="], - "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.0.5", "", { "os": "win32", "cpu": "x64" }, "sha512-Sw3rz2m6bBADeQpr3+MD7Ch4E1l15DTt/+dfqKnwkm3cn4BrYwnArmvKeZdVsFRDjMyjlKIP88bw1r7o+9aqzw=="], - - "@bottom-tabs/react-navigation": ["@bottom-tabs/react-navigation@0.8.6", "", { "dependencies": { "color": "^4.2.3" }, "peerDependencies": { "@react-navigation/native": ">=7", "react": "*", "react-native": "*", "react-native-bottom-tabs": "*" } }, "sha512-hLlyBAUz4ahaVK2Op2VcJeAkCSpm3KKho4IojkPyXsos4WEHtO44EYWC71TDbVGeOP5HQ9k7FSwAW3IiZs0wHw=="], + "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.1.2", "", { "os": "win32", "cpu": "x64" }, "sha512-9zajnk59PMpjBkty3bK2IrjUsUHvqe9HWwyAWQBjGLE7MIBjbX2vwv1XPEhmO2RRuGoTkVx3WCanHrjAytICLA=="], "@dominicstop/ts-event-emitter": ["@dominicstop/ts-event-emitter@1.1.0", "", {}, "sha512-CcxmJIvUb1vsFheuGGVSQf4KdPZC44XolpUT34+vlal+LyQoBUOn31pjFET5M9ctOxEpt8xa0M3/2M7uUiAoJw=="], @@ -653,11 +653,11 @@ "@react-native/normalize-colors": ["@react-native/normalize-colors@0.76.8", "", {}, "sha512-FRjRvs7RgsXjkbGSOjYSxhX5V70c0IzA/jy3HXeYpATMwD9fOR1DbveLW497QGsVdCa0vThbJUtR8rIzAfpHQA=="], - "@react-navigation/bottom-tabs": ["@react-navigation/bottom-tabs@7.2.0", "", { "dependencies": { "@react-navigation/elements": "^2.2.5", "color": "^4.2.3" }, "peerDependencies": { "@react-navigation/native": "^7.0.14", "react": ">= 18.2.0", "react-native": "*", "react-native-safe-area-context": ">= 4.0.0", "react-native-screens": ">= 4.0.0" } }, "sha512-1LxjgnbPyFINyf9Qr5d1YE0pYhuJayg5TCIIFQmbcX4PRhX7FKUXV7cX8OzrKXEdZi/UE/VNXugtozPAR9zgvA=="], + "@react-navigation/bottom-tabs": ["@react-navigation/bottom-tabs@7.4.2", "", { "dependencies": { "@react-navigation/elements": "^2.5.2", "color": "^4.2.3" }, "peerDependencies": { "@react-navigation/native": "^7.1.14", "react": ">= 18.2.0", "react-native": "*", "react-native-safe-area-context": ">= 4.0.0", "react-native-screens": ">= 4.0.0" } }, "sha512-jyBux5l3qqEucY5M/ZWxVvfA8TQu7DVl2gK+xB6iKqRUfLf7dSumyVxc7HemDwGFoz3Ug8dVZFvSMEs+mfrieQ=="], "@react-navigation/core": ["@react-navigation/core@7.3.1", "", { "dependencies": { "@react-navigation/routers": "^7.1.2", "escape-string-regexp": "^4.0.0", "nanoid": "3.3.8", "query-string": "^7.1.3", "react-is": "^18.2.0", "use-latest-callback": "^0.2.1", "use-sync-external-store": "^1.2.2" }, "peerDependencies": { "react": ">= 18.2.0" } }, "sha512-S3KCGvNsoqVk8ErAtQI2EAhg9185lahF5OY01ofrrD4Ij/uk3QEHHjoGQhR5l5DXSCSKr1JbMQA7MEKMsBiWZA=="], - "@react-navigation/elements": ["@react-navigation/elements@2.2.5", "", { "dependencies": { "color": "^4.2.3" }, "peerDependencies": { "@react-native-masked-view/masked-view": ">= 0.2.0", "@react-navigation/native": "^7.0.14", "react": ">= 18.2.0", "react-native": "*", "react-native-safe-area-context": ">= 4.0.0" }, "optionalPeers": ["@react-native-masked-view/masked-view"] }, "sha512-sDhE+W14P7MNWLMxXg1MEVXwkLUpMZJGflE6nQNzLmolJQIHgcia0Mrm8uRa3bQovhxYu1UzEojLZ+caoZt7Fg=="], + "@react-navigation/elements": ["@react-navigation/elements@2.5.2", "", { "dependencies": { "color": "^4.2.3", "use-latest-callback": "^0.2.4", "use-sync-external-store": "^1.5.0" }, "peerDependencies": { "@react-native-masked-view/masked-view": ">= 0.2.0", "@react-navigation/native": "^7.1.14", "react": ">= 18.2.0", "react-native": "*", "react-native-safe-area-context": ">= 4.0.0" }, "optionalPeers": ["@react-native-masked-view/masked-view"] }, "sha512-aGC3ukF5+lXuiF5bK7bJyRuWCE+Tk4MZ3GoQpAb7u7+m0KmsquliDhj4UCWEUU5kUoCeoRAUvv+1lKcYKf+WTQ=="], "@react-navigation/material-top-tabs": ["@react-navigation/material-top-tabs@7.1.0", "", { "dependencies": { "@react-navigation/elements": "^2.2.5", "color": "^4.2.3", "react-native-tab-view": "^4.0.5" }, "peerDependencies": { "@react-navigation/native": "^7.0.14", "react": ">= 18.2.0", "react-native": "*", "react-native-pager-view": ">= 6.0.0" } }, "sha512-bTFgeHWZmkyE9CAVMTc+lw/b1n2ES3bk0JZoCNSTIrDP+tXfsS8CB4lpOhBybfX1q0C4NQ/i4qMlV7p1iO0eKA=="], @@ -1193,7 +1193,7 @@ "expo-task-manager": ["expo-task-manager@12.0.6", "", { "dependencies": { "unimodules-app-loader": "~5.0.1" }, "peerDependencies": { "expo": "*", "react-native": "*" } }, "sha512-yGbS64OL95z7tAQAvryy0sGHuQgrcpvnJsdyuGL8MA9bcPtr+kytLZ4dOCDac7foQS7+FLDGgtiAR6v/64B5Pg=="], - "expo-updates": ["expo-updates@0.27.4", "", { "dependencies": { "@expo/code-signing-certificates": "0.0.5", "@expo/config": "~10.0.11", "@expo/config-plugins": "~9.0.17", "@expo/spawn-async": "^1.7.2", "arg": "4.1.0", "chalk": "^4.1.2", "expo-eas-client": "~0.13.3", "expo-manifests": "~0.15.7", "expo-structured-headers": "~4.0.0", "expo-updates-interface": "~1.0.0", "fast-glob": "^3.3.2", "fbemitter": "^3.0.0", "ignore": "^5.3.1", "resolve-from": "^5.0.0" }, "peerDependencies": { "expo": "*", "react": "*" }, "bin": { "expo-updates": "bin/cli.js" } }, "sha512-0rg4L2fFPEjTR/qnZ9Te4Q4irVC8uvNcTZW1pWnWbadG1SLv2PKjS1MYX5BboKzC3ao0H7m++5TP3hWhNg9org=="], + "expo-updates": ["expo-updates@0.26.19", "", { "dependencies": { "@expo/code-signing-certificates": "0.0.5", "@expo/config": "~10.0.10", "@expo/config-plugins": "~9.0.15", "@expo/spawn-async": "^1.7.2", "arg": "4.1.0", "chalk": "^4.1.2", "expo-eas-client": "~0.13.2", "expo-manifests": "~0.15.6", "expo-structured-headers": "~4.0.0", "expo-updates-interface": "~1.0.0", "fast-glob": "^3.3.2", "fbemitter": "^3.0.0", "ignore": "^5.3.1", "resolve-from": "^5.0.0" }, "peerDependencies": { "expo": "*", "react": "*" }, "bin": { "expo-updates": "bin/cli.js" } }, "sha512-h40UrG0n1nCb2na1ffz+mNQtsnr7/BxxK+EtXJSqCaD9PIGaTGe20tasmo1oVskv3s37zfv0x93+6uTjanieQg=="], "expo-updates-interface": ["expo-updates-interface@1.0.0", "", { "peerDependencies": { "expo": "*" } }, "sha512-93oWtvULJOj+Pp+N/lpTcFfuREX1wNeHtp7Lwn8EbzYYmdn37MvZU3TPW2tYYCZuhzmKEXnUblYcruYoDu7IrQ=="], @@ -1359,6 +1359,8 @@ "inline-style-prefixer": ["inline-style-prefixer@6.0.4", "", { "dependencies": { "css-in-js-utils": "^3.1.0", "fast-loops": "^1.1.3" } }, "sha512-FwXmZC2zbeeS7NzGjJ6pAiqRhXR0ugUShSNb6GApMl6da0/XGc4MOJsoWAywia52EEWbXNSy0pzkwz/+Y+swSg=="], + "install": ["install@0.13.0", "", {}, "sha512-zDml/jzr2PKU9I8J/xyZBQn8rPCAY//UOYNmR01XwNwyfhEWObo2SWfSl1+0tm1u6PhxLwDnfsT/6jB7OUxqFA=="], + "internal-ip": ["internal-ip@4.3.0", "", { "dependencies": { "default-gateway": "^4.2.0", "ipaddr.js": "^1.9.0" } }, "sha512-S1zBo1D6zcsyuC6PMmY5+55YMILQ9av8lotMx447Bq6SAgo/sDK6y6uUKmuYhW7eacnIhFfsPmCNYdDzsnnDCg=="], "invariant": ["invariant@2.2.4", "", { "dependencies": { "loose-envify": "^1.0.0" } }, "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA=="], @@ -1843,8 +1845,6 @@ "react-native-awesome-slider": ["react-native-awesome-slider@2.9.0", "", { "peerDependencies": { "react": "*", "react-native": "*", "react-native-gesture-handler": ">=2.0.0", "react-native-reanimated": ">=3.0.0" } }, "sha512-sc5qgX4YtM6IxjtosjgQLdsal120MvU+YWs0F2MdgQWijps22AXLDCUoBnZZ8vxVhVyJ2WnnIPrmtVBvVJjSuQ=="], - "react-native-bottom-tabs": ["react-native-bottom-tabs@0.8.6", "", { "dependencies": { "sf-symbols-typescript": "^2.0.0", "use-latest-callback": "^0.2.1" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-N5b3MoSfsEqlmvFyIyL0X0bd+QAtB+cXH1rl/+R2Kr0BefBTC7ZldGcPhgK3FhBbt0vJDpd3kLb/dvmqZd+Eag=="], - "react-native-circular-progress": ["react-native-circular-progress@1.4.1", "", { "dependencies": { "prop-types": "^15.8.1" }, "peerDependencies": { "react": ">=16.0.0", "react-native": ">=0.50.0", "react-native-svg": ">=7.0.0" } }, "sha512-HEzvI0WPuWvsCgWE3Ff2HBTMgAEQB2GvTFw0KHyD/t1STAlDDRiolu0mEGhVvihKR3jJu3v3V4qzvSklY/7XzQ=="], "react-native-collapsible": ["react-native-collapsible@1.6.2", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-MCOBVJWqHNjnDaGkvxX997VONmJeebh6wyJxnHEgg0L1PrlcXU1e/bo6eK+CDVFuMrCafw8Qh4DOv/C4V/+Iew=="], @@ -1857,7 +1857,7 @@ "react-native-edge-to-edge": ["react-native-edge-to-edge@1.4.3", "", { "peerDependencies": { "react": ">=18.2.0", "react-native": ">=0.74.0" } }, "sha512-fYchwiQ2D/8NzcvJK1sD9Cm25GFQfsLgYmGpohoSpRxwBwR5UCL0wUf4scoQgYncRh9Hmc2t8ml/sikTwMM3ng=="], - "react-native-gesture-handler": ["react-native-gesture-handler@2.20.2", "", { "dependencies": { "@egjs/hammerjs": "^2.0.17", "hoist-non-react-statics": "^3.3.0", "invariant": "^2.2.4", "prop-types": "^15.7.2" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-HqzFpFczV4qCnwKlvSAvpzEXisL+Z9fsR08YV5LfJDkzuArMhBu2sOoSPUF/K62PCoAb+ObGlTC83TKHfUd0vg=="], + "react-native-gesture-handler": ["react-native-gesture-handler@2.22.0", "", { "dependencies": { "@egjs/hammerjs": "^2.0.17", "hoist-non-react-statics": "^3.3.0", "invariant": "^2.2.4" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-m5Ps1cOSxSiMP4re+XsbeWcC9DNJuIEjMSmtUxBdyfYEJtdu5iAAiX7KlHHrf2mnK4I/56Ncy4PvPKWBwSpWpQ=="], "react-native-get-random-values": ["react-native-get-random-values@1.11.0", "", { "dependencies": { "fast-base64-decode": "^1.0.0" }, "peerDependencies": { "react-native": ">=0.56" } }, "sha512-4BTbDbRmS7iPdhYLRcz3PGFIpFJBwNZg9g42iwa2P6FOv9vZj/xJc678RZXnLNZzd0qd7Q3CCF6Yd+CU2eoXKQ=="], @@ -1883,11 +1883,11 @@ "react-native-reanimated-carousel": ["react-native-reanimated-carousel@3.5.1", "", { "peerDependencies": { "react": ">=16.8.0", "react-native": ">=0.6.0", "react-native-gesture-handler": ">=2.0.0", "react-native-reanimated": ">=3.0.0" } }, "sha512-9BBQV6JAYSQm2lV7MFtT4mzapXmW4IZO6s38gfiJL84Jg23ivGB1UykcNQauKgtHyhtW2NuZJzItb1s42lM+hA=="], - "react-native-safe-area-context": ["react-native-safe-area-context@4.12.0", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-ukk5PxcF4p3yu6qMZcmeiZgowhb5AsKRnil54YFUUAXVIS7PJcMHGGC+q44fCiBg44/1AJk5njGMez1m9H0BVQ=="], + "react-native-safe-area-context": ["react-native-safe-area-context@5.5.0", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-BQcSvVEJj3T4zBQH9YrnlfcLGHiVOsmeiE10PSBsmI/xyzULSZdJISFOH0HLcLU7/nePC+HsaaVzIsEa1CVBYw=="], - "react-native-screens": ["react-native-screens@4.4.0", "", { "dependencies": { "react-freeze": "^1.0.0", "warn-once": "^0.1.0" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-c7zc7Zwjty6/pGyuuvh9gK3YBYqHPOxrhXfG1lF4gHlojQSmIx2piNbNaV+Uykj+RDTmFXK0e/hA+fucw/Qozg=="], + "react-native-screens": ["react-native-screens@4.5.0", "", { "dependencies": { "react-freeze": "^1.0.0", "warn-once": "^0.1.0" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-yBWeN5EHNeew9f0ia9VE7JSlUQzCZEwkb87r7A7/Sg41OJHuRKHNRhmdCOiMBUqwwQi3F+b4NZGywjeM/gWMyg=="], - "react-native-svg": ["react-native-svg@15.8.0", "", { "dependencies": { "css-select": "^5.1.0", "css-tree": "^1.1.3", "warn-once": "0.1.1" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-KHJzKpgOjwj1qeZzsBjxNdoIgv2zNCO9fVcoq2TEhTRsVV5DGTZ9JzUZwybd7q4giT/H3RdtqC3u44dWdO0Ffw=="], + "react-native-svg": ["react-native-svg@15.11.1", "", { "dependencies": { "css-select": "^5.1.0", "css-tree": "^1.1.3", "warn-once": "0.1.1" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-Qmwx/yJKt+AHUr4zjxx/Q69qwKtRfr1+uIfFMQoq3WFRhqU76aL9db1DyvPiY632DAsVGba1pHf92OZPkpjrdQ=="], "react-native-tab-view": ["react-native-tab-view@4.0.5", "", { "dependencies": { "use-latest-callback": "^0.2.1" }, "peerDependencies": { "react": ">= 18.2.0", "react-native": "*", "react-native-pager-view": ">= 6.0.0" } }, "sha512-Xn3TpYo4yvKRC/f4+cOcvsXlitdnSaYkacshckrEI3JiDmFKNFIRVNxtZFggm4MwbJafq2RzuzR6xrgKoxgkTw=="], @@ -1905,7 +1905,7 @@ "react-native-web": ["react-native-web@0.19.13", "", { "dependencies": { "@babel/runtime": "^7.18.6", "@react-native/normalize-colors": "^0.74.1", "fbjs": "^3.0.4", "inline-style-prefixer": "^6.0.1", "memoize-one": "^6.0.0", "nullthrows": "^1.1.1", "postcss-value-parser": "^4.2.0", "styleq": "^0.1.3" }, "peerDependencies": { "react": "^18.0.0", "react-dom": "^18.0.0" } }, "sha512-etv3bN8rJglrRCp/uL4p7l8QvUNUC++QwDbdZ8CB7BvZiMvsxfFIRM1j04vxNldG3uo2puRd6OSWR3ibtmc29A=="], - "react-native-webview": ["react-native-webview@13.12.5", "", { "dependencies": { "escape-string-regexp": "^4.0.0", "invariant": "2.2.4" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-INOKPom4dFyzkbxbkuQNfeRG9/iYnyRDzrDkJeyvSWgJAW2IDdJkWFJBS2v0RxIL4gqLgHkiIZDOfiLaNnw83Q=="], + "react-native-webview": ["react-native-webview@13.13.2", "", { "dependencies": { "escape-string-regexp": "^4.0.0", "invariant": "2.2.4" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-zACPDTF0WnaEnKZ9mA/r/UpcOpV2gQM06AAIrOOexnO8UJvXL8Pjso0b/wTqKFxUZZnmjKuwd8gHVUosVOdVrw=="], "react-refresh": ["react-refresh@0.14.2", "", {}, "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA=="], @@ -2479,6 +2479,14 @@ "@react-navigation/core/react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], + "@react-navigation/elements/use-latest-callback": ["use-latest-callback@0.2.4", "", { "peerDependencies": { "react": ">=16.8" } }, "sha512-LS2s2n1usUUnDq4oVh1ca6JFX9uSqUncTfAm44WMg0v6TxL7POUTk1B044NH8TeLkFbNajIsgDHcgNpNzZucdg=="], + + "@react-navigation/elements/use-sync-external-store": ["use-sync-external-store@1.5.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A=="], + + "@react-navigation/material-top-tabs/@react-navigation/elements": ["@react-navigation/elements@2.2.5", "", { "dependencies": { "color": "^4.2.3" }, "peerDependencies": { "@react-native-masked-view/masked-view": ">= 0.2.0", "@react-navigation/native": "^7.0.14", "react": ">= 18.2.0", "react-native": "*", "react-native-safe-area-context": ">= 4.0.0" }, "optionalPeers": ["@react-native-masked-view/masked-view"] }, "sha512-sDhE+W14P7MNWLMxXg1MEVXwkLUpMZJGflE6nQNzLmolJQIHgcia0Mrm8uRa3bQovhxYu1UzEojLZ+caoZt7Fg=="], + + "@react-navigation/native-stack/@react-navigation/elements": ["@react-navigation/elements@2.2.5", "", { "dependencies": { "color": "^4.2.3" }, "peerDependencies": { "@react-native-masked-view/masked-view": ">= 0.2.0", "@react-navigation/native": "^7.0.14", "react": ">= 18.2.0", "react-native": "*", "react-native-safe-area-context": ">= 4.0.0" }, "optionalPeers": ["@react-native-masked-view/masked-view"] }, "sha512-sDhE+W14P7MNWLMxXg1MEVXwkLUpMZJGflE6nQNzLmolJQIHgcia0Mrm8uRa3bQovhxYu1UzEojLZ+caoZt7Fg=="], + "accepts/negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="], "ansi-fragments/colorette": ["colorette@1.4.0", "", {}, "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g=="], @@ -2527,6 +2535,8 @@ "expo-modules-autolinking/fs-extra": ["fs-extra@9.1.0", "", { "dependencies": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ=="], + "expo-router/@react-navigation/bottom-tabs": ["@react-navigation/bottom-tabs@7.2.0", "", { "dependencies": { "@react-navigation/elements": "^2.2.5", "color": "^4.2.3" }, "peerDependencies": { "@react-navigation/native": "^7.0.14", "react": ">= 18.2.0", "react-native": "*", "react-native-safe-area-context": ">= 4.0.0", "react-native-screens": ">= 4.0.0" } }, "sha512-1LxjgnbPyFINyf9Qr5d1YE0pYhuJayg5TCIIFQmbcX4PRhX7FKUXV7cX8OzrKXEdZi/UE/VNXugtozPAR9zgvA=="], + "expo-router/semver": ["semver@7.6.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A=="], "expo-system-ui/debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="], @@ -2911,6 +2921,8 @@ "expo-modules-autolinking/fs-extra/universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="], + "expo-router/@react-navigation/bottom-tabs/@react-navigation/elements": ["@react-navigation/elements@2.2.5", "", { "dependencies": { "color": "^4.2.3" }, "peerDependencies": { "@react-native-masked-view/masked-view": ">= 0.2.0", "@react-navigation/native": "^7.0.14", "react": ">= 18.2.0", "react-native": "*", "react-native-safe-area-context": ">= 4.0.0" }, "optionalPeers": ["@react-native-masked-view/masked-view"] }, "sha512-sDhE+W14P7MNWLMxXg1MEVXwkLUpMZJGflE6nQNzLmolJQIHgcia0Mrm8uRa3bQovhxYu1UzEojLZ+caoZt7Fg=="], + "expo-updates/@expo/config-plugins/@expo/config-types": ["@expo/config-types@52.0.5", "", {}, "sha512-AMDeuDLHXXqd8W+0zSjIt7f37vUd/BP8p43k68NHpyAvQO+z8mbQZm3cNQVAMySeayK2XoPigAFB1JF2NFajaA=="], "expo-updates/@expo/config-plugins/debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="], diff --git a/components/AddToFavorites.tsx b/components/AddToFavorites.tsx index 16e15694..156ed194 100644 --- a/components/AddToFavorites.tsx +++ b/components/AddToFavorites.tsx @@ -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; diff --git a/components/AudioTrackSelector.tsx b/components/AudioTrackSelector.tsx index c62140ab..e8228c86 100644 --- a/components/AudioTrackSelector.tsx +++ b/components/AudioTrackSelector.tsx @@ -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 = ({ 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 = ({ const { t } = useTranslation(); + if (isTv) return null; + return ( = ({ 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 ( - Platform.OS === "android" ? ( - - ) : ( - <> - ), + Platform.OS === "android" ? : null, [Platform.OS], ); diff --git a/components/ContinueWatchingPoster.tsx b/components/ContinueWatchingPoster.tsx index c15495ce..e840eaee 100644 --- a/components/ContinueWatchingPoster.tsx +++ b/components/ContinueWatchingPoster.tsx @@ -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 = { diff --git a/components/DownloadItem.tsx b/components/DownloadItem.tsx index d7939684..552484eb 100644 --- a/components/DownloadItem.tsx +++ b/components/DownloadItem.tsx @@ -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 = ({ bottomSheetModalRef.current?.present(); }, []); - const handleSheetChanges = useCallback((index: number) => {}, []); + const handleSheetChanges = useCallback((_index: number) => {}, []); const closeModal = useCallback(() => { bottomSheetModalRef.current?.dismiss(); diff --git a/components/ItemCardText.tsx b/components/ItemCardText.tsx index 8ce52da2..00b1ae3a 100644 --- a/components/ItemCardText.tsx +++ b/components/ItemCardText.tsx @@ -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"; diff --git a/components/ItemContent.tsx b/components/ItemContent.tsx index 33fd305a..d7a5d598 100644 --- a/components/ItemContent.tsx +++ b/components/ItemContent.tsx @@ -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( ), }); - }, [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 ; return ( = React.memo( onLoad={() => setLoadingLogo(false)} onError={() => setLoadingLogo(false)} /> - ) : null + ) : ( + + ) } > diff --git a/components/ItemHeader.tsx b/components/ItemHeader.tsx index 8d218d82..b9e3006d 100644 --- a/components/ItemHeader.tsx +++ b/components/ItemHeader.tsx @@ -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"; diff --git a/components/ItemTechnicalDetails.tsx b/components/ItemTechnicalDetails.tsx index 5222e6f8..81c04e0a 100644 --- a/components/ItemTechnicalDetails.tsx +++ b/components/ItemTechnicalDetails.tsx @@ -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 = ({ source, ...props }) => { +export const ItemTechnicalDetails: React.FC = ({ source }) => { const bottomSheetModalRef = useRef(null); const { t } = useTranslation(); @@ -55,7 +53,7 @@ export const ItemTechnicalDetails: React.FC = ({ source, ...props }) => { > - + {t("item_card.video")} @@ -64,7 +62,7 @@ export const ItemTechnicalDetails: React.FC = ({ source, ...props }) => { - + {t("item_card.audio")} @@ -77,7 +75,7 @@ export const ItemTechnicalDetails: React.FC = ({ source, ...props }) => { /> - + {t("item_card.subtitles")} @@ -103,7 +101,7 @@ const SubtitleStreamInfo = ({ }) => { return ( - {subtitleStreams.map((stream, index) => ( + {subtitleStreams.map((stream, _index) => ( {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 ( @@ -223,7 +219,11 @@ const VideoStreamInfo = ({ source }: { source?: MediaSourceInfo }) => { } - text={`${videoStream.AverageFrameRate?.toFixed(0)} fps`} + text={ + videoStream.AverageFrameRate != null + ? `${videoStream.AverageFrameRate.toFixed(0)} fps` + : "" + } /> ); diff --git a/components/JellyfinServerDiscovery.tsx b/components/JellyfinServerDiscovery.tsx index 0ea85a4a..689b266e 100644 --- a/components/JellyfinServerDiscovery.tsx +++ b/components/JellyfinServerDiscovery.tsx @@ -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"; diff --git a/components/Loader.tsx b/components/Loader.tsx index c3c3065e..29956697 100644 --- a/components/Loader.tsx +++ b/components/Loader.tsx @@ -2,7 +2,6 @@ import { ActivityIndicator, type ActivityIndicatorProps, Platform, - View, } from "react-native"; interface Props extends ActivityIndicatorProps {} diff --git a/components/MediaSourceSelector.tsx b/components/MediaSourceSelector.tsx index ac14f929..ed3e41b5 100644 --- a/components/MediaSourceSelector.tsx +++ b/components/MediaSourceSelector.tsx @@ -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 = ({ 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 = ({ return name?.replace(commonPrefix, "").toLowerCase(); }; + if (isTv) return null; + return ( = ({ 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(); diff --git a/components/PlayInRemoteSession.tsx b/components/PlayInRemoteSession.tsx index 5111068c..0143ab01 100644 --- a/components/PlayInRemoteSession.tsx +++ b/components/PlayInRemoteSession.tsx @@ -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 { item: BaseItemDto; diff --git a/components/PlayedStatus.tsx b/components/PlayedStatus.tsx index 00beb18f..cab1e34f 100644 --- a/components/PlayedStatus.tsx +++ b/components/PlayedStatus.tsx @@ -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 = ({ items, ...props }) => { const queryClient = useQueryClient(); - const invalidateQueries = () => { + const _invalidateQueries = () => { items.forEach((item) => { queryClient.invalidateQueries({ queryKey: ["item", item.Id], diff --git a/components/ProgressCircle.tsx b/components/ProgressCircle.tsx index f3c0a55c..63cd4f5d 100644 --- a/components/ProgressCircle.tsx +++ b/components/ProgressCircle.tsx @@ -1,5 +1,4 @@ import type React from "react"; -import { StyleSheet, View } from "react-native"; import { AnimatedCircularProgress } from "react-native-circular-progress"; type ProgressCircleProps = { diff --git a/components/Ratings.tsx b/components/Ratings.tsx index bb6e6107..5741233f 100644 --- a/components/Ratings.tsx +++ b/components/Ratings.tsx @@ -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 { diff --git a/components/RoundButton.tsx b/components/RoundButton.tsx index e54c9ccc..a57bce11 100644 --- a/components/RoundButton.tsx +++ b/components/RoundButton.tsx @@ -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; diff --git a/components/SimilarItems.tsx b/components/SimilarItems.tsx index 8b49def4..4288f1e7 100644 --- a/components/SimilarItems.tsx +++ b/components/SimilarItems.tsx @@ -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; diff --git a/components/SubtitleTrackSelector.tsx b/components/SubtitleTrackSelector.tsx index 1e74bbb6..81d3b885 100644 --- a/components/SubtitleTrackSelector.tsx +++ b/components/SubtitleTrackSelector.tsx @@ -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 = ({ 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 = ({ [subtitleStreams, selected], ); - if (subtitleStreams?.length === 0) return null; - const { t } = useTranslation(); + if (isTv) return null; + if (subtitleStreams?.length === 0) return null; + return ( { +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"; diff --git a/components/common/Dropdown.tsx b/components/common/Dropdown.tsx index 586fc65f..3ec4d5ce 100644 --- a/components/common/Dropdown.tsx +++ b/components/common/Dropdown.tsx @@ -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 { data: T[]; @@ -33,14 +34,17 @@ const Dropdown = ({ multiple = false, ...props }: PropsWithChildren & ViewProps>) => { - if (Platform.isTV) return null; + const isTv = Platform.isTV; + const [selected, setSelected] = useState(); useEffect(() => { if (selected !== undefined) { onSelected(...selected); } - }, [selected]); + }, [selected, onSelected]); + + if (isTv) return null; return ( @@ -58,7 +62,7 @@ const Dropdown = ({ ) : ( - <>{title} + title )} ({ sideOffset={0} > {label} - {data.map((item, idx) => + {data.map((item, _idx) => multiple ? ( ({ : "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") { diff --git a/components/common/HeaderBackButton.tsx b/components/common/HeaderBackButton.tsx index fdf77ec5..d728ca8d 100644 --- a/components/common/HeaderBackButton.tsx +++ b/components/common/HeaderBackButton.tsx @@ -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 { diff --git a/components/common/InfiniteHorrizontalScroll.tsx b/components/common/InfiniteHorrizontalScroll.tsx index 8682e964..4e2171ca 100644 --- a/components/common/InfiniteHorrizontalScroll.tsx +++ b/components/common/InfiniteHorrizontalScroll.tsx @@ -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"; diff --git a/components/common/ItemImage.tsx b/components/common/ItemImage.tsx index 5268cca6..621a925e 100644 --- a/components/common/ItemImage.tsx +++ b/components/common/ItemImage.tsx @@ -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; diff --git a/components/common/JellyseerrItemRouter.tsx b/components/common/JellyseerrItemRouter.tsx index 8222f187..09fc620e 100644 --- a/components/common/JellyseerrItemRouter.tsx +++ b/components/common/JellyseerrItemRouter.tsx @@ -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> = ({ if (from === "(home)" || from === "(search)" || from === "(libraries)") return ( - <> - - - { - if (!result) return; + + + { + if (!result) return; - // @ts-ignore - router.push({ - pathname: `/(auth)/(tabs)/${from}/jellyseerr/page`, - params: { - ...result, - mediaTitle, - releaseYear, - canRequest, - posterSrc, - mediaType, - }, - }); - }} - {...props} - > - {children} - - - - Actions - {canRequest && mediaType === MediaType.MOVIE && ( - { - if (autoApprove) { - request(); - } + {children} + + + + Actions + {canRequest && mediaType === MediaType.MOVIE && ( + { + if (autoApprove) { + request(); + } + }} + shouldDismissMenuOnSelect + > + + Request + + - - Request - - - - )} - - - + androidIconName='download' + /> + + )} + + ); }; diff --git a/components/common/Text.tsx b/components/common/Text.tsx index aac7dcf2..2780ede9 100644 --- a/components/common/Text.tsx +++ b/components/common/Text.tsx @@ -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 & { diff --git a/components/common/TouchableItemRouter.tsx b/components/common/TouchableItemRouter.tsx index 23cb6dd7..a0832304 100644 --- a/components/common/TouchableItemRouter.tsx +++ b/components/common/TouchableItemRouter.tsx @@ -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; diff --git a/components/common/VerticalSkeleton.tsx b/components/common/VerticalSkeleton.tsx index a4abcdd6..02a8a256 100644 --- a/components/common/VerticalSkeleton.tsx +++ b/components/common/VerticalSkeleton.tsx @@ -1,4 +1,3 @@ -import { Text } from "@/components/common/Text"; import { View, type ViewProps } from "react-native"; interface Props extends ViewProps { diff --git a/components/downloads/ActiveDownloads.tsx b/components/downloads/ActiveDownloads.tsx index 3f19f7cd..fd71a88d 100644 --- a/components/downloads/ActiveDownloads.tsx +++ b/components/downloads/ActiveDownloads.tsx @@ -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; diff --git a/components/downloads/DownloadSize.tsx b/components/downloads/DownloadSize.tsx index 22b00412..aa1beb37 100644 --- a/components/downloads/DownloadSize.tsx +++ b/components/downloads/DownloadSize.tsx @@ -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 = ({ }, [size]); return ( - <> - - {sizeText} - - + + {sizeText} + ); }; diff --git a/components/downloads/EpisodeCard.tsx b/components/downloads/EpisodeCard.tsx index 97a9308f..289160dc 100644 --- a/components/downloads/EpisodeCard.tsx +++ b/components/downloads/EpisodeCard.tsx @@ -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 = ({ item, ...props }) => { const { showActionSheetWithOptions } = useActionSheet(); const successHapticFeedback = useHaptic("success"); - const base64Image = useMemo(() => { + const _base64Image = useMemo(() => { return storage.getString(item.Id!); }, [item]); diff --git a/components/downloads/MovieCard.tsx b/components/downloads/MovieCard.tsx index e15fd003..5a79ae13 100644 --- a/components/downloads/MovieCard.tsx +++ b/components/downloads/MovieCard.tsx @@ -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 { diff --git a/components/downloads/SeriesCard.tsx b/components/downloads/SeriesCard.tsx index 948807e7..0cc75150 100644 --- a/components/downloads/SeriesCard.tsx +++ b/components/downloads/SeriesCard.tsx @@ -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 }) => { diff --git a/components/filters/FilterButton.tsx b/components/filters/FilterButton.tsx index 9715c17e..1f745ce0 100644 --- a/components/filters/FilterButton.tsx +++ b/components/filters/FilterButton.tsx @@ -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 extends ViewProps { diff --git a/components/filters/FilterSheet.tsx b/components/filters/FilterSheet.tsx index 8a8817a6..2cc69823 100644 --- a/components/filters/FilterSheet.tsx +++ b/components/filters/FilterSheet.tsx @@ -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"; diff --git a/components/filters/ResetFiltersButton.tsx b/components/filters/ResetFiltersButton.tsx index 6c48ee3f..c0cc6d68 100644 --- a/components/filters/ResetFiltersButton.tsx +++ b/components/filters/ResetFiltersButton.tsx @@ -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 {} diff --git a/components/home/Favorites.tsx b/components/home/Favorites.tsx index 6fe263a6..14636af5 100644 --- a/components/home/Favorites.tsx +++ b/components/home/Favorites.tsx @@ -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" diff --git a/components/home/LargeMovieCarousel.tsx b/components/home/LargeMovieCarousel.tsx index 9b0851ef..bc0b6479 100644 --- a/components/home/LargeMovieCarousel.tsx +++ b/components/home/LargeMovieCarousel.tsx @@ -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 {} diff --git a/components/home/ScrollingCollectionList.tsx b/components/home/ScrollingCollectionList.tsx index a77085de..483fd156 100644 --- a/components/home/ScrollingCollectionList.tsx +++ b/components/home/ScrollingCollectionList.tsx @@ -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 { diff --git a/components/inputs/Stepper.tsx b/components/inputs/Stepper.tsx index 454c9edc..20861fcb 100644 --- a/components/inputs/Stepper.tsx +++ b/components/inputs/Stepper.tsx @@ -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; diff --git a/components/jellyseerr/Cast.tsx b/components/jellyseerr/Cast.tsx index 27e8201c..6abd890a 100644 --- a/components/jellyseerr/Cast.tsx +++ b/components/jellyseerr/Cast.tsx @@ -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 diff --git a/components/jellyseerr/DetailFacts.tsx b/components/jellyseerr/DetailFacts.tsx index 4e9e1580..ead2156c 100644 --- a/components/jellyseerr/DetailFacts.tsx +++ b/components/jellyseerr/DetailFacts.tsx @@ -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; diff --git a/components/jellyseerr/JellyseerrIndexPage.tsx b/components/jellyseerr/JellyseerrIndexPage.tsx index 2a1509b2..42fd223b 100644 --- a/components/jellyseerr/JellyseerrIndexPage.tsx +++ b/components/jellyseerr/JellyseerrIndexPage.tsx @@ -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"; diff --git a/components/jellyseerr/JellyseerrMediaIcon.tsx b/components/jellyseerr/JellyseerrMediaIcon.tsx index 199b6554..83cdbb6f 100644 --- a/components/jellyseerr/JellyseerrMediaIcon.tsx +++ b/components/jellyseerr/JellyseerrMediaIcon.tsx @@ -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 diff --git a/components/jellyseerr/JellyseerrStatusIcon.tsx b/components/jellyseerr/JellyseerrStatusIcon.tsx index 6bed842e..e9b37af5 100644 --- a/components/jellyseerr/JellyseerrStatusIcon.tsx +++ b/components/jellyseerr/JellyseerrStatusIcon.tsx @@ -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; diff --git a/components/jellyseerr/ParallaxSlideShow.tsx b/components/jellyseerr/ParallaxSlideShow.tsx index 112a1e28..66269625 100644 --- a/components/jellyseerr/ParallaxSlideShow.tsx +++ b/components/jellyseerr/ParallaxSlideShow.tsx @@ -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; diff --git a/components/jellyseerr/PersonPoster.tsx b/components/jellyseerr/PersonPoster.tsx index 075fedf2..f02e43f5 100644 --- a/components/jellyseerr/PersonPoster.tsx +++ b/components/jellyseerr/PersonPoster.tsx @@ -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; diff --git a/components/jellyseerr/RequestModal.tsx b/components/jellyseerr/RequestModal.tsx index 06c6e3b9..343e7a78 100644 --- a/components/jellyseerr/RequestModal.tsx +++ b/components/jellyseerr/RequestModal.tsx @@ -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; diff --git a/components/jellyseerr/discover/CompanySlide.tsx b/components/jellyseerr/discover/CompanySlide.tsx index fab70288..816feee4 100644 --- a/components/jellyseerr/discover/CompanySlide.tsx +++ b/components/jellyseerr/discover/CompanySlide.tsx @@ -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) => ( navigate(item)}> = ({ 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 ( {sortedSliders.map((slide) => { @@ -60,6 +63,8 @@ const Discover: React.FC = ({ sliders }) => { contentContainerStyle={{ paddingBottom: 16 }} /> ); + default: + return null; } })} diff --git a/components/jellyseerr/discover/GenericSlideCard.tsx b/components/jellyseerr/discover/GenericSlideCard.tsx index 51292abd..80742bc7 100644 --- a/components/jellyseerr/discover/GenericSlideCard.tsx +++ b/components/jellyseerr/discover/GenericSlideCard.tsx @@ -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: { diff --git a/components/jellyseerr/discover/GenreSlide.tsx b/components/jellyseerr/discover/GenreSlide.tsx index 2cac3813..7bd8f6d3 100644 --- a/components/jellyseerr/discover/GenreSlide.tsx +++ b/components/jellyseerr/discover/GenreSlide.tsx @@ -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 = ({ slide, ...props }) => { const segments = useSegments(); @@ -43,7 +43,7 @@ const GenreSlide: React.FC = ({ slide, ...props }) => { slide={slide} data={data} keyExtractor={(item) => item.id.toString()} - renderItem={(item, index) => ( + renderItem={(item, _index) => ( navigate(item)}> = ({ slide, @@ -25,7 +25,7 @@ const MovieTvSlide: React.FC = ({ 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), }; diff --git a/components/jellyseerr/discover/RecentRequestsSlide.tsx b/components/jellyseerr/discover/RecentRequestsSlide.tsx index c4598661..c7a2214e 100644 --- a/components/jellyseerr/discover/RecentRequestsSlide.tsx +++ b/components/jellyseerr/discover/RecentRequestsSlide.tsx @@ -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(); diff --git a/components/jellyseerr/discover/Slide.tsx b/components/jellyseerr/discover/Slide.tsx index 74f78f13..5edb49f5 100644 --- a/components/jellyseerr/discover/Slide.tsx +++ b/components/jellyseerr/discover/Slide.tsx @@ -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 = ({ onEndReached={onEndReached} //@ts-ignore renderItem={({ item, index }) => - item ? renderItem(item, index) : <> + item ? renderItem(item, index) : null } /> diff --git a/components/library/LibraryItemCard.tsx b/components/library/LibraryItemCard.tsx index 34c6f8ef..e405f900 100644 --- a/components/library/LibraryItemCard.tsx +++ b/components/library/LibraryItemCard.tsx @@ -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 { diff --git a/components/list/ListGroup.tsx b/components/list/ListGroup.tsx index 28752978..b9752bac 100644 --- a/components/list/ListGroup.tsx +++ b/components/list/ListGroup.tsx @@ -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; diff --git a/components/livetv/HourHeader.tsx b/components/livetv/HourHeader.tsx index f412cb9b..76636ed0 100644 --- a/components/livetv/HourHeader.tsx +++ b/components/livetv/HourHeader.tsx @@ -1,4 +1,3 @@ -import React from "react"; import { View } from "react-native"; import { Text } from "../common/Text"; diff --git a/components/livetv/LiveTVGuideRow.tsx b/components/livetv/LiveTVGuideRow.tsx index 83a5ada1..fcdcd0d3 100644 --- a/components/livetv/LiveTVGuideRow.tsx +++ b/components/livetv/LiveTVGuideRow.tsx @@ -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) => { diff --git a/components/medialists/MediaListSection.tsx b/components/medialists/MediaListSection.tsx index 13db9826..da5ed8bf 100644 --- a/components/medialists/MediaListSection.tsx +++ b/components/medialists/MediaListSection.tsx @@ -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 { diff --git a/components/movies/MoviesTitleHeader.tsx b/components/movies/MoviesTitleHeader.tsx index 954aeaa3..f828825d 100644 --- a/components/movies/MoviesTitleHeader.tsx +++ b/components/movies/MoviesTitleHeader.tsx @@ -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; diff --git a/components/navigation/TabBarIcon.tsx b/components/navigation/TabBarIcon.tsx index 0cc6eff6..a28bba84 100644 --- a/components/navigation/TabBarIcon.tsx +++ b/components/navigation/TabBarIcon.tsx @@ -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({ diff --git a/components/posters/EpisodePoster.tsx b/components/posters/EpisodePoster.tsx index 5b33a486..af42989b 100644 --- a/components/posters/EpisodePoster.tsx +++ b/components/posters/EpisodePoster.tsx @@ -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 = ({ } }, [item]); - const [progress, setProgress] = useState( + const [progress, _setProgress] = useState( item.UserData?.PlayedPercentage || 0, ); diff --git a/components/posters/ItemPoster.tsx b/components/posters/ItemPoster.tsx index 95beb4d4..bcd857e7 100644 --- a/components/posters/ItemPoster.tsx +++ b/components/posters/ItemPoster.tsx @@ -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 = ({ showProgress, ...props }) => { - const [progress, setProgress] = useState( + const [progress, _setProgress] = useState( item.UserData?.PlayedPercentage || 0, ); diff --git a/components/posters/JellyseerrPoster.tsx b/components/posters/JellyseerrPoster.tsx index 63f1fd7a..5e3ca140 100644 --- a/components/posters/JellyseerrPoster.tsx +++ b/components/posters/JellyseerrPoster.tsx @@ -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; diff --git a/components/posters/MoviePoster.tsx b/components/posters/MoviePoster.tsx index f9fc3ec4..c8fbd791 100644 --- a/components/posters/MoviePoster.tsx +++ b/components/posters/MoviePoster.tsx @@ -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 = ({ }); }, [item]); - const [progress, setProgress] = useState( + const [progress, _setProgress] = useState( item.UserData?.PlayedPercentage || 0, ); diff --git a/components/posters/ParentPoster.tsx b/components/posters/ParentPoster.tsx index 6c4b4da8..47b62e4c 100644 --- a/components/posters/ParentPoster.tsx +++ b/components/posters/ParentPoster.tsx @@ -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; diff --git a/components/posters/SeriesPoster.tsx b/components/posters/SeriesPoster.tsx index 2deba076..07f212d7 100644 --- a/components/posters/SeriesPoster.tsx +++ b/components/posters/SeriesPoster.tsx @@ -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; diff --git a/components/search/LoadingSkeleton.tsx b/components/search/LoadingSkeleton.tsx index d98a4829..29efd428 100644 --- a/components/search/LoadingSkeleton.tsx +++ b/components/search/LoadingSkeleton.tsx @@ -1,7 +1,7 @@ import { View } from "react-native"; import Animated, { - useAnimatedStyle, useAnimatedReaction, + useAnimatedStyle, useSharedValue, withTiming, } from "react-native-reanimated"; diff --git a/components/search/SearchItemWrapper.tsx b/components/search/SearchItemWrapper.tsx index de1d954b..9aee433e 100644 --- a/components/search/SearchItemWrapper.tsx +++ b/components/search/SearchItemWrapper.tsx @@ -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 = { @@ -21,8 +18,8 @@ export const SearchItemWrapper = ({ header, onEndReached, }: PropsWithChildren>) => { - const [api] = useAtom(apiAtom); - const [user] = useAtom(userAtom); + const [_api] = useAtom(apiAtom); + const [_user] = useAtom(userAtom); if (!items || items.length === 0) return null; diff --git a/components/series/CastAndCrew.tsx b/components/series/CastAndCrew.tsx index a0948f2d..60fb9012 100644 --- a/components/series/CastAndCrew.tsx +++ b/components/series/CastAndCrew.tsx @@ -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 = ({ item, loading, ...props }) => { i.Id.toString()} + keyExtractor={(i, _idx) => i.Id.toString()} height={247} data={destinctPeople} renderItem={(i) => ( diff --git a/components/series/CurrentSeries.tsx b/components/series/CurrentSeries.tsx index e798bb22..19d80e50 100644 --- a/components/series/CurrentSeries.tsx +++ b/components/series/CurrentSeries.tsx @@ -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 = ({ item, ...props }) => { ( + renderItem={(item, _index) => ( router.push(`/series/${item.SeriesId}`)} diff --git a/components/series/EpisodeTitleHeader.tsx b/components/series/EpisodeTitleHeader.tsx index c740356f..a4f6fc89 100644 --- a/components/series/EpisodeTitleHeader.tsx +++ b/components/series/EpisodeTitleHeader.tsx @@ -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; diff --git a/components/series/JellyseerrSeasons.tsx b/components/series/JellyseerrSeasons.tsx index db06be59..6116313e 100644 --- a/components/series/JellyseerrSeasons.tsx +++ b/components/series/JellyseerrSeasons.tsx @@ -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 ( @@ -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}`} - {item.overview} @@ -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 ( @@ -269,7 +267,7 @@ const JellyseerrSeasons: React.FC<{ return ( 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 ( s.seasonNumber === season.seasonNumber, - )?.status - } + mediaStatus={season.status} showRequestIcon={canRequest} /> ); diff --git a/components/series/NextItemButton.tsx b/components/series/NextItemButton.tsx index 2f05eeaa..a2aae63f 100644 --- a/components/series/NextItemButton.tsx +++ b/components/series/NextItemButton.tsx @@ -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 { diff --git a/components/series/NextUp.tsx b/components/series/NextUp.tsx index e368bd8e..f0c15990 100644 --- a/components/series/NextUp.tsx +++ b/components/series/NextUp.tsx @@ -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); diff --git a/components/series/SeasonDropdown.tsx b/components/series/SeasonDropdown.tsx index 21ce2539..6cfef3c2 100644 --- a/components/series/SeasonDropdown.tsx +++ b/components/series/SeasonDropdown.tsx @@ -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 = ({ state, onSelect, }) => { - if (Platform.isTV) return null; + const isTv = Platform.isTV; const keys = useMemo( () => @@ -50,10 +52,11 @@ export const SeasonDropdown: React.FC = ({ 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 = ({ 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 ( diff --git a/components/series/SeasonEpisodesCarousel.tsx b/components/series/SeasonEpisodesCarousel.tsx index 1c1ec80b..d731d145 100644 --- a/components/series/SeasonEpisodesCarousel.tsx +++ b/components/series/SeasonEpisodesCarousel.tsx @@ -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 = ({ data={episodes} extraData={item} loading={loading || isLoading || isFetching} - renderItem={(_item, idx) => ( + renderItem={(_item, _idx) => ( { diff --git a/components/series/SeasonPicker.tsx b/components/series/SeasonPicker.tsx index f6f697aa..56c4cbfc 100644 --- a/components/series/SeasonPicker.tsx +++ b/components/series/SeasonPicker.tsx @@ -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; diff --git a/components/series/SeriesActions.tsx b/components/series/SeriesActions.tsx index a687ae60..64a9dbfa 100644 --- a/components/series/SeriesActions.tsx +++ b/components/series/SeriesActions.tsx @@ -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; diff --git a/components/series/SeriesHeader.tsx b/components/series/SeriesHeader.tsx index 4c28feb8..39498c2b 100644 --- a/components/series/SeriesHeader.tsx +++ b/components/series/SeriesHeader.tsx @@ -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 { diff --git a/components/settings/AppLanguageSelector.tsx b/components/settings/AppLanguageSelector.tsx index ca6d6da8..b05ab39e 100644 --- a/components/settings/AppLanguageSelector.tsx +++ b/components/settings/AppLanguageSelector.tsx @@ -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 }) => { - 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 ( diff --git a/components/settings/AudioToggles.tsx b/components/settings/AudioToggles.tsx index 532ce538..b7d41e72 100644 --- a/components/settings/AudioToggles.tsx +++ b/components/settings/AudioToggles.tsx @@ -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 }) => { - 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 ( diff --git a/components/settings/ChromecastSettings.tsx b/components/settings/ChromecastSettings.tsx index 33b67d4a..e2c1148d 100644 --- a/components/settings/ChromecastSettings.tsx +++ b/components/settings/ChromecastSettings.tsx @@ -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"; diff --git a/components/settings/Dashboard.tsx b/components/settings/Dashboard.tsx index a97d31e4..a9771643 100644 --- a/components/settings/Dashboard.tsx +++ b/components/settings/Dashboard.tsx @@ -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(); diff --git a/components/settings/DisabledSetting.tsx b/components/settings/DisabledSetting.tsx index d0d5c33d..04e24f86 100644 --- a/components/settings/DisabledSetting.tsx +++ b/components/settings/DisabledSetting.tsx @@ -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 diff --git a/components/settings/DownloadSettings.tsx b/components/settings/DownloadSettings.tsx index ca0c655b..dc4bb669 100644 --- a/components/settings/DownloadSettings.tsx +++ b/components/settings/DownloadSettings.tsx @@ -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"; diff --git a/components/settings/DownloadSettings.tv.tsx b/components/settings/DownloadSettings.tv.tsx index 8cd6fa73..c9de3346 100644 --- a/components/settings/DownloadSettings.tv.tsx +++ b/components/settings/DownloadSettings.tv.tsx @@ -1,5 +1,3 @@ -import React from "react"; - export default function DownloadSettings({ ...props }) { - return <>; + return null; } diff --git a/components/settings/HomeIndex.tsx b/components/settings/HomeIndex.tsx index 894e8702..bef4a6ae 100644 --- a/components/settings/HomeIndex.tsx +++ b/components/settings/HomeIndex.tsx @@ -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 ( diff --git a/components/settings/Jellyseerr.tsx b/components/settings/Jellyseerr.tsx index b1f938c7..20c8bcd6 100644 --- a/components/settings/Jellyseerr.tsx +++ b/components/settings/Jellyseerr.tsx @@ -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 diff --git a/components/settings/MediaContext.tsx b/components/settings/MediaContext.tsx index d9813e66..1f03a48f 100644 --- a/components/settings/MediaContext.tsx +++ b/components/settings/MediaContext.tsx @@ -1,5 +1,3 @@ -import { apiAtom } from "@/providers/JellyfinProvider"; -import { type Settings, useSettings } from "@/utils/atoms/settings"; import type { CultureDto, UserConfiguration, @@ -8,13 +6,9 @@ import type { import { getLocalizationApi, getUserApi } from "@jellyfin/sdk/lib/utils/api"; import { useQuery, useQueryClient } from "@tanstack/react-query"; import { useAtomValue } from "jotai"; -import React, { - createContext, - useContext, - type ReactNode, - useEffect, - useState, -} from "react"; +import { createContext, type ReactNode, useContext, useEffect } from "react"; +import { apiAtom } from "@/providers/JellyfinProvider"; +import { type Settings, useSettings } from "@/utils/atoms/settings"; interface MediaContextType { settings: Settings | null; @@ -51,7 +45,7 @@ export const MediaProvider = ({ children }: { children: ReactNode }) => { }, }); queryClient.invalidateQueries({ queryKey: ["authUser"] }); - } catch (error) {} + } catch (_error) {} } }; diff --git a/components/settings/MediaToggles.tsx b/components/settings/MediaToggles.tsx index a5d118af..2a448f23 100644 --- a/components/settings/MediaToggles.tsx +++ b/components/settings/MediaToggles.tsx @@ -1,10 +1,10 @@ -import { Stepper } from "@/components/inputs/Stepper"; -import DisabledSetting from "@/components/settings/DisabledSetting"; -import { useSettings } from "@/utils/atoms/settings"; import type React from "react"; import { useMemo } from "react"; import { useTranslation } from "react-i18next"; import type { ViewProps } from "react-native"; +import { Stepper } from "@/components/inputs/Stepper"; +import DisabledSetting from "@/components/settings/DisabledSetting"; +import { useSettings } from "@/utils/atoms/settings"; import { ListGroup } from "../list/ListGroup"; import { ListItem } from "../list/ListItem"; @@ -15,8 +15,6 @@ export const MediaToggles: React.FC = ({ ...props }) => { const [settings, updateSettings, pluginSettings] = useSettings(); - if (!settings) return null; - const disabled = useMemo( () => pluginSettings?.forwardSkipTime?.locked === true && @@ -24,6 +22,8 @@ export const MediaToggles: React.FC = ({ ...props }) => { [pluginSettings], ); + if (!settings) return null; + return ( diff --git a/components/settings/OtherSettings.tsx b/components/settings/OtherSettings.tsx index 800ca897..10695700 100644 --- a/components/settings/OtherSettings.tsx +++ b/components/settings/OtherSettings.tsx @@ -1,3 +1,11 @@ +import { Ionicons } from "@expo/vector-icons"; +import { useRouter } from "expo-router"; +import { TFunction } from "i18next"; +import type React from "react"; +import { useEffect, useMemo } from "react"; +import { useTranslation } from "react-i18next"; +import { Linking, Platform, Switch, TouchableOpacity } from "react-native"; +import { toast } from "sonner-native"; import { BITRATES } from "@/components/BitrateSelector"; import Dropdown from "@/components/common/Dropdown"; import DisabledSetting from "@/components/settings/DisabledSetting"; @@ -8,17 +16,10 @@ import { registerBackgroundFetchAsync, unregisterBackgroundFetchAsync, } from "@/utils/background-tasks"; -import { Ionicons } from "@expo/vector-icons"; -import { useRouter } from "expo-router"; -import i18n, { TFunction } from "i18next"; -import type React from "react"; -import { useEffect, useMemo } from "react"; -import { useTranslation } from "react-i18next"; -import { Linking, Platform, Switch, TouchableOpacity } from "react-native"; -import { toast } from "sonner-native"; import { Text } from "../common/Text"; import { ListGroup } from "../list/ListGroup"; import { ListItem } from "../list/ListItem"; + const BackgroundFetch = !Platform.isTV ? require("expo-background-fetch") : null; diff --git a/components/settings/PluginSettings.tsx b/components/settings/PluginSettings.tsx index f064c8c3..cfc78671 100644 --- a/components/settings/PluginSettings.tsx +++ b/components/settings/PluginSettings.tsx @@ -1,13 +1,12 @@ -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 { useSettings } from "@/utils/atoms/settings"; import { ListGroup } from "../list/ListGroup"; import { ListItem } from "../list/ListItem"; export const PluginSettings = () => { - const [settings, updateSettings] = useSettings(); + const [settings, _updateSettings] = useSettings(); const router = useRouter(); diff --git a/components/settings/QuickConnect.tsx b/components/settings/QuickConnect.tsx index 83a98a65..b61500ce 100644 --- a/components/settings/QuickConnect.tsx +++ b/components/settings/QuickConnect.tsx @@ -1,5 +1,3 @@ -import { useHaptic } from "@/hooks/useHaptic"; -import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; import { BottomSheetBackdrop, type BottomSheetBackdropProps, @@ -13,6 +11,8 @@ import type React from "react"; import { useCallback, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { Alert, View, type ViewProps } from "react-native"; +import { useHaptic } from "@/hooks/useHaptic"; +import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; import { Button } from "../Button"; import { Text } from "../common/Text"; import { ListGroup } from "../list/ListGroup"; @@ -63,7 +63,7 @@ export const QuickConnect: React.FC = ({ ...props }) => { t("home.settings.quick_connect.invalid_code"), ); } - } catch (e) { + } catch (_e) { errorHapticFeedback(); Alert.alert( t("home.settings.quick_connect.error"), diff --git a/components/settings/StorageSettings.tsx b/components/settings/StorageSettings.tsx index a62c2316..fb7472c7 100644 --- a/components/settings/StorageSettings.tsx +++ b/components/settings/StorageSettings.tsx @@ -1,12 +1,12 @@ -import { Text } from "@/components/common/Text"; -import { Colors } from "@/constants/Colors"; -import { useHaptic } from "@/hooks/useHaptic"; -import { useDownload } from "@/providers/DownloadProvider"; import { useQuery } from "@tanstack/react-query"; import * as FileSystem from "expo-file-system"; import { useTranslation } from "react-i18next"; import { View } from "react-native"; import { toast } from "sonner-native"; +import { Text } from "@/components/common/Text"; +import { Colors } from "@/constants/Colors"; +import { useHaptic } from "@/hooks/useHaptic"; +import { useDownload } from "@/providers/DownloadProvider"; import { ListGroup } from "../list/ListGroup"; import { ListItem } from "../list/ListItem"; @@ -32,7 +32,7 @@ export const StorageSettings = () => { try { await deleteAllFiles(); successHapticFeedback(); - } catch (e) { + } catch (_e) { errorHapticFeedback(); toast.error(t("home.settings.toasts.error_deleting_files")); } diff --git a/components/settings/SubtitleToggles.tsx b/components/settings/SubtitleToggles.tsx index ff0bfe6e..69a7d7ab 100644 --- a/components/settings/SubtitleToggles.tsx +++ b/components/settings/SubtitleToggles.tsx @@ -1,12 +1,14 @@ import { Platform, TouchableOpacity, View, type ViewProps } from "react-native"; -const DropdownMenu = !Platform.isTV ? require("zeego/dropdown-menu") : null; -import Dropdown from "@/components/common/Dropdown"; -import { Stepper } from "@/components/inputs/Stepper"; -import { useSettings } from "@/utils/atoms/settings"; + +const _DropdownMenu = !Platform.isTV ? require("zeego/dropdown-menu") : null; + import { Ionicons } from "@expo/vector-icons"; import { SubtitlePlaybackMode } from "@jellyfin/sdk/lib/generated-client"; import { useTranslation } from "react-i18next"; import { Switch } from "react-native-gesture-handler"; +import Dropdown from "@/components/common/Dropdown"; +import { Stepper } from "@/components/inputs/Stepper"; +import { useSettings } from "@/utils/atoms/settings"; import { Text } from "../common/Text"; import { ListGroup } from "../list/ListGroup"; import { ListItem } from "../list/ListItem"; @@ -15,13 +17,15 @@ import { useMedia } from "./MediaContext"; interface Props extends ViewProps {} export const SubtitleToggles: React.FC = ({ ...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; const subtitleModes = [ diff --git a/components/settings/UserInfo.tsx b/components/settings/UserInfo.tsx index 0bd2887a..56b6413d 100644 --- a/components/settings/UserInfo.tsx +++ b/components/settings/UserInfo.tsx @@ -1,11 +1,8 @@ -import { apiAtom, useJellyfin, userAtom } from "@/providers/JellyfinProvider"; import * as Application from "expo-application"; -import Constants from "expo-constants"; import { useAtom } from "jotai"; import { useTranslation } from "react-i18next"; import { View, type ViewProps } from "react-native"; -import { Button } from "../Button"; -import { Text } from "../common/Text"; +import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; import { ListGroup } from "../list/ListGroup"; import { ListItem } from "../list/ListItem"; diff --git a/components/video-player/controls/AudioSlider.tsx b/components/video-player/controls/AudioSlider.tsx index 0bc02cdd..f3e34455 100644 --- a/components/video-player/controls/AudioSlider.tsx +++ b/components/video-player/controls/AudioSlider.tsx @@ -3,9 +3,11 @@ import { useEffect, useRef } from "react"; import { Platform, StyleSheet, View } from "react-native"; import { Slider } from "react-native-awesome-slider"; import { useSharedValue } from "react-native-reanimated"; + const VolumeManager = Platform.isTV ? null : require("react-native-volume-manager"); + import { Ionicons } from "@expo/vector-icons"; import type { VolumeResult } from "react-native-volume-manager"; @@ -14,9 +16,7 @@ interface AudioSliderProps { } const AudioSlider: React.FC = ({ setVisibility }) => { - if (Platform.isTV) { - return; - } + const isTv = Platform.isTV; const volume = useSharedValue(50); // Explicitly type as number const min = useSharedValue(0); // Explicitly type as number @@ -25,6 +25,7 @@ const AudioSlider: React.FC = ({ setVisibility }) => { const timeoutRef = useRef(null); // Use a ref to store the timeout ID useEffect(() => { + if (isTv) return; const fetchInitialVolume = async () => { try { const { volume: initialVolume } = await VolumeManager.getVolume(); @@ -42,7 +43,7 @@ const AudioSlider: React.FC = ({ setVisibility }) => { // Re-enable the native volume UI when the component unmounts VolumeManager.showNativeVolumeUI({ enabled: true }); }; - }, []); + }, [isTv, volume]); const handleValueChange = async (value: number) => { volume.value = value; @@ -53,6 +54,7 @@ const AudioSlider: React.FC = ({ setVisibility }) => { }; useEffect(() => { + if (isTv) return; const volumeListener = VolumeManager.addVolumeListener( (result: VolumeResult) => { volume.value = result.volume * 100; @@ -76,7 +78,9 @@ const AudioSlider: React.FC = ({ setVisibility }) => { clearTimeout(timeoutRef.current); } }; - }, [volume]); + }, [isTv, volume, setVisibility]); + + if (isTv) return; return ( diff --git a/components/video-player/controls/BrightnessSlider.tsx b/components/video-player/controls/BrightnessSlider.tsx index f669ab59..12023a86 100644 --- a/components/video-player/controls/BrightnessSlider.tsx +++ b/components/video-player/controls/BrightnessSlider.tsx @@ -1,32 +1,36 @@ -import React, { useEffect } from "react"; +import { useEffect } from "react"; import { Platform, StyleSheet, View } from "react-native"; import { Slider } from "react-native-awesome-slider"; import { useSharedValue } from "react-native-reanimated"; + // import * as Brightness from "expo-brightness"; const Brightness = !Platform.isTV ? require("expo-brightness") : null; + import { Ionicons } from "@expo/vector-icons"; -import MaterialCommunityIcons from "@expo/vector-icons/MaterialCommunityIcons"; const BrightnessSlider = () => { - if (Platform.isTV) return; + const isTv = Platform.isTV; const brightness = useSharedValue(50); const min = useSharedValue(0); const max = useSharedValue(100); useEffect(() => { + if (isTv) return; const fetchInitialBrightness = async () => { const initialBrightness = await Brightness.getBrightnessAsync(); brightness.value = initialBrightness * 100; }; fetchInitialBrightness(); - }, []); + }, [brightness, isTv]); const handleValueChange = async (value: number) => { brightness.value = value; await Brightness.setBrightnessAsync(value / 100); }; + if (isTv) return; + return ( = ({ ({ isAutoPlay, resetWatchCount, - }: { isAutoPlay?: boolean; resetWatchCount?: boolean }) => { + }: { + isAutoPlay?: boolean; + resetWatchCount?: boolean; + }) => { if (!nextItem) { return; } diff --git a/components/video-player/controls/EpisodeList.tsx b/components/video-player/controls/EpisodeList.tsx index 5ce3d8ce..982a6d96 100644 --- a/components/video-player/controls/EpisodeList.tsx +++ b/components/video-player/controls/EpisodeList.tsx @@ -1,18 +1,3 @@ -import ContinueWatchingPoster from "@/components/ContinueWatchingPoster"; -import { DownloadSingleItem } from "@/components/DownloadItem"; -import { Loader } from "@/components/Loader"; -import { - HorizontalScroll, - type HorizontalScrollRef, -} from "@/components/common/HorrizontalScroll"; -import { Text } from "@/components/common/Text"; -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 } from "@expo/vector-icons"; import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; import { getTvShowsApi } from "@jellyfin/sdk/lib/utils/api"; @@ -21,6 +6,21 @@ import { atom, useAtom } from "jotai"; import { useEffect, useMemo, useRef, useState } from "react"; import { TouchableOpacity, View } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; +import ContinueWatchingPoster from "@/components/ContinueWatchingPoster"; +import { + HorizontalScroll, + type HorizontalScrollRef, +} from "@/components/common/HorrizontalScroll"; +import { Text } from "@/components/common/Text"; +import { DownloadSingleItem } from "@/components/DownloadItem"; +import { Loader } from "@/components/Loader"; +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"; type Props = { item: BaseItemDto; @@ -33,7 +33,7 @@ export const seasonIndexAtom = atom({}); export const EpisodeList: React.FC = ({ item, close, goToItem }) => { const [api] = useAtom(apiAtom); const [user] = useAtom(userAtom); - const insets = useSafeAreaInsets(); // Get safe area insets + const _insets = useSafeAreaInsets(); // Get safe area insets const [seasonIndexState, setSeasonIndexState] = useAtom(seasonIndexAtom); const scrollViewRef = useRef(null); // Reference to the HorizontalScroll const scrollToIndex = (index: number) => { @@ -163,92 +163,87 @@ export const EpisodeList: React.FC = ({ item, close, goToItem }) => { width: "100%", }} > - <> - - {seriesItem && ( - { - setSeasonIndexState((prev) => ({ - ...prev, - [item.SeriesId ?? ""]: season.IndexNumber, - })); - }} - /> - )} - { - close(); + + {seriesItem && ( + { + setSeasonIndexState((prev) => ({ + ...prev, + [item.SeriesId ?? ""]: season.IndexNumber, + })); }} - className='aspect-square flex flex-col bg-neutral-800/90 rounded-xl items-center justify-center p-2' - > - - - + /> + )} + { + close(); + }} + className='aspect-square flex flex-col bg-neutral-800/90 rounded-xl items-center justify-center p-2' + > + + + - ( - ( + + { + goToItem(_item.Id); + }} > - { - goToItem(_item.Id); + + + + - - - - - {_item.Name} - - - {`S${_item.ParentIndexNumber?.toString()}:E${_item.IndexNumber?.toString()}`} - - - {runtimeTicksToSeconds(_item.RunTimeTicks)} - - - - - - - {_item.Overview} + {_item.Name} + + + {`S${_item.ParentIndexNumber?.toString()}:E${_item.IndexNumber?.toString()}`} + + + {runtimeTicksToSeconds(_item.RunTimeTicks)} - )} - keyExtractor={(e: BaseItemDto) => e.Id ?? ""} - estimatedItemSize={200} - showsHorizontalScrollIndicator={false} - /> - + + + + + {_item.Overview} + + + )} + keyExtractor={(e: BaseItemDto) => e.Id ?? ""} + estimatedItemSize={200} + showsHorizontalScrollIndicator={false} + /> ); }; diff --git a/components/video-player/controls/NextEpisodeCountDownButton.tsx b/components/video-player/controls/NextEpisodeCountDownButton.tsx index 093ec4de..e769c2f5 100644 --- a/components/video-player/controls/NextEpisodeCountDownButton.tsx +++ b/components/video-player/controls/NextEpisodeCountDownButton.tsx @@ -1,5 +1,3 @@ -import { Text } from "@/components/common/Text"; -import { Colors } from "@/constants/Colors"; import type React from "react"; import { useEffect } from "react"; import { useTranslation } from "react-i18next"; @@ -9,12 +7,14 @@ import { View, } from "react-native"; import Animated, { + Easing, + runOnJS, useAnimatedStyle, useSharedValue, withTiming, - Easing, - runOnJS, } from "react-native-reanimated"; +import { Text } from "@/components/common/Text"; +import { Colors } from "@/constants/Colors"; interface NextEpisodeCountDownButtonProps extends TouchableOpacityProps { onFinish?: () => void; diff --git a/components/video-player/controls/SliderScrubbter.tsx b/components/video-player/controls/SliderScrubbter.tsx index 10555e68..7de3c7d5 100644 --- a/components/video-player/controls/SliderScrubbter.tsx +++ b/components/video-player/controls/SliderScrubbter.tsx @@ -1,12 +1,12 @@ -import { useTrickplay } from "@/hooks/useTrickplay"; -import { formatTimeString, msToTicks, ticksToSeconds } from "@/utils/time"; import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client"; import { Image } from "expo-image"; import type React from "react"; -import { useRef, useState } from "react"; +import { useState } from "react"; import { Text, View } from "react-native"; import { Slider } from "react-native-awesome-slider"; -import { type SharedValue, useSharedValue } from "react-native-reanimated"; +import { type SharedValue } from "react-native-reanimated"; +import { useTrickplay } from "@/hooks/useTrickplay"; +import { formatTimeString, msToTicks, ticksToSeconds } from "@/utils/time"; interface SliderScrubberProps { cacheProgress: SharedValue; diff --git a/components/video-player/controls/contexts/ControlContext.tsx b/components/video-player/controls/contexts/ControlContext.tsx index 3eed62bd..c13211c9 100644 --- a/components/video-player/controls/contexts/ControlContext.tsx +++ b/components/video-player/controls/contexts/ControlContext.tsx @@ -3,7 +3,7 @@ import type { MediaSourceInfo, } from "@jellyfin/sdk/lib/generated-client"; import type React from "react"; -import { type ReactNode, createContext, useContext, useState } from "react"; +import { createContext, type ReactNode, useContext } from "react"; interface ControlContextProps { item: BaseItemDto; diff --git a/components/vlc/VideoDebugInfo.tsx b/components/vlc/VideoDebugInfo.tsx index 59e6a018..bfdc9051 100644 --- a/components/vlc/VideoDebugInfo.tsx +++ b/components/vlc/VideoDebugInfo.tsx @@ -1,9 +1,9 @@ -import type { TrackInfo, VlcPlayerViewRef } from "@/modules/VlcPlayer.types"; import type React from "react"; import { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { TouchableOpacity, View, type ViewProps } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; +import type { TrackInfo, VlcPlayerViewRef } from "@/modules/VlcPlayer.types"; import { Text } from "../common/Text"; interface Props extends ViewProps { diff --git a/hooks/useAdjacentEpisodes.ts b/hooks/useAdjacentEpisodes.ts index 4c5a6e2b..22777836 100644 --- a/hooks/useAdjacentEpisodes.ts +++ b/hooks/useAdjacentEpisodes.ts @@ -1,9 +1,9 @@ -import { apiAtom } from "@/providers/JellyfinProvider"; import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client"; import { getTvShowsApi } from "@jellyfin/sdk/lib/utils/api"; import { useQuery } from "@tanstack/react-query"; import { useAtomValue } from "jotai"; import { useMemo } from "react"; +import { apiAtom } from "@/providers/JellyfinProvider"; interface AdjacentEpisodesProps { item?: BaseItemDto | null; diff --git a/hooks/useControlsVisibility.ts b/hooks/useControlsVisibility.ts index 71c6197d..caca0d84 100644 --- a/hooks/useControlsVisibility.ts +++ b/hooks/useControlsVisibility.ts @@ -1,9 +1,5 @@ -import { useCallback, useEffect, useRef, useState } from "react"; -import { - runOnJS, - useAnimatedReaction, - useSharedValue, -} from "react-native-reanimated"; +import { useCallback, useEffect, useRef } from "react"; +import { useSharedValue } from "react-native-reanimated"; export const useControlsVisibility = (timeout = 3000) => { const opacity = useSharedValue(1); diff --git a/hooks/useCreditSkipper.ts b/hooks/useCreditSkipper.ts index 0317f66b..9705c98b 100644 --- a/hooks/useCreditSkipper.ts +++ b/hooks/useCreditSkipper.ts @@ -1,10 +1,10 @@ +import { useQuery } from "@tanstack/react-query"; +import { useAtom } from "jotai"; +import { useCallback, useEffect, useState } from "react"; import { apiAtom } from "@/providers/JellyfinProvider"; import { getAuthHeaders } from "@/utils/jellyfin/jellyfin"; import { writeToLog } from "@/utils/log"; import { msToSeconds, secondsToMs } from "@/utils/time"; -import { useQuery } from "@tanstack/react-query"; -import { useAtom } from "jotai"; -import { useCallback, useEffect, useState } from "react"; import { useHaptic } from "./useHaptic"; interface CreditTimestamps { diff --git a/hooks/useDownloadedFileOpener.ts b/hooks/useDownloadedFileOpener.ts index 398bf448..6338517d 100644 --- a/hooks/useDownloadedFileOpener.ts +++ b/hooks/useDownloadedFileOpener.ts @@ -1,9 +1,9 @@ -import { usePlaySettings } from "@/providers/PlaySettingsProvider"; -import { writeToLog } from "@/utils/log"; import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client"; import * as FileSystem from "expo-file-system"; import { useRouter } from "expo-router"; import { useCallback } from "react"; +import { usePlaySettings } from "@/providers/PlaySettingsProvider"; +import { writeToLog } from "@/utils/log"; export const getDownloadedFileUrl = async (itemId: string): Promise => { const directory = FileSystem.documentDirectory; diff --git a/hooks/useFavorite.ts b/hooks/useFavorite.ts index 74a0216e..b9e47e08 100644 --- a/hooks/useFavorite.ts +++ b/hooks/useFavorite.ts @@ -1,9 +1,9 @@ -import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client"; import { getUserLibraryApi } from "@jellyfin/sdk/lib/utils/api"; import { useMutation, useQueryClient } from "@tanstack/react-query"; import { useAtom } from "jotai"; -import { useEffect, useMemo, useState } from "react"; +import { useEffect, useState } from "react"; +import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; export const useFavorite = (item: BaseItemDto) => { const queryClient = useQueryClient(); @@ -49,7 +49,7 @@ export const useFavorite = (item: BaseItemDto) => { return { previousItem }; }, - onError: (err, variables, context) => { + onError: (_err, _variables, context) => { if (context?.previousItem) { queryClient.setQueryData([type, item.Id], context.previousItem); } @@ -80,7 +80,7 @@ export const useFavorite = (item: BaseItemDto) => { return { previousItem }; }, - onError: (err, variables, context) => { + onError: (_err, _variables, context) => { if (context?.previousItem) { queryClient.setQueryData([type, item.Id], context.previousItem); } diff --git a/hooks/useHaptic.ts b/hooks/useHaptic.ts index 132a599c..02ca46b8 100644 --- a/hooks/useHaptic.ts +++ b/hooks/useHaptic.ts @@ -1,6 +1,7 @@ -import { useSettings } from "@/utils/atoms/settings"; import { useCallback, useMemo } from "react"; import { Platform } from "react-native"; +import { useSettings } from "@/utils/atoms/settings"; + const Haptics = !Platform.isTV ? require("expo-haptics") : null; export type HapticFeedbackType = @@ -14,10 +15,7 @@ export type HapticFeedbackType = export const useHaptic = (feedbackType: HapticFeedbackType = "selection") => { const [settings] = useSettings(); - - if (Platform.isTV) { - return () => {}; - } + const isTv = Platform.isTV; const createHapticHandler = useCallback( (type: typeof Haptics.ImpactFeedbackStyle) => { @@ -27,6 +25,7 @@ export const useHaptic = (feedbackType: HapticFeedbackType = "selection") => { }, [], ); + const createNotificationFeedback = useCallback( (type: typeof Haptics.NotificationFeedbackType) => { return Platform.OS === "web" || Platform.isTV @@ -56,6 +55,10 @@ export const useHaptic = (feedbackType: HapticFeedbackType = "selection") => { [createHapticHandler, createNotificationFeedback], ); + if (isTv) { + return () => {}; + } + if (settings?.disableHapticFeedback) { return () => {}; } diff --git a/hooks/useImageColors.ts b/hooks/useImageColors.ts index 0e00b88c..fa4a9780 100644 --- a/hooks/useImageColors.ts +++ b/hooks/useImageColors.ts @@ -1,3 +1,7 @@ +import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client"; +import { useAtom, useAtomValue } from "jotai"; +import { useEffect, useMemo } from "react"; +import { Platform } from "react-native"; import { apiAtom } from "@/providers/JellyfinProvider"; import { adjustToNearBlack, @@ -7,10 +11,7 @@ import { } from "@/utils/atoms/primaryColor"; import { getItemImage } from "@/utils/getItemImage"; import { storage } from "@/utils/mmkv"; -import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client"; -import { useAtom, useAtomValue } from "jotai"; -import { useEffect, useMemo } from "react"; -import { Platform } from "react-native"; + // import { getColors } from "react-native-image-colors"; const Colors = !Platform.isTV ? require("react-native-image-colors") : null; @@ -30,11 +31,11 @@ export const useImageColors = ({ url?: string | null; disabled?: boolean; }) => { - if (Platform.isTV) return; - const api = useAtomValue(apiAtom); const [, setPrimaryColor] = useAtom(itemThemeColorAtom); + const isTv = Platform.isTV; + const source = useMemo(() => { if (!api) return; if (url) return { uri: url }; @@ -47,16 +48,15 @@ export const useImageColors = ({ width: 300, }); return null; - }, [api, item]); + }, [api, item, url]); useEffect(() => { + if (isTv) return; if (disabled) return; if (source?.uri) { - // Check if colors are already cached in storage const _primary = storage.getString(`${source.uri}-primary`); const _text = storage.getString(`${source.uri}-text`); - // If colors are cached, use them and exit if (_primary && _text) { setPrimaryColor({ primary: _primary, @@ -65,7 +65,6 @@ export const useImageColors = ({ return; } - // Extract colors from the image Colors.getColors(source.uri, { fallback: "#fff", cache: false, @@ -82,7 +81,6 @@ export const useImageColors = ({ let text = "#000"; let backup = "#fff"; - // Select the appropriate color based on the platform if (colors.platform === "android") { primary = colors.dominant; backup = colors.vibrant; @@ -91,13 +89,11 @@ export const useImageColors = ({ backup = colors.primary; } - // Adjust the primary color if it's too close to black if (primary && isCloseToBlack(primary)) { if (backup && !isCloseToBlack(backup)) primary = backup; primary = adjustToNearBlack(primary); } - // Calculate the text color based on the primary color if (primary) text = calculateTextColor(primary); setPrimaryColor({ @@ -105,7 +101,6 @@ export const useImageColors = ({ text, }); - // Cache the colors in storage if (source.uri && primary) { storage.set(`${source.uri}-primary`, primary); storage.set(`${source.uri}-text`, text); @@ -116,5 +111,7 @@ export const useImageColors = ({ console.error("Error getting colors", error); }); } - }, [source?.uri, setPrimaryColor, disabled]); + }, [isTv, source?.uri, setPrimaryColor, disabled]); + + if (isTv) return; }; diff --git a/hooks/useImageStorage.ts b/hooks/useImageStorage.ts index 1c0bc362..ec66c505 100644 --- a/hooks/useImageStorage.ts +++ b/hooks/useImageStorage.ts @@ -1,5 +1,5 @@ -import { storage } from "@/utils/mmkv"; import { useCallback } from "react"; +import { storage } from "@/utils/mmkv"; const useImageStorage = () => { const saveBase64Image = useCallback(async (base64: string, key: string) => { diff --git a/hooks/useIntroSkipper.ts b/hooks/useIntroSkipper.ts index ab38148c..0ddc04d2 100644 --- a/hooks/useIntroSkipper.ts +++ b/hooks/useIntroSkipper.ts @@ -1,10 +1,10 @@ +import { useQuery } from "@tanstack/react-query"; +import { useAtom } from "jotai"; +import { useCallback, useEffect, useState } from "react"; import { apiAtom } from "@/providers/JellyfinProvider"; import { getAuthHeaders } from "@/utils/jellyfin/jellyfin"; import { writeToLog } from "@/utils/log"; import { msToSeconds, secondsToMs } from "@/utils/time"; -import { useQuery } from "@tanstack/react-query"; -import { useAtom } from "jotai"; -import { useCallback, useEffect, useState } from "react"; import { useHaptic } from "./useHaptic"; interface IntroTimestamps { diff --git a/hooks/useJellyseerr.ts b/hooks/useJellyseerr.ts index 1029b2ed..c5c681cc 100644 --- a/hooks/useJellyseerr.ts +++ b/hooks/useJellyseerr.ts @@ -1,3 +1,7 @@ +import axios, { type AxiosError, type AxiosInstance } from "axios"; +import { atom } from "jotai"; +import { useAtom } from "jotai/index"; +import { inRange } from "lodash"; import type { User as JellyseerrUser } from "@/utils/jellyseerr/server/entity/User"; import type { MovieResult, @@ -5,11 +9,11 @@ import type { TvResult, } from "@/utils/jellyseerr/server/models/Search"; import { storage } from "@/utils/mmkv"; -import axios, { type AxiosError, type AxiosInstance } from "axios"; -import { atom } from "jotai"; -import { useAtom } from "jotai/index"; -import { inRange } from "lodash"; import "@/augmentations"; +import { useQueryClient } from "@tanstack/react-query"; +import { t } from "i18next"; +import { useCallback, useMemo } from "react"; +import { toast } from "sonner-native"; import { useSettings } from "@/utils/atoms/settings"; import type { RTRating } from "@/utils/jellyseerr/server/api/rating/rottentomatoes"; import { @@ -43,10 +47,6 @@ import type { TvDetails, } from "@/utils/jellyseerr/server/models/Tv"; import { writeErrorLog } from "@/utils/log"; -import { useQueryClient } from "@tanstack/react-query"; -import { t } from "i18next"; -import { useCallback, useMemo } from "react"; -import { toast } from "sonner-native"; interface SearchParams { query: string; diff --git a/hooks/useMarkAsPlayed.ts b/hooks/useMarkAsPlayed.ts index 34d1d545..fde163a1 100644 --- a/hooks/useMarkAsPlayed.ts +++ b/hooks/useMarkAsPlayed.ts @@ -1,9 +1,9 @@ -import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; -import { markAsNotPlayed } from "@/utils/jellyfin/playstate/markAsNotPlayed"; -import { markAsPlayed } from "@/utils/jellyfin/playstate/markAsPlayed"; import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; import { useQueryClient } from "@tanstack/react-query"; import { useAtom } from "jotai"; +import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; +import { markAsNotPlayed } from "@/utils/jellyfin/playstate/markAsNotPlayed"; +import { markAsPlayed } from "@/utils/jellyfin/playstate/markAsPlayed"; import { useHaptic } from "./useHaptic"; export const useMarkAsPlayed = (items: BaseItemDto[]) => { diff --git a/hooks/useOrientation.ts b/hooks/useOrientation.ts index a485ec22..80a01ffb 100644 --- a/hooks/useOrientation.ts +++ b/hooks/useOrientation.ts @@ -1,7 +1,7 @@ -import * as ScreenOrientation from "@/packages/expo-screen-orientation"; -import orientationToOrientationLock from "@/utils/OrientationLockConverter"; import { useEffect, useState } from "react"; import { Platform } from "react-native"; +import * as ScreenOrientation from "@/packages/expo-screen-orientation"; +import orientationToOrientationLock from "@/utils/OrientationLockConverter"; export const useOrientation = () => { const [orientation, setOrientation] = useState( @@ -10,9 +10,9 @@ export const useOrientation = () => { : ScreenOrientation.OrientationLock.UNKNOWN, ); - if (Platform.isTV) return { orientation, setOrientation }; - useEffect(() => { + if (Platform.isTV) return; + const orientationSubscription = ScreenOrientation.addOrientationChangeListener((event) => { setOrientation( diff --git a/hooks/useSessions.ts b/hooks/useSessions.ts index 03996c88..5aba6515 100644 --- a/hooks/useSessions.ts +++ b/hooks/useSessions.ts @@ -1,10 +1,10 @@ -import { apiAtom } from "@/providers/JellyfinProvider"; -import { userAtom } from "@/providers/JellyfinProvider"; import { getSessionApi } from "@jellyfin/sdk/lib/utils/api/session-api"; import { useQuery } from "@tanstack/react-query"; import { useAtom } from "jotai"; import { Platform } from "react-native"; -const Notifications = !Platform.isTV ? require("expo-notifications") : null; +import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; + +const _Notifications = !Platform.isTV ? require("expo-notifications") : null; export interface useSessionsProps { refetchInterval: number; diff --git a/hooks/useTrickplay.ts b/hooks/useTrickplay.ts index e221b49e..09088019 100644 --- a/hooks/useTrickplay.ts +++ b/hooks/useTrickplay.ts @@ -1,9 +1,9 @@ -import { apiAtom } from "@/providers/JellyfinProvider"; -import { ticksToMs } from "@/utils/time"; import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client"; import { Image } from "expo-image"; import { useAtom } from "jotai"; import { useCallback, useMemo, useRef, useState } from "react"; +import { apiAtom } from "@/providers/JellyfinProvider"; +import { ticksToMs } from "@/utils/time"; interface TrickplayData { Interval?: number; diff --git a/hooks/useWebsockets.ts b/hooks/useWebsockets.ts index 4d7caa28..32b110a4 100644 --- a/hooks/useWebsockets.ts +++ b/hooks/useWebsockets.ts @@ -1,8 +1,8 @@ -import { useWebSocketContext } from "@/providers/WebSocketProvider"; import { useRouter } from "expo-router"; import { useEffect } from "react"; import { useTranslation } from "react-i18next"; import { Alert } from "react-native"; +import { useWebSocketContext } from "@/providers/WebSocketProvider"; interface UseWebSocketProps { isPlaying: boolean; @@ -88,7 +88,7 @@ export const useWebSocket = ({ if (!lastMessage) return; if (offline) return; - const messageType = lastMessage.MessageType; + const _messageType = lastMessage.MessageType; const command: string | undefined = lastMessage?.Data?.Command || lastMessage?.Data?.Name; @@ -252,7 +252,7 @@ export const useWebSocket = ({ }); if (itemIdsStr) { const itemIds = itemIdsStr.split(","); - let startPositionTicks: number | undefined = undefined; + let startPositionTicks: number | undefined; if (startPositionTicksStr) { const parsedTicks = Number.parseInt(startPositionTicksStr, 10); if (!Number.isNaN(parsedTicks)) { diff --git a/i18n.ts b/i18n.ts index 7f8d002b..1825069d 100644 --- a/i18n.ts +++ b/i18n.ts @@ -6,6 +6,7 @@ import de from "./translations/de.json"; import en from "./translations/en.json"; import eo from "./translations/eo.json"; import es from "./translations/es.json"; +import fi from "./translations/fi.json"; import fr from "./translations/fr.json"; import it from "./translations/it.json"; import ja from "./translations/ja.json"; @@ -14,7 +15,6 @@ import nl from "./translations/nl.json"; import nn from "./translations/nn.json"; import pl from "./translations/pl.json"; import ptBR from "./translations/pt-BR.json"; -import fi from "./translations/fi.json"; import ro from "./translations/ro.json"; import ru from "./translations/ru.json"; import sq from "./translations/sq.json"; diff --git a/modules/VlcPlayerView.tsx b/modules/VlcPlayerView.tsx index 70775876..aedceb63 100644 --- a/modules/VlcPlayerView.tsx +++ b/modules/VlcPlayerView.tsx @@ -1,8 +1,7 @@ import { requireNativeViewManager } from "expo-modules-core"; import * as React from "react"; - -import { VideoPlayer, useSettings } from "@/utils/atoms/settings"; import { Platform, ViewStyle } from "react-native"; +import { useSettings, VideoPlayer } from "@/utils/atoms/settings"; import type { VlcPlayerSource, VlcPlayerViewProps, diff --git a/modules/vlc-player/android/build.gradle b/modules/vlc-player/android/build.gradle index b2695833..b372dded 100644 --- a/modules/vlc-player/android/build.gradle +++ b/modules/vlc-player/android/build.gradle @@ -13,31 +13,24 @@ def kotlinVersion = findProperty('android.kotlinVersion') ?: '1.9.25' apply from: expoModulesCorePlugin applyKotlinExpoModulesCorePlugin() +useDefaultAndroidSdkVersions() useCoreDependencies() useExpoPublishing() -// If you want to use the managed Android SDK versions from expo-modules-core, set this to true. -// The Android SDK versions will be bumped from time to time in SDK releases and may introduce breaking changes in your module code. -// Most of the time, you may like to manage the Android SDK versions yourself. -def useManagedAndroidSdkVersions = false -if (useManagedAndroidSdkVersions) { - useDefaultAndroidSdkVersions() -} else { - buildscript { - repositories { - google() - mavenCentral() - } - dependencies { - classpath "com.android.tools.build:gradle:8.11.0" - } +android { + namespace "expo.modules.vlcplayer" + + compileOptions { + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 } - project.android { - compileSdkVersion safeExtGet("compileSdkVersion", 34) - defaultConfig { - minSdkVersion safeExtGet("minSdkVersion", 21) - targetSdkVersion safeExtGet("targetSdkVersion", 34) - } + + kotlinOptions { + jvmTarget = "17" + } + + lintOptions { + abortOnError false } } @@ -46,27 +39,6 @@ dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion" } -android { - namespace "expo.modules.vlcplayer" - compileSdkVersion 34 - defaultConfig { - minSdkVersion 21 - targetSdkVersion 34 - versionCode 1 - versionName "0.6.0" - } - compileOptions { - sourceCompatibility JavaVersion.VERSION_17 - targetCompatibility JavaVersion.VERSION_17 - } - kotlinOptions { - jvmTarget = "17" - } - lintOptions { - abortOnError false - } -} - tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach { kotlinOptions { freeCompilerArgs += ["-Xshow-kotlin-compiler-errors"] diff --git a/package.json b/package.json index 3d5c02c9..bb4fb062 100644 --- a/package.json +++ b/package.json @@ -14,11 +14,11 @@ "prebuild:tv": "cross-env EXPO_TV=1 bun run clean", "build:android:local": "cd android && cross-env NODE_ENV=production ./gradlew assembleRelease", "prepare": "husky", - "check": "biome check .", - "lint": "biome check --write --unsafe" + "check": "biome check . --max-diagnostics 1000", + "lint": "biome check --write --unsafe --max-diagnostics 1000", + "format": "biome format --write ." }, "dependencies": { - "@bottom-tabs/react-navigation": "0.8.6", "@expo/config-plugins": "~9.0.15", "@expo/react-native-action-sheet": "^4.1.1", "@expo/vector-icons": "^14.0.4", @@ -28,43 +28,44 @@ "@kesha-antonov/react-native-background-downloader": "3.2.6", "@react-native-community/netinfo": "11.4.1", "@react-native-menu/menu": "^1.2.2", - "@react-navigation/bottom-tabs": "^7.2.0", + "@react-navigation/bottom-tabs": "^7.4.2", "@react-navigation/material-top-tabs": "^7.1.0", "@react-navigation/native": "^7.0.14", "@shopify/flash-list": "1.7.3", "@tanstack/react-query": "^5.66.0", "add": "^2.0.6", "axios": "^1.7.9", - "expo": "~52.0.47", - "expo-asset": "~11.0.5", - "expo-background-fetch": "~13.0.6", + "expo": "~52.0.31", + "expo-asset": "~11.0.3", + "expo-background-fetch": "~13.0.5", "expo-blur": "~14.0.3", "expo-brightness": "~13.0.3", - "expo-build-properties": "~0.13.3", - "expo-constants": "~17.0.8", + "expo-build-properties": "~0.13.2", + "expo-constants": "~17.0.5", "expo-crypto": "~14.0.2", - "expo-dev-client": "~5.0.20", - "expo-device": "~7.0.3", + "expo-dev-client": "~5.0.11", + "expo-device": "~7.0.2", "expo-font": "~13.0.3", "expo-haptics": "~14.0.1", - "expo-image": "~2.0.7", + "expo-image": "~2.0.4", "expo-keep-awake": "~14.0.2", "expo-linear-gradient": "~14.0.2", "expo-linking": "~7.0.5", "expo-localization": "~16.0.1", "expo-network": "~7.0.5", - "expo-notifications": "~0.29.14", - "expo-router": "~4.0.21", + "expo-notifications": "~0.29.13", + "expo-router": "~4.0.17", "expo-screen-orientation": "~8.0.4", "expo-sensors": "~14.0.2", "expo-sharing": "~13.0.1", - "expo-splash-screen": "~0.29.24", + "expo-splash-screen": "~0.29.22", "expo-status-bar": "~2.0.1", - "expo-system-ui": "~4.0.9", - "expo-task-manager": "~12.0.6", - "expo-updates": "~0.27.4", + "expo-system-ui": "~4.0.8", + "expo-task-manager": "~12.0.5", + "expo-updates": "~0.26.17", "expo-web-browser": "~14.0.2", "i18next": "^25.0.0", + "install": "^0.13.0", "jotai": "^2.11.3", "lodash": "^4.17.21", "nativewind": "^2.0.11", @@ -73,14 +74,13 @@ "react-i18next": "^15.4.0", "react-native": "npm:react-native-tvos@~0.77.2-0", "react-native-awesome-slider": "^2.9.0", - "react-native-bottom-tabs": "0.8.6", "react-native-circular-progress": "^1.4.1", "react-native-collapsible": "^1.6.2", "react-native-compressor": "^1.10.3", "react-native-country-flag": "^2.0.2", "react-native-device-info": "^14.0.4", "react-native-edge-to-edge": "^1.4.3", - "react-native-gesture-handler": "~2.20.2", + "react-native-gesture-handler": "2.22.0", "react-native-get-random-values": "^1.11.0", "react-native-google-cast": "^4.8.3", "react-native-image-colors": "^2.4.0", @@ -91,9 +91,9 @@ "react-native-progress": "^5.0.1", "react-native-reanimated": "~3.16.7", "react-native-reanimated-carousel": "3.5.1", - "react-native-safe-area-context": "4.12.0", - "react-native-screens": "~4.4.0", - "react-native-svg": "15.8.0", + "react-native-safe-area-context": "5.5.0", + "react-native-screens": "~4.5.0", + "react-native-svg": "15.11.1", "react-native-tab-view": "^4.0.5", "react-native-udp": "^4.1.7", "react-native-uitextview": "^1.4.0", @@ -102,7 +102,7 @@ "react-native-video": "6.10.0", "react-native-volume-manager": "^2.0.8", "react-native-web": "~0.19.13", - "react-native-webview": "13.12.5", + "react-native-webview": "13.13.2", "sonner-native": "^0.17.0", "tailwindcss": "3.3.2", "use-debounce": "^10.0.4", @@ -112,7 +112,7 @@ }, "devDependencies": { "@babel/core": "^7.26.8", - "@biomejs/biome": "^2.0.0", + "@biomejs/biome": "^2.1.2", "@react-native-community/cli": "18.0.0", "@react-native-tvos/config-tv": "^0.1.1", "@types/jest": "^30.0.0", @@ -143,5 +143,9 @@ "*.json": [ "biome format --write" ] - } + }, + "trustedDependencies": [ + "postinstall-postinstall", + "unrs-resolver" + ] } diff --git a/providers/DownloadProvider.tsx b/providers/DownloadProvider.tsx index c1ea7b6f..a04083ac 100644 --- a/providers/DownloadProvider.tsx +++ b/providers/DownloadProvider.tsx @@ -1,31 +1,13 @@ -import { useHaptic } from "@/hooks/useHaptic"; -import useImageStorage from "@/hooks/useImageStorage"; -import { useInterval } from "@/hooks/useInterval"; -import { DownloadMethod, useSettings } from "@/utils/atoms/settings"; -import { getOrSetDeviceId } from "@/utils/device"; -import useDownloadHelper from "@/utils/download"; -import { getItemImage } from "@/utils/getItemImage"; -import { useLog, writeToLog } from "@/utils/log"; -import { storage } from "@/utils/mmkv"; -import { - type JobStatus, - cancelAllJobs, - cancelJobById, - deleteDownloadItemInfoFromDiskTmp, - getAllJobsByDeviceId, - getDownloadItemInfoFromDiskTmp, -} from "@/utils/optimize-server"; import type { BaseItemDto, MediaSourceInfo, } from "@jellyfin/sdk/lib/generated-client/models"; -import { getSessionApi } from "@jellyfin/sdk/lib/utils/api/session-api"; import BackGroundDownloader from "@kesha-antonov/react-native-background-downloader"; import { focusManager, useQuery, useQueryClient } from "@tanstack/react-query"; import axios from "axios"; import * as Application from "expo-application"; -import * as FileSystem from "expo-file-system"; import type { FileInfo } from "expo-file-system"; +import * as FileSystem from "expo-file-system"; import Notifications from "expo-notifications"; import { useRouter } from "expo-router"; import { atom, useAtom } from "jotai"; @@ -40,6 +22,23 @@ import { import { useTranslation } from "react-i18next"; import { AppState, type AppStateStatus, Platform } from "react-native"; import { toast } from "sonner-native"; +import { useHaptic } from "@/hooks/useHaptic"; +import useImageStorage from "@/hooks/useImageStorage"; +import { useInterval } from "@/hooks/useInterval"; +import { DownloadMethod, useSettings } from "@/utils/atoms/settings"; +import { getOrSetDeviceId } from "@/utils/device"; +import useDownloadHelper from "@/utils/download"; +import { getItemImage } from "@/utils/getItemImage"; +import { useLog, writeToLog } from "@/utils/log"; +import { storage } from "@/utils/mmkv"; +import { + cancelAllJobs, + cancelJobById, + deleteDownloadItemInfoFromDiskTmp, + getAllJobsByDeviceId, + getDownloadItemInfoFromDiskTmp, + type JobStatus, +} from "@/utils/optimize-server"; import { Bitrate } from "../components/BitrateSelector"; import { apiAtom } from "./JellyfinProvider"; @@ -842,37 +841,35 @@ export function DownloadProvider({ children }: { children: React.ReactNode }) { } export function useDownload() { + const context = useContext(DownloadContext); + if (Platform.isTV) { // Since tv doesn't do downloads, just return no-op functions for everything return { processes: [], - startBackgroundDownload: useCallback( - async ( - _url: string, - _item: BaseItemDto, - _mediaSource: MediaSourceInfo, - _maxBitrate?: Bitrate, - ) => {}, - [], - ), + startBackgroundDownload: async ( + _url: string, + _item: BaseItemDto, + _mediaSource: MediaSourceInfo, + _maxBitrate?: Bitrate, + ) => {}, downloadedFiles: [], deleteAllFiles: async (): Promise => {}, - deleteFile: async (id: string): Promise => {}, - deleteItems: async (items: BaseItemDto[]) => {}, - saveDownloadedItemInfo: (item: BaseItemDto, size?: number) => {}, - removeProcess: (id: string) => {}, + deleteFile: async (_id: string): Promise => {}, + deleteItems: async (_items: BaseItemDto[]) => {}, + saveDownloadedItemInfo: (_item: BaseItemDto, _size?: number) => {}, + removeProcess: (_id: string) => {}, setProcesses: () => {}, startDownload: async (_process: JobStatus): Promise => {}, - getDownloadedItem: (itemId: string) => {}, + getDownloadedItem: (_itemId: string) => {}, deleteFileByType: async (_type: BaseItemDto["Type"]) => {}, appSizeUsage: async () => 0, - getDownloadedItemSize: (itemId: string) => {}, + getDownloadedItemSize: (_itemId: string) => {}, APP_CACHE_DOWNLOAD_DIRECTORY: "", cleanCacheDirectory: async (): Promise => {}, }; } - const context = useContext(DownloadContext); if (context === null) { throw new Error("useDownload must be used within a DownloadProvider"); } diff --git a/providers/JobQueueProvider.tsx b/providers/JobQueueProvider.tsx index 232a5f02..87df1800 100644 --- a/providers/JobQueueProvider.tsx +++ b/providers/JobQueueProvider.tsx @@ -1,6 +1,6 @@ -import { useJobProcessor } from "@/utils/atoms/queue"; import type React from "react"; import { createContext } from "react"; +import { useJobProcessor } from "@/utils/atoms/queue"; const JobQueueContext = createContext(null); diff --git a/providers/PlaySettingsProvider.tsx b/providers/PlaySettingsProvider.tsx index cf61248d..df876d3e 100644 --- a/providers/PlaySettingsProvider.tsx +++ b/providers/PlaySettingsProvider.tsx @@ -1,21 +1,14 @@ -import type { Bitrate } from "@/components/BitrateSelector"; -import { settingsAtom } from "@/utils/atoms/settings"; -import { getStreamUrl } from "@/utils/jellyfin/media/getStreamUrl"; -import generateDeviceProfile from "@/utils/profiles/native"; import type { BaseItemDto, MediaSourceInfo, } from "@jellyfin/sdk/lib/generated-client"; -import { getSessionApi } from "@jellyfin/sdk/lib/utils/api"; import { useAtomValue } from "jotai"; import type React from "react"; -import { - createContext, - useCallback, - useContext, - useEffect, - useState, -} from "react"; +import { createContext, useCallback, useContext, useState } from "react"; +import type { Bitrate } from "@/components/BitrateSelector"; +import { settingsAtom } from "@/utils/atoms/settings"; +import { getStreamUrl } from "@/utils/jellyfin/media/getStreamUrl"; +import generateDeviceProfile from "@/utils/profiles/native"; import { apiAtom, userAtom } from "./JellyfinProvider"; export type PlaybackType = { diff --git a/providers/WebSocketProvider.tsx b/providers/WebSocketProvider.tsx index 05e74f0d..d5ffcc63 100644 --- a/providers/WebSocketProvider.tsx +++ b/providers/WebSocketProvider.tsx @@ -1,17 +1,17 @@ -import { apiAtom, getOrSetDeviceId } from "@/providers/JellyfinProvider"; import { getSessionApi } from "@jellyfin/sdk/lib/utils/api"; import { useRouter } from "expo-router"; import { useAtomValue } from "jotai"; -import React, { +import { createContext, + type ReactNode, + useCallback, useContext, useEffect, - useState, - type ReactNode, useMemo, - useCallback, + useState, } from "react"; import { AppState, type AppStateStatus } from "react-native"; +import { apiAtom, getOrSetDeviceId } from "@/providers/JellyfinProvider"; interface WebSocketMessage { MessageType: string; diff --git a/scripts/symlink-native-dirs.js b/scripts/symlink-native-dirs.js index d82698d5..dd014c99 100644 --- a/scripts/symlink-native-dirs.js +++ b/scripts/symlink-native-dirs.js @@ -1,6 +1,6 @@ #!/usr/bin/env node -const fs = require("node:fs"); +const _fs = require("node:fs"); const path = require("node:path"); const process = require("node:process"); const { execSync } = require("node:child_process"); diff --git a/utils/_jellyseerr/useJellyseerrCanRequest.ts b/utils/_jellyseerr/useJellyseerrCanRequest.ts index ed4cf39c..f1306c44 100644 --- a/utils/_jellyseerr/useJellyseerrCanRequest.ts +++ b/utils/_jellyseerr/useJellyseerrCanRequest.ts @@ -1,17 +1,17 @@ +import { useMemo } from "react"; import { useJellyseerr } from "@/hooks/useJellyseerr"; import { MediaRequestStatus, MediaStatus, } from "@/utils/jellyseerr/server/constants/media"; import { - Permission, hasPermission, + Permission, } from "@/utils/jellyseerr/server/lib/permissions"; import type { MovieResult, TvResult, } from "@/utils/jellyseerr/server/models/Search"; -import { useMemo } from "react"; import type MediaRequest from "../jellyseerr/server/entity/MediaRequest"; import type { MovieDetails } from "../jellyseerr/server/models/Movie"; import type { TvDetails } from "../jellyseerr/server/models/Tv"; diff --git a/utils/atoms/orientation.ts b/utils/atoms/orientation.ts index 42f21b3a..ab4fbafd 100644 --- a/utils/atoms/orientation.ts +++ b/utils/atoms/orientation.ts @@ -1,5 +1,5 @@ -import * as ScreenOrientation from "@/packages/expo-screen-orientation"; import { atom } from "jotai"; +import * as ScreenOrientation from "@/packages/expo-screen-orientation"; export const orientationAtom = atom( ScreenOrientation.OrientationLock.PORTRAIT_UP, diff --git a/utils/atoms/primaryColor.ts b/utils/atoms/primaryColor.ts index 2200c798..31cc7636 100644 --- a/utils/atoms/primaryColor.ts +++ b/utils/atoms/primaryColor.ts @@ -16,7 +16,7 @@ export const calculateTextColor = (backgroundColor: string): string => { const brightness = (r * 299 + g * 587 + b * 114) / 1000; // Calculate contrast ratio with white and black - const contrastWithWhite = calculateContrastRatio([255, 255, 255], [r, g, b]); + const _contrastWithWhite = calculateContrastRatio([255, 255, 255], [r, g, b]); const contrastWithBlack = calculateContrastRatio([0, 0, 0], [r, g, b]); // Use black text if the background is bright and has good contrast with black @@ -55,7 +55,7 @@ export const isCloseToBlack = (color: string): boolean => { return r < 20 && g < 20 && b < 20; }; -export const adjustToNearBlack = (color: string): string => { +export const adjustToNearBlack = (_color: string): string => { return "#313131"; // A very dark gray, almost black }; diff --git a/utils/atoms/queue.ts b/utils/atoms/queue.ts index 573d964f..2b510728 100644 --- a/utils/atoms/queue.ts +++ b/utils/atoms/queue.ts @@ -1,9 +1,9 @@ -import { processesAtom } from "@/providers/DownloadProvider"; -import { useSettings } from "@/utils/atoms/settings"; -import type { JobStatus } from "@/utils/optimize-server"; import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; import { atom, useAtom } from "jotai"; import { useEffect } from "react"; +import { processesAtom } from "@/providers/DownloadProvider"; +import { useSettings } from "@/utils/atoms/settings"; +import type { JobStatus } from "@/utils/optimize-server"; export interface Job { id: string; diff --git a/utils/atoms/settings.ts b/utils/atoms/settings.ts index d21d3839..255c1899 100644 --- a/utils/atoms/settings.ts +++ b/utils/atoms/settings.ts @@ -1,8 +1,3 @@ -import { BITRATES, type Bitrate } from "@/components/BitrateSelector"; -import * as ScreenOrientation from "@/packages/expo-screen-orientation"; -import { apiAtom } from "@/providers/JellyfinProvider"; -import { Video } from "@/utils/jellyseerr/server/models/Movie"; -import { writeInfoLog } from "@/utils/log"; import { type BaseItemKind, type CultureDto, @@ -14,9 +9,13 @@ import { import { atom, useAtom, useAtomValue } from "jotai"; import { useCallback, useEffect, useMemo } from "react"; import { Platform } from "react-native"; +import { BITRATES, type Bitrate } from "@/components/BitrateSelector"; +import * as ScreenOrientation from "@/packages/expo-screen-orientation"; +import { apiAtom } from "@/providers/JellyfinProvider"; +import { writeInfoLog } from "@/utils/log"; import { storage } from "../mmkv"; -const STREAMYFIN_PLUGIN_ID = "1e9e5d386e6746158719e98a5c34f004"; +const _STREAMYFIN_PLUGIN_ID = "1e9e5d386e6746158719e98a5c34f004"; const STREAMYFIN_PLUGIN_SETTINGS = "STREAMYFIN_PLUGIN_SETTINGS"; export type DownloadQuality = "original" | "high" | "low"; @@ -288,7 +287,7 @@ export const useSettings = () => { writeInfoLog("Got plugin settings", data?.settings); return data?.settings; }, - (err) => undefined, + (_err) => undefined, ); setPluginSettings(settings); return settings; diff --git a/utils/background-tasks.ts b/utils/background-tasks.ts index eb01c2c0..29fb8671 100644 --- a/utils/background-tasks.ts +++ b/utils/background-tasks.ts @@ -1,4 +1,5 @@ import { Platform } from "react-native"; + const BackgroundFetch = !Platform.isTV ? require("expo-background-fetch") : null; diff --git a/utils/download.ts b/utils/download.ts index 547aa15a..8be00f50 100644 --- a/utils/download.ts +++ b/utils/download.ts @@ -1,9 +1,9 @@ +import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client"; +import { useAtom } from "jotai"; import useImageStorage from "@/hooks/useImageStorage"; import { apiAtom } from "@/providers/JellyfinProvider"; import { getPrimaryImageUrlById } from "@/utils/jellyfin/image/getPrimaryImageUrlById"; import { storage } from "@/utils/mmkv"; -import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client"; -import { useAtom } from "jotai"; const useDownloadHelper = () => { const [api] = useAtom(apiAtom); diff --git a/utils/jellyfin/getDefaultPlaySettings.ts b/utils/jellyfin/getDefaultPlaySettings.ts index 2c32b615..88e97059 100644 --- a/utils/jellyfin/getDefaultPlaySettings.ts +++ b/utils/jellyfin/getDefaultPlaySettings.ts @@ -1,10 +1,11 @@ // utils/getDefaultPlaySettings.ts -import { BITRATES } from "@/components/BitrateSelector"; + import type { BaseItemDto, MediaSourceInfo, } from "@jellyfin/sdk/lib/generated-client"; -import { type Settings, useSettings } from "../atoms/settings"; +import { BITRATES } from "@/components/BitrateSelector"; +import { type Settings } from "../atoms/settings"; import { AudioStreamRanker, StreamRanker, @@ -52,10 +53,10 @@ export function getDefaultPlaySettings( // 2. Get default or preferred audio const defaultAudioIndex = mediaSource?.DefaultAudioStreamIndex; - const preferedAudioIndex = mediaSource?.MediaStreams?.find( + const _preferedAudioIndex = mediaSource?.MediaStreams?.find( (x) => x.Type === "Audio" && x.Language === settings?.defaultAudioLanguage, )?.Index; - const firstAudioIndex = mediaSource?.MediaStreams?.find( + const _firstAudioIndex = mediaSource?.MediaStreams?.find( (x) => x.Type === "Audio", )?.Index; diff --git a/utils/jellyfin/image/getParentBackdropImageUrl.ts b/utils/jellyfin/image/getParentBackdropImageUrl.ts index 024bb045..51be786b 100644 --- a/utils/jellyfin/image/getParentBackdropImageUrl.ts +++ b/utils/jellyfin/image/getParentBackdropImageUrl.ts @@ -1,9 +1,5 @@ import type { Api } from "@jellyfin/sdk"; -import { - type BaseItemDto, - BaseItemPerson, -} from "@jellyfin/sdk/lib/generated-client/models"; -import { isBaseItemDto } from "../jellyfin"; +import { type BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; /** * Retrieves the primary image URL for a given item. diff --git a/utils/jellyfin/image/getPrimaryParentImageUrl.ts b/utils/jellyfin/image/getPrimaryParentImageUrl.ts index 9a51edf4..f7b85ffd 100644 --- a/utils/jellyfin/image/getPrimaryParentImageUrl.ts +++ b/utils/jellyfin/image/getPrimaryParentImageUrl.ts @@ -1,9 +1,5 @@ import type { Api } from "@jellyfin/sdk"; -import { - type BaseItemDto, - BaseItemPerson, -} from "@jellyfin/sdk/lib/generated-client/models"; -import { isBaseItemDto } from "../jellyfin"; +import { type BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; /** * Retrieves the primary image URL for a given item. diff --git a/utils/jellyfin/jellyfin.ts b/utils/jellyfin/jellyfin.ts index 3db4ba8e..b88823f8 100644 --- a/utils/jellyfin/jellyfin.ts +++ b/utils/jellyfin/jellyfin.ts @@ -18,7 +18,7 @@ export const getAuthHeaders = (api: Api): Record => ({ * @returns {string} - The bitrate as a human-readable string. */ export const bitrateToString = (bitrate: number): string => { - const kbps = bitrate / 1000; + const _kbps = bitrate / 1000; const mbps = (bitrate / 1000000).toFixed(2); return `${mbps} Mb/s`; diff --git a/utils/jellyfin/media/getStreamUrl.ts b/utils/jellyfin/media/getStreamUrl.ts index 6960c388..86314fc0 100644 --- a/utils/jellyfin/media/getStreamUrl.ts +++ b/utils/jellyfin/media/getStreamUrl.ts @@ -1,12 +1,10 @@ -import generateDeviceProfile from "@/utils/profiles/native"; import type { Api } from "@jellyfin/sdk"; import type { BaseItemDto, MediaSourceInfo, - PlaybackInfoResponse, } from "@jellyfin/sdk/lib/generated-client/models"; import { getMediaInfoApi } from "@jellyfin/sdk/lib/utils/api"; -import { Alert } from "react-native"; +import generateDeviceProfile from "@/utils/profiles/native"; export const getStreamUrl = async ({ api, diff --git a/utils/jellyfin/playstate/markAsPlayed.ts b/utils/jellyfin/playstate/markAsPlayed.ts index e17638ec..d73bb0cc 100644 --- a/utils/jellyfin/playstate/markAsPlayed.ts +++ b/utils/jellyfin/playstate/markAsPlayed.ts @@ -31,7 +31,7 @@ export const markAsPlayed = async ({ }); return response.status === 200; - } catch (error) { + } catch (_error) { return false; } }; diff --git a/utils/jellyfin/playstate/reportPlaybackProgress.ts b/utils/jellyfin/playstate/reportPlaybackProgress.ts index 76e27c25..290321dd 100644 --- a/utils/jellyfin/playstate/reportPlaybackProgress.ts +++ b/utils/jellyfin/playstate/reportPlaybackProgress.ts @@ -1,15 +1,6 @@ -import { getOrSetDeviceId } from "@/providers/JellyfinProvider"; -import type { Settings } from "@/utils/atoms/settings"; -import old from "@/utils/profiles/old"; import type { Api } from "@jellyfin/sdk"; -import { DeviceProfile } from "@jellyfin/sdk/lib/generated-client"; -import { - getMediaInfoApi, - getPlaystateApi, - getSessionApi, -} from "@jellyfin/sdk/lib/utils/api"; -import { getAuthHeaders } from "../jellyfin"; -import { postCapabilities } from "../session/capabilities"; +import { getPlaystateApi } from "@jellyfin/sdk/lib/utils/api"; +import type { Settings } from "@/utils/atoms/settings"; interface ReportPlaybackProgressParams { api?: Api | null; diff --git a/utils/jellyfin/session/capabilities.ts b/utils/jellyfin/session/capabilities.ts index 50e8bcbd..f46f1dbf 100644 --- a/utils/jellyfin/session/capabilities.ts +++ b/utils/jellyfin/session/capabilities.ts @@ -1,7 +1,7 @@ -import type { Settings } from "@/utils/atoms/settings"; -import generateDeviceProfile from "@/utils/profiles/native"; import type { Api } from "@jellyfin/sdk"; import type { AxiosResponse } from "axios"; +import type { Settings } from "@/utils/atoms/settings"; +import generateDeviceProfile from "@/utils/profiles/native"; import { getAuthHeaders } from "../jellyfin"; interface PostCapabilitiesParams { @@ -50,7 +50,7 @@ export const postCapabilities = async ({ }, ); return d; - } catch (error) { + } catch (_error) { throw new Error("Failed to mark as not played"); } }; diff --git a/utils/jellyfin/tvshows/nextUp.ts b/utils/jellyfin/tvshows/nextUp.ts index dd7396d2..414a47a7 100644 --- a/utils/jellyfin/tvshows/nextUp.ts +++ b/utils/jellyfin/tvshows/nextUp.ts @@ -1,6 +1,5 @@ import type { Api } from "@jellyfin/sdk"; import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; -import { AxiosError } from "axios"; import { getAuthHeaders } from "../jellyfin"; interface NextUpParams { @@ -39,7 +38,7 @@ export const nextUp = async ({ ); return response.data.Items; - } catch (error) { + } catch (_error) { return []; } }; diff --git a/utils/log.tsx b/utils/log.tsx index b5a73f78..88ca475e 100644 --- a/utils/log.tsx +++ b/utils/log.tsx @@ -23,9 +23,9 @@ const logsAtom = atomWithStorage("logs", [], mmkvStorage); const LogContext = createContext | null>( null, ); -const DownloadContext = createContext | null>( - null, -); +const _DownloadContext = createContext | null>(null); function useLogProvider() { const { data: logs } = useQuery({ diff --git a/utils/optimize-server.ts b/utils/optimize-server.ts index 45186ec7..320b0f03 100644 --- a/utils/optimize-server.ts +++ b/utils/optimize-server.ts @@ -1,5 +1,3 @@ -import { itemRouter } from "@/components/common/TouchableItemRouter"; -import { DownloadedItem } from "@/providers/DownloadProvider"; import type { BaseItemDto, MediaSourceInfo, diff --git a/utils/profiles/native.js b/utils/profiles/native.js index ff4c0ea4..74d46fa4 100644 --- a/utils/profiles/native.js +++ b/utils/profiles/native.js @@ -1,5 +1,3 @@ -import { Platform } from "react-native"; -import DeviceInfo from "react-native-device-info"; /** * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this