diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100644 index 00000000..c27d8893 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1 @@ +lint-staged diff --git a/.vscode/settings.json b/.vscode/settings.json index c7cdc61f..b200b485 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,24 +1,24 @@ { - "[javascript]": { - "editor.defaultFormatter": "biomejs.biome", - "editor.formatOnSave": true - }, - "[typescriptreact]": { - "editor.defaultFormatter": "biomejs.biome", - "editor.formatOnSave": true - }, - "prettier.printWidth": 120, - "[swift]": { - "editor.defaultFormatter": "sswg.swift-lang" - }, - "editor.formatOnSave": true, - "editor.defaultFormatter": "biomejs.biome", - "[typescript]": { - "editor.defaultFormatter": "biomejs.biome", - "editor.formatOnSave": true - }, - "[javascriptreact]": { - "editor.defaultFormatter": "biomejs.biome", - "editor.formatOnSave": true - } + "[javascript]": { + "editor.defaultFormatter": "biomejs.biome", + "editor.formatOnSave": true + }, + "[typescriptreact]": { + "editor.defaultFormatter": "biomejs.biome", + "editor.formatOnSave": true + }, + "prettier.printWidth": 120, + "[swift]": { + "editor.defaultFormatter": "sswg.swift-lang" + }, + "editor.formatOnSave": true, + "editor.defaultFormatter": "biomejs.biome", + "[typescript]": { + "editor.defaultFormatter": "biomejs.biome", + "editor.formatOnSave": true + }, + "[javascriptreact]": { + "editor.defaultFormatter": "biomejs.biome", + "editor.formatOnSave": true + } } diff --git a/app/(auth)/(tabs)/(custom-links)/_layout.tsx b/app/(auth)/(tabs)/(custom-links)/_layout.tsx index c270b95d..a580146f 100644 --- a/app/(auth)/(tabs)/(custom-links)/_layout.tsx +++ b/app/(auth)/(tabs)/(custom-links)/_layout.tsx @@ -1,13 +1,13 @@ -import {Stack} from "expo-router"; -import { Platform } from "react-native"; +import { Stack } from "expo-router"; import { useTranslation } from "react-i18next"; +import { Platform } from "react-native"; export default function CustomMenuLayout() { const { t } = useTranslation(); return ( { try { const response = await api?.axiosInstance.get( - api?.basePath + "/web/config.json" + api?.basePath + "/web/config.json", ); const config = response?.data; @@ -46,7 +46,7 @@ export default function menuLinks() { }, []); return ( } + iconAfter={} /> )} @@ -76,8 +76,10 @@ export default function menuLinks() { /> )} ListEmptyComponent={ - - {t("custom_links.no_links")} + + + {t("custom_links.no_links")} + } /> diff --git a/app/(auth)/(tabs)/(favorites)/_layout.tsx b/app/(auth)/(tabs)/(favorites)/_layout.tsx index b408eab6..ba0844d8 100644 --- a/app/(auth)/(tabs)/(favorites)/_layout.tsx +++ b/app/(auth)/(tabs)/(favorites)/_layout.tsx @@ -1,14 +1,14 @@ import { nestedTabPageScreenOptions } from "@/components/stacks/NestedTabPageStack"; import { Stack } from "expo-router"; -import { Platform } from "react-native"; import { useTranslation } from "react-i18next"; +import { Platform } from "react-native"; export default function SearchLayout() { const { t } = useTranslation(); return ( } @@ -28,7 +28,7 @@ export default function favorites() { paddingBottom: 16, }} > - + diff --git a/app/(auth)/(tabs)/(home)/_layout.tsx b/app/(auth)/(tabs)/(home)/_layout.tsx index dee024fd..0d533ac9 100644 --- a/app/(auth)/(tabs)/(home)/_layout.tsx +++ b/app/(auth)/(tabs)/(home)/_layout.tsx @@ -1,12 +1,12 @@ import { nestedTabPageScreenOptions } from "@/components/stacks/NestedTabPageStack"; -import { Ionicons, Feather } from "@expo/vector-icons"; +import { Feather, Ionicons } from "@expo/vector-icons"; import { Stack, useRouter } from "expo-router"; -import { Platform, TouchableOpacity, View } from "react-native"; import { useTranslation } from "react-i18next"; +import { Platform, TouchableOpacity, View } from "react-native"; 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 { useSessions, useSessionsProps } from "@/hooks/useSessions"; +import { useAtom } from "jotai"; export default function IndexLayout() { const router = useRouter(); @@ -16,7 +16,7 @@ export default function IndexLayout() { return ( ( - + {!Platform.isTV && ( <> - {user && user.Policy?.IsAdministrator && ( - - )} + {user && user.Policy?.IsAdministrator && } )} @@ -43,61 +41,61 @@ export default function IndexLayout() { }} /> ))} { router.push("/(auth)/settings"); }} > - + ); }; @@ -145,9 +143,9 @@ const SessionsButton = () => { router.push("/(auth)/sessions"); }} > - + diff --git a/app/(auth)/(tabs)/(home)/downloads/[seriesId].tsx b/app/(auth)/(tabs)/(home)/downloads/[seriesId].tsx index e9c95657..59d111f4 100644 --- a/app/(auth)/(tabs)/(home)/downloads/[seriesId].tsx +++ b/app/(auth)/(tabs)/(home)/downloads/[seriesId].tsx @@ -1,16 +1,16 @@ import { Text } from "@/components/common/Text"; -import { useDownload } from "@/providers/DownloadProvider"; -import { router, useLocalSearchParams, useNavigation } from "expo-router"; -import React, { useCallback, useEffect, useMemo, useState } from "react"; -import { ScrollView, TouchableOpacity, View, Alert } from "react-native"; import { EpisodeCard } from "@/components/downloads/EpisodeCard"; -import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; import { SeasonDropdown, - SeasonIndexState, + type SeasonIndexState, } 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(); @@ -21,7 +21,7 @@ export default function page() { }; const [seasonIndexState, setSeasonIndexState] = useState( - {} + {}, ); const { downloadedFiles, deleteItems } = useDownload(); @@ -31,7 +31,7 @@ export default function page() { downloadedFiles ?.filter((f) => f.item.SeriesId == seriesId) ?.sort( - (a, b) => a?.item.ParentIndexNumber! - b.item.ParentIndexNumber! + (a, b) => a?.item.ParentIndexNumber! - b.item.ParentIndexNumber!, ) || [] ); } catch { @@ -64,7 +64,7 @@ export default function page() { () => Object.values(groupBySeason)?.[0]?.ParentIndexNumber ?? series?.[0]?.item?.ParentIndexNumber, - [groupBySeason] + [groupBySeason], ); useEffect(() => { @@ -92,14 +92,14 @@ export default function page() { onPress: () => deleteItems(groupBySeason), style: "destructive", }, - ] + ], ); }, [groupBySeason]); return ( - + {series.length > 0 && ( - + s.item)} @@ -112,17 +112,17 @@ export default function page() { })); }} /> - - {groupBySeason.length} + + {groupBySeason.length} - + - + )} - + {groupBySeason.map((episode, index) => ( ))} diff --git a/app/(auth)/(tabs)/(home)/downloads/index.tsx b/app/(auth)/(tabs)/(home)/downloads/index.tsx index 51991f1b..52c94c06 100644 --- a/app/(auth)/(tabs)/(home)/downloads/index.tsx +++ b/app/(auth)/(tabs)/(home)/downloads/index.tsx @@ -1,28 +1,28 @@ +import { Button } from "@/components/Button"; import { Text } from "@/components/common/Text"; import { ActiveDownloads } from "@/components/downloads/ActiveDownloads"; +import { DownloadSize } from "@/components/downloads/DownloadSize"; import { MovieCard } from "@/components/downloads/MovieCard"; import { SeriesCard } from "@/components/downloads/SeriesCard"; -import { DownloadedItem, useDownload } from "@/providers/DownloadProvider"; +import { type DownloadedItem, useDownload } from "@/providers/DownloadProvider"; import { queueAtom } from "@/utils/atoms/queue"; -import {DownloadMethod, useSettings} from "@/utils/atoms/settings"; +import { DownloadMethod, useSettings } from "@/utils/atoms/settings"; +import { writeToLog } from "@/utils/log"; import { Ionicons } from "@expo/vector-icons"; -import { useNavigation, useRouter } from "expo-router"; -import { useAtom } from "jotai"; -import React, { useEffect, useMemo, useRef } from "react"; -import { Alert, ScrollView, TouchableOpacity, View } from "react-native"; -import { Button } from "@/components/Button"; -import { useSafeAreaInsets } from "react-native-safe-area-context"; -import { useTranslation } from "react-i18next"; -import { t } from 'i18next'; -import { DownloadSize } from "@/components/downloads/DownloadSize"; import { BottomSheetBackdrop, - BottomSheetBackdropProps, + 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"; -import { writeToLog } from "@/utils/log"; export default function page() { const navigation = useNavigation(); @@ -45,7 +45,7 @@ export default function page() { const groupedBySeries = useMemo(() => { try { const episodes = downloadedFiles?.filter( - (f) => f.item.Type === "Episode" + (f) => f.item.Type === "Episode", ); const series: { [key: string]: DownloadedItem[] } = {}; episodes?.forEach((e) => { @@ -73,14 +73,22 @@ export default function page() { const deleteMovies = () => deleteFileByType("Movie") - .then(() => toast.success(t("home.downloads.toasts.deleted_all_movies_successfully"))) + .then(() => + toast.success( + t("home.downloads.toasts.deleted_all_movies_successfully"), + ), + ) .catch((reason) => { writeToLog("ERROR", reason); toast.error(t("home.downloads.toasts.failed_to_delete_all_movies")); }); const deleteShows = () => deleteFileByType("Episode") - .then(() => toast.success(t("home.downloads.toasts.deleted_all_tvseries_successfully"))) + .then(() => + toast.success( + t("home.downloads.toasts.deleted_all_tvseries_successfully"), + ), + ) .catch((reason) => { writeToLog("ERROR", reason); toast.error(t("home.downloads.toasts.failed_to_delete_all_tvseries")); @@ -97,26 +105,28 @@ export default function page() { paddingBottom: 100, }} > - - + + {settings?.downloadMethod === DownloadMethod.Remux && ( - - {t("home.downloads.queue")} - + + + {t("home.downloads.queue")} + + {t("home.downloads.queue_hint")} - + {queue.map((q, index) => ( router.push(`/(auth)/items/page?id=${q.item.Id}`) } - className="relative bg-neutral-900 border border-neutral-800 p-4 rounded-2xl overflow-hidden flex flex-row items-center justify-between" + className='relative bg-neutral-900 border border-neutral-800 p-4 rounded-2xl overflow-hidden flex flex-row items-center justify-between' key={index} > - {q.item.Name} - + {q.item.Name} + {q.item.Type} @@ -129,14 +139,16 @@ export default function page() { }); }} > - + ))} {queue.length === 0 && ( - {t("home.downloads.no_items_in_queue")} + + {t("home.downloads.no_items_in_queue")} + )} )} @@ -145,17 +157,19 @@ export default function page() { {movies.length > 0 && ( - - - {t("home.downloads.movies")} - - {movies?.length} + + + + {t("home.downloads.movies")} + + + {movies?.length} - + {movies?.map((item) => ( - + ))} @@ -164,20 +178,22 @@ export default function page() { )} {groupedBySeries.length > 0 && ( - - - {t("home.downloads.tvseries")} - - + + + + {t("home.downloads.tvseries")} + + + {groupedBySeries?.length} - + {groupedBySeries?.map((items) => ( )} {downloadedFiles?.length === 0 && ( - - {t("home.downloads.no_downloaded_items")} + + + {t("home.downloads.no_downloaded_items")} + )} @@ -215,14 +233,14 @@ export default function page() { )} > - - - - @@ -248,6 +266,6 @@ function migration_20241124() { 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 dcbd3bf9..6b914dd8 100644 --- a/app/(auth)/(tabs)/(home)/intro/page.tsx +++ b/app/(auth)/(tabs)/(home)/intro/page.tsx @@ -15,26 +15,26 @@ export default function page() { useFocusEffect( useCallback(() => { storage.set("hasShownIntro", true); - }, []) + }, []), ); return ( - + - + {t("home.intro.welcome_to_streamyfin")} - + {t("home.intro.a_free_and_open_source_client_for_jellyfin")} - + {t("home.intro.features_title")} - {t("home.intro.features_description")} - + {t("home.intro.features_description")} + - - Jellyseerr - + + Jellyseerr + {t("home.intro.jellyseerr_feature_description")} - + - + - - + + {t("home.intro.downloads_feature_title")} - + {t("home.intro.downloads_feature_description")} - + - + - - Chromecast - + + Chromecast + {t("home.intro.chromecast_feature_description")} - + - + - - + + {t("home.intro.centralised_settings_plugin_title")} - + {t("home.intro.centralised_settings_plugin_description")}{" "} { Linking.openURL( - "https://github.com/streamyfin/jellyfin-plugin-streamyfin" + "https://github.com/streamyfin/jellyfin-plugin-streamyfin", ); }} > @@ -120,7 +120,7 @@ export default function page() { onPress={() => { router.back(); }} - className="mt-4" + className='mt-4' > {t("home.intro.done_button")} @@ -129,9 +129,9 @@ export default function page() { router.back(); router.push("/settings"); }} - className="mt-4" + className='mt-4' > - + {t("home.intro.go_to_settings_button")} diff --git a/app/(auth)/(tabs)/(home)/sessions/index.tsx b/app/(auth)/(tabs)/(home)/sessions/index.tsx index c797b83e..590d89db 100644 --- a/app/(auth)/(tabs)/(home)/sessions/index.tsx +++ b/app/(auth)/(tabs)/(home)/sessions/index.tsx @@ -1,21 +1,29 @@ +import { Badge } from "@/components/Badge"; +import { Loader } from "@/components/Loader"; import { Text } from "@/components/common/Text"; -import { useSessions, useSessionsProps } from "@/hooks/useSessions"; +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 { + HardwareAccelerationType, + type SessionInfoDto, +} from "@jellyfin/sdk/lib/generated-client"; import { FlashList } from "@shopify/flash-list"; +import { useQuery } from "@tanstack/react-query"; +import { useAtomValue } from "jotai"; +import React, { useEffect, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { View } from "react-native"; -import { Loader } from "@/components/Loader"; -import { HardwareAccelerationType, SessionInfoDto } from "@jellyfin/sdk/lib/generated-client"; -import { useAtomValue } from "jotai"; -import { apiAtom } from "@/providers/JellyfinProvider"; -import Poster from "@/components/posters/Poster"; -import { getPrimaryImageUrl } from "@/utils/jellyfin/image/getPrimaryImageUrl"; -import { useInterval } from "@/hooks/useInterval"; -import React, { useEffect, useMemo, useState } from "react"; -import { formatTimeString } from "@/utils/time"; -import { formatBitrate } from "@/utils/bitrate"; -import { Ionicons, Entypo, AntDesign, MaterialCommunityIcons } from "@expo/vector-icons"; -import { Badge } from "@/components/Badge"; -import { useQuery } from "@tanstack/react-query"; export default function page() { const { sessions, isLoading } = useSessions({} as useSessionsProps); @@ -23,21 +31,23 @@ export default function page() { if (isLoading) return ( - + ); if (!sessions || sessions.length == 0) return ( - - {t("home.sessions.no_active_sessions")} + + + {t("home.sessions.no_active_sessions")} + ); return ( { } return Math.round( - (100 / session.NowPlayingItem?.RunTimeTicks) * (session.NowPlayingItem?.RunTimeTicks - remainingTicks) + (100 / session.NowPlayingItem?.RunTimeTicks) * + (session.NowPlayingItem?.RunTimeTicks - remainingTicks), ); }; useEffect(() => { const currentTime = session.PlayState?.PositionTicks; const duration = session.NowPlayingItem?.RunTimeTicks; - if (duration !== null && duration !== undefined && currentTime !== null && currentTime !== undefined) { + if ( + duration !== null && + duration !== undefined && + currentTime !== null && + currentTime !== undefined + ) { const remainingTimeTicks = duration - currentTime; setRemainingTicks(remainingTimeTicks); } @@ -85,9 +101,11 @@ const SessionCard = ({ session }: SessionCardProps) => { const { data: ipInfo } = useQuery({ queryKey: ["ipinfo", session.RemoteEndPoint], - cacheTime: Infinity, + cacheTime: Number.POSITIVE_INFINITY, queryFn: async () => { - const resp = await api.axiosInstance.get(`https://freeipapi.com/api/json/${session.RemoteEndPoint}`); + const resp = await api.axiosInstance.get( + `https://freeipapi.com/api/json/${session.RemoteEndPoint}`, + ); return resp.data; }, }); @@ -95,18 +113,23 @@ const SessionCard = ({ session }: SessionCardProps) => { useInterval(tick, 1000); return ( - - - - + + + + - - - + + + {session.NowPlayingItem?.Type === "Episode" ? ( <> - {session.NowPlayingItem?.Name} - + + {session.NowPlayingItem?.Name} + + {`S${session.NowPlayingItem.ParentIndexNumber?.toString()}:E${session.NowPlayingItem.IndexNumber?.toString()}`} {" - "} {session.NowPlayingItem.SeriesName} @@ -114,13 +137,19 @@ const SessionCard = ({ session }: SessionCardProps) => { ) : ( <> - {session.NowPlayingItem?.Name} - {session.NowPlayingItem?.ProductionYear} - {session.NowPlayingItem?.SeriesName} + + {session.NowPlayingItem?.Name} + + + {session.NowPlayingItem?.ProductionYear} + + + {session.NowPlayingItem?.SeriesName} + )} - + {session.UserName} {"\n"} {session.Client} @@ -130,21 +159,21 @@ const SessionCard = ({ session }: SessionCardProps) => { {ipInfo?.cityName} {ipInfo?.countryCode} - - - - + + + + {!session.PlayState?.IsPaused ? ( - + ) : ( - + )} - + {formatTimeString(remainingTicks, "tick")} left - + { const iconMap = { - bitrate: , - codec: , - videoRange: , - resolution: , - language: , - audioChannels: , - hwType: , + bitrate: , + codec: , + videoRange: ( + + ), + resolution: , + language: , + audioChannels: , + hwType: , } as const; const icon = (val: string) => { - return iconMap[val as keyof typeof iconMap] ?? ; + return ( + iconMap[val as keyof typeof iconMap] ?? ( + + ) + ); }; const formatVal = (key: string, val: any) => { @@ -195,8 +230,8 @@ const TranscodingBadges = ({ properties }: TranscodingBadgesProps) => { .map(([key]) => ( @@ -216,7 +251,7 @@ interface StreamProps { interface TranscodingStreamViewProps { title: string | undefined; value?: string; - isTranscoding: Boolean; + isTranscoding: boolean; transcodeValue?: string | undefined | null; properties: StreamProps; transcodeProperties?: StreamProps; @@ -231,20 +266,26 @@ const TranscodingStreamView = ({ transcodeValue, }: TranscodingStreamViewProps) => { return ( - - - {title} - + + + + {title} + + {isTranscoding && transcodeProperties ? ( <> - - - + + + - + @@ -256,21 +297,29 @@ const TranscodingStreamView = ({ const TranscodingView = ({ session }: SessionCardProps) => { const videoStream = useMemo(() => { - return session.NowPlayingItem?.MediaStreams?.filter((s) => s.Type == "Video")[0]; + return session.NowPlayingItem?.MediaStreams?.filter( + (s) => s.Type == "Video", + )[0]; }, [session]); const audioStream = useMemo(() => { const index = session.PlayState?.AudioStreamIndex; - return index !== null && index !== undefined ? session.NowPlayingItem?.MediaStreams?.[index] : undefined; + return index !== null && index !== undefined + ? session.NowPlayingItem?.MediaStreams?.[index] + : undefined; }, [session.PlayState?.AudioStreamIndex]); const subtitleStream = useMemo(() => { const index = session.PlayState?.SubtitleStreamIndex; - return index !== null && index !== undefined ? session.NowPlayingItem?.MediaStreams?.[index] : undefined; + return index !== null && index !== undefined + ? session.NowPlayingItem?.MediaStreams?.[index] + : undefined; }, [session.PlayState?.SubtitleStreamIndex]); const isTranscoding = useMemo(() => { - return session.PlayState?.PlayMethod == "Transcode" && session.TranscodingInfo; + return ( + session.PlayState?.PlayMethod == "Transcode" && session.TranscodingInfo + ); }, [session.PlayState?.PlayMethod, session.TranscodingInfo]); const videoStreamTitle = () => { @@ -278,9 +327,9 @@ const TranscodingView = ({ session }: SessionCardProps) => { }; return ( - + { bitrate: session.TranscodingInfo?.Bitrate, codec: session.TranscodingInfo?.VideoCodec, }} - isTranscoding={isTranscoding && !session.TranscodingInfo?.IsVideoDirect ? true : false} + isTranscoding={ + isTranscoding && !session.TranscodingInfo?.IsVideoDirect + ? true + : false + } /> { codec: session.TranscodingInfo?.AudioCodec, audioChannels: session.TranscodingInfo?.AudioChannels?.toString(), }} - isTranscoding={isTranscoding && !session.TranscodingInfo?.IsVideoDirect ? true : false} + isTranscoding={ + isTranscoding && !session.TranscodingInfo?.IsVideoDirect + ? true + : false + } /> {subtitleStream && ( <> - + {t("home.settings.log_out_button")} @@ -61,15 +61,15 @@ export default function settings() { paddingRight: insets.right, }} > - + - + - - - + + + @@ -90,7 +90,7 @@ export default function settings() { title={t("home.settings.intro.show_intro")} /> { storage.set("hasShownIntro", false); }} @@ -98,7 +98,7 @@ export default function settings() { /> - + router.push("/settings/logs/page")} @@ -106,7 +106,7 @@ export default function settings() { title={t("home.settings.logs.logs_title")} /> diff --git a/app/(auth)/(tabs)/(home)/settings/hide-libraries/page.tsx b/app/(auth)/(tabs)/(home)/settings/hide-libraries/page.tsx index 5b96ddbc..a90f7ef8 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 { Loader } from "@/components/Loader"; +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 { useTranslation } from "react-i18next"; -import DisabledSetting from "@/components/settings/DisabledSetting"; export default function page() { const [settings, updateSettings, pluginSettings] = useSettings(); @@ -18,7 +18,7 @@ export default function page() { const { t } = useTranslation(); - const { data, isLoading: isLoading } = useQuery({ + const { data, isLoading } = useQuery({ queryKey: ["user-views", user?.Id], queryFn: async () => { const response = await getUserViewsApi(api!).getUserViews({ @@ -33,7 +33,7 @@ export default function page() { if (isLoading) return ( - + ); @@ -41,7 +41,7 @@ export default function page() { return ( {data?.map((view) => ( @@ -59,8 +59,8 @@ export default function page() { ))} - - {t("home.settings.other.select_liraries_you_want_to_hide")} + + {t("home.settings.other.select_liraries_you_want_to_hide")} ); diff --git a/app/(auth)/(tabs)/(home)/settings/jellyseerr/page.tsx b/app/(auth)/(tabs)/(home)/settings/jellyseerr/page.tsx index 5da08ff1..507d01e2 100644 --- a/app/(auth)/(tabs)/(home)/settings/jellyseerr/page.tsx +++ b/app/(auth)/(tabs)/(home)/settings/jellyseerr/page.tsx @@ -1,6 +1,6 @@ +import DisabledSetting from "@/components/settings/DisabledSetting"; import { JellyseerrSettings } from "@/components/settings/Jellyseerr"; import { useSettings } from "@/utils/atoms/settings"; -import DisabledSetting from "@/components/settings/DisabledSetting"; export default function page() { const [settings, updateSettings, pluginSettings] = useSettings(); @@ -8,7 +8,7 @@ export default function page() { return ( diff --git a/app/(auth)/(tabs)/(home)/settings/logs/page.tsx b/app/(auth)/(tabs)/(home)/settings/logs/page.tsx index 1c59ba15..32fd3617 100644 --- a/app/(auth)/(tabs)/(home)/settings/logs/page.tsx +++ b/app/(auth)/(tabs)/(home)/settings/logs/page.tsx @@ -1,17 +1,17 @@ import { Text } from "@/components/common/Text"; import { useLog } from "@/utils/log"; -import { ScrollView, View } from "react-native"; import { useTranslation } from "react-i18next"; +import { ScrollView, View } from "react-native"; export default function page() { const { logs } = useLog(); const { t } = useTranslation(); return ( - - + + {logs?.map((log, index) => ( - + {log.level} - + {log.message} ))} {logs?.length === 0 && ( - {t("home.settings.logs.no_logs_available")} + + {t("home.settings.logs.no_logs_available")} + )} diff --git a/app/(auth)/(tabs)/(home)/settings/marlin-search/page.tsx b/app/(auth)/(tabs)/(home)/settings/marlin-search/page.tsx index b67f6ea0..c13390da 100644 --- a/app/(auth)/(tabs)/(home)/settings/marlin-search/page.tsx +++ b/app/(auth)/(tabs)/(home)/settings/marlin-search/page.tsx @@ -6,7 +6,8 @@ import { useQueryClient } from "@tanstack/react-query"; import { useNavigation } from "expo-router"; import { useTranslation } from "react-i18next"; -import React, {useEffect, useMemo, useState} from "react"; +import DisabledSetting from "@/components/settings/DisabledSetting"; +import React, { useEffect, useMemo, useState } from "react"; import { Linking, Switch, @@ -15,7 +16,6 @@ import { View, } from "react-native"; import { toast } from "sonner-native"; -import DisabledSetting from "@/components/settings/DisabledSetting"; export default function page() { const navigation = useNavigation(); @@ -39,7 +39,10 @@ export default function page() { }; const disabled = useMemo(() => { - return pluginSettings?.searchEngine?.locked === true && pluginSettings?.marlinServerUrl?.locked === true + return ( + pluginSettings?.searchEngine?.locked === true && + pluginSettings?.marlinServerUrl?.locked === true + ); }, [pluginSettings]); useEffect(() => { @@ -47,7 +50,9 @@ export default function page() { navigation.setOptions({ headerRight: () => ( onSave(value)}> - {t("home.settings.plugins.marlin_search.save_button")} + + {t("home.settings.plugins.marlin_search.save_button")} + ), }); @@ -57,17 +62,16 @@ export default function page() { if (!settings) return null; return ( - + { updateSettings({ searchEngine: "Jellyfin" }); queryClient.invalidateQueries({ queryKey: ["search"] }); @@ -87,28 +91,30 @@ export default function page() { - - {t("home.settings.plugins.marlin_search.url")} + + + {t("home.settings.plugins.marlin_search.url")} + setValue(text)} /> - + {t("home.settings.plugins.marlin_search.marlin_search_hint")}{" "} - + {t("home.settings.plugins.marlin_search.read_more_about_marlin")} diff --git a/app/(auth)/(tabs)/(home)/settings/optimized-server/page.tsx b/app/(auth)/(tabs)/(home)/settings/optimized-server/page.tsx index 8ffc2fc7..1c53d7e2 100644 --- a/app/(auth)/(tabs)/(home)/settings/optimized-server/page.tsx +++ b/app/(auth)/(tabs)/(home)/settings/optimized-server/page.tsx @@ -1,4 +1,5 @@ import { Text } from "@/components/common/Text"; +import DisabledSetting from "@/components/settings/DisabledSetting"; import { OptimizedServerForm } from "@/components/settings/OptimizedServerForm"; import { apiAtom } from "@/providers/JellyfinProvider"; import { useSettings } from "@/utils/atoms/settings"; @@ -8,10 +9,9 @@ import { useMutation } from "@tanstack/react-query"; import { useNavigation } from "expo-router"; import { useAtom } from "jotai"; import { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; import { ActivityIndicator, TouchableOpacity, View } from "react-native"; import { toast } from "sonner-native"; -import { useTranslation } from "react-i18next"; -import DisabledSetting from "@/components/settings/DisabledSetting"; export default function page() { const navigation = useNavigation(); @@ -67,8 +67,12 @@ export default function page() { saveMutation.isPending ? ( ) : ( - onSave(optimizedVersionsServerUrl)}> - {t("home.settings.downloads.save_button")} + onSave(optimizedVersionsServerUrl)} + > + + {t("home.settings.downloads.save_button")} + ), }); @@ -78,7 +82,7 @@ export default function page() { return ( { const local = useLocalSearchParams(); @@ -68,7 +68,7 @@ const page: React.FC = () => { return response.data; }, - [api, user?.Id, actorId] + [api, user?.Id, actorId], ); const backdropUrl = useMemo( @@ -79,12 +79,12 @@ const page: React.FC = () => { quality: 90, width: 1000, }), - [item] + [item], ); if (l1) return ( - + ); @@ -105,13 +105,13 @@ const page: React.FC = () => { /> } > - - - + + + - + {t("item_card.appeared_in")} { queryFn={fetchItems} queryKey={["actor", "movies", actorId]} /> - + ); 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 366ca284..b23ae9d7 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,23 @@ +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 { 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 { + SortByOption, + SortOrderOption, genreFilterAtom, sortByAtom, - SortByOption, sortOptions, sortOrderAtom, - SortOrderOption, sortOrderOptions, tagsFilterAtom, yearFilterAtom, } from "@/utils/atoms/filters"; -import { +import type { BaseItemDto, BaseItemDtoQueryResult, ItemSortBy, @@ -29,11 +30,11 @@ import { import { FlashList } from "@shopify/flash-list"; import { useInfiniteQuery, useQuery } from "@tanstack/react-query"; import { useLocalSearchParams, useNavigation } from "expo-router"; -import * as ScreenOrientation from "@/packages/expo-screen-orientation"; import { useAtom } from "jotai"; -import React, { useCallback, useEffect, useMemo, useState } from "react"; -import { FlatList, View } from "react-native"; +import type React from "react"; +import { useCallback, useEffect, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; +import { FlatList, View } from "react-native"; const page: React.FC = () => { const searchParams = useLocalSearchParams(); @@ -43,7 +44,7 @@ const page: React.FC = () => { const [user] = useAtom(userAtom); const navigation = useNavigation(); const [orientation, setOrientation] = useState( - ScreenOrientation.Orientation.PORTRAIT_UP + ScreenOrientation.Orientation.PORTRAIT_UP, ); const { t } = useTranslation(); @@ -111,7 +112,7 @@ const page: React.FC = () => { recursive: true, genres: selectedGenres, tags: selectedTags, - years: selectedYears.map((year) => parseInt(year)), + years: selectedYears.map((year) => Number.parseInt(year)), includeItemTypes: ["Movie", "Series"], }); @@ -126,7 +127,7 @@ const page: React.FC = () => { selectedTags, sortBy, sortOrder, - ] + ], ); const { data, isFetching, fetchNextPage, hasNextPage } = useInfiniteQuery({ @@ -151,7 +152,7 @@ const page: React.FC = () => { const totalItems = lastPage.TotalRecordCount; const accumulatedItems = pages.reduce( (acc, curr) => acc + (curr?.Items?.length || 0), - 0 + 0, ); if (accumulatedItems < totalItems) { @@ -188,8 +189,8 @@ const page: React.FC = () => { index % 3 === 0 ? "flex-end" : (index + 1) % 3 === 0 - ? "flex-start" - : "center", + ? "flex-start" + : "center", width: "89%", }} > @@ -199,14 +200,14 @@ const page: React.FC = () => { ), - [orientation] + [orientation], ); const keyExtractor = useCallback((item: BaseItemDto) => item.Id || "", []); const ListHeaderComponent = useCallback( () => ( - + { key: "genre", component: ( { if (!api) return null; const response = await getFilterApi( - api + api, ).getQueryFiltersLegacy({ userId: user?.Id, parentId: collectionId, @@ -259,13 +260,13 @@ const page: React.FC = () => { key: "year", component: ( { if (!api) return null; const response = await getFilterApi( - api + api, ).getQueryFiltersLegacy({ userId: user?.Id, parentId: collectionId, @@ -284,13 +285,13 @@ const page: React.FC = () => { key: "tags", component: ( { if (!api) return null; const response = await getFilterApi( - api + api, ).getQueryFiltersLegacy({ userId: user?.Id, parentId: collectionId, @@ -311,9 +312,9 @@ const page: React.FC = () => { key: "sortBy", component: ( sortOptions.map((s) => s.key)} set={setSortBy} values={sortBy} @@ -331,9 +332,9 @@ const page: React.FC = () => { key: "sortOrder", component: ( sortOrderOptions.map((s) => s.key)} set={setSortOrder} values={sortOrder} @@ -368,7 +369,7 @@ const page: React.FC = () => { sortOrder, setSortOrder, isFetching, - ] + ], ); if (!collection) return null; @@ -376,8 +377,10 @@ const page: React.FC = () => { return ( - {t("search.no_results")} + + + {t("search.no_results")} + } extraData={[ @@ -387,7 +390,7 @@ const page: React.FC = () => { sortBy, sortOrder, ]} - contentInsetAdjustmentBehavior="automatic" + contentInsetAdjustmentBehavior='automatic' data={flatData} renderItem={renderItem} keyExtractor={keyExtractor} 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 a61114bd..1f9d322b 100644 --- a/app/(auth)/(tabs)/(home,libraries,search,favorites)/items/page.tsx +++ b/app/(auth)/(tabs)/(home,libraries,search,favorites)/items/page.tsx @@ -1,11 +1,13 @@ -import { Text } from "@/components/common/Text"; 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"; import { useAtom } from "jotai"; -import React, { useEffect } from "react"; +import type React from "react"; +import { useEffect } from "react"; +import { useTranslation } from "react-i18next"; import { View } from "react-native"; import Animated, { runOnJS, @@ -13,7 +15,6 @@ import Animated, { useSharedValue, withTiming, } from "react-native-reanimated"; -import { useTranslation } from "react-i18next"; const Page: React.FC = () => { const [api] = useAtom(apiAtom); @@ -75,36 +76,36 @@ const Page: React.FC = () => { if (isError) return ( - + {t("item_card.could_not_load_item")} ); return ( - + - - - - - - - + + + + + + + - - - - + + + + {item && } 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 cf8111bb..1f4b139d 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,41 +1,43 @@ -import {useLocalSearchParams} from "expo-router"; -import React, {useMemo,} from "react"; -import {useInfiniteQuery} from "@tanstack/react-query"; -import {Endpoints, useJellyseerr} from "@/hooks/useJellyseerr"; -import {Image} from "expo-image"; -import {DiscoverSliderType} from "@/utils/jellyseerr/server/constants/discover"; import ParallaxSlideShow from "@/components/jellyseerr/ParallaxSlideShow"; -import {MovieResult, Results, TvResult} from "@/utils/jellyseerr/server/models/Search"; -import {COMPANY_LOGO_IMAGE_FILTER} from "@/utils/jellyseerr/src/components/Discover/NetworkSlider"; -import {uniqBy} from "lodash"; 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(); - const {jellyseerrApi} = useJellyseerr(); + const { jellyseerrApi } = useJellyseerr(); - const {companyId, name, image, type} = local as unknown as { - companyId: string, - name: string, - image: string, - type: DiscoverSliderType + const { companyId, name, image, type } = local as unknown as { + companyId: string; + name: string; + image: string; + type: DiscoverSliderType; }; - const {data, fetchNextPage, hasNextPage} = useInfiniteQuery({ + const { data, fetchNextPage, hasNextPage } = useInfiniteQuery({ queryKey: ["jellyseerr", "company", type, companyId], - queryFn: async ({pageParam}) => { - let params: any = { + queryFn: async ({ pageParam }) => { + const params: any = { page: Number(pageParam), }; return jellyseerrApi?.discover( - ( - type == DiscoverSliderType.NETWORKS - ? Endpoints.DISCOVER_TV_NETWORK - : Endpoints.DISCOVER_MOVIES_STUDIO - ) + `/${companyId}`, - params - ) + (type == DiscoverSliderType.NETWORKS + ? Endpoints.DISCOVER_TV_NETWORK + : Endpoints.DISCOVER_MOVIES_STUDIO) + `/${companyId}`, + params, + ); }, enabled: !!jellyseerrApi && !!companyId, initialPageParam: 1, @@ -46,46 +48,58 @@ export default function page() { }); const flatData = useMemo( - () => uniqBy(data?.pages?.filter((p) => p?.results.length).flatMap((p) => p?.results ?? []), "id")?? [], - [data] + () => + uniqBy( + data?.pages + ?.filter((p) => p?.results.length) + .flatMap((p) => p?.results ?? []), + "id", + ) ?? [], + [data], ); const backdrops = useMemo( - () => jellyseerrApi - ? flatData.map((r) => jellyseerrApi.imageProxy((r as TvResult | MovieResult).backdropPath, "w1920_and_h800_multi_faces")) - : [], - [jellyseerrApi, flatData] + () => + jellyseerrApi + ? flatData.map((r) => + jellyseerrApi.imageProxy( + (r as TvResult | MovieResult).backdropPath, + "w1920_and_h800_multi_faces", + ), + ) + : [], + [jellyseerrApi, flatData], ); return ( item.id.toString()} onEndReached={() => { if (hasNextPage) { - fetchNextPage() + fetchNextPage(); } }} logo={ } - 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 dbbce320..0d94a9a5 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,42 +1,46 @@ -import {router, useLocalSearchParams, useSegments,} from "expo-router"; -import React, {useMemo,} from "react"; -import {TouchableOpacity} from "react-native"; -import {useInfiniteQuery} from "@tanstack/react-query"; -import {Endpoints, useJellyseerr} from "@/hooks/useJellyseerr"; -import {Text} from "@/components/common/Text"; -import Poster from "@/components/posters/Poster"; +import { Text } from "@/components/common/Text"; import JellyseerrMediaIcon from "@/components/jellyseerr/JellyseerrMediaIcon"; -import {DiscoverSliderType} from "@/utils/jellyseerr/server/constants/discover"; import ParallaxSlideShow from "@/components/jellyseerr/ParallaxSlideShow"; -import {MovieResult, Results, TvResult} from "@/utils/jellyseerr/server/models/Search"; -import {uniqBy} from "lodash"; -import {textShadowStyle} from "@/components/jellyseerr/discover/GenericSlideCard"; +import { textShadowStyle } from "@/components/jellyseerr/discover/GenericSlideCard"; 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(); - const {jellyseerrApi} = useJellyseerr(); + const { jellyseerrApi } = useJellyseerr(); - const {genreId, name, type} = local as unknown as { - genreId: string, - name: string, - type: DiscoverSliderType + const { genreId, name, type } = local as unknown as { + genreId: string; + name: string; + type: DiscoverSliderType; }; - const {data, fetchNextPage, hasNextPage} = useInfiniteQuery({ + const { data, fetchNextPage, hasNextPage } = useInfiniteQuery({ queryKey: ["jellyseerr", "company", type, genreId], - queryFn: async ({pageParam}) => { - let params: any = { + queryFn: async ({ pageParam }) => { + const params: any = { page: Number(pageParam), - genre: genreId + genre: genreId, }; return jellyseerrApi?.discover( - type == DiscoverSliderType.MOVIE_GENRES - ? Endpoints.DISCOVER_MOVIES - : Endpoints.DISCOVER_TV, - params - ) + type == DiscoverSliderType.MOVIE_GENRES + ? Endpoints.DISCOVER_MOVIES + : Endpoints.DISCOVER_TV, + params, + ); }, enabled: !!jellyseerrApi && !!genreId, initialPageParam: 1, @@ -47,41 +51,54 @@ export default function page() { }); const flatData = useMemo( - () => uniqBy(data?.pages?.filter((p) => p?.results.length).flatMap((p) => p?.results ?? []), "id")?? [], - [data] + () => + uniqBy( + data?.pages + ?.filter((p) => p?.results.length) + .flatMap((p) => p?.results ?? []), + "id", + ) ?? [], + [data], ); const backdrops = useMemo( - () => jellyseerrApi - ? flatData.map((r) => jellyseerrApi.imageProxy((r as TvResult | MovieResult).backdropPath, "w1920_and_h800_multi_faces")) - : [], - [jellyseerrApi, flatData] + () => + jellyseerrApi + ? flatData.map((r) => + jellyseerrApi.imageProxy( + (r as TvResult | MovieResult).backdropPath, + "w1920_and_h800_multi_faces", + ), + ) + : [], + [jellyseerrApi, flatData], ); return ( item.id.toString()} onEndReached={() => { if (hasNextPage) { - fetchNextPage() + fetchNextPage(); } }} logo={ + shadowRadius: 10, + }} + > {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 45318462..845a7676 100644 --- a/app/(auth)/(tabs)/(home,libraries,search,favorites)/jellyseerr/page.tsx +++ b/app/(auth)/(tabs)/(home,libraries,search,favorites)/jellyseerr/page.tsx @@ -1,27 +1,29 @@ 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 { 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 { - IssueType, + type IssueType, IssueTypeName, } from "@/utils/jellyseerr/server/constants/issue"; import { MediaType } from "@/utils/jellyseerr/server/constants/media"; -import { MovieResult, TvResult } from "@/utils/jellyseerr/server/models/Search"; -import { TvDetails } from "@/utils/jellyseerr/server/models/Tv"; -import { useTranslation } from "react-i18next"; +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, - BottomSheetBackdropProps, + type BottomSheetBackdropProps, BottomSheetModal, BottomSheetTextInput, BottomSheetView, @@ -29,20 +31,16 @@ import { import { useQuery } from "@tanstack/react-query"; import { Image } from "expo-image"; import { useLocalSearchParams, useNavigation } from "expo-router"; -import React, { - useCallback, - useEffect, - useMemo, - useRef, - useState, -} from "react"; +import type React from "react"; +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"; 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 { MediaRequestBody } from "@/utils/jellyseerr/server/interfaces/api/requestInterfaces"; -import {MovieDetails} from "@/utils/jellyseerr/server/models/Movie"; +import type { MediaRequestBody } from "@/utils/jellyseerr/server/interfaces/api/requestInterfaces"; +import type { MovieDetails } from "@/utils/jellyseerr/server/models/Movie"; const Page: React.FC = () => { const insets = useSafeAreaInsets(); @@ -83,8 +81,8 @@ const Page: React.FC = () => { refetchInterval: 0, queryFn: async () => { return mediaType === MediaType.MOVIE - ? jellyseerrApi?.movieDetails(result.id!!) - : jellyseerrApi?.tvDetails(result.id!!); + ? jellyseerrApi?.movieDetails(result.id!) + : jellyseerrApi?.tvDetails(result.id!); }, }); @@ -99,7 +97,7 @@ const Page: React.FC = () => { appearsOnIndex={0} /> ), - [] + [], ); const submitIssue = useCallback(() => { @@ -114,15 +112,18 @@ const Page: React.FC = () => { } }, [jellyseerrApi, details, result, issueType, issueMessage]); - const setRequestBody = useCallback((body: MediaRequestBody) => { - _setRequestBody(body) - advancedReqModalRef?.current?.present?.(); - }, [requestBody, _setRequestBody, advancedReqModalRef]) + const setRequestBody = useCallback( + (body: MediaRequestBody) => { + _setRequestBody(body); + advancedReqModalRef?.current?.present?.(); + }, + [requestBody, _setRequestBody, advancedReqModalRef], + ); const request = useCallback(async () => { const body: MediaRequestBody = { - mediaId: Number(result.id!!), - mediaType: mediaType!!, + mediaId: Number(result.id!), + mediaType: mediaType!, tvdbId: details?.externalIds?.tvdbId, seasons: (details as TvDetails)?.seasons ?.filter?.((s) => s.seasonNumber !== 0) @@ -130,7 +131,7 @@ const Page: React.FC = () => { }; if (hasAdvancedRequestPermission) { - setRequestBody(body) + setRequestBody(body); return; } @@ -141,14 +142,14 @@ const Page: React.FC = () => { () => (details?.keywords.some((k) => k.id === ANIME_KEYWORD_ID) || false) && mediaType === MediaType.TV, - [details] + [details], ); useEffect(() => { if (details) { navigation.setOptions({ headerRight: () => ( - + ), @@ -158,14 +159,14 @@ const Page: React.FC = () => { return ( @@ -180,7 +181,7 @@ const Page: React.FC = () => { source={{ uri: jellyseerrApi?.imageProxy( result.backdropPath, - "w1920_and_h800_multi_faces" + "w1920_and_h800_multi_faces", ), }} /> @@ -190,12 +191,12 @@ const Page: React.FC = () => { width: "100%", height: "100%", }} - className="flex flex-col items-center justify-center border border-neutral-800 bg-neutral-900" + className='flex flex-col items-center justify-center border border-neutral-800 bg-neutral-900' > @@ -203,23 +204,31 @@ const Page: React.FC = () => { } > - - - - - - + + + + + + {mediaTitle} - {releaseYear} + {releaseYear} { }} /> - + g.name) || []} /> {isLoading || isFetching ? ( - + ) : canRequest ? ( - ) : ( )} - + {mediaType === MediaType.TV && ( @@ -261,13 +270,11 @@ const Page: React.FC = () => { details={details as TvDetails} refetch={refetch} hasAdvancedRequest={hasAdvancedRequestPermission} - onAdvancedRequest={(data) => - setRequestBody(data) - } + onAdvancedRequest={(data) => setRequestBody(data)} /> )} @@ -278,11 +285,11 @@ const Page: React.FC = () => { ref={advancedReqModalRef} requestBody={requestBody} title={mediaTitle} - id={result.id!!} + id={result.id!} type={mediaType} isAnime={isAnime} onRequested={() => { - _setRequestBody(undefined) + _setRequestBody(undefined); advancedReqModalRef?.current?.close(); refetch(); }} @@ -300,22 +307,22 @@ const Page: React.FC = () => { backdropComponent={renderBackdrop} > - + - + {t("jellyseerr.whats_wrong")} - - + + - - + + {t("jellyseerr.issue_type")} - - + + {issueType ? IssueTypeName[issueType] : t("jellyseerr.select_an_issue")} @@ -325,8 +332,8 @@ const Page: React.FC = () => { { - + { /> - 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 bd0fc216..bbe9f6cc 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,25 +1,30 @@ -import { - useLocalSearchParams, - useSegments, -} from "expo-router"; -import React, { useMemo } from "react"; -import { useQuery } from "@tanstack/react-query"; -import { useJellyseerr } from "@/hooks/useJellyseerr"; -import { Text } from "@/components/common/Text"; -import { Image } from "expo-image"; import { OverviewText } from "@/components/OverviewText"; -import {orderBy, uniqBy} from "lodash"; -import { PersonCreditCast } from "@/utils/jellyseerr/server/models/Person"; +import { Text } from "@/components/common/Text"; import ParallaxSlideShow from "@/components/jellyseerr/ParallaxSlideShow"; import JellyseerrPoster from "@/components/posters/JellyseerrPoster"; -import {MovieResult, TvResult} from "@/utils/jellyseerr/server/models/Search"; +import { useJellyseerr } from "@/hooks/useJellyseerr"; +import type { PersonCreditCast } from "@/utils/jellyseerr/server/models/Person"; +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(); const { t } = useTranslation(); - const { jellyseerrApi, jellyseerrUser, jellyseerrRegion: region, jellyseerrLocale: locale } = useJellyseerr(); + const { + jellyseerrApi, + jellyseerrUser, + jellyseerrRegion: region, + jellyseerrLocale: locale, + } = useJellyseerr(); const { personId } = local as { personId: string }; @@ -34,18 +39,27 @@ export default function page() { const castedRoles: PersonCreditCast[] = useMemo( () => - uniqBy(orderBy( - data?.combinedCredits?.cast, - ["voteCount", "voteAverage"], - "desc" - ), 'id'), - [data?.combinedCredits] + uniqBy( + orderBy( + data?.combinedCredits?.cast, + ["voteCount", "voteAverage"], + "desc", + ), + "id", + ), + [data?.combinedCredits], ); const backdrops = useMemo( - () => jellyseerrApi - ? castedRoles.map((c) => jellyseerrApi.imageProxy(c.backdropPath, "w1920_and_h800_multi_faces")) - : [], - [jellyseerrApi, data?.combinedCredits] + () => + jellyseerrApi + ? castedRoles.map((c) => + jellyseerrApi.imageProxy( + c.backdropPath, + "w1920_and_h800_multi_faces", + ), + ) + : [], + [jellyseerrApi, data?.combinedCredits], ); return ( @@ -58,15 +72,15 @@ export default function page() { ( <> - - {data?.details?.name} - - + {data?.details?.name} + {t("jellyseerr.born")}{" "} - {new Date(data?.details?.birthday!!).toLocaleDateString( + {new Date(data?.details?.birthday!).toLocaleDateString( `${locale}-${region}`, { year: "numeric", month: "long", day: "numeric", - } + }, )}{" "} | {data?.details?.placeOfBirth} )} 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 7225e677..9c6625e6 100644 --- a/app/(auth)/(tabs)/(home,libraries,search,favorites)/livetv/_layout.tsx +++ b/app/(auth)/(tabs)/(home,libraries,search,favorites)/livetv/_layout.tsx @@ -3,7 +3,10 @@ import type { MaterialTopTabNavigationOptions, } from "@react-navigation/material-top-tabs"; import { createMaterialTopTabNavigator } from "@react-navigation/material-top-tabs"; -import { ParamListBase, TabNavigationState } from "@react-navigation/native"; +import type { + ParamListBase, + TabNavigationState, +} from "@react-navigation/native"; import { Stack, withLayoutContext } from "expo-router"; import React from "react"; @@ -21,8 +24,8 @@ const Layout = () => { <> { tabBarScrollEnabled: true, }} > - - - - + + + + ); 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 dd1c1f85..eae563fb 100644 --- a/app/(auth)/(tabs)/(home,libraries,search,favorites)/livetv/channels.tsx +++ b/app/(auth)/(tabs)/(home,libraries,search,favorites)/livetv/channels.tsx @@ -31,13 +31,13 @@ export default function page() { }); return ( - + ( - - + + - {item.Name} + {item.Name} )} /> 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 398d74b6..7dbe3461 100644 --- a/app/(auth)/(tabs)/(home,libraries,search,favorites)/livetv/guide.tsx +++ b/app/(auth)/(tabs)/(home,libraries,search,favorites)/livetv/guide.tsx @@ -9,6 +9,7 @@ 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 { useTranslation } from "react-i18next"; import { Button, Dimensions, @@ -17,7 +18,6 @@ import { View, } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; -import { useTranslation } from "react-i18next"; const HOUR_HEIGHT = 30; const ITEMS_PER_PAGE = 20; @@ -71,7 +71,7 @@ export default function page() { MaxStartDate: endOfDay.toISOString(), MinEndDate: isToday ? now.toISOString() : startOfDay.toISOString(), ChannelIds: channels?.Items?.map((c) => c.Id).filter( - Boolean + Boolean, ) as string[], ImageTypeLimit: 1, EnableImages: false, @@ -100,7 +100,7 @@ export default function page() { return ( - - + + {channels?.Items?.map((c, i) => ( - + - + {channels?.Items?.map((c, i) => ( = ({ }) => { const { t } = useTranslation(); return ( - + @@ -199,11 +199,11 @@ const PageButtons: React.FC = ({ {t("live_tv.previous")} - Page {currentPage} + Page {currentPage} = ({ {t("live_tv.next")} diff --git a/app/(auth)/(tabs)/(home,libraries,search,favorites)/livetv/programs.tsx b/app/(auth)/(tabs)/(home,libraries,search,favorites)/livetv/programs.tsx index 76c07643..ec0a78ba 100644 --- a/app/(auth)/(tabs)/(home,libraries,search,favorites)/livetv/programs.tsx +++ b/app/(auth)/(tabs)/(home,libraries,search,favorites)/livetv/programs.tsx @@ -1,13 +1,13 @@ import { ScrollingCollectionList } from "@/components/home/ScrollingCollectionList"; import { TAB_HEIGHT } from "@/constants/Values"; import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; -import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client"; +import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client"; import { getLiveTvApi } from "@jellyfin/sdk/lib/utils/api"; import { useAtom } from "jotai"; import React from "react"; +import { useTranslation } from "react-i18next"; import { ScrollView, View } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; -import { useTranslation } from "react-i18next"; export default function page() { const [api] = useAtom(apiAtom); @@ -19,7 +19,7 @@ export default function page() { return ( - + diff --git a/app/(auth)/(tabs)/(home,libraries,search,favorites)/livetv/recordings.tsx b/app/(auth)/(tabs)/(home,libraries,search,favorites)/livetv/recordings.tsx index 4068f8a3..8fef80bd 100644 --- a/app/(auth)/(tabs)/(home,libraries,search,favorites)/livetv/recordings.tsx +++ b/app/(auth)/(tabs)/(home,libraries,search,favorites)/livetv/recordings.tsx @@ -1,12 +1,12 @@ import { Text } from "@/components/common/Text"; import React from "react"; -import { View } from "react-native"; import { useTranslation } from "react-i18next"; +import { View } from "react-native"; export default function page() { const { t } = useTranslation(); return ( - + {t("live_tv.coming_soon")} ); diff --git a/app/(auth)/(tabs)/(home,libraries,search,favorites)/series/[id].tsx b/app/(auth)/(tabs)/(home,libraries,search,favorites)/series/[id].tsx index eb9b660d..35845572 100644 --- a/app/(auth)/(tabs)/(home,libraries,search,favorites)/series/[id].tsx +++ b/app/(auth)/(tabs)/(home,libraries,search,favorites)/series/[id].tsx @@ -14,9 +14,10 @@ import { useQuery } from "@tanstack/react-query"; import { Image } from "expo-image"; import { useLocalSearchParams, useNavigation } from "expo-router"; import { useAtom } from "jotai"; -import React, { useEffect, useMemo } from "react"; -import { Platform, View } from "react-native"; +import type React from "react"; +import { useEffect, useMemo } from "react"; import { useTranslation } from "react-i18next"; +import { Platform, View } from "react-native"; const page: React.FC = () => { const navigation = useNavigation(); @@ -49,7 +50,7 @@ const page: React.FC = () => { quality: 90, width: 1000, }), - [item] + [item], ); const logoUrl = useMemo( @@ -58,7 +59,7 @@ const page: React.FC = () => { api, item, }), - [item] + [item], ); const { data: allEpisodes, isLoading } = useQuery({ @@ -83,22 +84,22 @@ const page: React.FC = () => { item && allEpisodes && allEpisodes.length > 0 && ( - + {!Platform.isTV && ( <> ( - + )} DownloadedIconComponent={() => ( )} /> @@ -142,9 +143,9 @@ const page: React.FC = () => { } > - + - + diff --git a/app/(auth)/(tabs)/(libraries)/[libraryId].tsx b/app/(auth)/(tabs)/(libraries)/[libraryId].tsx index a7b9cc1f..ec0e7250 100644 --- a/app/(auth)/(tabs)/(libraries)/[libraryId].tsx +++ b/app/(auth)/(tabs)/(libraries)/[libraryId].tsx @@ -1,35 +1,35 @@ +import * as ScreenOrientation from "@/packages/expo-screen-orientation"; import { useInfiniteQuery, useQuery } from "@tanstack/react-query"; import { useLocalSearchParams, useNavigation } from "expo-router"; -import * as ScreenOrientation from "@/packages/expo-screen-orientation"; import { useAtom } from "jotai"; import React, { useCallback, useEffect, useMemo } from "react"; -import { FlatList, useWindowDimensions, View } from "react-native"; +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 { ItemCardText } from "@/components/ItemCardText"; -import { Loader } from "@/components/Loader"; import { ItemPoster } from "@/components/posters/ItemPoster"; import { useOrientation } from "@/hooks/useOrientation"; import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; import { + SortByOption, + SortOrderOption, genreFilterAtom, getSortByPreference, getSortOrderPreference, sortByAtom, - SortByOption, sortByPreferenceAtom, sortOptions, sortOrderAtom, - SortOrderOption, sortOrderOptions, sortOrderPreferenceAtom, tagsFilterAtom, yearFilterAtom, } from "@/utils/atoms/filters"; -import { +import type { BaseItemDto, BaseItemDtoQueryResult, BaseItemKind, @@ -40,8 +40,8 @@ import { getUserLibraryApi, } from "@jellyfin/sdk/lib/utils/api"; import { FlashList } from "@shopify/flash-list"; -import { useSafeAreaInsets } from "react-native-safe-area-context"; import { useTranslation } from "react-i18next"; +import { useSafeAreaInsets } from "react-native-safe-area-context"; const Page = () => { const searchParams = useLocalSearchParams(); @@ -58,7 +58,7 @@ const Page = () => { const [sortOrder, _setSortOrder] = useAtom(sortOrderAtom); const [sortByPreference, setSortByPreference] = useAtom(sortByPreferenceAtom); const [sortOrderPreference, setOderByPreference] = useAtom( - sortOrderPreferenceAtom + sortOrderPreferenceAtom, ); const { orientation } = useOrientation(); @@ -88,7 +88,7 @@ const Page = () => { } _setSortBy(sortBy); }, - [libraryId, sortByPreference] + [libraryId, sortByPreference], ); const setSortOrder = useCallback( @@ -102,7 +102,7 @@ const Page = () => { } _setSortOrder(sortOrder); }, - [libraryId, sortOrderPreference] + [libraryId, sortOrderPreference], ); const nrOfCols = useMemo(() => { @@ -169,7 +169,7 @@ const Page = () => { fields: ["PrimaryImageAspectRatio", "SortName"], genres: selectedGenres, tags: selectedTags, - years: selectedYears.map((year) => parseInt(year)), + years: selectedYears.map((year) => Number.parseInt(year)), includeItemTypes: itemType ? [itemType] : undefined, }); @@ -185,7 +185,7 @@ const Page = () => { selectedTags, sortBy, sortOrder, - ] + ], ); const { data, isFetching, fetchNextPage, hasNextPage, isLoading } = @@ -211,7 +211,7 @@ const Page = () => { const totalItems = lastPage.TotalRecordCount; const accumulatedItems = pages.reduce( (acc, curr) => acc + (curr?.Items?.length || 0), - 0 + 0, ); if (accumulatedItems < totalItems) { @@ -248,8 +248,8 @@ const Page = () => { ? index % nrOfCols === 0 ? "flex-end" : (index + 1) % nrOfCols === 0 - ? "flex-start" - : "center" + ? "flex-start" + : "center" : "center", width: "89%", }} @@ -260,14 +260,14 @@ const Page = () => { ), - [orientation] + [orientation], ); const keyExtractor = useCallback((item: BaseItemDto) => item.Id || "", []); const ListHeaderComponent = useCallback( () => ( - + { key: "genre", component: ( { if (!api) return null; const response = await getFilterApi( - api + api, ).getQueryFiltersLegacy({ userId: user?.Id, parentId: libraryId, @@ -313,13 +313,13 @@ const Page = () => { key: "year", component: ( { if (!api) return null; const response = await getFilterApi( - api + api, ).getQueryFiltersLegacy({ userId: user?.Id, parentId: libraryId, @@ -338,13 +338,13 @@ const Page = () => { key: "tags", component: ( { if (!api) return null; const response = await getFilterApi( - api + api, ).getQueryFiltersLegacy({ userId: user?.Id, parentId: libraryId, @@ -365,9 +365,9 @@ const Page = () => { key: "sortBy", component: ( sortOptions.map((s) => s.key)} set={setSortBy} values={sortBy} @@ -385,9 +385,9 @@ const Page = () => { key: "sortOrder", component: ( sortOrderOptions.map((s) => s.key)} set={setSortOrder} values={sortOrder} @@ -422,22 +422,24 @@ const Page = () => { sortOrder, setSortOrder, isFetching, - ] + ], ); const insets = useSafeAreaInsets(); if (isLoading || isLibraryLoading) return ( - + ); if (flatData.length === 0) return ( - - {t("library.no_items_found")} + + + {t("library.no_items_found")} + ); @@ -445,11 +447,13 @@ const Page = () => { - {t("library.no_results")} + + + {t("library.no_results")} + } - contentInsetAdjustmentBehavior="automatic" + contentInsetAdjustmentBehavior='automatic' data={flatData} renderItem={renderItem} extraData={[orientation, nrOfCols]} diff --git a/app/(auth)/(tabs)/(libraries)/_layout.tsx b/app/(auth)/(tabs)/(libraries)/_layout.tsx index 3406bb09..36647450 100644 --- a/app/(auth)/(tabs)/(libraries)/_layout.tsx +++ b/app/(auth)/(tabs)/(libraries)/_layout.tsx @@ -16,7 +16,7 @@ export default function IndexLayout() { return ( {t("library.options.display")} - + - + {t("library.options.display")} updateSettings({ @@ -75,12 +75,12 @@ export default function IndexLayout() { } > - + {t("library.options.row")} updateSettings({ @@ -92,14 +92,14 @@ export default function IndexLayout() { } > - + {t("library.options.list")} - + {t("library.options.image_style")} - + {t("library.options.poster")} updateSettings({ @@ -141,17 +141,17 @@ export default function IndexLayout() { } > - + {t("library.options.cover")} - + { if (settings.libraryOptions.imageStyle === "poster") @@ -165,12 +165,12 @@ export default function IndexLayout() { }} > - + {t("library.options.show_titles")} { updateSettings({ @@ -182,7 +182,7 @@ export default function IndexLayout() { }} > - + {t("library.options.show_stats")} @@ -195,7 +195,7 @@ export default function IndexLayout() { }} /> ))} { const response = await getUserViewsApi(api!).getUserViews({ @@ -41,7 +41,7 @@ export default function index() { ?.filter((l) => !settings?.hiddenLibraries?.includes(l.Id!)) .filter((l) => l.CollectionType !== "music") .filter((l) => l.CollectionType !== "books") || [], - [data, settings?.hiddenLibraries] + [data, settings?.hiddenLibraries], ); useEffect(() => { @@ -65,22 +65,24 @@ export default function index() { if (isLoading) return ( - + ); if (!libraries) return ( - - {t("library.no_libraries_found")} + + + {t("library.no_libraries_found")} + ); return ( ) : ( - + ) } estimatedItemSize={200} diff --git a/app/(auth)/(tabs)/(search)/_layout.tsx b/app/(auth)/(tabs)/(search)/_layout.tsx index ce787316..6156546b 100644 --- a/app/(auth)/(tabs)/(search)/_layout.tsx +++ b/app/(auth)/(tabs)/(search)/_layout.tsx @@ -3,15 +3,15 @@ import { nestedTabPageScreenOptions, } from "@/components/stacks/NestedTabPageStack"; import { Stack } from "expo-router"; -import { Platform } from "react-native"; import { useTranslation } from "react-i18next"; +import { Platform } from "react-native"; export default function SearchLayout() { const { t } = useTranslation(); return ( ))} - + diff --git a/app/(auth)/(tabs)/(search)/index.tsx b/app/(auth)/(tabs)/(search)/index.tsx index 4d4686b9..aaef84d6 100644 --- a/app/(auth)/(tabs)/(search)/index.tsx +++ b/app/(auth)/(tabs)/(search)/index.tsx @@ -1,30 +1,43 @@ -import {Text} from "@/components/common/Text"; -import {TouchableItemRouter} from "@/components/common/TouchableItemRouter"; import ContinueWatchingPoster from "@/components/ContinueWatchingPoster"; -import {Tag} from "@/components/GenreTags"; -import {ItemCardText} from "@/components/ItemCardText"; -import {JellyseerrSearchSort, JellyserrIndexPage} from "@/components/jellyseerr/JellyseerrIndexPage"; +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 { + JellyseerrSearchSort, + JellyserrIndexPage, +} from "@/components/jellyseerr/JellyseerrIndexPage"; import MoviePoster from "@/components/posters/MoviePoster"; import SeriesPoster from "@/components/posters/SeriesPoster"; -import {LoadingSkeleton} from "@/components/search/LoadingSkeleton"; -import {SearchItemWrapper} from "@/components/search/SearchItemWrapper"; -import {useJellyseerr} from "@/hooks/useJellyseerr"; -import {apiAtom, userAtom} from "@/providers/JellyfinProvider"; -import {useSettings} from "@/utils/atoms/settings"; -import {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 { LoadingSkeleton } from "@/components/search/LoadingSkeleton"; +import { SearchItemWrapper } from "@/components/search/SearchItemWrapper"; +import { useJellyseerr } from "@/hooks/useJellyseerr"; +import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; +import { sortOrderOptions } from "@/utils/atoms/filters"; +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 {Platform, ScrollView, TouchableOpacity, View} from "react-native"; -import {useSafeAreaInsets} from "react-native-safe-area-context"; -import {useDebounce} from "use-debounce"; -import {useTranslation} from "react-i18next"; -import {eventBus} from "@/utils/eventBus"; -import {sortOrderOptions} from "@/utils/atoms/filters"; -import {FilterButton} from "@/components/filters/FilterButton"; +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"; @@ -55,8 +68,15 @@ export default function search() { const [settings] = useSettings(); const { jellyseerrApi } = useJellyseerr(); - const [jellyseerrOrderBy, setJellyseerrOrderBy] = useState(JellyseerrSearchSort[JellyseerrSearchSort.DEFAULT] as unknown as JellyseerrSearchSort) - const [jellyseerrSortOrder, setJellyseerrSortOrder] = useState<"asc" | "desc">("desc") + const [jellyseerrOrderBy, setJellyseerrOrderBy] = + useState( + JellyseerrSearchSort[ + JellyseerrSearchSort.DEFAULT + ] as unknown as JellyseerrSearchSort, + ); + const [jellyseerrSortOrder, setJellyseerrSortOrder] = useState< + "asc" | "desc" + >("desc"); const searchEngine = useMemo(() => { return settings?.searchEngine || "Jellyfin"; @@ -112,7 +132,7 @@ export default function search() { return []; // Ensure an empty array is returned in case of an error } }, - [api, searchEngine, settings] + [api, searchEngine, settings], ); type HeaderSearchBarRef = { @@ -220,26 +240,29 @@ export default function search() { return ( <> {jellyseerrApi && ( <> - + setSearchType("Library")}> setSearchType("Discover")}> - {searchType === "Discover" && !loading && noResults && debouncedSearch.length > 0 && ( - - Object.keys(JellyseerrSearchSort).filter(v => 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 === "Discover" && + !loading && + noResults && + debouncedSearch.length > 0 && ( + + + Object.keys(JellyseerrSearchSort).filter((v) => + 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} + /> + + )} )} - + @@ -294,14 +326,14 @@ export default function search() { renderItem={(item: BaseItemDto) => ( - + {item.Name} - + {item.ProductionYear} @@ -314,13 +346,13 @@ export default function search() { - + {item.Name} - + {item.ProductionYear} @@ -333,7 +365,7 @@ export default function search() { @@ -347,10 +379,10 @@ export default function search() { - + {item.Name} @@ -363,7 +395,7 @@ export default function search() { @@ -383,22 +415,22 @@ export default function search() { <> {!loading && noResults && debouncedSearch.length > 0 ? ( - + {t("search.no_results_found_for")} - + "{debouncedSearch}" ) : debouncedSearch.length === 0 ? ( - + {exampleSearches.map((e) => ( setSearch(e)} key={e} - className="mb-2" + className='mb-2' > - {e} + {e} ))} diff --git a/app/(auth)/(tabs)/_layout.tsx b/app/(auth)/(tabs)/_layout.tsx index 6f581ae0..f26309b7 100644 --- a/app/(auth)/(tabs)/_layout.tsx +++ b/app/(auth)/(tabs)/_layout.tsx @@ -1,26 +1,26 @@ import React, { useCallback, useRef } from "react"; -import { Platform } from "react-native"; import { useTranslation } from "react-i18next"; +import { Platform } from "react-native"; import { useFocusEffect, useRouter, withLayoutContext } from "expo-router"; import { + type NativeBottomTabNavigationEventMap, createNativeBottomTabNavigator, - NativeBottomTabNavigationEventMap, } from "@bottom-tabs/react-navigation"; const { Navigator } = createNativeBottomTabNavigator(); -import { BottomTabNavigationOptions } from "@react-navigation/bottom-tabs"; +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"; import type { ParamListBase, TabNavigationState, } from "@react-navigation/native"; import { SystemBars } from "react-native-edge-to-edge"; -import { eventBus } from "@/utils/eventBus"; export const NativeTabs = withLayoutContext< BottomTabNavigationOptions, @@ -46,12 +46,12 @@ export default function TabLayout() { clearTimeout(timer); }; } - }, []) + }, []), ); return ( <> - @@ -107,7 +108,7 @@ const Login: React.FC = () => { } else { Alert.alert( t("login.connection_failed"), - t("login.an_unexpected_error_occured") + t("login.an_unexpected_error_occured"), ); } } finally { @@ -176,7 +177,7 @@ const Login: React.FC = () => { if (result === undefined) { Alert.alert( t("login.connection_failed"), - t("login.could_not_connect_to_server") + t("login.could_not_connect_to_server"), ); return; } @@ -195,13 +196,13 @@ const Login: React.FC = () => { { text: t("login.got_it"), }, - ] + ], ); } } catch (error) { Alert.alert( t("login.error_title"), - t("login.failed_to_initiate_quick_connect") + t("login.failed_to_initiate_quick_connect"), ); } }; @@ -213,22 +214,22 @@ const Login: React.FC = () => { > {api?.basePath ? ( <> - - - - + + + + <> {serverName ? ( <> {t("login.login_to_title") + " "} - {serverName} + {serverName} ) : ( t("login.login_title") )} - + {api.basePath} { setCredentials({ ...credentials, username: text }) } value={credentials.username} - keyboardType="default" - returnKeyType="done" - autoCapitalize="none" + 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" + textContentType='oneTimeCode' + clearButtonMode='while-editing' maxLength={500} /> @@ -254,42 +255,42 @@ const Login: React.FC = () => { } value={credentials.password} secureTextEntry - keyboardType="default" - returnKeyType="done" - autoCapitalize="none" - textContentType="password" - clearButtonMode="while-editing" + keyboardType='default' + returnKeyType='done' + autoCapitalize='none' + textContentType='password' + clearButtonMode='while-editing' maxLength={500} /> - + - + ) : ( <> - - + + { }} source={require("@/assets/images/StreamyFinFinal.png")} /> - Streamyfin - + Streamyfin + {t("server.enter_url_to_jellyfin_server")} diff --git a/augmentations/api.ts b/augmentations/api.ts index fa20552d..b79e341a 100644 --- a/augmentations/api.ts +++ b/augmentations/api.ts @@ -1,21 +1,21 @@ -import { Api, AUTHORIZATION_HEADER } from "@jellyfin/sdk"; -import { AxiosRequestConfig, AxiosResponse } from "axios"; -import { StreamyfinPluginConfig } from "@/utils/atoms/settings"; +import type { StreamyfinPluginConfig } from "@/utils/atoms/settings"; +import { AUTHORIZATION_HEADER, Api } from "@jellyfin/sdk"; +import type { AxiosRequestConfig, AxiosResponse } from "axios"; declare module "@jellyfin/sdk" { interface Api { get( url: string, - config?: AxiosRequestConfig + config?: AxiosRequestConfig, ): Promise>; post( url: string, data: D, - config?: AxiosRequestConfig + config?: AxiosRequestConfig, ): Promise>; delete( url: string, - config?: AxiosRequestConfig + config?: AxiosRequestConfig, ): Promise>; getStreamyfinPluginConfig(): Promise>; } @@ -23,7 +23,7 @@ declare module "@jellyfin/sdk" { Api.prototype.get = function ( url: string, - config: AxiosRequestConfig = {} + config: AxiosRequestConfig = {}, ): Promise> { return this.axiosInstance.get(`${this.basePath}${url}`, { ...(config ?? {}), @@ -34,7 +34,7 @@ Api.prototype.get = function ( Api.prototype.post = function ( url: string, data: D, - config: AxiosRequestConfig + config: AxiosRequestConfig, ): Promise> { return this.axiosInstance.post(`${this.basePath}${url}`, data, { ...(config || {}), @@ -44,7 +44,7 @@ Api.prototype.post = function ( Api.prototype.delete = function ( url: string, - config: AxiosRequestConfig + config: AxiosRequestConfig, ): Promise> { return this.axiosInstance.delete(`${this.basePath}${url}`, { ...(config || {}), diff --git a/augmentations/mmkv.ts b/augmentations/mmkv.ts index 5667502f..a6b35d22 100644 --- a/augmentations/mmkv.ts +++ b/augmentations/mmkv.ts @@ -1,22 +1,21 @@ -import {MMKV} from "react-native-mmkv"; +import { MMKV } from "react-native-mmkv"; declare module "react-native-mmkv" { interface MMKV { - get(key: string): T | undefined - setAny(key: string, value: any | undefined): void + get(key: string): T | undefined; + setAny(key: string, value: any | undefined): void; } } -MMKV.prototype.get = function (key: string): T | undefined { +MMKV.prototype.get = function (key: string): T | undefined { const serializedItem = this.getString(key); return serializedItem ? JSON.parse(serializedItem) : undefined; -} +}; MMKV.prototype.setAny = function (key: string, value: any | undefined): void { if (value === undefined) { - this.delete(key) - } - else { + this.delete(key); + } else { this.set(key, JSON.stringify(value)); } -} \ No newline at end of file +}; diff --git a/augmentations/number.ts b/augmentations/number.ts index 15b70507..a60f8811 100644 --- a/augmentations/number.ts +++ b/augmentations/number.ts @@ -7,17 +7,19 @@ declare global { } } -Number.prototype.bytesToReadable = function (decimals: number = 2) { +Number.prototype.bytesToReadable = function (decimals = 2) { const bytes = this.valueOf(); - if (bytes === 0) return '0 Bytes'; + if (bytes === 0) return "0 Bytes"; const k = 1024; const dm = decimals < 0 ? 0 : decimals; - const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; + const sizes = ["Bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]; const i = Math.floor(Math.log(bytes) / Math.log(k)); - return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]; + return ( + Number.parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + " " + sizes[i] + ); }; Number.prototype.secondsToMilliseconds = function () { diff --git a/augmentations/string.ts b/augmentations/string.ts index 75a97f05..f4a50b55 100644 --- a/augmentations/string.ts +++ b/augmentations/string.ts @@ -5,12 +5,10 @@ declare global { } String.prototype.toTitle = function () { - return this - .replaceAll("_", " ") - .replace( - /\w\S*/g, - text => text.charAt(0).toUpperCase() + text.substring(1).toLowerCase() - ); -} + return this.replaceAll("_", " ").replace( + /\w\S*/g, + (text) => text.charAt(0).toUpperCase() + text.substring(1).toLowerCase(), + ); +}; -export {}; \ No newline at end of file +export {}; diff --git a/babel.config.js b/babel.config.js index 98ac332d..41dc7e41 100644 --- a/babel.config.js +++ b/babel.config.js @@ -1,4 +1,4 @@ -module.exports = function (api) { +module.exports = (api) => { api.cache(true); return { presets: ["babel-preset-expo"], diff --git a/biome.json b/biome.json index cfee4e6d..fea15f82 100644 --- a/biome.json +++ b/biome.json @@ -7,6 +7,10 @@ "linter": { "enabled": true, "rules": { + "style": { + "useImportType": "off", + "noNonNullAssertion": "off" + }, "recommended": true, "style": { "useImportType": "off", diff --git a/components/AddToFavorites.tsx b/components/AddToFavorites.tsx index b190dced..16e15694 100644 --- a/components/AddToFavorites.tsx +++ b/components/AddToFavorites.tsx @@ -1,21 +1,20 @@ - -import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client"; -import { useFavorite } from "@/hooks/useFavorite"; -import {View, ViewProps} from "react-native"; import { RoundButton } from "@/components/RoundButton"; -import {FC} from "react"; +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"; interface Props extends ViewProps { item: BaseItemDto; } -export const AddToFavorites:FC = ({ item, ...props }) => { +export const AddToFavorites: FC = ({ item, ...props }) => { const { isFavorite, toggleFavorite } = useFavorite(item); return ( { source?: MediaSourceInfo; @@ -20,31 +20,31 @@ export const AudioTrackSelector: React.FC = ({ if (Platform.isTV) return null; const audioStreams = useMemo( () => source?.MediaStreams?.filter((x) => x.Type === "Audio"), - [source] + [source], ); const selectedAudioSteam = useMemo( () => audioStreams?.find((x) => x.Index === selected), - [audioStreams, selected] + [audioStreams, selected], ); const { t } = useTranslation(); return ( - - + + {t("item_card.audio")} - - + + {selectedAudioSteam?.DisplayTitle} @@ -52,8 +52,8 @@ export const AudioTrackSelector: React.FC = ({ = ({ ${variant === "gray" && "bg-neutral-800"} `} > - {iconLeft && {iconLeft}} + {iconLeft && {iconLeft}} (b.value || Infinity) - (a.value || Infinity)); +].sort( + (a, b) => + (b.value || Number.POSITIVE_INFINITY) - + (a.value || Number.POSITIVE_INFINITY), +); interface Props extends React.ComponentProps { onChange: (value: Bitrate) => void; @@ -58,10 +62,14 @@ export const BitrateSelector: React.FC = ({ const sorted = useMemo(() => { if (inverted) return BITRATES.sort( - (a, b) => (a.value || Infinity) - (b.value || Infinity) + (a, b) => + (a.value || Number.POSITIVE_INFINITY) - + (b.value || Number.POSITIVE_INFINITY), ); return BITRATES.sort( - (a, b) => (b.value || Infinity) - (a.value || Infinity) + (a, b) => + (b.value || Number.POSITIVE_INFINITY) - + (a.value || Number.POSITIVE_INFINITY), ); }, []); @@ -69,7 +77,7 @@ export const BitrateSelector: React.FC = ({ return ( = ({ > - - + + {t("item_card.quality")} - - + + {BITRATES.find((b) => b.value === selected?.value)?.key} @@ -90,8 +98,8 @@ export const BitrateSelector: React.FC = ({ > = ({ {...props} > {loading ? ( - + ) : ( @@ -72,7 +73,7 @@ export const Button: React.FC> = ({ flex flex-row items-center justify-between w-full ${justify === "between" ? "justify-between" : "justify-center"}`} > - {iconLeft ? iconLeft : } + {iconLeft ? iconLeft : } > = ({ > {children} - {iconRight ? iconRight : } + {iconRight ? iconRight : } )} diff --git a/components/Chromecast.tsx b/components/Chromecast.tsx index 4c66c953..22fd6384 100644 --- a/components/Chromecast.tsx +++ b/components/Chromecast.tsx @@ -1,6 +1,6 @@ import { Feather } from "@expo/vector-icons"; import React, { useCallback, useEffect } from "react"; -import { Platform, TouchableOpacity, ViewProps } from "react-native"; +import { Platform, TouchableOpacity, type ViewProps } from "react-native"; import GoogleCast, { CastButton, CastContext, @@ -45,18 +45,18 @@ export function Chromecast({ const AndroidCastButton = useCallback( () => Platform.OS === "android" ? ( - + ) : ( <> ), - [Platform.OS] + [Platform.OS], ); if (background === "transparent") return ( { if (mediaStatus?.currentItemId) CastContext.showExpandedControls(); @@ -65,13 +65,13 @@ export function Chromecast({ {...props} > - + ); return ( { if (mediaStatus?.currentItemId) CastContext.showExpandedControls(); else CastContext.showCastDialog(); @@ -79,7 +79,7 @@ export function Chromecast({ {...props} > - + ); } diff --git a/components/ContinueWatchingPoster.tsx b/components/ContinueWatchingPoster.tsx index a011d23e..02218cff 100644 --- a/components/ContinueWatchingPoster.tsx +++ b/components/ContinueWatchingPoster.tsx @@ -1,12 +1,12 @@ import { apiAtom } from "@/providers/JellyfinProvider"; -import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; +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 { View } from "react-native"; import { WatchedIndicator } from "./WatchedIndicator"; -import React from "react"; -import { Ionicons } from "@expo/vector-icons"; type ContinueWatchingPosterProps = { item: BaseItemDto; @@ -71,7 +71,7 @@ const ContinueWatchingPoster: React.FC = ({ if (!url) return ( - + ); return ( @@ -81,7 +81,7 @@ const ContinueWatchingPoster: React.FC = ({ ${size === "small" ? "w-32" : "w-44"} `} > - + = ({ uri: url, }} cachePolicy={"memory-disk"} - contentFit="cover" - className="w-full h-full" + contentFit='cover' + className='w-full h-full' /> {showPlayButton && ( - - + + )} diff --git a/components/DownloadItem.tsx b/components/DownloadItem.tsx index e7286023..1ea49626 100644 --- a/components/DownloadItem.tsx +++ b/components/DownloadItem.tsx @@ -10,29 +10,30 @@ import download from "@/utils/profiles/download"; import Ionicons from "@expo/vector-icons/Ionicons"; import { BottomSheetBackdrop, - BottomSheetBackdropProps, + type BottomSheetBackdropProps, BottomSheetModal, BottomSheetView, } from "@gorhom/bottom-sheet"; -import { +import type { BaseItemDto, MediaSourceInfo, } from "@jellyfin/sdk/lib/generated-client/models"; -import { Href, router, useFocusEffect } from "expo-router"; +import { type Href, router, useFocusEffect } from "expo-router"; +import { t } from "i18next"; import { useAtom } from "jotai"; -import React, { useCallback, useMemo, useRef, useState } from "react"; -import { Alert, Platform, View, ViewProps } from "react-native"; +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 { AudioTrackSelector } from "./AudioTrackSelector"; -import { Bitrate, BitrateSelector } from "./BitrateSelector"; +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 { t } from "i18next"; +import { Text } from "./common/Text"; interface DownloadProps extends ViewProps { items: BaseItemDto[]; @@ -70,16 +71,16 @@ export const DownloadItems: React.FC = ({ settings?.defaultBitrate ?? { key: "Max", value: undefined, - } + }, ); const userCanDownload = useMemo( () => user?.Policy?.EnableContentDownloading, - [user] + [user], ); const usingOptimizedServer = useMemo( () => settings?.downloadMethod === DownloadMethod.Optimized, - [settings] + [settings], ); const bottomSheetModalRef = useRef(null); @@ -99,7 +100,7 @@ export const DownloadItems: React.FC = ({ const itemsNotDownloaded = useMemo( () => items.filter((i) => !downloadedFiles?.some((f) => f.item.Id === i.Id)), - [items, downloadedFiles] + [items, downloadedFiles], ); const allItemsDownloaded = useMemo(() => { @@ -108,7 +109,7 @@ export const DownloadItems: React.FC = ({ }, [items, itemsNotDownloaded]); const itemsProcesses = useMemo( () => processes?.filter((p) => itemIds.includes(p.item.Id)), - [processes, itemIds] + [processes, itemIds], ); const progress = useMemo(() => { @@ -140,7 +141,7 @@ export const DownloadItems: React.FC = ({ params: { episodeSeasonIndex: firstItem.ParentIndexNumber, }, - } as Href) + } as Href), ); }; @@ -160,12 +161,12 @@ export const DownloadItems: React.FC = ({ id: item.Id!, execute: async () => await initiateDownload(item), item, - })) + })), ); } } else { toast.error( - t("home.downloads.toasts.you_are_not_allowed_to_download_files") + t("home.downloads.toasts.you_are_not_allowed_to_download_files"), ); } }, [ @@ -189,7 +190,7 @@ export const DownloadItems: React.FC = ({ (itemsNotDownloaded.length === 1 && !selectedMediaSource?.Id) ) { throw new Error( - "DownloadItem ~ initiateDownload: No api or user or item" + "DownloadItem ~ initiateDownload: No api or user or item", ); } let mediaSource = selectedMediaSource; @@ -220,7 +221,7 @@ export const DownloadItems: React.FC = ({ if (!res) { Alert.alert( t("home.downloads.something_went_wrong"), - t("home.downloads.could_not_get_stream_url_from_jellyfin") + t("home.downloads.could_not_get_stream_url_from_jellyfin"), ); continue; } @@ -250,7 +251,7 @@ export const DownloadItems: React.FC = ({ usingOptimizedServer, startBackgroundDownload, startRemuxing, - ] + ], ); const renderBackdrop = useCallback( @@ -261,7 +262,7 @@ export const DownloadItems: React.FC = ({ appearsOnIndex={0} /> ), - [] + [], ); useFocusEffect( useCallback(() => { @@ -274,7 +275,7 @@ export const DownloadItems: React.FC = ({ setSelectedAudioStream(audioIndex ?? 0); setSelectedSubtitleStream(subtitleIndex ?? -1); setMaxBitrate(bitrate); - }, [items, itemsNotDownloaded, settings]) + }, [items, itemsNotDownloaded, settings]), ); const renderButtonContent = () => { @@ -282,18 +283,18 @@ export const DownloadItems: React.FC = ({ return progress === 0 ? ( ) : ( - + ); } else if (itemsQueued) { - return ; + return ; } else if (allItemsDownloaded) { return ; } else { @@ -331,19 +332,19 @@ export const DownloadItems: React.FC = ({ backdropComponent={renderBackdrop} > - + - + {title} - + {subtitle || t("item_card.download.download_x_item", { item_count: itemsNotDownloaded.length, })} - + = ({ selected={selectedMediaSource} /> {selectedMediaSource && ( - + = ({ )} - - + + {usingOptimizedServer ? t("item_card.download.using_optimized_server") : t("item_card.download.using_default_method")} @@ -411,10 +412,10 @@ export const DownloadSingleItem: React.FC<{ subtitle={item.Name!} items={[item]} MissingDownloadIconComponent={() => ( - + )} DownloadedIconComponent={() => ( - + )} /> ); diff --git a/components/GenreTags.tsx b/components/GenreTags.tsx index ce907d1b..6708269d 100644 --- a/components/GenreTags.tsx +++ b/components/GenreTags.tsx @@ -1,49 +1,57 @@ // GenreTags.tsx -import React from "react"; -import {StyleProp, TextStyle, View, ViewProps} from "react-native"; +import type React from "react"; +import { + type StyleProp, + type TextStyle, + View, + type ViewProps, +} from "react-native"; import { Text } from "./common/Text"; interface TagProps { tags?: string[]; - textClass?: ViewProps["className"] + textClass?: ViewProps["className"]; } -export const Tag: React.FC<{ text: string, textClass?: ViewProps["className"], textStyle?: StyleProp} & ViewProps> = ({ - text, - textClass, - textStyle, - ...props -}) => { +export const Tag: React.FC< + { + text: string; + textClass?: ViewProps["className"]; + textStyle?: StyleProp; + } & ViewProps +> = ({ text, textClass, textStyle, ...props }) => { return ( - - {text} + + + {text} + ); }; -export const Tags: React.FC = ({ - tags, - textClass = "text-xs", - tagProps, - ...props -}) => { +export const Tags: React.FC< + TagProps & { tagProps?: ViewProps } & ViewProps +> = ({ tags, textClass = "text-xs", tagProps, ...props }) => { if (!tags || tags.length === 0) return null; return ( - + {tags.map((tag, idx) => ( - + ))} ); }; -export const GenreTags: React.FC<{ genres?: string[]}> = ({ genres }) => { +export const GenreTags: React.FC<{ genres?: string[] }> = ({ genres }) => { return ( - - + + ); }; diff --git a/components/ItemCardText.tsx b/components/ItemCardText.tsx index 2a9995e1..8ce52da2 100644 --- a/components/ItemCardText.tsx +++ b/components/ItemCardText.tsx @@ -1,8 +1,8 @@ -import React from "react"; +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"; import { Text } from "./common/Text"; -import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; -import { tc } from "@/utils/textTools"; type ItemCardProps = { item: BaseItemDto; @@ -10,13 +10,13 @@ type ItemCardProps = { export const ItemCardText: React.FC = ({ item }) => { return ( - + {item.Type === "Episode" ? ( <> - + {item.Name} - + {`S${item.ParentIndexNumber?.toString()}:E${item.IndexNumber?.toString()}`} {" - "} {item.SeriesName} @@ -24,8 +24,10 @@ export const ItemCardText: React.FC = ({ item }) => { ) : ( <> - {item.Name} - {item.ProductionYear} + + {item.Name} + + {item.ProductionYear} )} diff --git a/components/ItemContent.tsx b/components/ItemContent.tsx index 1f9077f8..f27e0f41 100644 --- a/components/ItemContent.tsx +++ b/components/ItemContent.tsx @@ -1,5 +1,5 @@ import { AudioTrackSelector } from "@/components/AudioTrackSelector"; -import { Bitrate, BitrateSelector } from "@/components/BitrateSelector"; +import { type Bitrate, BitrateSelector } from "@/components/BitrateSelector"; import { DownloadSingleItem } from "@/components/DownloadItem"; import { OverviewText } from "@/components/OverviewText"; import { ParallaxScrollView } from "@/components/ParallaxPage"; @@ -19,7 +19,7 @@ import * as ScreenOrientation from "@/packages/expo-screen-orientation"; import { apiAtom } from "@/providers/JellyfinProvider"; import { useSettings } from "@/utils/atoms/settings"; import { getLogoImageUrlById } from "@/utils/jellyfin/image/getLogoImageUrlById"; -import { +import type { BaseItemDto, MediaSourceInfo, } from "@jellyfin/sdk/lib/generated-client/models"; @@ -86,18 +86,18 @@ export const ItemContent: React.FC<{ item: BaseItemDto }> = React.memo( navigation.setOptions({ headerRight: () => item && ( - + {item.Type !== "Program" && ( - + {!Platform.isTV && ( - + )} - + )} @@ -123,7 +123,7 @@ export const ItemContent: React.FC<{ item: BaseItemDto }> = React.memo( return ( = React.memo( } > - - - + + + {item.Type !== "Program" && !Platform.isTV && ( - + setSelectedOptions( - (prev) => prev && { ...prev, bitrate: val } + (prev) => prev && { ...prev, bitrate: val }, ) } selected={selectedOptions.bitrate} /> setSelectedOptions( @@ -188,13 +188,13 @@ export const ItemContent: React.FC<{ item: BaseItemDto }> = React.memo( prev && { ...prev, mediaSource: val, - } + }, ) } selected={selectedOptions.mediaSource} /> { setSelectedOptions( @@ -202,7 +202,7 @@ export const ItemContent: React.FC<{ item: BaseItemDto }> = React.memo( prev && { ...prev, audioIndex: val, - } + }, ); }} selected={selectedOptions.audioIndex} @@ -215,7 +215,7 @@ export const ItemContent: React.FC<{ item: BaseItemDto }> = React.memo( prev && { ...prev, subtitleIndex: val, - } + }, ) } selected={selectedOptions.subtitleIndex} @@ -224,7 +224,7 @@ export const ItemContent: React.FC<{ item: BaseItemDto }> = React.memo( )} @@ -235,24 +235,24 @@ export const ItemContent: React.FC<{ item: BaseItemDto }> = React.memo( )} - + {item.Type !== "Program" && ( <> {item.Type === "Episode" && ( - + )} - + {item.People && item.People.length > 0 && ( - + {item.People.slice(0, 3).map((person, idx) => ( ))} @@ -265,5 +265,5 @@ export const ItemContent: React.FC<{ item: BaseItemDto }> = React.memo( ); - } + }, ); diff --git a/components/ItemHeader.tsx b/components/ItemHeader.tsx index ac6bbe4c..8d218d82 100644 --- a/components/ItemHeader.tsx +++ b/components/ItemHeader.tsx @@ -1,9 +1,9 @@ -import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; -import React from "react"; -import { View, ViewProps } from "react-native"; +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 { MoviesTitleHeader } from "./movies/MoviesTitleHeader"; import { Ratings } from "./Ratings"; +import { MoviesTitleHeader } from "./movies/MoviesTitleHeader"; import { EpisodeTitleHeader } from "./series/EpisodeTitleHeader"; import { ItemActions } from "./series/SeriesActions"; @@ -15,21 +15,21 @@ export const ItemHeader: React.FC = ({ item, ...props }) => { if (!item) return ( - - - - + + + + ); return ( - - - - + + + + {item.Type === "Episode" && ( diff --git a/components/ItemTechnicalDetails.tsx b/components/ItemTechnicalDetails.tsx index e2570dc8..b0354fa0 100644 --- a/components/ItemTechnicalDetails.tsx +++ b/components/ItemTechnicalDetails.tsx @@ -1,22 +1,23 @@ +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, - type MediaStream, + MediaStream, } from "@jellyfin/sdk/lib/generated-client"; -import React, { useMemo, useRef } from "react"; +import type React from "react"; +import { useMemo, useRef } from "react"; +import { useTranslation } from "react-i18next"; import { TouchableOpacity, View } from "react-native"; import { Badge } from "./Badge"; -import { Text } from "./common/Text"; -import { - BottomSheetModal, - BottomSheetBackdropProps, - BottomSheetBackdrop, - BottomSheetView, - BottomSheetScrollView, -} from "@gorhom/bottom-sheet"; import { Button } from "./Button"; -import { useTranslation } from "react-i18next"; -import { formatBitrate } from "@/utils/bitrate"; +import { Text } from "./common/Text"; interface Props { source?: MediaSourceInfo; @@ -27,13 +28,13 @@ export const ItemTechnicalDetails: React.FC = ({ source, ...props }) => { const { t } = useTranslation(); return ( - - {t("item_card.video")} + + {t("item_card.video")} bottomSheetModalRef.current?.present()}> - + - {t("item_card.more_details")} + {t("item_card.more_details")} = ({ source, ...props }) => { )} > - - - + + + {t("item_card.video")} - + - - + + {t("item_card.audio")} stream.Type === "Audio" + (stream) => stream.Type === "Audio", ) || [] } /> - - + + {t("item_card.subtitles")} stream.Type === "Subtitle" + (stream) => stream.Type === "Subtitle", ) || [] } /> @@ -101,25 +102,25 @@ const SubtitleStreamInfo = ({ subtitleStreams: MediaStream[]; }) => { return ( - + {subtitleStreams.map((stream, index) => ( - - + + {stream.DisplayTitle} - + + } text={stream.Language} /> + } /> @@ -131,40 +132,40 @@ const SubtitleStreamInfo = ({ const AudioStreamInfo = ({ audioStreams }: { audioStreams: MediaStream[] }) => { return ( - + {audioStreams.map((audioStreams, index) => ( - - + + {audioStreams.DisplayTitle} - + + } text={audioStreams.Language} /> } text={audioStreams.Codec} /> } + variant='gray' + iconLeft={} text={audioStreams.ChannelLayout} /> + } text={formatBitrate(audioStreams.BitRate)} /> @@ -180,48 +181,48 @@ const VideoStreamInfo = ({ source }: { source?: MediaSourceInfo }) => { const videoStream = useMemo(() => { return source.MediaStreams?.find( - (stream) => stream.Type === "Video" + (stream) => stream.Type === "Video", ) as MediaStream; }, [source.MediaStreams]); if (!videoStream) return null; return ( - + } + variant='gray' + iconLeft={} text={formatFileSize(source.Size)} /> } + variant='gray' + iconLeft={} text={`${videoStream.Width}x${videoStream.Height}`} /> + } text={videoStream.VideoRange} /> + } text={videoStream.Codec} /> + } text={formatBitrate(videoStream.BitRate)} /> } + variant='gray' + iconLeft={} text={`${videoStream.AverageFrameRate?.toFixed(0)} fps`} /> @@ -233,6 +234,8 @@ const formatFileSize = (bytes?: number | null) => { const sizes = ["Bytes", "KB", "MB", "GB", "TB"]; if (bytes === 0) return "0 Byte"; - const i = parseInt(Math.floor(Math.log(bytes) / Math.log(1024)).toString()); + const i = Number.parseInt( + Math.floor(Math.log(bytes) / Math.log(1024)).toString(), + ); return Math.round((bytes / Math.pow(1024, i)) * 100) / 100 + " " + sizes[i]; }; diff --git a/components/JellyfinServerDiscovery.tsx b/components/JellyfinServerDiscovery.tsx index dc2c46ad..0ea85a4a 100644 --- a/components/JellyfinServerDiscovery.tsx +++ b/components/JellyfinServerDiscovery.tsx @@ -1,10 +1,10 @@ -import React from "react"; -import { View, Text, TouchableOpacity } from "react-native"; import { useJellyfinDiscovery } from "@/hooks/useJellyfinDiscovery"; +import type React from "react"; +import { useTranslation } from "react-i18next"; +import { Text, TouchableOpacity, View } from "react-native"; import { Button } from "./Button"; import { ListGroup } from "./list/ListGroup"; import { ListItem } from "./list/ListItem"; -import { useTranslation } from "react-i18next"; interface Props { onServerSelect?: (server: { address: string; serverName?: string }) => void; @@ -15,15 +15,17 @@ const JellyfinServerDiscovery: React.FC = ({ onServerSelect }) => { const { t } = useTranslation(); return ( - - {servers.length ? ( - + {servers.map((server) => ( { item: BaseItemDto; @@ -24,9 +24,9 @@ export const MediaSourceSelector: React.FC = ({ const selectedName = useMemo( () => item.MediaSources?.find((x) => x.Id === selected?.Id)?.MediaStreams?.find( - (x) => x.Type === "Video" + (x) => x.Type === "Video", )?.DisplayTitle || "", - [item, selected] + [item, selected], ); const { t } = useTranslation(); @@ -54,26 +54,26 @@ export const MediaSourceSelector: React.FC = ({ return ( - - + + {t("item_card.video")} - + {selectedName} = ({ return ( - - {t("item_card.more_with", {name: actor?.Name})} + + {t("item_card.more_with", { name: actor?.Name })} = ({ diff --git a/components/OverviewText.tsx b/components/OverviewText.tsx index b775b1c9..e7ed2e97 100644 --- a/components/OverviewText.tsx +++ b/components/OverviewText.tsx @@ -1,8 +1,8 @@ -import { TouchableOpacity, View, ViewProps } from "react-native"; import { Text } from "@/components/common/Text"; import { tc } from "@/utils/textTools"; import { useState } from "react"; import { useTranslation } from "react-i18next"; +import { TouchableOpacity, View, type ViewProps } from "react-native"; interface Props extends ViewProps { text?: string | null; @@ -20,20 +20,22 @@ export const OverviewText: React.FC = ({ if (!text) return null; return ( - - {t("item_card.overview")} + + {t("item_card.overview")} setLimit((prev) => - prev === characterLimit ? text.length : characterLimit + prev === characterLimit ? text.length : characterLimit, ) } > {tc(text, limit)} {text.length > characterLimit && ( - - {limit === characterLimit ? t("item_card.show_more") : t("item_card.show_less")} + + {limit === characterLimit + ? t("item_card.show_more") + : t("item_card.show_less")} )} diff --git a/components/ParallaxPage.tsx b/components/ParallaxPage.tsx index 5d7b28e0..2efb8395 100644 --- a/components/ParallaxPage.tsx +++ b/components/ParallaxPage.tsx @@ -1,6 +1,11 @@ import { LinearGradient } from "expo-linear-gradient"; -import { type PropsWithChildren, type ReactElement } from "react"; -import {NativeScrollEvent, NativeSyntheticEvent, View, ViewProps} from "react-native"; +import type { PropsWithChildren, ReactElement } from "react"; +import { + type NativeScrollEvent, + NativeSyntheticEvent, + View, + type ViewProps, +} from "react-native"; import Animated, { interpolate, useAnimatedRef, @@ -35,36 +40,40 @@ export const ParallaxScrollView: React.FC> = ({ translateY: interpolate( scrollOffset.value, [-headerHeight, 0, headerHeight], - [-headerHeight / 2, 0, headerHeight * 0.75] + [-headerHeight / 2, 0, headerHeight * 0.75], ), }, { scale: interpolate( scrollOffset.value, [-headerHeight, 0, headerHeight], - [2, 1, 1] + [2, 1, 1], ), }, ], }; }); - - function isCloseToBottom({layoutMeasurement, contentOffset, contentSize}: NativeScrollEvent) { - return layoutMeasurement.height + contentOffset.y >= contentSize.height - 20; + function isCloseToBottom({ + layoutMeasurement, + contentOffset, + contentSize, + }: NativeScrollEvent) { + return ( + layoutMeasurement.height + contentOffset.y >= contentSize.height - 20 + ); } return ( - + { - if (isCloseToBottom(e.nativeEvent)) - onEndReached?.() + onScroll={(e) => { + if (isCloseToBottom(e.nativeEvent)) onEndReached?.(); }} > {logo && ( @@ -73,7 +82,7 @@ export const ParallaxScrollView: React.FC> = ({ top: headerHeight - 200, height: 130, }} - className="absolute left-0 w-full z-40 px-4 flex justify-center items-center" + className='absolute left-0 w-full z-40 px-4 flex justify-center items-center' > {logo} @@ -95,7 +104,7 @@ export const ParallaxScrollView: React.FC> = ({ style={{ top: -50, }} - className="relative flex-1 bg-transparent pb-24" + className='relative flex-1 bg-transparent pb-24' > { item: BaseItemDto; @@ -74,7 +74,7 @@ export const PlayButton: React.FC = ({ (q: string) => { router.push(`/player/direct-player?${q}`); }, - [router] + [router], ); const onPress = useCallback(async () => { @@ -140,7 +140,7 @@ export const PlayButton: React.FC = ({ console.warn("No URL returned from getStreamUrl", data); Alert.alert( t("player.client_error"), - t("player.could_not_create_stream_for_chromecast") + t("player.could_not_create_stream_for_chromecast"), ); return; } @@ -170,36 +170,36 @@ export const PlayButton: React.FC = ({ ], } : item.Type === "Movie" - ? { - type: "movie", - title: item.Name || "", - subtitle: item.Overview || "", - images: [ - { - url: getPrimaryImageUrl({ - api, - item, - quality: 90, - width: 2000, - })!, - }, - ], - } - : { - type: "generic", - title: item.Name || "", - subtitle: item.Overview || "", - images: [ - { - url: getPrimaryImageUrl({ - api, - item, - quality: 90, - width: 2000, - })!, - }, - ], - }, + ? { + type: "movie", + title: item.Name || "", + subtitle: item.Overview || "", + images: [ + { + url: getPrimaryImageUrl({ + api, + item, + quality: 90, + width: 2000, + })!, + }, + ], + } + : { + type: "generic", + title: item.Name || "", + subtitle: item.Overview || "", + images: [ + { + url: getPrimaryImageUrl({ + api, + item, + quality: 90, + width: 2000, + })!, + }, + ], + }, }, startTime: 0, }) @@ -222,7 +222,7 @@ export const PlayButton: React.FC = ({ case cancelButtonIndex: break; } - } + }, ); }, [ item, @@ -243,7 +243,7 @@ export const PlayButton: React.FC = ({ return userData.PlaybackPositionTicks > 0 ? Math.max( (userData.PlaybackPositionTicks / item.RunTimeTicks) * 100, - MIN_PLAYBACK_WIDTH + MIN_PLAYBACK_WIDTH, ) : 0; } @@ -260,7 +260,7 @@ export const PlayButton: React.FC = ({ easing: Easing.bezier(0.7, 0, 0.3, 1.0), }); }, - [item] + [item], ); useAnimatedReaction( @@ -273,7 +273,7 @@ export const PlayButton: React.FC = ({ easing: Easing.bezier(0.9, 0, 0.31, 0.99), }); }, - [colorAtom] + [colorAtom], ); useEffect(() => { @@ -294,7 +294,7 @@ export const PlayButton: React.FC = ({ backgroundColor: interpolateColor( colorChangeProgress.value, [0, 1], - [startColor.value.primary, endColor.value.primary] + [startColor.value.primary, endColor.value.primary], ), })); @@ -302,7 +302,7 @@ export const PlayButton: React.FC = ({ backgroundColor: interpolateColor( colorChangeProgress.value, [0, 1], - [startColor.value.primary, endColor.value.primary] + [startColor.value.primary, endColor.value.primary], ), })); @@ -310,7 +310,7 @@ export const PlayButton: React.FC = ({ width: `${interpolate( widthProgress.value, [0, 1], - [startWidth.value, targetWidth.value] + [startWidth.value, targetWidth.value], )}%`, })); @@ -318,7 +318,7 @@ export const PlayButton: React.FC = ({ color: interpolateColor( colorChangeProgress.value, [0, 1], - [startColor.value.text, endColor.value.text] + [startColor.value.text, endColor.value.text], ), })); /** @@ -328,13 +328,13 @@ export const PlayButton: React.FC = ({ return ( - + = ({ = ({ borderColor: colorAtom.primary, borderStyle: "solid", }} - className="flex flex-row items-center justify-center bg-transparent rounded-xl z-20 h-12 w-full " + className='flex flex-row items-center justify-center bg-transparent rounded-xl z-20 h-12 w-full ' > - + {runtimeTicksToMinutes(item?.RunTimeTicks)} - + {client && ( - - + + )} {!client && settings?.openInVLC && ( diff --git a/components/PlayButton.tv.tsx b/components/PlayButton.tv.tsx index 50be8c13..a68058c5 100644 --- a/components/PlayButton.tv.tsx +++ b/components/PlayButton.tv.tsx @@ -1,14 +1,16 @@ -import { Platform } from "react-native"; +import { useHaptic } from "@/hooks/useHaptic"; import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; import { itemThemeColorAtom } from "@/utils/atoms/primaryColor"; import { useSettings } from "@/utils/atoms/settings"; import { runtimeTicksToMinutes } from "@/utils/time"; import { useActionSheet } from "@expo/react-native-action-sheet"; import { Feather, Ionicons, MaterialCommunityIcons } from "@expo/vector-icons"; -import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client"; +import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client"; import { useRouter } from "expo-router"; import { useAtom, useAtomValue } from "jotai"; import { useCallback, useEffect } from "react"; +import { useTranslation } from "react-i18next"; +import { Platform } from "react-native"; import { Alert, TouchableOpacity, View } from "react-native"; import Animated, { Easing, @@ -20,10 +22,8 @@ import Animated, { useSharedValue, withTiming, } from "react-native-reanimated"; -import { Button } from "./Button"; -import { SelectedOptions } from "./ItemContent"; -import { useTranslation } from "react-i18next"; -import { useHaptic } from "@/hooks/useHaptic"; +import type { Button } from "./Button"; +import type { SelectedOptions } from "./ItemContent"; interface Props extends React.ComponentProps { item: BaseItemDto; @@ -34,10 +34,10 @@ const ANIMATION_DURATION = 500; const MIN_PLAYBACK_WIDTH = 15; export const PlayButton: React.FC = ({ - item, - selectedOptions, - ...props - }: Props) => { + item, + selectedOptions, + ...props +}: Props) => { const { showActionSheetWithOptions } = useActionSheet(); const { t } = useTranslation(); @@ -60,7 +60,7 @@ export const PlayButton: React.FC = ({ (q: string) => { router.push(`/player/direct-player?${q}`); }, - [router] + [router], ); const onPress = () => { @@ -88,9 +88,9 @@ export const PlayButton: React.FC = ({ if (userData && userData.PlaybackPositionTicks) { return userData.PlaybackPositionTicks > 0 ? Math.max( - (userData.PlaybackPositionTicks / item.RunTimeTicks) * 100, - MIN_PLAYBACK_WIDTH - ) + (userData.PlaybackPositionTicks / item.RunTimeTicks) * 100, + MIN_PLAYBACK_WIDTH, + ) : 0; } return 0; @@ -106,7 +106,7 @@ export const PlayButton: React.FC = ({ easing: Easing.bezier(0.7, 0, 0.3, 1.0), }); }, - [item] + [item], ); useAnimatedReaction( @@ -119,7 +119,7 @@ export const PlayButton: React.FC = ({ easing: Easing.bezier(0.9, 0, 0.31, 0.99), }); }, - [colorAtom] + [colorAtom], ); useEffect(() => { @@ -140,7 +140,7 @@ export const PlayButton: React.FC = ({ backgroundColor: interpolateColor( colorChangeProgress.value, [0, 1], - [startColor.value.primary, endColor.value.primary] + [startColor.value.primary, endColor.value.primary], ), })); @@ -148,7 +148,7 @@ export const PlayButton: React.FC = ({ backgroundColor: interpolateColor( colorChangeProgress.value, [0, 1], - [startColor.value.primary, endColor.value.primary] + [startColor.value.primary, endColor.value.primary], ), })); @@ -156,7 +156,7 @@ export const PlayButton: React.FC = ({ width: `${interpolate( widthProgress.value, [0, 1], - [startWidth.value, targetWidth.value] + [startWidth.value, targetWidth.value], )}%`, })); @@ -164,7 +164,7 @@ export const PlayButton: React.FC = ({ color: interpolateColor( colorChangeProgress.value, [0, 1], - [startColor.value.text, endColor.value.text] + [startColor.value.text, endColor.value.text], ), })); /** @@ -173,13 +173,13 @@ export const PlayButton: React.FC = ({ return ( - + = ({ = ({ borderColor: colorAtom.primary, borderStyle: "solid", }} - className="flex flex-row items-center justify-center bg-transparent rounded-xl z-20 h-12 w-full " + className='flex flex-row items-center justify-center bg-transparent rounded-xl z-20 h-12 w-full ' > - + {runtimeTicksToMinutes(item?.RunTimeTicks)} - + {settings?.openInVLC && ( diff --git a/components/PlayedStatus.tsx b/components/PlayedStatus.tsx index 5d738af4..00beb18f 100644 --- a/components/PlayedStatus.tsx +++ b/components/PlayedStatus.tsx @@ -1,8 +1,8 @@ import { useMarkAsPlayed } from "@/hooks/useMarkAsPlayed"; -import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; +import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; import { useQueryClient } from "@tanstack/react-query"; -import React from "react"; -import { View, ViewProps } from "react-native"; +import type React from "react"; +import { View, type ViewProps } from "react-native"; import { RoundButton } from "./RoundButton"; interface Props extends ViewProps { @@ -18,7 +18,7 @@ export const PlayedStatus: React.FC = ({ items, ...props }) => { queryClient.invalidateQueries({ queryKey: ["item", item.Id], }); - }) + }); queryClient.invalidateQueries({ queryKey: ["resumeItems"], }); @@ -51,9 +51,9 @@ export const PlayedStatus: React.FC = ({ items, ...props }) => { { + onPress={async () => { console.log(allPlayed); - await markAsPlayedStatus(!allPlayed) + await markAsPlayedStatus(!allPlayed); }} size={props.size} /> diff --git a/components/PreviousServersList.tsx b/components/PreviousServersList.tsx index 437c756d..ffa310d3 100644 --- a/components/PreviousServersList.tsx +++ b/components/PreviousServersList.tsx @@ -1,9 +1,10 @@ -import React, { useMemo } from "react"; +import type React from "react"; +import { useMemo } from "react"; +import { useTranslation } from "react-i18next"; import { View } from "react-native"; import { useMMKVString } from "react-native-mmkv"; import { ListGroup } from "./list/ListGroup"; import { ListItem } from "./list/ListItem"; -import { useTranslation } from "react-i18next"; interface Server { address: string; @@ -29,7 +30,7 @@ export const PreviousServersList: React.FC = ({ return ( - + {previousServers.map((s) => ( = ({ setPreviousServers("[]"); }} title={t("server.clear_button")} - textColor="red" + textColor='red' /> diff --git a/components/ProgressCircle.tsx b/components/ProgressCircle.tsx index 20c4fbd3..f3c0a55c 100644 --- a/components/ProgressCircle.tsx +++ b/components/ProgressCircle.tsx @@ -1,5 +1,5 @@ -import React from "react"; -import { View, StyleSheet } from "react-native"; +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 c1c9b060..bb6e6107 100644 --- a/components/Ratings.tsx +++ b/components/Ratings.tsx @@ -1,15 +1,18 @@ -import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; -import { View, ViewProps } from "react-native"; -import { Badge } from "./Badge"; -import { Ionicons } from "@expo/vector-icons"; -import { Image } from "expo-image"; -import { MovieResult, TvResult } from "@/utils/jellyseerr/server/models/Search"; import { useJellyseerr } from "@/hooks/useJellyseerr"; -import { useQuery } from "@tanstack/react-query"; import { MediaType } from "@/utils/jellyseerr/server/constants/media"; -import {MovieDetails} from "@/utils/jellyseerr/server/models/Movie"; -import {TvDetails} from "@/utils/jellyseerr/server/models/Tv"; -import {useMemo} from "react"; +import type { MovieDetails } from "@/utils/jellyseerr/server/models/Movie"; +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 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 { item?: BaseItemDto | null; @@ -18,21 +21,21 @@ interface Props extends ViewProps { export const Ratings: React.FC = ({ item, ...props }) => { if (!item) return null; return ( - + {item.OfficialRating && ( - + )} {item.CommunityRating && ( } + variant='gray' + iconLeft={} /> )} {item.CriticRating && ( = ({ item, ...props }) => { ); }; -export const JellyserrRatings: React.FC<{ result: MovieResult | TvResult | TvDetails | MovieDetails }> = ({ - result, -}) => { +export const JellyserrRatings: React.FC<{ + result: MovieResult | TvResult | TvDetails | MovieDetails; +}> = ({ result }) => { const { jellyseerrApi, getMediaType } = useJellyseerr(); const mediaType = useMemo(() => getMediaType(result), [result]); @@ -76,14 +79,14 @@ export const JellyserrRatings: React.FC<{ result: MovieResult | TvResult | TvDet !!result.voteCount || (data?.criticsRating && !!data?.criticsScore) || (data?.audienceRating && !!data?.audienceScore)) && ( - + {data?.criticsRating && !!data?.criticsScore && ( void; diff --git a/components/SimilarItems.tsx b/components/SimilarItems.tsx index 45914d9f..8b49def4 100644 --- a/components/SimilarItems.tsx +++ b/components/SimilarItems.tsx @@ -1,18 +1,23 @@ import MoviePoster from "@/components/posters/MoviePoster"; import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; -import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; +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 { ScrollView, TouchableOpacity, View, ViewProps } from "react-native"; -import { Text } from "./common/Text"; +import { useTranslation } from "react-i18next"; +import { + ScrollView, + TouchableOpacity, + View, + type ViewProps, +} from "react-native"; import { ItemCardText } from "./ItemCardText"; import { Loader } from "./Loader"; import { HorizontalScroll } from "./common/HorrizontalScroll"; +import { Text } from "./common/Text"; import { TouchableItemRouter } from "./common/TouchableItemRouter"; -import { useTranslation } from "react-i18next"; interface SimilarItemsProps extends ViewProps { itemId?: string | null; @@ -39,17 +44,19 @@ export const SimilarItems: React.FC = ({ return response.data.Items || []; }, enabled: !!api && !!user?.Id, - staleTime: Infinity, + staleTime: Number.POSITIVE_INFINITY, }); const movies = useMemo( () => similarItems?.filter((i) => i.Type === "Movie") || [], - [similarItems] + [similarItems], ); return ( - {t("item_card.similar_items")} + + {t("item_card.similar_items")} + = ({ diff --git a/components/SubtitleTrackSelector.tsx b/components/SubtitleTrackSelector.tsx index ac5051e1..1e74bbb6 100644 --- a/components/SubtitleTrackSelector.tsx +++ b/components/SubtitleTrackSelector.tsx @@ -1,10 +1,10 @@ import { tc } from "@/utils/textTools"; -import { MediaSourceInfo } from "@jellyfin/sdk/lib/generated-client/models"; +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 { Text } from "./common/Text"; import { useTranslation } from "react-i18next"; +import { Text } from "./common/Text"; interface Props extends React.ComponentProps { source?: MediaSourceInfo; @@ -25,7 +25,7 @@ export const SubtitleTrackSelector: React.FC = ({ const selectedSubtitleSteam = useMemo( () => subtitleStreams?.find((x) => x.Index === selected), - [subtitleStreams, selected] + [subtitleStreams, selected], ); if (subtitleStreams?.length === 0) return null; @@ -34,7 +34,7 @@ export const SubtitleTrackSelector: React.FC = ({ return ( = ({ > - - + + {t("item_card.subtitles")} - - + + {selectedSubtitleSteam ? tc(selectedSubtitleSteam?.DisplayTitle, 7) : t("item_card.none")} @@ -57,8 +57,8 @@ export const SubtitleTrackSelector: React.FC = ({ = ({ item }) => { @@ -7,7 +7,7 @@ export const WatchedIndicator: React.FC<{ item: BaseItemDto }> = ({ item }) => { <> {item.UserData?.Played === false && (item.Type === "Movie" || item.Type === "Episode") && ( - + )} ); diff --git a/components/__tests__/ThemedText-test.tsx b/components/__tests__/ThemedText-test.tsx index 1ac32250..591f09e2 100644 --- a/components/__tests__/ThemedText-test.tsx +++ b/components/__tests__/ThemedText-test.tsx @@ -1,10 +1,12 @@ -import * as React from 'react'; -import renderer from 'react-test-renderer'; +import * as React from "react"; +import renderer from "react-test-renderer"; -import { ThemedText } from '../ThemedText'; +import { ThemedText } from "../ThemedText"; it(`renders correctly`, () => { - const tree = renderer.create(Snapshot test!).toJSON(); + const tree = renderer + .create(Snapshot test!) + .toJSON(); expect(tree).toMatchSnapshot(); }); diff --git a/components/_template.tsx b/components/_template.tsx index 64e7fc7f..2e3dab21 100644 --- a/components/_template.tsx +++ b/components/_template.tsx @@ -1,5 +1,5 @@ -import { View, ViewProps } from "react-native"; import { Text } from "@/components/common/Text"; +import { View, type ViewProps } from "react-native"; interface Props extends ViewProps {} diff --git a/components/common/ColumnItem.tsx b/components/common/ColumnItem.tsx index 6d6cab2b..ef6d827f 100644 --- a/components/common/ColumnItem.tsx +++ b/components/common/ColumnItem.tsx @@ -1,5 +1,5 @@ import { useMemo } from "react"; -import { StyleSheet, View, ViewProps } from "react-native"; +import { StyleSheet, View, type ViewProps } from "react-native"; const getItemStyle = (index: number, numColumns: number) => { const alignItems = (() => { @@ -29,7 +29,7 @@ export const ColumnItem = ({ ...rest }: ColumnItemProps) => { return ( - + { data: T[]; @@ -21,7 +21,7 @@ interface Props { multiple?: boolean; } -const Dropdown = ({ +const Dropdown = ({ data, disabled, placeholderText, @@ -47,10 +47,10 @@ const Dropdown = ({ {typeof title === "string" ? ( - - {title} - - + + {title} + + {selected?.length !== undefined ? selected.map(titleExtractor).join(",") : placeholderText} @@ -63,8 +63,8 @@ const Dropdown = ({ ({ } return [ ...prev.filter( - (p) => keyExtractor(p) !== keyExtractor(item) + (p) => keyExtractor(p) !== keyExtractor(item), ), ]; - }) + }); }} > @@ -107,7 +107,7 @@ const Dropdown = ({ {titleExtractor(item)} - ) + ), )} diff --git a/components/common/HeaderBackButton.tsx b/components/common/HeaderBackButton.tsx index 12d8071e..fdf77ec5 100644 --- a/components/common/HeaderBackButton.tsx +++ b/components/common/HeaderBackButton.tsx @@ -1,14 +1,14 @@ +import { Text } from "@/components/common/Text"; +import { Ionicons } from "@expo/vector-icons"; +import { BlurView, type BlurViewProps } from "expo-blur"; +import { useRouter } from "expo-router"; import { Platform, TouchableOpacity, - TouchableOpacityProps, + type TouchableOpacityProps, View, ViewProps, } from "react-native"; -import { Text } from "@/components/common/Text"; -import { useRouter } from "expo-router"; -import { Ionicons } from "@expo/vector-icons"; -import { BlurView, BlurViewProps } from "expo-blur"; interface Props extends BlurViewProps { background?: "blur" | "transparent"; @@ -31,13 +31,13 @@ export const HeaderBackButton: React.FC = ({ @@ -46,14 +46,14 @@ export const HeaderBackButton: React.FC = ({ return ( router.back()} - className=" bg-neutral-800/80 rounded-full p-2" + className=' bg-neutral-800/80 rounded-full p-2' {...touchableOpacityProps} > ); diff --git a/components/common/HorrizontalScroll.tsx b/components/common/HorrizontalScroll.tsx index 2dce75d4..2ba30f94 100644 --- a/components/common/HorrizontalScroll.tsx +++ b/components/common/HorrizontalScroll.tsx @@ -1,6 +1,6 @@ -import { FlashList, FlashListProps } from "@shopify/flash-list"; +import { FlashList, type FlashListProps } from "@shopify/flash-list"; import React, { forwardRef, useImperativeHandle, useRef } from "react"; -import { View, ViewStyle } from "react-native"; +import { View, type ViewStyle } from "react-native"; import { Text } from "./Text"; type PartialExcept = Partial & Pick; @@ -44,7 +44,7 @@ export const HorizontalScroll = forwardRef< noItemsText, ...props }: HorizontalScrollProps, - ref: React.ForwardedRef + ref: React.ForwardedRef, ) => { const flashListRef = useRef>(null); @@ -66,16 +66,16 @@ export const HorizontalScroll = forwardRef< item: T; index: number; }) => ( - + {renderItem(item, index)} ); if (!data || loading) { return ( - - - + + + ); } @@ -95,8 +95,8 @@ export const HorizontalScroll = forwardRef< }} keyExtractor={keyExtractor} ListEmptyComponent={() => ( - - + + {noItemsText || "No data available"} @@ -104,5 +104,5 @@ export const HorizontalScroll = forwardRef< {...props} /> ); - } + }, ); diff --git a/components/common/InfiniteHorrizontalScroll.tsx b/components/common/InfiniteHorrizontalScroll.tsx index f3c504f1..cf522a87 100644 --- a/components/common/InfiniteHorrizontalScroll.tsx +++ b/components/common/InfiniteHorrizontalScroll.tsx @@ -1,13 +1,14 @@ import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; -import { +import type { BaseItemDto, BaseItemDtoQueryResult, } from "@jellyfin/sdk/lib/generated-client/models"; -import { FlashList, FlashListProps } from "@shopify/flash-list"; +import { FlashList, type FlashListProps } from "@shopify/flash-list"; import { useInfiniteQuery } from "@tanstack/react-query"; +import { t } from "i18next"; import { useAtom } from "jotai"; import React, { useEffect, useMemo } from "react"; -import { View, ViewStyle } from "react-native"; +import { View, type ViewStyle } from "react-native"; import Animated, { useAnimatedStyle, useSharedValue, @@ -15,7 +16,6 @@ import Animated, { } from "react-native-reanimated"; import { Loader } from "../Loader"; import { Text } from "./Text"; -import { t } from "i18next"; interface HorizontalScrollProps extends Omit, "renderItem" | "data" | "style"> { @@ -70,7 +70,7 @@ export function InfiniteHorizontalScroll({ const totalItems = lastPage.TotalRecordCount; const accumulatedItems = pages.reduce( (acc, curr) => acc + (curr?.Items?.length || 0), - 0 + 0, ); if (accumulatedItems < totalItems) { @@ -118,7 +118,7 @@ export function InfiniteHorizontalScroll({ ( - + {renderItem(item, index)} )} @@ -136,8 +136,10 @@ export function InfiniteHorizontalScroll({ }} showsHorizontalScrollIndicator={false} ListEmptyComponent={ - - {t("item_card.no_data_available")} + + + {t("item_card.no_data_available")} + } {...props} diff --git a/components/common/Input.tsx b/components/common/Input.tsx index 637023bc..ff46d2cd 100644 --- a/components/common/Input.tsx +++ b/components/common/Input.tsx @@ -1,32 +1,35 @@ import React from "react"; -import {Platform, TextInput, TextInputProps, TouchableOpacity} from "react-native"; +import { + Platform, + TextInput, + type TextInputProps, + TouchableOpacity, +} from "react-native"; export function Input(props: TextInputProps) { const { style, ...otherProps } = props; const inputRef = React.useRef(null); return Platform.isTV ? ( - inputRef?.current?.focus?.()} - > - - + inputRef?.current?.focus?.()}> + + ) : ( - ) + ); } diff --git a/components/common/ItemImage.tsx b/components/common/ItemImage.tsx index 09d85467..412d6b74 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 { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; -import { Image, ImageProps } from "expo-image"; +import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; +import { Image, type ImageProps } from "expo-image"; import { useAtom } from "jotai"; -import {FC, useMemo} from "react"; -import { View, ViewProps } from "react-native"; +import { type FC, useMemo } from "react"; +import { View, type ViewProps } from "react-native"; interface Props extends ImageProps { item: BaseItemDto; @@ -52,13 +52,13 @@ export const ItemImage: FC = ({ if (!source?.uri) return ( diff --git a/components/common/JellyseerrItemRouter.tsx b/components/common/JellyseerrItemRouter.tsx index ec1a1801..8222f187 100644 --- a/components/common/JellyseerrItemRouter.tsx +++ b/components/common/JellyseerrItemRouter.tsx @@ -1,16 +1,20 @@ -import { useRouter, useSegments } from "expo-router"; -import React, { PropsWithChildren, useCallback, useMemo } from "react"; -import { TouchableOpacity, TouchableOpacityProps } from "react-native"; import * as ContextMenu from "@/components/ContextMenu"; -import { MovieResult, TvResult } from "@/utils/jellyseerr/server/models/Search"; import { useJellyseerr } from "@/hooks/useJellyseerr"; -import { - hasPermission, - Permission, -} from "@/utils/jellyseerr/server/lib/permissions"; import { MediaType } from "@/utils/jellyseerr/server/constants/media"; -import {TvDetails} from "@/utils/jellyseerr/server/models/Tv"; -import {MovieDetails} from "@/utils/jellyseerr/server/models/Movie"; +import { + Permission, + hasPermission, +} from "@/utils/jellyseerr/server/lib/permissions"; +import type { MovieDetails } from "@/utils/jellyseerr/server/models/Movie"; +import type { + MovieResult, + 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; @@ -46,16 +50,13 @@ export const TouchableJellyseerrRouter: React.FC> = ({ ); }, [jellyseerrApi, jellyseerrUser]); - const request = useCallback( - () => { - if (!result) return; - requestMedia(mediaTitle, { - mediaId: result.id, - mediaType, - }) - }, - [jellyseerrApi, result] - ); + const request = useCallback(() => { + if (!result) return; + requestMedia(mediaTitle, { + mediaId: result.id, + mediaType, + }); + }, [jellyseerrApi, result]); if (from === "(home)" || from === "(search)" || from === "(libraries)") return ( @@ -75,7 +76,7 @@ export const TouchableJellyseerrRouter: React.FC> = ({ releaseYear, canRequest, posterSrc, - mediaType + mediaType, }, }); }} @@ -91,10 +92,10 @@ export const TouchableJellyseerrRouter: React.FC> = ({ loop={false} key={"content"} > - Actions + Actions {canRequest && mediaType === MediaType.MOVIE && ( { if (autoApprove) { request(); @@ -102,7 +103,7 @@ export const TouchableJellyseerrRouter: React.FC> = ({ }} shouldDismissMenuOnSelect > - + Request > = ({ light: "purple", }, }} - androidIconName="download" + androidIconName='download' /> )} diff --git a/components/common/LargePoster.tsx b/components/common/LargePoster.tsx index 50c2bcab..a42241d7 100644 --- a/components/common/LargePoster.tsx +++ b/components/common/LargePoster.tsx @@ -4,16 +4,16 @@ import { View } from "react-native"; export const LargePoster: React.FC<{ url?: string | null }> = ({ url }) => { if (!url) return ( - - + + ); - + return ( - + ); diff --git a/components/common/Text.tsx b/components/common/Text.tsx index 624b9da6..fa82a4dc 100644 --- a/components/common/Text.tsx +++ b/components/common/Text.tsx @@ -1,11 +1,11 @@ import React from "react"; -import { Platform, TextProps } from "react-native"; -import { UITextView } from "react-native-uitextview"; +import { Platform, type TextProps } from "react-native"; import { Text as RNText } from "react-native"; +import { UITextView } from "react-native-uitextview"; export function Text( props: TextProps & { uiTextView?: boolean; - } + }, ) { const { style, ...otherProps } = props; if (Platform.isTV) diff --git a/components/common/TouchableItemRouter.tsx b/components/common/TouchableItemRouter.tsx index db89e52d..23cb6dd7 100644 --- a/components/common/TouchableItemRouter.tsx +++ b/components/common/TouchableItemRouter.tsx @@ -1,13 +1,13 @@ -import { useMarkAsPlayed } from "@/hooks/useMarkAsPlayed"; import { useFavorite } from "@/hooks/useFavorite"; -import { +import { useMarkAsPlayed } from "@/hooks/useMarkAsPlayed"; +import { useActionSheet } from "@expo/react-native-action-sheet"; +import type { BaseItemDto, BaseItemPerson, } from "@jellyfin/sdk/lib/generated-client/models"; import { useRouter, useSegments } from "expo-router"; -import { PropsWithChildren, useCallback } from "react"; -import { TouchableOpacity, TouchableOpacityProps } from "react-native"; -import { useActionSheet } from "@expo/react-native-action-sheet"; +import { type PropsWithChildren, useCallback } from "react"; +import { TouchableOpacity, type TouchableOpacityProps } from "react-native"; interface Props extends TouchableOpacityProps { item: BaseItemDto; @@ -15,7 +15,7 @@ interface Props extends TouchableOpacityProps { export const itemRouter = ( item: BaseItemDto | BaseItemPerson, - from: string + from: string, ) => { if ("CollectionType" in item && item.CollectionType === "livetv") { return `/(auth)/(tabs)/${from}/livetv`; @@ -58,12 +58,24 @@ export const TouchableItemRouter: React.FC> = ({ const { showActionSheetWithOptions } = useActionSheet(); const markAsPlayedStatus = useMarkAsPlayed([item]); const { isFavorite, toggleFavorite } = useFavorite(item); - + const from = segments[2]; const showActionSheet = useCallback(() => { - if (!(item.Type === "Movie" || item.Type === "Episode" || item.Type === "Series")) return; - const options = ["Mark as Played", "Mark as Not Played", isFavorite ? "Unmark as Favorite" : "Mark as Favorite", "Cancel"]; + if ( + !( + item.Type === "Movie" || + item.Type === "Episode" || + item.Type === "Series" + ) + ) + return; + const options = [ + "Mark as Played", + "Mark as Not Played", + isFavorite ? "Unmark as Favorite" : "Mark as Favorite", + "Cancel", + ]; const cancelButtonIndex = 3; showActionSheetWithOptions( @@ -77,9 +89,9 @@ export const TouchableItemRouter: React.FC> = ({ } else if (selectedIndex === 1) { await markAsPlayedStatus(false); } else if (selectedIndex === 2) { - toggleFavorite() + toggleFavorite(); } - } + }, ); }, [showActionSheetWithOptions, isFavorite, markAsPlayedStatus]); diff --git a/components/common/VerticalSkeleton.tsx b/components/common/VerticalSkeleton.tsx index 1b2b1457..45a87749 100644 --- a/components/common/VerticalSkeleton.tsx +++ b/components/common/VerticalSkeleton.tsx @@ -1,5 +1,5 @@ -import { View, ViewProps } from "react-native"; import { Text } from "@/components/common/Text"; +import { View, type ViewProps } from "react-native"; interface Props extends ViewProps { index: number; @@ -12,18 +12,18 @@ export const VerticalSkeleton: React.FC = ({ index, ...props }) => { style={{ width: "32%", }} - className="flex flex-col" + className='flex flex-col' {...props} > - - - + + + ); }; diff --git a/components/downloads/ActiveDownloads.tsx b/components/downloads/ActiveDownloads.tsx index 773efab4..7ab86a90 100644 --- a/components/downloads/ActiveDownloads.tsx +++ b/components/downloads/ActiveDownloads.tsx @@ -2,7 +2,7 @@ import { Text } from "@/components/common/Text"; import { useDownload } from "@/providers/DownloadProvider"; import { DownloadMethod, useSettings } from "@/utils/atoms/settings"; import { storage } from "@/utils/mmkv"; -import { JobStatus } from "@/utils/optimize-server"; +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"; @@ -14,9 +14,9 @@ import { ActivityIndicator, Platform, TouchableOpacity, - TouchableOpacityProps, + type TouchableOpacityProps, View, - ViewProps, + type ViewProps, } from "react-native"; import { toast } from "sonner-native"; import { Button } from "../Button"; @@ -33,22 +33,22 @@ export const ActiveDownloads: React.FC = ({ ...props }) => { const { processes } = useDownload(); if (processes?.length === 0) return ( - - + + {t("home.downloads.active_download")} - + {t("home.downloads.no_active_downloads")} ); return ( - - + + {t("home.downloads.active_downloads")} - + {processes?.map((p: JobStatus) => ( ))} @@ -89,7 +89,7 @@ const DownloadCard = ({ process, ...props }: DownloadCardProps) => { } else { FFmpegKitProvider.FFmpegKit.cancel(Number(id)); setProcesses((prev: any[]) => - prev.filter((p: { id: string }) => p.id !== id) + prev.filter((p: { id: string }) => p.id !== id), ); } }, @@ -117,7 +117,7 @@ const DownloadCard = ({ process, ...props }: DownloadCardProps) => { return ( router.push(`/(auth)/items/page?id=${process.item.Id}`)} - className="relative bg-neutral-900 border border-neutral-800 rounded-2xl overflow-hidden" + className='relative bg-neutral-900 border border-neutral-800 rounded-2xl overflow-hidden' {...props} > {(process.status === "optimizing" || @@ -133,10 +133,10 @@ const DownloadCard = ({ process, ...props }: DownloadCardProps) => { }} > )} - - + + {base64Image && ( - + { /> )} - - {process.item.Type} - {process.item.Name} - + + {process.item.Type} + {process.item.Name} + {process.item.ProductionYear} - + {process.progress === 0 ? ( ) : ( - {process.progress.toFixed(0)}% + {process.progress.toFixed(0)}% )} {process.speed && ( - {process.speed?.toFixed(2)}x + {process.speed?.toFixed(2)}x )} {eta(process) && ( - + {t("home.downloads.eta", { eta: eta(process) })} )} - - {process.status} + + {process.status} cancelJobMutation.mutate(process.id)} - className="ml-auto" + className='ml-auto' > {cancelJobMutation.isPending ? ( - + ) : ( - + )} {process.status === "completed" && ( - + diff --git a/components/downloads/DownloadSize.tsx b/components/downloads/DownloadSize.tsx index 48a52a29..22b00412 100644 --- a/components/downloads/DownloadSize.tsx +++ b/components/downloads/DownloadSize.tsx @@ -1,8 +1,9 @@ import { Text } from "@/components/common/Text"; import { useDownload } from "@/providers/DownloadProvider"; -import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; -import React, { useEffect, useMemo, useState } from "react"; -import { TextProps } from "react-native"; +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"; interface DownloadSizeProps extends TextProps { items: BaseItemDto[]; @@ -39,7 +40,7 @@ export const DownloadSize: React.FC = ({ return ( <> - + {sizeText} diff --git a/components/downloads/EpisodeCard.tsx b/components/downloads/EpisodeCard.tsx index 53b3ecec..97a9308f 100644 --- a/components/downloads/EpisodeCard.tsx +++ b/components/downloads/EpisodeCard.tsx @@ -1,22 +1,27 @@ -import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; import { useHaptic } from "@/hooks/useHaptic"; -import React, { useCallback, useMemo } from "react"; -import { TouchableOpacity, TouchableOpacityProps, View } from "react-native"; import { ActionSheetProvider, useActionSheet, } from "@expo/react-native-action-sheet"; +import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; +import type React from "react"; +import { useCallback, useMemo } from "react"; +import { + TouchableOpacity, + type TouchableOpacityProps, + View, +} from "react-native"; +import { Text } from "@/components/common/Text"; +import { DownloadSize } from "@/components/downloads/DownloadSize"; import { useDownloadedFileOpener } from "@/hooks/useDownloadedFileOpener"; import { useDownload } from "@/providers/DownloadProvider"; import { storage } from "@/utils/mmkv"; -import { Image } from "expo-image"; -import { Ionicons } from "@expo/vector-icons"; -import { Text } from "@/components/common/Text"; import { runtimeTicksToSeconds } from "@/utils/time"; -import { DownloadSize } from "@/components/downloads/DownloadSize"; -import { TouchableItemRouter } from "../common/TouchableItemRouter"; +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; @@ -67,7 +72,7 @@ export const EpisodeCard: React.FC = ({ item, ...props }) => { // Cancelled break; } - } + }, ); }, [showActionSheetWithOptions, handleDeleteFile]); @@ -76,27 +81,27 @@ export const EpisodeCard: React.FC = ({ item, ...props }) => { onPress={handleOpenFile} onLongPress={showActionSheet} key={item.Id} - className="flex flex-col mb-4" + className='flex flex-col mb-4' > - - - + + + - - + + {item.Name} - + {`S${item.ParentIndexNumber?.toString()}:E${item.IndexNumber?.toString()}`} - + {runtimeTicksToSeconds(item.RunTimeTicks)} - + {item.Overview} @@ -105,7 +110,7 @@ export const EpisodeCard: React.FC = ({ item, ...props }) => { // Wrap the parent component with ActionSheetProvider export const EpisodeCardWithActionSheet: React.FC = ( - props + props, ) => ( diff --git a/components/downloads/MovieCard.tsx b/components/downloads/MovieCard.tsx index bb61f3c8..e15fd003 100644 --- a/components/downloads/MovieCard.tsx +++ b/components/downloads/MovieCard.tsx @@ -1,10 +1,11 @@ +import { useHaptic } from "@/hooks/useHaptic"; import { ActionSheetProvider, useActionSheet, } from "@expo/react-native-action-sheet"; -import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; -import { useHaptic } from "@/hooks/useHaptic"; -import React, { useCallback, useMemo } from "react"; +import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; +import type React from "react"; +import { useCallback, useMemo } from "react"; import { TouchableOpacity, View } from "react-native"; import { DownloadSize } from "@/components/downloads/DownloadSize"; @@ -69,14 +70,14 @@ export const MovieCard: React.FC = ({ item }) => { // Cancelled break; } - } + }, ); }, [showActionSheetWithOptions, handleDeleteFile]); return ( {base64Image ? ( - + = ({ item }) => { /> ) : ( - + )} - + diff --git a/components/downloads/SeriesCard.tsx b/components/downloads/SeriesCard.tsx index 4c6efa1f..877a5240 100644 --- a/components/downloads/SeriesCard.tsx +++ b/components/downloads/SeriesCard.tsx @@ -1,16 +1,17 @@ -import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; -import {TouchableOpacity, View} from "react-native"; +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"; +import { Image } from "expo-image"; +import { router } from "expo-router"; +import type React from "react"; +import { useCallback, useMemo } from "react"; +import { TouchableOpacity, View } from "react-native"; import { Text } from "../common/Text"; -import React, {useCallback, useMemo} from "react"; -import {storage} from "@/utils/mmkv"; -import {Image} from "expo-image"; -import {Ionicons} from "@expo/vector-icons"; -import {router} from "expo-router"; -import {DownloadSize} from "@/components/downloads/DownloadSize"; -import {useDownload} from "@/providers/DownloadProvider"; -import {useActionSheet} from "@expo/react-native-action-sheet"; -export const SeriesCard: React.FC<{ items: BaseItemDto[] }> = ({items}) => { +export const SeriesCard: React.FC<{ items: BaseItemDto[] }> = ({ items }) => { const { deleteItems } = useDownload(); const { showActionSheetWithOptions } = useActionSheet(); @@ -18,16 +19,14 @@ export const SeriesCard: React.FC<{ items: BaseItemDto[] }> = ({items}) => { return storage.getString(items[0].SeriesId!); }, []); - const deleteSeries = useCallback( - async () => deleteItems(items), - [items] - ); + const deleteSeries = useCallback(async () => deleteItems(items), [items]); const showActionSheet = useCallback(() => { const options = ["Delete", "Cancel"]; const destructiveButtonIndex = 0; - showActionSheetWithOptions({ + showActionSheetWithOptions( + { options, destructiveButtonIndex, }, @@ -35,7 +34,7 @@ export const SeriesCard: React.FC<{ items: BaseItemDto[] }> = ({items}) => { if (selectedIndex == destructiveButtonIndex) { deleteSeries(); } - } + }, ); }, [showActionSheetWithOptions, deleteSeries]); @@ -45,7 +44,7 @@ export const SeriesCard: React.FC<{ items: BaseItemDto[] }> = ({items}) => { onLongPress={showActionSheet} > {base64Image ? ( - + = ({items}) => { resizeMode: "cover", }} /> - - {items.length} + + {items.length} ) : ( - + )} - - {items[0].SeriesName} - {items[0].ProductionYear} + + + {items[0].SeriesName} + + {items[0].ProductionYear} diff --git a/components/filters/FilterButton.tsx b/components/filters/FilterButton.tsx index a96e7348..84bf8bfb 100644 --- a/components/filters/FilterButton.tsx +++ b/components/filters/FilterButton.tsx @@ -2,7 +2,7 @@ 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, ViewProps } from "react-native"; +import { TouchableOpacity, View, type ViewProps } from "react-native"; import { FilterSheet } from "./FilterSheet"; interface FilterButtonProps extends ViewProps { @@ -68,16 +68,16 @@ export const FilterButton = ({ {icon === "filter" ? ( ) : ( )} diff --git a/components/filters/FilterSheet.tsx b/components/filters/FilterSheet.tsx index 7b14af03..f3508ffd 100644 --- a/components/filters/FilterSheet.tsx +++ b/components/filters/FilterSheet.tsx @@ -1,25 +1,25 @@ import { BottomSheetBackdrop, - BottomSheetBackdropProps, + type BottomSheetBackdropProps, BottomSheetFlatList, BottomSheetModal, BottomSheetScrollView, BottomSheetView, } from "@gorhom/bottom-sheet"; -import React, { - useCallback, - useEffect, - useMemo, - useRef, - useState, -} from "react"; +import type React from "react"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { Text } from "@/components/common/Text"; -import { StyleSheet, TouchableOpacity, View, ViewProps } from "react-native"; import { Ionicons } from "@expo/vector-icons"; +import { useTranslation } from "react-i18next"; +import { + StyleSheet, + TouchableOpacity, + View, + type ViewProps, +} from "react-native"; import { Button } from "../Button"; import { Input } from "../common/Input"; -import { useTranslation } from "react-i18next"; interface Props extends ViewProps { open: boolean; @@ -130,7 +130,7 @@ export const FilterSheet = ({ appearsOnIndex={0} /> ), - [] + [], ); return ( @@ -153,18 +153,20 @@ export const FilterSheet = ({ flex: 1, }} > - - {title} - {t("search.x_items", {count: _data?.length})} + + {title} + + {t("search.x_items", { count: _data?.length })} + {showSearch && ( { setSearch(text); }} - returnKeyType="done" + returnKeyType='done' /> )} ({ borderRadius: 20, overflow: "hidden", }} - className="mb-4 flex flex-col rounded-xl overflow-hidden" + className='mb-4 flex flex-col rounded-xl overflow-hidden' > {renderData?.map((item, index) => ( @@ -185,20 +187,20 @@ export const FilterSheet = ({ }, 250); } }} - className=" bg-neutral-800 px-4 py-3 flex flex-row items-center justify-between" + className=' bg-neutral-800 px-4 py-3 flex flex-row items-center justify-between' > {renderItemLabel(item)} {values.some((i) => i === item) ? ( - + ) : ( - + )} ))} diff --git a/components/filters/ResetFiltersButton.tsx b/components/filters/ResetFiltersButton.tsx index dfeee025..6c48ee3f 100644 --- a/components/filters/ResetFiltersButton.tsx +++ b/components/filters/ResetFiltersButton.tsx @@ -5,7 +5,7 @@ import { } from "@/utils/atoms/filters"; import { Ionicons } from "@expo/vector-icons"; import { useAtom } from "jotai"; -import { TouchableOpacity, TouchableOpacityProps } from "react-native"; +import { TouchableOpacity, type TouchableOpacityProps } from "react-native"; interface Props extends TouchableOpacityProps {} @@ -29,10 +29,10 @@ export const ResetFiltersButton: React.FC = ({ ...props }) => { setSelectedTags([]); setSelectedYears([]); }} - className="bg-purple-600 rounded-full w-[30px] h-[30px] flex items-center justify-center mr-1" + className='bg-purple-600 rounded-full w-[30px] h-[30px] flex items-center justify-center mr-1' {...props} > - + ); }; diff --git a/components/home/Favorites.tsx b/components/home/Favorites.tsx index fba0ca6c..6fe263a6 100644 --- a/components/home/Favorites.tsx +++ b/components/home/Favorites.tsx @@ -13,154 +13,154 @@ import { ScrollingCollectionList } from "./ScrollingCollectionList"; import heart from "@/assets/icons/heart.fill.png"; type FavoriteTypes = - | "Series" - | "Movie" - | "Episode" - | "Video" - | "BoxSet" - | "Playlist"; + | "Series" + | "Movie" + | "Episode" + | "Video" + | "BoxSet" + | "Playlist"; type EmptyState = Record; export const Favorites = () => { - const [api] = useAtom(apiAtom); - const [user] = useAtom(userAtom); - const [emptyState, setEmptyState] = useState({ - Series: false, - Movie: false, - Episode: false, - Video: false, - BoxSet: false, - Playlist: false, - }); + const [api] = useAtom(apiAtom); + const [user] = useAtom(userAtom); + const [emptyState, setEmptyState] = useState({ + Series: false, + Movie: false, + Episode: false, + Video: false, + BoxSet: false, + Playlist: false, + }); - const fetchFavoritesByType = useCallback( - async (itemType: BaseItemKind) => { - const response = await getItemsApi(api as Api).getItems({ - userId: user?.Id, - sortBy: ["SeriesSortName", "SortName"], - sortOrder: ["Ascending"], - filters: ["IsFavorite"], - recursive: true, - fields: ["PrimaryImageAspectRatio"], - collapseBoxSetItems: false, - excludeLocationTypes: ["Virtual"], - enableTotalRecordCount: false, - limit: 20, - includeItemTypes: [itemType], - }); - const items = response.data.Items || []; + const fetchFavoritesByType = useCallback( + async (itemType: BaseItemKind) => { + const response = await getItemsApi(api as Api).getItems({ + userId: user?.Id, + sortBy: ["SeriesSortName", "SortName"], + sortOrder: ["Ascending"], + filters: ["IsFavorite"], + recursive: true, + fields: ["PrimaryImageAspectRatio"], + collapseBoxSetItems: false, + excludeLocationTypes: ["Virtual"], + enableTotalRecordCount: false, + limit: 20, + includeItemTypes: [itemType], + }); + const items = response.data.Items || []; - // Update empty state for this specific type - setEmptyState((prev) => ({ - ...prev, - [itemType as FavoriteTypes]: items.length === 0, - })); + // Update empty state for this specific type + setEmptyState((prev) => ({ + ...prev, + [itemType as FavoriteTypes]: items.length === 0, + })); - return items; - }, - [api, user], - ); + return items; + }, + [api, user], + ); - // Reset empty state when component mounts or dependencies change - useEffect(() => { - setEmptyState({ - Series: false, - Movie: false, - Episode: false, - Video: false, - BoxSet: false, - Playlist: false, - }); - }, [api, user]); + // Reset empty state when component mounts or dependencies change + useEffect(() => { + setEmptyState({ + Series: false, + Movie: false, + Episode: false, + Video: false, + BoxSet: false, + Playlist: false, + }); + }, [api, user]); - // Check if all categories that have been loaded are empty - const areAllEmpty = () => { - const loadedCategories = Object.values(emptyState); - return ( - loadedCategories.length > 0 && - loadedCategories.every((isEmpty) => isEmpty) - ); - }; + // Check if all categories that have been loaded are empty + const areAllEmpty = () => { + const loadedCategories = Object.values(emptyState); + return ( + loadedCategories.length > 0 && + loadedCategories.every((isEmpty) => isEmpty) + ); + }; - const fetchFavoriteSeries = useCallback( - () => fetchFavoritesByType("Series"), - [fetchFavoritesByType], - ); - const fetchFavoriteMovies = useCallback( - () => fetchFavoritesByType("Movie"), - [fetchFavoritesByType], - ); - const fetchFavoriteEpisodes = useCallback( - () => fetchFavoritesByType("Episode"), - [fetchFavoritesByType], - ); - const fetchFavoriteVideos = useCallback( - () => fetchFavoritesByType("Video"), - [fetchFavoritesByType], - ); - const fetchFavoriteBoxsets = useCallback( - () => fetchFavoritesByType("BoxSet"), - [fetchFavoritesByType], - ); - const fetchFavoritePlaylists = useCallback( - () => fetchFavoritesByType("Playlist"), - [fetchFavoritesByType], - ); + const fetchFavoriteSeries = useCallback( + () => fetchFavoritesByType("Series"), + [fetchFavoritesByType], + ); + const fetchFavoriteMovies = useCallback( + () => fetchFavoritesByType("Movie"), + [fetchFavoritesByType], + ); + const fetchFavoriteEpisodes = useCallback( + () => fetchFavoritesByType("Episode"), + [fetchFavoritesByType], + ); + const fetchFavoriteVideos = useCallback( + () => fetchFavoritesByType("Video"), + [fetchFavoritesByType], + ); + const fetchFavoriteBoxsets = useCallback( + () => fetchFavoritesByType("BoxSet"), + [fetchFavoritesByType], + ); + const fetchFavoritePlaylists = useCallback( + () => fetchFavoritesByType("Playlist"), + [fetchFavoritesByType], + ); - return ( - - {areAllEmpty() && ( - - - - {t("favorites.noDataTitle")} - - - {t("favorites.noData")} - - - )} - - - - - - - - ); + return ( + + {areAllEmpty() && ( + + + + {t("favorites.noDataTitle")} + + + {t("favorites.noData")} + + + )} + + + + + + + + ); }; diff --git a/components/home/LargeMovieCarousel.tsx b/components/home/LargeMovieCarousel.tsx index 5b228901..9b0851ef 100644 --- a/components/home/LargeMovieCarousel.tsx +++ b/components/home/LargeMovieCarousel.tsx @@ -3,14 +3,14 @@ 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 { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; +import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; import { getItemsApi } from "@jellyfin/sdk/lib/utils/api"; import { useQuery } from "@tanstack/react-query"; import { Image } from "expo-image"; import { useRouter, useSegments } from "expo-router"; import { useAtom } from "jotai"; import React, { useCallback, useMemo } from "react"; -import { Dimensions, View, ViewProps } from "react-native"; +import { Dimensions, View, type ViewProps } from "react-native"; import { Gesture, GestureDetector } from "react-native-gesture-handler"; import Animated, { runOnJS, @@ -18,7 +18,7 @@ import Animated, { withTiming, } from "react-native-reanimated"; import Carousel, { - ICarouselInstance, + type ICarouselInstance, Pagination, } from "react-native-reanimated-carousel"; import { itemRouter } from "../common/TouchableItemRouter"; @@ -88,13 +88,13 @@ export const LargeMovieCarousel: React.FC = ({ ...props }) => { if (!popularItems) return null; return ( - + = ({ item }) => { style={{ opacity: opacity, }} - className="px-4" + className='px-4' > - + = ({ item }) => { overflow: "hidden", }} /> - + = ({ return ( - + {title} {isLoading === false && data?.length === 0 && ( - - {t("home.no_items")} + + {t("home.no_items")} )} {isLoading ? ( @@ -62,19 +62,19 @@ export const ScrollingCollectionList: React.FC = ({ `} > {[1, 2, 3].map((i) => ( - - - + + + Nisi mollit voluptate amet. - + Lorem ipsum @@ -85,7 +85,7 @@ export const ScrollingCollectionList: React.FC = ({ ) : ( - + {data?.map((item) => ( void, - appendValue?: string, + value: number; + disabled?: boolean; + step: number; + min: number; + max: number; + onUpdate: (value: number) => void; + appendValue?: string; } export const Stepper: React.FC = ({ @@ -19,33 +19,35 @@ export const Stepper: React.FC = ({ min, max, onUpdate, - appendValue + appendValue, }) => { return ( onUpdate(Math.max(min, value - step))} - className="w-8 h-8 bg-neutral-800 rounded-l-lg flex items-center justify-center" + className='w-8 h-8 bg-neutral-800 rounded-l-lg flex items-center justify-center' > - - {value}{appendValue} + {value} + {appendValue} onUpdate(Math.min(max, value + step))} > + - ) -} \ No newline at end of file + ); +}; diff --git a/components/jellyseerr/Cast.tsx b/components/jellyseerr/Cast.tsx index 8dcdd785..27e8201c 100644 --- a/components/jellyseerr/Cast.tsx +++ b/components/jellyseerr/Cast.tsx @@ -1,11 +1,11 @@ -import { View, ViewProps } from "react-native"; -import { MovieDetails } from "@/utils/jellyseerr/server/models/Movie"; -import { TvDetails } from "@/utils/jellyseerr/server/models/Tv"; -import React from "react"; -import { FlashList } from "@shopify/flash-list"; 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"; const CastSlide: React.FC< { details?: MovieDetails | TvDetails } & ViewProps @@ -15,12 +15,14 @@ const CastSlide: React.FC< details?.credits?.cast && details?.credits?.cast?.length > 0 && ( - {t("jellyseerr.cast")} + + {t("jellyseerr.cast")} + } + ItemSeparatorComponent={() => } estimatedItemSize={15} keyExtractor={(item) => item?.id?.toString()} contentContainerStyle={{ paddingHorizontal: 16 }} diff --git a/components/jellyseerr/DetailFacts.tsx b/components/jellyseerr/DetailFacts.tsx index e6ef013a..4e9e1580 100644 --- a/components/jellyseerr/DetailFacts.tsx +++ b/components/jellyseerr/DetailFacts.tsx @@ -1,15 +1,15 @@ -import { View, ViewProps } from "react-native"; -import { MovieDetails } from "@/utils/jellyseerr/server/models/Movie"; -import { TvDetails } from "@/utils/jellyseerr/server/models/Tv"; import { Text } from "@/components/common/Text"; -import { useMemo } from "react"; import { useJellyseerr } from "@/hooks/useJellyseerr"; -import { uniqBy } from "lodash"; -import { TmdbRelease } from "@/utils/jellyseerr/server/api/themoviedb/interfaces"; -import { Ionicons, MaterialCommunityIcons } from "@expo/vector-icons"; -import CountryFlag from "react-native-country-flag"; 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"; interface Release { certification: string; @@ -30,12 +30,12 @@ const Facts: React.FC< > = ({ title, facts, ...props }) => facts && facts?.length > 0 && ( - - {title} + + {title} - + {facts.map((f, idx) => - typeof f === "string" ? {f} : f + typeof f === "string" ? {f} : f, )} @@ -50,15 +50,19 @@ const Fact: React.FC<{ title: string; fact?: string | null } & ViewProps> = ({ const DetailFacts: React.FC< { details?: MovieDetails | TvDetails } & ViewProps > = ({ details, className, ...props }) => { - const { jellyseerrUser, jellyseerrRegion: region, jellyseerrLocale: locale } = useJellyseerr(); + const { + jellyseerrUser, + jellyseerrRegion: region, + jellyseerrLocale: locale, + } = useJellyseerr(); const { t } = useTranslation(); const releases = useMemo( () => (details as MovieDetails)?.releases?.results.find( - (r: TmdbRelease) => r.iso_3166_1 === region + (r: TmdbRelease) => r.iso_3166_1 === region, )?.release_dates as TmdbRelease["release_dates"], - [details] + [details], ); // Release date types: @@ -72,9 +76,9 @@ const DetailFacts: React.FC< () => uniqBy( releases?.filter((r: Release) => r.type > 2 && r.type < 6), - "type" + "type", ), - [releases] + [releases], ); const firstAirDate = useMemo(() => { @@ -82,7 +86,7 @@ const DetailFacts: React.FC< if (firstAirDate) { return new Date(firstAirDate).toLocaleDateString( `${locale}-${region}`, - dateOpts + dateOpts, ); } }, [details]); @@ -93,7 +97,7 @@ const DetailFacts: React.FC< if (nextAirDate && firstAirDate !== nextAirDate) { return new Date(nextAirDate).toLocaleDateString( `${locale}-${region}`, - dateOpts + dateOpts, ); } }, [details]); @@ -102,26 +106,26 @@ const DetailFacts: React.FC< () => (details as MovieDetails)?.revenue?.toLocaleString?.( `${locale}-${region}`, - { style: "currency", currency: "USD" } + { style: "currency", currency: "USD" }, ), - [details] + [details], ); const budget = useMemo( () => (details as MovieDetails)?.budget?.toLocaleString?.( `${locale}-${region}`, - { style: "currency", currency: "USD" } + { style: "currency", currency: "USD" }, ), - [details] + [details], ); const streamingProviders = useMemo( () => details?.watchProviders?.find( - (provider) => provider.iso_3166_1 === region + (provider) => provider.iso_3166_1 === region, )?.flatrate, - [details] + [details], ); const networks = useMemo(() => (details as TvDetails)?.networks, [details]); @@ -129,15 +133,15 @@ const DetailFacts: React.FC< const spokenLanguage = useMemo( () => details?.spokenLanguages.find( - (lng) => lng.iso_639_1 === details.originalLanguage + (lng) => lng.iso_639_1 === details.originalLanguage, )?.name, - [details] + [details], ); return ( details && ( - - {t("jellyseerr.details")} + + {t("jellyseerr.details")} {details.keywords.some( - (keyword) => keyword.id === ANIME_KEYWORD_ID - ) && } + (keyword) => keyword.id === ANIME_KEYWORD_ID, + ) && } ( - + {r.type === 3 ? ( // Theatrical - + ) : r.type === 4 ? ( // Digital - + ) : ( // Physical )} {new Date(r.release_date).toLocaleDateString( `${locale}-${region}`, - dateOpts + dateOpts, )} @@ -181,11 +185,14 @@ const DetailFacts: React.FC< - + ( - + {n.name} @@ -194,10 +201,13 @@ const DetailFacts: React.FC< n.name + (n) => n.name, )} /> - n.name)} /> + n.name)} + /> s.name)} diff --git a/components/jellyseerr/JellyseerrIndexPage.tsx b/components/jellyseerr/JellyseerrIndexPage.tsx index 51aeb938..e2467b31 100644 --- a/components/jellyseerr/JellyseerrIndexPage.tsx +++ b/components/jellyseerr/JellyseerrIndexPage.tsx @@ -1,14 +1,17 @@ import Discover from "@/components/jellyseerr/discover/Discover"; import { useJellyseerr } from "@/hooks/useJellyseerr"; import { MediaType } from "@/utils/jellyseerr/server/constants/media"; -import { +import type { MovieResult, PersonResult, TvResult, } from "@/utils/jellyseerr/server/models/Search"; import { useReactNavigationQuery } from "@/utils/useReactNavigationQuery"; -import React, {useMemo, useState} from "react"; -import { View, ViewProps } from "react-native"; +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, @@ -20,8 +23,6 @@ import JellyseerrPoster from "../posters/JellyseerrPoster"; import { LoadingSkeleton } from "../search/LoadingSkeleton"; import { SearchItemWrapper } from "../search/SearchItemWrapper"; import PersonPoster from "./PersonPoster"; -import { useTranslation } from "react-i18next"; -import {orderBy, uniqBy} from "lodash"; interface Props extends ViewProps { searchQuery: string; @@ -30,15 +31,15 @@ interface Props extends ViewProps { } export enum JellyseerrSearchSort { - DEFAULT, - VOTE_COUNT_AND_AVERAGE, - POPULARITY + DEFAULT = 0, + VOTE_COUNT_AND_AVERAGE = 1, + POPULARITY = 2, } export const JellyserrIndexPage: React.FC = ({ searchQuery, sortType, - order + order, }) => { const { jellyseerrApi } = useJellyseerr(); const opacity = useSharedValue(1); @@ -57,19 +58,24 @@ export const JellyserrIndexPage: React.FC = ({ const { data: jellyseerrResults, isFetching: f2, - isLoading: l2 + isLoading: l2, } = useReactNavigationQuery({ queryKey: ["search", "jellyseerr", "results", searchQuery], queryFn: async () => { const params = { - query: new URLSearchParams(searchQuery || "").toString() - } + query: new URLSearchParams(searchQuery || "").toString(), + }; return await Promise.all([ - jellyseerrApi?.search({...params, page: 1}), - jellyseerrApi?.search({...params, page: 2}), - jellyseerrApi?.search({...params, page: 3}), - jellyseerrApi?.search({...params, page: 4}) - ]).then(all => uniqBy(all.flatMap(v => v?.results || []), "id")) + jellyseerrApi?.search({ ...params, page: 1 }), + jellyseerrApi?.search({ ...params, page: 2 }), + jellyseerrApi?.search({ ...params, page: 3 }), + jellyseerrApi?.search({ ...params, page: 4 }), + ]).then((all) => + uniqBy( + all.flatMap((v) => v?.results || []), + "id", + ), + ); }, enabled: !!jellyseerrApi && searchQuery.length > 0, }); @@ -82,57 +88,66 @@ export const JellyserrIndexPage: React.FC = ({ } else { opacity.value = withTiming(0, { duration: 200 }); } - } + }, ); - const sortingType = useMemo( - () => { - if (!sortType) return; - switch (Number(JellyseerrSearchSort[sortType])) { - case JellyseerrSearchSort.VOTE_COUNT_AND_AVERAGE: - return ["voteCount", "voteAverage"]; - case JellyseerrSearchSort.POPULARITY: - return ["voteCount", "popularity"] - default: - return undefined - } - }, - [sortType, order] - ) + const sortingType = useMemo(() => { + if (!sortType) return; + switch (Number(JellyseerrSearchSort[sortType])) { + case JellyseerrSearchSort.VOTE_COUNT_AND_AVERAGE: + return ["voteCount", "voteAverage"]; + case JellyseerrSearchSort.POPULARITY: + return ["voteCount", "popularity"]; + default: + return undefined; + } + }, [sortType, order]); const jellyseerrMovieResults = useMemo( () => orderBy( - jellyseerrResults?.filter((r) => r.mediaType === MediaType.MOVIE) as MovieResult[], - sortingType || [m => m.title.toLowerCase() == searchQuery.toLowerCase()], - order || "desc" + jellyseerrResults?.filter( + (r) => r.mediaType === MediaType.MOVIE, + ) as MovieResult[], + sortingType || [ + (m) => m.title.toLowerCase() == searchQuery.toLowerCase(), + ], + order || "desc", ), - [jellyseerrResults, sortingType, order] + [jellyseerrResults, sortingType, order], ); const jellyseerrTvResults = useMemo( () => orderBy( - jellyseerrResults?.filter((r) => r.mediaType === MediaType.TV) as TvResult[], - sortingType || [t => t.name.toLowerCase() == searchQuery.toLowerCase()], - order || "desc" + jellyseerrResults?.filter( + (r) => r.mediaType === MediaType.TV, + ) as TvResult[], + sortingType || [ + (t) => t.name.toLowerCase() == searchQuery.toLowerCase(), + ], + order || "desc", ), - [jellyseerrResults, sortingType, order] + [jellyseerrResults, sortingType, order], ); const jellyseerrPersonResults = useMemo( () => orderBy( - jellyseerrResults?.filter((r) => r.mediaType === "person") as PersonResult[], - sortingType || [p => p.name.toLowerCase() == searchQuery.toLowerCase()], - order || "desc" + jellyseerrResults?.filter( + (r) => r.mediaType === "person", + ) as PersonResult[], + sortingType || [ + (p) => p.name.toLowerCase() == searchQuery.toLowerCase(), + ], + order || "desc", ), - [jellyseerrResults, sortingType, order] + [jellyseerrResults, sortingType, order], ); if (!searchQuery.length) return ( - + ); @@ -149,10 +164,10 @@ export const JellyserrIndexPage: React.FC = ({ !l1 && !l2 && ( - + {t("search.no_results_found_for")} - + "{searchQuery}" @@ -178,7 +193,7 @@ export const JellyserrIndexPage: React.FC = ({ items={jellyseerrPersonResults} renderItem={(item: PersonResult) => ( = ({ - mediaType, - className, - ...props -}) => { +const JellyseerrMediaIcon: React.FC< + { mediaType: "tv" | "movie" } & ViewProps +> = ({ mediaType, className, ...props }) => { const style = useMemo( - () => mediaType === MediaType.MOVIE - ? 'bg-blue-600/90 border-blue-400/40' - : 'bg-purple-600/90 border-purple-400/40', - [mediaType] + () => + mediaType === MediaType.MOVIE + ? "bg-blue-600/90 border-blue-400/40" + : "bg-purple-600/90 border-purple-400/40", + [mediaType], ); return ( - mediaType && - - {mediaType === MediaType.MOVIE ? ( - - ) : ( - - )} - - ) -} + mediaType && ( + + {mediaType === MediaType.MOVIE ? ( + + ) : ( + + )} + + ) + ); +}; -export default JellyseerrMediaIcon; \ No newline at end of file +export default JellyseerrMediaIcon; diff --git a/components/jellyseerr/JellyseerrStatusIcon.tsx b/components/jellyseerr/JellyseerrStatusIcon.tsx index 8fc593fa..26452a91 100644 --- a/components/jellyseerr/JellyseerrStatusIcon.tsx +++ b/components/jellyseerr/JellyseerrStatusIcon.tsx @@ -1,7 +1,7 @@ -import {useEffect, useState} from "react"; -import {MediaStatus} from "@/utils/jellyseerr/server/constants/media"; -import {MaterialCommunityIcons} from "@expo/vector-icons"; -import {TouchableOpacity, View, ViewProps} from "react-native"; +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"; interface Props { mediaStatus?: MediaStatus; @@ -15,7 +15,8 @@ const JellyseerrStatusIcon: React.FC = ({ onPress, ...props }) => { - const [badgeIcon, setBadgeIcon] = useState(); + const [badgeIcon, setBadgeIcon] = + useState(); const [badgeStyle, setBadgeStyle] = useState(); // Match similar to what Jellyseerr is currently using @@ -23,49 +24,54 @@ const JellyseerrStatusIcon: React.FC = ({ useEffect(() => { switch (mediaStatus) { case MediaStatus.PROCESSING: - setBadgeStyle('bg-indigo-500 border-indigo-400 ring-indigo-400 text-indigo-100'); - setBadgeIcon('clock'); + setBadgeStyle( + "bg-indigo-500 border-indigo-400 ring-indigo-400 text-indigo-100", + ); + setBadgeIcon("clock"); break; case MediaStatus.AVAILABLE: - setBadgeStyle('bg-purple-500 border-green-400 ring-green-400 text-green-100'); - setBadgeIcon('check') + setBadgeStyle( + "bg-purple-500 border-green-400 ring-green-400 text-green-100", + ); + setBadgeIcon("check"); break; case MediaStatus.PENDING: - setBadgeStyle('bg-yellow-500 border-yellow-400 ring-yellow-400 text-yellow-100'); - setBadgeIcon('bell') + setBadgeStyle( + "bg-yellow-500 border-yellow-400 ring-yellow-400 text-yellow-100", + ); + setBadgeIcon("bell"); break; case MediaStatus.BLACKLISTED: - setBadgeStyle('bg-red-500 border-white-400 ring-white-400 text-white'); - setBadgeIcon('eye-off') + setBadgeStyle("bg-red-500 border-white-400 ring-white-400 text-white"); + setBadgeIcon("eye-off"); break; case MediaStatus.PARTIALLY_AVAILABLE: - setBadgeStyle('bg-green-500 border-green-400 ring-green-400 text-green-100'); + setBadgeStyle( + "bg-green-500 border-green-400 ring-green-400 text-green-100", + ); setBadgeIcon("minus"); break; default: if (showRequestIcon) { - setBadgeStyle('bg-green-600'); - setBadgeIcon("plus") + setBadgeStyle("bg-green-600"); + setBadgeIcon("plus"); } break; } - }, [mediaStatus, showRequestIcon, setBadgeStyle, setBadgeIcon]) + }, [mediaStatus, showRequestIcon, setBadgeStyle, setBadgeIcon]); return ( - badgeIcon && - + badgeIcon && ( + - + - - ) -} + + ) + ); +}; -export default JellyseerrStatusIcon; \ No newline at end of file +export default JellyseerrStatusIcon; diff --git a/components/jellyseerr/ParallaxSlideShow.tsx b/components/jellyseerr/ParallaxSlideShow.tsx index 6a7fcb7f..1e7a6142 100644 --- a/components/jellyseerr/ParallaxSlideShow.tsx +++ b/components/jellyseerr/ParallaxSlideShow.tsx @@ -1,29 +1,27 @@ -import React, { - PropsWithChildren, +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, useCallback, useEffect, useRef, useState, } from "react"; -import {Dimensions, View, ViewProps} from "react-native"; -import { useSafeAreaInsets } from "react-native-safe-area-context"; -import { ParallaxScrollView } from "@/components/ParallaxPage"; -import { Text } from "@/components/common/Text"; +import { Dimensions, View, type ViewProps } from "react-native"; import { Animated } from "react-native"; -import { FlashList } from "@shopify/flash-list"; -import {useFocusEffect} from "expo-router"; +import { useSafeAreaInsets } from "react-native-safe-area-context"; const ANIMATION_ENTER = 250; const ANIMATION_EXIT = 250; const BACKDROP_DURATION = 5000; -type Render = React.ComponentType - | React.ReactElement - | null - | undefined; +type Render = React.ComponentType | React.ReactElement | null | undefined; interface Props { - data: T[] + data: T[]; images: string[]; logo?: React.ReactElement; HeaderContent?: () => React.ReactElement; @@ -34,7 +32,7 @@ interface Props { onEndReached?: (() => void) | null | undefined; } -const ParallaxSlideShow = ({ +const ParallaxSlideShow = ({ data, images, logo, @@ -45,8 +43,7 @@ const ParallaxSlideShow = ({ keyExtractor, onEndReached, ...props -}: PropsWithChildren & ViewProps> -) => { +}: PropsWithChildren & ViewProps>) => { const insets = useSafeAreaInsets(); const [currentIndex, setCurrentIndex] = useState(0); @@ -59,7 +56,7 @@ const ParallaxSlideShow = ({ duration: ANIMATION_ENTER, useNativeDriver: true, }), - [fadeAnim] + [fadeAnim], ); const exitAnimation = useCallback( @@ -69,7 +66,7 @@ const ParallaxSlideShow = ({ duration: ANIMATION_EXIT, useNativeDriver: true, }), - [fadeAnim] + [fadeAnim], ); useEffect(() => { @@ -77,31 +74,35 @@ const ParallaxSlideShow = ({ enterAnimation().start(); const intervalId = setInterval(() => { - Animated.sequence([ - enterAnimation(), - exitAnimation() - ]).start(() => { + Animated.sequence([enterAnimation(), exitAnimation()]).start(() => { fadeAnim.setValue(0); setCurrentIndex((prevIndex) => (prevIndex + 1) % images?.length); - }) + }); }, BACKDROP_DURATION); return () => { - clearInterval(intervalId) + clearInterval(intervalId); }; } - }, [fadeAnim, images, enterAnimation, exitAnimation, setCurrentIndex, currentIndex]); + }, [ + fadeAnim, + images, + enterAnimation, + exitAnimation, + setCurrentIndex, + currentIndex, + ]); return ( ({ } logo={logo} > - - - + + + {HeaderContent && HeaderContent()} @@ -131,30 +132,30 @@ const ParallaxSlideShow = ({ - + + No results } - contentInsetAdjustmentBehavior="automatic" + contentInsetAdjustmentBehavior='automatic' ListHeaderComponent={ - {listHeader} + {listHeader} } nestedScrollEnabled showsVerticalScrollIndicator={false} //@ts-ignore - renderItem={({ item, index}) => renderItem(item, index)} + renderItem={({ item, index }) => renderItem(item, index)} keyExtractor={keyExtractor} numColumns={3} estimatedItemSize={214} - ItemSeparatorComponent={() => } + ItemSeparatorComponent={() => } /> ); -} +}; -export default ParallaxSlideShow; \ No newline at end of file +export default ParallaxSlideShow; diff --git a/components/jellyseerr/PersonPoster.tsx b/components/jellyseerr/PersonPoster.tsx index 6e7d9aa6..075fedf2 100644 --- a/components/jellyseerr/PersonPoster.tsx +++ b/components/jellyseerr/PersonPoster.tsx @@ -1,15 +1,15 @@ -import {TouchableOpacity, View, ViewProps} from "react-native"; -import React from "react"; -import {Text} from "@/components/common/Text"; +import { Text } from "@/components/common/Text"; import Poster from "@/components/posters/Poster"; -import {useRouter, useSegments} from "expo-router"; -import {useJellyseerr} from "@/hooks/useJellyseerr"; +import { useJellyseerr } from "@/hooks/useJellyseerr"; +import { useRouter, useSegments } from "expo-router"; +import type React from "react"; +import { TouchableOpacity, View, type ViewProps } from "react-native"; interface Props { - id: string - posterPath?: string - name: string - subName?: string + id: string; + posterPath?: string; + name: string; + subName?: string; } const PersonPoster: React.FC = ({ @@ -19,24 +19,28 @@ const PersonPoster: React.FC = ({ subName, ...props }) => { - const {jellyseerrApi} = useJellyseerr(); + const { jellyseerrApi } = useJellyseerr(); const router = useRouter(); const segments = useSegments(); const from = segments[2]; if (from === "(home)" || from === "(search)" || from === "(libraries)") return ( - router.push(`/(auth)/(tabs)/${from}/jellyseerr/person/${id}`)}> - + + router.push(`/(auth)/(tabs)/${from}/jellyseerr/person/${id}`) + } + > + - {name} - {subName && {subName}} + {name} + {subName && {subName}} - ) -} + ); +}; -export default PersonPoster; \ No newline at end of file +export default PersonPoster; diff --git a/components/jellyseerr/RequestModal.tsx b/components/jellyseerr/RequestModal.tsx index 192d2d83..a777dde3 100644 --- a/components/jellyseerr/RequestModal.tsx +++ b/components/jellyseerr/RequestModal.tsx @@ -1,21 +1,30 @@ -import React, {forwardRef, useCallback, useMemo, useState} from "react"; -import {View, ViewProps} from "react-native"; -import {useJellyseerr} from "@/hooks/useJellyseerr"; -import {useQuery} from "@tanstack/react-query"; -import {MediaType} from "@/utils/jellyseerr/server/constants/media"; -import {BottomSheetBackdrop, BottomSheetBackdropProps, BottomSheetModal, BottomSheetView} from "@gorhom/bottom-sheet"; +import { Button } from "@/components/Button"; import Dropdown from "@/components/common/Dropdown"; -import {QualityProfile, RootFolder, Tag} from "@/utils/jellyseerr/server/api/servarr/base"; -import {MediaRequestBody} from "@/utils/jellyseerr/server/interfaces/api/requestInterfaces"; -import {BottomSheetModalMethods} from "@gorhom/bottom-sheet/lib/typescript/types"; -import {Button} from "@/components/Button"; -import {Text} from "@/components/common/Text"; +import { Text } from "@/components/common/Text"; +import { useJellyseerr } from "@/hooks/useJellyseerr"; +import type { + QualityProfile, + RootFolder, + Tag, +} from "@/utils/jellyseerr/server/api/servarr/base"; +import type { MediaType } from "@/utils/jellyseerr/server/constants/media"; +import type { MediaRequestBody } from "@/utils/jellyseerr/server/interfaces/api/requestInterfaces"; +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; - title: string, - requestBody?: MediaRequestBody, + title: string; + requestBody?: MediaRequestBody; type: MediaType; isAnime?: boolean; is4k?: boolean; @@ -23,216 +32,252 @@ interface Props { onDismiss?: () => void; } -const RequestModal = forwardRef>(({ - id, - title, - requestBody, - type, - isAnime = false, - onRequested, - onDismiss, - ...props -}, ref) => { - const {jellyseerrApi, jellyseerrUser, requestMedia} = useJellyseerr(); - const [requestOverrides, setRequestOverrides] = - useState({ +const RequestModal = forwardRef< + BottomSheetModalMethods, + Props & Omit +>( + ( + { + id, + title, + requestBody, + type, + isAnime = false, + onRequested, + onDismiss, + ...props + }, + ref, + ) => { + const { jellyseerrApi, jellyseerrUser, requestMedia } = useJellyseerr(); + const [requestOverrides, setRequestOverrides] = useState({ mediaId: Number(id), mediaType: type, - userId: jellyseerrUser?.id + userId: jellyseerrUser?.id, }); - const { t } = useTranslation(); + const { t } = useTranslation(); - const {data: serviceSettings} = useQuery({ - queryKey: ["jellyseerr", "request", type, 'service'], - queryFn: async () => jellyseerrApi?.service(type == 'movie' ? 'radarr' : 'sonarr'), - enabled: !!jellyseerrApi && !!jellyseerrUser, - refetchOnMount: 'always' - }); + const { data: serviceSettings } = useQuery({ + queryKey: ["jellyseerr", "request", type, "service"], + queryFn: async () => + jellyseerrApi?.service(type == "movie" ? "radarr" : "sonarr"), + enabled: !!jellyseerrApi && !!jellyseerrUser, + refetchOnMount: "always", + }); - const {data: users} = useQuery({ - queryKey: ["jellyseerr", "users"], - queryFn: async () => jellyseerrApi?.user({take: 1000, sort: 'displayname'}), - enabled: !!jellyseerrApi && !!jellyseerrUser, - refetchOnMount: 'always' - }); + const { data: users } = useQuery({ + queryKey: ["jellyseerr", "users"], + queryFn: async () => + jellyseerrApi?.user({ take: 1000, sort: "displayname" }), + enabled: !!jellyseerrApi && !!jellyseerrUser, + refetchOnMount: "always", + }); - const defaultService = useMemo( - () => serviceSettings?.find?.(v => v.isDefault), - [serviceSettings] - ); + const defaultService = useMemo( + () => serviceSettings?.find?.((v) => v.isDefault), + [serviceSettings], + ); - const {data: defaultServiceDetails} = useQuery({ - queryKey: ["jellyseerr", "request", type, 'service', 'details', defaultService?.id], - queryFn: async () => { - setRequestOverrides((prev) => ({ - ...prev, - serverId: defaultService?.id - })) - return jellyseerrApi?.serviceDetails(type === 'movie' ? 'radarr' : 'sonarr', defaultService!!.id) - }, - enabled: !!jellyseerrApi && !!jellyseerrUser && !!defaultService, - refetchOnMount: 'always', - }); + const { data: defaultServiceDetails } = useQuery({ + queryKey: [ + "jellyseerr", + "request", + type, + "service", + "details", + defaultService?.id, + ], + queryFn: async () => { + setRequestOverrides((prev) => ({ + ...prev, + serverId: defaultService?.id, + })); + return jellyseerrApi?.serviceDetails( + type === "movie" ? "radarr" : "sonarr", + defaultService!.id, + ); + }, + enabled: !!jellyseerrApi && !!jellyseerrUser && !!defaultService, + refetchOnMount: "always", + }); - const defaultProfile: QualityProfile = useMemo( - () => defaultServiceDetails?.profiles - .find(p => - p.id === (isAnime ? defaultServiceDetails.server?.activeAnimeProfileId : defaultServiceDetails.server?.activeProfileId) - ), - [defaultServiceDetails] - ); + const defaultProfile: QualityProfile = useMemo( + () => + defaultServiceDetails?.profiles.find( + (p) => + p.id === + (isAnime + ? defaultServiceDetails.server?.activeAnimeProfileId + : defaultServiceDetails.server?.activeProfileId), + ), + [defaultServiceDetails], + ); - const defaultFolder: RootFolder = useMemo( - () => defaultServiceDetails?.rootFolders - .find(f => - f.path === (isAnime ? defaultServiceDetails?.server.activeAnimeDirectory : defaultServiceDetails.server?.activeDirectory) - ), - [defaultServiceDetails] - ); + const defaultFolder: RootFolder = useMemo( + () => + defaultServiceDetails?.rootFolders.find( + (f) => + f.path === + (isAnime + ? defaultServiceDetails?.server.activeAnimeDirectory + : defaultServiceDetails.server?.activeDirectory), + ), + [defaultServiceDetails], + ); - const defaultTags: Tag[] = useMemo( - () => { - const tags = defaultServiceDetails?.tags - .filter(t => + const defaultTags: Tag[] = useMemo(() => { + const tags = + defaultServiceDetails?.tags.filter((t) => (isAnime ? defaultServiceDetails?.server.activeAnimeTags : defaultServiceDetails?.server.activeTags - )?.includes(t.id) - ) ?? [] - return tags - }, - [defaultServiceDetails] - ); + )?.includes(t.id), + ) ?? []; + return tags; + }, [defaultServiceDetails]); - const seasonTitle = useMemo( - () => { + const seasonTitle = useMemo(() => { if (requestBody?.seasons && requestBody?.seasons?.length > 1) { - return t("jellyseerr.season_all") + return t("jellyseerr.season_all"); } - return t("jellyseerr.season_number", {season_number: requestBody?.seasons}) - }, - [requestBody?.seasons] - ); + return t("jellyseerr.season_number", { + season_number: requestBody?.seasons, + }); + }, [requestBody?.seasons]); - const request = useCallback(() => {requestMedia( - seasonTitle ? `${title}, ${seasonTitle}` : title, - { - is4k: defaultService?.is4k || defaultServiceDetails?.server.is4k, - profileId: defaultProfile.id, - rootFolder: defaultFolder.path, - tags: defaultTags.map(t => t.id), - ...requestBody, - ...requestOverrides - }, - onRequested - ) - }, [requestBody, requestOverrides, defaultProfile, defaultFolder, defaultTags]); + const request = useCallback(() => { + requestMedia( + seasonTitle ? `${title}, ${seasonTitle}` : title, + { + is4k: defaultService?.is4k || defaultServiceDetails?.server.is4k, + profileId: defaultProfile.id, + rootFolder: defaultFolder.path, + tags: defaultTags.map((t) => t.id), + ...requestBody, + ...requestOverrides, + }, + onRequested, + ); + }, [ + requestBody, + requestOverrides, + defaultProfile, + defaultFolder, + defaultTags, + ]); - const pathTitleExtractor = (item: RootFolder) => `${item.path} (${item.freeSpace.bytesToReadable()})`; + const pathTitleExtractor = (item: RootFolder) => + `${item.path} (${item.freeSpace.bytesToReadable()})`; - return ( - - - } - > - - - - {t("jellyseerr.advanced")} - {seasonTitle && - {seasonTitle} - } + return ( + ( + + )} + > + + + + + {t("jellyseerr.advanced")} + + {seasonTitle && ( + {seasonTitle} + )} + + + {defaultService && defaultServiceDetails && users && ( + <> + item.name} + placeholderText={ + requestOverrides.profileName || defaultProfile.name + } + keyExtractor={(item) => item.id.toString()} + label={t("jellyseerr.quality_profile")} + onSelected={(item) => + item && + setRequestOverrides((prev) => ({ + ...prev, + profileId: item?.id, + })) + } + title={t("jellyseerr.quality_profile")} + /> + item.id.toString()} + label={t("jellyseerr.root_folder")} + onSelected={(item) => + item && + setRequestOverrides((prev) => ({ + ...prev, + rootFolder: item.path, + })) + } + title={t("jellyseerr.root_folder")} + /> + item.label} + placeholderText={defaultTags.map((t) => t.label).join(",")} + keyExtractor={(item) => item.id.toString()} + label={t("jellyseerr.tags")} + onSelected={(...selected) => + setRequestOverrides((prev) => ({ + ...prev, + tags: selected.map((i) => i.id), + })) + } + title={t("jellyseerr.tags")} + /> + item.displayName} + placeholderText={jellyseerrUser!.displayName} + keyExtractor={(item) => item.id.toString() || ""} + label={t("jellyseerr.request_as")} + onSelected={(item) => + item && + setRequestOverrides((prev) => ({ + ...prev, + userId: item?.id, + })) + } + title={t("jellyseerr.request_as")} + /> + + )} + + - - {(defaultService && defaultServiceDetails && users) && ( - <> - item.name} - placeholderText={requestOverrides.profileName || defaultProfile.name} - keyExtractor={(item) => item.id.toString()} - label={t("jellyseerr.quality_profile")} - onSelected={(item) => - item && setRequestOverrides((prev) => ({ - ...prev, - profileId: item?.id - })) - } - title={t("jellyseerr.quality_profile")} - /> - item.id.toString()} - label={t("jellyseerr.root_folder")} - onSelected={(item) => - item && setRequestOverrides((prev) => ({ - ...prev, - rootFolder: item.path - }))} - title={t("jellyseerr.root_folder")} - /> - item.label} - placeholderText={defaultTags.map(t => t.label).join(",")} - keyExtractor={(item) => item.id.toString()} - label={t("jellyseerr.tags")} - onSelected={(...selected) => - setRequestOverrides((prev) => ({ - ...prev, - tags: selected.map(i => i.id) - })) - } - title={t("jellyseerr.tags")} - /> - item.displayName} - placeholderText={jellyseerrUser!!.displayName} - keyExtractor={(item) => item.id.toString() || ""} - label={t("jellyseerr.request_as")} - onSelected={(item) => - item && setRequestOverrides((prev) => ({ - ...prev, - userId: item?.id - })) - } - title={t("jellyseerr.request_as")} - /> - - ) - } - - - - - - ); -}); + + + ); + }, +); -export default RequestModal; \ No newline at end of file +export default RequestModal; diff --git a/components/jellyseerr/discover/CompanySlide.tsx b/components/jellyseerr/discover/CompanySlide.tsx index abee4a9d..fab70288 100644 --- a/components/jellyseerr/discover/CompanySlide.tsx +++ b/components/jellyseerr/discover/CompanySlide.tsx @@ -1,14 +1,15 @@ import GenericSlideCard from "@/components/jellyseerr/discover/GenericSlideCard"; -import Slide, { SlideProps } from "@/components/jellyseerr/discover/Slide"; +import Slide, { type SlideProps } from "@/components/jellyseerr/discover/Slide"; import { useJellyseerr } from "@/hooks/useJellyseerr"; import { COMPANY_LOGO_IMAGE_FILTER, - Network, + type Network, } from "@/utils/jellyseerr/src/components/Discover/NetworkSlider"; -import { Studio } from "@/utils/jellyseerr/src/components/Discover/StudioSlider"; +import type { Studio } from "@/utils/jellyseerr/src/components/Discover/StudioSlider"; import { router, useSegments } from "expo-router"; -import React, { useCallback } from "react"; -import { TouchableOpacity, ViewProps } from "react-native"; +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 @@ -23,7 +24,7 @@ const CompanySlide: React.FC< pathname: `/(auth)/(tabs)/${from}/jellyseerr/company/${id}`, params: { id, image, name, type: slide.type }, }), - [slide] + [slide], ); return ( @@ -33,13 +34,13 @@ const CompanySlide: React.FC< data={data} keyExtractor={(item) => item.id.toString()} renderItem={(item, index) => ( - navigate(item)}> + navigate(item)}> diff --git a/components/jellyseerr/discover/Discover.tsx b/components/jellyseerr/discover/Discover.tsx index 847b7e7b..fb3c0084 100644 --- a/components/jellyseerr/discover/Discover.tsx +++ b/components/jellyseerr/discover/Discover.tsx @@ -1,50 +1,69 @@ -import React, {useMemo} from "react"; -import DiscoverSlider from "@/utils/jellyseerr/server/entity/DiscoverSlider"; -import {DiscoverSliderType} from "@/utils/jellyseerr/server/constants/discover"; -import {sortBy} from "lodash"; -import MovieTvSlide from "@/components/jellyseerr/discover/MovieTvSlide"; import CompanySlide from "@/components/jellyseerr/discover/CompanySlide"; -import {View} from "react-native"; -import {networks} from "@/utils/jellyseerr/src/components/Discover/NetworkSlider"; -import {studios} from "@/utils/jellyseerr/src/components/Discover/StudioSlider"; import GenreSlide from "@/components/jellyseerr/discover/GenreSlide"; +import MovieTvSlide from "@/components/jellyseerr/discover/MovieTvSlide"; import RecentRequestsSlide from "@/components/jellyseerr/discover/RecentRequestsSlide"; +import { DiscoverSliderType } from "@/utils/jellyseerr/server/constants/discover"; +import type DiscoverSlider from "@/utils/jellyseerr/server/entity/DiscoverSlider"; +import { networks } from "@/utils/jellyseerr/src/components/Discover/NetworkSlider"; +import { studios } from "@/utils/jellyseerr/src/components/Discover/StudioSlider"; +import { sortBy } from "lodash"; +import type React from "react"; +import { useMemo } from "react"; +import { View } from "react-native"; interface Props { sliders?: DiscoverSlider[]; } const Discover: React.FC = ({ sliders }) => { - if (!sliders) - return; + if (!sliders) return; const sortedSliders = useMemo( - () => sortBy(sliders.filter((s) => s.enabled), 'order', 'asc'), - [sliders] + () => + sortBy( + sliders.filter((s) => s.enabled), + "order", + "asc", + ), + [sliders], ); return ( - - {sortedSliders.map(slide => { + + {sortedSliders.map((slide) => { switch (slide.type) { case DiscoverSliderType.RECENT_REQUESTS: - return + return ( + + ); case DiscoverSliderType.NETWORKS: - return + return ( + + ); case DiscoverSliderType.STUDIOS: - return + return ; case DiscoverSliderType.MOVIE_GENRES: case DiscoverSliderType.TV_GENRES: - return + return ; case DiscoverSliderType.TRENDING: case DiscoverSliderType.POPULAR_MOVIES: case DiscoverSliderType.UPCOMING_MOVIES: case DiscoverSliderType.POPULAR_TV: case DiscoverSliderType.UPCOMING_TV: - return + return ( + + ); } })} - ) + ); }; export default Discover; diff --git a/components/jellyseerr/discover/GenericSlideCard.tsx b/components/jellyseerr/discover/GenericSlideCard.tsx index 776d1424..51292abd 100644 --- a/components/jellyseerr/discover/GenericSlideCard.tsx +++ b/components/jellyseerr/discover/GenericSlideCard.tsx @@ -1,8 +1,8 @@ -import React from "react"; -import {StyleSheet, View, ViewProps} from "react-native"; -import {Image, ImageContentFit} from "expo-image"; -import {Text} from "@/components/common/Text"; -import {LinearGradient} from "expo-linear-gradient"; +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"; export const textShadowStyle = StyleSheet.create({ shadow: { @@ -12,48 +12,59 @@ export const textShadowStyle = StyleSheet.create({ height: 1, }, shadowOpacity: 1, - shadowRadius: .5, + shadowRadius: 0.5, elevation: 6, - } -}) + }, +}); -const GenericSlideCard: React.FC<{id: string; url?: string, title?: string, colors?: string[], contentFit?: ImageContentFit} & ViewProps> = ({ +const GenericSlideCard: React.FC< + { + id: string; + url?: string; + title?: string; + colors?: string[]; + contentFit?: ImageContentFit; + } & ViewProps +> = ({ id, url, title, - colors = ['#9333ea', 'transparent'], + colors = ["#9333ea", "transparent"], contentFit = "contain", ...props }) => ( <> - + - {title && - + - {title} - - } + {title} + + + )} ); -export default GenericSlideCard; \ No newline at end of file +export default GenericSlideCard; diff --git a/components/jellyseerr/discover/GenreSlide.tsx b/components/jellyseerr/discover/GenreSlide.tsx index 3ec7f733..06b10a5a 100644 --- a/components/jellyseerr/discover/GenreSlide.tsx +++ b/components/jellyseerr/discover/GenreSlide.tsx @@ -1,13 +1,14 @@ import GenericSlideCard from "@/components/jellyseerr/discover/GenericSlideCard"; -import Slide, { SlideProps } from "@/components/jellyseerr/discover/Slide"; +import Slide, { type SlideProps } from "@/components/jellyseerr/discover/Slide"; import { Endpoints, useJellyseerr } from "@/hooks/useJellyseerr"; import { DiscoverSliderType } from "@/utils/jellyseerr/server/constants/discover"; -import { GenreSliderItem } from "@/utils/jellyseerr/server/interfaces/api/discoverInterfaces"; +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 React, { useCallback } from "react"; -import { TouchableOpacity, ViewProps } from "react-native"; +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(); @@ -20,7 +21,7 @@ const GenreSlide: React.FC = ({ slide, ...props }) => { pathname: `/(auth)/(tabs)/${from}/jellyseerr/genre/${genre.id}`, params: { type: slide.type, name: genre.name }, }), - [slide] + [slide], ); const { data, isFetching, isLoading } = useQuery({ @@ -29,7 +30,7 @@ const GenreSlide: React.FC = ({ slide, ...props }) => { return jellyseerrApi?.getGenreSliders( slide.type == DiscoverSliderType.MOVIE_GENRES ? Endpoints.MOVIE - : Endpoints.TV + : Endpoints.TV, ); }, enabled: !!jellyseerrApi, @@ -43,18 +44,18 @@ const GenreSlide: React.FC = ({ slide, ...props }) => { data={data} keyExtractor={(item) => item.id.toString()} renderItem={(item, index) => ( - navigate(item)}> + navigate(item)}> diff --git a/components/jellyseerr/discover/MovieTvSlide.tsx b/components/jellyseerr/discover/MovieTvSlide.tsx index c3d9d690..a5611849 100644 --- a/components/jellyseerr/discover/MovieTvSlide.tsx +++ b/components/jellyseerr/discover/MovieTvSlide.tsx @@ -1,18 +1,25 @@ -import React, {useMemo} from "react"; -import { DiscoverSliderType } from "@/utils/jellyseerr/server/constants/discover"; +import Slide, { type SlideProps } from "@/components/jellyseerr/discover/Slide"; +import JellyseerrPoster from "@/components/posters/JellyseerrPoster"; import { - DiscoverEndpoint, + type DiscoverEndpoint, Endpoints, useJellyseerr, } from "@/hooks/useJellyseerr"; +import { DiscoverSliderType } from "@/utils/jellyseerr/server/constants/discover"; +import type { + MovieResult, + TvResult, +} from "@/utils/jellyseerr/server/models/Search"; import { useInfiniteQuery } from "@tanstack/react-query"; -import { MovieResult, TvResult } from "@/utils/jellyseerr/server/models/Search"; -import JellyseerrPoster from "@/components/posters/JellyseerrPoster"; -import Slide, {SlideProps} from "@/components/jellyseerr/discover/Slide"; -import {ViewProps} from "react-native"; -import {uniqBy} from "lodash"; +import { uniqBy } from "lodash"; +import type React from "react"; +import { useMemo } from "react"; +import type { ViewProps } from "react-native"; -const MovieTvSlide: React.FC = ({ slide, ...props }) => { +const MovieTvSlide: React.FC = ({ + slide, + ...props +}) => { const { jellyseerrApi } = useJellyseerr(); const { data, fetchNextPage, hasNextPage } = useInfiniteQuery({ @@ -60,10 +67,12 @@ const MovieTvSlide: React.FC = ({ slide, ...props }) => const flatData = useMemo( () => uniqBy( - data?.pages?.filter((p) => p?.results.length).flatMap((p) => p?.results), - "id" + data?.pages + ?.filter((p) => p?.results.length) + .flatMap((p) => p?.results), + "id", ), - [data] + [data], ); return ( @@ -73,14 +82,16 @@ const MovieTvSlide: React.FC = ({ slide, ...props }) => {...props} slide={slide} data={flatData} - keyExtractor={(item) => item!!.id.toString()} + keyExtractor={(item) => item!.id.toString()} onEndReached={() => { - if (hasNextPage) - fetchNextPage() + if (hasNextPage) fetchNextPage(); }} - renderItem={(item) => - - } + renderItem={(item) => ( + + )} /> ) ); diff --git a/components/jellyseerr/discover/RecentRequestsSlide.tsx b/components/jellyseerr/discover/RecentRequestsSlide.tsx index dce6d7b9..7f88e40e 100644 --- a/components/jellyseerr/discover/RecentRequestsSlide.tsx +++ b/components/jellyseerr/discover/RecentRequestsSlide.tsx @@ -1,20 +1,28 @@ -import React from "react"; -import {useQuery} from "@tanstack/react-query"; -import {useJellyseerr} from "@/hooks/useJellyseerr"; -import Slide, {SlideProps} from "@/components/jellyseerr/discover/Slide"; -import {ViewProps} from "react-native"; -import MediaRequest from "@/utils/jellyseerr/server/entity/MediaRequest"; -import {NonFunctionProperties} from "@/utils/jellyseerr/server/interfaces/api/common"; -import {MediaType} from "@/utils/jellyseerr/server/constants/media"; +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(); +const RequestCard: React.FC<{ request: MediaRequest }> = ({ request }) => { + const { jellyseerrApi } = useJellyseerr(); - const { data: details, isLoading, isError } = useQuery({ - queryKey: ["jellyseerr", "detail", request.media.mediaType, request.media.tmdbId], + const { + data: details, + isLoading, + isError, + } = useQuery({ + queryKey: [ + "jellyseerr", + "detail", + request.media.mediaType, + request.media.tmdbId, + ], queryFn: async () => { - return request.media.mediaType == MediaType.MOVIE ? jellyseerrApi?.movieDetails(request.media.tmdbId) : jellyseerrApi?.tvDetails(request.media.tmdbId); @@ -34,34 +42,47 @@ const RequestCard: React.FC<{request: MediaRequest}> = ({request}) => { }); return ( - - ) -} + + ); +}; -const RecentRequestsSlide: React.FC = ({ slide, ...props }) => { - const {jellyseerrApi} = useJellyseerr(); +const RecentRequestsSlide: React.FC = ({ + slide, + ...props +}) => { + const { jellyseerrApi } = useJellyseerr(); - const { data: requests, isLoading, isError } = useQuery({ + const { + data: requests, + isLoading, + isError, + } = useQuery({ queryKey: ["jellyseerr", "recent_requests"], - queryFn: async () => jellyseerrApi?.requests(), + queryFn: async () => jellyseerrApi?.requests(), enabled: !!jellyseerrApi, refetchOnMount: true, staleTime: 0, }); return ( - requests && requests.results.length > 0 && ( + requests && + requests.results.length > 0 && ( item.id.toString()} renderItem={(item: NonFunctionProperties) => ( - + )} /> ) - ) + ); }; -export default RecentRequestsSlide; \ No newline at end of file +export default RecentRequestsSlide; diff --git a/components/jellyseerr/discover/Slide.tsx b/components/jellyseerr/discover/Slide.tsx index 19296fbf..9e2298dc 100644 --- a/components/jellyseerr/discover/Slide.tsx +++ b/components/jellyseerr/discover/Slide.tsx @@ -1,11 +1,12 @@ -import React, {PropsWithChildren} from "react"; -import DiscoverSlider from "@/utils/jellyseerr/server/entity/DiscoverSlider"; -import { DiscoverSliderType } from "@/utils/jellyseerr/server/constants/discover"; 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 {View, ViewProps} from "react-native"; +import type { ContentStyle } from "@shopify/flash-list/src/FlashListProps"; import { t } from "i18next"; -import {ContentStyle} from "@shopify/flash-list/src/FlashListProps"; +import type React from "react"; +import type { PropsWithChildren } from "react"; +import { View, type ViewProps } from "react-native"; export interface SlideProps { slide: DiscoverSlider; @@ -13,17 +14,16 @@ export interface SlideProps { } interface Props extends SlideProps { - data: T[] - renderItem: (item: T, index: number) => - | React.ComponentType - | React.ReactElement - | null - | undefined; + data: T[]; + renderItem: ( + item: T, + index: number, + ) => React.ComponentType | React.ReactElement | null | undefined; keyExtractor: (item: T) => string; onEndReached?: (() => void) | null | undefined; } -const Slide = ({ +const Slide = ({ data, slide, renderItem, @@ -31,18 +31,17 @@ const Slide = ({ onEndReached, contentContainerStyle, ...props -}: PropsWithChildren & ViewProps> -) => { +}: PropsWithChildren & ViewProps>) => { return ( - + {t("search." + DiscoverSliderType[slide.type].toString().toLowerCase())} ({ onEndReachedThreshold={1} onEndReached={onEndReached} //@ts-ignore - renderItem={({item, index}) => item ? renderItem(item, index) : <>} + renderItem={({ item, index }) => + item ? renderItem(item, index) : <> + } /> ); diff --git a/components/library/LibraryItemCard.tsx b/components/library/LibraryItemCard.tsx index a7b78f0a..34c6f8ef 100644 --- a/components/library/LibraryItemCard.tsx +++ b/components/library/LibraryItemCard.tsx @@ -3,7 +3,7 @@ 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 { +import type { BaseItemDto, BaseItemKind, CollectionType, @@ -13,9 +13,9 @@ import { useQuery } from "@tanstack/react-query"; import { Image } from "expo-image"; import { useAtom } from "jotai"; import { useMemo } from "react"; -import { TouchableOpacityProps, View } from "react-native"; +import { useTranslation } from "react-i18next"; +import { type TouchableOpacityProps, View } from "react-native"; import { TouchableItemRouter } from "../common/TouchableItemRouter"; -import { useTranslation } from "react-i18next"; interface Props extends TouchableOpacityProps { library: BaseItemDto; @@ -51,7 +51,7 @@ export const LibraryItemCard: React.FC = ({ library, ...props }) => { api, item: library, }), - [library] + [library], ); const itemType = useMemo(() => { @@ -102,18 +102,18 @@ export const LibraryItemCard: React.FC = ({ library, ...props }) => { if (settings?.libraryOptions?.display === "row") { return ( - - + + - + {library.Name} {settings?.libraryOptions?.showStats && ( - + {itemsCount} {itemTypeName} )} @@ -124,8 +124,8 @@ export const LibraryItemCard: React.FC = ({ library, ...props }) => { if (settings?.libraryOptions?.imageStyle === "cover") { return ( - - + + = ({ library, ...props }) => { /> {settings?.libraryOptions?.showTitles && ( - + {library.Name} )} {settings?.libraryOptions?.showStats && ( - + {itemsCount} {itemTypeName} )} @@ -173,21 +173,21 @@ export const LibraryItemCard: React.FC = ({ library, ...props }) => { return ( - - - + + + {library.Name} {settings?.libraryOptions?.showStats && ( - + {itemsCount} {itemTypeName} )} - + diff --git a/components/list/ListGroup.tsx b/components/list/ListGroup.tsx index 03f218d1..28752978 100644 --- a/components/list/ListGroup.tsx +++ b/components/list/ListGroup.tsx @@ -1,13 +1,13 @@ import { - PropsWithChildren, Children, - isValidElement, + type PropsWithChildren, + type ReactElement, cloneElement, - ReactElement, + isValidElement, } from "react"; -import { StyleSheet, View, ViewProps, ViewStyle } from "react-native"; -import { ListItem } from "./ListItem"; +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; @@ -24,12 +24,12 @@ export const ListGroup: React.FC> = ({ return ( - + {title} {Children.map(childrenArray, (child, index) => { if (isValidElement<{ style?: ViewStyle }>(child)) { @@ -38,14 +38,14 @@ export const ListGroup: React.FC> = ({ child.props.style, index < childrenArray.length - 1 ? styles.borderBottom - : undefined + : undefined, ), }); } return child; })} - {description && {description}} + {description && {description}} ); }; diff --git a/components/list/ListItem.tsx b/components/list/ListItem.tsx index ea7774a4..892e3b41 100644 --- a/components/list/ListItem.tsx +++ b/components/list/ListItem.tsx @@ -1,10 +1,10 @@ import { Ionicons } from "@expo/vector-icons"; -import { PropsWithChildren, ReactNode } from "react"; +import type { PropsWithChildren, ReactNode } from "react"; import { TouchableOpacity, - TouchableOpacityProps, + type TouchableOpacityProps, View, - ViewProps, + type ViewProps, } from "react-native"; import { Text } from "../common/Text"; @@ -86,10 +86,10 @@ const ListItemContent = ({ }: Props) => { return ( <> - + {icon && ( - - + + )} {title} {value && ( - - + + {value} )} - {children && {children}} + {children && {children}} {showArrow && ( - + )} diff --git a/components/livetv/HourHeader.tsx b/components/livetv/HourHeader.tsx index 99344e43..f412cb9b 100644 --- a/components/livetv/HourHeader.tsx +++ b/components/livetv/HourHeader.tsx @@ -10,7 +10,7 @@ export const HourHeader = ({ height }: { height: number }) => { return ( { }; const HourCell = ({ hour }: { hour: Date }) => ( - - + + {hour.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", diff --git a/components/livetv/LiveTVGuideRow.tsx b/components/livetv/LiveTVGuideRow.tsx index cbb70d19..83a5ada1 100644 --- a/components/livetv/LiveTVGuideRow.tsx +++ b/components/livetv/LiveTVGuideRow.tsx @@ -1,4 +1,4 @@ -import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client"; +import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client"; import { useMemo, useRef } from "react"; import { Dimensions, View } from "react-native"; import { Text } from "../common/Text"; @@ -53,7 +53,7 @@ export const LiveTVGuideRow = ({ } return ( - + {programsWithPositions?.map((p) => ( {(() => { return ( @@ -77,11 +77,11 @@ export const LiveTVGuideRow = ({ ? scrollX - p.position : 0, }} - className="px-4 self-start" + className='px-4 self-start' > {p.Name} diff --git a/components/medialists/MediaListSection.tsx b/components/medialists/MediaListSection.tsx index ff5e60a2..13db9826 100644 --- a/components/medialists/MediaListSection.tsx +++ b/components/medialists/MediaListSection.tsx @@ -1,22 +1,22 @@ import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; -import { +import type { BaseItemDto, BaseItemDtoQueryResult, } from "@jellyfin/sdk/lib/generated-client/models"; import { getItemsApi } from "@jellyfin/sdk/lib/utils/api"; +import { + type QueryFunction, + type QueryKey, + useQuery, +} from "@tanstack/react-query"; import { useAtom } from "jotai"; import { useCallback } from "react"; -import { View, ViewProps } from "react-native"; +import { View, type ViewProps } from "react-native"; +import { ItemCardText } from "../ItemCardText"; import { InfiniteHorizontalScroll } from "../common/InfiniteHorrizontalScroll"; import { Text } from "../common/Text"; import { TouchableItemRouter } from "../common/TouchableItemRouter"; -import { ItemCardText } from "../ItemCardText"; import MoviePoster from "../posters/MoviePoster"; -import { - type QueryKey, - type QueryFunction, - useQuery, -} from "@tanstack/react-query"; interface Props extends ViewProps { queryKey: QueryKey; @@ -54,14 +54,14 @@ export const MediaListSection: React.FC = ({ return response.data; }, - [api, user?.Id, collection?.Id] + [api, user?.Id, collection?.Id], ); if (!collection) return null; return ( - + {collection.Name} = ({ item, ...props }) => { return ( - + {item?.Name} - {item?.ProductionYear} + {item?.ProductionYear} ); }; diff --git a/components/navigation/TabBarIcon.tsx b/components/navigation/TabBarIcon.tsx index 17c2ebed..0cc6eff6 100644 --- a/components/navigation/TabBarIcon.tsx +++ b/components/navigation/TabBarIcon.tsx @@ -1,8 +1,8 @@ // 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 { type ComponentProps } from "react"; +import type { IconProps } from "@expo/vector-icons/build/createIconSet"; +import type { ComponentProps } from "react"; export function TabBarIcon({ style, diff --git a/components/posters/EpisodePoster.tsx b/components/posters/EpisodePoster.tsx index c82464d5..f5c92485 100644 --- a/components/posters/EpisodePoster.tsx +++ b/components/posters/EpisodePoster.tsx @@ -1,11 +1,11 @@ +import { WatchedIndicator } from "@/components/WatchedIndicator"; import { apiAtom } from "@/providers/JellyfinProvider"; import { getPrimaryImageUrl } from "@/utils/jellyfin/image/getPrimaryImageUrl"; -import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; +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"; type MoviePosterProps = { item: BaseItemDto; @@ -25,7 +25,7 @@ export const EpisodePoster: React.FC = ({ }, [item]); const [progress, setProgress] = useState( - item.UserData?.PlayedPercentage || 0 + item.UserData?.PlayedPercentage || 0, ); const blurhash = useMemo(() => { @@ -34,7 +34,7 @@ export const EpisodePoster: React.FC = ({ }, [item]); return ( - + = ({ : null } cachePolicy={"memory-disk"} - contentFit="cover" + contentFit='cover' style={{ aspectRatio: "10/15", width: "100%", @@ -57,7 +57,7 @@ export const EpisodePoster: React.FC = ({ /> {showProgress && progress > 0 && ( - + )} ); diff --git a/components/posters/ItemPoster.tsx b/components/posters/ItemPoster.tsx index 86575ab9..b8605f97 100644 --- a/components/posters/ItemPoster.tsx +++ b/components/posters/ItemPoster.tsx @@ -1,12 +1,12 @@ -import { View, ViewProps } from "react-native"; import { Text } from "@/components/common/Text"; import { - BaseItemDto, + type BaseItemDto, BaseItemKind, } from "@jellyfin/sdk/lib/generated-client/models"; -import { ItemImage } from "../common/ItemImage"; -import { WatchedIndicator } from "../WatchedIndicator"; import { useState } from "react"; +import { View, type ViewProps } from "react-native"; +import { WatchedIndicator } from "../WatchedIndicator"; +import { ItemImage } from "../common/ItemImage"; interface Props extends ViewProps { item: BaseItemDto; @@ -19,13 +19,13 @@ export const ItemPoster: React.FC = ({ ...props }) => { const [progress, setProgress] = useState( - item.UserData?.PlayedPercentage || 0 + item.UserData?.PlayedPercentage || 0, ); if (item.Type === "Movie" || item.Type === "Series" || item.Type === "BoxSet") return ( = ({ /> {showProgress && progress > 0 && ( - + )} ); return ( - + ); }; diff --git a/components/posters/JellyseerrPoster.tsx b/components/posters/JellyseerrPoster.tsx index 25c16d4f..b25f8974 100644 --- a/components/posters/JellyseerrPoster.tsx +++ b/components/posters/JellyseerrPoster.tsx @@ -1,23 +1,30 @@ -import {TouchableJellyseerrRouter} from "@/components/common/JellyseerrItemRouter"; -import {Text} from "@/components/common/Text"; +import { Tag, Tags } from "@/components/GenreTags"; +import { TouchableJellyseerrRouter } from "@/components/common/JellyseerrItemRouter"; +import { Text } from "@/components/common/Text"; import JellyseerrMediaIcon from "@/components/jellyseerr/JellyseerrMediaIcon"; import JellyseerrStatusIcon from "@/components/jellyseerr/JellyseerrStatusIcon"; -import {useJellyseerr} from "@/hooks/useJellyseerr"; -import {useJellyseerrCanRequest} from "@/utils/_jellyseerr/useJellyseerrCanRequest"; -import {MovieResult, TvResult} from "@/utils/jellyseerr/server/models/Search"; -import {Image} from "expo-image"; -import {useMemo} from "react"; -import {View, ViewProps} from "react-native"; -import Animated, {useAnimatedStyle, useSharedValue, withTiming,} from "react-native-reanimated"; -import {TvDetails} from "@/utils/jellyseerr/server/models/Tv"; -import {MovieDetails} from "@/utils/jellyseerr/server/models/Movie"; -import type {DownloadingItem} from "@/utils/jellyseerr/server/lib/downloadtracker"; -import MediaRequest from "@/utils/jellyseerr/server/entity/MediaRequest"; -import {useTranslation} from "react-i18next"; -import {MediaStatus} from "@/utils/jellyseerr/server/constants/media"; -import {textShadowStyle} from "@/components/jellyseerr/discover/GenericSlideCard"; -import {Colors} from "@/constants/Colors"; -import {Tag, Tags} from "@/components/GenreTags"; +import { textShadowStyle } from "@/components/jellyseerr/discover/GenericSlideCard"; +import { Colors } from "@/constants/Colors"; +import { useJellyseerr } from "@/hooks/useJellyseerr"; +import { useJellyseerrCanRequest } from "@/utils/_jellyseerr/useJellyseerrCanRequest"; +import { MediaStatus } from "@/utils/jellyseerr/server/constants/media"; +import type MediaRequest from "@/utils/jellyseerr/server/entity/MediaRequest"; +import type { DownloadingItem } from "@/utils/jellyseerr/server/lib/downloadtracker"; +import type { MovieDetails } from "@/utils/jellyseerr/server/models/Movie"; +import type { + MovieResult, + 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; @@ -36,7 +43,7 @@ const JellyseerrPoster: React.FC = ({ const { jellyseerrApi, getTitle, getYear, getMediaType } = useJellyseerr(); const loadingOpacity = useSharedValue(1); const imageOpacity = useSharedValue(0); - const {t} = useTranslation(); + const { t } = useTranslation(); const imageAnimatedStyle = useAnimatedStyle(() => ({ opacity: imageOpacity.value, @@ -48,65 +55,70 @@ const JellyseerrPoster: React.FC = ({ }; const backdropSrc = useMemo( - () => jellyseerrApi?.imageProxy(item?.backdropPath, "w1920_and_h800_multi_faces"), - [item, jellyseerrApi, horizontal] + () => + jellyseerrApi?.imageProxy( + item?.backdropPath, + "w1920_and_h800_multi_faces", + ), + [item, jellyseerrApi, horizontal], ); const posterSrc = useMemo( - () => jellyseerrApi?.imageProxy(item?.posterPath, "w300_and_h450_face",), - [item, jellyseerrApi, horizontal] + () => jellyseerrApi?.imageProxy(item?.posterPath, "w300_and_h450_face"), + [item, jellyseerrApi, horizontal], ); const title = useMemo(() => getTitle(item), [item]); const releaseYear = useMemo(() => getYear(item), [item]); const mediaType = useMemo(() => getMediaType(item), [item]); - const size = useMemo(() => horizontal ? 'h-28' : 'w-28', [horizontal]) - const ratio = useMemo(() => horizontal ? '15/10' : '10/15', [horizontal]) + const size = useMemo(() => (horizontal ? "h-28" : "w-28"), [horizontal]); + const ratio = useMemo(() => (horizontal ? "15/10" : "10/15"), [horizontal]); const [canRequest] = useJellyseerrCanRequest(item); - const is4k = useMemo( - () => mediaRequest?.is4k === true, - [mediaRequest] - ); + const is4k = useMemo(() => mediaRequest?.is4k === true, [mediaRequest]); const downloadItems = useMemo( - () => (is4k ? mediaRequest?.media.downloadStatus4k : mediaRequest?.media.downloadStatus) || [], - [mediaRequest, is4k] - ) + () => + (is4k + ? mediaRequest?.media.downloadStatus4k + : mediaRequest?.media.downloadStatus) || [], + [mediaRequest, is4k], + ); const progress = useMemo(() => { - const [totalSize, sizeLeft] = downloadItems - .reduce((sum: number[], next: DownloadingItem) => - [sum[0] + next.size, sum[1] + next.sizeLeft], - [0, 0] - ); + const [totalSize, sizeLeft] = downloadItems.reduce( + (sum: number[], next: DownloadingItem) => [ + sum[0] + next.size, + sum[1] + next.sizeLeft, + ], + [0, 0], + ); - return (((totalSize - sizeLeft) / totalSize) * 100); - }, - [downloadItems] - ); + return ((totalSize - sizeLeft) / totalSize) * 100; + }, [downloadItems]); - const requestedSeasons: string[] | undefined = useMemo( - () => { - const seasons = mediaRequest?.seasons?.flatMap(s => s.seasonNumber.toString()) || [] - if (seasons.length > 4) { - const [first, second, third, fourth, ...rest] = seasons; - return [first, second, third, fourth, t("home.settings.plugins.jellyseerr.plus_n_more", {n: rest.length })] - } - return seasons - }, - [mediaRequest] - ); + const requestedSeasons: string[] | undefined = useMemo(() => { + const seasons = + mediaRequest?.seasons?.flatMap((s) => s.seasonNumber.toString()) || []; + if (seasons.length > 4) { + const [first, second, third, fourth, ...rest] = seasons; + return [ + first, + second, + third, + fourth, + t("home.settings.plugins.jellyseerr.plus_n_more", { n: rest.length }), + ]; + } + return seasons; + }, [mediaRequest]); - const available = useMemo( - () => { - const status = mediaRequest?.media?.[is4k ? 'status4k' : 'status']; - return status === MediaStatus.AVAILABLE - }, - [mediaRequest, is4k] - ); + const available = useMemo(() => { + const status = mediaRequest?.media?.[is4k ? "status4k" : "status"]; + return status === MediaStatus.AVAILABLE; + }, [mediaRequest, is4k]); return ( = ({ mediaTitle={title} releaseYear={releaseYear} canRequest={canRequest} - posterSrc={posterSrc!!} + posterSrc={posterSrc!} mediaType={mediaType} > - + {mediaRequest && showDownloadInfo && ( <> - + {!available && !Number.isNaN(progress) && ( <> - - + + {progress?.toFixed(0)}% )} {requestedSeasons.length > 0 && ( @@ -172,19 +185,21 @@ const JellyseerrPoster: React.FC = ({ )} - + {title || ""} - {releaseYear || ""} + + {releaseYear || ""} + ); diff --git a/components/posters/MoviePoster.tsx b/components/posters/MoviePoster.tsx index 46776fb7..61fa3b03 100644 --- a/components/posters/MoviePoster.tsx +++ b/components/posters/MoviePoster.tsx @@ -1,7 +1,7 @@ import { WatchedIndicator } from "@/components/WatchedIndicator"; import { apiAtom } from "@/providers/JellyfinProvider"; import { getPrimaryImageUrl } from "@/utils/jellyfin/image/getPrimaryImageUrl"; -import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; +import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; import { Image } from "expo-image"; import { useAtom } from "jotai"; import { useMemo, useState } from "react"; @@ -27,7 +27,7 @@ const MoviePoster: React.FC = ({ }, [item]); const [progress, setProgress] = useState( - item.UserData?.PlayedPercentage || 0 + item.UserData?.PlayedPercentage || 0, ); const blurhash = useMemo(() => { @@ -36,7 +36,7 @@ const MoviePoster: React.FC = ({ }, [item]); return ( - + = ({ : null } cachePolicy={"memory-disk"} - contentFit="cover" + contentFit='cover' style={{ aspectRatio: "10/15", width: "100%", @@ -59,7 +59,7 @@ const MoviePoster: React.FC = ({ /> {showProgress && progress > 0 && ( - + )} ); diff --git a/components/posters/ParentPoster.tsx b/components/posters/ParentPoster.tsx index 70bc6629..65cc4493 100644 --- a/components/posters/ParentPoster.tsx +++ b/components/posters/ParentPoster.tsx @@ -14,13 +14,13 @@ const ParentPoster: React.FC = ({ id }) => { const url = useMemo( () => `${api?.basePath}/Items/${id}/Images/Primary`, - [id] + [id], ); if (!url || !id) return ( = ({ id }) => { ); return ( - + = ({ id }) => { uri: url, }} cachePolicy={"memory-disk"} - contentFit="cover" + contentFit='cover' style={{ aspectRatio: "10/15", }} diff --git a/components/posters/Poster.tsx b/components/posters/Poster.tsx index 68799f47..77718ad0 100644 --- a/components/posters/Poster.tsx +++ b/components/posters/Poster.tsx @@ -12,7 +12,7 @@ const Poster: React.FC = ({ id, url, blurhash }) => { if (!id && !url) return ( = ({ id, url, blurhash }) => { ); return ( - + = ({ id, url, blurhash }) => { : null } key={id} - id={id!!} + id={id!} source={ url ? { @@ -39,7 +39,7 @@ const Poster: React.FC = ({ id, url, blurhash }) => { : null } cachePolicy={"memory-disk"} - contentFit="cover" + contentFit='cover' style={{ aspectRatio: "10/15", }} diff --git a/components/posters/SeriesPoster.tsx b/components/posters/SeriesPoster.tsx index e551624a..893fae5a 100644 --- a/components/posters/SeriesPoster.tsx +++ b/components/posters/SeriesPoster.tsx @@ -1,11 +1,11 @@ +import { WatchedIndicator } from "@/components/WatchedIndicator"; import { apiAtom } from "@/providers/JellyfinProvider"; import { getPrimaryImageUrl } from "@/utils/jellyfin/image/getPrimaryImageUrl"; -import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; +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"; type MoviePosterProps = { item: BaseItemDto; @@ -32,7 +32,7 @@ const SeriesPoster: React.FC = ({ item }) => { }, [item]); return ( - + = ({ item }) => { : null } cachePolicy={"memory-disk"} - contentFit="cover" + contentFit='cover' style={{ height: "100%", width: "100%", diff --git a/components/search/LoadingSkeleton.tsx b/components/search/LoadingSkeleton.tsx index 8ac38ada..9df34938 100644 --- a/components/search/LoadingSkeleton.tsx +++ b/components/search/LoadingSkeleton.tsx @@ -1,11 +1,11 @@ import { View } from "react-native"; -import { Text } from "../common/Text"; import Animated, { useAnimatedStyle, useAnimatedReaction, useSharedValue, withTiming, } from "react-native-reanimated"; +import { Text } from "../common/Text"; interface Props { isLoading: boolean; @@ -28,29 +28,29 @@ export const LoadingSkeleton: React.FC = ({ isLoading }) => { } else { opacity.value = withTiming(0, { duration: 200 }); } - } + }, ); return ( - + {[1, 2, 3].map((s) => ( - - - + + + {[1, 2, 3].map((i) => ( - - - + + + Nisi mollit voluptate amet. - + Lorem ipsum diff --git a/components/search/SearchItemWrapper.tsx b/components/search/SearchItemWrapper.tsx index b24775ab..81d5e4ce 100644 --- a/components/search/SearchItemWrapper.tsx +++ b/components/search/SearchItemWrapper.tsx @@ -1,11 +1,12 @@ import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; import { getUserItemData } from "@/utils/jellyfin/user-library/getUserItemData"; -import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client"; +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 React, { PropsWithChildren } from "react"; +import type React from "react"; +import type { PropsWithChildren } from "react"; import { Text } from "../common/Text"; -import {FlashList} from "@shopify/flash-list"; type SearchItemWrapperProps = { ids?: string[] | null; @@ -15,12 +16,12 @@ type SearchItemWrapperProps = { onEndReached?: (() => void) | null | undefined; }; -export const SearchItemWrapper = ({ +export const SearchItemWrapper = ({ ids, items, renderItem, header, - onEndReached + onEndReached, }: PropsWithChildren>) => { const [api] = useAtom(apiAtom); const [user] = useAtom(userAtom); @@ -37,25 +38,25 @@ export const SearchItemWrapper = ({ api, userId: user.Id, itemId: id, - }) + }), ); const results = await Promise.all(itemPromises); // Filter out null items return results.filter( - (item) => item !== null + (item) => item !== null, ) as unknown as BaseItemDto[]; }, enabled: !!ids && ids.length > 0 && !!api && !!user?.Id, - staleTime: Infinity, + staleTime: Number.POSITIVE_INFINITY, }); if (!data && (!items || items.length === 0)) return null; return ( <> - {header} + {header} ({ onEndReachedThreshold={1} onEndReached={onEndReached} //@ts-ignore - renderItem={({item, index}) => item ? renderItem(item) : <>} + renderItem={({ item, index }) => (item ? renderItem(item) : <>)} /> ); diff --git a/components/series/CastAndCrew.tsx b/components/series/CastAndCrew.tsx index e774b561..a0948f2d 100644 --- a/components/series/CastAndCrew.tsx +++ b/components/series/CastAndCrew.tsx @@ -1,18 +1,19 @@ import { apiAtom } from "@/providers/JellyfinProvider"; import { getPrimaryImageUrl } from "@/utils/jellyfin/image/getPrimaryImageUrl"; -import { +import type { BaseItemDto, BaseItemPerson, } from "@jellyfin/sdk/lib/generated-client/models"; import { router, useSegments } from "expo-router"; import { useAtom } from "jotai"; -import React, { useMemo } from "react"; -import { TouchableOpacity, View, ViewProps } from "react-native"; +import type React from "react"; +import { useMemo } from "react"; +import { useTranslation } from "react-i18next"; +import { TouchableOpacity, View, type ViewProps } from "react-native"; import { HorizontalScroll } from "../common/HorrizontalScroll"; import { Text } from "../common/Text"; -import Poster from "../posters/Poster"; import { itemRouter } from "../common/TouchableItemRouter"; -import { useTranslation } from "react-i18next"; +import Poster from "../posters/Poster"; interface Props extends ViewProps { item?: BaseItemDto | null; @@ -41,8 +42,10 @@ export const CastAndCrew: React.FC = ({ item, loading, ...props }) => { if (!from) return null; return ( - - {t("item_card.cast_and_crew")} + + + {t("item_card.cast_and_crew")} + i.Id.toString()} @@ -55,11 +58,11 @@ export const CastAndCrew: React.FC = ({ item, loading, ...props }) => { // @ts-ignore router.push(url); }} - className="flex flex-col w-28" + className='flex flex-col w-28' > - {i.Name} - {i.Role} + {i.Name} + {i.Role} )} /> diff --git a/components/series/CurrentSeries.tsx b/components/series/CurrentSeries.tsx index 16536a6d..e798bb22 100644 --- a/components/series/CurrentSeries.tsx +++ b/components/series/CurrentSeries.tsx @@ -1,14 +1,14 @@ import { apiAtom } from "@/providers/JellyfinProvider"; -import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; +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 React from "react"; -import { TouchableOpacity, View, ViewProps } from "react-native"; -import Poster from "../posters/Poster"; +import type React from "react"; +import { useTranslation } from "react-i18next"; +import { TouchableOpacity, View, type ViewProps } from "react-native"; import { HorizontalScroll } from "../common/HorrizontalScroll"; import { Text } from "../common/Text"; -import { getPrimaryImageUrlById } from "@/utils/jellyfin/image/getPrimaryImageUrlById"; -import { useTranslation } from "react-i18next"; +import Poster from "../posters/Poster"; interface Props extends ViewProps { item?: BaseItemDto | null; @@ -20,7 +20,9 @@ export const CurrentSeries: React.FC = ({ item, ...props }) => { return ( - {t("item_card.series")} + + {t("item_card.series")} + = ({ item, ...props }) => { router.push(`/series/${item.SeriesId}`)} - className="flex flex-col space-y-2 w-28" + className='flex flex-col space-y-2 w-28' > = ({ item, ...props }) => { return ( - + {item?.Name} - + { router.push( // @ts-ignore - `/(auth)/series/${item.SeriesId}?seasonIndex=${item?.ParentIndexNumber}` + `/(auth)/series/${item.SeriesId}?seasonIndex=${item?.ParentIndexNumber}`, ); }} > - {item?.SeasonName} + {item?.SeasonName} - {"—"} - {`Episode ${item.IndexNumber}`} + {"—"} + {`Episode ${item.IndexNumber}`} - {item?.ProductionYear} + {item?.ProductionYear} ); }; diff --git a/components/series/JellyseerrSeasons.tsx b/components/series/JellyseerrSeasons.tsx index 0d77ac13..db06be59 100644 --- a/components/series/JellyseerrSeasons.tsx +++ b/components/series/JellyseerrSeasons.tsx @@ -1,30 +1,35 @@ -import { Text } from "@/components/common/Text"; -import React, { useCallback, useMemo, useState } from "react"; -import { Alert, TouchableOpacity, View } from "react-native"; -import { TvDetails } from "@/utils/jellyseerr/server/models/Tv"; -import { FlashList } from "@shopify/flash-list"; -import { orderBy } from "lodash"; 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 Season from "@/utils/jellyseerr/server/entity/Season"; +import { textShadowStyle } from "@/components/jellyseerr/discover/GenericSlideCard"; +import { useJellyseerr } from "@/hooks/useJellyseerr"; import { MediaStatus, MediaType, } from "@/utils/jellyseerr/server/constants/media"; -import { Ionicons } from "@expo/vector-icons"; -import { RoundButton } from "@/components/RoundButton"; -import { useJellyseerr } from "@/hooks/useJellyseerr"; +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 {QueryObserverResult, RefetchOptions, useQuery} from "@tanstack/react-query"; -import { HorizontalScroll } from "@/components/common/HorrizontalScroll"; +import type { TvDetails } from "@/utils/jellyseerr/server/models/Tv"; +import { Ionicons } from "@expo/vector-icons"; +import { FlashList } from "@shopify/flash-list"; +import { + type QueryObserverResult, + type RefetchOptions, + useQuery, +} from "@tanstack/react-query"; import { Image } from "expo-image"; -import MediaRequest from "@/utils/jellyseerr/server/entity/MediaRequest"; -import { Loader } from "../Loader"; import { t } from "i18next"; -import {MovieDetails} from "@/utils/jellyseerr/server/models/Movie"; -import {MediaRequestBody} from "@/utils/jellyseerr/server/interfaces/api/requestInterfaces"; -import {textShadowStyle} from "@/components/jellyseerr/discover/GenericSlideCard"; -import {dateOpts} from "@/components/jellyseerr/DetailFacts"; +import { orderBy } from "lodash"; +import type React from "react"; +import { useCallback, useMemo, useState } from "react"; +import { Alert, TouchableOpacity, View } from "react-native"; +import { Loader } from "../Loader"; const JellyseerrSeasonEpisodes: React.FC<{ details: TvDetails; @@ -54,26 +59,27 @@ const JellyseerrSeasonEpisodes: React.FC<{ }; const RenderItem = ({ item, index }: any) => { - const { jellyseerrApi, jellyseerrRegion: region, jellyseerrLocale: locale } = useJellyseerr(); + const { + jellyseerrApi, + jellyseerrRegion: region, + jellyseerrLocale: locale, + } = useJellyseerr(); const [imageError, setImageError] = useState(false); const upcomingAirDate = useMemo(() => { const airDate = item.airDate; if (airDate) { - let airDateObj = new Date(airDate); + const airDateObj = new Date(airDate); if (new Date() < airDateObj) { - return airDateObj.toLocaleDateString( - `${locale}-${region}`, - dateOpts - ); + return airDateObj.toLocaleDateString(`${locale}-${region}`, dateOpts); } } }, [item]); return ( - - + + {!imageError ? ( <> { uri: jellyseerrApi?.imageProxy(item.stillPath), }} cachePolicy={"memory-disk"} - contentFit="cover" - className="w-full h-full" + contentFit='cover' + className='w-full h-full' onError={(e) => { setImageError(true); }} /> {upcomingAirDate && ( - - - + + + {upcomingAirDate} @@ -100,26 +109,26 @@ const RenderItem = ({ item, index }: any) => { )} ) : ( - + )} - - + + {item.name} - + {`S${item.seasonNumber}:E${item.episodeNumber}`} - + {item.overview} @@ -129,9 +138,13 @@ const RenderItem = ({ item, index }: any) => { const JellyseerrSeasons: React.FC<{ isLoading: boolean; details?: TvDetails; - hasAdvancedRequest?: boolean, + hasAdvancedRequest?: boolean; onAdvancedRequest?: (data: MediaRequestBody) => void; - refetch: (options?: (RefetchOptions | undefined)) => Promise>; + refetch: ( + options?: RefetchOptions | undefined, + ) => Promise< + QueryObserverResult + >; }> = ({ isLoading, details, @@ -147,10 +160,10 @@ const JellyseerrSeasons: React.FC<{ }>(); const seasons = useMemo(() => { const mediaInfoSeasons = details?.mediaInfo?.seasons?.filter( - (s: Season) => s.seasonNumber !== 0 + (s: Season) => s.seasonNumber !== 0, ); const requestedSeasons = details?.mediaInfo?.requests?.flatMap( - (r: MediaRequest) => r.seasons + (r: MediaRequest) => r.seasons, ); return details.seasons?.map((season) => { return { @@ -159,11 +172,11 @@ const JellyseerrSeasons: React.FC<{ // What our library status is mediaInfoSeasons?.find( (mediaSeason: Season) => - mediaSeason.seasonNumber === season.seasonNumber + mediaSeason.seasonNumber === season.seasonNumber, )?.status ?? // What our request status is requestedSeasons?.find( - (s: Season) => s.seasonNumber === season.seasonNumber + (s: Season) => s.seasonNumber === season.seasonNumber, )?.status ?? // Otherwise set it as unknown MediaStatus.UNKNOWN, @@ -173,7 +186,7 @@ const JellyseerrSeasons: React.FC<{ const allSeasonsAvailable = useMemo( () => seasons?.every((season) => season.status === MediaStatus.AVAILABLE), - [seasons] + [seasons], ); const requestAll = useCallback(() => { @@ -184,13 +197,13 @@ const JellyseerrSeasons: React.FC<{ tvdbId: details.externalIds?.tvdbId, seasons: seasons .filter( - (s) => s.status === MediaStatus.UNKNOWN && s.seasonNumber !== 0 + (s) => s.status === MediaStatus.UNKNOWN && s.seasonNumber !== 0, ) .map((s) => s.seasonNumber), - } + }; if (hasAdvancedRequest) { - return onAdvancedRequest?.(body) + return onAdvancedRequest?.(body); } requestMedia(details.name, body, refetch); @@ -199,44 +212,53 @@ const JellyseerrSeasons: React.FC<{ const promptRequestAll = useCallback( () => - Alert.alert(t("jellyseerr.confirm"), t("jellyseerr.are_you_sure_you_want_to_request_all_seasons"), [ - { - text: t("jellyseerr.cancel"), - style: "cancel", - }, - { - text: t("jellyseerr.yes"), - onPress: requestAll, - }, - ]), - [requestAll] + Alert.alert( + t("jellyseerr.confirm"), + t("jellyseerr.are_you_sure_you_want_to_request_all_seasons"), + [ + { + text: t("jellyseerr.cancel"), + style: "cancel", + }, + { + text: t("jellyseerr.yes"), + onPress: requestAll, + }, + ], + ), + [requestAll], ); - const requestSeason = useCallback(async (canRequest: Boolean, seasonNumber: number) => { - if (canRequest) { - const body: MediaRequestBody = { - mediaId: details.id, - mediaType: MediaType.TV, - tvdbId: details.externalIds?.tvdbId, - seasons: [seasonNumber], - } + const requestSeason = useCallback( + async (canRequest: boolean, seasonNumber: number) => { + if (canRequest) { + const body: MediaRequestBody = { + mediaId: details.id, + mediaType: MediaType.TV, + tvdbId: details.externalIds?.tvdbId, + seasons: [seasonNumber], + }; - if (hasAdvancedRequest) { - return onAdvancedRequest?.(body) - } + if (hasAdvancedRequest) { + return onAdvancedRequest?.(body); + } - requestMedia(`${details.name}, Season ${seasonNumber}`, body, refetch); - } - }, [requestMedia, hasAdvancedRequest, onAdvancedRequest]); + requestMedia(`${details.name}, Season ${seasonNumber}`, body, refetch); + } + }, + [requestMedia, hasAdvancedRequest, onAdvancedRequest], + ); if (isLoading) return ( - - {t("item_card.seasons")} + + + {t("item_card.seasons")} + {!allSeasonsAvailable && ( - - + + )} @@ -249,19 +271,21 @@ const JellyseerrSeasons: React.FC<{ data={orderBy( details.seasons.filter((s) => s.seasonNumber !== 0), "seasonNumber", - "desc" + "desc", )} ListHeaderComponent={() => ( - - {t("item_card.seasons")} + + + {t("item_card.seasons")} + {!allSeasonsAvailable && ( - - + + )} )} - ItemSeparatorComponent={() => } + ItemSeparatorComponent={() => } estimatedItemSize={250} renderItem={({ item: season }) => ( <> @@ -272,17 +296,21 @@ const JellyseerrSeasons: React.FC<{ [season.seasonNumber]: !prevState?.[season.seasonNumber], })) } - className="px-4" + className='px-4' > {[0].map(() => { @@ -292,11 +320,13 @@ const JellyseerrSeasons: React.FC<{ return ( requestSeason(canRequest, season.seasonNumber)} + onPress={() => + requestSeason(canRequest, season.seasonNumber) + } className={canRequest ? "bg-gray-700/40" : undefined} mediaStatus={ seasons?.find( - (s) => s.seasonNumber === season.seasonNumber + (s) => s.seasonNumber === season.seasonNumber, )?.status } showRequestIcon={canRequest} diff --git a/components/series/NextItemButton.tsx b/components/series/NextItemButton.tsx index 02520c9a..7631dc5c 100644 --- a/components/series/NextItemButton.tsx +++ b/components/series/NextItemButton.tsx @@ -1,12 +1,12 @@ -import { Ionicons } from "@expo/vector-icons"; -import { Button } from "../Button"; -import { useRouter } from "expo-router"; -import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; -import { useQuery } from "@tanstack/react-query"; -import { useAtom } from "jotai"; 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"; +import { useQuery } from "@tanstack/react-query"; +import { useRouter } from "expo-router"; +import { useAtom } from "jotai"; import { useMemo } from "react"; +import { Button } from "../Button"; interface Props extends React.ComponentProps { item: BaseItemDto; @@ -62,9 +62,9 @@ export const NextItemButton: React.FC = ({ {...props} > {type === "next" ? ( - + ) : ( - + )} ); diff --git a/components/series/NextUp.tsx b/components/series/NextUp.tsx index c76a61c6..e368bd8e 100644 --- a/components/series/NextUp.tsx +++ b/components/series/NextUp.tsx @@ -1,18 +1,18 @@ 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 React from "react"; +import type React from "react"; +import { useTranslation } from "react-i18next"; import { TouchableOpacity, View } from "react-native"; -import { HorizontalScroll } from "../common/HorrizontalScroll"; -import { Text } from "../common/Text"; import ContinueWatchingPoster from "../ContinueWatchingPoster"; import { ItemCardText } from "../ItemCardText"; +import { HorizontalScroll } from "../common/HorrizontalScroll"; +import { Text } from "../common/Text"; import { TouchableItemRouter } from "../common/TouchableItemRouter"; -import { FlashList } from "@shopify/flash-list"; -import { useTranslation } from "react-i18next"; export const NextUp: React.FC<{ seriesId: string }> = ({ seriesId }) => { const [user] = useAtom(userAtom); @@ -38,15 +38,17 @@ export const NextUp: React.FC<{ seriesId: string }> = ({ seriesId }) => { if (!items?.length) return ( - - {t("item_card.next_up")} - {t("item_card.no_items_to_display")} + + {t("item_card.next_up")} + {t("item_card.no_items_to_display")} ); return ( - {t("item_card.next_up")} + + {t("item_card.next_up")} + = ({ seriesId }) => { diff --git a/components/series/SeasonDropdown.tsx b/components/series/SeasonDropdown.tsx index 25a09c17..21ce2539 100644 --- a/components/series/SeasonDropdown.tsx +++ b/components/series/SeasonDropdown.tsx @@ -1,9 +1,9 @@ -import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; +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 { Text } from "../common/Text"; import { t } from "i18next"; +import { Text } from "../common/Text"; type Props = { item: BaseItemDto; @@ -45,12 +45,12 @@ export const SeasonDropdown: React.FC = ({ title: "Name", index: "IndexNumber", }, - [item] + [item], ); const seasonIndex = useMemo( () => state[(item[keys.id] as string) ?? ""], - [state] + [state], ); useEffect(() => { @@ -60,7 +60,7 @@ export const SeasonDropdown: React.FC = ({ if (initialSeasonIndex !== undefined) { // Use the provided initialSeasonIndex if it exists in the seasons const seasonExists = seasons.some( - (season: any) => season[keys.index] === initialSeasonIndex + (season: any) => season[keys.index] === initialSeasonIndex, ); if (seasonExists) { initialIndex = initialSeasonIndex; @@ -77,7 +77,7 @@ export const SeasonDropdown: React.FC = ({ if (initialIndex !== undefined) { const initialSeason = seasons.find( - (season: any) => season[keys.index] === initialIndex + (season: any) => season[keys.index] === initialIndex, ); if (initialSeason) onSelect(initialSeason!); @@ -92,8 +92,8 @@ export const SeasonDropdown: React.FC = ({ return ( - - + + {t("item_card.season")} {seasonIndex} @@ -102,8 +102,8 @@ export const SeasonDropdown: React.FC = ({ = ({ headers: { Authorization: `MediaBrowser DeviceId="${api.deviceInfo.id}", Token="${api.accessToken}"`, }, - } + }, ); return response.data.Items as BaseItemDto[]; @@ -74,7 +74,7 @@ export const SeasonEpisodesCarousel: React.FC = ({ } const previousId = episodes?.find( - (ep) => ep.IndexNumber === item.IndexNumber! - 1 + (ep) => ep.IndexNumber === item.IndexNumber! - 1, )?.Id; if (previousId) { queryClient.prefetchQuery({ @@ -90,7 +90,7 @@ export const SeasonEpisodesCarousel: React.FC = ({ } const nextId = episodes?.find( - (ep) => ep.IndexNumber === item.IndexNumber! + 1 + (ep) => ep.IndexNumber === item.IndexNumber! + 1, )?.Id; if (nextId) { queryClient.prefetchQuery({ diff --git a/components/series/SeasonPicker.tsx b/components/series/SeasonPicker.tsx index 6851bbbc..1f86ff90 100644 --- a/components/series/SeasonPicker.tsx +++ b/components/series/SeasonPicker.tsx @@ -1,24 +1,24 @@ +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 { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; +import { Ionicons, MaterialCommunityIcons } 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"; import { atom, useAtom } from "jotai"; import { useEffect, useMemo, useState } from "react"; +import { useTranslation } from "react-i18next"; import { View } from "react-native"; import ContinueWatchingPoster from "../ContinueWatchingPoster"; import { DownloadItems, DownloadSingleItem } from "../DownloadItem"; import { Loader } from "../Loader"; -import { Text } from "../common/Text"; -import { getTvShowsApi } from "@jellyfin/sdk/lib/utils/api"; -import { getUserItemData } from "@/utils/jellyfin/user-library/getUserItemData"; -import { TouchableItemRouter } from "../common/TouchableItemRouter"; -import { - SeasonDropdown, - SeasonIndexState, -} from "@/components/series/SeasonDropdown"; -import { Ionicons, MaterialCommunityIcons } from "@expo/vector-icons"; import { PlayedStatus } from "../PlayedStatus"; -import { useTranslation } from "react-i18next"; +import { Text } from "../common/Text"; +import { TouchableItemRouter } from "../common/TouchableItemRouter"; type Props = { item: BaseItemDto; @@ -35,7 +35,7 @@ export const SeasonPicker: React.FC = ({ item, initialSeasonIndex }) => { const seasonIndex = useMemo( () => seasonIndexState[item.Id ?? ""], - [item, seasonIndexState] + [item, seasonIndexState], ); const { data: seasons } = useQuery({ @@ -54,7 +54,7 @@ export const SeasonPicker: React.FC = ({ item, initialSeasonIndex }) => { headers: { Authorization: `MediaBrowser DeviceId="${api.deviceInfo.id}", Token="${api.accessToken}"`, }, - } + }, ); return response.data.Items; @@ -66,7 +66,7 @@ export const SeasonPicker: React.FC = ({ item, initialSeasonIndex }) => { const selectedSeasonId: string | null = useMemo(() => { const season: BaseItemDto = seasons?.find( (s: BaseItemDto) => - s.IndexNumber === seasonIndex || s.Name === seasonIndex + s.IndexNumber === seasonIndex || s.Name === seasonIndex, ); if (!season?.Id) return null; @@ -92,7 +92,7 @@ export const SeasonPicker: React.FC = ({ item, initialSeasonIndex }) => { if (res.data.TotalRecordCount === 0) console.warn( "No episodes found for season with ID ~", - selectedSeasonId + selectedSeasonId, ); return res.data.Items; @@ -102,7 +102,7 @@ export const SeasonPicker: React.FC = ({ item, initialSeasonIndex }) => { const queryClient = useQueryClient(); useEffect(() => { - for (let e of episodes || []) { + for (const e of episodes || []) { queryClient.prefetchQuery({ queryKey: ["item", e.Id], queryFn: async () => { @@ -133,7 +133,7 @@ export const SeasonPicker: React.FC = ({ item, initialSeasonIndex }) => { minHeight: 144 * nrOfEpisodes, }} > - + = ({ item, initialSeasonIndex }) => { }} /> {episodes?.length || 0 > 0 ? ( - + ( - + )} DownloadedIconComponent={() => ( - + )} /> ) : null} - + {isFetching ? ( @@ -178,35 +178,35 @@ export const SeasonPicker: React.FC = ({ item, initialSeasonIndex }) => { - - + + - - + + {e.Name} - + {`S${e.ParentIndexNumber?.toString()}:E${e.IndexNumber?.toString()}`} - + {runtimeTicksToSeconds(e.RunTimeTicks)} - + {e.Overview} @@ -214,8 +214,8 @@ export const SeasonPicker: React.FC = ({ item, initialSeasonIndex }) => { )) )} {(episodes?.length || 0) === 0 ? ( - - + + {t("item_card.no_episodes_for_this_season")} diff --git a/components/series/SeriesActions.tsx b/components/series/SeriesActions.tsx index 569f719d..a687ae60 100644 --- a/components/series/SeriesActions.tsx +++ b/components/series/SeriesActions.tsx @@ -1,14 +1,14 @@ -import { MovieDetails } from "@/utils/jellyseerr/server/models/Movie"; -import { TvDetails } from "@/utils/jellyseerr/server/models/Tv"; +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 { BaseItemDto } from "@jellyfin/sdk/lib/generated-client"; +import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client"; import { useCallback, useMemo } from "react"; import { Alert, Linking, TouchableOpacity, View, - ViewProps, + type ViewProps, } from "react-native"; interface Props extends ViewProps { @@ -42,10 +42,10 @@ export const ItemActions = ({ item, ...props }: Props) => { }, [trailerLink]); return ( - + {trailerLink && ( - + )} diff --git a/components/series/SeriesHeader.tsx b/components/series/SeriesHeader.tsx index 78230c89..4c28feb8 100644 --- a/components/series/SeriesHeader.tsx +++ b/components/series/SeriesHeader.tsx @@ -1,8 +1,8 @@ -import { View } from "react-native"; -import { Text } from "../common/Text"; -import { Ratings } from "../Ratings"; -import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client"; +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 { ItemActions } from "./SeriesActions"; interface Props { @@ -51,14 +51,14 @@ export const SeriesHeader = ({ item }: Props) => { }, [startYear, endYear]); return ( - - {item?.Name} - {yearString} - - + + {item?.Name} + {yearString} + + - {item?.Overview} + {item?.Overview} ); }; diff --git a/components/settings/AppLanguageSelector.tsx b/components/settings/AppLanguageSelector.tsx index e9445136..ca6d6da8 100644 --- a/components/settings/AppLanguageSelector.tsx +++ b/components/settings/AppLanguageSelector.tsx @@ -1,11 +1,11 @@ const DropdownMenu = !Platform.isTV ? require("zeego/dropdown-menu") : null; -import { Platform, TouchableOpacity, View, ViewProps } from "react-native"; -import { Text } from "../common/Text"; +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 { Text } from "../common/Text"; import { ListGroup } from "../list/ListGroup"; import { ListItem } from "../list/ListItem"; -import { useTranslation } from "react-i18next"; -import { APP_LANGUAGES } from "@/i18n"; interface Props extends ViewProps {} @@ -22,18 +22,18 @@ export const AppLanguageSelector: React.FC = ({ ...props }) => { - + {APP_LANGUAGES.find( - (l) => l.value === settings?.preferedLanguage + (l) => l.value === settings?.preferedLanguage, )?.label || t("home.settings.languages.system")} = ({ ...props }) => { + {t("home.settings.audio.audio_hint")} } @@ -46,22 +46,22 @@ export const AudioToggles: React.FC = ({ ...props }) => { - - + + {settings?.defaultAudioLanguage?.DisplayName || t("home.settings.audio.none")} { diff --git a/components/settings/Dashboard.tsx b/components/settings/Dashboard.tsx index 1ffe57a1..798fef37 100644 --- a/components/settings/Dashboard.tsx +++ b/components/settings/Dashboard.tsx @@ -1,11 +1,11 @@ +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 { ListGroup } from "../list/ListGroup"; import { ListItem } from "../list/ListItem"; -import { useTranslation } from "react-i18next"; -import { useSessions, useSessionsProps } from "@/hooks/useSessions"; export const Dashboard = () => { const [settings, updateSettings] = useSettings(); @@ -17,7 +17,7 @@ export const Dashboard = () => { if (!settings) return null; return ( - + router.push("/settings/dashboard/sessions")} diff --git a/components/settings/DisabledSetting.tsx b/components/settings/DisabledSetting.tsx index b340fb96..d0d5c33d 100644 --- a/components/settings/DisabledSetting.tsx +++ b/components/settings/DisabledSetting.tsx @@ -1,13 +1,9 @@ -import {View, ViewProps} from "react-native"; -import {Text} from "@/components/common/Text"; +import { Text } from "@/components/common/Text"; +import { View, type ViewProps } from "react-native"; -const DisabledSetting: React.FC<{disabled: boolean, showText?: boolean, text?: string} & ViewProps> = ({ - disabled = false, - showText = true, - text, - children, - ...props -}) => ( +const DisabledSetting: React.FC< + { disabled: boolean; showText?: boolean; text?: string } & ViewProps +> = ({ disabled = false, showText = true, text, children, ...props }) => ( - {disabled && showText && - {text ?? "Currently disabled by admin."} - } + {disabled && showText && ( + + {text ?? "Currently disabled by admin."} + + )} {children} -) +); -export default DisabledSetting; \ No newline at end of file +export default DisabledSetting; diff --git a/components/settings/DownloadSettings.tsx b/components/settings/DownloadSettings.tsx index 0d1df837..549923de 100644 --- a/components/settings/DownloadSettings.tsx +++ b/components/settings/DownloadSettings.tsx @@ -1,17 +1,21 @@ import { Stepper } from "@/components/inputs/Stepper"; import { useDownload } from "@/providers/DownloadProvider"; -import { DownloadMethod, Settings, useSettings } from "@/utils/atoms/settings"; +import { + DownloadMethod, + 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 { Text } from "../common/Text"; import { ListGroup } from "../list/ListGroup"; import { ListItem } from "../list/ListItem"; -import { useTranslation } from "react-i18next"; -import DisabledSetting from "@/components/settings/DisabledSetting"; export default function DownloadSettings({ ...props }) { const [settings, updateSettings, pluginSettings] = useSettings(); @@ -25,13 +29,13 @@ export default function DownloadSettings({ ...props }) { pluginSettings?.downloadMethod?.locked === true && pluginSettings?.remuxConcurrentLimit?.locked === true && pluginSettings?.autoDownload.locked === true, - [pluginSettings] + [pluginSettings], ); if (!settings) return null; return ( - + - - + + {settings.downloadMethod === DownloadMethod.Remux ? t("home.settings.downloads.default") : t("home.settings.downloads.optimized")} { updateSettings({ downloadMethod: DownloadMethod.Remux }); setProcesses([]); @@ -76,7 +80,7 @@ export default function DownloadSettings({ ...props }) { { updateSettings({ downloadMethod: DownloadMethod.Optimized }); setProcesses([]); diff --git a/components/settings/HomeIndex.tsx b/components/settings/HomeIndex.tsx index 43e6e3d6..71325ccb 100644 --- a/components/settings/HomeIndex.tsx +++ b/components/settings/HomeIndex.tsx @@ -1,8 +1,8 @@ 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 { Loader } from "@/components/Loader"; import { MediaListSection } from "@/components/medialists/MediaListSection"; import { Colors } from "@/constants/Colors"; import { useInvalidatePlaybackProgressCache } from "@/hooks/useRevalidatePlaybackProgressCache"; @@ -11,8 +11,8 @@ 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 { Api } from "@jellyfin/sdk"; -import { +import type { Api } from "@jellyfin/sdk"; +import type { BaseItemDto, BaseItemKind, } from "@jellyfin/sdk/lib/generated-client/models"; @@ -24,7 +24,7 @@ import { getUserViewsApi, } from "@jellyfin/sdk/lib/utils/api"; import NetInfo from "@react-native-community/netinfo"; -import { QueryFunction, useQuery } from "@tanstack/react-query"; +import { type QueryFunction, useQuery } from "@tanstack/react-query"; import { useNavigation, usePathname, @@ -94,10 +94,10 @@ export const HomeIndex = () => { onPress={() => { router.push("/(auth)/downloads"); }} - className="p-2" + className='p-2' > @@ -108,7 +108,7 @@ export const HomeIndex = () => { useEffect(() => { cleanCacheDirectory().catch((e) => - console.error("Something went wrong cleaning cache directory") + console.error("Something went wrong cleaning cache directory"), ); }, []); @@ -174,14 +174,14 @@ export const HomeIndex = () => { const userViews = useMemo( () => data?.filter((l) => !settings?.hiddenLibraries?.includes(l.Id!)), - [data, settings?.hiddenLibraries] + [data, settings?.hiddenLibraries], ); const collections = useMemo(() => { const allow = ["movies", "tvshows"]; return ( userViews?.filter( - (c) => c.CollectionType && allow.includes(c.CollectionType) + (c) => c.CollectionType && allow.includes(c.CollectionType), ) || [] ); }, [userViews]); @@ -194,13 +194,13 @@ export const HomeIndex = () => { await invalidateCache(); setLoading(false); }; - + const createCollectionConfig = useCallback( ( title: string, queryKey: string[], includeItemTypes: BaseItemKind[], - parentId: string | undefined + parentId: string | undefined, ): ScrollingCollectionListSection => ({ title, queryKey, @@ -222,7 +222,7 @@ export const HomeIndex = () => { }, type: "ScrollingCollectionList", }), - [api, user?.Id] + [api, user?.Id], ); let sections: Section[] = []; @@ -244,7 +244,7 @@ export const HomeIndex = () => { title || "", queryKey, includeItemTypes, - c.Id + c.Id, ); }); @@ -312,7 +312,7 @@ export const HomeIndex = () => { try { const suggestions = await getSuggestions(api, user.Id); const nextUpPromises = suggestions.map((series) => - getNextUp(api, user.Id, series.Id) + getNextUp(api, user.Id, series.Id), ); const nextUpResults = await Promise.all(nextUpPromises); @@ -376,32 +376,32 @@ export const HomeIndex = () => { if (isConnected === false) { return ( - - {t("home.no_internet")} - + + {t("home.no_internet")} + {t("home.no_internet_message")} - + ) : ( - - + + {t("home.settings.plugins.jellyseerr.jellyseerr_warning")} - + {t("home.settings.plugins.jellyseerr.server_url")} - - + + {t("home.settings.plugins.jellyseerr.server_url_hint")} - + {t("home.settings.plugins.jellyseerr.password")} diff --git a/components/settings/StorageSettings.tsx b/components/settings/StorageSettings.tsx index aad3b1d3..d8516b79 100644 --- a/components/settings/StorageSettings.tsx +++ b/components/settings/StorageSettings.tsx @@ -1,14 +1,14 @@ 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 { ListGroup } from "../list/ListGroup"; import { ListItem } from "../list/ListItem"; -import { useTranslation } from "react-i18next"; -import {Colors} from "@/constants/Colors"; export const StorageSettings = () => { const { deleteAllFiles, appSizeUsage } = useDownload(); @@ -44,11 +44,11 @@ export const StorageSettings = () => { return ( - - - {t("home.settings.storage.storage_title")} + + + {t("home.settings.storage.storage_title")} {size && ( - + {t("home.settings.storage.size_used", { used: Number(size.total - size.remaining).bytesToReadable(), total: size.total?.bytesToReadable(), @@ -56,7 +56,7 @@ export const StorageSettings = () => { )} - + {size && ( <> { )} - + {size && ( <> - - - + + + {t("home.settings.storage.app_usage", { usedSpace: calculatePercentage(size.app, size.total), })} - - - + + + {t("home.settings.storage.device_usage", { availableSpace: calculatePercentage( size.total - size.remaining - size.app, - size.total + size.total, ), })} @@ -105,7 +105,7 @@ export const StorageSettings = () => { diff --git a/components/settings/SubtitleToggles.tsx b/components/settings/SubtitleToggles.tsx index 6bd5e6c2..ff0bfe6e 100644 --- a/components/settings/SubtitleToggles.tsx +++ b/components/settings/SubtitleToggles.tsx @@ -1,16 +1,16 @@ -import { Platform, TouchableOpacity, View, ViewProps } from "react-native"; +import { Platform, TouchableOpacity, View, type ViewProps } from "react-native"; const DropdownMenu = !Platform.isTV ? require("zeego/dropdown-menu") : null; -import { Text } from "../common/Text"; -import { useMedia } from "./MediaContext"; -import { Switch } from "react-native-gesture-handler"; -import { ListGroup } from "../list/ListGroup"; -import { ListItem } from "../list/ListItem"; +import Dropdown from "@/components/common/Dropdown"; +import { Stepper } from "@/components/inputs/Stepper"; +import { useSettings } from "@/utils/atoms/settings"; import { Ionicons } from "@expo/vector-icons"; import { SubtitlePlaybackMode } from "@jellyfin/sdk/lib/generated-client"; import { useTranslation } from "react-i18next"; -import { useSettings } from "@/utils/atoms/settings"; -import { Stepper } from "@/components/inputs/Stepper"; -import Dropdown from "@/components/common/Dropdown"; +import { Switch } from "react-native-gesture-handler"; +import { Text } from "../common/Text"; +import { ListGroup } from "../list/ListGroup"; +import { ListItem } from "../list/ListItem"; +import { useMedia } from "./MediaContext"; interface Props extends ViewProps {} @@ -46,7 +46,7 @@ export const SubtitleToggles: React.FC = ({ ...props }) => { + {t("home.settings.subtitles.subtitle_hint")} } @@ -65,15 +65,15 @@ export const SubtitleToggles: React.FC = ({ ...props }) => { } titleExtractor={(item) => item?.DisplayName} title={ - - + + {settings?.defaultSubtitleLanguage?.DisplayName || t("home.settings.subtitles.none")} } @@ -100,15 +100,15 @@ export const SubtitleToggles: React.FC = ({ ...props }) => { keyExtractor={String} titleExtractor={(item) => t(subtitleModeKeys[item]) || String(item)} title={ - - + + {t(subtitleModeKeys[settings?.subtitleMode]) || t("home.settings.subtitles.loading")} } diff --git a/components/settings/UserInfo.tsx b/components/settings/UserInfo.tsx index fdd3db44..d910028f 100644 --- a/components/settings/UserInfo.tsx +++ b/components/settings/UserInfo.tsx @@ -1,13 +1,13 @@ -import { View, ViewProps } from "react-native"; -import { Text } from "../common/Text"; -import { ListItem } from "../list/ListItem"; -import { Button } from "../Button"; import { apiAtom, useJellyfin, userAtom } from "@/providers/JellyfinProvider"; -import { useAtom } from "jotai"; -import Constants from "expo-constants"; import Application from "expo-application"; -import { ListGroup } from "../list/ListGroup"; +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 { ListGroup } from "../list/ListGroup"; +import { ListItem } from "../list/ListItem"; interface Props extends ViewProps {} @@ -24,10 +24,22 @@ export const UserInfo: React.FC = ({ ...props }) => { return ( - - - - + + + + ); diff --git a/components/stacks/NestedTabPageStack.tsx b/components/stacks/NestedTabPageStack.tsx index 2cfeed1d..8e66e0bf 100644 --- a/components/stacks/NestedTabPageStack.tsx +++ b/components/stacks/NestedTabPageStack.tsx @@ -1,6 +1,6 @@ -import { NativeStackNavigationOptions } from "@react-navigation/native-stack"; +import type { ParamListBase, RouteProp } from "@react-navigation/native"; +import type { NativeStackNavigationOptions } from "@react-navigation/native-stack"; import { HeaderBackButton } from "../common/HeaderBackButton"; -import { ParamListBase, RouteProp } from "@react-navigation/native"; type ICommonScreenOptions = | NativeStackNavigationOptions diff --git a/components/video-player/controls/AudioSlider.tsx b/components/video-player/controls/AudioSlider.tsx index c0066c2d..0bc02cdd 100644 --- a/components/video-player/controls/AudioSlider.tsx +++ b/components/video-player/controls/AudioSlider.tsx @@ -1,10 +1,13 @@ -import React, { useEffect, useRef } from "react"; -import { View, StyleSheet, Platform } from "react-native"; -import { useSharedValue } from "react-native-reanimated"; +import type React from "react"; +import { useEffect, useRef } from "react"; +import { Platform, StyleSheet, View } from "react-native"; import { Slider } from "react-native-awesome-slider"; -const VolumeManager = Platform.isTV ? null : require("react-native-volume-manager"); +import { useSharedValue } from "react-native-reanimated"; +const VolumeManager = Platform.isTV + ? null + : require("react-native-volume-manager"); import { Ionicons } from "@expo/vector-icons"; -import { VolumeResult } from "react-native-volume-manager"; +import type { VolumeResult } from "react-native-volume-manager"; interface AudioSliderProps { setVisibility: (show: boolean) => void; @@ -50,20 +53,22 @@ const AudioSlider: React.FC = ({ setVisibility }) => { }; useEffect(() => { - const volumeListener = VolumeManager.addVolumeListener((result: VolumeResult) => { - volume.value = result.volume * 100; - setVisibility(true); + const volumeListener = VolumeManager.addVolumeListener( + (result: VolumeResult) => { + volume.value = result.volume * 100; + setVisibility(true); - // Clear any existing timeout - if (timeoutRef.current) { - clearTimeout(timeoutRef.current); - } + // Clear any existing timeout + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + } - // Set a new timeout to hide the visibility after 2 seconds - timeoutRef.current = setTimeout(() => { - setVisibility(false); - }, 1000); - }); + // Set a new timeout to hide the visibility after 2 seconds + timeoutRef.current = setTimeout(() => { + setVisibility(false); + }, 1000); + }, + ); return () => { volumeListener.remove(); @@ -92,9 +97,9 @@ const AudioSlider: React.FC = ({ setVisibility }) => { }} /> { }} /> = ({ item, close, goToItem }) => { getUserItemData({ api, userId: user?.Id, itemId: item.SeriesId }).then( (res) => { setSeriesItem(res); - } + }, ); } }, [item.SeriesId]); @@ -80,7 +80,7 @@ export const EpisodeList: React.FC = ({ item, close, goToItem }) => { headers: { Authorization: `MediaBrowser DeviceId="${api.deviceInfo.id}", Token="${api.accessToken}"`, }, - } + }, ); return response.data.Items; }, @@ -90,7 +90,7 @@ export const EpisodeList: React.FC = ({ item, close, goToItem }) => { const selectedSeasonId: string | null = useMemo( () => seasons?.find((season: any) => season.IndexNumber === seasonIndex)?.Id, - [seasons, seasonIndex] + [seasons, seasonIndex], ); const { data: episodes, isFetching } = useQuery({ @@ -123,7 +123,7 @@ export const EpisodeList: React.FC = ({ item, close, goToItem }) => { const queryClient = useQueryClient(); useEffect(() => { - for (let e of episodes || []) { + for (const e of episodes || []) { queryClient.prefetchQuery({ queryKey: ["item", e.Id], queryFn: async () => { @@ -187,9 +187,9 @@ export const EpisodeList: React.FC = ({ item, close, goToItem }) => { onPress={async () => { close(); }} - className="aspect-square flex flex-col bg-neutral-800/90 rounded-xl items-center justify-center p-2" + className='aspect-square flex flex-col bg-neutral-800/90 rounded-xl items-center justify-center p-2' > - + @@ -216,7 +216,7 @@ export const EpisodeList: React.FC = ({ item, close, goToItem }) => { showPlayButton={_item.Id !== item.Id} /> - + = ({ item, close, goToItem }) => { > {_item.Name} - + {`S${_item.ParentIndexNumber?.toString()}:E${_item.IndexNumber?.toString()}`} - + {runtimeTicksToSeconds(_item.RunTimeTicks)} - + {_item.Overview} diff --git a/components/video-player/controls/NextEpisodeCountDownButton.tsx b/components/video-player/controls/NextEpisodeCountDownButton.tsx index 73e3f828..69e5b38b 100644 --- a/components/video-player/controls/NextEpisodeCountDownButton.tsx +++ b/components/video-player/controls/NextEpisodeCountDownButton.tsx @@ -1,6 +1,13 @@ -import React, { useEffect } from "react"; -import { TouchableOpacity, TouchableOpacityProps, View } from "react-native"; 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"; +import { + TouchableOpacity, + type TouchableOpacityProps, + View, +} from "react-native"; import Animated, { useAnimatedStyle, useSharedValue, @@ -8,8 +15,6 @@ import Animated, { Easing, runOnJS, } from "react-native-reanimated"; -import { Colors } from "@/constants/Colors"; -import { useTranslation } from "react-i18next"; interface NextEpisodeCountDownButtonProps extends TouchableOpacityProps { onFinish?: () => void; @@ -38,7 +43,7 @@ const NextEpisodeCountDownButton: React.FC = ({ if (finished && onFinish) { runOnJS(onFinish)(); } - } + }, ); } }, [show, onFinish]); @@ -68,13 +73,15 @@ const NextEpisodeCountDownButton: React.FC = ({ return ( - - {t("player.next_episode")} + + + {t("player.next_episode")} + ); diff --git a/components/video-player/controls/SkipButton.tsx b/components/video-player/controls/SkipButton.tsx index 15bd5fa6..016f94d1 100644 --- a/components/video-player/controls/SkipButton.tsx +++ b/components/video-player/controls/SkipButton.tsx @@ -1,5 +1,5 @@ -import React from "react"; -import { View, TouchableOpacity, Text, ViewProps } from "react-native"; +import type React from "react"; +import { Text, TouchableOpacity, View, type ViewProps } from "react-native"; interface SkipButtonProps extends ViewProps { onPress: () => void; @@ -17,9 +17,9 @@ const SkipButton: React.FC = ({ - {buttonText} + {buttonText} ); diff --git a/components/video-player/controls/SliderScrubbter.tsx b/components/video-player/controls/SliderScrubbter.tsx index a618a350..2259648e 100644 --- a/components/video-player/controls/SliderScrubbter.tsx +++ b/components/video-player/controls/SliderScrubbter.tsx @@ -1,11 +1,12 @@ -import { useTrickplay } from '@/hooks/useTrickplay'; -import { formatTimeString, msToTicks, ticksToSeconds } from '@/utils/time'; -import React, { useRef, useState } from 'react'; -import { View, Text } from 'react-native'; +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 { Text, View } from "react-native"; import { Slider } from "react-native-awesome-slider"; -import { SharedValue, useSharedValue } from 'react-native-reanimated'; -import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client'; +import { type SharedValue, useSharedValue } from "react-native-reanimated"; interface SliderScrubberProps { cacheProgress: SharedValue; @@ -30,12 +31,9 @@ const SliderScrubber: React.FC = ({ remainingTime, item, }) => { - - const [time, setTime] = useState({ hours: 0, minutes: 0, seconds: 0 }); - const { trickPlayUrl, calculateTrickplayUrl, trickplayInfo } = useTrickplay( - item, - ); + const { trickPlayUrl, calculateTrickplayUrl, trickplayInfo } = + useTrickplay(item); const handleSliderChange = (value: number) => { const progressInTicks = msToTicks(value); @@ -86,7 +84,7 @@ const SliderScrubber: React.FC = ({ marginTop: -tileHeight / 4 - 60, zIndex: 10, }} - className=" bg-neutral-800 overflow-hidden" + className=' bg-neutral-800 overflow-hidden' > = ({ ], }} source={{ uri: url }} - contentFit="cover" + contentFit='cover' /> = ({ > {`${time.hours > 0 ? `${time.hours}:` : ""}${ time.minutes < 10 ? `0${time.minutes}` : time.minutes - }:${ - time.seconds < 10 ? `0${time.seconds}` : time.seconds - }`} + }:${time.seconds < 10 ? `0${time.seconds}` : time.seconds}`} ); @@ -129,11 +125,11 @@ const SliderScrubber: React.FC = ({ minimumValue={min} maximumValue={max} /> - - + + {formatTimeString(currentTime, "ms")} - + -{formatTimeString(remainingTime, "ms")} @@ -141,4 +137,4 @@ const SliderScrubber: React.FC = ({ ); }; -export default SliderScrubber; \ No newline at end of file +export default SliderScrubber; diff --git a/components/video-player/controls/contexts/ControlContext.tsx b/components/video-player/controls/contexts/ControlContext.tsx index 30e9d50b..3eed62bd 100644 --- a/components/video-player/controls/contexts/ControlContext.tsx +++ b/components/video-player/controls/contexts/ControlContext.tsx @@ -1,8 +1,9 @@ -import { +import type { BaseItemDto, MediaSourceInfo, } from "@jellyfin/sdk/lib/generated-client"; -import React, { createContext, useContext, useState, ReactNode } from "react"; +import type React from "react"; +import { type ReactNode, createContext, useContext, useState } from "react"; interface ControlContextProps { item: BaseItemDto; @@ -11,7 +12,7 @@ interface ControlContextProps { } const ControlContext = createContext( - undefined + undefined, ); interface ControlProviderProps { diff --git a/components/video-player/controls/contexts/VideoContext.tsx b/components/video-player/controls/contexts/VideoContext.tsx index 094b7b04..0ef73f31 100644 --- a/components/video-player/controls/contexts/VideoContext.tsx +++ b/components/video-player/controls/contexts/VideoContext.tsx @@ -1,8 +1,16 @@ -import { TrackInfo } from "@/modules/VlcPlayer.types"; -import React, { createContext, useContext, useState, ReactNode, useEffect, useMemo } from "react"; -import { useControlContext } from "./ControlContext"; -import { Track } from "../types"; +import type { TrackInfo } from "@/modules/VlcPlayer.types"; import { router, useLocalSearchParams } from "expo-router"; +import type React from "react"; +import { + type ReactNode, + createContext, + useContext, + useEffect, + useMemo, + useState, +} from "react"; +import type { Track } from "../types"; +import { useControlContext } from "./ControlContext"; interface VideoContextProps { audioTracks: Track[] | null; @@ -16,8 +24,14 @@ const VideoContext = createContext(undefined); interface VideoProviderProps { children: ReactNode; - getAudioTracks: (() => Promise) | (() => TrackInfo[]) | undefined; - getSubtitleTracks: (() => Promise) | (() => TrackInfo[]) | undefined; + getAudioTracks: + | (() => Promise) + | (() => TrackInfo[]) + | undefined; + getSubtitleTracks: + | (() => Promise) + | (() => TrackInfo[]) + | undefined; setAudioTrack: ((index: number) => void) | undefined; setSubtitleTrack: ((index: number) => void) | undefined; setSubtitleURL: ((url: string, customName: string) => void) | undefined; @@ -38,20 +52,24 @@ export const VideoProvider: React.FC = ({ const isVideoLoaded = ControlContext?.isVideoLoaded; const mediaSource = ControlContext?.mediaSource; - const allSubs = mediaSource?.MediaStreams?.filter((s) => s.Type === "Subtitle") || []; + const allSubs = + mediaSource?.MediaStreams?.filter((s) => s.Type === "Subtitle") || []; - const { itemId, audioIndex, bitrateValue, subtitleIndex } = useLocalSearchParams<{ - itemId: string; - audioIndex: string; - subtitleIndex: string; - mediaSourceId: string; - bitrateValue: string; - }>(); + const { itemId, audioIndex, bitrateValue, subtitleIndex } = + useLocalSearchParams<{ + itemId: string; + audioIndex: string; + subtitleIndex: string; + mediaSourceId: string; + bitrateValue: string; + }>(); const onTextBasedSubtitle = useMemo( () => - allSubs.find((s) => s.Index?.toString() === subtitleIndex && s.IsTextSubtitleStream) || subtitleIndex === "-1", - [allSubs, subtitleIndex] + allSubs.find( + (s) => s.Index?.toString() === subtitleIndex && s.IsTextSubtitleStream, + ) || subtitleIndex === "-1", + [allSubs, subtitleIndex], ); const setPlayerParams = ({ @@ -74,14 +92,21 @@ export const VideoProvider: React.FC = ({ router.replace(`player/direct-player?${queryParams}`); }; - const setTrackParams = (type: "audio" | "subtitle", index: number, serverIndex: number) => { + const setTrackParams = ( + type: "audio" | "subtitle", + index: number, + serverIndex: number, + ) => { const setTrack = type === "audio" ? setAudioTrack : setSubtitleTrack; const paramKey = type === "audio" ? "audioIndex" : "subtitleIndex"; // If we're transcoding and we're going from a image based subtitle // to a text based subtitle, we need to change the player params. - const shouldChangePlayerParams = type === "subtitle" && mediaSource?.TranscodingUrl && !onTextBasedSubtitle; + const shouldChangePlayerParams = + type === "subtitle" && + mediaSource?.TranscodingUrl && + !onTextBasedSubtitle; console.log("Set player params", index, serverIndex); if (shouldChangePlayerParams) { @@ -102,16 +127,19 @@ export const VideoProvider: React.FC = ({ const subtitleData = await getSubtitleTracks(); // Step 1: Move external subs to the end, because VLC puts external subs at the end - const sortedSubs = allSubs.sort((a, b) => Number(a.IsExternal) - Number(b.IsExternal)); + const sortedSubs = allSubs.sort( + (a, b) => Number(a.IsExternal) - Number(b.IsExternal), + ); // Step 2: Apply VLC indexing logic let textSubIndex = 0; const processedSubs: Track[] = sortedSubs?.map((sub) => { // Always increment for non-transcoding subtitles // Only increment for text-based subtitles when transcoding - const shouldIncrement = !mediaSource?.TranscodingUrl || sub.IsTextSubtitleStream; + const shouldIncrement = + !mediaSource?.TranscodingUrl || sub.IsTextSubtitleStream; const vlcIndex = subtitleData?.at(textSubIndex)?.index ?? -1; - const finalIndex = shouldIncrement ? vlcIndex : sub.Index ?? -1; + const finalIndex = shouldIncrement ? vlcIndex : (sub.Index ?? -1); if (shouldIncrement) textSubIndex++; return { @@ -127,7 +155,9 @@ export const VideoProvider: React.FC = ({ }); // Step 3: Restore the original order - const subtitles: Track[] = processedSubs.sort((a, b) => a.index - b.index); + const subtitles: Track[] = processedSubs.sort( + (a, b) => a.index - b.index, + ); // Add a "Disable Subtitles" option subtitles.unshift({ @@ -143,20 +173,23 @@ export const VideoProvider: React.FC = ({ if (getAudioTracks) { const audioData = await getAudioTracks(); - const allAudio = mediaSource?.MediaStreams?.filter((s) => s.Type === "Audio") || []; + const allAudio = + mediaSource?.MediaStreams?.filter((s) => s.Type === "Audio") || []; const audioTracks: Track[] = allAudio?.map((audio, idx) => { if (!mediaSource?.TranscodingUrl) { const vlcIndex = audioData?.at(idx)?.index ?? -1; return { name: audio.DisplayTitle ?? "Undefined Audio", index: audio.Index ?? -1, - setTrack: () => setTrackParams("audio", vlcIndex, audio.Index ?? -1), + setTrack: () => + setTrackParams("audio", vlcIndex, audio.Index ?? -1), }; } return { name: audio.DisplayTitle ?? "Undefined Audio", index: audio.Index ?? -1, - setTrack: () => setPlayerParams({ chosenAudioIndex: audio.Index?.toString() }), + setTrack: () => + setPlayerParams({ chosenAudioIndex: audio.Index?.toString() }), }; }); setAudioTracks(audioTracks); diff --git a/components/video-player/controls/dropdown/DropdownView.tsx b/components/video-player/controls/dropdown/DropdownView.tsx index ed329659..3168e942 100644 --- a/components/video-player/controls/dropdown/DropdownView.tsx +++ b/components/video-player/controls/dropdown/DropdownView.tsx @@ -1,17 +1,20 @@ -import React, { useCallback } from "react"; -import { TouchableOpacity, Platform } from "react-native"; import { Ionicons } from "@expo/vector-icons"; +import React, { useCallback } from "react"; +import { Platform, TouchableOpacity } from "react-native"; const DropdownMenu = !Platform.isTV ? require("zeego/dropdown-menu") : null; -import { useVideoContext } from "../contexts/VideoContext"; -import { useLocalSearchParams, useRouter } from "expo-router"; import { BITRATES } from "@/components/BitrateSelector"; +import { useLocalSearchParams, useRouter } from "expo-router"; import { useControlContext } from "../contexts/ControlContext"; +import { useVideoContext } from "../contexts/VideoContext"; const DropdownView = () => { const videoContext = useVideoContext(); const { subtitleTracks, audioTracks } = videoContext; const ControlContext = useControlContext(); - const [item, mediaSource] = [ControlContext?.item, ControlContext?.mediaSource]; + const [item, mediaSource] = [ + ControlContext?.item, + ControlContext?.mediaSource, + ]; const router = useRouter(); const { subtitleIndex, audioIndex, bitrateValue } = useLocalSearchParams<{ @@ -34,27 +37,29 @@ const DropdownView = () => { // @ts-expect-error router.replace(`player/direct-player?${queryParams}`); }, - [item, mediaSource, subtitleIndex, audioIndex] + [item, mediaSource, subtitleIndex, audioIndex], ); return ( - - + + - Quality + + Quality + { changeBitrate(bitrate.value?.toString() ?? "")} + onValueChange={() => + changeBitrate(bitrate.value?.toString() ?? "") + } > - {bitrate.key} + + {bitrate.key} + ))} - Subtitle + + Subtitle + { value={subtitleIndex === sub.index.toString()} onValueChange={() => sub.setTrack()} > - {sub.name} + + {sub.name} + ))} - Audio + + Audio + { value={audioIndex === track.index.toString()} onValueChange={() => track.setTrack()} > - {track.name} + + {track.name} + ))} diff --git a/components/video-player/controls/types.ts b/components/video-player/controls/types.ts index 8040f6d3..f6c0e00a 100644 --- a/components/video-player/controls/types.ts +++ b/components/video-player/controls/types.ts @@ -23,4 +23,4 @@ type Track = { setTrack: () => void; }; -export { EmbeddedSubtitle, ExternalSubtitle, TranscodedSubtitle, Track }; +export type { EmbeddedSubtitle, ExternalSubtitle, TranscodedSubtitle, Track }; diff --git a/components/video-player/controls/useTapDetection.tsx b/components/video-player/controls/useTapDetection.tsx index 041e6d39..545e1c2c 100644 --- a/components/video-player/controls/useTapDetection.tsx +++ b/components/video-player/controls/useTapDetection.tsx @@ -1,5 +1,5 @@ import { useRef } from "react"; -import { GestureResponderEvent } from "react-native"; +import type { GestureResponderEvent } from "react-native"; interface TapDetectionOptions { maxDuration?: number; @@ -33,7 +33,7 @@ export const useTapDetection = ({ const touchDuration = touchEndTime - touchStartTime.current; const touchDistance = Math.sqrt( Math.pow(touchEndPosition.x - touchStartPosition.current.x, 2) + - Math.pow(touchEndPosition.y - touchStartPosition.current.y, 2) + Math.pow(touchEndPosition.y - touchStartPosition.current.y, 2), ); if (touchDuration < maxDuration && touchDistance < maxDistance) { diff --git a/components/vlc/VideoDebugInfo.tsx b/components/vlc/VideoDebugInfo.tsx index 0d2fb0df..d936cb90 100644 --- a/components/vlc/VideoDebugInfo.tsx +++ b/components/vlc/VideoDebugInfo.tsx @@ -1,12 +1,10 @@ -import { - TrackInfo, - VlcPlayerViewRef, -} from "@/modules/VlcPlayer.types"; -import React, { useEffect, useState } from "react"; -import { TouchableOpacity, View, ViewProps } from "react-native"; +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 { Text } from "../common/Text"; -import { useTranslation } from "react-i18next"; interface Props extends ViewProps { playerRef: React.RefObject; @@ -15,7 +13,7 @@ interface Props extends ViewProps { export const VideoDebugInfo: React.FC = ({ playerRef, ...props }) => { const [audioTracks, setAudioTracks] = useState(null); const [subtitleTracks, setSubtitleTracks] = useState( - null + null, ); useEffect(() => { @@ -45,15 +43,15 @@ export const VideoDebugInfo: React.FC = ({ playerRef, ...props }) => { }} {...props} > - {t("player.playback_state")} - {t("player.audio_tracks")} + {t("player.playback_state")} + {t("player.audio_tracks")} {audioTracks && audioTracks.map((track, index) => ( {track.name} ({t("player.index")} {track.index}) ))} - {t("player.subtitles_tracks")} + {t("player.subtitles_tracks")} {subtitleTracks && subtitleTracks.map((track, index) => ( @@ -61,7 +59,7 @@ export const VideoDebugInfo: React.FC = ({ playerRef, ...props }) => { ))} { if (playerRef.current) { playerRef.current.getAudioTracks().then(setAudioTracks); @@ -69,7 +67,9 @@ export const VideoDebugInfo: React.FC = ({ playerRef, ...props }) => { } }} > - {t("player.refresh_tracks")} + + {t("player.refresh_tracks")} + ); diff --git a/constants/Languages.ts b/constants/Languages.ts index 0a6d63b1..8014e380 100644 --- a/constants/Languages.ts +++ b/constants/Languages.ts @@ -1,4 +1,4 @@ -import { DefaultLanguageOption } from "@/utils/atoms/settings"; +import type { DefaultLanguageOption } from "@/utils/atoms/settings"; export const LANGUAGES: DefaultLanguageOption[] = [ { label: "English", value: "eng" }, diff --git a/hooks/useAdjacentEpisodes.ts b/hooks/useAdjacentEpisodes.ts index 6fb32ac3..4c5a6e2b 100644 --- a/hooks/useAdjacentEpisodes.ts +++ b/hooks/useAdjacentEpisodes.ts @@ -1,9 +1,9 @@ import { apiAtom } from "@/providers/JellyfinProvider"; -import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client"; +import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client"; import { getTvShowsApi } from "@jellyfin/sdk/lib/utils/api"; import { useQuery } from "@tanstack/react-query"; -import { useMemo } from "react"; import { useAtomValue } from "jotai"; +import { useMemo } from "react"; interface AdjacentEpisodesProps { item?: BaseItemDto | null; diff --git a/hooks/useControlsVisibility.ts b/hooks/useControlsVisibility.ts index 964c296a..71c6197d 100644 --- a/hooks/useControlsVisibility.ts +++ b/hooks/useControlsVisibility.ts @@ -5,11 +5,11 @@ import { useSharedValue, } from "react-native-reanimated"; -export const useControlsVisibility = (timeout: number = 3000) => { +export const useControlsVisibility = (timeout = 3000) => { const opacity = useSharedValue(1); const hideControlsTimerRef = useRef | null>( - null + null, ); const showControls = useCallback(() => { diff --git a/hooks/useCreditSkipper.ts b/hooks/useCreditSkipper.ts index 14a77161..0317f66b 100644 --- a/hooks/useCreditSkipper.ts +++ b/hooks/useCreditSkipper.ts @@ -1,10 +1,10 @@ -import { useCallback, useEffect, useState } from "react"; -import { useQuery } from "@tanstack/react-query"; -import { useAtom } from "jotai"; 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 { @@ -25,7 +25,7 @@ export const useCreditSkipper = ( currentTime: number, seek: (time: number) => void, play: () => void, - isVlc: boolean = false + isVlc = false, ) => { const [api] = useAtom(apiAtom); const [showSkipCreditButton, setShowSkipCreditButton] = useState(false); @@ -54,7 +54,7 @@ export const useCreditSkipper = ( `${api.basePath}/Episode/${itemId}/Timestamps`, { headers: getAuthHeaders(api), - } + }, ); if (res?.status !== 200) { @@ -71,7 +71,7 @@ export const useCreditSkipper = ( if (creditTimestamps) { setShowSkipCreditButton( currentTime > creditTimestamps.Credits.Start && - currentTime < creditTimestamps.Credits.End + currentTime < creditTimestamps.Credits.End, ); } }, [creditTimestamps, currentTime]); diff --git a/hooks/useDefaultPlaySettings.ts b/hooks/useDefaultPlaySettings.ts index 9e0424fe..0991def0 100644 --- a/hooks/useDefaultPlaySettings.ts +++ b/hooks/useDefaultPlaySettings.ts @@ -1,7 +1,7 @@ -import { Bitrate, BITRATES } from "@/components/BitrateSelector"; -import { Settings } from "@/utils/atoms/settings"; +import { BITRATES, Bitrate } from "@/components/BitrateSelector"; +import type { Settings } from "@/utils/atoms/settings"; import { - BaseItemDto, + type BaseItemDto, MediaSourceInfo, } from "@jellyfin/sdk/lib/generated-client"; import { useMemo } from "react"; @@ -9,7 +9,7 @@ import { useMemo } from "react"; // Used only for initial play settings. const useDefaultPlaySettings = ( item: BaseItemDto, - settings: Settings | null + settings: Settings | null, ) => { const playSettings = useMemo(() => { // 1. Get first media source @@ -21,11 +21,11 @@ const useDefaultPlaySettings = ( (x) => x.Type === "Audio" && x.Language === - settings?.defaultAudioLanguage?.ThreeLetterISOLanguageName + settings?.defaultAudioLanguage?.ThreeLetterISOLanguageName, )?.Index; const firstAudioIndex = mediaSource?.MediaStreams?.find( - (x) => x.Type === "Audio" + (x) => x.Type === "Audio", )?.Index; // 4. Get default bitrate from settings or fallback to max diff --git a/hooks/useDownloadedFileOpener.ts b/hooks/useDownloadedFileOpener.ts index 4c630710..91cf1fbe 100644 --- a/hooks/useDownloadedFileOpener.ts +++ b/hooks/useDownloadedFileOpener.ts @@ -1,6 +1,6 @@ import { usePlaySettings } from "@/providers/PlaySettingsProvider"; import { writeToLog } from "@/utils/log"; -import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client"; +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"; @@ -41,7 +41,7 @@ export const useDownloadedFileOpener = () => { console.error("Error opening file:", error); } }, - [setOfflineSettings, setPlayUrl, router] + [setOfflineSettings, setPlayUrl, router], ); return { openFile }; diff --git a/hooks/useFavorite.ts b/hooks/useFavorite.ts index 437d290e..74a0216e 100644 --- a/hooks/useFavorite.ts +++ b/hooks/useFavorite.ts @@ -1,9 +1,9 @@ import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; -import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client"; +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, useState, useMemo } from "react"; +import { useEffect, useMemo, useState } from "react"; export const useFavorite = (item: BaseItemDto) => { const queryClient = useQueryClient(); @@ -26,7 +26,7 @@ export const useFavorite = (item: BaseItemDto) => { ...newData, UserData: { ...old.UserData, ...newData.UserData }, }; - } + }, ); }; diff --git a/hooks/useHaptic.ts b/hooks/useHaptic.ts index 51b26f83..132a599c 100644 --- a/hooks/useHaptic.ts +++ b/hooks/useHaptic.ts @@ -1,6 +1,6 @@ +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 = @@ -25,7 +25,7 @@ export const useHaptic = (feedbackType: HapticFeedbackType = "selection") => { ? () => {} : () => Haptics.impactAsync(type); }, - [] + [], ); const createNotificationFeedback = useCallback( (type: typeof Haptics.NotificationFeedbackType) => { @@ -33,7 +33,7 @@ export const useHaptic = (feedbackType: HapticFeedbackType = "selection") => { ? () => {} : () => Haptics.notificationAsync(type); }, - [] + [], ); const hapticHandlers = useMemo( @@ -46,14 +46,14 @@ export const useHaptic = (feedbackType: HapticFeedbackType = "selection") => { ? () => {} : Haptics.selectionAsync, success: createNotificationFeedback( - Haptics.NotificationFeedbackType.Success + Haptics.NotificationFeedbackType.Success, ), warning: createNotificationFeedback( - Haptics.NotificationFeedbackType.Warning + Haptics.NotificationFeedbackType.Warning, ), error: createNotificationFeedback(Haptics.NotificationFeedbackType.Error), }), - [createHapticHandler, createNotificationFeedback] + [createHapticHandler, createNotificationFeedback], ); if (settings?.disableHapticFeedback) { diff --git a/hooks/useImageColors.ts b/hooks/useImageColors.ts index 4928ccc7..5f67f42f 100644 --- a/hooks/useImageColors.ts +++ b/hooks/useImageColors.ts @@ -7,7 +7,7 @@ import { } from "@/utils/atoms/primaryColor"; import { getItemImage } from "@/utils/getItemImage"; import { storage } from "@/utils/mmkv"; -import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client"; +import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client"; import { useAtom, useAtomValue } from "jotai"; import { useEffect, useMemo } from "react"; import { Platform } from "react-native"; @@ -70,40 +70,48 @@ export const useImageColors = ({ fallback: "#fff", cache: false, }) - .then((colors: { platform: string; dominant: string; vibrant: string; detail: string; primary: string; }) => { - let primary: string = "#fff"; - let text: string = "#000"; - let backup: string = "#fff"; + .then( + (colors: { + platform: string; + dominant: string; + vibrant: string; + detail: string; + primary: string; + }) => { + let primary = "#fff"; + let text = "#000"; + let backup = "#fff"; - // Select the appropriate color based on the platform - if (colors.platform === "android") { - primary = colors.dominant; - backup = colors.vibrant; - } else if (colors.platform === "ios") { - primary = colors.detail; - backup = colors.primary; - } + // Select the appropriate color based on the platform + if (colors.platform === "android") { + primary = colors.dominant; + backup = colors.vibrant; + } else if (colors.platform === "ios") { + primary = colors.detail; + 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); - } + // 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); + // Calculate the text color based on the primary color + if (primary) text = calculateTextColor(primary); - setPrimaryColor({ - primary, - text, - }); + setPrimaryColor({ + primary, + text, + }); - // Cache the colors in storage - if (source.uri && primary) { - storage.set(`${source.uri}-primary`, primary); - storage.set(`${source.uri}-text`, text); - } - }) + // Cache the colors in storage + if (source.uri && primary) { + storage.set(`${source.uri}-primary`, primary); + storage.set(`${source.uri}-text`, text); + } + }, + ) .catch((error: any) => { console.error("Error getting colors", error); }); diff --git a/hooks/useImageStorage.ts b/hooks/useImageStorage.ts index f379de1c..1c0bc362 100644 --- a/hooks/useImageStorage.ts +++ b/hooks/useImageStorage.ts @@ -62,7 +62,7 @@ const useImageStorage = () => { console.warn("Error saving image:", error); } }, - [] + [], ); const loadImage = useCallback(async (key: string) => { diff --git a/hooks/useIntroSkipper.ts b/hooks/useIntroSkipper.ts index b41872dc..ab38148c 100644 --- a/hooks/useIntroSkipper.ts +++ b/hooks/useIntroSkipper.ts @@ -1,10 +1,10 @@ -import { useCallback, useEffect, useState } from "react"; -import { useQuery } from "@tanstack/react-query"; -import { useAtom } from "jotai"; 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 { @@ -26,7 +26,7 @@ export const useIntroSkipper = ( currentTime: number, seek: (ticks: number) => void, play: () => void, - isVlc: boolean = false + isVlc = false, ) => { const [api] = useAtom(apiAtom); const [showSkipButton, setShowSkipButton] = useState(false); @@ -54,7 +54,7 @@ export const useIntroSkipper = ( `${api.basePath}/Episode/${itemId}/IntroTimestamps`, { headers: getAuthHeaders(api), - } + }, ); if (res?.status !== 200) { @@ -71,7 +71,7 @@ export const useIntroSkipper = ( if (introTimestamps) { setShowSkipButton( currentTime > introTimestamps.ShowSkipPromptAt && - currentTime < introTimestamps.HideSkipPromptAt + currentTime < introTimestamps.HideSkipPromptAt, ); } }, [introTimestamps, currentTime]); diff --git a/hooks/useJellyfinDiscovery.tsx b/hooks/useJellyfinDiscovery.tsx index 963dfe81..3bb0757a 100644 --- a/hooks/useJellyfinDiscovery.tsx +++ b/hooks/useJellyfinDiscovery.tsx @@ -1,4 +1,4 @@ -import { useState, useCallback } from "react"; +import { useCallback, useState } from "react"; import dgram from "react-native-udp"; const JELLYFIN_DISCOVERY_PORT = 7359; @@ -53,7 +53,7 @@ export const useJellyfinDiscovery = () => { return; } console.log("Discovery message sent successfully"); - } + }, ); discoveryTimeout = setTimeout(() => { diff --git a/hooks/useJellyseerr.ts b/hooks/useJellyseerr.ts index f7400dc1..920d3404 100644 --- a/hooks/useJellyseerr.ts +++ b/hooks/useJellyseerr.ts @@ -1,45 +1,52 @@ -import axios, { AxiosError, AxiosInstance } from "axios"; -import {MovieResult, Results, TvResult} from "@/utils/jellyseerr/server/models/Search"; +import type { User as JellyseerrUser } from "@/utils/jellyseerr/server/entity/User"; +import type { + MovieResult, + Results, + TvResult, +} from "@/utils/jellyseerr/server/models/Search"; import { storage } from "@/utils/mmkv"; -import { inRange } from "lodash"; -import { User as JellyseerrUser } from "@/utils/jellyseerr/server/entity/User"; +import axios, { type AxiosError, type AxiosInstance } from "axios"; import { atom } from "jotai"; import { useAtom } from "jotai/index"; +import { inRange } from "lodash"; import "@/augmentations"; -import { useCallback, useMemo } from "react"; import { useSettings } from "@/utils/atoms/settings"; -import { toast } from "sonner-native"; +import type { RTRating } from "@/utils/jellyseerr/server/api/rating/rottentomatoes"; +import { + IssueStatus, + type IssueType, +} from "@/utils/jellyseerr/server/constants/issue"; import { MediaRequestStatus, MediaType, } from "@/utils/jellyseerr/server/constants/media"; -import MediaRequest from "@/utils/jellyseerr/server/entity/MediaRequest"; -import {MediaRequestBody, RequestResultsResponse} from "@/utils/jellyseerr/server/interfaces/api/requestInterfaces"; -import { MovieDetails } from "@/utils/jellyseerr/server/models/Movie"; -import { - SeasonWithEpisodes, - TvDetails, -} from "@/utils/jellyseerr/server/models/Tv"; -import { - IssueStatus, - IssueType, -} from "@/utils/jellyseerr/server/constants/issue"; -import Issue from "@/utils/jellyseerr/server/entity/Issue"; -import { RTRating } from "@/utils/jellyseerr/server/api/rating/rottentomatoes"; -import { writeErrorLog } from "@/utils/log"; -import DiscoverSlider from "@/utils/jellyseerr/server/entity/DiscoverSlider"; -import { t } from "i18next"; -import { +import type DiscoverSlider from "@/utils/jellyseerr/server/entity/DiscoverSlider"; +import type Issue from "@/utils/jellyseerr/server/entity/Issue"; +import type MediaRequest from "@/utils/jellyseerr/server/entity/MediaRequest"; +import type { GenreSliderItem } from "@/utils/jellyseerr/server/interfaces/api/discoverInterfaces"; +import type { + MediaRequestBody, + RequestResultsResponse, +} from "@/utils/jellyseerr/server/interfaces/api/requestInterfaces"; +import type { + ServiceCommonServer, + ServiceCommonServerWithDetails, +} from "@/utils/jellyseerr/server/interfaces/api/serviceInterfaces"; +import type { UserResultsResponse } from "@/utils/jellyseerr/server/interfaces/api/userInterfaces"; +import type { MovieDetails } from "@/utils/jellyseerr/server/models/Movie"; +import type { CombinedCredit, PersonDetails, } from "@/utils/jellyseerr/server/models/Person"; +import type { + SeasonWithEpisodes, + TvDetails, +} from "@/utils/jellyseerr/server/models/Tv"; +import { writeErrorLog } from "@/utils/log"; import { useQueryClient } from "@tanstack/react-query"; -import {GenreSliderItem} from "@/utils/jellyseerr/server/interfaces/api/discoverInterfaces"; -import {UserResultsResponse} from "@/utils/jellyseerr/server/interfaces/api/userInterfaces"; -import { - ServiceCommonServer, - ServiceCommonServerWithDetails -} from "@/utils/jellyseerr/server/interfaces/api/serviceInterfaces"; +import { t } from "i18next"; +import { useCallback, useMemo } from "react"; +import { toast } from "sonner-native"; interface SearchParams { query: string; @@ -134,15 +141,16 @@ export class JellyseerrApi { const { status, headers, data } = response; if (inRange(status, 200, 299)) { if (data.version < "2.0.0") { - const error = - t("jellyseerr.toasts.jellyseer_does_not_meet_requirements"); + const error = t( + "jellyseerr.toasts.jellyseer_does_not_meet_requirements", + ); toast.error(error); throw Error(error); } storage.setAny( JELLYSEERR_COOKIES, - headers["set-cookie"]?.flatMap((c) => c.split("; ")) ?? [] + headers["set-cookie"]?.flatMap((c) => c.split("; ")) ?? [], ); return { isValid: true, @@ -154,7 +162,7 @@ export class JellyseerrApi { `Jellyseerr returned a ${status} for url:\n` + response.config.url + "\n" + - JSON.stringify(response.data) + JSON.stringify(response.data), ); return { isValid: false, @@ -190,14 +198,14 @@ export class JellyseerrApi { async discoverSettings(): Promise { return this.axios ?.get( - Endpoints.API_V1 + Endpoints.SETTINGS + Endpoints.DISCOVER + Endpoints.API_V1 + Endpoints.SETTINGS + Endpoints.DISCOVER, ) .then(({ data }) => data); } async discover( endpoint: DiscoverEndpoint | string, - params: any + params: any, ): Promise { return this.axios ?.get(Endpoints.API_V1 + endpoint, { params }) @@ -206,18 +214,23 @@ export class JellyseerrApi { async getGenreSliders( endpoint: Endpoints.TV | Endpoints.MOVIE, - params: any = undefined + params: any = undefined, ): Promise { return this.axios - ?.get(Endpoints.API_V1 + Endpoints.DISCOVER + Endpoints.GENRE_SLIDER + endpoint, { params }) + ?.get( + Endpoints.API_V1 + + Endpoints.DISCOVER + + Endpoints.GENRE_SLIDER + + endpoint, + { params }, + ) .then(({ data }) => data); } async search(params: SearchParams): Promise { - return this.axios?.get( - Endpoints.API_V1 + Endpoints.SEARCH, - { params } - ).then(({ data }) => data) + return this.axios + ?.get(Endpoints.API_V1 + Endpoints.SEARCH, { params }) + .then(({ data }) => data); } async request(request: MediaRequestBody): Promise { @@ -232,15 +245,19 @@ export class JellyseerrApi { .then(({ data }) => data); } - async requests(params = { - filter: "all", - take: 10, - sort: "modified", - skip: 0 - }): Promise { + async requests( + params = { + filter: "all", + take: 10, + sort: "modified", + skip: 0, + }, + ): Promise { return this.axios - ?.get(Endpoints.API_V1 + Endpoints.REQUEST, {params}) - .then(({data}) => data); + ?.get(Endpoints.API_V1 + Endpoints.REQUEST, { + params, + }) + .then(({ data }) => data); } async movieDetails(id: number) { @@ -265,7 +282,7 @@ export class JellyseerrApi { Endpoints.API_V1 + Endpoints.PERSON + `/${id}` + - Endpoints.COMBINED_CREDITS + Endpoints.COMBINED_CREDITS, ) .then((response) => { return response?.data; @@ -275,7 +292,7 @@ export class JellyseerrApi { async movieRatings(id: number) { return this.axios ?.get( - `${Endpoints.API_V1}${Endpoints.MOVIE}/${id}${Endpoints.RATINGS}` + `${Endpoints.API_V1}${Endpoints.MOVIE}/${id}${Endpoints.RATINGS}`, ) .then(({ data }) => data); } @@ -291,7 +308,7 @@ export class JellyseerrApi { async tvRatings(id: number) { return this.axios ?.get( - `${Endpoints.API_V1}${Endpoints.TV}/${id}${Endpoints.RATINGS}` + `${Endpoints.API_V1}${Endpoints.TV}/${id}${Endpoints.RATINGS}`, ) .then(({ data }) => data); } @@ -299,7 +316,7 @@ export class JellyseerrApi { async tvSeason(id: number, seasonId: number) { return this.axios ?.get( - `${Endpoints.API_V1}${Endpoints.TV}/${id}/season/${seasonId}` + `${Endpoints.API_V1}${Endpoints.TV}/${id}/season/${seasonId}`, ) .then((response) => { return response?.data; @@ -308,21 +325,18 @@ export class JellyseerrApi { async user(params: any) { return this.axios - ?.get(`${Endpoints.API_V1}${Endpoints.USER}`, { params }) - .then(({data}) => data.results) + ?.get(`${Endpoints.API_V1}${Endpoints.USER}`, { + params, + }) + .then(({ data }) => data.results); } - imageProxy( - path?: string, - filter: string = "original", - width: number = 1920, - quality: number = 75 - ) { + imageProxy(path?: string, filter = "original", width = 1920, quality = 75) { return path ? this.axios.defaults.baseURL + `/_next/image?` + new URLSearchParams( - `url=https://image.tmdb.org/t/p/${filter}/${path}&w=${width}&q=${quality}` + `url=https://image.tmdb.org/t/p/${filter}/${path}&w=${width}&q=${quality}`, ).toString() : this.axios?.defaults.baseURL + `/images/overseerr_poster_not_found_logo_top.png`; @@ -345,16 +359,20 @@ export class JellyseerrApi { }); } - async service(type: 'radarr' | 'sonarr') { + async service(type: "radarr" | "sonarr") { return this.axios - ?.get(Endpoints.API_V1 + Endpoints.SERVICE + `/${type}`) - .then(({data}) => data); + ?.get( + Endpoints.API_V1 + Endpoints.SERVICE + `/${type}`, + ) + .then(({ data }) => data); } - async serviceDetails(type: 'radarr' | 'sonarr', id: number) { + async serviceDetails(type: "radarr" | "sonarr", id: number) { return this.axios - ?.get(Endpoints.API_V1 + Endpoints.SERVICE + `/${type}` + `/${id}`) - .then(({data}) => data); + ?.get( + Endpoints.API_V1 + Endpoints.SERVICE + `/${type}` + `/${id}`, + ) + .then(({ data }) => data); } private setInterceptors() { @@ -364,7 +382,7 @@ export class JellyseerrApi { if (cookies) { storage.setAny( JELLYSEERR_COOKIES, - response.headers["set-cookie"]?.flatMap((c) => c.split("; ")) + response.headers["set-cookie"]?.flatMap((c) => c.split("; ")), ); } return response; @@ -378,20 +396,20 @@ export class JellyseerrApi { `error: ${error.toString()}\n` + `url: ${error?.config?.url}\n` + `data:\n` + - JSON.stringify(error.response?.data) + JSON.stringify(error.response?.data), ); if (error.status === 403) { clearJellyseerrStorageData(); } return Promise.reject(error); - } + }, ); this.axios.interceptors.request.use( async (config) => { const cookies = storage.get(JELLYSEERR_COOKIES); if (cookies) { - const headerName = this.axios.defaults.xsrfHeaderName!!; + const headerName = this.axios.defaults.xsrfHeaderName!; const xsrfToken = cookies .find((c) => c.includes(headerName)) ?.split(headerName + "=")?.[1]; @@ -403,7 +421,7 @@ export class JellyseerrApi { }, (error) => { console.error("Jellyseerr request error", error); - } + }, ); } } @@ -439,55 +457,74 @@ export const useJellyseerr = () => { switch (mediaRequest.status) { case MediaRequestStatus.PENDING: case MediaRequestStatus.APPROVED: - toast.success(t("jellyseerr.toasts.requested_item", {item: title})); - onSuccess?.() + toast.success( + t("jellyseerr.toasts.requested_item", { item: title }), + ); + onSuccess?.(); break; case MediaRequestStatus.DECLINED: - toast.error(t("jellyseerr.toasts.you_dont_have_permission_to_request")); + toast.error( + t("jellyseerr.toasts.you_dont_have_permission_to_request"), + ); break; case MediaRequestStatus.FAILED: - toast.error(t("jellyseerr.toasts.something_went_wrong_requesting_media")); + toast.error( + t("jellyseerr.toasts.something_went_wrong_requesting_media"), + ); break; } }); }, - [jellyseerrApi] + [jellyseerrApi], ); const isJellyseerrResult = ( - items: any | null | undefined + items: any | null | undefined, ): items is Results => { return ( items && - Object.hasOwn(items, "mediaType") && - Object.values(MediaType).includes(items["mediaType"]) - ) + Object.hasOwn(items, "mediaType") && + Object.values(MediaType).includes(items["mediaType"]) + ); }; - const getTitle = (item?: TvResult | TvDetails | MovieResult | MovieDetails) => { + const getTitle = ( + item?: TvResult | TvDetails | MovieResult | MovieDetails, + ) => { return isJellyseerrResult(item) - ? (item.mediaType == MediaType.MOVIE ? item?.title : item?.name) - : (item?.mediaInfo.mediaType == MediaType.MOVIE ? (item as MovieDetails)?.title : (item as TvDetails)?.name) + ? item.mediaType == MediaType.MOVIE + ? item?.title + : item?.name + : item?.mediaInfo.mediaType == MediaType.MOVIE + ? (item as MovieDetails)?.title + : (item as TvDetails)?.name; }; - const getYear = (item?: TvResult | TvDetails | MovieResult | MovieDetails) => { - return new Date(( - isJellyseerrResult(item) - ? (item.mediaType == MediaType.MOVIE ? item?.releaseDate : item?.firstAirDate) - : (item?.mediaInfo.mediaType == MediaType.MOVIE ? (item as MovieDetails)?.releaseDate : (item as TvDetails)?.firstAirDate)) - || "" - )?.getFullYear?.() + const getYear = ( + item?: TvResult | TvDetails | MovieResult | MovieDetails, + ) => { + return new Date( + (isJellyseerrResult(item) + ? item.mediaType == MediaType.MOVIE + ? item?.releaseDate + : item?.firstAirDate + : item?.mediaInfo.mediaType == MediaType.MOVIE + ? (item as MovieDetails)?.releaseDate + : (item as TvDetails)?.firstAirDate) || "", + )?.getFullYear?.(); }; - const getMediaType = (item?: TvResult | TvDetails | MovieResult | MovieDetails): MediaType => { + const getMediaType = ( + item?: TvResult | TvDetails | MovieResult | MovieDetails, + ): MediaType => { return isJellyseerrResult(item) ? item.mediaType - : item?.mediaInfo?.mediaType + : item?.mediaInfo?.mediaType; }; const jellyseerrRegion = useMemo( () => jellyseerrUser?.settings?.region || "US", - [jellyseerrUser] + [jellyseerrUser], ); const jellyseerrLocale = useMemo(() => { diff --git a/hooks/useMarkAsPlayed.ts b/hooks/useMarkAsPlayed.ts index 44731ce8..34d1d545 100644 --- a/hooks/useMarkAsPlayed.ts +++ b/hooks/useMarkAsPlayed.ts @@ -1,10 +1,10 @@ import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; import { markAsNotPlayed } from "@/utils/jellyfin/playstate/markAsNotPlayed"; import { markAsPlayed } from "@/utils/jellyfin/playstate/markAsPlayed"; -import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; +import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; import { useQueryClient } from "@tanstack/react-query"; -import { useHaptic } from "./useHaptic"; import { useAtom } from "jotai"; +import { useHaptic } from "./useHaptic"; export const useMarkAsPlayed = (items: BaseItemDto[]) => { const [api] = useAtom(apiAtom); @@ -24,10 +24,10 @@ export const useMarkAsPlayed = (items: BaseItemDto[]) => { ]; items.forEach((item) => { - if(!item.Id) return; + if (!item.Id) return; queriesToInvalidate.push(["item", item.Id]); }); - + queriesToInvalidate.forEach((queryKey) => { queryClient.invalidateQueries({ queryKey }); }); @@ -37,7 +37,7 @@ export const useMarkAsPlayed = (items: BaseItemDto[]) => { lightHapticFeedback(); items.forEach((item) => { - // Optimistic update + // Optimistic update queryClient.setQueryData( ["item", item.Id], (oldData: BaseItemDto | undefined) => { @@ -51,17 +51,19 @@ export const useMarkAsPlayed = (items: BaseItemDto[]) => { }; } return oldData; - } + }, ); - }) + }); try { // Process all items - await Promise.all(items.map(item => - played - ? markAsPlayed({ api, item, userId: user?.Id }) - : markAsNotPlayed({ api, itemId: item?.Id, userId: user?.Id }) - )); + await Promise.all( + items.map((item) => + played + ? markAsPlayed({ api, item, userId: user?.Id }) + : markAsNotPlayed({ api, itemId: item?.Id, userId: user?.Id }), + ), + ); // Bulk invalidate queryClient.invalidateQueries({ @@ -73,19 +75,21 @@ export const useMarkAsPlayed = (items: BaseItemDto[]) => { "episodes", "seasons", "home", - ...items.map(item => ["item", item.Id]) - ].flat() + ...items.map((item) => ["item", item.Id]), + ].flat(), }); } catch (error) { // Revert all optimistic updates on any failure - items.forEach(item => { + items.forEach((item) => { queryClient.setQueryData( ["item", item.Id], (oldData: BaseItemDto | undefined) => - oldData ? { - ...oldData, - UserData: { ...oldData.UserData, Played: played } - } : oldData + oldData + ? { + ...oldData, + UserData: { ...oldData.UserData, Played: played }, + } + : oldData, ); }); console.error("Error updating played status:", error); diff --git a/hooks/useOrientation.ts b/hooks/useOrientation.ts index dff4015d..a485ec22 100644 --- a/hooks/useOrientation.ts +++ b/hooks/useOrientation.ts @@ -1,5 +1,5 @@ -import orientationToOrientationLock from "@/utils/OrientationLockConverter"; import * as ScreenOrientation from "@/packages/expo-screen-orientation"; +import orientationToOrientationLock from "@/utils/OrientationLockConverter"; import { useEffect, useState } from "react"; import { Platform } from "react-native"; @@ -7,7 +7,7 @@ export const useOrientation = () => { const [orientation, setOrientation] = useState( Platform.isTV ? ScreenOrientation.OrientationLock.LANDSCAPE - : ScreenOrientation.OrientationLock.UNKNOWN + : ScreenOrientation.OrientationLock.UNKNOWN, ); if (Platform.isTV) return { orientation, setOrientation }; @@ -16,7 +16,7 @@ export const useOrientation = () => { const orientationSubscription = ScreenOrientation.addOrientationChangeListener((event) => { setOrientation( - orientationToOrientationLock(event.orientationInfo.orientation) + orientationToOrientationLock(event.orientationInfo.orientation), ); }); diff --git a/hooks/useRemuxHlsToMp4.ts b/hooks/useRemuxHlsToMp4.ts index 22ea02ce..67c8777d 100644 --- a/hooks/useRemuxHlsToMp4.ts +++ b/hooks/useRemuxHlsToMp4.ts @@ -2,7 +2,7 @@ import { useDownload } from "@/providers/DownloadProvider"; import { apiAtom } from "@/providers/JellyfinProvider"; import { getItemImage } from "@/utils/getItemImage"; import { writeErrorLog, writeInfoLog, writeToLog } from "@/utils/log"; -import { +import type { BaseItemDto, MediaSourceInfo, } from "@jellyfin/sdk/lib/generated-client/models"; @@ -14,16 +14,16 @@ import { useRouter } from "expo-router"; const FFMPEGKitReactNative = !Platform.isTV ? require("ffmpeg-kit-react-native") : null; +import { useSettings } from "@/utils/atoms/settings"; +import useDownloadHelper from "@/utils/download"; +import type { JobStatus } from "@/utils/optimize-server"; +import type { Api } from "@jellyfin/sdk"; import { useAtomValue } from "jotai"; import { useCallback } from "react"; +import { useTranslation } from "react-i18next"; +import { Platform } from "react-native"; import { toast } from "sonner-native"; import useImageStorage from "./useImageStorage"; -import useDownloadHelper from "@/utils/download"; -import { Api } from "@jellyfin/sdk"; -import { useSettings } from "@/utils/atoms/settings"; -import { JobStatus } from "@/utils/optimize-server"; -import { Platform } from "react-native"; -import { useTranslation } from "react-i18next"; type FFmpegSession = typeof FFMPEGKitReactNative.FFmpegSession; type Statistics = typeof FFMPEGKitReactNative.Statistics; @@ -107,7 +107,7 @@ export const useRemuxHlsToMp4 = () => { setProcesses((prev: any[]) => { return prev.filter( (process: { itemId: string | undefined }) => - process.itemId !== item.Id + process.itemId !== item.Id, ); }); } catch (e) { @@ -116,7 +116,7 @@ export const useRemuxHlsToMp4 = () => { console.log("completeCallback ~ end"); }, - [processes, setProcesses] + [processes, setProcesses], ); const statisticsCallback = useCallback( @@ -146,13 +146,13 @@ export const useRemuxHlsToMp4 = () => { }); }); }, - [setProcesses, completeCallback] + [setProcesses, completeCallback], ); const startRemuxing = useCallback( async (item: BaseItemDto, url: string, mediaSource: MediaSourceInfo) => { const cacheDir = await FileSystem.getInfoAsync( - APP_CACHE_DOWNLOAD_DIRECTORY + APP_CACHE_DOWNLOAD_DIRECTORY, ); if (!cacheDir.exists) { await FileSystem.makeDirectoryAsync(APP_CACHE_DOWNLOAD_DIRECTORY, { @@ -178,7 +178,7 @@ export const useRemuxHlsToMp4 = () => { toast.dismiss(); }, }, - } + }, ); try { @@ -201,25 +201,25 @@ export const useRemuxHlsToMp4 = () => { createFFmpegCommand(url, output).join(" "), (session: any) => completeCallback(session, item), undefined, - (s: any) => statisticsCallback(s, item) + (s: any) => statisticsCallback(s, item), ); } catch (e) { const error = e as Error; console.error("Failed to remux:", error); writeErrorLog( `useRemuxHlsToMp4 ~ remuxing failed for item: ${item.Name}, - Error: ${error.message}, Stack: ${error.stack}` + Error: ${error.message}, Stack: ${error.stack}`, ); setProcesses((prev: any[]) => { return prev.filter( (process: { itemId: string | undefined }) => - process.itemId !== item.Id + process.itemId !== item.Id, ); }); throw error; // Re-throw the error to propagate it to the caller } }, - [settings, processes, setProcesses, completeCallback, statisticsCallback] + [settings, processes, setProcesses, completeCallback, statisticsCallback], ); const cancelRemuxing = useCallback(() => { diff --git a/hooks/useSessions.ts b/hooks/useSessions.ts index c0337164..6552f6ea 100644 --- a/hooks/useSessions.ts +++ b/hooks/useSessions.ts @@ -1,8 +1,8 @@ -import { useQuery } from "@tanstack/react-query"; import { apiAtom } from "@/providers/JellyfinProvider"; -import { useAtom } from "jotai"; -import { getSessionApi } from "@jellyfin/sdk/lib/utils/api/session-api"; 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; @@ -11,7 +11,10 @@ export interface useSessionsProps { activeWithinSeconds: number; } -export const useSessions = ({ refetchInterval = 5 * 1000, activeWithinSeconds = 360 }: useSessionsProps) => { +export const useSessions = ({ + refetchInterval = 5 * 1000, + activeWithinSeconds = 360, +}: useSessionsProps) => { const [api] = useAtom(apiAtom); const [user] = useAtom(userAtom); @@ -24,13 +27,17 @@ export const useSessions = ({ refetchInterval = 5 * 1000, activeWithinSeconds = const response = await getSessionApi(api).getSessions({ activeWithinSeconds: activeWithinSeconds, }); - + const result = response.data .filter((s) => s.NowPlayingItem) - .sort((a, b) => (b.NowPlayingItem?.Name ?? "").localeCompare(a.NowPlayingItem?.Name ?? "")); - + .sort((a, b) => + (b.NowPlayingItem?.Name ?? "").localeCompare( + a.NowPlayingItem?.Name ?? "", + ), + ); + // Notifications.setBadgeCountAsync(result.length); - return result + return result; }, refetchInterval: refetchInterval, }); diff --git a/hooks/useTrickplay.ts b/hooks/useTrickplay.ts index 9bb3630f..e221b49e 100644 --- a/hooks/useTrickplay.ts +++ b/hooks/useTrickplay.ts @@ -1,6 +1,6 @@ import { apiAtom } from "@/providers/JellyfinProvider"; import { ticksToMs } from "@/utils/time"; -import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client"; +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"; @@ -107,7 +107,7 @@ export const useTrickplay = (item: BaseItemDto, enabled = true) => { setTrickPlayUrl(newTrickPlayUrl); return newTrickPlayUrl; }, - [trickplayInfo, item, api, enabled] + [trickplayInfo, item, api, enabled], ); const prefetchAllTrickplayImages = useCallback(() => { diff --git a/hooks/useWebsockets.ts b/hooks/useWebsockets.ts index d9e6096a..70c91b4f 100644 --- a/hooks/useWebsockets.ts +++ b/hooks/useWebsockets.ts @@ -1,8 +1,8 @@ -import { useEffect } from "react"; -import { Alert } from "react-native"; -import { useRouter } from "expo-router"; import { useWebSocketContext } from "@/providers/WebSocketProvider"; +import { useRouter } from "expo-router"; +import { useEffect } from "react"; import { useTranslation } from "react-i18next"; +import { Alert } from "react-native"; interface UseWebSocketProps { isPlaying: boolean; @@ -42,7 +42,7 @@ export const useWebSocket = ({ console.log("Command ~ DisplayMessage"); const title = json?.Data?.Arguments?.Header; const body = json?.Data?.Arguments?.Text; - Alert.alert(t("player.message_from_server", {message: title}), body); + Alert.alert(t("player.message_from_server", { message: title }), body); } }; diff --git a/i18n.ts b/i18n.ts index 806668c6..47246a59 100644 --- a/i18n.ts +++ b/i18n.ts @@ -1,20 +1,20 @@ import i18n from "i18next"; import { initReactI18next } from "react-i18next"; +import { getLocales } from "expo-localization"; import de from "./translations/de.json"; import en from "./translations/en.json"; import es from "./translations/es.json"; import fr from "./translations/fr.json"; import it from "./translations/it.json"; import ja from "./translations/ja.json"; -import tr from "./translations/tr.json"; import nl from "./translations/nl.json"; import pl from "./translations/pl.json"; import sv from "./translations/sv.json"; -import ua from "./translations/ua.json" -import zhCN from './translations/zh-CN.json'; -import zhTW from './translations/zh-TW.json'; -import { getLocales } from "expo-localization"; +import tr from "./translations/tr.json"; +import ua from "./translations/ua.json"; +import zhCN from "./translations/zh-CN.json"; +import zhTW from "./translations/zh-TW.json"; export const APP_LANGUAGES = [ { label: "Deutsch", value: "de" }, diff --git a/modules/VlcPlayerView.tsx b/modules/VlcPlayerView.tsx index 83cacb19..a08ed44e 100644 --- a/modules/VlcPlayerView.tsx +++ b/modules/VlcPlayerView.tsx @@ -1,13 +1,13 @@ import { requireNativeViewManager } from "expo-modules-core"; import * as React from "react"; -import { +import { VideoPlayer, useSettings } from "@/utils/atoms/settings"; +import { Platform } from "react-native"; +import type { + VlcPlayerSource, VlcPlayerViewProps, VlcPlayerViewRef, - VlcPlayerSource, } from "./VlcPlayer.types"; -import {useSettings, VideoPlayer} from "@/utils/atoms/settings"; -import {Platform} from "react-native"; interface NativeViewRef extends VlcPlayerViewRef { setNativeProps?: (props: Partial) => void; @@ -23,13 +23,13 @@ const NativeView = React.forwardRef( if (Platform.OS === "ios" || Platform.isTVOS) { if (settings.defaultPlayer == VideoPlayer.VLC_3) { - console.log("[Apple] Using Vlc Player 3") - return + console.log("[Apple] Using Vlc Player 3"); + return ; } } - console.log("Using default Vlc Player") - return - } + console.log("Using default Vlc Player"); + return ; + }, ); const VlcPlayerView = React.forwardRef( @@ -38,7 +38,7 @@ const VlcPlayerView = React.forwardRef( React.useImperativeHandle(ref, () => ({ startPictureInPicture: async () => { - await nativeRef.current?.startPictureInPicture() + await nativeRef.current?.startPictureInPicture(); }, play: async () => { await nativeRef.current?.play(); @@ -143,7 +143,7 @@ const VlcPlayerView = React.forwardRef( onPipStarted={onPipStarted} /> ); - } + }, ); export default VlcPlayerView; diff --git a/modules/index.ts b/modules/index.ts index 397fc0eb..63eb373e 100644 --- a/modules/index.ts +++ b/modules/index.ts @@ -1,16 +1,16 @@ -import VlcPlayerView from "./VlcPlayerView"; import { + ChapterInfo, PlaybackStatePayload, ProgressUpdatePayload, - VideoLoadStartPayload, - VideoStateChangePayload, - VideoProgressPayload, - VlcPlayerSource, TrackInfo, - ChapterInfo, + VideoLoadStartPayload, + VideoProgressPayload, + VideoStateChangePayload, + VlcPlayerSource, VlcPlayerViewProps, VlcPlayerViewRef, } from "./VlcPlayer.types"; +import VlcPlayerView from "./VlcPlayerView"; export { VlcPlayerView, diff --git a/modules/vlc-player-3/src/VlcPlayer3Module.ts b/modules/vlc-player-3/src/VlcPlayer3Module.ts index b292aaff..c0501304 100644 --- a/modules/vlc-player-3/src/VlcPlayer3Module.ts +++ b/modules/vlc-player-3/src/VlcPlayer3Module.ts @@ -1,5 +1,5 @@ -import { requireNativeModule } from 'expo-modules-core'; +import { requireNativeModule } from "expo-modules-core"; // It loads the native module object from the JSI or falls back to // the bridge module (from NativeModulesProxy) if the remote debugger is on. -export default requireNativeModule('VlcPlayer3'); +export default requireNativeModule("VlcPlayer3"); diff --git a/modules/vlc-player/src/VlcPlayerModule.ts b/modules/vlc-player/src/VlcPlayerModule.ts index 1db9b184..be5b1b65 100644 --- a/modules/vlc-player/src/VlcPlayerModule.ts +++ b/modules/vlc-player/src/VlcPlayerModule.ts @@ -1,5 +1,5 @@ -import { requireNativeModule } from 'expo-modules-core'; +import { requireNativeModule } from "expo-modules-core"; // It loads the native module object from the JSI or falls back to // the bridge module (from NativeModulesProxy) if the remote debugger is on. -export default requireNativeModule('VlcPlayer'); +export default requireNativeModule("VlcPlayer"); diff --git a/package.json b/package.json index 1d4c9fe8..5df34655 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "android:tv": "EXPO_TV=1 expo run:android", "prebuild": "EXPO_TV=0 bun run clean", "prebuild:tv": "EXPO_TV=1 bun run clean", - "test": "jest --watchAll", + "prepare": "husky", "postinstall": "patch-package", "lint": "biome format --write ." }, @@ -111,6 +111,7 @@ }, "devDependencies": { "@babel/core": "^7.26.8", + "@biomejs/biome": "^1.9.4", "@react-native-community/cli": "15.1.3", "@react-native-tvos/config-tv": "^0.1.1", "@types/jest": "^29.5.14", @@ -119,18 +120,20 @@ "@types/react-native-vector-icons": "^6.4.18", "@types/react-test-renderer": "^19.0.0", "@types/uuid": "^10.0.0", + "husky": "^9.1.7", + "lint-staged": "^15.5.0", "patch-package": "^8.0.0", "postinstall-postinstall": "^2.1.0", "react-test-renderer": "19.0.0", - "typescript": "~5.7.3", - "@biomejs/biome": "^1.9.4" + "typescript": "~5.7.3" }, "private": true, "expo": { "install": { - "exclude": [ - "react-native" - ] + "exclude": ["react-native"] } + }, + "lint-staged": { + "*": ["biome check --no-errors-on-unmatched --files-ignore-unknown=true"] } } diff --git a/plugins/withAndroidManifest.js b/plugins/withAndroidManifest.js index acc1192a..1896f813 100644 --- a/plugins/withAndroidManifest.js +++ b/plugins/withAndroidManifest.js @@ -1,7 +1,9 @@ -const { withAndroidManifest: NativeAndroidManifest } = require("@expo/config-plugins"); +const { + withAndroidManifest: NativeAndroidManifest, +} = require("@expo/config-plugins"); const withAndroidManifest = (config) => - NativeAndroidManifest(config, async (config) => { + NativeAndroidManifest(config, async (config) => { const mainApplication = config.modResults.manifest.application[0]; // Initialize activity array if it doesn't exist @@ -9,27 +11,30 @@ const withAndroidManifest = (config) => mainApplication.activity = []; } - const googleCastActivityExists = mainApplication.activity.some(activity => - activity.$?.["android:name"] === "com.reactnative.googlecast.RNGCExpandedControllerActivity" + const googleCastActivityExists = mainApplication.activity.some( + (activity) => + activity.$?.["android:name"] === + "com.reactnative.googlecast.RNGCExpandedControllerActivity", ); // Only add the activity if it doesn't already exist if (!googleCastActivityExists) { mainApplication.activity.push({ $: { - "android:name": "com.reactnative.googlecast.RNGCExpandedControllerActivity", + "android:name": + "com.reactnative.googlecast.RNGCExpandedControllerActivity", "android:theme": "@style/Theme.MaterialComponents.NoActionBar", "android:launchMode": "singleTask", }, }); } - const mainActivity = mainApplication.activity.find(activity => - activity.$?.["android:name"] === ".MainActivity" + const mainActivity = mainApplication.activity.find( + (activity) => activity.$?.["android:name"] === ".MainActivity", ); if (mainActivity) { - mainActivity.$["android:supportsPictureInPicture"] = "true" + mainActivity.$["android:supportsPictureInPicture"] = "true"; } return config; diff --git a/plugins/withChangeNativeAndroidTextToWhite.js b/plugins/withChangeNativeAndroidTextToWhite.js index 95c2165a..c84de48a 100644 --- a/plugins/withChangeNativeAndroidTextToWhite.js +++ b/plugins/withChangeNativeAndroidTextToWhite.js @@ -14,12 +14,15 @@ const withChangeNativeAndroidTextToWhite = (expoConfig) => "main", "res", "values", - "styles.xml" + "styles.xml", ); let stylesXml = readFileSync(stylesXmlPath, "utf8"); - stylesXml = stylesXml.replace(/@android:color\/black/g, "@android:color/white"); + stylesXml = stylesXml.replace( + /@android:color\/black/g, + "@android:color/white", + ); writeFileSync(stylesXmlPath, stylesXml, { encoding: "utf8" }); } @@ -27,4 +30,4 @@ const withChangeNativeAndroidTextToWhite = (expoConfig) => }, ]); -module.exports = withChangeNativeAndroidTextToWhite; \ No newline at end of file +module.exports = withChangeNativeAndroidTextToWhite; diff --git a/plugins/withGradleProperties.js b/plugins/withGradleProperties.js index 38ca08ac..23e4e34f 100644 --- a/plugins/withGradleProperties.js +++ b/plugins/withGradleProperties.js @@ -1,19 +1,20 @@ -const { withGradleProperties } = require('expo/config-plugins'); +const { withGradleProperties } = require("expo/config-plugins"); function setGradlePropertiesValue(config, key, value) { - return withGradleProperties(config, exportedConfig => { + return withGradleProperties(config, (exportedConfig) => { const props = exportedConfig.modResults; - const keyIdx = props.findIndex(item => item.type === 'property' && item.key === key); + const keyIdx = props.findIndex( + (item) => item.type === "property" && item.key === key, + ); const property = { - type: 'property', + type: "property", key, - value + value, }; if (keyIdx >= 0) { props.splice(keyIdx, 1, property); - } - else { + } else { props.push(property); } @@ -24,17 +25,13 @@ function setGradlePropertiesValue(config, key, value) { module.exports = function withCustomPlugin(config) { // Expo 52 is not setting this // https://github.com/expo/expo/issues/32558 - config = setGradlePropertiesValue( - config, - 'android.enableJetifier', - 'true', - ); + config = setGradlePropertiesValue(config, "android.enableJetifier", "true"); // Increase memory config = setGradlePropertiesValue( - config, - 'org.gradle.jvmargs', - '-Xmx4096m -XX:MaxMetaspaceSize=1024m', + config, + "org.gradle.jvmargs", + "-Xmx4096m -XX:MaxMetaspaceSize=1024m", ); return config; -}; \ No newline at end of file +}; diff --git a/plugins/withRNBackgroundDownloader.js b/plugins/withRNBackgroundDownloader.js index 1970ceb7..9185321b 100644 --- a/plugins/withRNBackgroundDownloader.js +++ b/plugins/withRNBackgroundDownloader.js @@ -16,12 +16,12 @@ function withRNBackgroundDownloader(expoConfig) { // Find the index of the AppDelegate import statement const importIndex = appDelegateLines.findIndex((line) => - /^#import "AppDelegate.h"/.test(line) + /^#import "AppDelegate.h"/.test(line), ); // Find the index of the last line before the @end statement const endStatementIndex = appDelegateLines.findIndex((line) => - /@end/.test(line) + /@end/.test(line), ); // Insert the import statement if it's not already present @@ -34,7 +34,7 @@ function withRNBackgroundDownloader(expoConfig) { appDelegateLines.splice( endStatementIndex, 0, - backgroundDownloaderDelegate + backgroundDownloaderDelegate, ); } diff --git a/plugins/withTrustLocalCerts.js b/plugins/withTrustLocalCerts.js index 13b326af..5cc22d4c 100644 --- a/plugins/withTrustLocalCerts.js +++ b/plugins/withTrustLocalCerts.js @@ -18,7 +18,7 @@ async function setCustomConfigAsync(config, androidManifest) { const res_file_path = path.join( await Paths.getResourceFolderAsync(config.modRequest.projectRoot), "xml", - "network_security_config.xml" + "network_security_config.xml", ); const res_dir = path.resolve(res_file_path, ".."); @@ -31,7 +31,7 @@ async function setCustomConfigAsync(config, androidManifest) { await fsPromises.copyFile(src_file_path, res_file_path); } catch (e) { throw new Error( - `Failed to copy network security config file from ${src_file_path} to ${res_file_path}: ${e.message}` + `Failed to copy network security config file from ${src_file_path} to ${res_file_path}: ${e.message}`, ); } const mainApplication = getMainApplicationOrThrow(androidManifest); diff --git a/providers/DownloadProvider.tsx b/providers/DownloadProvider.tsx index d59ebdeb..8c615101 100644 --- a/providers/DownloadProvider.tsx +++ b/providers/DownloadProvider.tsx @@ -7,14 +7,14 @@ import { getItemImage } from "@/utils/getItemImage"; import { useLog, writeToLog } from "@/utils/log"; import { storage } from "@/utils/mmkv"; import { + type JobStatus, cancelAllJobs, cancelJobById, deleteDownloadItemInfoFromDiskTmp, getAllJobsByDeviceId, getDownloadItemInfoFromDiskTmp, - JobStatus, } from "@/utils/optimize-server"; -import { +import type { BaseItemDto, MediaSourceInfo, } from "@jellyfin/sdk/lib/generated-client/models"; @@ -23,11 +23,12 @@ 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 { FileInfo } from "expo-file-system"; +import type { FileInfo } from "expo-file-system"; import Notifications from "expo-notifications"; import { useRouter } from "expo-router"; import { atom, useAtom } from "jotai"; -import React, { +import type React from "react"; +import { createContext, useCallback, useContext, @@ -35,7 +36,7 @@ import React, { useMemo, } from "react"; import { useTranslation } from "react-i18next"; -import { AppState, AppStateStatus, Platform } from "react-native"; +import { AppState, type AppStateStatus, Platform } from "react-native"; import { toast } from "sonner-native"; import { apiAtom } from "./JellyfinProvider"; @@ -113,12 +114,12 @@ function useDownloadProvider() { .filter((p) => jobs.some((j) => j.id === p.id)); const updatedProcesses = jobs.filter( - (j) => !downloadingProcesses.some((p) => p.id === j.id) + (j) => !downloadingProcesses.some((p) => p.id === j.id), ); setProcesses([...updatedProcesses, ...downloadingProcesses]); - for (let job of jobs) { + for (const job of jobs) { const process = processes.find((p) => p.id === job.id); if ( process && @@ -140,7 +141,7 @@ function useDownloadProvider() { toast.dismiss(); }, }, - } + }, ); Notifications.scheduleNotificationAsync({ content: { @@ -188,7 +189,7 @@ function useDownloadProvider() { console.error(error); } }, - [settings?.optimizedVersionsServerUrl, authHeader] + [settings?.optimizedVersionsServerUrl, authHeader], ); const APP_CACHE_DOWNLOAD_DIRECTORY = `${FileSystem.cacheDirectory}${Application.applicationId}/Downloads/`; @@ -206,8 +207,8 @@ function useDownloadProvider() { status: "downloading", progress: 0, } - : p - ) + : p, + ), ); BackGroundDownloader?.setConfig({ @@ -230,7 +231,7 @@ function useDownloadProvider() { toast.dismiss(); }, }, - } + }, ); const baseDirectory = FileSystem.documentDirectory; @@ -250,8 +251,8 @@ function useDownloadProvider() { status: "downloading", progress: 0, } - : p - ) + : p, + ), ); }) .progress((data) => { @@ -265,14 +266,14 @@ function useDownloadProvider() { status: "downloading", progress: percent, } - : p - ) + : p, + ), ); }) .done(async (doneHandler) => { await saveDownloadedItemInfo( process.item, - doneHandler.bytesDownloaded + doneHandler.bytesDownloaded, ); toast.success( t("home.downloads.toasts.download_completed_for_item", { @@ -287,7 +288,7 @@ function useDownloadProvider() { toast.dismiss(); }, }, - } + }, ); setTimeout(() => { BackGroundDownloader.completeHandler(process.id); @@ -308,7 +309,7 @@ function useDownloadProvider() { t("home.downloads.toasts.download_failed_for_item", { item: process.item.Name, error: errorMsg, - }) + }), ); writeToLog("ERROR", `Download failed for ${process.item.Name}`, { error, @@ -323,7 +324,7 @@ function useDownloadProvider() { }); }); }, - [queryClient, settings?.optimizedVersionsServerUrl, authHeader] + [queryClient, settings?.optimizedVersionsServerUrl, authHeader], ); const startBackgroundDownload = useCallback( @@ -359,7 +360,7 @@ function useDownloadProvider() { "Content-Type": "application/json", Authorization: authHeader, }, - } + }, ); if (response.status !== 201) { @@ -378,7 +379,7 @@ function useDownloadProvider() { toast.dismiss(); }, }, - } + }, ); } catch (error) { writeToLog("ERROR", "Error in startBackgroundDownload", error); @@ -394,13 +395,13 @@ function useDownloadProvider() { t("home.downloads.toasts.failed_to_start_download_for_item", { item: item.Name, message: error.message, - }) + }), ); if (error.response) { toast.error( t("home.downloads.toasts.server_responded_with_status", { statusCode: error.response.status, - }) + }), ); } else if (error.request) { t("home.downloads.toasts.no_response_received_from_server"); @@ -412,13 +413,13 @@ function useDownloadProvider() { toast.error( t( "home.downloads.toasts.failed_to_start_download_for_item_unexpected_error", - { item: item.Name } - ) + { item: item.Name }, + ), ); } } }, - [settings?.optimizedVersionsServerUrl, authHeader] + [settings?.optimizedVersionsServerUrl, authHeader], ); const deleteAllFiles = async (): Promise => { @@ -431,24 +432,24 @@ function useDownloadProvider() { .then(() => toast.success( t( - "home.downloads.toasts.all_files_folders_and_jobs_deleted_successfully" - ) - ) + "home.downloads.toasts.all_files_folders_and_jobs_deleted_successfully", + ), + ), ) .catch((reason) => { console.error("Failed to delete all files, folders, and jobs:", reason); toast.error( t( - "home.downloads.toasts.an_error_occured_while_deleting_files_and_jobs" - ) + "home.downloads.toasts.an_error_occured_while_deleting_files_and_jobs", + ), ); }); }; const forEveryDocumentDirFile = async ( - includeMMKV: boolean = true, + includeMMKV = true, ignoreList: string[] = [], - callback: (file: FileInfo) => void + callback: (file: FileInfo) => void, ) => { const baseDirectory = FileSystem.documentDirectory; if (!baseDirectory) { @@ -553,7 +554,7 @@ function useDownloadProvider() { } catch (error) { console.error( `Failed to delete file and storage entry for ID ${id}:`, - error + error, ); } }; @@ -563,17 +564,17 @@ function useDownloadProvider() { items.map((i) => { if (i.Id) return deleteFile(i.Id); return; - }) + }), ).then(() => successHapticFeedback()); }; const cleanCacheDirectory = async () => { const cacheDir = await FileSystem.getInfoAsync( - APP_CACHE_DOWNLOAD_DIRECTORY + APP_CACHE_DOWNLOAD_DIRECTORY, ); if (cacheDir.exists) { const cachedFiles = await FileSystem.readDirectoryAsync( - APP_CACHE_DOWNLOAD_DIRECTORY + APP_CACHE_DOWNLOAD_DIRECTORY, ); let position = 0; const batchSize = 3; @@ -584,14 +585,14 @@ function useDownloadProvider() { await Promise.all( itemsForBatch.map(async (file) => { const info = await FileSystem.getInfoAsync( - `${APP_CACHE_DOWNLOAD_DIRECTORY}${file}` + `${APP_CACHE_DOWNLOAD_DIRECTORY}${file}`, ); if (info.exists) { await FileSystem.deleteAsync(info.uri, { idempotent: true }); return Promise.resolve(file); } return Promise.reject(); - }) + }), ); position += batchSize; @@ -609,24 +610,24 @@ function useDownloadProvider() { promises.push(deleteFile(file.item.SeriesId)); promises.push(deleteFile(file.item.Id!)); return promises; - }) || [] + }) || [], ); }; const appSizeUsage = useMemo(async () => { const sizes: number[] = downloadedFiles?.map((d) => { - return getDownloadedItemSize(d.item.Id!!); + return getDownloadedItemSize(d.item.Id!); }) || []; await forEveryDocumentDirFile( true, - getAllDownloadedItems().map((d) => d.item.Id!!), + getAllDownloadedItems().map((d) => d.item.Id!), (file) => { if (file.exists) { sizes.push(file.size); } - } + }, ).catch((e) => { console.error(e); }); @@ -663,10 +664,10 @@ function useDownloadProvider() { } } - function saveDownloadedItemInfo(item: BaseItemDto, size: number = 0) { + function saveDownloadedItemInfo(item: BaseItemDto, size = 0) { try { const downloadedItems = storage.getString("downloadedItems"); - let items: DownloadedItem[] = downloadedItems + const items: DownloadedItem[] = downloadedItems ? JSON.parse(downloadedItems) : []; @@ -676,7 +677,7 @@ function useDownloadProvider() { if (!data?.mediaSource) throw new Error( - "Media source not found in tmp storage. Did you forget to save it before starting download?" + "Media source not found in tmp storage. Did you forget to save it before starting download?", ); const newItem = { item, mediaSource: data.mediaSource }; @@ -697,14 +698,14 @@ function useDownloadProvider() { } catch (error) { console.error( "Failed to save downloaded item information with media source:", - error + error, ); } } function getDownloadedItemSize(itemId: string): number { const size = storage.getString("downloadedItemSize-" + itemId); - return size ? parseInt(size) : 0; + return size ? Number.parseInt(size) : 0; } return { diff --git a/providers/DownloadProvider.tv.tsx b/providers/DownloadProvider.tv.tsx index 80d72026..c6319ea3 100644 --- a/providers/DownloadProvider.tv.tsx +++ b/providers/DownloadProvider.tv.tsx @@ -1,13 +1,14 @@ import { storage } from "@/utils/mmkv"; -import { JobStatus } from "@/utils/optimize-server"; -import { +import type { JobStatus } from "@/utils/optimize-server"; +import type { BaseItemDto, MediaSourceInfo, } from "@jellyfin/sdk/lib/generated-client/models"; import * as Application from "expo-application"; import * as FileSystem from "expo-file-system"; import { atom, useAtom } from "jotai"; -import React, { createContext, useCallback, useContext, useMemo } from "react"; +import type React from "react"; +import { createContext, useCallback, useContext, useMemo } from "react"; export type DownloadedItem = { item: Partial; @@ -38,7 +39,7 @@ function useDownloadProvider() { async (url: string, item: BaseItemDto, mediaSource: MediaSourceInfo) => { return null; }, - [] + [], ); const deleteAllFiles = async (): Promise => {}; @@ -59,11 +60,11 @@ function useDownloadProvider() { return null; } - function saveDownloadedItemInfo(item: BaseItemDto, size: number = 0) {} + function saveDownloadedItemInfo(item: BaseItemDto, size = 0) {} function getDownloadedItemSize(itemId: string): number { const size = storage.getString("downloadedItemSize-" + itemId); - return size ? parseInt(size) : 0; + return size ? Number.parseInt(size) : 0; } const APP_CACHE_DOWNLOAD_DIRECTORY = `${FileSystem.cacheDirectory}${Application.applicationId}/Downloads/`; diff --git a/providers/JellyfinProvider.tsx b/providers/JellyfinProvider.tsx index 1256d2ae..b8bab659 100644 --- a/providers/JellyfinProvider.tsx +++ b/providers/JellyfinProvider.tsx @@ -2,19 +2,21 @@ import "@/augmentations"; import { useInterval } from "@/hooks/useInterval"; import { JellyseerrApi, useJellyseerr } from "@/hooks/useJellyseerr"; import { useSettings } from "@/utils/atoms/settings"; +import { writeErrorLog, writeInfoLog } from "@/utils/log"; import { storage } from "@/utils/mmkv"; import { store } from "@/utils/store"; -import { Api, Jellyfin } from "@jellyfin/sdk"; -import { UserDto } from "@jellyfin/sdk/lib/generated-client/models"; +import { type Api, Jellyfin } from "@jellyfin/sdk"; +import type { UserDto } from "@jellyfin/sdk/lib/generated-client/models"; import { getUserApi } from "@jellyfin/sdk/lib/utils/api"; import { useMutation, useQuery } from "@tanstack/react-query"; import axios, { AxiosError } from "axios"; import { router, useSegments } from "expo-router"; import * as SplashScreen from "expo-splash-screen"; import { atom, useAtom } from "jotai"; -import React, { +import type React from "react"; +import { + type ReactNode, createContext, - ReactNode, useCallback, useContext, useEffect, @@ -25,7 +27,6 @@ import { useTranslation } from "react-i18next"; import { Platform } from "react-native"; import { getDeviceName } from "react-native-device-info"; import uuid from "react-native-uuid"; -import {writeErrorLog, writeInfoLog} from "@/utils/log"; interface Server { address: string; @@ -45,7 +46,7 @@ interface JellyfinContextValue { } const JellyfinContext = createContext( - undefined + undefined, ); export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({ @@ -68,7 +69,7 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({ name: deviceName, id, }, - }) + }), ); setDeviceId(id); })(); @@ -104,7 +105,7 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({ null, { headers, - } + }, ); if (response?.status === 200) { setSecret(response?.data?.Secret); @@ -124,7 +125,7 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({ try { const response = await api.axiosInstance.get( - `${api.basePath}/QuickConnect/Connect?Secret=${secret}` + `${api.basePath}/QuickConnect/Connect?Secret=${secret}`, ); if (response.status === 200) { @@ -138,7 +139,7 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({ }, { headers, - } + }, ); const { AccessToken, User } = authResponse.data; @@ -167,7 +168,7 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({ await refreshStreamyfinPluginSettings(); })(); }, []); - + useEffect(() => { store.set(apiAtom, api); }, [api]); @@ -176,9 +177,8 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({ useInterval(refreshStreamyfinPluginSettings, 60 * 5 * 1000); // 5 min const discoverServers = async (url: string): Promise => { - const servers = await jellyfin?.discovery.getRecommendedServerCandidates( - url - ); + const servers = + await jellyfin?.discovery.getRecommendedServerCandidates(url); return servers?.map((server) => ({ address: server.address })) || []; }; @@ -193,7 +193,7 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({ }, onSuccess: (_, server) => { const previousServers = JSON.parse( - storage.getString("previousServers") || "[]" + storage.getString("previousServers") || "[]", ); const updatedServers = [ server, @@ -201,7 +201,7 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({ ]; storage.set( "previousServers", - JSON.stringify(updatedServers.slice(0, 5)) + JSON.stringify(updatedServers.slice(0, 5)), ); }, onError: (error) => { @@ -241,7 +241,7 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({ const recentPluginSettings = await refreshStreamyfinPluginSettings(); if (recentPluginSettings?.jellyseerrServerUrl?.value) { const jellyseerrApi = new JellyseerrApi( - recentPluginSettings.jellyseerrServerUrl.value + recentPluginSettings.jellyseerrServerUrl.value, ); await jellyseerrApi.test().then((result) => { if (result.isValid && result.requiresPass) { @@ -257,23 +257,23 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({ throw new Error(t("login.invalid_username_or_password")); case 403: throw new Error( - t("login.user_does_not_have_permission_to_log_in") + t("login.user_does_not_have_permission_to_log_in"), ); case 408: throw new Error( - t("login.server_is_taking_too_long_to_respond_try_again_later") + t("login.server_is_taking_too_long_to_respond_try_again_later"), ); case 429: throw new Error( - t("login.server_received_too_many_requests_try_again_later") + t("login.server_received_too_many_requests_try_again_later"), ); case 500: throw new Error(t("login.there_is_a_server_error")); default: throw new Error( t( - "login.an_unexpected_error_occured_did_you_enter_the_correct_url" - ) + "login.an_unexpected_error_occured_did_you_enter_the_correct_url", + ), ); } } @@ -287,9 +287,12 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({ const logoutMutation = useMutation({ mutationFn: async () => { - api?.delete(`/Streamyfin/device/${deviceId}`) - .then(r => writeInfoLog("Deleted expo push token for device")) - .catch(e => writeErrorLog(`Failed to delete expo push token for device`)) + api + ?.delete(`/Streamyfin/device/${deviceId}`) + .then((r) => writeInfoLog("Deleted expo push token for device")) + .catch((e) => + writeErrorLog(`Failed to delete expo push token for device`), + ); storage.delete("token"); setUser(null); diff --git a/providers/JobQueueProvider.tsx b/providers/JobQueueProvider.tsx index 00358e48..232a5f02 100644 --- a/providers/JobQueueProvider.tsx +++ b/providers/JobQueueProvider.tsx @@ -1,5 +1,6 @@ -import React, { createContext } from "react"; import { useJobProcessor } from "@/utils/atoms/queue"; +import type React from "react"; +import { createContext } from "react"; const JobQueueContext = createContext(null); diff --git a/providers/PlaySettingsProvider.tsx b/providers/PlaySettingsProvider.tsx index ff80bb9e..fad502fb 100644 --- a/providers/PlaySettingsProvider.tsx +++ b/providers/PlaySettingsProvider.tsx @@ -1,14 +1,15 @@ -import { Bitrate } from "@/components/BitrateSelector"; +import type { Bitrate } from "@/components/BitrateSelector"; import { settingsAtom } from "@/utils/atoms/settings"; import { getStreamUrl } from "@/utils/jellyfin/media/getStreamUrl"; import native from "@/utils/profiles/native"; -import { +import type { BaseItemDto, MediaSourceInfo, } from "@jellyfin/sdk/lib/generated-client"; import { getSessionApi } from "@jellyfin/sdk/lib/utils/api"; import { useAtomValue } from "jotai"; -import React, { +import type React from "react"; +import { createContext, useCallback, useContext, @@ -32,7 +33,7 @@ type PlaySettingsContextType = { dataOrUpdater: | PlaybackType | null - | ((prev: PlaybackType | null) => PlaybackType | null) + | ((prev: PlaybackType | null) => PlaybackType | null), ) => Promise<{ url: string | null; sessionId: string | null } | null>; playUrl?: string | null; setPlayUrl: React.Dispatch>; @@ -41,7 +42,7 @@ type PlaySettingsContextType = { }; const PlaySettingsContext = createContext( - undefined + undefined, ); export const PlaySettingsProvider: React.FC<{ children: React.ReactNode }> = ({ @@ -65,7 +66,7 @@ export const PlaySettingsProvider: React.FC<{ children: React.ReactNode }> = ({ dataOrUpdater: | PlaybackType | null - | ((prev: PlaybackType | null) => PlaybackType | null) + | ((prev: PlaybackType | null) => PlaybackType | null), ): Promise<{ url: string | null; sessionId: string | null } | null> => { if (!api || !user || !settings) { _setPlaySettings(null); @@ -109,7 +110,7 @@ export const PlaySettingsProvider: React.FC<{ children: React.ReactNode }> = ({ return null; } }, - [api, user, settings, playSettings] + [api, user, settings, playSettings], ); // useEffect(() => { @@ -153,7 +154,7 @@ export const usePlaySettings = () => { const context = useContext(PlaySettingsContext); if (context === undefined) { throw new Error( - "usePlaySettings must be used within a PlaySettingsProvider" + "usePlaySettings must be used within a PlaySettingsProvider", ); } return context; diff --git a/providers/WebSocketProvider.tsx b/providers/WebSocketProvider.tsx index b61c1967..c5857e9a 100644 --- a/providers/WebSocketProvider.tsx +++ b/providers/WebSocketProvider.tsx @@ -1,19 +1,16 @@ +import { apiAtom, getOrSetDeviceId } from "@/providers/JellyfinProvider"; +import { getSessionApi } from "@jellyfin/sdk/lib/utils/api"; +import { useAtomValue } from "jotai"; import React, { createContext, useContext, useEffect, useState, - ReactNode, + type ReactNode, useMemo, useCallback, } from "react"; -import { AppState, AppStateStatus } from "react-native"; -import { useAtomValue } from "jotai"; -import { - apiAtom, - getOrSetDeviceId, -} from "@/providers/JellyfinProvider"; -import { getSessionApi } from "@jellyfin/sdk/lib/utils/api"; +import { AppState, type AppStateStatus } from "react-native"; interface WebSocketProviderProps { children: ReactNode; @@ -121,7 +118,7 @@ export const WebSocketProvider = ({ children }: WebSocketProviderProps) => { const subscription = AppState.addEventListener( "change", - handleAppStateChange + handleAppStateChange, ); return () => { @@ -141,7 +138,7 @@ export const useWebSocketContext = (): WebSocketContextType => { const context = useContext(WebSocketContext); if (!context) { throw new Error( - "useWebSocketContext must be used within a WebSocketProvider" + "useWebSocketContext must be used within a WebSocketProvider", ); } return context; diff --git a/scripts/symlink-native-dirs.js b/scripts/symlink-native-dirs.js index d7819611..9b5c8133 100644 --- a/scripts/symlink-native-dirs.js +++ b/scripts/symlink-native-dirs.js @@ -25,22 +25,22 @@ const paths = new Map([ if (isTV) { stdout = execSync( - `mkdir -p ${paths.get("tvos")}; ln -nsf ${paths.get("tvos")} ios` + `mkdir -p ${paths.get("tvos")}; ln -nsf ${paths.get("tvos")} ios`, ); console.log(stdout.toString()); stdout = execSync( `mkdir -p ${paths.get("androidtv")}; ln -nsf ${paths.get( - "androidtv" - )} android` + "androidtv", + )} android`, ); console.log(stdout.toString()); } else { stdout = execSync( - `mkdir -p ${paths.get("ios")}; ln -nsf ${paths.get("ios")} ios` + `mkdir -p ${paths.get("ios")}; ln -nsf ${paths.get("ios")} ios`, ); console.log(stdout.toString()); stdout = execSync( - `mkdir -p ${paths.get("android")}; ln -nsf ${paths.get("android")} android` + `mkdir -p ${paths.get("android")}; ln -nsf ${paths.get("android")} android`, ); console.log(stdout.toString()); } diff --git a/translations/de.json b/translations/de.json index d48e134e..993c2176 100644 --- a/translations/de.json +++ b/translations/de.json @@ -1,473 +1,473 @@ -{ - "login": { - "username_required": "Benutzername ist erforderlich", - "error_title": "Fehler", - "login_title": "Anmelden", - "login_to_title": "Anmelden bei", - "username_placeholder": "Benutzername", - "password_placeholder": "Passwort", - "login_button": "Anmelden", - "quick_connect": "Schnellverbindung", - "enter_code_to_login": "Gib den Code {{code}} ein, um dich anzumelden", - "failed_to_initiate_quick_connect": "Fehler beim Initiieren der Schnellverbindung", - "got_it": "Verstanden", - "connection_failed": "Verbindung fehlgeschlagen", - "could_not_connect_to_server": "Verbindung zum Server fehlgeschlagen. Bitte überprüf die URL und deine Netzwerkverbindung.", - "an_unexpected_error_occured": "Ein unerwarteter Fehler ist aufgetreten", - "change_server": "Server wechseln", - "invalid_username_or_password": "Ungültiger Benutzername oder Passwort", - "user_does_not_have_permission_to_log_in": "Benutzer hat keine Berechtigung, um sich anzumelden", - "server_is_taking_too_long_to_respond_try_again_later": "Der Server benötigt zu lange, um zu antworten. Bitte versuch es später erneut.", - "server_received_too_many_requests_try_again_later": "Der Server hat zu viele Anfragen erhalten. Bitte versuch es später erneut.", - "there_is_a_server_error": "Es gibt einen Serverfehler", - "an_unexpected_error_occured_did_you_enter_the_correct_url": "Ein unerwarteter Fehler ist aufgetreten. Hast du die Server-URL korrekt eingegeben?" - }, - "server": { - "enter_url_to_jellyfin_server": "Gib die URL zu deinem Jellyfin-Server ein", - "server_url_placeholder": "http(s)://dein-server.de", - "connect_button": "Verbinden", - "previous_servers": "Vorherige Server", - "clear_button": "Löschen", - "search_for_local_servers": "Nach lokalen Servern suchen", - "searching": "Suche...", - "servers": "Server" - }, - "home": { - "no_internet": "Kein Internet", - "no_items": "Keine Elemente", - "no_internet_message": "Keine Sorge, du kannst immer noch heruntergeladene Inhalte ansehen.", - "go_to_downloads": "Gehe zu den Downloads", - "oops": "Ups!", - "error_message": "Etwas ist schiefgelaufen.\nBitte melde dich ab und wieder an.", - "continue_watching": "Weiterschauen", - "next_up": "Als nächstes", - "recently_added_in": "Kürzlich hinzugefügt in {{libraryName}}", - "suggested_movies": "Empfohlene Filme", - "suggested_episodes": "Empfohlene Episoden", - "intro": { - "welcome_to_streamyfin": "Willkommen bei Streamyfin", - "a_free_and_open_source_client_for_jellyfin": "Ein kostenloser und Open-Source-Client für Jellyfin.", - "features_title": "Features", - "features_description": "Streamyfin hat viele Features und integriert sich mit einer Vielzahl von Software, die du im Einstellungsmenü findest. Dazu gehören:", - "jellyseerr_feature_description": "Verbinde dich mit deiner Jellyseerr-Instanz und frage Filme direkt in der App an.", - "downloads_feature_title": "Downloads", - "downloads_feature_description": "Lade Filme und Serien herunter, um sie offline anzusehen. Nutze entweder die Standardmethode oder installiere den optimierten Server, um Dateien im Hintergrund herunterzuladen.", - "chromecast_feature_description": "Übertrage Filme und Serien auf deine Chromecast-Geräte.", - "centralised_settings_plugin_title": "Zentralisiertes Einstellungs-Plugin", - "centralised_settings_plugin_description": "Konfiguriere Einstellungen an einem zentralen Ort auf deinem Jellyfin-Server. Alle Client-Einstellungen für alle Benutzer werden automatisch synchronisiert.", - "done_button": "Fertig", - "go_to_settings_button": "Gehe zu den Einstellungen", - "read_more": "Mehr Erfahren" - }, - "settings": { - "settings_title": "Einstellungen", - "log_out_button": "Abmelden", - "user_info": { - "user_info_title": "Benutzerinformationen", - "user": "Benutzer", - "server": "Server", - "token": "Token", - "app_version": "App-Version" - }, - "quick_connect": { - "quick_connect_title": "Schnellverbindung", - "authorize_button": "Schnellverbindung autorisieren", - "enter_the_quick_connect_code": "Gib den Schnellverbindungscode ein...", - "success": "Erfolg", - "quick_connect_autorized": "Schnellverbindung autorisiert", - "error": "Fehler", - "invalid_code": "Ungültiger Code", - "authorize": "Autorisieren" - }, - "media_controls": { - "media_controls_title": "Mediensteuerung", - "forward_skip_length": "Vorspulzeit", - "rewind_length": "Rückspulzeit", - "seconds_unit": "s" - }, - "audio": { - "audio_title": "Audio", - "set_audio_track": "Audiospur aus dem vorherigen Element festlegen", - "audio_language": "Audio-Sprache", - "audio_hint": "Wähl die Standardsprache für Audio aus.", - "none": "Keine", - "language": "Sprache" - }, - "subtitles": { - "subtitle_title": "Untertitel", - "subtitle_language": "Untertitel-Sprache", - "subtitle_mode": "Untertitel-Modus", - "set_subtitle_track": "Untertitel-Spur aus dem vorherigen Element festlegen", - "subtitle_size": "Untertitel-Größe", - "subtitle_hint": "Konfigurier die Untertitel-Präferenzen.", - "none": "Keine", - "language": "Sprache", - "loading": "Lädt", - "modes": { - "Default": "Standard", - "Smart": "Smart", - "Always": "Immer", - "None": "Keine", - "OnlyForced": "Nur erzwungen" - } - }, - "other": { - "other_title": "Sonstiges", - "follow_device_orientation": "Automatische Drehung", - "video_orientation": "Videoausrichtung", - "orientation": "Ausrichtung", - "orientations": { - "DEFAULT": "Standard", - "ALL": "Alle", - "PORTRAIT": "Hochformat", - "PORTRAIT_UP": "Hochformat oben", - "PORTRAIT_DOWN": "Hochformat unten", - "LANDSCAPE": "Querformat", - "LANDSCAPE_LEFT": "Querformat links", - "LANDSCAPE_RIGHT": "Querformat rechts", - "OTHER": "Andere", - "UNKNOWN": "Unbekannt" - }, - "safe_area_in_controls": "Sicherer Bereich in den Steuerungen", - "video_player": "Video player", - "video_players": { - "VLC_3": "VLC 3", - "VLC_4": "VLC 4 (Experimental + PiP)" - }, - "show_custom_menu_links": "Benutzerdefinierte Menülinks anzeigen", - "hide_libraries": "Bibliotheken ausblenden", - "select_liraries_you_want_to_hide": "Wähl die Bibliotheken aus, die du im Bibliothekstab und auf der Startseite ausblenden möchtest.", - "disable_haptic_feedback": "Haptisches Feedback deaktivieren", - "default_quality": "Standardqualität" - }, - "downloads": { - "downloads_title": "Downloads", - "download_method": "Download-Methode", - "remux_max_download": "Maximaler Remux-Download", - "auto_download": "Automatischer Download", - "optimized_versions_server": "Optimierter Versions-Server", - "save_button": "Speichern", - "optimized_server": "Optimierter Server", - "optimized": "Optimiert", - "default": "Standard", - "optimized_version_hint": "Gib die URL für den optimierten Server ein. Die URL sollte http oder https enthalten und optional den Port.", - "read_more_about_optimized_server": "Mehr über den optimierten Server lesen.", - "url": "URL", - "server_url_placeholder": "http(s)://domain.org:port" - }, - "plugins": { - "plugins_title": "Plugins", - "jellyseerr": { - "jellyseerr_warning": "Diese integration ist in einer frühen Entwicklungsphase. Erwarte Veränderungen.", - "server_url": "Server URL", - "server_url_hint": "Beispiel: http(s)://your-host.url\n(Portnummer hinzufügen, falls erforderlich)", - "server_url_placeholder": "Jellyseerr URL...", - "password": "Passwort", - "password_placeholder": "Passwort für Jellyfin Benutzer {{username}} eingeben", - "save_button": "Speichern", - "clear_button": "Löschen", - "login_button": "Anmelden", - "total_media_requests": "Gesamtanfragen", - "movie_quota_limit": "Film-Anfragelimit", - "movie_quota_days": "Film-Anfragetage", - "tv_quota_limit": "TV-Anfragelimit", - "tv_quota_days": "TV-Anfragetage", - "reset_jellyseerr_config_button": "Setze Jellyseerr-Konfiguration zurück", - "unlimited": "Unlimitiert", - "plus_n_more": "+{{n}} more", - "order_by": { - "DEFAULT": "Default", - "VOTE_COUNT_AND_AVERAGE": "Vote count and average", - "POPULARITY": "Popularity" - } - }, - "marlin_search": { - "enable_marlin_search": "Aktiviere Marlin Search", - "url": "URL", - "server_url_placeholder": "http(s)://domain.org:port", - "marlin_search_hint": "Gib die URL für den Marlin Server ein. Die URL sollte http oder https enthalten und optional den Port.", - "read_more_about_marlin": "Erfahre mehr über Marlin.", - "save_button": "Speichern", - "toasts": { - "saved": "Gespeichert" - } - } - }, - "storage": { - "storage_title": "Speicher", - "app_usage": "App {{usedSpace}}%", - "device_usage": "Gerät {{availableSpace}}%", - "size_used": "{{used}} von {{total}} benutzt", - "delete_all_downloaded_files": "Alle Downloads löschen" - }, - "intro": { - "show_intro": "Show intro", - "reset_intro": "Reset intro" - }, - "logs": { - "logs_title": "Logs", - "no_logs_available": "Keine Logs verfügbar", - "delete_all_logs": "Alle Logs löschen" - }, - "languages": { - "title": "Sprachen", - "app_language": "App-Sprache", - "app_language_description": "Wähle die Sprache für die App aus.", - "system": "System" - }, - "toasts": { - "error_deleting_files": "Fehler beim Löschen von Dateien", - "background_downloads_enabled": "Hintergrunddownloads aktiviert", - "background_downloads_disabled": "Hintergrunddownloads deaktiviert", - "connected": "Verbunden", - "could_not_connect": "Konnte keine Verbindung herstellen", - "invalid_url": "Ungültige URL" - } - }, - "downloads": { - "downloads_title": "Downloads", - "tvseries": "TV-Serien", - "movies": "Filme", - "queue": "Warteschlange", - "queue_hint": "Warteschlange und aktive Downloads gehen verloren bei App-Neustart", - "no_items_in_queue": "Keine Elemente in der Warteschlange", - "no_downloaded_items": "Keine heruntergeladenen Elemente", - "delete_all_movies_button": "Alle Filme löschen", - "delete_all_tvseries_button": "Alle TV-Serien löschen", - "delete_all_button": "Alles löschen", - "active_download": "Aktiver Download", - "no_active_downloads": "Keine aktiven Downloads", - "active_downloads": "Aktive Downloads", - "new_app_version_requires_re_download": "Die neue App-Version erfordert das erneute Herunterladen.", - "new_app_version_requires_re_download_description": "Die neue App-Version erfordert das erneute Herunterladen von Filmen und Serien. Bitte lösche alle heruntergeladenen Elemente und starte den Download erneut.", - "back": "Zurück", - "delete": "Löschen", - "something_went_wrong": "Etwas ist schiefgelaufen", - "could_not_get_stream_url_from_jellyfin": "Konnte keine Stream-URL von Jellyfin erhalten", - "eta": "ETA {{eta}}", - "methods": "Methoden", - "toasts": { - "you_are_not_allowed_to_download_files": "Du hast keine Berechtigung, Dateien herunterzuladen", - "deleted_all_movies_successfully": "Alle Filme erfolgreich gelöscht!", - "failed_to_delete_all_movies": "Fehler beim Löschen aller Filme", - "deleted_all_tvseries_successfully": "Alle TV-Serien erfolgreich gelöscht!", - "failed_to_delete_all_tvseries": "Fehler beim Löschen aller TV-Serien", - "download_cancelled": "Download abgebrochen", - "could_not_cancel_download": "Download konnte nicht abgebrochen werden", - "download_completed": "Download abgeschlossen", - "download_started_for": "Download für {{item}} gestartet", - "item_is_ready_to_be_downloaded": "{{item}} ist bereit zum Herunterladen", - "download_stated_for_item": "Download für {{item}} gestartet", - "download_failed_for_item": "Download für {{item}} fehlgeschlagen - {{error}}", - "download_completed_for_item": "Download für {{item}} ", - "queued_item_for_optimization": "{{item}} für Optimierung in die Warteschlange gestellt", - "failed_to_start_download_for_item": "Download konnte für {{item}} nicht gestartet werden: {{message}}", - "server_responded_with_status_code": "Server hat mit Status {{statusCode}} geantwortet", - "no_response_received_from_server": "Keine Antwort vom Server erhalten", - "error_setting_up_the_request": "Fehler beim Einrichten der Anfrage", - "failed_to_start_download_for_item_unexpected_error": "Fehler beim Starten des Downloads für {{item}}: Unerwarteter Fehler", - "all_files_folders_and_jobs_deleted_successfully": "Alle Dateien, Ordner und Jobs erfolgreich gelöscht", - "an_error_occured_while_deleting_files_and_jobs": "Ein Fehler ist beim Löschen von Dateien und Jobs aufgetreten", - "go_to_downloads": "Gehe zu den Downloads" - } - } - }, - "search": { - "search_here": "Hier Suchen...", - "search": "Suche...", - "x_items": "{{count}} Elemente", - "library": "Bibliothek", - "discover": "Entdecken", - "no_results": "Keine Ergebnisse", - "no_results_found_for": "Keine Ergebnisse gefunden für", - "movies": "Filme", - "series": "Serien", - "episodes": "Episoden", - "collections": "Sammlungen", - "actors": "Schauspieler", - "request_movies": "Film anfragen", - "request_series": "Serie anfragen", - "recently_added": "Kürzlich hinzugefügt", - "recent_requests": "Kürzlich angefragt", - "plex_watchlist": "Plex Watchlist", - "trending": "In den Trends", - "popular_movies": "Beliebte Filme", - "movie_genres": "Film-Genres", - "upcoming_movies": "Kommende Filme", - "studios": "Studios", - "popular_tv": "Beliebte TV-Serien", - "tv_genres": "TV-Serien-Genres", - "upcoming_tv": "Kommende TV-Serien", - "networks": "Netzwerke", - "tmdb_movie_keyword": "TMDB Film-Schlüsselwort", - "tmdb_movie_genre": "TMDB Film-Genre", - "tmdb_tv_keyword": "TMDB TV-Serien-Schlüsselwort", - "tmdb_tv_genre": "TMDB TV-Serien-Genre", - "tmdb_search": "TMDB Suche", - "tmdb_studio": "TMDB Studio", - "tmdb_network": "TMDB Netzwerk", - "tmdb_movie_streaming_services": "TMDB Film-Streaming-Dienste", - "tmdb_tv_streaming_services": "TMDB TV-Serien-Streaming-Dienste" - }, - "library": { - "no_items_found": "Keine Elemente gefunden", - "no_results": "Keine Ergebnisse", - "no_libraries_found": "Keine Bibliotheken gefunden", - "item_types": { - "movies": "Filme", - "series": "Serien", - "boxsets": "Boxsets", - "items": "Elemente" - }, - "options": { - "display": "Display", - "row": "Reihe", - "list": "Liste", - "image_style": "Bildstil", - "poster": "Poster", - "cover": "Cover", - "show_titles": "Titel anzeigen", - "show_stats": "Statistiken anzeigen" - }, - "filters": { - "genres": "Genres", - "years": "Jahre", - "sort_by": "Sortieren nach", - "sort_order": "Sortierreihenfolge", - "asc": "Ascending", - "desc": "Descending", - "tags": "Tags" - } - }, - "favorites": { - "series": "Serien", - "movies": "Filme", - "episodes": "Episoden", - "videos": "Videos", - "boxsets": "Boxsets", - "playlists": "Playlists", - "noDataTitle": "Noch keine Favoriten", - "noData": "Markiere Elemente als Favoriten, damit sie hier für einen schnellen Zugriff angezeigt werden." - }, - "custom_links": { - "no_links": "Keine Links" - }, - "player": { - "error": "Fehler", - "failed_to_get_stream_url": "Fehler beim Abrufen der Stream-URL", - "an_error_occured_while_playing_the_video": "Ein Fehler ist beim Abspielen des Videos aufgetreten. Überprüf die Logs in den Einstellungen.", - "client_error": "Client-Fehler", - "could_not_create_stream_for_chromecast": "Konnte keinen Stream für Chromecast erstellen", - "message_from_server": "Nachricht vom Server: {{message}}", - "video_has_finished_playing": "Video wurde fertig abgespielt!", - "no_video_source": "Keine Videoquelle...", - "next_episode": "Nächste Episode", - "refresh_tracks": "Spuren aktualisieren", - "subtitle_tracks": "Untertitel-Spuren:", - "audio_tracks": "Audiospuren:", - "playback_state": "Wiedergabestatus:", - "no_data_available": "Keine Daten verfügbar", - "index": "Index:" - }, - "item_card": { - "next_up": "Als Nächstes", - "no_items_to_display": "Keine Elemente zum Anzeigen", - "cast_and_crew": "Besetzung und Crew", - "series": "Serien", - "seasons": "Staffeln", - "season": "Staffel", - "no_episodes_for_this_season": "Keine Episoden für diese Staffel", - "overview": "Überblick", - "more_with": "Mehr mit {{name}}", - "similar_items": "Ähnliche Elemente", - "no_similar_items_found": "Keine ähnlichen Elemente gefunden", - "video": "Video", - "more_details": "Mehr Details", - "quality": "Qualität", - "audio": "Audio", - "subtitles": "Untertitel", - "show_more": "Mehr anzeigen", - "show_less": "Weniger anzeigen", - "appeared_in": "Erschienen in", - "could_not_load_item": "Konnte Element nicht laden", - "none": "Keine", - "download": { - "download_season": "Staffel herunterladen", - "download_series": "Serie herunterladen", - "download_episode": "Episode herunterladen", - "download_movie": "Film herunterladen", - "download_x_item": "{{item_count}} Elemente herunterladen", - "download_button": "Herunterladen", - "using_optimized_server": "Verwende optimierten Server", - "using_default_method": "Verwende Standardmethode" - } - }, - "live_tv": { - "next": "Nächster", - "previous": "Vorheriger", - "live_tv": "Live TV", - "coming_soon": "Demnächst", - "on_now": "Jetzt", - "shows": "Shows", - "movies": "Filme", - "sports": "Sport", - "for_kids": "Für Kinder", - "news": "Nachrichten" - }, - "jellyseerr": { - "confirm": "Bestätigen", - "cancel": "Abbrechen", - "yes": "Ja", - "whats_wrong": "Hast du Probleme?", - "issue_type": "Fehlerart", - "select_an_issue": "Wähle einen Fehlerart aus", - "types": "Arten", - "describe_the_issue": "(optional) Beschreibe das Problem", - "submit_button": "Absenden", - "report_issue_button": "Fehler melden", - "request_button": "Anfragen", - "are_you_sure_you_want_to_request_all_seasons": "Bist du sicher, dass du alle Staffeln anfragen möchtest?", - "failed_to_login": "Fehler beim Anmelden", - "cast": "Besetzung", - "details": "Details", - "status": "Status", - "original_title": "Original Titel", - "series_type": "Serien Typ", - "release_dates": "Veröffentlichungsdaten", - "first_air_date": "Erstausstrahlungsdatum", - "next_air_date": "Nächstes Ausstrahlungsdatum", - "revenue": "Einnahmen", - "budget": "Budget", - "original_language": "Originalsprache", - "production_country": "Produktionsland", - "studios": "Studios", - "network": "Netzwerk", - "currently_streaming_on": "Derzeit im Streaming auf", - "advanced": "Erweitert", - "request_as": "Anfragen als", - "tags": "Tags", - "quality_profile": "Qualitätsprofil", - "root_folder": "Root-Ordner", - "season_all": "Season (all)", - "season_number": "Staffel {{season_number}}", - "number_episodes": "{{episode_number}} Episodes", - "born": "Geboren", - "appearances": "Auftritte", - "toasts": { - "jellyseer_does_not_meet_requirements": "Jellyseerr Server erfüllt nicht die Anforderungsversion. Bitte aktualisiere deinen Jellyseerr Server auf mindestens 2.0.0", - "jellyseerr_test_failed": "Jellyseerr-Test fehlgeschlagen. Bitte versuche es erneut.", - "failed_to_test_jellyseerr_server_url": "Fehler beim Testen der Jellyseerr-Server-URL", - "issue_submitted": "Problem eingereicht!", - "requested_item": "{{item}} angefragt!", - "you_dont_have_permission_to_request": "Du hast keine Berechtigung Anfragen zu stellen", - "something_went_wrong_requesting_media": "Etwas ist schiefgelaufen beim Anfragen von Medien" - } - }, - "tabs": { - "home": "Startseite", - "search": "Suche", - "library": "Bibliothek", - "custom_links": "Benutzerdefinierte Links", - "favorites": "Favoriten" - } -} +{ + "login": { + "username_required": "Benutzername ist erforderlich", + "error_title": "Fehler", + "login_title": "Anmelden", + "login_to_title": "Anmelden bei", + "username_placeholder": "Benutzername", + "password_placeholder": "Passwort", + "login_button": "Anmelden", + "quick_connect": "Schnellverbindung", + "enter_code_to_login": "Gib den Code {{code}} ein, um dich anzumelden", + "failed_to_initiate_quick_connect": "Fehler beim Initiieren der Schnellverbindung", + "got_it": "Verstanden", + "connection_failed": "Verbindung fehlgeschlagen", + "could_not_connect_to_server": "Verbindung zum Server fehlgeschlagen. Bitte überprüf die URL und deine Netzwerkverbindung.", + "an_unexpected_error_occured": "Ein unerwarteter Fehler ist aufgetreten", + "change_server": "Server wechseln", + "invalid_username_or_password": "Ungültiger Benutzername oder Passwort", + "user_does_not_have_permission_to_log_in": "Benutzer hat keine Berechtigung, um sich anzumelden", + "server_is_taking_too_long_to_respond_try_again_later": "Der Server benötigt zu lange, um zu antworten. Bitte versuch es später erneut.", + "server_received_too_many_requests_try_again_later": "Der Server hat zu viele Anfragen erhalten. Bitte versuch es später erneut.", + "there_is_a_server_error": "Es gibt einen Serverfehler", + "an_unexpected_error_occured_did_you_enter_the_correct_url": "Ein unerwarteter Fehler ist aufgetreten. Hast du die Server-URL korrekt eingegeben?" + }, + "server": { + "enter_url_to_jellyfin_server": "Gib die URL zu deinem Jellyfin-Server ein", + "server_url_placeholder": "http(s)://dein-server.de", + "connect_button": "Verbinden", + "previous_servers": "Vorherige Server", + "clear_button": "Löschen", + "search_for_local_servers": "Nach lokalen Servern suchen", + "searching": "Suche...", + "servers": "Server" + }, + "home": { + "no_internet": "Kein Internet", + "no_items": "Keine Elemente", + "no_internet_message": "Keine Sorge, du kannst immer noch heruntergeladene Inhalte ansehen.", + "go_to_downloads": "Gehe zu den Downloads", + "oops": "Ups!", + "error_message": "Etwas ist schiefgelaufen.\nBitte melde dich ab und wieder an.", + "continue_watching": "Weiterschauen", + "next_up": "Als nächstes", + "recently_added_in": "Kürzlich hinzugefügt in {{libraryName}}", + "suggested_movies": "Empfohlene Filme", + "suggested_episodes": "Empfohlene Episoden", + "intro": { + "welcome_to_streamyfin": "Willkommen bei Streamyfin", + "a_free_and_open_source_client_for_jellyfin": "Ein kostenloser und Open-Source-Client für Jellyfin.", + "features_title": "Features", + "features_description": "Streamyfin hat viele Features und integriert sich mit einer Vielzahl von Software, die du im Einstellungsmenü findest. Dazu gehören:", + "jellyseerr_feature_description": "Verbinde dich mit deiner Jellyseerr-Instanz und frage Filme direkt in der App an.", + "downloads_feature_title": "Downloads", + "downloads_feature_description": "Lade Filme und Serien herunter, um sie offline anzusehen. Nutze entweder die Standardmethode oder installiere den optimierten Server, um Dateien im Hintergrund herunterzuladen.", + "chromecast_feature_description": "Übertrage Filme und Serien auf deine Chromecast-Geräte.", + "centralised_settings_plugin_title": "Zentralisiertes Einstellungs-Plugin", + "centralised_settings_plugin_description": "Konfiguriere Einstellungen an einem zentralen Ort auf deinem Jellyfin-Server. Alle Client-Einstellungen für alle Benutzer werden automatisch synchronisiert.", + "done_button": "Fertig", + "go_to_settings_button": "Gehe zu den Einstellungen", + "read_more": "Mehr Erfahren" + }, + "settings": { + "settings_title": "Einstellungen", + "log_out_button": "Abmelden", + "user_info": { + "user_info_title": "Benutzerinformationen", + "user": "Benutzer", + "server": "Server", + "token": "Token", + "app_version": "App-Version" + }, + "quick_connect": { + "quick_connect_title": "Schnellverbindung", + "authorize_button": "Schnellverbindung autorisieren", + "enter_the_quick_connect_code": "Gib den Schnellverbindungscode ein...", + "success": "Erfolg", + "quick_connect_autorized": "Schnellverbindung autorisiert", + "error": "Fehler", + "invalid_code": "Ungültiger Code", + "authorize": "Autorisieren" + }, + "media_controls": { + "media_controls_title": "Mediensteuerung", + "forward_skip_length": "Vorspulzeit", + "rewind_length": "Rückspulzeit", + "seconds_unit": "s" + }, + "audio": { + "audio_title": "Audio", + "set_audio_track": "Audiospur aus dem vorherigen Element festlegen", + "audio_language": "Audio-Sprache", + "audio_hint": "Wähl die Standardsprache für Audio aus.", + "none": "Keine", + "language": "Sprache" + }, + "subtitles": { + "subtitle_title": "Untertitel", + "subtitle_language": "Untertitel-Sprache", + "subtitle_mode": "Untertitel-Modus", + "set_subtitle_track": "Untertitel-Spur aus dem vorherigen Element festlegen", + "subtitle_size": "Untertitel-Größe", + "subtitle_hint": "Konfigurier die Untertitel-Präferenzen.", + "none": "Keine", + "language": "Sprache", + "loading": "Lädt", + "modes": { + "Default": "Standard", + "Smart": "Smart", + "Always": "Immer", + "None": "Keine", + "OnlyForced": "Nur erzwungen" + } + }, + "other": { + "other_title": "Sonstiges", + "follow_device_orientation": "Automatische Drehung", + "video_orientation": "Videoausrichtung", + "orientation": "Ausrichtung", + "orientations": { + "DEFAULT": "Standard", + "ALL": "Alle", + "PORTRAIT": "Hochformat", + "PORTRAIT_UP": "Hochformat oben", + "PORTRAIT_DOWN": "Hochformat unten", + "LANDSCAPE": "Querformat", + "LANDSCAPE_LEFT": "Querformat links", + "LANDSCAPE_RIGHT": "Querformat rechts", + "OTHER": "Andere", + "UNKNOWN": "Unbekannt" + }, + "safe_area_in_controls": "Sicherer Bereich in den Steuerungen", + "video_player": "Video player", + "video_players": { + "VLC_3": "VLC 3", + "VLC_4": "VLC 4 (Experimental + PiP)" + }, + "show_custom_menu_links": "Benutzerdefinierte Menülinks anzeigen", + "hide_libraries": "Bibliotheken ausblenden", + "select_liraries_you_want_to_hide": "Wähl die Bibliotheken aus, die du im Bibliothekstab und auf der Startseite ausblenden möchtest.", + "disable_haptic_feedback": "Haptisches Feedback deaktivieren", + "default_quality": "Standardqualität" + }, + "downloads": { + "downloads_title": "Downloads", + "download_method": "Download-Methode", + "remux_max_download": "Maximaler Remux-Download", + "auto_download": "Automatischer Download", + "optimized_versions_server": "Optimierter Versions-Server", + "save_button": "Speichern", + "optimized_server": "Optimierter Server", + "optimized": "Optimiert", + "default": "Standard", + "optimized_version_hint": "Gib die URL für den optimierten Server ein. Die URL sollte http oder https enthalten und optional den Port.", + "read_more_about_optimized_server": "Mehr über den optimierten Server lesen.", + "url": "URL", + "server_url_placeholder": "http(s)://domain.org:port" + }, + "plugins": { + "plugins_title": "Plugins", + "jellyseerr": { + "jellyseerr_warning": "Diese integration ist in einer frühen Entwicklungsphase. Erwarte Veränderungen.", + "server_url": "Server URL", + "server_url_hint": "Beispiel: http(s)://your-host.url\n(Portnummer hinzufügen, falls erforderlich)", + "server_url_placeholder": "Jellyseerr URL...", + "password": "Passwort", + "password_placeholder": "Passwort für Jellyfin Benutzer {{username}} eingeben", + "save_button": "Speichern", + "clear_button": "Löschen", + "login_button": "Anmelden", + "total_media_requests": "Gesamtanfragen", + "movie_quota_limit": "Film-Anfragelimit", + "movie_quota_days": "Film-Anfragetage", + "tv_quota_limit": "TV-Anfragelimit", + "tv_quota_days": "TV-Anfragetage", + "reset_jellyseerr_config_button": "Setze Jellyseerr-Konfiguration zurück", + "unlimited": "Unlimitiert", + "plus_n_more": "+{{n}} more", + "order_by": { + "DEFAULT": "Default", + "VOTE_COUNT_AND_AVERAGE": "Vote count and average", + "POPULARITY": "Popularity" + } + }, + "marlin_search": { + "enable_marlin_search": "Aktiviere Marlin Search", + "url": "URL", + "server_url_placeholder": "http(s)://domain.org:port", + "marlin_search_hint": "Gib die URL für den Marlin Server ein. Die URL sollte http oder https enthalten und optional den Port.", + "read_more_about_marlin": "Erfahre mehr über Marlin.", + "save_button": "Speichern", + "toasts": { + "saved": "Gespeichert" + } + } + }, + "storage": { + "storage_title": "Speicher", + "app_usage": "App {{usedSpace}}%", + "device_usage": "Gerät {{availableSpace}}%", + "size_used": "{{used}} von {{total}} benutzt", + "delete_all_downloaded_files": "Alle Downloads löschen" + }, + "intro": { + "show_intro": "Show intro", + "reset_intro": "Reset intro" + }, + "logs": { + "logs_title": "Logs", + "no_logs_available": "Keine Logs verfügbar", + "delete_all_logs": "Alle Logs löschen" + }, + "languages": { + "title": "Sprachen", + "app_language": "App-Sprache", + "app_language_description": "Wähle die Sprache für die App aus.", + "system": "System" + }, + "toasts": { + "error_deleting_files": "Fehler beim Löschen von Dateien", + "background_downloads_enabled": "Hintergrunddownloads aktiviert", + "background_downloads_disabled": "Hintergrunddownloads deaktiviert", + "connected": "Verbunden", + "could_not_connect": "Konnte keine Verbindung herstellen", + "invalid_url": "Ungültige URL" + } + }, + "downloads": { + "downloads_title": "Downloads", + "tvseries": "TV-Serien", + "movies": "Filme", + "queue": "Warteschlange", + "queue_hint": "Warteschlange und aktive Downloads gehen verloren bei App-Neustart", + "no_items_in_queue": "Keine Elemente in der Warteschlange", + "no_downloaded_items": "Keine heruntergeladenen Elemente", + "delete_all_movies_button": "Alle Filme löschen", + "delete_all_tvseries_button": "Alle TV-Serien löschen", + "delete_all_button": "Alles löschen", + "active_download": "Aktiver Download", + "no_active_downloads": "Keine aktiven Downloads", + "active_downloads": "Aktive Downloads", + "new_app_version_requires_re_download": "Die neue App-Version erfordert das erneute Herunterladen.", + "new_app_version_requires_re_download_description": "Die neue App-Version erfordert das erneute Herunterladen von Filmen und Serien. Bitte lösche alle heruntergeladenen Elemente und starte den Download erneut.", + "back": "Zurück", + "delete": "Löschen", + "something_went_wrong": "Etwas ist schiefgelaufen", + "could_not_get_stream_url_from_jellyfin": "Konnte keine Stream-URL von Jellyfin erhalten", + "eta": "ETA {{eta}}", + "methods": "Methoden", + "toasts": { + "you_are_not_allowed_to_download_files": "Du hast keine Berechtigung, Dateien herunterzuladen", + "deleted_all_movies_successfully": "Alle Filme erfolgreich gelöscht!", + "failed_to_delete_all_movies": "Fehler beim Löschen aller Filme", + "deleted_all_tvseries_successfully": "Alle TV-Serien erfolgreich gelöscht!", + "failed_to_delete_all_tvseries": "Fehler beim Löschen aller TV-Serien", + "download_cancelled": "Download abgebrochen", + "could_not_cancel_download": "Download konnte nicht abgebrochen werden", + "download_completed": "Download abgeschlossen", + "download_started_for": "Download für {{item}} gestartet", + "item_is_ready_to_be_downloaded": "{{item}} ist bereit zum Herunterladen", + "download_stated_for_item": "Download für {{item}} gestartet", + "download_failed_for_item": "Download für {{item}} fehlgeschlagen - {{error}}", + "download_completed_for_item": "Download für {{item}} ", + "queued_item_for_optimization": "{{item}} für Optimierung in die Warteschlange gestellt", + "failed_to_start_download_for_item": "Download konnte für {{item}} nicht gestartet werden: {{message}}", + "server_responded_with_status_code": "Server hat mit Status {{statusCode}} geantwortet", + "no_response_received_from_server": "Keine Antwort vom Server erhalten", + "error_setting_up_the_request": "Fehler beim Einrichten der Anfrage", + "failed_to_start_download_for_item_unexpected_error": "Fehler beim Starten des Downloads für {{item}}: Unerwarteter Fehler", + "all_files_folders_and_jobs_deleted_successfully": "Alle Dateien, Ordner und Jobs erfolgreich gelöscht", + "an_error_occured_while_deleting_files_and_jobs": "Ein Fehler ist beim Löschen von Dateien und Jobs aufgetreten", + "go_to_downloads": "Gehe zu den Downloads" + } + } + }, + "search": { + "search_here": "Hier Suchen...", + "search": "Suche...", + "x_items": "{{count}} Elemente", + "library": "Bibliothek", + "discover": "Entdecken", + "no_results": "Keine Ergebnisse", + "no_results_found_for": "Keine Ergebnisse gefunden für", + "movies": "Filme", + "series": "Serien", + "episodes": "Episoden", + "collections": "Sammlungen", + "actors": "Schauspieler", + "request_movies": "Film anfragen", + "request_series": "Serie anfragen", + "recently_added": "Kürzlich hinzugefügt", + "recent_requests": "Kürzlich angefragt", + "plex_watchlist": "Plex Watchlist", + "trending": "In den Trends", + "popular_movies": "Beliebte Filme", + "movie_genres": "Film-Genres", + "upcoming_movies": "Kommende Filme", + "studios": "Studios", + "popular_tv": "Beliebte TV-Serien", + "tv_genres": "TV-Serien-Genres", + "upcoming_tv": "Kommende TV-Serien", + "networks": "Netzwerke", + "tmdb_movie_keyword": "TMDB Film-Schlüsselwort", + "tmdb_movie_genre": "TMDB Film-Genre", + "tmdb_tv_keyword": "TMDB TV-Serien-Schlüsselwort", + "tmdb_tv_genre": "TMDB TV-Serien-Genre", + "tmdb_search": "TMDB Suche", + "tmdb_studio": "TMDB Studio", + "tmdb_network": "TMDB Netzwerk", + "tmdb_movie_streaming_services": "TMDB Film-Streaming-Dienste", + "tmdb_tv_streaming_services": "TMDB TV-Serien-Streaming-Dienste" + }, + "library": { + "no_items_found": "Keine Elemente gefunden", + "no_results": "Keine Ergebnisse", + "no_libraries_found": "Keine Bibliotheken gefunden", + "item_types": { + "movies": "Filme", + "series": "Serien", + "boxsets": "Boxsets", + "items": "Elemente" + }, + "options": { + "display": "Display", + "row": "Reihe", + "list": "Liste", + "image_style": "Bildstil", + "poster": "Poster", + "cover": "Cover", + "show_titles": "Titel anzeigen", + "show_stats": "Statistiken anzeigen" + }, + "filters": { + "genres": "Genres", + "years": "Jahre", + "sort_by": "Sortieren nach", + "sort_order": "Sortierreihenfolge", + "asc": "Ascending", + "desc": "Descending", + "tags": "Tags" + } + }, + "favorites": { + "series": "Serien", + "movies": "Filme", + "episodes": "Episoden", + "videos": "Videos", + "boxsets": "Boxsets", + "playlists": "Playlists", + "noDataTitle": "Noch keine Favoriten", + "noData": "Markiere Elemente als Favoriten, damit sie hier für einen schnellen Zugriff angezeigt werden." + }, + "custom_links": { + "no_links": "Keine Links" + }, + "player": { + "error": "Fehler", + "failed_to_get_stream_url": "Fehler beim Abrufen der Stream-URL", + "an_error_occured_while_playing_the_video": "Ein Fehler ist beim Abspielen des Videos aufgetreten. Überprüf die Logs in den Einstellungen.", + "client_error": "Client-Fehler", + "could_not_create_stream_for_chromecast": "Konnte keinen Stream für Chromecast erstellen", + "message_from_server": "Nachricht vom Server: {{message}}", + "video_has_finished_playing": "Video wurde fertig abgespielt!", + "no_video_source": "Keine Videoquelle...", + "next_episode": "Nächste Episode", + "refresh_tracks": "Spuren aktualisieren", + "subtitle_tracks": "Untertitel-Spuren:", + "audio_tracks": "Audiospuren:", + "playback_state": "Wiedergabestatus:", + "no_data_available": "Keine Daten verfügbar", + "index": "Index:" + }, + "item_card": { + "next_up": "Als Nächstes", + "no_items_to_display": "Keine Elemente zum Anzeigen", + "cast_and_crew": "Besetzung und Crew", + "series": "Serien", + "seasons": "Staffeln", + "season": "Staffel", + "no_episodes_for_this_season": "Keine Episoden für diese Staffel", + "overview": "Überblick", + "more_with": "Mehr mit {{name}}", + "similar_items": "Ähnliche Elemente", + "no_similar_items_found": "Keine ähnlichen Elemente gefunden", + "video": "Video", + "more_details": "Mehr Details", + "quality": "Qualität", + "audio": "Audio", + "subtitles": "Untertitel", + "show_more": "Mehr anzeigen", + "show_less": "Weniger anzeigen", + "appeared_in": "Erschienen in", + "could_not_load_item": "Konnte Element nicht laden", + "none": "Keine", + "download": { + "download_season": "Staffel herunterladen", + "download_series": "Serie herunterladen", + "download_episode": "Episode herunterladen", + "download_movie": "Film herunterladen", + "download_x_item": "{{item_count}} Elemente herunterladen", + "download_button": "Herunterladen", + "using_optimized_server": "Verwende optimierten Server", + "using_default_method": "Verwende Standardmethode" + } + }, + "live_tv": { + "next": "Nächster", + "previous": "Vorheriger", + "live_tv": "Live TV", + "coming_soon": "Demnächst", + "on_now": "Jetzt", + "shows": "Shows", + "movies": "Filme", + "sports": "Sport", + "for_kids": "Für Kinder", + "news": "Nachrichten" + }, + "jellyseerr": { + "confirm": "Bestätigen", + "cancel": "Abbrechen", + "yes": "Ja", + "whats_wrong": "Hast du Probleme?", + "issue_type": "Fehlerart", + "select_an_issue": "Wähle einen Fehlerart aus", + "types": "Arten", + "describe_the_issue": "(optional) Beschreibe das Problem", + "submit_button": "Absenden", + "report_issue_button": "Fehler melden", + "request_button": "Anfragen", + "are_you_sure_you_want_to_request_all_seasons": "Bist du sicher, dass du alle Staffeln anfragen möchtest?", + "failed_to_login": "Fehler beim Anmelden", + "cast": "Besetzung", + "details": "Details", + "status": "Status", + "original_title": "Original Titel", + "series_type": "Serien Typ", + "release_dates": "Veröffentlichungsdaten", + "first_air_date": "Erstausstrahlungsdatum", + "next_air_date": "Nächstes Ausstrahlungsdatum", + "revenue": "Einnahmen", + "budget": "Budget", + "original_language": "Originalsprache", + "production_country": "Produktionsland", + "studios": "Studios", + "network": "Netzwerk", + "currently_streaming_on": "Derzeit im Streaming auf", + "advanced": "Erweitert", + "request_as": "Anfragen als", + "tags": "Tags", + "quality_profile": "Qualitätsprofil", + "root_folder": "Root-Ordner", + "season_all": "Season (all)", + "season_number": "Staffel {{season_number}}", + "number_episodes": "{{episode_number}} Episodes", + "born": "Geboren", + "appearances": "Auftritte", + "toasts": { + "jellyseer_does_not_meet_requirements": "Jellyseerr Server erfüllt nicht die Anforderungsversion. Bitte aktualisiere deinen Jellyseerr Server auf mindestens 2.0.0", + "jellyseerr_test_failed": "Jellyseerr-Test fehlgeschlagen. Bitte versuche es erneut.", + "failed_to_test_jellyseerr_server_url": "Fehler beim Testen der Jellyseerr-Server-URL", + "issue_submitted": "Problem eingereicht!", + "requested_item": "{{item}} angefragt!", + "you_dont_have_permission_to_request": "Du hast keine Berechtigung Anfragen zu stellen", + "something_went_wrong_requesting_media": "Etwas ist schiefgelaufen beim Anfragen von Medien" + } + }, + "tabs": { + "home": "Startseite", + "search": "Suche", + "library": "Bibliothek", + "custom_links": "Benutzerdefinierte Links", + "favorites": "Favoriten" + } +} diff --git a/translations/en.json b/translations/en.json index d8d18b72..30d7d466 100644 --- a/translations/en.json +++ b/translations/en.json @@ -1,477 +1,477 @@ { - "login": { - "username_required": "Username is required", - "error_title": "Error", - "login_title": "Log in", - "login_to_title": "Log in to", - "username_placeholder": "Username", - "password_placeholder": "Password", - "login_button": "Log in", - "quick_connect": "Quick Connect", - "enter_code_to_login": "Enter code {{code}} to login", - "failed_to_initiate_quick_connect": "Failed to initiate Quick Connect", - "got_it": "Got it", - "connection_failed": "Connection failed", - "could_not_connect_to_server": "Could not connect to the server. Please check the URL and your network connection.", - "an_unexpected_error_occured": "An unexpected error occurred", - "change_server": "Change server", - "invalid_username_or_password": "Invalid username or password", - "user_does_not_have_permission_to_log_in": "User does not have permission to log in", - "server_is_taking_too_long_to_respond_try_again_later": "Server is taking too long to respond, try again later", - "server_received_too_many_requests_try_again_later": "Server received too many requests, try again later.", - "there_is_a_server_error": "There is a server error", - "an_unexpected_error_occured_did_you_enter_the_correct_url": "An unexpected error occurred. Did you enter the server URL correctly?" - }, - "server": { - "enter_url_to_jellyfin_server": "Enter the URL to your Jellyfin server", - "server_url_placeholder": "http(s)://your-server.com", - "connect_button": "Connect", - "previous_servers": "previous servers", - "clear_button": "Clear", - "search_for_local_servers": "Search for local servers", - "searching": "Searching...", - "servers": "Servers" - }, - "home": { - "no_internet": "No Internet", - "no_items": "No items", - "no_internet_message": "No worries, you can still watch\ndownloaded content.", - "go_to_downloads": "Go to downloads", - "oops": "Oops!", - "error_message": "Something went wrong.\nPlease log out and in again.", - "continue_watching": "Continue Watching", - "next_up": "Next Up", - "recently_added_in": "Recently Added in {{libraryName}}", - "suggested_movies": "Suggested Movies", - "suggested_episodes": "Suggested Episodes", - "intro": { - "welcome_to_streamyfin": "Welcome to Streamyfin", - "a_free_and_open_source_client_for_jellyfin": "A free and open-source client for Jellyfin.", - "features_title": "Features", - "features_description": "Streamyfin has a bunch of features and integrates with a wide array of software which you can find in the settings menu, these include:", - "jellyseerr_feature_description": "Connect to your Jellyseerr instance and request movies directly in the app.", - "downloads_feature_title": "Downloads", - "downloads_feature_description": "Download movies and tv-shows to view offline. Use either the default method or install the optimize server to download files in the background.", - "chromecast_feature_description": "Cast movies and tv-shows to your Chromecast devices.", - "centralised_settings_plugin_title": "Centralised Settings Plugin", - "centralised_settings_plugin_description": "Configure settings from a centralised location on your Jellyfin server. All client settings for all users will be synced automatically.", - "done_button": "Done", - "go_to_settings_button": "Go to settings", - "read_more": "Read more" - }, - "settings": { - "settings_title": "Settings", - "log_out_button": "Log out", - "user_info": { - "user_info_title": "User Info", - "user": "User", - "server": "Server", - "token": "Token", - "app_version": "App Version" - }, - "quick_connect": { - "quick_connect_title": "Quick Connect", - "authorize_button": "Authorize Quick Connect", - "enter_the_quick_connect_code": "Enter the quick connect code...", - "success": "Success", - "quick_connect_autorized": "Quick Connect authorized", - "error": "Error", - "invalid_code": "Invalid code", - "authorize": "Authorize" - }, - "media_controls": { - "media_controls_title": "Media Controls", - "forward_skip_length": "Forward skip length", - "rewind_length": "Rewind length", - "seconds_unit": "s" - }, - "audio": { - "audio_title": "Audio", - "set_audio_track": "Set Audio Track From Previous Item", - "audio_language": "Audio language", - "audio_hint": "Choose a default audio language.", - "none": "None", - "language": "Language" - }, - "subtitles": { - "subtitle_title": "Subtitles", - "subtitle_language": "Subtitle language", - "subtitle_mode": "Subtitle Mode", - "set_subtitle_track": "Set Subtitle Track From Previous Item", - "subtitle_size": "Subtitle Size", - "subtitle_hint": "Configure subtitle preference.", - "none": "None", - "language": "Language", - "loading": "Loading", - "modes": { - "Default": "Default", - "Smart": "Smart", - "Always": "Always", - "None": "None", - "OnlyForced": "OnlyForced" - } - }, - "other": { - "other_title": "Other", - "follow_device_orientation": "Auto rotate", - "video_orientation": "Video orientation", - "orientation": "Orientation", - "orientations": { - "DEFAULT": "Default", - "ALL": "All", - "PORTRAIT": "Portrait", - "PORTRAIT_UP": "Portrait Up", - "PORTRAIT_DOWN": "Portrait Down", - "LANDSCAPE": "Landscape", - "LANDSCAPE_LEFT": "Landscape Left", - "LANDSCAPE_RIGHT": "Landscape Right", - "OTHER": "Other", - "UNKNOWN": "Unknown" - }, - "safe_area_in_controls": "Safe area in controls", - "video_player": "Video player", - "video_players": { - "VLC_3": "VLC 3", - "VLC_4": "VLC 4 (Experimental + PiP)" - }, - "show_custom_menu_links": "Show Custom Menu Links", - "hide_libraries": "Hide Libraries", - "select_liraries_you_want_to_hide": "Select the libraries you want to hide from the Library tab and home page sections.", - "disable_haptic_feedback": "Disable Haptic Feedback", - "default_quality": "Default quality" - }, - "downloads": { - "downloads_title": "Downloads", - "download_method": "Download method", - "remux_max_download": "Remux max download", - "auto_download": "Auto download", - "optimized_versions_server": "Optimized versions server", - "save_button": "Save", - "optimized_server": "Optimized Server", - "optimized": "Optimized", - "default": "Default", - "optimized_version_hint": "Enter the URL for the optimize server. The URL should include http or https and optionally the port.", - "read_more_about_optimized_server": "Read more about the optimize server.", - "url": "URL", - "server_url_placeholder": "http(s)://domain.org:port" - }, - "plugins": { - "plugins_title": "Plugins", - "jellyseerr": { - "jellyseerr_warning": "This integration is in its early stages. Expect things to change.", - "server_url": "Server URL", - "server_url_hint": "Example: http(s)://your-host.url\n(add port if required)", - "server_url_placeholder": "Jellyseerr URL...", - "password": "Password", - "password_placeholder": "Enter password for Jellyfin user {{username}}", - "save_button": "Save", - "clear_button": "Clear", - "login_button": "Login", - "total_media_requests": "Total media requests", - "movie_quota_limit": "Movie quota limit", - "movie_quota_days": "Movie quota days", - "tv_quota_limit": "TV quota limit", - "tv_quota_days": "TV quota days", - "reset_jellyseerr_config_button": "Reset Jellyseerr config", - "unlimited": "Unlimited", - "plus_n_more": "+{{n}} more", - "order_by": { - "DEFAULT": "Default", - "VOTE_COUNT_AND_AVERAGE": "Vote count and average", - "POPULARITY": "Popularity" - } - }, - "marlin_search": { - "enable_marlin_search": "Enable Marlin Search ", - "url": "URL", - "server_url_placeholder": "http(s)://domain.org:port", - "marlin_search_hint": "Enter the URL for the Marlin server. The URL should include http or https and optionally the port.", - "read_more_about_marlin": "Read more about Marlin.", - "save_button": "Save", - "toasts": { - "saved": "Saved" - } - } - }, - "storage": { - "storage_title": "Storage", - "app_usage": "App {{usedSpace}}%", - "device_usage": "Device {{availableSpace}}%", - "size_used": "{{used}} of {{total}} used", - "delete_all_downloaded_files": "Delete All Downloaded Files" - }, - "intro": { - "show_intro": "Show intro", - "reset_intro": "Reset intro" - }, - "logs": { - "logs_title": "Logs", - "no_logs_available": "No logs available", - "delete_all_logs": "Delete all logs" - }, - "languages": { - "title": "Languages", - "app_language": "App language", - "app_language_description": "Select the language for the app.", - "system": "System" - }, - "toasts": { - "error_deleting_files": "Error deleting files", - "background_downloads_enabled": "Background downloads enabled", - "background_downloads_disabled": "Background downloads disabled", - "connected": "Connected", - "could_not_connect": "Could not connect", - "invalid_url": "Invalid URL" - } - }, - "sessions": { - "title": "Sessions", - "no_active_sessions": "No active sessions" - }, - "downloads": { - "downloads_title": "Downloads", - "tvseries": "TV-Series", - "movies": "Movies", - "queue": "Queue", - "queue_hint": "Queue and downloads will be lost on app restart", - "no_items_in_queue": "No items in queue", - "no_downloaded_items": "No downloaded items", - "delete_all_movies_button": "Delete all Movies", - "delete_all_tvseries_button": "Delete all TV-Series", - "delete_all_button": "Delete all", - "active_download": "Active download", - "no_active_downloads": "No active downloads", - "active_downloads": "Active downloads", - "new_app_version_requires_re_download": "New app version requires re-download", - "new_app_version_requires_re_download_description": "The new update requires content to be downloaded again. Please remove all downloaded content and try again.", - "back": "Back", - "delete": "Delete", - "something_went_wrong": "Something went wrong", - "could_not_get_stream_url_from_jellyfin": "Could not get the stream URL from Jellyfin", - "eta": "ETA {{eta}}", - "methods": "Methods", - "toasts": { - "you_are_not_allowed_to_download_files": "You are not allowed to download files.", - "deleted_all_movies_successfully": "Deleted all movies successfully!", - "failed_to_delete_all_movies": "Failed to delete all movies", - "deleted_all_tvseries_successfully": "Deleted all TV-Series successfully!", - "failed_to_delete_all_tvseries": "Failed to delete all TV-Series", - "download_cancelled": "Download cancelled", - "could_not_cancel_download": "Could not cancel download", - "download_completed": "Download completed", - "download_started_for": "Download started for {{item}}", - "item_is_ready_to_be_downloaded": "{{item}} is ready to be downloaded", - "download_stated_for_item": "Download started for {{item}}", - "download_failed_for_item": "Download failed for {{item}} - {{error}}", - "download_completed_for_item": "Download completed for {{item}}", - "queued_item_for_optimization": "Queued {{item}} for optimization", - "failed_to_start_download_for_item": "Failed to start downloading for {{item}}: {{message}}", - "server_responded_with_status_code": "Server responded with status {{statusCode}}", - "no_response_received_from_server": "No response received from the server", - "error_setting_up_the_request": "Error setting up the request", - "failed_to_start_download_for_item_unexpected_error": "Failed to start downloading for {{item}}: Unexpected error", - "all_files_folders_and_jobs_deleted_successfully": "All files, folders, and jobs deleted successfully", - "an_error_occured_while_deleting_files_and_jobs": "An error occurred while deleting files and jobs", - "go_to_downloads": "Go to downloads" - } - } - }, - "search": { - "search_here": "Search here...", - "search": "Search...", - "x_items": "{{count}} items", - "library": "Library", - "discover": "Discover", - "no_results": "No results", - "no_results_found_for": "No results found for", - "movies": "Movies", - "series": "Series", - "episodes": "Episodes", - "collections": "Collections", - "actors": "Actors", - "request_movies": "Request Movies", - "request_series": "Request Series", - "recently_added": "Recently Added", - "recent_requests": "Recent Requests", - "plex_watchlist": "Plex Watchlist", - "trending": "Trending", - "popular_movies": "Popular Movies", - "movie_genres": "Movie Genres", - "upcoming_movies": "Upcoming Movies", - "studios": "Studios", - "popular_tv": "Popular TV", - "tv_genres": "TV Genres", - "upcoming_tv": "Upcoming TV", - "networks": "Networks", - "tmdb_movie_keyword": "TMDB Movie Keyword", - "tmdb_movie_genre": "TMDB Movie Genre", - "tmdb_tv_keyword": "TMDB TV Keyword", - "tmdb_tv_genre": "TMDB TV Genre", - "tmdb_search": "TMDB Search", - "tmdb_studio": "TMDB Studio", - "tmdb_network": "TMDB Network", - "tmdb_movie_streaming_services": "TMDB Movie Streaming Services", - "tmdb_tv_streaming_services": "TMDB TV Streaming Services" - }, - "library": { - "no_items_found": "No items found", - "no_results": "No results", - "no_libraries_found": "No libraries found", - "item_types": { - "movies": "movies", - "series": "series", - "boxsets": "box sets", - "items": "items" - }, - "options": { - "display": "Display", - "row": "Row", - "list": "List", - "image_style": "Image style", - "poster": "Poster", - "cover": "Cover", - "show_titles": "Show titles", - "show_stats": "Show stats" - }, - "filters": { - "genres": "Genres", - "years": "Years", - "sort_by": "Sort By", - "sort_order": "Sort Order", - "asc": "Ascending", - "desc": "Descending", - "tags": "Tags" - } - }, - "favorites": { - "series": "Series", - "movies": "Movies", - "episodes": "Episodes", - "videos": "Videos", - "boxsets": "Boxsets", - "playlists": "Playlists", - "noDataTitle": "No favorites yet", - "noData": "Mark items as favorites to see them appear here for quick access." - }, - "custom_links": { - "no_links": "No links" - }, - "player": { - "error": "Error", - "failed_to_get_stream_url": "Failed to get the stream URL", - "an_error_occured_while_playing_the_video": "An error occurred while playing the video. Check logs in settings.", - "client_error": "Client error", - "could_not_create_stream_for_chromecast": "Could not create a stream for Chromecast", - "message_from_server": "Message from server: {{message}}", - "video_has_finished_playing": "Video has finished playing!", - "no_video_source": "No video source...", - "next_episode": "Next Episode", - "refresh_tracks": "Refresh Tracks", - "subtitle_tracks": "Subtitle Tracks:", - "audio_tracks": "Audio Tracks:", - "playback_state": "Playback State:", - "no_data_available": "No data available", - "index": "Index:" - }, - "item_card": { - "next_up": "Next up", - "no_items_to_display": "No items to display", - "cast_and_crew": "Cast & Crew", - "series": "Series", - "seasons": "Seasons", - "season": "Season", - "no_episodes_for_this_season": "No episodes for this season", - "overview": "Overview", - "more_with": "More with {{name}}", - "similar_items": "Similar items", - "no_similar_items_found": "No similar items found", - "video": "Video", - "more_details": "More details", - "quality": "Quality", - "audio": "Audio", - "subtitles": "Subtitle", - "show_more": "Show more", - "show_less": "Show less", - "appeared_in": "Appeared in", - "could_not_load_item": "Could not load item", - "none": "None", - "download": { - "download_season": "Download Season", - "download_series": "Download Series", - "download_episode": "Download Episode", - "download_movie": "Download Movie", - "download_x_item": "Download {{item_count}} items", - "download_button": "Download", - "using_optimized_server": "Using optimized server", - "using_default_method": "Using default method" - } - }, - "live_tv": { - "next": "Next", - "previous": "Previous", - "live_tv": "Live TV", - "coming_soon": "Coming soon", - "on_now": "On now", - "shows": "Shows", - "movies": "Movies", - "sports": "Sports", - "for_kids": "For Kids", - "news": "News" - }, - "jellyseerr": { - "confirm": "Confirm", - "cancel": "Cancel", - "yes": "Yes", - "whats_wrong": "What's wrong?", - "issue_type": "Issue type", - "select_an_issue": "Select an issue", - "types": "Types", - "describe_the_issue": "(optional) Describe the issue...", - "submit_button": "Submit", - "report_issue_button": "Report issue", - "request_button": "Request", - "are_you_sure_you_want_to_request_all_seasons": "Are you sure you want to request all seasons?", - "failed_to_login": "Failed to login", - "cast": "Cast", - "details": "Details", - "status": "Status", - "original_title": "Original Title", - "series_type": "Series Type", - "release_dates": "Release Dates", - "first_air_date": "First Air Date", - "next_air_date": "Next Air Date", - "revenue": "Revenue", - "budget": "Budget", - "original_language": "Original Language", - "production_country": "Production Country", - "studios": "Studios", - "network": "Network", - "currently_streaming_on": "Currently Streaming on", - "advanced": "Advanced", - "request_as": "Request As", - "tags": "Tags", - "quality_profile": "Quality Profile", - "root_folder": "Root Folder", - "season_all": "Season (all)", - "season_number": "Season {{season_number}}", - "number_episodes": "{{episode_number}} Episodes", - "born": "Born", - "appearances": "Appearances", - "toasts": { - "jellyseer_does_not_meet_requirements": "Jellyseerr server does not meet minimum version requirements! Please update to at least 2.0.0", - "jellyseerr_test_failed": "Jellyseerr test failed. Please try again.", - "failed_to_test_jellyseerr_server_url": "Failed to test jellyseerr server url", - "issue_submitted": "Issue submitted!", - "requested_item": "Requested {{item}}!", - "you_dont_have_permission_to_request": "You don't have permission to request!", - "something_went_wrong_requesting_media": "Something went wrong requesting media!" - } - }, - "tabs": { - "home": "Home", - "search": "Search", - "library": "Library", - "custom_links": "Custom Links", - "favorites": "Favorites" - } + "login": { + "username_required": "Username is required", + "error_title": "Error", + "login_title": "Log in", + "login_to_title": "Log in to", + "username_placeholder": "Username", + "password_placeholder": "Password", + "login_button": "Log in", + "quick_connect": "Quick Connect", + "enter_code_to_login": "Enter code {{code}} to login", + "failed_to_initiate_quick_connect": "Failed to initiate Quick Connect", + "got_it": "Got it", + "connection_failed": "Connection failed", + "could_not_connect_to_server": "Could not connect to the server. Please check the URL and your network connection.", + "an_unexpected_error_occured": "An unexpected error occurred", + "change_server": "Change server", + "invalid_username_or_password": "Invalid username or password", + "user_does_not_have_permission_to_log_in": "User does not have permission to log in", + "server_is_taking_too_long_to_respond_try_again_later": "Server is taking too long to respond, try again later", + "server_received_too_many_requests_try_again_later": "Server received too many requests, try again later.", + "there_is_a_server_error": "There is a server error", + "an_unexpected_error_occured_did_you_enter_the_correct_url": "An unexpected error occurred. Did you enter the server URL correctly?" + }, + "server": { + "enter_url_to_jellyfin_server": "Enter the URL to your Jellyfin server", + "server_url_placeholder": "http(s)://your-server.com", + "connect_button": "Connect", + "previous_servers": "previous servers", + "clear_button": "Clear", + "search_for_local_servers": "Search for local servers", + "searching": "Searching...", + "servers": "Servers" + }, + "home": { + "no_internet": "No Internet", + "no_items": "No items", + "no_internet_message": "No worries, you can still watch\ndownloaded content.", + "go_to_downloads": "Go to downloads", + "oops": "Oops!", + "error_message": "Something went wrong.\nPlease log out and in again.", + "continue_watching": "Continue Watching", + "next_up": "Next Up", + "recently_added_in": "Recently Added in {{libraryName}}", + "suggested_movies": "Suggested Movies", + "suggested_episodes": "Suggested Episodes", + "intro": { + "welcome_to_streamyfin": "Welcome to Streamyfin", + "a_free_and_open_source_client_for_jellyfin": "A free and open-source client for Jellyfin.", + "features_title": "Features", + "features_description": "Streamyfin has a bunch of features and integrates with a wide array of software which you can find in the settings menu, these include:", + "jellyseerr_feature_description": "Connect to your Jellyseerr instance and request movies directly in the app.", + "downloads_feature_title": "Downloads", + "downloads_feature_description": "Download movies and tv-shows to view offline. Use either the default method or install the optimize server to download files in the background.", + "chromecast_feature_description": "Cast movies and tv-shows to your Chromecast devices.", + "centralised_settings_plugin_title": "Centralised Settings Plugin", + "centralised_settings_plugin_description": "Configure settings from a centralised location on your Jellyfin server. All client settings for all users will be synced automatically.", + "done_button": "Done", + "go_to_settings_button": "Go to settings", + "read_more": "Read more" + }, + "settings": { + "settings_title": "Settings", + "log_out_button": "Log out", + "user_info": { + "user_info_title": "User Info", + "user": "User", + "server": "Server", + "token": "Token", + "app_version": "App Version" + }, + "quick_connect": { + "quick_connect_title": "Quick Connect", + "authorize_button": "Authorize Quick Connect", + "enter_the_quick_connect_code": "Enter the quick connect code...", + "success": "Success", + "quick_connect_autorized": "Quick Connect authorized", + "error": "Error", + "invalid_code": "Invalid code", + "authorize": "Authorize" + }, + "media_controls": { + "media_controls_title": "Media Controls", + "forward_skip_length": "Forward skip length", + "rewind_length": "Rewind length", + "seconds_unit": "s" + }, + "audio": { + "audio_title": "Audio", + "set_audio_track": "Set Audio Track From Previous Item", + "audio_language": "Audio language", + "audio_hint": "Choose a default audio language.", + "none": "None", + "language": "Language" + }, + "subtitles": { + "subtitle_title": "Subtitles", + "subtitle_language": "Subtitle language", + "subtitle_mode": "Subtitle Mode", + "set_subtitle_track": "Set Subtitle Track From Previous Item", + "subtitle_size": "Subtitle Size", + "subtitle_hint": "Configure subtitle preference.", + "none": "None", + "language": "Language", + "loading": "Loading", + "modes": { + "Default": "Default", + "Smart": "Smart", + "Always": "Always", + "None": "None", + "OnlyForced": "OnlyForced" + } + }, + "other": { + "other_title": "Other", + "follow_device_orientation": "Auto rotate", + "video_orientation": "Video orientation", + "orientation": "Orientation", + "orientations": { + "DEFAULT": "Default", + "ALL": "All", + "PORTRAIT": "Portrait", + "PORTRAIT_UP": "Portrait Up", + "PORTRAIT_DOWN": "Portrait Down", + "LANDSCAPE": "Landscape", + "LANDSCAPE_LEFT": "Landscape Left", + "LANDSCAPE_RIGHT": "Landscape Right", + "OTHER": "Other", + "UNKNOWN": "Unknown" + }, + "safe_area_in_controls": "Safe area in controls", + "video_player": "Video player", + "video_players": { + "VLC_3": "VLC 3", + "VLC_4": "VLC 4 (Experimental + PiP)" + }, + "show_custom_menu_links": "Show Custom Menu Links", + "hide_libraries": "Hide Libraries", + "select_liraries_you_want_to_hide": "Select the libraries you want to hide from the Library tab and home page sections.", + "disable_haptic_feedback": "Disable Haptic Feedback", + "default_quality": "Default quality" + }, + "downloads": { + "downloads_title": "Downloads", + "download_method": "Download method", + "remux_max_download": "Remux max download", + "auto_download": "Auto download", + "optimized_versions_server": "Optimized versions server", + "save_button": "Save", + "optimized_server": "Optimized Server", + "optimized": "Optimized", + "default": "Default", + "optimized_version_hint": "Enter the URL for the optimize server. The URL should include http or https and optionally the port.", + "read_more_about_optimized_server": "Read more about the optimize server.", + "url": "URL", + "server_url_placeholder": "http(s)://domain.org:port" + }, + "plugins": { + "plugins_title": "Plugins", + "jellyseerr": { + "jellyseerr_warning": "This integration is in its early stages. Expect things to change.", + "server_url": "Server URL", + "server_url_hint": "Example: http(s)://your-host.url\n(add port if required)", + "server_url_placeholder": "Jellyseerr URL...", + "password": "Password", + "password_placeholder": "Enter password for Jellyfin user {{username}}", + "save_button": "Save", + "clear_button": "Clear", + "login_button": "Login", + "total_media_requests": "Total media requests", + "movie_quota_limit": "Movie quota limit", + "movie_quota_days": "Movie quota days", + "tv_quota_limit": "TV quota limit", + "tv_quota_days": "TV quota days", + "reset_jellyseerr_config_button": "Reset Jellyseerr config", + "unlimited": "Unlimited", + "plus_n_more": "+{{n}} more", + "order_by": { + "DEFAULT": "Default", + "VOTE_COUNT_AND_AVERAGE": "Vote count and average", + "POPULARITY": "Popularity" + } + }, + "marlin_search": { + "enable_marlin_search": "Enable Marlin Search ", + "url": "URL", + "server_url_placeholder": "http(s)://domain.org:port", + "marlin_search_hint": "Enter the URL for the Marlin server. The URL should include http or https and optionally the port.", + "read_more_about_marlin": "Read more about Marlin.", + "save_button": "Save", + "toasts": { + "saved": "Saved" + } + } + }, + "storage": { + "storage_title": "Storage", + "app_usage": "App {{usedSpace}}%", + "device_usage": "Device {{availableSpace}}%", + "size_used": "{{used}} of {{total}} used", + "delete_all_downloaded_files": "Delete All Downloaded Files" + }, + "intro": { + "show_intro": "Show intro", + "reset_intro": "Reset intro" + }, + "logs": { + "logs_title": "Logs", + "no_logs_available": "No logs available", + "delete_all_logs": "Delete all logs" + }, + "languages": { + "title": "Languages", + "app_language": "App language", + "app_language_description": "Select the language for the app.", + "system": "System" + }, + "toasts": { + "error_deleting_files": "Error deleting files", + "background_downloads_enabled": "Background downloads enabled", + "background_downloads_disabled": "Background downloads disabled", + "connected": "Connected", + "could_not_connect": "Could not connect", + "invalid_url": "Invalid URL" + } + }, + "sessions": { + "title": "Sessions", + "no_active_sessions": "No active sessions" + }, + "downloads": { + "downloads_title": "Downloads", + "tvseries": "TV-Series", + "movies": "Movies", + "queue": "Queue", + "queue_hint": "Queue and downloads will be lost on app restart", + "no_items_in_queue": "No items in queue", + "no_downloaded_items": "No downloaded items", + "delete_all_movies_button": "Delete all Movies", + "delete_all_tvseries_button": "Delete all TV-Series", + "delete_all_button": "Delete all", + "active_download": "Active download", + "no_active_downloads": "No active downloads", + "active_downloads": "Active downloads", + "new_app_version_requires_re_download": "New app version requires re-download", + "new_app_version_requires_re_download_description": "The new update requires content to be downloaded again. Please remove all downloaded content and try again.", + "back": "Back", + "delete": "Delete", + "something_went_wrong": "Something went wrong", + "could_not_get_stream_url_from_jellyfin": "Could not get the stream URL from Jellyfin", + "eta": "ETA {{eta}}", + "methods": "Methods", + "toasts": { + "you_are_not_allowed_to_download_files": "You are not allowed to download files.", + "deleted_all_movies_successfully": "Deleted all movies successfully!", + "failed_to_delete_all_movies": "Failed to delete all movies", + "deleted_all_tvseries_successfully": "Deleted all TV-Series successfully!", + "failed_to_delete_all_tvseries": "Failed to delete all TV-Series", + "download_cancelled": "Download cancelled", + "could_not_cancel_download": "Could not cancel download", + "download_completed": "Download completed", + "download_started_for": "Download started for {{item}}", + "item_is_ready_to_be_downloaded": "{{item}} is ready to be downloaded", + "download_stated_for_item": "Download started for {{item}}", + "download_failed_for_item": "Download failed for {{item}} - {{error}}", + "download_completed_for_item": "Download completed for {{item}}", + "queued_item_for_optimization": "Queued {{item}} for optimization", + "failed_to_start_download_for_item": "Failed to start downloading for {{item}}: {{message}}", + "server_responded_with_status_code": "Server responded with status {{statusCode}}", + "no_response_received_from_server": "No response received from the server", + "error_setting_up_the_request": "Error setting up the request", + "failed_to_start_download_for_item_unexpected_error": "Failed to start downloading for {{item}}: Unexpected error", + "all_files_folders_and_jobs_deleted_successfully": "All files, folders, and jobs deleted successfully", + "an_error_occured_while_deleting_files_and_jobs": "An error occurred while deleting files and jobs", + "go_to_downloads": "Go to downloads" + } + } + }, + "search": { + "search_here": "Search here...", + "search": "Search...", + "x_items": "{{count}} items", + "library": "Library", + "discover": "Discover", + "no_results": "No results", + "no_results_found_for": "No results found for", + "movies": "Movies", + "series": "Series", + "episodes": "Episodes", + "collections": "Collections", + "actors": "Actors", + "request_movies": "Request Movies", + "request_series": "Request Series", + "recently_added": "Recently Added", + "recent_requests": "Recent Requests", + "plex_watchlist": "Plex Watchlist", + "trending": "Trending", + "popular_movies": "Popular Movies", + "movie_genres": "Movie Genres", + "upcoming_movies": "Upcoming Movies", + "studios": "Studios", + "popular_tv": "Popular TV", + "tv_genres": "TV Genres", + "upcoming_tv": "Upcoming TV", + "networks": "Networks", + "tmdb_movie_keyword": "TMDB Movie Keyword", + "tmdb_movie_genre": "TMDB Movie Genre", + "tmdb_tv_keyword": "TMDB TV Keyword", + "tmdb_tv_genre": "TMDB TV Genre", + "tmdb_search": "TMDB Search", + "tmdb_studio": "TMDB Studio", + "tmdb_network": "TMDB Network", + "tmdb_movie_streaming_services": "TMDB Movie Streaming Services", + "tmdb_tv_streaming_services": "TMDB TV Streaming Services" + }, + "library": { + "no_items_found": "No items found", + "no_results": "No results", + "no_libraries_found": "No libraries found", + "item_types": { + "movies": "movies", + "series": "series", + "boxsets": "box sets", + "items": "items" + }, + "options": { + "display": "Display", + "row": "Row", + "list": "List", + "image_style": "Image style", + "poster": "Poster", + "cover": "Cover", + "show_titles": "Show titles", + "show_stats": "Show stats" + }, + "filters": { + "genres": "Genres", + "years": "Years", + "sort_by": "Sort By", + "sort_order": "Sort Order", + "asc": "Ascending", + "desc": "Descending", + "tags": "Tags" + } + }, + "favorites": { + "series": "Series", + "movies": "Movies", + "episodes": "Episodes", + "videos": "Videos", + "boxsets": "Boxsets", + "playlists": "Playlists", + "noDataTitle": "No favorites yet", + "noData": "Mark items as favorites to see them appear here for quick access." + }, + "custom_links": { + "no_links": "No links" + }, + "player": { + "error": "Error", + "failed_to_get_stream_url": "Failed to get the stream URL", + "an_error_occured_while_playing_the_video": "An error occurred while playing the video. Check logs in settings.", + "client_error": "Client error", + "could_not_create_stream_for_chromecast": "Could not create a stream for Chromecast", + "message_from_server": "Message from server: {{message}}", + "video_has_finished_playing": "Video has finished playing!", + "no_video_source": "No video source...", + "next_episode": "Next Episode", + "refresh_tracks": "Refresh Tracks", + "subtitle_tracks": "Subtitle Tracks:", + "audio_tracks": "Audio Tracks:", + "playback_state": "Playback State:", + "no_data_available": "No data available", + "index": "Index:" + }, + "item_card": { + "next_up": "Next up", + "no_items_to_display": "No items to display", + "cast_and_crew": "Cast & Crew", + "series": "Series", + "seasons": "Seasons", + "season": "Season", + "no_episodes_for_this_season": "No episodes for this season", + "overview": "Overview", + "more_with": "More with {{name}}", + "similar_items": "Similar items", + "no_similar_items_found": "No similar items found", + "video": "Video", + "more_details": "More details", + "quality": "Quality", + "audio": "Audio", + "subtitles": "Subtitle", + "show_more": "Show more", + "show_less": "Show less", + "appeared_in": "Appeared in", + "could_not_load_item": "Could not load item", + "none": "None", + "download": { + "download_season": "Download Season", + "download_series": "Download Series", + "download_episode": "Download Episode", + "download_movie": "Download Movie", + "download_x_item": "Download {{item_count}} items", + "download_button": "Download", + "using_optimized_server": "Using optimized server", + "using_default_method": "Using default method" + } + }, + "live_tv": { + "next": "Next", + "previous": "Previous", + "live_tv": "Live TV", + "coming_soon": "Coming soon", + "on_now": "On now", + "shows": "Shows", + "movies": "Movies", + "sports": "Sports", + "for_kids": "For Kids", + "news": "News" + }, + "jellyseerr": { + "confirm": "Confirm", + "cancel": "Cancel", + "yes": "Yes", + "whats_wrong": "What's wrong?", + "issue_type": "Issue type", + "select_an_issue": "Select an issue", + "types": "Types", + "describe_the_issue": "(optional) Describe the issue...", + "submit_button": "Submit", + "report_issue_button": "Report issue", + "request_button": "Request", + "are_you_sure_you_want_to_request_all_seasons": "Are you sure you want to request all seasons?", + "failed_to_login": "Failed to login", + "cast": "Cast", + "details": "Details", + "status": "Status", + "original_title": "Original Title", + "series_type": "Series Type", + "release_dates": "Release Dates", + "first_air_date": "First Air Date", + "next_air_date": "Next Air Date", + "revenue": "Revenue", + "budget": "Budget", + "original_language": "Original Language", + "production_country": "Production Country", + "studios": "Studios", + "network": "Network", + "currently_streaming_on": "Currently Streaming on", + "advanced": "Advanced", + "request_as": "Request As", + "tags": "Tags", + "quality_profile": "Quality Profile", + "root_folder": "Root Folder", + "season_all": "Season (all)", + "season_number": "Season {{season_number}}", + "number_episodes": "{{episode_number}} Episodes", + "born": "Born", + "appearances": "Appearances", + "toasts": { + "jellyseer_does_not_meet_requirements": "Jellyseerr server does not meet minimum version requirements! Please update to at least 2.0.0", + "jellyseerr_test_failed": "Jellyseerr test failed. Please try again.", + "failed_to_test_jellyseerr_server_url": "Failed to test jellyseerr server url", + "issue_submitted": "Issue submitted!", + "requested_item": "Requested {{item}}!", + "you_dont_have_permission_to_request": "You don't have permission to request!", + "something_went_wrong_requesting_media": "Something went wrong requesting media!" + } + }, + "tabs": { + "home": "Home", + "search": "Search", + "library": "Library", + "custom_links": "Custom Links", + "favorites": "Favorites" + } } diff --git a/translations/es.json b/translations/es.json index 5182c31f..0745aad3 100644 --- a/translations/es.json +++ b/translations/es.json @@ -1,473 +1,473 @@ { - "login": { - "username_required": "Se requiere un nombre de usuario", - "error_title": "Error", - "login_title": "Iniciar sesión", - "login_to_title": "Iniciar sesión en", - "username_placeholder": "Nombre de usuario", - "password_placeholder": "Contraseña", - "login_button": "Iniciar sesión", - "quick_connect": "Conexión rápida", - "enter_code_to_login": "Introduce el código {{code}} para iniciar sesión", - "failed_to_initiate_quick_connect": "Error al iniciar la conexión rápida", - "got_it": "Entendido", - "connection_failed": "Conexión fallida", - "could_not_connect_to_server": "No se pudo conectar al servidor. Por favor comprueba la URL y tu conexión de red.", - "an_unexpected_error_occured": "Ha ocurrido un error inesperado", - "change_server": "Cambiar servidor", - "invalid_username_or_password": "Usuario o contraseña inválidos", - "user_does_not_have_permission_to_log_in": "El usuario no tiene permiso para iniciar sesión", - "server_is_taking_too_long_to_respond_try_again_later": "El servidor está tardando mucho en responder, inténtalo de nuevo más tarde.", - "server_received_too_many_requests_try_again_later": "El servidor está recibiendo muchas peticiones, inténtalo de nuevo más tarde.", - "there_is_a_server_error": "Hay un error en el servidor", - "an_unexpected_error_occured_did_you_enter_the_correct_url": "Ha ocurrido un error inesperado. ¿Has introducido la URL correcta?" - }, - "server": { - "enter_url_to_jellyfin_server": "Introduce la URL de tu servidor Jellyfin", - "server_url_placeholder": "http(s)://tu-servidor.com", - "connect_button": "Conectar", - "previous_servers": "Servidores previos", - "clear_button": "Limpiar", - "search_for_local_servers": "Buscar servidores locales", - "searching": "Buscando...", - "servers": "Servidores" - }, - "home": { - "no_internet": "Sin internet", - "no_items": "No hay ítems", - "no_internet_message": "No te preocupes, todavía puedes\nver el contenido descargado.", - "go_to_downloads": "Ir a descargas", - "oops": "¡Vaya!", - "error_message": "Algo ha salido mal.\nPor favor, cierra la sesión y vuelve a iniciar.", - "continue_watching": "Seguir viendo", - "next_up": "A continuación", - "recently_added_in": "Recientemente añadido en {{libraryName}}", - "suggested_movies": "Películas sugeridas", - "suggested_episodes": "Episodios sugeridos", - "intro": { - "welcome_to_streamyfin": "Bienvenido a Streamyfin", - "a_free_and_open_source_client_for_jellyfin": "Un cliente gratuito y de código abierto para Jellyfin.", - "features_title": "Características", - "features_description": "Streamyfin tiene una amplia gama de características y se integra con una variedad de software que puedes encontrar en el menú de configuración, esto incluye:", - "jellyseerr_feature_description": "Conéctate a tu servidor de Jellyseer y pide películas directamente desde la app.", - "downloads_feature_title": "Descargas", - "downloads_feature_description": "Descarga películas y series para ver sin conexión. Usa el método por defecto o el servidor optimizado para descargar archivos en segundo plano.", - "chromecast_feature_description": "Envía pelícuas y series a tus dispositivos Chromecast.", - "centralised_settings_plugin_title": "Plugin de configuración centralizada", - "centralised_settings_plugin_description": "Crea configuraciones desde una ubicación centralizada en tu servidor de Jellyfin. Todas las configuraciones para todos los usuarios se sincronizarán automáticamente.", - "done_button": "Hecho", - "go_to_settings_button": "Ir a la configuración", - "read_more": "Leer más" - }, - "settings": { - "settings_title": "Configuración", - "log_out_button": "Cerrar sesión", - "user_info": { - "user_info_title": "Información de usuario", - "user": "Usuario", - "server": "Servidor", - "token": "Token", - "app_version": "Versión de la app" - }, - "quick_connect": { - "quick_connect_title": "Conexión rápida", - "authorize_button": "Autorizar conexión rápida", - "enter_the_quick_connect_code": "Introduce el código de conexión rápida...", - "success": "Hecho", - "quick_connect_autorized": "Conexión rápida autorizada", - "error": "Error", - "invalid_code": "Código inválido", - "authorize": "Autorizar" - }, - "media_controls": { - "media_controls_title": "Controles de reproducción", - "forward_skip_length": "Longitud de avance", - "rewind_length": "Longitud de retroceso", - "seconds_unit": "s" - }, - "audio": { - "audio_title": "Audio", - "set_audio_track": "Establecer pista del elemento anterior", - "audio_language": "Idioma de audio", - "audio_hint": "Elige un idioma de audio por defecto.", - "none": "Ninguno", - "language": "Idioma" - }, - "subtitles": { - "subtitle_title": "Subtítulos", - "subtitle_language": "Idioma de subtítulos", - "subtitle_mode": "Modo de subtítulos", - "set_subtitle_track": "Establecer pista del elemento anterior", - "subtitle_size": "Tamaño de subtítulos", - "subtitle_hint": "Configurar preferencias de subtítulos.", - "none": "Ninguno", - "language": "Idioma", - "loading": "Cargando", - "modes": { - "Default": "Por defecto", - "Smart": "Inteligente", - "Always": "Siempre", - "None": "Nada", - "OnlyForced": "Solo forzados" - } - }, - "other": { - "other_title": "Otros", - "follow_device_orientation": "Rotación automática", - "video_orientation": "Orientación de vídeo", - "orientation": "Orientación", - "orientations": { - "DEFAULT": "Por defecto", - "ALL": "Todas", - "PORTRAIT": "Vertical", - "PORTRAIT_UP": "Vertical arriba", - "PORTRAIT_DOWN": "Vertical abajo", - "LANDSCAPE": "Horizontal", - "LANDSCAPE_LEFT": "Horizontal izquierda", - "LANDSCAPE_RIGHT": "Horizontal derecha", - "OTHER": "Otra", - "UNKNOWN": "Desconocida" - }, - "safe_area_in_controls": "Área segura en controles", - "video_player": "Video player", - "video_players": { - "VLC_3": "VLC 3", - "VLC_4": "VLC 4 (Experimental + PiP)" - }, - "show_custom_menu_links": "Mostrar enlaces de menú personalizados", - "hide_libraries": "Ocultar bibliotecas", - "select_liraries_you_want_to_hide": "Selecciona las bibliotecas que quieres ocultar de la pestaña Bibliotecas y de Inicio.", - "disable_haptic_feedback": "Desactivar feedback háptico", - "default_quality": "Calidad por defecto" - }, - "downloads": { - "downloads_title": "Descargas", - "download_method": "Método de descarga", - "remux_max_download": "Remux máx. descarga", - "auto_download": "Descarga automática", - "optimized_versions_server": "Servidor de versiones optimizadas", - "save_button": "Guardar", - "optimized_server": "Servidor optimizado", - "optimized": "Optimizado", - "default": "Por defecto", - "optimized_version_hint": "Introduce la URL del servidor de versiones optimizadas. La URL debe incluir http o https y opcionalmente el puerto.", - "read_more_about_optimized_server": "Leer más sobre el servidor de versiones optimizadas.", - "url": "URL", - "server_url_placeholder": "http(s)://dominio.org:puerto" - }, - "plugins": { - "plugins_title": "Plugins", - "jellyseerr": { - "jellyseerr_warning": "Esta integración está en sus primeras etapas. Cuenta con posibles cambios.", - "server_url": "URL del servidor", - "server_url_hint": "Ejemplo: http(s)://tu-dominio.url\n(añade el puerto si es necesario)", - "server_url_placeholder": "URL de Jellyseerr...", - "password": "Contrasñea", - "password_placeholder": "Introduce la contraseña de Jellyfin de {{username}}", - "save_button": "Guardar", - "clear_button": "Limpiar", - "login_button": "Iniciar sesión", - "total_media_requests": "Peticiones totales de medios", - "movie_quota_limit": "Límite de cuota de películas", - "movie_quota_days": "Días de cuota de películas", - "tv_quota_limit": "Límite de cuota de series", - "tv_quota_days": "Días de cuota de series", - "reset_jellyseerr_config_button": "Restablecer configuración de Jellyseerr", - "unlimited": "Ilimitado", - "plus_n_more": "+{{n}} more", - "order_by": { - "DEFAULT": "Default", - "VOTE_COUNT_AND_AVERAGE": "Vote count and average", - "POPULARITY": "Popularity" - } - }, - "marlin_search": { - "enable_marlin_search": "Habilitar búsqueda de Marlin", - "url": "URL", - "server_url_placeholder": "http(s)://dominio.org:puerto", - "marlin_search_hint": "Introduce la URL del servidor de Marlin. La URL debe incluir http o https y opcionalmente el puerto.", - "read_more_about_marlin": "Leer más sobre Marlin.", - "save_button": "Guardar", - "toasts": { - "saved": "Guardado" - } - } - }, - "storage": { - "storage_title": "Almacenamiento", - "app_usage": "App {{usedSpace}}%", - "device_usage": "Dispositivo {{availableSpace}}%", - "size_used": "{{used}} de {{total}} usado", - "delete_all_downloaded_files": "Eliminar todos los archivos descargados" - }, - "intro": { - "show_intro": "Mostrar intro", - "reset_intro": "Restablecer intro" - }, - "logs": { - "logs_title": "Registros", - "no_logs_available": "No hay registros disponibles", - "delete_all_logs": "Eliminar todos los registros" - }, - "languages": { - "title": "Idiomas", - "app_language": "Idioma de la app", - "app_language_description": "Selecciona el idioma de la app.", - "system": "Sistema" - }, - "toasts": { - "error_deleting_files": "Error al eliminar archivos", - "background_downloads_enabled": "Descargas en segundo plano habilitadas", - "background_downloads_disabled": "Descargas en segundo plano deshabilitadas", - "connected": "Conectado", - "could_not_connect": "No se pudo conectar", - "invalid_url": "URL inválida" - } - }, - "downloads": { - "downloads_title": "Descargas", - "tvseries": "Series", - "movies": "Películas", - "queue": "Cola", - "queue_hint": "La cola de series y películas se perderá al reiniciar la app", - "no_items_in_queue": "No hay ítems en la cola", - "no_downloaded_items": "No hay ítems descargados", - "delete_all_movies_button": "Eliminar todas las películas", - "delete_all_tvseries_button": "Eliminar todas las series", - "delete_all_button": "Eliminar todo", - "active_download": "Descarga activa", - "no_active_downloads": "No hay descargas activas", - "active_downloads": "Descargas activas", - "new_app_version_requires_re_download": "La nueva actualización requiere volver a descargar", - "new_app_version_requires_re_download_description": "La nueva actualización requiere volver a descargar el contenido. Por favor, elimina todo el código descargado y vuélvelo a intentar.", - "back": "Atrás", - "delete": "Borrar", - "something_went_wrong": "Algo ha salido mal", - "could_not_get_stream_url_from_jellyfin": "No se pudo obtener la URL del stream de Jellyfin", - "eta": "{{eta}} restante", - "methods": "Métodos", - "toasts": { - "you_are_not_allowed_to_download_files": "No tienes permiso para descargar archivos.", - "deleted_all_movies_successfully": "¡Todas las películas eliminadas con éxito!", - "failed_to_delete_all_movies": "Error al eliminar todas las películas", - "deleted_all_tvseries_successfully": "¡Todas las series eliminadas con éxito!", - "failed_to_delete_all_tvseries": "Error al eliminar todas las series", - "download_cancelled": "Descarga cancelada", - "could_not_cancel_download": "No se pudo cancelar la descarga", - "download_completed": "Descarga completada", - "download_started_for": "Descarga iniciada para {{item}}", - "item_is_ready_to_be_downloaded": "{{item}} está listo para ser descargado", - "download_stated_for_item": "Descarga iniciada para {{item}}", - "download_failed_for_item": "Descarga fallida para {{item}} - {{error}}", - "download_completed_for_item": "Descarga completada para {{item}}", - "queued_item_for_optimization": "{{item}} en cola para optimización", - "failed_to_start_download_for_item": "Error al iniciar la descarga para {{item}}: {{message}}", - "server_responded_with_status_code": "El servidor ha respondido con el estado {{statusCode}}", - "no_response_received_from_server": "No se ha recibido respuesta del servidor", - "error_setting_up_the_request": "Error al configurar la petición", - "failed_to_start_download_for_item_unexpected_error": "Error al iniciar la descarga para {{item}}: Error inesperado", - "all_files_folders_and_jobs_deleted_successfully": "Todos los archivos, carpetas y trabajos eliminados con éxito", - "an_error_occured_while_deleting_files_and_jobs": "Ha ocurrido un error al eliminar archivos y trabajos", - "go_to_downloads": "Ir a descargas" - } - } - }, - "search": { - "search_here": "Buscar aquí...", - "search": "Buscar...", - "x_items": "{{count}} ítems", - "library": "Biblioteca", - "discover": "Descubrir", - "no_results": "Sin resultados", - "no_results_found_for": "No se han encontrado resultados para", - "movies": "Películas", - "series": "Series", - "episodes": "Episodios", - "collections": "Colecciones", - "actors": "Actores", - "request_movies": "Solicitar películas", - "request_series": "Solicitar series", - "recently_added": "Recientemente añadido", - "recent_requests": "Solicitudes recientes", - "plex_watchlist": "Lista de seguimiento de Plex", - "trending": "Trending", - "popular_movies": "Películas populares", - "movie_genres": "Géneros de películas", - "upcoming_movies": "Próximas películas", - "studios": "Estudios", - "popular_tv": "Series populares", - "tv_genres": "Géneros de series", - "upcoming_tv": "Próximas series", - "networks": "Cadenas", - "tmdb_movie_keyword": "Palabra clave de película de TMDB", - "tmdb_movie_genre": "Género de película de TMDB", - "tmdb_tv_keyword": "Palabra clave de serie de TMDB", - "tmdb_tv_genre": "Género de serie de TMDB", - "tmdb_search": "Búsqueda de TMDB", - "tmdb_studio": "Estudio de TMDB", - "tmdb_network": "Cadena de TMDB", - "tmdb_movie_streaming_services": "Servicios de streaming de películas de TMDB", - "tmdb_tv_streaming_services": "Servicios de streaming de series de TMDB" - }, - "library": { - "no_items_found": "No se han encontrado ítems", - "no_results": "Sin resultados", - "no_libraries_found": "No se han encontrado bibliotecas", - "item_types": { - "movies": "películas", - "series": "series", - "boxsets": "colecciones", - "items": "ítems" - }, - "options": { - "display": "Mostrar", - "row": "Fila", - "list": "Lista", - "image_style": "Estilo de imagen", - "poster": "Poster", - "cover": "Portada", - "show_titles": "Mostrar títulos", - "show_stats": "Mostrar estadísticas" - }, - "filters": { - "genres": "Géneros", - "years": "Años", - "sort_by": "Ordenar por", - "sort_order": "Ordenar", - "asc": "Ascending", - "desc": "Descending", - "tags": "Etiquetas" - } - }, - "favorites": { - "series": "Series", - "movies": "Películas", - "episodes": "Episodios", - "videos": "Vídeos", - "boxsets": "Colecciones", - "playlists": "Playlists", - "noDataTitle": "Aún no hay favoritos", - "noData": "Marca elementos como favoritos para verlos aparecer aquí para un acceso rápido." - }, - "custom_links": { - "no_links": "Sin enlaces" - }, - "player": { - "error": "Error", - "failed_to_get_stream_url": "Error al obtener la URL del stream", - "an_error_occured_while_playing_the_video": "Ha ocurrido un error al reproducir el vídeo. Comprueba los registros en la configuración.", - "client_error": "Error del cliente", - "could_not_create_stream_for_chromecast": "No se pudo crear el stream para Chromecast", - "message_from_server": "Mensaje del servidor: {{message}}", - "video_has_finished_playing": "El vídeo ha terminado de reproducirse", - "no_video_source": "No hay fuente de vídeo...", - "next_episode": "Siguiente episodio", - "refresh_tracks": "Refrescar pistas", - "subtitle_tracks": "Pistas de subtítulos:", - "audio_tracks": "Pistas de audio:", - "playback_state": "Estado de la reproducción:", - "no_data_available": "No hay datos disponibles", - "index": "Índice:" - }, - "item_card": { - "next_up": "A continuación", - "no_items_to_display": "No hay ítems para mostrar", - "cast_and_crew": "Reparto y equipo", - "series": "Series", - "seasons": "Temporadas", - "season": "Temporada", - "no_episodes_for_this_season": "No hay episodios para esta temporada", - "overview": "Resumen", - "more_with": "Más con {{name}}", - "similar_items": "Ítems similares", - "no_similar_items_found": "No se han encontrado ítems similares", - "video": "Vídeo", - "more_details": "Más detalles", - "quality": "Calidad", - "audio": "Audio", - "subtitles": "Subtítulos", - "show_more": "Mostrar más", - "show_less": "Mostrar menos", - "appeared_in": "Apareció en", - "could_not_load_item": "No se pudo cargar el ítem", - "none": "Ninguno", - "download": { - "download_season": "Descargar temporada", - "download_series": "Descargar serie", - "download_episode": "Descargar episodio", - "download_movie": "Descargar película", - "download_x_item": "Descargar {{item_count}} ítems", - "download_button": "Descargar", - "using_optimized_server": "Usando servidor optimizado", - "using_default_method": "Usando método por defecto" - } - }, - "live_tv": { - "next": "Siguiente", - "previous": "Anterior", - "live_tv": "TV en directo", - "coming_soon": "Próximamente", - "on_now": "En directo", - "shows": "Programas", - "movies": "Películas", - "sports": "Deportes", - "for_kids": "Para niños", - "news": "Noticias" - }, - "jellyseerr": { - "confirm": "Confirmar", - "cancel": "Cancelar", - "yes": "Sí", - "whats_wrong": "¿Qué pasa?", - "issue_type": "Tipo de problema", - "select_an_issue": "Selecciona un problema", - "types": "Tipos", - "describe_the_issue": "(opcional) Describe el problema...", - "submit_button": "Enviar", - "report_issue_button": "Reportar problema", - "request_button": "Solicitar", - "are_you_sure_you_want_to_request_all_seasons": "¿Estás seguro de que quieres solicitar todas las temporadas?", - "failed_to_login": "Error al iniciar sesión", - "cast": "Reparto", - "details": "Detalles", - "status": "Estado", - "original_title": "Título original", - "series_type": "Tipo de serie", - "release_dates": "Fechas de estreno", - "first_air_date": "Primera fecha de emisión", - "next_air_date": "Próxima fecha de emisión", - "revenue": "Ingresos", - "budget": "Presupuesto", - "original_language": "Idioma original", - "production_country": "País de producción", - "studios": "Estudios", - "network": "Cadena", - "currently_streaming_on": "Actualmente en streaming en", - "advanced": "Avanzado", - "request_as": "Solicitar como", - "tags": "Etiquetas", - "quality_profile": "Perfil de calidad", - "root_folder": "Carpeta raíz", - "season_all": "Season (all)", - "season_number": "Temporada {{season_number}}", - "number_episodes": "{{episode_number}} episodios", - "born": "Nacido", - "appearances": "Apariciones", - "toasts": { - "jellyseer_does_not_meet_requirements": "¡Jellyseer no cumple con los requisitos! Por favor, actualízalo al menos a la versión 2.0.0.", - "jellyseerr_test_failed": "La prueba de Jellyseerr ha fallado. Por favor inténtalo de nuevo.", - "failed_to_test_jellyseerr_server_url": "Error al probar la URL del servidor de Jellyseerr", - "issue_submitted": "¡Problema enviado!", - "requested_item": "¡{{item}} solicitado!", - "you_dont_have_permission_to_request": "¡No tienes permiso para solicitar!", - "something_went_wrong_requesting_media": "¡Algo ha salido mal solicitando los medios!" - } - }, - "tabs": { - "home": "Inicio", - "search": "Buscar", - "library": "Bibliotecas", - "custom_links": "Enlaces personalizados", - "favorites": "Favoritos" - } + "login": { + "username_required": "Se requiere un nombre de usuario", + "error_title": "Error", + "login_title": "Iniciar sesión", + "login_to_title": "Iniciar sesión en", + "username_placeholder": "Nombre de usuario", + "password_placeholder": "Contraseña", + "login_button": "Iniciar sesión", + "quick_connect": "Conexión rápida", + "enter_code_to_login": "Introduce el código {{code}} para iniciar sesión", + "failed_to_initiate_quick_connect": "Error al iniciar la conexión rápida", + "got_it": "Entendido", + "connection_failed": "Conexión fallida", + "could_not_connect_to_server": "No se pudo conectar al servidor. Por favor comprueba la URL y tu conexión de red.", + "an_unexpected_error_occured": "Ha ocurrido un error inesperado", + "change_server": "Cambiar servidor", + "invalid_username_or_password": "Usuario o contraseña inválidos", + "user_does_not_have_permission_to_log_in": "El usuario no tiene permiso para iniciar sesión", + "server_is_taking_too_long_to_respond_try_again_later": "El servidor está tardando mucho en responder, inténtalo de nuevo más tarde.", + "server_received_too_many_requests_try_again_later": "El servidor está recibiendo muchas peticiones, inténtalo de nuevo más tarde.", + "there_is_a_server_error": "Hay un error en el servidor", + "an_unexpected_error_occured_did_you_enter_the_correct_url": "Ha ocurrido un error inesperado. ¿Has introducido la URL correcta?" + }, + "server": { + "enter_url_to_jellyfin_server": "Introduce la URL de tu servidor Jellyfin", + "server_url_placeholder": "http(s)://tu-servidor.com", + "connect_button": "Conectar", + "previous_servers": "Servidores previos", + "clear_button": "Limpiar", + "search_for_local_servers": "Buscar servidores locales", + "searching": "Buscando...", + "servers": "Servidores" + }, + "home": { + "no_internet": "Sin internet", + "no_items": "No hay ítems", + "no_internet_message": "No te preocupes, todavía puedes\nver el contenido descargado.", + "go_to_downloads": "Ir a descargas", + "oops": "¡Vaya!", + "error_message": "Algo ha salido mal.\nPor favor, cierra la sesión y vuelve a iniciar.", + "continue_watching": "Seguir viendo", + "next_up": "A continuación", + "recently_added_in": "Recientemente añadido en {{libraryName}}", + "suggested_movies": "Películas sugeridas", + "suggested_episodes": "Episodios sugeridos", + "intro": { + "welcome_to_streamyfin": "Bienvenido a Streamyfin", + "a_free_and_open_source_client_for_jellyfin": "Un cliente gratuito y de código abierto para Jellyfin.", + "features_title": "Características", + "features_description": "Streamyfin tiene una amplia gama de características y se integra con una variedad de software que puedes encontrar en el menú de configuración, esto incluye:", + "jellyseerr_feature_description": "Conéctate a tu servidor de Jellyseer y pide películas directamente desde la app.", + "downloads_feature_title": "Descargas", + "downloads_feature_description": "Descarga películas y series para ver sin conexión. Usa el método por defecto o el servidor optimizado para descargar archivos en segundo plano.", + "chromecast_feature_description": "Envía pelícuas y series a tus dispositivos Chromecast.", + "centralised_settings_plugin_title": "Plugin de configuración centralizada", + "centralised_settings_plugin_description": "Crea configuraciones desde una ubicación centralizada en tu servidor de Jellyfin. Todas las configuraciones para todos los usuarios se sincronizarán automáticamente.", + "done_button": "Hecho", + "go_to_settings_button": "Ir a la configuración", + "read_more": "Leer más" + }, + "settings": { + "settings_title": "Configuración", + "log_out_button": "Cerrar sesión", + "user_info": { + "user_info_title": "Información de usuario", + "user": "Usuario", + "server": "Servidor", + "token": "Token", + "app_version": "Versión de la app" + }, + "quick_connect": { + "quick_connect_title": "Conexión rápida", + "authorize_button": "Autorizar conexión rápida", + "enter_the_quick_connect_code": "Introduce el código de conexión rápida...", + "success": "Hecho", + "quick_connect_autorized": "Conexión rápida autorizada", + "error": "Error", + "invalid_code": "Código inválido", + "authorize": "Autorizar" + }, + "media_controls": { + "media_controls_title": "Controles de reproducción", + "forward_skip_length": "Longitud de avance", + "rewind_length": "Longitud de retroceso", + "seconds_unit": "s" + }, + "audio": { + "audio_title": "Audio", + "set_audio_track": "Establecer pista del elemento anterior", + "audio_language": "Idioma de audio", + "audio_hint": "Elige un idioma de audio por defecto.", + "none": "Ninguno", + "language": "Idioma" + }, + "subtitles": { + "subtitle_title": "Subtítulos", + "subtitle_language": "Idioma de subtítulos", + "subtitle_mode": "Modo de subtítulos", + "set_subtitle_track": "Establecer pista del elemento anterior", + "subtitle_size": "Tamaño de subtítulos", + "subtitle_hint": "Configurar preferencias de subtítulos.", + "none": "Ninguno", + "language": "Idioma", + "loading": "Cargando", + "modes": { + "Default": "Por defecto", + "Smart": "Inteligente", + "Always": "Siempre", + "None": "Nada", + "OnlyForced": "Solo forzados" + } + }, + "other": { + "other_title": "Otros", + "follow_device_orientation": "Rotación automática", + "video_orientation": "Orientación de vídeo", + "orientation": "Orientación", + "orientations": { + "DEFAULT": "Por defecto", + "ALL": "Todas", + "PORTRAIT": "Vertical", + "PORTRAIT_UP": "Vertical arriba", + "PORTRAIT_DOWN": "Vertical abajo", + "LANDSCAPE": "Horizontal", + "LANDSCAPE_LEFT": "Horizontal izquierda", + "LANDSCAPE_RIGHT": "Horizontal derecha", + "OTHER": "Otra", + "UNKNOWN": "Desconocida" + }, + "safe_area_in_controls": "Área segura en controles", + "video_player": "Video player", + "video_players": { + "VLC_3": "VLC 3", + "VLC_4": "VLC 4 (Experimental + PiP)" + }, + "show_custom_menu_links": "Mostrar enlaces de menú personalizados", + "hide_libraries": "Ocultar bibliotecas", + "select_liraries_you_want_to_hide": "Selecciona las bibliotecas que quieres ocultar de la pestaña Bibliotecas y de Inicio.", + "disable_haptic_feedback": "Desactivar feedback háptico", + "default_quality": "Calidad por defecto" + }, + "downloads": { + "downloads_title": "Descargas", + "download_method": "Método de descarga", + "remux_max_download": "Remux máx. descarga", + "auto_download": "Descarga automática", + "optimized_versions_server": "Servidor de versiones optimizadas", + "save_button": "Guardar", + "optimized_server": "Servidor optimizado", + "optimized": "Optimizado", + "default": "Por defecto", + "optimized_version_hint": "Introduce la URL del servidor de versiones optimizadas. La URL debe incluir http o https y opcionalmente el puerto.", + "read_more_about_optimized_server": "Leer más sobre el servidor de versiones optimizadas.", + "url": "URL", + "server_url_placeholder": "http(s)://dominio.org:puerto" + }, + "plugins": { + "plugins_title": "Plugins", + "jellyseerr": { + "jellyseerr_warning": "Esta integración está en sus primeras etapas. Cuenta con posibles cambios.", + "server_url": "URL del servidor", + "server_url_hint": "Ejemplo: http(s)://tu-dominio.url\n(añade el puerto si es necesario)", + "server_url_placeholder": "URL de Jellyseerr...", + "password": "Contrasñea", + "password_placeholder": "Introduce la contraseña de Jellyfin de {{username}}", + "save_button": "Guardar", + "clear_button": "Limpiar", + "login_button": "Iniciar sesión", + "total_media_requests": "Peticiones totales de medios", + "movie_quota_limit": "Límite de cuota de películas", + "movie_quota_days": "Días de cuota de películas", + "tv_quota_limit": "Límite de cuota de series", + "tv_quota_days": "Días de cuota de series", + "reset_jellyseerr_config_button": "Restablecer configuración de Jellyseerr", + "unlimited": "Ilimitado", + "plus_n_more": "+{{n}} more", + "order_by": { + "DEFAULT": "Default", + "VOTE_COUNT_AND_AVERAGE": "Vote count and average", + "POPULARITY": "Popularity" + } + }, + "marlin_search": { + "enable_marlin_search": "Habilitar búsqueda de Marlin", + "url": "URL", + "server_url_placeholder": "http(s)://dominio.org:puerto", + "marlin_search_hint": "Introduce la URL del servidor de Marlin. La URL debe incluir http o https y opcionalmente el puerto.", + "read_more_about_marlin": "Leer más sobre Marlin.", + "save_button": "Guardar", + "toasts": { + "saved": "Guardado" + } + } + }, + "storage": { + "storage_title": "Almacenamiento", + "app_usage": "App {{usedSpace}}%", + "device_usage": "Dispositivo {{availableSpace}}%", + "size_used": "{{used}} de {{total}} usado", + "delete_all_downloaded_files": "Eliminar todos los archivos descargados" + }, + "intro": { + "show_intro": "Mostrar intro", + "reset_intro": "Restablecer intro" + }, + "logs": { + "logs_title": "Registros", + "no_logs_available": "No hay registros disponibles", + "delete_all_logs": "Eliminar todos los registros" + }, + "languages": { + "title": "Idiomas", + "app_language": "Idioma de la app", + "app_language_description": "Selecciona el idioma de la app.", + "system": "Sistema" + }, + "toasts": { + "error_deleting_files": "Error al eliminar archivos", + "background_downloads_enabled": "Descargas en segundo plano habilitadas", + "background_downloads_disabled": "Descargas en segundo plano deshabilitadas", + "connected": "Conectado", + "could_not_connect": "No se pudo conectar", + "invalid_url": "URL inválida" + } + }, + "downloads": { + "downloads_title": "Descargas", + "tvseries": "Series", + "movies": "Películas", + "queue": "Cola", + "queue_hint": "La cola de series y películas se perderá al reiniciar la app", + "no_items_in_queue": "No hay ítems en la cola", + "no_downloaded_items": "No hay ítems descargados", + "delete_all_movies_button": "Eliminar todas las películas", + "delete_all_tvseries_button": "Eliminar todas las series", + "delete_all_button": "Eliminar todo", + "active_download": "Descarga activa", + "no_active_downloads": "No hay descargas activas", + "active_downloads": "Descargas activas", + "new_app_version_requires_re_download": "La nueva actualización requiere volver a descargar", + "new_app_version_requires_re_download_description": "La nueva actualización requiere volver a descargar el contenido. Por favor, elimina todo el código descargado y vuélvelo a intentar.", + "back": "Atrás", + "delete": "Borrar", + "something_went_wrong": "Algo ha salido mal", + "could_not_get_stream_url_from_jellyfin": "No se pudo obtener la URL del stream de Jellyfin", + "eta": "{{eta}} restante", + "methods": "Métodos", + "toasts": { + "you_are_not_allowed_to_download_files": "No tienes permiso para descargar archivos.", + "deleted_all_movies_successfully": "¡Todas las películas eliminadas con éxito!", + "failed_to_delete_all_movies": "Error al eliminar todas las películas", + "deleted_all_tvseries_successfully": "¡Todas las series eliminadas con éxito!", + "failed_to_delete_all_tvseries": "Error al eliminar todas las series", + "download_cancelled": "Descarga cancelada", + "could_not_cancel_download": "No se pudo cancelar la descarga", + "download_completed": "Descarga completada", + "download_started_for": "Descarga iniciada para {{item}}", + "item_is_ready_to_be_downloaded": "{{item}} está listo para ser descargado", + "download_stated_for_item": "Descarga iniciada para {{item}}", + "download_failed_for_item": "Descarga fallida para {{item}} - {{error}}", + "download_completed_for_item": "Descarga completada para {{item}}", + "queued_item_for_optimization": "{{item}} en cola para optimización", + "failed_to_start_download_for_item": "Error al iniciar la descarga para {{item}}: {{message}}", + "server_responded_with_status_code": "El servidor ha respondido con el estado {{statusCode}}", + "no_response_received_from_server": "No se ha recibido respuesta del servidor", + "error_setting_up_the_request": "Error al configurar la petición", + "failed_to_start_download_for_item_unexpected_error": "Error al iniciar la descarga para {{item}}: Error inesperado", + "all_files_folders_and_jobs_deleted_successfully": "Todos los archivos, carpetas y trabajos eliminados con éxito", + "an_error_occured_while_deleting_files_and_jobs": "Ha ocurrido un error al eliminar archivos y trabajos", + "go_to_downloads": "Ir a descargas" + } + } + }, + "search": { + "search_here": "Buscar aquí...", + "search": "Buscar...", + "x_items": "{{count}} ítems", + "library": "Biblioteca", + "discover": "Descubrir", + "no_results": "Sin resultados", + "no_results_found_for": "No se han encontrado resultados para", + "movies": "Películas", + "series": "Series", + "episodes": "Episodios", + "collections": "Colecciones", + "actors": "Actores", + "request_movies": "Solicitar películas", + "request_series": "Solicitar series", + "recently_added": "Recientemente añadido", + "recent_requests": "Solicitudes recientes", + "plex_watchlist": "Lista de seguimiento de Plex", + "trending": "Trending", + "popular_movies": "Películas populares", + "movie_genres": "Géneros de películas", + "upcoming_movies": "Próximas películas", + "studios": "Estudios", + "popular_tv": "Series populares", + "tv_genres": "Géneros de series", + "upcoming_tv": "Próximas series", + "networks": "Cadenas", + "tmdb_movie_keyword": "Palabra clave de película de TMDB", + "tmdb_movie_genre": "Género de película de TMDB", + "tmdb_tv_keyword": "Palabra clave de serie de TMDB", + "tmdb_tv_genre": "Género de serie de TMDB", + "tmdb_search": "Búsqueda de TMDB", + "tmdb_studio": "Estudio de TMDB", + "tmdb_network": "Cadena de TMDB", + "tmdb_movie_streaming_services": "Servicios de streaming de películas de TMDB", + "tmdb_tv_streaming_services": "Servicios de streaming de series de TMDB" + }, + "library": { + "no_items_found": "No se han encontrado ítems", + "no_results": "Sin resultados", + "no_libraries_found": "No se han encontrado bibliotecas", + "item_types": { + "movies": "películas", + "series": "series", + "boxsets": "colecciones", + "items": "ítems" + }, + "options": { + "display": "Mostrar", + "row": "Fila", + "list": "Lista", + "image_style": "Estilo de imagen", + "poster": "Poster", + "cover": "Portada", + "show_titles": "Mostrar títulos", + "show_stats": "Mostrar estadísticas" + }, + "filters": { + "genres": "Géneros", + "years": "Años", + "sort_by": "Ordenar por", + "sort_order": "Ordenar", + "asc": "Ascending", + "desc": "Descending", + "tags": "Etiquetas" + } + }, + "favorites": { + "series": "Series", + "movies": "Películas", + "episodes": "Episodios", + "videos": "Vídeos", + "boxsets": "Colecciones", + "playlists": "Playlists", + "noDataTitle": "Aún no hay favoritos", + "noData": "Marca elementos como favoritos para verlos aparecer aquí para un acceso rápido." + }, + "custom_links": { + "no_links": "Sin enlaces" + }, + "player": { + "error": "Error", + "failed_to_get_stream_url": "Error al obtener la URL del stream", + "an_error_occured_while_playing_the_video": "Ha ocurrido un error al reproducir el vídeo. Comprueba los registros en la configuración.", + "client_error": "Error del cliente", + "could_not_create_stream_for_chromecast": "No se pudo crear el stream para Chromecast", + "message_from_server": "Mensaje del servidor: {{message}}", + "video_has_finished_playing": "El vídeo ha terminado de reproducirse", + "no_video_source": "No hay fuente de vídeo...", + "next_episode": "Siguiente episodio", + "refresh_tracks": "Refrescar pistas", + "subtitle_tracks": "Pistas de subtítulos:", + "audio_tracks": "Pistas de audio:", + "playback_state": "Estado de la reproducción:", + "no_data_available": "No hay datos disponibles", + "index": "Índice:" + }, + "item_card": { + "next_up": "A continuación", + "no_items_to_display": "No hay ítems para mostrar", + "cast_and_crew": "Reparto y equipo", + "series": "Series", + "seasons": "Temporadas", + "season": "Temporada", + "no_episodes_for_this_season": "No hay episodios para esta temporada", + "overview": "Resumen", + "more_with": "Más con {{name}}", + "similar_items": "Ítems similares", + "no_similar_items_found": "No se han encontrado ítems similares", + "video": "Vídeo", + "more_details": "Más detalles", + "quality": "Calidad", + "audio": "Audio", + "subtitles": "Subtítulos", + "show_more": "Mostrar más", + "show_less": "Mostrar menos", + "appeared_in": "Apareció en", + "could_not_load_item": "No se pudo cargar el ítem", + "none": "Ninguno", + "download": { + "download_season": "Descargar temporada", + "download_series": "Descargar serie", + "download_episode": "Descargar episodio", + "download_movie": "Descargar película", + "download_x_item": "Descargar {{item_count}} ítems", + "download_button": "Descargar", + "using_optimized_server": "Usando servidor optimizado", + "using_default_method": "Usando método por defecto" + } + }, + "live_tv": { + "next": "Siguiente", + "previous": "Anterior", + "live_tv": "TV en directo", + "coming_soon": "Próximamente", + "on_now": "En directo", + "shows": "Programas", + "movies": "Películas", + "sports": "Deportes", + "for_kids": "Para niños", + "news": "Noticias" + }, + "jellyseerr": { + "confirm": "Confirmar", + "cancel": "Cancelar", + "yes": "Sí", + "whats_wrong": "¿Qué pasa?", + "issue_type": "Tipo de problema", + "select_an_issue": "Selecciona un problema", + "types": "Tipos", + "describe_the_issue": "(opcional) Describe el problema...", + "submit_button": "Enviar", + "report_issue_button": "Reportar problema", + "request_button": "Solicitar", + "are_you_sure_you_want_to_request_all_seasons": "¿Estás seguro de que quieres solicitar todas las temporadas?", + "failed_to_login": "Error al iniciar sesión", + "cast": "Reparto", + "details": "Detalles", + "status": "Estado", + "original_title": "Título original", + "series_type": "Tipo de serie", + "release_dates": "Fechas de estreno", + "first_air_date": "Primera fecha de emisión", + "next_air_date": "Próxima fecha de emisión", + "revenue": "Ingresos", + "budget": "Presupuesto", + "original_language": "Idioma original", + "production_country": "País de producción", + "studios": "Estudios", + "network": "Cadena", + "currently_streaming_on": "Actualmente en streaming en", + "advanced": "Avanzado", + "request_as": "Solicitar como", + "tags": "Etiquetas", + "quality_profile": "Perfil de calidad", + "root_folder": "Carpeta raíz", + "season_all": "Season (all)", + "season_number": "Temporada {{season_number}}", + "number_episodes": "{{episode_number}} episodios", + "born": "Nacido", + "appearances": "Apariciones", + "toasts": { + "jellyseer_does_not_meet_requirements": "¡Jellyseer no cumple con los requisitos! Por favor, actualízalo al menos a la versión 2.0.0.", + "jellyseerr_test_failed": "La prueba de Jellyseerr ha fallado. Por favor inténtalo de nuevo.", + "failed_to_test_jellyseerr_server_url": "Error al probar la URL del servidor de Jellyseerr", + "issue_submitted": "¡Problema enviado!", + "requested_item": "¡{{item}} solicitado!", + "you_dont_have_permission_to_request": "¡No tienes permiso para solicitar!", + "something_went_wrong_requesting_media": "¡Algo ha salido mal solicitando los medios!" + } + }, + "tabs": { + "home": "Inicio", + "search": "Buscar", + "library": "Bibliotecas", + "custom_links": "Enlaces personalizados", + "favorites": "Favoritos" + } } diff --git a/translations/fr.json b/translations/fr.json index 12f17ef0..1b707c94 100644 --- a/translations/fr.json +++ b/translations/fr.json @@ -1,473 +1,473 @@ { - "login": { - "username_required": "Nom d'utilisateur requis", - "error_title": "Erreur", - "login_title": "Se connecter", - "login_to_title": "Se connecter à", - "username_placeholder": "Nom d'utilisateur", - "password_placeholder": "Mot de passe", - "login_button": "Se connecter", - "quick_connect": "Connexion Rapide", - "enter_code_to_login": "Entrez le code {{code}} pour vous connecter", - "failed_to_initiate_quick_connect": "Échec de l'initialisation de Connexion Rapide", - "got_it": "D'accord", - "connection_failed": "La connexion a échoué", - "could_not_connect_to_server": "Impossible de se connecter au serveur. Veuillez vérifier l'URL et votre connection réseau.", - "an_unexpected_error_occured": "Une erreur inattendue s'est produite", - "change_server": "Changer de serveur", - "invalid_username_or_password": "Nom d'utilisateur ou mot de passe invalide", - "user_does_not_have_permission_to_log_in": "L'utilisateur n'a pas la permission de se connecter", - "server_is_taking_too_long_to_respond_try_again_later": "Le serveur prend trop de temps à répondre, réessayez plus tard", - "server_received_too_many_requests_try_again_later": "Le serveur a reçu trop de demandes, réessayez plus tard", - "there_is_a_server_error": "Il y a une erreur de serveur", - "an_unexpected_error_occured_did_you_enter_the_correct_url": "Une erreur inattendue s'est produite. Avez-vous entré la bonne URL?" - }, - "server": { - "enter_url_to_jellyfin_server": "Entrez l'URL du serveur Jellyfin", - "server_url_placeholder": "http(s)://votre-serveur.com", - "connect_button": "Connexion", - "previous_servers": "Serveurs précédents", - "clear_button": "Effacer", - "search_for_local_servers": "Rechercher des serveurs locaux", - "searching": "Recherche...", - "servers": "Serveurs" - }, - "home": { - "no_internet": "Pas d'Internet", - "no_items": "Aucun média", - "no_internet_message": "Aucun problème, vous pouvez toujours regarder\nle contenu téléchargé.", - "go_to_downloads": "Aller aux téléchargements", - "oops": "Oups!", - "error_message": "Quelque chose s'est mal passé.\nVeuillez vous reconnecter à nouveau.", - "continue_watching": "Continuer à regarder", - "next_up": "À suivre", - "recently_added_in": "Ajoutés récemment dans {{libraryName}}", - "suggested_movies": "Films suggérés", - "suggested_episodes": "Épisodes suggérés", - "intro": { - "welcome_to_streamyfin": "Bienvenue sur Streamyfin", - "a_free_and_open_source_client_for_jellyfin": "Un client gratuit et open source pour Jellyfin", - "features_title": "Fonctionnalités", - "features_description": "Streamyfin possède de nombreuses fonctionnalités et s'intègre à un large éventail de logiciels que vous pouvez trouver dans le menu des paramètres, notamment:", - "jellyseerr_feature_description": "Connectez-vous à votre instance Jellyseerr et demandez des films directement dans l'application.", - "downloads_feature_title": "Téléchargements", - "downloads_feature_description": "Téléchargez des films et des émissions de télévision pour les regarder hors ligne. Utilisez la méthode par défaut ou installez le serveur d'optimisation pour télécharger les fichiers en arrière-plan.", - "chromecast_feature_description": "Diffusez des films et des émissions de télévision sur vos appareils Chromecast.", - "centralised_settings_plugin_title": "Plugin de paramètres centralisés", - "centralised_settings_plugin_description": "Configuration des paramètres d'un emplacement centralisé sur votre serveur Jellyfin. Tous les paramètres clients pour tous les utilisateurs seront synchronisés automatiquement.", - "done_button": "Terminé", - "go_to_settings_button": "Allez dans les paramètres", - "read_more": "Lisez-en plus" - }, - "settings": { - "settings_title": "Paramètres", - "log_out_button": "Déconnexion", - "user_info": { - "user_info_title": "Informations utilisateur", - "user": "Utilisateur", - "server": "Serveur", - "token": "Jeton", - "app_version": "Version de l'application" - }, - "quick_connect": { - "quick_connect_title": "Connexion Rapide", - "authorize_button": "Autoriser Connexion Rapide", - "enter_the_quick_connect_code": "Entrez le code Connexion Rapide...", - "success": "Succès", - "quick_connect_autorized": "Connexion Rapide autorisé", - "error": "Erreur", - "invalid_code": "Code invalide", - "authorize": "Autoriser" - }, - "media_controls": { - "media_controls_title": "Contrôles Média", - "forward_skip_length": "Durée de saut en avant", - "rewind_length": "Durée de retour en arrière", - "seconds_unit": "s" - }, - "audio": { - "audio_title": "Audio", - "set_audio_track": "Piste audio de l'élément précédent", - "audio_language": "Langue audio", - "audio_hint": "Choisissez une langue audio par défaut.", - "none": "Aucune", - "language": "Langage" - }, - "subtitles": { - "subtitle_title": "Sous-titres", - "subtitle_language": "Langue des sous-titres", - "subtitle_mode": "Mode des sous-titres", - "set_subtitle_track": "Piste de sous-titres de l'élément précédent", - "subtitle_size": "Taille des sous-titres", - "subtitle_hint": "Configurez les préférences des sous-titres.", - "none": "Aucune", - "language": "Langage", - "loading": "Chargement", - "modes": { - "Default": "Par défaut", - "Smart": "Intelligent", - "Always": "Toujours", - "None": "Aucun", - "OnlyForced": "Forcés seulement" - } - }, - "other": { - "other_title": "Autres", - "follow_device_orientation": "Rotation automatique", - "video_orientation": "Orientation vidéo", - "orientation": "Orientation", - "orientations": { - "DEFAULT": "Par défaut", - "ALL": "Toutes", - "PORTRAIT": "Portrait", - "PORTRAIT_UP": "Portrait Haut", - "PORTRAIT_DOWN": "Portrait Bas", - "LANDSCAPE": "Paysage", - "LANDSCAPE_LEFT": "Paysage Gauche", - "LANDSCAPE_RIGHT": "Paysage Droite", - "OTHER": "Autre", - "UNKNOWN": "Inconnu" - }, - "safe_area_in_controls": "Zone de sécurité dans les contrôles", - "video_player": "Video player", - "video_players": { - "VLC_3": "VLC 3", - "VLC_4": "VLC 4 (Experimental + PiP)" - }, - "show_custom_menu_links": "Afficher les liens personnalisés", - "hide_libraries": "Cacher des bibliothèques", - "select_liraries_you_want_to_hide": "Sélectionnez les bibliothèques que vous souhaitez masquer dans l'onglet Bibliothèque et les sections de la page d'accueil.", - "disable_haptic_feedback": "Désactiver le retour haptique", - "default_quality": "Qualité par défaut" - }, - "downloads": { - "downloads_title": "Téléchargements", - "download_method": "Méthode de téléchargement", - "remux_max_download": "Téléchargement max remux", - "auto_download": "Téléchargement automatique", - "optimized_versions_server": "Serveur de versions optimisées", - "save_button": "Enregistrer", - "optimized_server": "Serveur optimisé", - "optimized": "Optimisé", - "default": "Par défaut", - "optimized_version_hint": "Entrez l'URL du serveur de versions optimisées. L'URL devrait inclure http ou https et optionnellement le port.", - "read_more_about_optimized_server": "Lisez-en plus sur le serveur de versions optimisées.", - "url": "URL", - "server_url_placeholder": "http(s)://domaine.org:port" - }, - "plugins": { - "plugins_title": "Plugins", - "jellyseerr": { - "jellyseerr_warning": "Cette intégration est dans ses débuts. Attendez-vous à ce que des choses changent.", - "server_url": "URL du serveur", - "server_url_hint": "Exemple: http(s)://votre-domaine.url\n(ajouter le port si nécessaire)", - "server_url_placeholder": "URL de Jellyseerr...", - "password": "Mot de passe", - "password_placeholder": "Entrez le mot de passe pour l'utilisateur Jellyfin {{username}}", - "save_button": "Enregistrer", - "clear_button": "Effacer", - "login_button": "Connexion", - "total_media_requests": "Total de demandes de médias", - "movie_quota_limit": "Limite de quota de film", - "movie_quota_days": "Jours de quota de film", - "tv_quota_limit": "Limite de quota TV", - "tv_quota_days": "Jours de quota TV", - "reset_jellyseerr_config_button": "Réinitialiser la configuration Jellyseerr", - "unlimited": "Illimité", - "plus_n_more": "+{{n}} more", - "order_by": { - "DEFAULT": "Default", - "VOTE_COUNT_AND_AVERAGE": "Vote count and average", - "POPULARITY": "Popularity" - } - }, - "marlin_search": { - "enable_marlin_search": "Activer Marlin Search ", - "url": "URL", - "server_url_placeholder": "http(s)://domaine.org:port", - "marlin_search_hint": "Entrez l'URL du serveur Marlin. L'URL devrait inclure http ou https et optionnellement le port.", - "read_more_about_marlin": "Lisez-en plus sur Marlin.", - "save_button": "Enregistrer", - "toasts": { - "saved": "Enregistré" - } - } - }, - "storage": { - "storage_title": "Stockage", - "app_usage": "App {{usedSpace}}%", - "device_usage": "Appareil {{availableSpace}}%", - "size_used": "{{used}} de {{total}} utilisés", - "delete_all_downloaded_files": "Supprimer tous les fichiers téléchargés" - }, - "intro": { - "show_intro": "Afficher l'intro", - "reset_intro": "Réinitialiser l'intro" - }, - "logs": { - "logs_title": "Journaux", - "no_logs_available": "Aucun journal disponible", - "delete_all_logs": "Supprimer tous les journaux" - }, - "languages": { - "title": "Langues", - "app_language": "Langue de l'application", - "app_language_description": "Sélectionnez la langue de l'application", - "system": "Système" - }, - "toasts": { - "error_deleting_files": "Erreur lors de la suppression des fichiers", - "background_downloads_enabled": "Téléchargements en arrière-plan activés", - "background_downloads_disabled": "Téléchargements en arrière-plan désactivés", - "connected": "Connecté", - "could_not_connect": "Impossible de se connecter", - "invalid_url": "URL invalide" - } - }, - "downloads": { - "downloads_title": "Téléchargements", - "tvseries": "Séries TV", - "movies": "Films", - "queue": "File d'attente", - "queue_hint": "La file d'attente et les téléchargements seront perdus au redémarrage de l'application", - "no_items_in_queue": "Aucun téléchargement de média dans la file d'attente", - "no_downloaded_items": "Aucun média téléchargé", - "delete_all_movies_button": "Supprimer tous les films", - "delete_all_tvseries_button": "Supprimer toutes les séries", - "delete_all_button": "Supprimer tout les médias", - "active_download": "Téléchargement actif", - "no_active_downloads": "Aucun téléchargements actifs", - "active_downloads": "Téléchargements actifs", - "new_app_version_requires_re_download": "La nouvelle version de l'application nécessite un nouveau téléchargement", - "new_app_version_requires_re_download_description": "Une nouvelle version de l'application est disponible. Veuillez supprimer tous les téléchargements et redémarrer l'application pour télécharger à nouveau", - "back": "Retour", - "delete": "Supprimer", - "something_went_wrong": "Quelque chose s'est mal passé", - "could_not_get_stream_url_from_jellyfin": "Impossible d'obtenir l'URL du flux depuis Jellyfin", - "eta": "ETA {{eta}}", - "methods": "Méthodes", - "toasts": { - "you_are_not_allowed_to_download_files": "Vous n'êtes pas autorisé à télécharger des fichiers", - "deleted_all_movies_successfully": "Tous les films ont été supprimés avec succès!", - "failed_to_delete_all_movies": "Échec de la suppression de tous les films", - "deleted_all_tvseries_successfully": "Toutes les séries ont été supprimées avec succès!", - "failed_to_delete_all_tvseries": "Échec de la suppression de toutes les séries", - "download_cancelled": "Téléchargement annulé", - "could_not_cancel_download": "Impossible d'annuler le téléchargement", - "download_completed": "Téléchargement terminé", - "download_started_for": "Téléchargement démarré pour {{item}}", - "item_is_ready_to_be_downloaded": "{{item}} est prêt à être téléchargé", - "download_stated_for_item": "Téléchargement démarré pour {{item}}", - "download_failed_for_item": "Échec du téléchargement pour {{item}} - {{error}}", - "download_completed_for_item": "Téléchargement terminé pour {{item}}", - "queued_item_for_optimization": "{{item}} mis en file d'attente pour l'optimisation", - "failed_to_start_download_for_item": "Échec du démarrage du téléchargement pour {{item}}: {{message}}", - "server_responded_with_status_code": "Le serveur a répondu avec le code de statut {{statusCode}}", - "no_response_received_from_server": "Aucune réponse reçue du serveur", - "error_setting_up_the_request": "Erreur lors de la configuration de la demande", - "failed_to_start_download_for_item_unexpected_error": "Échec du démarrage du téléchargement pour {{item}}: Erreur inattendue", - "all_files_folders_and_jobs_deleted_successfully": "Tous les fichiers, dossiers et tâches ont été supprimés avec succès", - "an_error_occured_while_deleting_files_and_jobs": "Une erreur s'est produite lors de la suppression des fichiers et des tâches", - "go_to_downloads": "Aller aux téléchargements" - } - } - }, - "search": { - "search_here": "Rechercher ici...", - "search": "Rechercher...", - "x_items": "{{count}} médias", - "library": "Bibliothèque", - "discover": "Découvrir", - "no_results": "Aucun résultat", - "no_results_found_for": "Aucun résultat trouvé pour", - "movies": "Films", - "series": "Séries", - "episodes": "Épisodes", - "collections": "Collections", - "actors": "Acteurs", - "request_movies": "Demander un film", - "request_series": "Demander une série", - "recently_added": "Ajoutés récemment", - "recent_requests": "Demandes récentes", - "plex_watchlist": "Liste de lecture Plex", - "trending": "Tendance", - "popular_movies": "Films populaires", - "movie_genres": "Genres de films", - "upcoming_movies": "Films à venir", - "studios": "Studios", - "popular_tv": "TV populaire", - "tv_genres": "Genres TV", - "upcoming_tv": "TV à venir", - "networks": "Réseaux", - "tmdb_movie_keyword": "Mot(s)-clé(s) Films TMDB", - "tmdb_movie_genre": "Genre de film TMDB", - "tmdb_tv_keyword": "Mot(s)-clé(s) TV TMDB", - "tmdb_tv_genre": "Genre TV TMDB", - "tmdb_search": "Recherche TMDB", - "tmdb_studio": "Studio TMDB", - "tmdb_network": "Réseau TMDB", - "tmdb_movie_streaming_services": "Services de streaming de films TMDB", - "tmdb_tv_streaming_services": "Services de streaming TV TMDB" - }, - "library": { - "no_items_found": "Aucun média trouvé", - "no_results": "Aucun résultat", - "no_libraries_found": "Aucune bibliothèque trouvée", - "item_types": { - "movies": "films", - "series": "séries", - "boxsets": "coffrets", - "items": "médias" - }, - "options": { - "display": "Affichage", - "row": "Rangée", - "list": "Liste", - "image_style": "Style d'image", - "poster": "Affiche", - "cover": "Couverture", - "show_titles": "Afficher les titres", - "show_stats": "Afficher les statistiques" - }, - "filters": { - "genres": "Genres", - "years": "Années", - "sort_by": "Trier par", - "sort_order": "Ordre de tri", - "asc": "Ascending", - "desc": "Descending", - "tags": "Tags" - } - }, - "favorites": { - "series": "Séries", - "movies": "Films", - "episodes": "Épisodes", - "videos": "Vidéos", - "boxsets": "Coffrets", - "playlists": "Listes de lecture", - "noDataTitle": "Pas encore de favoris", - "noData": "Marquez des éléments comme favoris pour les voir apparaître ici pour un accès rapide." - }, - "custom_links": { - "no_links": "Aucuns liens" - }, - "player": { - "error": "Erreur", - "failed_to_get_stream_url": "Échec de l'obtention de l'URL du flux", - "an_error_occured_while_playing_the_video": "Une erreur s'est produite lors de la lecture de la vidéo", - "client_error": "Erreur client", - "could_not_create_stream_for_chromecast": "Impossible de créer un flux sur la Chromecast", - "message_from_server": "Message du serveur: {{message}}", - "video_has_finished_playing": "La vidéo a fini de jouer!", - "no_video_source": "Aucune source vidéo...", - "next_episode": "Épisode suivant", - "refresh_tracks": "Rafraîchir les pistes", - "subtitle_tracks": "Pistes de sous-titres:", - "audio_tracks": "Pistes audio:", - "playback_state": "État de lecture:", - "no_data_available": "Aucune donnée disponible", - "index": "Index:" - }, - "item_card": { - "next_up": "À suivre", - "no_items_to_display": "Aucun médias à afficher", - "cast_and_crew": "Distribution et équipe", - "series": "Séries", - "seasons": "Saisons", - "season": "Saison", - "no_episodes_for_this_season": "Aucun épisode pour cette saison", - "overview": "Aperçu", - "more_with": "Plus avec {{name}}", - "similar_items": "Médias similaires", - "no_similar_items_found": "Aucun média similaire trouvé", - "video": "Vidéo", - "more_details": "Plus de détails", - "quality": "Qualité", - "audio": "Audio", - "subtitles": "Sous-titres", - "show_more": "Afficher plus", - "show_less": "Afficher moins", - "appeared_in": "Apparu dans", - "could_not_load_item": "Impossible de charger le média", - "none": "Aucun", - "download": { - "download_season": "Télécharger la saison", - "download_series": "Télécharger la série", - "download_episode": "Télécharger l'épisode", - "download_movie": "Télécharger le film", - "download_x_item": "Télécharger {{item_count}} médias", - "download_button": "Télécharger", - "using_optimized_server": "Avec le serveur optimisées", - "using_default_method": "Avec la méthode par défaut" - } - }, - "live_tv": { - "next": "Suivant", - "previous": "Précédent", - "live_tv": "TV en direct", - "coming_soon": "Bientôt", - "on_now": "En ce moment", - "shows": "Émissions", - "movies": "Films", - "sports": "Sports", - "for_kids": "Pour enfants", - "news": "Actualités" - }, - "jellyseerr": { - "confirm": "Confirmer", - "cancel": "Annuler", - "yes": "Oui", - "whats_wrong": "Quel est le problème?", - "issue_type": "Type de problème", - "select_an_issue": "Sélectionnez un problème", - "types": "Types", - "describe_the_issue": "(optionnel) Décrivez le problème...", - "submit_button": "Soumettre", - "report_issue_button": "Signaler un problème", - "request_button": "Demander", - "are_you_sure_you_want_to_request_all_seasons": "Êtes-vous sûr de vouloir demander toutes les saisons?", - "failed_to_login": "Échec de la connexion", - "cast": "Distribution", - "details": "Détails", - "status": "Statut", - "original_title": "Titre original", - "series_type": "Type de série", - "release_dates": "Dates de sortie", - "first_air_date": "Date de première diffusion", - "next_air_date": "Date de prochaine diffusion", - "revenue": "Revenu", - "budget": "Budget", - "original_language": "Langue originale", - "production_country": "Pays de production", - "studios": "Studios", - "network": "Réseaux", - "currently_streaming_on": "En streaming sur", - "advanced": "Avancé", - "request_as": "Demander en tant que", - "tags": "Tags", - "quality_profile": "Profil de qualité", - "root_folder": "Dossier racine", - "season_all": "Season (all)", - "season_number": "Saison {{season_number}}", - "number_episodes": "{{episode_number}} épisodes", - "born": "Né(e) le", - "appearances": "Apparences", - "toasts": { - "jellyseer_does_not_meet_requirements": "Jellyseer ne répond pas aux exigences! Veuillez mettre à jour au moins vers la version 2.0.0.", - "jellyseerr_test_failed": "Échec du test de Jellyseerr", - "failed_to_test_jellyseerr_server_url": "Échec du test de l'URL du serveur Jellyseerr", - "issue_submitted": "Problème soumis!", - "requested_item": "{{item}}} demandé!", - "you_dont_have_permission_to_request": "Vous n'avez pas la permission de demander {{item}}", - "something_went_wrong_requesting_media": "Quelque chose s'est mal passé en demandant le média!" - } - }, - "tabs": { - "home": "Accueil", - "search": "Recherche", - "library": "Bibliothèque", - "custom_links": "Liens personnalisés", - "favorites": "Favoris" - } + "login": { + "username_required": "Nom d'utilisateur requis", + "error_title": "Erreur", + "login_title": "Se connecter", + "login_to_title": "Se connecter à", + "username_placeholder": "Nom d'utilisateur", + "password_placeholder": "Mot de passe", + "login_button": "Se connecter", + "quick_connect": "Connexion Rapide", + "enter_code_to_login": "Entrez le code {{code}} pour vous connecter", + "failed_to_initiate_quick_connect": "Échec de l'initialisation de Connexion Rapide", + "got_it": "D'accord", + "connection_failed": "La connexion a échoué", + "could_not_connect_to_server": "Impossible de se connecter au serveur. Veuillez vérifier l'URL et votre connection réseau.", + "an_unexpected_error_occured": "Une erreur inattendue s'est produite", + "change_server": "Changer de serveur", + "invalid_username_or_password": "Nom d'utilisateur ou mot de passe invalide", + "user_does_not_have_permission_to_log_in": "L'utilisateur n'a pas la permission de se connecter", + "server_is_taking_too_long_to_respond_try_again_later": "Le serveur prend trop de temps à répondre, réessayez plus tard", + "server_received_too_many_requests_try_again_later": "Le serveur a reçu trop de demandes, réessayez plus tard", + "there_is_a_server_error": "Il y a une erreur de serveur", + "an_unexpected_error_occured_did_you_enter_the_correct_url": "Une erreur inattendue s'est produite. Avez-vous entré la bonne URL?" + }, + "server": { + "enter_url_to_jellyfin_server": "Entrez l'URL du serveur Jellyfin", + "server_url_placeholder": "http(s)://votre-serveur.com", + "connect_button": "Connexion", + "previous_servers": "Serveurs précédents", + "clear_button": "Effacer", + "search_for_local_servers": "Rechercher des serveurs locaux", + "searching": "Recherche...", + "servers": "Serveurs" + }, + "home": { + "no_internet": "Pas d'Internet", + "no_items": "Aucun média", + "no_internet_message": "Aucun problème, vous pouvez toujours regarder\nle contenu téléchargé.", + "go_to_downloads": "Aller aux téléchargements", + "oops": "Oups!", + "error_message": "Quelque chose s'est mal passé.\nVeuillez vous reconnecter à nouveau.", + "continue_watching": "Continuer à regarder", + "next_up": "À suivre", + "recently_added_in": "Ajoutés récemment dans {{libraryName}}", + "suggested_movies": "Films suggérés", + "suggested_episodes": "Épisodes suggérés", + "intro": { + "welcome_to_streamyfin": "Bienvenue sur Streamyfin", + "a_free_and_open_source_client_for_jellyfin": "Un client gratuit et open source pour Jellyfin", + "features_title": "Fonctionnalités", + "features_description": "Streamyfin possède de nombreuses fonctionnalités et s'intègre à un large éventail de logiciels que vous pouvez trouver dans le menu des paramètres, notamment:", + "jellyseerr_feature_description": "Connectez-vous à votre instance Jellyseerr et demandez des films directement dans l'application.", + "downloads_feature_title": "Téléchargements", + "downloads_feature_description": "Téléchargez des films et des émissions de télévision pour les regarder hors ligne. Utilisez la méthode par défaut ou installez le serveur d'optimisation pour télécharger les fichiers en arrière-plan.", + "chromecast_feature_description": "Diffusez des films et des émissions de télévision sur vos appareils Chromecast.", + "centralised_settings_plugin_title": "Plugin de paramètres centralisés", + "centralised_settings_plugin_description": "Configuration des paramètres d'un emplacement centralisé sur votre serveur Jellyfin. Tous les paramètres clients pour tous les utilisateurs seront synchronisés automatiquement.", + "done_button": "Terminé", + "go_to_settings_button": "Allez dans les paramètres", + "read_more": "Lisez-en plus" + }, + "settings": { + "settings_title": "Paramètres", + "log_out_button": "Déconnexion", + "user_info": { + "user_info_title": "Informations utilisateur", + "user": "Utilisateur", + "server": "Serveur", + "token": "Jeton", + "app_version": "Version de l'application" + }, + "quick_connect": { + "quick_connect_title": "Connexion Rapide", + "authorize_button": "Autoriser Connexion Rapide", + "enter_the_quick_connect_code": "Entrez le code Connexion Rapide...", + "success": "Succès", + "quick_connect_autorized": "Connexion Rapide autorisé", + "error": "Erreur", + "invalid_code": "Code invalide", + "authorize": "Autoriser" + }, + "media_controls": { + "media_controls_title": "Contrôles Média", + "forward_skip_length": "Durée de saut en avant", + "rewind_length": "Durée de retour en arrière", + "seconds_unit": "s" + }, + "audio": { + "audio_title": "Audio", + "set_audio_track": "Piste audio de l'élément précédent", + "audio_language": "Langue audio", + "audio_hint": "Choisissez une langue audio par défaut.", + "none": "Aucune", + "language": "Langage" + }, + "subtitles": { + "subtitle_title": "Sous-titres", + "subtitle_language": "Langue des sous-titres", + "subtitle_mode": "Mode des sous-titres", + "set_subtitle_track": "Piste de sous-titres de l'élément précédent", + "subtitle_size": "Taille des sous-titres", + "subtitle_hint": "Configurez les préférences des sous-titres.", + "none": "Aucune", + "language": "Langage", + "loading": "Chargement", + "modes": { + "Default": "Par défaut", + "Smart": "Intelligent", + "Always": "Toujours", + "None": "Aucun", + "OnlyForced": "Forcés seulement" + } + }, + "other": { + "other_title": "Autres", + "follow_device_orientation": "Rotation automatique", + "video_orientation": "Orientation vidéo", + "orientation": "Orientation", + "orientations": { + "DEFAULT": "Par défaut", + "ALL": "Toutes", + "PORTRAIT": "Portrait", + "PORTRAIT_UP": "Portrait Haut", + "PORTRAIT_DOWN": "Portrait Bas", + "LANDSCAPE": "Paysage", + "LANDSCAPE_LEFT": "Paysage Gauche", + "LANDSCAPE_RIGHT": "Paysage Droite", + "OTHER": "Autre", + "UNKNOWN": "Inconnu" + }, + "safe_area_in_controls": "Zone de sécurité dans les contrôles", + "video_player": "Video player", + "video_players": { + "VLC_3": "VLC 3", + "VLC_4": "VLC 4 (Experimental + PiP)" + }, + "show_custom_menu_links": "Afficher les liens personnalisés", + "hide_libraries": "Cacher des bibliothèques", + "select_liraries_you_want_to_hide": "Sélectionnez les bibliothèques que vous souhaitez masquer dans l'onglet Bibliothèque et les sections de la page d'accueil.", + "disable_haptic_feedback": "Désactiver le retour haptique", + "default_quality": "Qualité par défaut" + }, + "downloads": { + "downloads_title": "Téléchargements", + "download_method": "Méthode de téléchargement", + "remux_max_download": "Téléchargement max remux", + "auto_download": "Téléchargement automatique", + "optimized_versions_server": "Serveur de versions optimisées", + "save_button": "Enregistrer", + "optimized_server": "Serveur optimisé", + "optimized": "Optimisé", + "default": "Par défaut", + "optimized_version_hint": "Entrez l'URL du serveur de versions optimisées. L'URL devrait inclure http ou https et optionnellement le port.", + "read_more_about_optimized_server": "Lisez-en plus sur le serveur de versions optimisées.", + "url": "URL", + "server_url_placeholder": "http(s)://domaine.org:port" + }, + "plugins": { + "plugins_title": "Plugins", + "jellyseerr": { + "jellyseerr_warning": "Cette intégration est dans ses débuts. Attendez-vous à ce que des choses changent.", + "server_url": "URL du serveur", + "server_url_hint": "Exemple: http(s)://votre-domaine.url\n(ajouter le port si nécessaire)", + "server_url_placeholder": "URL de Jellyseerr...", + "password": "Mot de passe", + "password_placeholder": "Entrez le mot de passe pour l'utilisateur Jellyfin {{username}}", + "save_button": "Enregistrer", + "clear_button": "Effacer", + "login_button": "Connexion", + "total_media_requests": "Total de demandes de médias", + "movie_quota_limit": "Limite de quota de film", + "movie_quota_days": "Jours de quota de film", + "tv_quota_limit": "Limite de quota TV", + "tv_quota_days": "Jours de quota TV", + "reset_jellyseerr_config_button": "Réinitialiser la configuration Jellyseerr", + "unlimited": "Illimité", + "plus_n_more": "+{{n}} more", + "order_by": { + "DEFAULT": "Default", + "VOTE_COUNT_AND_AVERAGE": "Vote count and average", + "POPULARITY": "Popularity" + } + }, + "marlin_search": { + "enable_marlin_search": "Activer Marlin Search ", + "url": "URL", + "server_url_placeholder": "http(s)://domaine.org:port", + "marlin_search_hint": "Entrez l'URL du serveur Marlin. L'URL devrait inclure http ou https et optionnellement le port.", + "read_more_about_marlin": "Lisez-en plus sur Marlin.", + "save_button": "Enregistrer", + "toasts": { + "saved": "Enregistré" + } + } + }, + "storage": { + "storage_title": "Stockage", + "app_usage": "App {{usedSpace}}%", + "device_usage": "Appareil {{availableSpace}}%", + "size_used": "{{used}} de {{total}} utilisés", + "delete_all_downloaded_files": "Supprimer tous les fichiers téléchargés" + }, + "intro": { + "show_intro": "Afficher l'intro", + "reset_intro": "Réinitialiser l'intro" + }, + "logs": { + "logs_title": "Journaux", + "no_logs_available": "Aucun journal disponible", + "delete_all_logs": "Supprimer tous les journaux" + }, + "languages": { + "title": "Langues", + "app_language": "Langue de l'application", + "app_language_description": "Sélectionnez la langue de l'application", + "system": "Système" + }, + "toasts": { + "error_deleting_files": "Erreur lors de la suppression des fichiers", + "background_downloads_enabled": "Téléchargements en arrière-plan activés", + "background_downloads_disabled": "Téléchargements en arrière-plan désactivés", + "connected": "Connecté", + "could_not_connect": "Impossible de se connecter", + "invalid_url": "URL invalide" + } + }, + "downloads": { + "downloads_title": "Téléchargements", + "tvseries": "Séries TV", + "movies": "Films", + "queue": "File d'attente", + "queue_hint": "La file d'attente et les téléchargements seront perdus au redémarrage de l'application", + "no_items_in_queue": "Aucun téléchargement de média dans la file d'attente", + "no_downloaded_items": "Aucun média téléchargé", + "delete_all_movies_button": "Supprimer tous les films", + "delete_all_tvseries_button": "Supprimer toutes les séries", + "delete_all_button": "Supprimer tout les médias", + "active_download": "Téléchargement actif", + "no_active_downloads": "Aucun téléchargements actifs", + "active_downloads": "Téléchargements actifs", + "new_app_version_requires_re_download": "La nouvelle version de l'application nécessite un nouveau téléchargement", + "new_app_version_requires_re_download_description": "Une nouvelle version de l'application est disponible. Veuillez supprimer tous les téléchargements et redémarrer l'application pour télécharger à nouveau", + "back": "Retour", + "delete": "Supprimer", + "something_went_wrong": "Quelque chose s'est mal passé", + "could_not_get_stream_url_from_jellyfin": "Impossible d'obtenir l'URL du flux depuis Jellyfin", + "eta": "ETA {{eta}}", + "methods": "Méthodes", + "toasts": { + "you_are_not_allowed_to_download_files": "Vous n'êtes pas autorisé à télécharger des fichiers", + "deleted_all_movies_successfully": "Tous les films ont été supprimés avec succès!", + "failed_to_delete_all_movies": "Échec de la suppression de tous les films", + "deleted_all_tvseries_successfully": "Toutes les séries ont été supprimées avec succès!", + "failed_to_delete_all_tvseries": "Échec de la suppression de toutes les séries", + "download_cancelled": "Téléchargement annulé", + "could_not_cancel_download": "Impossible d'annuler le téléchargement", + "download_completed": "Téléchargement terminé", + "download_started_for": "Téléchargement démarré pour {{item}}", + "item_is_ready_to_be_downloaded": "{{item}} est prêt à être téléchargé", + "download_stated_for_item": "Téléchargement démarré pour {{item}}", + "download_failed_for_item": "Échec du téléchargement pour {{item}} - {{error}}", + "download_completed_for_item": "Téléchargement terminé pour {{item}}", + "queued_item_for_optimization": "{{item}} mis en file d'attente pour l'optimisation", + "failed_to_start_download_for_item": "Échec du démarrage du téléchargement pour {{item}}: {{message}}", + "server_responded_with_status_code": "Le serveur a répondu avec le code de statut {{statusCode}}", + "no_response_received_from_server": "Aucune réponse reçue du serveur", + "error_setting_up_the_request": "Erreur lors de la configuration de la demande", + "failed_to_start_download_for_item_unexpected_error": "Échec du démarrage du téléchargement pour {{item}}: Erreur inattendue", + "all_files_folders_and_jobs_deleted_successfully": "Tous les fichiers, dossiers et tâches ont été supprimés avec succès", + "an_error_occured_while_deleting_files_and_jobs": "Une erreur s'est produite lors de la suppression des fichiers et des tâches", + "go_to_downloads": "Aller aux téléchargements" + } + } + }, + "search": { + "search_here": "Rechercher ici...", + "search": "Rechercher...", + "x_items": "{{count}} médias", + "library": "Bibliothèque", + "discover": "Découvrir", + "no_results": "Aucun résultat", + "no_results_found_for": "Aucun résultat trouvé pour", + "movies": "Films", + "series": "Séries", + "episodes": "Épisodes", + "collections": "Collections", + "actors": "Acteurs", + "request_movies": "Demander un film", + "request_series": "Demander une série", + "recently_added": "Ajoutés récemment", + "recent_requests": "Demandes récentes", + "plex_watchlist": "Liste de lecture Plex", + "trending": "Tendance", + "popular_movies": "Films populaires", + "movie_genres": "Genres de films", + "upcoming_movies": "Films à venir", + "studios": "Studios", + "popular_tv": "TV populaire", + "tv_genres": "Genres TV", + "upcoming_tv": "TV à venir", + "networks": "Réseaux", + "tmdb_movie_keyword": "Mot(s)-clé(s) Films TMDB", + "tmdb_movie_genre": "Genre de film TMDB", + "tmdb_tv_keyword": "Mot(s)-clé(s) TV TMDB", + "tmdb_tv_genre": "Genre TV TMDB", + "tmdb_search": "Recherche TMDB", + "tmdb_studio": "Studio TMDB", + "tmdb_network": "Réseau TMDB", + "tmdb_movie_streaming_services": "Services de streaming de films TMDB", + "tmdb_tv_streaming_services": "Services de streaming TV TMDB" + }, + "library": { + "no_items_found": "Aucun média trouvé", + "no_results": "Aucun résultat", + "no_libraries_found": "Aucune bibliothèque trouvée", + "item_types": { + "movies": "films", + "series": "séries", + "boxsets": "coffrets", + "items": "médias" + }, + "options": { + "display": "Affichage", + "row": "Rangée", + "list": "Liste", + "image_style": "Style d'image", + "poster": "Affiche", + "cover": "Couverture", + "show_titles": "Afficher les titres", + "show_stats": "Afficher les statistiques" + }, + "filters": { + "genres": "Genres", + "years": "Années", + "sort_by": "Trier par", + "sort_order": "Ordre de tri", + "asc": "Ascending", + "desc": "Descending", + "tags": "Tags" + } + }, + "favorites": { + "series": "Séries", + "movies": "Films", + "episodes": "Épisodes", + "videos": "Vidéos", + "boxsets": "Coffrets", + "playlists": "Listes de lecture", + "noDataTitle": "Pas encore de favoris", + "noData": "Marquez des éléments comme favoris pour les voir apparaître ici pour un accès rapide." + }, + "custom_links": { + "no_links": "Aucuns liens" + }, + "player": { + "error": "Erreur", + "failed_to_get_stream_url": "Échec de l'obtention de l'URL du flux", + "an_error_occured_while_playing_the_video": "Une erreur s'est produite lors de la lecture de la vidéo", + "client_error": "Erreur client", + "could_not_create_stream_for_chromecast": "Impossible de créer un flux sur la Chromecast", + "message_from_server": "Message du serveur: {{message}}", + "video_has_finished_playing": "La vidéo a fini de jouer!", + "no_video_source": "Aucune source vidéo...", + "next_episode": "Épisode suivant", + "refresh_tracks": "Rafraîchir les pistes", + "subtitle_tracks": "Pistes de sous-titres:", + "audio_tracks": "Pistes audio:", + "playback_state": "État de lecture:", + "no_data_available": "Aucune donnée disponible", + "index": "Index:" + }, + "item_card": { + "next_up": "À suivre", + "no_items_to_display": "Aucun médias à afficher", + "cast_and_crew": "Distribution et équipe", + "series": "Séries", + "seasons": "Saisons", + "season": "Saison", + "no_episodes_for_this_season": "Aucun épisode pour cette saison", + "overview": "Aperçu", + "more_with": "Plus avec {{name}}", + "similar_items": "Médias similaires", + "no_similar_items_found": "Aucun média similaire trouvé", + "video": "Vidéo", + "more_details": "Plus de détails", + "quality": "Qualité", + "audio": "Audio", + "subtitles": "Sous-titres", + "show_more": "Afficher plus", + "show_less": "Afficher moins", + "appeared_in": "Apparu dans", + "could_not_load_item": "Impossible de charger le média", + "none": "Aucun", + "download": { + "download_season": "Télécharger la saison", + "download_series": "Télécharger la série", + "download_episode": "Télécharger l'épisode", + "download_movie": "Télécharger le film", + "download_x_item": "Télécharger {{item_count}} médias", + "download_button": "Télécharger", + "using_optimized_server": "Avec le serveur optimisées", + "using_default_method": "Avec la méthode par défaut" + } + }, + "live_tv": { + "next": "Suivant", + "previous": "Précédent", + "live_tv": "TV en direct", + "coming_soon": "Bientôt", + "on_now": "En ce moment", + "shows": "Émissions", + "movies": "Films", + "sports": "Sports", + "for_kids": "Pour enfants", + "news": "Actualités" + }, + "jellyseerr": { + "confirm": "Confirmer", + "cancel": "Annuler", + "yes": "Oui", + "whats_wrong": "Quel est le problème?", + "issue_type": "Type de problème", + "select_an_issue": "Sélectionnez un problème", + "types": "Types", + "describe_the_issue": "(optionnel) Décrivez le problème...", + "submit_button": "Soumettre", + "report_issue_button": "Signaler un problème", + "request_button": "Demander", + "are_you_sure_you_want_to_request_all_seasons": "Êtes-vous sûr de vouloir demander toutes les saisons?", + "failed_to_login": "Échec de la connexion", + "cast": "Distribution", + "details": "Détails", + "status": "Statut", + "original_title": "Titre original", + "series_type": "Type de série", + "release_dates": "Dates de sortie", + "first_air_date": "Date de première diffusion", + "next_air_date": "Date de prochaine diffusion", + "revenue": "Revenu", + "budget": "Budget", + "original_language": "Langue originale", + "production_country": "Pays de production", + "studios": "Studios", + "network": "Réseaux", + "currently_streaming_on": "En streaming sur", + "advanced": "Avancé", + "request_as": "Demander en tant que", + "tags": "Tags", + "quality_profile": "Profil de qualité", + "root_folder": "Dossier racine", + "season_all": "Season (all)", + "season_number": "Saison {{season_number}}", + "number_episodes": "{{episode_number}} épisodes", + "born": "Né(e) le", + "appearances": "Apparences", + "toasts": { + "jellyseer_does_not_meet_requirements": "Jellyseer ne répond pas aux exigences! Veuillez mettre à jour au moins vers la version 2.0.0.", + "jellyseerr_test_failed": "Échec du test de Jellyseerr", + "failed_to_test_jellyseerr_server_url": "Échec du test de l'URL du serveur Jellyseerr", + "issue_submitted": "Problème soumis!", + "requested_item": "{{item}}} demandé!", + "you_dont_have_permission_to_request": "Vous n'avez pas la permission de demander {{item}}", + "something_went_wrong_requesting_media": "Quelque chose s'est mal passé en demandant le média!" + } + }, + "tabs": { + "home": "Accueil", + "search": "Recherche", + "library": "Bibliothèque", + "custom_links": "Liens personnalisés", + "favorites": "Favoris" + } } diff --git a/translations/it.json b/translations/it.json index 38e96cc6..44f437b3 100644 --- a/translations/it.json +++ b/translations/it.json @@ -1,473 +1,473 @@ { - "login": { - "username_required": "Nome utente è obbligatorio", - "error_title": "Errore", - "login_title": "Accesso", - "login_to_title": "Accedi a", - "username_placeholder": "Nome utente", - "password_placeholder": "Password", - "login_button": "Accedi", - "quick_connect": "Connessione Rapida", - "enter_code_to_login": "Inserire {{code}} per accedere", - "failed_to_initiate_quick_connect": "Impossibile avviare la Connessione Rapida", - "got_it": "Capito", - "connection_failed": "Connessione fallita", - "could_not_connect_to_server": "Impossibile connettersi al server. Controllare l'URL e la connessione di rete.", - "an_unexpected_error_occured": "Si è verificato un errore inaspettato", - "change_server": "Cambiare il server", - "invalid_username_or_password": "Nome utente o password non validi", - "user_does_not_have_permission_to_log_in": "L'utente non ha il permesso di accedere", - "server_is_taking_too_long_to_respond_try_again_later": "Il server sta impiegando troppo tempo per rispondere, riprovare più tardi", - "server_received_too_many_requests_try_again_later": "Il server ha ricevuto troppe richieste, riprovare più tardi.", - "there_is_a_server_error": "Si è verificato un errore del server", - "an_unexpected_error_occured_did_you_enter_the_correct_url": "Si è verificato un errore imprevisto. L'URL del server è stato inserito correttamente?" - }, - "server": { - "enter_url_to_jellyfin_server": "Inserisci l'URL del tuo server Jellyfin", - "server_url_placeholder": "http(s)://tuo-server.com", - "connect_button": "Connetti", - "previous_servers": "server precedente", - "clear_button": "Cancella", - "search_for_local_servers": "Ricerca dei server locali", - "searching": "Cercando...", - "servers": "Servers" - }, - "home": { - "no_internet": "Nessun Internet", - "no_items": "Nessun oggetto", - "no_internet_message": "Non c'è da preoccuparsi, è ancora possibile guardare\n i contenuti scaricati.", - "go_to_downloads": "Vai agli elementi scaricati", - "oops": "Oops!", - "error_message": "Qualcosa è andato storto. \nEffetturare il logout e riaccedere.", - "continue_watching": "Continua a guardare", - "next_up": "Prossimo", - "recently_added_in": "Aggiunti di recente a {{libraryName}}", - "suggested_movies": "Film consigliati", - "suggested_episodes": "Episodi consigliati", - "intro": { - "welcome_to_streamyfin": "Benvenuto a Streamyfin", - "a_free_and_open_source_client_for_jellyfin": "Un client gratuito e open-source per Jellyfin.", - "features_title": "Funzioni", - "features_description": "Streamyfin dispone di numerose funzioni e si integra con un'ampia gamma di software che si possono trovare nel menu delle impostazioni:", - "jellyseerr_feature_description": "Connettetevi alla vostra istanza Jellyseerr e richiedete i film direttamente nell'app.", - "downloads_feature_title": "Scaricamento", - "downloads_feature_description": "Scaricate film e serie tv da vedere offline. Utilizzate il metodo predefinito o installate il server di ottimizzazione per scaricare i file in background.", - "chromecast_feature_description": "Trasmettete film e serie tv ai vostri dispositivi Chromecast.", - "centralised_settings_plugin_title": "Impostazioni dei Plugin Centralizzate", - "centralised_settings_plugin_description": "Configura le impostazioni da una posizione centralizzata sul server Jellyfin. Tutte le impostazioni del client per tutti gli utenti saranno sincronizzate automaticamente.", - "done_button": "Fatto", - "go_to_settings_button": "Vai alle impostazioni", - "read_more": "Leggi di più" - }, - "settings": { - "settings_title": "Impostazioni", - "log_out_button": "Esci", - "user_info": { - "user_info_title": "Info utente", - "user": "Utente", - "server": "Server", - "token": "Token", - "app_version": "Versione dell'App" - }, - "quick_connect": { - "quick_connect_title": "Connessione Rapida", - "authorize_button": "Autorizza Connessione Rapida", - "enter_the_quick_connect_code": "Inserisci il codice per la Connessione Rapida...", - "success": "Successo", - "quick_connect_autorized": "Connessione Rapida autorizzata", - "error": "Errore", - "invalid_code": "Codice invalido", - "authorize": "Autorizza" - }, - "media_controls": { - "media_controls_title": "Controlli multimediali", - "forward_skip_length": "Lunghezza del salto in avanti", - "rewind_length": "Lunghezza del riavvolgimento", - "seconds_unit": "s" - }, - "audio": { - "audio_title": "Audio", - "set_audio_track": "Imposta la traccia audio dall'elemento precedente", - "audio_language": "Lingua Audio", - "audio_hint": "Scegli la lingua audio predefinita.", - "none": "Nessuno", - "language": "Lingua" - }, - "subtitles": { - "subtitle_title": "Sottotitoli", - "subtitle_language": "Lingua dei sottotitoli", - "subtitle_mode": "Modalità dei sottotitoli", - "set_subtitle_track": "Imposta la traccia dei sottotitoli dall'elemento precedente", - "subtitle_size": "Dimensione dei sottotitoli", - "subtitle_hint": "Configura la preferenza dei sottotitoli.", - "none": "Nessuno", - "language": "Lingua", - "loading": "Caricamento", - "modes": { - "Default": "Predefinito", - "Smart": "Intelligente", - "Always": "Sempre", - "None": "Nessuno", - "OnlyForced": "Solo forzati" - } - }, - "other": { - "other_title": "Altro", - "follow_device_orientation": "Rotazione automatica", - "video_orientation": "Orientamento del video", - "orientation": "Orientamento", - "orientations": { - "DEFAULT": "Predefinito", - "ALL": "Tutto", - "PORTRAIT": "Verticale", - "PORTRAIT_UP": "Verticale sopra", - "PORTRAIT_DOWN": "Verticale sotto", - "LANDSCAPE": "Orizzontale", - "LANDSCAPE_LEFT": "Orizzontale sinitra", - "LANDSCAPE_RIGHT": "Orizzontale destra", - "OTHER": "Altro", - "UNKNOWN": "Sconosciuto" - }, - "safe_area_in_controls": "Area sicura per i controlli", - "video_player": "Video player", - "video_players": { - "VLC_3": "VLC 3", - "VLC_4": "VLC 4 (Experimental + PiP)" - }, - "show_custom_menu_links": "Mostra i link del menu personalizzato", - "hide_libraries": "Nascondi Librerie", - "select_liraries_you_want_to_hide": "Selezionate le librerie che volete nascondere dalla scheda Libreria e dalle sezioni della pagina iniziale.", - "disable_haptic_feedback": "Disabilita il feedback aptico", - "default_quality": "Qualità predefinita" - }, - "downloads": { - "downloads_title": "Scaricamento", - "download_method": "Metodo per lo scaricamento", - "remux_max_download": "Numero di Remux da scaricare al massimo", - "auto_download": "Scaricamento automatico", - "optimized_versions_server": "Versioni del server di ottimizzazione", - "save_button": "Salva", - "optimized_server": "Server di ottimizzazione", - "optimized": "Ottimizzato", - "default": "Predefinito", - "optimized_version_hint": "Inserire l'URL del server di ottimizzazione. L'URL deve includere http o https e, facoltativamente, la porta.", - "read_more_about_optimized_server": "Per saperne di più sul server di ottimizzazione.", - "url": "URL", - "server_url_placeholder": "http(s)://dominio.org:porta" - }, - "plugins": { - "plugins_title": "Plugin", - "jellyseerr": { - "jellyseerr_warning": "Questa integrazione è in fase iniziale. Aspettarsi cambiamenti.", - "server_url": "URL del Server", - "server_url_hint": "Esempio: http(s)://tuo-host.url\n(aggiungere la porta se richiesto)", - "server_url_placeholder": "URL di Jellyseerr...", - "password": "Password", - "password_placeholder": "Inserire la password per l'utente {{username}} di Jellyfin", - "save_button": "Salva", - "clear_button": "Cancella", - "login_button": "Accedi", - "total_media_requests": "Totale di richieste di media", - "movie_quota_limit": "Limite di quota per i film", - "movie_quota_days": "Giorni di quota per i film", - "tv_quota_limit": "Limite di quota per le serie TV", - "tv_quota_days": "Giorni di quota per le serie TV", - "reset_jellyseerr_config_button": "Ripristina la configurazione di Jellyseerr", - "unlimited": "Illimitato", - "plus_n_more": "+{{n}} more", - "order_by": { - "DEFAULT": "Default", - "VOTE_COUNT_AND_AVERAGE": "Vote count and average", - "POPULARITY": "Popularity" - } - }, - "marlin_search": { - "enable_marlin_search": "Abilita la ricerca Marlin ", - "url": "URL", - "server_url_placeholder": "http(s)://dominio.org:porta", - "marlin_search_hint": "Inserire l'URL del server Marlin. L'URL deve includere http o https e, facoltativamente, la porta.", - "read_more_about_marlin": "Leggi di più su Marlin.", - "save_button": "Salva", - "toasts": { - "saved": "Salvato" - } - } - }, - "storage": { - "storage_title": "Spazio", - "app_usage": "App {{usedSpace}}%", - "device_usage": "Dispositivo {{availableSpace}}%", - "size_used": "{{used}} di {{total}} usato", - "delete_all_downloaded_files": "Cancella Tutti i File Scaricati" - }, - "intro": { - "show_intro": "Mostra intro", - "reset_intro": "Ripristina intro" - }, - "logs": { - "logs_title": "Log", - "no_logs_available": "Nessun log disponibile", - "delete_all_logs": "Cancella tutti i log" - }, - "languages": { - "title": "Lingue", - "app_language": "Lingua dell'App", - "app_language_description": "Selezione la lingua dell'app.", - "system": "Sistema" - }, - "toasts": { - "error_deleting_files": "Errore nella cancellazione dei file", - "background_downloads_enabled": "Scaricamento in background abilitato", - "background_downloads_disabled": "Scaricamento in background disabilitato", - "connected": "Connesso", - "could_not_connect": "Non è stato possibile connettersi", - "invalid_url": "URL invalido" - } - }, - "downloads": { - "downloads_title": "Scaricati", - "tvseries": "Serie TV", - "movies": "Film", - "queue": "Coda", - "queue_hint": "La coda e gli elementi scaricati saranno persi con il riavvio dell'app", - "no_items_in_queue": "Nessun elemento in coda", - "no_downloaded_items": "Nessun elemento scaricato", - "delete_all_movies_button": "Cancella tutti i film", - "delete_all_tvseries_button": "Cancella tutte le serie TV", - "delete_all_button": "Cancella tutti", - "active_download": "Scaricamento in corso", - "no_active_downloads": "Nessun scaricamento in corso", - "active_downloads": "Scaricamenti in corso", - "new_app_version_requires_re_download": "La nuova verione dell'app richiede di scaricare nuovamente i contenuti", - "new_app_version_requires_re_download_description": "Il nuovo aggiornamento richiede di scaricare nuovamente i contenuti. Rimuovere tutti i contenuti scaricati e riprovare.", - "back": "Indietro", - "delete": "Cancella", - "something_went_wrong": "Qualcosa è andato storto", - "could_not_get_stream_url_from_jellyfin": "Impossibile ottenere l'URL del flusso da Jellyfin", - "eta": "ETA {{eta}}", - "methods": "Metodi", - "toasts": { - "you_are_not_allowed_to_download_files": "Non è consentito scaricare file.", - "deleted_all_movies_successfully": "Cancellati tutti i film con successo!", - "failed_to_delete_all_movies": "Impossibile eliminare tutti i film", - "deleted_all_tvseries_successfully": "Eliminate tutte le serie TV con successo!", - "failed_to_delete_all_tvseries": "Impossibile eliminare tutte le serie TV", - "download_cancelled": "Scaricamento annullato", - "could_not_cancel_download": "Impossibile annullare lo scaricamento", - "download_completed": "Scaricamento completato", - "download_started_for": "Scaricamento iniziato per {{item}}", - "item_is_ready_to_be_downloaded": "{{item}} è pronto per essere scaricato", - "download_stated_for_item": "Scaricamento iniziato per {{item}}", - "download_failed_for_item": "Scaricamento fallito per {{item}} - {{error}}", - "download_completed_for_item": "Scaricamento completato per {{item}}", - "queued_item_for_optimization": "Messo in coda {{item}} per l'ottimizzazione", - "failed_to_start_download_for_item": "Failed to start downloading for {{item}}: {{message}}", - "server_responded_with_status_code": "Server responded with status {{statusCode}}", - "no_response_received_from_server": "No response received from the server", - "error_setting_up_the_request": "Error setting up the request", - "failed_to_start_download_for_item_unexpected_error": "Impossibile avviare il download per {{item}}: Errore imprevisto", - "all_files_folders_and_jobs_deleted_successfully": "Tutti i file, le cartelle e i processi sono stati eliminati con successo.", - "an_error_occured_while_deleting_files_and_jobs": "Si è verificato un errore durante l'eliminazione di file e processi", - "go_to_downloads": "Vai agli elementi scaricati" - } - } - }, - "search": { - "search_here": "Cerca qui...", - "search": "Cerca...", - "x_items": "{{count}} elementi", - "library": "Libreria", - "discover": "Scopri", - "no_results": "Nessun risultato", - "no_results_found_for": "Nessun risultato trovato per", - "movies": "Film", - "series": "Serie", - "episodes": "Episodi", - "collections": "Collezioni", - "actors": "Attori", - "request_movies": "Film Richiesti", - "request_series": "Serie Richieste", - "recently_added": "Aggiunti di Recente", - "recent_requests": "Richiesti di Recente", - "plex_watchlist": "Plex Watchlist", - "trending": "In tendenza", - "popular_movies": "Film Popolari", - "movie_genres": "Generi Film", - "upcoming_movies": "Film in arrivo", - "studios": "Studio", - "popular_tv": "Serie Popolari", - "tv_genres": "Generi Televisivi", - "upcoming_tv": "Serie in Arrivo", - "networks": "Network", - "tmdb_movie_keyword": "TMDB Parola chiave del film", - "tmdb_movie_genre": "TMDB Genere Film", - "tmdb_tv_keyword": "TMDB Parola chiave della serie", - "tmdb_tv_genre": "TMDB Genere Televisivo", - "tmdb_search": "TMDB Cerca", - "tmdb_studio": "TMDB Studio", - "tmdb_network": "TMDB Network", - "tmdb_movie_streaming_services": "TMDB Servizi di Streaming di Film", - "tmdb_tv_streaming_services": "TMDB Servizi di Streaming di Serie" - }, - "library": { - "no_items_found": "Nessun elemento trovato", - "no_results": "Nessun risultato", - "no_libraries_found": "Nessuna libreria trovata", - "item_types": { - "movies": "film", - "series": "serie TV", - "boxsets": "cofanetti", - "items": "elementi" - }, - "options": { - "display": "Display", - "row": "Fila", - "list": "Lista", - "image_style": "Stile dell'immagine", - "poster": "Poster", - "cover": "Cover", - "show_titles": "Mostra titoli", - "show_stats": "Mostra statistiche" - }, - "filters": { - "genres": "Generi", - "years": "Anni", - "sort_by": "Ordina per", - "sort_order": "Criterio di ordinamento", - "asc": "Ascending", - "desc": "Descending", - "tags": "Tag" - } - }, - "favorites": { - "series": "Serie TV", - "movies": "Film", - "episodes": "Episodi", - "videos": "Video", - "boxsets": "Boxset", - "playlists": "Playlist", - "noDataTitle": "Ancora nessun preferito", - "noData": "Contrassegna gli elementi come preferiti per vederli apparire qui per un accesso rapido." - }, - "custom_links": { - "no_links": "Nessun link" - }, - "player": { - "error": "Errore", - "failed_to_get_stream_url": "Impossibile ottenere l'URL dello stream", - "an_error_occured_while_playing_the_video": "Si è verificato un errore durante la riproduzione del video. Controllare i log nelle impostazioni.", - "client_error": "Errore del client", - "could_not_create_stream_for_chromecast": "Impossibile creare uno stream per Chromecast", - "message_from_server": "Messaggio dal server", - "video_has_finished_playing": "La riproduzione del video è terminata!", - "no_video_source": "Nessuna sorgente video...", - "next_episode": "Prossimo Episodio", - "refresh_tracks": "Aggiorna tracce", - "subtitle_tracks": "Tracce di sottotitoli:", - "audio_tracks": "Tracce audio:", - "playback_state": "Stato della riproduzione:", - "no_data_available": "Nessun dato disponibile", - "index": "Indice:" - }, - "item_card": { - "next_up": "Il prossimo", - "no_items_to_display": "Nessun elemento da visualizzare", - "cast_and_crew": "Cast e Equipaggio", - "series": "Serie", - "seasons": "Stagioni", - "season": "Stagione", - "no_episodes_for_this_season": "Nessun episodio per questa stagione", - "overview": "Panoramica", - "more_with": "Altri con {{name}}", - "similar_items": "Elementi simili", - "no_similar_items_found": "Non sono stati trovati elementi simili", - "video": "Video", - "more_details": "Più dettagli", - "quality": "Qualità", - "audio": "Audio", - "subtitles": "Sottotitoli", - "show_more": "Mostra di più", - "show_less": "Mostra di meno", - "appeared_in": "Apparso in", - "could_not_load_item": "Impossibile caricare l'elemento", - "none": "Nessuno", - "download": { - "download_season": "Scarica Stagione", - "download_series": "Scarica Serie", - "download_episode": "Scarica Episodio", - "download_movie": "Scarica Film", - "download_x_item": "Scarica {{item_count}} elementi", - "download_button": "Scarica", - "using_optimized_server": "Utilizzando il server di ottimizzazione", - "using_default_method": "Utilizzando il metodo predefinito" - } - }, - "live_tv": { - "next": "Prossimo", - "previous": "Precedente", - "live_tv": "TV in diretta", - "coming_soon": "Prossimamente", - "on_now": "In onda ora", - "shows": "Programmi", - "movies": "Film", - "sports": "Sport", - "for_kids": "Per Bambini", - "news": "Notiziari" - }, - "jellyseerr": { - "confirm": "Conferma", - "cancel": "Cancella", - "yes": "Si", - "whats_wrong": "Cosa c'è che non va?", - "issue_type": "Tipo di problema", - "select_an_issue": "Seleziona un problema", - "types": "Tipi", - "describe_the_issue": "(facoltativo) Descrivere il problema...", - "submit_button": "Invia", - "report_issue_button": "Segnalare il problema", - "request_button": "Richiedi", - "are_you_sure_you_want_to_request_all_seasons": "Sei sicuro di voler richiedere tutte le stagioni?", - "failed_to_login": "Accesso non riuscito", - "cast": "Cast", - "details": "Dettagli", - "status": "Stato", - "original_title": "Titolo originale", - "series_type": "Tipo di Serie", - "release_dates": "Date di Uscita", - "first_air_date": "Prima Data di Messa in Onda", - "next_air_date": "Prossima Data di Messa in Onda", - "revenue": "Ricavi", - "budget": "Budget", - "original_language": "Lingua Originale", - "production_country": "Paese di Produzione", - "studios": "Studio", - "network": "Network", - "currently_streaming_on": "Attualmente in streaming su", - "advanced": "Avanzate", - "request_as": "Richiedi Come", - "tags": "Tag", - "quality_profile": "Profilo qualità", - "root_folder": "Cartella radice", - "season_all": "Season (all)", - "season_number": "Stagione {{season_number}}", - "number_episodes": "{{episode_number}} Episodio", - "born": "Nato", - "appearances": "Aspetto", - "toasts": { - "jellyseer_does_not_meet_requirements": "Il server Jellyseerr non soddisfa i requisiti minimi di versione! Aggiornare almeno alla versione 2.0.0.", - "jellyseerr_test_failed": "Il test di Jellyseerr non è riuscito. Riprovare.", - "failed_to_test_jellyseerr_server_url": "Fallito il test dell'url del server jellyseerr", - "issue_submitted": "Problema inviato!", - "requested_item": "Richiesto {{item}}!", - "you_dont_have_permission_to_request": "Non hai il permesso di richiedere!", - "something_went_wrong_requesting_media": "Qualcosa è andato storto nella richiesta dei media!" - } - }, - "tabs": { - "home": "Home", - "search": "Cerca", - "library": "Libreria", - "custom_links": "Collegamenti personalizzati", - "favorites": "Preferiti" - } + "login": { + "username_required": "Nome utente è obbligatorio", + "error_title": "Errore", + "login_title": "Accesso", + "login_to_title": "Accedi a", + "username_placeholder": "Nome utente", + "password_placeholder": "Password", + "login_button": "Accedi", + "quick_connect": "Connessione Rapida", + "enter_code_to_login": "Inserire {{code}} per accedere", + "failed_to_initiate_quick_connect": "Impossibile avviare la Connessione Rapida", + "got_it": "Capito", + "connection_failed": "Connessione fallita", + "could_not_connect_to_server": "Impossibile connettersi al server. Controllare l'URL e la connessione di rete.", + "an_unexpected_error_occured": "Si è verificato un errore inaspettato", + "change_server": "Cambiare il server", + "invalid_username_or_password": "Nome utente o password non validi", + "user_does_not_have_permission_to_log_in": "L'utente non ha il permesso di accedere", + "server_is_taking_too_long_to_respond_try_again_later": "Il server sta impiegando troppo tempo per rispondere, riprovare più tardi", + "server_received_too_many_requests_try_again_later": "Il server ha ricevuto troppe richieste, riprovare più tardi.", + "there_is_a_server_error": "Si è verificato un errore del server", + "an_unexpected_error_occured_did_you_enter_the_correct_url": "Si è verificato un errore imprevisto. L'URL del server è stato inserito correttamente?" + }, + "server": { + "enter_url_to_jellyfin_server": "Inserisci l'URL del tuo server Jellyfin", + "server_url_placeholder": "http(s)://tuo-server.com", + "connect_button": "Connetti", + "previous_servers": "server precedente", + "clear_button": "Cancella", + "search_for_local_servers": "Ricerca dei server locali", + "searching": "Cercando...", + "servers": "Servers" + }, + "home": { + "no_internet": "Nessun Internet", + "no_items": "Nessun oggetto", + "no_internet_message": "Non c'è da preoccuparsi, è ancora possibile guardare\n i contenuti scaricati.", + "go_to_downloads": "Vai agli elementi scaricati", + "oops": "Oops!", + "error_message": "Qualcosa è andato storto. \nEffetturare il logout e riaccedere.", + "continue_watching": "Continua a guardare", + "next_up": "Prossimo", + "recently_added_in": "Aggiunti di recente a {{libraryName}}", + "suggested_movies": "Film consigliati", + "suggested_episodes": "Episodi consigliati", + "intro": { + "welcome_to_streamyfin": "Benvenuto a Streamyfin", + "a_free_and_open_source_client_for_jellyfin": "Un client gratuito e open-source per Jellyfin.", + "features_title": "Funzioni", + "features_description": "Streamyfin dispone di numerose funzioni e si integra con un'ampia gamma di software che si possono trovare nel menu delle impostazioni:", + "jellyseerr_feature_description": "Connettetevi alla vostra istanza Jellyseerr e richiedete i film direttamente nell'app.", + "downloads_feature_title": "Scaricamento", + "downloads_feature_description": "Scaricate film e serie tv da vedere offline. Utilizzate il metodo predefinito o installate il server di ottimizzazione per scaricare i file in background.", + "chromecast_feature_description": "Trasmettete film e serie tv ai vostri dispositivi Chromecast.", + "centralised_settings_plugin_title": "Impostazioni dei Plugin Centralizzate", + "centralised_settings_plugin_description": "Configura le impostazioni da una posizione centralizzata sul server Jellyfin. Tutte le impostazioni del client per tutti gli utenti saranno sincronizzate automaticamente.", + "done_button": "Fatto", + "go_to_settings_button": "Vai alle impostazioni", + "read_more": "Leggi di più" + }, + "settings": { + "settings_title": "Impostazioni", + "log_out_button": "Esci", + "user_info": { + "user_info_title": "Info utente", + "user": "Utente", + "server": "Server", + "token": "Token", + "app_version": "Versione dell'App" + }, + "quick_connect": { + "quick_connect_title": "Connessione Rapida", + "authorize_button": "Autorizza Connessione Rapida", + "enter_the_quick_connect_code": "Inserisci il codice per la Connessione Rapida...", + "success": "Successo", + "quick_connect_autorized": "Connessione Rapida autorizzata", + "error": "Errore", + "invalid_code": "Codice invalido", + "authorize": "Autorizza" + }, + "media_controls": { + "media_controls_title": "Controlli multimediali", + "forward_skip_length": "Lunghezza del salto in avanti", + "rewind_length": "Lunghezza del riavvolgimento", + "seconds_unit": "s" + }, + "audio": { + "audio_title": "Audio", + "set_audio_track": "Imposta la traccia audio dall'elemento precedente", + "audio_language": "Lingua Audio", + "audio_hint": "Scegli la lingua audio predefinita.", + "none": "Nessuno", + "language": "Lingua" + }, + "subtitles": { + "subtitle_title": "Sottotitoli", + "subtitle_language": "Lingua dei sottotitoli", + "subtitle_mode": "Modalità dei sottotitoli", + "set_subtitle_track": "Imposta la traccia dei sottotitoli dall'elemento precedente", + "subtitle_size": "Dimensione dei sottotitoli", + "subtitle_hint": "Configura la preferenza dei sottotitoli.", + "none": "Nessuno", + "language": "Lingua", + "loading": "Caricamento", + "modes": { + "Default": "Predefinito", + "Smart": "Intelligente", + "Always": "Sempre", + "None": "Nessuno", + "OnlyForced": "Solo forzati" + } + }, + "other": { + "other_title": "Altro", + "follow_device_orientation": "Rotazione automatica", + "video_orientation": "Orientamento del video", + "orientation": "Orientamento", + "orientations": { + "DEFAULT": "Predefinito", + "ALL": "Tutto", + "PORTRAIT": "Verticale", + "PORTRAIT_UP": "Verticale sopra", + "PORTRAIT_DOWN": "Verticale sotto", + "LANDSCAPE": "Orizzontale", + "LANDSCAPE_LEFT": "Orizzontale sinitra", + "LANDSCAPE_RIGHT": "Orizzontale destra", + "OTHER": "Altro", + "UNKNOWN": "Sconosciuto" + }, + "safe_area_in_controls": "Area sicura per i controlli", + "video_player": "Video player", + "video_players": { + "VLC_3": "VLC 3", + "VLC_4": "VLC 4 (Experimental + PiP)" + }, + "show_custom_menu_links": "Mostra i link del menu personalizzato", + "hide_libraries": "Nascondi Librerie", + "select_liraries_you_want_to_hide": "Selezionate le librerie che volete nascondere dalla scheda Libreria e dalle sezioni della pagina iniziale.", + "disable_haptic_feedback": "Disabilita il feedback aptico", + "default_quality": "Qualità predefinita" + }, + "downloads": { + "downloads_title": "Scaricamento", + "download_method": "Metodo per lo scaricamento", + "remux_max_download": "Numero di Remux da scaricare al massimo", + "auto_download": "Scaricamento automatico", + "optimized_versions_server": "Versioni del server di ottimizzazione", + "save_button": "Salva", + "optimized_server": "Server di ottimizzazione", + "optimized": "Ottimizzato", + "default": "Predefinito", + "optimized_version_hint": "Inserire l'URL del server di ottimizzazione. L'URL deve includere http o https e, facoltativamente, la porta.", + "read_more_about_optimized_server": "Per saperne di più sul server di ottimizzazione.", + "url": "URL", + "server_url_placeholder": "http(s)://dominio.org:porta" + }, + "plugins": { + "plugins_title": "Plugin", + "jellyseerr": { + "jellyseerr_warning": "Questa integrazione è in fase iniziale. Aspettarsi cambiamenti.", + "server_url": "URL del Server", + "server_url_hint": "Esempio: http(s)://tuo-host.url\n(aggiungere la porta se richiesto)", + "server_url_placeholder": "URL di Jellyseerr...", + "password": "Password", + "password_placeholder": "Inserire la password per l'utente {{username}} di Jellyfin", + "save_button": "Salva", + "clear_button": "Cancella", + "login_button": "Accedi", + "total_media_requests": "Totale di richieste di media", + "movie_quota_limit": "Limite di quota per i film", + "movie_quota_days": "Giorni di quota per i film", + "tv_quota_limit": "Limite di quota per le serie TV", + "tv_quota_days": "Giorni di quota per le serie TV", + "reset_jellyseerr_config_button": "Ripristina la configurazione di Jellyseerr", + "unlimited": "Illimitato", + "plus_n_more": "+{{n}} more", + "order_by": { + "DEFAULT": "Default", + "VOTE_COUNT_AND_AVERAGE": "Vote count and average", + "POPULARITY": "Popularity" + } + }, + "marlin_search": { + "enable_marlin_search": "Abilita la ricerca Marlin ", + "url": "URL", + "server_url_placeholder": "http(s)://dominio.org:porta", + "marlin_search_hint": "Inserire l'URL del server Marlin. L'URL deve includere http o https e, facoltativamente, la porta.", + "read_more_about_marlin": "Leggi di più su Marlin.", + "save_button": "Salva", + "toasts": { + "saved": "Salvato" + } + } + }, + "storage": { + "storage_title": "Spazio", + "app_usage": "App {{usedSpace}}%", + "device_usage": "Dispositivo {{availableSpace}}%", + "size_used": "{{used}} di {{total}} usato", + "delete_all_downloaded_files": "Cancella Tutti i File Scaricati" + }, + "intro": { + "show_intro": "Mostra intro", + "reset_intro": "Ripristina intro" + }, + "logs": { + "logs_title": "Log", + "no_logs_available": "Nessun log disponibile", + "delete_all_logs": "Cancella tutti i log" + }, + "languages": { + "title": "Lingue", + "app_language": "Lingua dell'App", + "app_language_description": "Selezione la lingua dell'app.", + "system": "Sistema" + }, + "toasts": { + "error_deleting_files": "Errore nella cancellazione dei file", + "background_downloads_enabled": "Scaricamento in background abilitato", + "background_downloads_disabled": "Scaricamento in background disabilitato", + "connected": "Connesso", + "could_not_connect": "Non è stato possibile connettersi", + "invalid_url": "URL invalido" + } + }, + "downloads": { + "downloads_title": "Scaricati", + "tvseries": "Serie TV", + "movies": "Film", + "queue": "Coda", + "queue_hint": "La coda e gli elementi scaricati saranno persi con il riavvio dell'app", + "no_items_in_queue": "Nessun elemento in coda", + "no_downloaded_items": "Nessun elemento scaricato", + "delete_all_movies_button": "Cancella tutti i film", + "delete_all_tvseries_button": "Cancella tutte le serie TV", + "delete_all_button": "Cancella tutti", + "active_download": "Scaricamento in corso", + "no_active_downloads": "Nessun scaricamento in corso", + "active_downloads": "Scaricamenti in corso", + "new_app_version_requires_re_download": "La nuova verione dell'app richiede di scaricare nuovamente i contenuti", + "new_app_version_requires_re_download_description": "Il nuovo aggiornamento richiede di scaricare nuovamente i contenuti. Rimuovere tutti i contenuti scaricati e riprovare.", + "back": "Indietro", + "delete": "Cancella", + "something_went_wrong": "Qualcosa è andato storto", + "could_not_get_stream_url_from_jellyfin": "Impossibile ottenere l'URL del flusso da Jellyfin", + "eta": "ETA {{eta}}", + "methods": "Metodi", + "toasts": { + "you_are_not_allowed_to_download_files": "Non è consentito scaricare file.", + "deleted_all_movies_successfully": "Cancellati tutti i film con successo!", + "failed_to_delete_all_movies": "Impossibile eliminare tutti i film", + "deleted_all_tvseries_successfully": "Eliminate tutte le serie TV con successo!", + "failed_to_delete_all_tvseries": "Impossibile eliminare tutte le serie TV", + "download_cancelled": "Scaricamento annullato", + "could_not_cancel_download": "Impossibile annullare lo scaricamento", + "download_completed": "Scaricamento completato", + "download_started_for": "Scaricamento iniziato per {{item}}", + "item_is_ready_to_be_downloaded": "{{item}} è pronto per essere scaricato", + "download_stated_for_item": "Scaricamento iniziato per {{item}}", + "download_failed_for_item": "Scaricamento fallito per {{item}} - {{error}}", + "download_completed_for_item": "Scaricamento completato per {{item}}", + "queued_item_for_optimization": "Messo in coda {{item}} per l'ottimizzazione", + "failed_to_start_download_for_item": "Failed to start downloading for {{item}}: {{message}}", + "server_responded_with_status_code": "Server responded with status {{statusCode}}", + "no_response_received_from_server": "No response received from the server", + "error_setting_up_the_request": "Error setting up the request", + "failed_to_start_download_for_item_unexpected_error": "Impossibile avviare il download per {{item}}: Errore imprevisto", + "all_files_folders_and_jobs_deleted_successfully": "Tutti i file, le cartelle e i processi sono stati eliminati con successo.", + "an_error_occured_while_deleting_files_and_jobs": "Si è verificato un errore durante l'eliminazione di file e processi", + "go_to_downloads": "Vai agli elementi scaricati" + } + } + }, + "search": { + "search_here": "Cerca qui...", + "search": "Cerca...", + "x_items": "{{count}} elementi", + "library": "Libreria", + "discover": "Scopri", + "no_results": "Nessun risultato", + "no_results_found_for": "Nessun risultato trovato per", + "movies": "Film", + "series": "Serie", + "episodes": "Episodi", + "collections": "Collezioni", + "actors": "Attori", + "request_movies": "Film Richiesti", + "request_series": "Serie Richieste", + "recently_added": "Aggiunti di Recente", + "recent_requests": "Richiesti di Recente", + "plex_watchlist": "Plex Watchlist", + "trending": "In tendenza", + "popular_movies": "Film Popolari", + "movie_genres": "Generi Film", + "upcoming_movies": "Film in arrivo", + "studios": "Studio", + "popular_tv": "Serie Popolari", + "tv_genres": "Generi Televisivi", + "upcoming_tv": "Serie in Arrivo", + "networks": "Network", + "tmdb_movie_keyword": "TMDB Parola chiave del film", + "tmdb_movie_genre": "TMDB Genere Film", + "tmdb_tv_keyword": "TMDB Parola chiave della serie", + "tmdb_tv_genre": "TMDB Genere Televisivo", + "tmdb_search": "TMDB Cerca", + "tmdb_studio": "TMDB Studio", + "tmdb_network": "TMDB Network", + "tmdb_movie_streaming_services": "TMDB Servizi di Streaming di Film", + "tmdb_tv_streaming_services": "TMDB Servizi di Streaming di Serie" + }, + "library": { + "no_items_found": "Nessun elemento trovato", + "no_results": "Nessun risultato", + "no_libraries_found": "Nessuna libreria trovata", + "item_types": { + "movies": "film", + "series": "serie TV", + "boxsets": "cofanetti", + "items": "elementi" + }, + "options": { + "display": "Display", + "row": "Fila", + "list": "Lista", + "image_style": "Stile dell'immagine", + "poster": "Poster", + "cover": "Cover", + "show_titles": "Mostra titoli", + "show_stats": "Mostra statistiche" + }, + "filters": { + "genres": "Generi", + "years": "Anni", + "sort_by": "Ordina per", + "sort_order": "Criterio di ordinamento", + "asc": "Ascending", + "desc": "Descending", + "tags": "Tag" + } + }, + "favorites": { + "series": "Serie TV", + "movies": "Film", + "episodes": "Episodi", + "videos": "Video", + "boxsets": "Boxset", + "playlists": "Playlist", + "noDataTitle": "Ancora nessun preferito", + "noData": "Contrassegna gli elementi come preferiti per vederli apparire qui per un accesso rapido." + }, + "custom_links": { + "no_links": "Nessun link" + }, + "player": { + "error": "Errore", + "failed_to_get_stream_url": "Impossibile ottenere l'URL dello stream", + "an_error_occured_while_playing_the_video": "Si è verificato un errore durante la riproduzione del video. Controllare i log nelle impostazioni.", + "client_error": "Errore del client", + "could_not_create_stream_for_chromecast": "Impossibile creare uno stream per Chromecast", + "message_from_server": "Messaggio dal server", + "video_has_finished_playing": "La riproduzione del video è terminata!", + "no_video_source": "Nessuna sorgente video...", + "next_episode": "Prossimo Episodio", + "refresh_tracks": "Aggiorna tracce", + "subtitle_tracks": "Tracce di sottotitoli:", + "audio_tracks": "Tracce audio:", + "playback_state": "Stato della riproduzione:", + "no_data_available": "Nessun dato disponibile", + "index": "Indice:" + }, + "item_card": { + "next_up": "Il prossimo", + "no_items_to_display": "Nessun elemento da visualizzare", + "cast_and_crew": "Cast e Equipaggio", + "series": "Serie", + "seasons": "Stagioni", + "season": "Stagione", + "no_episodes_for_this_season": "Nessun episodio per questa stagione", + "overview": "Panoramica", + "more_with": "Altri con {{name}}", + "similar_items": "Elementi simili", + "no_similar_items_found": "Non sono stati trovati elementi simili", + "video": "Video", + "more_details": "Più dettagli", + "quality": "Qualità", + "audio": "Audio", + "subtitles": "Sottotitoli", + "show_more": "Mostra di più", + "show_less": "Mostra di meno", + "appeared_in": "Apparso in", + "could_not_load_item": "Impossibile caricare l'elemento", + "none": "Nessuno", + "download": { + "download_season": "Scarica Stagione", + "download_series": "Scarica Serie", + "download_episode": "Scarica Episodio", + "download_movie": "Scarica Film", + "download_x_item": "Scarica {{item_count}} elementi", + "download_button": "Scarica", + "using_optimized_server": "Utilizzando il server di ottimizzazione", + "using_default_method": "Utilizzando il metodo predefinito" + } + }, + "live_tv": { + "next": "Prossimo", + "previous": "Precedente", + "live_tv": "TV in diretta", + "coming_soon": "Prossimamente", + "on_now": "In onda ora", + "shows": "Programmi", + "movies": "Film", + "sports": "Sport", + "for_kids": "Per Bambini", + "news": "Notiziari" + }, + "jellyseerr": { + "confirm": "Conferma", + "cancel": "Cancella", + "yes": "Si", + "whats_wrong": "Cosa c'è che non va?", + "issue_type": "Tipo di problema", + "select_an_issue": "Seleziona un problema", + "types": "Tipi", + "describe_the_issue": "(facoltativo) Descrivere il problema...", + "submit_button": "Invia", + "report_issue_button": "Segnalare il problema", + "request_button": "Richiedi", + "are_you_sure_you_want_to_request_all_seasons": "Sei sicuro di voler richiedere tutte le stagioni?", + "failed_to_login": "Accesso non riuscito", + "cast": "Cast", + "details": "Dettagli", + "status": "Stato", + "original_title": "Titolo originale", + "series_type": "Tipo di Serie", + "release_dates": "Date di Uscita", + "first_air_date": "Prima Data di Messa in Onda", + "next_air_date": "Prossima Data di Messa in Onda", + "revenue": "Ricavi", + "budget": "Budget", + "original_language": "Lingua Originale", + "production_country": "Paese di Produzione", + "studios": "Studio", + "network": "Network", + "currently_streaming_on": "Attualmente in streaming su", + "advanced": "Avanzate", + "request_as": "Richiedi Come", + "tags": "Tag", + "quality_profile": "Profilo qualità", + "root_folder": "Cartella radice", + "season_all": "Season (all)", + "season_number": "Stagione {{season_number}}", + "number_episodes": "{{episode_number}} Episodio", + "born": "Nato", + "appearances": "Aspetto", + "toasts": { + "jellyseer_does_not_meet_requirements": "Il server Jellyseerr non soddisfa i requisiti minimi di versione! Aggiornare almeno alla versione 2.0.0.", + "jellyseerr_test_failed": "Il test di Jellyseerr non è riuscito. Riprovare.", + "failed_to_test_jellyseerr_server_url": "Fallito il test dell'url del server jellyseerr", + "issue_submitted": "Problema inviato!", + "requested_item": "Richiesto {{item}}!", + "you_dont_have_permission_to_request": "Non hai il permesso di richiedere!", + "something_went_wrong_requesting_media": "Qualcosa è andato storto nella richiesta dei media!" + } + }, + "tabs": { + "home": "Home", + "search": "Cerca", + "library": "Libreria", + "custom_links": "Collegamenti personalizzati", + "favorites": "Preferiti" + } } diff --git a/translations/ja.json b/translations/ja.json index 44ac71bd..261b6724 100644 --- a/translations/ja.json +++ b/translations/ja.json @@ -1,472 +1,472 @@ { - "login": { - "username_required": "ユーザー名は必須です", - "error_title": "エラー", - "login_title": "ログイン", - "login_to_title": "ログイン先", - "username_placeholder": "ユーザー名", - "password_placeholder": "パスワード", - "login_button": "ログイン", - "quick_connect": "クイックコネクト", - "enter_code_to_login": "ログインするにはコード {{code}} を入力してください", - "failed_to_initiate_quick_connect": "クイックコネクトを開始できませんでした", - "got_it": "了解", - "connection_failed": "接続に失敗しました", - "could_not_connect_to_server": "サーバーに接続できませんでした。URLとネットワーク接続を確認してください。", - "an_unexpected_error_occured": "予期しないエラーが発生しました", - "change_server": "サーバーの変更", - "invalid_username_or_password": "ユーザー名またはパスワードが無効です", - "user_does_not_have_permission_to_log_in": "ユーザーにログイン権限がありません", - "server_is_taking_too_long_to_respond_try_again_later": "サーバーの応答に時間がかかりすぎています。しばらくしてからもう一度お試しください。", - "server_received_too_many_requests_try_again_later": "サーバーにリクエストが多すぎます。後でもう一度お試しください。", - "there_is_a_server_error": "サーバーエラーが発生しました", - "an_unexpected_error_occured_did_you_enter_the_correct_url": "予期しないエラーが発生しました。サーバーのURLを正しく入力しましたか?" - }, - "server": { - "enter_url_to_jellyfin_server": "JellyfinサーバーのURLを入力してください", - "server_url_placeholder": "http(s)://your-server.com", - "connect_button": "接続", - "previous_servers": "前のサーバー", - "clear_button": "クリア", - "search_for_local_servers": "ローカルサーバーを検索", - "searching": "検索中...", - "servers": "サーバー" - }, - "home": { - "no_internet": "インターネット接続がありません", - "no_items": "アイテムはありません", - "no_internet_message": "心配しないでください。\nダウンロードしたコンテンツは引き続き視聴できます。", - "go_to_downloads": "ダウンロードに移動", - "oops": "おっと!", - "error_message": "何か問題が発生しました。\nログアウトして再度ログインしてください。", - "continue_watching": "続きを見る", - "next_up": "次の動画", - "recently_added_in": "{{libraryName}}に最近追加された", - "suggested_movies": "おすすめ映画", - "suggested_episodes": "おすすめエピソード", - "intro": { - "welcome_to_streamyfin": "Streamyfinへようこそ", - "a_free_and_open_source_client_for_jellyfin": "Jellyfinのためのフリーでオープンソースのクライアント。", - "features_title": "特長", - "features_description": "Streamyfinには多くの機能があり、設定メニューで見つけることができるさまざまなソフトウェアと統合されています。これには以下が含まれます。", - "jellyseerr_feature_description": "Jellyseerrインスタンスに接続し、アプリ内で直接映画をリクエストします。", - "downloads_feature_title": "ダウンロード", - "downloads_feature_description": "映画やテレビ番組をダウンロードしてオフラインで視聴します。デフォルトの方法を使用するか、バックグラウンドでファイルをダウンロードするために最適化されたサーバーをインストールしてください。", - "chromecast_feature_description": "映画とテレビ番組をChromecastデバイスにキャストします。", - "centralised_settings_plugin_title": "集中設定プラグイン", - "centralised_settings_plugin_description": "Jellyfinサーバーから設定を構成します。すべてのユーザーのすべてのクライアント設定は自動的に同期されます。", - "done_button": "完了", - "go_to_settings_button": "設定に移動", - "read_more": "続きを読む" - }, - "settings": { - "settings_title": "設定", - "log_out_button": "ログアウト", - "user_info": { - "user_info_title": "ユーザー情報", - "user": "ユーザー", - "server": "サーバー", - "token": "トークン", - "app_version": "アプリバージョン" - }, - "quick_connect": { - "quick_connect_title": "クイックコネクト", - "authorize_button": "クイックコネクトを承認する", - "enter_the_quick_connect_code": "クイックコネクトコードを入力...", - "success": "成功しました", - "quick_connect_autorized": "クイックコネクトが承認されました", - "error": "エラー", - "invalid_code": "無効なコードです", - "authorize": "承認" - }, - "media_controls": { - "media_controls_title": "メディアコントロール", - "forward_skip_length": "スキップの長さ", - "rewind_length": "巻き戻しの長さ", - "seconds_unit": "s" - }, - "audio": { - "audio_title": "オーディオ", - "set_audio_track": "前のアイテムからオーディオトラックを設定", - "audio_language": "オーディオ言語", - "audio_hint": "デフォルトのオーディオ言語を選択します。", - "none": "なし", - "language": "言語" - }, - "subtitles": { - "subtitle_title": "字幕", - "subtitle_language": "字幕の言語", - "subtitle_mode": "字幕モード", - "set_subtitle_track": "前のアイテムから字幕トラックを設定", - "subtitle_size": "字幕サイズ", - "subtitle_hint": "字幕設定を構成します。", - "none": "なし", - "language": "言語", - "loading": "ロード中", - "modes": { - "Default": "デフォルト", - "Smart": "スマート", - "Always": "常に", - "None": "なし", - "OnlyForced": "強制のみ" - } - }, - "other": { - "other_title": "その他", - "follow_device_orientation": "画面の自動回転", - "video_orientation": "動画の向き", - "orientation": "向き", - "orientations": { - "DEFAULT": "デフォルト", - "ALL": "すべて", - "PORTRAIT": "縦", - "PORTRAIT_UP": "縦向き(上)", - "PORTRAIT_DOWN": "縦方向", - "LANDSCAPE": "横方向", - "LANDSCAPE_LEFT": "横方向 左", - "LANDSCAPE_RIGHT": "横方向 右", - "OTHER": "その他", - "UNKNOWN": "不明" - }, - "safe_area_in_controls": "コントロールの安全エリア", - "video_player": "Video player", - "video_players": { - "VLC_3": "VLC 3", - "VLC_4": "VLC 4 (Experimental + PiP)" - }, - "show_custom_menu_links": "カスタムメニューのリンクを表示", - "hide_libraries": "ライブラリを非表示", - "select_liraries_you_want_to_hide": "ライブラリタブとホームページセクションから非表示にするライブラリを選択します。", - "disable_haptic_feedback": "触覚フィードバックを無効にする" - }, - "downloads": { - "downloads_title": "ダウンロード", - "download_method": "ダウンロード方法", - "remux_max_download": "Remux最大ダウンロード数", - "auto_download": "自動ダウンロード", - "optimized_versions_server": "Optimized versionsサーバー", - "save_button": "保存", - "optimized_server": "Optimizedサーバー", - "optimized": "最適化", - "default": "デフォルト", - "optimized_version_hint": "OptimizeサーバーのURLを入力します。URLにはhttpまたはhttpsを含め、オプションでポートを指定します。", - "read_more_about_optimized_server": "Optimizeサーバーの詳細をご覧ください。", - "url": "URL", - "server_url_placeholder": "http(s)://domain.org:ポート" - }, - "plugins": { - "plugins_title": "プラグイン", - "jellyseerr": { - "jellyseerr_warning": "この統合はまだ初期段階です。状況が変化する可能性があります。", - "server_url": "サーバーURL", - "server_url_hint": "例: http(s)://your-host.url\n(必要に応じてポートを追加)", - "server_url_placeholder": "Jellyseerr URL...", - "password": "パスワード", - "password_placeholder": "Jellyfinユーザー {{username}} のパスワードを入力してください", - "save_button": "保存", - "clear_button": "クリア", - "login_button": "ログイン", - "total_media_requests": "メディアリクエストの合計", - "movie_quota_limit": "映画のクオータ制限", - "movie_quota_days": "映画のクオータ日数", - "tv_quota_limit": "テレビのクオータ制限", - "tv_quota_days": "テレビのクオータ日数", - "reset_jellyseerr_config_button": "Jellyseerrの設定をリセット", - "unlimited": "無制限", - "plus_n_more": "+{{n}} more", - "order_by": { - "DEFAULT": "Default", - "VOTE_COUNT_AND_AVERAGE": "Vote count and average", - "POPULARITY": "Popularity" - } - }, - "marlin_search": { - "enable_marlin_search": "マーリン検索を有効にする ", - "url": "URL", - "server_url_placeholder": "http(s)://domain.org:ポート", - "marlin_search_hint": "MarlinサーバーのURLを入力します。URLにはhttpまたはhttpsを含め、オプションでポートを指定します。", - "read_more_about_marlin": "Marlinについて詳しく読む。", - "save_button": "保存", - "toasts": { - "saved": "保存しました" - } - } - }, - "storage": { - "storage_title": "ストレージ", - "app_usage": "アプリ {{usedSpace}}%", - "phone_usage": "電話 {{availableSpace}}%", - "size_used": "{{used}} / {{total}} 使用済み", - "delete_all_downloaded_files": "すべてのダウンロードファイルを削除" - }, - "intro": { - "show_intro": "イントロを表示", - "reset_intro": "イントロをリセット" - }, - "logs": { - "logs_title": "ログ", - "no_logs_available": "ログがありません", - "delete_all_logs": "すべてのログを削除" - }, - "languages": { - "title": "言語", - "app_language": "アプリの言語", - "app_language_description": "アプリの言語を選択。", - "system": "システム" - }, - "toasts": { - "error_deleting_files": "ファイルの削除エラー", - "background_downloads_enabled": "バックグラウンドでのダウンロードは有効です", - "background_downloads_disabled": "バックグラウンドでのダウンロードは無効です", - "connected": "接続済み", - "could_not_connect": "接続できません", - "invalid_url": "無効なURL" - } - }, - "downloads": { - "downloads_title": "ダウンロード", - "tvseries": "TVシリーズ", - "movies": "映画", - "queue": "キュー", - "queue_hint": "アプリを再起動するとキューとダウンロードは失われます", - "no_items_in_queue": "キューにアイテムがありません", - "no_downloaded_items": "ダウンロードしたアイテムはありません", - "delete_all_movies_button": "すべての映画を削除", - "delete_all_tvseries_button": "すべてのシリーズを削除", - "delete_all_button": "すべて削除", - "active_download": "アクティブなダウンロード", - "no_active_downloads": "アクティブなダウンロードはありません", - "active_downloads": "アクティブなダウンロード", - "new_app_version_requires_re_download": "新しいアプリバージョンでは再ダウンロードが必要です", - "new_app_version_requires_re_download_description": "新しいアップデートではコンテンツを再度ダウンロードする必要があります。ダウンロードしたコンテンツをすべて削除してもう一度お試しください。", - "back": "戻る", - "delete": "削除", - "something_went_wrong": "問題が発生しました", - "could_not_get_stream_url_from_jellyfin": "JellyfinからストリームURLを取得できませんでした", - "eta": "ETA {{eta}}", - "methods": "方法", - "toasts": { - "you_are_not_allowed_to_download_files": "ファイルをダウンロードする権限がありません。", - "deleted_all_movies_successfully": "すべての映画を正常に削除しました!", - "failed_to_delete_all_movies": "すべての映画を削除できませんでした", - "deleted_all_tvseries_successfully": "すべてのシリーズを正常に削除しました!", - "failed_to_delete_all_tvseries": "すべてのシリーズを削除できませんでした", - "download_cancelled": "ダウンロードをキャンセルしました", - "could_not_cancel_download": "ダウンロードをキャンセルできませんでした", - "download_completed": "ダウンロードが完了しました", - "download_started_for": "{{item}}のダウンロードが開始されました", - "item_is_ready_to_be_downloaded": "{{item}}をダウンロードする準備ができました", - "download_stated_for_item": "{{item}}のダウンロードが開始されました", - "download_failed_for_item": "{{item}}のダウンロードに失敗しました - {{error}}", - "download_completed_for_item": "{{item}}のダウンロードが完了しました", - "queued_item_for_optimization": "{{item}}をoptimizeのキューに追加しました", - "failed_to_start_download_for_item": "{{item}}のダウンロードを開始できませんでした: {{message}}", - "server_responded_with_status_code": "サーバーはステータス{{statusCode}}で応答しました", - "no_response_received_from_server": "サーバーからの応答がありません", - "error_setting_up_the_request": "リクエストの設定中にエラーが発生しました", - "failed_to_start_download_for_item_unexpected_error": "{{item}}のダウンロードを開始できませんでした: 予期しないエラーが発生しました", - "all_files_folders_and_jobs_deleted_successfully": "すべてのファイル、フォルダ、ジョブが正常に削除されました", - "an_error_occured_while_deleting_files_and_jobs": "ファイルとジョブの削除中にエラーが発生しました", - "go_to_downloads": "ダウンロードに移動" - } - } - }, - "search": { - "search_here": "ここを検索...", - "search": "検索...", - "x_items": "{{count}}のアイテム", - "library": "ライブラリ", - "discover": "見つける", - "no_results": "結果はありません", - "no_results_found_for": "結果が見つかりませんでした:", - "movies": "映画", - "series": "シリーズ", - "episodes": "エピソード", - "collections": "コレクション", - "actors": "俳優", - "request_movies": "映画をリクエスト", - "request_series": "シリーズをリクエスト", - "recently_added": "最近の追加", - "recent_requests": "最近のリクエスト", - "plex_watchlist": "Plexウォッチリスト", - "trending": "トレンド", - "popular_movies": "人気の映画", - "movie_genres": "映画のジャンル", - "upcoming_movies": "今後リリースされる映画", - "studios": "制作会社", - "popular_tv": "人気のテレビ番組", - "tv_genres": "シリーズのジャンル", - "upcoming_tv": "今後リリースされるシリーズ", - "networks": "ネットワーク", - "tmdb_movie_keyword": "TMDB映画キーワード", - "tmdb_movie_genre": "TMDB映画ジャンル", - "tmdb_tv_keyword": "TMDBシリーズキーワード", - "tmdb_tv_genre": "TMDBシリーズジャンル", - "tmdb_search": "TMDB検索", - "tmdb_studio": "TMDB 制作会社", - "tmdb_network": "TMDB ネットワーク", - "tmdb_movie_streaming_services": "TMDB映画ストリーミングサービス", - "tmdb_tv_streaming_services": "TMDBシリーズストリーミングサービス" - }, - "library": { - "no_items_found": "アイテムが見つかりません", - "no_results": "検索結果はありません", - "no_libraries_found": "ライブラリが見つかりません", - "item_types": { - "movies": "映画", - "series": "シリーズ", - "boxsets": "ボックスセット", - "items": "アイテム" - }, - "options": { - "display": "表示", - "row": "行", - "list": "リスト", - "image_style": "画像のスタイル", - "poster": "ポスター", - "cover": "カバー", - "show_titles": "タイトルの表示", - "show_stats": "統計を表示" - }, - "filters": { - "genres": "ジャンル", - "years": "年", - "sort_by": "ソート", - "sort_order": "ソート順", - "asc": "Ascending", - "desc": "Descending", - "tags": "タグ" - } - }, - "favorites": { - "series": "シリーズ", - "movies": "映画", - "episodes": "エピソード", - "videos": "ビデオ", - "boxsets": "ボックスセット", - "playlists": "プレイリスト", - "noDataTitle": "お気に入りはまだありません", - "noData": "アイテムをお気に入りとしてマークすると、ここに表示されクイックアクセスできるようになります。" - }, - "custom_links": { - "no_links": "リンクがありません" - }, - "player": { - "error": "エラー", - "failed_to_get_stream_url": "ストリームURLを取得できませんでした", - "an_error_occured_while_playing_the_video": "動画の再生中にエラーが発生しました。設定でログを確認してください。", - "client_error": "クライアントエラー", - "could_not_create_stream_for_chromecast": "Chromecastのストリームを作成できませんでした", - "message_from_server": "サーバーからのメッセージ", - "video_has_finished_playing": "ビデオの再生が終了しました!", - "no_video_source": "動画ソースがありません...", - "next_episode": "次のエピソード", - "refresh_tracks": "トラックを更新", - "subtitle_tracks": "字幕トラック:", - "audio_tracks": "音声トラック:", - "playback_state": "再生状態:", - "no_data_available": "データなし", - "index": "インデックス:" - }, - "item_card": { - "next_up": "次", - "no_items_to_display": "表示するアイテムがありません", - "cast_and_crew": "キャスト&クルー", - "series": "シリーズ", - "seasons": "シーズン", - "season": "シーズン", - "no_episodes_for_this_season": "このシーズンのエピソードはありません", - "overview": "ストーリー", - "more_with": "{{name}}の詳細", - "similar_items": "類似アイテム", - "no_similar_items_found": "類似のアイテムは見つかりませんでした", - "video": "映像", - "more_details": "さらに詳細を表示", - "quality": "画質", - "audio": "音声", - "subtitles": "字幕", - "show_more": "もっと見る", - "show_less": "少なく表示", - "appeared_in": "出演作品", - "could_not_load_item": "アイテムを読み込めませんでした", - "none": "なし", - "download": { - "download_season": "シーズンをダウンロード", - "download_series": "シリーズをダウンロード", - "download_episode": "エピソードをダウンロード", - "download_movie": "映画をダウンロード", - "download_x_item": "{{item_count}}のアイテムをダウンロード", - "download_button": "ダウンロード", - "using_optimized_server": "Optimizeサーバーを使用する", - "using_default_method": "デフォルトの方法を使用" - } - }, - "live_tv": { - "next": "次", - "previous": "前", - "live_tv": "ライブTV", - "coming_soon": "近日公開", - "on_now": "現在", - "shows": "表示", - "movies": "映画", - "sports": "スポーツ", - "for_kids": "子供向け", - "news": "ニュース" - }, - "jellyseerr": { - "confirm": "確認", - "cancel": "キャンセル", - "yes": "はい", - "whats_wrong": "どうしましたか?", - "issue_type": "問題の種類", - "select_an_issue": "問題を選択", - "types": "種類", - "describe_the_issue": "(オプション) 問題を説明してください...", - "submit_button": "送信", - "report_issue_button": "チケットを報告", - "request_button": "リクエスト", - "are_you_sure_you_want_to_request_all_seasons": "すべてのシーズンをリクエストしてもよろしいですか?", - "failed_to_login": "ログインに失敗しました", - "cast": "出演者", - "details": "詳細", - "status": "状態", - "original_title": "原題", - "series_type": "シリーズタイプ", - "release_dates": "公開日", - "first_air_date": "初放送日", - "next_air_date": "次回放送日", - "revenue": "収益", - "budget": "予算", - "original_language": "オリジナルの言語", - "production_country": "制作国", - "studios": "制作会社", - "network": "ネットワーク", - "currently_streaming_on": "ストリーミング中", - "advanced": "詳細", - "request_as": "別ユーザーとしてリクエスト", - "tags": "タグ", - "quality_profile": "画質プロファイル", - "root_folder": "ルートフォルダ", - "season_all": "Season (all)", - "season_number": "シーズン{{season_number}}", - "number_episodes": "エピソード{{episode_number}}", - "born": "生まれ", - "appearances": "出演", - "toasts": { - "jellyseer_does_not_meet_requirements": "Jellyseerrサーバーは最小バージョン要件を満たしていません。少なくとも 2.0.0 に更新してください。", - "jellyseerr_test_failed": "Jellyseerrテストに失敗しました。もう一度お試しください。", - "failed_to_test_jellyseerr_server_url": "JellyseerrサーバーのURLをテストに失敗しました", - "issue_submitted": "チケットを送信しました!", - "requested_item": "{{item}}をリクエスト!", - "you_dont_have_permission_to_request": "リクエストする権限がありません!", - "something_went_wrong_requesting_media": "メディアのリクエスト中に問題が発生しました。" - } - }, - "tabs": { - "home": "ホーム", - "search": "検索", - "library": "ライブラリ", - "custom_links": "カスタムリンク", - "favorites": "お気に入り" - } + "login": { + "username_required": "ユーザー名は必須です", + "error_title": "エラー", + "login_title": "ログイン", + "login_to_title": "ログイン先", + "username_placeholder": "ユーザー名", + "password_placeholder": "パスワード", + "login_button": "ログイン", + "quick_connect": "クイックコネクト", + "enter_code_to_login": "ログインするにはコード {{code}} を入力してください", + "failed_to_initiate_quick_connect": "クイックコネクトを開始できませんでした", + "got_it": "了解", + "connection_failed": "接続に失敗しました", + "could_not_connect_to_server": "サーバーに接続できませんでした。URLとネットワーク接続を確認してください。", + "an_unexpected_error_occured": "予期しないエラーが発生しました", + "change_server": "サーバーの変更", + "invalid_username_or_password": "ユーザー名またはパスワードが無効です", + "user_does_not_have_permission_to_log_in": "ユーザーにログイン権限がありません", + "server_is_taking_too_long_to_respond_try_again_later": "サーバーの応答に時間がかかりすぎています。しばらくしてからもう一度お試しください。", + "server_received_too_many_requests_try_again_later": "サーバーにリクエストが多すぎます。後でもう一度お試しください。", + "there_is_a_server_error": "サーバーエラーが発生しました", + "an_unexpected_error_occured_did_you_enter_the_correct_url": "予期しないエラーが発生しました。サーバーのURLを正しく入力しましたか?" + }, + "server": { + "enter_url_to_jellyfin_server": "JellyfinサーバーのURLを入力してください", + "server_url_placeholder": "http(s)://your-server.com", + "connect_button": "接続", + "previous_servers": "前のサーバー", + "clear_button": "クリア", + "search_for_local_servers": "ローカルサーバーを検索", + "searching": "検索中...", + "servers": "サーバー" + }, + "home": { + "no_internet": "インターネット接続がありません", + "no_items": "アイテムはありません", + "no_internet_message": "心配しないでください。\nダウンロードしたコンテンツは引き続き視聴できます。", + "go_to_downloads": "ダウンロードに移動", + "oops": "おっと!", + "error_message": "何か問題が発生しました。\nログアウトして再度ログインしてください。", + "continue_watching": "続きを見る", + "next_up": "次の動画", + "recently_added_in": "{{libraryName}}に最近追加された", + "suggested_movies": "おすすめ映画", + "suggested_episodes": "おすすめエピソード", + "intro": { + "welcome_to_streamyfin": "Streamyfinへようこそ", + "a_free_and_open_source_client_for_jellyfin": "Jellyfinのためのフリーでオープンソースのクライアント。", + "features_title": "特長", + "features_description": "Streamyfinには多くの機能があり、設定メニューで見つけることができるさまざまなソフトウェアと統合されています。これには以下が含まれます。", + "jellyseerr_feature_description": "Jellyseerrインスタンスに接続し、アプリ内で直接映画をリクエストします。", + "downloads_feature_title": "ダウンロード", + "downloads_feature_description": "映画やテレビ番組をダウンロードしてオフラインで視聴します。デフォルトの方法を使用するか、バックグラウンドでファイルをダウンロードするために最適化されたサーバーをインストールしてください。", + "chromecast_feature_description": "映画とテレビ番組をChromecastデバイスにキャストします。", + "centralised_settings_plugin_title": "集中設定プラグイン", + "centralised_settings_plugin_description": "Jellyfinサーバーから設定を構成します。すべてのユーザーのすべてのクライアント設定は自動的に同期されます。", + "done_button": "完了", + "go_to_settings_button": "設定に移動", + "read_more": "続きを読む" + }, + "settings": { + "settings_title": "設定", + "log_out_button": "ログアウト", + "user_info": { + "user_info_title": "ユーザー情報", + "user": "ユーザー", + "server": "サーバー", + "token": "トークン", + "app_version": "アプリバージョン" + }, + "quick_connect": { + "quick_connect_title": "クイックコネクト", + "authorize_button": "クイックコネクトを承認する", + "enter_the_quick_connect_code": "クイックコネクトコードを入力...", + "success": "成功しました", + "quick_connect_autorized": "クイックコネクトが承認されました", + "error": "エラー", + "invalid_code": "無効なコードです", + "authorize": "承認" + }, + "media_controls": { + "media_controls_title": "メディアコントロール", + "forward_skip_length": "スキップの長さ", + "rewind_length": "巻き戻しの長さ", + "seconds_unit": "s" + }, + "audio": { + "audio_title": "オーディオ", + "set_audio_track": "前のアイテムからオーディオトラックを設定", + "audio_language": "オーディオ言語", + "audio_hint": "デフォルトのオーディオ言語を選択します。", + "none": "なし", + "language": "言語" + }, + "subtitles": { + "subtitle_title": "字幕", + "subtitle_language": "字幕の言語", + "subtitle_mode": "字幕モード", + "set_subtitle_track": "前のアイテムから字幕トラックを設定", + "subtitle_size": "字幕サイズ", + "subtitle_hint": "字幕設定を構成します。", + "none": "なし", + "language": "言語", + "loading": "ロード中", + "modes": { + "Default": "デフォルト", + "Smart": "スマート", + "Always": "常に", + "None": "なし", + "OnlyForced": "強制のみ" + } + }, + "other": { + "other_title": "その他", + "follow_device_orientation": "画面の自動回転", + "video_orientation": "動画の向き", + "orientation": "向き", + "orientations": { + "DEFAULT": "デフォルト", + "ALL": "すべて", + "PORTRAIT": "縦", + "PORTRAIT_UP": "縦向き(上)", + "PORTRAIT_DOWN": "縦方向", + "LANDSCAPE": "横方向", + "LANDSCAPE_LEFT": "横方向 左", + "LANDSCAPE_RIGHT": "横方向 右", + "OTHER": "その他", + "UNKNOWN": "不明" + }, + "safe_area_in_controls": "コントロールの安全エリア", + "video_player": "Video player", + "video_players": { + "VLC_3": "VLC 3", + "VLC_4": "VLC 4 (Experimental + PiP)" + }, + "show_custom_menu_links": "カスタムメニューのリンクを表示", + "hide_libraries": "ライブラリを非表示", + "select_liraries_you_want_to_hide": "ライブラリタブとホームページセクションから非表示にするライブラリを選択します。", + "disable_haptic_feedback": "触覚フィードバックを無効にする" + }, + "downloads": { + "downloads_title": "ダウンロード", + "download_method": "ダウンロード方法", + "remux_max_download": "Remux最大ダウンロード数", + "auto_download": "自動ダウンロード", + "optimized_versions_server": "Optimized versionsサーバー", + "save_button": "保存", + "optimized_server": "Optimizedサーバー", + "optimized": "最適化", + "default": "デフォルト", + "optimized_version_hint": "OptimizeサーバーのURLを入力します。URLにはhttpまたはhttpsを含め、オプションでポートを指定します。", + "read_more_about_optimized_server": "Optimizeサーバーの詳細をご覧ください。", + "url": "URL", + "server_url_placeholder": "http(s)://domain.org:ポート" + }, + "plugins": { + "plugins_title": "プラグイン", + "jellyseerr": { + "jellyseerr_warning": "この統合はまだ初期段階です。状況が変化する可能性があります。", + "server_url": "サーバーURL", + "server_url_hint": "例: http(s)://your-host.url\n(必要に応じてポートを追加)", + "server_url_placeholder": "Jellyseerr URL...", + "password": "パスワード", + "password_placeholder": "Jellyfinユーザー {{username}} のパスワードを入力してください", + "save_button": "保存", + "clear_button": "クリア", + "login_button": "ログイン", + "total_media_requests": "メディアリクエストの合計", + "movie_quota_limit": "映画のクオータ制限", + "movie_quota_days": "映画のクオータ日数", + "tv_quota_limit": "テレビのクオータ制限", + "tv_quota_days": "テレビのクオータ日数", + "reset_jellyseerr_config_button": "Jellyseerrの設定をリセット", + "unlimited": "無制限", + "plus_n_more": "+{{n}} more", + "order_by": { + "DEFAULT": "Default", + "VOTE_COUNT_AND_AVERAGE": "Vote count and average", + "POPULARITY": "Popularity" + } + }, + "marlin_search": { + "enable_marlin_search": "マーリン検索を有効にする ", + "url": "URL", + "server_url_placeholder": "http(s)://domain.org:ポート", + "marlin_search_hint": "MarlinサーバーのURLを入力します。URLにはhttpまたはhttpsを含め、オプションでポートを指定します。", + "read_more_about_marlin": "Marlinについて詳しく読む。", + "save_button": "保存", + "toasts": { + "saved": "保存しました" + } + } + }, + "storage": { + "storage_title": "ストレージ", + "app_usage": "アプリ {{usedSpace}}%", + "phone_usage": "電話 {{availableSpace}}%", + "size_used": "{{used}} / {{total}} 使用済み", + "delete_all_downloaded_files": "すべてのダウンロードファイルを削除" + }, + "intro": { + "show_intro": "イントロを表示", + "reset_intro": "イントロをリセット" + }, + "logs": { + "logs_title": "ログ", + "no_logs_available": "ログがありません", + "delete_all_logs": "すべてのログを削除" + }, + "languages": { + "title": "言語", + "app_language": "アプリの言語", + "app_language_description": "アプリの言語を選択。", + "system": "システム" + }, + "toasts": { + "error_deleting_files": "ファイルの削除エラー", + "background_downloads_enabled": "バックグラウンドでのダウンロードは有効です", + "background_downloads_disabled": "バックグラウンドでのダウンロードは無効です", + "connected": "接続済み", + "could_not_connect": "接続できません", + "invalid_url": "無効なURL" + } + }, + "downloads": { + "downloads_title": "ダウンロード", + "tvseries": "TVシリーズ", + "movies": "映画", + "queue": "キュー", + "queue_hint": "アプリを再起動するとキューとダウンロードは失われます", + "no_items_in_queue": "キューにアイテムがありません", + "no_downloaded_items": "ダウンロードしたアイテムはありません", + "delete_all_movies_button": "すべての映画を削除", + "delete_all_tvseries_button": "すべてのシリーズを削除", + "delete_all_button": "すべて削除", + "active_download": "アクティブなダウンロード", + "no_active_downloads": "アクティブなダウンロードはありません", + "active_downloads": "アクティブなダウンロード", + "new_app_version_requires_re_download": "新しいアプリバージョンでは再ダウンロードが必要です", + "new_app_version_requires_re_download_description": "新しいアップデートではコンテンツを再度ダウンロードする必要があります。ダウンロードしたコンテンツをすべて削除してもう一度お試しください。", + "back": "戻る", + "delete": "削除", + "something_went_wrong": "問題が発生しました", + "could_not_get_stream_url_from_jellyfin": "JellyfinからストリームURLを取得できませんでした", + "eta": "ETA {{eta}}", + "methods": "方法", + "toasts": { + "you_are_not_allowed_to_download_files": "ファイルをダウンロードする権限がありません。", + "deleted_all_movies_successfully": "すべての映画を正常に削除しました!", + "failed_to_delete_all_movies": "すべての映画を削除できませんでした", + "deleted_all_tvseries_successfully": "すべてのシリーズを正常に削除しました!", + "failed_to_delete_all_tvseries": "すべてのシリーズを削除できませんでした", + "download_cancelled": "ダウンロードをキャンセルしました", + "could_not_cancel_download": "ダウンロードをキャンセルできませんでした", + "download_completed": "ダウンロードが完了しました", + "download_started_for": "{{item}}のダウンロードが開始されました", + "item_is_ready_to_be_downloaded": "{{item}}をダウンロードする準備ができました", + "download_stated_for_item": "{{item}}のダウンロードが開始されました", + "download_failed_for_item": "{{item}}のダウンロードに失敗しました - {{error}}", + "download_completed_for_item": "{{item}}のダウンロードが完了しました", + "queued_item_for_optimization": "{{item}}をoptimizeのキューに追加しました", + "failed_to_start_download_for_item": "{{item}}のダウンロードを開始できませんでした: {{message}}", + "server_responded_with_status_code": "サーバーはステータス{{statusCode}}で応答しました", + "no_response_received_from_server": "サーバーからの応答がありません", + "error_setting_up_the_request": "リクエストの設定中にエラーが発生しました", + "failed_to_start_download_for_item_unexpected_error": "{{item}}のダウンロードを開始できませんでした: 予期しないエラーが発生しました", + "all_files_folders_and_jobs_deleted_successfully": "すべてのファイル、フォルダ、ジョブが正常に削除されました", + "an_error_occured_while_deleting_files_and_jobs": "ファイルとジョブの削除中にエラーが発生しました", + "go_to_downloads": "ダウンロードに移動" + } + } + }, + "search": { + "search_here": "ここを検索...", + "search": "検索...", + "x_items": "{{count}}のアイテム", + "library": "ライブラリ", + "discover": "見つける", + "no_results": "結果はありません", + "no_results_found_for": "結果が見つかりませんでした:", + "movies": "映画", + "series": "シリーズ", + "episodes": "エピソード", + "collections": "コレクション", + "actors": "俳優", + "request_movies": "映画をリクエスト", + "request_series": "シリーズをリクエスト", + "recently_added": "最近の追加", + "recent_requests": "最近のリクエスト", + "plex_watchlist": "Plexウォッチリスト", + "trending": "トレンド", + "popular_movies": "人気の映画", + "movie_genres": "映画のジャンル", + "upcoming_movies": "今後リリースされる映画", + "studios": "制作会社", + "popular_tv": "人気のテレビ番組", + "tv_genres": "シリーズのジャンル", + "upcoming_tv": "今後リリースされるシリーズ", + "networks": "ネットワーク", + "tmdb_movie_keyword": "TMDB映画キーワード", + "tmdb_movie_genre": "TMDB映画ジャンル", + "tmdb_tv_keyword": "TMDBシリーズキーワード", + "tmdb_tv_genre": "TMDBシリーズジャンル", + "tmdb_search": "TMDB検索", + "tmdb_studio": "TMDB 制作会社", + "tmdb_network": "TMDB ネットワーク", + "tmdb_movie_streaming_services": "TMDB映画ストリーミングサービス", + "tmdb_tv_streaming_services": "TMDBシリーズストリーミングサービス" + }, + "library": { + "no_items_found": "アイテムが見つかりません", + "no_results": "検索結果はありません", + "no_libraries_found": "ライブラリが見つかりません", + "item_types": { + "movies": "映画", + "series": "シリーズ", + "boxsets": "ボックスセット", + "items": "アイテム" + }, + "options": { + "display": "表示", + "row": "行", + "list": "リスト", + "image_style": "画像のスタイル", + "poster": "ポスター", + "cover": "カバー", + "show_titles": "タイトルの表示", + "show_stats": "統計を表示" + }, + "filters": { + "genres": "ジャンル", + "years": "年", + "sort_by": "ソート", + "sort_order": "ソート順", + "asc": "Ascending", + "desc": "Descending", + "tags": "タグ" + } + }, + "favorites": { + "series": "シリーズ", + "movies": "映画", + "episodes": "エピソード", + "videos": "ビデオ", + "boxsets": "ボックスセット", + "playlists": "プレイリスト", + "noDataTitle": "お気に入りはまだありません", + "noData": "アイテムをお気に入りとしてマークすると、ここに表示されクイックアクセスできるようになります。" + }, + "custom_links": { + "no_links": "リンクがありません" + }, + "player": { + "error": "エラー", + "failed_to_get_stream_url": "ストリームURLを取得できませんでした", + "an_error_occured_while_playing_the_video": "動画の再生中にエラーが発生しました。設定でログを確認してください。", + "client_error": "クライアントエラー", + "could_not_create_stream_for_chromecast": "Chromecastのストリームを作成できませんでした", + "message_from_server": "サーバーからのメッセージ", + "video_has_finished_playing": "ビデオの再生が終了しました!", + "no_video_source": "動画ソースがありません...", + "next_episode": "次のエピソード", + "refresh_tracks": "トラックを更新", + "subtitle_tracks": "字幕トラック:", + "audio_tracks": "音声トラック:", + "playback_state": "再生状態:", + "no_data_available": "データなし", + "index": "インデックス:" + }, + "item_card": { + "next_up": "次", + "no_items_to_display": "表示するアイテムがありません", + "cast_and_crew": "キャスト&クルー", + "series": "シリーズ", + "seasons": "シーズン", + "season": "シーズン", + "no_episodes_for_this_season": "このシーズンのエピソードはありません", + "overview": "ストーリー", + "more_with": "{{name}}の詳細", + "similar_items": "類似アイテム", + "no_similar_items_found": "類似のアイテムは見つかりませんでした", + "video": "映像", + "more_details": "さらに詳細を表示", + "quality": "画質", + "audio": "音声", + "subtitles": "字幕", + "show_more": "もっと見る", + "show_less": "少なく表示", + "appeared_in": "出演作品", + "could_not_load_item": "アイテムを読み込めませんでした", + "none": "なし", + "download": { + "download_season": "シーズンをダウンロード", + "download_series": "シリーズをダウンロード", + "download_episode": "エピソードをダウンロード", + "download_movie": "映画をダウンロード", + "download_x_item": "{{item_count}}のアイテムをダウンロード", + "download_button": "ダウンロード", + "using_optimized_server": "Optimizeサーバーを使用する", + "using_default_method": "デフォルトの方法を使用" + } + }, + "live_tv": { + "next": "次", + "previous": "前", + "live_tv": "ライブTV", + "coming_soon": "近日公開", + "on_now": "現在", + "shows": "表示", + "movies": "映画", + "sports": "スポーツ", + "for_kids": "子供向け", + "news": "ニュース" + }, + "jellyseerr": { + "confirm": "確認", + "cancel": "キャンセル", + "yes": "はい", + "whats_wrong": "どうしましたか?", + "issue_type": "問題の種類", + "select_an_issue": "問題を選択", + "types": "種類", + "describe_the_issue": "(オプション) 問題を説明してください...", + "submit_button": "送信", + "report_issue_button": "チケットを報告", + "request_button": "リクエスト", + "are_you_sure_you_want_to_request_all_seasons": "すべてのシーズンをリクエストしてもよろしいですか?", + "failed_to_login": "ログインに失敗しました", + "cast": "出演者", + "details": "詳細", + "status": "状態", + "original_title": "原題", + "series_type": "シリーズタイプ", + "release_dates": "公開日", + "first_air_date": "初放送日", + "next_air_date": "次回放送日", + "revenue": "収益", + "budget": "予算", + "original_language": "オリジナルの言語", + "production_country": "制作国", + "studios": "制作会社", + "network": "ネットワーク", + "currently_streaming_on": "ストリーミング中", + "advanced": "詳細", + "request_as": "別ユーザーとしてリクエスト", + "tags": "タグ", + "quality_profile": "画質プロファイル", + "root_folder": "ルートフォルダ", + "season_all": "Season (all)", + "season_number": "シーズン{{season_number}}", + "number_episodes": "エピソード{{episode_number}}", + "born": "生まれ", + "appearances": "出演", + "toasts": { + "jellyseer_does_not_meet_requirements": "Jellyseerrサーバーは最小バージョン要件を満たしていません。少なくとも 2.0.0 に更新してください。", + "jellyseerr_test_failed": "Jellyseerrテストに失敗しました。もう一度お試しください。", + "failed_to_test_jellyseerr_server_url": "JellyseerrサーバーのURLをテストに失敗しました", + "issue_submitted": "チケットを送信しました!", + "requested_item": "{{item}}をリクエスト!", + "you_dont_have_permission_to_request": "リクエストする権限がありません!", + "something_went_wrong_requesting_media": "メディアのリクエスト中に問題が発生しました。" + } + }, + "tabs": { + "home": "ホーム", + "search": "検索", + "library": "ライブラリ", + "custom_links": "カスタムリンク", + "favorites": "お気に入り" + } } diff --git a/translations/nl.json b/translations/nl.json index 039803cc..eb213102 100644 --- a/translations/nl.json +++ b/translations/nl.json @@ -1,473 +1,473 @@ { - "login": { - "username_required": "Gebruikersnaam is verplicht", - "error_title": "Fout", - "login_title": "Aanmelden", - "login_to_title": "Aanmelden bij", - "username_placeholder": "Gebruikersnaam", - "password_placeholder": "Wachtwoord", - "login_button": "Aanmelden", - "quick_connect": "Snel Verbinden", - "enter_code_to_login": "Vul code {{code}} in om aan te melden", - "failed_to_initiate_quick_connect": "Mislukt om Snel Verbinden op te starten", - "got_it": "Begrepen", - "connection_failed": "Verbinding mislukt", - "could_not_connect_to_server": "Kon niet verbinden met de server. Controleer de URL en je netwerkverbinding.", - "an_unexpected_error_occured": "Er is een onverwachte fout opgetreden", - "change_server": "Server wijzigen", - "invalid_username_or_password": "Onjuiste gebruikersnaam of wachtwoord", - "user_does_not_have_permission_to_log_in": "Gebruiker heeft geen rechten om aan te melden", - "server_is_taking_too_long_to_respond_try_again_later": "De server doet er te lang over om te antwoorden, probeer later opnieuw", - "server_received_too_many_requests_try_again_later": "De server heeft te veel aanvragen ontvangen, probeer later opnieuw", - "there_is_a_server_error": "Er is een serverfout", - "an_unexpected_error_occured_did_you_enter_the_correct_url": "Er is een onverwachte fout opgetreden. Heb je de server URL correct ingegeven?" - }, - "server": { - "enter_url_to_jellyfin_server": "Geef de URL van je Jellyfin server in", - "server_url_placeholder": "http(s)://je-server.com", - "connect_button": "Verbinden", - "previous_servers": "vorige servers", - "clear_button": "Wissen", - "search_for_local_servers": "Zoek naar lokale servers", - "searching": "Zoeken...", - "servers": "Servers" - }, - "home": { - "no_internet": "Geen Internet", - "no_items": "Geen items", - "no_internet_message": "Geen zorgen, je kan nog steeds\ngedownloade content bekijken", - "go_to_downloads": "Ga naar downloads", - "oops": "Oeps!", - "error_message": "Er ging iets fout\nGelieve af en aan te melden.", - "continue_watching": "Verder Kijken", - "next_up": "Volgende", - "recently_added_in": "Recent toegevoegd in {{libraryName}}", - "suggested_movies": "Voorgestelde films", - "suggested_episodes": "Voorgestelde Afleveringen", - "intro": { - "welcome_to_streamyfin": "Welkom bij Streamyfin", - "a_free_and_open_source_client_for_jellyfin": "Een gratis en open-source client voor Jellyfin.", - "features_title": "Functies", - "features_description": "Streamyfin heeft een heleboel functies en integreert met een breed scala aan software die je kunt vinden in het instellingenmenu, onder andere:", - "jellyseerr_feature_description": "Verbind met je Jellyseerr instantie en vraag films direct in de app aan.", - "downloads_feature_title": "Downloads", - "downloads_feature_description": "Download films en series om offline te kijken. Gebruik de standaardmethode of installeer de optimalisatieserver om bestanden op de achtergrond te downloaden.", - "chromecast_feature_description": "Cast films en series naar je Chromecast toestellen.", - "centralised_settings_plugin_title": "Plugin voor gecentraliseerde instellingen", - "centralised_settings_plugin_description": "Configureer instellingen vanaf een centrale locatie op je Jellyfin server. Alle clientinstellingen voor alle gebruikers worden automatisch gesynchroniseerd.", - "done_button": "Gedaan", - "go_to_settings_button": "Ga naar instellingen", - "read_more": "Lees meer" - }, - "settings": { - "settings_title": "Instellingen", - "log_out_button": "Afmelden", - "user_info": { - "user_info_title": "Gebruiker Info", - "user": "Gebruiker", - "server": "Server", - "token": "Token", - "app_version": "App Versie" - }, - "quick_connect": { - "quick_connect_title": "Snel Verbinden", - "authorize_button": "Snel Verbinden toestaan", - "enter_the_quick_connect_code": "Vul de Snel Verbinden code in...", - "success": "Succes", - "quick_connect_autorized": "Snel Verbinden toegestaan", - "error": "Fout", - "invalid_code": "Ongeldige code", - "authorize": "Toestaan" - }, - "media_controls": { - "media_controls_title": "Media Bedieningen", - "forward_skip_length": "Duur voorwaarts overslaan", - "rewind_length": "Duur terugspoelen", - "seconds_unit": "s" - }, - "audio": { - "audio_title": "Audio", - "set_audio_track": "Gebruik Audio Track Van Vorig Item", - "audio_language": "Audio taal", - "audio_hint": "Kies een standaard audio taal.", - "none": "Geen", - "language": "Taal" - }, - "subtitles": { - "subtitle_title": "Ondertitels", - "subtitle_language": "Ondertitel taal", - "subtitle_mode": "Ondertitelmodus", - "set_subtitle_track": "Gebruik Ondertitel Track Van Vorig Item", - "subtitle_size": "Ondertitel Grootte", - "subtitle_hint": "Stel ondertitel voorkeuren in.", - "none": "Geen", - "language": "Taal", - "loading": "Laden", - "modes": { - "Default": "Standaard", - "Smart": "Slim", - "Always": "Altijd", - "None": "Geen", - "OnlyForced": "Alleen Geforceerd" - } - }, - "other": { - "other_title": "Andere", - "follow_device_orientation": "Automatisch draaien", - "video_orientation": "Video oriëntatie", - "orientation": "Oriëntatie", - "orientations": { - "DEFAULT": "Standaard", - "ALL": "Alle", - "PORTRAIT": "Portret", - "PORTRAIT_UP": "Portret Omhoog", - "PORTRAIT_DOWN": "Portret Omlaag", - "LANDSCAPE": "Landschap", - "LANDSCAPE_LEFT": "Landschap Links", - "LANDSCAPE_RIGHT": "Landschap Rechts", - "OTHER": "Andere", - "UNKNOWN": "Onbekend" - }, - "safe_area_in_controls": "Veilig gebied in bedieningen", - "video_player": "Video player", - "video_players": { - "VLC_3": "VLC 3", - "VLC_4": "VLC 4 (Experimental + PiP)" - }, - "show_custom_menu_links": "Aangepaste menulinks tonen", - "hide_libraries": "Verberg Bibliotheken", - "select_liraries_you_want_to_hide": "Selecteer de bibliotheken die je wil verbergen van de Bibliotheektab en hoofdpagina onderdelen.", - "disable_haptic_feedback": "Haptische feedback uitschakelen", - "default_quality": "Standaard kwaliteit" - }, - "downloads": { - "downloads_title": "Downloads", - "download_method": "Download methode", - "remux_max_download": "Maximale Remux-download", - "auto_download": "Auto download", - "optimized_versions_server": "Geoptimaliseerde server versies", - "save_button": "Opslaan", - "optimized_server": "Geoptimaliseerde Server", - "optimized": "Geoptimaliseerd", - "default": "Standaard", - "optimized_version_hint": "Vul de URL van de optimalisatieserver in. De URL moet http of https bevatten en eventueel de poort.", - "read_more_about_optimized_server": "Lees meer over de optimalisatieserver.", - "url": "URL", - "server_url_placeholder": "http(s)://domein.org:poort" - }, - "plugins": { - "plugins_title": "Plugins", - "jellyseerr": { - "jellyseerr_warning": "Deze integratie is nog in een vroeg stadium. Verwacht dat zaken nog veranderen.", - "server_url": "Server URL", - "server_url_hint": "Voorbeeld: http(s)://je-host.url\n(indien nodig: voeg de poort toe)", - "server_url_placeholder": "Jellyseerr URL...", - "password": "Wachtwoord", - "password_placeholder": "Voeg het wachtwoord in voor de Jellyfin gebruiker {{username}}", - "save_button": "Opslaan", - "clear_button": "Wissen", - "login_button": "Aanmelden", - "total_media_requests": "Totaal aantal mediaverzoeken", - "movie_quota_limit": "Limiet filmquota", - "movie_quota_days": "Filmquota dagen", - "tv_quota_limit": "Limiet serie quota", - "tv_quota_days": "Serie Quota dagen", - "reset_jellyseerr_config_button": "Jellyseerr opnieuw instellen", - "unlimited": "Ongelimiteerd", - "plus_n_more": "+{{n}} more", - "order_by": { - "DEFAULT": "Default", - "VOTE_COUNT_AND_AVERAGE": "Vote count and average", - "POPULARITY": "Popularity" - } - }, - "marlin_search": { - "enable_marlin_search": "Marlin Search inschakelen ", - "url": "URL", - "server_url_placeholder": "http(s)://domein.org:poort", - "marlin_search_hint": "Vul de URL van de Marlin Search server in. De URL moet http of https bevatten en eventueel de poort.", - "read_more_about_marlin": "Lees meer over Marlin.", - "save_button": "Opslaan", - "toasts": { - "saved": "Opgeslagen" - } - } - }, - "storage": { - "storage_title": "Opslag", - "app_usage": "App {{usedSpace}}%", - "device_usage": "Toestel {{availableSpace}}%", - "size_used": "{{used}} van {{total}} gebruikt", - "delete_all_downloaded_files": "Verwijder alle gedownloade bestanden" - }, - "intro": { - "show_intro": "Toon intro", - "reset_intro": "intro opnieuw instellen" - }, - "logs": { - "logs_title": "Logs", - "no_logs_available": "Geen logs beschikbaar", - "delete_all_logs": "Verwijder alle logs" - }, - "languages": { - "title": "Talen", - "app_language": "App taal", - "app_language_description": "Selecteer een taal voor de app.", - "system": "Systeem" - }, - "toasts": { - "error_deleting_files": "Fout bij het verwijderen van bestanden", - "background_downloads_enabled": "Downloads op de achtergrond ingeschakeld", - "background_downloads_disabled": "Downloads op de achtergrond uitgeschakeld", - "connected": "Verbonden", - "could_not_connect": "Kon niet verbinden", - "invalid_url": "Ongeldige URL" - } - }, - "downloads": { - "downloads_title": "Downloads", - "tvseries": "Series", - "movies": "Films", - "queue": "Wachtrij", - "queue_hint": "Wachtrij en downloads verdwijnen bij een herstart van de app", - "no_items_in_queue": "Geen items in wachtrij", - "no_downloaded_items": "Geen gedownloade items", - "delete_all_movies_button": "Verwijder alle films", - "delete_all_tvseries_button": "Verwijder alle Series", - "delete_all_button": "Verwijder alles", - "active_download": "Actieve download", - "no_active_downloads": "Geen actieve downloads", - "active_downloads": "Actieve downloads", - "new_app_version_requires_re_download": "Nieuwe app-versie vereist opnieuw downloaden", - "new_app_version_requires_re_download_description": "Voor de nieuwe update moet de content opnieuw worden gedownload. Verwijder alle gedownloade content en probeer het opnieuw.", - "back": "Terug", - "delete": "Verwijder", - "something_went_wrong": "Er ging iets mis", - "could_not_get_stream_url_from_jellyfin": "Kon de URL van de stream niet krijgen van Jellyfin", - "eta": "ETA {{eta}}", - "methods": "Methoden", - "toasts": { - "you_are_not_allowed_to_download_files": "Je mag geen bestanden downloaden.", - "deleted_all_movies_successfully": "Alle films succesvol verwijderd!", - "failed_to_delete_all_movies": "Alle films zijn niet verwijderd", - "deleted_all_tvseries_successfully": "Alle series succesvol verwijderd!", - "failed_to_delete_all_tvseries": "Alle series zijn niet verwijderd", - "download_cancelled": "Download geannuleerd", - "could_not_cancel_download": "Kon de download niet annuleren", - "download_completed": "Download afgerond", - "download_started_for": "Download gestart voor {{item}}", - "item_is_ready_to_be_downloaded": "{{item}} is klaar op te downloaden", - "download_stated_for_item": "Download gestart voor {{item}}", - "download_failed_for_item": "Download gefaald voor {{item}} - {{error}}", - "download_completed_for_item": "Download afgerond voor {{item}}", - "queued_item_for_optimization": "{{item}} in de wachtrij gezet voor optimalisatie", - "failed_to_start_download_for_item": "Kon de download voor {{item}} niet starten: {{message}}", - "server_responded_with_status_code": "Server heeft geantwoord met {{statusCode}}", - "no_response_received_from_server": "Geen antwoord gekregen van de server", - "error_setting_up_the_request": "Fout bij het opstellen van de aanvraag", - "failed_to_start_download_for_item_unexpected_error": "Kon de download voor {{item}} niet starten: Onverwachte fout", - "all_files_folders_and_jobs_deleted_successfully": "Alle bestanden, mappen en taken succesvol verwijderd", - "an_error_occured_while_deleting_files_and_jobs": "Er is een fout opgetreden tijdens het verwijderen van bestanden en taken", - "go_to_downloads": "Ga naar downloads" - } - } - }, - "search": { - "search_here": "Zoek hier...", - "search": "Zoek...", - "x_items": "{{count}} items", - "library": "Bibliotheek", - "discover": "Ontdek", - "no_results": "Geen resultaten", - "no_results_found_for": "Geen resultaten gevonden voor", - "movies": "Films", - "series": "Series", - "episodes": "Afleveringen", - "collections": "Collecties", - "actors": "Acteurs", - "request_movies": "Vraag films aan", - "request_series": "Vraag series aan", - "recently_added": "Recent Toegevoegd", - "recent_requests": "Recent Aangevraagd", - "plex_watchlist": "Plex Kijklijst", - "trending": "Trending", - "popular_movies": "Populaire films", - "movie_genres": "Film Genres", - "upcoming_movies": "Aankomende films", - "studios": "Studios", - "popular_tv": "Populaire TV", - "tv_genres": "TV Genres", - "upcoming_tv": "Aankomende TV", - "networks": "Netwerken", - "tmdb_movie_keyword": "TMDB Film Trefwoord", - "tmdb_movie_genre": "TMDB Filmgenres", - "tmdb_tv_keyword": "TMDB TV Trefwoord", - "tmdb_tv_genre": "TMDB TV-Genres", - "tmdb_search": "TMDB Zoeken", - "tmdb_studio": "TMDB Studio", - "tmdb_network": "TMDB Netwerk", - "tmdb_movie_streaming_services": "TMDB Film Streaming Diensten", - "tmdb_tv_streaming_services": "TMDB TV Streaming Diensten" - }, - "library": { - "no_items_found": "Geen items gevonden", - "no_results": "Geen resultaten", - "no_libraries_found": "Geen bibliotheken gevonden", - "item_types": { - "movies": "Films", - "series": "Series", - "boxsets": "Boxsets", - "items": "items" - }, - "options": { - "display": "Weergave", - "row": "Rij", - "list": "Lijst", - "image_style": "Stijl van afbeelding", - "poster": "Poster", - "cover": "Cover", - "show_titles": "Toon titels", - "show_stats": "Toon statistieken" - }, - "filters": { - "genres": "Genres", - "years": "Jaren", - "sort_by": "Sorteren op", - "sort_order": "Sorteer volgorde", - "asc": "Ascending", - "desc": "Descending", - "tags": "Labels" - } - }, - "favorites": { - "series": "Series", - "movies": "Films", - "episodes": "Afleveringen", - "videos": "Videos", - "boxsets": "Boxsets", - "playlists": "Afspeellijsten", - "noDataTitle": "Nog geen favorieten", - "noData": "Markeer items als favoriet om ze hier te laten verschijnen voor snelle toegang." - }, - "custom_links": { - "no_links": "Geen links" - }, - "player": { - "error": "Fout", - "failed_to_get_stream_url": "De stream-URL kon niet worden verkregen", - "an_error_occured_while_playing_the_video": "Er is een fout opgetreden tijdens het afspelen van de video. Controleer de logs in de instellingen.", - "client_error": "Fout van de client", - "could_not_create_stream_for_chromecast": "Kon geen stream maken voor Chromecast", - "message_from_server": "Bericht van de server", - "video_has_finished_playing": "Video is gedaan met spelen!", - "no_video_source": "Geen videobron...", - "next_episode": "Volgende Aflevering", - "refresh_tracks": "Tracks verversen", - "subtitle_tracks": "Ondertitel Tracks:", - "audio_tracks": "Audio Tracks:", - "playback_state": "Afspeelstatus:", - "no_data_available": "Geen data beschikbaar", - "index": "Index:" - }, - "item_card": { - "next_up": "Volgende", - "no_items_to_display": "Geen items om te tonen", - "cast_and_crew": "Cast & Crew", - "series": "Series", - "seasons": "Seizoenen", - "season": "Seizoen", - "no_episodes_for_this_season": "Geen afleveringen voor dit seizoen", - "overview": "Overzicht", - "more_with": "Meer met {{name}}", - "similar_items": "Gelijkaardige items", - "no_similar_items_found": "Geen gelijkaardige items gevonden", - "video": "Video", - "more_details": "Meer details", - "quality": "Kwaliteit", - "audio": "Audio", - "subtitles": "Ondertitel", - "show_more": "Toon meer", - "show_less": "Toon minder", - "appeared_in": "Verschenen in", - "could_not_load_item": "Kon item niet laden", - "none": "Geen", - "download": { - "download_season": "Download Seizoen", - "download_series": "Download Serie", - "download_episode": "Download Aflevering", - "download_movie": "Download Film", - "download_x_item": "Download {{item_count}} items", - "download_button": "Download", - "using_optimized_server": "Geoptimaliseerde server gebruiken", - "using_default_method": "Standaard methode gebruiken" - } - }, - "live_tv": { - "next": "Volgende ", - "previous": "Vorige", - "live_tv": "Live TV", - "coming_soon": "Binnenkort beschikbaar", - "on_now": "Nu op", - "shows": "Shows", - "movies": "Films", - "sports": "Sport", - "for_kids": "Voor kinderen", - "news": "Nieuws" - }, - "jellyseerr": { - "confirm": "Bevestig", - "cancel": "Annuleer", - "yes": "Ja", - "whats_wrong": "Wat is er mis?", - "issue_type": "Type probleem", - "select_an_issue": "Selecteer een probleem", - "types": "Types", - "describe_the_issue": "(optioneel) beschrijf het probleem...", - "submit_button": "Verzenden", - "report_issue_button": "Meld een probleem", - "request_button": "Aanvragen", - "are_you_sure_you_want_to_request_all_seasons": "Ben je zeker dat je alle seizoenen wil aanvragen?", - "failed_to_login": "Kon niet aanmelden", - "cast": "Cast", - "details": "Details", - "status": "Status", - "original_title": "Originele titel", - "series_type": "Serietype", - "release_dates": "Verschijningsdatums", - "first_air_date": "Eerste uitzenddatum", - "next_air_date": "Volgende uitzenddatum", - "revenue": "Inkomsten", - "budget": "Budget", - "original_language": "Originele taal", - "production_country": "Land van productie", - "studios": "Studio", - "network": "Netwerk", - "currently_streaming_on": "Momenteel te streamen op", - "advanced": "Geavanceerd", - "request_as": "Vraag aan als", - "tags": "Labels", - "quality_profile": "Kwaliteitsprofiel", - "root_folder": "Hoofdmap", - "season_all": "Season (all)", - "season_number": "Seizoen {{season_number}}", - "number_episodes": "{{episode_number}} Afleveringen", - "born": "Geboren", - "appearances": "Verschijningen", - "toasts": { - "jellyseer_does_not_meet_requirements": "Jellyseerr server voldoet niet aan de minimale versievereisten! Update naar minimaal 2.0.0", - "jellyseerr_test_failed": "Jellyseerr test mislukt. Probeer opnieuw.", - "failed_to_test_jellyseerr_server_url": "Mislukt bij het testen van jellyseerr server url", - "issue_submitted": "Probleem ingediend!", - "requested_item": "{{item}} aangevraagd!", - "you_dont_have_permission_to_request": "Je hebt geen toestemming om aanvragen te doen!", - "something_went_wrong_requesting_media": "Er ging iets mis met het aanvragen van media!" - } - }, - "tabs": { - "home": "Thuis", - "search": "Zoeken", - "library": "Bibliotheek", - "custom_links": "Aangepaste links", - "favorites": "Favorieten" - } + "login": { + "username_required": "Gebruikersnaam is verplicht", + "error_title": "Fout", + "login_title": "Aanmelden", + "login_to_title": "Aanmelden bij", + "username_placeholder": "Gebruikersnaam", + "password_placeholder": "Wachtwoord", + "login_button": "Aanmelden", + "quick_connect": "Snel Verbinden", + "enter_code_to_login": "Vul code {{code}} in om aan te melden", + "failed_to_initiate_quick_connect": "Mislukt om Snel Verbinden op te starten", + "got_it": "Begrepen", + "connection_failed": "Verbinding mislukt", + "could_not_connect_to_server": "Kon niet verbinden met de server. Controleer de URL en je netwerkverbinding.", + "an_unexpected_error_occured": "Er is een onverwachte fout opgetreden", + "change_server": "Server wijzigen", + "invalid_username_or_password": "Onjuiste gebruikersnaam of wachtwoord", + "user_does_not_have_permission_to_log_in": "Gebruiker heeft geen rechten om aan te melden", + "server_is_taking_too_long_to_respond_try_again_later": "De server doet er te lang over om te antwoorden, probeer later opnieuw", + "server_received_too_many_requests_try_again_later": "De server heeft te veel aanvragen ontvangen, probeer later opnieuw", + "there_is_a_server_error": "Er is een serverfout", + "an_unexpected_error_occured_did_you_enter_the_correct_url": "Er is een onverwachte fout opgetreden. Heb je de server URL correct ingegeven?" + }, + "server": { + "enter_url_to_jellyfin_server": "Geef de URL van je Jellyfin server in", + "server_url_placeholder": "http(s)://je-server.com", + "connect_button": "Verbinden", + "previous_servers": "vorige servers", + "clear_button": "Wissen", + "search_for_local_servers": "Zoek naar lokale servers", + "searching": "Zoeken...", + "servers": "Servers" + }, + "home": { + "no_internet": "Geen Internet", + "no_items": "Geen items", + "no_internet_message": "Geen zorgen, je kan nog steeds\ngedownloade content bekijken", + "go_to_downloads": "Ga naar downloads", + "oops": "Oeps!", + "error_message": "Er ging iets fout\nGelieve af en aan te melden.", + "continue_watching": "Verder Kijken", + "next_up": "Volgende", + "recently_added_in": "Recent toegevoegd in {{libraryName}}", + "suggested_movies": "Voorgestelde films", + "suggested_episodes": "Voorgestelde Afleveringen", + "intro": { + "welcome_to_streamyfin": "Welkom bij Streamyfin", + "a_free_and_open_source_client_for_jellyfin": "Een gratis en open-source client voor Jellyfin.", + "features_title": "Functies", + "features_description": "Streamyfin heeft een heleboel functies en integreert met een breed scala aan software die je kunt vinden in het instellingenmenu, onder andere:", + "jellyseerr_feature_description": "Verbind met je Jellyseerr instantie en vraag films direct in de app aan.", + "downloads_feature_title": "Downloads", + "downloads_feature_description": "Download films en series om offline te kijken. Gebruik de standaardmethode of installeer de optimalisatieserver om bestanden op de achtergrond te downloaden.", + "chromecast_feature_description": "Cast films en series naar je Chromecast toestellen.", + "centralised_settings_plugin_title": "Plugin voor gecentraliseerde instellingen", + "centralised_settings_plugin_description": "Configureer instellingen vanaf een centrale locatie op je Jellyfin server. Alle clientinstellingen voor alle gebruikers worden automatisch gesynchroniseerd.", + "done_button": "Gedaan", + "go_to_settings_button": "Ga naar instellingen", + "read_more": "Lees meer" + }, + "settings": { + "settings_title": "Instellingen", + "log_out_button": "Afmelden", + "user_info": { + "user_info_title": "Gebruiker Info", + "user": "Gebruiker", + "server": "Server", + "token": "Token", + "app_version": "App Versie" + }, + "quick_connect": { + "quick_connect_title": "Snel Verbinden", + "authorize_button": "Snel Verbinden toestaan", + "enter_the_quick_connect_code": "Vul de Snel Verbinden code in...", + "success": "Succes", + "quick_connect_autorized": "Snel Verbinden toegestaan", + "error": "Fout", + "invalid_code": "Ongeldige code", + "authorize": "Toestaan" + }, + "media_controls": { + "media_controls_title": "Media Bedieningen", + "forward_skip_length": "Duur voorwaarts overslaan", + "rewind_length": "Duur terugspoelen", + "seconds_unit": "s" + }, + "audio": { + "audio_title": "Audio", + "set_audio_track": "Gebruik Audio Track Van Vorig Item", + "audio_language": "Audio taal", + "audio_hint": "Kies een standaard audio taal.", + "none": "Geen", + "language": "Taal" + }, + "subtitles": { + "subtitle_title": "Ondertitels", + "subtitle_language": "Ondertitel taal", + "subtitle_mode": "Ondertitelmodus", + "set_subtitle_track": "Gebruik Ondertitel Track Van Vorig Item", + "subtitle_size": "Ondertitel Grootte", + "subtitle_hint": "Stel ondertitel voorkeuren in.", + "none": "Geen", + "language": "Taal", + "loading": "Laden", + "modes": { + "Default": "Standaard", + "Smart": "Slim", + "Always": "Altijd", + "None": "Geen", + "OnlyForced": "Alleen Geforceerd" + } + }, + "other": { + "other_title": "Andere", + "follow_device_orientation": "Automatisch draaien", + "video_orientation": "Video oriëntatie", + "orientation": "Oriëntatie", + "orientations": { + "DEFAULT": "Standaard", + "ALL": "Alle", + "PORTRAIT": "Portret", + "PORTRAIT_UP": "Portret Omhoog", + "PORTRAIT_DOWN": "Portret Omlaag", + "LANDSCAPE": "Landschap", + "LANDSCAPE_LEFT": "Landschap Links", + "LANDSCAPE_RIGHT": "Landschap Rechts", + "OTHER": "Andere", + "UNKNOWN": "Onbekend" + }, + "safe_area_in_controls": "Veilig gebied in bedieningen", + "video_player": "Video player", + "video_players": { + "VLC_3": "VLC 3", + "VLC_4": "VLC 4 (Experimental + PiP)" + }, + "show_custom_menu_links": "Aangepaste menulinks tonen", + "hide_libraries": "Verberg Bibliotheken", + "select_liraries_you_want_to_hide": "Selecteer de bibliotheken die je wil verbergen van de Bibliotheektab en hoofdpagina onderdelen.", + "disable_haptic_feedback": "Haptische feedback uitschakelen", + "default_quality": "Standaard kwaliteit" + }, + "downloads": { + "downloads_title": "Downloads", + "download_method": "Download methode", + "remux_max_download": "Maximale Remux-download", + "auto_download": "Auto download", + "optimized_versions_server": "Geoptimaliseerde server versies", + "save_button": "Opslaan", + "optimized_server": "Geoptimaliseerde Server", + "optimized": "Geoptimaliseerd", + "default": "Standaard", + "optimized_version_hint": "Vul de URL van de optimalisatieserver in. De URL moet http of https bevatten en eventueel de poort.", + "read_more_about_optimized_server": "Lees meer over de optimalisatieserver.", + "url": "URL", + "server_url_placeholder": "http(s)://domein.org:poort" + }, + "plugins": { + "plugins_title": "Plugins", + "jellyseerr": { + "jellyseerr_warning": "Deze integratie is nog in een vroeg stadium. Verwacht dat zaken nog veranderen.", + "server_url": "Server URL", + "server_url_hint": "Voorbeeld: http(s)://je-host.url\n(indien nodig: voeg de poort toe)", + "server_url_placeholder": "Jellyseerr URL...", + "password": "Wachtwoord", + "password_placeholder": "Voeg het wachtwoord in voor de Jellyfin gebruiker {{username}}", + "save_button": "Opslaan", + "clear_button": "Wissen", + "login_button": "Aanmelden", + "total_media_requests": "Totaal aantal mediaverzoeken", + "movie_quota_limit": "Limiet filmquota", + "movie_quota_days": "Filmquota dagen", + "tv_quota_limit": "Limiet serie quota", + "tv_quota_days": "Serie Quota dagen", + "reset_jellyseerr_config_button": "Jellyseerr opnieuw instellen", + "unlimited": "Ongelimiteerd", + "plus_n_more": "+{{n}} more", + "order_by": { + "DEFAULT": "Default", + "VOTE_COUNT_AND_AVERAGE": "Vote count and average", + "POPULARITY": "Popularity" + } + }, + "marlin_search": { + "enable_marlin_search": "Marlin Search inschakelen ", + "url": "URL", + "server_url_placeholder": "http(s)://domein.org:poort", + "marlin_search_hint": "Vul de URL van de Marlin Search server in. De URL moet http of https bevatten en eventueel de poort.", + "read_more_about_marlin": "Lees meer over Marlin.", + "save_button": "Opslaan", + "toasts": { + "saved": "Opgeslagen" + } + } + }, + "storage": { + "storage_title": "Opslag", + "app_usage": "App {{usedSpace}}%", + "device_usage": "Toestel {{availableSpace}}%", + "size_used": "{{used}} van {{total}} gebruikt", + "delete_all_downloaded_files": "Verwijder alle gedownloade bestanden" + }, + "intro": { + "show_intro": "Toon intro", + "reset_intro": "intro opnieuw instellen" + }, + "logs": { + "logs_title": "Logs", + "no_logs_available": "Geen logs beschikbaar", + "delete_all_logs": "Verwijder alle logs" + }, + "languages": { + "title": "Talen", + "app_language": "App taal", + "app_language_description": "Selecteer een taal voor de app.", + "system": "Systeem" + }, + "toasts": { + "error_deleting_files": "Fout bij het verwijderen van bestanden", + "background_downloads_enabled": "Downloads op de achtergrond ingeschakeld", + "background_downloads_disabled": "Downloads op de achtergrond uitgeschakeld", + "connected": "Verbonden", + "could_not_connect": "Kon niet verbinden", + "invalid_url": "Ongeldige URL" + } + }, + "downloads": { + "downloads_title": "Downloads", + "tvseries": "Series", + "movies": "Films", + "queue": "Wachtrij", + "queue_hint": "Wachtrij en downloads verdwijnen bij een herstart van de app", + "no_items_in_queue": "Geen items in wachtrij", + "no_downloaded_items": "Geen gedownloade items", + "delete_all_movies_button": "Verwijder alle films", + "delete_all_tvseries_button": "Verwijder alle Series", + "delete_all_button": "Verwijder alles", + "active_download": "Actieve download", + "no_active_downloads": "Geen actieve downloads", + "active_downloads": "Actieve downloads", + "new_app_version_requires_re_download": "Nieuwe app-versie vereist opnieuw downloaden", + "new_app_version_requires_re_download_description": "Voor de nieuwe update moet de content opnieuw worden gedownload. Verwijder alle gedownloade content en probeer het opnieuw.", + "back": "Terug", + "delete": "Verwijder", + "something_went_wrong": "Er ging iets mis", + "could_not_get_stream_url_from_jellyfin": "Kon de URL van de stream niet krijgen van Jellyfin", + "eta": "ETA {{eta}}", + "methods": "Methoden", + "toasts": { + "you_are_not_allowed_to_download_files": "Je mag geen bestanden downloaden.", + "deleted_all_movies_successfully": "Alle films succesvol verwijderd!", + "failed_to_delete_all_movies": "Alle films zijn niet verwijderd", + "deleted_all_tvseries_successfully": "Alle series succesvol verwijderd!", + "failed_to_delete_all_tvseries": "Alle series zijn niet verwijderd", + "download_cancelled": "Download geannuleerd", + "could_not_cancel_download": "Kon de download niet annuleren", + "download_completed": "Download afgerond", + "download_started_for": "Download gestart voor {{item}}", + "item_is_ready_to_be_downloaded": "{{item}} is klaar op te downloaden", + "download_stated_for_item": "Download gestart voor {{item}}", + "download_failed_for_item": "Download gefaald voor {{item}} - {{error}}", + "download_completed_for_item": "Download afgerond voor {{item}}", + "queued_item_for_optimization": "{{item}} in de wachtrij gezet voor optimalisatie", + "failed_to_start_download_for_item": "Kon de download voor {{item}} niet starten: {{message}}", + "server_responded_with_status_code": "Server heeft geantwoord met {{statusCode}}", + "no_response_received_from_server": "Geen antwoord gekregen van de server", + "error_setting_up_the_request": "Fout bij het opstellen van de aanvraag", + "failed_to_start_download_for_item_unexpected_error": "Kon de download voor {{item}} niet starten: Onverwachte fout", + "all_files_folders_and_jobs_deleted_successfully": "Alle bestanden, mappen en taken succesvol verwijderd", + "an_error_occured_while_deleting_files_and_jobs": "Er is een fout opgetreden tijdens het verwijderen van bestanden en taken", + "go_to_downloads": "Ga naar downloads" + } + } + }, + "search": { + "search_here": "Zoek hier...", + "search": "Zoek...", + "x_items": "{{count}} items", + "library": "Bibliotheek", + "discover": "Ontdek", + "no_results": "Geen resultaten", + "no_results_found_for": "Geen resultaten gevonden voor", + "movies": "Films", + "series": "Series", + "episodes": "Afleveringen", + "collections": "Collecties", + "actors": "Acteurs", + "request_movies": "Vraag films aan", + "request_series": "Vraag series aan", + "recently_added": "Recent Toegevoegd", + "recent_requests": "Recent Aangevraagd", + "plex_watchlist": "Plex Kijklijst", + "trending": "Trending", + "popular_movies": "Populaire films", + "movie_genres": "Film Genres", + "upcoming_movies": "Aankomende films", + "studios": "Studios", + "popular_tv": "Populaire TV", + "tv_genres": "TV Genres", + "upcoming_tv": "Aankomende TV", + "networks": "Netwerken", + "tmdb_movie_keyword": "TMDB Film Trefwoord", + "tmdb_movie_genre": "TMDB Filmgenres", + "tmdb_tv_keyword": "TMDB TV Trefwoord", + "tmdb_tv_genre": "TMDB TV-Genres", + "tmdb_search": "TMDB Zoeken", + "tmdb_studio": "TMDB Studio", + "tmdb_network": "TMDB Netwerk", + "tmdb_movie_streaming_services": "TMDB Film Streaming Diensten", + "tmdb_tv_streaming_services": "TMDB TV Streaming Diensten" + }, + "library": { + "no_items_found": "Geen items gevonden", + "no_results": "Geen resultaten", + "no_libraries_found": "Geen bibliotheken gevonden", + "item_types": { + "movies": "Films", + "series": "Series", + "boxsets": "Boxsets", + "items": "items" + }, + "options": { + "display": "Weergave", + "row": "Rij", + "list": "Lijst", + "image_style": "Stijl van afbeelding", + "poster": "Poster", + "cover": "Cover", + "show_titles": "Toon titels", + "show_stats": "Toon statistieken" + }, + "filters": { + "genres": "Genres", + "years": "Jaren", + "sort_by": "Sorteren op", + "sort_order": "Sorteer volgorde", + "asc": "Ascending", + "desc": "Descending", + "tags": "Labels" + } + }, + "favorites": { + "series": "Series", + "movies": "Films", + "episodes": "Afleveringen", + "videos": "Videos", + "boxsets": "Boxsets", + "playlists": "Afspeellijsten", + "noDataTitle": "Nog geen favorieten", + "noData": "Markeer items als favoriet om ze hier te laten verschijnen voor snelle toegang." + }, + "custom_links": { + "no_links": "Geen links" + }, + "player": { + "error": "Fout", + "failed_to_get_stream_url": "De stream-URL kon niet worden verkregen", + "an_error_occured_while_playing_the_video": "Er is een fout opgetreden tijdens het afspelen van de video. Controleer de logs in de instellingen.", + "client_error": "Fout van de client", + "could_not_create_stream_for_chromecast": "Kon geen stream maken voor Chromecast", + "message_from_server": "Bericht van de server", + "video_has_finished_playing": "Video is gedaan met spelen!", + "no_video_source": "Geen videobron...", + "next_episode": "Volgende Aflevering", + "refresh_tracks": "Tracks verversen", + "subtitle_tracks": "Ondertitel Tracks:", + "audio_tracks": "Audio Tracks:", + "playback_state": "Afspeelstatus:", + "no_data_available": "Geen data beschikbaar", + "index": "Index:" + }, + "item_card": { + "next_up": "Volgende", + "no_items_to_display": "Geen items om te tonen", + "cast_and_crew": "Cast & Crew", + "series": "Series", + "seasons": "Seizoenen", + "season": "Seizoen", + "no_episodes_for_this_season": "Geen afleveringen voor dit seizoen", + "overview": "Overzicht", + "more_with": "Meer met {{name}}", + "similar_items": "Gelijkaardige items", + "no_similar_items_found": "Geen gelijkaardige items gevonden", + "video": "Video", + "more_details": "Meer details", + "quality": "Kwaliteit", + "audio": "Audio", + "subtitles": "Ondertitel", + "show_more": "Toon meer", + "show_less": "Toon minder", + "appeared_in": "Verschenen in", + "could_not_load_item": "Kon item niet laden", + "none": "Geen", + "download": { + "download_season": "Download Seizoen", + "download_series": "Download Serie", + "download_episode": "Download Aflevering", + "download_movie": "Download Film", + "download_x_item": "Download {{item_count}} items", + "download_button": "Download", + "using_optimized_server": "Geoptimaliseerde server gebruiken", + "using_default_method": "Standaard methode gebruiken" + } + }, + "live_tv": { + "next": "Volgende ", + "previous": "Vorige", + "live_tv": "Live TV", + "coming_soon": "Binnenkort beschikbaar", + "on_now": "Nu op", + "shows": "Shows", + "movies": "Films", + "sports": "Sport", + "for_kids": "Voor kinderen", + "news": "Nieuws" + }, + "jellyseerr": { + "confirm": "Bevestig", + "cancel": "Annuleer", + "yes": "Ja", + "whats_wrong": "Wat is er mis?", + "issue_type": "Type probleem", + "select_an_issue": "Selecteer een probleem", + "types": "Types", + "describe_the_issue": "(optioneel) beschrijf het probleem...", + "submit_button": "Verzenden", + "report_issue_button": "Meld een probleem", + "request_button": "Aanvragen", + "are_you_sure_you_want_to_request_all_seasons": "Ben je zeker dat je alle seizoenen wil aanvragen?", + "failed_to_login": "Kon niet aanmelden", + "cast": "Cast", + "details": "Details", + "status": "Status", + "original_title": "Originele titel", + "series_type": "Serietype", + "release_dates": "Verschijningsdatums", + "first_air_date": "Eerste uitzenddatum", + "next_air_date": "Volgende uitzenddatum", + "revenue": "Inkomsten", + "budget": "Budget", + "original_language": "Originele taal", + "production_country": "Land van productie", + "studios": "Studio", + "network": "Netwerk", + "currently_streaming_on": "Momenteel te streamen op", + "advanced": "Geavanceerd", + "request_as": "Vraag aan als", + "tags": "Labels", + "quality_profile": "Kwaliteitsprofiel", + "root_folder": "Hoofdmap", + "season_all": "Season (all)", + "season_number": "Seizoen {{season_number}}", + "number_episodes": "{{episode_number}} Afleveringen", + "born": "Geboren", + "appearances": "Verschijningen", + "toasts": { + "jellyseer_does_not_meet_requirements": "Jellyseerr server voldoet niet aan de minimale versievereisten! Update naar minimaal 2.0.0", + "jellyseerr_test_failed": "Jellyseerr test mislukt. Probeer opnieuw.", + "failed_to_test_jellyseerr_server_url": "Mislukt bij het testen van jellyseerr server url", + "issue_submitted": "Probleem ingediend!", + "requested_item": "{{item}} aangevraagd!", + "you_dont_have_permission_to_request": "Je hebt geen toestemming om aanvragen te doen!", + "something_went_wrong_requesting_media": "Er ging iets mis met het aanvragen van media!" + } + }, + "tabs": { + "home": "Thuis", + "search": "Zoeken", + "library": "Bibliotheek", + "custom_links": "Aangepaste links", + "favorites": "Favorieten" + } } diff --git a/translations/sv.json b/translations/sv.json index c5a10124..65cc3aed 100644 --- a/translations/sv.json +++ b/translations/sv.json @@ -1,34 +1,34 @@ { - "login": { - "username_required": "Användarnamn krävs", - "error_title": "Fel", - "login_title": "Logga in", - "username_placeholder": "Användarnamn", - "password_placeholder": "Lösenord", - "login_button": "Logga in" - }, - "server": { - "server_url_placeholder": "Server URL", - "connect_button": "Anslut" - }, - "home": { - "home": "Hem", - "no_internet": "Ingen Internet", - "no_internet_message": "Ingen fara, du kan fortfarande titta\npå nedladdat innehåll.", - "go_to_downloads": "Gå till nedladdningar", - "oops": "Hoppsan!", - "error_message": "Något gick fel.\nLogga ut och in igen.", - "continue_watching": "Fortsätt titta", - "next_up": "Nästa upp", - "recently_added_in": "Nyligen tillagt i {{libraryName}}" - }, - "favorites": { - "noDataTitle": "Inga favoriter än", - "noData": "Markera objekt som favoriter för att se dem visas här för snabb åtkomst." - }, - "tabs": { - "home": "Hem", - "search": "Sök", - "library": "Bibliotek" - } + "login": { + "username_required": "Användarnamn krävs", + "error_title": "Fel", + "login_title": "Logga in", + "username_placeholder": "Användarnamn", + "password_placeholder": "Lösenord", + "login_button": "Logga in" + }, + "server": { + "server_url_placeholder": "Server URL", + "connect_button": "Anslut" + }, + "home": { + "home": "Hem", + "no_internet": "Ingen Internet", + "no_internet_message": "Ingen fara, du kan fortfarande titta\npå nedladdat innehåll.", + "go_to_downloads": "Gå till nedladdningar", + "oops": "Hoppsan!", + "error_message": "Något gick fel.\nLogga ut och in igen.", + "continue_watching": "Fortsätt titta", + "next_up": "Nästa upp", + "recently_added_in": "Nyligen tillagt i {{libraryName}}" + }, + "favorites": { + "noDataTitle": "Inga favoriter än", + "noData": "Markera objekt som favoriter för att se dem visas här för snabb åtkomst." + }, + "tabs": { + "home": "Hem", + "search": "Sök", + "library": "Bibliotek" + } } diff --git a/translations/tr.json b/translations/tr.json index 7886423c..6d1a6714 100644 --- a/translations/tr.json +++ b/translations/tr.json @@ -1,472 +1,472 @@ { - "login": { - "username_required": "Kullanıcı adı gereklidir", - "error_title": "Hata", - "login_title": "Giriş yap", - "login_to_title": " 'e giriş yap", - "username_placeholder": "Kullanıcı adı", - "password_placeholder": "Şifre", - "login_button": "Giriş yap", - "quick_connect": "Quick Connect", - "enter_code_to_login": "Giriş yapmak için {{code}} kodunu girin", - "failed_to_initiate_quick_connect": "Quick Connect başlatılamadı", - "got_it": "Anlaşıldı", - "connection_failed": "Bağlantı başarısız", - "could_not_connect_to_server": "Sunucuya bağlanılamadı. Lütfen URL'yi ve ağ bağlantınızı kontrol edin", - "an_unexpected_error_occured": "Beklenmedik bir hata oluştu", - "change_server": "Sunucuyu değiştir", - "invalid_username_or_password": "Geçersiz kullanıcı adı veya şifre", - "user_does_not_have_permission_to_log_in": "Kullanıcının giriş yapma izni yok", - "server_is_taking_too_long_to_respond_try_again_later": "Sunucu yanıt vermekte çok uzun sürüyor, lütfen tekrar deneyin", - "server_received_too_many_requests_try_again_later": "Sunucu çok fazla istek aldı, lütfen tekrar deneyin.", - "there_is_a_server_error": "Sunucu hatası var", - "an_unexpected_error_occured_did_you_enter_the_correct_url": "Beklenmedik bir hata oluştu. Sunucu URL'sini doğru girdiğinizden emin oldunuz mu?" - }, - "server": { - "enter_url_to_jellyfin_server": "Jellyfin sunucunusun URL'sini girin", - "server_url_placeholder": "http(s)://sunucunuz.com", - "connect_button": "Bağlan", - "previous_servers": "Önceki sunucular", - "clear_button": "Temizle", - "search_for_local_servers": "Yerel sunucuları ara", - "searching": "Aranıyor...", - "servers": "Sunucular" - }, - "home": { - "no_internet": "İnternet Yok", - "no_items": "Öge Yok", - "no_internet_message": "Endişelenmeyin, hala\ndownloaded içerik izleyebilirsiniz.", - "go_to_downloads": "İndirmelere Git", - "oops": "Hups!", - "error_message": "Bir şeyler ters gitti.\nLütfen çıkış yapın ve tekrar giriş yapın.", - "continue_watching": "İzlemeye Devam Et", - "next_up": "Sonraki", - "recently_added_in": "{{libraryName}}'de Yakınlarda Eklendi", - "suggested_movies": "Önerilen Filmler", - "suggested_episodes": "Önerilen Bölümler", - "intro": { - "welcome_to_streamyfin": "Streamyfin'e Hoş Geldiniz", - "a_free_and_open_source_client_for_jellyfin": "Jellyfin için ücretsiz ve açık kaynak bir istemci.", - "features_title": "Özellikler", - "features_description": "Streamyfin birçok özelliğe sahip ve ayarlar menüsünde bulabileceğiniz çeşitli yazılımlarla entegre olabiliyor. Bunlar arasında şunlar bulunuyor:", - "jellyseerr_feature_description": "Jellyseerr örneğinizle bağlantı kurun ve uygulama içinde doğrudan film talep edin.", - "downloads_feature_title": "İndirmeler", - "downloads_feature_description": "Filmleri ve TV dizilerini çevrimdışı izlemek için indirin. Varsayılan yöntemi veya dosyaları arka planda indirmek için optimize sunucuyu kurabilirsiniz.", - "chromecast_feature_description": "Filmleri ve TV dizilerini Chromecast cihazlarınıza aktarın.", - "centralised_settings_plugin_title": "Merkezi Ayarlar Eklentisi", - "centralised_settings_plugin_description": "Jellyfin sunucunuzda merkezi bir yerden ayarları yapılandırın. Tüm istemci ayarları tüm kullanıcılar için otomatik olarak senkronize edilecektir.", - "done_button": "Tamam", - "go_to_settings_button": "Ayrıntılara Git", - "read_more": "Daha fazla oku" - }, - "settings": { - "settings_title": "Ayarlar", - "log_out_button": "Çıkış Yap", - "user_info": { - "user_info_title": "Kullanıcı Bilgisi", - "user": "Kullanıcı", - "server": "Sunucu", - "token": "Token", - "app_version": "Uygulama Sürümü" - }, - "quick_connect": { - "quick_connect_title": "Hızlı Bağlantı", - "authorize_button": "Hızlı Bağlantıyı Yetkilendir", - "enter_the_quick_connect_code": "Hızlı bağlantı kodunu girin...", - "success": "Başarılı", - "quick_connect_autorized": "Hızlı Bağlantı Yetkilendirildi", - "error": "Hata", - "invalid_code": "Geçersiz kod", - "authorize": "Yetkilendir" - }, - "media_controls": { - "media_controls_title": "Medya Kontrolleri", - "forward_skip_length": "İleri Sarma Uzunluğu", - "rewind_length": "Geri Sarma Uzunluğu", - "seconds_unit": "s" - }, - "audio": { - "audio_title": "Ses", - "set_audio_track": "Önceki Öğeden Ses Parçası Ayarla", - "audio_language": "Ses Dili", - "audio_hint": "Varsayılan ses dilini seçin.", - "none": "Yok", - "language": "Dil" - }, - "subtitles": { - "subtitle_title": "Altyazılar", - "subtitle_language": "Altyazı Dili", - "subtitle_mode": "Altyazı Modu", - "set_subtitle_track": "Önceki Öğeden Altyazı Parçası Ayarla", - "subtitle_size": "Altyazı Boyutu", - "subtitle_hint": "Altyazı tercihini yapılandırın.", - "none": "Yok", - "language": "Dil", - "loading": "Yükleniyor", - "modes": { - "Default": "Varsayılan", - "Smart": "Akıllı", - "Always": "Her Zaman", - "None": "Yok", - "OnlyForced": "Sadece Zorunlu" - } - }, - "other": { - "other_title": "Diğer", - "follow_device_orientation": "Otomatik Döndürme", - "video_orientation": "Video Yönü", - "orientation": "Yön", - "orientations": { - "DEFAULT": "Varsayılan", - "ALL": "Tümü", - "PORTRAIT": "Dikey", - "PORTRAIT_UP": "Dikey Yukarı", - "PORTRAIT_DOWN": "Dikey Aşağı", - "LANDSCAPE": "Yatay", - "LANDSCAPE_LEFT": "Yatay Sol", - "LANDSCAPE_RIGHT": "Yatay Sağ", - "OTHER": "Diğer", - "UNKNOWN": "Bilinmeyen" - }, - "safe_area_in_controls": "Kontrollerde Güvenli Alan", - "video_player": "Video player", - "video_players": { - "VLC_3": "VLC 3", - "VLC_4": "VLC 4 (Experimental + PiP)" - }, - "show_custom_menu_links": "Özel Menü Bağlantılarını Göster", - "hide_libraries": "Kütüphaneleri Gizle", - "select_liraries_you_want_to_hide": "Kütüphane sekmesinden ve ana sayfa bölümlerinden gizlemek istediğiniz kütüphaneleri seçin.", - "disable_haptic_feedback": "Dokunsal Geri Bildirimi Devre Dışı Bırak" - }, - "downloads": { - "downloads_title": "İndirmeler", - "download_method": "İndirme Yöntemi", - "remux_max_download": "Remux max indirme", - "auto_download": "Otomatik İndirme", - "optimized_versions_server": "Optimize edilmiş sürümler sunucusu", - "save_button": "Kaydet", - "optimized_server": "Optimize Sunucu", - "optimized": "Optimize", - "default": "Varsayılan", - "optimized_version_hint": "Optimize sunucusu için URL girin. URL, http veya https içermeli ve isteğe bağlı olarak portu içerebilir.", - "read_more_about_optimized_server": "Optimize sunucusu hakkında daha fazla oku.", - "url": "URL", - "server_url_placeholder": "http(s)://domain.org:port" - }, - "plugins": { - "plugins_title": "Eklentiler", - "jellyseerr": { - "jellyseerr_warning": "Bu entegrasyon erken aşamalardadır. Değişiklikler olabilir.", - "server_url": "Sunucu URL'si", - "server_url_hint": "Örnek: http(s)://your-host.url\n(port gerekiyorsa ekleyin)", - "server_url_placeholder": "Jellyseerr URL...", - "password": "Şifre", - "password_placeholder": "Jellyfin kullanıcısı {{username}} için şifre girin", - "save_button": "Kaydet", - "clear_button": "Temizle", - "login_button": "Giriş Yap", - "total_media_requests": "Toplam medya istekleri", - "movie_quota_limit": "Film kota limiti", - "movie_quota_days": "Film kota günleri", - "tv_quota_limit": "TV kota limiti", - "tv_quota_days": "TV kota günleri", - "reset_jellyseerr_config_button": "Jellyseerr yapılandırmasını sıfırla", - "unlimited": "Sınırsız", - "plus_n_more": "+{{n}} more", - "order_by": { - "DEFAULT": "Default", - "VOTE_COUNT_AND_AVERAGE": "Vote count and average", - "POPULARITY": "Popularity" - } - }, - "marlin_search": { - "enable_marlin_search": "Marlin Aramasını Etkinleştir ", - "url": "URL", - "server_url_placeholder": "http(s)://domain.org:port", - "marlin_search_hint": "Marlin sunucu URL'sini girin. URL, http veya https içermeli ve isteğe bağlı olarak portu içerebilir.", - "read_more_about_marlin": "Marlin hakkında daha fazla oku.", - "save_button": "Kaydet", - "toasts": { - "saved": "Kaydedildi" - } - } - }, - "storage": { - "storage_title": "Depolama", - "app_usage": "Uygulama {{usedSpace}}%", - "device_usage": "Cihaz {{availableSpace}}%", - "size_used": "{{used}} / {{total}} kullanıldı", - "delete_all_downloaded_files": "Tüm indirilen dosyaları sil" - }, - "intro": { - "show_intro": "Tanıtımı Göster", - "reset_intro": "Tanıtımı Sıfırla" - }, - "logs": { - "logs_title": "Günlükler", - "no_logs_available": "Günlükler mevcut değil", - "delete_all_logs": "Tüm günlükleri sil" - }, - "languages": { - "title": "Diller", - "app_language": "Uygulama dili", - "app_language_description": "Uygulama dilini seçin.", - "system": "Sistem" - }, - "toasts": { - "error_deleting_files": "Dosyalar silinirken hata oluştu", - "background_downloads_enabled": "Arka plan indirmeleri etkinleştirildi", - "background_downloads_disabled": "Arka plan indirmeleri devre dışı bırakıldı", - "connected": "Bağlandı", - "could_not_connect": "Bağlanılamadı", - "invalid_url": "Geçersiz URL" - } - }, - "downloads": { - "downloads_title": "İndirilenler", - "tvseries": "Diziler", - "movies": "Filmler", - "queue": "Sıra", - "queue_hint": "Sıra ve indirmeler uygulama yeniden başlatıldığında kaybolacaktır", - "no_items_in_queue": "Sırada öğe yok", - "no_downloaded_items": "İndirilen öğe yok", - "delete_all_movies_button": "Tüm Filmleri Sil", - "delete_all_tvseries_button": "Tüm Dizileri Sil", - "delete_all_button": "Tümünü Sil", - "active_download": "Aktif indirme", - "no_active_downloads": "Aktif indirme yok", - "active_downloads": "Aktif indirmeler", - "new_app_version_requires_re_download": "Yeni uygulama sürümü yeniden indirme gerektiriyor", - "new_app_version_requires_re_download_description": "Yeni güncelleme, içeriğin yeniden indirilmesini gerektiriyor. Lütfen tüm indirilen içerikleri kaldırıp tekrar deneyin.", - "back": "Geri", - "delete": "Sil", - "something_went_wrong": "Bir şeyler ters gitti", - "could_not_get_stream_url_from_jellyfin": "Jellyfin'den yayın URL'si alınamadı", - "eta": "Tahmini Süre {{eta}}", - "methods": "Yöntemler", - "toasts": { - "you_are_not_allowed_to_download_files": "Dosyaları indirme izniniz yok.", - "deleted_all_movies_successfully": "Tüm filmler başarıyla silindi!", - "failed_to_delete_all_movies": "Filmler silinemedi", - "deleted_all_tvseries_successfully": "Tüm diziler başarıyla silindi!", - "failed_to_delete_all_tvseries": "Diziler silinemedi", - "download_cancelled": "İndirme iptal edildi", - "could_not_cancel_download": "İndirme iptal edilemedi", - "download_completed": "İndirme tamamlandı", - "download_started_for": "{{item}} için indirme başlatıldı", - "item_is_ready_to_be_downloaded": "{{item}} indirmeye hazır", - "download_stated_for_item": "{{item}} için indirme başlatıldı", - "download_failed_for_item": "{{item}} için indirme başarısız oldu - {{error}}", - "download_completed_for_item": "{{item}} için indirme tamamlandı", - "queued_item_for_optimization": "{{item}} optimizasyon için sıraya alındı", - "failed_to_start_download_for_item": "{{item}} için indirme başlatılamadı: {{message}}", - "server_responded_with_status_code": "Sunucu şu durum koduyla yanıt verdi: {{statusCode}}", - "no_response_received_from_server": "Sunucudan yanıt alınamadı", - "error_setting_up_the_request": "İstek ayarlanırken hata oluştu", - "failed_to_start_download_for_item_unexpected_error": "{{item}} için indirme başlatılamadı: Beklenmeyen hata", - "all_files_folders_and_jobs_deleted_successfully": "Tüm dosyalar, klasörler ve işler başarıyla silindi", - "an_error_occured_while_deleting_files_and_jobs": "Dosyalar ve işler silinirken hata oluştu", - "go_to_downloads": "İndirmelere git" - } - } - }, - "search": { - "search_here": "Burada ara...", - "search": "Ara...", - "x_items": "{{count}} öge(ler)", - "library": "Kütüphane", - "discover": "Keşfet", - "no_results": "Sonuç bulunamadı", - "no_results_found_for": "\"{{query}}\" için sonuç bulunamadı", - "movies": "Filmler", - "series": "Diziler", - "episodes": "Bölümler", - "collections": "Koleksiyonlar", - "actors": "Oyuncular", - "request_movies": "Film Talep Et", - "request_series": "Dizi Talep Et", - "recently_added": "Son Eklenenler", - "recent_requests": "Son Talepler", - "plex_watchlist": "Plex İzleme Listesi", - "trending": "Şu An Popüler", - "popular_movies": "Popüler Filmler", - "movie_genres": "Film Türleri", - "upcoming_movies": "Yaklaşan Filmler", - "studios": "Stüdyolar", - "popular_tv": "Popüler Diziler", - "tv_genres": "Dizi Türleri", - "upcoming_tv": "Yaklaşan Diziler", - "networks": "Ağlar", - "tmdb_movie_keyword": "TMDB Film Anahtar Kelimesi", - "tmdb_movie_genre": "TMDB Film Türü", - "tmdb_tv_keyword": "TMDB Dizi Anahtar Kelimesi", - "tmdb_tv_genre": "TMDB Dizi Türü", - "tmdb_search": "TMDB Arama", - "tmdb_studio": "TMDB Stüdyo", - "tmdb_network": "TMDB Ağ", - "tmdb_movie_streaming_services": "TMDB Film Yayın Servisleri", - "tmdb_tv_streaming_services": "TMDB Dizi Yayın Servisleri" - }, - "library": { - "no_items_found": "Öğe bulunamadı", - "no_results": "Sonuç bulunamadı", - "no_libraries_found": "Kütüphane bulunamadı", - "item_types": { - "movies": "filmler", - "series": "diziler", - "boxsets": "koleksiyonlar", - "items": "ögeler" - }, - "options": { - "display": "Görüntüleme", - "row": "Satır", - "list": "Liste", - "image_style": "Görsel stili", - "poster": "Poster", - "cover": "Kapak", - "show_titles": "Başlıkları göster", - "show_stats": "İstatistikleri göster" - }, - "filters": { - "genres": "Türler", - "years": "Yıllar", - "sort_by": "Sırala", - "sort_order": "Sıralama düzeni", - "asc": "Ascending", - "desc": "Descending", - "tags": "Etiketler" - } - }, - "favorites": { - "series": "Diziler", - "movies": "Filmler", - "episodes": "Bölümler", - "videos": "Videolar", - "boxsets": "Koleksiyonlar", - "playlists": "Çalma listeleri", - "noDataTitle": "Henüz favori yok", - "noData": "Hızlı erişim için öğeleri favori olarak işaretleyin ve burada görünmelerini sağlayın." - }, - "custom_links": { - "no_links": "Bağlantı yok" - }, - "player": { - "error": "Hata", - "failed_to_get_stream_url": "Yayın URL'si alınamadı", - "an_error_occured_while_playing_the_video": "Video oynatılırken bir hata oluştu. Ayarlardaki günlüklere bakın.", - "client_error": "İstemci hatası", - "could_not_create_stream_for_chromecast": "Chromecast için yayın oluşturulamadı", - "message_from_server": "Sunucudan mesaj: {{message}}", - "video_has_finished_playing": "Video oynatıldı!", - "no_video_source": "Video kaynağı yok...", - "next_episode": "Sonraki bölüm", - "refresh_tracks": "Parçaları yenile", - "subtitle_tracks": "Altyazı Parçaları:", - "audio_tracks": "Ses Parçaları:", - "playback_state": "Oynatma Durumu:", - "no_data_available": "Veri bulunamadı", - "index": "İndeks:" - }, - "item_card": { - "next_up": "Sıradaki", - "no_items_to_display": "Görüntülenecek öğe yok", - "cast_and_crew": "Oyuncular & Ekip", - "series": "Dizi", - "seasons": "Sezonlar", - "season": "Sezon", - "no_episodes_for_this_season": "Bu sezona ait bölüm yok", - "overview": "Özet", - "more_with": "Daha fazla {{name}}", - "similar_items": "Benzer ögeler", - "no_similar_items_found": "Benzer öge bulunamadı", - "video": "Video", - "more_details": "Daha fazla detay", - "quality": "Kalite", - "audio": "Ses", - "subtitles": "Altyazı", - "show_more": "Daha fazla göster", - "show_less": "Daha az göster", - "appeared_in": "Şurada yer aldı", - "could_not_load_item": "Öge yüklenemedi", - "none": "Hiçbiri", - "download": { - "download_season": "Sezonu indir", - "download_series": "Diziyi indir", - "download_episode": "Bölümü indir", - "download_movie": "Filmi indir", - "download_x_item": "{{item_count}} tane ögeyi indir", - "download_button": "İndir", - "using_optimized_server": "Optimize edilmiş sunucu kullanılıyor", - "using_default_method": "Varsayılan yöntem kullanılıyor" - } - }, - "live_tv": { - "next": "Sonraki", - "previous": "Önceki", - "live_tv": "Canlı TV", - "coming_soon": "Yakında", - "on_now": "Şu anda yayında", - "shows": "Programlar", - "movies": "Filmler", - "sports": "Spor", - "for_kids": "Çocuklar İçin", - "news": "Haberler" - }, - "jellyseerr": { - "confirm": "Onayla", - "cancel": "İptal", - "yes": "Evet", - "whats_wrong": "Problem nedir?", - "issue_type": "Sorun türü", - "select_an_issue": "Bir sorun seçin", - "types": "Türler", - "describe_the_issue": "(isteğe bağlı) Sorunu açıklayın...", - "submit_button": "Gönder", - "report_issue_button": "Sorunu bildir", - "request_button": "Talep et", - "are_you_sure_you_want_to_request_all_seasons": "Tüm sezonları talep etmek istediğinizden emin misiniz?", - "failed_to_login": "Giriş yapılamadı", - "cast": "Oyuncular", - "details": "Detaylar", - "status": "Durum", - "original_title": "Orijinal Başlık", - "series_type": "Dizi Türü", - "release_dates": "Yayın Tarihleri", - "first_air_date": "İlk Yayın Tarihi", - "next_air_date": "Sonraki Yayın Tarihi", - "revenue": "Gelir", - "budget": "Bütçe", - "original_language": "Orijinal Dil", - "production_country": "Yapım Ülkesi", - "studios": "Stüdyolar", - "network": "Ağ", - "currently_streaming_on": "Şu anda yayınlanıyor", - "advanced": "Gelişmiş", - "request_as": "Şu olarak iste", - "tags": "Etiketler", - "quality_profile": "Kalite Profili", - "root_folder": "Kök Klasör", - "season_all": "Season (all)", - "season_number": "Sezon {{season_number}}", - "number_episodes": "Bölüm {{episode_number}}", - "born": "Doğum", - "appearances": "Görünmeler", - "toasts": { - "jellyseer_does_not_meet_requirements": "Jellyseerr sunucusu minimum sürüm gereksinimlerini karşılamıyor! Lütfen en az 2.0.0 sürümüne güncelleyin", - "jellyseerr_test_failed": "Jellyseerr testi başarısız oldu. Lütfen tekrar deneyin.", - "failed_to_test_jellyseerr_server_url": "Jellyseerr sunucu URL'si test edilemedi", - "issue_submitted": "Sorun gönderildi!", - "requested_item": "{{item}} talep edildi!", - "you_dont_have_permission_to_request": "İstek göndermeye izniniz yok!", - "something_went_wrong_requesting_media": "Medya talep edilirken bir şeyler ters gitti!" - } - }, - "tabs": { - "home": "Ana Sayfa", - "search": "Ara", - "library": "Kütüphane", - "custom_links": "Özel Bağlantılar", - "favorites": "Favoriler" - } + "login": { + "username_required": "Kullanıcı adı gereklidir", + "error_title": "Hata", + "login_title": "Giriş yap", + "login_to_title": " 'e giriş yap", + "username_placeholder": "Kullanıcı adı", + "password_placeholder": "Şifre", + "login_button": "Giriş yap", + "quick_connect": "Quick Connect", + "enter_code_to_login": "Giriş yapmak için {{code}} kodunu girin", + "failed_to_initiate_quick_connect": "Quick Connect başlatılamadı", + "got_it": "Anlaşıldı", + "connection_failed": "Bağlantı başarısız", + "could_not_connect_to_server": "Sunucuya bağlanılamadı. Lütfen URL'yi ve ağ bağlantınızı kontrol edin", + "an_unexpected_error_occured": "Beklenmedik bir hata oluştu", + "change_server": "Sunucuyu değiştir", + "invalid_username_or_password": "Geçersiz kullanıcı adı veya şifre", + "user_does_not_have_permission_to_log_in": "Kullanıcının giriş yapma izni yok", + "server_is_taking_too_long_to_respond_try_again_later": "Sunucu yanıt vermekte çok uzun sürüyor, lütfen tekrar deneyin", + "server_received_too_many_requests_try_again_later": "Sunucu çok fazla istek aldı, lütfen tekrar deneyin.", + "there_is_a_server_error": "Sunucu hatası var", + "an_unexpected_error_occured_did_you_enter_the_correct_url": "Beklenmedik bir hata oluştu. Sunucu URL'sini doğru girdiğinizden emin oldunuz mu?" + }, + "server": { + "enter_url_to_jellyfin_server": "Jellyfin sunucunusun URL'sini girin", + "server_url_placeholder": "http(s)://sunucunuz.com", + "connect_button": "Bağlan", + "previous_servers": "Önceki sunucular", + "clear_button": "Temizle", + "search_for_local_servers": "Yerel sunucuları ara", + "searching": "Aranıyor...", + "servers": "Sunucular" + }, + "home": { + "no_internet": "İnternet Yok", + "no_items": "Öge Yok", + "no_internet_message": "Endişelenmeyin, hala\ndownloaded içerik izleyebilirsiniz.", + "go_to_downloads": "İndirmelere Git", + "oops": "Hups!", + "error_message": "Bir şeyler ters gitti.\nLütfen çıkış yapın ve tekrar giriş yapın.", + "continue_watching": "İzlemeye Devam Et", + "next_up": "Sonraki", + "recently_added_in": "{{libraryName}}'de Yakınlarda Eklendi", + "suggested_movies": "Önerilen Filmler", + "suggested_episodes": "Önerilen Bölümler", + "intro": { + "welcome_to_streamyfin": "Streamyfin'e Hoş Geldiniz", + "a_free_and_open_source_client_for_jellyfin": "Jellyfin için ücretsiz ve açık kaynak bir istemci.", + "features_title": "Özellikler", + "features_description": "Streamyfin birçok özelliğe sahip ve ayarlar menüsünde bulabileceğiniz çeşitli yazılımlarla entegre olabiliyor. Bunlar arasında şunlar bulunuyor:", + "jellyseerr_feature_description": "Jellyseerr örneğinizle bağlantı kurun ve uygulama içinde doğrudan film talep edin.", + "downloads_feature_title": "İndirmeler", + "downloads_feature_description": "Filmleri ve TV dizilerini çevrimdışı izlemek için indirin. Varsayılan yöntemi veya dosyaları arka planda indirmek için optimize sunucuyu kurabilirsiniz.", + "chromecast_feature_description": "Filmleri ve TV dizilerini Chromecast cihazlarınıza aktarın.", + "centralised_settings_plugin_title": "Merkezi Ayarlar Eklentisi", + "centralised_settings_plugin_description": "Jellyfin sunucunuzda merkezi bir yerden ayarları yapılandırın. Tüm istemci ayarları tüm kullanıcılar için otomatik olarak senkronize edilecektir.", + "done_button": "Tamam", + "go_to_settings_button": "Ayrıntılara Git", + "read_more": "Daha fazla oku" + }, + "settings": { + "settings_title": "Ayarlar", + "log_out_button": "Çıkış Yap", + "user_info": { + "user_info_title": "Kullanıcı Bilgisi", + "user": "Kullanıcı", + "server": "Sunucu", + "token": "Token", + "app_version": "Uygulama Sürümü" + }, + "quick_connect": { + "quick_connect_title": "Hızlı Bağlantı", + "authorize_button": "Hızlı Bağlantıyı Yetkilendir", + "enter_the_quick_connect_code": "Hızlı bağlantı kodunu girin...", + "success": "Başarılı", + "quick_connect_autorized": "Hızlı Bağlantı Yetkilendirildi", + "error": "Hata", + "invalid_code": "Geçersiz kod", + "authorize": "Yetkilendir" + }, + "media_controls": { + "media_controls_title": "Medya Kontrolleri", + "forward_skip_length": "İleri Sarma Uzunluğu", + "rewind_length": "Geri Sarma Uzunluğu", + "seconds_unit": "s" + }, + "audio": { + "audio_title": "Ses", + "set_audio_track": "Önceki Öğeden Ses Parçası Ayarla", + "audio_language": "Ses Dili", + "audio_hint": "Varsayılan ses dilini seçin.", + "none": "Yok", + "language": "Dil" + }, + "subtitles": { + "subtitle_title": "Altyazılar", + "subtitle_language": "Altyazı Dili", + "subtitle_mode": "Altyazı Modu", + "set_subtitle_track": "Önceki Öğeden Altyazı Parçası Ayarla", + "subtitle_size": "Altyazı Boyutu", + "subtitle_hint": "Altyazı tercihini yapılandırın.", + "none": "Yok", + "language": "Dil", + "loading": "Yükleniyor", + "modes": { + "Default": "Varsayılan", + "Smart": "Akıllı", + "Always": "Her Zaman", + "None": "Yok", + "OnlyForced": "Sadece Zorunlu" + } + }, + "other": { + "other_title": "Diğer", + "follow_device_orientation": "Otomatik Döndürme", + "video_orientation": "Video Yönü", + "orientation": "Yön", + "orientations": { + "DEFAULT": "Varsayılan", + "ALL": "Tümü", + "PORTRAIT": "Dikey", + "PORTRAIT_UP": "Dikey Yukarı", + "PORTRAIT_DOWN": "Dikey Aşağı", + "LANDSCAPE": "Yatay", + "LANDSCAPE_LEFT": "Yatay Sol", + "LANDSCAPE_RIGHT": "Yatay Sağ", + "OTHER": "Diğer", + "UNKNOWN": "Bilinmeyen" + }, + "safe_area_in_controls": "Kontrollerde Güvenli Alan", + "video_player": "Video player", + "video_players": { + "VLC_3": "VLC 3", + "VLC_4": "VLC 4 (Experimental + PiP)" + }, + "show_custom_menu_links": "Özel Menü Bağlantılarını Göster", + "hide_libraries": "Kütüphaneleri Gizle", + "select_liraries_you_want_to_hide": "Kütüphane sekmesinden ve ana sayfa bölümlerinden gizlemek istediğiniz kütüphaneleri seçin.", + "disable_haptic_feedback": "Dokunsal Geri Bildirimi Devre Dışı Bırak" + }, + "downloads": { + "downloads_title": "İndirmeler", + "download_method": "İndirme Yöntemi", + "remux_max_download": "Remux max indirme", + "auto_download": "Otomatik İndirme", + "optimized_versions_server": "Optimize edilmiş sürümler sunucusu", + "save_button": "Kaydet", + "optimized_server": "Optimize Sunucu", + "optimized": "Optimize", + "default": "Varsayılan", + "optimized_version_hint": "Optimize sunucusu için URL girin. URL, http veya https içermeli ve isteğe bağlı olarak portu içerebilir.", + "read_more_about_optimized_server": "Optimize sunucusu hakkında daha fazla oku.", + "url": "URL", + "server_url_placeholder": "http(s)://domain.org:port" + }, + "plugins": { + "plugins_title": "Eklentiler", + "jellyseerr": { + "jellyseerr_warning": "Bu entegrasyon erken aşamalardadır. Değişiklikler olabilir.", + "server_url": "Sunucu URL'si", + "server_url_hint": "Örnek: http(s)://your-host.url\n(port gerekiyorsa ekleyin)", + "server_url_placeholder": "Jellyseerr URL...", + "password": "Şifre", + "password_placeholder": "Jellyfin kullanıcısı {{username}} için şifre girin", + "save_button": "Kaydet", + "clear_button": "Temizle", + "login_button": "Giriş Yap", + "total_media_requests": "Toplam medya istekleri", + "movie_quota_limit": "Film kota limiti", + "movie_quota_days": "Film kota günleri", + "tv_quota_limit": "TV kota limiti", + "tv_quota_days": "TV kota günleri", + "reset_jellyseerr_config_button": "Jellyseerr yapılandırmasını sıfırla", + "unlimited": "Sınırsız", + "plus_n_more": "+{{n}} more", + "order_by": { + "DEFAULT": "Default", + "VOTE_COUNT_AND_AVERAGE": "Vote count and average", + "POPULARITY": "Popularity" + } + }, + "marlin_search": { + "enable_marlin_search": "Marlin Aramasını Etkinleştir ", + "url": "URL", + "server_url_placeholder": "http(s)://domain.org:port", + "marlin_search_hint": "Marlin sunucu URL'sini girin. URL, http veya https içermeli ve isteğe bağlı olarak portu içerebilir.", + "read_more_about_marlin": "Marlin hakkında daha fazla oku.", + "save_button": "Kaydet", + "toasts": { + "saved": "Kaydedildi" + } + } + }, + "storage": { + "storage_title": "Depolama", + "app_usage": "Uygulama {{usedSpace}}%", + "device_usage": "Cihaz {{availableSpace}}%", + "size_used": "{{used}} / {{total}} kullanıldı", + "delete_all_downloaded_files": "Tüm indirilen dosyaları sil" + }, + "intro": { + "show_intro": "Tanıtımı Göster", + "reset_intro": "Tanıtımı Sıfırla" + }, + "logs": { + "logs_title": "Günlükler", + "no_logs_available": "Günlükler mevcut değil", + "delete_all_logs": "Tüm günlükleri sil" + }, + "languages": { + "title": "Diller", + "app_language": "Uygulama dili", + "app_language_description": "Uygulama dilini seçin.", + "system": "Sistem" + }, + "toasts": { + "error_deleting_files": "Dosyalar silinirken hata oluştu", + "background_downloads_enabled": "Arka plan indirmeleri etkinleştirildi", + "background_downloads_disabled": "Arka plan indirmeleri devre dışı bırakıldı", + "connected": "Bağlandı", + "could_not_connect": "Bağlanılamadı", + "invalid_url": "Geçersiz URL" + } + }, + "downloads": { + "downloads_title": "İndirilenler", + "tvseries": "Diziler", + "movies": "Filmler", + "queue": "Sıra", + "queue_hint": "Sıra ve indirmeler uygulama yeniden başlatıldığında kaybolacaktır", + "no_items_in_queue": "Sırada öğe yok", + "no_downloaded_items": "İndirilen öğe yok", + "delete_all_movies_button": "Tüm Filmleri Sil", + "delete_all_tvseries_button": "Tüm Dizileri Sil", + "delete_all_button": "Tümünü Sil", + "active_download": "Aktif indirme", + "no_active_downloads": "Aktif indirme yok", + "active_downloads": "Aktif indirmeler", + "new_app_version_requires_re_download": "Yeni uygulama sürümü yeniden indirme gerektiriyor", + "new_app_version_requires_re_download_description": "Yeni güncelleme, içeriğin yeniden indirilmesini gerektiriyor. Lütfen tüm indirilen içerikleri kaldırıp tekrar deneyin.", + "back": "Geri", + "delete": "Sil", + "something_went_wrong": "Bir şeyler ters gitti", + "could_not_get_stream_url_from_jellyfin": "Jellyfin'den yayın URL'si alınamadı", + "eta": "Tahmini Süre {{eta}}", + "methods": "Yöntemler", + "toasts": { + "you_are_not_allowed_to_download_files": "Dosyaları indirme izniniz yok.", + "deleted_all_movies_successfully": "Tüm filmler başarıyla silindi!", + "failed_to_delete_all_movies": "Filmler silinemedi", + "deleted_all_tvseries_successfully": "Tüm diziler başarıyla silindi!", + "failed_to_delete_all_tvseries": "Diziler silinemedi", + "download_cancelled": "İndirme iptal edildi", + "could_not_cancel_download": "İndirme iptal edilemedi", + "download_completed": "İndirme tamamlandı", + "download_started_for": "{{item}} için indirme başlatıldı", + "item_is_ready_to_be_downloaded": "{{item}} indirmeye hazır", + "download_stated_for_item": "{{item}} için indirme başlatıldı", + "download_failed_for_item": "{{item}} için indirme başarısız oldu - {{error}}", + "download_completed_for_item": "{{item}} için indirme tamamlandı", + "queued_item_for_optimization": "{{item}} optimizasyon için sıraya alındı", + "failed_to_start_download_for_item": "{{item}} için indirme başlatılamadı: {{message}}", + "server_responded_with_status_code": "Sunucu şu durum koduyla yanıt verdi: {{statusCode}}", + "no_response_received_from_server": "Sunucudan yanıt alınamadı", + "error_setting_up_the_request": "İstek ayarlanırken hata oluştu", + "failed_to_start_download_for_item_unexpected_error": "{{item}} için indirme başlatılamadı: Beklenmeyen hata", + "all_files_folders_and_jobs_deleted_successfully": "Tüm dosyalar, klasörler ve işler başarıyla silindi", + "an_error_occured_while_deleting_files_and_jobs": "Dosyalar ve işler silinirken hata oluştu", + "go_to_downloads": "İndirmelere git" + } + } + }, + "search": { + "search_here": "Burada ara...", + "search": "Ara...", + "x_items": "{{count}} öge(ler)", + "library": "Kütüphane", + "discover": "Keşfet", + "no_results": "Sonuç bulunamadı", + "no_results_found_for": "\"{{query}}\" için sonuç bulunamadı", + "movies": "Filmler", + "series": "Diziler", + "episodes": "Bölümler", + "collections": "Koleksiyonlar", + "actors": "Oyuncular", + "request_movies": "Film Talep Et", + "request_series": "Dizi Talep Et", + "recently_added": "Son Eklenenler", + "recent_requests": "Son Talepler", + "plex_watchlist": "Plex İzleme Listesi", + "trending": "Şu An Popüler", + "popular_movies": "Popüler Filmler", + "movie_genres": "Film Türleri", + "upcoming_movies": "Yaklaşan Filmler", + "studios": "Stüdyolar", + "popular_tv": "Popüler Diziler", + "tv_genres": "Dizi Türleri", + "upcoming_tv": "Yaklaşan Diziler", + "networks": "Ağlar", + "tmdb_movie_keyword": "TMDB Film Anahtar Kelimesi", + "tmdb_movie_genre": "TMDB Film Türü", + "tmdb_tv_keyword": "TMDB Dizi Anahtar Kelimesi", + "tmdb_tv_genre": "TMDB Dizi Türü", + "tmdb_search": "TMDB Arama", + "tmdb_studio": "TMDB Stüdyo", + "tmdb_network": "TMDB Ağ", + "tmdb_movie_streaming_services": "TMDB Film Yayın Servisleri", + "tmdb_tv_streaming_services": "TMDB Dizi Yayın Servisleri" + }, + "library": { + "no_items_found": "Öğe bulunamadı", + "no_results": "Sonuç bulunamadı", + "no_libraries_found": "Kütüphane bulunamadı", + "item_types": { + "movies": "filmler", + "series": "diziler", + "boxsets": "koleksiyonlar", + "items": "ögeler" + }, + "options": { + "display": "Görüntüleme", + "row": "Satır", + "list": "Liste", + "image_style": "Görsel stili", + "poster": "Poster", + "cover": "Kapak", + "show_titles": "Başlıkları göster", + "show_stats": "İstatistikleri göster" + }, + "filters": { + "genres": "Türler", + "years": "Yıllar", + "sort_by": "Sırala", + "sort_order": "Sıralama düzeni", + "asc": "Ascending", + "desc": "Descending", + "tags": "Etiketler" + } + }, + "favorites": { + "series": "Diziler", + "movies": "Filmler", + "episodes": "Bölümler", + "videos": "Videolar", + "boxsets": "Koleksiyonlar", + "playlists": "Çalma listeleri", + "noDataTitle": "Henüz favori yok", + "noData": "Hızlı erişim için öğeleri favori olarak işaretleyin ve burada görünmelerini sağlayın." + }, + "custom_links": { + "no_links": "Bağlantı yok" + }, + "player": { + "error": "Hata", + "failed_to_get_stream_url": "Yayın URL'si alınamadı", + "an_error_occured_while_playing_the_video": "Video oynatılırken bir hata oluştu. Ayarlardaki günlüklere bakın.", + "client_error": "İstemci hatası", + "could_not_create_stream_for_chromecast": "Chromecast için yayın oluşturulamadı", + "message_from_server": "Sunucudan mesaj: {{message}}", + "video_has_finished_playing": "Video oynatıldı!", + "no_video_source": "Video kaynağı yok...", + "next_episode": "Sonraki bölüm", + "refresh_tracks": "Parçaları yenile", + "subtitle_tracks": "Altyazı Parçaları:", + "audio_tracks": "Ses Parçaları:", + "playback_state": "Oynatma Durumu:", + "no_data_available": "Veri bulunamadı", + "index": "İndeks:" + }, + "item_card": { + "next_up": "Sıradaki", + "no_items_to_display": "Görüntülenecek öğe yok", + "cast_and_crew": "Oyuncular & Ekip", + "series": "Dizi", + "seasons": "Sezonlar", + "season": "Sezon", + "no_episodes_for_this_season": "Bu sezona ait bölüm yok", + "overview": "Özet", + "more_with": "Daha fazla {{name}}", + "similar_items": "Benzer ögeler", + "no_similar_items_found": "Benzer öge bulunamadı", + "video": "Video", + "more_details": "Daha fazla detay", + "quality": "Kalite", + "audio": "Ses", + "subtitles": "Altyazı", + "show_more": "Daha fazla göster", + "show_less": "Daha az göster", + "appeared_in": "Şurada yer aldı", + "could_not_load_item": "Öge yüklenemedi", + "none": "Hiçbiri", + "download": { + "download_season": "Sezonu indir", + "download_series": "Diziyi indir", + "download_episode": "Bölümü indir", + "download_movie": "Filmi indir", + "download_x_item": "{{item_count}} tane ögeyi indir", + "download_button": "İndir", + "using_optimized_server": "Optimize edilmiş sunucu kullanılıyor", + "using_default_method": "Varsayılan yöntem kullanılıyor" + } + }, + "live_tv": { + "next": "Sonraki", + "previous": "Önceki", + "live_tv": "Canlı TV", + "coming_soon": "Yakında", + "on_now": "Şu anda yayında", + "shows": "Programlar", + "movies": "Filmler", + "sports": "Spor", + "for_kids": "Çocuklar İçin", + "news": "Haberler" + }, + "jellyseerr": { + "confirm": "Onayla", + "cancel": "İptal", + "yes": "Evet", + "whats_wrong": "Problem nedir?", + "issue_type": "Sorun türü", + "select_an_issue": "Bir sorun seçin", + "types": "Türler", + "describe_the_issue": "(isteğe bağlı) Sorunu açıklayın...", + "submit_button": "Gönder", + "report_issue_button": "Sorunu bildir", + "request_button": "Talep et", + "are_you_sure_you_want_to_request_all_seasons": "Tüm sezonları talep etmek istediğinizden emin misiniz?", + "failed_to_login": "Giriş yapılamadı", + "cast": "Oyuncular", + "details": "Detaylar", + "status": "Durum", + "original_title": "Orijinal Başlık", + "series_type": "Dizi Türü", + "release_dates": "Yayın Tarihleri", + "first_air_date": "İlk Yayın Tarihi", + "next_air_date": "Sonraki Yayın Tarihi", + "revenue": "Gelir", + "budget": "Bütçe", + "original_language": "Orijinal Dil", + "production_country": "Yapım Ülkesi", + "studios": "Stüdyolar", + "network": "Ağ", + "currently_streaming_on": "Şu anda yayınlanıyor", + "advanced": "Gelişmiş", + "request_as": "Şu olarak iste", + "tags": "Etiketler", + "quality_profile": "Kalite Profili", + "root_folder": "Kök Klasör", + "season_all": "Season (all)", + "season_number": "Sezon {{season_number}}", + "number_episodes": "Bölüm {{episode_number}}", + "born": "Doğum", + "appearances": "Görünmeler", + "toasts": { + "jellyseer_does_not_meet_requirements": "Jellyseerr sunucusu minimum sürüm gereksinimlerini karşılamıyor! Lütfen en az 2.0.0 sürümüne güncelleyin", + "jellyseerr_test_failed": "Jellyseerr testi başarısız oldu. Lütfen tekrar deneyin.", + "failed_to_test_jellyseerr_server_url": "Jellyseerr sunucu URL'si test edilemedi", + "issue_submitted": "Sorun gönderildi!", + "requested_item": "{{item}} talep edildi!", + "you_dont_have_permission_to_request": "İstek göndermeye izniniz yok!", + "something_went_wrong_requesting_media": "Medya talep edilirken bir şeyler ters gitti!" + } + }, + "tabs": { + "home": "Ana Sayfa", + "search": "Ara", + "library": "Kütüphane", + "custom_links": "Özel Bağlantılar", + "favorites": "Favoriler" + } } diff --git a/translations/zh-CN.json b/translations/zh-CN.json index 7d8e09db..e9e0391a 100644 --- a/translations/zh-CN.json +++ b/translations/zh-CN.json @@ -1,472 +1,472 @@ { - "login": { - "username_required": "需要用户名", - "error_title": "错误", - "login_title": "登录", - "login_to_title": "登录至", - "username_placeholder": "用户名", - "password_placeholder": "密码", - "login_button": "登录", - "quick_connect": "快速连接", - "enter_code_to_login": "输入代码 {{code}} 以登录", - "failed_to_initiate_quick_connect": "无法启动快速连接", - "got_it": "了解", - "connection_failed": "连接失败", - "could_not_connect_to_server": "无法连接到服务器。请检查 URL 和您的网络连接。", - "an_unexpected_error_occured": "发生意外错误", - "change_server": "更改服务器", - "invalid_username_or_password": "无效的用户名或密码", - "user_does_not_have_permission_to_log_in": "用户没有登录权限", - "server_is_taking_too_long_to_respond_try_again_later": "服务器长时间未响应,请稍后再试", - "server_received_too_many_requests_try_again_later": "服务器收到过多请求,请稍后再试。", - "there_is_a_server_error": "服务器出错", - "an_unexpected_error_occured_did_you_enter_the_correct_url": "发生意外错误。您是否正确输入了服务器 URL?" - }, - "server": { - "enter_url_to_jellyfin_server": "输入您的 Jellyfin 服务器 URL", - "server_url_placeholder": "http(s)://your-server.com", - "connect_button": "连接", - "previous_servers": "上一个服务器", - "clear_button": "清除", - "search_for_local_servers": "搜索本地服务器", - "searching": "搜索中...", - "servers": "服务器" - }, - "home": { - "no_internet": "无网络", - "no_items": "无项目", - "no_internet_message": "别担心,您仍可以观看\n已下载的项目。", - "go_to_downloads": "前往下载", - "oops": "哎呀!", - "error_message": "出错了。\n请注销重新登录。", - "continue_watching": "继续观看", - "next_up": "下一个", - "recently_added_in": "最近添加于 {{libraryName}}", - "suggested_movies": "推荐电影", - "suggested_episodes": "推荐剧集", - "intro": { - "welcome_to_streamyfin": "欢迎来到 Streamyfin", - "a_free_and_open_source_client_for_jellyfin": "一个免费且开源的 Jellyfin 客户端。", - "features_title": "功能", - "features_description": "Streamyfin 拥有许多功能,并与多种服务整合,您可以在设置菜单中找到这些功能,包括:", - "jellyseerr_feature_description": "连接到您的 Jellyseerr 实例并直接在应用中请求电影。", - "downloads_feature_title": "下载", - "downloads_feature_description": "下载电影和节目以离线观看。使用默认方法或安装 Optimized Server 以在后台下载文件。", - "chromecast_feature_description": "将电影和节目投屏到您的 Chromecast 设备。", - "centralised_settings_plugin_title": "统一设置插件", - "centralised_settings_plugin_description": "从 Jellyfin 服务器上的统一位置改变设置。所有用户的所有客户端设置将会自动同步。", - "done_button": "完成", - "go_to_settings_button": "前往设置", - "read_more": "了解更多" - }, - "settings": { - "settings_title": "设置", - "log_out_button": "登出", - "user_info": { - "user_info_title": "用户信息", - "user": "用户", - "server": "服务器", - "token": "密钥", - "app_version": "应用版本" - }, - "quick_connect": { - "quick_connect_title": "快速连接", - "authorize_button": "授权快速连接", - "enter_the_quick_connect_code": "输入快速连接代码...", - "success": "成功", - "quick_connect_autorized": "快速连接已授权", - "error": "错误", - "invalid_code": "无效代码", - "authorize": "授权" - }, - "media_controls": { - "media_controls_title": "媒体控制", - "forward_skip_length": "快进时长", - "rewind_length": "快退时长", - "seconds_unit": "秒" - }, - "audio": { - "audio_title": "音频", - "set_audio_track": "从上一个项目设置音轨", - "audio_language": "音频语言", - "audio_hint": "选择默认音频语言。", - "none": "无", - "language": "语言" - }, - "subtitles": { - "subtitle_title": "字幕", - "subtitle_language": "字幕语言", - "subtitle_mode": "字幕模式", - "set_subtitle_track": "从上一个项目设置字幕", - "subtitle_size": "字幕大小", - "subtitle_hint": "设置字幕偏好。", - "none": "无", - "language": "语言", - "loading": "加载中", - "modes": { - "Default": "默认", - "Smart": "智能", - "Always": "总是", - "None": "无", - "OnlyForced": "仅强制字幕" - } - }, - "other": { - "other_title": "其他", - "follow_device_orientation": "自动旋转", - "video_orientation": "视频方向", - "orientation": "方向", - "orientations": { - "DEFAULT": "默认", - "ALL": "全部", - "PORTRAIT": "纵向", - "PORTRAIT_UP": "纵向向上", - "PORTRAIT_DOWN": "纵向向下", - "LANDSCAPE": "横向", - "LANDSCAPE_LEFT": "横向左", - "LANDSCAPE_RIGHT": "横向右", - "OTHER": "其他", - "UNKNOWN": "未知" - }, - "safe_area_in_controls": "控制中的安全区域", - "video_player": "Video player", - "video_players": { - "VLC_3": "VLC 3", - "VLC_4": "VLC 4 (Experimental + PiP)" - }, - "show_custom_menu_links": "显示自定义菜单链接", - "hide_libraries": "隐藏媒体库", - "select_liraries_you_want_to_hide": "选择您想从媒体库页面和主页隐藏的媒体库。", - "disable_haptic_feedback": "禁用触觉反馈" - }, - "downloads": { - "downloads_title": "下载", - "download_method": "下载方法", - "remux_max_download": "Remux 最大下载", - "auto_download": "自动下载", - "optimized_versions_server": "Optimized Version 服务器", - "save_button": "保存", - "optimized_server": "Optimized Server", - "optimized": "已优化", - "default": "默认", - "optimized_version_hint": "输入 Optimized Server 的 URL。URL 应包括 http(s) 和端口 (可选)。", - "read_more_about_optimized_server": "查看更多关于 Optimized Server 的信息。", - "url": "URL", - "server_url_placeholder": "http(s)://domain.org:port" - }, - "plugins": { - "plugins_title": "插件", - "jellyseerr": { - "jellyseerr_warning": "此插件处于早期阶段,功能可能会有变化。", - "server_url": "服务器 URL", - "server_url_hint": "示例:http(s)://your-host.url\n(如果需要,添加端口)", - "server_url_placeholder": "Jellyseerr URL...", - "password": "密码", - "password_placeholder": "输入 Jellyfin 用户 {{username}} 的密码", - "save_button": "保存", - "clear_button": "清除", - "login_button": "登录", - "total_media_requests": "总媒体请求", - "movie_quota_limit": "电影配额限制", - "movie_quota_days": "电影配额天数", - "tv_quota_limit": "剧集配额限制", - "tv_quota_days": "剧集配额天数", - "reset_jellyseerr_config_button": "重置 Jellyseerr 设置", - "unlimited": "无限制", - "plus_n_more": "+{{n}} more", - "order_by": { - "DEFAULT": "Default", - "VOTE_COUNT_AND_AVERAGE": "Vote count and average", - "POPULARITY": "Popularity" - } - }, - "marlin_search": { - "enable_marlin_search": "启用 Marlin 搜索", - "url": "URL", - "server_url_placeholder": "http(s)://domain.org:port", - "marlin_search_hint": "输入 Marlin 服务器的 URL。URL 应包括 http(s) 和端口 (可选)。", - "read_more_about_marlin": "查看更多关于 Marlin 的信息。", - "save_button": "保存", - "toasts": { - "saved": "已保存" - } - } - }, - "storage": { - "storage_title": "存储", - "app_usage": "应用 {{usedSpace}}%", - "device_usage": "设备 {{availableSpace}}%", - "size_used": "已使用 {{used}} / {{total}}", - "delete_all_downloaded_files": "删除所有已下载文件" - }, - "intro": { - "show_intro": "显示介绍", - "reset_intro": "重置介绍" - }, - "logs": { - "logs_title": "日志", - "no_logs_available": "无可用日志", - "delete_all_logs": "删除所有日志" - }, - "languages": { - "title": "语言", - "app_language": "应用语言", - "app_language_description": "选择应用的语言。", - "system": "系统" - }, - "toasts": { - "error_deleting_files": "删除文件时出错", - "background_downloads_enabled": "后台下载已启用", - "background_downloads_disabled": "后台下载已禁用", - "connected": "已连接", - "could_not_connect": "无法连接", - "invalid_url": "无效 URL" - } - }, - "downloads": { - "downloads_title": "下载", - "tvseries": "剧集", - "movies": "电影", - "queue": "队列", - "queue_hint": "应用重启后队列和下载将会丢失", - "no_items_in_queue": "队列中无项目", - "no_downloaded_items": "无已下载项目", - "delete_all_movies_button": "删除所有电影", - "delete_all_tvseries_button": "删除所有剧集", - "delete_all_button": "删除全部", - "active_download": "活跃下载", - "no_active_downloads": "无活跃下载", - "active_downloads": "活跃下载", - "new_app_version_requires_re_download": "更新版本需要重新下载", - "new_app_version_requires_re_download_description": "更新版本需要重新下载内容。请删除所有已下载项后重试。", - "back": "返回", - "delete": "删除", - "something_went_wrong": "出现问题", - "could_not_get_stream_url_from_jellyfin": "无法从 Jellyfin 获取串流 URL", - "eta": "预计完成时间 {{eta}}", - "methods": "方法", - "toasts": { - "you_are_not_allowed_to_download_files": "您无权下载文件。", - "deleted_all_movies_successfully": "成功删除所有电影!", - "failed_to_delete_all_movies": "删除所有电影失败", - "deleted_all_tvseries_successfully": "成功删除所有剧集!", - "failed_to_delete_all_tvseries": "删除所有剧集失败", - "download_cancelled": "下载已取消", - "could_not_cancel_download": "无法取消下载", - "download_completed": "下载完成", - "download_started_for": "开始下载 {{item}}", - "item_is_ready_to_be_downloaded": "{{item}} 准备好下载", - "download_stated_for_item": "开始下载 {{item}}", - "download_failed_for_item": "下载失败 {{item}} - {{error}}", - "download_completed_for_item": "下载完成 {{item}}", - "queued_item_for_optimization": "已将 {{item}} 队列进行优化", - "failed_to_start_download_for_item": "无法开始下载 {{item}}: {{message}}", - "server_responded_with_status_code": "服务器响应状态 {{statusCode}}", - "no_response_received_from_server": "未收到服务器响应", - "error_setting_up_the_request": "设置请求时出错", - "failed_to_start_download_for_item_unexpected_error": "无法开始下载 {{item}}: 发生意外错误", - "all_files_folders_and_jobs_deleted_successfully": "所有文件、文件夹和任务成功删除", - "an_error_occured_while_deleting_files_and_jobs": "删除文件和任务时发生错误", - "go_to_downloads": "前往下载" - } - } - }, - "search": { - "search_here": "在此搜索...", - "search": "搜索...", - "x_items": "{{count}} 项目", - "library": "媒体库", - "discover": "发现", - "no_results": "没有结果", - "no_results_found_for": "未找到结果", - "movies": "电影", - "series": "剧集", - "episodes": "单集", - "collections": "收藏", - "actors": "演员", - "request_movies": "请求电影", - "request_series": "请求系列", - "recently_added": "最近添加", - "recent_requests": "最近请求", - "plex_watchlist": "Plex 观影清单", - "trending": "趋势", - "popular_movies": "热门电影", - "movie_genres": "电影类型", - "upcoming_movies": "即将上映的电影", - "studios": "工作室", - "popular_tv": "热门电影", - "tv_genres": "剧集类型", - "upcoming_tv": "即将上映的剧集", - "networks": "网络", - "tmdb_movie_keyword": "TMDB 电影关键词", - "tmdb_movie_genre": "TMDB 电影类型", - "tmdb_tv_keyword": "TMDB 剧集关键词", - "tmdb_tv_genre": "TMDB 剧集类型", - "tmdb_search": "TMDB 搜索", - "tmdb_studio": "TMDB 工作室", - "tmdb_network": "TMDB 网络", - "tmdb_movie_streaming_services": "TMDB 电影流媒体服务", - "tmdb_tv_streaming_services": "TMDB 剧集流媒体服务" - }, - "library": { - "no_items_found": "未找到项目", - "no_results": "没有结果", - "no_libraries_found": "未找到媒体库", - "item_types": { - "movies": "电影", - "series": "剧集", - "boxsets": "套装", - "items": "项" - }, - "options": { - "display": "显示", - "row": "行", - "list": "列表", - "image_style": "图片样式", - "poster": "海报", - "cover": "封面", - "show_titles": "显示标题", - "show_stats": "显示统计" - }, - "filters": { - "genres": "类型", - "years": "年份", - "sort_by": "排序依据", - "sort_order": "排序顺序", - "asc": "Ascending", - "desc": "Descending", - "tags": "标签" - } - }, - "favorites": { - "series": "剧集", - "movies": "电影", - "episodes": "单集", - "videos": "视频", - "boxsets": "套装", - "playlists": "播放列表", - "noDataTitle": "暂无收藏", - "noData": "将项目标记为收藏,它们将显示在此处以便快速访问。" - }, - "custom_links": { - "no_links": "无链接" - }, - "player": { - "error": "错误", - "failed_to_get_stream_url": "无法获取流 URL", - "an_error_occured_while_playing_the_video": "播放视频时发生错误。请检查设置中的日志。", - "client_error": "客户端错误", - "could_not_create_stream_for_chromecast": "无法为 Chromecast 建立串流", - "message_from_server": "来自服务器的消息", - "video_has_finished_playing": "视频播放完成!", - "no_video_source": "无视频来源...", - "next_episode": "下一集", - "refresh_tracks": "刷新轨道", - "subtitle_tracks": "字幕轨道:", - "audio_tracks": "音频轨道:", - "playback_state": "播放状态:", - "no_data_available": "无可用数据", - "index": "索引:" - }, - "item_card": { - "next_up": "下一个", - "no_items_to_display": "无项目显示", - "cast_and_crew": "演员和工作人员", - "series": "剧集", - "seasons": "季", - "season": "季", - "no_episodes_for_this_season": "本季无剧集", - "overview": "概览", - "more_with": "更多 {{name}} 的作品", - "similar_items": "类似项目", - "no_similar_items_found": "未找到类似项目", - "video": "视频", - "more_details": "更多详情", - "quality": "质量", - "audio": "音频", - "subtitles": "字幕", - "show_more": "显示更多", - "show_less": "显示更少", - "appeared_in": "出现于", - "could_not_load_item": "无法加载项目", - "none": "无", - "download": { - "download_season": "下载季", - "download_series": "下载剧集", - "download_episode": "下载单集", - "download_movie": "下载电影", - "download_x_item": "下载 {{item_count}} 项目", - "download_button": "下载", - "using_optimized_server": "使用 Optimized Server", - "using_default_method": "使用默认方法" - } - }, - "live_tv": { - "next": "下一个", - "previous": "上一个", - "live_tv": "直播电视", - "coming_soon": "即将播出", - "on_now": "正在播放", - "shows": "节目", - "movies": "电影", - "sports": "体育", - "for_kids": "儿童", - "news": "新闻" - }, - "jellyseerr": { - "confirm": "确认", - "cancel": "取消", - "yes": "是", - "whats_wrong": "出了什么问题?", - "issue_type": "问题类型", - "select_an_issue": "选择一个问题", - "types": "类型", - "describe_the_issue": "(可选)描述问题...", - "submit_button": "提交", - "report_issue_button": "报告问题", - "request_button": "请求", - "are_you_sure_you_want_to_request_all_seasons": "您确定要请求所有季度的剧集吗?", - "failed_to_login": "登录失败", - "cast": "演员", - "details": "详情", - "status": "状态", - "original_title": "原标题", - "series_type": "剧集类型", - "release_dates": "发行日期", - "first_air_date": "首次播出日期", - "next_air_date": "下次播出日期", - "revenue": "收入", - "budget": "预算", - "original_language": "原始语言", - "production_country": "制作国家/地区", - "studios": "工作室", - "network": "网络", - "currently_streaming_on": "目前在以下流媒体上播放", - "advanced": "高级设置", - "request_as": "选择用户以请求", - "tags": "标签", - "quality_profile": "质量配置文件", - "root_folder": "根文件夹", - "season_all": "Season (all)", - "season_number": "第 {{season_number}} 季", - "number_episodes": "{{episode_number}} 集", - "born": "出生", - "appearances": "出场", - "toasts": { - "jellyseer_does_not_meet_requirements": "Jellyseerr 服务器不符合最低版本要求!请使用 2.0.0 及以上版本", - "jellyseerr_test_failed": "Jellyseerr 测试失败。请重试。", - "failed_to_test_jellyseerr_server_url": "无法测试 Jellyseerr 服务器 URL", - "issue_submitted": "问题已提交!", - "requested_item": "已请求 {{item}}!", - "you_dont_have_permission_to_request": "您无权请求媒体!", - "something_went_wrong_requesting_media": "请求媒体时出了些问题!" - } - }, - "tabs": { - "home": "主页", - "search": "搜索", - "library": "媒体库", - "custom_links": "自定义链接", - "favorites": "收藏" - } + "login": { + "username_required": "需要用户名", + "error_title": "错误", + "login_title": "登录", + "login_to_title": "登录至", + "username_placeholder": "用户名", + "password_placeholder": "密码", + "login_button": "登录", + "quick_connect": "快速连接", + "enter_code_to_login": "输入代码 {{code}} 以登录", + "failed_to_initiate_quick_connect": "无法启动快速连接", + "got_it": "了解", + "connection_failed": "连接失败", + "could_not_connect_to_server": "无法连接到服务器。请检查 URL 和您的网络连接。", + "an_unexpected_error_occured": "发生意外错误", + "change_server": "更改服务器", + "invalid_username_or_password": "无效的用户名或密码", + "user_does_not_have_permission_to_log_in": "用户没有登录权限", + "server_is_taking_too_long_to_respond_try_again_later": "服务器长时间未响应,请稍后再试", + "server_received_too_many_requests_try_again_later": "服务器收到过多请求,请稍后再试。", + "there_is_a_server_error": "服务器出错", + "an_unexpected_error_occured_did_you_enter_the_correct_url": "发生意外错误。您是否正确输入了服务器 URL?" + }, + "server": { + "enter_url_to_jellyfin_server": "输入您的 Jellyfin 服务器 URL", + "server_url_placeholder": "http(s)://your-server.com", + "connect_button": "连接", + "previous_servers": "上一个服务器", + "clear_button": "清除", + "search_for_local_servers": "搜索本地服务器", + "searching": "搜索中...", + "servers": "服务器" + }, + "home": { + "no_internet": "无网络", + "no_items": "无项目", + "no_internet_message": "别担心,您仍可以观看\n已下载的项目。", + "go_to_downloads": "前往下载", + "oops": "哎呀!", + "error_message": "出错了。\n请注销重新登录。", + "continue_watching": "继续观看", + "next_up": "下一个", + "recently_added_in": "最近添加于 {{libraryName}}", + "suggested_movies": "推荐电影", + "suggested_episodes": "推荐剧集", + "intro": { + "welcome_to_streamyfin": "欢迎来到 Streamyfin", + "a_free_and_open_source_client_for_jellyfin": "一个免费且开源的 Jellyfin 客户端。", + "features_title": "功能", + "features_description": "Streamyfin 拥有许多功能,并与多种服务整合,您可以在设置菜单中找到这些功能,包括:", + "jellyseerr_feature_description": "连接到您的 Jellyseerr 实例并直接在应用中请求电影。", + "downloads_feature_title": "下载", + "downloads_feature_description": "下载电影和节目以离线观看。使用默认方法或安装 Optimized Server 以在后台下载文件。", + "chromecast_feature_description": "将电影和节目投屏到您的 Chromecast 设备。", + "centralised_settings_plugin_title": "统一设置插件", + "centralised_settings_plugin_description": "从 Jellyfin 服务器上的统一位置改变设置。所有用户的所有客户端设置将会自动同步。", + "done_button": "完成", + "go_to_settings_button": "前往设置", + "read_more": "了解更多" + }, + "settings": { + "settings_title": "设置", + "log_out_button": "登出", + "user_info": { + "user_info_title": "用户信息", + "user": "用户", + "server": "服务器", + "token": "密钥", + "app_version": "应用版本" + }, + "quick_connect": { + "quick_connect_title": "快速连接", + "authorize_button": "授权快速连接", + "enter_the_quick_connect_code": "输入快速连接代码...", + "success": "成功", + "quick_connect_autorized": "快速连接已授权", + "error": "错误", + "invalid_code": "无效代码", + "authorize": "授权" + }, + "media_controls": { + "media_controls_title": "媒体控制", + "forward_skip_length": "快进时长", + "rewind_length": "快退时长", + "seconds_unit": "秒" + }, + "audio": { + "audio_title": "音频", + "set_audio_track": "从上一个项目设置音轨", + "audio_language": "音频语言", + "audio_hint": "选择默认音频语言。", + "none": "无", + "language": "语言" + }, + "subtitles": { + "subtitle_title": "字幕", + "subtitle_language": "字幕语言", + "subtitle_mode": "字幕模式", + "set_subtitle_track": "从上一个项目设置字幕", + "subtitle_size": "字幕大小", + "subtitle_hint": "设置字幕偏好。", + "none": "无", + "language": "语言", + "loading": "加载中", + "modes": { + "Default": "默认", + "Smart": "智能", + "Always": "总是", + "None": "无", + "OnlyForced": "仅强制字幕" + } + }, + "other": { + "other_title": "其他", + "follow_device_orientation": "自动旋转", + "video_orientation": "视频方向", + "orientation": "方向", + "orientations": { + "DEFAULT": "默认", + "ALL": "全部", + "PORTRAIT": "纵向", + "PORTRAIT_UP": "纵向向上", + "PORTRAIT_DOWN": "纵向向下", + "LANDSCAPE": "横向", + "LANDSCAPE_LEFT": "横向左", + "LANDSCAPE_RIGHT": "横向右", + "OTHER": "其他", + "UNKNOWN": "未知" + }, + "safe_area_in_controls": "控制中的安全区域", + "video_player": "Video player", + "video_players": { + "VLC_3": "VLC 3", + "VLC_4": "VLC 4 (Experimental + PiP)" + }, + "show_custom_menu_links": "显示自定义菜单链接", + "hide_libraries": "隐藏媒体库", + "select_liraries_you_want_to_hide": "选择您想从媒体库页面和主页隐藏的媒体库。", + "disable_haptic_feedback": "禁用触觉反馈" + }, + "downloads": { + "downloads_title": "下载", + "download_method": "下载方法", + "remux_max_download": "Remux 最大下载", + "auto_download": "自动下载", + "optimized_versions_server": "Optimized Version 服务器", + "save_button": "保存", + "optimized_server": "Optimized Server", + "optimized": "已优化", + "default": "默认", + "optimized_version_hint": "输入 Optimized Server 的 URL。URL 应包括 http(s) 和端口 (可选)。", + "read_more_about_optimized_server": "查看更多关于 Optimized Server 的信息。", + "url": "URL", + "server_url_placeholder": "http(s)://domain.org:port" + }, + "plugins": { + "plugins_title": "插件", + "jellyseerr": { + "jellyseerr_warning": "此插件处于早期阶段,功能可能会有变化。", + "server_url": "服务器 URL", + "server_url_hint": "示例:http(s)://your-host.url\n(如果需要,添加端口)", + "server_url_placeholder": "Jellyseerr URL...", + "password": "密码", + "password_placeholder": "输入 Jellyfin 用户 {{username}} 的密码", + "save_button": "保存", + "clear_button": "清除", + "login_button": "登录", + "total_media_requests": "总媒体请求", + "movie_quota_limit": "电影配额限制", + "movie_quota_days": "电影配额天数", + "tv_quota_limit": "剧集配额限制", + "tv_quota_days": "剧集配额天数", + "reset_jellyseerr_config_button": "重置 Jellyseerr 设置", + "unlimited": "无限制", + "plus_n_more": "+{{n}} more", + "order_by": { + "DEFAULT": "Default", + "VOTE_COUNT_AND_AVERAGE": "Vote count and average", + "POPULARITY": "Popularity" + } + }, + "marlin_search": { + "enable_marlin_search": "启用 Marlin 搜索", + "url": "URL", + "server_url_placeholder": "http(s)://domain.org:port", + "marlin_search_hint": "输入 Marlin 服务器的 URL。URL 应包括 http(s) 和端口 (可选)。", + "read_more_about_marlin": "查看更多关于 Marlin 的信息。", + "save_button": "保存", + "toasts": { + "saved": "已保存" + } + } + }, + "storage": { + "storage_title": "存储", + "app_usage": "应用 {{usedSpace}}%", + "device_usage": "设备 {{availableSpace}}%", + "size_used": "已使用 {{used}} / {{total}}", + "delete_all_downloaded_files": "删除所有已下载文件" + }, + "intro": { + "show_intro": "显示介绍", + "reset_intro": "重置介绍" + }, + "logs": { + "logs_title": "日志", + "no_logs_available": "无可用日志", + "delete_all_logs": "删除所有日志" + }, + "languages": { + "title": "语言", + "app_language": "应用语言", + "app_language_description": "选择应用的语言。", + "system": "系统" + }, + "toasts": { + "error_deleting_files": "删除文件时出错", + "background_downloads_enabled": "后台下载已启用", + "background_downloads_disabled": "后台下载已禁用", + "connected": "已连接", + "could_not_connect": "无法连接", + "invalid_url": "无效 URL" + } + }, + "downloads": { + "downloads_title": "下载", + "tvseries": "剧集", + "movies": "电影", + "queue": "队列", + "queue_hint": "应用重启后队列和下载将会丢失", + "no_items_in_queue": "队列中无项目", + "no_downloaded_items": "无已下载项目", + "delete_all_movies_button": "删除所有电影", + "delete_all_tvseries_button": "删除所有剧集", + "delete_all_button": "删除全部", + "active_download": "活跃下载", + "no_active_downloads": "无活跃下载", + "active_downloads": "活跃下载", + "new_app_version_requires_re_download": "更新版本需要重新下载", + "new_app_version_requires_re_download_description": "更新版本需要重新下载内容。请删除所有已下载项后重试。", + "back": "返回", + "delete": "删除", + "something_went_wrong": "出现问题", + "could_not_get_stream_url_from_jellyfin": "无法从 Jellyfin 获取串流 URL", + "eta": "预计完成时间 {{eta}}", + "methods": "方法", + "toasts": { + "you_are_not_allowed_to_download_files": "您无权下载文件。", + "deleted_all_movies_successfully": "成功删除所有电影!", + "failed_to_delete_all_movies": "删除所有电影失败", + "deleted_all_tvseries_successfully": "成功删除所有剧集!", + "failed_to_delete_all_tvseries": "删除所有剧集失败", + "download_cancelled": "下载已取消", + "could_not_cancel_download": "无法取消下载", + "download_completed": "下载完成", + "download_started_for": "开始下载 {{item}}", + "item_is_ready_to_be_downloaded": "{{item}} 准备好下载", + "download_stated_for_item": "开始下载 {{item}}", + "download_failed_for_item": "下载失败 {{item}} - {{error}}", + "download_completed_for_item": "下载完成 {{item}}", + "queued_item_for_optimization": "已将 {{item}} 队列进行优化", + "failed_to_start_download_for_item": "无法开始下载 {{item}}: {{message}}", + "server_responded_with_status_code": "服务器响应状态 {{statusCode}}", + "no_response_received_from_server": "未收到服务器响应", + "error_setting_up_the_request": "设置请求时出错", + "failed_to_start_download_for_item_unexpected_error": "无法开始下载 {{item}}: 发生意外错误", + "all_files_folders_and_jobs_deleted_successfully": "所有文件、文件夹和任务成功删除", + "an_error_occured_while_deleting_files_and_jobs": "删除文件和任务时发生错误", + "go_to_downloads": "前往下载" + } + } + }, + "search": { + "search_here": "在此搜索...", + "search": "搜索...", + "x_items": "{{count}} 项目", + "library": "媒体库", + "discover": "发现", + "no_results": "没有结果", + "no_results_found_for": "未找到结果", + "movies": "电影", + "series": "剧集", + "episodes": "单集", + "collections": "收藏", + "actors": "演员", + "request_movies": "请求电影", + "request_series": "请求系列", + "recently_added": "最近添加", + "recent_requests": "最近请求", + "plex_watchlist": "Plex 观影清单", + "trending": "趋势", + "popular_movies": "热门电影", + "movie_genres": "电影类型", + "upcoming_movies": "即将上映的电影", + "studios": "工作室", + "popular_tv": "热门电影", + "tv_genres": "剧集类型", + "upcoming_tv": "即将上映的剧集", + "networks": "网络", + "tmdb_movie_keyword": "TMDB 电影关键词", + "tmdb_movie_genre": "TMDB 电影类型", + "tmdb_tv_keyword": "TMDB 剧集关键词", + "tmdb_tv_genre": "TMDB 剧集类型", + "tmdb_search": "TMDB 搜索", + "tmdb_studio": "TMDB 工作室", + "tmdb_network": "TMDB 网络", + "tmdb_movie_streaming_services": "TMDB 电影流媒体服务", + "tmdb_tv_streaming_services": "TMDB 剧集流媒体服务" + }, + "library": { + "no_items_found": "未找到项目", + "no_results": "没有结果", + "no_libraries_found": "未找到媒体库", + "item_types": { + "movies": "电影", + "series": "剧集", + "boxsets": "套装", + "items": "项" + }, + "options": { + "display": "显示", + "row": "行", + "list": "列表", + "image_style": "图片样式", + "poster": "海报", + "cover": "封面", + "show_titles": "显示标题", + "show_stats": "显示统计" + }, + "filters": { + "genres": "类型", + "years": "年份", + "sort_by": "排序依据", + "sort_order": "排序顺序", + "asc": "Ascending", + "desc": "Descending", + "tags": "标签" + } + }, + "favorites": { + "series": "剧集", + "movies": "电影", + "episodes": "单集", + "videos": "视频", + "boxsets": "套装", + "playlists": "播放列表", + "noDataTitle": "暂无收藏", + "noData": "将项目标记为收藏,它们将显示在此处以便快速访问。" + }, + "custom_links": { + "no_links": "无链接" + }, + "player": { + "error": "错误", + "failed_to_get_stream_url": "无法获取流 URL", + "an_error_occured_while_playing_the_video": "播放视频时发生错误。请检查设置中的日志。", + "client_error": "客户端错误", + "could_not_create_stream_for_chromecast": "无法为 Chromecast 建立串流", + "message_from_server": "来自服务器的消息", + "video_has_finished_playing": "视频播放完成!", + "no_video_source": "无视频来源...", + "next_episode": "下一集", + "refresh_tracks": "刷新轨道", + "subtitle_tracks": "字幕轨道:", + "audio_tracks": "音频轨道:", + "playback_state": "播放状态:", + "no_data_available": "无可用数据", + "index": "索引:" + }, + "item_card": { + "next_up": "下一个", + "no_items_to_display": "无项目显示", + "cast_and_crew": "演员和工作人员", + "series": "剧集", + "seasons": "季", + "season": "季", + "no_episodes_for_this_season": "本季无剧集", + "overview": "概览", + "more_with": "更多 {{name}} 的作品", + "similar_items": "类似项目", + "no_similar_items_found": "未找到类似项目", + "video": "视频", + "more_details": "更多详情", + "quality": "质量", + "audio": "音频", + "subtitles": "字幕", + "show_more": "显示更多", + "show_less": "显示更少", + "appeared_in": "出现于", + "could_not_load_item": "无法加载项目", + "none": "无", + "download": { + "download_season": "下载季", + "download_series": "下载剧集", + "download_episode": "下载单集", + "download_movie": "下载电影", + "download_x_item": "下载 {{item_count}} 项目", + "download_button": "下载", + "using_optimized_server": "使用 Optimized Server", + "using_default_method": "使用默认方法" + } + }, + "live_tv": { + "next": "下一个", + "previous": "上一个", + "live_tv": "直播电视", + "coming_soon": "即将播出", + "on_now": "正在播放", + "shows": "节目", + "movies": "电影", + "sports": "体育", + "for_kids": "儿童", + "news": "新闻" + }, + "jellyseerr": { + "confirm": "确认", + "cancel": "取消", + "yes": "是", + "whats_wrong": "出了什么问题?", + "issue_type": "问题类型", + "select_an_issue": "选择一个问题", + "types": "类型", + "describe_the_issue": "(可选)描述问题...", + "submit_button": "提交", + "report_issue_button": "报告问题", + "request_button": "请求", + "are_you_sure_you_want_to_request_all_seasons": "您确定要请求所有季度的剧集吗?", + "failed_to_login": "登录失败", + "cast": "演员", + "details": "详情", + "status": "状态", + "original_title": "原标题", + "series_type": "剧集类型", + "release_dates": "发行日期", + "first_air_date": "首次播出日期", + "next_air_date": "下次播出日期", + "revenue": "收入", + "budget": "预算", + "original_language": "原始语言", + "production_country": "制作国家/地区", + "studios": "工作室", + "network": "网络", + "currently_streaming_on": "目前在以下流媒体上播放", + "advanced": "高级设置", + "request_as": "选择用户以请求", + "tags": "标签", + "quality_profile": "质量配置文件", + "root_folder": "根文件夹", + "season_all": "Season (all)", + "season_number": "第 {{season_number}} 季", + "number_episodes": "{{episode_number}} 集", + "born": "出生", + "appearances": "出场", + "toasts": { + "jellyseer_does_not_meet_requirements": "Jellyseerr 服务器不符合最低版本要求!请使用 2.0.0 及以上版本", + "jellyseerr_test_failed": "Jellyseerr 测试失败。请重试。", + "failed_to_test_jellyseerr_server_url": "无法测试 Jellyseerr 服务器 URL", + "issue_submitted": "问题已提交!", + "requested_item": "已请求 {{item}}!", + "you_dont_have_permission_to_request": "您无权请求媒体!", + "something_went_wrong_requesting_media": "请求媒体时出了些问题!" + } + }, + "tabs": { + "home": "主页", + "search": "搜索", + "library": "媒体库", + "custom_links": "自定义链接", + "favorites": "收藏" + } } diff --git a/translations/zh-TW.json b/translations/zh-TW.json index 5cfb2066..e533b56a 100644 --- a/translations/zh-TW.json +++ b/translations/zh-TW.json @@ -1,472 +1,472 @@ { - "login": { - "username_required": "需要用戶名", - "error_title": "錯誤", - "login_title": "登入", - "login_to_title": "登入至", - "username_placeholder": "用戶名", - "password_placeholder": "密碼", - "login_button": "登入", - "quick_connect": "快速連接", - "enter_code_to_login": "輸入代碼 {{code}} 以登入", - "failed_to_initiate_quick_connect": "無法啟動快速連接", - "got_it": "知道了", - "connection_failed": "連接失敗", - "could_not_connect_to_server": "無法連接到伺服器。請檢查 URL 和您的網絡連接。", - "an_unexpected_error_occured": "發生意外錯誤", - "change_server": "更改伺服器", - "invalid_username_or_password": "無效的用戶名或密碼", - "user_does_not_have_permission_to_log_in": "用戶無權登入", - "server_is_taking_too_long_to_respond_try_again_later": "伺服器響應時間過長,請稍後再試", - "server_received_too_many_requests_try_again_later": "伺服器收到太多請求,請稍後再試。", - "there_is_a_server_error": "伺服器出錯", - "an_unexpected_error_occured_did_you_enter_the_correct_url": "發生意外錯誤。您是否正確輸入了伺服器 URL?" - }, - "server": { - "enter_url_to_jellyfin_server": "輸入您的 Jellyfin 伺服器的 URL", - "server_url_placeholder": "http(s)://your-server.com", - "connect_button": "連接", - "previous_servers": "先前的伺服器", - "clear_button": "清除", - "search_for_local_servers": "搜尋本地伺服器", - "searching": "搜尋中...", - "servers": "伺服器" - }, - "home": { - "no_internet": "無網絡", - "no_items": "無項目", - "no_internet_message": "別擔心,您仍然可以觀看\n已下載的內容。", - "go_to_downloads": "前往下載", - "oops": "哎呀!", - "error_message": "出錯了。\n請重新登出並登入。", - "continue_watching": "繼續觀看", - "next_up": "下一個", - "recently_added_in": "最近添加於 {{libraryName}}", - "suggested_movies": "推薦電影", - "suggested_episodes": "推薦劇集", - "intro": { - "welcome_to_streamyfin": "歡迎來到 Streamyfin", - "a_free_and_open_source_client_for_jellyfin": "一個免費且開源的 Jellyfin 客戶端。", - "features_title": "功能", - "features_description": "Streamyfin 擁有許多功能,並與多種軟體整合,您可以在設置菜單中找到這些功能,包括:", - "jellyseerr_feature_description": "連接到您的 Jellyseerr 實例並直接在應用程序中請求電影。", - "downloads_feature_title": "下載", - "downloads_feature_description": "下載電影和電視節目以離線觀看。使用默認方法或安裝 Optimized Server 以在背景中下載文件。", - "chromecast_feature_description": "將電影和電視節目投射到您的 Chromecast 設備。", - "centralised_settings_plugin_title": "統一設置插件", - "centralised_settings_plugin_description": "從 Jellyfin 伺服器上的統一位置改變設置。所有用戶的所有客戶端設置將會自動同步。", - "done_button": "完成", - "go_to_settings_button": "前往設置", - "read_more": "閱讀更多" - }, - "settings": { - "settings_title": "設置", - "log_out_button": "登出", - "user_info": { - "user_info_title": "用戶信息", - "user": "用戶", - "server": "伺服器", - "token": "令牌", - "app_version": "應用版本" - }, - "quick_connect": { - "quick_connect_title": "快速連接", - "authorize_button": "授權快速連接", - "enter_the_quick_connect_code": "輸入快速連接代碼...", - "success": "成功", - "quick_connect_autorized": "快速連接已授權", - "error": "錯誤", - "invalid_code": "無效代碼", - "authorize": "授權" - }, - "media_controls": { - "media_controls_title": "媒體控制", - "forward_skip_length": "快進秒數", - "rewind_length": "倒帶秒數", - "seconds_unit": "秒" - }, - "audio": { - "audio_title": "音頻", - "set_audio_track": "從上一個項目設置音軌", - "audio_language": "音頻語言", - "audio_hint": "選擇默認音頻語言。", - "none": "無", - "language": "語言" - }, - "subtitles": { - "subtitle_title": "字幕", - "subtitle_language": "字幕語言", - "subtitle_mode": "字幕模式", - "set_subtitle_track": "從上一個項目設置字幕軌道", - "subtitle_size": "字幕大小", - "subtitle_hint": "配置字幕偏好。", - "none": "無", - "language": "語言", - "loading": "加載中", - "modes": { - "Default": "默認", - "Smart": "智能", - "Always": "總是", - "None": "無", - "OnlyForced": "僅強制字幕" - } - }, - "other": { - "other_title": "其他", - "follow_device_orientation": "自動旋轉", - "video_orientation": "影片方向", - "orientation": "方向", - "orientations": { - "DEFAULT": "默認", - "ALL": "全部", - "PORTRAIT": "縱向", - "PORTRAIT_UP": "縱向向上", - "PORTRAIT_DOWN": "縱向向下", - "LANDSCAPE": "橫向", - "LANDSCAPE_LEFT": "橫向左", - "LANDSCAPE_RIGHT": "橫向右", - "OTHER": "其他", - "UNKNOWN": "未知" - }, - "safe_area_in_controls": "控制中的安全區域", - "video_player": "Video player", - "video_players": { - "VLC_3": "VLC 3", - "VLC_4": "VLC 4 (Experimental + PiP)" - }, - "show_custom_menu_links": "顯示自定義菜單鏈接", - "hide_libraries": "隱藏媒體庫", - "select_liraries_you_want_to_hide": "選擇您想從媒體庫頁面和主頁隱藏的媒體庫。", - "disable_haptic_feedback": "禁用觸覺回饋" - }, - "downloads": { - "downloads_title": "下載", - "download_method": "下載方法", - "remux_max_download": "Remux 最大下載", - "auto_download": "自動下載", - "optimized_versions_server": "Optimized Version 伺服器", - "save_button": "保存", - "optimized_server": "Optimized Server", - "optimized": "已優化", - "default": "默認", - "optimized_version_hint": "輸入 Optimized Server 的 URL。URL 應包括 http(s) 和端口 (可選)。", - "read_more_about_optimized_server": "閱讀更多關於 Optimized Server 的信息。", - "url": "URL", - "server_url_placeholder": "http(s)://domain.org:port" - }, - "plugins": { - "plugins_title": "插件", - "jellyseerr": { - "jellyseerr_warning": "此插件處於早期階段。功能可能會有變化。", - "server_url": "伺服器 URL", - "server_url_hint": "示例:http(s)://your-host.url\n(如果需要,添加端口)", - "server_url_placeholder": "Jellyseerr URL...", - "password": "密碼", - "password_placeholder": "輸入 Jellyfin 用戶 {{username}} 的密碼", - "save_button": "保存", - "clear_button": "清除", - "login_button": "登入", - "total_media_requests": "總媒體請求", - "movie_quota_limit": "電影配額限制", - "movie_quota_days": "電影配額天數", - "tv_quota_limit": "電視配額限制", - "tv_quota_days": "電視配額天數", - "reset_jellyseerr_config_button": "重置 Jellyseerr 配置", - "unlimited": "無限制", - "plus_n_more": "+{{n}} more", - "order_by": { - "DEFAULT": "Default", - "VOTE_COUNT_AND_AVERAGE": "Vote count and average", - "POPULARITY": "Popularity" - } - }, - "marlin_search": { - "enable_marlin_search": "啟用 Marlin 搜索", - "url": "URL", - "server_url_placeholder": "http(s)://domain.org:port", - "marlin_search_hint": "輸入 Marlin 伺服器的 URL。URL 應包括 http(s) 和端口 (可選)。", - "read_more_about_marlin": "閱讀更多關於 Marlin 的信息。", - "save_button": "保存", - "toasts": { - "saved": "已保存" - } - } - }, - "storage": { - "storage_title": "存儲", - "app_usage": "應用 {{usedSpace}}%", - "device_usage": "設備 {{availableSpace}}%", - "size_used": "已使用 {{used}} / {{total}}", - "delete_all_downloaded_files": "刪除所有已下載文件" - }, - "intro": { - "show_intro": "顯示介紹", - "reset_intro": "重置介紹" - }, - "logs": { - "logs_title": "日誌", - "no_logs_available": "無可用日誌", - "delete_all_logs": "刪除所有日誌" - }, - "languages": { - "title": "語言", - "app_language": "應用語言", - "app_language_description": "選擇應用的語言。", - "system": "系統" - }, - "toasts": { - "error_deleting_files": "刪除文件時出錯", - "background_downloads_enabled": "背景下載已啟用", - "background_downloads_disabled": "背景下載已禁用", - "connected": "已連接", - "could_not_connect": "無法連接", - "invalid_url": "無效的 URL" - } - }, - "downloads": { - "downloads_title": "下載", - "tvseries": "電視劇", - "movies": "電影", - "queue": "隊列", - "queue_hint": "應用重啟後隊列和下載將會丟失", - "no_items_in_queue": "隊列中無項目", - "no_downloaded_items": "無已下載項目", - "delete_all_movies_button": "刪除所有電影", - "delete_all_tvseries_button": "刪除所有電視劇", - "delete_all_button": "刪除全部", - "active_download": "活動下載", - "no_active_downloads": "無活動下載", - "active_downloads": "活動下載", - "new_app_version_requires_re_download": "新應用版本需要重新下載", - "new_app_version_requires_re_download_description": "新更新需要重新下載內容。請刪除所有已下載內容後再重試。", - "back": "返回", - "delete": "刪除", - "something_went_wrong": "出了些問題", - "could_not_get_stream_url_from_jellyfin": "無法從 Jellyfin 獲取串流 URL", - "eta": "預計完成時間 {{eta}}", - "methods": "方法", - "toasts": { - "you_are_not_allowed_to_download_files": "您無權下載文件。", - "deleted_all_movies_successfully": "成功刪除所有電影!", - "failed_to_delete_all_movies": "刪除所有電影失敗", - "deleted_all_tvseries_successfully": "成功刪除所有電視劇!", - "failed_to_delete_all_tvseries": "刪除所有電視劇失敗", - "download_cancelled": "下載已取消", - "could_not_cancel_download": "無法取消下載", - "download_completed": "下載完成", - "download_started_for": "開始下載 {{item}}", - "item_is_ready_to_be_downloaded": "{{item}} 準備好下載", - "download_stated_for_item": "開始下載 {{item}}", - "download_failed_for_item": "下載失敗 {{item}} - {{error}}", - "download_completed_for_item": "下載完成 {{item}}", - "queued_item_for_optimization": "已將 {{item}} 排隊進行優化", - "failed_to_start_download_for_item": "無法開始下載 {{item}}: {{message}}", - "server_responded_with_status_code": "伺服器響應狀態 {{statusCode}}", - "no_response_received_from_server": "未收到伺服器的響應", - "error_setting_up_the_request": "設置請求時出錯", - "failed_to_start_download_for_item_unexpected_error": "無法開始下載 {{item}}: 發生意外錯誤", - "all_files_folders_and_jobs_deleted_successfully": "所有文件、文件夾和任務成功刪除", - "an_error_occured_while_deleting_files_and_jobs": "刪除文件和任務時發生錯誤", - "go_to_downloads": "前往下載" - } - } - }, - "search": { - "search_here": "在這裡搜索...", - "search": "搜索...", - "x_items": "{{count}} 項目", - "library": "媒體庫", - "discover": "發現", - "no_results": "沒有結果", - "no_results_found_for": "未找到結果", - "movies": "電影", - "series": "系列", - "episodes": "劇集", - "collections": "收藏", - "actors": "演員", - "request_movies": "請求電影", - "request_series": "請求系列", - "recently_added": "最近添加", - "recent_requests": "最近請求", - "plex_watchlist": "Plex 觀影清單", - "trending": "趨勢", - "popular_movies": "熱門電影", - "movie_genres": "電影類型", - "upcoming_movies": "即將上映的電影", - "studios": "工作室", - "popular_tv": "熱門電視", - "tv_genres": "電視類型", - "upcoming_tv": "即將上映的電視", - "networks": "網絡", - "tmdb_movie_keyword": "TMDB 電影關鍵詞", - "tmdb_movie_genre": "TMDB 電影類型", - "tmdb_tv_keyword": "TMDB 電視關鍵詞", - "tmdb_tv_genre": "TMDB 電視類型", - "tmdb_search": "TMDB 搜索", - "tmdb_studio": "TMDB 工作室", - "tmdb_network": "TMDB 網絡", - "tmdb_movie_streaming_services": "TMDB 電影流媒體服務", - "tmdb_tv_streaming_services": "TMDB 電視流媒體服務" - }, - "library": { - "no_items_found": "未找到項目", - "no_results": "沒有結果", - "no_libraries_found": "未找到媒體庫", - "item_types": { - "movies": "電影", - "series": "系列", - "boxsets": "套裝", - "items": "項目" - }, - "options": { - "display": "顯示", - "row": "行", - "list": "列表", - "image_style": "圖片樣式", - "poster": "海報", - "cover": "封面", - "show_titles": "顯示標題", - "show_stats": "顯示統計" - }, - "filters": { - "genres": "類型", - "years": "年份", - "sort_by": "排序依據", - "sort_order": "排序順序", - "asc": "Ascending", - "desc": "Descending", - "tags": "標籤" - } - }, - "favorites": { - "series": "系列", - "movies": "電影", - "episodes": "劇集", - "videos": "影片", - "boxsets": "套裝", - "playlists": "播放列表", - "noDataTitle": "尚無收藏", - "noData": "將項目標記為收藏,它們將顯示在此處以便快速訪問。" - }, - "custom_links": { - "no_links": "無鏈接" - }, - "player": { - "error": "錯誤", - "failed_to_get_stream_url": "無法獲取流 URL", - "an_error_occured_while_playing_the_video": "播放影片時發生錯誤。請檢查設置中的日誌。", - "client_error": "客戶端錯誤", - "could_not_create_stream_for_chromecast": "無法為 Chromecast 建立串流", - "message_from_server": "來自伺服器的消息", - "video_has_finished_playing": "影片播放完畢!", - "no_video_source": "無影片來源...", - "next_episode": "下一集", - "refresh_tracks": "刷新軌道", - "subtitle_tracks": "字幕軌道:", - "audio_tracks": "音頻軌道:", - "playback_state": "播放狀態:", - "no_data_available": "無可用數據", - "index": "索引:" - }, - "item_card": { - "next_up": "下一個", - "no_items_to_display": "無項目顯示", - "cast_and_crew": "演員和工作人員", - "series": "系列", - "seasons": "季", - "season": "季", - "no_episodes_for_this_season": "本季無劇集", - "overview": "概覽", - "more_with": "更多 {{name}} 的作品", - "similar_items": "類似項目", - "no_similar_items_found": "未找到類似項目", - "video": "影片", - "more_details": "更多詳情", - "quality": "質量", - "audio": "音頻", - "subtitles": "字幕", - "show_more": "顯示更多", - "show_less": "顯示更少", - "appeared_in": "出現於", - "could_not_load_item": "無法加載項目", - "none": "無", - "download": { - "download_season": "下載季度", - "download_series": "下載系列", - "download_episode": "下載劇集", - "download_movie": "下載電影", - "download_x_item": "下載 {{item_count}} 項目", - "download_button": "下載", - "using_optimized_server": "使用 Optimized Server", - "using_default_method": "使用默認方法" - } - }, - "live_tv": { - "next": "下一個", - "previous": "上一個", - "live_tv": "直播電視", - "coming_soon": "即將推出", - "on_now": "正在播放", - "shows": "節目", - "movies": "電影", - "sports": "體育", - "for_kids": "兒童", - "news": "新聞" - }, - "jellyseerr": { - "confirm": "確認", - "cancel": "取消", - "yes": "是", - "whats_wrong": "出了什麼問題?", - "issue_type": "問題類型", - "select_an_issue": "選擇一個問題", - "types": "類型", - "describe_the_issue": "(可選)描述問題...", - "submit_button": "提交", - "report_issue_button": "報告問題", - "request_button": "請求", - "are_you_sure_you_want_to_request_all_seasons": "您確定要請求所有季度的劇集嗎?", - "failed_to_login": "登入失敗", - "cast": "演員", - "details": "詳情", - "status": "狀態", - "original_title": "原標題", - "series_type": "系列類型", - "release_dates": "發行日期", - "first_air_date": "首次播出日期", - "next_air_date": "下次播出日期", - "revenue": "收入", - "budget": "預算", - "original_language": "原始語言", - "production_country": "製作國家", - "studios": "工作室", - "network": "網絡", - "currently_streaming_on": "目前在以下流媒體上播放", - "advanced": "高級設定", - "request_as": "選擇用戶以作請求", - "tags": "標籤", - "quality_profile": "質量配置文件", - "root_folder": "根文件夾", - "season_all": "Season (all)", - "season_number": "第 {{season_number}} 季", - "number_episodes": "{{episode_number}} 集", - "born": "出生", - "appearances": "出場", - "toasts": { - "jellyseer_does_not_meet_requirements": "Jellyseerr 伺服器不符合最低版本要求!請使用 2.0.0 及以上版本。", - "jellyseerr_test_failed": "Jellyseerr 測試失敗。請再試一次。", - "failed_to_test_jellyseerr_server_url": "無法測試 Jellyseerr 伺服器 URL", - "issue_submitted": "問題已提交!", - "requested_item": "已請求 {{item}}!", - "you_dont_have_permission_to_request": "您無權請求媒體!", - "something_went_wrong_requesting_media": "請求媒體時出了些問題!" - } - }, - "tabs": { - "home": "主頁", - "search": "搜索", - "library": "媒體庫", - "custom_links": "自定義鏈接", - "favorites": "收藏" - } + "login": { + "username_required": "需要用戶名", + "error_title": "錯誤", + "login_title": "登入", + "login_to_title": "登入至", + "username_placeholder": "用戶名", + "password_placeholder": "密碼", + "login_button": "登入", + "quick_connect": "快速連接", + "enter_code_to_login": "輸入代碼 {{code}} 以登入", + "failed_to_initiate_quick_connect": "無法啟動快速連接", + "got_it": "知道了", + "connection_failed": "連接失敗", + "could_not_connect_to_server": "無法連接到伺服器。請檢查 URL 和您的網絡連接。", + "an_unexpected_error_occured": "發生意外錯誤", + "change_server": "更改伺服器", + "invalid_username_or_password": "無效的用戶名或密碼", + "user_does_not_have_permission_to_log_in": "用戶無權登入", + "server_is_taking_too_long_to_respond_try_again_later": "伺服器響應時間過長,請稍後再試", + "server_received_too_many_requests_try_again_later": "伺服器收到太多請求,請稍後再試。", + "there_is_a_server_error": "伺服器出錯", + "an_unexpected_error_occured_did_you_enter_the_correct_url": "發生意外錯誤。您是否正確輸入了伺服器 URL?" + }, + "server": { + "enter_url_to_jellyfin_server": "輸入您的 Jellyfin 伺服器的 URL", + "server_url_placeholder": "http(s)://your-server.com", + "connect_button": "連接", + "previous_servers": "先前的伺服器", + "clear_button": "清除", + "search_for_local_servers": "搜尋本地伺服器", + "searching": "搜尋中...", + "servers": "伺服器" + }, + "home": { + "no_internet": "無網絡", + "no_items": "無項目", + "no_internet_message": "別擔心,您仍然可以觀看\n已下載的內容。", + "go_to_downloads": "前往下載", + "oops": "哎呀!", + "error_message": "出錯了。\n請重新登出並登入。", + "continue_watching": "繼續觀看", + "next_up": "下一個", + "recently_added_in": "最近添加於 {{libraryName}}", + "suggested_movies": "推薦電影", + "suggested_episodes": "推薦劇集", + "intro": { + "welcome_to_streamyfin": "歡迎來到 Streamyfin", + "a_free_and_open_source_client_for_jellyfin": "一個免費且開源的 Jellyfin 客戶端。", + "features_title": "功能", + "features_description": "Streamyfin 擁有許多功能,並與多種軟體整合,您可以在設置菜單中找到這些功能,包括:", + "jellyseerr_feature_description": "連接到您的 Jellyseerr 實例並直接在應用程序中請求電影。", + "downloads_feature_title": "下載", + "downloads_feature_description": "下載電影和電視節目以離線觀看。使用默認方法或安裝 Optimized Server 以在背景中下載文件。", + "chromecast_feature_description": "將電影和電視節目投射到您的 Chromecast 設備。", + "centralised_settings_plugin_title": "統一設置插件", + "centralised_settings_plugin_description": "從 Jellyfin 伺服器上的統一位置改變設置。所有用戶的所有客戶端設置將會自動同步。", + "done_button": "完成", + "go_to_settings_button": "前往設置", + "read_more": "閱讀更多" + }, + "settings": { + "settings_title": "設置", + "log_out_button": "登出", + "user_info": { + "user_info_title": "用戶信息", + "user": "用戶", + "server": "伺服器", + "token": "令牌", + "app_version": "應用版本" + }, + "quick_connect": { + "quick_connect_title": "快速連接", + "authorize_button": "授權快速連接", + "enter_the_quick_connect_code": "輸入快速連接代碼...", + "success": "成功", + "quick_connect_autorized": "快速連接已授權", + "error": "錯誤", + "invalid_code": "無效代碼", + "authorize": "授權" + }, + "media_controls": { + "media_controls_title": "媒體控制", + "forward_skip_length": "快進秒數", + "rewind_length": "倒帶秒數", + "seconds_unit": "秒" + }, + "audio": { + "audio_title": "音頻", + "set_audio_track": "從上一個項目設置音軌", + "audio_language": "音頻語言", + "audio_hint": "選擇默認音頻語言。", + "none": "無", + "language": "語言" + }, + "subtitles": { + "subtitle_title": "字幕", + "subtitle_language": "字幕語言", + "subtitle_mode": "字幕模式", + "set_subtitle_track": "從上一個項目設置字幕軌道", + "subtitle_size": "字幕大小", + "subtitle_hint": "配置字幕偏好。", + "none": "無", + "language": "語言", + "loading": "加載中", + "modes": { + "Default": "默認", + "Smart": "智能", + "Always": "總是", + "None": "無", + "OnlyForced": "僅強制字幕" + } + }, + "other": { + "other_title": "其他", + "follow_device_orientation": "自動旋轉", + "video_orientation": "影片方向", + "orientation": "方向", + "orientations": { + "DEFAULT": "默認", + "ALL": "全部", + "PORTRAIT": "縱向", + "PORTRAIT_UP": "縱向向上", + "PORTRAIT_DOWN": "縱向向下", + "LANDSCAPE": "橫向", + "LANDSCAPE_LEFT": "橫向左", + "LANDSCAPE_RIGHT": "橫向右", + "OTHER": "其他", + "UNKNOWN": "未知" + }, + "safe_area_in_controls": "控制中的安全區域", + "video_player": "Video player", + "video_players": { + "VLC_3": "VLC 3", + "VLC_4": "VLC 4 (Experimental + PiP)" + }, + "show_custom_menu_links": "顯示自定義菜單鏈接", + "hide_libraries": "隱藏媒體庫", + "select_liraries_you_want_to_hide": "選擇您想從媒體庫頁面和主頁隱藏的媒體庫。", + "disable_haptic_feedback": "禁用觸覺回饋" + }, + "downloads": { + "downloads_title": "下載", + "download_method": "下載方法", + "remux_max_download": "Remux 最大下載", + "auto_download": "自動下載", + "optimized_versions_server": "Optimized Version 伺服器", + "save_button": "保存", + "optimized_server": "Optimized Server", + "optimized": "已優化", + "default": "默認", + "optimized_version_hint": "輸入 Optimized Server 的 URL。URL 應包括 http(s) 和端口 (可選)。", + "read_more_about_optimized_server": "閱讀更多關於 Optimized Server 的信息。", + "url": "URL", + "server_url_placeholder": "http(s)://domain.org:port" + }, + "plugins": { + "plugins_title": "插件", + "jellyseerr": { + "jellyseerr_warning": "此插件處於早期階段。功能可能會有變化。", + "server_url": "伺服器 URL", + "server_url_hint": "示例:http(s)://your-host.url\n(如果需要,添加端口)", + "server_url_placeholder": "Jellyseerr URL...", + "password": "密碼", + "password_placeholder": "輸入 Jellyfin 用戶 {{username}} 的密碼", + "save_button": "保存", + "clear_button": "清除", + "login_button": "登入", + "total_media_requests": "總媒體請求", + "movie_quota_limit": "電影配額限制", + "movie_quota_days": "電影配額天數", + "tv_quota_limit": "電視配額限制", + "tv_quota_days": "電視配額天數", + "reset_jellyseerr_config_button": "重置 Jellyseerr 配置", + "unlimited": "無限制", + "plus_n_more": "+{{n}} more", + "order_by": { + "DEFAULT": "Default", + "VOTE_COUNT_AND_AVERAGE": "Vote count and average", + "POPULARITY": "Popularity" + } + }, + "marlin_search": { + "enable_marlin_search": "啟用 Marlin 搜索", + "url": "URL", + "server_url_placeholder": "http(s)://domain.org:port", + "marlin_search_hint": "輸入 Marlin 伺服器的 URL。URL 應包括 http(s) 和端口 (可選)。", + "read_more_about_marlin": "閱讀更多關於 Marlin 的信息。", + "save_button": "保存", + "toasts": { + "saved": "已保存" + } + } + }, + "storage": { + "storage_title": "存儲", + "app_usage": "應用 {{usedSpace}}%", + "device_usage": "設備 {{availableSpace}}%", + "size_used": "已使用 {{used}} / {{total}}", + "delete_all_downloaded_files": "刪除所有已下載文件" + }, + "intro": { + "show_intro": "顯示介紹", + "reset_intro": "重置介紹" + }, + "logs": { + "logs_title": "日誌", + "no_logs_available": "無可用日誌", + "delete_all_logs": "刪除所有日誌" + }, + "languages": { + "title": "語言", + "app_language": "應用語言", + "app_language_description": "選擇應用的語言。", + "system": "系統" + }, + "toasts": { + "error_deleting_files": "刪除文件時出錯", + "background_downloads_enabled": "背景下載已啟用", + "background_downloads_disabled": "背景下載已禁用", + "connected": "已連接", + "could_not_connect": "無法連接", + "invalid_url": "無效的 URL" + } + }, + "downloads": { + "downloads_title": "下載", + "tvseries": "電視劇", + "movies": "電影", + "queue": "隊列", + "queue_hint": "應用重啟後隊列和下載將會丟失", + "no_items_in_queue": "隊列中無項目", + "no_downloaded_items": "無已下載項目", + "delete_all_movies_button": "刪除所有電影", + "delete_all_tvseries_button": "刪除所有電視劇", + "delete_all_button": "刪除全部", + "active_download": "活動下載", + "no_active_downloads": "無活動下載", + "active_downloads": "活動下載", + "new_app_version_requires_re_download": "新應用版本需要重新下載", + "new_app_version_requires_re_download_description": "新更新需要重新下載內容。請刪除所有已下載內容後再重試。", + "back": "返回", + "delete": "刪除", + "something_went_wrong": "出了些問題", + "could_not_get_stream_url_from_jellyfin": "無法從 Jellyfin 獲取串流 URL", + "eta": "預計完成時間 {{eta}}", + "methods": "方法", + "toasts": { + "you_are_not_allowed_to_download_files": "您無權下載文件。", + "deleted_all_movies_successfully": "成功刪除所有電影!", + "failed_to_delete_all_movies": "刪除所有電影失敗", + "deleted_all_tvseries_successfully": "成功刪除所有電視劇!", + "failed_to_delete_all_tvseries": "刪除所有電視劇失敗", + "download_cancelled": "下載已取消", + "could_not_cancel_download": "無法取消下載", + "download_completed": "下載完成", + "download_started_for": "開始下載 {{item}}", + "item_is_ready_to_be_downloaded": "{{item}} 準備好下載", + "download_stated_for_item": "開始下載 {{item}}", + "download_failed_for_item": "下載失敗 {{item}} - {{error}}", + "download_completed_for_item": "下載完成 {{item}}", + "queued_item_for_optimization": "已將 {{item}} 排隊進行優化", + "failed_to_start_download_for_item": "無法開始下載 {{item}}: {{message}}", + "server_responded_with_status_code": "伺服器響應狀態 {{statusCode}}", + "no_response_received_from_server": "未收到伺服器的響應", + "error_setting_up_the_request": "設置請求時出錯", + "failed_to_start_download_for_item_unexpected_error": "無法開始下載 {{item}}: 發生意外錯誤", + "all_files_folders_and_jobs_deleted_successfully": "所有文件、文件夾和任務成功刪除", + "an_error_occured_while_deleting_files_and_jobs": "刪除文件和任務時發生錯誤", + "go_to_downloads": "前往下載" + } + } + }, + "search": { + "search_here": "在這裡搜索...", + "search": "搜索...", + "x_items": "{{count}} 項目", + "library": "媒體庫", + "discover": "發現", + "no_results": "沒有結果", + "no_results_found_for": "未找到結果", + "movies": "電影", + "series": "系列", + "episodes": "劇集", + "collections": "收藏", + "actors": "演員", + "request_movies": "請求電影", + "request_series": "請求系列", + "recently_added": "最近添加", + "recent_requests": "最近請求", + "plex_watchlist": "Plex 觀影清單", + "trending": "趨勢", + "popular_movies": "熱門電影", + "movie_genres": "電影類型", + "upcoming_movies": "即將上映的電影", + "studios": "工作室", + "popular_tv": "熱門電視", + "tv_genres": "電視類型", + "upcoming_tv": "即將上映的電視", + "networks": "網絡", + "tmdb_movie_keyword": "TMDB 電影關鍵詞", + "tmdb_movie_genre": "TMDB 電影類型", + "tmdb_tv_keyword": "TMDB 電視關鍵詞", + "tmdb_tv_genre": "TMDB 電視類型", + "tmdb_search": "TMDB 搜索", + "tmdb_studio": "TMDB 工作室", + "tmdb_network": "TMDB 網絡", + "tmdb_movie_streaming_services": "TMDB 電影流媒體服務", + "tmdb_tv_streaming_services": "TMDB 電視流媒體服務" + }, + "library": { + "no_items_found": "未找到項目", + "no_results": "沒有結果", + "no_libraries_found": "未找到媒體庫", + "item_types": { + "movies": "電影", + "series": "系列", + "boxsets": "套裝", + "items": "項目" + }, + "options": { + "display": "顯示", + "row": "行", + "list": "列表", + "image_style": "圖片樣式", + "poster": "海報", + "cover": "封面", + "show_titles": "顯示標題", + "show_stats": "顯示統計" + }, + "filters": { + "genres": "類型", + "years": "年份", + "sort_by": "排序依據", + "sort_order": "排序順序", + "asc": "Ascending", + "desc": "Descending", + "tags": "標籤" + } + }, + "favorites": { + "series": "系列", + "movies": "電影", + "episodes": "劇集", + "videos": "影片", + "boxsets": "套裝", + "playlists": "播放列表", + "noDataTitle": "尚無收藏", + "noData": "將項目標記為收藏,它們將顯示在此處以便快速訪問。" + }, + "custom_links": { + "no_links": "無鏈接" + }, + "player": { + "error": "錯誤", + "failed_to_get_stream_url": "無法獲取流 URL", + "an_error_occured_while_playing_the_video": "播放影片時發生錯誤。請檢查設置中的日誌。", + "client_error": "客戶端錯誤", + "could_not_create_stream_for_chromecast": "無法為 Chromecast 建立串流", + "message_from_server": "來自伺服器的消息", + "video_has_finished_playing": "影片播放完畢!", + "no_video_source": "無影片來源...", + "next_episode": "下一集", + "refresh_tracks": "刷新軌道", + "subtitle_tracks": "字幕軌道:", + "audio_tracks": "音頻軌道:", + "playback_state": "播放狀態:", + "no_data_available": "無可用數據", + "index": "索引:" + }, + "item_card": { + "next_up": "下一個", + "no_items_to_display": "無項目顯示", + "cast_and_crew": "演員和工作人員", + "series": "系列", + "seasons": "季", + "season": "季", + "no_episodes_for_this_season": "本季無劇集", + "overview": "概覽", + "more_with": "更多 {{name}} 的作品", + "similar_items": "類似項目", + "no_similar_items_found": "未找到類似項目", + "video": "影片", + "more_details": "更多詳情", + "quality": "質量", + "audio": "音頻", + "subtitles": "字幕", + "show_more": "顯示更多", + "show_less": "顯示更少", + "appeared_in": "出現於", + "could_not_load_item": "無法加載項目", + "none": "無", + "download": { + "download_season": "下載季度", + "download_series": "下載系列", + "download_episode": "下載劇集", + "download_movie": "下載電影", + "download_x_item": "下載 {{item_count}} 項目", + "download_button": "下載", + "using_optimized_server": "使用 Optimized Server", + "using_default_method": "使用默認方法" + } + }, + "live_tv": { + "next": "下一個", + "previous": "上一個", + "live_tv": "直播電視", + "coming_soon": "即將推出", + "on_now": "正在播放", + "shows": "節目", + "movies": "電影", + "sports": "體育", + "for_kids": "兒童", + "news": "新聞" + }, + "jellyseerr": { + "confirm": "確認", + "cancel": "取消", + "yes": "是", + "whats_wrong": "出了什麼問題?", + "issue_type": "問題類型", + "select_an_issue": "選擇一個問題", + "types": "類型", + "describe_the_issue": "(可選)描述問題...", + "submit_button": "提交", + "report_issue_button": "報告問題", + "request_button": "請求", + "are_you_sure_you_want_to_request_all_seasons": "您確定要請求所有季度的劇集嗎?", + "failed_to_login": "登入失敗", + "cast": "演員", + "details": "詳情", + "status": "狀態", + "original_title": "原標題", + "series_type": "系列類型", + "release_dates": "發行日期", + "first_air_date": "首次播出日期", + "next_air_date": "下次播出日期", + "revenue": "收入", + "budget": "預算", + "original_language": "原始語言", + "production_country": "製作國家", + "studios": "工作室", + "network": "網絡", + "currently_streaming_on": "目前在以下流媒體上播放", + "advanced": "高級設定", + "request_as": "選擇用戶以作請求", + "tags": "標籤", + "quality_profile": "質量配置文件", + "root_folder": "根文件夾", + "season_all": "Season (all)", + "season_number": "第 {{season_number}} 季", + "number_episodes": "{{episode_number}} 集", + "born": "出生", + "appearances": "出場", + "toasts": { + "jellyseer_does_not_meet_requirements": "Jellyseerr 伺服器不符合最低版本要求!請使用 2.0.0 及以上版本。", + "jellyseerr_test_failed": "Jellyseerr 測試失敗。請再試一次。", + "failed_to_test_jellyseerr_server_url": "無法測試 Jellyseerr 伺服器 URL", + "issue_submitted": "問題已提交!", + "requested_item": "已請求 {{item}}!", + "you_dont_have_permission_to_request": "您無權請求媒體!", + "something_went_wrong_requesting_media": "請求媒體時出了些問題!" + } + }, + "tabs": { + "home": "主頁", + "search": "搜索", + "library": "媒體庫", + "custom_links": "自定義鏈接", + "favorites": "收藏" + } } diff --git a/types.d.ts b/types.d.ts index 8be427bb..684f73c5 100644 --- a/types.d.ts +++ b/types.d.ts @@ -1,9 +1,9 @@ declare module "*.svg" { - const content: any; - export default content; + const content: any; + export default content; } declare module "*.png" { - const value: any; - export default value; + const value: any; + export default value; } diff --git a/utils/OrientationLockConverter.ts b/utils/OrientationLockConverter.ts index 498e01cb..29d6c802 100644 --- a/utils/OrientationLockConverter.ts +++ b/utils/OrientationLockConverter.ts @@ -4,7 +4,7 @@ import { } from "@/packages/expo-screen-orientation"; function orientationToOrientationLock( - orientation: Orientation + orientation: Orientation, ): OrientationLock { switch (orientation) { case Orientation.PORTRAIT_UP: diff --git a/utils/_jellyseerr/useJellyseerrCanRequest.ts b/utils/_jellyseerr/useJellyseerrCanRequest.ts index c58a8928..22439b92 100644 --- a/utils/_jellyseerr/useJellyseerrCanRequest.ts +++ b/utils/_jellyseerr/useJellyseerrCanRequest.ts @@ -4,17 +4,20 @@ import { MediaStatus, } from "@/utils/jellyseerr/server/constants/media"; import { - hasPermission, Permission, + hasPermission, } from "@/utils/jellyseerr/server/lib/permissions"; -import { MovieResult, TvResult } from "@/utils/jellyseerr/server/models/Search"; +import type { + MovieResult, + TvResult, +} from "@/utils/jellyseerr/server/models/Search"; import { useMemo } from "react"; -import MediaRequest from "../jellyseerr/server/entity/MediaRequest"; -import { MovieDetails } from "../jellyseerr/server/models/Movie"; -import { TvDetails } from "../jellyseerr/server/models/Tv"; +import type MediaRequest from "../jellyseerr/server/entity/MediaRequest"; +import type { MovieDetails } from "../jellyseerr/server/models/Movie"; +import type { TvDetails } from "../jellyseerr/server/models/Tv"; export const useJellyseerrCanRequest = ( - item?: MovieResult | TvResult | MovieDetails | TvDetails + item?: MovieResult | TvResult | MovieDetails | TvDetails, ) => { const { jellyseerrUser } = useJellyseerr(); @@ -25,7 +28,7 @@ export const useJellyseerrCanRequest = ( item?.mediaInfo?.requests?.some( (r: MediaRequest) => r.status == MediaRequestStatus.PENDING || - r.status == MediaRequestStatus.APPROVED + r.status == MediaRequestStatus.APPROVED, ) || item.mediaInfo?.status === MediaStatus.AVAILABLE || item.mediaInfo?.status === MediaStatus.BLACKLISTED || @@ -42,26 +45,21 @@ export const useJellyseerrCanRequest = ( : Permission.REQUEST_TV, ], jellyseerrUser.permissions, - { type: "or" } + { type: "or" }, ); return userHasPermission && !canNotRequest; }, [item, jellyseerrUser]); const hasAdvancedRequestPermission = useMemo(() => { - if (!jellyseerrUser) return false; + if (!jellyseerrUser) return false; - return hasPermission( - [ - Permission.REQUEST_ADVANCED, - Permission.MANAGE_REQUESTS - ], - jellyseerrUser.permissions, - {type: 'or'} - ) - }, - [jellyseerrUser] - ); + return hasPermission( + [Permission.REQUEST_ADVANCED, Permission.MANAGE_REQUESTS], + jellyseerrUser.permissions, + { type: "or" }, + ); + }, [jellyseerrUser]); return [canRequest, hasAdvancedRequestPermission]; }; diff --git a/utils/atoms/filters.ts b/utils/atoms/filters.ts index e2c9e60c..df9c5c78 100644 --- a/utils/atoms/filters.ts +++ b/utils/atoms/filters.ts @@ -93,7 +93,7 @@ export const sortByPreferenceAtom = atomWithStorage( removeItem: (key) => { storage.delete(key); }, - } + }, ); export const sortOrderPreferenceAtom = atomWithStorage( @@ -110,19 +110,19 @@ export const sortOrderPreferenceAtom = atomWithStorage( removeItem: (key) => { storage.delete(key); }, - } + }, ); export const getSortByPreference = ( libraryId: string, - preferences: SortPreference + preferences: SortPreference, ) => { return preferences?.[libraryId] || null; }; export const getSortOrderPreference = ( libraryId: string, - preferences: SortOrderPreference + preferences: SortOrderPreference, ) => { return preferences?.[libraryId] || null; }; diff --git a/utils/atoms/orientation.ts b/utils/atoms/orientation.ts index 4ee340a2..42f21b3a 100644 --- a/utils/atoms/orientation.ts +++ b/utils/atoms/orientation.ts @@ -2,5 +2,5 @@ import * as ScreenOrientation from "@/packages/expo-screen-orientation"; import { atom } from "jotai"; export const orientationAtom = atom( - ScreenOrientation.OrientationLock.PORTRAIT_UP + ScreenOrientation.OrientationLock.PORTRAIT_UP, ); diff --git a/utils/atoms/primaryColor.ts b/utils/atoms/primaryColor.ts index cd348ca5..b2574262 100644 --- a/utils/atoms/primaryColor.ts +++ b/utils/atoms/primaryColor.ts @@ -7,9 +7,9 @@ interface ThemeColors { export const calculateTextColor = (backgroundColor: string): string => { // Convert hex to RGB - const r = parseInt(backgroundColor.slice(1, 3), 16); - const g = parseInt(backgroundColor.slice(3, 5), 16); - const b = parseInt(backgroundColor.slice(5, 7), 16); + const r = Number.parseInt(backgroundColor.slice(1, 3), 16); + const g = Number.parseInt(backgroundColor.slice(3, 5), 16); + const b = Number.parseInt(backgroundColor.slice(5, 7), 16); // Calculate perceived brightness // Using the formula: (R * 299 + G * 587 + B * 114) / 1000 @@ -47,9 +47,9 @@ const calculateRelativeLuminance = (rgb: number[]): number => { }; export const isCloseToBlack = (color: string): boolean => { - const r = parseInt(color.slice(1, 3), 16); - const g = parseInt(color.slice(3, 5), 16); - const b = parseInt(color.slice(5, 7), 16); + const r = Number.parseInt(color.slice(1, 3), 16); + const g = Number.parseInt(color.slice(3, 5), 16); + const b = Number.parseInt(color.slice(5, 7), 16); // Check if the color is very dark (close to black) return r < 20 && g < 20 && b < 20; diff --git a/utils/atoms/queue.ts b/utils/atoms/queue.ts index 70f85de9..573d964f 100644 --- a/utils/atoms/queue.ts +++ b/utils/atoms/queue.ts @@ -1,9 +1,9 @@ -import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; +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 {JobStatus} from "@/utils/optimize-server"; -import {processesAtom} from "@/providers/DownloadProvider"; -import {useSettings} from "@/utils/atoms/settings"; export interface Job { id: string; @@ -24,7 +24,7 @@ export const queueActions = { processJob: async ( queue: Job[], setQueue: (update: Job[]) => void, - setProcessing: (processing: boolean) => void + setProcessing: (processing: boolean) => void, ) => { const [job, ...rest] = queue; @@ -45,7 +45,7 @@ export const queueActions = { }, clear: ( setQueue: (update: Job[]) => void, - setProcessing: (processing: boolean) => void + setProcessing: (processing: boolean) => void, ) => { setQueue([]); setProcessing(false); @@ -59,7 +59,12 @@ export const useJobProcessor = () => { const [settings] = useSettings(); useEffect(() => { - if (!running && queue.length > 0 && settings && processes.length < settings?.remuxConcurrentLimit) { + if ( + !running && + queue.length > 0 && + settings && + processes.length < settings?.remuxConcurrentLimit + ) { console.info("Processing queue", queue); queueActions.processJob(queue, setQueue, setRunning); } diff --git a/utils/atoms/settings.ts b/utils/atoms/settings.ts index 50046a7e..bd7b9ab2 100644 --- a/utils/atoms/settings.ts +++ b/utils/atoms/settings.ts @@ -1,20 +1,20 @@ +import { BITRATES, type Bitrate } from "@/components/BitrateSelector"; +import * as ScreenOrientation from "@/packages/expo-screen-orientation"; +import { apiAtom } from "@/providers/JellyfinProvider"; +import { Video } from "@/utils/jellyseerr/server/models/Movie"; +import { writeInfoLog } from "@/utils/log"; +import { + type BaseItemKind, + type CultureDto, + type ItemFilter, + type ItemSortBy, + type SortOrder, + SubtitlePlaybackMode, +} from "@jellyfin/sdk/lib/generated-client"; import { atom, useAtom, useAtomValue } from "jotai"; import { useCallback, useEffect, useMemo } from "react"; -import * as ScreenOrientation from "@/packages/expo-screen-orientation"; -import { storage } from "../mmkv"; import { Platform } from "react-native"; -import { - CultureDto, - SubtitlePlaybackMode, - ItemSortBy, - SortOrder, - BaseItemKind, - ItemFilter, -} from "@jellyfin/sdk/lib/generated-client"; -import { Bitrate, BITRATES } from "@/components/BitrateSelector"; -import { apiAtom } from "@/providers/JellyfinProvider"; -import { writeInfoLog } from "@/utils/log"; -import { Video } from "@/utils/jellyseerr/server/models/Movie"; +import { storage } from "../mmkv"; const STREAMYFIN_PLUGIN_ID = "1e9e5d386e6746158719e98a5c34f004"; const STREAMYFIN_PLUGIN_SETTINGS = "STREAMYFIN_PLUGIN_SETTINGS"; @@ -26,17 +26,30 @@ export type DownloadOption = { value: DownloadQuality; }; -export const ScreenOrientationEnum: Record = { - [ScreenOrientation.OrientationLock.DEFAULT]: "home.settings.other.orientations.DEFAULT", - [ScreenOrientation.OrientationLock.ALL]: "home.settings.other.orientations.ALL", - [ScreenOrientation.OrientationLock.PORTRAIT]: "home.settings.other.orientations.PORTRAIT", - [ScreenOrientation.OrientationLock.PORTRAIT_UP]: "home.settings.other.orientations.PORTRAIT_UP", - [ScreenOrientation.OrientationLock.PORTRAIT_DOWN]: "home.settings.other.orientations.PORTRAIT_DOWN", - [ScreenOrientation.OrientationLock.LANDSCAPE]: "home.settings.other.orientations.LANDSCAPE", - [ScreenOrientation.OrientationLock.LANDSCAPE_LEFT]: "home.settings.other.orientations.LANDSCAPE_LEFT", - [ScreenOrientation.OrientationLock.LANDSCAPE_RIGHT]: "home.settings.other.orientations.LANDSCAPE_RIGHT", - [ScreenOrientation.OrientationLock.OTHER]: "home.settings.other.orientations.OTHER", - [ScreenOrientation.OrientationLock.UNKNOWN]: "home.settings.other.orientations.UNKNOWN", +export const ScreenOrientationEnum: Record< + ScreenOrientation.OrientationLock, + string +> = { + [ScreenOrientation.OrientationLock.DEFAULT]: + "home.settings.other.orientations.DEFAULT", + [ScreenOrientation.OrientationLock.ALL]: + "home.settings.other.orientations.ALL", + [ScreenOrientation.OrientationLock.PORTRAIT]: + "home.settings.other.orientations.PORTRAIT", + [ScreenOrientation.OrientationLock.PORTRAIT_UP]: + "home.settings.other.orientations.PORTRAIT_UP", + [ScreenOrientation.OrientationLock.PORTRAIT_DOWN]: + "home.settings.other.orientations.PORTRAIT_DOWN", + [ScreenOrientation.OrientationLock.LANDSCAPE]: + "home.settings.other.orientations.LANDSCAPE", + [ScreenOrientation.OrientationLock.LANDSCAPE_LEFT]: + "home.settings.other.orientations.LANDSCAPE_LEFT", + [ScreenOrientation.OrientationLock.LANDSCAPE_RIGHT]: + "home.settings.other.orientations.LANDSCAPE_RIGHT", + [ScreenOrientation.OrientationLock.OTHER]: + "home.settings.other.orientations.OTHER", + [ScreenOrientation.OrientationLock.UNKNOWN]: + "home.settings.other.orientations.UNKNOWN", }; export const DownloadOptions: DownloadOption[] = [ @@ -102,8 +115,8 @@ export type HomeSectionNextUpResolver = { export enum VideoPlayer { // NATIVE, //todo: changes will make this a lot more easier to implement if we want. delete if not wanted - VLC_3, - VLC_4, + VLC_3 = 0, + VLC_4 = 1, } export type Settings = { @@ -201,7 +214,8 @@ const defaultValues: Settings = { const loadSettings = (): Partial => { try { const jsonValue = storage.getString("settings"); - const loadedValues: Partial = jsonValue != null ? JSON.parse(jsonValue) : {}; + const loadedValues: Partial = + jsonValue != null ? JSON.parse(jsonValue) : {}; return loadedValues; } catch (error) { @@ -223,7 +237,9 @@ const saveSettings = (settings: Settings) => { }; export const settingsAtom = atom | null>(null); -export const pluginSettingsAtom = atom(storage.get(STREAMYFIN_PLUGIN_SETTINGS)); +export const pluginSettingsAtom = atom( + storage.get(STREAMYFIN_PLUGIN_SETTINGS), +); export const useSettings = () => { const api = useAtomValue(apiAtom); @@ -242,7 +258,7 @@ export const useSettings = () => { storage.setAny(STREAMYFIN_PLUGIN_SETTINGS, settings); _setPluginSettings(settings); }, - [_setPluginSettings] + [_setPluginSettings], ); const refreshStreamyfinPluginSettings = useCallback(async () => { @@ -252,7 +268,7 @@ export const useSettings = () => { writeInfoLog(`Got remote settings: ${data?.settings}`); return data?.settings; }, - (err) => undefined + (err) => undefined, ); setPluginSettings(settings); return settings; @@ -260,11 +276,17 @@ export const useSettings = () => { const updateSettings = (update: Partial) => { if (!_settings) return; - const hasChanges = Object.entries(update).some(([key, value]) => _settings[key as keyof Settings] !== value); + const hasChanges = Object.entries(update).some( + ([key, value]) => _settings[key as keyof Settings] !== value, + ); if (hasChanges) { // Merge default settings, current settings, and updates to ensure all required properties exist - const newSettings = { ...defaultValues, ..._settings, ...update } as Settings; + const newSettings = { + ...defaultValues, + ..._settings, + ...update, + } as Settings; setSettings(newSettings); saveSettings(newSettings); } @@ -275,24 +297,33 @@ export const useSettings = () => { // use user settings first and fallback on admin setting if required. const settings: Settings = useMemo(() => { let unlockedPluginDefaults = {} as Settings; - const overrideSettings = Object.entries(pluginSettings || {}).reduce((acc, [key, setting]) => { - if (setting) { - const { value, locked } = setting; + const overrideSettings = Object.entries(pluginSettings || {}).reduce( + (acc, [key, setting]) => { + if (setting) { + const { value, locked } = setting; - // Make sure we override default settings with plugin settings when they are not locked. - // Admin decided what users defaults should be and grants them the ability to change them too. - if (locked === false && value && _settings?.[key as keyof Settings] !== value) { - unlockedPluginDefaults = Object.assign(unlockedPluginDefaults, { - [key as keyof Settings]: value, + // Make sure we override default settings with plugin settings when they are not locked. + // Admin decided what users defaults should be and grants them the ability to change them too. + if ( + locked === false && + value && + _settings?.[key as keyof Settings] !== value + ) { + unlockedPluginDefaults = Object.assign(unlockedPluginDefaults, { + [key as keyof Settings]: value, + }); + } + + acc = Object.assign(acc, { + [key]: locked + ? value + : (_settings?.[key as keyof Settings] ?? value), }); } - - acc = Object.assign(acc, { - [key]: locked ? value : _settings?.[key as keyof Settings] ?? value, - }); - } - return acc; - }, {} as Settings); + return acc; + }, + {} as Settings, + ); return { ...defaultValues, @@ -301,5 +332,11 @@ export const useSettings = () => { }; }, [_settings, pluginSettings]); - return [settings, updateSettings, pluginSettings, setPluginSettings, refreshStreamyfinPluginSettings] as const; + return [ + settings, + updateSettings, + pluginSettings, + setPluginSettings, + refreshStreamyfinPluginSettings, + ] as const; }; diff --git a/utils/background-tasks.ts b/utils/background-tasks.ts index 6f92d83f..eb01c2c0 100644 --- a/utils/background-tasks.ts +++ b/utils/background-tasks.ts @@ -25,8 +25,7 @@ export async function unregisterBackgroundFetchAsync() { } } -export const BACKGROUND_FETCH_TASK_SESSIONS = - "background-fetch-sessions"; +export const BACKGROUND_FETCH_TASK_SESSIONS = "background-fetch-sessions"; export async function registerBackgroundFetchAsyncSessions() { try { @@ -47,4 +46,4 @@ export async function unregisterBackgroundFetchAsyncSessions() { } catch (error) { console.log("Error unregistering background fetch task", error); } -} \ No newline at end of file +} diff --git a/utils/bitrate.ts b/utils/bitrate.ts index 7f1d0f47..1bd4db6d 100644 --- a/utils/bitrate.ts +++ b/utils/bitrate.ts @@ -3,6 +3,8 @@ export const formatBitrate = (bitrate?: number | null) => { const sizes = ["bps", "Kbps", "Mbps", "Gbps", "Tbps"]; if (bitrate === 0) return "0 bps"; - const i = parseInt(Math.floor(Math.log(bitrate) / Math.log(1000)).toString()); + const i = Number.parseInt( + Math.floor(Math.log(bitrate) / Math.log(1000)).toString(), + ); return Math.round((bitrate / Math.pow(1000, i)) * 100) / 100 + " " + sizes[i]; }; diff --git a/utils/collectionTypeToItemType.ts b/utils/collectionTypeToItemType.ts index f37fe5f4..b4f1d58e 100644 --- a/utils/collectionTypeToItemType.ts +++ b/utils/collectionTypeToItemType.ts @@ -20,7 +20,7 @@ import { readonly Folders: "folders"; */ export const colletionTypeToItemType = ( - collectionType?: CollectionType | null + collectionType?: CollectionType | null, ): BaseItemKind | undefined => { if (!collectionType) return undefined; diff --git a/utils/device.ts b/utils/device.ts index 29a988a5..d49ffc67 100644 --- a/utils/device.ts +++ b/utils/device.ts @@ -13,7 +13,7 @@ export const getOrSetDeviceId = () => { }; export const getDeviceId = () => { - let deviceId = storage.getString("deviceId"); + const deviceId = storage.getString("deviceId"); return deviceId || null; }; diff --git a/utils/download.ts b/utils/download.ts index 94a06b7a..547aa15a 100644 --- a/utils/download.ts +++ b/utils/download.ts @@ -2,7 +2,7 @@ import useImageStorage from "@/hooks/useImageStorage"; import { apiAtom } from "@/providers/JellyfinProvider"; import { getPrimaryImageUrlById } from "@/utils/jellyfin/image/getPrimaryImageUrlById"; import { storage } from "@/utils/mmkv"; -import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client"; +import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client"; import { useAtom } from "jotai"; const useDownloadHelper = () => { @@ -19,7 +19,7 @@ const useDownloadHelper = () => { console.log(`Saving primary image for series: ${item.SeriesId}`); await saveImage( item.SeriesId, - getPrimaryImageUrlById({ api, id: item.SeriesId }) + getPrimaryImageUrlById({ api, id: item.SeriesId }), ); console.log(`Primary image saved for series: ${item.SeriesId}`); } else { diff --git a/utils/eventBus.ts b/utils/eventBus.ts index 4df6b19f..e4bd0fc1 100644 --- a/utils/eventBus.ts +++ b/utils/eventBus.ts @@ -14,7 +14,7 @@ class EventBus { off(event: string, callback: Listener): void { if (!this.listeners[event]) return; this.listeners[event] = this.listeners[event].filter( - (fn) => fn !== callback + (fn) => fn !== callback, ); } diff --git a/utils/getItemImage.ts b/utils/getItemImage.ts index d106b0cf..747beec8 100644 --- a/utils/getItemImage.ts +++ b/utils/getItemImage.ts @@ -1,6 +1,6 @@ -import { Api } from "@jellyfin/sdk"; -import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; -import { ImageSource } from "expo-image"; +import type { Api } from "@jellyfin/sdk"; +import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; +import type { ImageSource } from "expo-image"; interface Props { item: BaseItemDto; diff --git a/utils/hls/parseM3U8ForSubtitles.ts b/utils/hls/parseM3U8ForSubtitles.ts index fb1902b0..ce962100 100644 --- a/utils/hls/parseM3U8ForSubtitles.ts +++ b/utils/hls/parseM3U8ForSubtitles.ts @@ -11,7 +11,7 @@ export interface SubtitleTrack { } export async function parseM3U8ForSubtitles( - url: string + url: string, ): Promise { try { const response = await axios.get(url, { responseType: "text" }); diff --git a/utils/jellyfin/getDefaultPlaySettings.ts b/utils/jellyfin/getDefaultPlaySettings.ts index 068d6058..2c32b615 100644 --- a/utils/jellyfin/getDefaultPlaySettings.ts +++ b/utils/jellyfin/getDefaultPlaySettings.ts @@ -1,10 +1,10 @@ // utils/getDefaultPlaySettings.ts import { BITRATES } from "@/components/BitrateSelector"; -import { +import type { BaseItemDto, MediaSourceInfo, } from "@jellyfin/sdk/lib/generated-client"; -import { Settings, useSettings } from "../atoms/settings"; +import { type Settings, useSettings } from "../atoms/settings"; import { AudioStreamRanker, StreamRanker, @@ -34,7 +34,7 @@ export function getDefaultPlaySettings( item: BaseItemDto, settings: Settings, previousIndexes?: previousIndexes, - previousSource?: MediaSourceInfo + previousSource?: MediaSourceInfo, ): PlaySettings { if (item.Type === "Program") { return { @@ -53,14 +53,14 @@ export function getDefaultPlaySettings( // 2. Get default or preferred audio const defaultAudioIndex = mediaSource?.DefaultAudioStreamIndex; const preferedAudioIndex = mediaSource?.MediaStreams?.find( - (x) => x.Type === "Audio" && x.Language === settings?.defaultAudioLanguage + (x) => x.Type === "Audio" && x.Language === settings?.defaultAudioLanguage, )?.Index; const firstAudioIndex = mediaSource?.MediaStreams?.find( - (x) => x.Type === "Audio" + (x) => x.Type === "Audio", )?.Index; // We prefer the previous track over the default track. - let trackOptions: TrackOptions = { + const trackOptions: TrackOptions = { DefaultAudioStreamIndex: defaultAudioIndex ?? -1, DefaultSubtitleStreamIndex: mediaSource?.DefaultSubtitleStreamIndex ?? -1, }; @@ -74,7 +74,7 @@ export function getDefaultPlaySettings( previousIndexes.subtitleIndex, previousSource, mediaStreams, - trackOptions + trackOptions, ); } } @@ -87,7 +87,7 @@ export function getDefaultPlaySettings( previousIndexes.audioIndex, previousSource, mediaStreams, - trackOptions + trackOptions, ); } } diff --git a/utils/jellyfin/image/getBackdropUrl.ts b/utils/jellyfin/image/getBackdropUrl.ts index dd138aab..eb55e9d4 100644 --- a/utils/jellyfin/image/getBackdropUrl.ts +++ b/utils/jellyfin/image/getBackdropUrl.ts @@ -1,5 +1,5 @@ -import { Api } from "@jellyfin/sdk"; -import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; +import type { Api } from "@jellyfin/sdk"; +import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; import { getPrimaryImageUrl } from "./getPrimaryImageUrl"; /** diff --git a/utils/jellyfin/image/getLogoImageUrlById.ts b/utils/jellyfin/image/getLogoImageUrlById.ts index 3712b888..97ca3fbe 100644 --- a/utils/jellyfin/image/getLogoImageUrlById.ts +++ b/utils/jellyfin/image/getLogoImageUrlById.ts @@ -1,5 +1,5 @@ -import { Api } from "@jellyfin/sdk"; -import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; +import type { Api } from "@jellyfin/sdk"; +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/getParentBackdropImageUrl.ts b/utils/jellyfin/image/getParentBackdropImageUrl.ts index 4a03795b..024bb045 100644 --- a/utils/jellyfin/image/getParentBackdropImageUrl.ts +++ b/utils/jellyfin/image/getParentBackdropImageUrl.ts @@ -1,6 +1,6 @@ -import { Api } from "@jellyfin/sdk"; +import type { Api } from "@jellyfin/sdk"; import { - BaseItemDto, + type BaseItemDto, BaseItemPerson, } from "@jellyfin/sdk/lib/generated-client/models"; import { isBaseItemDto } from "../jellyfin"; diff --git a/utils/jellyfin/image/getPrimaryImageUrl.ts b/utils/jellyfin/image/getPrimaryImageUrl.ts index cd354308..18124374 100644 --- a/utils/jellyfin/image/getPrimaryImageUrl.ts +++ b/utils/jellyfin/image/getPrimaryImageUrl.ts @@ -1,5 +1,5 @@ -import { Api } from "@jellyfin/sdk"; -import { +import type { Api } from "@jellyfin/sdk"; +import type { BaseItemDto, BaseItemPerson, } from "@jellyfin/sdk/lib/generated-client/models"; diff --git a/utils/jellyfin/image/getPrimaryImageUrlById.ts b/utils/jellyfin/image/getPrimaryImageUrlById.ts index 736d1a0e..e9388233 100644 --- a/utils/jellyfin/image/getPrimaryImageUrlById.ts +++ b/utils/jellyfin/image/getPrimaryImageUrlById.ts @@ -1,4 +1,4 @@ -import { Api } from "@jellyfin/sdk"; +import type { Api } from "@jellyfin/sdk"; /** * Retrieves the primary image URL for a given item. diff --git a/utils/jellyfin/image/getPrimaryParentImageUrl.ts b/utils/jellyfin/image/getPrimaryParentImageUrl.ts index ff862624..9a51edf4 100644 --- a/utils/jellyfin/image/getPrimaryParentImageUrl.ts +++ b/utils/jellyfin/image/getPrimaryParentImageUrl.ts @@ -1,6 +1,6 @@ -import { Api } from "@jellyfin/sdk"; +import type { Api } from "@jellyfin/sdk"; import { - BaseItemDto, + type BaseItemDto, BaseItemPerson, } from "@jellyfin/sdk/lib/generated-client/models"; import { isBaseItemDto } from "../jellyfin"; diff --git a/utils/jellyfin/jellyfin.ts b/utils/jellyfin/jellyfin.ts index 80738422..3db4ba8e 100644 --- a/utils/jellyfin/jellyfin.ts +++ b/utils/jellyfin/jellyfin.ts @@ -1,5 +1,5 @@ -import { Api } from "@jellyfin/sdk"; -import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; +import type { Api } from "@jellyfin/sdk"; +import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; /** * Generates the authorization headers for Jellyfin API requests. diff --git a/utils/jellyfin/media/getPlaybackUrl.ts b/utils/jellyfin/media/getPlaybackUrl.ts index 74257348..0fd23d0c 100644 --- a/utils/jellyfin/media/getPlaybackUrl.ts +++ b/utils/jellyfin/media/getPlaybackUrl.ts @@ -1,4 +1,4 @@ -import { Api } from "@jellyfin/sdk"; +import type { Api } from "@jellyfin/sdk"; import { getMediaInfoApi } from "@jellyfin/sdk/lib/utils/api"; /** diff --git a/utils/jellyfin/media/getStreamUrl.ts b/utils/jellyfin/media/getStreamUrl.ts index 982bb413..18f9357a 100644 --- a/utils/jellyfin/media/getStreamUrl.ts +++ b/utils/jellyfin/media/getStreamUrl.ts @@ -1,6 +1,6 @@ import native from "@/utils/profiles/native"; -import { Api } from "@jellyfin/sdk"; -import { +import type { Api } from "@jellyfin/sdk"; +import type { BaseItemDto, MediaSourceInfo, PlaybackInfoResponse, @@ -63,7 +63,7 @@ export const getStreamUrl = async ({ data: { deviceProfile, }, - } + }, ); const transcodeUrl = res0.data.MediaSources?.[0].TranscodingUrl; sessionId = res0.data.PlaySessionId || null; @@ -95,7 +95,7 @@ export const getStreamUrl = async ({ audioStreamIndex, subtitleStreamIndex, }, - } + }, ); if (res2.status !== 200) { @@ -105,7 +105,7 @@ export const getStreamUrl = async ({ sessionId = res2.data.PlaySessionId || null; mediaSource = res2.data.MediaSources?.find( - (source: MediaSourceInfo) => source.Id === mediaSourceId + (source: MediaSourceInfo) => source.Id === mediaSourceId, ); if (item.MediaType === "Video") { diff --git a/utils/jellyfin/playstate/markAsNotPlayed.ts b/utils/jellyfin/playstate/markAsNotPlayed.ts index c84ceb8a..1478c965 100644 --- a/utils/jellyfin/playstate/markAsNotPlayed.ts +++ b/utils/jellyfin/playstate/markAsNotPlayed.ts @@ -1,5 +1,5 @@ -import { Api } from "@jellyfin/sdk"; -import { AxiosError } from "axios"; +import type { Api } from "@jellyfin/sdk"; +import type { AxiosError } from "axios"; interface MarkAsNotPlayedParams { api: Api | null | undefined; diff --git a/utils/jellyfin/playstate/markAsPlayed.ts b/utils/jellyfin/playstate/markAsPlayed.ts index 9b6ee13d..e17638ec 100644 --- a/utils/jellyfin/playstate/markAsPlayed.ts +++ b/utils/jellyfin/playstate/markAsPlayed.ts @@ -1,5 +1,5 @@ -import { Api } from "@jellyfin/sdk"; -import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; +import type { Api } from "@jellyfin/sdk"; +import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; import { getPlaystateApi } from "@jellyfin/sdk/lib/utils/api"; interface MarkAsPlayedParams { @@ -14,7 +14,11 @@ interface MarkAsPlayedParams { * @param params - The parameters for marking an item as played∏ * @returns A promise that resolves to true if the operation was successful, false otherwise */ -export const markAsPlayed = async ({ api, item, userId }: MarkAsPlayedParams): Promise => { +export const markAsPlayed = async ({ + api, + item, + userId, +}: MarkAsPlayedParams): Promise => { if (!api || !item?.Id || !userId || !item.RunTimeTicks) { console.error("Invalid parameters for markAsPlayed"); return false; diff --git a/utils/jellyfin/playstate/reportPlaybackProgress.ts b/utils/jellyfin/playstate/reportPlaybackProgress.ts index 1342690a..282a9b59 100644 --- a/utils/jellyfin/playstate/reportPlaybackProgress.ts +++ b/utils/jellyfin/playstate/reportPlaybackProgress.ts @@ -1,17 +1,17 @@ -import { Api } from "@jellyfin/sdk"; -import { getAuthHeaders } from "../jellyfin"; -import { postCapabilities } from "../session/capabilities"; -import { Settings } from "@/utils/atoms/settings"; +import { getOrSetDeviceId } from "@/providers/JellyfinProvider"; +import type { Settings } from "@/utils/atoms/settings"; +import ios from "@/utils/profiles/ios"; +import native from "@/utils/profiles/native"; +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 { DeviceProfile } from "@jellyfin/sdk/lib/generated-client"; -import { getOrSetDeviceId } from "@/providers/JellyfinProvider"; -import ios from "@/utils/profiles/ios"; -import native from "@/utils/profiles/native"; -import old from "@/utils/profiles/old"; +import { getAuthHeaders } from "../jellyfin"; +import { postCapabilities } from "../session/capabilities"; interface ReportPlaybackProgressParams { api?: Api | null; diff --git a/utils/jellyfin/session/capabilities.ts b/utils/jellyfin/session/capabilities.ts index c0f3b295..c1b53906 100644 --- a/utils/jellyfin/session/capabilities.ts +++ b/utils/jellyfin/session/capabilities.ts @@ -1,7 +1,7 @@ -import { Settings } from "@/utils/atoms/settings"; +import type { Settings } from "@/utils/atoms/settings"; import native from "@/utils/profiles/native"; -import { Api } from "@jellyfin/sdk"; -import { AxiosError, AxiosResponse } from "axios"; +import type { Api } from "@jellyfin/sdk"; +import type { AxiosError, AxiosResponse } from "axios"; import { getAuthHeaders } from "../jellyfin"; interface PostCapabilitiesParams { @@ -47,7 +47,7 @@ export const postCapabilities = async ({ }, { headers: getAuthHeaders(api), - } + }, ); return d; } catch (error: any | AxiosError) { diff --git a/utils/jellyfin/tvshows/nextUp.ts b/utils/jellyfin/tvshows/nextUp.ts index 22468b0f..dd7396d2 100644 --- a/utils/jellyfin/tvshows/nextUp.ts +++ b/utils/jellyfin/tvshows/nextUp.ts @@ -1,5 +1,5 @@ -import { Api } from "@jellyfin/sdk"; -import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; +import type { Api } from "@jellyfin/sdk"; +import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; import { AxiosError } from "axios"; import { getAuthHeaders } from "../jellyfin"; diff --git a/utils/jellyfin/user-library/getItemById.ts b/utils/jellyfin/user-library/getItemById.ts index 79914abc..261733b6 100644 --- a/utils/jellyfin/user-library/getItemById.ts +++ b/utils/jellyfin/user-library/getItemById.ts @@ -1,5 +1,5 @@ -import { Api } from "@jellyfin/sdk"; -import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; +import type { Api } from "@jellyfin/sdk"; +import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; import { getUserLibraryApi } from "@jellyfin/sdk/lib/utils/api"; /** diff --git a/utils/jellyfin/user-library/getUserItemData.ts b/utils/jellyfin/user-library/getUserItemData.ts index 56b0dae0..d74db625 100644 --- a/utils/jellyfin/user-library/getUserItemData.ts +++ b/utils/jellyfin/user-library/getUserItemData.ts @@ -1,5 +1,5 @@ -import { Api } from "@jellyfin/sdk"; -import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; +import type { Api } from "@jellyfin/sdk"; +import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; import { getUserLibraryApi } from "@jellyfin/sdk/lib/utils/api"; /** diff --git a/utils/log.tsx b/utils/log.tsx index 45999062..3344b9ff 100644 --- a/utils/log.tsx +++ b/utils/log.tsx @@ -1,7 +1,8 @@ -import { atomWithStorage, createJSONStorage } from "jotai/utils"; -import { storage } from "./mmkv"; import { useQuery } from "@tanstack/react-query"; -import React, { createContext, useContext } from "react"; +import { atomWithStorage, createJSONStorage } from "jotai/utils"; +import type React from "react"; +import { createContext, useContext } from "react"; +import { storage } from "./mmkv"; type LogLevel = "INFO" | "WARN" | "ERROR"; @@ -20,10 +21,10 @@ const mmkvStorage = createJSONStorage(() => ({ const logsAtom = atomWithStorage("logs", [], mmkvStorage); const LogContext = createContext | null>( - null + null, ); const DownloadContext = createContext | null>( - null + null, ); function useLogProvider() { diff --git a/utils/optimize-server.ts b/utils/optimize-server.ts index 61d17a9a..70e10419 100644 --- a/utils/optimize-server.ts +++ b/utils/optimize-server.ts @@ -1,12 +1,12 @@ import { itemRouter } from "@/components/common/TouchableItemRouter"; -import { +import { DownloadedItem } from "@/providers/DownloadProvider"; +import type { BaseItemDto, MediaSourceInfo, } from "@jellyfin/sdk/lib/generated-client"; import axios from "axios"; -import { writeToLog } from "./log"; -import { DownloadedItem } from "@/providers/DownloadProvider"; import { MMKV } from "react-native-mmkv"; +import { writeToLog } from "./log"; interface IJobInput { deviceId?: string | null; @@ -63,7 +63,7 @@ export async function getAllJobsByDeviceId({ console.error( statusResponse.status, statusResponse.data, - statusResponse.statusText + statusResponse.statusText, ); throw new Error("Failed to fetch job status"); } @@ -172,7 +172,7 @@ export async function getStatistics({ export function saveDownloadItemInfoToDiskTmp( item: BaseItemDto, mediaSource: MediaSourceInfo, - url: string + url: string, ): boolean { try { const storage = new MMKV(); diff --git a/utils/profiles/chromecast.ts b/utils/profiles/chromecast.ts index 5199dfa7..3844e699 100644 --- a/utils/profiles/chromecast.ts +++ b/utils/profiles/chromecast.ts @@ -1,4 +1,4 @@ -import { DeviceProfile } from "@jellyfin/sdk/lib/generated-client/models"; +import type { DeviceProfile } from "@jellyfin/sdk/lib/generated-client/models"; export const chromecast: DeviceProfile = { Name: "Chromecast Video Profile", diff --git a/utils/profiles/chromecasth265.ts b/utils/profiles/chromecasth265.ts index e74827b2..42bb1712 100644 --- a/utils/profiles/chromecasth265.ts +++ b/utils/profiles/chromecasth265.ts @@ -1,4 +1,4 @@ -import { DeviceProfile } from "@jellyfin/sdk/lib/generated-client/models"; +import type { DeviceProfile } from "@jellyfin/sdk/lib/generated-client/models"; export const chromecasth265: DeviceProfile = { Name: "Chromecast Video Profile", diff --git a/utils/store.ts b/utils/store.ts index 09a7aa5b..2ff6e1b4 100644 --- a/utils/store.ts +++ b/utils/store.ts @@ -1,3 +1,3 @@ -import { createStore } from 'jotai'; +import { createStore } from "jotai"; export const store = createStore(); diff --git a/utils/streamRanker.ts b/utils/streamRanker.ts index 665e57be..7e958435 100644 --- a/utils/streamRanker.ts +++ b/utils/streamRanker.ts @@ -1,4 +1,4 @@ -import { +import type { MediaSourceInfo, MediaStream, } from "@jellyfin/sdk/lib/generated-client"; @@ -10,14 +10,14 @@ abstract class StreamRankerStrategy { prevIndex: number, prevSource: MediaSourceInfo, mediaStreams: MediaStream[], - trackOptions: any + trackOptions: any, ): void; protected rank( prevIndex: number, prevSource: MediaSourceInfo, mediaStreams: MediaStream[], - trackOptions: any + trackOptions: any, ): void { if (prevIndex == -1) { console.debug(`AutoSet Subtitle - No Stream Set`); @@ -41,7 +41,7 @@ abstract class StreamRankerStrategy { } console.debug( - `AutoSet ${this.streamType} - Previous was ${prevStream.Index} - ${prevStream.DisplayTitle}` + `AutoSet ${this.streamType} - Previous was ${prevStream.Index} - ${prevStream.DisplayTitle}`, ); let prevRelIndex = 0; @@ -74,7 +74,7 @@ abstract class StreamRankerStrategy { score += 2; console.debug( - `AutoSet ${this.streamType} - Score ${score} for ${stream.Index} - ${stream.DisplayTitle}` + `AutoSet ${this.streamType} - Score ${score} for ${stream.Index} - ${stream.DisplayTitle}`, ); if (score > bestStreamScore && score >= 3) { bestStreamScore = score; @@ -86,12 +86,12 @@ abstract class StreamRankerStrategy { if (bestStreamIndex != null) { console.debug( - `AutoSet ${this.streamType} - Using ${bestStreamIndex} score ${bestStreamScore}.` + `AutoSet ${this.streamType} - Using ${bestStreamIndex} score ${bestStreamScore}.`, ); trackOptions[`Default${this.streamType}StreamIndex`] = bestStreamIndex; } else { console.debug( - `AutoSet ${this.streamType} - Threshold not met. Using default.` + `AutoSet ${this.streamType} - Threshold not met. Using default.`, ); } } @@ -104,7 +104,7 @@ class SubtitleStreamRanker extends StreamRankerStrategy { prevIndex: number, prevSource: MediaSourceInfo, mediaStreams: MediaStream[], - trackOptions: any + trackOptions: any, ): void { super.rank(prevIndex, prevSource, mediaStreams, trackOptions); } @@ -117,7 +117,7 @@ class AudioStreamRanker extends StreamRankerStrategy { prevIndex: number, prevSource: MediaSourceInfo, mediaStreams: MediaStream[], - trackOptions: any + trackOptions: any, ): void { super.rank(prevIndex, prevSource, mediaStreams, trackOptions); } @@ -138,7 +138,7 @@ class StreamRanker { prevIndex: number, prevSource: MediaSourceInfo, mediaStreams: MediaStream[], - trackOptions: any + trackOptions: any, ) { this.strategy.rankStream(prevIndex, prevSource, mediaStreams, trackOptions); } diff --git a/utils/textTools.ts b/utils/textTools.ts index ce12b61b..9472765c 100644 --- a/utils/textTools.ts +++ b/utils/textTools.ts @@ -1,7 +1,7 @@ /* * Truncate a text longer than a certain length */ -export const tc = (text: string | null | undefined, length: number = 20) => { +export const tc = (text: string | null | undefined, length = 20) => { if (!text) return ""; return text.length > length ? text.substr(0, length) + "..." : text; }; diff --git a/utils/time.ts b/utils/time.ts index 24baf21f..76e02077 100644 --- a/utils/time.ts +++ b/utils/time.ts @@ -6,7 +6,7 @@ * @returns A string formatted as "Xh Ym" where X is hours and Y is minutes. */ export const runtimeTicksToMinutes = ( - ticks: number | null | undefined + ticks: number | null | undefined, ): string => { if (!ticks) return "0h 0m"; @@ -21,7 +21,7 @@ export const runtimeTicksToMinutes = ( }; export const runtimeTicksToSeconds = ( - ticks: number | null | undefined + ticks: number | null | undefined, ): string => { if (!ticks) return "0h 0m"; @@ -39,7 +39,7 @@ export const runtimeTicksToSeconds = ( // t: ms export const formatTimeString = ( t: number | null | undefined, - unit: "s" | "ms" | "tick" = "ms" + unit: "s" | "ms" | "tick" = "ms", ): string => { if (t === null || t === undefined) return "0:00"; diff --git a/utils/useReactNavigationQuery.ts b/utils/useReactNavigationQuery.ts index a0c5b307..1cbe40e8 100644 --- a/utils/useReactNavigationQuery.ts +++ b/utils/useReactNavigationQuery.ts @@ -1,9 +1,9 @@ import { useFocusEffect } from "@react-navigation/core"; import { - QueryKey, + type QueryKey, + type UseQueryOptions, + type UseQueryResult, useQuery, - UseQueryOptions, - UseQueryResult, } from "@tanstack/react-query"; import { useCallback } from "react"; @@ -11,9 +11,9 @@ export function useReactNavigationQuery< TQueryFnData = unknown, TError = unknown, TData = TQueryFnData, - TQueryKey extends QueryKey = QueryKey + TQueryKey extends QueryKey = QueryKey, >( - options: UseQueryOptions + options: UseQueryOptions, ): UseQueryResult { const useQueryReturn = useQuery(options); @@ -25,7 +25,7 @@ export function useReactNavigationQuery< options.enabled !== false ) useQueryReturn.refetch(); - }, [options.enabled, options.refetchOnWindowFocus]) + }, [options.enabled, options.refetchOnWindowFocus]), ); return useQueryReturn;