mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-01-15 15:48:05 +00:00
feat: prefer downloaded file (#1217)
Some checks failed
🏗️ Build Apps / 🤖 Build Android APK (Phone) (push) Has been cancelled
🏗️ Build Apps / 🤖 Build Android APK (TV) (push) Has been cancelled
🏗️ Build Apps / 🍎 Build iOS IPA (Phone) (push) Has been cancelled
🔒 Lockfile Consistency Check / 🔍 Check bun.lock and package.json consistency (push) Has been cancelled
🛡️ CodeQL Analysis / 🔎 Analyze with CodeQL (actions) (push) Has been cancelled
🛡️ CodeQL Analysis / 🔎 Analyze with CodeQL (javascript-typescript) (push) Has been cancelled
🏷️🔀Merge Conflict Labeler / 🏷️ Labeling Merge Conflicts (push) Has been cancelled
🚦 Security & Quality Gate / 🚑 Expo Doctor Check (push) Has been cancelled
🚦 Security & Quality Gate / 🔍 Lint & Test (check) (push) Has been cancelled
🚦 Security & Quality Gate / 📝 Validate PR Title (push) Has been cancelled
🚦 Security & Quality Gate / 🔍 Vulnerable Dependencies (push) Has been cancelled
🚦 Security & Quality Gate / 🔍 Lint & Test (format) (push) Has been cancelled
🚦 Security & Quality Gate / 🔍 Lint & Test (lint) (push) Has been cancelled
🚦 Security & Quality Gate / 🔍 Lint & Test (typecheck) (push) Has been cancelled
🌐 Translation Sync / sync-translations (push) Has been cancelled
🕒 Handle Stale Issues / 🗑️ Cleanup Stale Issues (push) Has been cancelled
Some checks failed
🏗️ Build Apps / 🤖 Build Android APK (Phone) (push) Has been cancelled
🏗️ Build Apps / 🤖 Build Android APK (TV) (push) Has been cancelled
🏗️ Build Apps / 🍎 Build iOS IPA (Phone) (push) Has been cancelled
🔒 Lockfile Consistency Check / 🔍 Check bun.lock and package.json consistency (push) Has been cancelled
🛡️ CodeQL Analysis / 🔎 Analyze with CodeQL (actions) (push) Has been cancelled
🛡️ CodeQL Analysis / 🔎 Analyze with CodeQL (javascript-typescript) (push) Has been cancelled
🏷️🔀Merge Conflict Labeler / 🏷️ Labeling Merge Conflicts (push) Has been cancelled
🚦 Security & Quality Gate / 🚑 Expo Doctor Check (push) Has been cancelled
🚦 Security & Quality Gate / 🔍 Lint & Test (check) (push) Has been cancelled
🚦 Security & Quality Gate / 📝 Validate PR Title (push) Has been cancelled
🚦 Security & Quality Gate / 🔍 Vulnerable Dependencies (push) Has been cancelled
🚦 Security & Quality Gate / 🔍 Lint & Test (format) (push) Has been cancelled
🚦 Security & Quality Gate / 🔍 Lint & Test (lint) (push) Has been cancelled
🚦 Security & Quality Gate / 🔍 Lint & Test (typecheck) (push) Has been cancelled
🌐 Translation Sync / sync-translations (push) Has been cancelled
🕒 Handle Stale Issues / 🗑️ Cleanup Stale Issues (push) Has been cancelled
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
06349a4319
commit
3c57829360
@@ -2,7 +2,6 @@ import type React from "react";
|
|||||||
import {
|
import {
|
||||||
type PropsWithChildren,
|
type PropsWithChildren,
|
||||||
type ReactNode,
|
type ReactNode,
|
||||||
useMemo,
|
|
||||||
useRef,
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
} from "react";
|
} from "react";
|
||||||
@@ -18,6 +17,58 @@ import {
|
|||||||
import { useHaptic } from "@/hooks/useHaptic";
|
import { useHaptic } from "@/hooks/useHaptic";
|
||||||
import { Loader } from "./Loader";
|
import { Loader } from "./Loader";
|
||||||
|
|
||||||
|
const getColorClasses = (
|
||||||
|
color: "purple" | "red" | "black" | "transparent" | "white",
|
||||||
|
variant: "solid" | "border",
|
||||||
|
focused: boolean,
|
||||||
|
): string => {
|
||||||
|
if (variant === "border") {
|
||||||
|
switch (color) {
|
||||||
|
case "purple":
|
||||||
|
return focused
|
||||||
|
? "bg-transparent border-2 border-purple-400"
|
||||||
|
: "bg-transparent border-2 border-purple-600";
|
||||||
|
case "red":
|
||||||
|
return focused
|
||||||
|
? "bg-transparent border-2 border-red-400"
|
||||||
|
: "bg-transparent border-2 border-red-600";
|
||||||
|
case "black":
|
||||||
|
return focused
|
||||||
|
? "bg-transparent border-2 border-neutral-700"
|
||||||
|
: "bg-transparent border-2 border-neutral-900";
|
||||||
|
case "white":
|
||||||
|
return focused
|
||||||
|
? "bg-transparent border-2 border-gray-100"
|
||||||
|
: "bg-transparent border-2 border-white";
|
||||||
|
case "transparent":
|
||||||
|
return focused
|
||||||
|
? "bg-transparent border-2 border-gray-400"
|
||||||
|
: "bg-transparent border-2 border-gray-600";
|
||||||
|
default:
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
switch (color) {
|
||||||
|
case "purple":
|
||||||
|
return focused
|
||||||
|
? "bg-purple-500 border-2 border-white"
|
||||||
|
: "bg-purple-600 border border-purple-700";
|
||||||
|
case "red":
|
||||||
|
return "bg-red-600";
|
||||||
|
case "black":
|
||||||
|
return "bg-neutral-900";
|
||||||
|
case "white":
|
||||||
|
return focused
|
||||||
|
? "bg-gray-100 border-2 border-gray-300"
|
||||||
|
: "bg-white border border-gray-200";
|
||||||
|
case "transparent":
|
||||||
|
return "bg-transparent";
|
||||||
|
default:
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export interface ButtonProps
|
export interface ButtonProps
|
||||||
extends React.ComponentProps<typeof TouchableOpacity> {
|
extends React.ComponentProps<typeof TouchableOpacity> {
|
||||||
onPress?: () => void;
|
onPress?: () => void;
|
||||||
@@ -26,7 +77,8 @@ export interface ButtonProps
|
|||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
children?: string | ReactNode;
|
children?: string | ReactNode;
|
||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
color?: "purple" | "red" | "black" | "transparent";
|
color?: "purple" | "red" | "black" | "transparent" | "white";
|
||||||
|
variant?: "solid" | "border";
|
||||||
iconRight?: ReactNode;
|
iconRight?: ReactNode;
|
||||||
iconLeft?: ReactNode;
|
iconLeft?: ReactNode;
|
||||||
justify?: "center" | "between";
|
justify?: "center" | "between";
|
||||||
@@ -39,6 +91,7 @@ export const Button: React.FC<PropsWithChildren<ButtonProps>> = ({
|
|||||||
disabled = false,
|
disabled = false,
|
||||||
loading = false,
|
loading = false,
|
||||||
color = "purple",
|
color = "purple",
|
||||||
|
variant = "solid",
|
||||||
iconRight,
|
iconRight,
|
||||||
iconLeft,
|
iconLeft,
|
||||||
children,
|
children,
|
||||||
@@ -56,23 +109,13 @@ export const Button: React.FC<PropsWithChildren<ButtonProps>> = ({
|
|||||||
useNativeDriver: true,
|
useNativeDriver: true,
|
||||||
}).start();
|
}).start();
|
||||||
|
|
||||||
const colorClasses = useMemo(() => {
|
const colorClasses = getColorClasses(color, variant, focused);
|
||||||
switch (color) {
|
|
||||||
case "purple":
|
|
||||||
return focused
|
|
||||||
? "bg-purple-500 border-2 border-white"
|
|
||||||
: "bg-purple-600 border border-purple-700";
|
|
||||||
case "red":
|
|
||||||
return "bg-red-600";
|
|
||||||
case "black":
|
|
||||||
return "bg-neutral-900";
|
|
||||||
case "transparent":
|
|
||||||
return "bg-transparent";
|
|
||||||
}
|
|
||||||
}, [color, focused]);
|
|
||||||
|
|
||||||
const lightHapticFeedback = useHaptic("light");
|
const lightHapticFeedback = useHaptic("light");
|
||||||
|
|
||||||
|
const textColorClass =
|
||||||
|
color === "white" && variant === "solid" ? "text-black" : "text-white";
|
||||||
|
|
||||||
return Platform.isTV ? (
|
return Platform.isTV ? (
|
||||||
<Pressable
|
<Pressable
|
||||||
className='w-full'
|
className='w-full'
|
||||||
@@ -98,10 +141,12 @@ export const Button: React.FC<PropsWithChildren<ButtonProps>> = ({
|
|||||||
>
|
>
|
||||||
<View
|
<View
|
||||||
className={`rounded-2xl py-5 items-center justify-center
|
className={`rounded-2xl py-5 items-center justify-center
|
||||||
${focused ? "bg-purple-500 border-2 border-white" : "bg-purple-600 border border-purple-700"}
|
${colorClasses}
|
||||||
${className}`}
|
${className}`}
|
||||||
>
|
>
|
||||||
<Text className='text-white text-xl font-bold'>{children}</Text>
|
<Text className={`${textColorClass} text-xl font-bold`}>
|
||||||
|
{children}
|
||||||
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
@@ -135,7 +180,7 @@ export const Button: React.FC<PropsWithChildren<ButtonProps>> = ({
|
|||||||
{iconLeft ? iconLeft : <View className='w-4' />}
|
{iconLeft ? iconLeft : <View className='w-4' />}
|
||||||
<Text
|
<Text
|
||||||
className={`
|
className={`
|
||||||
text-white font-bold text-base
|
${textColorClass} font-bold text-base
|
||||||
${disabled ? "text-gray-300" : ""}
|
${disabled ? "text-gray-300" : ""}
|
||||||
${textClassName}
|
${textClassName}
|
||||||
${iconRight ? "mr-2" : ""}
|
${iconRight ? "mr-2" : ""}
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import { useActionSheet } from "@expo/react-native-action-sheet";
|
import { useActionSheet } from "@expo/react-native-action-sheet";
|
||||||
import { Feather, Ionicons, MaterialCommunityIcons } from "@expo/vector-icons";
|
import { Feather, Ionicons, MaterialCommunityIcons } from "@expo/vector-icons";
|
||||||
|
import { BottomSheetView } from "@gorhom/bottom-sheet";
|
||||||
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client";
|
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client";
|
||||||
import { useRouter } from "expo-router";
|
import { useRouter } from "expo-router";
|
||||||
import { useAtom, useAtomValue } from "jotai";
|
import { useAtom, useAtomValue } from "jotai";
|
||||||
import { useCallback, useEffect } from "react";
|
import { useCallback, useEffect } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Alert, TouchableOpacity, View } from "react-native";
|
import { Alert, Platform, TouchableOpacity, View } from "react-native";
|
||||||
import CastContext, {
|
import CastContext, {
|
||||||
CastButton,
|
CastButton,
|
||||||
PlayServicesState,
|
PlayServicesState,
|
||||||
@@ -24,6 +25,8 @@ import Animated, {
|
|||||||
} from "react-native-reanimated";
|
} from "react-native-reanimated";
|
||||||
import { useHaptic } from "@/hooks/useHaptic";
|
import { useHaptic } from "@/hooks/useHaptic";
|
||||||
import type { ThemeColors } from "@/hooks/useImageColorsReturn";
|
import type { ThemeColors } from "@/hooks/useImageColorsReturn";
|
||||||
|
import { getDownloadedItemById } from "@/providers/Downloads/database";
|
||||||
|
import { useGlobalModal } from "@/providers/GlobalModalProvider";
|
||||||
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
|
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
|
||||||
import { itemThemeColorAtom } from "@/utils/atoms/primaryColor";
|
import { itemThemeColorAtom } from "@/utils/atoms/primaryColor";
|
||||||
import { useSettings } from "@/utils/atoms/settings";
|
import { useSettings } from "@/utils/atoms/settings";
|
||||||
@@ -33,6 +36,8 @@ import { getStreamUrl } from "@/utils/jellyfin/media/getStreamUrl";
|
|||||||
import { chromecast } from "@/utils/profiles/chromecast";
|
import { chromecast } from "@/utils/profiles/chromecast";
|
||||||
import { chromecasth265 } from "@/utils/profiles/chromecasth265";
|
import { chromecasth265 } from "@/utils/profiles/chromecasth265";
|
||||||
import { runtimeTicksToMinutes } from "@/utils/time";
|
import { runtimeTicksToMinutes } from "@/utils/time";
|
||||||
|
import { Button } from "./Button";
|
||||||
|
import { Text } from "./common/Text";
|
||||||
import type { SelectedOptions } from "./ItemContent";
|
import type { SelectedOptions } from "./ItemContent";
|
||||||
|
|
||||||
interface Props extends React.ComponentProps<typeof TouchableOpacity> {
|
interface Props extends React.ComponentProps<typeof TouchableOpacity> {
|
||||||
@@ -55,6 +60,7 @@ export const PlayButton: React.FC<Props> = ({
|
|||||||
const client = useRemoteMediaClient();
|
const client = useRemoteMediaClient();
|
||||||
const mediaStatus = useMediaStatus();
|
const mediaStatus = useMediaStatus();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const { showModal, hideModal } = useGlobalModal();
|
||||||
|
|
||||||
const [globalColorAtom] = useAtom(itemThemeColorAtom);
|
const [globalColorAtom] = useAtom(itemThemeColorAtom);
|
||||||
const api = useAtomValue(apiAtom);
|
const api = useAtomValue(apiAtom);
|
||||||
@@ -84,12 +90,9 @@ export const PlayButton: React.FC<Props> = ({
|
|||||||
[router, isOffline],
|
[router, isOffline],
|
||||||
);
|
);
|
||||||
|
|
||||||
const onPress = useCallback(async () => {
|
const handleNormalPlayFlow = useCallback(async () => {
|
||||||
console.log("onPress");
|
|
||||||
if (!item) return;
|
if (!item) return;
|
||||||
|
|
||||||
lightHapticFeedback();
|
|
||||||
|
|
||||||
const queryParams = new URLSearchParams({
|
const queryParams = new URLSearchParams({
|
||||||
itemId: item.Id!,
|
itemId: item.Id!,
|
||||||
audioIndex: selectedOptions.audioIndex?.toString() ?? "",
|
audioIndex: selectedOptions.audioIndex?.toString() ?? "",
|
||||||
@@ -271,6 +274,118 @@ export const PlayButton: React.FC<Props> = ({
|
|||||||
showActionSheetWithOptions,
|
showActionSheetWithOptions,
|
||||||
mediaStatus,
|
mediaStatus,
|
||||||
selectedOptions,
|
selectedOptions,
|
||||||
|
goToPlayer,
|
||||||
|
isOffline,
|
||||||
|
t,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const onPress = useCallback(async () => {
|
||||||
|
console.log("onPress");
|
||||||
|
if (!item) return;
|
||||||
|
|
||||||
|
lightHapticFeedback();
|
||||||
|
|
||||||
|
// Check if item is downloaded
|
||||||
|
const downloadedItem = item.Id ? getDownloadedItemById(item.Id) : undefined;
|
||||||
|
|
||||||
|
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(() => {
|
const derivedTargetWidth = useDerivedValue(() => {
|
||||||
|
|||||||
@@ -429,7 +429,12 @@
|
|||||||
"playback_state": "Playback State:",
|
"playback_state": "Playback State:",
|
||||||
"index": "Index:",
|
"index": "Index:",
|
||||||
"continue_watching": "Continue Watching",
|
"continue_watching": "Continue Watching",
|
||||||
"go_back": "Go Back"
|
"go_back": "Go Back",
|
||||||
|
"downloaded_file_title": "You have this file downloaded",
|
||||||
|
"downloaded_file_message": "Do you want to play the downloaded file?",
|
||||||
|
"downloaded_file_yes": "Yes",
|
||||||
|
"downloaded_file_no": "No",
|
||||||
|
"downloaded_file_cancel": "Cancel"
|
||||||
},
|
},
|
||||||
"item_card": {
|
"item_card": {
|
||||||
"next_up": "Next Up",
|
"next_up": "Next Up",
|
||||||
|
|||||||
Reference in New Issue
Block a user