wip: refactor

This commit is contained in:
Fredrik Burmester
2024-08-17 19:11:42 +02:00
parent ba6c2d5409
commit 30781a6dfe
28 changed files with 586 additions and 439 deletions

View File

@@ -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<boolean | null>(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<BaseItemDto[]>({
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<boolean | null>(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 (
<View className="flex flex-col items-center justify-center h-full -mt-6 px-8">
@@ -318,13 +298,6 @@ export default function index() {
loading={isLoadingRecentlyAddedTVShows}
/>
<ScrollingCollectionList
title="Collections"
data={collections}
loading={isLoadingCollections}
orientation="horizontal"
/>
<ScrollingCollectionList
title="Suggestions"
data={suggestions}

View File

@@ -1,63 +1,32 @@
import * as DropdownMenu from "zeego/dropdown-menu";
import ArtistPoster from "@/components/ArtistPoster";
import { ColumnItem } from "@/components/common/ColumnItem";
import { Text } from "@/components/common/Text";
import { TouchableItemRouter } from "@/components/common/TouchableItemRouter";
import { FilterButton } from "@/components/filters/FilterButton";
import { ResetFiltersButton } from "@/components/filters/ResetFiltersButton";
import { SortButton } from "@/components/filters/SortButton";
import { ItemCardText } from "@/components/ItemCardText";
import { Loading } from "@/components/Loading";
import MoviePoster from "@/components/MoviePoster";
import MoviePoster from "@/components/posters/MoviePoster";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { Ionicons } from "@expo/vector-icons";
import {
BaseItemDto,
genreFilterAtom,
sortByAtom,
sortOrderAtom,
tagsFilterAtom,
yearFilterAtom,
} from "@/utils/atoms/filters";
import {
BaseItemDtoQueryResult,
BaseItemKind,
ItemSortBy,
NameGuidPair,
} from "@jellyfin/sdk/lib/generated-client/models";
import { getFilterApi, getItemsApi } from "@jellyfin/sdk/lib/utils/api";
import { FlashList } from "@shopify/flash-list";
import {
QueryFilters,
useInfiniteQuery,
useQuery,
useQueryClient,
} from "@tanstack/react-query";
import {
Stack,
router,
useLocalSearchParams,
useNavigation,
} from "expo-router";
import { useInfiniteQuery, useQuery } from "@tanstack/react-query";
import { useLocalSearchParams } from "expo-router";
import { useAtom } from "jotai";
import React, {
SetStateAction,
useCallback,
useEffect,
useMemo,
useState,
} from "react";
import {
ActivityIndicator,
Platform,
RefreshControl,
ScrollView,
TouchableOpacity,
View,
ViewProps,
} from "react-native";
import {
genreFilterAtom,
yearFilterAtom,
sortByAtom,
tagsFilterAtom,
} from "@/utils/atoms/filters";
import { ResetFiltersButton } from "@/components/filters/ResetFiltersButton";
import { FilterButton } from "@/components/filters/FilterButton";
import { TouchableItemRouter } from "@/components/common/TouchableItemRouter";
import React, { useCallback, useMemo } from "react";
import { ActivityIndicator, ScrollView, View } from "react-native";
const page: React.FC = () => {
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<BaseItemDtoQueryResult | null> => {
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={
<View className="mb-4">
<View className="flex flex-row space-x-1">
<ResetFiltersButton />
<FilterButton
collectionId={collectionId}
queryKey="genreFilter"
queryFn={async () => {
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"
/>
<FilterButton
collectionId={collectionId}
queryKey="tagsFilter"
queryFn={async () => {
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"
/>
<FilterButton
collectionId={collectionId}
queryKey="yearFilter"
queryFn={async () => {
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"
/>
</View>
<ScrollView horizontal>
<View className="flex flex-row space-x-1">
<ResetFiltersButton />
<FilterButton
collectionId={collectionId}
queryKey="genreFilter"
queryFn={async () => {
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"
/>
<FilterButton
collectionId={collectionId}
queryKey="tagsFilter"
queryFn={async () => {
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"
/>
<FilterButton
collectionId={collectionId}
queryKey="yearFilter"
queryFn={async () => {
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"
/>
<SortButton title="Sort by" />
</View>
</ScrollView>
{!type && isFetching && (
<ActivityIndicator
style={{

View File

@@ -1,6 +1,6 @@
import { Text } from "@/components/common/Text";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { getItemsApi } from "@jellyfin/sdk/lib/utils/api";
import { getItemsApi, getUserViewsApi } from "@jellyfin/sdk/lib/utils/api";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { useRouter } from "expo-router";
import { useAtom } from "jotai";
@@ -31,20 +31,17 @@ export default function index() {
const [settings, _] = useSettings();
const { data, isLoading: isLoading } = useQuery({
queryKey: ["collections", user?.Id],
queryKey: ["user-views", user?.Id],
queryFn: async () => {
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<Props> = ({ collection }) => {
api,
item: collection,
}),
[collection],
[collection]
);
if (!url) return null;
@@ -100,7 +97,7 @@ const CollectionCard: React.FC<Props> = ({ collection }) => {
router.push(`/library/collections/${collection.Id}`);
}}
>
<View className="flex items-center justify-center rounded-xl w-full aspect-video relative border border-neutral-900">
<View className="flex justify-center rounded-xl w-full relative border border-neutral-900 h-20 ">
<Image
source={{ uri: url }}
style={{
@@ -112,7 +109,9 @@ const CollectionCard: React.FC<Props> = ({ collection }) => {
left: 0,
}}
/>
<Text className="font-bold text-2xl">{collection.Name}</Text>
<Text className="font-bold text-xl text-start px-4">
{collection.Name}
</Text>
</View>
</TouchableOpacity>
);

View File

@@ -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<string>("");
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 (
<>
<ScrollView
keyboardDismissMode="on-drag"
contentInsetAdjustmentBehavior="automatic"
>
<View className="flex flex-col pt-2 pb-20">
<View className="flex flex-col pt-4 pb-32">
{Platform.OS === "android" && (
<View className="mb-4 px-4">
<Input
@@ -99,8 +182,8 @@ export default function search() {
/>
</View>
)}
<Text className="font-bold text-2xl px-4 mb-2">Movies</Text>
<SearchItemWrapper
header="Movies"
ids={movies?.map((m) => m.Id!)}
renderItem={(data) => (
<HorizontalScroll<BaseItemDto>
@@ -112,7 +195,9 @@ export default function search() {
onPress={() => router.push(`/items/${item.Id}`)}
>
<MoviePoster item={item} key={item.Id} />
<Text className="mt-2">{item.Name}</Text>
<Text numberOfLines={2} className="mt-2">
{item.Name}
</Text>
<Text className="opacity-50 text-xs">
{item.ProductionYear}
</Text>
@@ -121,9 +206,9 @@ export default function search() {
/>
)}
/>
<Text className="font-bold text-2xl px-4 my-2">Series</Text>
<SearchItemWrapper
ids={series?.map((m) => m.Id!)}
header="Series"
renderItem={(data) => (
<HorizontalScroll<BaseItemDto>
data={data}
@@ -133,12 +218,10 @@ export default function search() {
onPress={() => router.push(`/series/${item.Id}`)}
className="flex flex-col w-32"
>
<Poster
item={item}
key={item.Id}
url={getPrimaryImageUrl({ api, item })}
/>
<Text className="mt-2">{item.Name}</Text>
<SeriesPoster item={item} key={item.Id} />
<Text numberOfLines={2} className="mt-2">
{item.Name}
</Text>
<Text className="opacity-50 text-xs">
{item.ProductionYear}
</Text>
@@ -147,9 +230,9 @@ export default function search() {
/>
)}
/>
<Text className="font-bold text-2xl px-4 my-2">Episodes</Text>
<SearchItemWrapper
ids={episodes?.map((m) => m.Id!)}
header="Episodes"
renderItem={(data) => (
<HorizontalScroll<BaseItemDto>
data={data}
@@ -166,6 +249,89 @@ export default function search() {
/>
)}
/>
<SearchItemWrapper
ids={artists?.map((m) => m.Id!)}
header="Artists"
renderItem={(data) => (
<HorizontalScroll<BaseItemDto>
data={data}
renderItem={(item) => (
<TouchableItemRouter
item={item}
key={item.Id}
className="flex flex-col w-32"
>
<AlbumCover id={item.Id} />
<ItemCardText item={item} />
</TouchableItemRouter>
)}
/>
)}
/>
<SearchItemWrapper
ids={albums?.map((m) => m.Id!)}
header="Albums"
renderItem={(data) => (
<HorizontalScroll<BaseItemDto>
data={data}
renderItem={(item) => (
<TouchableItemRouter
item={item}
key={item.Id}
className="flex flex-col w-32"
>
<AlbumCover id={item.Id} />
<ItemCardText item={item} />
</TouchableItemRouter>
)}
/>
)}
/>
<SearchItemWrapper
ids={songs?.map((m) => m.Id!)}
header="Songs"
renderItem={(data) => (
<HorizontalScroll<BaseItemDto>
data={data}
renderItem={(item) => (
<TouchableItemRouter
item={item}
key={item.Id}
className="flex flex-col w-32"
>
<AlbumCover id={item.AlbumId} />
<ItemCardText item={item} />
</TouchableItemRouter>
)}
/>
)}
/>
{loading ? (
<View className="mt-4 flex justify-center items-center">
<ActivityIndicator size="small" color="white" />
</View>
) : noResults && debouncedSearch.length > 0 ? (
<View>
<Text className="text-center text-lg font-bold mt-4">
No results found for
</Text>
<Text className="text-xs text-purple-600 text-center">
"{debouncedSearch}"
</Text>
</View>
) : debouncedSearch.length === 0 ? (
<View className="mt-4 flex flex-col items-center space-y-2">
{exampleSearches.map((e) => (
<TouchableOpacity
onPress={() => setSearch(e)}
key={e}
className="mb-2"
>
<Text className="text-purple-600">{e}</Text>
</TouchableOpacity>
))}
</View>
) : null}
</View>
</ScrollView>
</>
@@ -175,9 +341,10 @@ export default function search() {
type Props = {
ids?: string[] | null;
renderItem: (data: BaseItemDto[]) => React.ReactNode;
header?: string;
};
const SearchItemWrapper: React.FC<Props> = ({ ids, renderItem }) => {
const SearchItemWrapper: React.FC<Props> = ({ ids, renderItem, header }) => {
const [api] = useAtom(apiAtom);
const [user] = useAtom(userAtom);
@@ -193,21 +360,26 @@ const SearchItemWrapper: React.FC<Props> = ({ 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 <Text className="opacity-50 text-xs px-4">No results</Text>;
if (!data) return null;
return renderItem(data);
return (
<>
<Text className="font-bold text-2xl px-4 my-2">{header}</Text>
{renderItem(data)}
</>
);
};

View File

@@ -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<number>(0);
const navigation = useNavigation();
useEffect(() => {
@@ -119,6 +99,21 @@ export default function page() {
<View className="flex flex-col shrink">
<Text className="font-bold text-3xl">{album?.Name}</Text>
<Text className="">{album?.ProductionYear}</Text>
<View className="flex flex-row space-x-2 mt-1">
{album.AlbumArtists?.map((a) => (
<TouchableOpacity
key={a.Id}
onPress={() => {
router.push(`/artists/${a.Id}/page`);
}}
>
<Text className="font-bold text-purple-600">
{album?.AlbumArtist}
</Text>
</TouchableOpacity>
))}
</View>
</View>
</View>
<SongsList

View File

@@ -1,4 +1,4 @@
import ArtistPoster from "@/components/ArtistPoster";
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";

View File

@@ -1,26 +1,14 @@
import ArtistPoster from "@/components/ArtistPoster";
import { Text } from "@/components/common/Text";
import { Loading } from "@/components/Loading";
import MoviePoster from "@/components/MoviePoster";
import ArtistPoster from "@/components/posters/ArtistPoster";
import MoviePoster from "@/components/posters/MoviePoster";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { Ionicons } from "@expo/vector-icons";
import {
BaseItemDto,
BaseItemKind,
ItemSortBy,
} from "@jellyfin/sdk/lib/generated-client/models";
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 { useEffect, useMemo, useState } from "react";
import {
ActivityIndicator,
FlatList,
ScrollView,
TouchableOpacity,
View,
} from "react-native";
import { useMemo, useState } from "react";
import { FlatList, TouchableOpacity, View } from "react-native";
export default function page() {
const searchParams = useLocalSearchParams();

View File

@@ -1,7 +1,6 @@
import ArtistPoster from "@/components/ArtistPoster";
import { Text } from "@/components/common/Text";
import { Loading } from "@/components/Loading";
import MoviePoster from "@/components/MoviePoster";
import ArtistPoster from "@/components/posters/ArtistPoster";
import MoviePoster from "@/components/posters/MoviePoster";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { Ionicons } from "@expo/vector-icons";
import {
@@ -13,7 +12,7 @@ import { getItemsApi } from "@jellyfin/sdk/lib/utils/api";
import { useQuery } from "@tanstack/react-query";
import { router, useLocalSearchParams } from "expo-router";
import { useAtom } from "jotai";
import { useEffect, useMemo, useState } from "react";
import { useMemo, useState } from "react";
import {
ActivityIndicator,
ScrollView,

View File

@@ -19,12 +19,15 @@ import { CurrentSeries } from "@/components/series/CurrentSeries";
import { NextEpisodeButton } from "@/components/series/NextEpisodeButton";
import { SeriesTitleHeader } from "@/components/series/SeriesTitleHeader";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { useSettings } from "@/utils/atoms/settings";
import { getBackdropUrl } from "@/utils/jellyfin/image/getBackdropUrl";
import { getLogoImageUrlById } from "@/utils/jellyfin/image/getLogoImageUrlById";
import { getStreamUrl } from "@/utils/jellyfin/media/getStreamUrl";
import { getUserItemData } from "@/utils/jellyfin/user-library/getUserItemData";
import { chromecastProfile } from "@/utils/profiles/chromecast";
import ios12 from "@/utils/profiles/ios12";
import ios from "@/utils/profiles/ios";
import native from "@/utils/profiles/native";
import old from "@/utils/profiles/old";
import { getMediaInfoApi } from "@jellyfin/sdk/lib/utils/api";
import { useQuery } from "@tanstack/react-query";
import { Image } from "expo-image";
@@ -38,10 +41,6 @@ import CastContext, {
useRemoteMediaClient,
} from "react-native-google-cast";
import { ParallaxScrollView } from "../../../components/ParallaxPage";
import { useSettings } from "@/utils/atoms/settings";
import ios from "@/utils/profiles/ios";
import native from "@/utils/profiles/native";
import old from "@/utils/profiles/old";
const page: React.FC = () => {
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 = () => {
<Ratings item={item} />
</View>
<View className="flex flex-row justify-between items-center w-full my-4">
<View className="flex flex-row justify-between items-center mb-2">
{playbackUrl ? (
<DownloadItem item={item} playbackUrl={playbackUrl} />
) : (
@@ -283,29 +282,6 @@ const page: React.FC = () => {
<NextEpisodeButton item={item} className="ml-2" />
</View>
</View>
<ScrollView horizontal className="flex px-4 mb-4">
<View className="flex flex-row space-x-2 ">
<View className="flex flex-col">
<Text className="text-sm opacity-70">Video</Text>
<Text className="text-sm opacity-70">Audio</Text>
<Text className="text-sm opacity-70">Subtitles</Text>
</View>
<View className="flex flex-col">
<Text className="text-sm opacity-70">
{item.MediaStreams?.find((i) => i.Type === "Video")?.DisplayTitle}
</Text>
<Text className="text-sm opacity-70">
{item.MediaStreams?.find((i) => i.Type === "Audio")?.DisplayTitle}
</Text>
<Text className="text-sm opacity-70">
{
item.MediaStreams?.find((i) => i.Type === "Subtitle")
?.DisplayTitle
}
</Text>
</View>
</View>
</ScrollView>
<CastAndCrew item={item} />

View File

@@ -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]);