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

BIN
bun.lockb

Binary file not shown.

View File

@@ -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<MoviePosterProps> = ({
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 (
<View
className="rounded-md overflow-hidden border border-neutral-900"
style={{
aspectRatio: "10/15",
}}
></View>
);
return (
<View className="relative rounded-md overflow-hidden border border-neutral-900">
<Image
key={item.Id}
id={item.Id}
source={{
uri: url,
}}
cachePolicy={"memory-disk"}
contentFit="cover"
style={{
aspectRatio: "10/15",
width: "100%",
}}
/>
<WatchedIndicator item={item} />
{showProgress && progress > 0 && (
<View className="h-1 bg-red-600 w-full"></View>
)}
</View>
);
};
export default MoviePoster;

View File

@@ -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 (
<View>

View File

@@ -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<SimilarItemsProps> = ({ itemId }) => {
const movies = useMemo(
() => similarItems?.filter((i) => i.Type === "Movie") || [],
[similarItems],
[similarItems]
);
return (

View File

@@ -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<PropsWithChildren<Props>> = ({
return (
<TouchableOpacity
onPress={() => {
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}
>

View File

@@ -33,6 +33,8 @@ export const FilterButton: React.FC<Props> = ({
staleTime: 0,
});
if (filters?.length === 0) return null;
return (
<DropdownMenu.Root>
<DropdownMenu.Trigger>

View File

@@ -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> = ({ ...props }) => {
});
};
const { data: mediaListCollection } = useQuery<string | null>({
const { data: mediaListCollection, isLoading: l1 } = useQuery<string | null>({
queryKey: ["mediaListCollection", user?.Id],
queryFn: async () => {
if (!api || !user?.Id) return null;
@@ -62,9 +62,7 @@ export const LargeMovieCarousel: React.FC<Props> = ({ ...props }) => {
staleTime: 0,
});
const { data: popularItems, isLoading: isLoadingPopular } = useQuery<
BaseItemDto[]
>({
const { data: popularItems, isLoading: l2 } = useQuery<BaseItemDto[]>({
queryKey: ["popular", user?.Id],
queryFn: async () => {
if (!api || !user?.Id || !mediaListCollection) return [];
@@ -83,6 +81,13 @@ export const LargeMovieCarousel: React.FC<Props> = ({ ...props }) => {
const width = Dimensions.get("screen").width;
if (l1 || l2)
return (
<View className="h-[242px] flex items-center justify-center">
<ActivityIndicator size={"small"} color="#fff" />
</View>
);
if (!popularItems) return null;
return (

View File

@@ -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<Props> = ({
return (
<View {...props}>
<Text className="px-4 text-2xl font-bold mb-2">{title}</Text>
<Text className="px-4 text-2xl font-bold mb-2 text-neutral-100">
{title}
</Text>
<HorizontalScroll<BaseItemDto>
data={data}
height={orientation === "vertical" ? 247 : 164}
loading={loading}
renderItem={(item, index) => (
<TouchableOpacity
<TouchableItemRouter
key={index}
onPress={() => {
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<Props> = ({
)}
<ItemCardText item={item} />
</View>
</TouchableOpacity>
</TouchableItemRouter>
)}
/>
</View>

View File

@@ -28,7 +28,7 @@ export const MediaListSection: React.FC<Props> = ({ 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 [];

View File

@@ -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<ArtistPosterProps> = ({ 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 (
<View className="relative rounded-md overflow-hidden border border-neutral-900">
<Image
key={id}
id={id}
source={
url2
? {
uri: url2,
}
: null
}
cachePolicy={"memory-disk"}
contentFit="cover"
style={{
aspectRatio: "1/1",
}}
/>
</View>
);
if (item)
return (
<View className="relative rounded-md overflow-hidden border border-neutral-900">
<Image
key={item.Id}
id={item.Id}
source={
url
? {
uri: url,
}
: null
}
cachePolicy={"memory-disk"}
contentFit="cover"
style={{
aspectRatio: "1/1",
}}
/>
</View>
);
};
export default AlbumCover;

View File

@@ -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<ArtistPosterProps> = ({
api,
item,
}),
[item],
[item]
);
if (!url)

View File

@@ -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";

View File

@@ -9,10 +9,11 @@ type PosterProps = {
item?: BaseItemDto | BaseItemPerson | null;
url?: string | null;
showProgress?: boolean;
blurhash?: string | null;
};
const Poster: React.FC<PosterProps> = ({ item, url }) => {
if (!url || !item)
const Poster: React.FC<PosterProps> = ({ item, url, blurhash }) => {
if (!item)
return (
<View
className="border border-neutral-900"
@@ -25,11 +26,22 @@ const Poster: React.FC<PosterProps> = ({ item, url }) => {
return (
<View className="rounded-md overflow-hidden border border-neutral-900">
<Image
placeholder={
blurhash
? {
blurhash,
}
: null
}
key={item.Id}
id={item.Id}
source={{
uri: url,
}}
source={
url
? {
uri: url,
}
: null
}
cachePolicy={"memory-disk"}
contentFit="cover"
style={{

View File

@@ -6,7 +6,7 @@ import React from "react";
import { Linking, TouchableOpacity, View } from "react-native";
import { HorizontalScroll } from "../common/HorrizontalScroll";
import { Text } from "../common/Text";
import Poster from "../Poster";
import Poster from "../posters/Poster";
import { useAtom } from "jotai";
import { apiAtom } from "@/providers/JellyfinProvider";
import { getPrimaryImageUrl } from "@/utils/jellyfin/image/getPrimaryImageUrl";

View File

@@ -4,7 +4,7 @@ import { router } from "expo-router";
import { useAtom } from "jotai";
import React from "react";
import { TouchableOpacity, View } from "react-native";
import Poster from "../Poster";
import Poster from "../posters/Poster";
import { HorizontalScroll } from "../common/HorrizontalScroll";
import { Text } from "../common/Text";
import { getPrimaryImageUrlById } from "@/utils/jellyfin/image/getPrimaryImageUrlById";

View File

@@ -3,7 +3,7 @@ import React from "react";
import { TouchableOpacity, View } from "react-native";
import { HorizontalScroll } from "../common/HorrizontalScroll";
import { Text } from "../common/Text";
import Poster from "../Poster";
import Poster from "../posters/Poster";
import ContinueWatchingPoster from "../ContinueWatchingPoster";
import { ItemCardText } from "../ItemCardText";
import { router } from "expo-router";

View File

@@ -26,6 +26,7 @@
"@react-navigation/native": "^6.0.2",
"@shopify/flash-list": "1.6.4",
"@tanstack/react-query": "^5.51.16",
"@types/lodash": "^4.17.7",
"@types/uuid": "^10.0.0",
"axios": "^1.7.3",
"expo": "~51.0.27",
@@ -50,6 +51,7 @@
"expo-web-browser": "~13.0.3",
"ffmpeg-kit-react-native": "^6.0.2",
"jotai": "^2.9.1",
"lodash": "^4.17.21",
"nativewind": "^2.0.11",
"react": "18.2.0",
"react-dom": "18.2.0",
@@ -71,6 +73,7 @@
"react-native-video": "^6.4.3",
"react-native-web": "~0.19.10",
"tailwindcss": "3.3.2",
"use-debounce": "^10.0.3",
"uuid": "^10.0.0",
"zeego": "^1.10.0",
"zod": "^3.23.8"

View File

@@ -1,7 +1,47 @@
import { NameGuidPair } from "@jellyfin/sdk/lib/generated-client/models";
import {
ItemFilter,
ItemSortBy,
NameGuidPair,
SortOrder,
} from "@jellyfin/sdk/lib/generated-client/models";
import { atom, useAtom } from "jotai";
export const sortOptions: {
key: ItemSortBy;
value: string;
}[] = [
{ key: "SortName", value: "Name" },
{ key: "CommunityRating", value: "Community Rating" },
{ key: "CriticRating", value: "Critics Rating" },
{ key: "DateLastContentAdded", value: "Content Added" },
{ key: "DatePlayed", value: "Date Played" },
{ key: "PlayCount", value: "Play Count" },
{ key: "ProductionYear", value: "Production Year" },
{ key: "Runtime", value: "Runtime" },
{ key: "OfficialRating", value: "Official Rating" },
{ key: "PremiereDate", value: "Premiere Date" },
{ key: "StartDate", value: "Start Date" },
{ key: "IsUnplayed", value: "Is Unplayed" },
{ key: "IsPlayed", value: "Is Played" },
{ key: "VideoBitRate", value: "Video Bit Rate" },
{ key: "AirTime", value: "Air Time" },
{ key: "Studio", value: "Studio" },
{ key: "IsFavoriteOrLiked", value: "Is Favorite Or Liked" },
{ key: "Random", value: "Random" },
];
export const sortOrderOptions: {
key: SortOrder;
value: string;
}[] = [
{ key: "Ascending", value: "Ascending" },
{ key: "Descending", value: "Descending" },
];
export const genreFilterAtom = atom<string[]>([]);
export const tagsFilterAtom = atom<string[]>([]);
export const yearFilterAtom = atom<string[]>([]);
export const sortByAtom = atom<string>("title");
export const sortByAtom = atom<(typeof sortOptions)[number]>(sortOptions[0]);
export const sortOrderAtom = atom<(typeof sortOrderOptions)[number]>(
sortOrderOptions[0],
);