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" && (