mirror of
https://github.com/streamyfin/streamyfin.git
synced 2025-08-20 18:37:18 +02:00
fix: design
This commit is contained in:
@@ -42,7 +42,7 @@ export const AudioTrackSelector: React.FC<Props> = ({
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger>
|
||||
<View className="flex flex-col">
|
||||
<Text className="opacity-50 mb-1 text-xs">Audio streams</Text>
|
||||
<Text className="opacity-50 mb-1 text-xs">Audio</Text>
|
||||
<View className="flex flex-row">
|
||||
<TouchableOpacity className="bg-neutral-900 max-w-32 h-10 rounded-xl border-neutral-900 border px-3 py-2 flex flex-row items-center justify-between">
|
||||
<Text className="">
|
||||
|
||||
@@ -55,7 +55,7 @@ export const BitrateSelector: React.FC<Props> = ({
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger>
|
||||
<View className="flex flex-col">
|
||||
<Text className="opacity-50 mb-1 text-xs">Bitrate</Text>
|
||||
<Text className="opacity-50 mb-1 text-xs">Quality</Text>
|
||||
<View className="flex flex-row">
|
||||
<TouchableOpacity className="bg-neutral-900 h-10 rounded-xl border-neutral-900 border px-3 py-2 flex flex-row items-center justify-between">
|
||||
<Text className="">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { BlurView } from "expo-blur";
|
||||
import React, { useEffect } from "react";
|
||||
import { View } from "react-native";
|
||||
import { View, ViewProps } from "react-native";
|
||||
import GoogleCast, {
|
||||
CastButton,
|
||||
useCastDevice,
|
||||
@@ -8,16 +8,17 @@ import GoogleCast, {
|
||||
useRemoteMediaClient,
|
||||
} from "react-native-google-cast";
|
||||
|
||||
type Props = {
|
||||
interface Props extends ViewProps {
|
||||
width?: number;
|
||||
height?: number;
|
||||
background?: "blur" | "transparent";
|
||||
};
|
||||
}
|
||||
|
||||
export const Chromecast: React.FC<Props> = ({
|
||||
width = 48,
|
||||
height = 48,
|
||||
background = "transparent",
|
||||
...props
|
||||
}) => {
|
||||
const client = useRemoteMediaClient();
|
||||
const castDevice = useCastDevice();
|
||||
@@ -37,7 +38,10 @@ export const Chromecast: React.FC<Props> = ({
|
||||
|
||||
if (background === "transparent")
|
||||
return (
|
||||
<View className=" rounded h-10 aspect-square flex items-center justify-center">
|
||||
<View
|
||||
className=" rounded h-10 aspect-square flex items-center justify-center"
|
||||
{...props}
|
||||
>
|
||||
<CastButton style={{ tintColor: "white", height, width }} />
|
||||
</View>
|
||||
);
|
||||
@@ -46,6 +50,7 @@ export const Chromecast: React.FC<Props> = ({
|
||||
<BlurView
|
||||
intensity={100}
|
||||
className="rounded-full overflow-hidden h-10 aspect-square flex items-center justify-center"
|
||||
{...props}
|
||||
>
|
||||
<CastButton style={{ tintColor: "white", height, width }} />
|
||||
</BlurView>
|
||||
|
||||
@@ -26,7 +26,7 @@ import ios from "@/utils/profiles/ios";
|
||||
import native from "@/utils/profiles/native";
|
||||
import old from "@/utils/profiles/old";
|
||||
|
||||
interface DownloadProps extends TouchableOpacityProps {
|
||||
interface DownloadProps extends ViewProps {
|
||||
item: BaseItemDto;
|
||||
}
|
||||
|
||||
@@ -143,23 +143,19 @@ export const DownloadItem: React.FC<DownloadProps> = ({ item, ...props }) => {
|
||||
enabled: !!item.Id,
|
||||
});
|
||||
|
||||
if (isFetching) {
|
||||
return (
|
||||
<View className="rounded h-10 aspect-square flex items-center justify-center">
|
||||
return (
|
||||
<View
|
||||
className="bg-neutral-800/80 rounded-full h-10 w-10 flex items-center justify-center"
|
||||
{...props}
|
||||
>
|
||||
{isFetching ? (
|
||||
<Loader />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
if (process && process?.item.Id === item.Id) {
|
||||
return (
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
router.push("/downloads");
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
<View className="rounded h-10 aspect-square flex items-center justify-center">
|
||||
) : process && process?.item.Id === item.Id ? (
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
router.push("/downloads");
|
||||
}}
|
||||
>
|
||||
{process.progress === 0 ? (
|
||||
<Loader />
|
||||
) : (
|
||||
@@ -173,61 +169,41 @@ export const DownloadItem: React.FC<DownloadProps> = ({ item, ...props }) => {
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
|
||||
if (queue.some((i) => i.id === item.Id)) {
|
||||
return (
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
router.push("/downloads");
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
<View className="rounded h-10 aspect-square flex items-center justify-center opacity-50">
|
||||
</TouchableOpacity>
|
||||
) : queue.some((i) => i.id === item.Id) ? (
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
router.push("/downloads");
|
||||
}}
|
||||
>
|
||||
<Ionicons name="hourglass" size={24} color="white" />
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
|
||||
if (downloaded) {
|
||||
return (
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
router.push("/downloads");
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
<View className="rounded h-10 aspect-square flex items-center justify-center">
|
||||
</TouchableOpacity>
|
||||
) : downloaded ? (
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
router.push("/downloads");
|
||||
}}
|
||||
>
|
||||
<Ionicons name="cloud-download" size={26} color="#9333ea" />
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
queueActions.enqueue(queue, setQueue, {
|
||||
id: item.Id!,
|
||||
execute: async () => {
|
||||
// await startRemuxing(playbackUrl);
|
||||
if (!settings?.downloadQuality?.value) {
|
||||
throw new Error("No download quality selected");
|
||||
}
|
||||
await initiateDownload(settings?.downloadQuality?.value);
|
||||
},
|
||||
item,
|
||||
});
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
<View className="rounded h-10 aspect-square flex items-center justify-center">
|
||||
<Ionicons name="cloud-download-outline" size={26} color="white" />
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
</TouchableOpacity>
|
||||
) : (
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
queueActions.enqueue(queue, setQueue, {
|
||||
id: item.Id!,
|
||||
execute: async () => {
|
||||
if (!settings?.downloadQuality?.value) {
|
||||
throw new Error("No download quality selected");
|
||||
}
|
||||
await initiateDownload(settings?.downloadQuality?.value);
|
||||
},
|
||||
item,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Ionicons name="cloud-download-outline" size={24} color="white" />
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -5,16 +5,12 @@ import { OverviewText } from "@/components/OverviewText";
|
||||
import { ParallaxScrollView } from "@/components/ParallaxPage";
|
||||
import { PlayButton } from "@/components/PlayButton";
|
||||
import { PlayedStatus } from "@/components/PlayedStatus";
|
||||
import { Ratings } from "@/components/Ratings";
|
||||
import { SimilarItems } from "@/components/SimilarItems";
|
||||
import { SubtitleTrackSelector } from "@/components/SubtitleTrackSelector";
|
||||
import { ItemImage } from "@/components/common/ItemImage";
|
||||
import { Text } from "@/components/common/Text";
|
||||
import { MoviesTitleHeader } from "@/components/movies/MoviesTitleHeader";
|
||||
import { CastAndCrew } from "@/components/series/CastAndCrew";
|
||||
import { CurrentSeries } from "@/components/series/CurrentSeries";
|
||||
import { SeasonEpisodesCarousel } from "@/components/series/SeasonEpisodesCarousel";
|
||||
import { EpisodeTitleHeader } from "@/components/series/EpisodeTitleHeader";
|
||||
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
|
||||
import { useSettings } from "@/utils/atoms/settings";
|
||||
import { getLogoImageUrlById } from "@/utils/jellyfin/image/getLogoImageUrlById";
|
||||
@@ -24,16 +20,19 @@ import { chromecastProfile } from "@/utils/profiles/chromecast";
|
||||
import ios from "@/utils/profiles/ios";
|
||||
import native from "@/utils/profiles/native";
|
||||
import old from "@/utils/profiles/old";
|
||||
import { MediaSourceInfo } from "@jellyfin/sdk/lib/generated-client/models";
|
||||
import { getMediaInfoApi } from "@jellyfin/sdk/lib/utils/api";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { Image } from "expo-image";
|
||||
import { useNavigation } from "expo-router";
|
||||
import { useAtom } from "jotai";
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
import React, { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { View } from "react-native";
|
||||
import { useCastDevice } from "react-native-google-cast";
|
||||
import { Chromecast } from "./Chromecast";
|
||||
import { ItemHeader } from "./ItemHeader";
|
||||
import { MediaSourceSelector } from "./MediaSourceSelector";
|
||||
import { MediaSourceInfo } from "@jellyfin/sdk/lib/generated-client/models";
|
||||
import { useImageColors } from "@/hooks/useImageColors";
|
||||
|
||||
export const ItemContent: React.FC<{ id: string }> = React.memo(({ id }) => {
|
||||
const [api] = useAtom(apiAtom);
|
||||
@@ -41,6 +40,7 @@ export const ItemContent: React.FC<{ id: string }> = React.memo(({ id }) => {
|
||||
|
||||
const [settings] = useSettings();
|
||||
const castDevice = useCastDevice();
|
||||
const navigation = useNavigation();
|
||||
|
||||
const [selectedMediaSource, setSelectedMediaSource] =
|
||||
useState<MediaSourceInfo | null>(null);
|
||||
@@ -52,22 +52,45 @@ export const ItemContent: React.FC<{ id: string }> = React.memo(({ id }) => {
|
||||
value: undefined,
|
||||
});
|
||||
|
||||
const headerHeightRef = useRef(0);
|
||||
|
||||
const {
|
||||
data: item,
|
||||
isLoading,
|
||||
isFetching,
|
||||
} = useQuery({
|
||||
queryKey: ["item", id],
|
||||
queryFn: async () =>
|
||||
await getUserItemData({
|
||||
queryFn: async () => {
|
||||
const res = await getUserItemData({
|
||||
api,
|
||||
userId: user?.Id,
|
||||
itemId: id,
|
||||
}),
|
||||
});
|
||||
|
||||
return res;
|
||||
},
|
||||
enabled: !!id && !!api,
|
||||
staleTime: 60 * 1000,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
navigation.setOptions({
|
||||
headerRight: () =>
|
||||
item && (
|
||||
<View className="flex flex-row items-center space-x-2">
|
||||
<Chromecast background="blur" width={22} height={22} />
|
||||
<DownloadItem item={item} />
|
||||
<PlayedStatus item={item} />
|
||||
</View>
|
||||
),
|
||||
});
|
||||
}, [item]);
|
||||
|
||||
useEffect(() => {
|
||||
if (item?.Type === "Episode") headerHeightRef.current = 400;
|
||||
else headerHeightRef.current = 500;
|
||||
}, [item]);
|
||||
|
||||
const { data: sessionData } = useQuery({
|
||||
queryKey: ["sessionData", item?.Id],
|
||||
queryFn: async () => {
|
||||
@@ -139,12 +162,14 @@ export const ItemContent: React.FC<{ id: string }> = React.memo(({ id }) => {
|
||||
|
||||
return (
|
||||
<ParallaxScrollView
|
||||
headerHeight={300}
|
||||
headerHeight={headerHeightRef.current}
|
||||
headerImage={
|
||||
<>
|
||||
{item ? (
|
||||
<ItemImage
|
||||
variant={item.Type === "Movie" ? "Backdrop" : "Primary"}
|
||||
variant={
|
||||
item.Type === "Movie" && logoUrl ? "Backdrop" : "Primary"
|
||||
}
|
||||
item={item}
|
||||
style={{
|
||||
width: "100%",
|
||||
@@ -179,23 +204,12 @@ export const ItemContent: React.FC<{ id: string }> = React.memo(({ id }) => {
|
||||
</>
|
||||
}
|
||||
>
|
||||
<View className="flex flex-col">
|
||||
<View className="flex flex-col px-4 w-full space-y-1 pt-4">
|
||||
<ItemHeader item={item} />
|
||||
<View className="flex flex-col bg-transparent">
|
||||
<View className="flex flex-col px-4 w-full space-y-2 pt-2 mb-2">
|
||||
<ItemHeader item={item} className="mb-4" />
|
||||
|
||||
{item ? (
|
||||
<View className="flex flex-row justify-between items-center mb-2">
|
||||
<DownloadItem item={item} />
|
||||
<PlayedStatus item={item} />
|
||||
</View>
|
||||
) : (
|
||||
<View>
|
||||
<View className="bg-neutral-950 h-8 w-full rounded-lg my-2"></View>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{item ? (
|
||||
<View className="flex flex-row items-center space-x-2 w-full mb-1">
|
||||
<View className="flex flex-row items-center space-x-2 w-full">
|
||||
<BitrateSelector
|
||||
onChange={(val) => setMaxBitrate(val)}
|
||||
selected={maxBitrate}
|
||||
@@ -227,7 +241,7 @@ export const ItemContent: React.FC<{ id: string }> = React.memo(({ id }) => {
|
||||
</View>
|
||||
)}
|
||||
|
||||
<PlayButton item={item} url={playbackUrl} className="grow mb-2" />
|
||||
<PlayButton item={item} url={playbackUrl} className="grow" />
|
||||
</View>
|
||||
|
||||
{item?.Type === "Episode" && (
|
||||
|
||||
@@ -21,10 +21,10 @@ export const ItemHeader: React.FC<Props> = ({ item, ...props }) => {
|
||||
);
|
||||
|
||||
return (
|
||||
<View {...props}>
|
||||
<View className="flex flex-col" {...props}>
|
||||
<Ratings item={item} className="mb-2" />
|
||||
{item.Type === "Episode" && <EpisodeTitleHeader item={item} />}
|
||||
{item.Type === "Movie" && <MoviesTitleHeader item={item} />}
|
||||
<Ratings item={item} />
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -34,14 +34,14 @@ export const MediaSourceSelector: React.FC<Props> = ({
|
||||
|
||||
useEffect(() => {
|
||||
if (mediaSources?.length) onChange(mediaSources[0]);
|
||||
}, []);
|
||||
}, [mediaSources]);
|
||||
|
||||
return (
|
||||
<View className="flex flex-row items-center justify-between" {...props}>
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger>
|
||||
<View className="flex flex-col">
|
||||
<Text className="opacity-50 mb-1 text-xs">Video streams</Text>
|
||||
<Text className="opacity-50 mb-1 text-xs">Video</Text>
|
||||
<View className="flex flex-row">
|
||||
<TouchableOpacity className="bg-neutral-900 max-w-32 h-10 rounded-xl border-neutral-900 border px-3 py-2 flex flex-row items-center justify-between">
|
||||
<Text className="">{tc(selectedMediaSource, 7)}</Text>
|
||||
@@ -58,7 +58,7 @@ export const MediaSourceSelector: React.FC<Props> = ({
|
||||
collisionPadding={8}
|
||||
sideOffset={8}
|
||||
>
|
||||
<DropdownMenu.Label>Video streams</DropdownMenu.Label>
|
||||
<DropdownMenu.Label>Media sources</DropdownMenu.Label>
|
||||
{mediaSources?.map((source, idx: number) => (
|
||||
<DropdownMenu.Item
|
||||
key={idx.toString()}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { LinearGradient } from "expo-linear-gradient";
|
||||
import { type PropsWithChildren, type ReactElement } from "react";
|
||||
import { View } from "react-native";
|
||||
import Animated, {
|
||||
@@ -57,7 +58,7 @@ export const ParallaxScrollView: React.FC<Props> = ({
|
||||
{logo && (
|
||||
<View
|
||||
style={{
|
||||
top: headerHeight - 150,
|
||||
top: headerHeight - 200,
|
||||
height: 130,
|
||||
}}
|
||||
className="absolute left-0 w-full z-40 px-4 flex justify-center items-center"
|
||||
@@ -66,14 +67,6 @@ export const ParallaxScrollView: React.FC<Props> = ({
|
||||
</View>
|
||||
)}
|
||||
|
||||
{episodePoster && (
|
||||
<View className="absolute top-[290px] h-[120px] w-full left-0 flex justify-center items-center z-50">
|
||||
<View className="h-full aspect-video border border-neutral-800">
|
||||
{episodePoster}
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
|
||||
<Animated.View
|
||||
style={[
|
||||
{
|
||||
@@ -86,7 +79,34 @@ export const ParallaxScrollView: React.FC<Props> = ({
|
||||
{headerImage}
|
||||
</Animated.View>
|
||||
|
||||
<View className="flex-1 overflow-hidden bg-black pb-24">
|
||||
<View
|
||||
style={{
|
||||
top: -50,
|
||||
}}
|
||||
className="relative flex-1 bg-transparent pb-24"
|
||||
>
|
||||
<LinearGradient
|
||||
// Background Linear Gradient
|
||||
colors={["transparent", "rgba(0,0,0,1)"]}
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: -150,
|
||||
height: 200,
|
||||
}}
|
||||
/>
|
||||
<View
|
||||
// Background Linear Gradient
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 50,
|
||||
height: "100%",
|
||||
backgroundColor: "black",
|
||||
}}
|
||||
/>
|
||||
{children}
|
||||
</View>
|
||||
</Animated.ScrollView>
|
||||
|
||||
@@ -11,6 +11,8 @@ import CastContext, {
|
||||
} from "react-native-google-cast";
|
||||
import { Button } from "./Button";
|
||||
import { Text } from "./common/Text";
|
||||
import { useAtom } from "jotai";
|
||||
import { itemThemeColorAtom } from "@/utils/atoms/primaryColor";
|
||||
|
||||
interface Props extends React.ComponentProps<typeof Button> {
|
||||
item?: BaseItemDto | null;
|
||||
@@ -22,6 +24,8 @@ export const PlayButton: React.FC<Props> = ({ item, url, ...props }) => {
|
||||
const client = useRemoteMediaClient();
|
||||
const { setCurrentlyPlayingState } = usePlayback();
|
||||
|
||||
const [color] = useAtom(itemThemeColorAtom);
|
||||
|
||||
const onPress = async () => {
|
||||
if (!url || !item) return;
|
||||
|
||||
@@ -88,23 +92,30 @@ export const PlayButton: React.FC<Props> = ({ item, url, ...props }) => {
|
||||
? "100%"
|
||||
: `${Math.max(playbackPercent, 15)}%`,
|
||||
height: "100%",
|
||||
backgroundColor: color.primary,
|
||||
}}
|
||||
className="absolute w-full h-full top-0 left-0 rounded-xl bg-purple-600 z-10"
|
||||
className="absolute w-full h-full top-0 left-0 rounded-xl z-10"
|
||||
></View>
|
||||
<View
|
||||
style={{
|
||||
height: "100%",
|
||||
width: "100%",
|
||||
backgroundColor: color.primary,
|
||||
}}
|
||||
className="absolute w-full h-full top-0 left-0 rounded-xl bg-purple-500 opacity-40"
|
||||
className="absolute w-full h-full top-0 left-0 rounded-xl opacity-40"
|
||||
></View>
|
||||
<View className="flex flex-row items-center justify-center bg-transparent rounded-xl z-20 h-12 w-full ">
|
||||
<View className="flex flex-row items-center space-x-2">
|
||||
<Text className="font-bold">
|
||||
<Text
|
||||
className="font-bold"
|
||||
style={{
|
||||
color: color.text,
|
||||
}}
|
||||
>
|
||||
{runtimeTicksToMinutes(item?.RunTimeTicks)}
|
||||
</Text>
|
||||
<Ionicons name="play-circle" size={24} color="white" />
|
||||
{client && <Feather name="cast" size={22} color="white" />}
|
||||
<Ionicons name="play-circle" size={24} color={color.text} />
|
||||
{client && <Feather name="cast" size={22} color={color.text} />}
|
||||
</View>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
|
||||
@@ -7,9 +7,13 @@ import { useQueryClient } from "@tanstack/react-query";
|
||||
import * as Haptics from "expo-haptics";
|
||||
import { useAtom } from "jotai";
|
||||
import React from "react";
|
||||
import { TouchableOpacity, View } from "react-native";
|
||||
import { TouchableOpacity, View, ViewProps } from "react-native";
|
||||
|
||||
export const PlayedStatus: React.FC<{ item: BaseItemDto }> = ({ item }) => {
|
||||
interface Props extends ViewProps {
|
||||
item: BaseItemDto;
|
||||
}
|
||||
|
||||
export const PlayedStatus: React.FC<Props> = ({ item, ...props }) => {
|
||||
const [api] = useAtom(apiAtom);
|
||||
const [user] = useAtom(userAtom);
|
||||
|
||||
@@ -37,7 +41,10 @@ export const PlayedStatus: React.FC<{ item: BaseItemDto }> = ({ item }) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<View>
|
||||
<View
|
||||
className=" bg-neutral-800/80 rounded-full h-10 w-10 flex items-center justify-center"
|
||||
{...props}
|
||||
>
|
||||
{item.UserData?.Played ? (
|
||||
<TouchableOpacity
|
||||
onPress={async () => {
|
||||
@@ -51,7 +58,7 @@ export const PlayedStatus: React.FC<{ item: BaseItemDto }> = ({ item }) => {
|
||||
}}
|
||||
>
|
||||
<View className="rounded h-10 aspect-square flex items-center justify-center">
|
||||
<Ionicons name="checkmark-circle" size={30} color="white" />
|
||||
<Ionicons name="checkmark-circle" size={24} color="white" />
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
) : (
|
||||
@@ -67,7 +74,7 @@ export const PlayedStatus: React.FC<{ item: BaseItemDto }> = ({ item }) => {
|
||||
}}
|
||||
>
|
||||
<View className="rounded h-10 aspect-square flex items-center justify-center">
|
||||
<Ionicons name="checkmark-circle-outline" size={30} color="white" />
|
||||
<Ionicons name="checkmark-circle-outline" size={24} color="white" />
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
|
||||
@@ -8,10 +8,10 @@ interface Props extends ViewProps {
|
||||
item?: BaseItemDto | null;
|
||||
}
|
||||
|
||||
export const Ratings: React.FC<Props> = ({ item }) => {
|
||||
export const Ratings: React.FC<Props> = ({ item, ...props }) => {
|
||||
if (!item) return null;
|
||||
return (
|
||||
<View className="flex flex-row items-center justify-center mt-2 space-x-2">
|
||||
<View className="flex flex-row items-center mt-2 space-x-2" {...props}>
|
||||
{item.OfficialRating && (
|
||||
<Badge text={item.OfficialRating} variant="gray" />
|
||||
)}
|
||||
|
||||
@@ -48,7 +48,7 @@ export const SubtitleTrackSelector: React.FC<Props> = ({
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger>
|
||||
<View className="flex flex-col">
|
||||
<Text className="opacity-50 mb-1 text-xs">Subtitles</Text>
|
||||
<Text className="opacity-50 mb-1 text-xs">Subtitle</Text>
|
||||
<View className="flex flex-row">
|
||||
<TouchableOpacity className="bg-neutral-900 max-w-32 h-10 rounded-xl border-neutral-900 border px-3 py-2 flex flex-row items-center justify-between">
|
||||
<Text className="">
|
||||
@@ -69,7 +69,7 @@ export const SubtitleTrackSelector: React.FC<Props> = ({
|
||||
collisionPadding={8}
|
||||
sideOffset={8}
|
||||
>
|
||||
<DropdownMenu.Label>Subtitles</DropdownMenu.Label>
|
||||
<DropdownMenu.Label>Subtitle tracks</DropdownMenu.Label>
|
||||
<DropdownMenu.Item
|
||||
key={"-1"}
|
||||
onSelect={() => {
|
||||
|
||||
@@ -36,7 +36,7 @@ export const HeaderBackButton: React.FC<Props> = ({
|
||||
className="drop-shadow-2xl"
|
||||
name="arrow-back"
|
||||
size={24}
|
||||
color="#077DF2"
|
||||
color="white"
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</BlurView>
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { View, ViewProps } from "react-native";
|
||||
import { Text } from "@/components/common/Text";
|
||||
import { useImageColors } from "@/hooks/useImageColors";
|
||||
import { apiAtom } from "@/providers/JellyfinProvider";
|
||||
import { itemThemeColorAtom } from "@/utils/atoms/primaryColor";
|
||||
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
|
||||
import { Image, ImageProps, ImageSource } from "expo-image";
|
||||
import { useMemo, useState } from "react";
|
||||
import { useAtom } from "jotai";
|
||||
import { apiAtom } from "@/providers/JellyfinProvider";
|
||||
import { useEffect, useMemo } from "react";
|
||||
import { getColors } from "react-native-image-colors";
|
||||
|
||||
interface Props extends ImageProps {
|
||||
item: BaseItemDto;
|
||||
@@ -81,6 +82,8 @@ export const ItemImage: React.FC<Props> = ({
|
||||
return src;
|
||||
}, [item.ImageTags]);
|
||||
|
||||
useImageColors(source?.uri);
|
||||
|
||||
return (
|
||||
<Image
|
||||
transition={300}
|
||||
|
||||
@@ -8,9 +8,9 @@ interface Props extends ViewProps {
|
||||
|
||||
export const MoviesTitleHeader: React.FC<Props> = ({ item, ...props }) => {
|
||||
return (
|
||||
<View className="flex flex-col" {...props}>
|
||||
<Text className="text-center font-bold text-2xl">{item?.Name}</Text>
|
||||
<Text className="text-center opacity-50">{item?.ProductionYear}</Text>
|
||||
<View {...props}>
|
||||
<Text className=" font-bold text-2xl mb-1">{item?.Name}</Text>
|
||||
<Text className=" opacity-50">{item?.ProductionYear}</Text>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -11,14 +11,9 @@ export const EpisodeTitleHeader: React.FC<Props> = ({ item, ...props }) => {
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<View {...props} className="flex flex-col">
|
||||
<TouchableOpacity
|
||||
onPress={() => router.push(`/(auth)/series/${item.SeriesId}`)}
|
||||
>
|
||||
<Text className="text-center opacity-50">{item?.SeriesName}</Text>
|
||||
</TouchableOpacity>
|
||||
<Text className="text-center font-bold text-2xl">{item?.Name}</Text>
|
||||
<View className="flex flex-row items-center self-center">
|
||||
<View {...props}>
|
||||
<Text className="font-bold text-2xl">{item?.Name}</Text>
|
||||
<View className="flex flex-row items-center mb-1">
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
router.push(
|
||||
@@ -27,14 +22,13 @@ export const EpisodeTitleHeader: React.FC<Props> = ({ item, ...props }) => {
|
||||
);
|
||||
}}
|
||||
>
|
||||
<Text className="text-center opacity-50">{item?.SeasonName}</Text>
|
||||
<Text className="opacity-50">{item?.SeasonName}</Text>
|
||||
</TouchableOpacity>
|
||||
<Text className="text-center opacity-50 mx-2">{"—"}</Text>
|
||||
<Text className="text-center opacity-50">
|
||||
{`Episode ${item.IndexNumber}`}
|
||||
</Text>
|
||||
|
||||
<Text className="opacity-50 mx-2">{"—"}</Text>
|
||||
<Text className="opacity-50">{`Episode ${item.IndexNumber}`}</Text>
|
||||
</View>
|
||||
<Text className="text-center opacity-50">{item?.ProductionYear}</Text>
|
||||
<Text className="opacity-50">{item?.ProductionYear}</Text>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -107,14 +107,12 @@ export const SeasonEpisodesCarousel: React.FC<Props> = ({
|
||||
}, [episodes, api, user?.Id, item]);
|
||||
|
||||
useEffect(() => {
|
||||
if (item?.Type === "Episode") {
|
||||
if (item?.Type === "Episode" && item.Id) {
|
||||
const index = episodes?.findIndex((ep) => ep.Id === item.Id);
|
||||
if (index !== undefined && index !== -1) {
|
||||
setTimeout(() => {
|
||||
scrollToIndex(index);
|
||||
}, 400);
|
||||
} else {
|
||||
console.warn("Episode not found in the list:", item.Id);
|
||||
}
|
||||
}
|
||||
}, [episodes, item]);
|
||||
|
||||
@@ -8,7 +8,6 @@ const commonScreenOptions = {
|
||||
headerTransparent: true,
|
||||
headerShadowVisible: false,
|
||||
headerLeft: () => <HeaderBackButton />,
|
||||
headerRight: () => <Chromecast background="blur" width={22} height={22} />,
|
||||
};
|
||||
|
||||
const routes = [
|
||||
|
||||
Reference in New Issue
Block a user