mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-04-21 00:04:42 +01:00
feat: use custom google cast player instead of default controls
This commit is contained in:
@@ -15,6 +15,7 @@ import GoogleCast, {
|
||||
} from "react-native-google-cast";
|
||||
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
|
||||
import { RoundButton } from "./RoundButton";
|
||||
import { useRouter } from "expo-router";
|
||||
|
||||
export function Chromecast({
|
||||
width = 48,
|
||||
@@ -33,6 +34,8 @@ export function Chromecast({
|
||||
|
||||
const lastReportedProgressRef = useRef(0);
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
if (!discoveryManager) {
|
||||
@@ -121,7 +124,7 @@ export function Chromecast({
|
||||
className='mr-2'
|
||||
background={false}
|
||||
onPress={() => {
|
||||
if (mediaStatus?.currentItemId) CastContext.showExpandedControls();
|
||||
if (mediaStatus?.currentItemId) router.push('/player/google-cast-player');
|
||||
else CastContext.showCastDialog();
|
||||
}}
|
||||
{...props}
|
||||
@@ -135,7 +138,7 @@ export function Chromecast({
|
||||
<RoundButton
|
||||
size='large'
|
||||
onPress={() => {
|
||||
if (mediaStatus?.currentItemId) CastContext.showExpandedControls();
|
||||
if (mediaStatus?.currentItemId) router.push('/player/google-cast-player');
|
||||
else CastContext.showCastDialog();
|
||||
}}
|
||||
{...props}
|
||||
|
||||
@@ -1,14 +1,20 @@
|
||||
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
|
||||
import { itemThemeColorAtom } from "@/utils/atoms/primaryColor";
|
||||
import { useSettings } from "@/utils/atoms/settings";
|
||||
import { getParentBackdropImageUrl } from "@/utils/jellyfin/image/getParentBackdropImageUrl";
|
||||
import { getPrimaryImageUrl } from "@/utils/jellyfin/image/getPrimaryImageUrl";
|
||||
import { getStreamUrl } from "@/utils/jellyfin/media/getStreamUrl";
|
||||
import ios from "@/utils/profiles/ios";
|
||||
import { runtimeTicksToMinutes } from "@/utils/time";
|
||||
import { useActionSheet } from "@expo/react-native-action-sheet";
|
||||
import { Feather, Ionicons } from "@expo/vector-icons";
|
||||
import { BottomSheetView } from "@gorhom/bottom-sheet";
|
||||
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client";
|
||||
import { Feather, Ionicons, MaterialCommunityIcons } from "@expo/vector-icons";
|
||||
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client";
|
||||
import { useRouter } from "expo-router";
|
||||
import { useAtom, useAtomValue } from "jotai";
|
||||
import { useCallback, useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Alert, Platform, TouchableOpacity, View } from "react-native";
|
||||
import { Alert, TouchableOpacity, View } from "react-native";
|
||||
import CastContext, {
|
||||
CastButton,
|
||||
MediaStreamType,
|
||||
PlayServicesState,
|
||||
useMediaStatus,
|
||||
useRemoteMediaClient,
|
||||
@@ -23,29 +29,15 @@ import Animated, {
|
||||
useSharedValue,
|
||||
withTiming,
|
||||
} from "react-native-reanimated";
|
||||
import useRouter from "@/hooks/useAppRouter";
|
||||
import { useHaptic } from "@/hooks/useHaptic";
|
||||
import type { ThemeColors } from "@/hooks/useImageColorsReturn";
|
||||
import { getDownloadedItemById } from "@/providers/Downloads/database";
|
||||
import { useGlobalModal } from "@/providers/GlobalModalProvider";
|
||||
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
|
||||
import { useOfflineMode } from "@/providers/OfflineModeProvider";
|
||||
import { itemThemeColorAtom } from "@/utils/atoms/primaryColor";
|
||||
import { useSettings } from "@/utils/atoms/settings";
|
||||
import { getParentBackdropImageUrl } from "@/utils/jellyfin/image/getParentBackdropImageUrl";
|
||||
import { getPrimaryImageUrl } from "@/utils/jellyfin/image/getPrimaryImageUrl";
|
||||
import { getStreamUrl } from "@/utils/jellyfin/media/getStreamUrl";
|
||||
import { chromecast } from "@/utils/profiles/chromecast";
|
||||
import { chromecasth265 } from "@/utils/profiles/chromecasth265";
|
||||
import { runtimeTicksToMinutes } from "@/utils/time";
|
||||
import { Button } from "./Button";
|
||||
import { Text } from "./common/Text";
|
||||
import type { SelectedOptions } from "./ItemContent";
|
||||
import { SelectedOptions } from "./ItemContent";
|
||||
import { chromecastProfile } from "@/utils/profiles/chromecast";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useHaptic } from "@/hooks/useHaptic";
|
||||
|
||||
interface Props extends React.ComponentProps<typeof TouchableOpacity> {
|
||||
interface Props extends React.ComponentProps<typeof Button> {
|
||||
item: BaseItemDto;
|
||||
selectedOptions: SelectedOptions;
|
||||
colors?: ThemeColors;
|
||||
}
|
||||
|
||||
const ANIMATION_DURATION = 500;
|
||||
@@ -54,60 +46,56 @@ const MIN_PLAYBACK_WIDTH = 15;
|
||||
export const PlayButton: React.FC<Props> = ({
|
||||
item,
|
||||
selectedOptions,
|
||||
colors,
|
||||
...props
|
||||
}: Props) => {
|
||||
const isOffline = useOfflineMode();
|
||||
const { showActionSheetWithOptions } = useActionSheet();
|
||||
const client = useRemoteMediaClient();
|
||||
const mediaStatus = useMediaStatus();
|
||||
const { t } = useTranslation();
|
||||
const { showModal, hideModal } = useGlobalModal();
|
||||
|
||||
const [globalColorAtom] = useAtom(itemThemeColorAtom);
|
||||
const [colorAtom] = useAtom(itemThemeColorAtom);
|
||||
const api = useAtomValue(apiAtom);
|
||||
const user = useAtomValue(userAtom);
|
||||
|
||||
// Use colors prop if provided, otherwise fallback to global atom
|
||||
const effectiveColors = colors || globalColorAtom;
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const startWidth = useSharedValue(0);
|
||||
const targetWidth = useSharedValue(0);
|
||||
const endColor = useSharedValue(effectiveColors);
|
||||
const startColor = useSharedValue(effectiveColors);
|
||||
const endColor = useSharedValue(colorAtom);
|
||||
const startColor = useSharedValue(colorAtom);
|
||||
const widthProgress = useSharedValue(0);
|
||||
const colorChangeProgress = useSharedValue(0);
|
||||
const { settings, updateSettings } = useSettings();
|
||||
const [settings] = useSettings();
|
||||
const lightHapticFeedback = useHaptic("light");
|
||||
|
||||
const goToPlayer = useCallback(
|
||||
(q: string) => {
|
||||
if (settings.maxAutoPlayEpisodeCount.value !== -1) {
|
||||
updateSettings({ autoPlayEpisodeCount: 0 });
|
||||
(q: string, bitrateValue: number | undefined) => {
|
||||
if (!bitrateValue) {
|
||||
router.push(`/player/direct-player?${q}`);
|
||||
return;
|
||||
}
|
||||
router.push(`/player/direct-player?${q}`);
|
||||
router.push(`/player/transcoding-player?${q}`);
|
||||
},
|
||||
[router, isOffline],
|
||||
[router]
|
||||
);
|
||||
|
||||
const handleNormalPlayFlow = useCallback(async () => {
|
||||
const onPress = useCallback(async () => {
|
||||
if (!item) return;
|
||||
|
||||
lightHapticFeedback();
|
||||
|
||||
const queryParams = new URLSearchParams({
|
||||
itemId: item.Id!,
|
||||
audioIndex: selectedOptions.audioIndex?.toString() ?? "",
|
||||
subtitleIndex: selectedOptions.subtitleIndex?.toString() ?? "",
|
||||
mediaSourceId: selectedOptions.mediaSource?.Id ?? "",
|
||||
bitrateValue: selectedOptions.bitrate?.value?.toString() ?? "",
|
||||
playbackPosition: item.UserData?.PlaybackPositionTicks?.toString() ?? "0",
|
||||
offline: isOffline ? "true" : "false",
|
||||
});
|
||||
|
||||
const queryString = queryParams.toString();
|
||||
|
||||
if (!client) {
|
||||
goToPlayer(queryString);
|
||||
goToPlayer(queryString, selectedOptions.bitrate?.value);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -127,155 +115,106 @@ export const PlayButton: React.FC<Props> = ({
|
||||
switch (selectedIndex) {
|
||||
case 0:
|
||||
await CastContext.getPlayServicesState().then(async (state) => {
|
||||
if (state && state !== PlayServicesState.SUCCESS) {
|
||||
if (state && state !== PlayServicesState.SUCCESS)
|
||||
CastContext.showPlayServicesErrorDialog(state);
|
||||
} else {
|
||||
// Check if user wants H265 for Chromecast
|
||||
const enableH265 = settings.enableH265ForChromecast;
|
||||
else {
|
||||
// Get a new URL with the Chromecast device profile:
|
||||
const data = await getStreamUrl({
|
||||
api,
|
||||
item,
|
||||
deviceProfile: chromecastProfile,
|
||||
startTimeTicks: item?.UserData?.PlaybackPositionTicks!,
|
||||
userId: user?.Id,
|
||||
audioStreamIndex: selectedOptions.audioIndex,
|
||||
maxStreamingBitrate: selectedOptions.bitrate?.value,
|
||||
mediaSourceId: selectedOptions.mediaSource?.Id,
|
||||
subtitleStreamIndex: selectedOptions.subtitleIndex,
|
||||
});
|
||||
|
||||
// Validate required parameters before calling getStreamUrl
|
||||
if (!api) {
|
||||
console.warn("API not available for Chromecast streaming");
|
||||
if (!data?.url) {
|
||||
console.warn("No URL returned from getStreamUrl", data);
|
||||
Alert.alert(
|
||||
t("player.client_error"),
|
||||
t("player.missing_parameters"),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (!user?.Id) {
|
||||
console.warn(
|
||||
"User not authenticated for Chromecast streaming",
|
||||
);
|
||||
Alert.alert(
|
||||
t("player.client_error"),
|
||||
t("player.missing_parameters"),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (!item?.Id) {
|
||||
console.warn("Item not available for Chromecast streaming");
|
||||
Alert.alert(
|
||||
t("player.client_error"),
|
||||
t("player.missing_parameters"),
|
||||
t("player.could_not_create_stream_for_chromecast")
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Get a new URL with the Chromecast device profile
|
||||
try {
|
||||
const data = await getStreamUrl({
|
||||
api,
|
||||
item,
|
||||
deviceProfile: enableH265 ? chromecasth265 : chromecast,
|
||||
startTimeTicks: item?.UserData?.PlaybackPositionTicks ?? 0,
|
||||
userId: user.Id,
|
||||
audioStreamIndex: selectedOptions.audioIndex,
|
||||
maxStreamingBitrate: selectedOptions.bitrate?.value,
|
||||
mediaSourceId: selectedOptions.mediaSource?.Id,
|
||||
subtitleStreamIndex: selectedOptions.subtitleIndex,
|
||||
});
|
||||
|
||||
console.log("URL: ", data?.url, enableH265);
|
||||
|
||||
if (!data?.url) {
|
||||
console.warn("No URL returned from getStreamUrl", data);
|
||||
Alert.alert(
|
||||
t("player.client_error"),
|
||||
t("player.could_not_create_stream_for_chromecast"),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate start time in seconds from playback position
|
||||
const startTimeSeconds =
|
||||
(item?.UserData?.PlaybackPositionTicks ?? 0) / 10000000;
|
||||
|
||||
// Calculate stream duration in seconds from runtime
|
||||
const streamDurationSeconds = item.RunTimeTicks
|
||||
? item.RunTimeTicks / 10000000
|
||||
: undefined;
|
||||
|
||||
client
|
||||
.loadMedia({
|
||||
mediaInfo: {
|
||||
contentId: item.Id,
|
||||
contentUrl: data?.url,
|
||||
contentType: "video/mp4",
|
||||
streamType: MediaStreamType.BUFFERED,
|
||||
streamDuration: streamDurationSeconds,
|
||||
metadata:
|
||||
item.Type === "Episode"
|
||||
? {
|
||||
type: "tvShow",
|
||||
title: item.Name || "",
|
||||
episodeNumber: item.IndexNumber || 0,
|
||||
seasonNumber: item.ParentIndexNumber || 0,
|
||||
seriesTitle: item.SeriesName || "",
|
||||
images: [
|
||||
{
|
||||
url: getParentBackdropImageUrl({
|
||||
api,
|
||||
item,
|
||||
quality: 90,
|
||||
width: 2000,
|
||||
})!,
|
||||
},
|
||||
],
|
||||
}
|
||||
: item.Type === "Movie"
|
||||
? {
|
||||
type: "movie",
|
||||
title: item.Name || "",
|
||||
subtitle: item.Overview || "",
|
||||
images: [
|
||||
{
|
||||
url: getPrimaryImageUrl({
|
||||
api,
|
||||
item,
|
||||
quality: 90,
|
||||
width: 2000,
|
||||
})!,
|
||||
},
|
||||
],
|
||||
}
|
||||
: {
|
||||
type: "generic",
|
||||
title: item.Name || "",
|
||||
subtitle: item.Overview || "",
|
||||
images: [
|
||||
{
|
||||
url: getPrimaryImageUrl({
|
||||
api,
|
||||
item,
|
||||
quality: 90,
|
||||
width: 2000,
|
||||
})!,
|
||||
},
|
||||
],
|
||||
client
|
||||
.loadMedia({
|
||||
mediaInfo: {
|
||||
contentUrl: data?.url,
|
||||
contentType: "video/mp4",
|
||||
metadata:
|
||||
item.Type === "Episode"
|
||||
? {
|
||||
type: "tvShow",
|
||||
title: item.Name || "",
|
||||
episodeNumber: item.IndexNumber || 0,
|
||||
seasonNumber: item.ParentIndexNumber || 0,
|
||||
seriesTitle: item.SeriesName || "",
|
||||
images: [
|
||||
{
|
||||
url: getParentBackdropImageUrl({
|
||||
api,
|
||||
item,
|
||||
quality: 90,
|
||||
width: 2000,
|
||||
})!,
|
||||
},
|
||||
},
|
||||
startTime: startTimeSeconds,
|
||||
})
|
||||
.then(() => {
|
||||
// state is already set when reopening current media, so skip it here.
|
||||
if (isOpeningCurrentlyPlayingMedia) {
|
||||
return;
|
||||
}
|
||||
CastContext.showExpandedControls();
|
||||
});
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
],
|
||||
}
|
||||
: item.Type === "Movie"
|
||||
? {
|
||||
type: "movie",
|
||||
title: item.Name || "",
|
||||
subtitle: item.Overview || "",
|
||||
images: [
|
||||
{
|
||||
url: getPrimaryImageUrl({
|
||||
api,
|
||||
item,
|
||||
quality: 90,
|
||||
width: 2000,
|
||||
})!,
|
||||
},
|
||||
],
|
||||
}
|
||||
: {
|
||||
type: "generic",
|
||||
title: item.Name || "",
|
||||
subtitle: item.Overview || "",
|
||||
images: [
|
||||
{
|
||||
url: getPrimaryImageUrl({
|
||||
api,
|
||||
item,
|
||||
quality: 90,
|
||||
width: 2000,
|
||||
})!,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
startTime: 0,
|
||||
})
|
||||
.then(() => {
|
||||
// state is already set when reopening current media, so skip it here.
|
||||
if (isOpeningCurrentlyPlayingMedia) {
|
||||
return;
|
||||
}
|
||||
router.push('/player/google-cast-player')
|
||||
});
|
||||
}
|
||||
});
|
||||
break;
|
||||
case 1:
|
||||
goToPlayer(queryString);
|
||||
goToPlayer(queryString, selectedOptions.bitrate?.value);
|
||||
break;
|
||||
case cancelButtonIndex:
|
||||
break;
|
||||
}
|
||||
},
|
||||
}
|
||||
);
|
||||
}, [
|
||||
item,
|
||||
@@ -287,140 +226,16 @@ export const PlayButton: React.FC<Props> = ({
|
||||
showActionSheetWithOptions,
|
||||
mediaStatus,
|
||||
selectedOptions,
|
||||
goToPlayer,
|
||||
isOffline,
|
||||
t,
|
||||
]);
|
||||
|
||||
const onPress = useCallback(async () => {
|
||||
if (!item) return;
|
||||
|
||||
lightHapticFeedback();
|
||||
|
||||
// Check if item is downloaded
|
||||
const downloadedItem = item.Id ? getDownloadedItemById(item.Id) : undefined;
|
||||
|
||||
// If already in offline mode, play downloaded file directly
|
||||
if (isOffline && downloadedItem) {
|
||||
const queryParams = new URLSearchParams({
|
||||
itemId: item.Id!,
|
||||
offline: "true",
|
||||
playbackPosition:
|
||||
item.UserData?.PlaybackPositionTicks?.toString() ?? "0",
|
||||
});
|
||||
goToPlayer(queryParams.toString());
|
||||
return;
|
||||
}
|
||||
|
||||
// If online but file is downloaded, ask user which version to play
|
||||
if (downloadedItem) {
|
||||
if (Platform.OS === "android") {
|
||||
// Show bottom sheet for Android
|
||||
showModal(
|
||||
<BottomSheetView>
|
||||
<View className='px-4 mt-4 mb-12'>
|
||||
<View className='pb-6'>
|
||||
<Text className='text-2xl font-bold mb-2'>
|
||||
{t("player.downloaded_file_title")}
|
||||
</Text>
|
||||
<Text className='opacity-70 text-base'>
|
||||
{t("player.downloaded_file_message")}
|
||||
</Text>
|
||||
</View>
|
||||
<View className='space-y-3'>
|
||||
<Button
|
||||
onPress={() => {
|
||||
hideModal();
|
||||
const queryParams = new URLSearchParams({
|
||||
itemId: item.Id!,
|
||||
offline: "true",
|
||||
playbackPosition:
|
||||
item.UserData?.PlaybackPositionTicks?.toString() ?? "0",
|
||||
});
|
||||
goToPlayer(queryParams.toString());
|
||||
}}
|
||||
color='purple'
|
||||
>
|
||||
{Platform.OS === "android"
|
||||
? "Play downloaded file"
|
||||
: t("player.downloaded_file_yes")}
|
||||
</Button>
|
||||
<Button
|
||||
onPress={() => {
|
||||
hideModal();
|
||||
handleNormalPlayFlow();
|
||||
}}
|
||||
color='white'
|
||||
variant='border'
|
||||
>
|
||||
{Platform.OS === "android"
|
||||
? "Stream file"
|
||||
: t("player.downloaded_file_no")}
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
</BottomSheetView>,
|
||||
{
|
||||
snapPoints: ["35%"],
|
||||
enablePanDownToClose: true,
|
||||
},
|
||||
);
|
||||
} else {
|
||||
// Show alert for iOS
|
||||
Alert.alert(
|
||||
t("player.downloaded_file_title"),
|
||||
t("player.downloaded_file_message"),
|
||||
[
|
||||
{
|
||||
text: t("player.downloaded_file_yes"),
|
||||
onPress: () => {
|
||||
const queryParams = new URLSearchParams({
|
||||
itemId: item.Id!,
|
||||
offline: "true",
|
||||
playbackPosition:
|
||||
item.UserData?.PlaybackPositionTicks?.toString() ?? "0",
|
||||
});
|
||||
goToPlayer(queryParams.toString());
|
||||
},
|
||||
isPreferred: true,
|
||||
},
|
||||
{
|
||||
text: t("player.downloaded_file_no"),
|
||||
onPress: () => {
|
||||
handleNormalPlayFlow();
|
||||
},
|
||||
},
|
||||
{
|
||||
text: t("player.downloaded_file_cancel"),
|
||||
style: "cancel",
|
||||
},
|
||||
],
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// If not downloaded, proceed with normal flow
|
||||
handleNormalPlayFlow();
|
||||
}, [
|
||||
item,
|
||||
lightHapticFeedback,
|
||||
handleNormalPlayFlow,
|
||||
goToPlayer,
|
||||
t,
|
||||
showModal,
|
||||
hideModal,
|
||||
effectiveColors,
|
||||
]);
|
||||
|
||||
const derivedTargetWidth = useDerivedValue(() => {
|
||||
if (!item || !item.RunTimeTicks) return 0;
|
||||
const userData = item.UserData;
|
||||
if (userData?.PlaybackPositionTicks) {
|
||||
if (userData && userData.PlaybackPositionTicks) {
|
||||
return userData.PlaybackPositionTicks > 0
|
||||
? Math.max(
|
||||
(userData.PlaybackPositionTicks / item.RunTimeTicks) * 100,
|
||||
MIN_PLAYBACK_WIDTH,
|
||||
MIN_PLAYBACK_WIDTH
|
||||
)
|
||||
: 0;
|
||||
}
|
||||
@@ -437,11 +252,11 @@ export const PlayButton: React.FC<Props> = ({
|
||||
easing: Easing.bezier(0.7, 0, 0.3, 1.0),
|
||||
});
|
||||
},
|
||||
[item],
|
||||
[item]
|
||||
);
|
||||
|
||||
useAnimatedReaction(
|
||||
() => effectiveColors,
|
||||
() => colorAtom,
|
||||
(newColor) => {
|
||||
endColor.value = newColor;
|
||||
colorChangeProgress.value = 0;
|
||||
@@ -450,19 +265,19 @@ export const PlayButton: React.FC<Props> = ({
|
||||
easing: Easing.bezier(0.9, 0, 0.31, 0.99),
|
||||
});
|
||||
},
|
||||
[effectiveColors],
|
||||
[colorAtom]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const timeout_2 = setTimeout(() => {
|
||||
startColor.value = effectiveColors;
|
||||
startColor.value = colorAtom;
|
||||
startWidth.value = targetWidth.value;
|
||||
}, ANIMATION_DURATION);
|
||||
|
||||
return () => {
|
||||
clearTimeout(timeout_2);
|
||||
};
|
||||
}, [effectiveColors, item]);
|
||||
}, [colorAtom, item]);
|
||||
|
||||
/**
|
||||
* ANIMATED STYLES
|
||||
@@ -471,7 +286,7 @@ export const PlayButton: React.FC<Props> = ({
|
||||
backgroundColor: interpolateColor(
|
||||
colorChangeProgress.value,
|
||||
[0, 1],
|
||||
[startColor.value.primary, endColor.value.primary],
|
||||
[startColor.value.primary, endColor.value.primary]
|
||||
),
|
||||
}));
|
||||
|
||||
@@ -479,7 +294,7 @@ export const PlayButton: React.FC<Props> = ({
|
||||
backgroundColor: interpolateColor(
|
||||
colorChangeProgress.value,
|
||||
[0, 1],
|
||||
[startColor.value.primary, endColor.value.primary],
|
||||
[startColor.value.primary, endColor.value.primary]
|
||||
),
|
||||
}));
|
||||
|
||||
@@ -487,7 +302,7 @@ export const PlayButton: React.FC<Props> = ({
|
||||
width: `${interpolate(
|
||||
widthProgress.value,
|
||||
[0, 1],
|
||||
[startWidth.value, targetWidth.value],
|
||||
[startWidth.value, targetWidth.value]
|
||||
)}%`,
|
||||
}));
|
||||
|
||||
@@ -495,61 +310,83 @@ export const PlayButton: React.FC<Props> = ({
|
||||
color: interpolateColor(
|
||||
colorChangeProgress.value,
|
||||
[0, 1],
|
||||
[startColor.value.text, endColor.value.text],
|
||||
[startColor.value.text, endColor.value.text]
|
||||
),
|
||||
}));
|
||||
/**
|
||||
* *********************
|
||||
*/
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
disabled={!item}
|
||||
accessibilityLabel='Play button'
|
||||
accessibilityHint='Tap to play the media'
|
||||
onPress={onPress}
|
||||
className={"relative flex-1"}
|
||||
>
|
||||
<View className='absolute w-full h-full top-0 left-0 rounded-full z-10 overflow-hidden'>
|
||||
<Animated.View
|
||||
style={[
|
||||
animatedPrimaryStyle,
|
||||
animatedWidthStyle,
|
||||
{
|
||||
height: "100%",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<Animated.View
|
||||
style={[animatedAverageStyle, { opacity: 0.5 }]}
|
||||
className='absolute w-full h-full top-0 left-0 rounded-full'
|
||||
/>
|
||||
<View
|
||||
style={{
|
||||
borderWidth: 1,
|
||||
borderColor: effectiveColors.primary,
|
||||
borderStyle: "solid",
|
||||
}}
|
||||
className='flex flex-row items-center justify-center bg-transparent rounded-full z-20 h-12 w-full '
|
||||
<View>
|
||||
<TouchableOpacity
|
||||
disabled={!item}
|
||||
accessibilityLabel="Play button"
|
||||
accessibilityHint="Tap to play the media"
|
||||
onPress={onPress}
|
||||
className={`relative`}
|
||||
{...props}
|
||||
>
|
||||
<View className='flex flex-row items-center space-x-2'>
|
||||
<Animated.Text style={[animatedTextStyle, { fontWeight: "bold" }]}>
|
||||
{runtimeTicksToMinutes(
|
||||
(item?.RunTimeTicks || 0) -
|
||||
(item?.UserData?.PlaybackPositionTicks || 0),
|
||||
)}
|
||||
{(item?.UserData?.PlaybackPositionTicks || 0) > 0 && " left"}
|
||||
</Animated.Text>
|
||||
<Animated.Text style={animatedTextStyle}>
|
||||
<Ionicons name='play-circle' size={24} />
|
||||
</Animated.Text>
|
||||
{client && (
|
||||
<Animated.Text style={animatedTextStyle}>
|
||||
<Feather name='cast' size={22} />
|
||||
<CastButton tintColor='transparent' />
|
||||
</Animated.Text>
|
||||
)}
|
||||
<View className="absolute w-full h-full top-0 left-0 rounded-xl z-10 overflow-hidden">
|
||||
<Animated.View
|
||||
style={[
|
||||
animatedPrimaryStyle,
|
||||
animatedWidthStyle,
|
||||
{
|
||||
height: "100%",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
|
||||
<Animated.View
|
||||
style={[animatedAverageStyle, { opacity: 0.5 }]}
|
||||
className="absolute w-full h-full top-0 left-0 rounded-xl"
|
||||
/>
|
||||
<View
|
||||
style={{
|
||||
borderWidth: 1,
|
||||
borderColor: colorAtom.primary,
|
||||
borderStyle: "solid",
|
||||
}}
|
||||
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">
|
||||
<Animated.Text style={[animatedTextStyle, { fontWeight: "bold" }]}>
|
||||
{runtimeTicksToMinutes(item?.RunTimeTicks)}
|
||||
</Animated.Text>
|
||||
<Animated.Text style={animatedTextStyle}>
|
||||
<Ionicons name="play-circle" size={24} />
|
||||
</Animated.Text>
|
||||
{client && (
|
||||
<Animated.Text style={animatedTextStyle}>
|
||||
<Feather name="cast" size={22} />
|
||||
<CastButton tintColor="transparent" />
|
||||
</Animated.Text>
|
||||
)}
|
||||
{!client && settings?.openInVLC && (
|
||||
<Animated.Text style={animatedTextStyle}>
|
||||
<MaterialCommunityIcons
|
||||
name="vlc"
|
||||
size={18}
|
||||
color={animatedTextStyle.color}
|
||||
/>
|
||||
</Animated.Text>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
{/* <View className="mt-2 flex flex-row items-center">
|
||||
<Ionicons
|
||||
name="information-circle"
|
||||
size={12}
|
||||
className=""
|
||||
color={"#9BA1A6"}
|
||||
/>
|
||||
<Text className="text-neutral-500 ml-1">
|
||||
{directStream ? "Direct stream" : "Transcoded stream"}
|
||||
</Text>
|
||||
</View> */}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user