diff --git a/app/(auth)/(tabs)/(home,libraries,search,favorites)/albums/[albumId].tsx b/app/(auth)/(tabs)/(home,libraries,search,favorites)/albums/[albumId].tsx deleted file mode 100644 index 565f84c8..00000000 --- a/app/(auth)/(tabs)/(home,libraries,search,favorites)/albums/[albumId].tsx +++ /dev/null @@ -1,128 +0,0 @@ -import { Chromecast } from "@/components/Chromecast"; -import { ItemImage } from "@/components/common/ItemImage"; -import { Text } from "@/components/common/Text"; -import { TouchableItemRouter } from "@/components/common/TouchableItemRouter"; -import { SongsList } from "@/components/music/SongsList"; -import { ParallaxScrollView } from "@/components/ParallaxPage"; -import ArtistPoster from "@/components/posters/ArtistPoster"; -import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; -import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; -import { getItemsApi } from "@jellyfin/sdk/lib/utils/api"; -import { useQuery } from "@tanstack/react-query"; -import { router, useLocalSearchParams, useNavigation } from "expo-router"; -import { useAtom } from "jotai"; -import { useEffect, useState } from "react"; -import { ScrollView, TouchableOpacity, View } from "react-native"; -import { useSafeAreaInsets } from "react-native-safe-area-context"; - -export default function page() { - const searchParams = useLocalSearchParams(); - const { collectionId, artistId, albumId } = searchParams as { - collectionId: string; - artistId: string; - albumId: string; - }; - - const [api] = useAtom(apiAtom); - const [user] = useAtom(userAtom); - - const navigation = useNavigation(); - - useEffect(() => { - navigation.setOptions({ - headerRight: () => ( - - - - ), - }); - }); - - const { data: album } = useQuery({ - queryKey: ["album", albumId, artistId], - queryFn: async () => { - if (!api) return null; - const response = await getItemsApi(api).getItems({ - userId: user?.Id, - ids: [albumId], - }); - const data = response.data.Items?.[0]; - return data; - }, - enabled: !!api && !!user?.Id && !!albumId, - staleTime: 0, - }); - - const { - data: songs, - isLoading, - isError, - } = useQuery<{ - Items: BaseItemDto[]; - TotalRecordCount: number; - }>({ - queryKey: ["songs", artistId, albumId], - queryFn: async () => { - if (!api) - return { - Items: [], - TotalRecordCount: 0, - }; - - const response = await getItemsApi(api).getItems({ - userId: user?.Id, - parentId: albumId, - fields: [ - "ItemCounts", - "PrimaryImageAspectRatio", - "CanDelete", - "MediaSourceCount", - ], - sortBy: ["ParentIndexNumber", "IndexNumber", "SortName"], - }); - - const data = response.data.Items; - - return { - Items: data || [], - TotalRecordCount: response.data.TotalRecordCount || 0, - }; - }, - enabled: !!api && !!user?.Id, - }); - - const insets = useSafeAreaInsets(); - - if (!album) return null; - - return ( - - } - > - - {album?.Name} - - {songs?.TotalRecordCount} songs - - - - - - - ); -} diff --git a/app/(auth)/(tabs)/(home,libraries,search,favorites)/artists/[artistId].tsx b/app/(auth)/(tabs)/(home,libraries,search,favorites)/artists/[artistId].tsx deleted file mode 100644 index 8d82d205..00000000 --- a/app/(auth)/(tabs)/(home,libraries,search,favorites)/artists/[artistId].tsx +++ /dev/null @@ -1,130 +0,0 @@ -import ArtistPoster from "@/components/posters/ArtistPoster"; -import { Text } from "@/components/common/Text"; -import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; -import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; -import { getItemsApi } from "@jellyfin/sdk/lib/utils/api"; -import { useQuery } from "@tanstack/react-query"; -import { router, useLocalSearchParams, useNavigation } from "expo-router"; -import { useAtom } from "jotai"; -import { useEffect, useState } from "react"; -import { FlatList, ScrollView, TouchableOpacity, View } from "react-native"; -import { useSafeAreaInsets } from "react-native-safe-area-context"; -import { ItemImage } from "@/components/common/ItemImage"; -import { ParallaxScrollView } from "@/components/ParallaxPage"; -import { TouchableItemRouter } from "@/components/common/TouchableItemRouter"; - -export default function page() { - const searchParams = useLocalSearchParams(); - const { artistId } = searchParams as { - artistId: string; - }; - - const [api] = useAtom(apiAtom); - const [user] = useAtom(userAtom); - - const navigation = useNavigation(); - - const [startIndex, setStartIndex] = useState(0); - - const { data: artist } = useQuery({ - queryKey: ["album", artistId], - queryFn: async () => { - if (!api) return null; - const response = await getItemsApi(api).getItems({ - userId: user?.Id, - ids: [artistId], - }); - const data = response.data.Items?.[0]; - return data; - }, - enabled: !!api && !!user?.Id && !!artistId, - staleTime: 0, - }); - - const { - data: albums, - isLoading, - isError, - } = useQuery<{ - Items: BaseItemDto[]; - TotalRecordCount: number; - }>({ - queryKey: ["albums", artistId, startIndex], - queryFn: async () => { - if (!api) - return { - Items: [], - TotalRecordCount: 0, - }; - - const response = await getItemsApi(api).getItems({ - userId: user?.Id, - parentId: artistId, - sortOrder: ["Descending", "Descending", "Ascending"], - includeItemTypes: ["MusicAlbum"], - recursive: true, - fields: [ - "ParentId", - "PrimaryImageAspectRatio", - "ParentId", - "PrimaryImageAspectRatio", - ], - collapseBoxSetItems: false, - albumArtistIds: [artistId], - startIndex, - limit: 100, - sortBy: ["PremiereDate", "ProductionYear", "SortName"], - }); - - const data = response.data.Items; - - return { - Items: data || [], - TotalRecordCount: response.data.TotalRecordCount || 0, - }; - }, - enabled: !!api && !!user?.Id, - }); - - const insets = useSafeAreaInsets(); - - if (!artist || !albums) return null; - - return ( - - } - > - - {artist?.Name} - - {albums.TotalRecordCount} albums - - - - {albums.Items.map((item, idx) => ( - - - - {item.Name} - {item.ProductionYear} - - - ))} - - - ); -} diff --git a/app/(auth)/(tabs)/(home,libraries,search,favorites)/artists/index.tsx b/app/(auth)/(tabs)/(home,libraries,search,favorites)/artists/index.tsx deleted file mode 100644 index 4827287e..00000000 --- a/app/(auth)/(tabs)/(home,libraries,search,favorites)/artists/index.tsx +++ /dev/null @@ -1,117 +0,0 @@ -import { Text } from "@/components/common/Text"; -import { TouchableItemRouter } from "@/components/common/TouchableItemRouter"; -import ArtistPoster from "@/components/posters/ArtistPoster"; -import MoviePoster from "@/components/posters/MoviePoster"; -import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; -import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; -import { getArtistsApi, getItemsApi } from "@jellyfin/sdk/lib/utils/api"; -import { useQuery } from "@tanstack/react-query"; -import { router, useLocalSearchParams } from "expo-router"; -import { useAtom } from "jotai"; -import { useMemo, useState } from "react"; -import { FlatList, TouchableOpacity, View } from "react-native"; - -export default function page() { - const searchParams = useLocalSearchParams(); - const { collectionId } = searchParams as { collectionId: string }; - - const [api] = useAtom(apiAtom); - const [user] = useAtom(userAtom); - - const { data: collection } = useQuery({ - queryKey: ["collection", collectionId], - queryFn: async () => { - if (!api) return null; - const response = await getItemsApi(api).getItems({ - userId: user?.Id, - ids: [collectionId], - }); - const data = response.data.Items?.[0]; - return data; - }, - enabled: !!api && !!user?.Id && !!collectionId, - staleTime: 0, - }); - - const [startIndex, setStartIndex] = useState(0); - - const { data, isLoading, isError } = useQuery<{ - Items: BaseItemDto[]; - TotalRecordCount: number; - }>({ - queryKey: ["collection-items", collection?.Id, startIndex], - queryFn: async () => { - if (!api || !collectionId) - return { - Items: [], - TotalRecordCount: 0, - }; - - const response = await getArtistsApi(api).getArtists({ - sortBy: ["SortName"], - sortOrder: ["Ascending"], - fields: ["PrimaryImageAspectRatio", "SortName"], - imageTypeLimit: 1, - enableImageTypes: ["Primary", "Backdrop", "Banner", "Thumb"], - parentId: collectionId, - userId: user?.Id, - }); - - const data = response.data.Items; - - return { - Items: data || [], - TotalRecordCount: response.data.TotalRecordCount || 0, - }; - }, - enabled: !!collection?.Id && !!api && !!user?.Id, - }); - - const totalItems = useMemo(() => { - return data?.TotalRecordCount; - }, [data]); - - if (!data) return null; - - return ( - - Artists - - } - nestedScrollEnabled - data={data.Items} - numColumns={3} - columnWrapperStyle={{ - justifyContent: "space-between", - }} - renderItem={({ item, index }) => ( - - - {collection?.CollectionType === "movies" && ( - - )} - {collection?.CollectionType === "music" && ( - - )} - {item.Name} - {item.ProductionYear} - - - )} - keyExtractor={(item) => item.Id || ""} - /> - ); -} 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 4c2b72ae..57393ce5 100644 --- a/app/(auth)/(tabs)/(home,libraries,search,favorites)/collections/[collectionId].tsx +++ b/app/(auth)/(tabs)/(home,libraries,search,favorites)/collections/[collectionId].tsx @@ -109,7 +109,7 @@ const page: React.FC = () => { genres: selectedGenres, tags: selectedTags, years: selectedYears.map((year) => parseInt(year)), - includeItemTypes: ["Movie", "Series", "MusicAlbum"], + includeItemTypes: ["Movie", "Series"], }); return response.data || null; diff --git a/app/(auth)/(tabs)/(libraries)/[libraryId].tsx b/app/(auth)/(tabs)/(libraries)/[libraryId].tsx index 7c0dbc91..414f6e90 100644 --- a/app/(auth)/(tabs)/(libraries)/[libraryId].tsx +++ b/app/(auth)/(tabs)/(libraries)/[libraryId].tsx @@ -150,8 +150,6 @@ const Page = () => { itemType = "Series"; } else if (library.CollectionType === "boxsets") { itemType = "BoxSet"; - } else if (library.CollectionType === "music") { - itemType = "MusicAlbum"; } const response = await getItemsApi(api).getItems({ diff --git a/app/(auth)/(tabs)/(libraries)/index.tsx b/app/(auth)/(tabs)/(libraries)/index.tsx index e8c3f766..08adacc5 100644 --- a/app/(auth)/(tabs)/(libraries)/index.tsx +++ b/app/(auth)/(tabs)/(libraries)/index.tsx @@ -33,7 +33,11 @@ export default function index() { }); const libraries = useMemo( - () => data?.filter((l) => !settings?.hiddenLibraries?.includes(l.Id!)), + () => + data + ?.filter((l) => !settings?.hiddenLibraries?.includes(l.Id!)) + .filter((l) => l.CollectionType !== "music") + .filter((l) => l.CollectionType !== "books") || [], [data, settings?.hiddenLibraries] ); diff --git a/app/(auth)/(tabs)/(search)/index.tsx b/app/(auth)/(tabs)/(search)/index.tsx index 39bb0b2a..b420be69 100644 --- a/app/(auth)/(tabs)/(search)/index.tsx +++ b/app/(auth)/(tabs)/(search)/index.tsx @@ -5,7 +5,6 @@ import ContinueWatchingPoster from "@/components/ContinueWatchingPoster"; import { Tag } from "@/components/GenreTags"; import { ItemCardText } from "@/components/ItemCardText"; import { JellyserrIndexPage } from "@/components/jellyseerr/JellyseerrIndexPage"; -import AlbumCover from "@/components/posters/AlbumCover"; import MoviePoster from "@/components/posters/MoviePoster"; import SeriesPoster from "@/components/posters/SeriesPoster"; import { LoadingSkeleton } from "@/components/search/LoadingSkeleton"; @@ -184,52 +183,19 @@ export default function search() { enabled: searchType === "Library" && debouncedSearch.length > 0, }); - const { data: artists, isFetching: l4 } = useQuery({ - queryKey: ["search", "artists", debouncedSearch], - queryFn: () => - searchFn({ - query: debouncedSearch, - types: ["MusicArtist"], - }), - enabled: searchType === "Library" && debouncedSearch.length > 0, - }); - - const { data: albums, isFetching: l5 } = useQuery({ - queryKey: ["search", "albums", debouncedSearch], - queryFn: () => - searchFn({ - query: debouncedSearch, - types: ["MusicAlbum"], - }), - enabled: searchType === "Library" && debouncedSearch.length > 0, - }); - - const { data: songs, isFetching: l6 } = useQuery({ - queryKey: ["search", "songs", debouncedSearch], - queryFn: () => - searchFn({ - query: debouncedSearch, - types: ["Audio"], - }), - enabled: searchType === "Library" && debouncedSearch.length > 0, - }); - const noResults = useMemo(() => { return !( - artists?.length || - albums?.length || - songs?.length || movies?.length || episodes?.length || series?.length || collections?.length || actors?.length ); - }, [artists, episodes, albums, songs, movies, series, collections, actors]); + }, [episodes, movies, series, collections, actors]); const loading = useMemo(() => { - return l1 || l2 || l3 || l4 || l5 || l6 || l7 || l8; - }, [l1, l2, l3, l4, l5, l6, l7, l8]); + return l1 || l2 || l3 || l7 || l8; + }, [l1, l2, l3, l7, l8]); return ( <> @@ -365,48 +331,6 @@ export default function search() { )} /> - m.Id!)} - header="Artists" - renderItem={(item: BaseItemDto) => ( - - - - - )} - /> - m.Id!)} - header="Albums" - renderItem={(item: BaseItemDto) => ( - - - - - )} - /> - m.Id!)} - header="Songs" - renderItem={(item: BaseItemDto) => ( - - - - - )} - /> ) : ( diff --git a/app/(auth)/player/_layout.tsx b/app/(auth)/player/_layout.tsx index 96d08058..4b0ae1c1 100644 --- a/app/(auth)/player/_layout.tsx +++ b/app/(auth)/player/_layout.tsx @@ -25,15 +25,6 @@ export default function Layout() { animation: "fade", }} /> - ); diff --git a/app/(auth)/player/music-player.tsx b/app/(auth)/player/music-player.tsx deleted file mode 100644 index fc4b8863..00000000 --- a/app/(auth)/player/music-player.tsx +++ /dev/null @@ -1,419 +0,0 @@ -import { Text } from "@/components/common/Text"; -import { Loader } from "@/components/Loader"; -import { Controls } from "@/components/video-player/controls/Controls"; -import { useOrientation } from "@/hooks/useOrientation"; -import { useOrientationSettings } from "@/hooks/useOrientationSettings"; -import { useWebSocket } from "@/hooks/useWebsockets"; -import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; -import { useSettings } from "@/utils/atoms/settings"; -import { getBackdropUrl } from "@/utils/jellyfin/image/getBackdropUrl"; -import { getAuthHeaders } from "@/utils/jellyfin/jellyfin"; -import { getStreamUrl } from "@/utils/jellyfin/media/getStreamUrl"; -import { secondsToTicks } from "@/utils/secondsToTicks"; -import { Api } from "@jellyfin/sdk"; -import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client"; -import { - getPlaystateApi, - getUserLibraryApi, -} from "@jellyfin/sdk/lib/utils/api"; -import { useQuery } from "@tanstack/react-query"; -import { useHaptic } from "@/hooks/useHaptic"; -import { Image } from "expo-image"; -import { useFocusEffect, useLocalSearchParams } from "expo-router"; -import { useAtomValue } from "jotai"; -import React, { useCallback, useMemo, useRef, useState } from "react"; -import { Pressable, useWindowDimensions, View } from "react-native"; -import { useSharedValue } from "react-native-reanimated"; -import Video, { OnProgressData, VideoRef } from "react-native-video"; - -export default function page() { - const api = useAtomValue(apiAtom); - const user = useAtomValue(userAtom); - const [settings] = useSettings(); - const videoRef = useRef(null); - const windowDimensions = useWindowDimensions(); - - const firstTime = useRef(true); - - const [isPlaybackStopped, setIsPlaybackStopped] = useState(false); - const [showControls, setShowControls] = useState(true); - const [ignoreSafeAreas, setIgnoreSafeAreas] = useState(false); - const [isPlaying, setIsPlaying] = useState(false); - const [isBuffering, setIsBuffering] = useState(true); - - const progress = useSharedValue(0); - const isSeeking = useSharedValue(false); - const cacheProgress = useSharedValue(0); - - const lightHapticFeedback = useHaptic("light"); - - const { - itemId, - audioIndex: audioIndexStr, - subtitleIndex: subtitleIndexStr, - mediaSourceId, - bitrateValue: bitrateValueStr, - } = useLocalSearchParams<{ - itemId: string; - audioIndex: string; - subtitleIndex: string; - mediaSourceId: string; - bitrateValue: string; - }>(); - - const audioIndex = audioIndexStr ? parseInt(audioIndexStr, 10) : undefined; - const subtitleIndex = subtitleIndexStr - ? parseInt(subtitleIndexStr, 10) - : undefined; - const bitrateValue = bitrateValueStr - ? parseInt(bitrateValueStr, 10) - : undefined; - - const { - data: item, - isLoading: isLoadingItem, - isError: isErrorItem, - } = useQuery({ - queryKey: ["item", itemId], - queryFn: async () => { - if (!api) return; - const res = await getUserLibraryApi(api).getItem({ - itemId, - userId: user?.Id, - }); - - return res.data; - }, - enabled: !!itemId && !!api, - staleTime: 0, - }); - - const { - data: stream, - isLoading: isLoadingStreamUrl, - isError: isErrorStreamUrl, - } = useQuery({ - queryKey: ["stream-url"], - queryFn: async () => { - if (!api) return; - const res = await getStreamUrl({ - api, - item, - startTimeTicks: item?.UserData?.PlaybackPositionTicks!, - userId: user?.Id, - audioStreamIndex: audioIndex, - maxStreamingBitrate: bitrateValue, - mediaSourceId: mediaSourceId, - subtitleStreamIndex: subtitleIndex, - }); - - if (!res) return null; - - const { mediaSource, sessionId, url } = res; - - if (!sessionId || !mediaSource || !url) return null; - - return { - mediaSource, - sessionId, - url, - }; - }, - }); - - const poster = usePoster(item, api); - const videoSource = useVideoSource(item, api, poster, stream?.url); - - const togglePlay = useCallback( - async (ticks: number) => { - lightHapticFeedback(); - if (isPlaying) { - videoRef.current?.pause(); - await getPlaystateApi(api!).onPlaybackProgress({ - itemId: item?.Id!, - audioStreamIndex: audioIndex ? audioIndex : undefined, - subtitleStreamIndex: subtitleIndex ? subtitleIndex : undefined, - mediaSourceId: mediaSourceId, - positionTicks: Math.floor(ticks), - isPaused: true, - playMethod: stream?.url.includes("m3u8") - ? "Transcode" - : "DirectStream", - playSessionId: stream?.sessionId, - }); - } else { - videoRef.current?.resume(); - await getPlaystateApi(api!).onPlaybackProgress({ - itemId: item?.Id!, - audioStreamIndex: audioIndex ? audioIndex : undefined, - subtitleStreamIndex: subtitleIndex ? subtitleIndex : undefined, - mediaSourceId: mediaSourceId, - positionTicks: Math.floor(ticks), - isPaused: false, - playMethod: stream?.url.includes("m3u8") - ? "Transcode" - : "DirectStream", - playSessionId: stream?.sessionId, - }); - } - }, - [ - isPlaying, - api, - item, - videoRef, - settings, - audioIndex, - subtitleIndex, - mediaSourceId, - stream, - ] - ); - - const play = useCallback(() => { - videoRef.current?.resume(); - reportPlaybackStart(); - }, [videoRef]); - - const pause = useCallback(() => { - videoRef.current?.pause(); - }, [videoRef]); - - const stop = useCallback(() => { - setIsPlaybackStopped(true); - videoRef.current?.pause(); - reportPlaybackStopped(); - }, [videoRef]); - - const seek = useCallback( - (seconds: number) => { - videoRef.current?.seek(seconds); - }, - [videoRef] - ); - - const reportPlaybackStopped = async () => { - if (!item?.Id) return; - await getPlaystateApi(api!).onPlaybackStopped({ - itemId: item.Id, - mediaSourceId: mediaSourceId, - positionTicks: Math.floor(progress.value), - playSessionId: stream?.sessionId, - }); - }; - - const reportPlaybackStart = async () => { - if (!item?.Id) return; - await getPlaystateApi(api!).onPlaybackStart({ - itemId: item?.Id, - audioStreamIndex: audioIndex ? audioIndex : undefined, - subtitleStreamIndex: subtitleIndex ? subtitleIndex : undefined, - mediaSourceId: mediaSourceId, - playMethod: stream?.url.includes("m3u8") ? "Transcode" : "DirectStream", - playSessionId: stream?.sessionId, - }); - }; - - const onProgress = useCallback( - async (data: OnProgressData) => { - if (isSeeking.value === true) return; - if (isPlaybackStopped === true) return; - - const ticks = data.currentTime * 10000000; - - progress.value = secondsToTicks(data.currentTime); - cacheProgress.value = secondsToTicks(data.playableDuration); - setIsBuffering(data.playableDuration === 0); - - if (!item?.Id || data.currentTime === 0) return; - - await getPlaystateApi(api!).onPlaybackProgress({ - itemId: item.Id!, - audioStreamIndex: audioIndex ? audioIndex : undefined, - subtitleStreamIndex: subtitleIndex ? subtitleIndex : undefined, - mediaSourceId: mediaSourceId, - positionTicks: Math.round(ticks), - isPaused: !isPlaying, - playMethod: stream?.url.includes("m3u8") ? "Transcode" : "DirectStream", - playSessionId: stream?.sessionId, - }); - }, - [ - item, - isPlaying, - api, - isPlaybackStopped, - audioIndex, - subtitleIndex, - mediaSourceId, - stream, - ] - ); - - useFocusEffect( - useCallback(() => { - play(); - - return () => { - stop(); - }; - }, [play, stop]) - ); - - useOrientation(); - useOrientationSettings(); - - useWebSocket({ - isPlaying: isPlaying, - pauseVideo: pause, - playVideo: play, - stopPlayback: stop, - }); - - if (isLoadingItem || isLoadingStreamUrl) - return ( - - - - ); - - if (isErrorItem || isErrorStreamUrl) - return ( - - Error - - ); - - if (!item || !stream) - return ( - - Error - - ); - - return ( - - - - - - { - setShowControls(!showControls); - }} - className="absolute z-0 h-full w-full opacity-0" - > - {videoSource && ( - - - - - ); -} - -export function usePoster( - item: BaseItemDto | null | undefined, - api: Api | null -): string | undefined { - const poster = useMemo(() => { - if (!item || !api) return undefined; - return item.Type === "Audio" - ? `${api.basePath}/Items/${item.AlbumId}/Images/Primary?tag=${item.AlbumPrimaryImageTag}&quality=90&maxHeight=200&maxWidth=200` - : getBackdropUrl({ - api, - item: item, - quality: 70, - width: 200, - }); - }, [item, api]); - - return poster ?? undefined; -} - -export function useVideoSource( - item: BaseItemDto | null | undefined, - api: Api | null, - poster: string | undefined, - url?: string | null -) { - const videoSource = useMemo(() => { - if (!item || !api || !url) { - return null; - } - - const startPosition = item?.UserData?.PlaybackPositionTicks - ? Math.round(item.UserData.PlaybackPositionTicks / 10000) - : 0; - - return { - uri: url, - isNetwork: true, - startPosition, - headers: getAuthHeaders(api), - metadata: { - artist: item?.AlbumArtist ?? undefined, - title: item?.Name || "Unknown", - description: item?.Overview ?? undefined, - imageUri: poster, - subtitle: item?.Album ?? undefined, - }, - }; - }, [item, api, poster]); - - return videoSource; -} diff --git a/app/(auth)/player/transcoding-player.tsx b/app/(auth)/player/transcoding-player.tsx index 971410f4..8c9a4b84 100644 --- a/app/(auth)/player/transcoding-player.tsx +++ b/app/(auth)/player/transcoding-player.tsx @@ -414,7 +414,6 @@ const Player = () => { playWhenInactive={true} allowsExternalPlayback={true} playInBackground={true} - pictureInPicture={true} showNotificationControls={true} ignoreSilentSwitch="ignore" fullscreen={false} @@ -532,7 +531,6 @@ export function useVideoSource( startPosition, headers: getAuthHeaders(api), metadata: { - artist: item?.AlbumArtist ?? undefined, title: item?.Name || "Unknown", description: item?.Overview ?? undefined, imageUri: poster, diff --git a/bun.lockb b/bun.lockb index 935cb03c..de11fa7d 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/components/common/TouchableItemRouter.tsx b/components/common/TouchableItemRouter.tsx index 04d36dc7..9ca97bee 100644 --- a/components/common/TouchableItemRouter.tsx +++ b/components/common/TouchableItemRouter.tsx @@ -26,18 +26,6 @@ export const itemRouter = ( return `/(auth)/(tabs)/${from}/series/${item.Id}`; } - if (item.Type === "MusicAlbum") { - return `/(auth)/(tabs)/${from}/albums/${item.Id}`; - } - - if (item.Type === "Audio") { - return `/(auth)/(tabs)/${from}/albums/${item.AlbumId}`; - } - - if (item.Type === "MusicArtist") { - return `/(auth)/(tabs)/${from}/artists/${item.Id}`; - } - if (item.Type === "Person" || item.Type === "Actor") { return `/(auth)/(tabs)/${from}/actors/${item.Id}`; } diff --git a/components/home/Favorites.tsx b/components/home/Favorites.tsx index 6cf05109..95920ea5 100644 --- a/components/home/Favorites.tsx +++ b/components/home/Favorites.tsx @@ -54,14 +54,6 @@ export const Favorites = () => { () => fetchFavoritesByType("Playlist"), [fetchFavoritesByType] ); - const fetchFavoriteMusicAlbum = useCallback( - () => fetchFavoritesByType("MusicAlbum"), - [fetchFavoritesByType] - ); - const fetchFavoriteAudio = useCallback( - () => fetchFavoritesByType("Audio"), - [fetchFavoritesByType] - ); return ( @@ -102,18 +94,6 @@ export const Favorites = () => { title="Playlists" hideIfEmpty /> - - ); }; diff --git a/components/library/LibraryItemCard.tsx b/components/library/LibraryItemCard.tsx index 17595db6..7a370405 100644 --- a/components/library/LibraryItemCard.tsx +++ b/components/library/LibraryItemCard.tsx @@ -60,8 +60,6 @@ export const LibraryItemCard: React.FC = ({ library, ...props }) => { _itemType = "Series"; } else if (library.CollectionType === "boxsets") { _itemType = "BoxSet"; - } else if (library.CollectionType === "music") { - _itemType = "MusicAlbum"; } return _itemType; @@ -76,8 +74,6 @@ export const LibraryItemCard: React.FC = ({ library, ...props }) => { nameStr = "series"; } else if (library.CollectionType === "boxsets") { nameStr = "box sets"; - } else if (library.CollectionType === "music") { - nameStr = "albums"; } else { nameStr = "items"; } diff --git a/components/music/SongsList.tsx b/components/music/SongsList.tsx deleted file mode 100644 index 4d576f3c..00000000 --- a/components/music/SongsList.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; -import { useRouter } from "expo-router"; -import { View, ViewProps } from "react-native"; -import { SongsListItem } from "./SongsListItem"; - -interface Props extends ViewProps { - songs?: BaseItemDto[] | null; - collectionId: string; - artistId: string; - albumId: string; -} - -export const SongsList: React.FC = ({ - collectionId, - artistId, - albumId, - songs = [], - ...props -}) => { - const router = useRouter(); - return ( - - {songs?.map((item: BaseItemDto, index: number) => ( - - ))} - - ); -}; diff --git a/components/music/SongsListItem.tsx b/components/music/SongsListItem.tsx deleted file mode 100644 index 552baa69..00000000 --- a/components/music/SongsListItem.tsx +++ /dev/null @@ -1,128 +0,0 @@ -import { Text } from "@/components/common/Text"; -import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; -import { usePlaySettings } from "@/providers/PlaySettingsProvider"; -import { runtimeTicksToSeconds } from "@/utils/time"; -import { useActionSheet } from "@expo/react-native-action-sheet"; -import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; -import { useRouter } from "expo-router"; -import { useAtom } from "jotai"; -import { useCallback } from "react"; -import { TouchableOpacity, TouchableOpacityProps, View } from "react-native"; -import CastContext, { - PlayServicesState, - useCastDevice, - useRemoteMediaClient, -} from "react-native-google-cast"; - -interface Props extends TouchableOpacityProps { - collectionId: string; - artistId: string; - albumId: string; - item: BaseItemDto; - index: number; -} - -export const SongsListItem: React.FC = ({ - collectionId, - artistId, - albumId, - item, - index, - ...props -}) => { - 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; - } - } - ); - }; - - const play = useCallback(async (type: "device" | "cast") => { - if (!user?.Id || !api || !item.Id) { - console.warn("No user, api or item", user, api, item.Id); - return; - } - - const data = await setPlaySettings({ - item, - }); - - if (!data?.url) { - throw new Error("play-music ~ No stream url"); - } - - if (type === "cast" && client) { - await CastContext.getPlayServicesState().then((state) => { - if (state && state !== PlayServicesState.SUCCESS) - CastContext.showPlayServicesErrorDialog(state); - else { - client.loadMedia({ - mediaInfo: { - contentUrl: data.url!, - contentType: "video/mp4", - metadata: { - type: item.Type === "Episode" ? "tvShow" : "movie", - title: item.Name || "", - subtitle: item.Overview || "", - }, - }, - startTime: 0, - }); - } - }); - } else { - console.log("Playing on device", data.url, item.Id); - router.push("/music-player"); - } - }, []); - - return ( - { - openSelect(); - }} - {...props} - > - - {index + 1} - - {item.Name} - - {runtimeTicksToSeconds(item.RunTimeTicks)} - - - - - ); -}; diff --git a/components/posters/AlbumCover.tsx b/components/posters/AlbumCover.tsx deleted file mode 100644 index 870dce6a..00000000 --- a/components/posters/AlbumCover.tsx +++ /dev/null @@ -1,82 +0,0 @@ -import { apiAtom } from "@/providers/JellyfinProvider"; -import { getPrimaryImageUrl } from "@/utils/jellyfin/image/getPrimaryImageUrl"; -import { getPrimaryImageUrlById } from "@/utils/jellyfin/image/getPrimaryImageUrlById"; -import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; -import { Image } from "expo-image"; -import { useAtom } from "jotai"; -import { useMemo } from "react"; -import { View } from "react-native"; - -type ArtistPosterProps = { - item?: BaseItemDto | null; - id?: string | null; - showProgress?: boolean; -}; - -const AlbumCover: React.FC = ({ item, id }) => { - const [api] = useAtom(apiAtom); - - const url = useMemo(() => { - const u = getPrimaryImageUrl({ - api, - item, - }); - return u; - }, [item]); - - const url2 = useMemo(() => { - const u = getPrimaryImageUrlById({ - api, - id, - quality: 85, - width: 300, - }); - return u; - }, [item]); - - if (!item && id) - return ( - - - - ); - - if (item) - return ( - - - - ); -}; - -export default AlbumCover; diff --git a/components/posters/ArtistPoster.tsx b/components/posters/ArtistPoster.tsx deleted file mode 100644 index d64818b6..00000000 --- a/components/posters/ArtistPoster.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import { apiAtom } from "@/providers/JellyfinProvider"; -import { getPrimaryImageUrl } from "@/utils/jellyfin/image/getPrimaryImageUrl"; -import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; -import { Image } from "expo-image"; -import { useAtom } from "jotai"; -import { useMemo } from "react"; -import { View } from "react-native"; - -type ArtistPosterProps = { - item: BaseItemDto; - showProgress?: boolean; -}; - -const ArtistPoster: React.FC = ({ - item, - showProgress = false, -}) => { - const [api] = useAtom(apiAtom); - - const url = useMemo( - () => - getPrimaryImageUrl({ - api, - item, - }), - [item] - ); - - if (!url) - return ( - - ); - - return ( - - - - ); -}; - -export default ArtistPoster; diff --git a/components/stacks/NestedTabPageStack.tsx b/components/stacks/NestedTabPageStack.tsx index 024b1272..2cfeed1d 100644 --- a/components/stacks/NestedTabPageStack.tsx +++ b/components/stacks/NestedTabPageStack.tsx @@ -17,14 +17,7 @@ export const commonScreenOptions: ICommonScreenOptions = { headerLeft: () => , }; -const routes = [ - "actors/[actorId]", - "albums/[albumId]", - "artists/index", - "artists/[artistId]", - "items/page", - "series/[id]", -]; +const routes = ["actors/[actorId]", "items/page", "series/[id]"]; export const nestedTabPageScreenOptions: Record = Object.fromEntries(routes.map((route) => [route, commonScreenOptions])); diff --git a/components/video-player/controls/Controls.tsx b/components/video-player/controls/Controls.tsx index 6a5f3caa..f209a411 100644 --- a/components/video-player/controls/Controls.tsx +++ b/components/video-player/controls/Controls.tsx @@ -449,12 +449,11 @@ export const Controls: React.FC = ({ = ({ }, ]} pointerEvents={showControls ? "auto" : "none"} - className={`flex flex-row w-full p-4 `} + className={`flex flex-row w-full pt-2`} > = ({ onPress={() => { switchOnEpisodeMode(); }} - className="aspect-square flex flex-col bg-neutral-800/90 rounded-xl items-center justify-center p-2" + className="aspect-square flex flex-col rounded-xl items-center justify-center p-2" > @@ -565,7 +564,7 @@ export const Controls: React.FC = ({ {previousItem && !offline && ( @@ -574,7 +573,7 @@ export const Controls: React.FC = ({ {nextItem && !offline && ( @@ -583,7 +582,7 @@ export const Controls: React.FC = ({ {/* {mediaSource?.TranscodingUrl && ( */} = ({ ); router.back(); }} - className="aspect-square flex flex-col bg-neutral-800/90 rounded-xl items-center justify-center p-2" + className="aspect-square flex flex-col rounded-xl items-center justify-center p-2" > @@ -730,11 +729,11 @@ export const Controls: React.FC = ({ bottom: settings?.safeAreaInControlsEnabled ? insets.bottom : 0, }, ]} - className={`flex flex-col p-4`} + className={`flex flex-col px-2`} onTouchStart={handleControlsInteraction} > = ({ }} pointerEvents={showControls ? "box-none" : "none"} > - {item?.Name} {item?.Type === "Episode" && ( - {item.SeriesName} + + {`${item.SeriesName} - ${item.SeasonName} Episode ${item.IndexNumber}`} + )} + {item?.Name} {item?.Type === "Movie" && ( {item?.ProductionYear} @@ -786,7 +787,7 @@ export const Controls: React.FC = ({ = ({ bubbleTextColor: "#666", heartbeatColor: "#999", }} - renderThumb={() => ( - - )} + renderThumb={() => null} cache={cacheProgress} onSlidingStart={handleSliderStart} onSlidingComplete={handleSliderComplete} @@ -829,7 +818,7 @@ export const Controls: React.FC = ({ minimumValue={min} maximumValue={max} /> - + {formatTimeString(currentTime, isVlc ? "ms" : "s")} diff --git a/components/video-player/controls/VideoTouchOverlay.tsx b/components/video-player/controls/VideoTouchOverlay.tsx index 03b0b8a3..85385acf 100644 --- a/components/video-player/controls/VideoTouchOverlay.tsx +++ b/components/video-player/controls/VideoTouchOverlay.tsx @@ -31,7 +31,7 @@ export const VideoTouchOverlay = ({ right: 0, top: 0, bottom: 0, - opacity: showControls ? 0.5 : 0, + opacity: showControls ? 0.75 : 0, }} /> ); diff --git a/components/video-player/controls/dropdown/DropdownViewDirect.tsx b/components/video-player/controls/dropdown/DropdownViewDirect.tsx index 28b55fa0..e2ba25fd 100644 --- a/components/video-player/controls/dropdown/DropdownViewDirect.tsx +++ b/components/video-player/controls/dropdown/DropdownViewDirect.tsx @@ -74,7 +74,7 @@ const DropdownViewDirect: React.FC = ({ return ( - + diff --git a/components/video-player/controls/dropdown/DropdownViewTranscoding.tsx b/components/video-player/controls/dropdown/DropdownViewTranscoding.tsx index d57cc126..5a05dd17 100644 --- a/components/video-player/controls/dropdown/DropdownViewTranscoding.tsx +++ b/components/video-player/controls/dropdown/DropdownViewTranscoding.tsx @@ -121,7 +121,7 @@ const DropdownView: React.FC = ({ showControls }) => { - + diff --git a/package.json b/package.json index 07fedc03..f3cc630f 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,8 @@ "preset": "jest-expo" }, "dependencies": { - "@bottom-tabs/react-navigation": "^0.7.1", + "@bottom-tabs/react-navigation": "0.7.8", + "react-native-bottom-tabs": "0.7.8", "@config-plugins/ffmpeg-kit-react-native": "^8.0.0", "@expo/react-native-action-sheet": "^4.1.0", "@expo/vector-icons": "^14.0.4", @@ -73,7 +74,6 @@ "react-dom": "18.2.0", "react-native": "0.74.5", "react-native-awesome-slider": "^2.5.6", - "react-native-bottom-tabs": "0.7.8", "react-native-circular-progress": "^1.4.1", "react-native-compressor": "^1.9.0", "react-native-country-flag": "^2.0.2", diff --git a/providers/PlaySettingsProvider.tsx b/providers/PlaySettingsProvider.tsx index 50f780cd..ff80bb9e 100644 --- a/providers/PlaySettingsProvider.tsx +++ b/providers/PlaySettingsProvider.tsx @@ -38,7 +38,6 @@ type PlaySettingsContextType = { setPlayUrl: React.Dispatch>; playSessionId?: string | null; setOfflineSettings: (data: PlaybackType) => void; - setMusicPlaySettings: (item: BaseItemDto, url: string) => void; }; const PlaySettingsContext = createContext( @@ -61,13 +60,6 @@ export const PlaySettingsProvider: React.FC<{ children: React.ReactNode }> = ({ _setPlaySettings(data); }, []); - const setMusicPlaySettings = (item: BaseItemDto, url: string) => { - setPlaySettings({ - item: item, - }); - setPlayUrl(url); - }; - const setPlaySettings = useCallback( async ( dataOrUpdater: @@ -147,7 +139,6 @@ export const PlaySettingsProvider: React.FC<{ children: React.ReactNode }> = ({ setPlaySettings, playUrl, setPlayUrl, - setMusicPlaySettings, setOfflineSettings, playSessionId, mediaSource, diff --git a/utils/collectionTypeToItemType.ts b/utils/collectionTypeToItemType.ts index 64c23ff0..f37fe5f4 100644 --- a/utils/collectionTypeToItemType.ts +++ b/utils/collectionTypeToItemType.ts @@ -10,8 +10,6 @@ import { * readonly Unknown: "unknown"; readonly Movies: "movies"; readonly Tvshows: "tvshows"; - readonly Music: "music"; - readonly Musicvideos: "musicvideos"; readonly Trailers: "trailers"; readonly Homevideos: "homevideos"; readonly Boxsets: "boxsets"; @@ -33,8 +31,6 @@ export const colletionTypeToItemType = ( return BaseItemKind.Series; case CollectionType.Homevideos: return BaseItemKind.Video; - case CollectionType.Musicvideos: - return BaseItemKind.MusicVideo; case CollectionType.Books: return BaseItemKind.Book; case CollectionType.Playlists: