diff --git a/app/(auth)/(tabs)/(home)/_layout.tsx b/app/(auth)/(tabs)/(home)/_layout.tsx index 0117a1ee..1ca00059 100644 --- a/app/(auth)/(tabs)/(home)/_layout.tsx +++ b/app/(auth)/(tabs)/(home)/_layout.tsx @@ -1,4 +1,3 @@ -import { Chromecast } from "@/components/Chromecast"; import { nestedTabPageScreenOptions } from "@/components/stacks/NestedTabPageStack"; import { Feather } from "@expo/vector-icons"; import { Stack, useRouter } from "expo-router"; @@ -19,7 +18,6 @@ export default function IndexLayout() { headerShadowVisible: false, headerRight: () => ( - { router.push("/(auth)/settings"); diff --git a/app/(auth)/(tabs)/(home,libraries,search)/albums/[albumId].tsx b/app/(auth)/(tabs)/(home,libraries,search)/albums/[albumId].tsx index 565f84c8..44b6d053 100644 --- a/app/(auth)/(tabs)/(home,libraries,search)/albums/[albumId].tsx +++ b/app/(auth)/(tabs)/(home,libraries,search)/albums/[albumId].tsx @@ -1,4 +1,3 @@ -import { Chromecast } from "@/components/Chromecast"; import { ItemImage } from "@/components/common/ItemImage"; import { Text } from "@/components/common/Text"; import { TouchableItemRouter } from "@/components/common/TouchableItemRouter"; @@ -28,16 +27,6 @@ export default function page() { const navigation = useNavigation(); - useEffect(() => { - navigation.setOptions({ - headerRight: () => ( - - - - ), - }); - }); - const { data: album } = useQuery({ queryKey: ["album", albumId, artistId], queryFn: async () => { diff --git a/app/(auth)/player/transcoding-player.tsx b/app/(auth)/player/transcoding-player.tsx index f382411b..2191e441 100644 --- a/app/(auth)/player/transcoding-player.tsx +++ b/app/(auth)/player/transcoding-player.tsx @@ -306,7 +306,6 @@ const Player = () => { isPlaying: isPlaying, togglePlay: togglePlay, stopPlayback: stop, - offline: false, }); const [selectedTextTrack, setSelectedTextTrack] = useState< diff --git a/components/Chromecast.tsx b/components/Chromecast.tsx deleted file mode 100644 index 3e5c8546..00000000 --- a/components/Chromecast.tsx +++ /dev/null @@ -1,83 +0,0 @@ -import { Feather } from "@expo/vector-icons"; -import { BlurView } from "expo-blur"; -import React, { useCallback, useEffect } from "react"; -import { Platform, TouchableOpacity, ViewProps } from "react-native"; -import GoogleCast, { - CastButton, - CastContext, - useCastDevice, - useDevices, - useMediaStatus, - useRemoteMediaClient, -} from "react-native-google-cast"; -import { RoundButton } from "./RoundButton"; - -interface Props extends ViewProps { - width?: number; - height?: number; - background?: "blur" | "transparent"; -} - -export const Chromecast: React.FC = ({ - width = 48, - height = 48, - background = "transparent", - ...props -}) => { - const client = useRemoteMediaClient(); - const castDevice = useCastDevice(); - const devices = useDevices(); - const sessionManager = GoogleCast.getSessionManager(); - const discoveryManager = GoogleCast.getDiscoveryManager(); - const mediaStatus = useMediaStatus(); - - useEffect(() => { - (async () => { - if (!discoveryManager) { - return; - } - - await discoveryManager.startDiscovery(); - })(); - }, [client, devices, castDevice, sessionManager, discoveryManager]); - - // Android requires the cast button to be present for startDiscovery to work - const AndroidCastButton = useCallback( - () => - Platform.OS === "android" ? ( - - ) : ( - <> - ), - [Platform.OS] - ); - - if (background === "transparent") - return ( - { - if (mediaStatus?.currentItemId) CastContext.showExpandedControls(); - else CastContext.showCastDialog(); - }} - {...props} - > - - - ); - - return ( - { - if (mediaStatus?.currentItemId) CastContext.showExpandedControls(); - else CastContext.showCastDialog(); - }} - {...props} - > - - - ); -}; diff --git a/components/ItemContent.tsx b/components/ItemContent.tsx index e46646a2..a1d82b83 100644 --- a/components/ItemContent.tsx +++ b/components/ItemContent.tsx @@ -28,7 +28,6 @@ import { useAtom } from "jotai"; import React, { useEffect, useMemo, useState } from "react"; import { View } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; -import { Chromecast } from "./Chromecast"; import { ItemHeader } from "./ItemHeader"; import { ItemTechnicalDetails } from "./ItemTechnicalDetails"; import { MediaSourceSelector } from "./MediaSourceSelector"; @@ -84,7 +83,6 @@ export const ItemContent: React.FC<{ item: BaseItemDto }> = React.memo( headerRight: () => item && ( - {item.Type !== "Program" && ( diff --git a/components/PlayButton.tsx b/components/PlayButton.tsx index e5c5dd87..d0c2c227 100644 --- a/components/PlayButton.tsx +++ b/components/PlayButton.tsx @@ -1,24 +1,15 @@ import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; import { itemThemeColorAtom } from "@/utils/atoms/primaryColor"; import { useSettings } from "@/utils/atoms/settings"; -import { getParentBackdropImageUrl } from "@/utils/jellyfin/image/getParentBackdropImageUrl"; -import { getPrimaryImageUrl } from "@/utils/jellyfin/image/getPrimaryImageUrl"; -import { getStreamUrl } from "@/utils/jellyfin/media/getStreamUrl"; -import ios from "@/utils/profiles/ios"; import { runtimeTicksToMinutes } from "@/utils/time"; import { useActionSheet } from "@expo/react-native-action-sheet"; -import { Feather, Ionicons, MaterialCommunityIcons } from "@expo/vector-icons"; +import { Ionicons, MaterialCommunityIcons } from "@expo/vector-icons"; import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client"; +import * as Haptics from "expo-haptics"; import { useRouter } from "expo-router"; import { useAtom, useAtomValue } from "jotai"; import { useCallback, useEffect } from "react"; -import { Alert, TouchableOpacity, View } from "react-native"; -import CastContext, { - CastButton, - PlayServicesState, - useMediaStatus, - useRemoteMediaClient, -} from "react-native-google-cast"; +import { TouchableOpacity, View } from "react-native"; import Animated, { Easing, interpolate, @@ -31,8 +22,6 @@ import Animated, { } from "react-native-reanimated"; import { Button } from "./Button"; import { SelectedOptions } from "./ItemContent"; -import { chromecastProfile } from "@/utils/profiles/chromecast"; -import * as Haptics from "expo-haptics"; interface Props extends React.ComponentProps { item: BaseItemDto; @@ -48,8 +37,6 @@ export const PlayButton: React.FC = ({ ...props }: Props) => { const { showActionSheetWithOptions } = useActionSheet(); - const client = useRemoteMediaClient(); - const mediaStatus = useMediaStatus(); const [colorAtom] = useAtom(itemThemeColorAtom); const api = useAtomValue(apiAtom); @@ -91,137 +78,16 @@ export const PlayButton: React.FC = ({ const queryString = queryParams.toString(); - if (!client) { - goToPlayer(queryString, selectedOptions.bitrate?.value); - return; - } + goToPlayer(queryString, selectedOptions.bitrate?.value); - const options = ["Chromecast", "Device", "Cancel"]; - const cancelButtonIndex = 2; - showActionSheetWithOptions( - { - options, - cancelButtonIndex, - }, - async (selectedIndex: number | undefined) => { - if (!api) return; - const currentTitle = mediaStatus?.mediaInfo?.metadata?.title; - const isOpeningCurrentlyPlayingMedia = - currentTitle && currentTitle === item?.Name; - - switch (selectedIndex) { - case 0: - await CastContext.getPlayServicesState().then(async (state) => { - if (state && state !== PlayServicesState.SUCCESS) - CastContext.showPlayServicesErrorDialog(state); - else { - // Get a new URL with the Chromecast device profile: - 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, - }); - - if (!data?.url) { - console.warn("No URL returned from getStreamUrl", data); - Alert.alert( - "Client error", - "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 || "", - subtitle: item.Overview || "", - images: [ - { - url: getPrimaryImageUrl({ - api, - item, - quality: 90, - width: 2000, - })!, - }, - ], - } - : { - type: "generic", - title: item.Name || "", - subtitle: item.Overview || "", - images: [ - { - url: getPrimaryImageUrl({ - api, - item, - quality: 90, - width: 2000, - })!, - }, - ], - }, - }, - startTime: 0, - }) - .then(() => { - // state is already set when reopening current media, so skip it here. - if (isOpeningCurrentlyPlayingMedia) { - return; - } - CastContext.showExpandedControls(); - }); - } - }); - break; - case 1: - goToPlayer(queryString, selectedOptions.bitrate?.value); - break; - case cancelButtonIndex: - break; - } - } - ); + return; }, [ item, - client, settings, api, user, router, showActionSheetWithOptions, - mediaStatus, selectedOptions, ]); @@ -355,21 +221,13 @@ export const PlayButton: React.FC = ({ - {client && ( - - - - - )} - {!client && settings?.openInVLC && ( - - - - )} + + + diff --git a/components/RoundButton.tsx b/components/RoundButton.tsx index 6feafae5..7bcecdcb 100644 --- a/components/RoundButton.tsx +++ b/components/RoundButton.tsx @@ -9,7 +9,7 @@ import { import * as Haptics from "expo-haptics"; interface Props extends TouchableOpacityProps { - onPress?: () => void, + onPress?: () => void; icon?: keyof typeof Ionicons.glyphMap; background?: boolean; size?: "default" | "large"; @@ -98,7 +98,6 @@ export const RoundButton: React.FC> = ({ {icon ? ( = ({ }) => { const [api] = useAtom(apiAtom); const [user] = useAtom(userAtom); - const castDevice = useCastDevice(); const router = useRouter(); - const client = useRemoteMediaClient(); const { showActionSheetWithOptions } = useActionSheet(); const { setPlaySettings } = usePlaySettings(); const openSelect = () => { - if (!castDevice?.deviceId) { - play("device"); - return; - } - - const options = ["Chromecast", "Device", "Cancel"]; - const cancelButtonIndex = 2; - - showActionSheetWithOptions( - { - options, - cancelButtonIndex, - }, - (selectedIndex: number | undefined) => { - switch (selectedIndex) { - case 0: - play("cast"); - break; - case 1: - play("device"); - break; - case cancelButtonIndex: - break; - } - } - ); + play("device"); + return; }; const play = useCallback(async (type: "device" | "cast") => { diff --git a/components/video-player/controls/EpisodeList.tsx b/components/video-player/controls/EpisodeList.tsx index c68e21eb..9fae9c1a 100644 --- a/components/video-player/controls/EpisodeList.tsx +++ b/components/video-player/controls/EpisodeList.tsx @@ -1,25 +1,25 @@ -import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; -import { runtimeTicksToSeconds } from "@/utils/time"; -import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; -import { useQuery, useQueryClient } from "@tanstack/react-query"; -import { atom, useAtom } from "jotai"; -import { useEffect, useMemo, useState, useRef } from "react"; -import { View, TouchableOpacity } from "react-native"; -import { getTvShowsApi } from "@jellyfin/sdk/lib/utils/api"; -import { getUserItemData } from "@/utils/jellyfin/user-library/getUserItemData"; -import { Ionicons } from "@expo/vector-icons"; -import { Loader } from "@/components/Loader"; import ContinueWatchingPoster from "@/components/ContinueWatchingPoster"; -import { Text } from "@/components/common/Text"; -import { useSafeAreaInsets } from "react-native-safe-area-context"; +import { Loader } from "@/components/Loader"; import { HorizontalScroll, HorizontalScrollRef, } from "@/components/common/HorrizontalScroll"; +import { Text } from "@/components/common/Text"; import { SeasonDropdown, 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 { getTvShowsApi } from "@jellyfin/sdk/lib/utils/api"; +import { useQuery, useQueryClient } from "@tanstack/react-query"; +import { atom, useAtom } from "jotai"; +import { useEffect, useMemo, useRef, useState } from "react"; +import { TouchableOpacity, View } from "react-native"; +import { useSafeAreaInsets } from "react-native-safe-area-context"; type Props = { item: BaseItemDto; @@ -32,7 +32,6 @@ export const seasonIndexAtom = atom({}); export const EpisodeList: React.FC = ({ item, close, goToItem }) => { const [api] = useAtom(apiAtom); const [user] = useAtom(userAtom); - const insets = useSafeAreaInsets(); // Get safe area insets const [seasonIndexState, setSeasonIndexState] = useAtom(seasonIndexAtom); const scrollViewRef = useRef(null); // Reference to the HorizontalScroll const scrollToIndex = (index: number) => { diff --git a/providers/JobQueueProvider.tsx b/providers/JobQueueProvider.tsx deleted file mode 100644 index 00358e48..00000000 --- a/providers/JobQueueProvider.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import React, { createContext } from "react"; -import { useJobProcessor } from "@/utils/atoms/queue"; - -const JobQueueContext = createContext(null); - -export const JobQueueProvider: React.FC<{ children: React.ReactNode }> = ({ - children, -}) => { - useJobProcessor(); - - return ( - {children} - ); -}; diff --git a/tsconfig.json b/tsconfig.json index 909e9010..c9bf9603 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,15 +3,9 @@ "compilerOptions": { "strict": true, "paths": { - "@/*": [ - "./*" - ] + "@/*": ["./*"] } }, - "include": [ - "**/*.ts", - "**/*.tsx", - ".expo/types/**/*.ts", - "expo-env.d.ts" - ] + "include": ["**/*.ts", "**/*.tsx", ".expo/types/**/*.ts", "expo-env.d.ts"], + "exclude": ["node_modules", "utils/jellyseerr/**/*.ts"] } diff --git a/utils/profiles/chromecast.ts b/utils/profiles/chromecast.ts deleted file mode 100644 index 83bb6eb2..00000000 --- a/utils/profiles/chromecast.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { DeviceProfile } from "@jellyfin/sdk/lib/generated-client/models"; - -export const chromecastProfile: DeviceProfile = { - Name: "Chromecast Video Profile", - MaxStreamingBitrate: 8000000, // 8 Mbps - MaxStaticBitrate: 8000000, // 8 Mbps - MusicStreamingTranscodingBitrate: 384000, // 384 kbps - CodecProfiles: [ - { - Type: "Video", - Codec: "h264", - }, - { - Type: "Audio", - Codec: "aac,mp3,flac,opus,vorbis", - }, - ], - DirectPlayProfiles: [ - { - Container: "mp4", - Type: "Video", - VideoCodec: "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: "h264", - AudioCodec: "aac,mp3", - Protocol: "hls", - Context: "Streaming", - MaxAudioChannels: "2", - MinSegments: 2, - BreakOnNonKeyFrames: true, - }, - { - Container: "mp4", - Type: "Video", - VideoCodec: "h264", - AudioCodec: "aac", - Protocol: "http", - Context: "Streaming", - MaxAudioChannels: "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", - }, - ], -};