diff --git a/app/(auth)/(tabs)/(favorites)/_layout.tsx b/app/(auth)/(tabs)/(favorites)/_layout.tsx new file mode 100644 index 00000000..f96cd516 --- /dev/null +++ b/app/(auth)/(tabs)/(favorites)/_layout.tsx @@ -0,0 +1,24 @@ +import { nestedTabPageScreenOptions } from "@/components/stacks/NestedTabPageStack"; +import { Stack } from "expo-router"; +import { Platform } from "react-native"; + +export default function SearchLayout() { + return ( + + + {Object.entries(nestedTabPageScreenOptions).map(([name, options]) => ( + + ))} + + ); +} diff --git a/app/(auth)/(tabs)/(favorites)/index.tsx b/app/(auth)/(tabs)/(favorites)/index.tsx new file mode 100644 index 00000000..e01f975e --- /dev/null +++ b/app/(auth)/(tabs)/(favorites)/index.tsx @@ -0,0 +1,34 @@ +import { Favorites } from "@/components/home/Favorites"; +import { useInvalidatePlaybackProgressCache } from "@/hooks/useRevalidatePlaybackProgressCache"; +import React, { useCallback, useState } from "react"; +import { RefreshControl, ScrollView, View } from "react-native"; +import { useSafeAreaInsets } from "react-native-safe-area-context"; + +export default function favorites() { + const invalidateCache = useInvalidatePlaybackProgressCache(); + + const [loading, setLoading] = useState(false); + const refetch = useCallback(async () => { + setLoading(true); + await invalidateCache(); + setLoading(false); + }, []); + const insets = useSafeAreaInsets(); + + return ( + + } + contentContainerStyle={{ + paddingLeft: insets.left, + paddingRight: insets.right, + paddingBottom: 16, + }} + > + + + ); +} diff --git a/app/(auth)/(tabs)/(home,libraries,search)/actors/[actorId].tsx b/app/(auth)/(tabs)/(home,libraries,search,favorites)/actors/[actorId].tsx similarity index 100% rename from app/(auth)/(tabs)/(home,libraries,search)/actors/[actorId].tsx rename to app/(auth)/(tabs)/(home,libraries,search,favorites)/actors/[actorId].tsx diff --git a/app/(auth)/(tabs)/(home,libraries,search)/albums/[albumId].tsx b/app/(auth)/(tabs)/(home,libraries,search,favorites)/albums/[albumId].tsx similarity index 100% rename from app/(auth)/(tabs)/(home,libraries,search)/albums/[albumId].tsx rename to app/(auth)/(tabs)/(home,libraries,search,favorites)/albums/[albumId].tsx diff --git a/app/(auth)/(tabs)/(home,libraries,search)/artists/[artistId].tsx b/app/(auth)/(tabs)/(home,libraries,search,favorites)/artists/[artistId].tsx similarity index 100% rename from app/(auth)/(tabs)/(home,libraries,search)/artists/[artistId].tsx rename to app/(auth)/(tabs)/(home,libraries,search,favorites)/artists/[artistId].tsx diff --git a/app/(auth)/(tabs)/(home,libraries,search)/artists/index.tsx b/app/(auth)/(tabs)/(home,libraries,search,favorites)/artists/index.tsx similarity index 100% rename from app/(auth)/(tabs)/(home,libraries,search)/artists/index.tsx rename to app/(auth)/(tabs)/(home,libraries,search,favorites)/artists/index.tsx diff --git a/app/(auth)/(tabs)/(home,libraries,search)/collections/[collectionId].tsx b/app/(auth)/(tabs)/(home,libraries,search,favorites)/collections/[collectionId].tsx similarity index 100% rename from app/(auth)/(tabs)/(home,libraries,search)/collections/[collectionId].tsx rename to app/(auth)/(tabs)/(home,libraries,search,favorites)/collections/[collectionId].tsx diff --git a/app/(auth)/(tabs)/(home,libraries,search)/items/page.tsx b/app/(auth)/(tabs)/(home,libraries,search,favorites)/items/page.tsx similarity index 100% rename from app/(auth)/(tabs)/(home,libraries,search)/items/page.tsx rename to app/(auth)/(tabs)/(home,libraries,search,favorites)/items/page.tsx diff --git a/app/(auth)/(tabs)/(home,libraries,search)/jellyseerr/page.tsx b/app/(auth)/(tabs)/(home,libraries,search,favorites)/jellyseerr/page.tsx similarity index 100% rename from app/(auth)/(tabs)/(home,libraries,search)/jellyseerr/page.tsx rename to app/(auth)/(tabs)/(home,libraries,search,favorites)/jellyseerr/page.tsx diff --git a/app/(auth)/(tabs)/(home,libraries,search)/livetv/_layout.tsx b/app/(auth)/(tabs)/(home,libraries,search,favorites)/livetv/_layout.tsx similarity index 100% rename from app/(auth)/(tabs)/(home,libraries,search)/livetv/_layout.tsx rename to app/(auth)/(tabs)/(home,libraries,search,favorites)/livetv/_layout.tsx diff --git a/app/(auth)/(tabs)/(home,libraries,search)/livetv/channels.tsx b/app/(auth)/(tabs)/(home,libraries,search,favorites)/livetv/channels.tsx similarity index 100% rename from app/(auth)/(tabs)/(home,libraries,search)/livetv/channels.tsx rename to app/(auth)/(tabs)/(home,libraries,search,favorites)/livetv/channels.tsx diff --git a/app/(auth)/(tabs)/(home,libraries,search)/livetv/guide.tsx b/app/(auth)/(tabs)/(home,libraries,search,favorites)/livetv/guide.tsx similarity index 100% rename from app/(auth)/(tabs)/(home,libraries,search)/livetv/guide.tsx rename to app/(auth)/(tabs)/(home,libraries,search,favorites)/livetv/guide.tsx diff --git a/app/(auth)/(tabs)/(home,libraries,search)/livetv/programs.tsx b/app/(auth)/(tabs)/(home,libraries,search,favorites)/livetv/programs.tsx similarity index 100% rename from app/(auth)/(tabs)/(home,libraries,search)/livetv/programs.tsx rename to app/(auth)/(tabs)/(home,libraries,search,favorites)/livetv/programs.tsx diff --git a/app/(auth)/(tabs)/(home,libraries,search)/livetv/recordings.tsx b/app/(auth)/(tabs)/(home,libraries,search,favorites)/livetv/recordings.tsx similarity index 100% rename from app/(auth)/(tabs)/(home,libraries,search)/livetv/recordings.tsx rename to app/(auth)/(tabs)/(home,libraries,search,favorites)/livetv/recordings.tsx diff --git a/app/(auth)/(tabs)/(home,libraries,search)/series/[id].tsx b/app/(auth)/(tabs)/(home,libraries,search,favorites)/series/[id].tsx similarity index 100% rename from app/(auth)/(tabs)/(home,libraries,search)/series/[id].tsx rename to app/(auth)/(tabs)/(home,libraries,search,favorites)/series/[id].tsx diff --git a/app/(auth)/(tabs)/(search)/index.tsx b/app/(auth)/(tabs)/(search)/index.tsx index ac3d83fe..48507dbe 100644 --- a/app/(auth)/(tabs)/(search)/index.tsx +++ b/app/(auth)/(tabs)/(search)/index.tsx @@ -318,7 +318,7 @@ export default function search() { text="Library" textClass="p-1" className={ - searchType === "Library" ? "bg-neutral-600" : undefined + searchType === "Library" ? "bg-purple-600" : undefined } /> @@ -327,7 +327,7 @@ export default function search() { text="Discover" textClass="p-1" className={ - searchType === "Discover" ? "bg-neutral-600" : undefined + searchType === "Discover" ? "bg-purple-600" : undefined } /> diff --git a/app/(auth)/(tabs)/_layout.tsx b/app/(auth)/(tabs)/_layout.tsx index f256ab50..11fa4b67 100644 --- a/app/(auth)/(tabs)/_layout.tsx +++ b/app/(auth)/(tabs)/_layout.tsx @@ -62,6 +62,17 @@ export default function TabLayout() { : () => ({ sfSymbol: "magnifyingglass" }), }} /> + + require("@/assets/icons/heart.png") + : () => ({ sfSymbol: "heart" }), + }} + /> > = ({ const markAsPlayedStatus = useMarkAsPlayed(item); - if (from === "(home)" || from === "(search)" || from === "(libraries)") + if ( + from === "(home)" || + from === "(search)" || + from === "(libraries)" || + from === "(favorites)" + ) return ( diff --git a/components/home/Favorites.tsx b/components/home/Favorites.tsx new file mode 100644 index 00000000..90e55b1a --- /dev/null +++ b/components/home/Favorites.tsx @@ -0,0 +1,119 @@ +import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; +import { getItemsApi } from "@jellyfin/sdk/lib/utils/api"; +import { useAtom } from "jotai"; +import { View } from "react-native"; +import { ScrollingCollectionList } from "./ScrollingCollectionList"; +import { useCallback } from "react"; +import { BaseItemKind } from "@jellyfin/sdk/lib/generated-client"; + +export const Favorites = () => { + const [api] = useAtom(apiAtom); + const [user] = useAtom(userAtom); + + const fetchFavoritesByType = useCallback( + async (itemType: BaseItemKind) => { + const response = await getItemsApi(api!).getItems({ + userId: user?.Id!, + sortBy: ["SeriesSortName", "SortName"], + sortOrder: ["Ascending"], + filters: ["IsFavorite"], + recursive: true, + fields: ["PrimaryImageAspectRatio"], + collapseBoxSetItems: false, + excludeLocationTypes: ["Virtual"], + enableTotalRecordCount: false, + limit: 20, + includeItemTypes: [itemType], + }); + return response.data.Items || []; + }, + [api, user] + ); + + const fetchFavoriteSeries = useCallback( + () => fetchFavoritesByType("Series"), + [fetchFavoritesByType] + ); + const fetchFavoriteMovies = useCallback( + () => fetchFavoritesByType("Movie"), + [fetchFavoritesByType] + ); + const fetchFavoriteEpisodes = useCallback( + () => fetchFavoritesByType("Episode"), + [fetchFavoritesByType] + ); + const fetchFavoriteVideos = useCallback( + () => fetchFavoritesByType("Video"), + [fetchFavoritesByType] + ); + const fetchFavoriteBoxsets = useCallback( + () => fetchFavoritesByType("BoxSet"), + [fetchFavoritesByType] + ); + const fetchFavoritePlaylists = useCallback( + () => fetchFavoritesByType("Playlist"), + [fetchFavoritesByType] + ); + const fetchFavoriteMusicAlbum = useCallback( + () => fetchFavoritesByType("MusicAlbum"), + [fetchFavoritesByType] + ); + const fetchFavoriteAudio = useCallback( + () => fetchFavoritesByType("Audio"), + [fetchFavoritesByType] + ); + + return ( + + + + + + + + + + + ); +}; diff --git a/components/home/ScrollingCollectionList.tsx b/components/home/ScrollingCollectionList.tsx index 04dd6004..2fddec7f 100644 --- a/components/home/ScrollingCollectionList.tsx +++ b/components/home/ScrollingCollectionList.tsx @@ -11,6 +11,7 @@ import ContinueWatchingPoster from "../ContinueWatchingPoster"; import { ItemCardText } from "../ItemCardText"; import { TouchableItemRouter } from "../common/TouchableItemRouter"; import SeriesPoster from "../posters/SeriesPoster"; +import { useEffect } from "react"; interface Props extends ViewProps { title?: string | null; @@ -18,6 +19,7 @@ interface Props extends ViewProps { disabled?: boolean; queryKey: QueryKey; queryFn: QueryFunction; + hideIfEmpty?: boolean; } export const ScrollingCollectionList: React.FC = ({ @@ -26,10 +28,9 @@ export const ScrollingCollectionList: React.FC = ({ disabled = false, queryFn, queryKey, + hideIfEmpty = false, ...props }) => { - // console.log(queryKey); - const { data, isLoading } = useQuery({ queryKey: queryKey, queryFn, @@ -41,6 +42,8 @@ export const ScrollingCollectionList: React.FC = ({ if (disabled || !title) return null; + if (hideIfEmpty === true && data?.length === 0) return null; + return ( @@ -86,11 +89,9 @@ export const ScrollingCollectionList: React.FC = ({ {item.Type === "Episode" && orientation === "horizontal" && (