diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..56dea966 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +.modules/vlc-player/Frameworks/*.xcframework filter=lfs diff=lfs merge=lfs -text diff --git a/.gitignore b/.gitignore index d7428e98..6dc107d8 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,8 @@ npm-debug.* *.orig.* web-build/ modules/vlc-player/android/build +modules/vlc-player/android/.gradle +bun.lockb # macOS .DS_Store @@ -42,4 +44,6 @@ credentials.json .vscode/ .idea/ .ruby-lsp -modules/hls-downloader/android/build \ No newline at end of file +modules/hls-downloader/android/build +streamyfin-4fec1-firebase-adminsdk.json +.env \ No newline at end of file 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 22480b68..b200b485 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,15 +1,24 @@ { - "editor.defaultFormatter": "esbenp.prettier-vscode", - "editor.formatOnSave": true, "[javascript]": { - "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.defaultFormatter": "biomejs.biome", "editor.formatOnSave": true }, "[typescriptreact]": { - "editor.defaultFormatter": "esbenp.prettier-vscode", + "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/Makefile b/Makefile new file mode 100644 index 00000000..c2f6701a --- /dev/null +++ b/Makefile @@ -0,0 +1,6 @@ +e2e: + maestro start-device --platform android + maestro test login.yaml + +e2e-setup: +curl -fsSL "https://get.maestro.mobile.dev" | bash diff --git a/app.config.js b/app.config.js index 30ae0d5b..b67ee80f 100644 --- a/app.config.js +++ b/app.config.js @@ -1,11 +1,14 @@ module.exports = ({ config }) => { - if (process.env.EXPO_TV != "1") { + if (process.env.EXPO_TV !== "1") { config.plugins.push([ "react-native-google-cast", { useDefaultExpandedMediaControls: true }, ]); } return { + android: { + googleServicesFile: process.env.GOOGLE_SERVICES_JSON, + }, ...config, }; }; diff --git a/app.json b/app.json index 140a7f9a..8f10deb3 100644 --- a/app.json +++ b/app.json @@ -31,16 +31,18 @@ }, "android": { "jsEngine": "hermes", - "versionCode": 53, + "versionCode": 54, "adaptiveIcon": { - "foregroundImage": "./assets/images/adaptive_icon.png" + "foregroundImage": "./assets/images/adaptive_icon.png", + "backgroundColor": "#464646" }, "package": "com.fredrikburmester.streamyfin", "permissions": [ "android.permission.FOREGROUND_SERVICE", "android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK", "android.permission.WRITE_SETTINGS" - ] + ], + "googleServicesFile": "./google-services.json" }, "plugins": [ "@react-native-tvos/config-tv", @@ -118,6 +120,13 @@ "image": "./assets/images/StreamyFinFinal.png", "imageWidth": 100 } + ], + [ + "expo-notifications", + { + "icon": "./assets/images/notification.png", + "color": "#9333EA" + } ] ], "experiments": { @@ -131,7 +140,7 @@ "projectId": "e79219d1-797f-4fbe-9fa1-cfd360690a68" } }, - "owner": "fredrikburmester", + "owner": "streamyfin", "runtimeVersion": { "policy": "appVersion" }, 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 0b9b9c11..0d533ac9 100644 --- a/app/(auth)/(tabs)/(home)/_layout.tsx +++ b/app/(auth)/(tabs)/(home)/_layout.tsx @@ -1,17 +1,22 @@ import { nestedTabPageScreenOptions } from "@/components/stacks/NestedTabPageStack"; -import { 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"; -const Chromecast = !Platform.isTV ? require("@/components/Chromecast") : null; +import { Platform, TouchableOpacity, View } from "react-native"; +const Chromecast = Platform.isTV ? null : require("@/components/Chromecast"); +import { useSessions, type useSessionsProps } from "@/hooks/useSessions"; +import { userAtom } from "@/providers/JellyfinProvider"; +import { useAtom } from "jotai"; export default function IndexLayout() { const router = useRouter(); + const [user] = useAtom(userAtom); const { t } = useTranslation(); + return ( ( - + {!Platform.isTV && ( <> - { - router.push("/(auth)/settings"); - }} - > - - + {user && user.Policy?.IsAdministrator && } + )} @@ -41,55 +41,61 @@ export default function IndexLayout() { }} /> + ))} ); } + +const SettingsButton = () => { + const router = useRouter(); + + return ( + { + router.push("/(auth)/settings"); + }} + > + + + ); +}; + +const SessionsButton = () => { + const router = useRouter(); + const { sessions = [] } = useSessions({} as useSessionsProps); + + return ( + { + 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)/index.tsx b/app/(auth)/(tabs)/(home)/index.tsx index 89d03d0c..dc04e43b 100644 --- a/app/(auth)/(tabs)/(home)/index.tsx +++ b/app/(auth)/(tabs)/(home)/index.tsx @@ -1,5 +1,5 @@ -import { SettingsIndex } from "@/components/settings/SettingsIndex"; +import { HomeIndex } from "@/components/settings/HomeIndex"; export default function page() { - return ; + return ; } 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 new file mode 100644 index 00000000..590d89db --- /dev/null +++ b/app/(auth)/(tabs)/(home)/sessions/index.tsx @@ -0,0 +1,384 @@ +import { Badge } from "@/components/Badge"; +import { Loader } from "@/components/Loader"; +import { Text } from "@/components/common/Text"; +import Poster from "@/components/posters/Poster"; +import { useInterval } from "@/hooks/useInterval"; +import { useSessions, type useSessionsProps } from "@/hooks/useSessions"; +import { apiAtom } from "@/providers/JellyfinProvider"; +import { formatBitrate } from "@/utils/bitrate"; +import { getPrimaryImageUrl } from "@/utils/jellyfin/image/getPrimaryImageUrl"; +import { formatTimeString } from "@/utils/time"; +import { + AntDesign, + Entypo, + Ionicons, + MaterialCommunityIcons, +} from "@expo/vector-icons"; +import { + 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"; + +export default function page() { + const { sessions, isLoading } = useSessions({} as useSessionsProps); + const { t } = useTranslation(); + + if (isLoading) + return ( + + + + ); + + if (!sessions || sessions.length == 0) + return ( + + + {t("home.sessions.no_active_sessions")} + + + ); + + return ( + } + keyExtractor={(item) => item.Id || ""} + estimatedItemSize={200} + /> + ); +} + +interface SessionCardProps { + session: SessionInfoDto; +} + +const SessionCard = ({ session }: SessionCardProps) => { + const api = useAtomValue(apiAtom); + const [remainingTicks, setRemainingTicks] = useState(0); + + const tick = () => { + if (session.PlayState?.IsPaused) return; + setRemainingTicks(remainingTicks - 10000000); + }; + + const getProgressPercentage = () => { + if (!session.NowPlayingItem || !session.NowPlayingItem.RunTimeTicks) { + return 0; + } + + return Math.round( + (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 + ) { + const remainingTimeTicks = duration - currentTime; + setRemainingTicks(remainingTimeTicks); + } + }, [session]); + + const { data: ipInfo } = useQuery({ + queryKey: ["ipinfo", session.RemoteEndPoint], + cacheTime: Number.POSITIVE_INFINITY, + queryFn: async () => { + const resp = await api.axiosInstance.get( + `https://freeipapi.com/api/json/${session.RemoteEndPoint}`, + ); + return resp.data; + }, + }); + + useInterval(tick, 1000); + + return ( + + + + + + + + + {session.NowPlayingItem?.Type === "Episode" ? ( + <> + + {session.NowPlayingItem?.Name} + + + {`S${session.NowPlayingItem.ParentIndexNumber?.toString()}:E${session.NowPlayingItem.IndexNumber?.toString()}`} + {" - "} + {session.NowPlayingItem.SeriesName} + + + ) : ( + <> + + {session.NowPlayingItem?.Name} + + + {session.NowPlayingItem?.ProductionYear} + + + {session.NowPlayingItem?.SeriesName} + + + )} + + + {session.UserName} + {"\n"} + {session.Client} + {"\n"} + {session.DeviceName} + {"\n"} + {ipInfo?.cityName} {ipInfo?.countryCode} + + + + + + + {!session.PlayState?.IsPaused ? ( + + ) : ( + + )} + + + {formatTimeString(remainingTicks, "tick")} left + + + + + + + + + + + ); +}; + +interface TranscodingBadgesProps { + properties: StreamProps; +} + +const TranscodingBadges = ({ properties }: TranscodingBadgesProps) => { + const iconMap = { + bitrate: , + codec: , + videoRange: ( + + ), + resolution: , + language: , + audioChannels: , + hwType: , + } as const; + + const icon = (val: string) => { + return ( + iconMap[val as keyof typeof iconMap] ?? ( + + ) + ); + }; + + const formatVal = (key: string, val: any) => { + switch (key) { + case "bitrate": + return formatBitrate(val); + case "hwType": + return val === HardwareAccelerationType.None ? "sw" : "hw"; + default: + return val; + } + }; + + return Object.entries(properties) + .filter(([_, value]) => value !== undefined && value !== null) + .map(([key]) => ( + + )); +}; + +interface StreamProps { + hwType?: HardwareAccelerationType | null | undefined; + resolution?: string | null | undefined; + language?: string | null | undefined; + codec?: string | null | undefined; + bitrate?: number | null | undefined; + videoRange?: string | null | undefined; + audioChannels?: string | null | undefined; +} + +interface TranscodingStreamViewProps { + title: string | undefined; + value?: string; + isTranscoding: boolean; + transcodeValue?: string | undefined | null; + properties: StreamProps; + transcodeProperties?: StreamProps; +} + +const TranscodingStreamView = ({ + title, + isTranscoding, + properties, + transcodeProperties, + value, + transcodeValue, +}: TranscodingStreamViewProps) => { + return ( + + + + {title} + + + + + + {isTranscoding && transcodeProperties ? ( + <> + + + + + + + + + + ) : null} + + ); +}; + +const TranscodingView = ({ session }: SessionCardProps) => { + const videoStream = useMemo(() => { + 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; + }, [session.PlayState?.AudioStreamIndex]); + + const subtitleStream = useMemo(() => { + const index = session.PlayState?.SubtitleStreamIndex; + return index !== null && index !== undefined + ? session.NowPlayingItem?.MediaStreams?.[index] + : undefined; + }, [session.PlayState?.SubtitleStreamIndex]); + + const isTranscoding = useMemo(() => { + return ( + session.PlayState?.PlayMethod == "Transcode" && session.TranscodingInfo + ); + }, [session.PlayState?.PlayMethod, session.TranscodingInfo]); + + const videoStreamTitle = () => { + return videoStream?.DisplayTitle?.split(" ")[0]; + }; + + return ( + + + + + + {subtitleStream && ( + <> + + + )} + + ); +}; diff --git a/app/(auth)/(tabs)/(home)/settings.tsx b/app/(auth)/(tabs)/(home)/settings.tsx index aba54ae1..c7d9618e 100644 --- a/app/(auth)/(tabs)/(home)/settings.tsx +++ b/app/(auth)/(tabs)/(home)/settings.tsx @@ -3,6 +3,7 @@ import { ListGroup } from "@/components/list/ListGroup"; import { ListItem } from "@/components/list/ListItem"; import { AppLanguageSelector } from "@/components/settings/AppLanguageSelector"; import { AudioToggles } from "@/components/settings/AudioToggles"; +import { ChromecastSettings } from "@/components/settings/ChromecastSettings"; import DownloadSettings from "@/components/settings/DownloadSettings"; import { MediaProvider } from "@/components/settings/MediaContext"; import { MediaToggles } from "@/components/settings/MediaToggles"; @@ -14,17 +15,20 @@ import { SubtitleToggles } from "@/components/settings/SubtitleToggles"; import { UserInfo } from "@/components/settings/UserInfo"; import { useHaptic } from "@/hooks/useHaptic"; import { useJellyfin } from "@/providers/JellyfinProvider"; +import { userAtom } from "@/providers/JellyfinProvider"; import { clearLogs } from "@/utils/log"; import { storage } from "@/utils/mmkv"; import { useNavigation, useRouter } from "expo-router"; import { t } from "i18next"; +import { useAtom } from "jotai"; import React, { useEffect } from "react"; -import { ScrollView, TouchableOpacity, View } from "react-native"; +import { ScrollView, Switch, TouchableOpacity, View } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; export default function settings() { const router = useRouter(); const insets = useSafeAreaInsets(); + const [user] = useAtom(userAtom); const { logout } = useJellyfin(); const successHapticFeedback = useHaptic("success"); @@ -42,7 +46,7 @@ export default function settings() { logout(); }} > - + {t("home.settings.log_out_button")} @@ -57,14 +61,15 @@ export default function settings() { paddingRight: insets.right, }} > - + - + + - - - + + + @@ -75,6 +80,8 @@ export default function settings() { + + { @@ -83,7 +90,7 @@ export default function settings() { title={t("home.settings.intro.show_intro")} /> { storage.set("hasShownIntro", false); }} @@ -91,7 +98,7 @@ export default function settings() { /> - + router.push("/settings/logs/page")} @@ -99,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 988651f0..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(); @@ -38,7 +38,7 @@ export default function page() { }); return await getStatistics({ - url: settings?.optimizedVersionsServerUrl, + url: updatedUrl, authHeader: api?.accessToken, deviceId: getOrSetDeviceId(), }); @@ -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 c5eda557..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,45 +1,43 @@ -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 {Image} from "expo-image"; -import Poster from "@/components/posters/Poster"; -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 {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, @@ -50,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 52e64bc5..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,38 +31,37 @@ 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 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(); const params = useLocalSearchParams(); const { t } = useTranslation(); - const { mediaTitle, releaseYear, posterSrc, ...result } = + const { mediaTitle, releaseYear, posterSrc, mediaType, ...result } = params as unknown as { mediaTitle: string; releaseYear: number; canRequest: string; posterSrc: string; - } & Partial; + mediaType: MediaType; + } & Partial; const navigation = useNavigation(); const { jellyseerrApi, requestMedia } = useJellyseerr(); const [issueType, setIssueType] = useState(); const [issueMessage, setIssueMessage] = useState(); + const [requestBody, _setRequestBody] = useState(); const advancedReqModalRef = useRef(null); const bottomSheetModalRef = useRef(null); @@ -71,7 +72,7 @@ const Page: React.FC = () => { refetch, } = useQuery({ enabled: !!jellyseerrApi && !!result && !!result.id, - queryKey: ["jellyseerr", "detail", result.mediaType, result.id], + queryKey: ["jellyseerr", "detail", mediaType, result.id], staleTime: 0, refetchOnMount: true, refetchOnReconnect: true, @@ -79,9 +80,9 @@ const Page: React.FC = () => { retryOnMount: true, refetchInterval: 0, queryFn: async () => { - return result.mediaType === MediaType.MOVIE - ? jellyseerrApi?.movieDetails(result.id!!) - : jellyseerrApi?.tvDetails(result.id!!); + return mediaType === MediaType.MOVIE + ? jellyseerrApi?.movieDetails(result.id!) + : jellyseerrApi?.tvDetails(result.id!); }, }); @@ -96,7 +97,7 @@ const Page: React.FC = () => { appearsOnIndex={0} /> ), - [] + [], ); const submitIssue = useCallback(() => { @@ -111,10 +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 request = useCallback(async () => { const body: MediaRequestBody = { - mediaId: Number(result.id!!), - mediaType: result.mediaType!!, + mediaId: Number(result.id!), + mediaType: mediaType!, tvdbId: details?.externalIds?.tvdbId, seasons: (details as TvDetails)?.seasons ?.filter?.((s) => s.seasonNumber !== 0) @@ -122,7 +131,7 @@ const Page: React.FC = () => { }; if (hasAdvancedRequestPermission) { - advancedReqModalRef?.current?.present?.(body); + setRequestBody(body); return; } @@ -132,15 +141,15 @@ const Page: React.FC = () => { const isAnime = useMemo( () => (details?.keywords.some((k) => k.id === ANIME_KEYWORD_ID) || false) && - result.mediaType === MediaType.TV, - [details] + mediaType === MediaType.TV, + [details], ); useEffect(() => { if (details) { navigation.setOptions({ headerRight: () => ( - + ), @@ -150,14 +159,14 @@ const Page: React.FC = () => { return ( @@ -172,7 +181,7 @@ const Page: React.FC = () => { source={{ uri: jellyseerrApi?.imageProxy( result.backdropPath, - "w1920_and_h800_multi_faces" + "w1920_and_h800_multi_faces", ), }} /> @@ -182,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' > @@ -195,23 +204,31 @@ const Page: React.FC = () => { } > - - - - - - + + + + + + {mediaTitle} - {releaseYear} + {releaseYear} { }} /> - + g.name) || []} /> {isLoading || isFetching ? ( - + ) : canRequest ? ( - ) : ( )} - + - {result.mediaType === MediaType.TV && ( + {mediaType === MediaType.TV && ( - advancedReqModalRef?.current?.present(data) - } + onAdvancedRequest={(data) => setRequestBody(data)} /> )} @@ -269,14 +283,17 @@ const Page: React.FC = () => { { + _setRequestBody(undefined); advancedReqModalRef?.current?.close(); refetch(); }} + onDismiss={() => _setRequestBody(undefined)} /> { backdropComponent={renderBackdrop} > - + - + {t("jellyseerr.whats_wrong")} - - + + - - + + {t("jellyseerr.issue_type")} - - + + {issueType ? IssueTypeName[issueType] : t("jellyseerr.select_an_issue")} @@ -315,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 a62405e1..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 { 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,23 +84,27 @@ const page: React.FC = () => { item && allEpisodes && allEpisodes.length > 0 && ( - - - ( - - )} - DownloadedIconComponent={() => ( - + + {!Platform.isTV && ( + <> + ( + + )} + DownloadedIconComponent={() => ( + + )} /> - )} - /> + + )} ), }); @@ -138,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 11a38636..aaef84d6 100644 --- a/app/(auth)/(tabs)/(search)/index.tsx +++ b/app/(auth)/(tabs)/(search)/index.tsx @@ -1,37 +1,43 @@ -import { Input } from "@/components/common/Input"; -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 { JellyserrIndexPage } from "@/components/jellyseerr/JellyseerrIndexPage"; +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 { sortOrderOptions } from "@/utils/atoms/filters"; import { useSettings } from "@/utils/atoms/settings"; -import { +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 { Href, router, useLocalSearchParams, useNavigation } from "expo-router"; +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"; -import { useTranslation } from "react-i18next"; type SearchType = "Library" | "Discover"; @@ -62,6 +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 searchEngine = useMemo(() => { return settings?.searchEngine || "Jellyfin"; @@ -117,24 +132,47 @@ export default function search() { return []; // Ensure an empty array is returned in case of an error } }, - [api, searchEngine, settings] + [api, searchEngine, settings], ); + type HeaderSearchBarRef = { + focus: () => void; + blur: () => void; + setText: (text: string) => void; + clearText: () => void; + cancelSearch: () => void; + }; + + const searchBarRef = useRef(null); const navigation = useNavigation(); useLayoutEffect(() => { navigation.setOptions({ headerSearchBarOptions: { + ref: searchBarRef, placeholder: t("search.search"), onChangeText: (e: any) => { router.setParams({ q: "" }); setSearch(e.nativeEvent.text); }, hideWhenScrolling: false, - autoFocus: true, + autoFocus: false, }, }); }, [navigation]); + useEffect(() => { + const unsubscribe = eventBus.on("searchTabPressed", () => { + // Screen not actuve + if (!searchBarRef.current) return; + // Screen is active, focus search bar + searchBarRef.current?.focus(); + }); + + return () => { + unsubscribe(); + }; + }, []); + const { data: movies, isFetching: l1 } = useQuery({ queryKey: ["search", "movies", debouncedSearch], queryFn: () => @@ -202,43 +240,81 @@ export default function search() { return ( <> {jellyseerrApi && ( - - setSearchType("Library")}> - - - setSearchType("Discover")}> - - - + <> + + 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} + /> + + )} + + )} - + @@ -250,14 +326,14 @@ export default function search() { renderItem={(item: BaseItemDto) => ( - + {item.Name} - + {item.ProductionYear} @@ -270,13 +346,13 @@ export default function search() { - + {item.Name} - + {item.ProductionYear} @@ -289,7 +365,7 @@ export default function search() { @@ -303,10 +379,10 @@ export default function search() { - + {item.Name} @@ -319,7 +395,7 @@ export default function search() { @@ -328,29 +404,33 @@ export default function search() { /> ) : ( - + )} {searchType === "Library" && ( <> {!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 011ae3fa..f26309b7 100644 --- a/app/(auth)/(tabs)/_layout.tsx +++ b/app/(auth)/(tabs)/_layout.tsx @@ -1,20 +1,20 @@ 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, @@ -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,12 +177,12 @@ 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; } - setServer({ address: url }); + await setServer({ address: url }); }, []); const handleQuickConnect = async () => { @@ -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} - secureTextEntry={false} - keyboardType="default" - returnKeyType="done" - autoCapitalize="none" - textContentType="username" - clearButtonMode="while-editing" + keyboardType='default' + returnKeyType='done' + autoCapitalize='none' + // Changed from username to oneTimeCode because it is a known issue in RN + // https://github.com/facebook/react-native/issues/47106#issuecomment-2521270037 + textContentType='oneTimeCode' + clearButtonMode='while-editing' maxLength={500} /> @@ -253,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")} { + onServerSelect={async (server) => { setServerURL(server.address); if (server.serverName) { setServerName(server.serverName); } - handleConnect(server.address); + await handleConnect(server.address); }} /> { - handleConnect(s.address); + onServerSelect={async (s) => { + await handleConnect(s.address); }} /> diff --git a/assets/images/adaptive_icon.png b/assets/images/adaptive_icon.png index 8443e717..3d08940d 100644 Binary files a/assets/images/adaptive_icon.png and b/assets/images/adaptive_icon.png differ diff --git a/assets/images/notification.png b/assets/images/notification.png new file mode 100644 index 00000000..b50e56ae Binary files /dev/null and b/assets/images/notification.png differ diff --git a/augmentations/api.ts b/augmentations/api.ts index da5c02a9..b79e341a 100644 --- a/augmentations/api.ts +++ b/augmentations/api.ts @@ -1,17 +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, ): Promise>; getStreamyfinPluginConfig(): Promise>; } @@ -19,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 ?? {}), @@ -30,11 +34,20 @@ Api.prototype.get = function ( Api.prototype.post = function ( url: string, data: D, - config: AxiosRequestConfig + config: AxiosRequestConfig, ): Promise> { - return this.axiosInstance.post(`${this.basePath}${url}`, { + return this.axiosInstance.post(`${this.basePath}${url}`, data, { + ...(config || {}), + headers: { [AUTHORIZATION_HEADER]: this.authorizationHeader }, + }); +}; + +Api.prototype.delete = function ( + url: string, + config: AxiosRequestConfig, +): Promise> { + return this.axiosInstance.delete(`${this.basePath}${url}`, { ...(config || {}), - data, headers: { [AUTHORIZATION_HEADER]: this.authorizationHeader }, }); }; 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 new file mode 100644 index 00000000..bf2ae1c8 --- /dev/null +++ b/biome.json @@ -0,0 +1,54 @@ +{ + "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", + "organizeImports": { + "enabled": true + }, + "files": { + "ignore": [ + "node_modules", + "ios", + "android", + "Streamyfin.app", + "utils/jellyseerr" + ] + }, + "linter": { + "enabled": true, + "rules": { + "style": { + "useImportType": "off", + "noNonNullAssertion": "off" + }, + "recommended": true, + "correctness": { "useExhaustiveDependencies": "off" }, + "suspicious": { + "noExplicitAny": "off" + } + } + }, + "formatter": { + "enabled": true, + "formatWithErrors": true, + "attributePosition": "auto", + "indentStyle": "space", + "indentWidth": 2, + "lineEnding": "lf", + "lineWidth": 80 + }, + "javascript": { + "formatter": { + "arrowParentheses": "always", + "bracketSameLine": false, + "bracketSpacing": true, + "jsxQuoteStyle": "single", + "quoteProperties": "asNeeded", + "semicolons": "always", + "lineWidth": 80 + } + }, + "json": { + "formatter": { + "trailingCommas": "none" + } + } +} diff --git a/bun.lock b/bun.lock index 33b28594..c2fdfe7d 100644 --- a/bun.lock +++ b/bun.lock @@ -60,7 +60,7 @@ "react-i18next": "^15.4.0", "react-native": "npm:react-native-tvos@~0.77.0-0", "react-native-awesome-slider": "^2.9.0", - "react-native-bottom-tabs": "0.8.7", + "react-native-bottom-tabs": "0.8.6", "react-native-circular-progress": "^1.4.1", "react-native-compressor": "^1.10.3", "react-native-country-flag": "^2.0.2", @@ -98,6 +98,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", @@ -106,6 +107,8 @@ "@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", @@ -376,6 +379,24 @@ "@babel/types": ["@babel/types@7.26.9", "", { "dependencies": { "@babel/helper-string-parser": "^7.25.9", "@babel/helper-validator-identifier": "^7.25.9" } }, "sha512-Y3IR1cRnOxOCDvMmNiym7XpXQ93iGDDPHx+Zj+NM+rg0fBaShfQLkg+hKPaZCEvg5N/LeCo4+Rj/i3FuJsIQaw=="], + "@biomejs/biome": ["@biomejs/biome@1.9.4", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "1.9.4", "@biomejs/cli-darwin-x64": "1.9.4", "@biomejs/cli-linux-arm64": "1.9.4", "@biomejs/cli-linux-arm64-musl": "1.9.4", "@biomejs/cli-linux-x64": "1.9.4", "@biomejs/cli-linux-x64-musl": "1.9.4", "@biomejs/cli-win32-arm64": "1.9.4", "@biomejs/cli-win32-x64": "1.9.4" }, "bin": { "biome": "bin/biome" } }, "sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog=="], + + "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@1.9.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-bFBsPWrNvkdKrNCYeAp+xo2HecOGPAy9WyNyB/jKnnedgzl4W4Hb9ZMzYNbf8dMCGmUdSavlYHiR01QaYR58cw=="], + + "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@1.9.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-ngYBh/+bEedqkSevPVhLP4QfVPCpb+4BBe2p7Xs32dBgs7rh9nY2AIYUL6BgLw1JVXV8GlpKmb/hNiuIxfPfZg=="], + + "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@1.9.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-fJIW0+LYujdjUgJJuwesP4EjIBl/N/TcOX3IvIHJQNsAqvV2CHIogsmA94BPG6jZATS4Hi+xv4SkBBQSt1N4/g=="], + + "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@1.9.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-v665Ct9WCRjGa8+kTr0CzApU0+XXtRgwmzIf1SeKSGAv+2scAlW6JR5PMFo6FzqqZ64Po79cKODKf3/AAmECqA=="], + + "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@1.9.4", "", { "os": "linux", "cpu": "x64" }, "sha512-lRCJv/Vi3Vlwmbd6K+oQ0KhLHMAysN8lXoCI7XeHlxaajk06u7G+UsFSO01NAs5iYuWKmVZjmiOzJ0OJmGsMwg=="], + + "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@1.9.4", "", { "os": "linux", "cpu": "x64" }, "sha512-gEhi/jSBhZ2m6wjV530Yy8+fNqG8PAinM3oV7CyO+6c3CEh16Eizm21uHVsyVBEB6RIM8JHIl6AGYCv6Q6Q9Tg=="], + + "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@1.9.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-tlbhLk+WXZmgwoIKwHIHEBZUwxml7bRJgk0X2sPyNR3S93cdRq6XulAZRQJ17FYGGzWne0fgrXBKpl7l4M87Hg=="], + + "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@1.9.4", "", { "os": "win32", "cpu": "x64" }, "sha512-8Y5wMhVIPaWe6jw2H+KlEm4wP/f7EW3810ZLmDlrEEy5KvBsb9ECEfu/kMWD484ijfQ8+nIi0giMgu9g1UAuuA=="], + "@bottom-tabs/react-navigation": ["@bottom-tabs/react-navigation@0.8.6", "", { "dependencies": { "color": "^4.2.3" }, "peerDependencies": { "@react-navigation/native": ">=7", "react": "*", "react-native": "*", "react-native-bottom-tabs": "*" } }, "sha512-hLlyBAUz4ahaVK2Op2VcJeAkCSpm3KKho4IojkPyXsos4WEHtO44EYWC71TDbVGeOP5HQ9k7FSwAW3IiZs0wHw=="], "@config-plugins/ffmpeg-kit-react-native": ["@config-plugins/ffmpeg-kit-react-native@9.0.0", "", { "dependencies": { "semver": "^7.3.5" }, "peerDependencies": { "expo": "^52" } }, "sha512-04bXwdq7pmUPoGqYV0YGsrW/8Db+TNicn2Hznb5t+Dl740z9QkNGP4A38y1Mdz7mCU2EW0riASwl/JTH+6rBvw=="], @@ -386,7 +407,7 @@ "@expo/bunyan": ["@expo/bunyan@4.0.1", "", { "dependencies": { "uuid": "^8.0.0" } }, "sha512-+Lla7nYSiHZirgK+U/uYzsLv/X+HaJienbD5AKX1UQZHYfWaP+9uuQluRB4GrEVWF0GZ7vEVp/jzaOT9k/SQlg=="], - "@expo/cli": ["@expo/cli@0.22.16", "", { "dependencies": { "@0no-co/graphql.web": "^1.0.8", "@babel/runtime": "^7.20.0", "@expo/code-signing-certificates": "^0.0.5", "@expo/config": "~10.0.10", "@expo/config-plugins": "~9.0.15", "@expo/devcert": "^1.1.2", "@expo/env": "~0.4.2", "@expo/image-utils": "^0.6.5", "@expo/json-file": "^9.0.2", "@expo/metro-config": "~0.19.10", "@expo/osascript": "^2.1.6", "@expo/package-manager": "^1.7.2", "@expo/plist": "^0.2.2", "@expo/prebuild-config": "^8.0.27", "@expo/rudder-sdk-node": "^1.1.1", "@expo/spawn-async": "^1.7.2", "@expo/ws-tunnel": "^1.0.1", "@expo/xcpretty": "^4.3.0", "@react-native/dev-middleware": "0.76.7", "@urql/core": "^5.0.6", "@urql/exchange-retry": "^1.3.0", "accepts": "^1.3.8", "arg": "^5.0.2", "better-opn": "~3.0.2", "bplist-creator": "0.0.7", "bplist-parser": "^0.3.1", "cacache": "^18.0.2", "chalk": "^4.0.0", "ci-info": "^3.3.0", "compression": "^1.7.4", "connect": "^3.7.0", "debug": "^4.3.4", "env-editor": "^0.4.1", "fast-glob": "^3.3.2", "form-data": "^3.0.1", "freeport-async": "^2.0.0", "fs-extra": "~8.1.0", "getenv": "^1.0.0", "glob": "^10.4.2", "internal-ip": "^4.3.0", "is-docker": "^2.0.0", "is-wsl": "^2.1.1", "lodash.debounce": "^4.0.8", "minimatch": "^3.0.4", "node-forge": "^1.3.1", "npm-package-arg": "^11.0.0", "ora": "^3.4.0", "picomatch": "^3.0.1", "pretty-bytes": "^5.6.0", "pretty-format": "^29.7.0", "progress": "^2.0.3", "prompts": "^2.3.2", "qrcode-terminal": "0.11.0", "require-from-string": "^2.0.2", "requireg": "^0.2.2", "resolve": "^1.22.2", "resolve-from": "^5.0.0", "resolve.exports": "^2.0.3", "semver": "^7.6.0", "send": "^0.19.0", "slugify": "^1.3.4", "source-map-support": "~0.5.21", "stacktrace-parser": "^0.1.10", "structured-headers": "^0.4.1", "tar": "^6.2.1", "temp-dir": "^2.0.0", "tempy": "^0.7.1", "terminal-link": "^2.1.1", "undici": "^6.18.2", "unique-string": "~2.0.0", "wrap-ansi": "^7.0.0", "ws": "^8.12.1" }, "bin": { "expo-internal": "build/bin/cli" } }, "sha512-a8Ulbnji9kFatnOtsWGCRs6nMUj9UNC0/WhE74HQdXGDGMn5Pl8eNe3cLMy9G54DdqAmEZmRZpgXmcudT78fEQ=="], + "@expo/cli": ["@expo/cli@0.22.18", "", { "dependencies": { "@0no-co/graphql.web": "^1.0.8", "@babel/runtime": "^7.20.0", "@expo/code-signing-certificates": "^0.0.5", "@expo/config": "~10.0.10", "@expo/config-plugins": "~9.0.15", "@expo/devcert": "^1.1.2", "@expo/env": "~0.4.2", "@expo/image-utils": "^0.6.5", "@expo/json-file": "^9.0.2", "@expo/metro-config": "~0.19.11", "@expo/osascript": "^2.1.6", "@expo/package-manager": "^1.7.2", "@expo/plist": "^0.2.2", "@expo/prebuild-config": "^8.0.28", "@expo/rudder-sdk-node": "^1.1.1", "@expo/spawn-async": "^1.7.2", "@expo/ws-tunnel": "^1.0.1", "@expo/xcpretty": "^4.3.0", "@react-native/dev-middleware": "0.76.7", "@urql/core": "^5.0.6", "@urql/exchange-retry": "^1.3.0", "accepts": "^1.3.8", "arg": "^5.0.2", "better-opn": "~3.0.2", "bplist-creator": "0.0.7", "bplist-parser": "^0.3.1", "cacache": "^18.0.2", "chalk": "^4.0.0", "ci-info": "^3.3.0", "compression": "^1.7.4", "connect": "^3.7.0", "debug": "^4.3.4", "env-editor": "^0.4.1", "fast-glob": "^3.3.2", "form-data": "^3.0.1", "freeport-async": "^2.0.0", "fs-extra": "~8.1.0", "getenv": "^1.0.0", "glob": "^10.4.2", "internal-ip": "^4.3.0", "is-docker": "^2.0.0", "is-wsl": "^2.1.1", "lodash.debounce": "^4.0.8", "minimatch": "^3.0.4", "node-forge": "^1.3.1", "npm-package-arg": "^11.0.0", "ora": "^3.4.0", "picomatch": "^3.0.1", "pretty-bytes": "^5.6.0", "pretty-format": "^29.7.0", "progress": "^2.0.3", "prompts": "^2.3.2", "qrcode-terminal": "0.11.0", "require-from-string": "^2.0.2", "requireg": "^0.2.2", "resolve": "^1.22.2", "resolve-from": "^5.0.0", "resolve.exports": "^2.0.3", "semver": "^7.6.0", "send": "^0.19.0", "slugify": "^1.3.4", "source-map-support": "~0.5.21", "stacktrace-parser": "^0.1.10", "structured-headers": "^0.4.1", "tar": "^6.2.1", "temp-dir": "^2.0.0", "tempy": "^0.7.1", "terminal-link": "^2.1.1", "undici": "^6.18.2", "unique-string": "~2.0.0", "wrap-ansi": "^7.0.0", "ws": "^8.12.1" }, "bin": { "expo-internal": "build/bin/cli" } }, "sha512-TWGKHWTYU9xE7YETPk2zQzLPl+bldpzZCa0Cqg0QeENpu03ZEnMxUqrgHwrbWGTf7ONTYC1tODBkFCFw/qgPGA=="], "@expo/code-signing-certificates": ["@expo/code-signing-certificates@0.0.5", "", { "dependencies": { "node-forge": "^1.2.1", "nullthrows": "^1.1.1" } }, "sha512-BNhXkY1bblxKZpltzAx98G2Egj9g1Q+JRcvR7E99DOj862FTCX+ZPsAUtPTr7aHxwtrL7+fL3r0JSmM9kBm+Bw=="], @@ -400,13 +421,13 @@ "@expo/env": ["@expo/env@0.4.2", "", { "dependencies": { "chalk": "^4.0.0", "debug": "^4.3.4", "dotenv": "~16.4.5", "dotenv-expand": "~11.0.6", "getenv": "^1.0.0" } }, "sha512-TgbCgvSk0Kq0e2fLoqHwEBL4M0ztFjnBEz0YCDm5boc1nvkV1VMuIMteVdeBwnTh8Z0oPJTwHCD49vhMEt1I6A=="], - "@expo/fingerprint": ["@expo/fingerprint@0.11.10", "", { "dependencies": { "@expo/spawn-async": "^1.7.2", "arg": "^5.0.2", "chalk": "^4.1.2", "debug": "^4.3.4", "find-up": "^5.0.0", "getenv": "^1.0.0", "minimatch": "^3.0.4", "p-limit": "^3.1.0", "resolve-from": "^5.0.0", "semver": "^7.6.0" }, "bin": { "fingerprint": "bin/cli.js" } }, "sha512-34ZwPjbnnD7KHSyceaxcLQbClCkYHbEp6wBDe+aqimvQw25m2LnliN1cMCVQnpOHkBFRTcbKlowby0fIxAm2bQ=="], + "@expo/fingerprint": ["@expo/fingerprint@0.11.11", "", { "dependencies": { "@expo/spawn-async": "^1.7.2", "arg": "^5.0.2", "chalk": "^4.1.2", "debug": "^4.3.4", "find-up": "^5.0.0", "getenv": "^1.0.0", "minimatch": "^3.0.4", "p-limit": "^3.1.0", "resolve-from": "^5.0.0", "semver": "^7.6.0" }, "bin": { "fingerprint": "bin/cli.js" } }, "sha512-gNyn1KnAOpEa8gSNsYqXMTcq0fSwqU/vit6fP5863vLSKxHm/dNt/gm/uZJxrRZxKq71KUJWF6I7d3z8qIfq5g=="], "@expo/image-utils": ["@expo/image-utils@0.6.5", "", { "dependencies": { "@expo/spawn-async": "^1.7.2", "chalk": "^4.0.0", "fs-extra": "9.0.0", "getenv": "^1.0.0", "jimp-compact": "0.16.1", "parse-png": "^2.1.0", "resolve-from": "^5.0.0", "semver": "^7.6.0", "temp-dir": "~2.0.0", "unique-string": "~2.0.0" } }, "sha512-RsS/1CwJYzccvlprYktD42KjyfWZECH6PPIEowvoSmXfGLfdViwcUEI4RvBfKX5Jli6P67H+6YmHvPTbGOboew=="], "@expo/json-file": ["@expo/json-file@9.0.2", "", { "dependencies": { "@babel/code-frame": "~7.10.4", "json5": "^2.2.3", "write-file-atomic": "^2.3.0" } }, "sha512-yAznIUrybOIWp3Uax7yRflB0xsEpvIwIEqIjao9SGi2Gaa+N0OamWfe0fnXBSWF+2zzF4VvqwT4W5zwelchfgw=="], - "@expo/metro-config": ["@expo/metro-config@0.19.10", "", { "dependencies": { "@babel/core": "^7.20.0", "@babel/generator": "^7.20.5", "@babel/parser": "^7.20.0", "@babel/types": "^7.20.0", "@expo/config": "~10.0.9", "@expo/env": "~0.4.1", "@expo/json-file": "~9.0.1", "@expo/spawn-async": "^1.7.2", "chalk": "^4.1.0", "debug": "^4.3.2", "fs-extra": "^9.1.0", "getenv": "^1.0.0", "glob": "^10.4.2", "jsc-safe-url": "^0.2.4", "lightningcss": "~1.27.0", "minimatch": "^3.0.4", "postcss": "~8.4.32", "resolve-from": "^5.0.0" } }, "sha512-/CtsMLhELJRJjAllM4EUnlPUAixn8Q2YhorKBa4uXZ6FvTEZWHJjqsXnQD39gWSEuAIVwLfJ1qgJi8666+dW2w=="], + "@expo/metro-config": ["@expo/metro-config@0.19.11", "", { "dependencies": { "@babel/core": "^7.20.0", "@babel/generator": "^7.20.5", "@babel/parser": "^7.20.0", "@babel/types": "^7.20.0", "@expo/config": "~10.0.10", "@expo/env": "~0.4.2", "@expo/json-file": "~9.0.2", "@expo/spawn-async": "^1.7.2", "chalk": "^4.1.0", "debug": "^4.3.2", "fs-extra": "^9.1.0", "getenv": "^1.0.0", "glob": "^10.4.2", "jsc-safe-url": "^0.2.4", "lightningcss": "~1.27.0", "minimatch": "^3.0.4", "postcss": "~8.4.32", "resolve-from": "^5.0.0" } }, "sha512-XaobHTcsoHQdKEH7PI/DIpr2QiugkQmPYolbfzkpSJMplNWfSh+cTRjrm4//mS2Sb78qohtu0u2CGJnFqFUGag=="], "@expo/metro-runtime": ["@expo/metro-runtime@4.0.1", "", { "peerDependencies": { "react-native": "*" } }, "sha512-CRpbLvdJ1T42S+lrYa1iZp1KfDeBp4oeZOK3hdpiS5n0vR0nhD6sC1gGF0sTboCTp64tLteikz5Y3j53dvgOIw=="], @@ -416,7 +437,7 @@ "@expo/plist": ["@expo/plist@0.2.2", "", { "dependencies": { "@xmldom/xmldom": "~0.7.7", "base64-js": "^1.2.3", "xmlbuilder": "^14.0.0" } }, "sha512-ZZGvTO6vEWq02UAPs3LIdja+HRO18+LRI5QuDl6Hs3Ps7KX7xU6Y6kjahWKY37Rx2YjNpX07dGpBFzzC+vKa2g=="], - "@expo/prebuild-config": ["@expo/prebuild-config@8.0.27", "", { "dependencies": { "@expo/config": "~10.0.9", "@expo/config-plugins": "~9.0.15", "@expo/config-types": "^52.0.4", "@expo/image-utils": "^0.6.4", "@expo/json-file": "^9.0.1", "@react-native/normalize-colors": "0.76.7", "debug": "^4.3.1", "fs-extra": "^9.0.0", "resolve-from": "^5.0.0", "semver": "^7.6.0", "xml2js": "0.6.0" } }, "sha512-UFGOx4TfiT2gOde8RylwmXctp/WvqBQ4TN7z1YL0WWXfG9TWfO7HdsUnqQhGMW+CDDc7FOJMEo8q1a6xiikfYA=="], + "@expo/prebuild-config": ["@expo/prebuild-config@8.0.28", "", { "dependencies": { "@expo/config": "~10.0.10", "@expo/config-plugins": "~9.0.15", "@expo/config-types": "^52.0.4", "@expo/image-utils": "^0.6.5", "@expo/json-file": "^9.0.2", "@react-native/normalize-colors": "0.76.7", "debug": "^4.3.1", "fs-extra": "^9.0.0", "resolve-from": "^5.0.0", "semver": "^7.6.0", "xml2js": "0.6.0" } }, "sha512-SDDgCKKS1wFNNm3de2vBP8Q5bnxcabuPDE9Mnk9p7Gb4qBavhwMbAtrLcAyZB+WRb4QM+yan3z3K95vvCfI/+A=="], "@expo/react-native-action-sheet": ["@expo/react-native-action-sheet@4.1.0", "", { "dependencies": { "@types/hoist-non-react-statics": "^3.3.1", "hoist-non-react-statics": "^3.3.0" }, "peerDependencies": { "react": ">=18.0.0" } }, "sha512-RILoWhREgjMdr1NUSmZa/cHg8onV2YPDAMOy0iIP1c3H7nT9QQZf5dQNHK8ehcLM82sarVxriBJyYSSHAx7j6w=="], @@ -430,7 +451,7 @@ "@expo/vector-icons": ["@expo/vector-icons@14.0.4", "", { "dependencies": { "prop-types": "^15.8.1" } }, "sha512-+yKshcbpDfbV4zoXOgHxCwh7lkE9VVTT5T03OUlBsqfze1PLy6Hi4jp1vSb1GVbY6eskvMIivGVc9SKzIv0oEQ=="], - "@expo/ws-tunnel": ["@expo/ws-tunnel@1.0.4", "", {}, "sha512-spXCVXxbeKOe8YZ9igd+MDfXZe6LeDvFAdILijeTSG+XcxGrZLmqMWWkFKR0nV8lTWZ+NugUT3CoiXmEuKKQ7w=="], + "@expo/ws-tunnel": ["@expo/ws-tunnel@1.0.5", "", {}, "sha512-Ta9KzslHAIbw2ZoyZ7Ud7/QImucy+K4YvOqo9AhGfUfH76hQzaffQreOySzYusDfW8Y+EXh0ZNWE68dfCumFFw=="], "@expo/xcpretty": ["@expo/xcpretty@4.3.2", "", { "dependencies": { "@babel/code-frame": "7.10.4", "chalk": "^4.1.0", "find-up": "^5.0.0", "js-yaml": "^4.1.0" }, "bin": { "excpretty": "build/cli.js" } }, "sha512-ReZxZ8pdnoI3tP/dNnJdnmAk7uLT4FjsKDGW7YeDdvdOMz2XCQSmSCM9IWlrXuWtMF9zeSB6WJtEhCQ41gQOfw=="], @@ -676,9 +697,9 @@ "@sinonjs/fake-timers": ["@sinonjs/fake-timers@10.3.0", "", { "dependencies": { "@sinonjs/commons": "^3.0.0" } }, "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA=="], - "@tanstack/query-core": ["@tanstack/query-core@5.66.0", "", {}, "sha512-J+JeBtthiKxrpzUu7rfIPDzhscXF2p5zE/hVdrqkACBP8Yu0M96mwJ5m/8cPPYQE9aRNvXztXHlNwIh4FEeMZw=="], + "@tanstack/query-core": ["@tanstack/query-core@5.66.4", "", {}, "sha512-skM/gzNX4shPkqmdTCSoHtJAPMTtmIJNS0hE+xwTTUVYwezArCT34NMermABmBVUg5Ls5aiUXEDXfqwR1oVkcA=="], - "@tanstack/react-query": ["@tanstack/react-query@5.66.0", "", { "dependencies": { "@tanstack/query-core": "5.66.0" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-z3sYixFQJe8hndFnXgWu7C79ctL+pI0KAelYyW+khaNJ1m22lWrhJU2QrsTcRKMuVPtoZvfBYrTStIdKo+x0Xw=="], + "@tanstack/react-query": ["@tanstack/react-query@5.66.9", "", { "dependencies": { "@tanstack/query-core": "5.66.4" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-NRI02PHJsP5y2gAuWKP+awamTIBFBSKMnO6UVzi03GTclmHHHInH5UzVgzi5tpu4+FmGfsdT7Umqegobtsp23A=="], "@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="], @@ -762,7 +783,7 @@ "anser": ["anser@1.4.10", "", {}, "sha512-hCv9AqTQ8ycjpSd3upOJd7vFwW1JaoYQ7tpham03GJ1ca8/65rqn0RpaWpItOAd6ylW9wAw6luXYPJIyPFVOww=="], - "ansi-escapes": ["ansi-escapes@4.3.2", "", { "dependencies": { "type-fest": "^0.21.3" } }, "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ=="], + "ansi-escapes": ["ansi-escapes@7.0.0", "", { "dependencies": { "environment": "^1.0.0" } }, "sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw=="], "ansi-fragments": ["ansi-fragments@0.2.1", "", { "dependencies": { "colorette": "^1.0.7", "slice-ansi": "^2.0.0", "strip-ansi": "^5.0.0" } }, "sha512-DykbNHxuXQwUDRv5ibc2b0x7uw7wmwOGLBUd5RmaQ5z8Lhx19vwvKV+FAsM5rEA6dEcHxX+/Ad5s9eF2k2bB+w=="], @@ -828,7 +849,7 @@ "babel-preset-current-node-syntax": ["babel-preset-current-node-syntax@1.1.0", "", { "dependencies": { "@babel/plugin-syntax-async-generators": "^7.8.4", "@babel/plugin-syntax-bigint": "^7.8.3", "@babel/plugin-syntax-class-properties": "^7.12.13", "@babel/plugin-syntax-class-static-block": "^7.14.5", "@babel/plugin-syntax-import-attributes": "^7.24.7", "@babel/plugin-syntax-import-meta": "^7.10.4", "@babel/plugin-syntax-json-strings": "^7.8.3", "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", "@babel/plugin-syntax-numeric-separator": "^7.10.4", "@babel/plugin-syntax-object-rest-spread": "^7.8.3", "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", "@babel/plugin-syntax-optional-chaining": "^7.8.3", "@babel/plugin-syntax-private-property-in-object": "^7.14.5", "@babel/plugin-syntax-top-level-await": "^7.14.5" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-ldYss8SbBlWva1bs28q78Ju5Zq1F+8BrqBZZ0VFhLBvhh6lCpC2o3gDJi/5DRLs9FgYZCnmPYIVFU4lRXCkyUw=="], - "babel-preset-expo": ["babel-preset-expo@12.0.8", "", { "dependencies": { "@babel/plugin-proposal-decorators": "^7.12.9", "@babel/plugin-transform-export-namespace-from": "^7.22.11", "@babel/plugin-transform-object-rest-spread": "^7.12.13", "@babel/plugin-transform-parameters": "^7.22.15", "@babel/preset-react": "^7.22.15", "@babel/preset-typescript": "^7.23.0", "@react-native/babel-preset": "0.76.7", "babel-plugin-react-native-web": "~0.19.13", "react-refresh": "^0.14.2" }, "peerDependencies": { "babel-plugin-react-compiler": "^19.0.0-beta-9ee70a1-20241017", "react-compiler-runtime": "^19.0.0-beta-8a03594-20241020" }, "optionalPeers": ["babel-plugin-react-compiler", "react-compiler-runtime"] }, "sha512-bojAddWZJusLs3NVdF+jN3WweTYVEZXBKIeO0sOhqOg7UPh5w1bnMkx7SDua0FgQMGBxb13qM31Y46yeZnmXjw=="], + "babel-preset-expo": ["babel-preset-expo@12.0.9", "", { "dependencies": { "@babel/plugin-proposal-decorators": "^7.12.9", "@babel/plugin-transform-export-namespace-from": "^7.22.11", "@babel/plugin-transform-object-rest-spread": "^7.12.13", "@babel/plugin-transform-parameters": "^7.22.15", "@babel/preset-react": "^7.22.15", "@babel/preset-typescript": "^7.23.0", "@react-native/babel-preset": "0.76.7", "babel-plugin-react-native-web": "~0.19.13", "react-refresh": "^0.14.2" }, "peerDependencies": { "babel-plugin-react-compiler": "^19.0.0-beta-9ee70a1-20241017", "react-compiler-runtime": "^19.0.0-beta-8a03594-20241020" }, "optionalPeers": ["babel-plugin-react-compiler", "react-compiler-runtime"] }, "sha512-1c+ysrTavT49WgVAj0OX/TEzt1kU2mfPhDaDajstshNHXFKPenMPWSViA/DHrJKVIMwaqr+z3GbUOD9GtKgpdg=="], "babel-preset-jest": ["babel-preset-jest@29.6.3", "", { "dependencies": { "babel-plugin-jest-hoist": "^29.6.3", "babel-preset-current-node-syntax": "^1.0.0" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA=="], @@ -896,7 +917,7 @@ "camelize": ["camelize@1.0.1", "", {}, "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ=="], - "caniuse-lite": ["caniuse-lite@1.0.30001699", "", {}, "sha512-b+uH5BakXZ9Do9iK+CkDmctUSEqZl+SP056vc5usa0PL+ev5OHw003rZXcnjNDv3L8P5j6rwT6C0BPKSikW08w=="], + "caniuse-lite": ["caniuse-lite@1.0.30001700", "", {}, "sha512-2S6XIXwaE7K7erT8dY+kLQcpa5ms63XlRkMkReXjle+kf6c5g38vyMl+Z5y8dSxOFDhcFe+nxnn261PLxBSQsQ=="], "centra": ["centra@2.7.0", "", { "dependencies": { "follow-redirects": "^1.15.6" } }, "sha512-PbFMgMSrmgx6uxCdm57RUos9Tc3fclMvhLSATYN39XsDV29B89zZ3KA89jmY0vwSGazyU+uerqwa6t+KaodPcg=="], @@ -920,6 +941,8 @@ "cli-spinners": ["cli-spinners@2.9.2", "", {}, "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg=="], + "cli-truncate": ["cli-truncate@4.0.0", "", { "dependencies": { "slice-ansi": "^5.0.0", "string-width": "^7.0.0" } }, "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA=="], + "client-only": ["client-only@0.0.1", "", {}, "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="], "cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], @@ -936,7 +959,7 @@ "color-string": ["color-string@1.9.1", "", { "dependencies": { "color-name": "^1.0.0", "simple-swizzle": "^0.2.2" } }, "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg=="], - "colorette": ["colorette@1.4.0", "", {}, "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g=="], + "colorette": ["colorette@2.0.20", "", {}, "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w=="], "combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="], @@ -1056,7 +1079,7 @@ "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], - "electron-to-chromium": ["electron-to-chromium@1.5.100", "", {}, "sha512-u1z9VuzDXV86X2r3vAns0/5ojfXBue9o0+JDUDBKYqGLjxLkSqsSUoPU/6kW0gx76V44frHaf6Zo+QF74TQCMg=="], + "electron-to-chromium": ["electron-to-chromium@1.5.103", "", {}, "sha512-P6+XzIkfndgsrjROJWfSvVEgNHtPgbhVyTkwLjUM2HU/h7pZRORgaTlHqfAikqxKmdJMLW8fftrdGWbd/Ds0FA=="], "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], @@ -1072,6 +1095,8 @@ "envinfo": ["envinfo@7.14.0", "", { "bin": { "envinfo": "dist/cli.js" } }, "sha512-CO40UI41xDQzhLB1hWyqUKgFhs250pNcGbyGKe1l/e4FSaI/+YE4IMG76GDt0In67WLPACIITC+sOi08x4wIvg=="], + "environment": ["environment@1.1.0", "", {}, "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q=="], + "eol": ["eol@0.9.1", "", {}, "sha512-Ds/TEoZjwggRoz/Q2O7SE3i4Jm66mqTDfmdHdq/7DKVk3bro9Q8h6WdXKdPqFLMoqxrDK5SVRzHVPOS6uuGtrg=="], "error-ex": ["error-ex@1.3.2", "", { "dependencies": { "is-arrayish": "^0.2.1" } }, "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g=="], @@ -1086,6 +1111,8 @@ "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="], + "es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="], + "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], "escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="], @@ -1100,6 +1127,8 @@ "event-target-shim": ["event-target-shim@5.0.1", "", {}, "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ=="], + "eventemitter3": ["eventemitter3@5.0.1", "", {}, "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA=="], + "events": ["events@3.3.0", "", {}, "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q=="], "exec-async": ["exec-async@2.2.0", "", {}, "sha512-87OpwcEiMia/DeiKFzaQNBNFeN3XkkpYIh9FyOqq5mS2oKv3CBE67PXoEKcr6nodWdXNogTiQ0jE2NGuoffXPw=="], @@ -1110,11 +1139,11 @@ "expect": ["expect@29.7.0", "", { "dependencies": { "@jest/expect-utils": "^29.7.0", "jest-get-type": "^29.6.3", "jest-matcher-utils": "^29.7.0", "jest-message-util": "^29.7.0", "jest-util": "^29.7.0" } }, "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw=="], - "expo": ["expo@52.0.35", "", { "dependencies": { "@babel/runtime": "^7.20.0", "@expo/cli": "0.22.16", "@expo/config": "~10.0.10", "@expo/config-plugins": "~9.0.15", "@expo/fingerprint": "0.11.10", "@expo/metro-config": "0.19.10", "@expo/vector-icons": "^14.0.0", "babel-preset-expo": "~12.0.8", "expo-asset": "~11.0.3", "expo-constants": "~17.0.6", "expo-file-system": "~18.0.10", "expo-font": "~13.0.3", "expo-keep-awake": "~14.0.2", "expo-modules-autolinking": "2.0.7", "expo-modules-core": "2.2.2", "fbemitter": "^3.0.0", "web-streams-polyfill": "^3.3.2", "whatwg-url-without-unicode": "8.0.0-3" }, "peerDependencies": { "@expo/dom-webview": "*", "@expo/metro-runtime": "*", "react": "*", "react-native": "*", "react-native-webview": "*" }, "optionalPeers": ["@expo/dom-webview", "@expo/metro-runtime", "react-native-webview"], "bin": { "expo": "bin/cli" } }, "sha512-VagwS6MJbU0Eky18i4amkkSy7FTi0v31B0W+qoEcsU4x5OurA381rxw4qGsQE+8pmSD/Gf3DGb8ygJw+HoAsXw=="], + "expo": ["expo@52.0.37", "", { "dependencies": { "@babel/runtime": "^7.20.0", "@expo/cli": "0.22.18", "@expo/config": "~10.0.10", "@expo/config-plugins": "~9.0.15", "@expo/fingerprint": "0.11.11", "@expo/metro-config": "0.19.11", "@expo/vector-icons": "^14.0.0", "babel-preset-expo": "~12.0.9", "expo-asset": "~11.0.4", "expo-constants": "~17.0.7", "expo-file-system": "~18.0.11", "expo-font": "~13.0.4", "expo-keep-awake": "~14.0.3", "expo-modules-autolinking": "2.0.8", "expo-modules-core": "2.2.2", "fbemitter": "^3.0.0", "web-streams-polyfill": "^3.3.2", "whatwg-url-without-unicode": "8.0.0-3" }, "peerDependencies": { "@expo/dom-webview": "*", "@expo/metro-runtime": "*", "react": "*", "react-native": "*", "react-native-webview": "*" }, "optionalPeers": ["@expo/dom-webview", "@expo/metro-runtime", "react-native-webview"], "bin": { "expo": "bin/cli" } }, "sha512-fo37ClqjNLOVInerm7BU27H8lfPfeTC7Pmu72roPzq46DnJfs+KzTxTzE34GcJ0b6hMUx9FRSSGyTQqxzo2TVQ=="], "expo-application": ["expo-application@6.0.2", "", { "peerDependencies": { "expo": "*" } }, "sha512-qcj6kGq3mc7x5yIb5KxESurFTJCoEKwNEL34RdPEvTB/xhl7SeVZlu05sZBqxB1V4Ryzq/LsCb7NHNfBbb3L7A=="], - "expo-asset": ["expo-asset@11.0.3", "", { "dependencies": { "@expo/image-utils": "^0.6.4", "expo-constants": "~17.0.5", "invariant": "^2.2.4", "md5-file": "^3.2.3" }, "peerDependencies": { "expo": "*", "react": "*", "react-native": "*" } }, "sha512-vgJnC82IooAVMy5PxbdFIMNJhW4hKAUyxc5VIiAPPf10vFYw6CqHm+hrehu4ST1I4bvg5PV4uKdPxliebcbgLg=="], + "expo-asset": ["expo-asset@11.0.4", "", { "dependencies": { "@expo/image-utils": "^0.6.5", "expo-constants": "~17.0.7", "invariant": "^2.2.4", "md5-file": "^3.2.3" }, "peerDependencies": { "expo": "*", "react": "*", "react-native": "*" } }, "sha512-CdIywU0HrR3wsW5c3n0cT3jW9hccZdnqGsRqY+EY/RWzJbDXtDfAQVEiFHO3mDK7oveUwrP2jK/6ZRNek41/sg=="], "expo-background-fetch": ["expo-background-fetch@13.0.5", "", { "dependencies": { "expo-task-manager": "~12.0.5" }, "peerDependencies": { "expo": "*" } }, "sha512-rLRM+rYDRT0fA0Oaet5ibJK3nKVRkfdjXjISHxjUvIE4ktD9pE+UjAPPdjTXZ5CkNb3JyNNhQGJEGpdJC2HLKw=="], @@ -1124,7 +1153,7 @@ "expo-build-properties": ["expo-build-properties@0.13.2", "", { "dependencies": { "ajv": "^8.11.0", "semver": "^7.6.0" }, "peerDependencies": { "expo": "*" } }, "sha512-ML2GwBgn0Bo4yPgnSGb7h3XVxCigS/KFdid3xPC2HldEioTP3UewB/2Qa4WBsam9Fb7lAuRyVHAfRoA3swpDzg=="], - "expo-constants": ["expo-constants@17.0.6", "", { "dependencies": { "@expo/config": "~10.0.9", "@expo/env": "~0.4.1" }, "peerDependencies": { "expo": "*", "react-native": "*" } }, "sha512-rl3/hBIIkh4XDkCEMzGpmY6kWj2G1TA4Mq2joeyzoFBepJuGjqnGl7phf/71sTTgamQ1hmhKCLRNXMpRqzzqxw=="], + "expo-constants": ["expo-constants@17.0.7", "", { "dependencies": { "@expo/config": "~10.0.10", "@expo/env": "~0.4.2" }, "peerDependencies": { "expo": "*", "react-native": "*" } }, "sha512-sp5NUiV17I3JblVPIBDgoxgt7JIZS30vcyydCYHxsEoo+aKaeRYXxGYilCvb9lgI6BBwSL24sQ6ZjWsCWoF1VA=="], "expo-crypto": ["expo-crypto@14.0.2", "", { "dependencies": { "base64-js": "^1.3.0" }, "peerDependencies": { "expo": "*" } }, "sha512-WRc9PBpJraJN29VD5Ef7nCecxJmZNyRKcGkNiDQC1nhY5agppzwhqh7zEzNFarE/GqDgSiaDHS8yd5EgFhP9AQ=="], @@ -1140,17 +1169,17 @@ "expo-eas-client": ["expo-eas-client@0.13.2", "", {}, "sha512-2RAAGtkO9vseoJZuW4mhJkiNQ6+FfLrX66OTMq4Qj9mRKZV2Uq/ZquxUGIeJyYqBy4vNYeKbuPd2oJtsV9LBGQ=="], - "expo-file-system": ["expo-file-system@18.0.10", "", { "dependencies": { "web-streams-polyfill": "^3.3.2" }, "peerDependencies": { "expo": "*", "react-native": "*" } }, "sha512-+GnxkI+J9tOzUQMx+uIOLBEBsO2meyoYHxd87m9oT9M//BpepYqI1AvYBH8YM4dgr9HaeaeLr7z5XFVqfL8tWg=="], + "expo-file-system": ["expo-file-system@18.0.11", "", { "dependencies": { "web-streams-polyfill": "^3.3.2" }, "peerDependencies": { "expo": "*", "react-native": "*" } }, "sha512-yDwYfEzWgPXsBZHJW2RJ8Q66ceiFN9Wa5D20pp3fjXVkzPBDwxnYwiPWk4pVmCa5g4X5KYMoMne1pUrsL4OEpg=="], - "expo-font": ["expo-font@13.0.3", "", { "dependencies": { "fontfaceobserver": "^2.1.0" }, "peerDependencies": { "expo": "*", "react": "*" } }, "sha512-9IdYz+A+b3KvuCYP7DUUXF4VMZjPU+IsvAnLSVJ2TfP6zUD2JjZFx3jeo/cxWRkYk/aLj5+53Te7elTAScNl4Q=="], + "expo-font": ["expo-font@13.0.4", "", { "dependencies": { "fontfaceobserver": "^2.1.0" }, "peerDependencies": { "expo": "*", "react": "*" } }, "sha512-eAP5hyBgC8gafFtprsz0HMaB795qZfgJWqTmU0NfbSin1wUuVySFMEPMOrTkTgmazU73v4Cb4x7p86jY1XXYUw=="], "expo-haptics": ["expo-haptics@14.0.1", "", { "peerDependencies": { "expo": "*" } }, "sha512-V81FZ7xRUfqM6uSI6FA1KnZ+QpEKnISqafob/xEfcx1ymwhm4V3snuLWWFjmAz+XaZQTqlYa8z3QbqEXz7G63w=="], - "expo-image": ["expo-image@2.0.5", "", { "peerDependencies": { "expo": "*", "react": "*", "react-native": "*", "react-native-web": "*" }, "optionalPeers": ["react-native-web"] }, "sha512-FAq7uyaTAfLWER3lN+KVAtep7IfGPZN9ygnVKW4GvgnvR4hKhTtZ5WNxiJ18KKLVb4nUKuHOpQeJNnljy3dtmA=="], + "expo-image": ["expo-image@2.0.6", "", { "peerDependencies": { "expo": "*", "react": "*", "react-native": "*", "react-native-web": "*" }, "optionalPeers": ["react-native-web"] }, "sha512-NHpIZmGnrPbyDadil6eK+sUgyFMQfapEVb7YaGgxSFWBUQ1rSpjqdIQrCD24IZTO9uSH8V+hMh2ROxrAjAixzQ=="], "expo-json-utils": ["expo-json-utils@0.14.0", "", {}, "sha512-xjGfK9dL0B1wLnOqNkX0jM9p48Y0I5xEPzHude28LY67UmamUyAACkqhZGaPClyPNfdzczk7Ej6WaRMT3HfXvw=="], - "expo-keep-awake": ["expo-keep-awake@14.0.2", "", { "peerDependencies": { "expo": "*", "react": "*" } }, "sha512-71XAMnoWjKZrN8J7Q3+u0l9Ytp4OfhNAYz8BCWF1/9aFUw09J3I7Z5DuI3MUsVMa/KWi+XhG+eDUFP8cVA19Uw=="], + "expo-keep-awake": ["expo-keep-awake@14.0.3", "", { "peerDependencies": { "expo": "*", "react": "*" } }, "sha512-6Jh94G6NvTZfuLnm2vwIpKe3GdOiVBuISl7FI8GqN0/9UOg9E0WXXp5cDcfAG8bn80RfgLJS8P7EPUGTZyOvhg=="], "expo-linear-gradient": ["expo-linear-gradient@14.0.2", "", { "peerDependencies": { "expo": "*", "react": "*", "react-native": "*" } }, "sha512-nvac1sPUfFFJ4mY25UkvubpUV/olrBH+uQw5k+beqSvQaVQiUfFtYzfRr+6HhYBNb4AEsOtpsCRkpDww3M2iGQ=="], @@ -1160,7 +1189,7 @@ "expo-manifests": ["expo-manifests@0.15.6", "", { "dependencies": { "@expo/config": "~10.0.9", "expo-json-utils": "~0.14.0" }, "peerDependencies": { "expo": "*" } }, "sha512-z+TFICrijMaqBvcJkVx8WzgmOsV6ZJGvaPNQKZr4DA6uqugFMtvAQVikDjIq7SEc3n7IgPk0GR4ZN3/KnnkeVA=="], - "expo-modules-autolinking": ["expo-modules-autolinking@2.0.7", "", { "dependencies": { "@expo/spawn-async": "^1.7.2", "chalk": "^4.1.0", "commander": "^7.2.0", "fast-glob": "^3.2.5", "find-up": "^5.0.0", "fs-extra": "^9.1.0", "require-from-string": "^2.0.2", "resolve-from": "^5.0.0" }, "bin": { "expo-modules-autolinking": "bin/expo-modules-autolinking.js" } }, "sha512-rkGc6a/90AC3q8wSy4V+iIpq6Fd0KXmQICKrvfmSWwrMgJmLfwP4QTrvLYPYOOMjFwNJcTaohcH8vzW/wYKrMg=="], + "expo-modules-autolinking": ["expo-modules-autolinking@2.0.8", "", { "dependencies": { "@expo/spawn-async": "^1.7.2", "chalk": "^4.1.0", "commander": "^7.2.0", "fast-glob": "^3.2.5", "find-up": "^5.0.0", "fs-extra": "^9.1.0", "require-from-string": "^2.0.2", "resolve-from": "^5.0.0" }, "bin": { "expo-modules-autolinking": "bin/expo-modules-autolinking.js" } }, "sha512-DezgnEYFQYic8hKGhkbztBA3QUmSftjaNDIKNAtS2iGJmzCcNIkatjN2slFDSWjSTNo8gOvPQyMKfyHWFvLpOQ=="], "expo-modules-core": ["expo-modules-core@2.2.2", "", { "dependencies": { "invariant": "^2.2.4" } }, "sha512-SgjK86UD89gKAscRK3bdpn6Ojfs/KU4GujtuFx1wm4JaBjmXH4aakWkItkPlAV2pjIiHJHWQbENL9xjbw/Qr/g=="], @@ -1184,7 +1213,7 @@ "expo-task-manager": ["expo-task-manager@12.0.5", "", { "dependencies": { "unimodules-app-loader": "~5.0.1" }, "peerDependencies": { "expo": "*", "react-native": "*" } }, "sha512-tDHOBYORA6wuO32NWwz/Egrvn+N6aANHAa0DFs+01VK/IJZfU9D05ZN6M5XYIlZv5ll4GSX1wJZyTCY0HZGapw=="], - "expo-updates": ["expo-updates@0.26.18", "", { "dependencies": { "@expo/code-signing-certificates": "0.0.5", "@expo/config": "~10.0.9", "@expo/config-plugins": "~9.0.15", "@expo/spawn-async": "^1.7.2", "arg": "4.1.0", "chalk": "^4.1.2", "expo-eas-client": "~0.13.2", "expo-manifests": "~0.15.5", "expo-structured-headers": "~4.0.0", "expo-updates-interface": "~1.0.0", "fast-glob": "^3.3.2", "fbemitter": "^3.0.0", "ignore": "^5.3.1", "resolve-from": "^5.0.0" }, "peerDependencies": { "expo": "*", "react": "*" }, "bin": { "expo-updates": "bin/cli.js" } }, "sha512-i9on8jMLrDxtr3Jwpmqj14oa4PWxSKYrHhJYK40xATV6qrauTija9R7BkN0hQjD4LpElt5UJW2/YUP30UsTFqA=="], + "expo-updates": ["expo-updates@0.26.19", "", { "dependencies": { "@expo/code-signing-certificates": "0.0.5", "@expo/config": "~10.0.10", "@expo/config-plugins": "~9.0.15", "@expo/spawn-async": "^1.7.2", "arg": "4.1.0", "chalk": "^4.1.2", "expo-eas-client": "~0.13.2", "expo-manifests": "~0.15.6", "expo-structured-headers": "~4.0.0", "expo-updates-interface": "~1.0.0", "fast-glob": "^3.3.2", "fbemitter": "^3.0.0", "ignore": "^5.3.1", "resolve-from": "^5.0.0" }, "peerDependencies": { "expo": "*", "react": "*" }, "bin": { "expo-updates": "bin/cli.js" } }, "sha512-h40UrG0n1nCb2na1ffz+mNQtsnr7/BxxK+EtXJSqCaD9PIGaTGe20tasmo1oVskv3s37zfv0x93+6uTjanieQg=="], "expo-updates-interface": ["expo-updates-interface@1.0.0", "", { "peerDependencies": { "expo": "*" } }, "sha512-93oWtvULJOj+Pp+N/lpTcFfuREX1wNeHtp7Lwn8EbzYYmdn37MvZU3TPW2tYYCZuhzmKEXnUblYcruYoDu7IrQ=="], @@ -1206,7 +1235,7 @@ "fast-uri": ["fast-uri@3.0.6", "", {}, "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw=="], - "fast-xml-parser": ["fast-xml-parser@4.5.1", "", { "dependencies": { "strnum": "^1.0.5" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-y655CeyUQ+jj7KBbYMc4FG01V8ZQqjN+gDYGJ50RtfsUB8iG9AmwmwoAgeKLJdmueKKMrH1RJ7yXHTSoczdv5w=="], + "fast-xml-parser": ["fast-xml-parser@4.5.3", "", { "dependencies": { "strnum": "^1.1.1" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-RKihhV+SHsIUGXObeVy9AXiBbFwkVk7Syp8XgwN5U3JV416+Gwp/GO9i0JYKmikykgz/UHRrrV4ROuZEo/T0ig=="], "fastq": ["fastq@1.19.0", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-7SFSRCNjBQIZH/xZR3iy5iQYR8aGBE0h3VG6/cwlbrpdciNYBMotQav8c1XI3HjHH+NikUpP53nPdlZSdWmFzA=="], @@ -1238,7 +1267,7 @@ "flow-enums-runtime": ["flow-enums-runtime@0.0.6", "", {}, "sha512-3PYnM29RFXwvAN6Pc/scUfkI7RwhQ/xqyLUyPNlXUp9S40zI8nup9tUSrTLSVnWGBN38FNiGWbwZOB6uR4OGdw=="], - "flow-parser": ["flow-parser@0.261.1", "", {}, "sha512-2l5bBKeVtT+d+1CYSsTLJ+iP2FuoR7zjbDQI/v6dDRiBpx3Lb20Z/tLS37ReX/lcodyGSHC2eA/Nk63hB+mkYg=="], + "flow-parser": ["flow-parser@0.261.2", "", {}, "sha512-RtunoakA3YjtpAxPSOBVW6lmP5NYmETwkpAfNkdr8Ovf86ENkbD3mtPWnswFTIUtRvjwv0i8ZSkHK+AzsUg1JA=="], "follow-redirects": ["follow-redirects@1.15.9", "", {}, "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ=="], @@ -1248,7 +1277,7 @@ "foreground-child": ["foreground-child@3.3.0", "", { "dependencies": { "cross-spawn": "^7.0.0", "signal-exit": "^4.0.1" } }, "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg=="], - "form-data": ["form-data@4.0.1", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "mime-types": "^2.1.12" } }, "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw=="], + "form-data": ["form-data@4.0.2", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "mime-types": "^2.1.12" } }, "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w=="], "freeport-async": ["freeport-async@2.0.0", "", {}, "sha512-K7od3Uw45AJg00XUmy15+Hae2hOcgKcmN3/EF6Y7i01O0gaqiRx8sUSpsb9+BRNL8RPBrhzPsVfy8q9ADlJuWQ=="], @@ -1268,6 +1297,8 @@ "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], + "get-east-asian-width": ["get-east-asian-width@1.3.0", "", {}, "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ=="], + "get-intrinsic": ["get-intrinsic@1.2.7", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "function-bind": "^1.1.2", "get-proto": "^1.0.0", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA=="], "get-nonce": ["get-nonce@1.0.1", "", {}, "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="], @@ -1322,6 +1353,8 @@ "human-signals": ["human-signals@2.1.0", "", {}, "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw=="], + "husky": ["husky@9.1.7", "", { "bin": { "husky": "bin.js" } }, "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA=="], + "hyphenate-style-name": ["hyphenate-style-name@1.1.0", "", {}, "sha512-WDC/ui2VVRrz3jOVi+XtjqkDjiVjTtFaAGiW37k6b+ohyQ5wYDOGkvCZa8+H0nx3gyvv0+BST9xuOgIyGQ00gw=="], "i18next": ["i18next@24.2.2", "", { "dependencies": { "@babel/runtime": "^7.23.2" }, "peerDependencies": { "typescript": "^5" }, "optionalPeers": ["typescript"] }, "sha512-NE6i86lBCKRYZa5TaUDkU5S4HFgLIEJRLr3Whf2psgaxBleQ2LC1YW1Vc+SCgkAW7VEzndT6al6+CzegSUHcTQ=="], @@ -1446,7 +1479,7 @@ "join-component": ["join-component@1.1.0", "", {}, "sha512-bF7vcQxbODoGK1imE2P9GS9aw4zD0Sd+Hni68IMZLj7zRnquH7dXUmMw9hDI5S/Jzt7q+IyTXN0rSg2GI0IKhQ=="], - "jotai": ["jotai@2.12.0", "", { "peerDependencies": { "@types/react": ">=17.0.0", "react": ">=17.0.0" }, "optionalPeers": ["@types/react", "react"] }, "sha512-j5B4NmUw8gbuN7AG4NufWw00rfpm6hexL2CVhKD7juoP2YyD9FEUV5ar921JMvadyrxQhU1NpuKUL3QfsAlVpA=="], + "jotai": ["jotai@2.12.1", "", { "peerDependencies": { "@types/react": ">=17.0.0", "react": ">=17.0.0" }, "optionalPeers": ["@types/react", "react"] }, "sha512-VUW0nMPYIru5g89tdxwr9ftiVdc/nGV9jvHISN8Ucx+m1vI9dBeHemfqYzEuw5XSkmYjD/MEyApN9k6yrATsZQ=="], "jpeg-js": ["jpeg-js@0.4.4", "", {}, "sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg=="], @@ -1508,10 +1541,14 @@ "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.27.0", "", { "os": "win32", "cpu": "x64" }, "sha512-/OJLj94Zm/waZShL8nB5jsNj3CfNATLCTyFxZyouilfTmSoLDX7VlVAmhPHoZWVFp4vdmoiEbPEYC8HID3m6yw=="], - "lilconfig": ["lilconfig@2.1.0", "", {}, "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ=="], + "lilconfig": ["lilconfig@3.1.3", "", {}, "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw=="], "lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="], + "lint-staged": ["lint-staged@15.5.0", "", { "dependencies": { "chalk": "^5.4.1", "commander": "^13.1.0", "debug": "^4.4.0", "execa": "^8.0.1", "lilconfig": "^3.1.3", "listr2": "^8.2.5", "micromatch": "^4.0.8", "pidtree": "^0.6.0", "string-argv": "^0.3.2", "yaml": "^2.7.0" }, "bin": { "lint-staged": "bin/lint-staged.js" } }, "sha512-WyCzSbfYGhK7cU+UuDDkzUiytbfbi0ZdPy2orwtM75P3WTtQBzmG40cCxIa8Ii2+XjfxzLH6Be46tUfWS85Xfg=="], + + "listr2": ["listr2@8.2.5", "", { "dependencies": { "cli-truncate": "^4.0.0", "colorette": "^2.0.20", "eventemitter3": "^5.0.1", "log-update": "^6.1.0", "rfdc": "^1.4.1", "wrap-ansi": "^9.0.0" } }, "sha512-iyAZCeyD+c1gPyE9qpFu8af0Y+MRtmKOncdGoA2S5EY8iFq99dmmvkNnHiWo+pj0s7yH7l3KPIgee77tKpXPWQ=="], + "load-bmfont": ["load-bmfont@1.4.2", "", { "dependencies": { "buffer-equal": "0.0.1", "mime": "^1.3.4", "parse-bmfont-ascii": "^1.0.3", "parse-bmfont-binary": "^1.0.5", "parse-bmfont-xml": "^1.1.4", "phin": "^3.7.1", "xhr": "^2.0.1", "xtend": "^4.0.0" } }, "sha512-qElWkmjW9Oq1F9EI5Gt7aD9zcdHb9spJCW1L/dmPf7KzCCEJxq8nhHz5eCgI9aMf7vrG/wyaCqdsI+Iy9ZTlog=="], "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], @@ -1524,6 +1561,8 @@ "log-symbols": ["log-symbols@4.1.0", "", { "dependencies": { "chalk": "^4.1.0", "is-unicode-supported": "^0.1.0" } }, "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg=="], + "log-update": ["log-update@6.1.0", "", { "dependencies": { "ansi-escapes": "^7.0.0", "cli-cursor": "^5.0.0", "slice-ansi": "^7.1.0", "strip-ansi": "^7.1.0", "wrap-ansi": "^9.0.0" } }, "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w=="], + "logkitty": ["logkitty@0.7.1", "", { "dependencies": { "ansi-fragments": "^0.2.1", "dayjs": "^1.8.15", "yargs": "^15.1.0" }, "bin": { "logkitty": "bin/logkitty.js" } }, "sha512-/3ER20CTTbahrCrpYfPn7Xavv9diBROZpoXGVZDWMw4b/X4uuUwAC0ki85tgsdMRONURyIJbcOvS94QsUBYPbQ=="], "loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="], @@ -1588,6 +1627,8 @@ "mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="], + "mimic-function": ["mimic-function@5.0.1", "", {}, "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA=="], + "min-document": ["min-document@2.19.0", "", { "dependencies": { "dom-walk": "^0.1.0" } }, "sha512-9Wy1B3m3f66bPPmU5hdA4DR4PB2OfDU/+GS3yAB7IQozE3tqXaVv2zOjgla7MEGSRv95+ILmOuvhLkOK6wJtCQ=="], "minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], @@ -1732,7 +1773,9 @@ "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], - "picomatch": ["picomatch@3.0.1", "", {}, "sha512-I3EurrIQMlRc9IaAZnqRR044Phh2DXY+55o7uJ0V+hYZAcQYSuFWsc9q5PvyDHUSCe1Qxn/iBz+78s86zWnGag=="], + "picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + + "pidtree": ["pidtree@0.6.0", "", { "bin": { "pidtree": "bin/pidtree.js" } }, "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g=="], "pify": ["pify@2.3.0", "", {}, "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog=="], @@ -1748,7 +1791,7 @@ "possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="], - "postcss": ["postcss@8.5.2", "", { "dependencies": { "nanoid": "^3.3.8", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-MjOadfU3Ys9KYoX0AdkBlFEF1Vx37uCCeN4ZHnmwm9FfpbsGWMZeBLMmmpY+6Ocqod7mkdZ0DT31OlbsFrLlkA=="], + "postcss": ["postcss@8.5.3", "", { "dependencies": { "nanoid": "^3.3.8", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A=="], "postcss-calc": ["postcss-calc@8.2.4", "", { "dependencies": { "postcss-selector-parser": "^6.0.9", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.2.2" } }, "sha512-SmWMSJmB8MRnnULldx0lQIyhSNvuDl9HfrZkaqqE/WHAhToYsAvDq+yAsA/kIyINDszOp3Rh0GFoNuH5Ypsm3Q=="], @@ -1818,7 +1861,7 @@ "react-helmet-async": ["react-helmet-async@1.3.0", "", { "dependencies": { "@babel/runtime": "^7.12.5", "invariant": "^2.2.4", "prop-types": "^15.7.2", "react-fast-compare": "^3.2.0", "shallowequal": "^1.1.0" }, "peerDependencies": { "react": "^16.6.0 || ^17.0.0 || ^18.0.0", "react-dom": "^16.6.0 || ^17.0.0 || ^18.0.0" } }, "sha512-9jZ57/dAn9t3q6hneQS0wukqC2ENOBgMNVEhb/ZG9ZSxUetzVIw4iAmEU38IaVg3QGYauQPhSeUTuIUtFglWpg=="], - "react-i18next": ["react-i18next@15.4.0", "", { "dependencies": { "@babel/runtime": "^7.25.0", "html-parse-stringify": "^3.0.1" }, "peerDependencies": { "i18next": ">= 23.2.3", "react": ">= 16.8.0" } }, "sha512-Py6UkX3zV08RTvL6ZANRoBh9sL/ne6rQq79XlkHEdd82cZr2H9usbWpUNVadJntIZP2pu3M2rL1CN+5rQYfYFw=="], + "react-i18next": ["react-i18next@15.4.1", "", { "dependencies": { "@babel/runtime": "^7.25.0", "html-parse-stringify": "^3.0.1" }, "peerDependencies": { "i18next": ">= 23.2.3", "react": ">= 16.8.0" } }, "sha512-ahGab+IaSgZmNPYXdV1n+OYky95TGpFwnKRflX/16dY04DsYYKHtVLjeny7sBSCREEcoMbAgSkFiGLF5g5Oofw=="], "react-is": ["react-is@19.0.0", "", {}, "sha512-H91OHcwjZsbq3ClIDHMzBShc1rotbfACdWENsmEf0IFvZ3FgGPtdHMcsv45bQ1hAbgdfiA8SnxTKfDS+x/8m2g=="], @@ -1826,11 +1869,11 @@ "react-native-awesome-slider": ["react-native-awesome-slider@2.9.0", "", { "peerDependencies": { "react": "*", "react-native": "*", "react-native-gesture-handler": ">=2.0.0", "react-native-reanimated": ">=3.0.0" } }, "sha512-sc5qgX4YtM6IxjtosjgQLdsal120MvU+YWs0F2MdgQWijps22AXLDCUoBnZZ8vxVhVyJ2WnnIPrmtVBvVJjSuQ=="], - "react-native-bottom-tabs": ["react-native-bottom-tabs@0.8.7", "", { "dependencies": { "react-freeze": "^1.0.0", "sf-symbols-typescript": "^2.0.0", "use-latest-callback": "^0.2.1" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-cVQYs4r8Hb9V9oOO/SqsmBaZ7IzE/3Tpvz4mmRjNXKi1cBWC+ZpKTuqRx6EPjBCYTVK+vbAfoTM6IHS+6NVg4w=="], + "react-native-bottom-tabs": ["react-native-bottom-tabs@0.8.6", "", { "dependencies": { "sf-symbols-typescript": "^2.0.0", "use-latest-callback": "^0.2.1" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-N5b3MoSfsEqlmvFyIyL0X0bd+QAtB+cXH1rl/+R2Kr0BefBTC7ZldGcPhgK3FhBbt0vJDpd3kLb/dvmqZd+Eag=="], "react-native-circular-progress": ["react-native-circular-progress@1.4.1", "", { "dependencies": { "prop-types": "^15.8.1" }, "peerDependencies": { "react": ">=16.0.0", "react-native": ">=0.50.0", "react-native-svg": ">=7.0.0" } }, "sha512-HEzvI0WPuWvsCgWE3Ff2HBTMgAEQB2GvTFw0KHyD/t1STAlDDRiolu0mEGhVvihKR3jJu3v3V4qzvSklY/7XzQ=="], - "react-native-compressor": ["react-native-compressor@1.10.3", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-i51DfTwfLcKorWbTXtnPOcQC4SQDuC+DqKkSl9wF9qAUmNS9PtipYZCXOvWShYFnX0mmcWw5vwEp2b2V73PaDQ=="], + "react-native-compressor": ["react-native-compressor@1.10.4", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-58gbmJ+8IvsKP8JKK1E8XW5trfQY3dNuH7S0hYw0tSRQc6l0GZ3k8TYtoUbySOc1xcQSrUo51o0Chwe8x7mUTg=="], "react-native-country-flag": ["react-native-country-flag@2.0.2", "", {}, "sha512-5LMWxS79ZQ0Q9ntYgDYzWp794+HcQGXQmzzZNBR1AT7z5HcJHtX7rlk8RHi7RVzfp5gW6plWSZ4dKjRpu/OafQ=="], @@ -1904,7 +1947,7 @@ "readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], - "readable-web-to-node-stream": ["readable-web-to-node-stream@3.0.3", "", { "dependencies": { "process": "^0.11.10", "readable-stream": "^4.7.0" } }, "sha512-In3boYjBnbGVrLuuRu/Ath/H6h1jgk30nAsk/71tCare1dTVoe1oMBGRn5LGf0n3c1BcHwwAqpraxX4AUAP5KA=="], + "readable-web-to-node-stream": ["readable-web-to-node-stream@3.0.4", "", { "dependencies": { "readable-stream": "^4.7.0" } }, "sha512-9nX56alTf5bwXQ3ZDipHJhusu9NTQJ/CVPtb/XHAJCXihZeitfJvIRS4GqQ/mfIoOE3IelHMrpayVrosdHBuLw=="], "readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="], @@ -1950,6 +1993,8 @@ "reusify": ["reusify@1.0.4", "", {}, "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw=="], + "rfdc": ["rfdc@1.4.1", "", {}, "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA=="], + "rimraf": ["rimraf@2.7.1", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "./bin.js" } }, "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w=="], "rtl-detect": ["rtl-detect@1.1.2", "", {}, "sha512-PGMBq03+TTG/p/cRB7HCLKJ1MgDIi07+QU1faSjiYRfmY5UsAttV9Hs08jDAHVwcOwmVLcSJkpwyfXszVjWfIQ=="], @@ -2018,7 +2063,7 @@ "slash": ["slash@2.0.0", "", {}, "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A=="], - "slice-ansi": ["slice-ansi@2.1.0", "", { "dependencies": { "ansi-styles": "^3.2.0", "astral-regex": "^1.0.0", "is-fullwidth-code-point": "^2.0.0" } }, "sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ=="], + "slice-ansi": ["slice-ansi@5.0.0", "", { "dependencies": { "ansi-styles": "^6.0.0", "is-fullwidth-code-point": "^4.0.0" } }, "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ=="], "slugify": ["slugify@1.6.6", "", {}, "sha512-h+z7HKHYXj6wJU+AnS/+IH8Uh9fdcX1Lrhg1/VMdf9PwoBQXFcXiAdsy2tSK0P6gKwJLXp02r90ahUCqHk9rrw=="], @@ -2042,7 +2087,7 @@ "stackframe": ["stackframe@1.3.4", "", {}, "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw=="], - "stacktrace-parser": ["stacktrace-parser@0.1.10", "", { "dependencies": { "type-fest": "^0.7.1" } }, "sha512-KJP1OCML99+8fhOHxwwzyWrlUuVX5GQ0ZpJTd1DFXhdkrvg1szxfHhawXUZ3g9TkXORQd4/WG68jMlQZ2p8wlg=="], + "stacktrace-parser": ["stacktrace-parser@0.1.11", "", { "dependencies": { "type-fest": "^0.7.1" } }, "sha512-WjlahMgHmCJpqzU8bIBy4qtsZdU9lRlcZE3Lvyej6t4tuOuv1vk57OW3MBrj6hXBFx/nNoC9MPMTcr5YA7NQbg=="], "statuses": ["statuses@2.0.1", "", {}, "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="], @@ -2052,6 +2097,8 @@ "strict-uri-encode": ["strict-uri-encode@2.0.0", "", {}, "sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ=="], + "string-argv": ["string-argv@0.3.2", "", {}, "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q=="], + "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], "string-width-cjs": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], @@ -2068,7 +2115,7 @@ "strip-json-comments": ["strip-json-comments@2.0.1", "", {}, "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ=="], - "strnum": ["strnum@1.0.5", "", {}, "sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA=="], + "strnum": ["strnum@1.1.1", "", {}, "sha512-O7aCHfYCamLCctjAiaucmE+fHf2DYHkus2OKCn4Wv03sykfFtgeECn505X6K4mPl8CRNd/qurC9guq+ynoN4pw=="], "strtok3": ["strtok3@6.3.0", "", { "dependencies": { "@tokenizer/token": "^0.3.0", "peek-readable": "^4.1.0" } }, "sha512-fZtbhtvI9I48xDSywd/somNqgUHl2L2cstmXCCif0itOf96jeW18MBSyrLuNicYQVkvpOxkZtkzujiTJ9LW5Jw=="], @@ -2192,7 +2239,7 @@ "utils-merge": ["utils-merge@1.0.1", "", {}, "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA=="], - "uuid": ["uuid@11.0.5", "", { "bin": { "uuid": "dist/esm/bin/uuid" } }, "sha512-508e6IcKLrhxKdBbcA2b4KQZlLVp2+J5UwQ6F7Drckkc5N9ZJwFa4TgWtsww9UG8fGHbm6gbV19TdM5pQ4GaIA=="], + "uuid": ["uuid@11.1.0", "", { "bin": { "uuid": "dist/esm/bin/uuid" } }, "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A=="], "validate-npm-package-name": ["validate-npm-package-name@5.0.1", "", {}, "sha512-OljLrQ9SQdOUqTaQxqL5dEfZWrXExyyWsozYlAWFawPVNuD83igl7uJD2RTkNMbniIYgt8l81eCJGIdQF7avLQ=="], @@ -2228,7 +2275,7 @@ "wonka": ["wonka@6.3.4", "", {}, "sha512-CjpbqNtBGNAeyNS/9W6q3kSkKE52+FjIj7AkFlLr11s/VWGUu6a2CdYSdGxocIhIVjaW/zchesBQUKPVU69Cqg=="], - "wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], + "wrap-ansi": ["wrap-ansi@9.0.0", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q=="], "wrap-ansi-cjs": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], @@ -2286,15 +2333,19 @@ "@expo/cli/arg": ["arg@5.0.2", "", {}, "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg=="], - "@expo/cli/form-data": ["form-data@3.0.2", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "mime-types": "^2.1.12" } }, "sha512-sJe+TQb2vIaIyO783qN6BlMYWMw3WBOHA1Ay2qxsnjuafEOQFJ2JakedOQirT6D5XPRxDvS7AHYyem9fTpb4LQ=="], + "@expo/cli/form-data": ["form-data@3.0.3", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "mime-types": "^2.1.35" } }, "sha512-q5YBMeWy6E2Un0nMGWMgI65MAKtaylxfNJGJxpGh45YDciZB4epbWpaAfImil6CPAPTYB4sh0URQNDRIZG5F2w=="], "@expo/cli/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], "@expo/cli/ora": ["ora@3.4.0", "", { "dependencies": { "chalk": "^2.4.2", "cli-cursor": "^2.1.0", "cli-spinners": "^2.0.0", "log-symbols": "^2.2.0", "strip-ansi": "^5.2.0", "wcwidth": "^1.0.1" } }, "sha512-eNwHudNbO1folBP3JsZ19v9azXWtQZjICdr3Q0TDPIaeBQ3mXLrh54wM+er0+hSp+dWKf+Z8KM58CYzEyIYxYg=="], + "@expo/cli/picomatch": ["picomatch@3.0.1", "", {}, "sha512-I3EurrIQMlRc9IaAZnqRR044Phh2DXY+55o7uJ0V+hYZAcQYSuFWsc9q5PvyDHUSCe1Qxn/iBz+78s86zWnGag=="], + "@expo/cli/semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="], - "@expo/cli/ws": ["ws@8.18.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw=="], + "@expo/cli/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], + + "@expo/cli/ws": ["ws@8.18.1", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w=="], "@expo/config/@babel/code-frame": ["@babel/code-frame@7.10.4", "", { "dependencies": { "@babel/highlight": "^7.10.4" } }, "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg=="], @@ -2410,9 +2461,9 @@ "accepts/negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="], - "ansi-escapes/type-fest": ["type-fest@0.21.3", "", {}, "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w=="], + "ansi-fragments/colorette": ["colorette@1.4.0", "", {}, "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g=="], - "anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + "ansi-fragments/slice-ansi": ["slice-ansi@2.1.0", "", { "dependencies": { "ansi-styles": "^3.2.0", "astral-regex": "^1.0.0", "is-fullwidth-code-point": "^2.0.0" } }, "sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ=="], "babel-jest/slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="], @@ -2428,8 +2479,12 @@ "chromium-edge-launcher/rimraf": ["rimraf@3.0.2", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "bin.js" } }, "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA=="], + "cli-truncate/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], + "cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + "cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], + "compressible/mime-db": ["mime-db@1.53.0", "", {}, "sha512-oHlN/w+3MQ3rba9rqFr6V/ypF10LSkdwUysQL7GkXoTgIWeV+tcXGA852TBxH+gsh8UWoyhR1hKcoMJTuWflpg=="], "compression/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], @@ -2484,8 +2539,6 @@ "jest-message-util/slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="], - "jest-util/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], - "jest-worker/supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="], "jscodeshift/tmp": ["tmp@0.2.3", "", {}, "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w=="], @@ -2494,10 +2547,22 @@ "lighthouse-logger/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], + "lint-staged/chalk": ["chalk@5.4.1", "", {}, "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w=="], + + "lint-staged/commander": ["commander@13.1.0", "", {}, "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw=="], + + "lint-staged/execa": ["execa@8.0.1", "", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^8.0.1", "human-signals": "^5.0.0", "is-stream": "^3.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^5.1.0", "onetime": "^6.0.0", "signal-exit": "^4.1.0", "strip-final-newline": "^3.0.0" } }, "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg=="], + "load-bmfont/mime": ["mime@1.6.0", "", { "bin": { "mime": "cli.js" } }, "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="], "load-bmfont/phin": ["phin@3.7.1", "", { "dependencies": { "centra": "^2.7.0" } }, "sha512-GEazpTWwTZaEQ9RhL7Nyz0WwqilbqgLahDM3D0hxWwmVDI52nXEybHqiN6/elwpkJBhcuj+WbBu+QfT0uhPGfQ=="], + "log-update/cli-cursor": ["cli-cursor@5.0.0", "", { "dependencies": { "restore-cursor": "^5.0.0" } }, "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw=="], + + "log-update/slice-ansi": ["slice-ansi@7.1.0", "", { "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^5.0.0" } }, "sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg=="], + + "log-update/strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="], + "logkitty/yargs": ["yargs@15.4.1", "", { "dependencies": { "cliui": "^6.0.0", "decamelize": "^1.2.0", "find-up": "^4.1.0", "get-caller-file": "^2.0.1", "require-directory": "^2.1.1", "require-main-filename": "^2.0.0", "set-blocking": "^2.0.0", "string-width": "^4.2.0", "which-module": "^2.0.0", "y18n": "^4.0.0", "yargs-parser": "^18.1.2" } }, "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A=="], "make-dir/pify": ["pify@4.0.1", "", {}, "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g=="], @@ -2514,8 +2579,6 @@ "metro-file-map/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], - "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], - "minipass-flush/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], "minipass-pipeline/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], @@ -2536,6 +2599,8 @@ "parse-bmfont-xml/xml2js": ["xml2js@0.5.0", "", { "dependencies": { "sax": ">=0.6.0", "xmlbuilder": "~11.0.0" } }, "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA=="], + "password-prompt/ansi-escapes": ["ansi-escapes@4.3.2", "", { "dependencies": { "type-fest": "^0.21.3" } }, "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ=="], + "patch-package/fs-extra": ["fs-extra@9.1.0", "", { "dependencies": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ=="], "patch-package/semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="], @@ -2550,8 +2615,6 @@ "postcss-css-variables/escape-string-regexp": ["escape-string-regexp@1.0.5", "", {}, "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="], - "postcss-load-config/lilconfig": ["lilconfig@3.1.3", "", {}, "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw=="], - "pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], "pretty-format/react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], @@ -2588,8 +2651,6 @@ "readable-web-to-node-stream/readable-stream": ["readable-stream@4.7.0", "", { "dependencies": { "abort-controller": "^3.0.0", "buffer": "^6.0.3", "events": "^3.3.0", "process": "^0.11.10", "string_decoder": "^1.3.0" } }, "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg=="], - "readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], - "recast/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], "regjsparser/jsesc": ["jsesc@3.0.2", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g=="], @@ -2608,9 +2669,9 @@ "simple-plist/bplist-parser": ["bplist-parser@0.3.1", "", { "dependencies": { "big-integer": "1.6.x" } }, "sha512-PyJxiNtA5T2PlLIeBot4lbp7rj4OadzjnMZD/G5zuBNt8ei/yCU7+wW0h2bag9vr8c+/WuRWmSxbqAl9hL1rBA=="], - "slice-ansi/ansi-styles": ["ansi-styles@3.2.1", "", { "dependencies": { "color-convert": "^1.9.0" } }, "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA=="], + "slice-ansi/ansi-styles": ["ansi-styles@6.2.1", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="], - "slice-ansi/is-fullwidth-code-point": ["is-fullwidth-code-point@2.0.0", "", {}, "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w=="], + "slice-ansi/is-fullwidth-code-point": ["is-fullwidth-code-point@4.0.0", "", {}, "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ=="], "source-map-support/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], @@ -2626,6 +2687,8 @@ "tailwindcss/arg": ["arg@5.0.2", "", {}, "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg=="], + "tailwindcss/lilconfig": ["lilconfig@2.1.0", "", {}, "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ=="], + "tailwindcss/postcss-nested": ["postcss-nested@6.2.0", "", { "dependencies": { "postcss-selector-parser": "^6.1.1" }, "peerDependencies": { "postcss": "^8.2.14" } }, "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ=="], "tar/fs-minipass": ["fs-minipass@2.1.0", "", { "dependencies": { "minipass": "^3.0.0" } }, "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg=="], @@ -2640,6 +2703,8 @@ "tempy/type-fest": ["type-fest@0.16.0", "", {}, "sha512-eaBzG6MxNzEn9kiwvtre90cXaNLkmadMWa1zQMs3XORCXNbsH/OewwbxC5ia9dCxIxnTAsSxXJaa/p5y8DlvJg=="], + "terminal-link/ansi-escapes": ["ansi-escapes@4.3.2", "", { "dependencies": { "type-fest": "^0.21.3" } }, "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ=="], + "terser/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="], "test-exclude/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], @@ -2650,7 +2715,11 @@ "whatwg-url/webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="], - "wrap-ansi/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + "wrap-ansi/ansi-styles": ["ansi-styles@6.2.1", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="], + + "wrap-ansi/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], + + "wrap-ansi/strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="], "wrap-ansi-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], @@ -2672,6 +2741,8 @@ "@expo/cli/ora/log-symbols": ["log-symbols@2.2.0", "", { "dependencies": { "chalk": "^2.0.1" } }, "sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg=="], + "@expo/cli/wrap-ansi/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + "@expo/fingerprint/minimatch/brace-expansion": ["brace-expansion@1.1.11", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA=="], "@expo/image-utils/fs-extra/jsonfile": ["jsonfile@6.1.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ=="], @@ -2730,8 +2801,16 @@ "@react-native/metro-babel-transformer/@react-native/babel-preset/@react-native/babel-plugin-codegen": ["@react-native/babel-plugin-codegen@0.77.0", "", { "dependencies": { "@babel/traverse": "^7.25.3", "@react-native/codegen": "0.77.0" } }, "sha512-5TYPn1k+jdDOZJU4EVb1kZ0p9TCVICXK3uplRev5Gul57oWesAaiWGZOzfRS3lonWeuR4ij8v8PFfIHOaq0vmA=="], + "ansi-fragments/slice-ansi/ansi-styles": ["ansi-styles@3.2.1", "", { "dependencies": { "color-convert": "^1.9.0" } }, "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA=="], + + "ansi-fragments/slice-ansi/is-fullwidth-code-point": ["is-fullwidth-code-point@2.0.0", "", {}, "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w=="], + "chromium-edge-launcher/rimraf/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], + "cli-truncate/string-width/emoji-regex": ["emoji-regex@10.4.0", "", {}, "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw=="], + + "cli-truncate/string-width/strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="], + "compression/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], "connect/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], @@ -2756,6 +2835,28 @@ "lighthouse-logger/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], + "lint-staged/execa/get-stream": ["get-stream@8.0.1", "", {}, "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA=="], + + "lint-staged/execa/human-signals": ["human-signals@5.0.0", "", {}, "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ=="], + + "lint-staged/execa/is-stream": ["is-stream@3.0.0", "", {}, "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA=="], + + "lint-staged/execa/npm-run-path": ["npm-run-path@5.3.0", "", { "dependencies": { "path-key": "^4.0.0" } }, "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ=="], + + "lint-staged/execa/onetime": ["onetime@6.0.0", "", { "dependencies": { "mimic-fn": "^4.0.0" } }, "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ=="], + + "lint-staged/execa/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], + + "lint-staged/execa/strip-final-newline": ["strip-final-newline@3.0.0", "", {}, "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw=="], + + "log-update/cli-cursor/restore-cursor": ["restore-cursor@5.1.0", "", { "dependencies": { "onetime": "^7.0.0", "signal-exit": "^4.1.0" } }, "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA=="], + + "log-update/slice-ansi/ansi-styles": ["ansi-styles@6.2.1", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="], + + "log-update/slice-ansi/is-fullwidth-code-point": ["is-fullwidth-code-point@5.0.0", "", { "dependencies": { "get-east-asian-width": "^1.0.0" } }, "sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA=="], + + "log-update/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="], + "logkitty/yargs/cliui": ["cliui@6.0.0", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", "wrap-ansi": "^6.2.0" } }, "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ=="], "logkitty/yargs/find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="], @@ -2782,6 +2883,8 @@ "parse-bmfont-xml/xml2js/xmlbuilder": ["xmlbuilder@11.0.1", "", {}, "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA=="], + "password-prompt/ansi-escapes/type-fest": ["type-fest@0.21.3", "", {}, "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w=="], + "patch-package/fs-extra/jsonfile": ["jsonfile@6.1.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ=="], "patch-package/fs-extra/universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="], @@ -2798,14 +2901,18 @@ "send/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], - "slice-ansi/ansi-styles/color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="], - "tar/fs-minipass/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], "temp/rimraf/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], + "terminal-link/ansi-escapes/type-fest": ["type-fest@0.21.3", "", {}, "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w=="], + "test-exclude/minimatch/brace-expansion": ["brace-expansion@1.1.11", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA=="], + "wrap-ansi/string-width/emoji-regex": ["emoji-regex@10.4.0", "", {}, "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw=="], + + "wrap-ansi/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="], + "@babel/highlight/chalk/ansi-styles/color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="], "@babel/highlight/chalk/supports-color/has-flag": ["has-flag@3.0.0", "", {}, "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw=="], @@ -2840,8 +2947,12 @@ "@react-native/codegen/glob/minimatch/brace-expansion": ["brace-expansion@1.1.11", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA=="], + "ansi-fragments/slice-ansi/ansi-styles/color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="], + "chromium-edge-launcher/rimraf/glob/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], + "cli-truncate/string-width/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="], + "default-gateway/execa/cross-spawn/path-key": ["path-key@2.0.1", "", {}, "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw=="], "default-gateway/execa/cross-spawn/semver": ["semver@5.7.2", "", { "bin": { "semver": "bin/semver" } }, "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g=="], @@ -2854,6 +2965,14 @@ "del/rimraf/glob/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], + "lint-staged/execa/npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="], + + "lint-staged/execa/onetime/mimic-fn": ["mimic-fn@4.0.0", "", {}, "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw=="], + + "log-update/cli-cursor/restore-cursor/onetime": ["onetime@7.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="], + + "log-update/cli-cursor/restore-cursor/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], + "logkitty/yargs/cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], "logkitty/yargs/cliui/wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="], @@ -2876,8 +2995,6 @@ "rimraf/glob/minimatch/brace-expansion": ["brace-expansion@1.1.11", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA=="], - "slice-ansi/ansi-styles/color-convert/color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="], - "temp/rimraf/glob/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], "@babel/highlight/chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="], @@ -2902,6 +3019,8 @@ "@react-native/babel-plugin-codegen/@react-native/codegen/jscodeshift/recast/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + "ansi-fragments/slice-ansi/ansi-styles/color-convert/color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="], + "chromium-edge-launcher/rimraf/glob/minimatch/brace-expansion": ["brace-expansion@1.1.11", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA=="], "default-gateway/execa/cross-spawn/shebang-command/shebang-regex": ["shebang-regex@1.0.0", "", {}, "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ=="], diff --git a/components/AddToFavorites.tsx b/components/AddToFavorites.tsx index 2f53bbbd..16e15694 100644 --- a/components/AddToFavorites.tsx +++ b/components/AddToFavorites.tsx @@ -1,113 +1,23 @@ -import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; -import { 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 { useMemo } from "react"; -import { TouchableOpacityProps, View, ViewProps } from "react-native"; -import { RoundButton } from "./RoundButton"; +import { RoundButton } from "@/components/RoundButton"; +import { useFavorite } from "@/hooks/useFavorite"; +import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client"; +import type { FC } from "react"; +import { View, type ViewProps } from "react-native"; interface Props extends ViewProps { item: BaseItemDto; - type: "item" | "series"; } -export const AddToFavorites: React.FC = ({ item, type, ...props }) => { - const queryClient = useQueryClient(); - const [api] = useAtom(apiAtom); - const [user] = useAtom(userAtom); - - const isFavorite = useMemo(() => { - return item.UserData?.IsFavorite; - }, [item.UserData?.IsFavorite]); - - const updateItemInQueries = (newData: Partial) => { - queryClient.setQueryData( - [type, item.Id], - (old) => { - if (!old) return old; - return { - ...old, - ...newData, - UserData: { ...old.UserData, ...newData.UserData }, - }; - } - ); - }; - - const markFavoriteMutation = useMutation({ - mutationFn: async () => { - if (api && user) { - await getUserLibraryApi(api).markFavoriteItem({ - userId: user.Id, - itemId: item.Id!, - }); - } - }, - onMutate: async () => { - await queryClient.cancelQueries({ queryKey: [type, item.Id] }); - const previousItem = queryClient.getQueryData([ - type, - item.Id, - ]); - updateItemInQueries({ UserData: { IsFavorite: true } }); - - return { previousItem }; - }, - onError: (err, variables, context) => { - if (context?.previousItem) { - queryClient.setQueryData([type, item.Id], context.previousItem); - } - }, - onSettled: () => { - queryClient.invalidateQueries({ queryKey: [type, item.Id] }); - queryClient.invalidateQueries({ queryKey: ["home", "favorites"] }); - }, - }); - - const unmarkFavoriteMutation = useMutation({ - mutationFn: async () => { - if (api && user) { - await getUserLibraryApi(api).unmarkFavoriteItem({ - userId: user.Id, - itemId: item.Id!, - }); - } - }, - onMutate: async () => { - await queryClient.cancelQueries({ queryKey: [type, item.Id] }); - const previousItem = queryClient.getQueryData([ - type, - item.Id, - ]); - updateItemInQueries({ UserData: { IsFavorite: false } }); - - return { previousItem }; - }, - onError: (err, variables, context) => { - if (context?.previousItem) { - queryClient.setQueryData([type, item.Id], context.previousItem); - } - }, - onSettled: () => { - queryClient.invalidateQueries({ queryKey: [type, item.Id] }); - queryClient.invalidateQueries({ queryKey: ["home", "favorites"] }); - }, - }); +export const AddToFavorites: FC = ({ item, ...props }) => { + const { isFavorite, toggleFavorite } = useFavorite(item); return ( { - if (isFavorite) { - unmarkFavoriteMutation.mutate(); - } else { - markFavoriteMutation.mutate(); - } - }} + onPress={toggleFavorite} /> ); diff --git a/components/AudioTrackSelector.tsx b/components/AudioTrackSelector.tsx index ad4b08c0..c62140ab 100644 --- a/components/AudioTrackSelector.tsx +++ b/components/AudioTrackSelector.tsx @@ -1,9 +1,9 @@ -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; @@ -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 befc34ca..1ea49626 100644 --- a/components/DownloadItem.tsx +++ b/components/DownloadItem.tsx @@ -2,7 +2,7 @@ import { useRemuxHlsToMp4 } from "@/hooks/useRemuxHlsToMp4"; import { useDownload } from "@/providers/DownloadProvider"; import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; import { queueActions, queueAtom } from "@/utils/atoms/queue"; -import {DownloadMethod, useSettings} from "@/utils/atoms/settings"; +import { DownloadMethod, useSettings } from "@/utils/atoms/settings"; import { getDefaultPlaySettings } from "@/utils/jellyfin/getDefaultPlaySettings"; import { getStreamUrl } from "@/utils/jellyfin/media/getStreamUrl"; import { saveDownloadItemInfoToDiskTmp } from "@/utils/optimize-server"; @@ -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, 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[]; @@ -66,18 +67,20 @@ export const DownloadItems: React.FC = ({ const [selectedAudioStream, setSelectedAudioStream] = useState(-1); const [selectedSubtitleStream, setSelectedSubtitleStream] = useState(0); - const [maxBitrate, setMaxBitrate] = useState(settings?.defaultBitrate ?? { - key: "Max", - value: undefined, - }); + const [maxBitrate, setMaxBitrate] = useState( + 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); @@ -97,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(() => { @@ -106,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(() => { @@ -138,7 +141,7 @@ export const DownloadItems: React.FC = ({ params: { episodeSeasonIndex: firstItem.ParentIndexNumber, }, - } as Href) + } as Href), ); }; @@ -158,11 +161,13 @@ 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")); + toast.error( + t("home.downloads.toasts.you_are_not_allowed_to_download_files"), + ); } }, [ queue, @@ -185,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; @@ -216,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; } @@ -246,7 +251,7 @@ export const DownloadItems: React.FC = ({ usingOptimizedServer, startBackgroundDownload, startRemuxing, - ] + ], ); const renderBackdrop = useCallback( @@ -257,7 +262,7 @@ export const DownloadItems: React.FC = ({ appearsOnIndex={0} /> ), - [] + [], ); useFocusEffect( useCallback(() => { @@ -270,7 +275,7 @@ export const DownloadItems: React.FC = ({ setSelectedAudioStream(audioIndex ?? 0); setSelectedSubtitleStream(subtitleIndex ?? -1); setMaxBitrate(bitrate); - }, [items, itemsNotDownloaded, settings]) + }, [items, itemsNotDownloaded, settings]), ); const renderButtonContent = () => { @@ -278,18 +283,18 @@ export const DownloadItems: React.FC = ({ return progress === 0 ? ( ) : ( - + ); } else if (itemsQueued) { - return ; + return ; } else if (allItemsDownloaded) { return ; } else { @@ -327,16 +332,19 @@ export const DownloadItems: React.FC = ({ backdropComponent={renderBackdrop} > - + - + {title} - - {subtitle || t("item_card.download.download_x_item", {item_count: itemsNotDownloaded.length})} + + {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")} @@ -391,19 +399,23 @@ export const DownloadSingleItem: React.FC<{ size?: "default" | "large"; item: BaseItemDto; }> = ({ item, size = "default" }) => { + if (Platform.isTV) return; + return ( ( - + )} DownloadedIconComponent={() => ( - + )} /> ); diff --git a/components/GenreTags.tsx b/components/GenreTags.tsx index 35de54a6..6708269d 100644 --- a/components/GenreTags.tsx +++ b/components/GenreTags.tsx @@ -1,44 +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", ...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 dd9176b0..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 39aa1660..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"; @@ -15,26 +15,26 @@ import { SeasonEpisodesCarousel } from "@/components/series/SeasonEpisodesCarous import useDefaultPlaySettings from "@/hooks/useDefaultPlaySettings"; import { useImageColors } from "@/hooks/useImageColors"; import { useOrientation } from "@/hooks/useOrientation"; +import * as ScreenOrientation from "@/packages/expo-screen-orientation"; import { apiAtom } from "@/providers/JellyfinProvider"; import { useSettings } from "@/utils/atoms/settings"; import { getLogoImageUrlById } from "@/utils/jellyfin/image/getLogoImageUrlById"; -import { +import type { BaseItemDto, MediaSourceInfo, } from "@jellyfin/sdk/lib/generated-client/models"; import { Image } from "expo-image"; import { useNavigation } from "expo-router"; -import * as ScreenOrientation from "@/packages/expo-screen-orientation"; import { useAtom } from "jotai"; import React, { useEffect, useMemo, useState } from "react"; import { Platform, View } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; -const Chromecast = !Platform.isTV ? require("./Chromecast") : null; +import { AddToFavorites } from "./AddToFavorites"; import { ItemHeader } from "./ItemHeader"; import { ItemTechnicalDetails } from "./ItemTechnicalDetails"; import { MediaSourceSelector } from "./MediaSourceSelector"; import { MoreMoviesWithActor } from "./MoreMoviesWithActor"; -import { AddToFavorites } from "./AddToFavorites"; +const Chromecast = !Platform.isTV ? require("./Chromecast") : null; export type SelectedOptions = { bitrate: Bitrate; @@ -86,17 +86,19 @@ export const ItemContent: React.FC<{ item: BaseItemDto }> = React.memo( navigation.setOptions({ headerRight: () => item && ( - + {item.Type !== "Program" && ( - - - - + + {!Platform.isTV && ( + + )} + + )} @@ -121,7 +123,7 @@ export const ItemContent: React.FC<{ item: BaseItemDto }> = React.memo( return ( = React.memo( } > - - {/* {!Platform.isTV && ( */} - - + + + {item.Type !== "Program" && !Platform.isTV && ( - + setSelectedOptions( - (prev) => prev && { ...prev, bitrate: val } + (prev) => prev && { ...prev, bitrate: val }, ) } selected={selectedOptions.bitrate} /> setSelectedOptions( @@ -187,13 +188,13 @@ export const ItemContent: React.FC<{ item: BaseItemDto }> = React.memo( prev && { ...prev, mediaSource: val, - } + }, ) } selected={selectedOptions.mediaSource} /> { setSelectedOptions( @@ -201,7 +202,7 @@ export const ItemContent: React.FC<{ item: BaseItemDto }> = React.memo( prev && { ...prev, audioIndex: val, - } + }, ); }} selected={selectedOptions.audioIndex} @@ -214,7 +215,7 @@ export const ItemContent: React.FC<{ item: BaseItemDto }> = React.memo( prev && { ...prev, subtitleIndex: val, - } + }, ) } selected={selectedOptions.subtitleIndex} @@ -222,13 +223,11 @@ export const ItemContent: React.FC<{ item: BaseItemDto }> = React.memo( )} - {/* {!Platform.isTV && ( */} - {/* )} */} {item.Type === "Episode" && ( @@ -236,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) => ( ))} @@ -266,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 6b5852a4..b0354fa0 100644 --- a/components/ItemTechnicalDetails.tsx +++ b/components/ItemTechnicalDetails.tsx @@ -1,21 +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 { Text } from "./common/Text"; interface Props { source?: MediaSourceInfo; @@ -26,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.video")} + + - - {t("item_card.audio")} + + + {t("item_card.audio")} + stream.Type === "Audio" + (stream) => stream.Type === "Audio", ) || [] } /> - - {t("item_card.subtitles")} + + + {t("item_card.subtitles")} + stream.Type === "Subtitle" + (stream) => stream.Type === "Subtitle", ) || [] } /> @@ -94,25 +102,25 @@ const SubtitleStreamInfo = ({ subtitleStreams: MediaStream[]; }) => { return ( - + {subtitleStreams.map((stream, index) => ( - - + + {stream.DisplayTitle} - + + } text={stream.Language} /> + } /> @@ -124,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)} /> @@ -173,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`} /> @@ -226,15 +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]; }; - -const formatBitrate = (bitrate?: number | null) => { - if (!bitrate) return "N/A"; - - 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()); - return Math.round((bitrate / Math.pow(1000, 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; @@ -72,13 +71,14 @@ export const PlayButton: React.FC = ({ const lightHapticFeedback = useHaptic("light"); const goToPlayer = useCallback( - (q: string, bitrateValue: number | undefined) => { + (q: string) => { router.push(`/player/direct-player?${q}`); }, - [router] + [router], ); const onPress = useCallback(async () => { + console.log("onPress"); if (!item) return; lightHapticFeedback(); @@ -94,7 +94,7 @@ export const PlayButton: React.FC = ({ const queryString = queryParams.toString(); if (!client) { - goToPlayer(queryString, selectedOptions.bitrate?.value); + goToPlayer(queryString); return; } @@ -113,59 +113,63 @@ export const PlayButton: React.FC = ({ switch (selectedIndex) { case 0: - if (!Platform.isTV) { - await CastContext.getPlayServicesState().then(async (state) => { - if (state && state !== PlayServicesState.SUCCESS) { - CastContext.showPlayServicesErrorDialog(state); - } else { - // Get a new URL with the Chromecast device profile: - try { - const data = await getStreamUrl({ - api, - item, - deviceProfile: chromecastProfile, - startTimeTicks: item?.UserData?.PlaybackPositionTicks!, - userId: user?.Id, - audioStreamIndex: selectedOptions.audioIndex, - maxStreamingBitrate: selectedOptions.bitrate?.value, - mediaSourceId: selectedOptions.mediaSource?.Id, - subtitleStreamIndex: selectedOptions.subtitleIndex, - }); + await CastContext.getPlayServicesState().then(async (state) => { + if (state && state !== PlayServicesState.SUCCESS) { + CastContext.showPlayServicesErrorDialog(state); + } else { + // Check if user wants H265 for Chromecast + const enableH265 = settings.enableH265ForChromecast; - if (!data?.url) { - console.warn("No URL returned from getStreamUrl", data); - Alert.alert( - t("player.client_error"), - t("player.could_not_create_stream_for_chromecast") - ); - return; - } + // Get a new URL with the Chromecast device profile + try { + const data = await getStreamUrl({ + api, + item, + deviceProfile: enableH265 ? chromecasth265 : chromecast, + startTimeTicks: item?.UserData?.PlaybackPositionTicks!, + userId: user?.Id, + audioStreamIndex: selectedOptions.audioIndex, + maxStreamingBitrate: selectedOptions.bitrate?.value, + mediaSourceId: selectedOptions.mediaSource?.Id, + subtitleStreamIndex: selectedOptions.subtitleIndex, + }); - client - .loadMedia({ - mediaInfo: { - contentUrl: data?.url, - contentType: "video/mp4", - metadata: - item.Type === "Episode" - ? { - type: "tvShow", - title: item.Name || "", - episodeNumber: item.IndexNumber || 0, - seasonNumber: item.ParentIndexNumber || 0, - seriesTitle: item.SeriesName || "", - images: [ - { - url: getParentBackdropImageUrl({ - api, - item, - quality: 90, - width: 2000, - })!, - }, - ], - } - : item.Type === "Movie" + console.log("URL: ", data?.url, enableH265); + + if (!data?.url) { + console.warn("No URL returned from getStreamUrl", data); + Alert.alert( + t("player.client_error"), + t("player.could_not_create_stream_for_chromecast"), + ); + return; + } + + client + .loadMedia({ + mediaInfo: { + contentUrl: data?.url, + contentType: "video/mp4", + metadata: + item.Type === "Episode" + ? { + type: "tvShow", + title: item.Name || "", + episodeNumber: item.IndexNumber || 0, + seasonNumber: item.ParentIndexNumber || 0, + seriesTitle: item.SeriesName || "", + images: [ + { + url: getParentBackdropImageUrl({ + api, + item, + quality: 90, + width: 2000, + })!, + }, + ], + } + : item.Type === "Movie" ? { type: "movie", title: item.Name || "", @@ -196,30 +200,29 @@ export const PlayButton: React.FC = ({ }, ], }, - }, - startTime: 0, - }) - .then(() => { - // state is already set when reopening current media, so skip it here. - if (isOpeningCurrentlyPlayingMedia) { - return; - } - CastContext.showExpandedControls(); - }); - } catch (e) { - console.log(e); - } + }, + startTime: 0, + }) + .then(() => { + // state is already set when reopening current media, so skip it here. + if (isOpeningCurrentlyPlayingMedia) { + return; + } + CastContext.showExpandedControls(); + }); + } catch (e) { + console.log(e); } - }); - } + } + }); break; case 1: - goToPlayer(queryString, selectedOptions.bitrate?.value); + goToPlayer(queryString); break; case cancelButtonIndex: break; } - } + }, ); }, [ item, @@ -240,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; } @@ -257,7 +260,7 @@ export const PlayButton: React.FC = ({ easing: Easing.bezier(0.7, 0, 0.3, 1.0), }); }, - [item] + [item], ); useAnimatedReaction( @@ -270,7 +273,7 @@ export const PlayButton: React.FC = ({ easing: Easing.bezier(0.9, 0, 0.31, 0.99), }); }, - [colorAtom] + [colorAtom], ); useEffect(() => { @@ -291,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], ), })); @@ -299,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], ), })); @@ -307,7 +310,7 @@ export const PlayButton: React.FC = ({ width: `${interpolate( widthProgress.value, [0, 1], - [startWidth.value, targetWidth.value] + [startWidth.value, targetWidth.value], )}%`, })); @@ -315,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], ), })); /** @@ -323,75 +326,62 @@ export const PlayButton: React.FC = ({ */ return ( - - - - - - + + - - - - {runtimeTicksToMinutes(item?.RunTimeTicks)} - + + + + + + + {runtimeTicksToMinutes(item?.RunTimeTicks)} + + + + + {client && ( - + + - {client && ( - - - - - )} - {!client && settings?.openInVLC && ( - - - - )} - + )} + {!client && settings?.openInVLC && ( + + + + )} - - {/* - - - {directStream ? "Direct stream" : "Transcoded stream"} - - */} - + + ); }; diff --git a/components/PlayButton.tv.tsx b/components/PlayButton.tv.tsx index 128c2184..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; @@ -57,13 +57,14 @@ export const PlayButton: React.FC = ({ const lightHapticFeedback = useHaptic("light"); const goToPlayer = useCallback( - (q: string, bitrateValue: number | undefined) => { + (q: string) => { router.push(`/player/direct-player?${q}`); }, - [router] + [router], ); - const onPress = useCallback(async () => { + const onPress = () => { + console.log("onpress"); if (!item) return; lightHapticFeedback(); @@ -77,17 +78,9 @@ export const PlayButton: React.FC = ({ }); const queryString = queryParams.toString(); - goToPlayer(queryString, selectedOptions.bitrate?.value); + goToPlayer(queryString); return; - }, [ - item, - settings, - api, - user, - router, - showActionSheetWithOptions, - selectedOptions, - ]); + }; const derivedTargetWidth = useDerivedValue(() => { if (!item || !item.RunTimeTicks) return 0; @@ -96,7 +89,7 @@ export const PlayButton: React.FC = ({ return userData.PlaybackPositionTicks > 0 ? Math.max( (userData.PlaybackPositionTicks / item.RunTimeTicks) * 100, - MIN_PLAYBACK_WIDTH + MIN_PLAYBACK_WIDTH, ) : 0; } @@ -113,7 +106,7 @@ export const PlayButton: React.FC = ({ easing: Easing.bezier(0.7, 0, 0.3, 1.0), }); }, - [item] + [item], ); useAnimatedReaction( @@ -126,7 +119,7 @@ export const PlayButton: React.FC = ({ easing: Easing.bezier(0.9, 0, 0.31, 0.99), }); }, - [colorAtom] + [colorAtom], ); useEffect(() => { @@ -147,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], ), })); @@ -155,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], ), })); @@ -163,7 +156,7 @@ export const PlayButton: React.FC = ({ width: `${interpolate( widthProgress.value, [0, 1], - [startWidth.value, targetWidth.value] + [startWidth.value, targetWidth.value], )}%`, })); @@ -171,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], ), })); /** @@ -179,69 +172,55 @@ export const PlayButton: React.FC = ({ */ return ( - - - - - - + + - - - - {runtimeTicksToMinutes(item?.RunTimeTicks)} - + + + + + + + {runtimeTicksToMinutes(item?.RunTimeTicks)} + + + + + {settings?.openInVLC && ( - + - {settings?.openInVLC && ( - - - - )} - + )} - - {/* - - - {directStream ? "Direct stream" : "Transcoded stream"} - - */} - + + ); }; 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 64d3d83b..bb6e6107 100644 --- a/components/Ratings.tsx +++ b/components/Ratings.tsx @@ -1,12 +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 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; @@ -15,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 }> = ({ - result, -}) => { - const { jellyseerrApi } = useJellyseerr(); +export const JellyserrRatings: React.FC<{ + result: MovieResult | TvResult | TvDetails | MovieDetails; +}> = ({ result }) => { + const { jellyseerrApi, getMediaType } = useJellyseerr(); + + const mediaType = useMemo(() => getMediaType(result), [result]); + const { data, isLoading } = useQuery({ - queryKey: ["jellyseerr", result.id, result.mediaType, "ratings"], + queryKey: ["jellyseerr", result.id, mediaType, "ratings"], queryFn: async () => { - return result.mediaType === MediaType.MOVIE + return mediaType === MediaType.MOVIE ? jellyseerrApi?.movieRatings(result.id) : jellyseerrApi?.tvRatings(result.id); }, @@ -70,14 +79,14 @@ export const JellyserrRatings: React.FC<{ result: MovieResult | TvResult }> = ({ !!result.voteCount || (data?.criticsRating && !!data?.criticsScore) || (data?.audienceRating && !!data?.audienceScore)) && ( - + {data?.criticsRating && !!data?.criticsScore && ( = ({ {data?.audienceRating && !!data?.audienceScore && ( = ({ {!!result.voteCount && ( 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 77e26c1b..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[]; @@ -18,10 +18,10 @@ interface Props { title: string | ReactNode; label: string; onSelected: (...item: T[]) => void; - multi?: boolean; + multiple?: boolean; } -const Dropdown = ({ +const Dropdown = ({ data, disabled, placeholderText, @@ -30,7 +30,7 @@ const Dropdown = ({ title, label, onSelected, - multi = false, + multiple = false, ...props }: PropsWithChildren & ViewProps>) => { if (Platform.isTV) return null; @@ -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 = ({ ({ > {label} {data.map((item, idx) => - multi ? ( + multiple ? ( keyExtractor(s) == keyExtractor(item)) @@ -80,7 +80,7 @@ const Dropdown = ({ : "off" } key={keyExtractor(item)} - onValueChange={(next, previous) => + onValueChange={(next: "on" | "off", previous: "on" | "off") => { setSelected((p) => { const prev = p || []; if (next == "on") { @@ -88,11 +88,11 @@ const Dropdown = ({ } return [ ...prev.filter( - (p) => keyExtractor(p) !== keyExtractor(item) + (p) => keyExtractor(p) !== keyExtractor(item), ), ]; - }) - } + }); + }} > {titleExtractor(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 9e38bc06..412d6b74 100644 --- a/components/common/ItemImage.tsx +++ b/components/common/ItemImage.tsx @@ -1,12 +1,11 @@ -import { useImageColors } from "@/hooks/useImageColors"; 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, ImageSource } 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 { useMemo } from "react"; -import { View } from "react-native"; +import { type FC, useMemo } from "react"; +import { View, type ViewProps } from "react-native"; interface Props extends ImageProps { item: BaseItemDto; @@ -25,7 +24,7 @@ interface Props extends ImageProps { onError?: () => void; } -export const ItemImage: React.FC = ({ +export const ItemImage: FC = ({ item, variant = "Primary", quality = 90, @@ -53,13 +52,13 @@ export const ItemImage: React.FC = ({ if (!source?.uri) return ( diff --git a/components/common/JellyseerrItemRouter.tsx b/components/common/JellyseerrItemRouter.tsx index 198d5a45..8222f187 100644 --- a/components/common/JellyseerrItemRouter.tsx +++ b/components/common/JellyseerrItemRouter.tsx @@ -1,21 +1,28 @@ -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 { + 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; + result?: MovieResult | TvResult | MovieDetails | TvDetails; mediaTitle: string; releaseYear: number; canRequest: boolean; posterSrc: string; + mediaType: MediaType; } export const TouchableJellyseerrRouter: React.FC> = ({ @@ -24,6 +31,7 @@ export const TouchableJellyseerrRouter: React.FC> = ({ releaseYear, canRequest, posterSrc, + mediaType, children, ...props }) => { @@ -42,14 +50,13 @@ export const TouchableJellyseerrRouter: React.FC> = ({ ); }, [jellyseerrApi, jellyseerrUser]); - const request = useCallback( - () => - requestMedia(mediaTitle, { - mediaId: result.id, - mediaType: result.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 ( @@ -58,6 +65,8 @@ export const TouchableJellyseerrRouter: React.FC> = ({ { + if (!result) return; + // @ts-ignore router.push({ pathname: `/(auth)/(tabs)/${from}/jellyseerr/page`, @@ -67,6 +76,7 @@ export const TouchableJellyseerrRouter: React.FC> = ({ releaseYear, canRequest, posterSrc, + mediaType, }, }); }} @@ -82,10 +92,10 @@ export const TouchableJellyseerrRouter: React.FC> = ({ loop={false} key={"content"} > - Actions - {canRequest && result.mediaType === MediaType.MOVIE && ( + Actions + {canRequest && mediaType === MediaType.MOVIE && ( { if (autoApprove) { request(); @@ -93,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 ef7a6491..fa82a4dc 100644 --- a/components/common/Text.tsx +++ b/components/common/Text.tsx @@ -1,19 +1,27 @@ import React from "react"; -import { TextProps } from "react-native"; +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; - - return ( - - ); + if (Platform.isTV) + return ( + + ); + else + return ( + + ); } diff --git a/components/common/TouchableItemRouter.tsx b/components/common/TouchableItemRouter.tsx index d4d53e79..23cb6dd7 100644 --- a/components/common/TouchableItemRouter.tsx +++ b/components/common/TouchableItemRouter.tsx @@ -1,13 +1,13 @@ +import { useFavorite } from "@/hooks/useFavorite"; import { useMarkAsPlayed } from "@/hooks/useMarkAsPlayed"; -import { +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 { useHaptic } from "@/hooks/useHaptic"; +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`; @@ -57,14 +57,26 @@ export const TouchableItemRouter: React.FC> = ({ const segments = useSegments(); 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")) return; - - const options = ["Mark as Played", "Mark as Not Played", "Cancel"]; - const cancelButtonIndex = 2; + 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( { @@ -74,14 +86,14 @@ export const TouchableItemRouter: React.FC> = ({ async (selectedIndex) => { if (selectedIndex === 0) { await markAsPlayedStatus(true); - // Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success); } else if (selectedIndex === 1) { await markAsPlayedStatus(false); - // Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success); + } else if (selectedIndex === 2) { + toggleFavorite(); } - } + }, ); - }, [showActionSheetWithOptions, markAsPlayedStatus]); + }, [showActionSheetWithOptions, isFavorite, markAsPlayedStatus]); if ( from === "(home)" || 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 7b6316f8..7ab86a90 100644 --- a/components/downloads/ActiveDownloads.tsx +++ b/components/downloads/ActiveDownloads.tsx @@ -1,30 +1,31 @@ import { Text } from "@/components/common/Text"; import { useDownload } from "@/providers/DownloadProvider"; -import {DownloadMethod, useSettings} from "@/utils/atoms/settings"; -import { JobStatus } from "@/utils/optimize-server"; +import { DownloadMethod, useSettings } from "@/utils/atoms/settings"; +import { storage } from "@/utils/mmkv"; +import type { JobStatus } from "@/utils/optimize-server"; import { formatTimeString } from "@/utils/time"; import { Ionicons } from "@expo/vector-icons"; -const BackGroundDownloader = !Platform.isTV - ? require("@kesha-antonov/react-native-background-downloader") - : null; import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { Image } from "expo-image"; import { useRouter } from "expo-router"; -const FFmpegKitProvider = !Platform.isTV ? require("ffmpeg-kit-react-native") : null; -import { useAtom } from "jotai"; +import { t } from "i18next"; +import { useMemo } from "react"; import { ActivityIndicator, Platform, TouchableOpacity, - TouchableOpacityProps, + type TouchableOpacityProps, View, - ViewProps, + type ViewProps, } from "react-native"; import { toast } from "sonner-native"; import { Button } from "../Button"; -import { Image } from "expo-image"; -import { useMemo } from "react"; -import { storage } from "@/utils/mmkv"; -import { t } from "i18next"; +const BackGroundDownloader = !Platform.isTV + ? require("@kesha-antonov/react-native-background-downloader") + : null; +const FFmpegKitProvider = !Platform.isTV + ? require("ffmpeg-kit-react-native") + : null; interface Props extends ViewProps {} @@ -32,16 +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")} + + + {t("home.downloads.active_download")} + + + {t("home.downloads.no_active_downloads")} + ); return ( - - {t("home.downloads.active_downloads")} - + + + {t("home.downloads.active_downloads")} + + {processes?.map((p: JobStatus) => ( ))} @@ -81,7 +88,9 @@ const DownloadCard = ({ process, ...props }: DownloadCardProps) => { } } else { FFmpegKitProvider.FFmpegKit.cancel(Number(id)); - setProcesses((prev: any[]) => prev.filter((p: { id: string; }) => p.id !== id)); + setProcesses((prev: any[]) => + prev.filter((p: { id: string }) => p.id !== id), + ); } }, onSuccess: () => { @@ -108,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" || @@ -124,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)})} + + {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 de8caa2e..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 { @@ -13,7 +13,7 @@ interface FilterButtonProps extends ViewProps { title: string; set: (value: T[]) => void; queryFn: (params: any) => Promise; - searchFilter: (item: T, query: string) => boolean; + searchFilter?: (item: T, query: string) => boolean; renderItemLabel: (item: T) => React.ReactNode; icon?: "filter" | "sort"; } @@ -68,16 +68,16 @@ export const FilterButton = ({ {icon === "filter" ? ( ) : ( )} diff --git a/components/filters/FilterSheet.tsx b/components/filters/FilterSheet.tsx index cc5d4300..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; @@ -28,7 +28,7 @@ interface Props extends ViewProps { values: T[]; set: (value: T[]) => void; title: string; - searchFilter: (item: T, query: string) => boolean; + searchFilter?: (item: T, query: string) => boolean; renderItemLabel: (item: T) => React.ReactNode; showSearch?: boolean; } @@ -88,7 +88,7 @@ export const FilterSheet = ({ if (!search) return _data; const results = []; for (let i = 0; i < (_data?.length || 0); i++) { - if (_data && searchFilter(_data[i], search)) { + if (_data && searchFilter?.(_data[i], search)) { results.push(_data[i]); } } @@ -130,7 +130,7 @@ export const FilterSheet = ({ appearsOnIndex={0} /> ), - [] + [], ); return ( @@ -153,18 +153,20 @@ export const FilterSheet = ({ flex: 1, }} > - - {title} - {t("search.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 c4ab373e..6fe263a6 100644 --- a/components/home/Favorites.tsx +++ b/components/home/Favorites.tsx @@ -1,20 +1,42 @@ +import { Colors } from "@/constants/Colors"; import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; +import type { Api } from "@jellyfin/sdk"; +import type { BaseItemKind } from "@jellyfin/sdk/lib/generated-client"; import { getItemsApi } from "@jellyfin/sdk/lib/utils/api"; -import { useAtom } from "jotai"; -import { View } from "react-native"; -import { ScrollingCollectionList } from "./ScrollingCollectionList"; -import { useCallback } from "react"; -import { BaseItemKind } from "@jellyfin/sdk/lib/generated-client"; import { t } from "i18next"; +import { useAtom } from "jotai"; +import { useCallback, useEffect, useState } from "react"; +import { Image, Text, View } from "react-native"; +import { ScrollingCollectionList } from "./ScrollingCollectionList"; + +// PNG ASSET +import heart from "@/assets/icons/heart.fill.png"; + +type FavoriteTypes = + | "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 fetchFavoritesByType = useCallback( async (itemType: BaseItemKind) => { - const response = await getItemsApi(api!).getItems({ - userId: user?.Id!, + const response = await getItemsApi(api as Api).getItems({ + userId: user?.Id, sortBy: ["SeriesSortName", "SortName"], sortOrder: ["Ascending"], filters: ["IsFavorite"], @@ -26,38 +48,82 @@ export const Favorites = () => { limit: 20, includeItemTypes: [itemType], }); - return response.data.Items || []; + const items = response.data.Items || []; + + // Update empty state for this specific type + setEmptyState((prev) => ({ + ...prev, + [itemType as FavoriteTypes]: items.length === 0, + })); + + return items; }, - [api, user] + [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) + ); + }; + const fetchFavoriteSeries = useCallback( () => fetchFavoritesByType("Series"), - [fetchFavoritesByType] + [fetchFavoritesByType], ); const fetchFavoriteMovies = useCallback( () => fetchFavoritesByType("Movie"), - [fetchFavoritesByType] + [fetchFavoritesByType], ); const fetchFavoriteEpisodes = useCallback( () => fetchFavoritesByType("Episode"), - [fetchFavoritesByType] + [fetchFavoritesByType], ); const fetchFavoriteVideos = useCallback( () => fetchFavoritesByType("Video"), - [fetchFavoritesByType] + [fetchFavoritesByType], ); const fetchFavoriteBoxsets = useCallback( () => fetchFavoritesByType("BoxSet"), - [fetchFavoritesByType] + [fetchFavoritesByType], ); const fetchFavoritePlaylists = useCallback( () => fetchFavoritesByType("Playlist"), - [fetchFavoritesByType] + [fetchFavoritesByType], ); return ( - + + {areAllEmpty() && ( + + + + {t("favorites.noDataTitle")} + + + {t("favorites.noData")} + + + )} { queryKey={["home", "favorites", "movies"]} title={t("favorites.movies")} hideIfEmpty - orientation="vertical" + orientation='vertical' /> = ({ ...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 cd093deb..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 } 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,13 +23,24 @@ import JellyseerrPoster from "../posters/JellyseerrPoster"; import { LoadingSkeleton } from "../search/LoadingSkeleton"; import { SearchItemWrapper } from "../search/SearchItemWrapper"; import PersonPoster from "./PersonPoster"; -import { useTranslation } from "react-i18next"; interface Props extends ViewProps { searchQuery: string; + sortType?: JellyseerrSearchSort; + order?: "asc" | "desc"; } -export const JellyserrIndexPage: React.FC = ({ searchQuery }) => { +export enum JellyseerrSearchSort { + DEFAULT = 0, + VOTE_COUNT_AND_AVERAGE = 1, + POPULARITY = 2, +} + +export const JellyserrIndexPage: React.FC = ({ + searchQuery, + sortType, + order, +}) => { const { jellyseerrApi } = useJellyseerr(); const opacity = useSharedValue(1); const { t } = useTranslation(); @@ -48,22 +62,24 @@ export const JellyserrIndexPage: React.FC = ({ searchQuery }) => { } = useReactNavigationQuery({ queryKey: ["search", "jellyseerr", "results", searchQuery], queryFn: async () => { - const response = await jellyseerrApi?.search({ - query: new URLSearchParams(searchQuery).toString(), - page: 1, - language: "en", - }); - return response?.results; + const params = { + 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", + ), + ); }, enabled: !!jellyseerrApi && searchQuery.length > 0, }); - const animatedStyle = useAnimatedStyle(() => { - return { - opacity: opacity.value, - }; - }); - useAnimatedReaction( () => f1 || f2 || l1 || l2, (isLoading) => { @@ -72,36 +88,66 @@ export const JellyserrIndexPage: React.FC = ({ searchQuery }) => { } 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 jellyseerrMovieResults = useMemo( () => - jellyseerrResults?.filter( - (r) => r.mediaType === MediaType.MOVIE - ) as MovieResult[], - [jellyseerrResults] + orderBy( + jellyseerrResults?.filter( + (r) => r.mediaType === MediaType.MOVIE, + ) as MovieResult[], + sortingType || [ + (m) => m.title.toLowerCase() == searchQuery.toLowerCase(), + ], + order || "desc", + ), + [jellyseerrResults, sortingType, order], ); const jellyseerrTvResults = useMemo( () => - jellyseerrResults?.filter( - (r) => r.mediaType === MediaType.TV - ) as TvResult[], - [jellyseerrResults] + orderBy( + jellyseerrResults?.filter( + (r) => r.mediaType === MediaType.TV, + ) as TvResult[], + sortingType || [ + (t) => t.name.toLowerCase() == searchQuery.toLowerCase(), + ], + order || "desc", + ), + [jellyseerrResults, sortingType, order], ); const jellyseerrPersonResults = useMemo( () => - jellyseerrResults?.filter( - (r) => r.mediaType === "person" - ) as PersonResult[], - [jellyseerrResults] + orderBy( + jellyseerrResults?.filter( + (r) => r.mediaType === "person", + ) as PersonResult[], + sortingType || [ + (p) => p.name.toLowerCase() == searchQuery.toLowerCase(), + ], + order || "desc", + ), + [jellyseerrResults, sortingType, order], ); if (!searchQuery.length) return ( - + ); @@ -118,10 +164,10 @@ export const JellyserrIndexPage: React.FC = ({ searchQuery }) => { !l1 && !l2 && ( - + {t("search.no_results_found_for")} - + "{searchQuery}" @@ -147,7 +193,7 @@ export const JellyserrIndexPage: React.FC = ({ searchQuery }) => { 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 ec1ac97b..a777dde3 100644 --- a/components/jellyseerr/RequestModal.tsx +++ b/components/jellyseerr/RequestModal.tsx @@ -1,174 +1,221 @@ -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, + title: string; + requestBody?: MediaRequestBody; type: MediaType; isAnime?: boolean; is4k?: boolean; onRequested?: () => void; + onDismiss?: () => void; } -const RequestModal = forwardRef>(({ - id, - title, - type, - isAnime = false, - onRequested, - ...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 [modalRequestProps, setModalRequestProps] = useState(); + 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) - ) ?? [] + )?.includes(t.id), + ) ?? []; + return tags; + }, [defaultServiceDetails]); - console.log(tags) - return tags - }, - [defaultServiceDetails] - ); - - const seasonTitle = useMemo( - () => modalRequestProps?.seasons?.length ? t("jellyseerr.season_x", {seasons: modalRequestProps?.seasons}) : undefined, - [modalRequestProps?.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), - ...modalRequestProps, - ...requestOverrides - }, - onRequested - ) - }, [modalRequestProps, requestOverrides, defaultProfile, defaultFolder, defaultTags]); - - const pathTitleExtractor = (item: RootFolder) => `${item.path} (${item.freeSpace.bytesToReadable()})`; - - return ( - setModalRequestProps(undefined)} - handleIndicatorStyle={{ - backgroundColor: "white", - }} - backgroundStyle={{ - backgroundColor: "#171717", - }} - backdropComponent={(sheetProps: BottomSheetBackdropProps) => - + const seasonTitle = useMemo(() => { + if (requestBody?.seasons && requestBody?.seasons?.length > 1) { + return t("jellyseerr.season_all"); } - > - {(data) => { - setModalRequestProps(data?.data as MediaRequestBody) - return - + 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 pathTitleExtractor = (item: RootFolder) => + `${item.path} (${item.freeSpace.bytesToReadable()})`; + + return ( + ( + + )} + > + + - {t("jellyseerr.advanced")} - {seasonTitle && - {seasonTitle} - } + + {t("jellyseerr.advanced")} + + {seasonTitle && ( + {seasonTitle} + )} - - {(defaultService && defaultServiceDetails && users) && ( + + {defaultService && defaultServiceDetails && users && ( <> item.name} - placeholderText={defaultProfile.name} + placeholderText={ + requestOverrides.profileName || defaultProfile.name + } keyExtractor={(item) => item.id.toString()} label={t("jellyseerr.quality_profile")} onSelected={(item) => - item && setRequestOverrides((prev) => ({ + item && + setRequestOverrides((prev) => ({ ...prev, - profileId: item?.id + profileId: item?.id, })) } title={t("jellyseerr.quality_profile")} @@ -176,27 +223,31 @@ const RequestModal = forwardRef item.id.toString()} label={t("jellyseerr.root_folder")} onSelected={(item) => - item && setRequestOverrides((prev) => ({ + item && + setRequestOverrides((prev) => ({ ...prev, - rootFolder: item.path - }))} + rootFolder: item.path, + })) + } title={t("jellyseerr.root_folder")} /> item.label} - placeholderText={defaultTags.map(t => t.label).join(",")} + placeholderText={defaultTags.map((t) => t.label).join(",")} keyExtractor={(item) => item.id.toString()} label={t("jellyseerr.tags")} - onSelected={(...item) => - item && setRequestOverrides((prev) => ({ + onSelected={(...selected) => + setRequestOverrides((prev) => ({ ...prev, - tags: item.map(i => i.id) + tags: selected.map((i) => i.id), })) } title={t("jellyseerr.tags")} @@ -204,33 +255,29 @@ const RequestModal = forwardRef item.displayName} - placeholderText={jellyseerrUser!!.displayName} + placeholderText={jellyseerrUser!.displayName} keyExtractor={(item) => item.id.toString() || ""} label={t("jellyseerr.request_as")} onSelected={(item) => - item && setRequestOverrides((prev) => ({ + item && + setRequestOverrides((prev) => ({ ...prev, - userId: item?.id + 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 6270ad2b..fb3c0084 100644 --- a/components/jellyseerr/discover/Discover.tsx +++ b/components/jellyseerr/discover/Discover.tsx @@ -1,47 +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 ( + + ); 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 723658c8..a5611849 100644 --- a/components/jellyseerr/discover/MovieTvSlide.tsx +++ b/components/jellyseerr/discover/MovieTvSlide.tsx @@ -1,17 +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 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({ @@ -57,8 +65,14 @@ const MovieTvSlide: React.FC = ({ slide, ...props }) => }); const flatData = useMemo( - () => data?.pages?.filter((p) => p?.results.length).flatMap((p) => p?.results), - [data] + () => + uniqBy( + data?.pages + ?.filter((p) => p?.results.length) + .flatMap((p) => p?.results), + "id", + ), + [data], ); return ( @@ -68,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 new file mode 100644 index 00000000..7f88e40e --- /dev/null +++ b/components/jellyseerr/discover/RecentRequestsSlide.tsx @@ -0,0 +1,88 @@ +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 { + 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); + }, + enabled: !!jellyseerrApi, + refetchOnMount: true, + staleTime: 0, + }); + + const { data: refreshedRequest } = useQuery({ + queryKey: ["jellyseerr", "requests", request.media.mediaType, request.id], + queryFn: async () => jellyseerrApi?.getRequest(request.id), + enabled: !!jellyseerrApi, + refetchOnMount: true, + refetchInterval: 5000, + staleTime: 0, + }); + + return ( + + ); +}; + +const RecentRequestsSlide: React.FC = ({ + slide, + ...props +}) => { + const { jellyseerrApi } = useJellyseerr(); + + const { + data: requests, + isLoading, + isError, + } = useQuery({ + queryKey: ["jellyseerr", "recent_requests"], + queryFn: async () => jellyseerrApi?.requests(), + enabled: !!jellyseerrApi, + refetchOnMount: true, + staleTime: 0, + }); + + return ( + requests && + requests.results.length > 0 && ( + item.id.toString()} + renderItem={(item: NonFunctionProperties) => ( + + )} + /> + ) + ); +}; + +export default RecentRequestsSlide; diff --git a/components/jellyseerr/discover/Slide.tsx b/components/jellyseerr/discover/Slide.tsx index f110eb15..9e2298dc 100644 --- a/components/jellyseerr/discover/Slide.tsx +++ b/components/jellyseerr/discover/Slide.tsx @@ -1,44 +1,47 @@ -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 type React from "react"; +import type { PropsWithChildren } from "react"; +import { View, type ViewProps } from "react-native"; export interface SlideProps { slide: DiscoverSlider; + contentContainerStyle?: ContentStyle; } 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, keyExtractor, 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 c1d706a4..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 403b33dc..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"; @@ -36,7 +36,7 @@ export const ListItem: React.FC> = ({ > = ({ ); return ( { 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 1c3ce45b..b25f8974 100644 --- a/components/posters/JellyseerrPoster.tsx +++ b/components/posters/JellyseerrPoster.tsx @@ -1,14 +1,25 @@ +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 { textShadowStyle } from "@/components/jellyseerr/discover/GenericSlideCard"; +import { Colors } from "@/constants/Colors"; import { useJellyseerr } from "@/hooks/useJellyseerr"; import { useJellyseerrCanRequest } from "@/utils/_jellyseerr/useJellyseerrCanRequest"; -import { MediaType } from "@/utils/jellyseerr/server/constants/media"; -import { MovieResult, TvResult } from "@/utils/jellyseerr/server/models/Search"; +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 { View, ViewProps } from "react-native"; +import { useTranslation } from "react-i18next"; +import { View, type ViewProps } from "react-native"; import Animated, { useAnimatedStyle, useSharedValue, @@ -16,17 +27,23 @@ import Animated, { } from "react-native-reanimated"; interface Props extends ViewProps { - item: MovieResult | TvResult; + item?: MovieResult | TvResult | MovieDetails | TvDetails; + horizontal?: boolean; + showDownloadInfo?: boolean; + mediaRequest?: MediaRequest; } -const JellyseerrPoster: React.FC = ({ item, ...props }) => { - const { jellyseerrApi } = useJellyseerr(); +const JellyseerrPoster: React.FC = ({ + item, + horizontal, + showDownloadInfo, + mediaRequest, + ...props +}) => { + const { jellyseerrApi, getTitle, getYear, getMediaType } = useJellyseerr(); const loadingOpacity = useSharedValue(1); const imageOpacity = useSharedValue(0); - - const loadingAnimatedStyle = useAnimatedStyle(() => ({ - opacity: loadingOpacity.value, - })); + const { t } = useTranslation(); const imageAnimatedStyle = useAnimatedStyle(() => ({ opacity: imageOpacity.value, @@ -37,66 +54,152 @@ const JellyseerrPoster: React.FC = ({ item, ...props }) => { imageOpacity.value = withTiming(1, { duration: 300 }); }; - const imageSrc = useMemo( - () => jellyseerrApi?.imageProxy(item.posterPath, "w300_and_h450_face"), - [item, jellyseerrApi] - ); - - const title = useMemo( - () => (item.mediaType === MediaType.MOVIE ? item.title : item.name), - [item] - ); - - const releaseYear = useMemo( + const backdropSrc = useMemo( () => - new Date( - item.mediaType === MediaType.MOVIE - ? item.releaseDate - : item.firstAirDate - ).getFullYear(), - [item] + 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], + ); + + 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 [canRequest] = useJellyseerrCanRequest(item); + const is4k = useMemo(() => mediaRequest?.is4k === true, [mediaRequest]); + + const downloadItems = useMemo( + () => + (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], + ); + + 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 available = useMemo(() => { + const status = mediaRequest?.media?.[is4k ? "status4k" : "status"]; + return status === MediaStatus.AVAILABLE; + }, [mediaRequest, is4k]); + return ( - - + + + {mediaRequest && showDownloadInfo && ( + <> + + {!available && !Number.isNaN(progress) && ( + <> + + + + {progress?.toFixed(0)}% + + + + )} + + {requestedSeasons.length > 0 && ( + + )} + + )} - - {title} - {releaseYear} - + + + {title || ""} + + {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 45c3e341..81d5e4ce 100644 --- a/components/search/SearchItemWrapper.tsx +++ b/components/search/SearchItemWrapper.tsx @@ -1,10 +1,11 @@ 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 { PropsWithChildren } from "react"; -import { ScrollView } from "react-native"; +import type React from "react"; +import type { PropsWithChildren } from "react"; import { Text } from "../common/Text"; type SearchItemWrapperProps = { @@ -12,13 +13,15 @@ type SearchItemWrapperProps = { items?: T[]; renderItem: (item: any) => React.ReactNode; header?: string; + onEndReached?: (() => void) | null | undefined; }; -export const SearchItemWrapper = ({ +export const SearchItemWrapper = ({ ids, items, renderItem, header, + onEndReached, }: PropsWithChildren>) => { const [api] = useAtom(apiAtom); const [user] = useAtom(userAtom); @@ -35,36 +38,41 @@ 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} + - {data && data?.length > 0 - ? data.map((item) => renderItem(item)) - : items && items?.length > 0 - ? items.map((i) => renderItem(i)) - : undefined} - + keyExtractor={(_, index) => index.toString()} + estimatedItemSize={250} + /*@ts-ignore */ + data={data || items} + onEndReachedThreshold={1} + onEndReached={onEndReached} + //@ts-ignore + 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 a1a1fb7c..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} @@ -128,14 +137,16 @@ const RenderItem = ({ item, index }: any) => { const JellyseerrSeasons: React.FC<{ isLoading: boolean; - result?: TvResult; details?: TvDetails; - hasAdvancedRequest?: boolean, + hasAdvancedRequest?: boolean; onAdvancedRequest?: (data: MediaRequestBody) => void; - refetch: (options?: (RefetchOptions | undefined)) => Promise>; + refetch: ( + options?: RefetchOptions | undefined, + ) => Promise< + QueryObserverResult + >; }> = ({ isLoading, - result, details, refetch, hasAdvancedRequest, @@ -149,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 { @@ -161,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, @@ -175,7 +186,7 @@ const JellyseerrSeasons: React.FC<{ const allSeasonsAvailable = useMemo( () => seasons?.every((season) => season.status === MediaStatus.AVAILABLE), - [seasons] + [seasons], ); const requestAll = useCallback(() => { @@ -186,59 +197,68 @@ 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(result?.name!!, body, refetch); + requestMedia(details.name, body, refetch); } }, [jellyseerrApi, seasons, details, hasAdvancedRequest, onAdvancedRequest]); 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(`${result?.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 && ( - - + + )} @@ -251,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 }) => ( <> @@ -274,17 +296,21 @@ const JellyseerrSeasons: React.FC<{ [season.seasonNumber]: !prevState?.[season.seasonNumber], })) } - className="px-4" + className='px-4' > {[0].map(() => { @@ -294,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")} { + const [settings, updateSettings] = useSettings(); + return ( + + + + + updateSettings({ enableH265ForChromecast }) + } + /> + + + + ); +}; diff --git a/components/settings/Dashboard.tsx b/components/settings/Dashboard.tsx new file mode 100644 index 00000000..798fef37 --- /dev/null +++ b/components/settings/Dashboard.tsx @@ -0,0 +1,30 @@ +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"; + +export const Dashboard = () => { + const [settings, updateSettings] = useSettings(); + const { sessions = [], isLoading } = useSessions({} as useSessionsProps); + const router = useRouter(); + + const { t } = useTranslation(); + + if (!settings) return null; + return ( + + + router.push("/settings/dashboard/sessions")} + title={t("home.settings.dashboard.sessions_title")} + showArrow + /> + + + ); +}; 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/SettingsIndex.tsx b/components/settings/HomeIndex.tsx similarity index 87% rename from components/settings/SettingsIndex.tsx rename to components/settings/HomeIndex.tsx index 964bee31..71325ccb 100644 --- a/components/settings/SettingsIndex.tsx +++ b/components/settings/HomeIndex.tsx @@ -1,17 +1,18 @@ 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"; import { useDownload } from "@/providers/DownloadProvider"; import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; import { useSettings } from "@/utils/atoms/settings"; +import { eventBus } from "@/utils/eventBus"; import { Feather, Ionicons } from "@expo/vector-icons"; -import { Api } from "@jellyfin/sdk"; -import { +import type { Api } from "@jellyfin/sdk"; +import type { BaseItemDto, BaseItemKind, } from "@jellyfin/sdk/lib/generated-client/models"; @@ -23,10 +24,15 @@ import { getUserViewsApi, } from "@jellyfin/sdk/lib/utils/api"; import NetInfo from "@react-native-community/netinfo"; -import { QueryFunction, useQuery } from "@tanstack/react-query"; -import { useNavigation, useRouter } from "expo-router"; +import { type QueryFunction, useQuery } from "@tanstack/react-query"; +import { + useNavigation, + usePathname, + useRouter, + useSegments, +} from "expo-router"; import { useAtomValue } from "jotai"; -import { useCallback, useEffect, useMemo, useState } from "react"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { ActivityIndicator, @@ -53,7 +59,7 @@ type MediaListSection = { type Section = ScrollingCollectionListSection | MediaListSection; -export const SettingsIndex = () => { +export const HomeIndex = () => { const router = useRouter(); const { t } = useTranslation(); @@ -77,6 +83,8 @@ export const SettingsIndex = () => { const insets = useSafeAreaInsets(); + const scrollViewRef = useRef(null); + const { downloadedFiles, cleanCacheDirectory } = useDownload(); useEffect(() => { const hasDownloads = downloadedFiles && downloadedFiles.length > 0; @@ -86,10 +94,10 @@ export const SettingsIndex = () => { onPress={() => { router.push("/(auth)/downloads"); }} - className="p-2" + className='p-2' > @@ -100,10 +108,22 @@ export const SettingsIndex = () => { useEffect(() => { cleanCacheDirectory().catch((e) => - console.error("Something went wrong cleaning cache directory") + console.error("Something went wrong cleaning cache directory"), ); }, []); + const segments = useSegments(); + useEffect(() => { + const unsubscribe = eventBus.on("scrollToTop", () => { + if (segments[2] === "(home)") + scrollViewRef.current?.scrollTo({ y: -152, animated: true }); + }); + + return () => { + unsubscribe(); + }; + }, [segments]); + const checkConnection = useCallback(async () => { setLoadingRetry(true); const state = await NetInfo.fetch(); @@ -154,33 +174,33 @@ export const SettingsIndex = () => { 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]); const invalidateCache = useInvalidatePlaybackProgressCache(); - const refetch = useCallback(async () => { + const refetch = async () => { setLoading(true); await refreshStreamyfinPluginSettings(); await invalidateCache(); setLoading(false); - }, []); + }; const createCollectionConfig = useCallback( ( title: string, queryKey: string[], includeItemTypes: BaseItemKind[], - parentId: string | undefined + parentId: string | undefined, ): ScrollingCollectionListSection => ({ title, queryKey, @@ -202,7 +222,7 @@ export const SettingsIndex = () => { }, type: "ScrollingCollectionList", }), - [api, user?.Id] + [api, user?.Id], ); let sections: Section[] = []; @@ -224,7 +244,7 @@ export const SettingsIndex = () => { title || "", queryKey, includeItemTypes, - c.Id + c.Id, ); }); @@ -292,7 +312,7 @@ export const SettingsIndex = () => { 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); @@ -356,32 +376,32 @@ export const SettingsIndex = () => { 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/SettingsIndex.tv.tsx b/components/settings/SettingsIndex.tv.tsx deleted file mode 100644 index afde6937..00000000 --- a/components/settings/SettingsIndex.tv.tsx +++ /dev/null @@ -1,453 +0,0 @@ -import { Button } from "@/components/Button"; -import { Text } from "@/components/common/Text"; -import { LargeMovieCarousel } from "@/components/home/LargeMovieCarousel"; -import { ScrollingCollectionList } from "@/components/home/ScrollingCollectionList"; -import { Loader } from "@/components/Loader"; -import { MediaListSection } from "@/components/medialists/MediaListSection"; -import { useInvalidatePlaybackProgressCache } from "@/hooks/useRevalidatePlaybackProgressCache"; -import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; -import { useSettings } from "@/utils/atoms/settings"; -import { Ionicons } from "@expo/vector-icons"; -import { Api } from "@jellyfin/sdk"; -import { - BaseItemDto, - BaseItemKind, -} from "@jellyfin/sdk/lib/generated-client/models"; -import { - getItemsApi, - getSuggestionsApi, - getTvShowsApi, - getUserLibraryApi, - getUserViewsApi, -} from "@jellyfin/sdk/lib/utils/api"; -import NetInfo from "@react-native-community/netinfo"; -import { QueryFunction, useQuery } from "@tanstack/react-query"; -import { useRouter } from "expo-router"; -import { useAtomValue } from "jotai"; -import { useCallback, useEffect, useMemo, useState } from "react"; -import { useTranslation } from "react-i18next"; -import { - ActivityIndicator, - RefreshControl, - ScrollView, - View, -} from "react-native"; -import { useSafeAreaInsets } from "react-native-safe-area-context"; - -type ScrollingCollectionListSection = { - type: "ScrollingCollectionList"; - title?: string; - queryKey: (string | undefined | null)[]; - queryFn: QueryFunction; - orientation?: "horizontal" | "vertical"; -}; - -type MediaListSection = { - type: "MediaListSection"; - queryKey: (string | undefined)[]; - queryFn: QueryFunction; -}; - -type Section = ScrollingCollectionListSection | MediaListSection; - -export const SettingsIndex = () => { - const router = useRouter(); - - const { t } = useTranslation(); - - const api = useAtomValue(apiAtom); - const user = useAtomValue(userAtom); - - const [loading, setLoading] = useState(false); - const [ - settings, - updateSettings, - pluginSettings, - setPluginSettings, - refreshStreamyfinPluginSettings, - ] = useSettings(); - - const [isConnected, setIsConnected] = useState(null); - const [loadingRetry, setLoadingRetry] = useState(false); - - const insets = useSafeAreaInsets(); - - const checkConnection = useCallback(async () => { - setLoadingRetry(true); - const state = await NetInfo.fetch(); - setIsConnected(state.isConnected); - setLoadingRetry(false); - }, []); - - useEffect(() => { - const unsubscribe = NetInfo.addEventListener((state) => { - if (state.isConnected == false || state.isInternetReachable === false) - setIsConnected(false); - else setIsConnected(true); - }); - - NetInfo.fetch().then((state) => { - setIsConnected(state.isConnected); - }); - - // cleanCacheDirectory().catch((e) => - // console.error("Something went wrong cleaning cache directory") - // ); - - return () => { - unsubscribe(); - }; - }, []); - - const { - data, - isError: e1, - isLoading: l1, - } = useQuery({ - queryKey: ["home", "userViews", user?.Id], - queryFn: async () => { - if (!api || !user?.Id) { - return null; - } - - const response = await getUserViewsApi(api).getUserViews({ - userId: user.Id, - }); - - return response.data.Items || null; - }, - enabled: !!api && !!user?.Id, - staleTime: 60 * 1000, - }); - - const userViews = useMemo( - () => data?.filter((l) => !settings?.hiddenLibraries?.includes(l.Id!)), - [data, settings?.hiddenLibraries] - ); - - const collections = useMemo(() => { - const allow = ["movies", "tvshows"]; - return ( - userViews?.filter( - (c) => c.CollectionType && allow.includes(c.CollectionType) - ) || [] - ); - }, [userViews]); - - const invalidateCache = useInvalidatePlaybackProgressCache(); - - const refetch = useCallback(async () => { - setLoading(true); - await refreshStreamyfinPluginSettings(); - await invalidateCache(); - setLoading(false); - }, []); - - const createCollectionConfig = useCallback( - ( - title: string, - queryKey: string[], - includeItemTypes: BaseItemKind[], - parentId: string | undefined - ): ScrollingCollectionListSection => ({ - title, - queryKey, - queryFn: async () => { - if (!api) return []; - return ( - ( - await getUserLibraryApi(api).getLatestMedia({ - userId: user?.Id, - limit: 20, - fields: ["PrimaryImageAspectRatio", "Path"], - imageTypeLimit: 1, - enableImageTypes: ["Primary", "Backdrop", "Thumb"], - includeItemTypes, - parentId, - }) - ).data || [] - ); - }, - type: "ScrollingCollectionList", - }), - [api, user?.Id] - ); - - let sections: Section[] = []; - if (!settings?.home || !settings?.home?.sections) { - sections = useMemo(() => { - if (!api || !user?.Id) return []; - - const latestMediaViews = collections.map((c) => { - const includeItemTypes: BaseItemKind[] = - c.CollectionType === "tvshows" ? ["Series"] : ["Movie"]; - const title = t("home.recently_added_in", { libraryName: c.Name }); - const queryKey = [ - "home", - "recentlyAddedIn" + c.CollectionType, - user?.Id!, - c.Id!, - ]; - return createCollectionConfig( - title || "", - queryKey, - includeItemTypes, - c.Id - ); - }); - - const ss: Section[] = [ - { - title: t("home.continue_watching"), - queryKey: ["home", "resumeItems"], - queryFn: async () => - ( - await getItemsApi(api).getResumeItems({ - userId: user.Id, - enableImageTypes: ["Primary", "Backdrop", "Thumb"], - includeItemTypes: ["Movie", "Series", "Episode"], - }) - ).data.Items || [], - type: "ScrollingCollectionList", - orientation: "horizontal", - }, - { - title: t("home.next_up"), - queryKey: ["home", "nextUp-all"], - queryFn: async () => - ( - await getTvShowsApi(api).getNextUp({ - userId: user?.Id, - fields: ["MediaSourceCount"], - limit: 20, - enableImageTypes: ["Primary", "Backdrop", "Thumb"], - enableResumable: false, - }) - ).data.Items || [], - type: "ScrollingCollectionList", - orientation: "horizontal", - }, - ...latestMediaViews, - // ...(mediaListCollections?.map( - // (ml) => - // ({ - // title: ml.Name, - // queryKey: ["home", "mediaList", ml.Id!], - // queryFn: async () => ml, - // type: "MediaListSection", - // orientation: "vertical", - // } as Section) - // ) || []), - { - title: t("home.suggested_movies"), - queryKey: ["home", "suggestedMovies", user?.Id], - queryFn: async () => - ( - await getSuggestionsApi(api).getSuggestions({ - userId: user?.Id, - limit: 10, - mediaType: ["Video"], - type: ["Movie"], - }) - ).data.Items || [], - type: "ScrollingCollectionList", - orientation: "vertical", - }, - { - title: t("home.suggested_episodes"), - queryKey: ["home", "suggestedEpisodes", user?.Id], - queryFn: async () => { - try { - const suggestions = await getSuggestions(api, user.Id); - const nextUpPromises = suggestions.map((series) => - getNextUp(api, user.Id, series.Id) - ); - const nextUpResults = await Promise.all(nextUpPromises); - - return nextUpResults.filter((item) => item !== null) || []; - } catch (error) { - console.error("Error fetching data:", error); - return []; - } - }, - type: "ScrollingCollectionList", - orientation: "horizontal", - }, - ]; - return ss; - }, [api, user?.Id, collections]); - } else { - sections = useMemo(() => { - if (!api || !user?.Id) return []; - const ss: Section[] = []; - - for (const key in settings.home?.sections) { - // @ts-expect-error - const section = settings.home?.sections[key]; - const id = section.title || key; - ss.push({ - title: id, - queryKey: ["home", id], - queryFn: async () => { - if (section.items) { - const response = await getItemsApi(api).getItems({ - userId: user?.Id, - limit: section.items?.limit || 25, - recursive: true, - includeItemTypes: section.items?.includeItemTypes, - sortBy: section.items?.sortBy, - sortOrder: section.items?.sortOrder, - filters: section.items?.filters, - parentId: section.items?.parentId, - }); - return response.data.Items || []; - } else if (section.nextUp) { - const response = await getTvShowsApi(api).getNextUp({ - userId: user?.Id, - fields: ["MediaSourceCount"], - limit: section.items?.limit || 25, - enableImageTypes: ["Primary", "Backdrop", "Thumb"], - enableResumable: section.items?.enableResumable || false, - enableRewatching: section.items?.enableRewatching || false, - }); - return response.data.Items || []; - } - return []; - }, - type: "ScrollingCollectionList", - orientation: section?.orientation || "vertical", - }); - } - return ss; - }, [api, user?.Id, settings.home?.sections]); - } - - if (isConnected === false) { - return ( - - {t("home.no_internet")} - - {t("home.no_internet_message")} - - - - - - - ); - } - - if (e1) - return ( - - {t("home.oops")} - - {t("home.error_message")} - - - ); - - if (l1) - return ( - - - - ); - - return ( - - } - contentContainerStyle={{ - paddingLeft: insets.left, - paddingRight: insets.right, - paddingBottom: 16, - }} - > - - - - {sections.map((section, index) => { - if (section.type === "ScrollingCollectionList") { - return ( - - ); - } else if (section.type === "MediaListSection") { - return ( - - ); - } - return null; - })} - - - ); -}; - -// Function to get suggestions -async function getSuggestions(api: Api, userId: string | undefined) { - if (!userId) return []; - const response = await getSuggestionsApi(api).getSuggestions({ - userId, - limit: 10, - mediaType: ["Unknown"], - type: ["Series"], - }); - return response.data.Items ?? []; -} - -// Function to get the next up TV show for a series -async function getNextUp( - api: Api, - userId: string | undefined, - seriesId: string | undefined -) { - if (!userId || !seriesId) return null; - const response = await getTvShowsApi(api).getNextUp({ - userId, - seriesId, - limit: 1, - }); - return response.data.Items?.[0] ?? null; -} diff --git a/components/settings/StorageSettings.tsx b/components/settings/StorageSettings.tsx index 8525afd0..d8516b79 100644 --- a/components/settings/StorageSettings.tsx +++ b/components/settings/StorageSettings.tsx @@ -1,13 +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"; export const StorageSettings = () => { const { deleteAllFiles, appSizeUsage } = useDownload(); @@ -43,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(), @@ -55,13 +56,13 @@ export const StorageSettings = () => { )} - + {size && ( <> { ((size.total - size.remaining - size.app) / size.total) * 100 }%`, - backgroundColor: "rgb(192 132 252)", + backgroundColor: Colors.primaryLightRGB, }} /> )} - + {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, ), })} @@ -104,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 90326204..0bc02cdd 100644 --- a/components/video-player/controls/AudioSlider.tsx +++ b/components/video-player/controls/AudioSlider.tsx @@ -1,25 +1,28 @@ -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"; -// import { VolumeManager } from "react-native-volume-manager"; -const VolumeManager = !Platform.isTV - ? require("react-native-volume-manager") - : null; +import { useSharedValue } from "react-native-reanimated"; +const VolumeManager = Platform.isTV + ? null + : require("react-native-volume-manager"); import { Ionicons } from "@expo/vector-icons"; +import type { VolumeResult } from "react-native-volume-manager"; interface AudioSliderProps { setVisibility: (show: boolean) => void; } const AudioSlider: React.FC = ({ setVisibility }) => { - if (Platform.isTV) return; + if (Platform.isTV) { + return; + } const volume = useSharedValue(50); // Explicitly type as number const min = useSharedValue(0); // Explicitly type as number const max = useSharedValue(100); // Explicitly type as number - const timeoutRef = useRef(null); // Use a ref to store the timeout ID + const timeoutRef = useRef(null); // Use a ref to store the timeout ID useEffect(() => { const fetchInitialVolume = async () => { @@ -50,20 +53,22 @@ const AudioSlider: React.FC = ({ setVisibility }) => { }; useEffect(() => { - const volumeListener = VolumeManager.addVolumeListener((result) => { - 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 }) => { }} /> { }} /> ; + videoRef: MutableRefObject; isPlaying: boolean; isSeeking: SharedValue; cacheProgress: SharedValue; @@ -71,7 +73,7 @@ interface Props { isBuffering: boolean; showControls: boolean; ignoreSafeAreas?: boolean; - setIgnoreSafeAreas: React.Dispatch>; + setIgnoreSafeAreas: Dispatch>; enableTrickplay?: boolean; togglePlay: () => void; setShowControls: (shown: boolean) => void; @@ -87,13 +89,12 @@ interface Props { setSubtitleURL?: (url: string, customName: string) => void; setSubtitleTrack?: (index: number) => void; setAudioTrack?: (index: number) => void; - stop: (() => Promise) | (() => void); isVlc?: boolean; } const CONTROLS_TIMEOUT = 4000; -export const Controls: React.FC = ({ +export const Controls: FC = ({ item, seek, startPictureInPicture, @@ -116,7 +117,6 @@ export const Controls: React.FC = ({ setSubtitleURL, setSubtitleTrack, setAudioTrack, - stop, offline = false, enableTrickplay = true, isVlc = false, @@ -142,7 +142,7 @@ export const Controls: React.FC = ({ } = useTrickplay(item, !offline && enableTrickplay); const [currentTime, setCurrentTime] = useState(0); - const [remainingTime, setRemainingTime] = useState(Infinity); + const [remainingTime, setRemainingTime] = useState(Number.POSITIVE_INFINITY); const min = useSharedValue(0); const max = useSharedValue(item.RunTimeTicks || 0); @@ -178,7 +178,7 @@ export const Controls: React.FC = ({ currentTime, seek, play, - isVlc + isVlc, ); const { showSkipCreditButton, skipCredit } = useCreditSkipper( @@ -186,78 +186,69 @@ export const Controls: React.FC = ({ currentTime, seek, play, - isVlc + isVlc, + ); + + const goToItemCommon = useCallback( + (item: BaseItemDto) => { + if (!item || !settings) { + return; + } + + lightHapticFeedback(); + + const previousIndexes = { + subtitleIndex: subtitleIndex + ? Number.parseInt(subtitleIndex) + : undefined, + audioIndex: audioIndex ? Number.parseInt(audioIndex) : undefined, + }; + + const { + mediaSource: newMediaSource, + audioIndex: defaultAudioIndex, + subtitleIndex: defaultSubtitleIndex, + } = getDefaultPlaySettings( + item, + settings, + previousIndexes, + mediaSource ?? undefined, + ); + + const queryParams = new URLSearchParams({ + itemId: item.Id ?? "", + audioIndex: defaultAudioIndex?.toString() ?? "", + subtitleIndex: defaultSubtitleIndex?.toString() ?? "", + mediaSourceId: newMediaSource?.Id ?? "", + bitrateValue: bitrateValue?.toString(), + }).toString(); + + // @ts-expect-error + router.replace(`player/direct-player?${queryParams}`); + }, + [settings, subtitleIndex, audioIndex, mediaSource, bitrateValue, router], ); const goToPreviousItem = useCallback(() => { - if (!previousItem || !settings) return; - - lightHapticFeedback(); - - const previousIndexes: previousIndexes = { - subtitleIndex: subtitleIndex ? parseInt(subtitleIndex) : undefined, - audioIndex: audioIndex ? parseInt(audioIndex) : undefined, - }; - - const { - mediaSource: newMediaSource, - audioIndex: defaultAudioIndex, - subtitleIndex: defaultSubtitleIndex, - } = getDefaultPlaySettings( - previousItem, - settings, - previousIndexes, - mediaSource ?? undefined - ); - - const queryParams = new URLSearchParams({ - itemId: previousItem.Id ?? "", // Ensure itemId is a string - audioIndex: defaultAudioIndex?.toString() ?? "", - subtitleIndex: defaultSubtitleIndex?.toString() ?? "", - mediaSourceId: newMediaSource?.Id ?? "", // Ensure mediaSourceId is a string - bitrateValue: bitrateValue.toString(), - }).toString(); - - stop(); - - // @ts-expect-error - router.replace(`player/direct-player?${queryParams}`); - }, [previousItem, settings, subtitleIndex, audioIndex]); + if (!previousItem) { + return; + } + goToItemCommon(previousItem); + }, [previousItem, goToItemCommon]); const goToNextItem = useCallback(() => { - if (!nextItem || !settings) return; + if (!nextItem) return; + goToItemCommon(nextItem); + }, [nextItem, goToItemCommon]); - lightHapticFeedback(); - - const previousIndexes: previousIndexes = { - subtitleIndex: subtitleIndex ? parseInt(subtitleIndex) : undefined, - audioIndex: audioIndex ? parseInt(audioIndex) : undefined, - }; - - const { - mediaSource: newMediaSource, - audioIndex: defaultAudioIndex, - subtitleIndex: defaultSubtitleIndex, - } = getDefaultPlaySettings( - nextItem, - settings, - previousIndexes, - mediaSource ?? undefined - ); - - const queryParams = new URLSearchParams({ - itemId: nextItem.Id ?? "", // Ensure itemId is a string - audioIndex: defaultAudioIndex?.toString() ?? "", - subtitleIndex: defaultSubtitleIndex?.toString() ?? "", - mediaSourceId: newMediaSource?.Id ?? "", // Ensure mediaSourceId is a string - bitrateValue: bitrateValue.toString(), - }).toString(); - - stop(); - - // @ts-expect-error - router.replace(`player/direct-player?${queryParams}`); - }, [nextItem, settings, subtitleIndex, audioIndex]); + const goToItem = useCallback( + async (itemId: string) => { + const gotoItem = await getItemById(api, itemId); + if (!gotoItem) return; + goToItemCommon(gotoItem); + }, + [goToItemCommon, api], + ); const updateTimes = useCallback( (currentProgress: number, maxValue: number) => { @@ -269,7 +260,7 @@ export const Controls: React.FC = ({ setCurrentTime(current); setRemainingTime(remaining); }, - [goToNextItem, isVlc] + [goToNextItem, isVlc], ); useAnimatedReaction( @@ -279,11 +270,11 @@ export const Controls: React.FC = ({ isSeeking: isSeeking.value, }), (result) => { - if (result.isSeeking === false) { + if (!result.isSeeking) { runOnJS(updateTimes)(result.progress, result.max); } }, - [updateTimes] + [updateTimes], ); const hideControls = useCallback(() => { @@ -309,7 +300,7 @@ export const Controls: React.FC = ({ }; const handleSliderStart = useCallback(() => { - if (showControls === false) return; + if (!showControls) return; setIsSliding(true); wasPlayingRef.current = isPlaying; @@ -326,9 +317,11 @@ export const Controls: React.FC = ({ setIsSliding(false); seek(Math.max(0, Math.floor(isVlc ? value : ticksToSeconds(value)))); - if (wasPlayingRef.current === true) play(); + if (wasPlayingRef.current) { + play(); + } }, - [isVlc] + [isVlc], ); const [time, setTime] = useState({ hours: 0, minutes: 0, seconds: 0 }); @@ -342,7 +335,7 @@ export const Controls: React.FC = ({ const seconds = progressInSeconds % 60; setTime({ hours, minutes, seconds }); }, 3), - [] + [], ); const handleSkipBackward = useCallback(async () => { @@ -356,7 +349,9 @@ export const Controls: React.FC = ({ ? Math.max(0, curr - secondsToMs(settings.rewindSkipTime)) : Math.max(0, ticksToSeconds(curr) - settings.rewindSkipTime); seek(newTime); - if (wasPlayingRef.current === true) play(); + if (wasPlayingRef.current) { + play(); + } } } catch (error) { writeToLog("ERROR", "Error seeking video backwards", error); @@ -364,7 +359,9 @@ export const Controls: React.FC = ({ }, [settings, isPlaying, isVlc]); const handleSkipForward = useCallback(async () => { - if (!settings?.forwardSkipTime) return; + if (!settings?.forwardSkipTime) { + return; + } wasPlayingRef.current = isPlaying; lightHapticFeedback(); try { @@ -374,56 +371,13 @@ export const Controls: React.FC = ({ ? curr + secondsToMs(settings.forwardSkipTime) : ticksToSeconds(curr) + settings.forwardSkipTime; seek(Math.max(0, newTime)); - if (wasPlayingRef.current === true) play(); + if (wasPlayingRef.current) play(); } } catch (error) { writeToLog("ERROR", "Error seeking video forwards", error); } }, [settings, isPlaying, isVlc]); - const goToItem = useCallback( - async (itemId: string) => { - try { - const gotoItem = await getItemById(api, itemId); - if (!settings || !gotoItem) return; - - lightHapticFeedback(); - - const previousIndexes: previousIndexes = { - subtitleIndex: subtitleIndex ? parseInt(subtitleIndex) : undefined, - audioIndex: audioIndex ? parseInt(audioIndex) : undefined, - }; - - const { - mediaSource: newMediaSource, - audioIndex: defaultAudioIndex, - subtitleIndex: defaultSubtitleIndex, - } = getDefaultPlaySettings( - gotoItem, - settings, - previousIndexes, - mediaSource ?? undefined - ); - - const queryParams = new URLSearchParams({ - itemId: gotoItem.Id ?? "", // Ensure itemId is a string - audioIndex: defaultAudioIndex?.toString() ?? "", - subtitleIndex: defaultSubtitleIndex?.toString() ?? "", - mediaSourceId: newMediaSource?.Id ?? "", // Ensure mediaSourceId is a string - bitrateValue: bitrateValue.toString(), - }).toString(); - - stop(); - - // @ts-expect-error - router.replace(`player/direct-player?${queryParams}`); - } catch (error) { - console.error("Error in gotoEpisode:", error); - } - }, - [settings, subtitleIndex, audioIndex] - ); - const toggleIgnoreSafeAreas = useCallback(() => { setIgnoreSafeAreas((prev) => !prev); lightHapticFeedback(); @@ -431,7 +385,9 @@ export const Controls: React.FC = ({ const switchOnEpisodeMode = useCallback(() => { setEpisodeView(true); - if (isPlaying) togglePlay(); + if (isPlaying) { + togglePlay(); + } }, [isPlaying, togglePlay]); const memoizedRenderBubble = useCallback(() => { @@ -463,7 +419,7 @@ export const Controls: React.FC = ({ transform: [{ scale: 1.4 }], borderRadius: 5, }} - className="bg-neutral-800 overflow-hidden" + className='bg-neutral-800 overflow-hidden' > = ({ resizeMode: "cover", }} source={{ uri: url }} - contentFit="cover" + contentFit='cover' /> = ({ fontSize: 16, }} > - {`${time.hours > 0 ? `${time.hours}:` : ""}${ - time.minutes < 10 ? `0${time.minutes}` : time.minutes - }:${time.seconds < 10 ? `0${time.seconds}` : time.seconds}`} + {`${time.hours > 0 ? `${time.hours}:` : ""}${time.minutes < 10 ? `0${time.minutes}` : time.minutes}:${ + time.seconds < 10 ? `0${time.seconds}` : time.seconds + }`} ); }, [trickPlayUrl, trickplayInfo, time]); const onClose = async () => { - stop(); lightHapticFeedback(); await ScreenOrientation.lockAsync( - ScreenOrientation.OrientationLock.PORTRAIT_UP + ScreenOrientation.OrientationLock.PORTRAIT_UP, ); router.back(); }; @@ -538,80 +493,83 @@ export const Controls: React.FC = ({ }, ]} pointerEvents={showControls ? "auto" : "none"} - className={`flex flex-row w-full pt-2`} + className={"flex flex-row w-full pt-2"} > - - - - - - - - {!Platform.isTV && ( - + - - - )} + + + + )} + + + {!Platform.isTV && + settings.defaultPlayer === VideoPlayer.VLC_4 && ( + + + + )} {item?.Type === "Episode" && !offline && ( { switchOnEpisodeMode(); }} - className="aspect-square flex flex-col rounded-xl items-center justify-center p-2" + className='aspect-square flex flex-col rounded-xl items-center justify-center p-2' > - + )} {previousItem && !offline && ( - + )} {nextItem && !offline && ( - + )} {/* {mediaSource?.TranscodingUrl && ( */} {/* )} */} - + @@ -652,9 +610,9 @@ export const Controls: React.FC = ({ }} > = ({ = ({ opacity: showControls ? 1 : 0, }} > - + = ({ bottom: settings?.safeAreaInControlsEnabled ? insets.bottom : 0, }, ]} - className={`flex flex-col px-2`} + className={"flex flex-col px-2"} onTouchStart={handleControlsInteraction} > = ({ pointerEvents={showControls ? "box-none" : "none"} > {item?.Type === "Episode" && ( - + {`${item.SeriesName} - ${item.SeasonName} Episode ${item.IndexNumber}`} )} - {item?.Name} + {item?.Name} {item?.Type === "Movie" && ( - + {item?.ProductionYear} )} {item?.Type === "Audio" && ( - {item?.Album} + {item?.Album} )} - + = ({ - + = ({ minimumValue={min} maximumValue={max} /> - - + + {formatTimeString(currentTime, isVlc ? "ms" : "s")} - + -{formatTimeString(remainingTime, isVlc ? "ms" : "s")} diff --git a/components/video-player/controls/EpisodeList.tsx b/components/video-player/controls/EpisodeList.tsx index 422a2fc3..cd36edf7 100644 --- a/components/video-player/controls/EpisodeList.tsx +++ b/components/video-player/controls/EpisodeList.tsx @@ -1,20 +1,20 @@ -import { - HorizontalScroll, - HorizontalScrollRef, -} from "@/components/common/HorrizontalScroll"; -import { Text } from "@/components/common/Text"; import ContinueWatchingPoster from "@/components/ContinueWatchingPoster"; import { DownloadSingleItem } from "@/components/DownloadItem"; import { Loader } from "@/components/Loader"; +import { + HorizontalScroll, + type HorizontalScrollRef, +} from "@/components/common/HorrizontalScroll"; +import { Text } from "@/components/common/Text"; import { SeasonDropdown, - SeasonIndexState, + type SeasonIndexState, } from "@/components/series/SeasonDropdown"; import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; import { getUserItemData } from "@/utils/jellyfin/user-library/getUserItemData"; import { runtimeTicksToSeconds } from "@/utils/time"; import { Ionicons } from "@expo/vector-icons"; -import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; +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"; @@ -59,7 +59,7 @@ export const EpisodeList: React.FC = ({ 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 7847f0a9..3eed62bd 100644 --- a/components/video-player/controls/contexts/ControlContext.tsx +++ b/components/video-player/controls/contexts/ControlContext.tsx @@ -1,9 +1,9 @@ -import { TrackInfo } from "@/modules/vlc-player"; -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; @@ -12,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 7a4e5161..0ef73f31 100644 --- a/components/video-player/controls/contexts/VideoContext.tsx +++ b/components/video-player/controls/contexts/VideoContext.tsx @@ -1,19 +1,16 @@ -import { TrackInfo } from "@/modules/vlc-player"; +import type { TrackInfo } from "@/modules/VlcPlayer.types"; +import { router, useLocalSearchParams } from "expo-router"; +import type React from "react"; import { - BaseItemDto, - MediaSourceInfo, -} from "@jellyfin/sdk/lib/generated-client"; -import React, { + type ReactNode, createContext, useContext, - useState, - ReactNode, useEffect, useMemo, + useState, } from "react"; +import type { Track } from "../types"; import { useControlContext } from "./ControlContext"; -import { Track } from "../types"; -import { router, useLocalSearchParams } from "expo-router"; interface VideoContextProps { audioTracks: Track[] | null; @@ -70,9 +67,9 @@ export const VideoProvider: React.FC = ({ const onTextBasedSubtitle = useMemo( () => allSubs.find( - (s) => s.Index?.toString() === subtitleIndex && s.IsTextSubtitleStream + (s) => s.Index?.toString() === subtitleIndex && s.IsTextSubtitleStream, ) || subtitleIndex === "-1", - [allSubs, subtitleIndex] + [allSubs, subtitleIndex], ); const setPlayerParams = ({ @@ -98,7 +95,7 @@ export const VideoProvider: React.FC = ({ const setTrackParams = ( type: "audio" | "subtitle", index: number, - serverIndex: number + serverIndex: number, ) => { const setTrack = type === "audio" ? setAudioTrack : setSubtitleTrack; const paramKey = type === "audio" ? "audioIndex" : "subtitleIndex"; @@ -129,23 +126,25 @@ export const VideoProvider: React.FC = ({ if (getSubtitleTracks) { 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), + ); + + // Step 2: Apply VLC indexing logic let textSubIndex = 0; - const subtitles: Track[] = allSubs?.map((sub) => { + 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 displayTitle = sub.DisplayTitle || "Undefined Subtitle"; 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 { - name: displayTitle, + name: sub.DisplayTitle || "Undefined Subtitle", index: sub.Index ?? -1, - originalIndex: finalIndex, setTrack: () => shouldIncrement ? setTrackParams("subtitle", finalIndex, sub.Index ?? -1) @@ -155,6 +154,11 @@ export const VideoProvider: React.FC = ({ }; }); + // Step 3: Restore the original order + const subtitles: Track[] = processedSubs.sort( + (a, b) => a.index - b.index, + ); + // Add a "Disable Subtitles" option subtitles.unshift({ name: "Disable", @@ -164,21 +168,13 @@ export const VideoProvider: React.FC = ({ ? setTrackParams("subtitle", -1, -1) : setPlayerParams({ chosenSubtitleIndex: "-1" }), }); - setSubtitleTracks(subtitles); } - if ( - getAudioTracks && - (audioTracks === null || audioTracks.length === 0) - ) { + if (getAudioTracks) { const audioData = await getAudioTracks(); - if (!audioData) return; - - console.log("audioData", audioData); 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; diff --git a/components/video-player/controls/dropdown/DropdownView.tsx b/components/video-player/controls/dropdown/DropdownView.tsx index 0ee51dc1..3168e942 100644 --- a/components/video-player/controls/dropdown/DropdownView.tsx +++ b/components/video-player/controls/dropdown/DropdownView.tsx @@ -1,23 +1,23 @@ -import React 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 { BITRATES } from "@/components/BitrateSelector"; +import { useLocalSearchParams, useRouter } from "expo-router"; +import { useControlContext } from "../contexts/ControlContext"; import { useVideoContext } from "../contexts/VideoContext"; -import { useLocalSearchParams } from "expo-router"; -interface DropdownViewProps { - showControls: boolean; - offline?: boolean; // used to disable external subs for downloads -} - -const DropdownView: React.FC = ({ - showControls, - offline = false, -}) => { +const DropdownView = () => { const videoContext = useVideoContext(); const { subtitleTracks, audioTracks } = videoContext; + const ControlContext = useControlContext(); + const [item, mediaSource] = [ + ControlContext?.item, + ControlContext?.mediaSource, + ]; + const router = useRouter(); - const { subtitleIndex, audioIndex } = useLocalSearchParams<{ + const { subtitleIndex, audioIndex, bitrateValue } = useLocalSearchParams<{ itemId: string; audioIndex: string; subtitleIndex: string; @@ -25,24 +25,65 @@ const DropdownView: React.FC = ({ bitrateValue: string; }>(); + const changeBitrate = useCallback( + (bitrate: string) => { + const queryParams = new URLSearchParams({ + itemId: item.Id ?? "", + audioIndex: audioIndex?.toString() ?? "", + subtitleIndex: subtitleIndex.toString() ?? "", + mediaSourceId: mediaSource?.Id ?? "", + bitrateValue: bitrate.toString(), + }).toString(); + // @ts-expect-error + router.replace(`player/direct-player?${queryParams}`); + }, + [item, mediaSource, subtitleIndex, audioIndex], + ); + return ( - - + + - + + Quality + + + {BITRATES?.map((bitrate, idx: number) => ( + + changeBitrate(bitrate.value?.toString() ?? "") + } + > + + {bitrate.key} + + + ))} + + + + Subtitle = ({ - + Audio 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 8a37659a..d936cb90 100644 --- a/components/vlc/VideoDebugInfo.tsx +++ b/components/vlc/VideoDebugInfo.tsx @@ -1,12 +1,10 @@ -import { - TrackInfo, - VlcPlayerViewRef, -} from "@/modules/vlc-player/src/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/Colors.ts b/constants/Colors.ts index 69734810..8702c547 100644 --- a/constants/Colors.ts +++ b/constants/Colors.ts @@ -1,5 +1,7 @@ export const Colors = { primary: "#9334E9", + primaryRGB: "rgb(147 51 234)", + primaryLightRGB: "rgb(192 132 252)", text: "#ECEDEE", background: "#151718", tint: "#fff", 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/google-services.json b/google-services.json new file mode 100644 index 00000000..6901246b --- /dev/null +++ b/google-services.json @@ -0,0 +1,29 @@ +{ + "project_info": { + "project_number": "1039325086281", + "project_id": "streamyfin-4fec1", + "storage_bucket": "streamyfin-4fec1.firebasestorage.app" + }, + "client": [ + { + "client_info": { + "mobilesdk_app_id": "1:1039325086281:android:b270168004aa30c1e1f598", + "android_client_info": { + "package_name": "com.fredrikburmester.streamyfin" + } + }, + "oauth_client": [], + "api_key": [ + { + "current_key": "AIzaSyABKBTRL9dvGuZbx009YFLUAPurFIDovFQ" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [] + } + } + } + ], + "configuration_version": "1" +} 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 new file mode 100644 index 00000000..74a0216e --- /dev/null +++ b/hooks/useFavorite.ts @@ -0,0 +1,109 @@ +import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; +import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client"; +import { getUserLibraryApi } from "@jellyfin/sdk/lib/utils/api"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { useAtom } from "jotai"; +import { useEffect, useMemo, useState } from "react"; + +export const useFavorite = (item: BaseItemDto) => { + const queryClient = useQueryClient(); + const [api] = useAtom(apiAtom); + const [user] = useAtom(userAtom); + const type = "item"; + const [isFavorite, setIsFavorite] = useState(item.UserData?.IsFavorite); + + useEffect(() => { + setIsFavorite(item.UserData?.IsFavorite); + }, [item.UserData?.IsFavorite]); + + const updateItemInQueries = (newData: Partial) => { + queryClient.setQueryData( + [type, item.Id], + (old) => { + if (!old) return old; + return { + ...old, + ...newData, + UserData: { ...old.UserData, ...newData.UserData }, + }; + }, + ); + }; + + const markFavoriteMutation = useMutation({ + mutationFn: async () => { + if (api && user) { + await getUserLibraryApi(api).markFavoriteItem({ + userId: user.Id, + itemId: item.Id!, + }); + } + }, + onMutate: async () => { + await queryClient.cancelQueries({ queryKey: [type, item.Id] }); + const previousItem = queryClient.getQueryData([ + type, + item.Id, + ]); + updateItemInQueries({ UserData: { IsFavorite: true } }); + + return { previousItem }; + }, + onError: (err, variables, context) => { + if (context?.previousItem) { + queryClient.setQueryData([type, item.Id], context.previousItem); + } + }, + onSettled: () => { + queryClient.invalidateQueries({ queryKey: [type, item.Id] }); + queryClient.invalidateQueries({ queryKey: ["home", "favorites"] }); + setIsFavorite(true); + }, + }); + + const unmarkFavoriteMutation = useMutation({ + mutationFn: async () => { + if (api && user) { + await getUserLibraryApi(api).unmarkFavoriteItem({ + userId: user.Id, + itemId: item.Id!, + }); + } + }, + onMutate: async () => { + await queryClient.cancelQueries({ queryKey: [type, item.Id] }); + const previousItem = queryClient.getQueryData([ + type, + item.Id, + ]); + updateItemInQueries({ UserData: { IsFavorite: false } }); + + return { previousItem }; + }, + onError: (err, variables, context) => { + if (context?.previousItem) { + queryClient.setQueryData([type, item.Id], context.previousItem); + } + }, + onSettled: () => { + queryClient.invalidateQueries({ queryKey: [type, item.Id] }); + queryClient.invalidateQueries({ queryKey: ["home", "favorites"] }); + setIsFavorite(false); + }, + }); + + const toggleFavorite = () => { + if (isFavorite) { + unmarkFavoriteMutation.mutate(); + } else { + markFavoriteMutation.mutate(); + } + }; + + return { + isFavorite, + toggleFavorite, + markFavoriteMutation, + unmarkFavoriteMutation, + }; +}; 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 9d3b37c7..920d3404 100644 --- a/hooks/useJellyseerr.ts +++ b/hooks/useJellyseerr.ts @@ -1,50 +1,57 @@ -import axios, { AxiosError, AxiosInstance } from "axios"; -import { Results } 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 } 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; page: number; - language: string; + // language: string; } interface SearchResults { @@ -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,19 +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 { - const response = await this.axios?.get( - Endpoints.API_V1 + Endpoints.SEARCH, - { params } - ); - return response?.data; + return this.axios + ?.get(Endpoints.API_V1 + Endpoints.SEARCH, { params }) + .then(({ data }) => data); } async request(request: MediaRequestBody): Promise { @@ -227,6 +239,27 @@ export class JellyseerrApi { .then(({ data }) => data); } + async getRequest(id: number): Promise { + return this.axios + ?.get(Endpoints.API_V1 + Endpoints.REQUEST + `/${id}`) + .then(({ data }) => data); + } + + 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); + } + async movieDetails(id: number) { return this.axios ?.get(Endpoints.API_V1 + Endpoints.MOVIE + `/${id}`) @@ -249,7 +282,7 @@ export class JellyseerrApi { Endpoints.API_V1 + Endpoints.PERSON + `/${id}` + - Endpoints.COMBINED_CREDITS + Endpoints.COMBINED_CREDITS, ) .then((response) => { return response?.data; @@ -259,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); } @@ -275,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); } @@ -283,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; @@ -292,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`; @@ -329,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() { @@ -348,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; @@ -362,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]; @@ -387,7 +421,7 @@ export class JellyseerrApi { }, (error) => { console.error("Jellyseerr request error", error); - } + }, ); } } @@ -423,35 +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 is Results[] => { + items: any | null | undefined, + ): items is Results => { return ( - !items || - (items.length >= 0 && - Object.hasOwn(items[0], "mediaType") && - Object.values(MediaType).includes(items[0]["mediaType"])) + items && + Object.hasOwn(items, "mediaType") && + Object.values(MediaType).includes(items["mediaType"]) ); }; + 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; + }; + + 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 => { + return isJellyseerrResult(item) + ? item.mediaType + : item?.mediaInfo?.mediaType; + }; + const jellyseerrRegion = useMemo( () => jellyseerrUser?.settings?.region || "US", - [jellyseerrUser] + [jellyseerrUser], ); const jellyseerrLocale = useMemo(() => { @@ -464,6 +537,9 @@ export const useJellyseerr = () => { setJellyseerrUser, clearAllJellyseerData, isJellyseerrResult, + getTitle, + getYear, + getMediaType, jellyseerrRegion, jellyseerrLocale, requestMedia, 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/useOrientationSettings.ts b/hooks/useOrientationSettings.ts deleted file mode 100644 index 7b657d77..00000000 --- a/hooks/useOrientationSettings.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { useSettings } from "@/utils/atoms/settings"; -import * as ScreenOrientation from "@/packages/expo-screen-orientation"; -import { useEffect } from "react"; -import { Platform } from "react-native"; - -export const useOrientationSettings = () => { - if (Platform.isTV) return; - - const [settings] = useSettings(); - - useEffect(() => { - if (settings?.autoRotate) { - ScreenOrientation.lockAsync( - ScreenOrientation.OrientationLock.LANDSCAPE_RIGHT - ); - } else if (settings?.defaultVideoOrientation) { - ScreenOrientation.lockAsync(settings.defaultVideoOrientation); - } - - return () => { - if (settings?.autoRotate) { - ScreenOrientation.unlockAsync(); - } else { - ScreenOrientation.lockAsync( - ScreenOrientation.OrientationLock.PORTRAIT_UP - ); - } - }; - }, [settings]); -}; diff --git a/hooks/useRemuxHlsToMp4.ts b/hooks/useRemuxHlsToMp4.ts index 925d9778..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"; @@ -11,21 +11,25 @@ import * as FileSystem from "expo-file-system"; import { useRouter } from "expo-router"; // import { FFmpegKit, FFmpegSession, Statistics } from "ffmpeg-kit-react-native"; -const FFMPEGKitReactNative = !Platform.isTV ? require("ffmpeg-kit-react-native") : null; +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 -const FFmpegKit = FFMPEGKitReactNative.FFmpegKit; +type Statistics = typeof FFMPEGKitReactNative.Statistics; +const FFmpegKit = Platform.isTV + ? null + : (FFMPEGKitReactNative.FFmpegKit as typeof FFMPEGKitReactNative.FFmpegKit); const createFFmpegCommand = (url: string, output: string) => [ "-y", // overwrite output files without asking "-thread_queue_size 512", // https://ffmpeg.org/ffmpeg.html#toc-Advanced-options @@ -101,7 +105,10 @@ export const useRemuxHlsToMp4 = () => { } setProcesses((prev: any[]) => { - return prev.filter((process: { itemId: string | undefined; }) => process.itemId !== item.Id); + return prev.filter( + (process: { itemId: string | undefined }) => + process.itemId !== item.Id, + ); }); } catch (e) { console.error(e); @@ -109,7 +116,7 @@ export const useRemuxHlsToMp4 = () => { console.log("completeCallback ~ end"); }, - [processes, setProcesses] + [processes, setProcesses], ); const statisticsCallback = useCallback( @@ -126,7 +133,7 @@ export const useRemuxHlsToMp4 = () => { if (!item.Id) throw new Error("Item is undefined"); setProcesses((prev: any[]) => { - return prev.map((process: { itemId: string | undefined; }) => { + return prev.map((process: { itemId: string | undefined }) => { if (process.itemId === item.Id) { return { ...process, @@ -139,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, { @@ -161,15 +168,18 @@ export const useRemuxHlsToMp4 = () => { // First lets save any important assets we want to present to the user offline await onSaveAssets(api, item); - toast.success(t("home.downloads.toasts.download_started_for", {item: item.Name}), { - action: { - label: "Go to download", - onClick: () => { - router.push("/downloads"); - toast.dismiss(); + toast.success( + t("home.downloads.toasts.download_started_for", { item: item.Name }), + { + action: { + label: "Go to download", + onClick: () => { + router.push("/downloads"); + toast.dismiss(); + }, }, }, - }); + ); try { const job: JobStatus = { @@ -191,22 +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); + return prev.filter( + (process: { itemId: string | undefined }) => + 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 new file mode 100644 index 00000000..6552f6ea --- /dev/null +++ b/hooks/useSessions.ts @@ -0,0 +1,46 @@ +import { apiAtom } from "@/providers/JellyfinProvider"; +import { userAtom } from "@/providers/JellyfinProvider"; +import { getSessionApi } from "@jellyfin/sdk/lib/utils/api/session-api"; +import { useQuery } from "@tanstack/react-query"; +import { useAtom } from "jotai"; +import { Platform } from "react-native"; +const Notifications = !Platform.isTV ? require("expo-notifications") : null; + +export interface useSessionsProps { + refetchInterval: number; + activeWithinSeconds: number; +} + +export const useSessions = ({ + refetchInterval = 5 * 1000, + activeWithinSeconds = 360, +}: useSessionsProps) => { + const [api] = useAtom(apiAtom); + const [user] = useAtom(userAtom); + + const { data, isLoading } = useQuery({ + queryKey: ["sessions"], + queryFn: async () => { + if (!api || !user || !user.Policy?.IsAdministrator) { + return []; + } + 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 ?? "", + ), + ); + + // Notifications.setBadgeCountAsync(result.length); + return result; + }, + refetchInterval: refetchInterval, + }); + + return { sessions: data, isLoading }; +}; 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 ff8cc4e5..040e1a4f 100644 --- a/i18n.ts +++ b/i18n.ts @@ -1,24 +1,34 @@ 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 nl from "./translations/nl.json"; -import sv from "./translations/sv.json"; import it from "./translations/it.json"; -import zhTW from './translations/zh-TW.json'; -import { getLocales } from "expo-localization"; +import ja from "./translations/ja.json"; +import nl from "./translations/nl.json"; +import pl from "./translations/pl.json"; +import sv from "./translations/sv.json"; +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" }, { label: "English", value: "en" }, { label: "Español", value: "es" }, { label: "Français", value: "fr" }, - { label: "Nederlands", value: "nl" }, - { label: "Svenska", value: "sv" }, { label: "Italiano", value: "it" }, + { label: "日本語", value: "ja" }, + { label: "Türkçe", value: "tr" }, + { label: "Nederlands", value: "nl" }, + { label: "Polski", value: "pl" }, + { label: "Svenska", value: "sv" }, + { label: "Українська", value: "ua" }, + { label: "简体中文", value: "zh-CN" }, { label: "繁體中文", value: "zh-TW" }, ]; @@ -29,9 +39,14 @@ i18n.use(initReactI18next).init({ en: { translation: en }, es: { translation: es }, fr: { translation: fr }, - nl: { translation: nl }, - sv: { translation: sv }, it: { translation: it }, + ja: { translation: ja }, + nl: { translation: nl }, + pl: { translation: pl }, + sv: { translation: sv }, + tr: { translation: tr }, + ua: { translation: ua }, + "zh-CN": { translation: zhCN }, "zh-TW": { translation: zhTW }, }, diff --git a/login.yaml b/login.yaml new file mode 100644 index 00000000..54418a9b --- /dev/null +++ b/login.yaml @@ -0,0 +1,6 @@ +# login.yaml + +appId: your.app.id +--- +- launchApp +- tapOn: "Text on the screen" diff --git a/modules/vlc-player/src/VlcPlayer.types.ts b/modules/VlcPlayer.types.ts similarity index 98% rename from modules/vlc-player/src/VlcPlayer.types.ts rename to modules/VlcPlayer.types.ts index e1c37797..128ac60c 100644 --- a/modules/vlc-player/src/VlcPlayer.types.ts +++ b/modules/VlcPlayer.types.ts @@ -59,7 +59,7 @@ export type ChapterInfo = { export type VlcPlayerViewProps = { source: VlcPlayerSource; - style?: Object; + style?: Record; progressUpdateInterval?: number; paused?: boolean; muted?: boolean; diff --git a/modules/vlc-player/src/VlcPlayerView.tsx b/modules/VlcPlayerView.tsx similarity index 85% rename from modules/vlc-player/src/VlcPlayerView.tsx rename to modules/VlcPlayerView.tsx index 8195d6a9..a08ed44e 100644 --- a/modules/vlc-player/src/VlcPlayerView.tsx +++ b/modules/VlcPlayerView.tsx @@ -1,21 +1,35 @@ 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"; interface NativeViewRef extends VlcPlayerViewRef { setNativeProps?: (props: Partial) => void; } -const NativeViewManager = requireNativeViewManager("VlcPlayer"); +const VLCViewManager = requireNativeViewManager("VlcPlayer"); +const VLC3ViewManager = requireNativeViewManager("VlcPlayer3"); // Create a forwarded ref version of the native view const NativeView = React.forwardRef( - (props, ref) => + (props, ref) => { + const [settings] = useSettings(); + + if (Platform.OS === "ios" || Platform.isTVOS) { + if (settings.defaultPlayer == VideoPlayer.VLC_3) { + console.log("[Apple] Using Vlc Player 3"); + return ; + } + } + console.log("Using default Vlc Player"); + return ; + }, ); const VlcPlayerView = React.forwardRef( @@ -24,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(); @@ -129,7 +143,7 @@ const VlcPlayerView = React.forwardRef( onPipStarted={onPipStarted} /> ); - } + }, ); export default VlcPlayerView; diff --git a/modules/index.ts b/modules/index.ts new file mode 100644 index 00000000..63eb373e --- /dev/null +++ b/modules/index.ts @@ -0,0 +1,27 @@ +import { + ChapterInfo, + PlaybackStatePayload, + ProgressUpdatePayload, + TrackInfo, + VideoLoadStartPayload, + VideoProgressPayload, + VideoStateChangePayload, + VlcPlayerSource, + VlcPlayerViewProps, + VlcPlayerViewRef, +} from "./VlcPlayer.types"; +import VlcPlayerView from "./VlcPlayerView"; + +export { + VlcPlayerView, + VlcPlayerViewProps, + VlcPlayerViewRef, + PlaybackStatePayload, + ProgressUpdatePayload, + VideoLoadStartPayload, + VideoStateChangePayload, + VideoProgressPayload, + VlcPlayerSource, + TrackInfo, + ChapterInfo, +}; diff --git a/modules/vlc-player-3/expo-module.config.json b/modules/vlc-player-3/expo-module.config.json new file mode 100644 index 00000000..1e6766d7 --- /dev/null +++ b/modules/vlc-player-3/expo-module.config.json @@ -0,0 +1,6 @@ +{ + "platforms": ["ios", "tvos"], + "ios": { + "modules": ["VlcPlayer3Module"] + } +} diff --git a/modules/vlc-player-3/ios/VlcPlayer3.podspec b/modules/vlc-player-3/ios/VlcPlayer3.podspec new file mode 100644 index 00000000..15274a12 --- /dev/null +++ b/modules/vlc-player-3/ios/VlcPlayer3.podspec @@ -0,0 +1,23 @@ +Pod::Spec.new do |s| + s.name = 'VlcPlayer3' + s.version = '3.6.1b1' + s.summary = 'A sample project summary' + s.description = 'A sample project description' + s.author = '' + s.homepage = 'https://docs.expo.dev/modules/' + s.platforms = { :ios => '13.4', :tvos => '13.4' } + s.source = { git: '' } + s.static_framework = true + + s.dependency 'ExpoModulesCore' + s.ios.dependency 'MobileVLCKit', s.version + s.tvos.dependency 'TVVLCKit', s.version + + # Swift/Objective-C compatibility + s.pod_target_xcconfig = { + 'DEFINES_MODULE' => 'YES', + 'SWIFT_COMPILATION_MODE' => 'wholemodule' + } + + s.source_files = "*.{h,m,mm,swift,hpp,cpp}" +end diff --git a/modules/vlc-player-3/ios/VlcPlayer3Module.swift b/modules/vlc-player-3/ios/VlcPlayer3Module.swift new file mode 100644 index 00000000..c0e32606 --- /dev/null +++ b/modules/vlc-player-3/ios/VlcPlayer3Module.swift @@ -0,0 +1,71 @@ +import ExpoModulesCore + +public class VlcPlayer3Module: Module { + public func definition() -> ModuleDefinition { + Name("VlcPlayer3") + View(VlcPlayer3View.self) { + Prop("source") { (view: VlcPlayer3View, source: [String: Any]) in + view.setSource(source) + } + + Prop("paused") { (view: VlcPlayer3View, paused: Bool) in + if paused { + view.pause() + } else { + view.play() + } + } + + Events( + "onPlaybackStateChanged", + "onVideoStateChange", + "onVideoLoadStart", + "onVideoLoadEnd", + "onVideoProgress", + "onVideoError", + "onPipStarted" + ) + + AsyncFunction("startPictureInPicture") { (view: VlcPlayer3View) in + view.startPictureInPicture() + } + + AsyncFunction("play") { (view: VlcPlayer3View) in + view.play() + } + + AsyncFunction("pause") { (view: VlcPlayer3View) in + view.pause() + } + + AsyncFunction("stop") { (view: VlcPlayer3View) in + view.stop() + } + + AsyncFunction("seekTo") { (view: VlcPlayer3View, time: Int32) in + view.seekTo(time) + } + + AsyncFunction("setAudioTrack") { (view: VlcPlayer3View, trackIndex: Int) in + view.setAudioTrack(trackIndex) + } + + AsyncFunction("getAudioTracks") { (view: VlcPlayer3View) -> [[String: Any]]? in + return view.getAudioTracks() + } + + AsyncFunction("setSubtitleTrack") { (view: VlcPlayer3View, trackIndex: Int) in + view.setSubtitleTrack(trackIndex) + } + + AsyncFunction("getSubtitleTracks") { (view: VlcPlayer3View) -> [[String: Any]]? in + return view.getSubtitleTracks() + } + + AsyncFunction("setSubtitleURL") { + (view: VlcPlayer3View, url: String, name: String) in + view.setSubtitleURL(url, name: name) + } + } + } +} diff --git a/modules/vlc-player-3/ios/VlcPlayer3View.swift b/modules/vlc-player-3/ios/VlcPlayer3View.swift new file mode 100644 index 00000000..b9189f9f --- /dev/null +++ b/modules/vlc-player-3/ios/VlcPlayer3View.swift @@ -0,0 +1,388 @@ +import ExpoModulesCore +#if os(tvOS) +import TVVLCKit +#else +import MobileVLCKit +#endif +import UIKit + +class VlcPlayer3View: ExpoView { + private var mediaPlayer: VLCMediaPlayer? + private var videoView: UIView? + private var progressUpdateInterval: TimeInterval = 1.0 // Update interval set to 1 second + private var isPaused: Bool = false + private var currentGeometryCString: [CChar]? + private var lastReportedState: VLCMediaPlayerState? + private var lastReportedIsPlaying: Bool? + private var customSubtitles: [(internalName: String, originalName: String)] = [] + private var startPosition: Int32 = 0 + private var isMediaReady: Bool = false + private var externalTrack: [String: String]? + private var progressTimer: DispatchSourceTimer? + private var isStopping: Bool = false // Define isStopping here + private var lastProgressCall = Date().timeIntervalSince1970 + var hasSource = false + + // MARK: - Initialization + + required init(appContext: AppContext? = nil) { + super.init(appContext: appContext) + setupView() + setupNotifications() + } + + // MARK: - Setup + + private func setupView() { + DispatchQueue.main.async { + self.backgroundColor = .black + self.videoView = UIView() + self.videoView?.translatesAutoresizingMaskIntoConstraints = false + + if let videoView = self.videoView { + self.addSubview(videoView) + NSLayoutConstraint.activate([ + videoView.leadingAnchor.constraint(equalTo: self.leadingAnchor), + videoView.trailingAnchor.constraint(equalTo: self.trailingAnchor), + videoView.topAnchor.constraint(equalTo: self.topAnchor), + videoView.bottomAnchor.constraint(equalTo: self.bottomAnchor), + ]) + } + } + } + + private func setupNotifications() { + NotificationCenter.default.addObserver( + self, selector: #selector(applicationWillResignActive), + name: UIApplication.willResignActiveNotification, object: nil) + NotificationCenter.default.addObserver( + self, selector: #selector(applicationDidBecomeActive), + name: UIApplication.didBecomeActiveNotification, object: nil) + } + + // MARK: - Public Methods + func startPictureInPicture() { } + + @objc func play() { + self.mediaPlayer?.play() + self.isPaused = false + print("Play") + } + + @objc func pause() { + self.mediaPlayer?.pause() + self.isPaused = true + } + + @objc func seekTo(_ time: Int32) { + guard let player = self.mediaPlayer else { return } + + let wasPlaying = player.isPlaying + if wasPlaying { + self.pause() + } + + if let duration = player.media?.length.intValue { + print("Seeking to time: \(time) Video Duration \(duration)") + + // If the specified time is greater than the duration, seek to the end + let seekTime = time > duration ? duration - 1000 : time + player.time = VLCTime(int: seekTime) + + if wasPlaying { + self.play() + } + self.updatePlayerState() + } else { + print("Error: Unable to retrieve video duration") + } + } + + @objc func setSource(_ source: [String: Any]) { + DispatchQueue.main.async { [weak self] in + guard let self = self else { return } + if self.hasSource { + return + } + + let mediaOptions = source["mediaOptions"] as? [String: Any] ?? [:] + self.externalTrack = source["externalTrack"] as? [String: String] + var initOptions = source["initOptions"] as? [Any] ?? [] + self.startPosition = source["startPosition"] as? Int32 ?? 0 + initOptions.append("--start-time=\(self.startPosition)") + + guard let uri = source["uri"] as? String, !uri.isEmpty else { + print("Error: Invalid or empty URI") + self.onVideoError?(["error": "Invalid or empty URI"]) + return + } + + let autoplay = source["autoplay"] as? Bool ?? false + let isNetwork = source["isNetwork"] as? Bool ?? false + + self.onVideoLoadStart?(["target": self.reactTag ?? NSNull()]) + self.mediaPlayer = VLCMediaPlayer(options: initOptions) + self.mediaPlayer?.delegate = self + self.mediaPlayer?.drawable = self.videoView + self.mediaPlayer?.scaleFactor = 0 + + let media: VLCMedia + if isNetwork { + print("Loading network file: \(uri)") + media = VLCMedia(url: URL(string: uri)!) + } else { + print("Loading local file: \(uri)") + if uri.starts(with: "file://"), let url = URL(string: uri) { + media = VLCMedia(url: url) + } else { + media = VLCMedia(path: uri) + } + } + + print("Debug: Media options: \(mediaOptions)") + media.addOptions(mediaOptions) + + self.mediaPlayer?.media = media + self.hasSource = true + + if autoplay { + print("Playing...") + self.play() + } + } + } + + @objc func setAudioTrack(_ trackIndex: Int) { + self.mediaPlayer?.currentAudioTrackIndex = Int32(trackIndex) + } + + @objc func getAudioTracks() -> [[String: Any]]? { + guard let trackNames = mediaPlayer?.audioTrackNames, + let trackIndexes = mediaPlayer?.audioTrackIndexes + else { + return nil + } + + return zip(trackNames, trackIndexes).map { name, index in + return ["name": name, "index": index] + } + } + + @objc func setSubtitleTrack(_ trackIndex: Int) { + print("Debug: Attempting to set subtitle track to index: \(trackIndex)") + self.mediaPlayer?.currentVideoSubTitleIndex = Int32(trackIndex) + print( + "Debug: Current subtitle track index after setting: \(self.mediaPlayer?.currentVideoSubTitleIndex ?? -1)" + ) + } + + @objc func setSubtitleURL(_ subtitleURL: String, name: String) { + guard let url = URL(string: subtitleURL) else { + print("Error: Invalid subtitle URL") + return + } + + let result = self.mediaPlayer?.addPlaybackSlave(url, type: .subtitle, enforce: true) + if let result = result { + let internalName = "Track \(self.customSubtitles.count + 1)" + print("Subtitle added with result: \(result) \(internalName)") + self.customSubtitles.append((internalName: internalName, originalName: name)) + } else { + print("Failed to add subtitle") + } + } + + @objc func getSubtitleTracks() -> [[String: Any]]? { + guard let mediaPlayer = self.mediaPlayer else { + return nil + } + + let count = mediaPlayer.numberOfSubtitlesTracks + print("Debug: Number of subtitle tracks: \(count)") + + guard count > 0 else { + return nil + } + + var tracks: [[String: Any]] = [] + + if let names = mediaPlayer.videoSubTitlesNames as? [String], + let indexes = mediaPlayer.videoSubTitlesIndexes as? [NSNumber] + { + for (index, name) in zip(indexes, names) { + if let customSubtitle = customSubtitles.first(where: { $0.internalName == name }) { + tracks.append(["name": customSubtitle.originalName, "index": index.intValue]) + } else { + tracks.append(["name": name, "index": index.intValue]) + } + } + } + + print("Debug: Subtitle tracks: \(tracks)") + return tracks + } + + @objc func stop(completion: (() -> Void)? = nil) { + guard !isStopping else { + completion?() + return + } + isStopping = true + + // If we're not on the main thread, dispatch to main thread + if !Thread.isMainThread { + DispatchQueue.main.async { [weak self] in + self?.performStop(completion: completion) + } + } else { + performStop(completion: completion) + } + } + + // MARK: - Private Methods + + @objc private func applicationWillResignActive() { + + } + + @objc private func applicationDidBecomeActive() { + + } + + private func performStop(completion: (() -> Void)? = nil) { + // Stop the media player + mediaPlayer?.stop() + + // Remove observer + NotificationCenter.default.removeObserver(self) + + // Clear the video view + videoView?.removeFromSuperview() + videoView = nil + + // Release the media player + mediaPlayer?.delegate = nil + mediaPlayer = nil + + isStopping = false + completion?() + } + + private func updateVideoProgress() { + guard let player = self.mediaPlayer else { return } + + let currentTimeMs = player.time.intValue + let durationMs = player.media?.length.intValue ?? 0 + + print("Debug: Current time: \(currentTimeMs)") + if currentTimeMs >= 0 && currentTimeMs < durationMs { + if player.isPlaying && !self.isMediaReady { + self.isMediaReady = true + // Set external track subtitle when starting. + if let externalTrack = self.externalTrack { + if let name = externalTrack["name"], !name.isEmpty { + let deliveryUrl = externalTrack["DeliveryUrl"] ?? "" + self.setSubtitleURL(deliveryUrl, name: name) + } + } + } + self.onVideoProgress?([ + "currentTime": currentTimeMs, + "duration": durationMs, + ]) + } + } + + // MARK: - Expo Events + + @objc var onPlaybackStateChanged: RCTDirectEventBlock? + @objc var onVideoLoadStart: RCTDirectEventBlock? + @objc var onVideoStateChange: RCTDirectEventBlock? + @objc var onVideoProgress: RCTDirectEventBlock? + @objc var onVideoLoadEnd: RCTDirectEventBlock? + @objc var onVideoError: RCTDirectEventBlock? + @objc var onPipStarted: RCTDirectEventBlock? + + // MARK: - Deinitialization + + deinit { + performStop() + } +} + +extension VlcPlayer3View: VLCMediaPlayerDelegate { + func mediaPlayerTimeChanged(_ aNotification: Notification) { + // self?.updateVideoProgress() + let timeNow = Date().timeIntervalSince1970 + if timeNow - lastProgressCall >= 1 { + lastProgressCall = timeNow + updateVideoProgress() + } + } + + func mediaPlayerStateChanged(_ aNotification: Notification) { + self.updatePlayerState() + } + + private func updatePlayerState() { + guard let player = self.mediaPlayer else { return } + let currentState = player.state + + var stateInfo: [String: Any] = [ + "target": self.reactTag ?? NSNull(), + "currentTime": player.time.intValue, + "duration": player.media?.length.intValue ?? 0, + "error": false, + ] + + if player.isPlaying { + stateInfo["isPlaying"] = true + stateInfo["isBuffering"] = false + stateInfo["state"] = "Playing" + } else { + stateInfo["isPlaying"] = false + stateInfo["state"] = "Paused" + } + + if player.state == VLCMediaPlayerState.buffering { + stateInfo["isBuffering"] = true + stateInfo["state"] = "Buffering" + } else if player.state == VLCMediaPlayerState.error { + print("player.state ~ error") + stateInfo["state"] = "Error" + self.onVideoLoadEnd?(stateInfo) + } else if player.state == VLCMediaPlayerState.opening { + print("player.state ~ opening") + stateInfo["state"] = "Opening" + } + + if self.lastReportedState != currentState + || self.lastReportedIsPlaying != player.isPlaying + { + self.lastReportedState = currentState + self.lastReportedIsPlaying = player.isPlaying + self.onVideoStateChange?(stateInfo) + } + + } +} + +extension VlcPlayer3View: VLCMediaDelegate { + // Implement VLCMediaDelegate methods if needed +} + +extension VLCMediaPlayerState { + var description: String { + switch self { + case .opening: return "Opening" + case .buffering: return "Buffering" + case .playing: return "Playing" + case .paused: return "Paused" + case .stopped: return "Stopped" + case .ended: return "Ended" + case .error: return "Error" + case .esAdded: return "ESAdded" + @unknown default: return "Unknown" + } + } +} diff --git a/modules/vlc-player-3/src/VlcPlayer3Module.ts b/modules/vlc-player-3/src/VlcPlayer3Module.ts new file mode 100644 index 00000000..c0501304 --- /dev/null +++ b/modules/vlc-player-3/src/VlcPlayer3Module.ts @@ -0,0 +1,5 @@ +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"); diff --git a/modules/vlc-player/android/.gradle/8.9/checksums/checksums.lock b/modules/vlc-player/android/.gradle/8.9/checksums/checksums.lock deleted file mode 100644 index 52a7f0f4..00000000 Binary files a/modules/vlc-player/android/.gradle/8.9/checksums/checksums.lock and /dev/null differ diff --git a/modules/vlc-player/android/.gradle/8.9/dependencies-accessors/gc.properties b/modules/vlc-player/android/.gradle/8.9/dependencies-accessors/gc.properties deleted file mode 100644 index e69de29b..00000000 diff --git a/modules/vlc-player/android/.gradle/8.9/fileChanges/last-build.bin b/modules/vlc-player/android/.gradle/8.9/fileChanges/last-build.bin deleted file mode 100644 index f76dd238..00000000 Binary files a/modules/vlc-player/android/.gradle/8.9/fileChanges/last-build.bin and /dev/null differ diff --git a/modules/vlc-player/android/.gradle/8.9/fileHashes/fileHashes.lock b/modules/vlc-player/android/.gradle/8.9/fileHashes/fileHashes.lock deleted file mode 100644 index 5743f6c9..00000000 Binary files a/modules/vlc-player/android/.gradle/8.9/fileHashes/fileHashes.lock and /dev/null differ diff --git a/modules/vlc-player/android/.gradle/8.9/gc.properties b/modules/vlc-player/android/.gradle/8.9/gc.properties deleted file mode 100644 index e69de29b..00000000 diff --git a/modules/vlc-player/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock b/modules/vlc-player/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock deleted file mode 100644 index 964eb6bf..00000000 Binary files a/modules/vlc-player/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock and /dev/null differ diff --git a/modules/vlc-player/android/.gradle/buildOutputCleanup/cache.properties b/modules/vlc-player/android/.gradle/buildOutputCleanup/cache.properties deleted file mode 100644 index c062e482..00000000 --- a/modules/vlc-player/android/.gradle/buildOutputCleanup/cache.properties +++ /dev/null @@ -1,2 +0,0 @@ -#Sun Nov 17 18:25:45 AEDT 2024 -gradle.version=8.9 diff --git a/modules/vlc-player/android/.gradle/vcs-1/gc.properties b/modules/vlc-player/android/.gradle/vcs-1/gc.properties deleted file mode 100644 index e69de29b..00000000 diff --git a/modules/vlc-player/android/src/main/java/expo/modules/vlcplayer/VlcPlayerView.kt b/modules/vlc-player/android/src/main/java/expo/modules/vlcplayer/VlcPlayerView.kt index 97d1d7aa..7b4d8721 100644 --- a/modules/vlc-player/android/src/main/java/expo/modules/vlcplayer/VlcPlayerView.kt +++ b/modules/vlc-player/android/src/main/java/expo/modules/vlcplayer/VlcPlayerView.kt @@ -354,7 +354,9 @@ class VlcPlayerView(context: Context, appContext: AppContext) : ExpoView(context ) } - currentActivity.unregisterReceiver(actionReceiver) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + currentActivity.unregisterReceiver(actionReceiver) + } currentActivity.removeOnPictureInPictureModeChangedListener(pipChangeListener) VLCManager.listeners.clear() diff --git a/modules/vlc-player/index.ts b/modules/vlc-player/index.ts deleted file mode 100644 index d4b089cf..00000000 --- a/modules/vlc-player/index.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { - EventEmitter, - EventSubscription, -} from "expo-modules-core"; - -import VlcPlayerModule from "./src/VlcPlayerModule"; -import VlcPlayerView from "./src/VlcPlayerView"; -import { - PlaybackStatePayload, - ProgressUpdatePayload, - VideoLoadStartPayload, - VideoStateChangePayload, - VideoProgressPayload, - VlcPlayerSource, - TrackInfo, - ChapterInfo, - VlcPlayerViewProps, - VlcPlayerViewRef, -} from "./src/VlcPlayer.types"; - -const emitter = new EventEmitter(VlcPlayerModule); - -export function addPlaybackStateListener( - listener: (event: PlaybackStatePayload) => void -): EventSubscription { - return emitter.addListener( - "onPlaybackStateChanged", - listener - ); -} - -export function addVideoLoadStartListener( - listener: (event: VideoLoadStartPayload) => void -): EventSubscription { - return emitter.addListener( - "onVideoLoadStart", - listener - ); -} - -export function addVideoStateChangeListener( - listener: (event: VideoStateChangePayload) => void -): EventSubscription { - return emitter.addListener( - "onVideoStateChange", - listener - ); -} - -export function addVideoProgressListener( - listener: (event: VideoProgressPayload) => void -): EventSubscription { - return emitter.addListener("onVideoProgress", listener); -} - -export { - VlcPlayerView, - VlcPlayerViewProps, - VlcPlayerViewRef, - PlaybackStatePayload, - ProgressUpdatePayload, - VideoLoadStartPayload, - VideoStateChangePayload, - VideoProgressPayload, - VlcPlayerSource, - TrackInfo, - ChapterInfo, -}; diff --git a/modules/vlc-player/ios/VlcPlayer.podspec b/modules/vlc-player/ios/VlcPlayer.podspec index 97f58881..46dbafd1 100644 --- a/modules/vlc-player/ios/VlcPlayer.podspec +++ b/modules/vlc-player/ios/VlcPlayer.podspec @@ -5,19 +5,19 @@ Pod::Spec.new do |s| s.description = 'A sample project description' s.author = '' s.homepage = 'https://docs.expo.dev/modules/' - s.platforms = { :ios => '13.4', :tvos => '13.4' } + s.platforms = { :ios => '13.4', :tvos => '16' } s.source = { git: '' } s.static_framework = true s.dependency 'ExpoModulesCore' s.ios.dependency 'VLCKit', s.version s.tvos.dependency 'VLCKit', s.version + s.dependency 'Alamofire', '~> 5.10' # Swift/Objective-C compatibility s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'SWIFT_COMPILATION_MODE' => 'wholemodule' } - - s.source_files = "**/*.{h,m,mm,swift,hpp,cpp}" + s.source_files = "*.{h,m,mm,swift,hpp,cpp}" end diff --git a/modules/vlc-player/ios/VlcPlayerView.swift b/modules/vlc-player/ios/VlcPlayerView.swift index 1c6dd9ea..f02478d2 100644 --- a/modules/vlc-player/ios/VlcPlayerView.swift +++ b/modules/vlc-player/ios/VlcPlayerView.swift @@ -3,7 +3,6 @@ import UIKit import VLCKit import os - public class VLCPlayerView: UIView { func setupView(parent: UIView) { self.backgroundColor = .black @@ -402,7 +401,7 @@ class VlcPlayerView: ExpoView { } private func updateVideoProgress() { - guard let media = self.vlc.player.media else { return } + guard self.vlc.player.media != nil else { return } let currentTimeMs = self.vlc.player.time.intValue let durationMs = self.vlc.player.media?.length.intValue ?? 0 @@ -459,7 +458,9 @@ extension VlcPlayerView: SimpleAppLifecycleListener { } // Current solution to fixing black screen when re-entering application - if let videoTrack = self.vlc.player.videoTracks.first { $0.isSelected == true }, !self.vlc.isMediaPlaying() { + if let videoTrack = self.vlc.player.videoTracks.first(where: { $0.isSelected == true }), + !self.vlc.isMediaPlaying() + { videoTrack.isSelected = false videoTrack.isSelectedExclusively = true self.vlc.player.play() @@ -477,6 +478,7 @@ extension VLCMediaPlayerState { case .paused: return "Paused" case .stopped: return "Stopped" case .error: return "Error" + case .stopping: return "Stopping" @unknown default: return "Unknown" } } 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 a04b6b4b..c8592aff 100644 --- a/package.json +++ b/package.json @@ -12,10 +12,8 @@ "android:tv": "EXPO_TV=1 expo run:android", "prebuild": "EXPO_TV=0 bun run clean", "prebuild:tv": "EXPO_TV=1 bun run clean", - "prebuild:tv-new": "EXPO_TV=1 node ./scripts/symlink-native-dirs.js; bun run prebuild:tv", - "test": "jest --watchAll", - "lint": "expo lint", - "postinstall": "patch-package" + "prepare": "husky", + "lint": "biome format --write ." }, "dependencies": { "@bottom-tabs/react-navigation": "0.8.6", @@ -74,7 +72,7 @@ "react-i18next": "^15.4.0", "react-native": "npm:react-native-tvos@~0.77.0-0", "react-native-awesome-slider": "^2.9.0", - "react-native-bottom-tabs": "0.8.7", + "react-native-bottom-tabs": "0.8.6", "react-native-circular-progress": "^1.4.1", "react-native-compressor": "^1.10.3", "react-native-country-flag": "^2.0.2", @@ -112,25 +110,28 @@ }, "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", + "@types/lodash": "^4.17.15", "@types/react": "~18.3.12", + "@types/react-native-vector-icons": "^6.4.18", "@types/react-test-renderer": "^19.0.0", - "patch-package": "^8.0.0", + "@types/uuid": "^10.0.0", + "husky": "^9.1.7", + "lint-staged": "^15.5.0", "postinstall-postinstall": "^2.1.0", "react-test-renderer": "19.0.0", - "typescript": "~5.7.3", - "@types/lodash": "^4.17.15", - "@types/react-native-vector-icons": "^6.4.18", - "@types/uuid": "^10.0.0" + "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/patches/@expo+react-native-action-sheet+4.1.0.patch b/patches/@expo+react-native-action-sheet+4.1.0.patch deleted file mode 100644 index c2640492..00000000 --- a/patches/@expo+react-native-action-sheet+4.1.0.patch +++ /dev/null @@ -1,18 +0,0 @@ -diff --git a/node_modules/@expo/react-native-action-sheet/lib/commonjs/ActionSheet/CustomActionSheet.js b/node_modules/@expo/react-native-action-sheet/lib/commonjs/ActionSheet/CustomActionSheet.js -index 2a6943f..42d40e0 100644 ---- a/node_modules/@expo/react-native-action-sheet/lib/commonjs/ActionSheet/CustomActionSheet.js -+++ b/node_modules/@expo/react-native-action-sheet/lib/commonjs/ActionSheet/CustomActionSheet.js -@@ -1,2 +1,2 @@ --var _interopRequireDefault=require("@babel/runtime/helpers/interopRequireDefault");var _interopRequireWildcard=require("@babel/runtime/helpers/interopRequireWildcard");Object.defineProperty(exports,"__esModule",{value:true});exports.default=void 0;var _classCallCheck2=_interopRequireDefault(require("@babel/runtime/helpers/classCallCheck"));var _createClass2=_interopRequireDefault(require("@babel/runtime/helpers/createClass"));var _assertThisInitialized2=_interopRequireDefault(require("@babel/runtime/helpers/assertThisInitialized"));var _inherits2=_interopRequireDefault(require("@babel/runtime/helpers/inherits"));var _possibleConstructorReturn2=_interopRequireDefault(require("@babel/runtime/helpers/possibleConstructorReturn"));var _getPrototypeOf2=_interopRequireDefault(require("@babel/runtime/helpers/getPrototypeOf"));var React=_interopRequireWildcard(require("react"));var _reactNative=require("react-native");var _ActionGroup=_interopRequireDefault(require("./ActionGroup"));var _jsxFileName="/home/runner/work/react-native-action-sheet/react-native-action-sheet/src/ActionSheet/CustomActionSheet.tsx";function _createSuper(Derived){var hasNativeReflectConstruct=_isNativeReflectConstruct();return function _createSuperInternal(){var Super=(0,_getPrototypeOf2.default)(Derived),result;if(hasNativeReflectConstruct){var NewTarget=(0,_getPrototypeOf2.default)(this).constructor;result=Reflect.construct(Super,arguments,NewTarget);}else{result=Super.apply(this,arguments);}return(0,_possibleConstructorReturn2.default)(this,result);};}function _isNativeReflectConstruct(){if(typeof Reflect==="undefined"||!Reflect.construct)return false;if(Reflect.construct.sham)return false;if(typeof Proxy==="function")return true;try{Date.prototype.toString.call(Reflect.construct(Date,[],function(){}));return true;}catch(e){return false;}}var OPACITY_ANIMATION_IN_TIME=225;var OPACITY_ANIMATION_OUT_TIME=195;var EASING_OUT=_reactNative.Easing.bezier(0.25,0.46,0.45,0.94);var EASING_IN=_reactNative.Easing.out(EASING_OUT);var ESCAPE_KEY='Escape';var CustomActionSheet=function(_React$Component){(0,_inherits2.default)(CustomActionSheet,_React$Component);var _super=_createSuper(CustomActionSheet);function CustomActionSheet(){var _this;(0,_classCallCheck2.default)(this,CustomActionSheet);for(var _len=arguments.length,args=new Array(_len),_key=0;_key<_len;_key++){args[_key]=arguments[_key];}_this=_super.call.apply(_super,[this].concat(args));_this._actionSheetHeight=360;_this.state={isVisible:false,isAnimating:false,options:null,onSelect:null,overlayOpacity:new _reactNative.Animated.Value(0),sheetOpacity:new _reactNative.Animated.Value(0)};_this._deferAfterAnimation=undefined;_this._handleWebKeyDown=function(event){if(event.key===ESCAPE_KEY&&_this.state.isVisible){event.preventDefault();_this._selectCancelButton();}};_this._setActionSheetHeight=function(_ref){var nativeEvent=_ref.nativeEvent;return _this._actionSheetHeight=nativeEvent.layout.height;};_this.showActionSheetWithOptions=function(options,onSelect){var _this$state=_this.state,isVisible=_this$state.isVisible,overlayOpacity=_this$state.overlayOpacity,sheetOpacity=_this$state.sheetOpacity;var _this$props$useNative=_this.props.useNativeDriver,useNativeDriver=_this$props$useNative===void 0?true:_this$props$useNative;if(isVisible){_this._deferAfterAnimation=_this.showActionSheetWithOptions.bind((0,_assertThisInitialized2.default)(_this),options,onSelect);return;}_this.setState({options:options,onSelect:onSelect,isVisible:true,isAnimating:true});overlayOpacity.setValue(0);sheetOpacity.setValue(0);_reactNative.Animated.parallel([_reactNative.Animated.timing(overlayOpacity,{toValue:0.32,easing:EASING_OUT,duration:OPACITY_ANIMATION_IN_TIME,useNativeDriver:useNativeDriver}),_reactNative.Animated.timing(sheetOpacity,{toValue:1,easing:EASING_OUT,duration:OPACITY_ANIMATION_IN_TIME,useNativeDriver:useNativeDriver})]).start(function(result){if(result.finished){_this.setState({isAnimating:false});_this._deferAfterAnimation=undefined;}});_reactNative.BackHandler.addEventListener('actionSheetHardwareBackPress',_this._selectCancelButton);};_this._selectCancelButton=function(){var options=_this.state.options;if(!options){return false;}if(typeof options.cancelButtonIndex==='undefined'){return false;}else if(typeof options.cancelButtonIndex==='number'){return _this._onSelect(options.cancelButtonIndex);}else{return _this._animateOut();}};_this._onSelect=function(index){var _this$state2=_this.state,isAnimating=_this$state2.isAnimating,onSelect=_this$state2.onSelect;if(isAnimating){return false;}if(onSelect){_this._deferAfterAnimation=onSelect.bind((0,_assertThisInitialized2.default)(_this),index);}return _this._animateOut();};_this._animateOut=function(){var _this$state3=_this.state,isAnimating=_this$state3.isAnimating,overlayOpacity=_this$state3.overlayOpacity,sheetOpacity=_this$state3.sheetOpacity;var _this$props$useNative2=_this.props.useNativeDriver,useNativeDriver=_this$props$useNative2===void 0?true:_this$props$useNative2;if(isAnimating){return false;}_reactNative.BackHandler.removeEventListener('actionSheetHardwareBackPress',_this._selectCancelButton);_this.setState({isAnimating:true});_reactNative.Animated.parallel([_reactNative.Animated.timing(overlayOpacity,{toValue:0,easing:EASING_IN,duration:OPACITY_ANIMATION_OUT_TIME,useNativeDriver:useNativeDriver}),_reactNative.Animated.timing(sheetOpacity,{toValue:0,easing:EASING_IN,duration:OPACITY_ANIMATION_OUT_TIME,useNativeDriver:useNativeDriver})]).start(function(result){if(result.finished){_this.setState({isVisible:false,isAnimating:false});if(_this._deferAfterAnimation){_this._deferAfterAnimation();}}});return true;};return _this;}(0,_createClass2.default)(CustomActionSheet,[{key:"componentDidMount",value:function componentDidMount(){if(_reactNative.Platform.OS==='web'){document.addEventListener('keydown',this._handleWebKeyDown);}}},{key:"componentWillUnmount",value:function componentWillUnmount(){if(_reactNative.Platform.OS==='web'){document.removeEventListener('keydown',this._handleWebKeyDown);}}},{key:"render",value:function render(){var _this$state4=this.state,isVisible=_this$state4.isVisible,overlayOpacity=_this$state4.overlayOpacity,options=_this$state4.options;var useModal=options?options.autoFocus||options.useModal===true:false;var overlay=isVisible?React.createElement(_reactNative.Animated.View,{style:[styles.overlay,{opacity:overlayOpacity}],__source:{fileName:_jsxFileName,lineNumber:79,columnNumber:7}}):null;var appContent=React.createElement(_reactNative.View,{style:styles.flexContainer,importantForAccessibility:isVisible?'no-hide-descendants':'auto',__source:{fileName:_jsxFileName,lineNumber:91,columnNumber:7}},React.Children.only(this.props.children));return React.createElement(_reactNative.View,{pointerEvents:this.props.pointerEvents,style:styles.flexContainer,__source:{fileName:_jsxFileName,lineNumber:99,columnNumber:7}},appContent,isVisible&&!useModal&&React.createElement(React.Fragment,null,overlay,this._renderSheet()),isVisible&&useModal&&React.createElement(_reactNative.Modal,{animationType:"none",transparent:true,onRequestClose:this._selectCancelButton,__source:{fileName:_jsxFileName,lineNumber:108,columnNumber:11}},overlay,this._renderSheet()));}},{key:"_renderSheet",value:function _renderSheet(){var _this$state5=this.state,options=_this$state5.options,isAnimating=_this$state5.isAnimating,sheetOpacity=_this$state5.sheetOpacity;if(!options){return null;}var optionsArray=options.options,icons=options.icons,tintIcons=options.tintIcons,destructiveButtonIndex=options.destructiveButtonIndex,disabledButtonIndices=options.disabledButtonIndices,destructiveColor=options.destructiveColor,textStyle=options.textStyle,tintColor=options.tintColor,title=options.title,titleTextStyle=options.titleTextStyle,message=options.message,messageTextStyle=options.messageTextStyle,autoFocus=options.autoFocus,showSeparators=options.showSeparators,containerStyle=options.containerStyle,separatorStyle=options.separatorStyle,cancelButtonIndex=options.cancelButtonIndex,cancelButtonTintColor=options.cancelButtonTintColor;return React.createElement(_reactNative.TouchableWithoutFeedback,{importantForAccessibility:"yes",onPress:this._selectCancelButton,__source:{fileName:_jsxFileName,lineNumber:145,columnNumber:7}},React.createElement(_reactNative.Animated.View,{needsOffscreenAlphaCompositing:isAnimating,style:[styles.sheetContainer,{opacity:sheetOpacity,transform:[{translateY:sheetOpacity.interpolate({inputRange:[0,1],outputRange:[this._actionSheetHeight,0]})}]}],__source:{fileName:_jsxFileName,lineNumber:146,columnNumber:9}},React.createElement(_reactNative.View,{style:styles.sheet,onLayout:this._setActionSheetHeight,__source:{fileName:_jsxFileName,lineNumber:162,columnNumber:11}},React.createElement(_ActionGroup.default,{options:optionsArray,icons:icons,tintIcons:tintIcons===undefined?true:tintIcons,cancelButtonIndex:cancelButtonIndex,cancelButtonTintColor:cancelButtonTintColor,destructiveButtonIndex:destructiveButtonIndex,destructiveColor:destructiveColor,disabledButtonIndices:disabledButtonIndices,onSelect:this._onSelect,startIndex:0,length:optionsArray.length,textStyle:textStyle||{},tintColor:tintColor,title:title||undefined,titleTextStyle:titleTextStyle,message:message||undefined,messageTextStyle:messageTextStyle,autoFocus:autoFocus,showSeparators:showSeparators,containerStyle:containerStyle,separatorStyle:separatorStyle,__source:{fileName:_jsxFileName,lineNumber:163,columnNumber:13}}))));}}]);return CustomActionSheet;}(React.Component);exports.default=CustomActionSheet;var styles=_reactNative.StyleSheet.create({flexContainer:{flex:1},overlay:{position:'absolute',top:0,right:0,bottom:0,left:0,backgroundColor:'black'},sheetContainer:{position:'absolute',left:0,right:0,bottom:0,top:0,backgroundColor:'transparent',alignItems:'flex-end',justifyContent:'center',flexDirection:'row'},sheet:{flex:1,backgroundColor:'transparent'}}); -+var _interopRequireDefault=require("@babel/runtime/helpers/interopRequireDefault");var _interopRequireWildcard=require("@babel/runtime/helpers/interopRequireWildcard");Object.defineProperty(exports,"__esModule",{value:true});exports.default=void 0;var _classCallCheck2=_interopRequireDefault(require("@babel/runtime/helpers/classCallCheck"));var _createClass2=_interopRequireDefault(require("@babel/runtime/helpers/createClass"));var _assertThisInitialized2=_interopRequireDefault(require("@babel/runtime/helpers/assertThisInitialized"));var _inherits2=_interopRequireDefault(require("@babel/runtime/helpers/inherits"));var _possibleConstructorReturn2=_interopRequireDefault(require("@babel/runtime/helpers/possibleConstructorReturn"));var _getPrototypeOf2=_interopRequireDefault(require("@babel/runtime/helpers/getPrototypeOf"));var React=_interopRequireWildcard(require("react"));var _reactNative=require("react-native");var _ActionGroup=_interopRequireDefault(require("./ActionGroup"));var _jsxFileName="/home/runner/work/react-native-action-sheet/react-native-action-sheet/src/ActionSheet/CustomActionSheet.tsx";function _createSuper(Derived){var hasNativeReflectConstruct=_isNativeReflectConstruct();return function _createSuperInternal(){var Super=(0,_getPrototypeOf2.default)(Derived),result;if(hasNativeReflectConstruct){var NewTarget=(0,_getPrototypeOf2.default)(this).constructor;result=Reflect.construct(Super,arguments,NewTarget);}else{result=Super.apply(this,arguments);}return(0,_possibleConstructorReturn2.default)(this,result);};}function _isNativeReflectConstruct(){if(typeof Reflect==="undefined"||!Reflect.construct)return false;if(Reflect.construct.sham)return false;if(typeof Proxy==="function")return true;try{Date.prototype.toString.call(Reflect.construct(Date,[],function(){}));return true;}catch(e){return false;}}var OPACITY_ANIMATION_IN_TIME=225;var OPACITY_ANIMATION_OUT_TIME=195;var EASING_OUT=_reactNative.Easing.bezier(0.25,0.46,0.45,0.94);var EASING_IN=_reactNative.Easing.out(EASING_OUT);var ESCAPE_KEY='Escape';var CustomActionSheet=function(_React$Component){(0,_inherits2.default)(CustomActionSheet,_React$Component);var _super=_createSuper(CustomActionSheet);function CustomActionSheet(){var _this;(0,_classCallCheck2.default)(this,CustomActionSheet);for(var _len=arguments.length,args=new Array(_len),_key=0;_key<_len;_key++){args[_key]=arguments[_key];}_this=_super.call.apply(_super,[this].concat(args));_this._actionSheetHeight=360;_this._backHandlerListener=null;_this.state={isVisible:false,isAnimating:false,options:null,onSelect:null,overlayOpacity:new _reactNative.Animated.Value(0),sheetOpacity:new _reactNative.Animated.Value(0)};_this._deferAfterAnimation=undefined;_this._handleWebKeyDown=function(event){if(event.key===ESCAPE_KEY&&_this.state.isVisible){event.preventDefault();_this._selectCancelButton();}};_this._setActionSheetHeight=function(_ref){var nativeEvent=_ref.nativeEvent;return _this._actionSheetHeight=nativeEvent.layout.height;};_this.showActionSheetWithOptions=function(options,onSelect){var _this$state=_this.state,isVisible=_this$state.isVisible,overlayOpacity=_this$state.overlayOpacity,sheetOpacity=_this$state.sheetOpacity;var _this$props$useNative=_this.props.useNativeDriver,useNativeDriver=_this$props$useNative===void 0?true:_this$props$useNative;if(isVisible){_this._deferAfterAnimation=_this.showActionSheetWithOptions.bind((0,_assertThisInitialized2.default)(_this),options,onSelect);return;}_this.setState({options:options,onSelect:onSelect,isVisible:true,isAnimating:true});overlayOpacity.setValue(0);sheetOpacity.setValue(0);_reactNative.Animated.parallel([_reactNative.Animated.timing(overlayOpacity,{toValue:0.32,easing:EASING_OUT,duration:OPACITY_ANIMATION_IN_TIME,useNativeDriver:useNativeDriver}),_reactNative.Animated.timing(sheetOpacity,{toValue:1,easing:EASING_OUT,duration:OPACITY_ANIMATION_IN_TIME,useNativeDriver:useNativeDriver})]).start(function(result){if(result.finished){_this.setState({isAnimating:false});_this._deferAfterAnimation=undefined;}});_this._backHandlerListener=_reactNative.BackHandler.addEventListener('actionSheetHardwareBackPress',_this._selectCancelButton);};_this._selectCancelButton=function(){var options=_this.state.options;if(!options){return false;}if(typeof options.cancelButtonIndex==='undefined'){return false;}else if(typeof options.cancelButtonIndex==='number'){return _this._onSelect(options.cancelButtonIndex);}else{return _this._animateOut();}};_this._onSelect=function(index){var _this$state2=_this.state,isAnimating=_this$state2.isAnimating,onSelect=_this$state2.onSelect;if(isAnimating){return false;}if(onSelect){_this._deferAfterAnimation=onSelect.bind((0,_assertThisInitialized2.default)(_this),index);}return _this._animateOut();};_this._animateOut=function(){var _this$state3=_this.state,isAnimating=_this$state3.isAnimating,overlayOpacity=_this$state3.overlayOpacity,sheetOpacity=_this$state3.sheetOpacity;var _this$props$useNative2=_this.props.useNativeDriver,useNativeDriver=_this$props$useNative2===void 0?true:_this$props$useNative2;if(isAnimating){return false;}if(_this._backHandlerListener){_this._backHandlerListener.remove();};_this.setState({isAnimating:true});_reactNative.Animated.parallel([_reactNative.Animated.timing(overlayOpacity,{toValue:0,easing:EASING_IN,duration:OPACITY_ANIMATION_OUT_TIME,useNativeDriver:useNativeDriver}),_reactNative.Animated.timing(sheetOpacity,{toValue:0,easing:EASING_IN,duration:OPACITY_ANIMATION_OUT_TIME,useNativeDriver:useNativeDriver})]).start(function(result){if(result.finished){_this.setState({isVisible:false,isAnimating:false});if(_this._deferAfterAnimation){_this._deferAfterAnimation();}}});return true;};return _this;}(0,_createClass2.default)(CustomActionSheet,[{key:"componentDidMount",value:function componentDidMount(){if(_reactNative.Platform.OS==='web'){document.addEventListener('keydown',this._handleWebKeyDown);}}},{key:"componentWillUnmount",value:function componentWillUnmount(){if(_reactNative.Platform.OS==='web'){document.removeEventListener('keydown',this._handleWebKeyDown);}}},{key:"render",value:function render(){var _this$state4=this.state,isVisible=_this$state4.isVisible,overlayOpacity=_this$state4.overlayOpacity,options=_this$state4.options;var useModal=options?options.autoFocus||options.useModal===true:false;var overlay=isVisible?React.createElement(_reactNative.Animated.View,{style:[styles.overlay,{opacity:overlayOpacity}],__source:{fileName:_jsxFileName,lineNumber:79,columnNumber:7}}):null;var appContent=React.createElement(_reactNative.View,{style:styles.flexContainer,importantForAccessibility:isVisible?'no-hide-descendants':'auto',__source:{fileName:_jsxFileName,lineNumber:91,columnNumber:7}},React.Children.only(this.props.children));return React.createElement(_reactNative.View,{pointerEvents:this.props.pointerEvents,style:styles.flexContainer,__source:{fileName:_jsxFileName,lineNumber:99,columnNumber:7}},appContent,isVisible&&!useModal&&React.createElement(React.Fragment,null,overlay,this._renderSheet()),isVisible&&useModal&&React.createElement(_reactNative.Modal,{animationType:"none",transparent:true,onRequestClose:this._selectCancelButton,__source:{fileName:_jsxFileName,lineNumber:108,columnNumber:11}},overlay,this._renderSheet()));}},{key:"_renderSheet",value:function _renderSheet(){var _this$state5=this.state,options=_this$state5.options,isAnimating=_this$state5.isAnimating,sheetOpacity=_this$state5.sheetOpacity;if(!options){return null;}var optionsArray=options.options,icons=options.icons,tintIcons=options.tintIcons,destructiveButtonIndex=options.destructiveButtonIndex,disabledButtonIndices=options.disabledButtonIndices,destructiveColor=options.destructiveColor,textStyle=options.textStyle,tintColor=options.tintColor,title=options.title,titleTextStyle=options.titleTextStyle,message=options.message,messageTextStyle=options.messageTextStyle,autoFocus=options.autoFocus,showSeparators=options.showSeparators,containerStyle=options.containerStyle,separatorStyle=options.separatorStyle,cancelButtonIndex=options.cancelButtonIndex,cancelButtonTintColor=options.cancelButtonTintColor;return React.createElement(_reactNative.TouchableWithoutFeedback,{importantForAccessibility:"yes",onPress:this._selectCancelButton,__source:{fileName:_jsxFileName,lineNumber:145,columnNumber:7}},React.createElement(_reactNative.Animated.View,{needsOffscreenAlphaCompositing:isAnimating,style:[styles.sheetContainer,{opacity:sheetOpacity,transform:[{translateY:sheetOpacity.interpolate({inputRange:[0,1],outputRange:[this._actionSheetHeight,0]})}]}],__source:{fileName:_jsxFileName,lineNumber:146,columnNumber:9}},React.createElement(_reactNative.View,{style:styles.sheet,onLayout:this._setActionSheetHeight,__source:{fileName:_jsxFileName,lineNumber:162,columnNumber:11}},React.createElement(_ActionGroup.default,{options:optionsArray,icons:icons,tintIcons:tintIcons===undefined?true:tintIcons,cancelButtonIndex:cancelButtonIndex,cancelButtonTintColor:cancelButtonTintColor,destructiveButtonIndex:destructiveButtonIndex,destructiveColor:destructiveColor,disabledButtonIndices:disabledButtonIndices,onSelect:this._onSelect,startIndex:0,length:optionsArray.length,textStyle:textStyle||{},tintColor:tintColor,title:title||undefined,titleTextStyle:titleTextStyle,message:message||undefined,messageTextStyle:messageTextStyle,autoFocus:autoFocus,showSeparators:showSeparators,containerStyle:containerStyle,separatorStyle:separatorStyle,__source:{fileName:_jsxFileName,lineNumber:163,columnNumber:13}}))));}}]);return CustomActionSheet;}(React.Component);exports.default=CustomActionSheet;var styles=_reactNative.StyleSheet.create({flexContainer:{flex:1},overlay:{position:'absolute',top:0,right:0,bottom:0,left:0,backgroundColor:'black'},sheetContainer:{position:'absolute',left:0,right:0,bottom:0,top:0,backgroundColor:'transparent',alignItems:'flex-end',justifyContent:'center',flexDirection:'row'},sheet:{flex:1,backgroundColor:'transparent'}}); - //# sourceMappingURL=CustomActionSheet.js.map -\ No newline at end of file -diff --git a/node_modules/@expo/react-native-action-sheet/lib/module/ActionSheet/CustomActionSheet.js b/node_modules/@expo/react-native-action-sheet/lib/module/ActionSheet/CustomActionSheet.js -index 253c851..2eb2ba2 100644 ---- a/node_modules/@expo/react-native-action-sheet/lib/module/ActionSheet/CustomActionSheet.js -+++ b/node_modules/@expo/react-native-action-sheet/lib/module/ActionSheet/CustomActionSheet.js -@@ -1,2 +1,2 @@ --import _classCallCheck from"@babel/runtime/helpers/classCallCheck";import _createClass from"@babel/runtime/helpers/createClass";import _assertThisInitialized from"@babel/runtime/helpers/assertThisInitialized";import _inherits from"@babel/runtime/helpers/inherits";import _possibleConstructorReturn from"@babel/runtime/helpers/possibleConstructorReturn";import _getPrototypeOf from"@babel/runtime/helpers/getPrototypeOf";var _jsxFileName="/home/runner/work/react-native-action-sheet/react-native-action-sheet/src/ActionSheet/CustomActionSheet.tsx";function _createSuper(Derived){var hasNativeReflectConstruct=_isNativeReflectConstruct();return function _createSuperInternal(){var Super=_getPrototypeOf(Derived),result;if(hasNativeReflectConstruct){var NewTarget=_getPrototypeOf(this).constructor;result=Reflect.construct(Super,arguments,NewTarget);}else{result=Super.apply(this,arguments);}return _possibleConstructorReturn(this,result);};}function _isNativeReflectConstruct(){if(typeof Reflect==="undefined"||!Reflect.construct)return false;if(Reflect.construct.sham)return false;if(typeof Proxy==="function")return true;try{Date.prototype.toString.call(Reflect.construct(Date,[],function(){}));return true;}catch(e){return false;}}import*as React from'react';import{Animated,BackHandler,Easing,Modal,Platform,StyleSheet,TouchableWithoutFeedback,View}from'react-native';import ActionGroup from'./ActionGroup';var OPACITY_ANIMATION_IN_TIME=225;var OPACITY_ANIMATION_OUT_TIME=195;var EASING_OUT=Easing.bezier(0.25,0.46,0.45,0.94);var EASING_IN=Easing.out(EASING_OUT);var ESCAPE_KEY='Escape';var CustomActionSheet=function(_React$Component){_inherits(CustomActionSheet,_React$Component);var _super=_createSuper(CustomActionSheet);function CustomActionSheet(){var _this;_classCallCheck(this,CustomActionSheet);for(var _len=arguments.length,args=new Array(_len),_key=0;_key<_len;_key++){args[_key]=arguments[_key];}_this=_super.call.apply(_super,[this].concat(args));_this._actionSheetHeight=360;_this.state={isVisible:false,isAnimating:false,options:null,onSelect:null,overlayOpacity:new Animated.Value(0),sheetOpacity:new Animated.Value(0)};_this._deferAfterAnimation=undefined;_this._handleWebKeyDown=function(event){if(event.key===ESCAPE_KEY&&_this.state.isVisible){event.preventDefault();_this._selectCancelButton();}};_this._setActionSheetHeight=function(_ref){var nativeEvent=_ref.nativeEvent;return _this._actionSheetHeight=nativeEvent.layout.height;};_this.showActionSheetWithOptions=function(options,onSelect){var _this$state=_this.state,isVisible=_this$state.isVisible,overlayOpacity=_this$state.overlayOpacity,sheetOpacity=_this$state.sheetOpacity;var _this$props$useNative=_this.props.useNativeDriver,useNativeDriver=_this$props$useNative===void 0?true:_this$props$useNative;if(isVisible){_this._deferAfterAnimation=_this.showActionSheetWithOptions.bind(_assertThisInitialized(_this),options,onSelect);return;}_this.setState({options:options,onSelect:onSelect,isVisible:true,isAnimating:true});overlayOpacity.setValue(0);sheetOpacity.setValue(0);Animated.parallel([Animated.timing(overlayOpacity,{toValue:0.32,easing:EASING_OUT,duration:OPACITY_ANIMATION_IN_TIME,useNativeDriver:useNativeDriver}),Animated.timing(sheetOpacity,{toValue:1,easing:EASING_OUT,duration:OPACITY_ANIMATION_IN_TIME,useNativeDriver:useNativeDriver})]).start(function(result){if(result.finished){_this.setState({isAnimating:false});_this._deferAfterAnimation=undefined;}});BackHandler.addEventListener('actionSheetHardwareBackPress',_this._selectCancelButton);};_this._selectCancelButton=function(){var options=_this.state.options;if(!options){return false;}if(typeof options.cancelButtonIndex==='undefined'){return false;}else if(typeof options.cancelButtonIndex==='number'){return _this._onSelect(options.cancelButtonIndex);}else{return _this._animateOut();}};_this._onSelect=function(index){var _this$state2=_this.state,isAnimating=_this$state2.isAnimating,onSelect=_this$state2.onSelect;if(isAnimating){return false;}if(onSelect){_this._deferAfterAnimation=onSelect.bind(_assertThisInitialized(_this),index);}return _this._animateOut();};_this._animateOut=function(){var _this$state3=_this.state,isAnimating=_this$state3.isAnimating,overlayOpacity=_this$state3.overlayOpacity,sheetOpacity=_this$state3.sheetOpacity;var _this$props$useNative2=_this.props.useNativeDriver,useNativeDriver=_this$props$useNative2===void 0?true:_this$props$useNative2;if(isAnimating){return false;}BackHandler.removeEventListener('actionSheetHardwareBackPress',_this._selectCancelButton);_this.setState({isAnimating:true});Animated.parallel([Animated.timing(overlayOpacity,{toValue:0,easing:EASING_IN,duration:OPACITY_ANIMATION_OUT_TIME,useNativeDriver:useNativeDriver}),Animated.timing(sheetOpacity,{toValue:0,easing:EASING_IN,duration:OPACITY_ANIMATION_OUT_TIME,useNativeDriver:useNativeDriver})]).start(function(result){if(result.finished){_this.setState({isVisible:false,isAnimating:false});if(_this._deferAfterAnimation){_this._deferAfterAnimation();}}});return true;};return _this;}_createClass(CustomActionSheet,[{key:"componentDidMount",value:function componentDidMount(){if(Platform.OS==='web'){document.addEventListener('keydown',this._handleWebKeyDown);}}},{key:"componentWillUnmount",value:function componentWillUnmount(){if(Platform.OS==='web'){document.removeEventListener('keydown',this._handleWebKeyDown);}}},{key:"render",value:function render(){var _this$state4=this.state,isVisible=_this$state4.isVisible,overlayOpacity=_this$state4.overlayOpacity,options=_this$state4.options;var useModal=options?options.autoFocus||options.useModal===true:false;var overlay=isVisible?React.createElement(Animated.View,{style:[styles.overlay,{opacity:overlayOpacity}],__source:{fileName:_jsxFileName,lineNumber:79,columnNumber:7}}):null;var appContent=React.createElement(View,{style:styles.flexContainer,importantForAccessibility:isVisible?'no-hide-descendants':'auto',__source:{fileName:_jsxFileName,lineNumber:91,columnNumber:7}},React.Children.only(this.props.children));return React.createElement(View,{pointerEvents:this.props.pointerEvents,style:styles.flexContainer,__source:{fileName:_jsxFileName,lineNumber:99,columnNumber:7}},appContent,isVisible&&!useModal&&React.createElement(React.Fragment,null,overlay,this._renderSheet()),isVisible&&useModal&&React.createElement(Modal,{animationType:"none",transparent:true,onRequestClose:this._selectCancelButton,__source:{fileName:_jsxFileName,lineNumber:108,columnNumber:11}},overlay,this._renderSheet()));}},{key:"_renderSheet",value:function _renderSheet(){var _this$state5=this.state,options=_this$state5.options,isAnimating=_this$state5.isAnimating,sheetOpacity=_this$state5.sheetOpacity;if(!options){return null;}var optionsArray=options.options,icons=options.icons,tintIcons=options.tintIcons,destructiveButtonIndex=options.destructiveButtonIndex,disabledButtonIndices=options.disabledButtonIndices,destructiveColor=options.destructiveColor,textStyle=options.textStyle,tintColor=options.tintColor,title=options.title,titleTextStyle=options.titleTextStyle,message=options.message,messageTextStyle=options.messageTextStyle,autoFocus=options.autoFocus,showSeparators=options.showSeparators,containerStyle=options.containerStyle,separatorStyle=options.separatorStyle,cancelButtonIndex=options.cancelButtonIndex,cancelButtonTintColor=options.cancelButtonTintColor;return React.createElement(TouchableWithoutFeedback,{importantForAccessibility:"yes",onPress:this._selectCancelButton,__source:{fileName:_jsxFileName,lineNumber:145,columnNumber:7}},React.createElement(Animated.View,{needsOffscreenAlphaCompositing:isAnimating,style:[styles.sheetContainer,{opacity:sheetOpacity,transform:[{translateY:sheetOpacity.interpolate({inputRange:[0,1],outputRange:[this._actionSheetHeight,0]})}]}],__source:{fileName:_jsxFileName,lineNumber:146,columnNumber:9}},React.createElement(View,{style:styles.sheet,onLayout:this._setActionSheetHeight,__source:{fileName:_jsxFileName,lineNumber:162,columnNumber:11}},React.createElement(ActionGroup,{options:optionsArray,icons:icons,tintIcons:tintIcons===undefined?true:tintIcons,cancelButtonIndex:cancelButtonIndex,cancelButtonTintColor:cancelButtonTintColor,destructiveButtonIndex:destructiveButtonIndex,destructiveColor:destructiveColor,disabledButtonIndices:disabledButtonIndices,onSelect:this._onSelect,startIndex:0,length:optionsArray.length,textStyle:textStyle||{},tintColor:tintColor,title:title||undefined,titleTextStyle:titleTextStyle,message:message||undefined,messageTextStyle:messageTextStyle,autoFocus:autoFocus,showSeparators:showSeparators,containerStyle:containerStyle,separatorStyle:separatorStyle,__source:{fileName:_jsxFileName,lineNumber:163,columnNumber:13}}))));}}]);return CustomActionSheet;}(React.Component);export{CustomActionSheet as default};var styles=StyleSheet.create({flexContainer:{flex:1},overlay:{position:'absolute',top:0,right:0,bottom:0,left:0,backgroundColor:'black'},sheetContainer:{position:'absolute',left:0,right:0,bottom:0,top:0,backgroundColor:'transparent',alignItems:'flex-end',justifyContent:'center',flexDirection:'row'},sheet:{flex:1,backgroundColor:'transparent'}}); -+import _classCallCheck from"@babel/runtime/helpers/classCallCheck";import _createClass from"@babel/runtime/helpers/createClass";import _assertThisInitialized from"@babel/runtime/helpers/assertThisInitialized";import _inherits from"@babel/runtime/helpers/inherits";import _possibleConstructorReturn from"@babel/runtime/helpers/possibleConstructorReturn";import _getPrototypeOf from"@babel/runtime/helpers/getPrototypeOf";var _jsxFileName="/home/runner/work/react-native-action-sheet/react-native-action-sheet/src/ActionSheet/CustomActionSheet.tsx";function _createSuper(Derived){var hasNativeReflectConstruct=_isNativeReflectConstruct();return function _createSuperInternal(){var Super=_getPrototypeOf(Derived),result;if(hasNativeReflectConstruct){var NewTarget=_getPrototypeOf(this).constructor;result=Reflect.construct(Super,arguments,NewTarget);}else{result=Super.apply(this,arguments);}return _possibleConstructorReturn(this,result);};}function _isNativeReflectConstruct(){if(typeof Reflect==="undefined"||!Reflect.construct)return false;if(Reflect.construct.sham)return false;if(typeof Proxy==="function")return true;try{Date.prototype.toString.call(Reflect.construct(Date,[],function(){}));return true;}catch(e){return false;}}import*as React from'react';import{Animated,BackHandler,Easing,Modal,Platform,StyleSheet,TouchableWithoutFeedback,View}from'react-native';import ActionGroup from'./ActionGroup';var OPACITY_ANIMATION_IN_TIME=225;var OPACITY_ANIMATION_OUT_TIME=195;var EASING_OUT=Easing.bezier(0.25,0.46,0.45,0.94);var EASING_IN=Easing.out(EASING_OUT);var ESCAPE_KEY='Escape';var CustomActionSheet=function(_React$Component){_inherits(CustomActionSheet,_React$Component);var _super=_createSuper(CustomActionSheet);function CustomActionSheet(){var _this;_classCallCheck(this,CustomActionSheet);for(var _len=arguments.length,args=new Array(_len),_key=0;_key<_len;_key++){args[_key]=arguments[_key];}_this=_super.call.apply(_super,[this].concat(args));_this._actionSheetHeight=360;_this._backHandlerListener=null;_this.state={isVisible:false,isAnimating:false,options:null,onSelect:null,overlayOpacity:new Animated.Value(0),sheetOpacity:new Animated.Value(0)};_this._deferAfterAnimation=undefined;_this._handleWebKeyDown=function(event){if(event.key===ESCAPE_KEY&&_this.state.isVisible){event.preventDefault();_this._selectCancelButton();}};_this._setActionSheetHeight=function(_ref){var nativeEvent=_ref.nativeEvent;return _this._actionSheetHeight=nativeEvent.layout.height;};_this.showActionSheetWithOptions=function(options,onSelect){var _this$state=_this.state,isVisible=_this$state.isVisible,overlayOpacity=_this$state.overlayOpacity,sheetOpacity=_this$state.sheetOpacity;var _this$props$useNative=_this.props.useNativeDriver,useNativeDriver=_this$props$useNative===void 0?true:_this$props$useNative;if(isVisible){_this._deferAfterAnimation=_this.showActionSheetWithOptions.bind(_assertThisInitialized(_this),options,onSelect);return;}_this.setState({options:options,onSelect:onSelect,isVisible:true,isAnimating:true});overlayOpacity.setValue(0);sheetOpacity.setValue(0);Animated.parallel([Animated.timing(overlayOpacity,{toValue:0.32,easing:EASING_OUT,duration:OPACITY_ANIMATION_IN_TIME,useNativeDriver:useNativeDriver}),Animated.timing(sheetOpacity,{toValue:1,easing:EASING_OUT,duration:OPACITY_ANIMATION_IN_TIME,useNativeDriver:useNativeDriver})]).start(function(result){if(result.finished){_this.setState({isAnimating:false});_this._deferAfterAnimation=undefined;}});_this._backHandlerListener=BackHandler.addEventListener('actionSheetHardwareBackPress',_this._selectCancelButton);};_this._selectCancelButton=function(){var options=_this.state.options;if(!options){return false;}if(typeof options.cancelButtonIndex==='undefined'){return false;}else if(typeof options.cancelButtonIndex==='number'){return _this._onSelect(options.cancelButtonIndex);}else{return _this._animateOut();}};_this._onSelect=function(index){var _this$state2=_this.state,isAnimating=_this$state2.isAnimating,onSelect=_this$state2.onSelect;if(isAnimating){return false;}if(onSelect){_this._deferAfterAnimation=onSelect.bind(_assertThisInitialized(_this),index);}return _this._animateOut();};_this._animateOut=function(){var _this$state3=_this.state,isAnimating=_this$state3.isAnimating,overlayOpacity=_this$state3.overlayOpacity,sheetOpacity=_this$state3.sheetOpacity;var _this$props$useNative2=_this.props.useNativeDriver,useNativeDriver=_this$props$useNative2===void 0?true:_this$props$useNative2;if(isAnimating){return false;}if(_this._backHandlerListener){_this._backHandlerListener.remove();};_this.setState({isAnimating:true});Animated.parallel([Animated.timing(overlayOpacity,{toValue:0,easing:EASING_IN,duration:OPACITY_ANIMATION_OUT_TIME,useNativeDriver:useNativeDriver}),Animated.timing(sheetOpacity,{toValue:0,easing:EASING_IN,duration:OPACITY_ANIMATION_OUT_TIME,useNativeDriver:useNativeDriver})]).start(function(result){if(result.finished){_this.setState({isVisible:false,isAnimating:false});if(_this._deferAfterAnimation){_this._deferAfterAnimation();}}});return true;};return _this;}_createClass(CustomActionSheet,[{key:"componentDidMount",value:function componentDidMount(){if(Platform.OS==='web'){document.addEventListener('keydown',this._handleWebKeyDown);}}},{key:"componentWillUnmount",value:function componentWillUnmount(){if(Platform.OS==='web'){document.removeEventListener('keydown',this._handleWebKeyDown);}}},{key:"render",value:function render(){var _this$state4=this.state,isVisible=_this$state4.isVisible,overlayOpacity=_this$state4.overlayOpacity,options=_this$state4.options;var useModal=options?options.autoFocus||options.useModal===true:false;var overlay=isVisible?React.createElement(Animated.View,{style:[styles.overlay,{opacity:overlayOpacity}],__source:{fileName:_jsxFileName,lineNumber:79,columnNumber:7}}):null;var appContent=React.createElement(View,{style:styles.flexContainer,importantForAccessibility:isVisible?'no-hide-descendants':'auto',__source:{fileName:_jsxFileName,lineNumber:91,columnNumber:7}},React.Children.only(this.props.children));return React.createElement(View,{pointerEvents:this.props.pointerEvents,style:styles.flexContainer,__source:{fileName:_jsxFileName,lineNumber:99,columnNumber:7}},appContent,isVisible&&!useModal&&React.createElement(React.Fragment,null,overlay,this._renderSheet()),isVisible&&useModal&&React.createElement(Modal,{animationType:"none",transparent:true,onRequestClose:this._selectCancelButton,__source:{fileName:_jsxFileName,lineNumber:108,columnNumber:11}},overlay,this._renderSheet()));}},{key:"_renderSheet",value:function _renderSheet(){var _this$state5=this.state,options=_this$state5.options,isAnimating=_this$state5.isAnimating,sheetOpacity=_this$state5.sheetOpacity;if(!options){return null;}var optionsArray=options.options,icons=options.icons,tintIcons=options.tintIcons,destructiveButtonIndex=options.destructiveButtonIndex,disabledButtonIndices=options.disabledButtonIndices,destructiveColor=options.destructiveColor,textStyle=options.textStyle,tintColor=options.tintColor,title=options.title,titleTextStyle=options.titleTextStyle,message=options.message,messageTextStyle=options.messageTextStyle,autoFocus=options.autoFocus,showSeparators=options.showSeparators,containerStyle=options.containerStyle,separatorStyle=options.separatorStyle,cancelButtonIndex=options.cancelButtonIndex,cancelButtonTintColor=options.cancelButtonTintColor;return React.createElement(TouchableWithoutFeedback,{importantForAccessibility:"yes",onPress:this._selectCancelButton,__source:{fileName:_jsxFileName,lineNumber:145,columnNumber:7}},React.createElement(Animated.View,{needsOffscreenAlphaCompositing:isAnimating,style:[styles.sheetContainer,{opacity:sheetOpacity,transform:[{translateY:sheetOpacity.interpolate({inputRange:[0,1],outputRange:[this._actionSheetHeight,0]})}]}],__source:{fileName:_jsxFileName,lineNumber:146,columnNumber:9}},React.createElement(View,{style:styles.sheet,onLayout:this._setActionSheetHeight,__source:{fileName:_jsxFileName,lineNumber:162,columnNumber:11}},React.createElement(ActionGroup,{options:optionsArray,icons:icons,tintIcons:tintIcons===undefined?true:tintIcons,cancelButtonIndex:cancelButtonIndex,cancelButtonTintColor:cancelButtonTintColor,destructiveButtonIndex:destructiveButtonIndex,destructiveColor:destructiveColor,disabledButtonIndices:disabledButtonIndices,onSelect:this._onSelect,startIndex:0,length:optionsArray.length,textStyle:textStyle||{},tintColor:tintColor,title:title||undefined,titleTextStyle:titleTextStyle,message:message||undefined,messageTextStyle:messageTextStyle,autoFocus:autoFocus,showSeparators:showSeparators,containerStyle:containerStyle,separatorStyle:separatorStyle,__source:{fileName:_jsxFileName,lineNumber:163,columnNumber:13}}))));}}]);return CustomActionSheet;}(React.Component);export{CustomActionSheet as default};var styles=StyleSheet.create({flexContainer:{flex:1},overlay:{position:'absolute',top:0,right:0,bottom:0,left:0,backgroundColor:'black'},sheetContainer:{position:'absolute',left:0,right:0,bottom:0,top:0,backgroundColor:'transparent',alignItems:'flex-end',justifyContent:'center',flexDirection:'row'},sheet:{flex:1,backgroundColor:'transparent'}}); - //# sourceMappingURL=CustomActionSheet.js.map -\ No newline at end of file \ No newline at end of file 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 ea473d43..b8bab659 100644 --- a/providers/JellyfinProvider.tsx +++ b/providers/JellyfinProvider.tsx @@ -2,18 +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 { Api, Jellyfin } from "@jellyfin/sdk"; -import { UserDto } from "@jellyfin/sdk/lib/generated-client/models"; +import { store } from "@/utils/store"; +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, @@ -43,7 +46,7 @@ interface JellyfinContextValue { } const JellyfinContext = createContext( - undefined + undefined, ); export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({ @@ -66,7 +69,7 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({ name: deviceName, id, }, - }) + }), ); setDeviceId(id); })(); @@ -102,7 +105,7 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({ null, { headers, - } + }, ); if (response?.status === 200) { setSecret(response?.data?.Secret); @@ -122,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) { @@ -136,7 +139,7 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({ }, { headers, - } + }, ); const { AccessToken, User } = authResponse.data; @@ -166,13 +169,16 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({ })(); }, []); + useEffect(() => { + store.set(apiAtom, api); + }, [api]); + useInterval(pollQuickConnect, isPolling ? 1000 : null); 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 })) || []; }; @@ -187,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, @@ -195,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) => { @@ -235,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) { @@ -251,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", + ), ); } } @@ -281,6 +287,13 @@ 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`), + ); + storage.delete("token"); setUser(null); setApi(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 1311459f..c5857e9a 100644 --- a/providers/WebSocketProvider.tsx +++ b/providers/WebSocketProvider.tsx @@ -1,22 +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 { Alert, AppState, AppStateStatus } from "react-native"; -import { useAtomValue } from "jotai"; -import { useQuery } from "@tanstack/react-query"; -import { - apiAtom, - getOrSetDeviceId, - userAtom, -} from "@/providers/JellyfinProvider"; -import { getSessionApi } from "@jellyfin/sdk/lib/utils/api"; -import native from "@/utils/profiles/native"; +import { AppState, type AppStateStatus } from "react-native"; interface WebSocketProviderProps { children: ReactNode; @@ -30,7 +24,6 @@ interface WebSocketContextType { const WebSocketContext = createContext(null); export const WebSocketProvider = ({ children }: WebSocketProviderProps) => { - const user = useAtomValue(userAtom); const api = useAtomValue(apiAtom); const [ws, setWs] = useState(null); const [isConnected, setIsConnected] = useState(false); @@ -40,7 +33,9 @@ export const WebSocketProvider = ({ children }: WebSocketProviderProps) => { }, []); const connectWebSocket = useCallback(() => { - if (!deviceId || !api?.accessToken) return; + if (!deviceId || !api?.accessToken) { + return; + } const protocol = api.basePath.includes("https") ? "wss" : "ws"; const url = `${protocol}://${api.basePath @@ -50,7 +45,7 @@ export const WebSocketProvider = ({ children }: WebSocketProviderProps) => { }&deviceId=${deviceId}`; const newWebSocket = new WebSocket(url); - let keepAliveInterval: NodeJS.Timeout | null = null; + let keepAliveInterval: number | null = null; newWebSocket.onopen = () => { setIsConnected(true); @@ -67,14 +62,18 @@ export const WebSocketProvider = ({ children }: WebSocketProviderProps) => { }; newWebSocket.onclose = () => { - if (keepAliveInterval) clearInterval(keepAliveInterval); + if (keepAliveInterval) { + clearInterval(keepAliveInterval); + } setIsConnected(false); }; setWs(newWebSocket); return () => { - if (keepAliveInterval) clearInterval(keepAliveInterval); + if (keepAliveInterval) { + clearInterval(keepAliveInterval); + } newWebSocket.close(); }; }, [api, deviceId]); @@ -85,7 +84,9 @@ export const WebSocketProvider = ({ children }: WebSocketProviderProps) => { }, [connectWebSocket]); useEffect(() => { - if (!deviceId || !api || !api?.accessToken) return; + if (!deviceId || !api || !api?.accessToken) { + return; + } const init = async () => { await getSessionApi(api).postFullCapabilities({ @@ -117,7 +118,7 @@ export const WebSocketProvider = ({ children }: WebSocketProviderProps) => { const subscription = AppState.addEventListener( "change", - handleAppStateChange + handleAppStateChange, ); return () => { @@ -137,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 18c3de25..993c2176 100644 --- a/translations/de.json +++ b/translations/de.json @@ -1,458 +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", - "auto_rotate": "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", - "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" - }, - "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", - "tags": "Tags" - } - }, - "favorites": { - "series": "Serien", - "movies": "Filme", - "episodes": "Episoden", - "videos": "Videos", - "boxsets": "Boxsets", - "playlists": "Playlists" - }, - "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_x": "Staffel {{seasons}}", - "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 d2f54e99..30d7d466 100644 --- a/translations/en.json +++ b/translations/en.json @@ -113,7 +113,7 @@ }, "other": { "other_title": "Other", - "auto_rotate": "Auto rotate", + "follow_device_orientation": "Auto rotate", "video_orientation": "Video orientation", "orientation": "Orientation", "orientations": { @@ -129,6 +129,11 @@ "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.", @@ -147,7 +152,7 @@ "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", + "url": "URL", "server_url_placeholder": "http(s)://domain.org:port" }, "plugins": { @@ -168,7 +173,13 @@ "tv_quota_limit": "TV quota limit", "tv_quota_days": "TV quota days", "reset_jellyseerr_config_button": "Reset Jellyseerr config", - "unlimited": "Unlimited" + "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 ", @@ -204,7 +215,7 @@ "app_language_description": "Select the language for the app.", "system": "System" }, - "toasts":{ + "toasts": { "error_deleting_files": "Error deleting files", "background_downloads_enabled": "Background downloads enabled", "background_downloads_disabled": "Background downloads disabled", @@ -213,6 +224,10 @@ "invalid_url": "Invalid URL" } }, + "sessions": { + "title": "Sessions", + "no_active_sessions": "No active sessions" + }, "downloads": { "downloads_title": "Downloads", "tvseries": "TV-Series", @@ -323,6 +338,8 @@ "years": "Years", "sort_by": "Sort By", "sort_order": "Sort Order", + "asc": "Ascending", + "desc": "Descending", "tags": "Tags" } }, @@ -332,7 +349,9 @@ "episodes": "Episodes", "videos": "Videos", "boxsets": "Boxsets", - "playlists": "Playlists" + "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" @@ -399,7 +418,7 @@ "for_kids": "For Kids", "news": "News" }, - "jellyseerr":{ + "jellyseerr": { "confirm": "Confirm", "cancel": "Cancel", "yes": "Yes", @@ -433,7 +452,7 @@ "tags": "Tags", "quality_profile": "Quality Profile", "root_folder": "Root Folder", - "season_x": "Season {{seasons}}", + "season_all": "Season (all)", "season_number": "Season {{season_number}}", "number_episodes": "{{episode_number}} Episodes", "born": "Born", diff --git a/translations/es.json b/translations/es.json index 8883c2be..0745aad3 100644 --- a/translations/es.json +++ b/translations/es.json @@ -20,7 +20,7 @@ "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?" + "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", @@ -113,7 +113,7 @@ }, "other": { "other_title": "Otros", - "auto_rotate": "Rotación automática", + "follow_device_orientation": "Rotación automática", "video_orientation": "Orientación de vídeo", "orientation": "Orientación", "orientations": { @@ -129,6 +129,11 @@ "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.", @@ -168,7 +173,13 @@ "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" + "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", @@ -204,7 +215,7 @@ "app_language_description": "Selecciona el idioma de la app.", "system": "Sistema" }, - "toasts":{ + "toasts": { "error_deleting_files": "Error al eliminar archivos", "background_downloads_enabled": "Descargas en segundo plano habilitadas", "background_downloads_disabled": "Descargas en segundo plano deshabilitadas", @@ -323,6 +334,8 @@ "years": "Años", "sort_by": "Ordenar por", "sort_order": "Ordenar", + "asc": "Ascending", + "desc": "Descending", "tags": "Etiquetas" } }, @@ -332,7 +345,9 @@ "episodes": "Episodios", "videos": "Vídeos", "boxsets": "Colecciones", - "playlists": "Playlists" + "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" @@ -399,7 +414,7 @@ "for_kids": "Para niños", "news": "Noticias" }, - "jellyseerr":{ + "jellyseerr": { "confirm": "Confirmar", "cancel": "Cancelar", "yes": "Sí", @@ -433,7 +448,7 @@ "tags": "Etiquetas", "quality_profile": "Perfil de calidad", "root_folder": "Carpeta raíz", - "season_x": "Temporada {{seasons}}", + "season_all": "Season (all)", "season_number": "Temporada {{season_number}}", "number_episodes": "{{episode_number}} episodios", "born": "Nacido", diff --git a/translations/fr.json b/translations/fr.json index f197494d..1b707c94 100644 --- a/translations/fr.json +++ b/translations/fr.json @@ -113,7 +113,7 @@ }, "other": { "other_title": "Autres", - "auto_rotate": "Rotation automatique", + "follow_device_orientation": "Rotation automatique", "video_orientation": "Orientation vidéo", "orientation": "Orientation", "orientations": { @@ -129,12 +129,16 @@ "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.", + "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", @@ -169,7 +173,13 @@ "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é" + "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 ", @@ -205,7 +215,7 @@ "app_language_description": "Sélectionnez la langue de l'application", "system": "Système" }, - "toasts":{ + "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", @@ -324,6 +334,8 @@ "years": "Années", "sort_by": "Trier par", "sort_order": "Ordre de tri", + "asc": "Ascending", + "desc": "Descending", "tags": "Tags" } }, @@ -333,7 +345,9 @@ "episodes": "Épisodes", "videos": "Vidéos", "boxsets": "Coffrets", - "playlists": "Listes de lecture" + "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" @@ -400,7 +414,7 @@ "for_kids": "Pour enfants", "news": "Actualités" }, - "jellyseerr":{ + "jellyseerr": { "confirm": "Confirmer", "cancel": "Annuler", "yes": "Oui", @@ -434,7 +448,7 @@ "tags": "Tags", "quality_profile": "Profil de qualité", "root_folder": "Dossier racine", - "season_x": "Saison {{seasons}}", + "season_all": "Season (all)", "season_number": "Saison {{season_number}}", "number_episodes": "{{episode_number}} épisodes", "born": "Né(e) le", diff --git a/translations/it.json b/translations/it.json index 314fc233..44f437b3 100644 --- a/translations/it.json +++ b/translations/it.json @@ -1,458 +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?" + "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ù" }, - "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" }, - "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", - "auto_rotate": "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", - "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" - }, - "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" + "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": "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" + "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" + } } - } - }, - "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" + "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" }, - "filters": { - "genres": "Generi", - "years": "Anni", - "sort_by": "Ordina per", - "sort_order": "Criterio di ordinamento", - "tags": "Tag" - } - }, - "favorites": { - "series": "Serie TV", - "movies": "Film", - "episodes": "Episodi", - "videos": "Video", - "boxsets": "Boxset", - "playlists": "Playlist" - }, - "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: {{messagge}}", - "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_x": "Stagione {{seasons}}", - "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!" + "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" } }, - "tabs": { - "home": "Home", - "search": "Cerca", - "library": "Libreria", - "custom_links": "Collegamenti personalizzati", - "favorites": "Preferiti" + "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" + } } - } \ No newline at end of file + }, + "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 new file mode 100644 index 00000000..261b6724 --- /dev/null +++ b/translations/ja.json @@ -0,0 +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": "お気に入り" + } +} diff --git a/translations/nl.json b/translations/nl.json index 929224c9..eb213102 100644 --- a/translations/nl.json +++ b/translations/nl.json @@ -9,13 +9,13 @@ "login_button": "Aanmelden", "quick_connect": "Snel Verbinden", "enter_code_to_login": "Vul code {{code}} in om aan te melden", - "failed_to_initiate_quick_connect": "Gefaald om Snel Verbinden op te starten", + "failed_to_initiate_quick_connect": "Mislukt om Snel Verbinden op te starten", "got_it": "Begrepen", - "connection_failed": "Verbinding gefaald", + "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": "Verander server", - "invalid_username_or_password": "Ongeldige gebruikersnaam of wachtwoord", + "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", @@ -42,7 +42,7 @@ "continue_watching": "Verder Kijken", "next_up": "Volgende", "recently_added_in": "Recent toegevoegd in {{libraryName}}", - "suggested_movies": "Voorgestelde Films", + "suggested_movies": "Voorgestelde films", "suggested_episodes": "Voorgestelde Afleveringen", "intro": { "welcome_to_streamyfin": "Welkom bij Streamyfin", @@ -56,7 +56,7 @@ "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": "Go naar instellingen", + "go_to_settings_button": "Ga naar instellingen", "read_more": "Lees meer" }, "settings": { @@ -82,7 +82,7 @@ "media_controls": { "media_controls_title": "Media Bedieningen", "forward_skip_length": "Duur voorwaarts overslaan", - "rewind_length": "Duur terugspeolen", + "rewind_length": "Duur terugspoelen", "seconds_unit": "s" }, "audio": { @@ -96,7 +96,7 @@ "subtitles": { "subtitle_title": "Ondertitels", "subtitle_language": "Ondertitel taal", - "subtitle_mode": "Ondertitle Modus", + "subtitle_mode": "Ondertitelmodus", "set_subtitle_track": "Gebruik Ondertitel Track Van Vorig Item", "subtitle_size": "Ondertitel Grootte", "subtitle_hint": "Stel ondertitel voorkeuren in.", @@ -108,12 +108,12 @@ "Smart": "Slim", "Always": "Altijd", "None": "Geen", - "OnlyForced": "Alleen Geforceeerd" + "OnlyForced": "Alleen Geforceerd" } }, "other": { "other_title": "Andere", - "auto_rotate": "Automatisch draaien", + "follow_device_orientation": "Automatisch draaien", "video_orientation": "Video oriëntatie", "orientation": "Oriëntatie", "orientations": { @@ -129,25 +129,30 @@ "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 Bibliotheek tab en hoofdpagina onderdelen.", + "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": "Remux max download", + "remux_max_download": "Maximale Remux-download", "auto_download": "Auto download", "optimized_versions_server": "Geoptimaliseerde server versies", "save_button": "Opslaan", - "optimized_server": "Geoptimailseerde Server", + "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", + "url": "URL", "server_url_placeholder": "http(s)://domein.org:poort" }, "plugins": { @@ -161,17 +166,23 @@ "password_placeholder": "Voeg het wachtwoord in voor de Jellyfin gebruiker {{username}}", "save_button": "Opslaan", "clear_button": "Wissen", - "login_button": "Aannmelden", + "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" + "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 inschakeln ", + "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.", @@ -204,8 +215,8 @@ "app_language_description": "Selecteer een taal voor de app.", "system": "Systeem" }, - "toasts":{ - "error_deleting_files": "Fout bij het verwijden van bestanden", + "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", @@ -237,7 +248,7 @@ "methods": "Methoden", "toasts": { "you_are_not_allowed_to_download_files": "Je mag geen bestanden downloaden.", - "deleted_all_movies_successfully": "Alle filns succesvol verwijderd!", + "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", @@ -280,18 +291,18 @@ "recent_requests": "Recent Aangevraagd", "plex_watchlist": "Plex Kijklijst", "trending": "Trending", - "popular_movies": "Populaire Films", + "popular_movies": "Populaire films", "movie_genres": "Film Genres", - "upcoming_movies": "Aankomende Movies", + "upcoming_movies": "Aankomende films", "studios": "Studios", "popular_tv": "Populaire TV", "tv_genres": "TV Genres", - "upcoming_tv": "Opkomend TV", + "upcoming_tv": "Aankomende TV", "networks": "Netwerken", "tmdb_movie_keyword": "TMDB Film Trefwoord", - "tmdb_movie_genre": "TMDB Film Genre", + "tmdb_movie_genre": "TMDB Filmgenres", "tmdb_tv_keyword": "TMDB TV Trefwoord", - "tmdb_tv_genre": "TMDB TV Genre", + "tmdb_tv_genre": "TMDB TV-Genres", "tmdb_search": "TMDB Zoeken", "tmdb_studio": "TMDB Studio", "tmdb_network": "TMDB Netwerk", @@ -303,9 +314,9 @@ "no_results": "Geen resultaten", "no_libraries_found": "Geen bibliotheken gevonden", "item_types": { - "movies": "films", - "series": "series", - "boxsets": "box sets", + "movies": "Films", + "series": "Series", + "boxsets": "Boxsets", "items": "items" }, "options": { @@ -323,6 +334,8 @@ "years": "Jaren", "sort_by": "Sorteren op", "sort_order": "Sorteer volgorde", + "asc": "Ascending", + "desc": "Descending", "tags": "Labels" } }, @@ -332,7 +345,9 @@ "episodes": "Afleveringen", "videos": "Videos", "boxsets": "Boxsets", - "playlists": "Afspeellijsten" + "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" @@ -343,9 +358,9 @@ "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: {{message}}", + "message_from_server": "Bericht van de server", "video_has_finished_playing": "Video is gedaan met spelen!", - "no_video_source": "Geen video bron...", + "no_video_source": "Geen videobron...", "next_episode": "Volgende Aflevering", "refresh_tracks": "Tracks verversen", "subtitle_tracks": "Ondertitel Tracks:", @@ -372,7 +387,7 @@ "audio": "Audio", "subtitles": "Ondertitel", "show_more": "Toon meer", - "show_less": "Toon minden", + "show_less": "Toon minder", "appeared_in": "Verschenen in", "could_not_load_item": "Kon item niet laden", "none": "Geen", @@ -399,7 +414,7 @@ "for_kids": "Voor kinderen", "news": "Nieuws" }, - "jellyseerr":{ + "jellyseerr": { "confirm": "Bevestig", "cancel": "Annuleer", "yes": "Ja", @@ -417,7 +432,7 @@ "details": "Details", "status": "Status", "original_title": "Originele titel", - "series_type": "Serie Type", + "series_type": "Serietype", "release_dates": "Verschijningsdatums", "first_air_date": "Eerste uitzenddatum", "next_air_date": "Volgende uitzenddatum", @@ -433,19 +448,19 @@ "tags": "Labels", "quality_profile": "Kwaliteitsprofiel", "root_folder": "Hoofdmap", - "season_x": "Seizoen {{seasons}}", + "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 gefaald. Probeer opnieuw.", + "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 iets mis met het aavragen van media!" + "something_went_wrong_requesting_media": "Er ging iets mis met het aanvragen van media!" } }, "tabs": { diff --git a/translations/pl.json b/translations/pl.json new file mode 100644 index 00000000..0f6c31bd --- /dev/null +++ b/translations/pl.json @@ -0,0 +1,477 @@ +{ + "login": { + "username_required": "Nazwa użytkownika jest wymagana", + "error_title": "Błąd", + "login_title": "Zaloguj się", + "login_to_title": "Zaloguj się do", + "username_placeholder": "Nazwa użytkownika", + "password_placeholder": "Hasło", + "login_button": "Zaloguj się", + "quick_connect": "Szybkie połączenie", + "enter_code_to_login": "Wpisz kod {{code}}, aby się zalogować", + "failed_to_initiate_quick_connect": "Nie udało się zainicjować szybkiego połączenia", + "got_it": "Rozumiem", + "connection_failed": "Połączenie nieudane", + "could_not_connect_to_server": "Nie można połączyć się z serwerem. Sprawdź adres URL oraz połączenie sieciowe.", + "an_unexpected_error_occured": "Wystąpił nieoczekiwany błąd", + "change_server": "Zmień serwer", + "invalid_username_or_password": "Nieprawidłowa nazwa użytkownika lub hasło", + "user_does_not_have_permission_to_log_in": "Użytkownik nie ma uprawnień do logowania", + "server_is_taking_too_long_to_respond_try_again_later": "Serwer zbyt długo nie odpowiada – spróbuj ponownie później", + "server_received_too_many_requests_try_again_later": "Serwer otrzymał zbyt wiele żądań – spróbuj ponownie później.", + "there_is_a_server_error": "Wystąpił błąd serwera", + "an_unexpected_error_occured_did_you_enter_the_correct_url": "Wystąpił nieoczekiwany błąd. Czy wpisałeś poprawny adres URL?" + }, + "server": { + "enter_url_to_jellyfin_server": "Podaj adres URL serwera Jellyfin", + "server_url_placeholder": "http(s)://twoj-serwer.com", + "connect_button": "Połącz", + "previous_servers": "Poprzednie serwery", + "clear_button": "Wyczyść", + "search_for_local_servers": "Wyszukaj lokalne serwery", + "searching": "Wyszukiwanie...", + "servers": "Serwery" + }, + "home": { + "no_internet": "Brak Internetu", + "no_items": "Brak elementów", + "no_internet_message": "Spokojnie, nadal możesz oglądać\npobrane treści.", + "go_to_downloads": "Przejdź do pobranych", + "oops": "Ups!", + "error_message": "Coś poszło nie tak.\nWyloguj się i zaloguj ponownie.", + "continue_watching": "Kontynuuj oglądanie", + "next_up": "Następne w kolejce", + "recently_added_in": "Ostatnio dodano w {{libraryName}}", + "suggested_movies": "Sugerowane filmy", + "suggested_episodes": "Sugerowane odcinki", + "intro": { + "welcome_to_streamyfin": "Witamy w Streamyfin", + "a_free_and_open_source_client_for_jellyfin": "Darmowy i otwartoźródłowy klient dla Jellyfin.", + "features_title": "Funkcje", + "features_description": "Streamyfin posiada wiele funkcji i integruje się z szeroką gamą oprogramowania (możesz je znaleźć w menu ustawień), w tym:", + "jellyseerr_feature_description": "Połącz się ze swoim serwerem Jellyseerr i zamawiaj filmy bezpośrednio w aplikacji.", + "downloads_feature_title": "Pobieranie", + "downloads_feature_description": "Pobieraj filmy oraz seriale do oglądania offline. Użyj domyślnej metody lub zainstaluj serwer do optymalizacji, aby pobierać pliki w tle.", + "chromecast_feature_description": "Przesyłaj filmy i seriale na urządzenia Chromecast.", + "centralised_settings_plugin_title": "Scentralizowana wtyczka ustawień", + "centralised_settings_plugin_description": "Konfiguruj ustawienia z jednego miejsca na serwerze Jellyfin. Wszystkie ustawienia klientów dla wszystkich użytkowników będą synchronizowane automatycznie.", + "done_button": "Gotowe", + "go_to_settings_button": "Przejdź do ustawień", + "read_more": "Czytaj więcej" + }, + "settings": { + "settings_title": "Ustawienia", + "log_out_button": "Wyloguj się", + "user_info": { + "user_info_title": "Informacje o użytkowniku", + "user": "Użytkownik", + "server": "Serwer", + "token": "Token", + "app_version": "Wersja aplikacji" + }, + "quick_connect": { + "quick_connect_title": "Szybkie połączenie", + "authorize_button": "Autoryzuj szybkie połączenie", + "enter_the_quick_connect_code": "Wpisz kod szybkiego połączenia...", + "success": "Sukces", + "quick_connect_autorized": "Szybkie połączenie autoryzowane", + "error": "Błąd", + "invalid_code": "Nieprawidłowy kod", + "authorize": "Autoryzuj" + }, + "media_controls": { + "media_controls_title": "Sterowanie multimediami", + "forward_skip_length": "Długość przewijania do przodu", + "rewind_length": "Długość przewijania do tyłu", + "seconds_unit": "s" + }, + "audio": { + "audio_title": "Audio", + "set_audio_track": "Ustaw ścieżkę audio z poprzedniego elementu", + "audio_language": "Język audio", + "audio_hint": "Wybierz domyślny język audio.", + "none": "Brak", + "language": "Język" + }, + "subtitles": { + "subtitle_title": "Napisy", + "subtitle_language": "Język napisów", + "subtitle_mode": "Tryb napisów", + "set_subtitle_track": "Ustaw ścieżkę napisów z poprzedniego elementu", + "subtitle_size": "Rozmiar napisów", + "subtitle_hint": "Skonfiguruj preferencje dotyczące napisów.", + "none": "Brak", + "language": "Język", + "loading": "Ładowanie", + "modes": { + "Default": "Domyślny", + "Smart": "Inteligentny", + "Always": "Zawsze", + "None": "Brak", + "OnlyForced": "Tylko wymuszone" + } + }, + "other": { + "other_title": "Inne", + "follow_device_orientation": "Podążaj za orientacją urządzenia", + "video_orientation": "Orientacja wideo", + "orientation": "Orientacja", + "orientations": { + "DEFAULT": "Domyślna", + "ALL": "Wszystkie", + "PORTRAIT": "Pionowa", + "PORTRAIT_UP": "Pionowa w górę", + "PORTRAIT_DOWN": "Pionowa w dół", + "LANDSCAPE": "Pozioma", + "LANDSCAPE_LEFT": "Pozioma w lewo", + "LANDSCAPE_RIGHT": "Pozioma w prawo", + "OTHER": "Inna", + "UNKNOWN": "Nieznana" + }, + "safe_area_in_controls": "Bezpieczny obszar w kontrolkach", + "video_player": "Odtwarzacz wideo", + "video_players": { + "VLC_3": "VLC 3", + "VLC_4": "VLC 4 (Eksperymentalny + PiP)" + }, + "show_custom_menu_links": "Pokaż niestandardowe odnośniki w menu", + "hide_libraries": "Ukryj biblioteki", + "select_liraries_you_want_to_hide": "Wybierz biblioteki, które chcesz ukryć na karcie Biblioteka i w sekcjach strony głównej.", + "disable_haptic_feedback": "Wyłącz wibracje", + "default_quality": "Domyślna jakość" + }, + "downloads": { + "downloads_title": "Pobieranie", + "download_method": "Metoda pobierania", + "remux_max_download": "Maksymalne pobieranie remux", + "auto_download": "Automatyczne pobieranie", + "optimized_versions_server": "Serwer zoptymalizowanych wersji", + "save_button": "Zapisz", + "optimized_server": "Serwer zoptymalizowany", + "optimized": "Zoptymalizowany", + "default": "Domyślny", + "optimized_version_hint": "Podaj adres URL dla serwera optymalizującego. Adres powinien zawierać http lub https oraz opcjonalnie port.", + "read_more_about_optimized_server": "Dowiedz się więcej o serwerze optymalizującym.", + "url": "URL", + "server_url_placeholder": "http(s)://domena.org:port" + }, + "plugins": { + "plugins_title": "Wtyczki", + "jellyseerr": { + "jellyseerr_warning": "Ta integracja jest na wczesnym etapie. Należy oczekiwać zmian.", + "server_url": "URL serwera", + "server_url_hint": "Przykład: http(s)://twoja-nazwa.url\n(dodaj port, jeśli jest wymagany)", + "server_url_placeholder": "Adres URL Jellyseerr...", + "password": "Hasło", + "password_placeholder": "Wpisz hasło użytkownika Jellyfin {{username}}", + "save_button": "Zapisz", + "clear_button": "Wyczyść", + "login_button": "Zaloguj", + "total_media_requests": "Łączna liczba próśb o media", + "movie_quota_limit": "Limit zapytań o filmy", + "movie_quota_days": "Okres limitu (dni) dla filmów", + "tv_quota_limit": "Limit zapytań o seriale", + "tv_quota_days": "Okres limitu (dni) dla seriali", + "reset_jellyseerr_config_button": "Resetuj konfigurację Jellyseerr", + "unlimited": "Bez limitu", + "plus_n_more": "+{{n}} więcej", + "order_by": { + "DEFAULT": "Domyślny", + "VOTE_COUNT_AND_AVERAGE": "Liczba głosów i średnia", + "POPULARITY": "Popularność" + } + }, + "marlin_search": { + "enable_marlin_search": "Włącz wyszukiwanie Marlin", + "url": "URL", + "server_url_placeholder": "http(s)://domena.org:port", + "marlin_search_hint": "Podaj adres URL serwera Marlin. Adres powinien zawierać http lub https oraz opcjonalnie port.", + "read_more_about_marlin": "Dowiedz się więcej o Marlin.", + "save_button": "Zapisz", + "toasts": { + "saved": "Zapisano" + } + } + }, + "storage": { + "storage_title": "Pamięć", + "app_usage": "Aplikacja {{usedSpace}}%", + "device_usage": "Urządzenie {{availableSpace}}%", + "size_used": "{{used}} z {{total}} wykorzystane", + "delete_all_downloaded_files": "Usuń wszystkie pobrane pliki" + }, + "intro": { + "show_intro": "Pokaż wprowadzenie", + "reset_intro": "Zresetuj wprowadzenie" + }, + "logs": { + "logs_title": "Logi", + "no_logs_available": "Brak dostępnych logów", + "delete_all_logs": "Usuń wszystkie logi" + }, + "languages": { + "title": "Języki", + "app_language": "Język aplikacji", + "app_language_description": "Wybierz język aplikacji.", + "system": "System" + }, + "toasts": { + "error_deleting_files": "Błąd podczas usuwania plików", + "background_downloads_enabled": "Pobieranie w tle włączone", + "background_downloads_disabled": "Pobieranie w tle wyłączone", + "connected": "Połączono", + "could_not_connect": "Nie udało się połączyć", + "invalid_url": "Nieprawidłowy URL" + } + }, + "sessions": { + "title": "Sesje", + "no_active_sessions": "Brak aktywnych sesji" + }, + "downloads": { + "downloads_title": "Pobrane", + "tvseries": "Seriale", + "movies": "Filmy", + "queue": "Kolejka", + "queue_hint": "Kolejka i pobierania zostaną utracone po ponownym uruchomieniu aplikacji", + "no_items_in_queue": "Brak elementów w kolejce", + "no_downloaded_items": "Brak pobranych elementów", + "delete_all_movies_button": "Usuń wszystkie filmy", + "delete_all_tvseries_button": "Usuń wszystkie seriale", + "delete_all_button": "Usuń wszystko", + "active_download": "Aktywne pobieranie", + "no_active_downloads": "Brak aktywnych pobrań", + "active_downloads": "Aktywne pobrania", + "new_app_version_requires_re_download": "Nowa wersja aplikacji wymaga ponownego pobrania", + "new_app_version_requires_re_download_description": "Nowa aktualizacja wymaga ponownego pobrania treści. Usuń wszystkie pobrane materiały i spróbuj ponownie.", + "back": "Wstecz", + "delete": "Usuń", + "something_went_wrong": "Coś poszło nie tak", + "could_not_get_stream_url_from_jellyfin": "Nie udało się pobrać URL transmisji z Jellyfin", + "eta": "Szacowany czas: {{eta}}", + "methods": "Metody", + "toasts": { + "you_are_not_allowed_to_download_files": "Nie masz uprawnień do pobierania plików.", + "deleted_all_movies_successfully": "Wszystkie filmy zostały pomyślnie usunięte!", + "failed_to_delete_all_movies": "Nie udało się usunąć wszystkich filmów", + "deleted_all_tvseries_successfully": "Wszystkie seriale zostały pomyślnie usunięte!", + "failed_to_delete_all_tvseries": "Nie udało się usunąć wszystkich seriali", + "download_cancelled": "Pobieranie anulowane", + "could_not_cancel_download": "Nie udało się anulować pobierania", + "download_completed": "Pobieranie zakończone", + "download_started_for": "Rozpoczęto pobieranie: {{item}}", + "item_is_ready_to_be_downloaded": "{{item}} jest gotowe do pobrania", + "download_stated_for_item": "Rozpoczęto pobieranie: {{item}}", + "download_failed_for_item": "Pobieranie nie powiodło się dla {{item}} – {{error}}", + "download_completed_for_item": "Zakończono pobieranie: {{item}}", + "queued_item_for_optimization": "Dodano do kolejki optymalizacji: {{item}}", + "failed_to_start_download_for_item": "Nie udało się rozpocząć pobierania: {{item}}: {{message}}", + "server_responded_with_status_code": "Serwer zwrócił status {{statusCode}}", + "no_response_received_from_server": "Serwer nie zwrócił odpowiedzi", + "error_setting_up_the_request": "Błąd podczas konfiguracji żądania", + "failed_to_start_download_for_item_unexpected_error": "Nie udało się rozpocząć pobierania dla {{item}}: Nieoczekiwany błąd", + "all_files_folders_and_jobs_deleted_successfully": "Wszystkie pliki, foldery i zadania zostały pomyślnie usunięte", + "an_error_occured_while_deleting_files_and_jobs": "Wystąpił błąd podczas usuwania plików i zadań", + "go_to_downloads": "Przejdź do pobranych" + } + } + }, + "search": { + "search_here": "Szukaj tutaj...", + "search": "Szukaj...", + "x_items": "{{count}} elementów", + "library": "Biblioteka", + "discover": "Odkrywaj", + "no_results": "Brak wyników", + "no_results_found_for": "Nie znaleziono wyników dla", + "movies": "Filmy", + "series": "Seriale", + "episodes": "Odcinki", + "collections": "Kolekcje", + "actors": "Aktorzy", + "request_movies": "Zamów filmy", + "request_series": "Zamów seriale", + "recently_added": "Ostatnio dodane", + "recent_requests": "Ostatnie zamówienia", + "plex_watchlist": "Plex Watchlist", + "trending": "Popularne", + "popular_movies": "Popularne filmy", + "movie_genres": "Gatunki filmowe", + "upcoming_movies": "Nadchodzące filmy", + "studios": "Studia", + "popular_tv": "Popularne seriale", + "tv_genres": "Gatunki seriali", + "upcoming_tv": "Nadchodzące seriale", + "networks": "Sieci", + "tmdb_movie_keyword": "Słowo kluczowe filmu (TMDB)", + "tmdb_movie_genre": "Gatunek filmu (TMDB)", + "tmdb_tv_keyword": "Słowo kluczowe serialu (TMDB)", + "tmdb_tv_genre": "Gatunek serialu (TMDB)", + "tmdb_search": "Wyszukiwanie TMDB", + "tmdb_studio": "Studio TMDB", + "tmdb_network": "Sieć TMDB", + "tmdb_movie_streaming_services": "Usługi streamingowe filmów (TMDB)", + "tmdb_tv_streaming_services": "Usługi streamingowe seriali (TMDB)" + }, + "library": { + "no_items_found": "Nie znaleziono elementów", + "no_results": "Brak wyników", + "no_libraries_found": "Nie znaleziono bibliotek", + "item_types": { + "movies": "filmy", + "series": "seriale", + "boxsets": "zestawy", + "items": "elementy" + }, + "options": { + "display": "Wyświetlanie", + "row": "Wiersz", + "list": "Lista", + "image_style": "Styl obrazu", + "poster": "Plakat", + "cover": "Okładka", + "show_titles": "Pokaż tytuły", + "show_stats": "Pokaż statystyki" + }, + "filters": { + "genres": "Gatunki", + "years": "Lata", + "sort_by": "Sortuj według", + "sort_order": "Kolejność sortowania", + "asc": "Rosnąco", + "desc": "Malejąco", + "tags": "Tagi" + } + }, + "favorites": { + "series": "Seriale", + "movies": "Filmy", + "episodes": "Odcinki", + "videos": "Filmy wideo", + "boxsets": "Zestawy", + "playlists": "Playlisty", + "noDataTitle": "Brak ulubionych jeszcze", + "noData": "Dodaj elementy do ulubionych, aby zobaczyć je tutaj dla szybkiego dostępu." + }, + "custom_links": { + "no_links": "Brak odnośników" + }, + "player": { + "error": "Błąd", + "failed_to_get_stream_url": "Nie udało się pobrać adresu strumienia", + "an_error_occured_while_playing_the_video": "Wystąpił błąd podczas odtwarzania wideo. Sprawdź logi w ustawieniach.", + "client_error": "Błąd klienta", + "could_not_create_stream_for_chromecast": "Nie udało się utworzyć strumienia dla Chromecasta", + "message_from_server": "Wiadomość z serwera: {{message}}", + "video_has_finished_playing": "Wideo zostało odtworzone do końca!", + "no_video_source": "Brak źródła wideo...", + "next_episode": "Następny odcinek", + "refresh_tracks": "Odśwież ścieżki", + "subtitle_tracks": "Ścieżki napisów:", + "audio_tracks": "Ścieżki audio:", + "playback_state": "Stan odtwarzania:", + "no_data_available": "Brak dostępnych danych", + "index": "Indeks:" + }, + "item_card": { + "next_up": "Następne", + "no_items_to_display": "Brak elementów do wyświetlenia", + "cast_and_crew": "Obsada i ekipa", + "series": "Serial", + "seasons": "Sezony", + "season": "Sezon", + "no_episodes_for_this_season": "Brak odcinków w tym sezonie", + "overview": "Opis", + "more_with": "Więcej z {{name}}", + "similar_items": "Podobne elementy", + "no_similar_items_found": "Nie znaleziono podobnych elementów", + "video": "Wideo", + "more_details": "Więcej szczegółów", + "quality": "Jakość", + "audio": "Audio", + "subtitles": "Napisy", + "show_more": "Pokaż więcej", + "show_less": "Pokaż mniej", + "appeared_in": "Wystąpił w", + "could_not_load_item": "Nie udało się wczytać elementu", + "none": "Brak", + "download": { + "download_season": "Pobierz sezon", + "download_series": "Pobierz serial", + "download_episode": "Pobierz odcinek", + "download_movie": "Pobierz film", + "download_x_item": "Pobierz {{item_count}} elementów", + "download_button": "Pobierz", + "using_optimized_server": "Używanie serwera zoptymalizowanego", + "using_default_method": "Używanie metody domyślnej" + } + }, + "live_tv": { + "next": "Następny", + "previous": "Poprzedni", + "live_tv": "Telewizja na żywo", + "coming_soon": "Już wkrótce", + "on_now": "Teraz na żywo", + "shows": "Programy", + "movies": "Filmy", + "sports": "Sport", + "for_kids": "Dla dzieci", + "news": "Wiadomości" + }, + "jellyseerr": { + "confirm": "Potwierdź", + "cancel": "Anuluj", + "yes": "Tak", + "whats_wrong": "Co jest nie tak?", + "issue_type": "Typ problemu", + "select_an_issue": "Wybierz problem", + "types": "Typy", + "describe_the_issue": "(opcjonalnie) Opisz problem...", + "submit_button": "Zgłoś", + "report_issue_button": "Zgłoś problem", + "request_button": "Poproś", + "are_you_sure_you_want_to_request_all_seasons": "Czy na pewno chcesz zamówić wszystkie sezony?", + "failed_to_login": "Logowanie nie powiodło się", + "cast": "Obsada", + "details": "Szczegóły", + "status": "Status", + "original_title": "Oryginalny tytuł", + "series_type": "Typ serialu", + "release_dates": "Daty premiery", + "first_air_date": "Data pierwszej emisji", + "next_air_date": "Data następnej emisji", + "revenue": "Przychód", + "budget": "Budżet", + "original_language": "Oryginalny język", + "production_country": "Kraj produkcji", + "studios": "Studia", + "network": "Sieć", + "currently_streaming_on": "Aktualnie dostępne w streamingu na", + "advanced": "Zaawansowane", + "request_as": "Poproś jako", + "tags": "Tagi", + "quality_profile": "Profil jakości", + "root_folder": "Folder główny", + "season_all": "Sezon (wszystkie)", + "season_number": "Sezon {{season_number}}", + "number_episodes": "{{episode_number}} odcinków", + "born": "Urodzony", + "appearances": "Występy", + "toasts": { + "jellyseer_does_not_meet_requirements": "Serwer Jellyseerr nie spełnia minimalnych wymagań wersji! Zaktualizuj go co najmniej do wersji 2.0.0", + "jellyseerr_test_failed": "Test Jellyseerr nie powiódł się. Spróbuj ponownie.", + "failed_to_test_jellyseerr_server_url": "Nie udało się przetestować adresu URL Jellyseerr", + "issue_submitted": "Zgłoszenie zostało przesłane!", + "requested_item": "Poproszono o {{item}}!", + "you_dont_have_permission_to_request": "Nie masz uprawnień, aby złożyć zamówienie!", + "something_went_wrong_requesting_media": "Coś poszło nie tak podczas zamawiania materiałów!" + } + }, + "tabs": { + "home": "Strona główna", + "search": "Szukaj", + "library": "Biblioteka", + "custom_links": "Niestandardowe odnośniki", + "favorites": "Ulubione" + } +} diff --git a/translations/sv.json b/translations/sv.json index d35f6c82..65cc3aed 100644 --- a/translations/sv.json +++ b/translations/sv.json @@ -22,6 +22,10 @@ "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", diff --git a/translations/tr.json b/translations/tr.json index 7b3a2320..6d1a6714 100644 --- a/translations/tr.json +++ b/translations/tr.json @@ -113,7 +113,7 @@ }, "other": { "other_title": "Diğer", - "auto_rotate": "Otomatik Döndürme", + "follow_device_orientation": "Otomatik Döndürme", "video_orientation": "Video Yönü", "orientation": "Yön", "orientations": { @@ -129,6 +129,11 @@ "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.", @@ -167,7 +172,13 @@ "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" + "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 ", @@ -322,6 +333,8 @@ "years": "Yıllar", "sort_by": "Sırala", "sort_order": "Sıralama düzeni", + "asc": "Ascending", + "desc": "Descending", "tags": "Etiketler" } }, @@ -331,7 +344,9 @@ "episodes": "Bölümler", "videos": "Videolar", "boxsets": "Koleksiyonlar", - "playlists": "Çalma listeleri" + "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" @@ -432,7 +447,7 @@ "tags": "Etiketler", "quality_profile": "Kalite Profili", "root_folder": "Kök Klasör", - "season_x": "Sezon {{seasons}}", + "season_all": "Season (all)", "season_number": "Sezon {{season_number}}", "number_episodes": "Bölüm {{episode_number}}", "born": "Doğum", diff --git a/translations/ua.json b/translations/ua.json new file mode 100644 index 00000000..d2a40d71 --- /dev/null +++ b/translations/ua.json @@ -0,0 +1,475 @@ +{ + "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": "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": "Введіть URL вашого Jellyfin сервера", + "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": "Вільний і open-source клієнт для 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", + "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": "Smart", + "Always": "Завжди", + "None": "Някий", + "OnlyForced": "Виключно Форсовані" + } + }, + "other": { + "other_title": "Інші", + "follow_device_orientation": "Дотримуйтесь орієнтації пристрою", + "video_orientation": "Орієнтація відео", + "orientation": "Orientation", + "orientations": { + "DEFAULT": "За змовчуванням", + "ALL": "Всі", + "PORTRAIT": "Портретна", + "PORTRAIT_UP": "Портретна Догори", + "PORTRAIT_DOWN": "Портретна Донизу", + "LANDSCAPE": "Альбомна", + "LANDSCAPE_LEFT": "Альбомна Ліва", + "LANDSCAPE_RIGHT": "Альбомна Права", + "OTHER": "Інше", + "UNKNOWN": "Невідомо" + }, + "safe_area_in_controls": "Безпечна зона в елементах керування", + "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": "Вимкнути тактильний зворотний зв'язок", + "default_quality": "Якість за замовченням" + }, + "downloads": { + "downloads_title": "Завантаження", + "download_method": "Метод завантаження", + "remux_max_download": "Remux max download", + "auto_download": "Авто-завантаження", + "optimized_versions_server": "Optimized versions server", + "save_button": "Зберегти", + "optimized_server": "Оптимізований Сервер", + "optimized": "Оптимізований", + "default": "За замовченням", + "optimized_version_hint": "Введіть URL-адресу сервера для оптимізації. URL-адреса має містити http або https і, за бажанням, порт.", + "read_more_about_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}} ще", + "order_by": { + "DEFAULT": "За замовченням", + "VOTE_COUNT_AND_AVERAGE": "Кількість голосів і середнє", + "POPULARITY": "Популярність" + } + }, + "marlin_search": { + "enable_marlin_search": "Увімкнути Marlin Search ", + "url": "URL", + "server_url_placeholder": "http(s)://domain.org:port", + "marlin_search_hint": "Введіть URL-адресу сервера Marlin. Адреса повинна містити http або https і, за бажанням, порт.", + "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" + } + }, + "sessions": { + "title": "Сесії", + "no_active_sessions": "Нема активних сесій" + }, + "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": "Не вдалося отримати URL-адресу потоку від Jellyfin", + "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}} в черзі на оптимізацію", + "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": "За зростанням", + "desc": "За спаданням", + "tags": "Теги" + } + }, + "favorites": { + "series": "Серіали", + "movies": "Фільми", + "episodes": "Епізоди", + "videos": "Відео", + "boxsets": "Бокс-сети", + "playlists": "Плейлісти" + }, + "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": "Повідомлення від серверу: {{message}}", + "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": "Використовуючи сервер оптимізації", + "using_default_method": "Використовуючи метод за замовченням" + } + }, + "live_tv": { + "next": "Наступний", + "previous": "Попередній", + "live_tv": "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_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": "Не вдалося перевірити URL-адресу сервера jellyseerr", + "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-CN.json b/translations/zh-CN.json new file mode 100644 index 00000000..e9e0391a --- /dev/null +++ b/translations/zh-CN.json @@ -0,0 +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": "收藏" + } +} diff --git a/translations/zh-TW.json b/translations/zh-TW.json index bd5bba84..e533b56a 100644 --- a/translations/zh-TW.json +++ b/translations/zh-TW.json @@ -81,8 +81,8 @@ }, "media_controls": { "media_controls_title": "媒體控制", - "forward_skip_length": "前進跳過長度", - "rewind_length": "倒帶長度", + "forward_skip_length": "快進秒數", + "rewind_length": "倒帶秒數", "seconds_unit": "秒" }, "audio": { @@ -108,12 +108,12 @@ "Smart": "智能", "Always": "總是", "None": "無", - "OnlyForced": "僅強制" + "OnlyForced": "僅強制字幕" } }, "other": { "other_title": "其他", - "auto_rotate": "自動旋轉", + "follow_device_orientation": "自動旋轉", "video_orientation": "影片方向", "orientation": "方向", "orientations": { @@ -129,6 +129,11 @@ "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": "選擇您想從媒體庫頁面和主頁隱藏的媒體庫。", @@ -142,7 +147,7 @@ "optimized_versions_server": "Optimized Version 伺服器", "save_button": "保存", "optimized_server": "Optimized Server", - "optimized": "優化", + "optimized": "已優化", "default": "默認", "optimized_version_hint": "輸入 Optimized Server 的 URL。URL 應包括 http(s) 和端口 (可選)。", "read_more_about_optimized_server": "閱讀更多關於 Optimized Server 的信息。", @@ -152,7 +157,7 @@ "plugins": { "plugins_title": "插件", "jellyseerr": { - "jellyseerr_warning": "此集成處於早期階段。功能可能會有變化。", + "jellyseerr_warning": "此插件處於早期階段。功能可能會有變化。", "server_url": "伺服器 URL", "server_url_hint": "示例:http(s)://your-host.url\n(如果需要,添加端口)", "server_url_placeholder": "Jellyseerr URL...", @@ -167,7 +172,13 @@ "tv_quota_limit": "電視配額限制", "tv_quota_days": "電視配額天數", "reset_jellyseerr_config_button": "重置 Jellyseerr 配置", - "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": "啟用 Marlin 搜索", @@ -322,6 +333,8 @@ "years": "年份", "sort_by": "排序依據", "sort_order": "排序順序", + "asc": "Ascending", + "desc": "Descending", "tags": "標籤" } }, @@ -331,7 +344,9 @@ "episodes": "劇集", "videos": "影片", "boxsets": "套裝", - "playlists": "播放列表" + "playlists": "播放列表", + "noDataTitle": "尚無收藏", + "noData": "將項目標記為收藏,它們將顯示在此處以便快速訪問。" }, "custom_links": { "no_links": "無鏈接" @@ -342,7 +357,7 @@ "an_error_occured_while_playing_the_video": "播放影片時發生錯誤。請檢查設置中的日誌。", "client_error": "客戶端錯誤", "could_not_create_stream_for_chromecast": "無法為 Chromecast 建立串流", - "message_from_server": "來自伺服器的消息:{{message}}", + "message_from_server": "來自伺服器的消息", "video_has_finished_playing": "影片播放完畢!", "no_video_source": "無影片來源...", "next_episode": "下一集", @@ -410,7 +425,7 @@ "submit_button": "提交", "report_issue_button": "報告問題", "request_button": "請求", - "are_you_sure_you_want_to_request_all_seasons": "您確定要請求所有季度的節目嗎?", + "are_you_sure_you_want_to_request_all_seasons": "您確定要請求所有季度的劇集嗎?", "failed_to_login": "登入失敗", "cast": "演員", "details": "詳情", @@ -427,18 +442,18 @@ "studios": "工作室", "network": "網絡", "currently_streaming_on": "目前在以下流媒體上播放", - "advanced": "高級", - "request_as": "請求為", + "advanced": "高級設定", + "request_as": "選擇用戶以作請求", "tags": "標籤", "quality_profile": "質量配置文件", "root_folder": "根文件夾", - "season_x": "第 {{seasons}} 季", + "season_all": "Season (all)", "season_number": "第 {{season_number}} 季", "number_episodes": "{{episode_number}} 集", "born": "出生", "appearances": "出場", "toasts": { - "jellyseer_does_not_meet_requirements": "Jellyseerr 伺服器不符合最低版本要求!請更新至至少 2.0.0", + "jellyseer_does_not_meet_requirements": "Jellyseerr 伺服器不符合最低版本要求!請使用 2.0.0 及以上版本。", "jellyseerr_test_failed": "Jellyseerr 測試失敗。請再試一次。", "failed_to_test_jellyseerr_server_url": "無法測試 Jellyseerr 伺服器 URL", "issue_submitted": "問題已提交!", @@ -450,7 +465,7 @@ "tabs": { "home": "主頁", "search": "搜索", - "library": "庫", + "library": "媒體庫", "custom_links": "自定義鏈接", "favorites": "收藏" } diff --git a/types.d.ts b/types.d.ts new file mode 100644 index 00000000..684f73c5 --- /dev/null +++ b/types.d.ts @@ -0,0 +1,9 @@ +declare module "*.svg" { + const content: any; + export default content; +} + +declare module "*.png" { + 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 52ccf719..bd7b9ab2 100644 --- a/utils/atoms/settings.ts +++ b/utils/atoms/settings.ts @@ -1,19 +1,20 @@ -import { atom, useAtom } from "jotai"; -import { useCallback, useEffect, useMemo } from "react"; +import { BITRATES, type Bitrate } from "@/components/BitrateSelector"; 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 { 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 { Platform } from "react-native"; +import { storage } from "../mmkv"; const STREAMYFIN_PLUGIN_ID = "1e9e5d386e6746158719e98a5c34f004"; const STREAMYFIN_PLUGIN_SETTINGS = "STREAMYFIN_PLUGIN_SETTINGS"; @@ -112,9 +113,15 @@ export type HomeSectionNextUpResolver = { enableRewatching?: boolean; }; +export enum VideoPlayer { + // NATIVE, //todo: changes will make this a lot more easier to implement if we want. delete if not wanted + VLC_3 = 0, + VLC_4 = 1, +} + export type Settings = { home?: Home | null; - autoRotate?: boolean; + followDeviceOrientation?: boolean; forceLandscapeInVideoPlayer?: boolean; deviceProfile?: "Expo" | "Native" | "Old"; mediaListCollectionIds?: string[]; @@ -145,6 +152,8 @@ export type Settings = { safeAreaInControlsEnabled: boolean; jellyseerrServerUrl?: string; hiddenLibraries?: string[]; + enableH265ForChromecast: boolean; + defaultPlayer: VideoPlayer; }; export interface Lockable { @@ -161,7 +170,7 @@ export type StreamyfinPluginConfig = { const defaultValues: Settings = { home: null, - autoRotate: true, + followDeviceOrientation: true, forceLandscapeInVideoPlayer: false, deviceProfile: "Expo", mediaListCollectionIds: [], @@ -198,6 +207,8 @@ const defaultValues: Settings = { safeAreaInControlsEnabled: true, jellyseerrServerUrl: undefined, hiddenLibraries: [], + enableH265ForChromecast: false, + defaultPlayer: VideoPlayer.VLC_3, // ios only setting. does not matter what this is for android }; const loadSettings = (): Partial => { @@ -227,11 +238,11 @@ const saveSettings = (settings: Settings) => { export const settingsAtom = atom | null>(null); export const pluginSettingsAtom = atom( - storage.get(STREAMYFIN_PLUGIN_SETTINGS) + storage.get(STREAMYFIN_PLUGIN_SETTINGS), ); export const useSettings = () => { - const [api] = useAtom(apiAtom); + const api = useAtomValue(apiAtom); const [_settings, setSettings] = useAtom(settingsAtom); const [pluginSettings, _setPluginSettings] = useAtom(pluginSettingsAtom); @@ -247,7 +258,7 @@ export const useSettings = () => { storage.setAny(STREAMYFIN_PLUGIN_SETTINGS, settings); _setPluginSettings(settings); }, - [_setPluginSettings] + [_setPluginSettings], ); const refreshStreamyfinPluginSettings = useCallback(async () => { @@ -257,19 +268,26 @@ export const useSettings = () => { writeInfoLog(`Got remote settings: ${data?.settings}`); return data?.settings; }, - (err) => undefined + (err) => undefined, ); setPluginSettings(settings); return settings; }, [api]); const updateSettings = (update: Partial) => { - if (settings) { - const newSettings = { ..._settings, ...update }; + if (!_settings) return; + 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; setSettings(newSettings); - - // @ts-expect-error saveSettings(newSettings); } }; @@ -297,12 +315,14 @@ export const useSettings = () => { } acc = Object.assign(acc, { - [key]: locked ? value : _settings?.[key as keyof Settings] ?? value, + [key]: locked + ? value + : (_settings?.[key as keyof Settings] ?? value), }); } return acc; }, - {} as Settings + {} as Settings, ); return { diff --git a/utils/background-tasks.ts b/utils/background-tasks.ts index 0e9a408a..eb01c2c0 100644 --- a/utils/background-tasks.ts +++ b/utils/background-tasks.ts @@ -24,3 +24,26 @@ export async function unregisterBackgroundFetchAsync() { console.log("Error unregistering background fetch task", error); } } + +export const BACKGROUND_FETCH_TASK_SESSIONS = "background-fetch-sessions"; + +export async function registerBackgroundFetchAsyncSessions() { + try { + console.log("Registering background fetch sessions"); + BackgroundFetch.registerTaskAsync(BACKGROUND_FETCH_TASK_SESSIONS, { + minimumInterval: 1 * 60, // 1 minutes + stopOnTerminate: false, // android only, + startOnBoot: true, // android only + }); + } catch (error) { + console.log("Error registering background fetch task", error); + } +} + +export async function unregisterBackgroundFetchAsyncSessions() { + try { + BackgroundFetch.unregisterTaskAsync(BACKGROUND_FETCH_TASK_SESSIONS); + } catch (error) { + console.log("Error unregistering background fetch task", error); + } +} diff --git a/utils/bitrate.ts b/utils/bitrate.ts new file mode 100644 index 00000000..1bd4db6d --- /dev/null +++ b/utils/bitrate.ts @@ -0,0 +1,10 @@ +export const formatBitrate = (bitrate?: number | null) => { + if (!bitrate) return "N/A"; + + const sizes = ["bps", "Kbps", "Mbps", "Gbps", "Tbps"]; + if (bitrate === 0) return "0 bps"; + 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 new file mode 100644 index 00000000..e4bd0fc1 --- /dev/null +++ b/utils/eventBus.ts @@ -0,0 +1,26 @@ +type Listener = (data?: T) => void; + +class EventBus { + private listeners: Record[]> = {}; + + on(event: string, callback: Listener): () => void { + if (!this.listeners[event]) { + this.listeners[event] = []; + } + this.listeners[event].push(callback); + return () => this.off(event, callback); + } + + off(event: string, callback: Listener): void { + if (!this.listeners[event]) return; + this.listeners[event] = this.listeners[event].filter( + (fn) => fn !== callback, + ); + } + + emit(event: string, data?: T): void { + this.listeners[event]?.forEach((callback) => callback(data)); + } +} + +export const eventBus = new EventBus(); 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 482f7833..18f9357a 100644 --- a/utils/jellyfin/media/getStreamUrl.ts +++ b/utils/jellyfin/media/getStreamUrl.ts @@ -1,11 +1,12 @@ import native from "@/utils/profiles/native"; -import { Api } from "@jellyfin/sdk"; -import { +import type { Api } from "@jellyfin/sdk"; +import type { BaseItemDto, MediaSourceInfo, PlaybackInfoResponse, } from "@jellyfin/sdk/lib/generated-client/models"; import { getMediaInfoApi } from "@jellyfin/sdk/lib/utils/api"; +import { Alert } from "react-native"; export const getStreamUrl = async ({ api, @@ -62,7 +63,7 @@ export const getStreamUrl = async ({ data: { deviceProfile, }, - } + }, ); const transcodeUrl = res0.data.MediaSources?.[0].TranscodingUrl; sessionId = res0.data.PlaySessionId || null; @@ -80,7 +81,6 @@ export const getStreamUrl = async ({ const res2 = await getMediaInfoApi(api).getPlaybackInfo( { - userId, itemId: item.Id!, }, { @@ -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") { @@ -148,4 +148,8 @@ export const getStreamUrl = async ({ }; } } + + Alert.alert("Error", "Could not play this item"); + + return null; }; 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 94c62cb7..e17638ec 100644 --- a/utils/jellyfin/playstate/markAsPlayed.ts +++ b/utils/jellyfin/playstate/markAsPlayed.ts @@ -1,7 +1,6 @@ -import { Api } from "@jellyfin/sdk"; -import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; -import { AxiosError } from "axios"; -import { getAuthHeaders } from "../jellyfin"; +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 { api: Api | null | undefined; @@ -12,7 +11,7 @@ interface MarkAsPlayedParams { /** * Marks a media item as played and updates its progress to completion. * - * @param params - The parameters for marking an item as played + * @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 ({ @@ -26,24 +25,12 @@ export const markAsPlayed = async ({ } try { - const [playedResponse, progressResponse] = await Promise.all([ - api.axiosInstance.post( - `${api.basePath}/UserPlayedItems/${item.Id}`, - { userId, datePlayed: new Date().toISOString() }, - { headers: getAuthHeaders(api) }, - ), - api.axiosInstance.post( - `${api.basePath}/Sessions/Playing/Progress`, - { - ItemId: item.Id, - PositionTicks: item.RunTimeTicks, - MediaSourceId: item.Id, - }, - { headers: getAuthHeaders(api) }, - ), - ]); + const response = await getPlaystateApi(api).markPlayedItem({ + itemId: item.Id, + datePlayed: new Date().toISOString(), + }); - return playedResponse.status === 200 && progressResponse.status === 200; + return response.status === 200; } catch (error) { 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 99ef5cb1..1c2a04af 100644 --- a/utils/jellyfin/session/capabilities.ts +++ b/utils/jellyfin/session/capabilities.ts @@ -1,12 +1,8 @@ -import { Settings } from "@/utils/atoms/settings"; -import ios from "@/utils/profiles/ios"; +import type { Settings } from "@/utils/atoms/settings"; import native from "@/utils/profiles/native"; -import old from "@/utils/profiles/old"; -import { Api } from "@jellyfin/sdk"; -import { AxiosError, AxiosResponse } from "axios"; -import { useMemo } from "react"; +import type { Api } from "@jellyfin/sdk"; +import type { AxiosResponse } from "axios"; import { getAuthHeaders } from "../jellyfin"; -import iosFmp4 from "@/utils/profiles/iosFmp4"; interface PostCapabilitiesParams { api: Api | null | undefined; @@ -25,7 +21,6 @@ export const postCapabilities = async ({ api, itemId, sessionId, - deviceProfile, }: PostCapabilitiesParams): Promise => { if (!api || !itemId || !sessionId) { throw new Error("Missing parameters for marking item as not played"); @@ -33,7 +28,7 @@ export const postCapabilities = async ({ try { const d = api.axiosInstance.post( - api.basePath + "/Sessions/Capabilities/Full", + `${api.basePath}/Sessions/Capabilities/Full`, { playableMediaTypes: ["Audio", "Video"], supportedCommands: [ @@ -52,10 +47,10 @@ export const postCapabilities = async ({ }, { headers: getAuthHeaders(api), - } + }, ); return d; - } catch (error: any | AxiosError) { + } catch (error) { throw new Error("Failed to mark as not played"); } }; diff --git a/utils/jellyfin/tvshows/nextUp.ts b/utils/jellyfin/tvshows/nextUp.ts index 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 da4d0588..3844e699 100644 --- a/utils/profiles/chromecast.ts +++ b/utils/profiles/chromecast.ts @@ -1,9 +1,9 @@ -import { DeviceProfile } from "@jellyfin/sdk/lib/generated-client/models"; +import type { DeviceProfile } from "@jellyfin/sdk/lib/generated-client/models"; -export const chromecastProfile: DeviceProfile = { +export const chromecast: DeviceProfile = { Name: "Chromecast Video Profile", - MaxStreamingBitrate: 8000000, // 8 Mbps - MaxStaticBitrate: 8000000, // 8 Mbps + MaxStreamingBitrate: 16000000, // 16 Mbps + MaxStaticBitrate: 16000000, // 16 Mbps MusicStreamingTranscodingBitrate: 384000, // 384 kbps CodecProfiles: [ { @@ -60,6 +60,7 @@ export const chromecastProfile: DeviceProfile = { Protocol: "http", Context: "Streaming", MaxAudioChannels: "2", + MinSegments: 2, }, { Container: "mp3", diff --git a/utils/profiles/chromecasth265.ts b/utils/profiles/chromecasth265.ts new file mode 100644 index 00000000..42bb1712 --- /dev/null +++ b/utils/profiles/chromecasth265.ts @@ -0,0 +1,92 @@ +import type { DeviceProfile } from "@jellyfin/sdk/lib/generated-client/models"; + +export const chromecasth265: DeviceProfile = { + Name: "Chromecast Video Profile", + MaxStreamingBitrate: 16000000, // 16Mbps + MaxStaticBitrate: 16000000, // 16 Mbps + MusicStreamingTranscodingBitrate: 384000, // 384 kbps + CodecProfiles: [ + { + Type: "Video", + Codec: "hevc,h264", + }, + { + Type: "Audio", + Codec: "aac,mp3,flac,opus,vorbis", + }, + ], + ContainerProfiles: [], + DirectPlayProfiles: [ + { + Container: "mp4,mkv", + Type: "Video", + VideoCodec: "hevc,h264", + AudioCodec: "aac,mp3,opus,vorbis", + }, + { + Container: "mp3", + Type: "Audio", + }, + { + Container: "aac", + Type: "Audio", + }, + { + Container: "flac", + Type: "Audio", + }, + { + Container: "wav", + Type: "Audio", + }, + ], + TranscodingProfiles: [ + { + Container: "ts", + Type: "Video", + VideoCodec: "hevc,h264", + AudioCodec: "aac,mp3", + Protocol: "hls", + Context: "Streaming", + MaxAudioChannels: "2", + MinSegments: 2, + BreakOnNonKeyFrames: true, + }, + { + Container: "mp4,mkv", + Type: "Video", + VideoCodec: "hevc,h264", + AudioCodec: "aac", + Protocol: "http", + Context: "Streaming", + MaxAudioChannels: "2", + MinSegments: 2, + }, + { + Container: "mp3", + Type: "Audio", + AudioCodec: "mp3", + Protocol: "http", + Context: "Streaming", + MaxAudioChannels: "2", + }, + { + Container: "aac", + Type: "Audio", + AudioCodec: "aac", + Protocol: "http", + Context: "Streaming", + MaxAudioChannels: "2", + }, + ], + SubtitleProfiles: [ + { + Format: "vtt", + Method: "Encode", + }, + { + Format: "vtt", + Method: "Encode", + }, + ], +}; diff --git a/utils/profiles/native.js b/utils/profiles/native.js index 72d4e3b6..92f36b02 100644 --- a/utils/profiles/native.js +++ b/utils/profiles/native.js @@ -28,7 +28,7 @@ export default { Container: "mp4,mkv,avi,mov,flv,ts,m2ts,webm,ogv,3gp,hls", VideoCodec: "h264,hevc,mpeg4,divx,xvid,wmv,vc1,vp8,vp9,av1,avi,mpeg,mpeg2video", - AudioCodec: "aac,ac3,eac3,mp3,flac,alac,opus,vorbis,wma", + AudioCodec: "aac,ac3,eac3,mp3,flac,alac,opus,vorbis,wma,dts", }, { Type: MediaTypes.Audio, diff --git a/utils/store.ts b/utils/store.ts new file mode 100644 index 00000000..2ff6e1b4 --- /dev/null +++ b/utils/store.ts @@ -0,0 +1,3 @@ +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;