diff --git a/app/(auth)/(tabs)/(home)/settings/logs/page.tsx b/app/(auth)/(tabs)/(home)/settings/logs/page.tsx index 12878717..87831925 100644 --- a/app/(auth)/(tabs)/(home)/settings/logs/page.tsx +++ b/app/(auth)/(tabs)/(home)/settings/logs/page.tsx @@ -1,9 +1,9 @@ import { File, Paths } from "expo-file-system"; import { useNavigation } from "expo-router"; -import * as Sharing from "expo-sharing"; +import type * as SharingType from "expo-sharing"; import { useCallback, useEffect, useId, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; -import { ScrollView, TouchableOpacity, View } from "react-native"; +import { Platform, ScrollView, TouchableOpacity, View } from "react-native"; import Collapsible from "react-native-collapsible"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { Text } from "@/components/common/Text"; @@ -11,6 +11,11 @@ import { FilterButton } from "@/components/filters/FilterButton"; import { Loader } from "@/components/Loader"; import { LogLevel, useLog, writeErrorLog } from "@/utils/log"; +// Conditionally import expo-sharing only on non-TV platforms +const Sharing = Platform.isTV + ? null + : (require("expo-sharing") as typeof SharingType); + export default function Page() { const navigation = useNavigation(); const { logs } = useLog(); @@ -49,6 +54,8 @@ export default function Page() { // Sharing it as txt while its formatted allows us to share it with many more applications const share = useCallback(async () => { + if (!Sharing) return; + const logsFile = new File(Paths.document, "logs.txt"); setLoading(true); @@ -60,9 +67,11 @@ export default function Page() { } finally { setLoading(false); } - }, [filteredLogs]); + }, [filteredLogs, Sharing]); useEffect(() => { + if (Platform.isTV) return; + navigation.setOptions({ headerRight: () => loading ? ( diff --git a/app/login.tsx b/app/login.tsx index 5298467d..46e6ab14 100644 --- a/app/login.tsx +++ b/app/login.tsx @@ -42,14 +42,14 @@ const Login: React.FC = () => { const [loadingServerCheck, setLoadingServerCheck] = useState(false); const [loading, setLoading] = useState(false); - const [serverURL, setServerURL] = useState(_apiUrl); + const [serverURL, setServerURL] = useState(_apiUrl || ""); const [serverName, setServerName] = useState(""); const [credentials, setCredentials] = useState<{ username: string; password: string; }>({ - username: _username, - password: _password, + username: _username || "", + password: _password || "", }); /** @@ -278,6 +278,8 @@ const Login: React.FC = () => { clearButtonMode='while-editing' maxLength={500} extraClassName='mb-4' + autoFocus={false} + blurOnSubmit={true} /> {/* Password */} @@ -301,6 +303,8 @@ const Login: React.FC = () => { clearButtonMode='while-editing' maxLength={500} extraClassName='mb-4' + autoFocus={false} + blurOnSubmit={true} /> @@ -351,6 +355,8 @@ const Login: React.FC = () => { autoCapitalize='none' textContentType='URL' maxLength={500} + autoFocus={false} + blurOnSubmit={true} /> {/* Full-width primary button */} diff --git a/components/common/Input.tsx b/components/common/Input.tsx index 89fb8463..8d7f602f 100644 --- a/components/common/Input.tsx +++ b/components/common/Input.tsx @@ -16,7 +16,10 @@ export function Input(props: InputProps) { const [isFocused, setIsFocused] = useState(false); return Platform.isTV ? ( - inputRef?.current?.focus?.()}> + inputRef?.current?.focus?.()} + activeOpacity={1} + > { + .then((colors: ImageColorsType.ImageColorsResult) => { let primary = "#fff"; let text = "#000"; let backup = "#fff"; diff --git a/hooks/useImageColorsReturn.ts b/hooks/useImageColorsReturn.ts index b9c53e61..c7ece78c 100644 --- a/hooks/useImageColorsReturn.ts +++ b/hooks/useImageColorsReturn.ts @@ -2,8 +2,14 @@ import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client"; import { useAtomValue } from "jotai"; import { useEffect, useMemo, useState } from "react"; import { Platform } from "react-native"; -import { getColors, ImageColorsResult } from "react-native-image-colors"; +import type * as ImageColorsType from "react-native-image-colors"; import { apiAtom } from "@/providers/JellyfinProvider"; + +// Conditionally import react-native-image-colors only on non-TV platforms +const ImageColors = Platform.isTV + ? null + : (require("react-native-image-colors") as typeof ImageColorsType); + import { adjustToNearBlack, calculateTextColor, @@ -80,11 +86,13 @@ export const useImageColorsReturn = ({ } // Extract colors from the image - getColors(source.uri, { + if (!ImageColors?.getColors) return; + + ImageColors.getColors(source.uri, { fallback: "#fff", cache: false, }) - .then((colors: ImageColorsResult) => { + .then((colors: ImageColorsType.ImageColorsResult) => { let primary = "#fff"; let text = "#000"; let backup = "#fff"; diff --git a/providers/Downloads/notifications.ts b/providers/Downloads/notifications.ts index 6b1acef6..d2d1a7a1 100644 --- a/providers/Downloads/notifications.ts +++ b/providers/Downloads/notifications.ts @@ -1,8 +1,13 @@ import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; -import * as Notifications from "expo-notifications"; +import type * as NotificationsType from "expo-notifications"; import type { TFunction } from "i18next"; import { Platform } from "react-native"; +// Conditionally import expo-notifications only on non-TV platforms +const Notifications = Platform.isTV + ? null + : (require("expo-notifications") as typeof NotificationsType); + /** * Generate notification content based on item type */ @@ -60,7 +65,7 @@ export async function sendDownloadNotification( body: string, data?: Record, ): Promise { - if (Platform.isTV) return; + if (Platform.isTV || !Notifications) return; try { await Notifications.scheduleNotificationAsync({ diff --git a/react-native.config.js b/react-native.config.js index 35a42e1c..e18e9923 100644 --- a/react-native.config.js +++ b/react-native.config.js @@ -3,6 +3,16 @@ const isTV = process.env?.EXPO_TV === "1"; +const disableForTV = (_moduleName) => + isTV + ? { + platforms: { + ios: null, + android: null, + }, + } + : undefined; + module.exports = { dependencies: { "react-native-volume-manager": !isTV @@ -16,5 +26,16 @@ module.exports = { android: null, }, }, + "expo-notifications": disableForTV("expo-notifications"), + "react-native-image-colors": disableForTV("react-native-image-colors"), + "expo-sharing": disableForTV("expo-sharing"), + "expo-haptics": disableForTV("expo-haptics"), + "expo-brightness": disableForTV("expo-brightness"), + "expo-sensors": disableForTV("expo-sensors"), + "react-native-ios-context-menu": disableForTV( + "react-native-ios-context-menu", + ), + "react-native-ios-utilities": disableForTV("react-native-ios-utilities"), + "react-native-pager-view": disableForTV("react-native-pager-view"), }, };