mirror of
https://github.com/streamyfin/streamyfin.git
synced 2025-08-20 18:37:18 +02:00
wip
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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,22 +173,20 @@ 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 }]}>
|
||||
<ItemImage
|
||||
variant={
|
||||
item.Type === "Movie" && logoUrl ? "Backdrop" : "Primary"
|
||||
}
|
||||
item={item}
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
}}
|
||||
/>
|
||||
</Animated.View>
|
||||
</>
|
||||
<View style={[{ flex: 1 }]}>
|
||||
<ItemImage
|
||||
variant={
|
||||
item.Type === "Movie" && logoUrl ? "Backdrop" : "Primary"
|
||||
}
|
||||
item={item}
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
}
|
||||
logo={
|
||||
<>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -44,6 +44,9 @@ export const PlayedStatus: React.FC<Props> = ({ item, ...props }) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["nextUp-all"],
|
||||
});
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["home"],
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -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 }) => {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>({
|
||||
|
||||
Reference in New Issue
Block a user