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/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index bfee8601..5ba823bb 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -43,7 +43,7 @@ body: label: Version description: What version of Streamyfin are you running? options: - - 0.28.1 + - 0.29.0 - 0.28.0 - 0.27.0 - 0.26.1 diff --git a/.github/workflows/linting.yml b/.github/workflows/linting.yml index df021a59..56ac30fc 100644 --- a/.github/workflows/linting.yml +++ b/.github/workflows/linting.yml @@ -22,7 +22,7 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - uses: marocchino/sticky-pull-request-comment@d2ad0de260ae8b0235ce059e63f2949ba9e05943 # v2.9.3 + - uses: marocchino/sticky-pull-request-comment@773744901bac0e8cbb5a0dc842800d45e9b2b405 # v2.9.4 if: always() && (steps.lint_pr_title.outputs.error_message != null) with: header: pr-title-lint-error @@ -36,7 +36,7 @@ jobs: ``` - if: ${{ steps.lint_pr_title.outputs.error_message == null }} - uses: marocchino/sticky-pull-request-comment@d2ad0de260ae8b0235ce059e63f2949ba9e05943 # v2.9.3 + uses: marocchino/sticky-pull-request-comment@773744901bac0e8cbb5a0dc842800d45e9b2b405 # v2.9.4 with: header: pr-title-lint-error delete: true 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/README.md b/README.md index 01f56fb4..7201d511 100644 --- a/README.md +++ b/README.md @@ -107,13 +107,13 @@ Key points of the MPL-2.0: - You must disclose your source code for any modifications to the covered files - Larger works may combine MPL code with code under other licenses - MPL-licensed components must remain under the MPL, but the larger work can be under a different license -- For the full text of the license, please see the LICENSE file in this repository. +- For the full text of the license, please see the LICENSE file in this repository ## 🌐 Connect with Us Join our Discord: [https://discord.gg/aJvAYeycyY](https://discord.gg/aJvAYeycyY) -If you have questions or need support, feel free to reach out: +Need support or have questions: - GitHub Issues: Report bugs or request features here. - Email: [fredrik.burmester@gmail.com](mailto:fredrik.burmester@gmail.com) @@ -139,77 +139,74 @@ Special shoutout to the JF official clients for being an inspiration to ours. Thanks to the following contributors for their significant contributions: +
- + + +
- +
@Alexk2309
- +
@herrrta
- +
@lostb1t
- +
@Simon-Eklundh
- +
@topiga
- +
@simoncaron
- +
@jakequade
- +
@Ryan0204
- +
@retardgerman
- +
@whoopsi-daisy
+
And all other developers who have contributed to Streamyfin, thank you for your contributions. @@ -228,4 +225,4 @@ I'd also like to thank the following people and projects for their contributions Streamyfin does not promote, support, or condone piracy in any form. The app is intended solely for streaming media that you personally own and control. It does not provide or include any media content. Any discussions or support requests related to piracy are strictly prohibited across all our channels. ## 🤝 Sponsorship -VPS hosting generously provided by [Hexabyte](https://hexabyte.se/en/vps/?currency=eur) +VPS hosting generously provided by [Hexabyte](https://hexabyte.se/en/vps/?currency=eur) and [SweHosting](https://swehosting.se/en/#tj%C3%A4nster) diff --git a/app.json b/app.json index 823f085f..4fa7289f 100644 --- a/app.json +++ b/app.json @@ -2,7 +2,7 @@ "expo": { "name": "Streamyfin", "slug": "streamyfin", - "version": "0.28.1", + "version": "0.29.1", "orientation": "default", "icon": "./assets/images/icon.png", "scheme": "streamyfin", @@ -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/eas.json b/eas.json index a51b7f46..79b995a8 100644 --- a/eas.json +++ b/eas.json @@ -1,7 +1,6 @@ { "cli": { - "version": ">= 9.1.0", - "appVersionSource": "local" + "version": ">= 9.1.0" }, "build": { "development": { @@ -47,14 +46,14 @@ }, "production": { "environment": "production", - "channel": "0.28.1", + "channel": "0.29.1", "android": { "image": "latest" } }, "production-apk": { "environment": "production", - "channel": "0.28.1", + "channel": "0.29.1", "android": { "buildType": "apk", "image": "latest" @@ -62,7 +61,7 @@ }, "production-apk-tv": { "environment": "production", - "channel": "0.28.1", + "channel": "0.29.1", "android": { "buildType": "apk", "image": "latest" 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 160185b9..1825069d 100644 --- a/i18n.ts +++ b/i18n.ts @@ -1,17 +1,23 @@ import { getLocales } from "expo-localization"; import i18n from "i18next"; import { initReactI18next } from "react-i18next"; +import da from "./translations/da.json"; 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"; +import nb from "./translations/nb.json"; 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 ro from "./translations/ro.json"; import ru from "./translations/ru.json"; +import sq from "./translations/sq.json"; import sv from "./translations/sv.json"; import tlh from "./translations/tlh.json"; import tr from "./translations/tr.json"; @@ -20,6 +26,7 @@ import zhCN from "./translations/zh-CN.json"; import zhTW from "./translations/zh-TW.json"; export const APP_LANGUAGES = [ + { label: "Dansk", value: "da" }, { label: "Deutsch", value: "de" }, { label: "English", value: "en" }, { label: "Español", value: "es" }, @@ -32,10 +39,14 @@ export const APP_LANGUAGES = [ { label: "Nederlands", value: "nl" }, { label: "Polski", value: "pl" }, { label: "Português (Brasil)", value: "pt-BR" }, + { label: "Română", value: "ro" }, { label: "Svenska", value: "sv" }, + { label: "Norsk Bokmål", value: "nb" }, + { label: "Norsk Nynorsk", value: "nn" }, + { label: "Suomi", value: "fi" }, + { label: "Shqip", value: "sq" }, { label: "Русский", value: "ru" }, { label: "Українська", value: "uk" }, - { label: "Українська", value: "uk" }, { label: "简体中文", value: "zh-CN" }, { label: "繁體中文", value: "zh-TW" }, ]; @@ -43,6 +54,7 @@ export const APP_LANGUAGES = [ i18n.use(initReactI18next).init({ compatibilityJSON: "v4", resources: { + da: { translation: da }, de: { translation: de }, en: { translation: en }, es: { translation: es }, @@ -53,7 +65,12 @@ i18n.use(initReactI18next).init({ nl: { translation: nl }, pl: { translation: pl }, "pt-BR": { translation: ptBR }, + ro: { translation: ro }, sv: { translation: sv }, + nb: { translation: nb }, + nn: { translation: nn }, + fi: { translation: fi }, + sq: { translation: sq }, ru: { translation: ru }, tr: { translation: tr }, tlh: { translation: tlh }, diff --git a/modules/VlcPlayerView.tsx b/modules/VlcPlayerView.tsx index 750b6ba2..6bcbcd69 100644 --- a/modules/VlcPlayerView.tsx +++ b/modules/VlcPlayerView.tsx @@ -1,6 +1,7 @@ import { requireNativeViewManager } from "expo-modules-core"; import * as React from "react"; -import { ViewStyle } from "react-native"; +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/translations/da.json b/translations/da.json new file mode 100644 index 00000000..f7d21d06 --- /dev/null +++ b/translations/da.json @@ -0,0 +1,480 @@ +{ + "login": { + "username_required": "Brugernavn er påkrævet", + "error_title": "Fejl", + "login_title": "Log ind", + "login_to_title": "Log ind på", + "username_placeholder": "Brugernavn", + "password_placeholder": "Adgangskode", + "login_button": "Log ind", + "quick_connect": "Hurtigforbindelse", + "enter_code_to_login": "Indtast koden {{code}} for at logge ind", + "failed_to_initiate_quick_connect": "Kunne ikke starte hurtigforbindelse", + "got_it": "Forstået", + "connection_failed": "Forbindelsen mislykkedes", + "could_not_connect_to_server": "Kunne ikke oprette forbindelse til serveren. Tjek venligst URL'en og din netværksforbindelse.", + "an_unexpected_error_occured": "Der opstod en uventet fejl", + "change_server": "Skift server", + "invalid_username_or_password": "Ugyldigt brugernavn eller adgangskode", + "user_does_not_have_permission_to_log_in": "Brugeren har ikke tilladelse til at logge ind", + "server_is_taking_too_long_to_respond_try_again_later": "Serveren svarer for langsomt, prøv igen senere", + "server_received_too_many_requests_try_again_later": "Serveren modtog for mange forespørgsler, prøv igen senere.", + "there_is_a_server_error": "Der er en serverfejl", + "an_unexpected_error_occured_did_you_enter_the_correct_url": "Der opstod en uventet fejl. Har du indtastet serverens URL korrekt?" + }, + "server": { + "enter_url_to_jellyfin_server": "Indtast URL'en til din Jellyfin server", + "server_url_placeholder": "http(s)://din-server.com", + "connect_button": "Forbind", + "previous_servers": "Tidligere servere", + "clear_button": "Ryd", + "search_for_local_servers": "Søg efter lokale servere", + "searching": "Søger...", + "servers": "Servere" + }, + "home": { + "no_internet": "Ingen internetforbindelse", + "no_items": "Ingen elementer", + "no_internet_message": "Ingen bekymringer, du kan stadig se\ndownloadet indhold.", + "go_to_downloads": "Gå til downloads", + "oops": "Ups!", + "error_message": "Noget gik galt.\nLog venligst ud og ind igen.", + "continue_watching": "Fortsæt med at se", + "next_up": "Næste", + "recently_added_in": "Senest tilføjet i {{libraryName}}", + "suggested_movies": "Foreslåede film", + "suggested_episodes": "Foreslåede episoder", + "intro": { + "welcome_to_streamyfin": "Velkommen til Streamyfin", + "a_free_and_open_source_client_for_jellyfin": "En gratis og open-source klient til Jellyfin.", + "features_title": "Funktioner", + "features_description": "Streamyfin har en masse funktioner og integrerer med en bred vifte af software, som du kan finde i indstillingsmenuen, disse omfatter:", + "jellyseerr_feature_description": "Forbind til din Jellyseerr instans og anmod om film direkte i appen.", + "downloads_feature_title": "Downloads", + "downloads_feature_description": "Download film og tv-serier til offline visning. Brug enten standardmetoden eller installer optimiseret server for at downloade filer i baggrunden.", + "chromecast_feature_description": "Cast film og tv-serier til dine Chromecast enheder.", + "centralised_settings_plugin_title": "Centraliseret indstillingsplugin", + "centralised_settings_plugin_description": "Konfigurer indstillinger fra et centraliseret sted på din Jellyfin server. Alle klientindstillinger for alle brugere synkroniseres automatisk.", + "done_button": "Færdig", + "go_to_settings_button": "Gå til indstillinger", + "read_more": "Læs mere" + }, + "settings": { + "settings_title": "Indstillinger", + "log_out_button": "Log ud", + "user_info": { + "user_info_title": "Brugerinfo", + "user": "Bruger", + "server": "Server", + "token": "Token", + "app_version": "App version" + }, + "quick_connect": { + "quick_connect_title": "Hurtigforbindelse", + "authorize_button": "Autoriser Hurtigforbindelse", + "enter_the_quick_connect_code": "Indtast koden til hurtigforbindelse...", + "success": "Succes", + "quick_connect_autorized": "Hurtigforbindelse autoriseret", + "error": "Fejl", + "invalid_code": "Ugyldig kode", + "authorize": "Autoriser" + }, + "media_controls": { + "media_controls_title": "Mediekontrol", + "forward_skip_length": "Spol frem længde", + "rewind_length": "Spol tilbage længde", + "seconds_unit": "s" + }, + "audio": { + "audio_title": "Lyd", + "set_audio_track": "Sæt lydspor fra forrige element", + "audio_language": "Lydsprog", + "audio_hint": "Vælg et standardsprog for lyd.", + "none": "Ingen", + "language": "Sprog" + }, + "subtitles": { + "subtitle_title": "Undertekster", + "subtitle_language": "Undertekstsprog", + "subtitle_mode": "Underteksttilstand", + "set_subtitle_track": "Sæt undertekstspor fra forrige element", + "subtitle_size": "Undertekststørrelse", + "subtitle_hint": "Konfigurer undertekstpræference.", + "none": "Ingen", + "language": "Sprog", + "loading": "Indlæser", + "modes": { + "Default": "Standard", + "Smart": "Smart", + "Always": "Altid", + "None": "Ingen", + "OnlyForced": "Kun tvungne undertekster" + } + }, + "other": { + "other_title": "Andet", + "follow_device_orientation": "Auto rotér", + "video_orientation": "Videoorientering", + "orientation": "Orientering", + "orientations": { + "DEFAULT": "Standard", + "ALL": "Alle", + "PORTRAIT": "Portræt", + "PORTRAIT_UP": "Portræt op", + "PORTRAIT_DOWN": "Portræt ned", + "LANDSCAPE": "Landskab", + "LANDSCAPE_LEFT": "Landskab venstre", + "LANDSCAPE_RIGHT": "Landskab højre", + "OTHER": "Andet", + "UNKNOWN": "Ukendt" + }, + "safe_area_in_controls": "Sikkert område i kontroller", + "video_player": "Videospiller", + "video_players": { + "VLC_3": "VLC 3", + "VLC_4": "VLC 4 (Eksperimentel + PiP)" + }, + "show_custom_menu_links": "Vis tilpassede menulinks", + "hide_libraries": "Skjul biblioteker", + "select_liraries_you_want_to_hide": "Vælg de biblioteker, du ønsker at skjule fra fanen Bibliotek og startside sektionerne.", + "disable_haptic_feedback": "Deaktiver haptisk feedback", + "default_quality": "Standard kvalitet" + }, + "downloads": { + "downloads_title": "Downloads", + "download_method": "Downloadmetode", + "remux_max_download": "Remux maks download", + "auto_download": "Auto download", + "optimized_versions_server": "Optimeret versionsserver", + "save_button": "Gem", + "optimized_server": "Optimeret server", + "optimized": "Optimeret", + "default": "Standard", + "optimized_version_hint": "Indtast URL'en til den optimerede server. URL'en skal inkludere http eller https og eventuelt porten.", + "read_more_about_optimized_server": "Læs mere om den optimerede server.", + "url": "URL", + "server_url_placeholder": "http(s)://domain.org:port" + }, + "plugins": { + "plugins_title": "Plugins", + "jellyseerr": { + "jellyseerr_warning": "Denne integration er i en tidlig fase. Forvent, at tingene ændres.", + "server_url": "Server URL", + "server_url_hint": "Eksempel: http(s)://din-host.url\n(tilføj port hvis nødvendigt)", + "server_url_placeholder": "Jellyseerr URL...", + "password": "Adgangskode", + "password_placeholder": "Indtast adgangskode for Jellyfin bruger {{username}}", + "save_button": "Gem", + "clear_button": "Ryd", + "login_button": "Log ind", + "total_media_requests": "Samlede medieanmodninger", + "movie_quota_limit": "Begrænsning for filmkvote", + "movie_quota_days": "Filmkvotedage", + "tv_quota_limit": "Begrænsning for TV kvote", + "tv_quota_days": "TV kvotedage", + "reset_jellyseerr_config_button": "Nulstil Jellyseerr konfiguration", + "unlimited": "Ubegrænset", + "plus_n_more": "+{{n}} mere", + "order_by": { + "DEFAULT": "Standard", + "VOTE_COUNT_AND_AVERAGE": "Antal stemmer og gennemsnit", + "POPULARITY": "Popularitet" + } + }, + "marlin_search": { + "enable_marlin_search": "Aktivér Marlin søgning", + "url": "URL", + "server_url_placeholder": "http(s)://domain.org:port", + "marlin_search_hint": "Indtast URL'en til Marlin serveren. URL'en skal inkludere http eller https og eventuelt porten.", + "read_more_about_marlin": "Læs mere om Marlin.", + "save_button": "Gem", + "toasts": { + "saved": "Gemt" + } + } + }, + "storage": { + "storage_title": "Lagring", + "app_usage": "App {{usedSpace}}% brugt", + "device_usage": "Enhedsforbrug: {{availableSpace}}%", + "size_used": "{{used}} af {{total}} brugt", + "delete_all_downloaded_files": "Slet alle downloadede filer" + }, + "intro": { + "show_intro": "Vis intro", + "reset_intro": "Nulstil intro" + }, + "logs": { + "logs_title": "Logfiler", + "export_logs": "Eksporter logfiler", + "click_for_more_info": "Klik for mere info", + "level": "Niveau", + "no_logs_available": "Ingen logfiler tilgængelige", + "delete_all_logs": "Slet alle logfiler" + }, + "languages": { + "title": "Sprog", + "app_language": "App sprog", + "app_language_description": "Vælg sproget for appen.", + "system": "System" + }, + "toasts": { + "error_deleting_files": "Fejl ved sletning af filer", + "background_downloads_enabled": "Baggrundsdownloads aktiveret", + "background_downloads_disabled": "Baggrundsdownloads deaktiveret", + "connected": "Forbundet", + "could_not_connect": "Kunne ikke oprette forbindelse", + "invalid_url": "Ugyldig URL" + } + }, + "sessions": { + "title": "Sessioner", + "no_active_sessions": "Ingen aktive sessioner" + }, + "downloads": { + "downloads_title": "Downloads", + "tvseries": "TV-serier", + "movies": "Film", + "queue": "Kø", + "queue_hint": "Kø og downloads vil gå tabt ved genstart af appen", + "no_items_in_queue": "Ingen elementer i køen", + "no_downloaded_items": "Ingen downloadede elementer", + "delete_all_movies_button": "Slet alle film", + "delete_all_tvseries_button": "Slet alle TV-serier", + "delete_all_button": "Slet alle", + "active_download": "Aktiv download", + "no_active_downloads": "Ingen aktive downloads", + "active_downloads": "Aktive downloads", + "new_app_version_requires_re_download": "Ny app version kræver ny download", + "new_app_version_requires_re_download_description": "Den nye opdatering kræver, at indhold downloades igen. Fjern venligst alt downloadet indhold og prøv igen.", + "back": "Tilbage", + "delete": "Slet", + "something_went_wrong": "Noget gik galt", + "could_not_get_stream_url_from_jellyfin": "Kunne ikke hente stream URL'en fra Jellyfin", + "eta": "ETA {{eta}}", + "methods": "Metoder", + "toasts": { + "you_are_not_allowed_to_download_files": "Du har ikke tilladelse til at downloade filer.", + "deleted_all_movies_successfully": "Alle film er slettet med succes!", + "failed_to_delete_all_movies": "Kunne ikke slette alle film", + "deleted_all_tvseries_successfully": "Alle TV-serier er slettet med succes!", + "failed_to_delete_all_tvseries": "Kunne ikke slette alle TV-serier", + "download_cancelled": "Download afbrudt", + "could_not_cancel_download": "Kunne ikke annullere download", + "download_completed": "Download fuldført", + "download_started_for": "Download startet for {{item}}", + "item_is_ready_to_be_downloaded": "{{item}} er klar til download", + "download_stated_for_item": "Download startet for {{item}}", + "download_failed_for_item": "Download mislykkedes for {{item}} - {{error}}", + "download_completed_for_item": "Download fuldført for {{item}}", + "queued_item_for_optimization": "Sat {{item}} i kø til optimering", + "failed_to_start_download_for_item": "Kunne ikke starte download for {{item}}: {{message}}", + "server_responded_with_status_code": "Serveren svarede med status {{statusCode}}", + "no_response_received_from_server": "Intet svar modtaget fra serveren", + "error_setting_up_the_request": "Fejl ved opsætning af anmodningen", + "failed_to_start_download_for_item_unexpected_error": "Kunne ikke starte download for {{item}}: Uventet fejl", + "all_files_folders_and_jobs_deleted_successfully": "Alle filer, mapper og jobs blev slettet med succes", + "an_error_occured_while_deleting_files_and_jobs": "Der opstod en fejl under sletning af filer og jobs", + "go_to_downloads": "Gå til downloads" + } + } + }, + "search": { + "search_here": "Søg her...", + "search": "Søg...", + "x_items": "{{count}} elementer", + "library": "Bibliotek", + "discover": "Udforsk", + "no_results": "Ingen resultater", + "no_results_found_for": "Ingen resultater fundet for", + "movies": "Film", + "series": "Serier", + "episodes": "Episoder", + "collections": "Samlinger", + "actors": "Skuespillere", + "request_movies": "Anmod om film", + "request_series": "Anmod om serier", + "recently_added": "Senest tilføjet", + "recent_requests": "Seneste anmodninger", + "plex_watchlist": "Plex ønskeliste", + "trending": "Trendende", + "popular_movies": "Populære film", + "movie_genres": "Filmgenrer", + "upcoming_movies": "Kommende film", + "studios": "Studier", + "popular_tv": "Populær TV", + "tv_genres": "TV-genrer", + "upcoming_tv": "Kommende TV", + "networks": "Netværk", + "tmdb_movie_keyword": "TMDB film-nøgleord", + "tmdb_movie_genre": "TMDB filmgenre", + "tmdb_tv_keyword": "TMDB TV-nøgleord", + "tmdb_tv_genre": "TMDB TV-genre", + "tmdb_search": "TMDB-søgning", + "tmdb_studio": "TMDB-studie", + "tmdb_network": "TMDB-netværk", + "tmdb_movie_streaming_services": "TMDB film streamingtjenester", + "tmdb_tv_streaming_services": "TMDB TV-streamingtjenester" + }, + "library": { + "no_items_found": "Ingen elementer fundet", + "no_results": "Ingen resultater", + "no_libraries_found": "Ingen biblioteker fundet", + "item_types": { + "movies": "film", + "series": "serier", + "boxsets": "box sæt", + "items": "elementer" + }, + "options": { + "display": "Visning", + "row": "Række", + "list": "Liste", + "image_style": "Billedstil", + "poster": "Plakat", + "cover": "Omslag", + "show_titles": "Vis titler", + "show_stats": "Vis statistik" + }, + "filters": { + "genres": "Genrer", + "years": "År", + "sort_by": "Sortér efter", + "sort_order": "Sorteringsrækkefølge", + "asc": "Stigende", + "desc": "Faldende", + "tags": "Tags" + } + }, + "favorites": { + "series": "Serier", + "movies": "Film", + "episodes": "Episoder", + "videos": "Videoer", + "boxsets": "Boxsets", + "playlists": "Playlister", + "noDataTitle": "Ingen favoritter endnu", + "noData": "Markér elementer som favoritter for at få dem vist her for hurtig adgang." + }, + "custom_links": { + "no_links": "Ingen links" + }, + "player": { + "error": "Fejl", + "failed_to_get_stream_url": "Kunne ikke hente stream URL'en", + "an_error_occured_while_playing_the_video": "Der opstod en fejl under afspilning af videoen. Tjek logfilerne i indstillinger.", + "client_error": "Klientfejl", + "could_not_create_stream_for_chromecast": "Kunne ikke oprette en stream til Chromecast", + "message_from_server": "Besked fra server: {{message}}", + "video_has_finished_playing": "Videoen er færdig med at spille!", + "no_video_source": "Ingen videokilde...", + "next_episode": "Næste episode", + "refresh_tracks": "Opdater spor", + "subtitle_tracks": "Undertekstspor:", + "audio_tracks": "Lydspor:", + "playback_state": "Afspilningstilstand:", + "no_data_available": "Ingen data tilgængelig", + "index": "Indeks:" + }, + "item_card": { + "next_up": "Næste", + "no_items_to_display": "Ingen elementer at vise", + "cast_and_crew": "Medvirkende & besætning", + "series": "Serier", + "seasons": "Sæsoner", + "season": "Sæson", + "no_episodes_for_this_season": "Ingen episoder for denne sæson", + "overview": "Oversigt", + "more_with": "Mere med {{name}}", + "similar_items": "Lignende elementer", + "no_similar_items_found": "Ingen lignende elementer fundet", + "video": "Video", + "more_details": "Flere detaljer", + "quality": "Kvalitet", + "audio": "Lyd", + "subtitles": "Undertekster", + "show_more": "Vis mere", + "show_less": "Vis mindre", + "appeared_in": "Medvirket i", + "could_not_load_item": "Kunne ikke indlæse elementet", + "none": "Ingen", + "download": { + "download_season": "Download sæson", + "download_series": "Download serie", + "download_episode": "Download episode", + "download_movie": "Download film", + "download_x_item": "Download {{item_count}} elementer", + "download_button": "Download", + "using_optimized_server": "Bruger optimeret server", + "using_default_method": "Bruger standardmetode" + } + }, + "live_tv": { + "next": "Næste", + "previous": "Forrige", + "live_tv": "Live TV", + "coming_soon": "Kommer snart", + "on_now": "Lige nu", + "shows": "Shows", + "movies": "Film", + "sports": "Sport", + "for_kids": "For børn", + "news": "Nyheder" + }, + "jellyseerr": { + "confirm": "Bekræft", + "cancel": "Annuller", + "yes": "Ja", + "whats_wrong": "Hvad er galt?", + "issue_type": "Problemtype", + "select_an_issue": "Vælg et problem", + "types": "Typer", + "describe_the_issue": "(valgfrit) Beskriv problemet...", + "submit_button": "Indsend", + "report_issue_button": "Rapportér problem", + "request_button": "Anmod", + "are_you_sure_you_want_to_request_all_seasons": "Er du sikker på, at du vil anmode om alle sæsoner?", + "failed_to_login": "Kunne ikke logge ind", + "cast": "Medvirkende", + "details": "Detaljer", + "status": "Status", + "original_title": "Original titel", + "series_type": "Serietype", + "release_dates": "Udgivelsesdatoer", + "first_air_date": "Første sendingsdato", + "next_air_date": "Næste sendingsdato", + "revenue": "Omsætning", + "budget": "Budget", + "original_language": "Originalt sprog", + "production_country": "Produktionsland", + "studios": "Studier", + "network": "Netværk", + "currently_streaming_on": "Streames aktuelt på", + "advanced": "Avanceret", + "request_as": "Anmod som", + "tags": "Tags", + "quality_profile": "Kvalitetsprofil", + "root_folder": "Rodmappe", + "season_all": "Sæson (alle)", + "season_number": "Sæson {{season_number}}", + "number_episodes": "{{episode_number}} episoder", + "born": "Født", + "appearances": "Medvirkninger", + "toasts": { + "jellyseer_does_not_meet_requirements": "Jellyseerr serveren opfylder ikke minimumskravene! Opdater venligst til mindst 2.0.0", + "jellyseerr_test_failed": "Jellyseerr test mislykkedes. Prøv venligst igen.", + "failed_to_test_jellyseerr_server_url": "Kunne ikke teste jellyseerr serverens URL", + "issue_submitted": "Problem indsendt!", + "requested_item": "Anmodet om {{item}}!", + "you_dont_have_permission_to_request": "Du har ikke tilladelse til at anmode!", + "something_went_wrong_requesting_media": "Noget gik galt ved anmodning om medie!" + } + }, + "tabs": { + "home": "Hjem", + "search": "Søg", + "library": "Bibliotek", + "custom_links": "Tilpassede links", + "favorites": "Favoritter" + } +} diff --git a/translations/fi.json b/translations/fi.json new file mode 100644 index 00000000..215ecf59 --- /dev/null +++ b/translations/fi.json @@ -0,0 +1,480 @@ +{ + "login": { + "username_required": "Käyttäjätunnus on pakollinen", + "error_title": "Virhe", + "login_title": "Kirjaudu sisään", + "login_to_title": "Kirjaudu sisään palveluun", + "username_placeholder": "Käyttäjätunnus", + "password_placeholder": "Salasana", + "login_button": "Kirjaudu sisään", + "quick_connect": "Pikayhdistys", + "enter_code_to_login": "Syötä koodi {{code}} kirjautuaksesi", + "failed_to_initiate_quick_connect": "Pikayhdistyksen aloittaminen epäonnistui", + "got_it": "Okei", + "connection_failed": "Yhteys epäonnistui", + "could_not_connect_to_server": "Yhteyttä palvelimeen ei voitu muodostaa. Tarkista URL-osoite ja verkkoyhteytesi.", + "an_unexpected_error_occured": "Odottamaton virhe tapahtui", + "change_server": "Vaihda palvelinta", + "invalid_username_or_password": "Virheellinen käyttäjätunnus tai salasana", + "user_does_not_have_permission_to_log_in": "Käyttäjällä ei ole oikeuksia kirjautua sisään", + "server_is_taking_too_long_to_respond_try_again_later": "Palvelin reagoi liian hitaasti, yritä myöhemmin uudelleen", + "server_received_too_many_requests_try_again_later": "Palvelin sai liian monta pyyntöä, yritä myöhemmin uudelleen.", + "there_is_a_server_error": "Palvelimessa on virhe", + "an_unexpected_error_occured_did_you_enter_the_correct_url": "Odottamaton virhe tapahtui. Syötitkö palvelimen URL-osoitteen oikein?" + }, + "server": { + "enter_url_to_jellyfin_server": "Syötä URL-osoite Jellyfin-palvelimellesi", + "server_url_placeholder": "http(s)://oma-palvelin.com", + "connect_button": "Yhdistä", + "previous_servers": "edelliset palvelimet", + "clear_button": "Tyhjennä", + "search_for_local_servers": "Etsi paikallisia palvelimia", + "searching": "Etsitään...", + "servers": "Palvelimet" + }, + "home": { + "no_internet": "Ei internet-yhteyttä", + "no_items": "Ei kohteita", + "no_internet_message": "Ei hätää, voit silti katsella\nladattua sisältöä.", + "go_to_downloads": "Siirry latauksiin", + "oops": "Ups!", + "error_message": "Jotain meni pieleen.\nKirjaudu ulos ja takaisin sisään.", + "continue_watching": "Jatka katsomista", + "next_up": "Seuraavaksi", + "recently_added_in": "Äskettäin lisätty {{libraryName}}-kirjastoon", + "suggested_movies": "Ehdotetut elokuvat", + "suggested_episodes": "Ehdotetut jaksot", + "intro": { + "welcome_to_streamyfin": "Tervetuloa Streamyfiniin", + "a_free_and_open_source_client_for_jellyfin": "Ilmainen ja avoimen lähdekoodin asiakas Jellyfinille.", + "features_title": "Ominaisuudet", + "features_description": "Streamyfin sisältää useita ominaisuuksia ja integroituu laajaan valikoimaan ohjelmistoja, jotka löydät asetukset-valikosta. Näitä ovat:", + "jellyseerr_feature_description": "Yhdistä Jellyseerr-instanssiisi ja pyydä elokuvia suoraan sovelluksessa.", + "downloads_feature_title": "Lataukset", + "downloads_feature_description": "Lataa elokuvia ja TV-sarjoja katsottavaksi offline-tilassa. Käytä joko oletusmenetelmää tai asenna optimoitu palvelin ladataksesi tiedostoja taustalla.", + "chromecast_feature_description": "Lähetä elokuvia ja TV-sarjoja Chromecast-laitteillesi.", + "centralised_settings_plugin_title": "Keskitetty asetusten liitännäinen", + "centralised_settings_plugin_description": "Määritä asetukset keskitetystä sijainnista Jellyfin-palvelimellasi. Kaikki asiakasasetukset kaikille käyttäjille synkronoidaan automaattisesti.", + "done_button": "Valmis", + "go_to_settings_button": "Siirry asetuksiin", + "read_more": "Lue lisää" + }, + "settings": { + "settings_title": "Asetukset", + "log_out_button": "Kirjaudu ulos", + "user_info": { + "user_info_title": "Käyttäjätiedot", + "user": "Käyttäjä", + "server": "Palvelin", + "token": "Token", + "app_version": "Sovelluksen versio" + }, + "quick_connect": { + "quick_connect_title": "Pikayhdistys", + "authorize_button": "Valtuuta Pikayhdistys", + "enter_the_quick_connect_code": "Syötä nopean yhteyden koodi...", + "success": "Onnistui", + "quick_connect_autorized": "Pikayhdistys valtuutettu", + "error": "Virhe", + "invalid_code": "Virheellinen koodi", + "authorize": "Valtuuta" + }, + "media_controls": { + "media_controls_title": "Mediaohjaimet", + "forward_skip_length": "Eteenpäin hyppäämisen pituus", + "rewind_length": "Taaksepäin hyppäämisen pituus", + "seconds_unit": "s" + }, + "audio": { + "audio_title": "Ääni", + "set_audio_track": "Aseta ääniura edellisestä kohteesta", + "audio_language": "Ääni kieli", + "audio_hint": "Valitse oletusäänen kieli.", + "none": "Ei mitään", + "language": "Kieli" + }, + "subtitles": { + "subtitle_title": "Tekstitykset", + "subtitle_language": "Tekstityksen kieli", + "subtitle_mode": "Tekstitysmoodi", + "set_subtitle_track": "Aseta tekstitys edellisestä kohteesta", + "subtitle_size": "Tekstityksen koko", + "subtitle_hint": "Määritä tekstitysasetukset.", + "none": "Ei mitään", + "language": "Kieli", + "loading": "Ladataan", + "modes": { + "Default": "Oletus", + "Smart": "Älykäs", + "Always": "Aina", + "None": "Ei mitään", + "OnlyForced": "Vain pakotettu" + } + }, + "other": { + "other_title": "Muut", + "follow_device_orientation": "Automaattinen kierto", + "video_orientation": "Videon suunta", + "orientation": "Suunta", + "orientations": { + "DEFAULT": "Oletus", + "ALL": "Kaikki", + "PORTRAIT": "Pystysuora", + "PORTRAIT_UP": "Pystysuora ylös", + "PORTRAIT_DOWN": "Pystysuora alas", + "LANDSCAPE": "Vaakasuora", + "LANDSCAPE_LEFT": "Vaakasuora vasemmalle", + "LANDSCAPE_RIGHT": "Vaakasuora oikealle", + "OTHER": "Muu", + "UNKNOWN": "Tuntematon" + }, + "safe_area_in_controls": "Turvallinen alue ohjaimissa", + "video_player": "Videosoitin", + "video_players": { + "VLC_3": "VLC 3", + "VLC_4": "VLC 4 (Kokeellinen + PiP)" + }, + "show_custom_menu_links": "Näytä mukautetut valikkolinkit", + "hide_libraries": "Piilota kirjastot", + "select_liraries_you_want_to_hide": "Valitse kirjastot, jotka haluat piilottaa Kirjasto-välilehdeltä ja etusivun osioista.", + "disable_haptic_feedback": "Poista haptinen palautteet käytöstä", + "default_quality": "Oletuslaatu" + }, + "downloads": { + "downloads_title": "Lataukset", + "download_method": "Latausmenetelmä", + "remux_max_download": "Remuxin maksimi lataus", + "auto_download": "Automaattinen lataus", + "optimized_versions_server": "Optimoitujen versioiden palvelin", + "save_button": "Tallenna", + "optimized_server": "Optimoitu palvelin", + "optimized": "Optimoitu", + "default": "Oletus", + "optimized_version_hint": "Syötä optimoidun palvelimen URL-osoite. URL-osoitteen tulee sisältää http tai https ja valinnaisesti portti.", + "read_more_about_optimized_server": "Lue lisää optimoidusta palvelimesta.", + "url": "URL", + "server_url_placeholder": "http(s)://verkkotunnus.org:portti" + }, + "plugins": { + "plugins_title": "Liitännäiset", + "jellyseerr": { + "jellyseerr_warning": "Tämä integraatio on alkuvaiheessa. Odota muutoksia.", + "server_url": "Palvelimen URL", + "server_url_hint": "Esimerkki: http(s)://verkkotunnus.url\n(lisää portti tarvittaessa)", + "server_url_placeholder": "Jellyseerr URL...", + "password": "Salasana", + "password_placeholder": "Syötä salasana Jellyfin-käyttäjälle {{username}}", + "save_button": "Tallenna", + "clear_button": "Tyhjennä", + "login_button": "Kirjaudu sisään", + "total_media_requests": "Yhteensä mediapyynnöt", + "movie_quota_limit": "Elokuvakiintiön raja", + "movie_quota_days": "Elokuvakiintiön päivät", + "tv_quota_limit": "TV-kiintiön raja", + "tv_quota_days": "TV-kiintiön päivät", + "reset_jellyseerr_config_button": "Nollaa Jellyseerr-asetukset", + "unlimited": "Rajoittamaton", + "plus_n_more": "+{{n}} lisää", + "order_by": { + "DEFAULT": "Oletus", + "VOTE_COUNT_AND_AVERAGE": "Äänimäärä ja keskiarvo", + "POPULARITY": "Suosio" + } + }, + "marlin_search": { + "enable_marlin_search": "Ota Marlin-haku käyttöön", + "url": "URL", + "server_url_placeholder": "http(s)://verkkotunnus.org:portti", + "marlin_search_hint": "Syötä Marlin-palvelimen URL-osoite. URL-osoitteen tulee sisältää http tai https ja valinnaisesti portti.", + "read_more_about_marlin": "Lue lisää Marlinista.", + "save_button": "Tallenna", + "toasts": { + "saved": "Tallennettu" + } + } + }, + "storage": { + "storage_title": "Tallennustila", + "app_usage": "Sovelluksen käyttö {{usedSpace}}%", + "device_usage": "Laitteen käyttö {{availableSpace}}%", + "size_used": "{{used}} / {{total}} käytössä", + "delete_all_downloaded_files": "Poista kaikki ladatut tiedostot" + }, + "intro": { + "show_intro": "Näytä intro", + "reset_intro": "Nollaa intro" + }, + "logs": { + "logs_title": "Lokit", + "export_logs": "Vie lokit", + "click_for_more_info": "Napsauta lisätietoja varten", + "level": "Taso", + "no_logs_available": "Ei lokitietoja saatavilla", + "delete_all_logs": "Poista kaikki lokit" + }, + "languages": { + "title": "Kielet", + "app_language": "Sovelluksen kieli", + "app_language_description": "Valitse sovelluksen kieli.", + "system": "Järjestelmä" + }, + "toasts": { + "error_deleting_files": "Virhe tiedostojen poistamisessa", + "background_downloads_enabled": "Taustalataukset käytössä", + "background_downloads_disabled": "Taustalataukset pois käytöstä", + "connected": "Yhdistetty", + "could_not_connect": "Yhteyttä ei voitu muodostaa", + "invalid_url": "Virheellinen URL" + } + }, + "sessions": { + "title": "Istunnot", + "no_active_sessions": "Ei aktiivisia istuntoja" + }, + "downloads": { + "downloads_title": "Lataukset", + "tvseries": "TV-sarjat", + "movies": "Elokuvat", + "queue": "Jonot", + "queue_hint": "Jonot ja lataukset menetetään sovelluksen uudelleenkäynnistyksen yhteydessä", + "no_items_in_queue": "Ei kohteita jonossa", + "no_downloaded_items": "Ei ladattuja kohteita", + "delete_all_movies_button": "Poista kaikki elokuvat", + "delete_all_tvseries_button": "Poista kaikki TV-sarjat", + "delete_all_button": "Poista kaikki", + "active_download": "Aktiivinen lataus", + "no_active_downloads": "Ei aktiivisia latauksia", + "active_downloads": "Aktiiviset lataukset", + "new_app_version_requires_re_download": "Uusi sovellusversio vaatii uudelleen latauksen", + "new_app_version_requires_re_download_description": "Uusi päivitys vaatii sisällön lataamista uudelleen. Poista kaikki ladattu sisältö ja yritä uudelleen.", + "back": "Takaisin", + "delete": "Poista", + "something_went_wrong": "Jotain meni pieleen", + "could_not_get_stream_url_from_jellyfin": "Ei voitu saada suoratoiston URL:ia Jellyfinilta", + "eta": "Arvio {{eta}}", + "methods": "Menetelmät", + "toasts": { + "you_are_not_allowed_to_download_files": "Sinulla ei ole lupaa ladata tiedostoja.", + "deleted_all_movies_successfully": "Kaikki elokuvat poistettu onnistuneesti!", + "failed_to_delete_all_movies": "Kaikkien elokuvien poistaminen epäonnistui", + "deleted_all_tvseries_successfully": "Kaikki TV-sarjat poistettu onnistuneesti!", + "failed_to_delete_all_tvseries": "Kaikkien TV-sarjojen poistaminen epäonnistui", + "download_cancelled": "Lataus peruutettu", + "could_not_cancel_download": "Latausta ei voitu peruuttaa", + "download_completed": "Lataus valmis", + "download_started_for": "Lataus aloitettu kohteelle {{item}}", + "item_is_ready_to_be_downloaded": "{{item}} on valmis ladattavaksi", + "download_stated_for_item": "Lataus aloitettu kohteelle {{item}}", + "download_failed_for_item": "Lataus epäonnistui kohteelle {{item}} - {{error}}", + "download_completed_for_item": "Lataus valmis kohteelle {{item}}", + "queued_item_for_optimization": "Jonotettu {{item}} optimointia varten", + "failed_to_start_download_for_item": "Latauksen aloittaminen kohteelle {{item}} epäonnistui: {{message}}", + "server_responded_with_status_code": "Palvelin vastasi tilakoodilla {{statusCode}}", + "no_response_received_from_server": "Palvelimelta ei saatu vastausta", + "error_setting_up_the_request": "Virhe pyynnön asetuksessa", + "failed_to_start_download_for_item_unexpected_error": "Latauksen aloittaminen kohteelle {{item}} epäonnistui: Odottamaton virhe", + "all_files_folders_and_jobs_deleted_successfully": "Kaikki tiedostot, kansiot ja tehtävät poistettu onnistuneesti", + "an_error_occured_while_deleting_files_and_jobs": "Virhe tapahtui tiedostojen ja tehtävien poistamisen aikana", + "go_to_downloads": "Siirry latauksiin" + } + } + }, + "search": { + "search_here": "Hae täältä...", + "search": "Haku...", + "x_items": "{{count}} kohdetta", + "library": "Kirjasto", + "discover": "Löydä", + "no_results": "Ei tuloksia", + "no_results_found_for": "Ei tuloksia löydetty", + "movies": "Elokuvat", + "series": "Sarjat", + "episodes": "Jaksot", + "collections": "Kokoelmat", + "actors": "Näyttelijät", + "request_movies": "Pyydä elokuvia", + "request_series": "Pyydä sarjoja", + "recently_added": "Äskettäin lisätty", + "recent_requests": "Äskettäin pyydetyt", + "plex_watchlist": "Plexin seurantalista", + "trending": "Trendikkäät", + "popular_movies": "Suositut elokuvat", + "movie_genres": "Elokuvagenret", + "upcoming_movies": "Tulevat elokuvat", + "studios": "Studios", + "popular_tv": "Suositut TV-ohjelmat", + "tv_genres": "TV-genret", + "upcoming_tv": "Tulevat TV-ohjelmat", + "networks": "Verkot", + "tmdb_movie_keyword": "TMDB Elokuvan avainsana", + "tmdb_movie_genre": "TMDB Elokuvagenre", + "tmdb_tv_keyword": "TMDB TV:n avainsana", + "tmdb_tv_genre": "TMDB TV-genre", + "tmdb_search": "TMDB Haku", + "tmdb_studio": "TMDB Studio", + "tmdb_network": "TMDB Verkko", + "tmdb_movie_streaming_services": "TMDB Elokuvan suoratoistopalvelut", + "tmdb_tv_streaming_services": "TMDB TV:n suoratoistopalvelut" + }, + "library": { + "no_items_found": "Ei kohteita löytynyt", + "no_results": "Ei tuloksia", + "no_libraries_found": "Ei kirjastoja löytynyt", + "item_types": { + "movies": "elokuvat", + "series": "sarjat", + "boxsets": "bokset", + "items": "kohteet" + }, + "options": { + "display": "Näyttö", + "row": "Rivi", + "list": "Lista", + "image_style": "Kuvatyylit", + "poster": "Juliste", + "cover": "Kansi", + "show_titles": "Näytä otsikot", + "show_stats": "Näytä tilastot" + }, + "filters": { + "genres": "Genret", + "years": "Vuodet", + "sort_by": "Lajittele", + "sort_order": "Lajittelujärjestys", + "asc": "Nouseva", + "desc": "Laskeva", + "tags": "Tunnisteet" + }, + "favorites": { + "series": "Sarjat", + "movies": "Elokuvat", + "episodes": "Jaksot", + "videos": "Videot", + "boxsets": "Bokset", + "playlists": "Soittolistat", + "noDataTitle": "Ei suosikkeja vielä", + "noData": "Merkitse kohteita suosikeiksi, jotta ne näkyvät täällä nopeaa pääsyä varten." + }, + "custom_links": { + "no_links": "Ei linkkejä" + }, + "player": { + "error": "Virhe", + "failed_to_get_stream_url": "Suoratoisto-URL:n saaminen epäonnistui", + "an_error_occured_while_playing_the_video": "Virhe tapahtui videota toistettaessa. Tarkista lokit asetuksista.", + "client_error": "Asiakasvirhe", + "could_not_create_stream_for_chromecast": "Suoratoistoa ei voitu luoda Chromecastille", + "message_from_server": "Viesti palvelimelta: {{message}}", + "video_has_finished_playing": "Video on lopettanut toistamisen!", + "no_video_source": "Ei videolähdettä...", + "next_episode": "Seuraava jakso", + "refresh_tracks": "Päivitä raidat", + "subtitle_tracks": "Tekstitysraidat:", + "audio_tracks": "Ääniraidat:", + "playback_state": "Toistotila:", + "no_data_available": "Ei tietoja saatavilla", + "index": "Indeksi:" + }, + "item_card": { + "next_up": "Seuraavaksi", + "no_items_to_display": "Ei näytettäviä kohteita", + "cast_and_crew": "Näyttelijät ja työryhmä", + "series": "Sarjat", + "seasons": "Kausi(t)", + "season": "Kausi", + "no_episodes_for_this_season": "Ei jaksoja tälle kaudelle", + "overview": "Yhteenveto", + "more_with": "Lisää {{name}}:n kanssa", + "similar_items": "Samanlaiset kohteet", + "no_similar_items_found": "Ei löytynyt samanlaisia kohteita", + "video": "Video", + "more_details": "Lisätietoja", + "quality": "Laatu", + "audio": "Ääni", + "subtitles": "Tekstitys", + "show_more": "Näytä lisää", + "show_less": "Näytä vähemmän", + "appeared_in": "Ilmestyi", + "could_not_load_item": "Kohteen lataaminen epäonnistui", + "none": "Ei mitään", + "download": { + "download_season": "Lataa kausi", + "download_series": "Lataa sarja", + "download_episode": "Lataa jakso", + "download_movie": "Lataa elokuva", + "download_x_item": "Lataa {{item_count}} kohdetta", + "download_button": "Lataa", + "using_optimized_server": "Käytetään optimoitua palvelinta", + "using_default_method": "Käytetään oletusmenetelmää" + } + } + }, + "live_tv": { + "next": "Seuraava", + "previous": "Edellinen", + "live_tv": "Live TV", + "coming_soon": "Tulossa pian", + "on_now": "Nykyään", + "shows": "Ohjelmat", + "movies": "Elokuvat", + "sports": "Urheilu", + "for_kids": "Lapsille", + "news": "Uutiset" + }, + "jellyseerr": { + "confirm": "Vahvista", + "cancel": "Peruuta", + "yes": "Kyllä", + "whats_wrong": "Mikä on vialla?", + "issue_type": "Ongelmatyyppi", + "select_an_issue": "Valitse ongelma", + "types": "Tyypit", + "describe_the_issue": "(valinnainen) Kuvaile ongelmaa...", + "submit_button": "Lähetä", + "report_issue_button": "Ilmoita ongelmasta", + "request_button": "Pyydä", + "are_you_sure_you_want_to_request_all_seasons": "Oletko varma, että haluat pyytää kaikki kaudet?", + "failed_to_login": "Kirjautuminen epäonnistui", + "cast": "Näyttelijät", + "details": "Yksityiskohdat", + "status": "Tila", + "original_title": "Alkuperäinen otsikko", + "series_type": "Sarjan tyyppi", + "release_dates": "Julkaisupäivät", + "first_air_date": "Ensimmäinen esitys", + "next_air_date": "Seuraava esitys", + "revenue": "Tulot", + "budget": "Budjetti", + "original_language": "Alkuperäinen kieli", + "production_country": "Tuotantomaa", + "studios": "Studio", + "network": "Verkko", + "currently_streaming_on": "Nykyään suoratoistona", + "advanced": "Lisäasetukset", + "request_as": "Pyydä muodossa", + "tags": "Tunnisteet", + "quality_profile": "Laatuprofiili", + "root_folder": "Juurikansio", + "season_all": "Kausi (kaikki)", + "season_number": "Kausi {{season_number}}", + "number_episodes": "{{episode_number}} jaksoa", + "born": "Syntynyt", + "appearances": "Ulkonäöt", + "toasts": { + "jellyseer_does_not_meet_requirements": "Jellyseerr-palvelin ei täytä vähimmäisversiovaatimuksia! Päivitä vähintään versioon 2.0.0", + "jellyseerr_test_failed": "Jellyseerr-testi epäonnistui. Yritä uudelleen.", + "failed_to_test_jellyseerr_server_url": "Jellyseerr-palvelimen URL-osoitteen testaaminen epäonnistui", + "issue_submitted": "Ongelma lähetetty!", + "requested_item": "Pyydetty {{item}}!", + "you_dont_have_permission_to_request": "Sinulla ei ole lupaa pyytää!", + "something_went_wrong_requesting_media": "Jotain meni pieleen mediaa pyydettäessä!" + } + }, + "tabs": { + "home": "Etusivu", + "search": "Haku", + "library": "Kirjasto", + "custom_links": "Mukautetut linkit", + "favorites": "Suosikit" + } +} diff --git a/translations/nb.json b/translations/nb.json new file mode 100644 index 00000000..cbbd081b --- /dev/null +++ b/translations/nb.json @@ -0,0 +1,480 @@ +{ + "login": { + "username_required": "Brukernavn er obligatorisk", + "error_title": "Feil", + "login_title": "Logg inn", + "login_to_title": "Logg inn på", + "username_placeholder": "Brukernavn", + "password_placeholder": "Passord", + "login_button": "Logg inn", + "quick_connect": "Hurtigtilkobling", + "enter_code_to_login": "Skriv inn kode {{code}} for å logge inn", + "failed_to_initiate_quick_connect": "Kunne ikke starte hurtigtilkobling", + "got_it": "Forstått", + "connection_failed": "Tilkobling feilet", + "could_not_connect_to_server": "Kunne ikke koble til serveren. Sjekk URL-adressen og nettverkstilkoblingen din.", + "an_unexpected_error_occured": "Det oppsto en uventet feil", + "change_server": "Endre server", + "invalid_username_or_password": "Feil brukernavn eller passord", + "user_does_not_have_permission_to_log_in": "Brukeren har ikke tillatelse til å logge inn", + "server_is_taking_too_long_to_respond_try_again_later": "Serveren bruker for lang tid på å svare. Prøv igjen senere.", + "server_received_too_many_requests_try_again_later": "Serveren mottok for mange forespørsler. Prøv på nytt senere.", + "there_is_a_server_error": "Det er en serverfeil", + "an_unexpected_error_occured_did_you_enter_the_correct_url": "Det oppsto en uventet feil. Skrev du inn server-URL-en riktig?" + }, + "server": { + "enter_url_to_jellyfin_server": "Skriv inn URL-en til Jellyfin-serveren din", + "server_url_placeholder": "http(s)://din-server.com", + "connect_button": "Koble til", + "previous_servers": "tidligere servere", + "clear_button": "Fjern", + "search_for_local_servers": "Søk etter lokale servere", + "searching": "Søker...", + "servers": "Servere" + }, + "home": { + "no_internet": "Ingen internett-tilkobling", + "no_items": "Ingen elementer", + "no_internet_message": "Ingen fare, du kan fortsatt se\nnedlastet innhold.", + "go_to_downloads": "Gå til nedlastinger", + "oops": "Oops!", + "error_message": "Noe gikk galt.\nVennligst logg ut og inn igjen.", + "continue_watching": "Fortsett å se", + "next_up": "Neste", + "recently_added_in": "Nylig lagt til i {{libraryName}}", + "suggested_movies": "Foreslåtte filmer", + "suggested_episodes": "Foreslåtte episoder", + "intro": { + "welcome_to_streamyfin": "Velkommen til Streamyfin", + "a_free_and_open_source_client_for_jellyfin": "En gratis og åpen kildekode-klient for Jellyfin.", + "features_title": "Funksjoner", + "features_description": "Streamyfin har en rekke funksjoner og integreres med et bredt utvalg av programvare som du finner i innstillingsmenyen, disse inkluderer:", + "jellyseerr_feature_description": "Koble til Jellyseerr-instansen din og be om filmer direkte i appen.", + "downloads_feature_title": "Nedlastinger", + "downloads_feature_description": "Last ned filmer og TV-serier for å se dem offline. Bruk enten standardmetoden eller installer optimize-serveren for å laste ned filer i bakgrunnen.", + "chromecast_feature_description": "Cast filmer og TV-serier til Chromecast-enhetene dine.", + "centralised_settings_plugin_title": "Sentraliserte innstillinger-plugin", + "centralised_settings_plugin_description": "Konfigurer innstillinger fra en sentralisert plassering på Jellyfin-serveren din. Alle klientinnstillinger for alle brukere synkroniseres automatisk.", + "done_button": "Fortsett", + "go_to_settings_button": "Gå til innstillinger", + "read_more": "Les mer" + }, + "settings": { + "settings_title": "Innstillinger", + "log_out_button": "Logg ut", + "user_info": { + "user_info_title": "Brukerinformasjon", + "user": "Bruker", + "server": "Server", + "token": "Token", + "app_version": "App Versjon" + }, + "quick_connect": { + "quick_connect_title": "Hurtigtilkobling", + "authorize_button": "Autoriser hurtigtilkobling", + "enter_the_quick_connect_code": "Skriv inn hurtigkoblingskoden...", + "success": "Suksess", + "quick_connect_autorized": "Hurtigtilkobling autorisert", + "error": "Feil", + "invalid_code": "Ugyldig kode", + "authorize": "Autoriser" + }, + "media_controls": { + "media_controls_title": "Mediekontroller", + "forward_skip_length": "Lengde foroverhopp", + "rewind_length": "Tilbakespolingslengde", + "seconds_unit": "s" + }, + "audio": { + "audio_title": "Lyd", + "set_audio_track": "Angi lydspor fra forrige element", + "audio_language": "Lydspråk", + "audio_hint": "Velg et standard lydspråk.", + "none": "Ingen", + "language": "Språk" + }, + "subtitles": { + "subtitle_title": "Undertekster", + "subtitle_language": "Undertekstspråk", + "subtitle_mode": "Tekstingmodus", + "set_subtitle_track": "Sett undertekstspor fra forrige element", + "subtitle_size": "Størrelse på underteksten", + "subtitle_hint": "Konfigurer preferanser for undertekster.", + "none": "Ingen", + "language": "Språk", + "loading": "Laster", + "modes": { + "Default": "Standard", + "Smart": "Smart", + "Always": "Alltid", + "None": "Ingen", + "OnlyForced": "BareTvunget" + } + }, + "other": { + "other_title": "Annet", + "follow_device_orientation": "Automatisk rotasjon", + "video_orientation": "Videoorientering", + "orientation": "Rotasjon", + "orientations": { + "DEFAULT": "Standard", + "ALL": "Alle", + "PORTRAIT": "Portrett", + "PORTRAIT_UP": "Portrett Opp", + "PORTRAIT_DOWN": "Portrett Ned", + "LANDSCAPE": "Landskap", + "LANDSCAPE_LEFT": "Landskap Venstre", + "LANDSCAPE_RIGHT": "Landskap Høyre", + "OTHER": "Annet", + "UNKNOWN": "Ukjent" + }, + "safe_area_in_controls": "Trygt område i kontrollene", + "video_player": "Videospiller", + "video_players": { + "VLC_3": "VLC 3", + "VLC_4": "VLC 4 (Eksperimentell + PiP)" + }, + "show_custom_menu_links": "Vis Tilpassede Meny Linker", + "hide_libraries": "Skjul biblioteker", + "select_liraries_you_want_to_hide": "Velg bibliotekene du vil skjule fra Bibliotek-fanen og hjemmesidedelene.", + "disable_haptic_feedback": "Deaktiver haptisk tilbakemelding", + "default_quality": "Standardkvalitet" + }, + "downloads": { + "downloads_title": "Nedlastinger", + "download_method": "Nedlastingsmetode", + "remux_max_download": "Remux max nedlasting", + "auto_download": "Automatisk nedlasting", + "optimized_versions_server": "Optimaliserte versjoner server", + "save_button": "Lagre", + "optimized_server": "Optimalisert Server", + "optimized": "Optimalisert", + "default": "Standard", + "optimized_version_hint": "Skriv inn URL-en for optimeringsserveren. URL-en skal inneholde http eller https og eventuelt porten.", + "read_more_about_optimized_server": "Les mer om optimeringsserveren.", + "url": "URL", + "server_url_placeholder": "http(s)://domene.org:port" + }, + "plugins": { + "plugins_title": "Plugins", + "jellyseerr": { + "jellyseerr_warning": "Denne integrasjonen er i en tidlig fase. Forvent at ting vil endre seg.", + "server_url": "Server URL", + "server_url_hint": "Eksempel: http(s)://din-host.url\n(legg til port om nødvendig)", + "server_url_placeholder": "Jellyseerr URL...", + "password": "Passord", + "password_placeholder": "Skriv inn passord for Jellyfin-bruker {{username}}", + "save_button": "Lagre", + "clear_button": "Fjern", + "login_button": "Logg inn", + "total_media_requests": "Totalt antall medieforespørsler", + "movie_quota_limit": "Grense for filmkvote", + "movie_quota_days": "Antall dager for filmkvote", + "tv_quota_limit": "Grense for TV-kvote", + "tv_quota_days": "Antall dager for TV-kvote", + "reset_jellyseerr_config_button": "Tilbakestill Jellyseerr-konfigurasjon", + "unlimited": "Ubegrenset", + "plus_n_more": "+{{n}} til", + "order_by": { + "DEFAULT": "Standard", + "VOTE_COUNT_AND_AVERAGE": "Antall stemmer og gjennomsnitt", + "POPULARITY": "Popularitet" + } + }, + "marlin_search": { + "enable_marlin_search": "Aktiver Marlin-søk", + "url": "URL", + "server_url_placeholder": "http(s)://domene.org:port", + "marlin_search_hint": "Skriv inn URL-en til Marlin-serveren. URL-en bør inkludere http eller https og eventuelt portnummer.", + "read_more_about_marlin": "Les mer om Marlin.", + "save_button": "Lagre", + "toasts": { + "saved": "Lagret" + } + } + }, + "storage": { + "storage_title": "Lagring", + "app_usage": "App {{usedSpace}}%", + "device_usage": "Enhet {{availableSpace}}%", + "size_used": "{{used}} av {{total}} brukt", + "delete_all_downloaded_files": "Slett alle nedlastede filer" + }, + "intro": { + "show_intro": "Vis introduksjon", + "reset_intro": "Tilbakestill introduksjon" + }, + "logs": { + "logs_title": "Logger", + "export_logs": "Eksporter logger", + "click_for_more_info": "Klikk for mer informasjon", + "level": "Nivå", + "no_logs_available": "Ingen logger tilgjengelig", + "delete_all_logs": "Slett alle logger" + }, + "languages": { + "title": "Språk", + "app_language": "Appspråk", + "app_language_description": "Velg språk for appen.", + "system": "System" + }, + "toasts": { + "error_deleting_files": "Feil ved sletting av filer", + "background_downloads_enabled": "Bakgrunnsnedlastinger aktivert", + "background_downloads_disabled": "Bakgrunnsnedlastinger deaktivert", + "connected": "Tilkoblet", + "could_not_connect": "Kunne ikke koble til", + "invalid_url": "Ugyldig URL" + } + }, + "sessions": { + "title": "Økter", + "no_active_sessions": "Ingen aktive økter" + }, + "downloads": { + "downloads_title": "Nedlastinger", + "tvseries": "TV-serier", + "movies": "Filmer", + "queue": "Kø", + "queue_hint": "Kø og nedlastinger vil gå tapt ved omstart av appen", + "no_items_in_queue": "Ingen elementer i køen", + "no_downloaded_items": "Ingen nedlastede elementer", + "delete_all_movies_button": "Slett alle filmer", + "delete_all_tvseries_button": "Slett alle TV-serier", + "delete_all_button": "Slett alt", + "active_download": "Aktiv nedlasting", + "no_active_downloads": "Ingen aktive nedlastinger", + "active_downloads": "Aktive nedlastinger", + "new_app_version_requires_re_download": "Ny appversjon krever ny nedlasting", + "new_app_version_requires_re_download_description": "Den nye oppdateringen krever at innholdet lastes ned på nytt. Vennligst fjern alt nedlastet innhold og prøv igjen.", + "back": "Tilbake", + "delete": "Slett", + "something_went_wrong": "Noe gikk galt", + "could_not_get_stream_url_from_jellyfin": "Kunne ikke hente strømme-URL fra Jellyfin", + "eta": "ETA {{eta}}", + "methods": "Metoder", + "toasts": { + "you_are_not_allowed_to_download_files": "Du har ikke tillatelse til å laste ned filer.", + "deleted_all_movies_successfully": "Alle filmer ble slettet!", + "failed_to_delete_all_movies": "Kunne ikke slette alle filmer", + "deleted_all_tvseries_successfully": "Alle TV-serier ble slettet!", + "failed_to_delete_all_tvseries": "Kunne ikke slette alle TV-serier", + "download_cancelled": "Nedlasting avbrutt", + "could_not_cancel_download": "Kunne ikke avbryte nedlastingen", + "download_completed": "Nedlasting fullført", + "download_started_for": "Nedlasting startet for {{item}}", + "item_is_ready_to_be_downloaded": "{{item}} er klar til å lastes ned", + "download_stated_for_item": "Nedlasting startet for {{item}}", + "download_failed_for_item": "Nedlasting mislyktes for {{item}} – {{error}}", + "download_completed_for_item": "Nedlasting fullført for {{item}}", + "queued_item_for_optimization": "Køet {{item}} for optimalisering", + "failed_to_start_download_for_item": "Kunne ikke starte nedlasting for {{item}}: {{message}}", + "server_responded_with_status_code": "Serveren svarte med statuskode {{statusCode}}", + "no_response_received_from_server": "Ingen respons mottatt fra serveren", + "error_setting_up_the_request": "Feil under oppsett av forespørselen", + "failed_to_start_download_for_item_unexpected_error": "Kunne ikke starte nedlasting for {{item}}: Uventet feil", + "all_files_folders_and_jobs_deleted_successfully": "Alle filer, mapper og jobber ble slettet", + "an_error_occured_while_deleting_files_and_jobs": "En feil oppstod under sletting av filer og jobber", + "go_to_downloads": "Gå til nedlastinger" + } + } + }, + "search": { + "search_here": "Søk her...", + "search": "Søk...", + "x_items": "{{count}} elementer", + "library": "Bibliotek", + "discover": "Oppdag", + "no_results": "Ingen resultater", + "no_results_found_for": "Ingen resultater funnet for", + "movies": "Filmer", + "series": "Serier", + "episodes": "Episoder", + "collections": "Samlinger", + "actors": "Skuespillere", + "request_movies": "Be om filmer", + "request_series": "Be om serier", + "recently_added": "Nylig lagt til", + "recent_requests": "Nye forespørsler", + "plex_watchlist": "Plex se-liste", + "trending": "Trender nå", + "popular_movies": "Populære filmer", + "movie_genres": "Filmgenrer", + "upcoming_movies": "Kommende filmer", + "studios": "Studioer", + "popular_tv": "Populære TV-serier", + "tv_genres": "TV-genrer", + "upcoming_tv": "Kommende TV-serier", + "networks": "TV-nettverk", + "tmdb_movie_keyword": "TMDB filmnøkkelord", + "tmdb_movie_genre": "TMDB filmgenre", + "tmdb_tv_keyword": "TMDB TV-nøkkelord", + "tmdb_tv_genre": "TMDB TV-genre", + "tmdb_search": "TMDB-søk", + "tmdb_studio": "TMDB-studio", + "tmdb_network": "TMDB-nettverk", + "tmdb_movie_streaming_services": "TMDB filmstrømmetjenester", + "tmdb_tv_streaming_services": "TMDB TV-strømmetjenester" + }, + "library": { + "no_items_found": "Ingen elementer funnet", + "no_results": "Ingen resultater", + "no_libraries_found": "Ingen biblioteker funnet", + "item_types": { + "movies": "filmer", + "series": "serier", + "boxsets": "samlebokser", + "items": "elementer" + }, + "options": { + "display": "Visning", + "row": "Rad", + "list": "Liste", + "image_style": "Bildestil", + "poster": "Plakat", + "cover": "Omslag", + "show_titles": "Vis titler", + "show_stats": "Vis statistikk" + }, + "filters": { + "genres": "Sjanger", + "years": "År", + "sort_by": "Sorter etter", + "sort_order": "Sorteringsrekkefølge", + "asc": "Stigende", + "desc": "Synkende", + "tags": "Tagger" + } + }, + "favorites": { + "series": "Serier", + "movies": "Filmer", + "episodes": "Episoder", + "videos": "Videoer", + "boxsets": "Samlebokser", + "playlists": "Spillelister", + "noDataTitle": "Ingen favoritter ennå", + "noData": "Marker elementer som favoritter for å se dem her for rask tilgang." + }, + "custom_links": { + "no_links": "Ingen lenker" + }, + "player": { + "error": "Feil", + "failed_to_get_stream_url": "Kunne ikke hente stream-URL", + "an_error_occured_while_playing_the_video": "En feil oppstod under avspilling av videoen. Sjekk loggene i innstillingene.", + "client_error": "Kundefeil", + "could_not_create_stream_for_chromecast": "Kunne ikke lage stream for Chromecast", + "message_from_server": "Melding fra server: {{message}}", + "video_has_finished_playing": "Videoen har avsluttet avspilling!", + "no_video_source": "Ingen videosource...", + "next_episode": "Neste episode", + "refresh_tracks": "Oppdater spor", + "subtitle_tracks": "Undertekstspor:", + "audio_tracks": "Lydspor:", + "playback_state": "Avspillingsstatus:", + "no_data_available": "Ingen data tilgjengelig", + "index": "Indeks:" + }, + "item_card": { + "next_up": "Neste opp", + "no_items_to_display": "Ingen elementer å vise", + "cast_and_crew": "Skuespillere & Crew", + "series": "Serier", + "seasons": "Sesonger", + "season": "Sesong", + "no_episodes_for_this_season": "Ingen episoder for denne sesongen", + "overview": "Oversikt", + "more_with": "Mer med {{name}}", + "similar_items": "Lignende elementer", + "no_similar_items_found": "Ingen lignende elementer funnet", + "video": "Video", + "more_details": "Mer detaljer", + "quality": "Kvalitet", + "audio": "Lyd", + "subtitles": "Undertekster", + "show_more": "Vis mer", + "show_less": "Vis mindre", + "appeared_in": "Viste seg i", + "could_not_load_item": "Kunne ikke laste element", + "none": "Ingen", + "download": { + "download_season": "Last ned sesong", + "download_series": "Last ned serie", + "download_episode": "Last ned episode", + "download_movie": "Last ned film", + "download_x_item": "Last ned {{item_count}} elementer", + "download_button": "Last ned", + "using_optimized_server": "Bruker optimalisert server", + "using_default_method": "Bruker standard metode" + } + }, + "live_tv": { + "next": "Neste", + "previous": "Forrige", + "live_tv": "Direkte TV", + "coming_soon": "Kommer snart", + "on_now": "Vises nå", + "shows": "Programmer", + "movies": "Filmer", + "sports": "Sport", + "for_kids": "For barn", + "news": "Nyheter" + }, + "jellyseerr": { + "confirm": "Bekreft", + "cancel": "Avbryt", + "yes": "Ja", + "whats_wrong": "Hva er galt?", + "issue_type": "Problemetype", + "select_an_issue": "Velg et problem", + "types": "Typer", + "describe_the_issue": "(valgfritt) Beskriv problemet...", + "submit_button": "Send inn", + "report_issue_button": "Rapporter problem", + "request_button": "Be om", + "are_you_sure_you_want_to_request_all_seasons": "Er du sikker på at du vil be om alle sesongene?", + "failed_to_login": "Kunne ikke logge inn", + "cast": "Skuespillere", + "details": "Detaljer", + "status": "Status", + "original_title": "Original tittel", + "series_type": "Serietype", + "release_dates": "Utgivelsesdatoer", + "first_air_date": "Første visningsdato", + "next_air_date": "Neste visningsdato", + "revenue": "Inntekter", + "budget": "Budsjett", + "original_language": "Opprinnelig språk", + "production_country": "Produksjonsland", + "studios": "Studios", + "network": "Nettverk", + "currently_streaming_on": "Strømmes nå på", + "advanced": "Avansert", + "request_as": "Be om som", + "tags": "Tags", + "quality_profile": "Kvalitetsprofil", + "root_folder": "Rotmappe", + "season_all": "Sesong (alle)", + "season_number": "Sesong {{season_number}}", + "number_episodes": "{{episode_number}} Episoder", + "born": "Født", + "appearances": "Opptredener", + "toasts": { + "jellyseer_does_not_meet_requirements": "Jellyseerr-serveren oppfyller ikke minimumskravene! Vennligst oppdater til minst versjon 2.0.0", + "jellyseerr_test_failed": "Jellyseerr-testen feilet. Vennligst prøv igjen.", + "failed_to_test_jellyseerr_server_url": "Kunne ikke teste jellyseerr-serverens URL", + "issue_submitted": "Problemet er sendt inn!", + "requested_item": "Be om {{item}}!", + "you_dont_have_permission_to_request": "Du har ikke tillatelse til å be om!", + "something_went_wrong_requesting_media": "Noe gikk galt med å be om media!" + } + }, + "tabs": { + "home": "Hjem", + "search": "Søk", + "library": "Bibliotek", + "custom_links": "Egendefinerte lenker", + "favorites": "Favoritter" + } +} diff --git a/translations/nn.json b/translations/nn.json new file mode 100644 index 00000000..e3795f00 --- /dev/null +++ b/translations/nn.json @@ -0,0 +1,480 @@ +{ + "login": { + "username_required": "Brukarnamn er obligatorisk", + "error_title": "Feil", + "login_title": "Logg inn", + "login_to_title": "Logg inn på", + "username_placeholder": "Brukarnamn", + "password_placeholder": "Passord", + "login_button": "Logg inn", + "quick_connect": "Hurtigtilkobling", + "enter_code_to_login": "Skriv inn kode {{code}} for å logga inn", + "failed_to_initiate_quick_connect": "Kunne ikkje starta hurtigtilkopling", + "got_it": "Forstått", + "connection_failed": "Tilkopling feila", + "could_not_connect_to_server": "Kunne ikkje kopla til serveren. Sjekk URL-adressa og nettverkstilkoplinga di.", + "an_unexpected_error_occured": "Det oppstod ein uventa feil", + "change_server": "Endre servar", + "invalid_username_or_password": "Feil brukarnamn eller passord", + "user_does_not_have_permission_to_log_in": "Brukaren har ikkje løyve til å logga inn", + "server_is_taking_too_long_to_respond_try_again_later": "Serveren bruker for lang tid på å svara. Prøv igjen seinare.", + "server_received_too_many_requests_try_again_later": "Serveren fekk for mange førespurnader. Prøv på nytt seinare.", + "there_is_a_server_error": "Det er ein serverfeil", + "an_unexpected_error_occured_did_you_enter_the_correct_url": "Det oppstod ein uventa feil. Skreiv du inn server-URL-en rett?" + }, + "server": { + "enter_url_to_jellyfin_server": "Skriv inn URL-en til Jellyfin-serveren din", + "server_url_placeholder": "http(s)://din-server.com", + "connect_button": "Kopl til", + "previous_servers": "tidlegare serverar", + "clear_button": "Fjern", + "search_for_local_servers": "Søk etter lokale serverar", + "searching": "Søkjer...", + "servers": "Serverar" + }, + "home": { + "no_internet": "Inga internett-tilkopling", + "no_items": "Ingen elementer", + "no_internet_message": "Ingen fare, du kan framleis sjå\nnedlastet innhald.", + "go_to_downloads": "Gå til nedlastingar", + "oops": "Oops!", + "error_message": "Noe gjekk gale.\nVer vennleg og logg ut og inn igjen.", + "continue_watching": "Hald fram med å sjå", + "next_up": "Neste", + "recently_added_in": "Nyleg lagt til i {{libraryName}}", + "suggested_movies": "Foreslåtte filmar", + "suggested_episodes": "Foreslåtte episodar", + "intro": { + "welcome_to_streamyfin": "Velkommen til Streamyfin", + "a_free_and_open_source_client_for_jellyfin": "Ein gratis og open kjeldekode-klient for Jellyfin.", + "features_title": "Funksjonar", + "features_description": "Streamyfin har ei rekkje funksjonar og blir integrerte med eit breitt utval av programvare som du finn i innstillingsmenyen, desse inkluderer:", + "jellyseerr_feature_description": "Kopl til Jellyseerr-instansen din og be om filmar direkte i appen.", + "downloads_feature_title": "Nedlastingar", + "downloads_feature_description": "Last ned filmar og TV-seriar for å sjå dei offline. Bruk anten standardmetoden eller installer optimize-serveren for å lasta ned filer i bakgrunnen.", + "chromecast_feature_description": "Cast filmar og TV-seriar til Chromecast-einingane dine.", + "centralised_settings_plugin_title": "Sentraliserte innstillingar-plugin", + "centralised_settings_plugin_description": "Konfigurer innstillingar frå ei sentralisert plassering på Jellyfin-serveren din. Alle klientinnstillingar for alle brukarar blir automatisk synkroniserte.", + "done_button": "Hald fram", + "go_to_settings_button": "Gå til innstillingar", + "read_more": "Les meir" + }, + "settings": { + "settings_title": "Innstillingar", + "log_out_button": "Logg ut", + "user_info": { + "user_info_title": "Brukarinformasjon", + "user": "Bruker", + "server": "Servar", + "token": "Token", + "app_version": "App Versjon" + }, + "quick_connect": { + "quick_connect_title": "Hurtigtilkopling", + "authorize_button": "Autoriser hurtigtilkopling", + "enter_the_quick_connect_code": "Skriv inn hurtigkoplingskoden...", + "success": "Suksess", + "quick_connect_autorized": "Hurtigtilkopling autorisert", + "error": "Feil", + "invalid_code": "Ugyldig kode", + "authorize": "Autoriser" + }, + "media_controls": { + "media_controls_title": "Mediekontrollar", + "forward_skip_length": "Lengd framoverhopp", + "rewind_length": "Tilbakespolingslengde", + "seconds_unit": "s" + }, + "audio": { + "audio_title": "Lyd", + "set_audio_track": "Oppgi lydspor frå førre element", + "audio_language": "Lydspråk", + "audio_hint": "Vel eit standard lydspråk.", + "none": "Ingen", + "language": "Språk" + }, + "subtitles": { + "subtitle_title": "Undertekstar", + "subtitle_language": "Undertekstspråk", + "subtitle_mode": "Tekstingmodus", + "set_subtitle_track": "Set undertekstspor frå førre element", + "subtitle_size": "Storleik på underteksten", + "subtitle_hint": "Konfigurer preferansar for undertekstar.", + "none": "Ingen", + "language": "Språk", + "loading": "Lastar", + "modes": { + "Default": "Standard", + "Smart": "Smart", + "Always": "Alltid", + "None": "Ingen", + "OnlyForced": "BareTvunget" + } + }, + "other": { + "other_title": "Anna", + "follow_device_orientation": "Automatisk rotasjon", + "video_orientation": "Videoorientering", + "orientation": "Rotasjon", + "orientations": { + "DEFAULT": "Standard", + "ALL": "Alle", + "PORTRAIT": "Portrett", + "PORTRAIT_UP": "Portrett Opp", + "PORTRAIT_DOWN": "Portrett Ned", + "LANDSCAPE": "Landskap", + "LANDSCAPE_LEFT": "Landskap Venstre", + "LANDSCAPE_RIGHT": "Landskap Høgre", + "OTHER": "Annet", + "UNKNOWN": "Ukjent" + }, + "safe_area_in_controls": "Trygt område i kontrollane", + "video_player": "Videospelar", + "video_players": { + "VLC_3": "VLC 3", + "VLC_4": "VLC 4 (Eksperimentell + PiP)" + }, + "show_custom_menu_links": "Vis Tilpassede Meny Linker", + "hide_libraries": "Skjul bibliotek", + "select_liraries_you_want_to_hide": "Vel biblioteka du vil skjula frå Bibliotek-fanen og nettsidedelane.", + "disable_haptic_feedback": "Deaktiver haptisk tilbakemelding", + "default_quality": "Standardkvalitet" + }, + "downloads": { + "downloads_title": "Nedlastingar", + "download_method": "Nedlastingsmetode", + "remux_max_download": "Remux max nedlasting", + "auto_download": "Automatisk nedlasting", + "optimized_versions_server": "Optimaliserte versjonar servar", + "save_button": "Lagre", + "optimized_server": "Optimalisert Servar", + "optimized": "Optimalisert", + "default": "Standard", + "optimized_version_hint": "Skriv inn URL-en for optimeringsserveren. URL-en skal innehalda http eller https og eventuelt porten.", + "read_more_about_optimized_server": "Les meir om optimeringsserveren.", + "url": "URL", + "server_url_placeholder": "http(s)://domene.org:port" + }, + "plugins": { + "plugins_title": "Tillegg", + "jellyseerr": { + "jellyseerr_warning": "Denne integrasjonen er i ein tidleg fase. Forvent at ting vil endra seg.", + "server_url": "Server-URL", + "server_url_hint": "Eksempel: http(s)://din-host.url\n(legg til port om naudsynt)", + "server_url_placeholder": "Jellyseerr-URL...", + "password": "Passord", + "password_placeholder": "Skriv inn passord for Jellyfin-brukar {{username}}", + "save_button": "Lagra", + "clear_button": "Fjern", + "login_button": "Logg inn", + "total_media_requests": "Totalt tal på medieførespurnader", + "movie_quota_limit": "Grense for filmkvote", + "movie_quota_days": "Tal på dagar for filmkvote", + "tv_quota_limit": "Grense for TV-kvote", + "tv_quota_days": "Tal på dagar for TV-kvote", + "reset_jellyseerr_config_button": "Tilbakestill Jellyseerr-konfigurasjon", + "unlimited": "Uavgrensa", + "plus_n_more": "+{{n}} til", + "order_by": { + "DEFAULT": "Standard", + "VOTE_COUNT_AND_AVERAGE": "Tal på røyster og gjennomsnitt", + "POPULARITY": "Popularitet" + } + }, + "marlin_search": { + "enable_marlin_search": "Aktiver Marlin-søk", + "url": "URL", + "server_url_placeholder": "http(s)://domene.org:port", + "marlin_search_hint": "Skriv inn URL-en til Marlin-serveren. URL-en bør inkludera http eller https og eventuelt portnummer.", + "read_more_about_marlin": "Les meir om Marlin.", + "save_button": "Lagra", + "toasts": { + "saved": "Lagra" + } + } + }, + "storage": { + "storage_title": "Lagring", + "app_usage": "App {{usedSpace}}%", + "device_usage": "Eining {{availableSpace}}%", + "size_used": "{{used}} av {{total}} brukt", + "delete_all_downloaded_files": "Slett alle nedlasta filer" + }, + "intro": { + "show_intro": "Vis introduksjon", + "reset_intro": "Tilbakestill introduksjon" + }, + "logs": { + "logs_title": "Loggar", + "export_logs": "Eksporter loggar", + "click_for_more_info": "Klikk for meir informasjon", + "level": "Nivå", + "no_logs_available": "Ingen loggar tilgjengelege", + "delete_all_logs": "Slett alle loggar" + }, + "languages": { + "title": "Språk", + "app_language": "Appspråk", + "app_language_description": "Vel språk for appen.", + "system": "System" + }, + "toasts": { + "error_deleting_files": "Feil ved sletting av filer", + "background_downloads_enabled": "Bakgrunnsnedlastingar aktiverte", + "background_downloads_disabled": "Bakgrunnsnedlastingar deaktiverte", + "connected": "Tilkopla", + "could_not_connect": "Kunne ikkje kopla til", + "invalid_url": "Ugyldig URL" + } + }, + "sessions": { + "title": "Økter", + "no_active_sessions": "Ingen aktive økter" + }, + "downloads": { + "downloads_title": "Nedlastingar", + "tvseries": "TV-seriar", + "movies": "Filmar", + "queue": "Kø", + "queue_hint": "Kø og nedlastingar vil gå tapt ved omstart av appen", + "no_items_in_queue": "Ingen element i køen", + "no_downloaded_items": "Ingen nedlasta element", + "delete_all_movies_button": "Slett alle filmar", + "delete_all_tvseries_button": "Slett alle TV-seriar", + "delete_all_button": "Slett alt", + "active_download": "Aktiv nedlasting", + "no_active_downloads": "Ingen aktive nedlastingar", + "active_downloads": "Aktive nedlastingar", + "new_app_version_requires_re_download": "Ny appversjon krev ny nedlasting", + "new_app_version_requires_re_download_description": "Den nye oppdateringa krev at innhaldet blir lasta ned på nytt. Ver venleg og fjern alt nedlasta innhald og prøv igjen.", + "back": "Tilbake", + "delete": "Slett", + "something_went_wrong": "Noko gjekk gale", + "could_not_get_stream_url_from_jellyfin": "Kunne ikkje henta strøyme-URL frå Jellyfin", + "eta": "ETA {{eta}}", + "methods": "Metodar", + "toasts": { + "you_are_not_allowed_to_download_files": "Du har ikkje løyve til å lasta ned filer.", + "deleted_all_movies_successfully": "Alle filmar vart sletta!", + "failed_to_delete_all_movies": "Kunne ikkje sletta alle filmar", + "deleted_all_tvseries_successfully": "Alle TV-seriar vart sletta!", + "failed_to_delete_all_tvseries": "Kunne ikkje sletta alle TV-seriar", + "download_cancelled": "Nedlasting avbroten", + "could_not_cancel_download": "Kunne ikkje avbryta nedlastinga", + "download_completed": "Nedlasting fullført", + "download_started_for": "Nedlasting starta for {{item}}", + "item_is_ready_to_be_downloaded": "{{item}} er klar til å lastast ned", + "download_stated_for_item": "Nedlasting starta for {{item}}", + "download_failed_for_item": "Nedlasting mislukkast for {{item}} – {{error}}", + "download_completed_for_item": "Nedlasting fullført for {{item}}", + "queued_item_for_optimization": "La {{item}} i kø for optimalisering", + "failed_to_start_download_for_item": "Kunne ikkje starta nedlasting for {{item}}: {{message}}", + "server_responded_with_status_code": "Serveren svara med statuskode {{statusCode}}", + "no_response_received_from_server": "Ingen respons motteken frå serveren", + "error_setting_up_the_request": "Feil under oppsett av førespurnaden", + "failed_to_start_download_for_item_unexpected_error": "Kunne ikkje starta nedlasting for {{item}}: Uventa feil", + "all_files_folders_and_jobs_deleted_successfully": "Alle filer, mapper og jobbar vart sletta", + "an_error_occured_while_deleting_files_and_jobs": "Ein feil oppstod under sletting av filer og jobbar", + "go_to_downloads": "Gå til nedlastingar" + } + } + }, + "search": { + "search_here": "Søk her...", + "search": "Søk...", + "x_items": "{{count}} element", + "library": "Bibliotek", + "discover": "Oppdag", + "no_results": "Ingen resultat", + "no_results_found_for": "Ingen resultat funne for", + "movies": "Filmar", + "series": "Seriar", + "episodes": "Episodar", + "collections": "Samlingar", + "actors": "Skodespelarar", + "request_movies": "Be om filmar", + "request_series": "Be om seriar", + "recently_added": "Nyleg lagt til", + "recent_requests": "Nye førespurnader", + "plex_watchlist": "Plex sjå-liste", + "trending": "Trendar no", + "popular_movies": "Populære filmar", + "movie_genres": "Filmgenrar", + "upcoming_movies": "Komande filmar", + "studios": "Studio", + "popular_tv": "Populære TV-seriar", + "tv_genres": "TV-genrar", + "upcoming_tv": "Komande TV-seriar", + "networks": "TV-nettverk", + "tmdb_movie_keyword": "TMDB filmnøkkelord", + "tmdb_movie_genre": "TMDB filmgenre", + "tmdb_tv_keyword": "TMDB TV-nøkkelord", + "tmdb_tv_genre": "TMDB TV-genre", + "tmdb_search": "TMDB-søk", + "tmdb_studio": "TMDB-studio", + "tmdb_network": "TMDB-nettverk", + "tmdb_movie_streaming_services": "TMDB filmstrøymetenester", + "tmdb_tv_streaming_services": "TMDB TV-strøymetenester" + }, + "library": { + "no_items_found": "Ingen element funne", + "no_results": "Ingen resultat", + "no_libraries_found": "Ingen bibliotek funne", + "item_types": { + "movies": "filmar", + "series": "seriar", + "boxsets": "samleboksar", + "items": "element" + }, + "options": { + "display": "Visning", + "row": "Rad", + "list": "Liste", + "image_style": "Bildestil", + "poster": "Plakat", + "cover": "Omslag", + "show_titles": "Vis titlar", + "show_stats": "Vis statistikk" + }, + "filters": { + "genres": "Sjanger", + "years": "År", + "sort_by": "Sorter etter", + "sort_order": "Sorteringsrekkjefølgje", + "asc": "Stigande", + "desc": "Søkkande", + "tags": "Taggar" + } + }, + "favorites": { + "series": "Seriar", + "movies": "Filmar", + "episodes": "Episodar", + "videos": "Videoar", + "boxsets": "Samleboksar", + "playlists": "Spelelister", + "noDataTitle": "Ingen favorittar enno", + "noData": "Marker element som favorittar for å sjå dei her for rask tilgang." + }, + "custom_links": { + "no_links": "Ingen lenker" + }, + "player": { + "error": "Feil", + "failed_to_get_stream_url": "Kunne ikkje henta strøyme-URL", + "an_error_occured_while_playing_the_video": "Ein feil oppstod under avspeling av videoen. Sjekk loggane i innstillingane.", + "client_error": "Kundefeil", + "could_not_create_stream_for_chromecast": "Kunne ikkje laga strøyme for Chromecast", + "message_from_server": "Melding frå tenar: {{message}}", + "video_has_finished_playing": "Videoen er ferdig avspelt!", + "no_video_source": "Ingen videokjelde...", + "next_episode": "Neste episode", + "refresh_tracks": "Oppdater spor", + "subtitle_tracks": "Undertekstspor:", + "audio_tracks": "Lydspor:", + "playback_state": "Avspelingstatus:", + "no_data_available": "Ingen data tilgjengelege", + "index": "Indeks:" + }, + "item_card": { + "next_up": "Neste opp", + "no_items_to_display": "Ingen element å visa", + "cast_and_crew": "Skodespelarar & Stab", + "series": "Seriar", + "seasons": "Sesongar", + "season": "Sesong", + "no_episodes_for_this_season": "Ingen episodar for denne sesongen", + "overview": "Oversikt", + "more_with": "Meir med {{name}}", + "similar_items": "Liknande element", + "no_similar_items_found": "Ingen liknande element funne", + "video": "Video", + "more_details": "Meir detaljar", + "quality": "Kvalitet", + "audio": "Lyd", + "subtitles": "Undertekstar", + "show_more": "Vis meir", + "show_less": "Vis mindre", + "appeared_in": "Viste seg i", + "could_not_load_item": "Kunne ikkje lasta element", + "none": "Ingen", + "download": { + "download_season": "Last ned sesong", + "download_series": "Last ned serie", + "download_episode": "Last ned episode", + "download_movie": "Last ned film", + "download_x_item": "Last ned {{item_count}} element", + "download_button": "Last ned", + "using_optimized_server": "Brukar optimalisert tenar", + "using_default_method": "Brukar standard metode" + } + }, + "live_tv": { + "next": "Neste", + "previous": "Førre", + "live_tv": "Direkte TV", + "coming_soon": "Kjem snart", + "on_now": "Visast no", + "shows": "Program", + "movies": "Filmar", + "sports": "Sport", + "for_kids": "For barn", + "news": "Nyheiter" + }, + "jellyseerr": { + "confirm": "Stadfest", + "cancel": "Avbryt", + "yes": "Ja", + "whats_wrong": "Kva er gale?", + "issue_type": "Problemtype", + "select_an_issue": "Vel eit problem", + "types": "Typar", + "describe_the_issue": "(valfritt) Beskriv problemet...", + "submit_button": "Send inn", + "report_issue_button": "Rapporter problem", + "request_button": "Be om", + "are_you_sure_you_want_to_request_all_seasons": "Er du sikker på at du vil be om alle sesongane?", + "failed_to_login": "Kunne ikkje logga inn", + "cast": "Skodespelarar", + "details": "Detaljar", + "status": "Status", + "original_title": "Original tittel", + "series_type": "Serietype", + "release_dates": "Utgivingsdatoar", + "first_air_date": "Første visningsdato", + "next_air_date": "Neste visningsdato", + "revenue": "Inntekter", + "budget": "Budsjett", + "original_language": "Opphavleg språk", + "production_country": "Produksjonsland", + "studios": "Studios", + "network": "Nettverk", + "currently_streaming_on": "Strøymast no på", + "advanced": "Avansert", + "request_as": "Be om som", + "tags": "Taggar", + "quality_profile": "Kvalitetsprofil", + "root_folder": "Rotmappe", + "season_all": "Sesong (alle)", + "season_number": "Sesong {{season_number}}", + "number_episodes": "{{episode_number}} Episodar", + "born": "Fødd", + "appearances": "Opptredenar", + "toasts": { + "jellyseer_does_not_meet_requirements": "Jellyseerr-tenaren oppfyller ikkje minimumskrava! Ver venleg og oppdater til minst versjon 2.0.0", + "jellyseerr_test_failed": "Jellyseerr-testen feila. Ver venleg og prøv igjen.", + "failed_to_test_jellyseerr_server_url": "Kunne ikkje testa jellyseerr-tenaren sin URL", + "issue_submitted": "Problemet er sendt inn!", + "requested_item": "Be om {{item}}!", + "you_dont_have_permission_to_request": "Du har ikkje løyve til å be om!", + "something_went_wrong_requesting_media": "Noko gjekk gale med å be om media!" + } + }, + "tabs": { + "home": "Heim", + "search": "Søk", + "library": "Bibliotek", + "custom_links": "Eigendefinerte lenker", + "favorites": "Favorittar" + } +} diff --git a/translations/ro.json b/translations/ro.json new file mode 100644 index 00000000..19b2f1fe --- /dev/null +++ b/translations/ro.json @@ -0,0 +1,484 @@ +{ + "login": { + "username_required": "Numele de utilizator este obligatoriu", + "error_title": "Eroare", + "login_title": "Conectare", + "login_to_title": "Conectare la", + "username_placeholder": "Utilizator", + "password_placeholder": "Parola", + "login_button": "Conectare", + "quick_connect": "Conectare rapidă", + "enter_code_to_login": "Introduceți codul {{code}} pentru a vă conecta", + "failed_to_initiate_quick_connect": "Eroare la conectarea rapidă", + "got_it": "Am înţeles", + "connection_failed": "Conectare eșuată", + "could_not_connect_to_server": "Nu s-a putut conecta la server. Verificați adresa URL și conexiunea la internet.", + "an_unexpected_error_occured": "A apărut o eroare neașteptată", + "change_server": "Schimba", + "invalid_username_or_password": "Nume de utilizator sau parolă greșită", + "user_does_not_have_permission_to_log_in": "Utilizatorul nu are permisiunea de a se conecta", + "server_is_taking_too_long_to_respond_try_again_later": "Serverul răspunde prea greu, încearcă din nou mai târziu.", + "server_received_too_many_requests_try_again_later": "Serverul a primit prea multe solicitări, încercați din nou mai târziu.", + "there_is_a_server_error": "Eroare de server", + "an_unexpected_error_occured_did_you_enter_the_correct_url": "A apărut o eroare neașteptată. Ați introdus corect adresa URL a serverului?" + }, + "server": { + "enter_url_to_jellyfin_server": "Introduceți adresa URL a serverul dvs. Jellyfin", + "server_url_placeholder": "http(s)://server-dvs.ro", + "connect_button": "Conectare", + "previous_servers": "Servere anterioare", + "clear_button": "Șterge", + "search_for_local_servers": "Caută", + "searching": "Căutare în curs...", + "servers": "Servere" + }, + "home": { + "no_internet": "Fără conexiune", + "no_items": "Niciun articol", + "no_internet_message": "Nicio grijă, poți viziona în continuare conținutul descărcat.", + "go_to_downloads": "Accesați descărcările", + "oops": "Oops!", + "error_message": "Ceva nu e bine.\nAutentificați-vă din nou.", + "continue_watching": "Continuă vizionarea", + "next_up": "Urmează", + "recently_added_in": "Adăugat recent în {{libraryName}}", + "suggested_movies": "Filme sugerate", + "suggested_episodes": "Episoade sugerate", + "intro": { + "welcome_to_streamyfin": "Bun venit la Streamyfin", + "a_free_and_open_source_client_for_jellyfin": "Un client gratuit și open-source pentru Jellyfin.", + "features_title": "Caracteristici", + "features_description": "Streamyfin are o mulțime de funcții și se integrează cu o gamă largă de software pe care le puteți găsi în meniul de setări, printre care:", + "jellyseerr_feature_description": "Conectează-te la Jellyseerr și solicită filme direct în aplicație.", + "downloads_feature_title": "Descărcări", + "downloads_feature_description": "Descarcă filme și seriale pentru a le viziona offline. Folosește fie metoda implicită, fie instalează serverul de optimizare pentru a descărca fișiere în fundal.", + "chromecast_feature_description": "Transmiteți filme și seriale pe dispozitivele dvs. Chromecast.", + "centralised_settings_plugin_title": "Plugin de setări centralizate", + "centralised_settings_plugin_description": "Configurați setările dintr-o locație centralizată pe serverul Jellyfin. Toate setările clientului pentru toți utilizatorii vor fi sincronizate automat.", + "done_button": "Efectuat", + "go_to_settings_button": "Accesați setările", + "read_more": "Citeşte mai mult" + }, + "settings": { + "settings_title": "Setări", + "log_out_button": "Deconectare", + "user_info": { + "user_info_title": "Informații utilizator", + "user": "Utilizator", + "server": "Server", + "token": "Cheie", + "app_version": "Versiune aplicație" + }, + "quick_connect": { + "quick_connect_title": "Conectare rapidă", + "authorize_button": "Autorizare conectare rapidă", + "enter_the_quick_connect_code": "Introduceți codul pt. conectare rapidă...", + "success": "Succes", + "quick_connect_autorized": "Conectare rapidă autorizată", + "error": "Eroare", + "invalid_code": "Cod invalid.", + "authorize": "Autorizează" + }, + "media_controls": { + "media_controls_title": "Controale media", + "forward_skip_length": "Durata saltului înainte", + "rewind_length": "Durata saltului înapoi", + "seconds_unit": "s" + }, + "audio": { + "audio_title": "Audio", + "set_audio_track": "Setează pista audio de la elementul anterior", + "audio_language": "Limba audio", + "audio_hint": "Alege o limbă audio implicită.", + "none": "Niciuna", + "language": "Limba" + }, + "subtitles": { + "subtitle_title": "Subtitrări", + "subtitle_language": "Limbă subtitrări", + "subtitle_mode": "Modalitate subtitrare", + "set_subtitle_track": "Aplică subtitrarea folosită anterior.", + "subtitle_size": "Mărime subtitrare", + "subtitle_hint": "Configurați preferințele pentru subtitrări.", + "none": "Niciuna", + "language": "Limba", + "loading": "Încărcare", + "modes": { + "Default": "Implicit", + "Smart": "Smart", + "Always": "Întotdeauna", + "None": "Niciuna", + "OnlyForced": "OnlyForced" + } + }, + "other": { + "other_title": "Altele", + "follow_device_orientation": "Rotire automată", + "video_orientation": "Orientarea video", + "orientation": "Orientare", + "orientations": { + "DEFAULT": "Implicit", + "ALL": "Toate", + "PORTRAIT": "Portret", + "PORTRAIT_UP": "Portret sus", + "PORTRAIT_DOWN": "Portret jos", + "LANDSCAPE": "Landscape", + "LANDSCAPE_LEFT": "Landscape stânga", + "LANDSCAPE_RIGHT": "Landscape dreapta", + "OTHER": "Altele", + "UNKNOWN": "Necunoscut" + }, + "safe_area_in_controls": "Zona sigură pentru controale", + "video_player": "Player video", + "video_players": { + "VLC_3": "VLC 3", + "VLC_4": "VLC 4 (Experimental + PiP)" + }, + "show_custom_menu_links": "Afișează link-uri personalizate în meniu", + "hide_libraries": "Ascunde bibliotecile", + "select_liraries_you_want_to_hide": "Selectează bibliotecile pe care dorești să le ascunzi din fila Bibliotecă și din secțiunile paginii principale.", + "disable_haptic_feedback": "Dezactivează vibrațiile tactile", + "default_quality": "Calitate implicită", + "max_auto_play_episode_count": "Maxim episoade redare automată", + "disabled": "Dezactivat" + }, + "downloads": { + "downloads_title": "Descărcări", + "download_method": "Metoda de descărcare", + "remux_max_download": "Remux max download", + "auto_download": "Descărcare automată", + "optimized_versions_server": "Optimized versions server", + "save_button": "Salvează", + "optimized_server": "Server optimizat", + "optimized": "Optimizat", + "default": "Implicit", + "optimized_version_hint": "Introduceți adresa URL pentru serverul de optimizare. Adresa URL trebuie să includă http sau https și, opțional, portul.", + "read_more_about_optimized_server": "Citește mai multe despre optimizarea serverului.", + "url": "URL", + "server_url_placeholder": "http(s)://domeniu.org:port" + }, + "plugins": { + "plugins_title": "Plugin-uri", + "jellyseerr": { + "jellyseerr_warning": "Această integrare este în stadii incipiente. Așteptați-vă ca lucrurile să se schimbe.", + "server_url": "Server URL", + "server_url_hint": "Exemplu: http(s)://your-host.url\n(adăugați portul dacă este necesar)", + "server_url_placeholder": "Jellyseerr URL...", + "password": "Parolă", + "password_placeholder": "Introduceți parola pentru utilizatorul Jellyfin {{username}}", + "save_button": "Salvează", + "clear_button": "Șterge", + "login_button": "Login", + "total_media_requests": "Total solicitări media", + "movie_quota_limit": "Limită solicitări filme", + "movie_quota_days": "Limită solicitări zile", + "tv_quota_limit": "Limită solicitări seriale", + "tv_quota_days": "Limită solicitări seriale zile", + "reset_jellyseerr_config_button": "Resetează configurația", + "unlimited": "Nelimitat", + "plus_n_more": "+{{n}} mai mult", + "order_by": { + "DEFAULT": "Implicit", + "VOTE_COUNT_AND_AVERAGE": "Numărul și media voturilor", + "POPULARITY": "Popularitate" + } + }, + "marlin_search": { + "enable_marlin_search": "Activează căutarea Marlin", + "url": "URL", + "server_url_placeholder": "http(s)://domain.org:port", + "marlin_search_hint": "Introduceți adresa URL pentru serverul Marlin. Adresa URL trebuie să includă http sau https și, opțional, portul.", + "read_more_about_marlin": "Citește mai multe despre Marlin.", + "save_button": "Salvează", + "toasts": { + "saved": "Salvat" + } + } + }, + "storage": { + "storage_title": "Memorie", + "app_usage": "App {{usedSpace}}%", + "device_usage": "Dispozitiv {{availableSpace}}%", + "size_used": "{{used}} din {{total}} folosit", + "delete_all_downloaded_files": "Ștergeți toate fișierele descărcate" + }, + "intro": { + "show_intro": "Afișează intro", + "reset_intro": "Resetează intro" + }, + "logs": { + "logs_title": "Loguri", + "export_logs": "Export loguri", + "click_for_more_info": "Apasă pt mai multe informații", + "level": "Nivel", + "no_logs_available": "Niciun log disponibil", + "delete_all_logs": "Șterge toate logurile" + }, + "languages": { + "title": "Limbi", + "app_language": "Limbă aplicație", + "app_language_description": "Selectează limba pt aplicație.", + "system": "Sistem" + }, + "toasts": { + "error_deleting_files": "Eroare la ștergerea fișierelor", + "background_downloads_enabled": "Descărcări în fundal activate", + "background_downloads_disabled": "Descărcări în fundal dezactivate", + "connected": "Conectat", + "could_not_connect": "Nu se poate conecta", + "invalid_url": "URL invalid" + } + }, + "sessions": { + "title": "Sesiuni", + "no_active_sessions": "Nicio sesiune activă" + }, + "downloads": { + "downloads_title": "Descărcări", + "tvseries": "Seriale", + "movies": "Filme", + "queue": "Coadă", + "queue_hint": "Descărcările se vor pierde la repornirea aplicației", + "no_items_in_queue": "Niciun articol în coadă", + "no_downloaded_items": "Niciun element descărcat", + "delete_all_movies_button": "Șterge toate filmele", + "delete_all_tvseries_button": "Șterge toate serialele", + "delete_all_button": "Șterge tot", + "active_download": "Descărcare activă", + "no_active_downloads": "Nicio descărcare activă", + "active_downloads": "Descărcări active", + "new_app_version_requires_re_download": "Noua versiune a aplicației necesită o nouă descărcare.", + "new_app_version_requires_re_download_description": "Noua actualizare necesită descărcarea din nou a conținutului. Vă rugăm să eliminați tot conținutul descărcat și să încercați din nou.", + "back": "Înapoi", + "delete": "Șterge", + "something_went_wrong": "Ceva nu a mers bine.", + "could_not_get_stream_url_from_jellyfin": "Nu s-a putut obține adresa URL a fluxului de la Jellyfin", + "eta": "Estimat {{eta}}", + "methods": "Metode", + "toasts": { + "you_are_not_allowed_to_download_files": "Nu aveți voie să descărcați fișiere.", + "deleted_all_movies_successfully": "Toate filmele au fost șterse cu succes!", + "failed_to_delete_all_movies": "Nu s-au putut șterge toate filmele", + "deleted_all_tvseries_successfully": "Toate serialele au fost șterse cu succes!", + "failed_to_delete_all_tvseries": "Nu s-au putut șterge toate serialele", + "download_cancelled": "Descărcare anulată", + "could_not_cancel_download": "Nu s-a putut anula descărcarea.", + "download_completed": "Descărcare completă", + "download_started_for": "Descărcarea a început pt {{item}}", + "item_is_ready_to_be_downloaded": "{{item}} este gata de descărcat", + "download_stated_for_item": "Descărcarea a început pt {{item}}", + "download_failed_for_item": "Descărcarea a eșuat {{item}} - {{error}}", + "download_completed_for_item": "Descărcare completă pt {{item}}", + "queued_item_for_optimization": "Adăugat {{item}} pt optimizare", + "failed_to_start_download_for_item": "Nu s-a putut începe descărcarea pt {{item}}: {{message}}", + "server_responded_with_status_code": "Serverul a răspuns cu statusul {{statusCode}}", + "no_response_received_from_server": "Niciun răspuns primit de la server", + "error_setting_up_the_request": "Eroare la configurarea solicitării", + "failed_to_start_download_for_item_unexpected_error": "Nu s-a putut începe descărcarea pt {{item}}: Eroare neașteptată", + "all_files_folders_and_jobs_deleted_successfully": "Toate fișierele, folderele și lucrările au fost șterse cu succes", + "an_error_occured_while_deleting_files_and_jobs": "A apărut o eroare la ștergerea fișierelor și a lucrărilor", + "go_to_downloads": "Accesați descărcările" + } + } + }, + "search": { + "search_here": "Caută aici...", + "search": "Caută...", + "x_items": "{{count}} elemente", + "library": "Bibliotecă", + "discover": "Descoperă", + "no_results": "Niciun rezultat", + "no_results_found_for": "Niciun rezultat găsit pt", + "movies": "Filme", + "series": "Seriale", + "episodes": "Episoade", + "collections": "Collecții", + "actors": "Actori", + "request_movies": "Cereri Filme", + "request_series": "Cereri seriale", + "recently_added": "Adăugat recent", + "recent_requests": "Cereri recente", + "plex_watchlist": "Plex Watchlist", + "trending": "Tendințe", + "popular_movies": "Filme populare", + "movie_genres": "Genuri de filme", + "upcoming_movies": "Filme viitoare", + "studios": "Studiouri", + "popular_tv": "Seriale populare", + "tv_genres": "Genuri de seriale", + "upcoming_tv": "Seriale viitoare", + "networks": "Rețele", + "tmdb_movie_keyword": "Cuvinte cheie pentru film TMDB", + "tmdb_movie_genre": "Genul de film TMDB", + "tmdb_tv_keyword": "Cuvinte cheie TMDB seriale", + "tmdb_tv_genre": "Genul TV TMDB", + "tmdb_search": "Căutare TMDB", + "tmdb_studio": "TMDB Studio", + "tmdb_network": "Rețeaua TMDB", + "tmdb_movie_streaming_services": "Servicii de streaming de filme TMDB", + "tmdb_tv_streaming_services": "Servicii de streaming TV TMDB" + }, + "library": { + "no_items_found": "Nu s-au găsit articole", + "no_results": "Niciun rezultat", + "no_libraries_found": "Nu au fost găsite biblioteci", + "item_types": { + "movies": "filme", + "series": "seriale", + "boxsets": "box sets", + "items": "articole" + }, + "options": { + "display": "Afișează", + "row": "Rând", + "list": "Listă", + "image_style": "Stilul imaginii", + "poster": "Poster", + "cover": "Cover", + "show_titles": "Afișează titlurile", + "show_stats": "Afișează statisticile" + }, + "filters": { + "genres": "Genuri", + "years": "Ani", + "sort_by": "Sortează după", + "sort_order": "Ordine de sortare", + "asc": "Crescător", + "desc": "Descrescător", + "tags": "Taguri" + } + }, + "favorites": { + "series": "Seriale", + "movies": "Filme", + "episodes": "Episoade", + "videos": "Videouri", + "boxsets": "Boxsets", + "playlists": "Playlist-uri", + "noDataTitle": "Niciun element favorit", + "noData": "Marcați elementele ca favorite pentru a le vedea aici și pentru acces rapid." + }, + "custom_links": { + "no_links": "Niciun link" + }, + "player": { + "error": "Eroare", + "failed_to_get_stream_url": "Nu s-a putut obține adresa URL a fluxului", + "an_error_occured_while_playing_the_video": "A apărut o eroare la redarea videoclipului. Verificați jurnalele în setări.", + "client_error": "Eroare client", + "could_not_create_stream_for_chromecast": "Nu s-a putut crea un flux pentru Chromecast", + "message_from_server": "Mesaj de la server: {{message}}", + "video_has_finished_playing": "Redarea videoului s-a terminat!", + "no_video_source": "Nicio sursă video...", + "next_episode": "Episodul următor", + "refresh_tracks": "Refresh Tracks", + "subtitle_tracks": "Subtitrări:", + "audio_tracks": "Audio:", + "playback_state": "Stare de redare:", + "no_data_available": "Nu sunt disponibile date", + "index": "Index:", + "continue_watching": "Continuă să vizionezi", + "go_back": "Înapoi" + }, + "item_card": { + "next_up": "Urmează", + "no_items_to_display": "Niciun element de afișat", + "cast_and_crew": "Distribuție și echipă de producție”", + "series": "Seriale", + "seasons": "Sezoane", + "season": "Sezon", + "no_episodes_for_this_season": "Niciun episod pt acest sezon", + "overview": "Prezentare generală", + "more_with": "Mai multe cu {{name}}", + "similar_items": "Articole similare", + "no_similar_items_found": "Nu s-au găsit articole similare", + "video": "Video", + "more_details": "Mai multe detalii", + "quality": "Calitate", + "audio": "Audio", + "subtitles": "Subtitrare", + "show_more": "Arată mai mult", + "show_less": "Arată mai puțin", + "appeared_in": "Apare în", + "could_not_load_item": "Nu s-a putut încărca elementul", + "none": "Nimic", + "download": { + "download_season": "Descărcați sezonul", + "download_series": "Descărcați serialul", + "download_episode": "Descărcați episodul", + "download_movie": "Descărcați filmul", + "download_x_item": "Descărcați {{item_count}} articole", + "download_button": "Descarcă", + "using_optimized_server": "Utilizeză server optimizat", + "using_default_method": "Utilizeză metoda implicită" + } + }, + "live_tv": { + "next": "Următorul", + "previous": "Anterior", + "live_tv": "Live TV", + "coming_soon": "În curând", + "on_now": "Acum", + "shows": "Emisiuni", + "movies": "Filme", + "sports": "Sport", + "for_kids": "Pt copii", + "news": "Știri" + }, + "jellyseerr": { + "confirm": "Confirmă", + "cancel": "Anulează", + "yes": "Da", + "whats_wrong": "Ce s-a întâmplat?", + "issue_type": "Tipul problemei", + "select_an_issue": "Selectați o problemă", + "types": "Tipuri", + "describe_the_issue": "(opțional) Descrieți problema...", + "submit_button": "Trimite", + "report_issue_button": "Raportează o problemă", + "request_button": "Cere", + "are_you_sure_you_want_to_request_all_seasons": "Sigur vrei să ceri toate sezoanele?", + "failed_to_login": "Nu s-a putut conecta", + "cast": "Distribuție", + "details": "Detalii", + "status": "Stare", + "original_title": "Titlu original", + "series_type": "Genul serialului", + "release_dates": "Date de lansare", + "first_air_date": "Prima dată de difuzare", + "next_air_date": "Următoarea dată de difuzare", + "revenue": "Venituri", + "budget": "Buget", + "original_language": "Limba originală", + "production_country": "Țara de producție", + "studios": "Studiouri", + "network": "Rețea", + "currently_streaming_on": "În prezent difuzat pe", + "advanced": "Avansat", + "request_as": "Cere ca", + "tags": "Taguri", + "quality_profile": "Profil calitate", + "root_folder": "Dosarul rădăcină", + "season_all": "Sezoane (toate)", + "season_number": "Sezon {{season_number}}", + "number_episodes": "{{episode_number}} Episoade", + "born": "Născut", + "appearances": "Apariții", + "toasts": { + "jellyseer_does_not_meet_requirements": "Serverul Jellyseerr nu îndeplinește cerințele minime de versiune! Vă rugăm să actualizați cel puțin la versiunea 2.0.0", + "jellyseerr_test_failed": "Testul Jellyseerr a eșuat. Vă rugăm să încercați din nou.", + "failed_to_test_jellyseerr_server_url": "Nu s-a putut testa adresa URL a serverului jellyseerr.", + "issue_submitted": "Problemă trimisă!", + "requested_item": "Cerere {{item}}!", + "you_dont_have_permission_to_request": "Nu aveți permisiunea de a face cereri!", + "something_went_wrong_requesting_media": "Ceva nu a mers bine la cererea fișierelor media!" + } + }, + "tabs": { + "home": "Acasă", + "search": "Caută", + "library": "Bibiliotecă", + "custom_links": "Linkuri personalizate", + "favorites": "Favorite" + } +} diff --git a/translations/sq.json b/translations/sq.json new file mode 100644 index 00000000..51bf7f6f --- /dev/null +++ b/translations/sq.json @@ -0,0 +1,480 @@ +{ + "login": { + "username_required": "Emri i përdoruesit është i detyrueshëm", + "error_title": "Gabim", + "login_title": "Hyrje", + "login_to_title": "Hyrje në", + "username_placeholder": "Emri i përdoruesit", + "password_placeholder": "Fjalëkalimi", + "login_button": "Hyr", + "quick_connect": "Lidhje e shpejtë", + "enter_code_to_login": "Shkruani fajlëkalimin {{code}} për kyçje", + "failed_to_initiate_quick_connect": "Dështojë niset lidhja e shpejtë", + "got_it": "E kuptova", + "connection_failed": "Lidhja dështojë", + "could_not_connect_to_server": "Nuk u arrit të lidhej me serverin. Ju lutem kontrolloni URL-në dhe lidhjen tuaj të rrjetit.", + "an_unexpected_error_occured": "Ndodhi një gabim i papritur", + "change_server": "Ndrysho serverin", + "invalid_username_or_password": "Emër përdoruesi ose fjalëkalimi i pavlefshëm", + "user_does_not_have_permission_to_log_in": "Përdoruesi nuk ka leje për kyçje", + "server_is_taking_too_long_to_respond_try_again_later": "Serveri po merr shumë kohë për të reaguar, provo përsëri më vonë", + "server_received_too_many_requests_try_again_later": "Serveri mori shumë kërkesa, provo përsëri më vonë", + "there_is_a_server_error": "Ka një gabim në server", + "an_unexpected_error_occured_did_you_enter_the_correct_url": "Ndodhi një gabim i papritur. A e keni shkruar URL-në e serverit saktë?" + }, + "server": { + "enter_url_to_jellyfin_server": "Shruajeni URL-në e Jellyfin serverit tuaj", + "server_url_placeholder": "http(s)://serveri-juaj.com", + "connect_button": "Lidhu", + "previous_servers": "Serverat e mëparshëm", + "clear_button": "Pastro", + "search_for_local_servers": "Kërko për servera lokalë", + "searching": "Duke kërkuar...", + "servers": "Servera" + }, + "home": { + "no_internet": "Nuk ka qasje ne rrjet", + "no_items": "Nuk ka elemente", + "no_internet_message": "Mos u shqetëso, akoma mund të shikosh\npërmbajtjen e shkarkuar.", + "go_to_downloads": "Shko te shkarkimet", + "oops": "Ups!", + "error_message": "Diçka shkoj keq.\nJu lutemi dilni dhe hyni përsëri.", + "continue_watching": "Vazhdo shikimin", + "next_up": "I ardhshëm", + "recently_added_in": "Shtuar kohët e fundit në {{libraryName}}", + "suggested_movies": "Filma të sugjeruar", + "suggested_episodes": "Episodat të sugjeruara", + "intro": { + "welcome_to_streamyfin": "Mirë se vini në Streamyfin", + "a_free_and_open_source_client_for_jellyfin": "Një klient falas dhe me burim të hapur për Jellyfin.", + "features_title": "Karakteristikat", + "features_description": "Streamyfin ka shumë funksione dhe integron me shumë software tjera, të cilët mund ti gjeni në menynë e cilësimeve, përfshirë këto:", + "jellyseerr_feature_description": "Lidhu me instancën tuaj të Jellyseerr dhe kërko filma direkt në aplikacionin.", + "downloads_feature_title": "Shkarkime", + "downloads_feature_description": "Shkarkoni filma dhe seriale për shikim offline. Përdorni ose metodën standarde ose instaloni serverin e optimizuar për të shkarkuar skedarët në sfond.", + "chromecast_feature_description": "Dërgo filma dhe seriale në pajisjet tuaja Chromecast.", + "centralised_settings_plugin_title": "Plugin i Cilësimeve Qendrore", + "centralised_settings_plugin_description": "Konfiguroni cilësimet nga një vendndodhje qendrore në Jellyfin serverin tuaj. Të gjitha cilësimet e klientit për të gjithë përdoruesit do të sinkronizohen automatikisht.", + "done_button": "Kryer", + "go_to_settings_button": "Shko te cilësimet", + "read_more": "Lexo më shumë" + }, + "settings": { + "settings_title": "Cilësimet", + "log_out_button": "Dil", + "user_info": { + "user_info_title": "Informacion përdoruesi", + "user": "Përdoruesi", + "server": "Server", + "token": "Token", + "app_version": "Versioni i aplikacionit" + }, + "quick_connect": { + "quick_connect_title": "Lidhje e shpejtë", + "authorize_button": "Autorizo Lidhjen e shpejtë", + "enter_the_quick_connect_code": "Shkruajni kodin e lidhjes së shpejtë...", + "success": "Sukses", + "quick_connect_autorized": "Lidhja e shpejtë është autorizuar", + "error": "Gabim", + "invalid_code": "Kod i pavlefshëm", + "authorize": "Autorizo" + }, + "media_controls": { + "media_controls_title": "Kontrollet e medias", + "forward_skip_length": "Gjatësia e kalimit përpara", + "rewind_length": "Gjatësia e rikthimit", + "seconds_unit": "s" + }, + "audio": { + "audio_title": "Audio", + "set_audio_track": "Vendos shtigjet e audios nga elementi i mëparshëm", + "audio_language": "Gjuha e audios", + "audio_hint": "Zgjidhni gjuhën standarde per audio.", + "none": "Asnjëra", + "language": "Gjuha" + }, + "subtitles": { + "subtitle_title": "Nëntekstet", + "subtitle_language": "Gjuha e nënteksteve", + "subtitle_mode": "Mënyra e nënteksteve", + "set_subtitle_track": "Vendos nëntekstin nga elementi i mëparshëm", + "subtitle_size": "Madhësia e nënteksteve", + "subtitle_hint": "Konfiguro preferencën e nënteksteve.", + "none": "Asnjëra", + "language": "Gjuha", + "loading": "Duke u ngarkuar", + "modes": { + "Default": "Standard", + "Smart": "Smart", + "Always": "Gjithmonë", + "None": "Asnjëra", + "OnlyForced": "Vetëm të detyruara" + } + }, + "other": { + "other_title": "Tjetër", + "follow_device_orientation": "Auto rrotullimi", + "video_orientation": "Orientimi i videos", + "orientation": "Orientimi", + "orientations": { + "DEFAULT": "Standard", + "ALL": "Të gjitha", + "PORTRAIT": "Portret", + "PORTRAIT_UP": "Portret lart", + "PORTRAIT_DOWN": "Portret poshtë", + "LANDSCAPE": "Peizazh", + "LANDSCAPE_LEFT": "Peizazh nga e majta", + "LANDSCAPE_RIGHT": "Peizazh nga e djathta", + "OTHER": "Tjetër", + "UNKNOWN": "E panjohur" + }, + "safe_area_in_controls": "Zonë e sigurt në kontrolla", + "video_player": "Video lexues", + "video_players": { + "VLC_3": "VLC 3", + "VLC_4": "VLC 4 (Eksperimentale + PiP)" + }, + "show_custom_menu_links": "Shfaq lidhje menuje të personalizuara", + "hide_libraries": "Fsheh bibliotekat", + "select_liraries_you_want_to_hide": "Zgjidhni bibliotekat që dëshironi të fshehni nga skeda e Bibliotekut dhe seksionet e faqes kryesore.", + "disable_haptic_feedback": "Deaktivizo reagimin haptik", + "default_quality": "Kvaliteti standard" + }, + "downloads": { + "downloads_title": "Shkarkime", + "download_method": "Metoda e shkarkimit", + "remux_max_download": "Remux maks shkarkimi", + "auto_download": "Shkarkim automatik", + "optimized_versions_server": "Serveri i versioneve të optimizuara", + "save_button": "Ruaj", + "optimized_server": "Server i optimizuar", + "optimized": "I optimizuar", + "default": "Standard", + "optimized_version_hint": "Vendos URL-në për serverin e optimizimit. URL-ja duhet të përfshijë http ose https dhe opsionalisht portën.", + "read_more_about_optimized_server": "Lexo më shumë rreth serverit të optimizimit.", + "url": "URL", + "server_url_placeholder": "http(s)://domain.org:port" + }, + "plugins": { + "plugins_title": "Shtojca", + "jellyseerr": { + "jellyseerr_warning": "Ky integrim është në fazat e para. Prisni ndryshime.", + "server_url": "URL e serverit", + "server_url_hint": "Shembull: http(s)://hosti-juaj.url\n(shtoni portën nëse është e nevojshme)", + "server_url_placeholder": "URL e Jellyseerr...", + "password": "Fjalëkalimi", + "password_placeholder": "Vendos fjalëkalimin për përdoruesin Jellyfin {{username}}", + "save_button": "Ruaj", + "clear_button": "Pastro", + "login_button": "Hyr", + "total_media_requests": "Totali i kërkesave për media", + "movie_quota_limit": "Limiti i kuotës për filma", + "movie_quota_days": "Ditët e kuotës për filma", + "tv_quota_limit": "Limiti i kuotës për TV", + "tv_quota_days": "Ditët e kuotës për TV", + "reset_jellyseerr_config_button": "Rivendos konfigurimin e Jellyseerr", + "unlimited": "I pakufizuar", + "plus_n_more": "+{{n}} më shumë", + "order_by": { + "DEFAULT": "Standard", + "VOTE_COUNT_AND_AVERAGE": "Numri i votave dhe mesatarja", + "POPULARITY": "Popullaritet" + } + }, + "marlin_search": { + "enable_marlin_search": "Aktivizo kërkimin Marlin", + "url": "URL", + "server_url_placeholder": "http(s)://domain.org:port", + "marlin_search_hint": "Vendos URL-në për serverin Marlin. URL-ja duhet të përfshijë http ose https dhe opsionalisht portën.", + "read_more_about_marlin": "Lexo më shumë rreth Marlin.", + "save_button": "Ruaj", + "toasts": { + "saved": "U ruajt" + } + } + }, + "storage": { + "storage_title": "Hapësira", + "app_usage": "Aplikacioni {{usedSpace}}%", + "device_usage": "Pajisja {{availableSpace}}%", + "size_used": "{{used}} prej {{total}} të përdorura", + "delete_all_downloaded_files": "Fshijë të gjitha skedarët e shkarkuar" + }, + "intro": { + "show_intro": "Shfaq prezantimin", + "reset_intro": "Rivendos prezantimin" + }, + "logs": { + "logs_title": "Regjistri", + "export_logs": "Eksporto regjistrin", + "click_for_more_info": "Kliko për më shumë informacion", + "level": "Nivele", + "no_logs_available": "Nuk ka regjistrime të disponueshme", + "delete_all_logs": "Fshijë të gjitha regjistrimet" + }, + "languages": { + "title": "Gjuhët", + "app_language": "Gjuha e aplikacionit", + "app_language_description": "Zgjidhni gjuhën për aplikacionin.", + "system": "Sistemi" + }, + "toasts": { + "error_deleting_files": "Gabim gjatë fshirjes së skedarëve", + "background_downloads_enabled": "Shkarkimet në sfond aktivizuar", + "background_downloads_disabled": "Shkarkimet në sfond deaktivizuar", + "connected": "Lidhur", + "could_not_connect": "Nuk u mundet te vendoset kyqja", + "invalid_url": "URL i pavlefshme" + } + }, + "sessions": { + "title": "Sesione", + "no_active_sessions": "Nuk sesione aktive" + }, + "downloads": { + "downloads_title": "Shkarkimet", + "tvseries": "Seriale TV", + "movies": "Filma", + "queue": "Rradhë", + "queue_hint": "Rradhat dhe shkarkimet do të humbasin pas genstartit të aplikacionit", + "no_items_in_queue": "Nuk ka elemente në rradhë", + "no_downloaded_items": "Nuk ka shkarkime", + "delete_all_movies_button": "Fshijë të gjithë filmat", + "delete_all_tvseries_button": "Fshijë të gjitha serialet TV", + "delete_all_button": "Fshijë të gjitha", + "active_download": "Shkarkim aktiv", + "no_active_downloads": "Nuk ka shkarkime aktive", + "active_downloads": "Shkarkime aktive", + "new_app_version_requires_re_download": "Versioni i ri i aplikacionit kërkon shkarkim të ri", + "new_app_version_requires_re_download_description": "Përditësimi i ri kërkon që përmbajtja të shkarkohet përsëri. Ju lutem fshini të gjithë përmbajtjen e shkarkuar dhe provoni përsëri.", + "back": "Kthehu", + "delete": "Fshijë", + "something_went_wrong": "Diçka shkoj keq", + "could_not_get_stream_url_from_jellyfin": "Nuk u arrit të merrej URL-ja e transmetimit nga Jellyfin", + "eta": "ETA {{eta}}", + "methods": "Metodat", + "toasts": { + "you_are_not_allowed_to_download_files": "Nuk keni të drejtë të shkarkoni skedarë.", + "deleted_all_movies_successfully": "Të gjithë filmat u fshinë me sukses!", + "failed_to_delete_all_movies": "Dështojë fshirja e të gjithë filmave", + "deleted_all_tvseries_successfully": "Të gjitha serialet TV u fshinë me sukses!", + "failed_to_delete_all_tvseries": "Dështojë fshirja e të gjitha serialeve TV", + "download_cancelled": "Shkarkimi u anulua", + "could_not_cancel_download": "Nuk mundet të anulohet shkarkimi", + "download_completed": "Shkarkimi u përfundua", + "download_started_for": "Shkarkimi filloi për {{item}}", + "item_is_ready_to_be_downloaded": "{{item}} është gati për shkarkim", + "download_stated_for_item": "Shkarkimi filloi për {{item}}", + "download_failed_for_item": "Shkarkimi për {{item}} dështoj - {{error}}", + "download_completed_for_item": "Shkarkimi për {{item}} u përfundua", + "queued_item_for_optimization": "{{item}} u shtua në radhë për optimizim", + "failed_to_start_download_for_item": "Dështoijë fillimi i shkarkimit për {{item}}: {{message}}", + "server_responded_with_status_code": "Serveri u përgjigj me statusin {{statusCode}}", + "no_response_received_from_server": "Nuk u mor asnjë përgjigje nga serveri", + "error_setting_up_the_request": "Gabim gjatë konfigurimit të kërkesës", + "failed_to_start_download_for_item_unexpected_error": "Dështoj fillimi i shkarkimit për {{item}}: Gabim i papritur", + "all_files_folders_and_jobs_deleted_successfully": "Të gjitha skedarët, dosjet dhe detyrat u fshinë me sukses", + "an_error_occured_while_deleting_files_and_jobs": "Ndodhi një gabim gjatë fshirjes së skedarëve dhe detyrave", + "go_to_downloads": "Shko te shkarkimet" + } + } + }, + "search": { + "search_here": "Kërko këtu...", + "search": "Kërko...", + "x_items": "{{count}} elemente", + "library": "Bibliotekë", + "discover": "Zbulo", + "no_results": "Nuk ka rezultate", + "no_results_found_for": "Nuk u gjetën rezultate për", + "movies": "Filma", + "series": "Seriale", + "episodes": "Epizoda", + "collections": "Koleksione", + "actors": "Aktoret", + "request_movies": "Kërko filma", + "request_series": "Kërko seriale", + "recently_added": "Shtuar së fundmi", + "recent_requests": "Kërkesa të reja", + "plex_watchlist": "Lista a deshirave Plex", + "trending": "Në trend", + "popular_movies": "Filma popullorë", + "movie_genres": "Zhanre të filmave", + "upcoming_movies": "Filma që vijnë", + "studios": "Studio", + "popular_tv": "TV popullore", + "tv_genres": "Zhanre të TV-së", + "upcoming_tv": "TV e ardhshme", + "networks": "Rrjetet", + "tmdb_movie_keyword": "Fjalë kyçe për filma TMDB", + "tmdb_movie_genre": "Zhanri i filmave TMDB", + "tmdb_tv_keyword": "Fjalë kyçe për TV TMDB", + "tmdb_tv_genre": "Zhanri i TV-së TMDB", + "tmdb_search": "Kërkim TMDB", + "tmdb_studio": "Studio TMDB", + "tmdb_network": "Rrjet TMDB", + "tmdb_movie_streaming_services": "Shërbimet e transmetimit të filmave TMDB", + "tmdb_tv_streaming_services": "Shërbimet e transmetimit të TV-së TMDB" + }, + "library": { + "no_items_found": "Nuk u gjetën elemente", + "no_results": "Nuk ka rezultate", + "no_libraries_found": "Nuk u gjetur biblioteka", + "item_types": { + "movies": "filma", + "series": "seriale", + "boxsets": "box set", + "items": "elemente" + }, + "options": { + "display": "Shfaqje", + "row": "Rresht", + "list": "Listë", + "image_style": "Stili i fotos", + "poster": "Poster", + "cover": "Kopertin", + "show_titles": "Trego titujt", + "show_stats": "Trego statistikat" + }, + "filters": { + "genres": "Zhanre", + "years": "Vite", + "sort_by": "Rendit sipas", + "sort_order": "Rendi i renditjes", + "asc": "Rritësisht", + "desc": "Zbritësisht", + "tags": "Etiketa" + } + }, + "favorites": { + "series": "Seriale", + "movies": "Filma", + "episodes": "Epizoda", + "videos": "Video", + "boxsets": "set kutish", + "playlists": "Playliste", + "noDataTitle": "Ende nuk keni të preferuar", + "noData": "Shejzoj elementet si të preferuara për ti pasër këtu për qasje të shpejtë." + }, + "custom_links": { + "no_links": "Asnjë lidhje" + }, + "player": { + "error": "Gabim", + "failed_to_get_stream_url": "Dështojë çasja e URL-së së transmetimit", + "an_error_occured_while_playing_the_video": "Ndodhi një gabim gjatë shfaqjes së videos. Kontrolloni regjistrat në cilësimet.", + "client_error": "Gabim i klientit", + "could_not_create_stream_for_chromecast": "Nuk u arrit të krijohej transmetimi për Chromecast", + "message_from_server": "Mesazh nga serveri: {{message}}", + "video_has_finished_playing": "Videoja ka përfunduar shfaqjen!", + "no_video_source": "Asnjë burim video...", + "next_episode": "Epizoda e ardhshme", + "refresh_tracks": "Rifresko shtigjet", + "subtitle_tracks": "Shtigjet e nënteksteve:", + "audio_tracks": "Shtigjet audio:", + "playback_state": "Gjendja e rishikimit:", + "no_data_available": "Nuk ka të dhëna të disponueshme", + "index": "Indeksi:" + }, + "item_card": { + "next_up": "E ardhshme", + "no_items_to_display": "Nuk ka elemente për të shfaqur", + "cast_and_crew": "Aktorët & ekipi", + "series": "Seriale", + "seasons": "Sezone", + "season": "Sezon", + "no_episodes_for_this_season": "Nuk ka episode për këtë sezon", + "overview": "Përmbledhje", + "more_with": "Më shumë me {{name}}", + "similar_items": "Elemente të ngjashme", + "no_similar_items_found": "Nuk u gjetën elemente të ngjashme", + "video": "Video", + "more_details": "Më shumë detaje", + "quality": "Kvalitet", + "audio": "Audio", + "subtitles": "Nëntekste", + "show_more": "Trego më shumë", + "show_less": "Trego më pak", + "appeared_in": "Moren pjesë në", + "could_not_load_item": "Nuk u arrit ngarkimi i elementit", + "none": "Asnjë", + "download": { + "download_season": "Shkarko sezonin", + "download_series": "Shkarko serialin", + "download_episode": "Shkarko epizodën", + "download_movie": "Shkarko filmin", + "download_x_item": "Shkarko {{item_count}} elemente", + "download_button": "Shkarko", + "using_optimized_server": "Duke përdorur serverin e optimizuar", + "using_default_method": "Duke përdorur metodën standarde" + } + }, + "live_tv": { + "next": "E ardhmja", + "previous": "E para", + "live_tv": "TV e drejtpërdrejtë", + "coming_soon": "Së shpejti", + "on_now": "Tani", + "shows": "Shou", + "movies": "Filma", + "sports": "Sport", + "for_kids": "Për fëmijë", + "news": "Lajme" + }, + "jellyseerr": { + "confirm": "Konfirmo", + "cancel": "Anullo", + "yes": "Po", + "whats_wrong": "Çka është problemi?", + "issue_type": "Lloji i problemit", + "select_an_issue": "Zgjidh një problem", + "types": "Lloje", + "describe_the_issue": "(opsionale) Përshkruaj problemin...", + "submit_button": "Dërgo", + "report_issue_button": "Raporto problemin", + "request_button": "Kërko", + "are_you_sure_you_want_to_request_all_seasons": "A jeni të sigurt se dëshironi të kërkoni të gjitha sezonat?", + "failed_to_login": "Hyrja dështojë", + "cast": "Aktorët", + "details": "Detaje", + "status": "Status", + "original_title": "Titulli origjinal", + "series_type": "Lloji i serialit", + "release_dates": "Datat e publikimit", + "first_air_date": "Data e parë e transmetimit", + "next_air_date": "Data e ardhshme e transmetimit", + "revenue": "Të ardhurat", + "budget": "Buxheti", + "original_language": "Gjuha origjinale", + "production_country": "Vendi i prodhimit", + "studios": "Studiot", + "network": "Rrjeti", + "currently_streaming_on": "Aktualisht transmetohet në", + "advanced": "Avancuar", + "request_as": "Kërko si", + "tags": "Etiketa", + "quality_profile": "Profili i kvalitetit", + "root_folder": "Dosja kryesore", + "season_all": "Sezon (të gjitha)", + "season_number": "Sezoni {{season_number}}", + "number_episodes": "{{episode_number}} episode", + "born": "Lindur", + "appearances": "Shfaqjet", + "toasts": { + "jellyseer_does_not_meet_requirements": "Serveri Jellyseerr nuk plotëson kërkesat minimale! Ju lutemi përditësoni në të paktën versionin 2.0.0", + "jellyseerr_test_failed": "Testi i Jellyseerr dështojë. Provoni përsëri.", + "failed_to_test_jellyseerr_server_url": "Dështojë testimi i URL-së së serverit Jellyseerr", + "issue_submitted": "Problemi u dërgua!", + "requested_item": "Kërkimi për {{item}} u dërgua!", + "you_dont_have_permission_to_request": "Nuk keni leje për të kërkuar!", + "something_went_wrong_requesting_media": "Diçka shkoj keq gjatë kërkimit të medias!" + } + }, + "tabs": { + "home": "Shtëpi", + "search": "Kërkim", + "library": "Bibliotekë", + "custom_links": "Lidhje të personalizuara", + "favorites": "Preferuarat" + } +} 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/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/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 670d9f89..d0686edd 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 @@ -7,28 +5,7 @@ import DeviceInfo from "react-native-device-info"; */ import MediaTypes from "../../constants/MediaTypes"; -// Helper function to detect Dolby Vision support -const supportsDolbyVision = async () => { - if (Platform.OS === "ios") { - const deviceModel = await DeviceInfo.getModel(); - // iPhone 12 and newer generally support Dolby Vision - const modelNumber = Number.parseInt(deviceModel.replace(/iPhone/, ""), 10); - return !Number.isNaN(modelNumber) && modelNumber >= 12; - } - - if (Platform.OS === "android") { - const apiLevel = await DeviceInfo.getApiLevel(); - const isHighEndDevice = - (await DeviceInfo.getTotalMemory()) > 4 * 1024 * 1024 * 1024; // >4GB RAM - // Very rough approximation - Android 10+ on higher-end devices may support it - return apiLevel >= 29 && isHighEndDevice; - } - - return false; -}; - export const generateDeviceProfile = async () => { - const dolbyVisionSupported = await supportsDolbyVision(); /** * Device profile for Native video player */ @@ -51,7 +28,12 @@ export const generateDeviceProfile = async () => { Value: "153", IsRequired: false, }, - // We'll add Dolby Vision condition below if not supported + { + Condition: "NotEquals", + Property: "VideoRangeType", + Value: "DOVI", //no dolby vision at all + IsRequired: true, + }, ], }, { @@ -172,22 +154,6 @@ export const generateDeviceProfile = async () => { ], }; - // Add Dolby Vision restriction if not supported - if (!dolbyVisionSupported) { - const hevcProfile = profile.CodecProfiles.find( - (p) => p.Type === MediaTypes.Video && p.Codec.includes("hevc"), - ); - - if (hevcProfile) { - hevcProfile.Conditions.push({ - Condition: "NotEquals", - Property: "VideoRangeType", - Value: "DOVI", //no dolby vision at all - IsRequired: true, - }); - } - } - return profile; };