Compare commits

...

3 Commits

Author SHA1 Message Date
Fredrik Burmester
ece3bc001f fix: id collision 2025-01-01 16:11:04 +01:00
Fredrik Burmester
27609e7789 feat: favorites tab 2025-01-01 15:46:22 +01:00
Fredrik Burmester
a994868be4 chore 2024-12-31 23:13:58 +01:00
26 changed files with 234 additions and 23 deletions

View File

@@ -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 (
<Stack>
<Stack.Screen
name="index"
options={{
headerShown: true,
headerLargeTitle: true,
headerTitle: "Favorites",
headerBlurEffect: "prominent",
headerTransparent: Platform.OS === "ios" ? true : false,
headerShadowVisible: false,
}}
/>
{Object.entries(nestedTabPageScreenOptions).map(([name, options]) => (
<Stack.Screen key={name} name={name} options={options} />
))}
</Stack>
);
}

View File

@@ -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 (
<ScrollView
nestedScrollEnabled
contentInsetAdjustmentBehavior="automatic"
refreshControl={
<RefreshControl refreshing={loading} onRefresh={refetch} />
}
contentContainerStyle={{
paddingLeft: insets.left,
paddingRight: insets.right,
paddingBottom: 16,
}}
>
<Favorites />
</ScrollView>
);
}

View File

@@ -1,6 +1,6 @@
import { Chromecast } from "@/components/Chromecast";
import { nestedTabPageScreenOptions } from "@/components/stacks/NestedTabPageStack";
import { Feather } from "@expo/vector-icons";
import { Feather, Ionicons } from "@expo/vector-icons";
import { Stack, useRouter } from "expo-router";
import { Platform, TouchableOpacity, View } from "react-native";

View File

@@ -107,9 +107,9 @@ export default function index() {
setIsConnected(state.isConnected);
});
cleanCacheDirectory()
.then(r => console.log("Cache directory cleaned"))
.catch(e => console.error("Something went wrong cleaning cache directory"))
cleanCacheDirectory().catch((e) =>
console.error("Something went wrong cleaning cache directory")
);
return () => {
unsubscribe();
};

View File

@@ -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
}
/>
</TouchableOpacity>
@@ -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
}
/>
</TouchableOpacity>

View File

@@ -48,7 +48,10 @@ export default function TabLayout() {
Platform.OS == "android"
? ({ color, focused, size }) =>
require("@/assets/icons/house.fill.png")
: () => ({ sfSymbol: "house" }),
: ({ focused }) =>
focused
? { sfSymbol: "house.fill" }
: { sfSymbol: "house" },
}}
/>
<NativeTabs.Screen
@@ -59,7 +62,26 @@ export default function TabLayout() {
Platform.OS == "android"
? ({ color, focused, size }) =>
require("@/assets/icons/magnifyingglass.png")
: () => ({ sfSymbol: "magnifyingglass" }),
: ({ focused }) =>
focused
? { sfSymbol: "magnifyingglass" }
: { sfSymbol: "magnifyingglass" },
}}
/>
<NativeTabs.Screen
name="(favorites)"
options={{
title: "Favorites",
tabBarIcon:
Platform.OS == "android"
? ({ color, focused, size }) =>
focused
? require("@/assets/icons/heart.fill.png")
: require("@/assets/icons/heart.png")
: ({ focused }) =>
focused
? { sfSymbol: "heart.fill" }
: { sfSymbol: "heart" },
}}
/>
<NativeTabs.Screen
@@ -70,7 +92,10 @@ export default function TabLayout() {
Platform.OS == "android"
? ({ color, focused, size }) =>
require("@/assets/icons/server.rack.png")
: () => ({ sfSymbol: "rectangle.stack" }),
: ({ focused }) =>
focused
? { sfSymbol: "rectangle.stack.fill" }
: { sfSymbol: "rectangle.stack" },
}}
/>
<NativeTabs.Screen
@@ -81,8 +106,11 @@ export default function TabLayout() {
tabBarItemHidden: settings?.showCustomMenuLinks ? false : true,
tabBarIcon:
Platform.OS == "android"
? () => require("@/assets/icons/list.png")
: () => ({ sfSymbol: "list.dash" }),
? ({ focused }) => require("@/assets/icons/list.png")
: ({ focused }) =>
focused
? { sfSymbol: "list.dash.fill" }
: { sfSymbol: "list.dash" },
}}
/>
</NativeTabs>

View File

