diff --git a/app/(auth)/(tabs)/(home)/index.tsx b/app/(auth)/(tabs)/(home)/index.tsx index c3a48f93..0259bbbb 100644 --- a/app/(auth)/(tabs)/(home)/index.tsx +++ b/app/(auth)/(tabs)/(home)/index.tsx @@ -117,7 +117,7 @@ export default function index() { isError: e1, isLoading: l1, } = useQuery({ - queryKey: ["userViews", user?.Id], + queryKey: ["home", "userViews", user?.Id], queryFn: async () => { if (!api || !user?.Id) { return null; @@ -138,7 +138,7 @@ export default function index() { isError: e2, isLoading: l2, } = useQuery({ - queryKey: ["sf_promoted", user?.Id, settings?.usePopularPlugin], + queryKey: ["home", "sf_promoted", user?.Id, settings?.usePopularPlugin], queryFn: async () => { if (!api || !user?.Id) return []; @@ -167,9 +167,26 @@ export default function index() { const refetch = useCallback(async () => { setLoading(true); - await queryClient.invalidateQueries(); + await queryClient.invalidateQueries({ + queryKey: ["home"], + refetchType: "all", + type: "all", + exact: false, + }); + await queryClient.invalidateQueries({ + queryKey: ["home"], + refetchType: "all", + type: "all", + exact: false, + }); + await queryClient.invalidateQueries({ + queryKey: ["item"], + refetchType: "all", + type: "all", + exact: false, + }); setLoading(false); - }, []); + }, [queryClient]); const createCollectionConfig = useCallback( ( @@ -208,7 +225,12 @@ export default function index() { const includeItemTypes: BaseItemKind[] = c.CollectionType === "tvshows" ? ["Series"] : ["Movie"]; const title = "Recently Added in " + c.Name; - const queryKey = ["recentlyAddedIn" + c.CollectionType, user?.Id!, c.Id!]; + const queryKey = [ + "home", + "recentlyAddedIn" + c.CollectionType, + user?.Id!, + c.Id!, + ]; return createCollectionConfig( title || "", queryKey, @@ -220,7 +242,7 @@ export default function index() { const ss: Section[] = [ { title: "Continue Watching", - queryKey: ["resumeItems", user.Id], + queryKey: ["home", "resumeItems", user.Id], queryFn: async () => ( await getItemsApi(api).getResumeItems({ @@ -234,7 +256,7 @@ export default function index() { }, { title: "Next Up", - queryKey: ["nextUp-all", user?.Id], + queryKey: ["home", "nextUp-all", user?.Id], queryFn: async () => ( await getTvShowsApi(api).getNextUp({ @@ -252,7 +274,7 @@ export default function index() { (ml) => ({ title: ml.Name, - queryKey: ["mediaList", ml.Id!], + queryKey: ["home", "mediaList", ml.Id!], queryFn: async () => ml, type: "MediaListSection", orientation: "vertical", @@ -260,7 +282,7 @@ export default function index() { ) || []), { title: "Suggested Movies", - queryKey: ["suggestedMovies", user?.Id], + queryKey: ["home", "suggestedMovies", user?.Id], queryFn: async () => ( await getSuggestionsApi(api).getSuggestions({ @@ -275,7 +297,7 @@ export default function index() { }, { title: "Suggested Episodes", - queryKey: ["suggestedEpisodes", user?.Id], + queryKey: ["home", "suggestedEpisodes", user?.Id], queryFn: async () => { try { const suggestions = await getSuggestions(api, user.Id); diff --git a/app/(auth)/(tabs)/(home,libraries,search)/actors/[actorId].tsx b/app/(auth)/(tabs)/(home,libraries,search)/actors/[actorId].tsx index 446eb807..45dc8a4d 100644 --- a/app/(auth)/(tabs)/(home,libraries,search)/actors/[actorId].tsx +++ b/app/(auth)/(tabs)/(home,libraries,search)/actors/[actorId].tsx @@ -50,7 +50,7 @@ const page: React.FC = () => { userId: user.Id, personIds: [actorId], startIndex: pageParam, - limit: 8, + limit: 16, sortOrder: ["Descending", "Descending", "Ascending"], includeItemTypes: ["Movie", "Series"], recursive: true, diff --git a/components/ContinueWatchingPoster.tsx b/components/ContinueWatchingPoster.tsx index 3a1899ec..641927f2 100644 --- a/components/ContinueWatchingPoster.tsx +++ b/components/ContinueWatchingPoster.tsx @@ -1,7 +1,7 @@ import { apiAtom } from "@/providers/JellyfinProvider"; import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; import { Image } from "expo-image"; -import { useAtom } from "jotai"; +import { useAtom, useAtomValue } from "jotai"; import { useMemo, useState } from "react"; import { View } from "react-native"; import { WatchedIndicator } from "./WatchedIndicator"; @@ -18,7 +18,7 @@ const ContinueWatchingPoster: React.FC = ({ useEpisodePoster = false, size = "normal", }) => { - const [api] = useAtom(apiAtom); + const api = useAtomValue(apiAtom); /** * Get horrizontal poster for movie and episode, with failover to primary. @@ -59,7 +59,7 @@ const ContinueWatchingPoster: React.FC = ({ } else { return item.UserData?.PlayedPercentage || 0; } - }, []); + }, [item]); if (!url) return ( diff --git a/components/ItemContent.tsx b/components/ItemContent.tsx index 4c3cc719..aee6cad0 100644 --- a/components/ItemContent.tsx +++ b/components/ItemContent.tsx @@ -1,9 +1,5 @@ import { AudioTrackSelector } from "@/components/AudioTrackSelector"; -import { - Bitrate, - BITRATES, - BitrateSelector, -} from "@/components/BitrateSelector"; +import { Bitrate, BitrateSelector } from "@/components/BitrateSelector"; import { DownloadItem } from "@/components/DownloadItem"; import { OverviewText } from "@/components/OverviewText"; import { ParallaxScrollView } from "@/components/ParallaxPage"; @@ -18,6 +14,8 @@ import { SeasonEpisodesCarousel } from "@/components/series/SeasonEpisodesCarous import { useImageColors } from "@/hooks/useImageColors"; import { apiAtom } from "@/providers/JellyfinProvider"; import { usePlaySettings } from "@/providers/PlaySettingsProvider"; +import { useSettings } from "@/utils/atoms/settings"; +import { getDefaultPlaySettings } from "@/utils/jellyfin/getDefaultPlaySettings"; import { getLogoImageUrlById } from "@/utils/jellyfin/image/getLogoImageUrlById"; import { BaseItemDto, @@ -27,23 +25,13 @@ import { Image } from "expo-image"; import { useFocusEffect, useNavigation } from "expo-router"; import * as ScreenOrientation from "expo-screen-orientation"; import { useAtom } from "jotai"; -import React, { - useCallback, - useEffect, - useMemo, - useRef, - useState, -} from "react"; +import React, { useCallback, useEffect, useMemo, useState } from "react"; import { View } from "react-native"; -import { useCastDevice } from "react-native-google-cast"; -import Animated from "react-native-reanimated"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { Chromecast } from "./Chromecast"; import { ItemHeader } from "./ItemHeader"; import { MediaSourceSelector } from "./MediaSourceSelector"; import { MoreMoviesWithActor } from "./MoreMoviesWithActor"; -import { useSettings } from "@/utils/atoms/settings"; -import { getDefaultPlaySettings } from "@/utils/jellyfin/getDefaultPlaySettings"; export const ItemContent: React.FC<{ item: BaseItemDto }> = React.memo( ({ item }) => { @@ -135,7 +123,7 @@ export const ItemContent: React.FC<{ item: BaseItemDto }> = React.memo( }; }, []); - const headerHeightRef = useRef(400); + const [headerHeight, setHeaderHeight] = useState(350); useImageColors({ item }); @@ -154,26 +142,18 @@ export const ItemContent: React.FC<{ item: BaseItemDto }> = React.memo( ), }); - - // setPlaySettings((prev) => ({ - // audioIndex: undefined, - // subtitleIndex: undefined, - // mediaSourceId: undefined, - // bitrate: undefined, - // mediaSource: item.MediaSources?.[0], - // item, - // })); }, [item]); useEffect(() => { + // If landscape if (orientation !== ScreenOrientation.Orientation.PORTRAIT_UP) { - headerHeightRef.current = 230; + setHeaderHeight(230); return; } - if (item.Type === "Episode") headerHeightRef.current = 400; - else if (item.Type === "Movie") headerHeightRef.current = 500; - else headerHeightRef.current = 400; - }, [item, orientation]); + + if (item.Type === "Movie") setHeaderHeight(500); + else setHeaderHeight(350); + }, [item.Type, orientation]); const logoUrl = useMemo(() => getLogoImageUrlById({ api, item }), [item]); @@ -193,22 +173,20 @@ export const ItemContent: React.FC<{ item: BaseItemDto }> = React.memo( > - - - - + + + } logo={ <> diff --git a/components/PlayButton.tsx b/components/PlayButton.tsx index a91e8688..f87eebf8 100644 --- a/components/PlayButton.tsx +++ b/components/PlayButton.tsx @@ -4,11 +4,11 @@ import { getParentBackdropImageUrl } from "@/utils/jellyfin/image/getParentBackd import { getPrimaryImageUrl } from "@/utils/jellyfin/image/getPrimaryImageUrl"; import { runtimeTicksToMinutes } from "@/utils/time"; import { useActionSheet } from "@expo/react-native-action-sheet"; -import { Feather, Ionicons } from "@expo/vector-icons"; +import { Feather, Ionicons, MaterialCommunityIcons } from "@expo/vector-icons"; import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; import { useAtom } from "jotai"; import { useEffect, useMemo } from "react"; -import { TouchableOpacity, View } from "react-native"; +import { Linking, TouchableOpacity, View } from "react-native"; import CastContext, { PlayServicesState, useMediaStatus, @@ -27,6 +27,7 @@ import Animated, { import { Button } from "./Button"; import { Text } from "./common/Text"; import { useRouter } from "expo-router"; +import { useSettings } from "@/utils/atoms/settings"; interface Props extends React.ComponentProps { item?: BaseItemDto | null; @@ -55,6 +56,7 @@ export const PlayButton: React.FC = ({ item, url, ...props }) => { const startColor = useSharedValue(memoizedColor); const widthProgress = useSharedValue(0); const colorChangeProgress = useSharedValue(0); + const [settings] = useSettings(); const directStream = useMemo(() => { return !url?.includes("m3u8"); @@ -69,10 +71,18 @@ export const PlayButton: React.FC = ({ item, url, ...props }) => { ); return; } + if (!client) { + const vlcLink = "vlc://" + url; + if (vlcLink && settings?.openInVLC) { + Linking.openURL(vlcLink); + return; + } + router.push("/play-video"); return; } + const options = ["Chromecast", "Device", "Cancel"]; const cancelButtonIndex = 2; showActionSheetWithOptions( @@ -310,6 +320,15 @@ export const PlayButton: React.FC = ({ item, url, ...props }) => { )} + {!client && settings?.openInVLC && ( + + + + )} diff --git a/components/PlayedStatus.tsx b/components/PlayedStatus.tsx index 375cf22b..82e9057d 100644 --- a/components/PlayedStatus.tsx +++ b/components/PlayedStatus.tsx @@ -44,6 +44,9 @@ export const PlayedStatus: React.FC = ({ item, ...props }) => { queryClient.invalidateQueries({ queryKey: ["nextUp-all"], }); + queryClient.invalidateQueries({ + queryKey: ["home"], + }); }; return ( diff --git a/components/WatchedIndicator.tsx b/components/WatchedIndicator.tsx index 1ed5d0e4..d445c021 100644 --- a/components/WatchedIndicator.tsx +++ b/components/WatchedIndicator.tsx @@ -1,4 +1,5 @@ import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; +import React from "react"; import { View } from "react-native"; export const WatchedIndicator: React.FC<{ item: BaseItemDto }> = ({ item }) => { diff --git a/components/home/ScrollingCollectionList.tsx b/components/home/ScrollingCollectionList.tsx index 4cf596dd..03cb37e9 100644 --- a/components/home/ScrollingCollectionList.tsx +++ b/components/home/ScrollingCollectionList.tsx @@ -9,10 +9,8 @@ import { import { ScrollView, View, ViewProps } from "react-native"; import ContinueWatchingPoster from "../ContinueWatchingPoster"; import { ItemCardText } from "../ItemCardText"; -import { HorizontalScroll } from "../common/HorrizontalScroll"; import { TouchableItemRouter } from "../common/TouchableItemRouter"; import SeriesPoster from "../posters/SeriesPoster"; -import { FlashList } from "@shopify/flash-list"; interface Props extends ViewProps { title?: string | null; @@ -34,7 +32,7 @@ export const ScrollingCollectionList: React.FC = ({ queryKey, queryFn, enabled: !disabled, - staleTime: 60 * 1000, + staleTime: 0, }); if (disabled || !title) return null; diff --git a/components/medialists/MediaListSection.tsx b/components/medialists/MediaListSection.tsx index 93b7a5c0..8474b866 100644 --- a/components/medialists/MediaListSection.tsx +++ b/components/medialists/MediaListSection.tsx @@ -31,10 +31,10 @@ export const MediaListSection: React.FC = ({ const [api] = useAtom(apiAtom); const [user] = useAtom(userAtom); - const { data: collection, isLoading } = useQuery({ + const { data: collection } = useQuery({ queryKey, queryFn, - staleTime: 60 * 1000, + staleTime: 0, }); const fetchItems = useCallback( diff --git a/hooks/useImageColors.ts b/hooks/useImageColors.ts index 0a7cf821..57fb6c80 100644 --- a/hooks/useImageColors.ts +++ b/hooks/useImageColors.ts @@ -8,7 +8,7 @@ import { import { getItemImage } from "@/utils/getItemImage"; import { storage } from "@/utils/mmkv"; import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client"; -import { useAtom } from "jotai"; +import { useAtom, useAtomValue } from "jotai"; import { useEffect, useMemo } from "react"; import { getColors } from "react-native-image-colors"; @@ -28,7 +28,7 @@ export const useImageColors = ({ url?: string | null; disabled?: boolean; }) => { - const [api] = useAtom(apiAtom); + const api = useAtomValue(apiAtom); const [, setPrimaryColor] = useAtom(itemThemeColorAtom); const source = useMemo(() => { @@ -42,7 +42,7 @@ export const useImageColors = ({ quality: 80, width: 300, }); - else return; + else return null; }, [api, item]); useEffect(() => { @@ -54,7 +54,7 @@ export const useImageColors = ({ // If colors are cached, use them and exit if (_primary && _text) { - console.info("[useImageColors] Using cached colors for performance."); + console.info("useImageColors ~ Using cached colors for performance."); setPrimaryColor({ primary: _primary, text: _text, @@ -65,22 +65,25 @@ export const useImageColors = ({ // Extract colors from the image getColors(source.uri, { fallback: "#fff", - cache: true, - key: source.uri, + cache: false, }) .then((colors) => { let primary: string = "#fff"; let text: string = "#000"; + let backup: string = "#fff"; // Select the appropriate color based on the platform if (colors.platform === "android") { primary = colors.dominant; + backup = colors.vibrant; } else if (colors.platform === "ios") { - primary = colors.primary; + primary = colors.detail; + backup = colors.primary; } // Adjust the primary color if it's too close to black if (primary && isCloseToBlack(primary)) { + if (backup && !isCloseToBlack(backup)) primary = backup; primary = adjustToNearBlack(primary); } diff --git a/providers/PlaySettingsProvider.tsx b/providers/PlaySettingsProvider.tsx index aef28a13..7e07fcdb 100644 --- a/providers/PlaySettingsProvider.tsx +++ b/providers/PlaySettingsProvider.tsx @@ -95,8 +95,6 @@ export const PlaySettingsProvider: React.FC<{ children: React.ReactNode }> = ({ if (settings?.deviceProfile === "Native") deviceProfile = native; if (settings?.deviceProfile === "Old") deviceProfile = old; - console.log("Selected sub index: ", newSettings?.subtitleIndex); - try { const data = await getStreamUrl({ api, @@ -108,8 +106,7 @@ export const PlaySettingsProvider: React.FC<{ children: React.ReactNode }> = ({ audioStreamIndex: newSettings?.audioIndex ?? 0, subtitleStreamIndex: newSettings?.subtitleIndex ?? -1, userId: user.Id, - forceDirectPlay: false, - sessionData: null, + forceDirectPlay: settings.forceDirectPlay, }); console.log("getStreamUrl ~ ", data?.url); diff --git a/utils/atoms/primaryColor.ts b/utils/atoms/primaryColor.ts index 4422dee9..cd348ca5 100644 --- a/utils/atoms/primaryColor.ts +++ b/utils/atoms/primaryColor.ts @@ -56,7 +56,7 @@ export const isCloseToBlack = (color: string): boolean => { }; export const adjustToNearBlack = (color: string): string => { - return "#212121"; // A very dark gray, almost black + return "#313131"; // A very dark gray, almost black }; export const itemThemeColorAtom = atom({