This commit is contained in:
Fredrik Burmester
2024-10-08 18:43:25 +02:00
parent ec0843d737
commit 317e719460
12 changed files with 100 additions and 79 deletions

View File

@@ -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);

View File

@@ -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,

View File

@@ -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<ContinueWatchingPosterProps> = ({
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<ContinueWatchingPosterProps> = ({
} else {
return item.UserData?.PlayedPercentage || 0;
}
}, []);
}, [item]);
if (!url)
return (

View File

@@ -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(
</View>
),
});
// 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,10 +173,9 @@ export const ItemContent: React.FC<{ item: BaseItemDto }> = React.memo(
>
<ParallaxScrollView
className={`flex-1 ${loading ? "opacity-0" : "opacity-100"}`}
headerHeight={headerHeightRef.current}
headerHeight={headerHeight}
headerImage={
<>
<Animated.View style={[{ flex: 1 }]}>
<View style={[{ flex: 1 }]}>
<ItemImage
variant={
item.Type === "Movie" && logoUrl ? "Backdrop" : "Primary"
@@ -207,8 +186,7 @@ export const ItemContent: React.FC<{ item: BaseItemDto }> = React.memo(
height: "100%",
}}
/>
</Animated.View>
</>
</View>
}
logo={
<>

View File

@@ -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<typeof Button> {
item?: BaseItemDto | null;
@@ -55,6 +56,7 @@ export const PlayButton: React.FC<Props> = ({ 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<Props> = ({ 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<Props> = ({ item, url, ...props }) => {
<Feather name="cast" size={22} />
</Animated.Text>
)}
{!client && settings?.openInVLC && (
<Animated.Text style={animatedTextStyle}>
<MaterialCommunityIcons
name="vlc"
size={18}
color={animatedTextStyle.color}
/>
</Animated.Text>
)}
</View>
</View>
</TouchableOpacity>

View File

@@ -44,6 +44,9 @@ export const PlayedStatus: React.FC<Props> = ({ item, ...props }) => {
queryClient.invalidateQueries({
queryKey: ["nextUp-all"],
});
queryClient.invalidateQueries({
queryKey: ["home"],
});
};
return (

View File

@@ -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 }) => {

View File

@@ -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<Props> = ({
queryKey,
queryFn,
enabled: !disabled,
staleTime: 60 * 1000,
staleTime: 0,
});
if (disabled || !title) return null;

View File

@@ -31,10 +31,10 @@ export const MediaListSection: React.FC<Props> = ({
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(

View File

@@ -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);
}

View File

@@ -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);

View File

@@ -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<ThemeColors>({