@@ -1,4 +1,5 @@
import "@/augmentations";
import { Text } from "@/components/common/Text";
import { DownloadProvider } from "@/providers/DownloadProvider";
import {
getOrSetDeviceId,
@@ -36,7 +37,7 @@ import * as SplashScreen from "expo-splash-screen";
import * as TaskManager from "expo-task-manager";
import { Provider as JotaiProvider, useAtom } from "jotai";
import { useEffect, useRef } from "react";
import { Appearance, AppState } from "react-native";
import { Appearance, AppState, TouchableOpacity } from "react-native";
import { SystemBars } from "react-native-edge-to-edge";
import { GestureHandlerRootView } from "react-native-gesture-handler";
import "react-native-reanimated";

BIN
assets/icons/heart.fill.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
assets/icons/heart.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

BIN
bun.lockb

Binary file not shown.

View File

@@ -66,7 +66,12 @@ export const TouchableItemRouter: React.FC<PropsWithChildren<Props>> = ({
const markAsPlayedStatus = useMarkAsPlayed(item);
if (from === "(home)" || from === "(search)" || from === "(libraries)")
if (
from === "(home)" ||
from === "(search)" ||
from === "(libraries)" ||
from === "(favorites)"
)
return (
<ContextMenu.Root>
<ContextMenu.Trigger>

View File

@@ -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 (
<View className="flex flex-col space-y-4">
<ScrollingCollectionList
queryFn={fetchFavoriteSeries}
queryKey={["home", "favorites", "series"]}
title="Series"
hideIfEmpty
/>
<ScrollingCollectionList
queryFn={fetchFavoriteMovies}
queryKey={["home", "favorites", "movies"]}
title="Movies"
hideIfEmpty
orientation="vertical"
/>
<ScrollingCollectionList
queryFn={fetchFavoriteEpisodes}
queryKey={["home", "favorites", "episodes"]}
title="Episodes"
hideIfEmpty
/>
<ScrollingCollectionList
queryFn={fetchFavoriteVideos}
queryKey={["home", "favorites", "videos"]}
title="Videos"
hideIfEmpty
/>
<ScrollingCollectionList
queryFn={fetchFavoriteBoxsets}
queryKey={["home", "favorites", "boxsets"]}
title="Boxsets"
hideIfEmpty
/>
<ScrollingCollectionList
queryFn={fetchFavoritePlaylists}
queryKey={["home", "favorites", "playlists"]}
title="Playlists"
hideIfEmpty
/>
<ScrollingCollectionList
queryFn={fetchFavoriteMusicAlbum}
queryKey={["home", "favorites", "musicAlbums"]}
title="Music Albums"
hideIfEmpty
/>
<ScrollingCollectionList
queryFn={fetchFavoriteAudio}
queryKey={["home", "favorites", "audio"]}
title="Audio"
hideIfEmpty
/>
</View>
);
};

View File

@@ -18,6 +18,7 @@ interface Props extends ViewProps {
disabled?: boolean;
queryKey: QueryKey;
queryFn: QueryFunction<BaseItemDto[]>;
hideIfEmpty?: boolean;
}
export const ScrollingCollectionList: React.FC<Props> = ({
@@ -26,10 +27,9 @@ export const ScrollingCollectionList: React.FC<Props> = ({
disabled = false,
queryFn,
queryKey,
hideIfEmpty = false,
...props
}) => {
// console.log(queryKey);
const { data, isLoading } = useQuery({
queryKey: queryKey,
queryFn,
@@ -41,8 +41,10 @@ export const ScrollingCollectionList: React.FC<Props> = ({
if (disabled || !title) return null;
if (hideIfEmpty === true && data?.length === 0) return null;
return (
<View {...props} className="">
<View {...props}>
<Text className="px-4 text-lg font-bold mb-2 text-neutral-100">
{title}
</Text>
@@ -82,15 +84,13 @@ export const ScrollingCollectionList: React.FC<Props> = ({
) : (
<ScrollView horizontal showsHorizontalScrollIndicator={false}>
<View className="px-4 flex flex-row">
{data?.map((item, index) => (
{data?.map((item) => (
<TouchableItemRouter
item={item}
key={index}
className={`
mr-2
${orientation === "horizontal" ? "w-44" : "w-28"}
`}
key={item.Id}
className={`mr-2
${orientation === "horizontal" ? "w-44" : "w-28"}
`}
>
{item.Type === "Episode" && orientation === "horizontal" && (
<ContinueWatchingPoster item={item} />