feat: add kefintweaks watchlist integration properly

This commit is contained in:
Simon Eklundh
2026-06-08 19:11:11 +02:00
parent 1685571406
commit eba72e9d73
17 changed files with 657 additions and 96 deletions

View File

@@ -1,14 +1,24 @@
import { useCallback, useState } from "react";
import { useTranslation } from "react-i18next";
import { Platform, RefreshControl, ScrollView, View } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { FavoritesTabButtons } from "@/components/favorites/FavoritesTabButtons";
import { Favorites } from "@/components/home/Favorites";
import { Favorites as TVFavorites } from "@/components/home/Favorites.tv";
import { useInvalidatePlaybackProgressCache } from "@/hooks/useRevalidatePlaybackProgressCache";
import { useSettings } from "@/utils/atoms/settings";
export default function FavoritesPage() {
const invalidateCache = useInvalidatePlaybackProgressCache();
const { t } = useTranslation();
const { settings } = useSettings();
const [loading, setLoading] = useState(false);
// KefinTweaks watchlist (Likes-backed) view, toggled in-place like Discover.
const watchlistEnabled = settings?.useKefinTweaks ?? false;
const [viewType, setViewType] = useState<"Favorites" | "Watchlist">(
"Favorites",
);
const refetch = useCallback(async () => {
setLoading(true);
await invalidateCache();
@@ -20,6 +30,8 @@ export default function FavoritesPage() {
return <TVFavorites />;
}
const isWatchlist = watchlistEnabled && viewType === "Watchlist";
return (
<ScrollView
nestedScrollEnabled
@@ -34,7 +46,25 @@ export default function FavoritesPage() {
}}
>
<View style={{ paddingTop: Platform.OS === "android" ? 10 : 0 }}>
<Favorites />
{watchlistEnabled && (
<View className='pl-4 pr-4 flex flex-row mb-2'>
<FavoritesTabButtons
viewType={viewType}
setViewType={setViewType}
t={t}
/>
</View>
)}
{isWatchlist ? (
<Favorites
filter='Likes'
queryKeyBase='watchlist'
emptyTitleKey='favorites.noWatchlistTitle'
emptyTextKey='favorites.noWatchlistData'
/>
) : (
<Favorites />
)}
</View>
</ScrollView>
);

View File

@@ -2,6 +2,7 @@ import type { Api } from "@jellyfin/sdk";
import type {
BaseItemDto,
BaseItemKind,
ItemFilter,
} from "@jellyfin/sdk/lib/generated-client";
import { getItemsApi } from "@jellyfin/sdk/lib/utils/api";
import { FlashList } from "@shopify/flash-list";
@@ -52,9 +53,13 @@ export default function FavoritesSeeAllScreen() {
const searchParams = useLocalSearchParams<{
type?: string;
title?: string;
filter?: string;
}>();
const typeParam = searchParams.type;
const titleParam = searchParams.title;
// Watchlist (KefinTweaks) reuses this screen with the "Likes" filter.
const filter: ItemFilter =
searchParams.filter === "Likes" ? "Likes" : "IsFavorite";
const itemType = useMemo(() => {
if (!isFavoriteType(typeParam)) return null;
@@ -77,7 +82,7 @@ export default function FavoritesSeeAllScreen() {
userId: user.Id,
sortBy: ["SeriesSortName", "SortName"],
sortOrder: ["Ascending"],
filters: ["IsFavorite"],
filters: [filter],
recursive: true,
fields: ["PrimaryImageAspectRatio"],
collapseBoxSetItems: false,
@@ -90,12 +95,12 @@ export default function FavoritesSeeAllScreen() {
return response.data.Items || [];
},
[api, itemType, user?.Id],
[api, itemType, user?.Id, filter],
);
const { data, isFetching, fetchNextPage, hasNextPage, isLoading } =
useInfiniteQuery({
queryKey: ["favorites", "see-all", itemType],
queryKey: ["favorites", "see-all", itemType, filter],
queryFn: ({ pageParam = 0 }) => fetchItems({ pageParam }),
getNextPageParam: (lastPage, pages) => {
if (!lastPage || lastPage.length < pageSize) return undefined;

View File

@@ -9,6 +9,7 @@ import { useEffect, useMemo } from "react";
import { useTranslation } from "react-i18next";
import { Platform, View } from "react-native";
import { AddToFavorites } from "@/components/AddToFavorites";
import { AddToKefinWatchlist } from "@/components/AddToKefinWatchlist";
import { DownloadItems } from "@/components/DownloadItem";
import { ParallaxScrollView } from "@/components/ParallaxPage";
import { NextUp } from "@/components/series/NextUp";
@@ -18,6 +19,7 @@ import { TVSeriesPage } from "@/components/series/TVSeriesPage";
import { useDownload } from "@/providers/DownloadProvider";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { OfflineModeProvider } from "@/providers/OfflineModeProvider";
import { useSettings } from "@/utils/atoms/settings";
import {
buildOfflineSeriesFromEpisodes,
getDownloadedEpisodesForSeries,
@@ -30,6 +32,7 @@ import { storage } from "@/utils/mmkv";
const page: React.FC = () => {
const navigation = useNavigation();
const { t } = useTranslation();
const { settings } = useSettings();
const params = useLocalSearchParams();
const {
id: seriesId,
@@ -137,6 +140,7 @@ const page: React.FC = () => {
!isLoading && item && allEpisodes && allEpisodes.length > 0 ? (
<View className='flex flex-row items-center space-x-2'>
<AddToFavorites item={item} />
{settings?.useKefinTweaks && <AddToKefinWatchlist item={item} />}
{!Platform.isTV && (
<DownloadItems
size='large'
@@ -157,7 +161,7 @@ const page: React.FC = () => {
</View>
) : null,
});
}, [allEpisodes, isLoading, item, isOffline]);
}, [allEpisodes, isLoading, item, isOffline, settings?.useKefinTweaks]);
// For offline mode, we can show the page even without backdropUrl
if (!item || (!isOffline && !backdropUrl)) return null;