forked from Ninjalama/streamyfin_mirror
feat: add favorites functionality with FavoriteButton component and layout
This commit is contained in:
4
.gitignore
vendored
4
.gitignore
vendored
@@ -35,4 +35,6 @@ credentials.json
|
||||
*.ipa
|
||||
.continuerc.json
|
||||
|
||||
.vscode/
|
||||
.vscode/
|
||||
|
||||
bun.lockb
|
||||
35
app/(auth)/(favorites)/_layout.tsx
Normal file
35
app/(auth)/(favorites)/_layout.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import { Chromecast } from "@/components/Chromecast";
|
||||
import { nestedTabPageScreenOptions } from "@/components/stacks/NestedTabPageStack";
|
||||
import { Feather } from "@expo/vector-icons";
|
||||
import { Stack, useRouter } from "expo-router";
|
||||
import { Platform, TouchableOpacity, View } from "react-native";
|
||||
import { Menu, Divider, Provider } from "react-native-paper";
|
||||
import {useState} from "react";
|
||||
|
||||
|
||||
export default function IndexLayout() {
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<Provider>
|
||||
<Stack>
|
||||
<Stack.Screen
|
||||
name="index"
|
||||
options={{
|
||||
headerShown: true,
|
||||
headerLargeTitle: true,
|
||||
headerTitle: "Favorites",
|
||||
headerBlurEffect: "prominent",
|
||||
headerTransparent: Platform.OS === "ios" ? true : false,
|
||||
headerShadowVisible: false,
|
||||
headerRight: () => (
|
||||
<View className="flex flex-row items-center space-x-2">
|
||||
<Chromecast />
|
||||
</View>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
</Provider>
|
||||
);
|
||||
}
|
||||
32
app/(auth)/(favorites)/index.tsx
Normal file
32
app/(auth)/(favorites)/index.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import { useAtom } from "jotai";
|
||||
import { useEffect, useState } from "react";
|
||||
import { View, Text, FlatList } from "react-native";
|
||||
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
|
||||
import { getItemsApi } from "@jellyfin/sdk/lib/utils/api";
|
||||
|
||||
export default function Favorites() {
|
||||
const [favorites, setFavorites] = useState([]);
|
||||
const [api] = useAtom(apiAtom);
|
||||
const [user] = useAtom(userAtom);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchFavorites = async () => {
|
||||
if (api && user) {
|
||||
const response = await getItemsApi(api).getItems({
|
||||
userId: user.Id,
|
||||
isFavorite: true,
|
||||
});
|
||||
if (response.data.Items) {
|
||||
setFavorites(response.data.Items);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
fetchFavorites();
|
||||
}, [api, user]);
|
||||
|
||||
return (
|
||||
<View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -3,10 +3,19 @@ import { nestedTabPageScreenOptions } from "@/components/stacks/NestedTabPageSta
|
||||
import { Feather } from "@expo/vector-icons";
|
||||
import { Stack, useRouter } from "expo-router";
|
||||
import { Platform, TouchableOpacity, View } from "react-native";
|
||||
import { Menu, Divider, Provider } from "react-native-paper";
|
||||
import {useState} from "react";
|
||||
|
||||
|
||||
export default function IndexLayout() {
|
||||
const router = useRouter();
|
||||
const [menuVisible, setMenuVisible] = useState(false);
|
||||
|
||||
const openMenu = () => setMenuVisible(true);
|
||||
const closeMenu = () => setMenuVisible(false);
|
||||
|
||||
return (
|
||||
<Provider>
|
||||
<Stack>
|
||||
<Stack.Screen
|
||||
name="index"
|
||||
@@ -20,6 +29,23 @@ export default function IndexLayout() {
|
||||
headerRight: () => (
|
||||
<View className="flex flex-row items-center space-x-2">
|
||||
<Chromecast />
|
||||
<Menu
|
||||
visible={menuVisible}
|
||||
onDismiss={closeMenu}
|
||||
anchor={
|
||||
<TouchableOpacity onPress={openMenu}>
|
||||
<Feather name="menu" color={"white"} size={22} />
|
||||
</TouchableOpacity>
|
||||
}
|
||||
>
|
||||
<Menu.Item
|
||||
onPress={() => {
|
||||
closeMenu();
|
||||
router.push("/(auth)/(favorites)");
|
||||
}}
|
||||
title="Favorites"
|
||||
/>
|
||||
</Menu>
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
router.push("/(auth)/settings");
|
||||
@@ -63,5 +89,6 @@ export default function IndexLayout() {
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
</Provider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { NextUp } from "@/components/series/NextUp";
|
||||
import { SeasonPicker } from "@/components/series/SeasonPicker";
|
||||
import { SeriesActions } from "@/components/series/SeriesActions";
|
||||
import { SeriesHeader } from "@/components/series/SeriesHeader";
|
||||
import { FavoriteButton} from "@/components/series/FavoriteButton";
|
||||
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
|
||||
import { getBackdropUrl } from "@/utils/jellyfin/image/getBackdropUrl";
|
||||
import { getLogoImageUrlById } from "@/utils/jellyfin/image/getLogoImageUrlById";
|
||||
@@ -83,6 +84,10 @@ const page: React.FC = () => {
|
||||
!isLoading &&
|
||||
allEpisodes &&
|
||||
allEpisodes.length > 0 && (
|
||||
<>
|
||||
<View className="flex flex-row items-center space-x-2 right-3">
|
||||
{item && <FavoriteButton seriesId={seriesId} />}
|
||||
</View>
|
||||
<View className="flex flex-row items-center space-x-2">
|
||||
<DownloadItems
|
||||
title="Download Series"
|
||||
@@ -99,6 +104,7 @@ const page: React.FC = () => {
|
||||
)}
|
||||
/>
|
||||
</View>
|
||||
</>
|
||||
),
|
||||
});
|
||||
}, [allEpisodes, isLoading]);
|
||||
|
||||
70
components/series/FavoriteButton.tsx
Normal file
70
components/series/FavoriteButton.tsx
Normal file
@@ -0,0 +1,70 @@
|
||||
import Ionicons from '@expo/vector-icons/Ionicons';
|
||||
import { useAtom } from "jotai";
|
||||
import { useCallback, useState, useEffect } from "react";
|
||||
import { View } from "react-native";
|
||||
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
|
||||
import { getItemsApi, getUserLibraryApi } from "@jellyfin/sdk/lib/utils/api";
|
||||
import { RoundButton } from "../RoundButton";
|
||||
|
||||
interface Props {
|
||||
seriesId: string;
|
||||
}
|
||||
|
||||
export const FavoriteButton: React.FC<Props> = ({ seriesId }) => {
|
||||
const [isFavorited, setIsFavorited] = useState(false);
|
||||
const [api] = useAtom(apiAtom);
|
||||
const [user] = useAtom(userAtom);
|
||||
|
||||
const checkIfFavorited = useCallback(async () => {
|
||||
if (api && user) {
|
||||
const response = await getItemsApi(api).getItems({
|
||||
userId: user.Id,
|
||||
isFavorite: true,
|
||||
recursive: true,
|
||||
});
|
||||
if (!response.data.Items) return;
|
||||
const isFavorite = response.data.Items.some((x: any) => x.Id === seriesId);
|
||||
setIsFavorited(isFavorite);
|
||||
}
|
||||
}, [api, user, seriesId]);
|
||||
|
||||
useEffect(() => {
|
||||
checkIfFavorited();
|
||||
}, [checkIfFavorited]);
|
||||
|
||||
const markFavorite = useCallback(async () => {
|
||||
if (api && user) {
|
||||
await getUserLibraryApi(api).markFavoriteItem({
|
||||
userId: user.Id,
|
||||
itemId: seriesId,
|
||||
});
|
||||
}
|
||||
}, [api, user, seriesId]);
|
||||
|
||||
const unmarkFavorite = useCallback(async () => {
|
||||
if (api && user) {
|
||||
await getUserLibraryApi(api).unmarkFavoriteItem({
|
||||
userId: user.Id,
|
||||
itemId: seriesId,
|
||||
});
|
||||
}
|
||||
}, [api, user, seriesId]);
|
||||
|
||||
const onFavorite = useCallback(async () => {
|
||||
if (isFavorited) {
|
||||
setIsFavorited(false);
|
||||
await unmarkFavorite();
|
||||
} else {
|
||||
setIsFavorited(true);
|
||||
await markFavorite();
|
||||
}
|
||||
}, [isFavorited, markFavorite, unmarkFavorite]);
|
||||
|
||||
return (
|
||||
<View>
|
||||
<RoundButton onPress={onFavorite}>
|
||||
<Ionicons name={isFavorited ? "heart-sharp" : "heart-outline"} size={22} color={isFavorited ? "purple" : "white"} />
|
||||
</RoundButton>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
@@ -85,6 +85,7 @@
|
||||
"react-native-ios-utilities": "^4.5.1",
|
||||
"react-native-mmkv": "^2.12.2",
|
||||
"react-native-pager-view": "6.3.0",
|
||||
"react-native-paper": "^5.12.5",
|
||||
"react-native-progress": "^5.0.1",
|
||||
"react-native-reanimated": "~3.10.1",
|
||||
"react-native-reanimated-carousel": "4.0.0-canary.15",
|
||||
|
||||
Reference in New Issue
Block a user