diff --git a/app/(auth)/(tabs)/home/index.tsx b/app/(auth)/(tabs)/home/index.tsx index f79056e2..3dde6087 100644 --- a/app/(auth)/(tabs)/home/index.tsx +++ b/app/(auth)/(tabs)/home/index.tsx @@ -1,21 +1,20 @@ -import { HorizontalScroll } from "@/components/common/HorrizontalScroll"; +import { Button } from "@/components/Button"; import { Text } from "@/components/common/Text"; -import ContinueWatchingPoster from "@/components/ContinueWatchingPoster"; -import { ItemCardText } from "@/components/ItemCardText"; +import { LargeMovieCarousel } from "@/components/home/LargeMovieCarousel"; +import { ScrollingCollectionList } from "@/components/home/ScrollingCollectionList"; +import { MediaListSection } from "@/components/medialists/MediaListSection"; import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; +import { useSettings } from "@/utils/atoms/settings"; +import { Ionicons } from "@expo/vector-icons"; +import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; import { - BaseItemDto, - ItemFields, - ItemFilter, -} from "@jellyfin/sdk/lib/generated-client/models"; -import { - getChannelsApi, getItemsApi, getSuggestionsApi, getTvShowsApi, - getUserApi, getUserLibraryApi, + getUserViewsApi, } from "@jellyfin/sdk/lib/utils/api"; +import NetInfo from "@react-native-community/netinfo"; import { useQuery, useQueryClient } from "@tanstack/react-query"; import { useRouter } from "expo-router"; import { useAtom } from "jotai"; @@ -24,17 +23,8 @@ import { ActivityIndicator, RefreshControl, ScrollView, - TouchableOpacity, View, } from "react-native"; -import NetInfo, { NetInfoState } from "@react-native-community/netinfo"; -import { Button } from "@/components/Button"; -import { Ionicons } from "@expo/vector-icons"; -import MoviePoster from "@/components/MoviePoster"; -import { ScrollingCollectionList } from "@/components/home/ScrollingCollectionList"; -import { useSettings } from "@/utils/atoms/settings"; -import { LargeMovieCarousel } from "@/components/home/LargeMovieCarousel"; -import { MediaListSection } from "@/components/medialists/MediaListSection"; export default function index() { const router = useRouter(); @@ -46,6 +36,24 @@ export default function index() { const [loading, setLoading] = useState(false); const [settings, _] = useSettings(); + const [isConnected, setIsConnected] = useState(null); + + 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); + }); + + return () => { + unsubscribe(); + }; + }, []); + const { data, isLoading, isError } = useQuery({ queryKey: ["resumeItems", user?.Id], queryFn: async () => @@ -79,35 +87,21 @@ export default function index() { return _nextUpData?.filter((i) => !data?.find((d) => d.Id === i.Id)); }, [_nextUpData]); - const { data: collections, isLoading: isLoadingCollections } = useQuery({ - queryKey: ["collections", user?.Id], + const { data: collections } = useQuery({ + queryKey: ["collectinos", user?.Id], queryFn: async () => { if (!api || !user?.Id) { - return []; + return null; } - const data = ( - await getItemsApi(api).getItems({ - userId: user.Id, - }) - ).data; - - const order = ["boxsets", "tvshows", "movies"]; - - const cs = data.Items?.sort((a, b) => { - if ( - order.indexOf(a.CollectionType!) < order.indexOf(b.CollectionType!) - ) { - return 1; - } - - return -1; + const response = await getUserViewsApi(api).getUserViews({ + userId: user.Id, }); - return cs || []; + return response.data.Items || null; }, enabled: !!api && !!user?.Id, - staleTime: 0, + staleTime: 60 * 1000, }); const movieCollectionId = useMemo(() => { @@ -180,33 +174,6 @@ export default function index() { staleTime: 60 * 1000, }); - const refetch = useCallback(async () => { - setLoading(true); - await queryClient.refetchQueries({ queryKey: ["resumeItems", user?.Id] }); - await queryClient.refetchQueries({ queryKey: ["items", user?.Id] }); - await queryClient.refetchQueries({ queryKey: ["suggestions", user?.Id] }); - await queryClient.refetchQueries({ queryKey: ["recentlyAddedInMovies"] }); - setLoading(false); - }, [queryClient, user?.Id]); - - const [isConnected, setIsConnected] = useState(null); - - 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); - }); - - return () => { - unsubscribe(); - }; - }, []); - const { data: mediaListCollections } = useQuery({ queryKey: [ "mediaListCollections-home", @@ -228,7 +195,7 @@ export default function index() { response.data.Items?.filter( (c) => c.Name !== "cf_carousel" && - settings?.mediaListCollectionIds?.includes(c.Id!), + settings?.mediaListCollectionIds?.includes(c.Id!) ) ?? []; return ids; @@ -237,6 +204,19 @@ export default function index() { staleTime: 0, }); + const refetch = useCallback(async () => { + setLoading(true); + await queryClient.refetchQueries({ queryKey: ["resumeItems"] }); + await queryClient.refetchQueries({ queryKey: ["nextUp-all"] }); + await queryClient.refetchQueries({ queryKey: ["recentlyAddedInMovies"] }); + await queryClient.refetchQueries({ queryKey: ["recentlyAddedInTVShows"] }); + await queryClient.refetchQueries({ queryKey: ["suggestions"] }); + await queryClient.refetchQueries({ + queryKey: ["mediaListCollections-home"], + }); + setLoading(false); + }, [queryClient, user?.Id]); + if (isConnected === false) { return ( @@ -318,13 +298,6 @@ export default function index() { loading={isLoadingRecentlyAddedTVShows} /> - - { const searchParams = useLocalSearchParams(); - const navigation = useNavigation(); const { collectionId } = searchParams as { collectionId: string }; const [api] = useAtom(apiAtom); @@ -66,7 +35,8 @@ const page: React.FC = () => { const [selectedGenres, setSelectedGenres] = useAtom(genreFilterAtom); const [selectedYears, setSelectedYears] = useAtom(yearFilterAtom); const [selectedTags, setSelectedTags] = useAtom(tagsFilterAtom); - const [sortBy, setSortBy] = useAtom(sortByAtom); + const [sortBy] = useAtom(sortByAtom); + const [sortOrder] = useAtom(sortOrderAtom); const { data: collection } = useQuery({ queryKey: ["collection", collectionId], @@ -91,21 +61,8 @@ const page: React.FC = () => { }): Promise => { if (!api || !collection) return null; - const sortBy: ItemSortBy[] = []; const includeItemTypes: BaseItemKind[] = []; - switch (collection?.CollectionType) { - case "movies": - sortBy.push("SortName", "ProductionYear"); - break; - case "boxsets": - sortBy.push("IsFolder", "SortName"); - break; - default: - sortBy.push("SortName"); - break; - } - switch (collection?.CollectionType) { case "movies": includeItemTypes.push("Movie"); @@ -128,8 +85,8 @@ const page: React.FC = () => { parentId: collectionId, limit: 50, startIndex: pageParam, - sortBy, - sortOrder: ["Ascending"], + sortBy: [sortBy.key, "SortName", "ProductionYear"], + sortOrder: [sortOrder.key], includeItemTypes, enableImageTypes: ["Primary", "Backdrop", "Banner", "Thumb"], recursive: true, @@ -150,7 +107,9 @@ const page: React.FC = () => { selectedGenres, selectedYears, selectedTags, - ], + sortBy, + sortOrder, + ] ); const { @@ -171,6 +130,7 @@ const page: React.FC = () => { selectedYears, selectedTags, sortBy, + sortOrder, ], queryFn: fetchItems, getNextPageParam: (lastPage, pages) => { @@ -225,67 +185,70 @@ const page: React.FC = () => { estimatedItemSize={200} ListHeaderComponent={ - - - { - if (!api) return null; - const response = await getFilterApi( - api, - ).getQueryFiltersLegacy({ - userId: user?.Id, - includeItemTypes: type ? [type] : [], - parentId: collectionId, - }); - return response.data.Genres || []; - }} - set={setSelectedGenres} - values={selectedGenres} - title="Genres" - /> - { - if (!api) return null; - const response = await getFilterApi( - api, - ).getQueryFiltersLegacy({ - userId: user?.Id, - includeItemTypes: type ? [type] : [], - parentId: collectionId, - }); - return response.data.Tags || []; - }} - set={setSelectedTags} - values={selectedTags} - title="Tags" - /> - { - if (!api) return null; - const response = await getFilterApi( - api, - ).getQueryFiltersLegacy({ - userId: user?.Id, - includeItemTypes: type ? [type] : [], - parentId: collectionId, - }); - return ( - response.data.Years?.sort((a, b) => b - a).map((y) => - y.toString(), - ) || [] - ); - }} - set={setSelectedYears} - values={selectedYears} - title="Years" - /> - + + + + { + if (!api) return null; + const response = await getFilterApi( + api + ).getQueryFiltersLegacy({ + userId: user?.Id, + includeItemTypes: type ? [type] : [], + parentId: collectionId, + }); + return response.data.Genres || []; + }} + set={setSelectedGenres} + values={selectedGenres} + title="Genres" + /> + { + if (!api) return null; + const response = await getFilterApi( + api + ).getQueryFiltersLegacy({ + userId: user?.Id, + includeItemTypes: type ? [type] : [], + parentId: collectionId, + }); + return response.data.Tags || []; + }} + set={setSelectedTags} + values={selectedTags} + title="Tags" + /> + { + if (!api) return null; + const response = await getFilterApi( + api + ).getQueryFiltersLegacy({ + userId: user?.Id, + includeItemTypes: type ? [type] : [], + parentId: collectionId, + }); + return ( + response.data.Years?.sort((a, b) => b - a).map((y) => + y.toString() + ) || [] + ); + }} + set={setSelectedYears} + values={selectedYears} + title="Years" + /> + + + {!type && isFetching && ( { if (!api || !user?.Id) { - return []; + return null; } - const data = ( - await getItemsApi(api).getItems({ - userId: user.Id, - sortBy: ["SortName", "DateCreated"], - }) - ).data; + const response = await getUserViewsApi(api).getUserViews({ + userId: user.Id, + }); - return data.Items || []; + return response.data.Items || null; }, enabled: !!api && !!user?.Id, staleTime: 60 * 1000, @@ -89,7 +86,7 @@ const CollectionCard: React.FC = ({ collection }) => { api, item: collection, }), - [collection], + [collection] ); if (!url) return null; @@ -100,7 +97,7 @@ const CollectionCard: React.FC = ({ collection }) => { router.push(`/library/collections/${collection.Id}`); }} > - + = ({ collection }) => { left: 0, }} /> - {collection.Name} + + {collection.Name} + ); diff --git a/app/(auth)/(tabs)/search/index.tsx b/app/(auth)/(tabs)/search/index.tsx index fd9c4b2c..7465a3a7 100644 --- a/app/(auth)/(tabs)/search/index.tsx +++ b/app/(auth)/(tabs)/search/index.tsx @@ -1,10 +1,13 @@ import { HorizontalScroll } from "@/components/common/HorrizontalScroll"; import { Input } from "@/components/common/Input"; import { Text } from "@/components/common/Text"; +import { TouchableItemRouter } from "@/components/common/TouchableItemRouter"; import ContinueWatchingPoster from "@/components/ContinueWatchingPoster"; import { ItemCardText } from "@/components/ItemCardText"; -import MoviePoster from "@/components/MoviePoster"; -import Poster from "@/components/Poster"; +import AlbumCover from "@/components/posters/AlbumCover"; +import MoviePoster from "@/components/posters/MoviePoster"; +import Poster from "@/components/posters/Poster"; +import SeriesPoster from "@/components/posters/SeriesPoster"; import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; import { getPrimaryImageUrl } from "@/utils/jellyfin/image/getPrimaryImageUrl"; import { getUserItemData } from "@/utils/jellyfin/user-library/getUserItemData"; @@ -14,12 +17,31 @@ import { getSearchApi } from "@jellyfin/sdk/lib/utils/api"; import { useQuery } from "@tanstack/react-query"; import { router, Stack, useNavigation } from "expo-router"; import { useAtom } from "jotai"; -import React, { useLayoutEffect, useState } from "react"; -import { Platform, ScrollView, TouchableOpacity, View } from "react-native"; +import React, { useLayoutEffect, useMemo, useState } from "react"; +import { + ActivityIndicator, + Platform, + ScrollView, + TouchableOpacity, + View, +} from "react-native"; +import _ from "lodash"; +import { useDebounce } from "use-debounce"; + +const exampleSearches = [ + "Lord of the rings", + "Avengers", + "Game of Thrones", + "Breaking Bad", + "Stranger Things", + "The Mandalorian", +]; export default function search() { const [search, setSearch] = useState(""); + const [debouncedSearch] = useDebounce(search, 500); + const [api] = useAtom(apiAtom); const [user] = useAtom(userAtom); @@ -36,13 +58,13 @@ export default function search() { }); }, [navigation]); - const { data: movies } = useQuery({ - queryKey: ["search-movies", search], + const { data: movies, isLoading: l1 } = useQuery({ + queryKey: ["search-movies", debouncedSearch], queryFn: async () => { - if (!api || !user || search.length === 0) return []; + if (!api || !user || debouncedSearch.length === 0) return []; const searchApi = await getSearchApi(api).getSearchHints({ - searchTerm: search, + searchTerm: debouncedSearch, limit: 10, includeItemTypes: ["Movie"], }); @@ -51,13 +73,13 @@ export default function search() { }, }); - const { data: series } = useQuery({ - queryKey: ["search-series", search], + const { data: series, isLoading: l2 } = useQuery({ + queryKey: ["search-series", debouncedSearch], queryFn: async () => { - if (!api || !user || search.length === 0) return []; + if (!api || !user || debouncedSearch.length === 0) return []; const searchApi = await getSearchApi(api).getSearchHints({ - searchTerm: search, + searchTerm: debouncedSearch, limit: 10, includeItemTypes: ["Series"], }); @@ -65,13 +87,14 @@ export default function search() { return searchApi.data.SearchHints; }, }); - const { data: episodes } = useQuery({ - queryKey: ["search-episodes", search], + + const { data: episodes, isLoading: l3 } = useQuery({ + queryKey: ["search-episodes", debouncedSearch], queryFn: async () => { - if (!api || !user || search.length === 0) return []; + if (!api || !user || debouncedSearch.length === 0) return []; const searchApi = await getSearchApi(api).getSearchHints({ - searchTerm: search, + searchTerm: debouncedSearch, limit: 10, includeItemTypes: ["Episode"], }); @@ -80,13 +103,73 @@ export default function search() { }, }); + const { data: artists, isLoading: l4 } = useQuery({ + queryKey: ["search-artists", debouncedSearch], + queryFn: async () => { + if (!api || !user || debouncedSearch.length === 0) return []; + + const searchApi = await getSearchApi(api).getSearchHints({ + searchTerm: debouncedSearch, + limit: 10, + includeItemTypes: ["MusicArtist"], + }); + + return searchApi.data.SearchHints; + }, + }); + + const { data: albums, isLoading: l5 } = useQuery({ + queryKey: ["search-albums", debouncedSearch], + queryFn: async () => { + if (!api || !user || debouncedSearch.length === 0) return []; + + const searchApi = await getSearchApi(api).getSearchHints({ + searchTerm: debouncedSearch, + limit: 10, + includeItemTypes: ["MusicAlbum"], + }); + + return searchApi.data.SearchHints; + }, + }); + + const { data: songs, isLoading: l6 } = useQuery({ + queryKey: ["search-songs", debouncedSearch], + queryFn: async () => { + if (!api || !user || debouncedSearch.length === 0) return []; + + const searchApi = await getSearchApi(api).getSearchHints({ + searchTerm: debouncedSearch, + limit: 10, + includeItemTypes: ["Audio"], + }); + + return searchApi.data.SearchHints; + }, + }); + + const noResults = useMemo(() => { + return !( + artists?.length || + albums?.length || + songs?.length || + movies?.length || + episodes?.length || + series?.length + ); + }, [artists, episodes, albums, songs, movies, series]); + + const loading = useMemo(() => { + return l1 || l2 || l3 || l4 || l5 || l6; + }, [l1, l2, l3, l4, l5, l6]); + return ( <> - + {Platform.OS === "android" && ( )} - Movies m.Id!)} renderItem={(data) => ( @@ -112,7 +195,9 @@ export default function search() { onPress={() => router.push(`/items/${item.Id}`)} > - {item.Name} + + {item.Name} + {item.ProductionYear} @@ -121,9 +206,9 @@ export default function search() { /> )} /> - Series m.Id!)} + header="Series" renderItem={(data) => ( data={data} @@ -133,12 +218,10 @@ export default function search() { onPress={() => router.push(`/series/${item.Id}`)} className="flex flex-col w-32" > - - {item.Name} + + + {item.Name} + {item.ProductionYear} @@ -147,9 +230,9 @@ export default function search() { /> )} /> - Episodes m.Id!)} + header="Episodes" renderItem={(data) => ( data={data} @@ -166,6 +249,89 @@ export default function search() { /> )} /> + m.Id!)} + header="Artists" + renderItem={(data) => ( + + data={data} + renderItem={(item) => ( + + + + + )} + /> + )} + /> + m.Id!)} + header="Albums" + renderItem={(data) => ( + + data={data} + renderItem={(item) => ( + + + + + )} + /> + )} + /> + m.Id!)} + header="Songs" + renderItem={(data) => ( + + data={data} + renderItem={(item) => ( + + + + + )} + /> + )} + /> + {loading ? ( + + + + ) : noResults && debouncedSearch.length > 0 ? ( + + + No results found for + + + "{debouncedSearch}" + + + ) : debouncedSearch.length === 0 ? ( + + {exampleSearches.map((e) => ( + setSearch(e)} + key={e} + className="mb-2" + > + {e} + + ))} + + ) : null} @@ -175,9 +341,10 @@ export default function search() { type Props = { ids?: string[] | null; renderItem: (data: BaseItemDto[]) => React.ReactNode; + header?: string; }; -const SearchItemWrapper: React.FC = ({ ids, renderItem }) => { +const SearchItemWrapper: React.FC = ({ ids, renderItem, header }) => { const [api] = useAtom(apiAtom); const [user] = useAtom(userAtom); @@ -193,21 +360,26 @@ const SearchItemWrapper: React.FC = ({ ids, renderItem }) => { 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, }); - if (!data) return No results; + if (!data) return null; - return renderItem(data); + return ( + <> + {header} + {renderItem(data)} + + ); }; diff --git a/app/(auth)/albums/[albumId].tsx b/app/(auth)/albums/[albumId].tsx index 57faba7d..0a4a991d 100644 --- a/app/(auth)/albums/[albumId].tsx +++ b/app/(auth)/albums/[albumId].tsx @@ -1,33 +1,15 @@ -import ArtistPoster from "@/components/ArtistPoster"; import { Chromecast } from "@/components/Chromecast"; import { Text } from "@/components/common/Text"; -import { Loading } from "@/components/Loading"; -import MoviePoster from "@/components/MoviePoster"; import { SongsList } from "@/components/music/SongsList"; +import ArtistPoster from "@/components/posters/ArtistPoster"; import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; -import { getUserItemData } from "@/utils/jellyfin/user-library/getUserItemData"; -import { Ionicons } from "@expo/vector-icons"; -import { - BaseItemDto, - BaseItemKind, - ItemSortBy, -} from "@jellyfin/sdk/lib/generated-client/models"; -import { - getArtistsApi, - getItemsApi, - getUserApi, - getUserLibraryApi, -} from "@jellyfin/sdk/lib/utils/api"; +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, useMemo, useState } from "react"; -import { - ActivityIndicator, - ScrollView, - TouchableOpacity, - View, -} from "react-native"; +import { useEffect, useState } from "react"; +import { ScrollView, TouchableOpacity, View } from "react-native"; export default function page() { const searchParams = useLocalSearchParams(); @@ -40,8 +22,6 @@ export default function page() { const [api] = useAtom(apiAtom); const [user] = useAtom(userAtom); - const [startIndex, setStartIndex] = useState(0); - const navigation = useNavigation(); useEffect(() => { @@ -119,6 +99,21 @@ export default function page() { {album?.Name} {album?.ProductionYear} + + + {album.AlbumArtists?.map((a) => ( + { + router.push(`/artists/${a.Id}/page`); + }} + > + + {album?.AlbumArtist} + + + ))} + { const local = useLocalSearchParams(); @@ -173,7 +172,7 @@ const page: React.FC = () => { } } }, - [playbackUrl, item, settings], + [playbackUrl, item, settings] ); const backdropUrl = useMemo( @@ -184,12 +183,12 @@ const page: React.FC = () => { quality: 90, width: 1000, }), - [item], + [item] ); const logoUrl = useMemo( () => (item?.Type === "Movie" ? getLogoImageUrlById({ api, item }) : null), - [item], + [item] ); if (l1) @@ -244,7 +243,7 @@ const page: React.FC = () => { - + {playbackUrl ? ( ) : ( @@ -283,29 +282,6 @@ const page: React.FC = () => { - - - - Video - Audio - Subtitles - - - - {item.MediaStreams?.find((i) => i.Type === "Video")?.DisplayTitle} - - - {item.MediaStreams?.find((i) => i.Type === "Audio")?.DisplayTitle} - - - { - item.MediaStreams?.find((i) => i.Type === "Subtitle") - ?.DisplayTitle - } - - - - diff --git a/app/_layout.tsx b/app/_layout.tsx index 785d1fe2..0161ca59 100644 --- a/app/_layout.tsx +++ b/app/_layout.tsx @@ -62,7 +62,7 @@ function Layout() { retryOnMount: true, }, }, - }), + }) ); useEffect(() => { @@ -70,7 +70,7 @@ function Layout() { ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.DEFAULT); else ScreenOrientation.lockAsync( - ScreenOrientation.OrientationLock.PORTRAIT_UP, + ScreenOrientation.OrientationLock.PORTRAIT_UP ); }, [settings]); diff --git a/bun.lockb b/bun.lockb index e500cc50..5ec4bf6c 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/components/MoviePoster.tsx b/components/MoviePoster.tsx deleted file mode 100644 index c1e7a10d..00000000 --- a/components/MoviePoster.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import { apiAtom } from "@/providers/JellyfinProvider"; -import { 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 "./WatchedIndicator"; -import { getPrimaryImageUrl } from "@/utils/jellyfin/image/getPrimaryImageUrl"; - -type MoviePosterProps = { - item: BaseItemDto; - showProgress?: boolean; -}; - -const MoviePoster: React.FC = ({ - item, - showProgress = false, -}) => { - const [api] = useAtom(apiAtom); - - const url = useMemo( - () => - getPrimaryImageUrl({ - api, - item, - }), - [item], - ); - - const [progress, setProgress] = useState( - item.UserData?.PlayedPercentage || 0, - ); - - if (!url) - return ( - - ); - - return ( - - - - {showProgress && progress > 0 && ( - - )} - - ); -}; - -export default MoviePoster; diff --git a/components/PlayedStatus.tsx b/components/PlayedStatus.tsx index 87a1816f..1cb65034 100644 --- a/components/PlayedStatus.tsx +++ b/components/PlayedStatus.tsx @@ -6,7 +6,7 @@ import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; import { useQueryClient } from "@tanstack/react-query"; import * as Haptics from "expo-haptics"; import { useAtom } from "jotai"; -import React, { useCallback } from "react"; +import React from "react"; import { TouchableOpacity, View } from "react-native"; export const PlayedStatus: React.FC<{ item: BaseItemDto }> = ({ item }) => { @@ -15,15 +15,15 @@ export const PlayedStatus: React.FC<{ item: BaseItemDto }> = ({ item }) => { const queryClient = useQueryClient(); - const invalidateQueries = useCallback(() => { + const invalidateQueries = () => { queryClient.invalidateQueries({ - queryKey: ["item", item.Id], + queryKey: ["item"], }); queryClient.invalidateQueries({ - queryKey: ["resumeItems", user?.Id], + queryKey: ["resumeItems"], }); queryClient.invalidateQueries({ - queryKey: ["nextUp", item.SeriesId], + queryKey: ["nextUp"], }); queryClient.invalidateQueries({ queryKey: ["episodes"], @@ -31,7 +31,10 @@ export const PlayedStatus: React.FC<{ item: BaseItemDto }> = ({ item }) => { queryClient.invalidateQueries({ queryKey: ["seasons"], }); - }, [api, item.Id, queryClient, user?.Id]); + queryClient.invalidateQueries({ + queryKey: ["nextUp-all"], + }); + }; return ( diff --git a/components/SimilarItems.tsx b/components/SimilarItems.tsx index 5da622b3..2879c05d 100644 --- a/components/SimilarItems.tsx +++ b/components/SimilarItems.tsx @@ -1,20 +1,19 @@ +import MoviePoster from "@/components/posters/MoviePoster"; import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; import { 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 { ActivityIndicator, ScrollView, TouchableOpacity, View, } from "react-native"; -import ContinueWatchingPoster from "./ContinueWatchingPoster"; import { ItemCardText } from "./ItemCardText"; import { Text } from "./common/Text"; -import MoviePoster from "./MoviePoster"; -import { useMemo } from "react"; type SimilarItemsProps = { itemId: string; @@ -42,7 +41,7 @@ export const SimilarItems: React.FC = ({ itemId }) => { const movies = useMemo( () => similarItems?.filter((i) => i.Type === "Movie") || [], - [similarItems], + [similarItems] ); return ( diff --git a/components/common/TouchableItemRouter.tsx b/components/common/TouchableItemRouter.tsx index 9bd6a546..9bb83768 100644 --- a/components/common/TouchableItemRouter.tsx +++ b/components/common/TouchableItemRouter.tsx @@ -8,6 +8,7 @@ import { Text } from "@/components/common/Text"; import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; import { PropsWithChildren } from "react"; import { useRouter } from "expo-router"; +import * as Haptics from "expo-haptics"; interface Props extends TouchableOpacityProps { item: BaseItemDto; @@ -22,11 +23,18 @@ export const TouchableItemRouter: React.FC> = ({ return ( { + Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); + if (item.Type === "Series") router.push(`/series/${item.Id}`); if (item.Type === "Episode") router.push(`/items/${item.Id}`); if (item.Type === "MusicAlbum") router.push(`/albums/${item.Id}`); - if (item.Type === "Movie") router.push(`/songs/${item.Id}`); + if (item.Type === "Audio") router.push(`/albums/${item.AlbumId}`); + if (item.Type === "MusicArtist") + router.push(`/artists/${item.Id}/page`); + + // Movies and all other cases if (item.Type === "BoxSet") router.push(`/collections/${item.Id}`); + router.push(`/items/${item.Id}`); }} {...props} > diff --git a/components/filters/FilterButton.tsx b/components/filters/FilterButton.tsx index 7cb65c9a..72c66b8a 100644 --- a/components/filters/FilterButton.tsx +++ b/components/filters/FilterButton.tsx @@ -33,6 +33,8 @@ export const FilterButton: React.FC = ({ staleTime: 0, }); + if (filters?.length === 0) return null; + return ( diff --git a/components/home/LargeMovieCarousel.tsx b/components/home/LargeMovieCarousel.tsx index b66105e2..1ca37a44 100644 --- a/components/home/LargeMovieCarousel.tsx +++ b/components/home/LargeMovieCarousel.tsx @@ -1,4 +1,4 @@ -import { View, ViewProps } from "react-native"; +import { ActivityIndicator, View, ViewProps } from "react-native"; import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; import { getItemsApi } from "@jellyfin/sdk/lib/utils/api"; import { useQuery, useQueryClient } from "@tanstack/react-query"; @@ -42,7 +42,7 @@ export const LargeMovieCarousel: React.FC = ({ ...props }) => { }); }; - const { data: mediaListCollection } = useQuery({ + const { data: mediaListCollection, isLoading: l1 } = useQuery({ queryKey: ["mediaListCollection", user?.Id], queryFn: async () => { if (!api || !user?.Id) return null; @@ -62,9 +62,7 @@ export const LargeMovieCarousel: React.FC = ({ ...props }) => { staleTime: 0, }); - const { data: popularItems, isLoading: isLoadingPopular } = useQuery< - BaseItemDto[] - >({ + const { data: popularItems, isLoading: l2 } = useQuery({ queryKey: ["popular", user?.Id], queryFn: async () => { if (!api || !user?.Id || !mediaListCollection) return []; @@ -83,6 +81,13 @@ export const LargeMovieCarousel: React.FC = ({ ...props }) => { const width = Dimensions.get("screen").width; + if (l1 || l2) + return ( + + + + ); + if (!popularItems) return null; return ( diff --git a/components/home/ScrollingCollectionList.tsx b/components/home/ScrollingCollectionList.tsx index 9ee4be30..d4164f7d 100644 --- a/components/home/ScrollingCollectionList.tsx +++ b/components/home/ScrollingCollectionList.tsx @@ -1,11 +1,11 @@ -import { TouchableOpacity, View, ViewProps } from "react-native"; import { Text } from "@/components/common/Text"; +import MoviePoster from "@/components/posters/MoviePoster"; import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; -import { router } from "expo-router"; +import { View, ViewProps } from "react-native"; import ContinueWatchingPoster from "../ContinueWatchingPoster"; import { ItemCardText } from "../ItemCardText"; import { HorizontalScroll } from "../common/HorrizontalScroll"; -import MoviePoster from "../MoviePoster"; +import { TouchableItemRouter } from "../common/TouchableItemRouter"; interface Props extends ViewProps { title: string; @@ -29,22 +29,17 @@ export const ScrollingCollectionList: React.FC = ({ return ( - {title} + + {title} + data={data} height={orientation === "vertical" ? 247 : 164} loading={loading} renderItem={(item, index) => ( - { - if (item.Type === "Series") router.push(`/series/${item.Id}`); - else if (item.CollectionType === "music") - router.push(`/artists/page?collectionId=${item.Id}`); - else if (item.Type === "CollectionFolder") - router.push(`/collections/${item.Id}`); - else router.push(`/items/${item.Id}`); - }} + item={item} className={`flex flex-col ${orientation === "vertical" ? "w-32" : "w-48"} `} @@ -57,7 +52,7 @@ export const ScrollingCollectionList: React.FC = ({ )} - + )} /> diff --git a/components/medialists/MediaListSection.tsx b/components/medialists/MediaListSection.tsx index 35ce7b94..6b449eec 100644 --- a/components/medialists/MediaListSection.tsx +++ b/components/medialists/MediaListSection.tsx @@ -28,7 +28,7 @@ export const MediaListSection: React.FC = ({ collection, ...props }) => { const { data: popularItems, isLoading: isLoadingPopular } = useQuery< BaseItemDto[] >({ - queryKey: ["popular", user?.Id], + queryKey: [collection.Id, user?.Id], queryFn: async () => { if (!api || !user?.Id || !collection.Id) return []; diff --git a/components/posters/AlbumCover.tsx b/components/posters/AlbumCover.tsx new file mode 100644 index 00000000..c1c376e1 --- /dev/null +++ b/components/posters/AlbumCover.tsx @@ -0,0 +1,84 @@ +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, + }); + console.log("Image A", u); + return u; + }, [item]); + + const url2 = useMemo(() => { + const u = getPrimaryImageUrlById({ + api, + id, + quality: 85, + width: 300, + }); + console.log("Image B", u); + return u; + }, [item]); + + if (!item && id) + return ( + + + + ); + + if (item) + return ( + + + + ); +}; + +export default AlbumCover; diff --git a/components/ArtistPoster.tsx b/components/posters/ArtistPoster.tsx similarity index 91% rename from components/ArtistPoster.tsx rename to components/posters/ArtistPoster.tsx index b0f32c49..2b9f2899 100644 --- a/components/ArtistPoster.tsx +++ b/components/posters/ArtistPoster.tsx @@ -1,11 +1,10 @@ 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, useState } from "react"; +import { useMemo } from "react"; import { View } from "react-native"; -import { WatchedIndicator } from "./WatchedIndicator"; -import { getPrimaryImageUrl } from "@/utils/jellyfin/image/getPrimaryImageUrl"; type ArtistPosterProps = { item: BaseItemDto; @@ -24,7 +23,7 @@ const ArtistPoster: React.FC = ({ api, item, }), - [item], + [item] ); if (!url) diff --git a/components/ParentPoster.tsx b/components/posters/ParentPoster.tsx similarity index 95% rename from components/ParentPoster.tsx rename to components/posters/ParentPoster.tsx index db4c9ec3..04e2c72b 100644 --- a/components/ParentPoster.tsx +++ b/components/posters/ParentPoster.tsx @@ -1,5 +1,4 @@ import { apiAtom } from "@/providers/JellyfinProvider"; -import { useQuery } from "@tanstack/react-query"; import { Image } from "expo-image"; import { useAtom } from "jotai"; import { useMemo } from "react"; diff --git a/components/Poster.tsx b/components/posters/Poster.tsx similarity index 68% rename from components/Poster.tsx rename to components/posters/Poster.tsx index d5b17e27..795c9df2 100644 --- a/components/Poster.tsx +++ b/components/posters/Poster.tsx @@ -9,10 +9,11 @@ type PosterProps = { item?: BaseItemDto | BaseItemPerson | null; url?: string | null; showProgress?: boolean; + blurhash?: string | null; }; -const Poster: React.FC = ({ item, url }) => { - if (!url || !item) +const Poster: React.FC = ({ item, url, blurhash }) => { + if (!item) return ( = ({ item, url }) => { return ( ([]); export const tagsFilterAtom = atom([]); export const yearFilterAtom = atom([]); -export const sortByAtom = atom("title"); +export const sortByAtom = atom<(typeof sortOptions)[number]>(sortOptions[0]); +export const sortOrderAtom = atom<(typeof sortOrderOptions)[number]>( + sortOrderOptions[0], +);