mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-06-29 17:20:30 +01:00
refactor
This commit is contained in:
@@ -1,10 +1,7 @@
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import type { Api } from "@jellyfin/sdk";
|
||||
import type {
|
||||
BaseItemDto,
|
||||
MediaSourceInfo,
|
||||
} from "@jellyfin/sdk/lib/generated-client";
|
||||
import { BlurView } from "expo-blur";
|
||||
import { useLocalSearchParams } from "expo-router";
|
||||
import { useAtomValue } from "jotai";
|
||||
import {
|
||||
@@ -16,17 +13,9 @@ import {
|
||||
useState,
|
||||
} from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
Image,
|
||||
Pressable,
|
||||
Animated as RNAnimated,
|
||||
StyleSheet,
|
||||
View,
|
||||
} from "react-native";
|
||||
import { StyleSheet, View } from "react-native";
|
||||
import Animated, {
|
||||
cancelAnimation,
|
||||
Easing,
|
||||
runOnJS,
|
||||
type SharedValue,
|
||||
useAnimatedReaction,
|
||||
useAnimatedStyle,
|
||||
@@ -35,7 +24,7 @@ import Animated, {
|
||||
} from "react-native-reanimated";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import { Text } from "@/components/common/Text";
|
||||
import { useTVFocusAnimation } from "@/components/tv";
|
||||
import { TVControlButton, TVNextEpisodeCountdown } from "@/components/tv";
|
||||
import useRouter from "@/hooks/useAppRouter";
|
||||
import { usePlaybackManager } from "@/hooks/usePlaybackManager";
|
||||
import { useTrickplay } from "@/hooks/useTrickplay";
|
||||
@@ -45,7 +34,6 @@ import { apiAtom } from "@/providers/JellyfinProvider";
|
||||
import { useSettings } from "@/utils/atoms/settings";
|
||||
import type { TVOptionItem } from "@/utils/atoms/tvOptionModal";
|
||||
import { getDefaultPlaySettings } from "@/utils/jellyfin/getDefaultPlaySettings";
|
||||
import { getPrimaryImageUrl } from "@/utils/jellyfin/image/getPrimaryImageUrl";
|
||||
import { formatTimeString, msToTicks, ticksToMs } from "@/utils/time";
|
||||
import { CONTROLS_CONSTANTS } from "./constants";
|
||||
import { useRemoteControl } from "./hooks/useRemoteControl";
|
||||
@@ -84,214 +72,6 @@ interface Props {
|
||||
const TV_SEEKBAR_HEIGHT = 16;
|
||||
const TV_AUTO_HIDE_TIMEOUT = 5000;
|
||||
|
||||
// TV Control Button for player controls (icon only, no label)
|
||||
const TVControlButton: FC<{
|
||||
icon: keyof typeof Ionicons.glyphMap;
|
||||
onPress: () => void;
|
||||
onLongPress?: () => void;
|
||||
onPressOut?: () => void;
|
||||
disabled?: boolean;
|
||||
hasTVPreferredFocus?: boolean;
|
||||
size?: number;
|
||||
delayLongPress?: number;
|
||||
}> = ({
|
||||
icon,
|
||||
onPress,
|
||||
onLongPress,
|
||||
onPressOut,
|
||||
disabled,
|
||||
hasTVPreferredFocus,
|
||||
size = 32,
|
||||
delayLongPress = 300,
|
||||
}) => {
|
||||
const { focused, handleFocus, handleBlur, animatedStyle } =
|
||||
useTVFocusAnimation({ scaleAmount: 1.15, duration: 120 });
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
onPress={onPress}
|
||||
onLongPress={onLongPress}
|
||||
onPressOut={onPressOut}
|
||||
delayLongPress={delayLongPress}
|
||||
onFocus={handleFocus}
|
||||
onBlur={handleBlur}
|
||||
disabled={disabled}
|
||||
focusable={!disabled}
|
||||
hasTVPreferredFocus={hasTVPreferredFocus && !disabled}
|
||||
>
|
||||
<RNAnimated.View
|
||||
style={[
|
||||
controlButtonStyles.button,
|
||||
animatedStyle,
|
||||
{
|
||||
backgroundColor: focused
|
||||
? "rgba(255,255,255,0.3)"
|
||||
: "rgba(255,255,255,0.1)",
|
||||
borderColor: focused
|
||||
? "rgba(255,255,255,0.8)"
|
||||
: "rgba(255,255,255,0.2)",
|
||||
opacity: disabled ? 0.3 : 1,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Ionicons name={icon} size={size} color='#fff' />
|
||||
</RNAnimated.View>
|
||||
</Pressable>
|
||||
);
|
||||
};
|
||||
|
||||
const controlButtonStyles = StyleSheet.create({
|
||||
button: {
|
||||
width: 64,
|
||||
height: 64,
|
||||
borderRadius: 32,
|
||||
borderWidth: 2,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
},
|
||||
});
|
||||
|
||||
// TV Next Episode Countdown component - horizontal layout with animated progress bar
|
||||
const TVNextEpisodeCountdown: FC<{
|
||||
nextItem: BaseItemDto;
|
||||
api: Api | null;
|
||||
show: boolean;
|
||||
isPlaying: boolean;
|
||||
onFinish: () => void;
|
||||
}> = ({ nextItem, api, show, isPlaying, onFinish }) => {
|
||||
const { t } = useTranslation();
|
||||
const progress = useSharedValue(0);
|
||||
const onFinishRef = useRef(onFinish);
|
||||
|
||||
onFinishRef.current = onFinish;
|
||||
|
||||
const imageUrl = getPrimaryImageUrl({
|
||||
api,
|
||||
item: nextItem,
|
||||
width: 360,
|
||||
quality: 80,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (show && isPlaying) {
|
||||
progress.value = 0;
|
||||
progress.value = withTiming(
|
||||
1,
|
||||
{
|
||||
duration: 8000,
|
||||
easing: Easing.linear,
|
||||
},
|
||||
(finished) => {
|
||||
if (finished && onFinishRef.current) {
|
||||
runOnJS(onFinishRef.current)();
|
||||
}
|
||||
},
|
||||
);
|
||||
} else {
|
||||
cancelAnimation(progress);
|
||||
progress.value = 0;
|
||||
}
|
||||
}, [show, isPlaying, progress]);
|
||||
|
||||
const progressStyle = useAnimatedStyle(() => ({
|
||||
width: `${progress.value * 100}%`,
|
||||
}));
|
||||
|
||||
if (!show) return null;
|
||||
|
||||
return (
|
||||
<View style={countdownStyles.container} pointerEvents='none'>
|
||||
<BlurView intensity={80} tint='dark' style={countdownStyles.blur}>
|
||||
<View style={countdownStyles.innerContainer}>
|
||||
{imageUrl && (
|
||||
<Image
|
||||
source={{ uri: imageUrl }}
|
||||
style={countdownStyles.thumbnail}
|
||||
resizeMode='cover'
|
||||
/>
|
||||
)}
|
||||
|
||||
<View style={countdownStyles.content}>
|
||||
<Text style={countdownStyles.label}>
|
||||
{t("player.next_episode")}
|
||||
</Text>
|
||||
|
||||
<Text style={countdownStyles.seriesName} numberOfLines={1}>
|
||||
{nextItem.SeriesName}
|
||||
</Text>
|
||||
|
||||
<Text style={countdownStyles.episodeInfo} numberOfLines={1}>
|
||||
S{nextItem.ParentIndexNumber}E{nextItem.IndexNumber} -{" "}
|
||||
{nextItem.Name}
|
||||
</Text>
|
||||
|
||||
<View style={countdownStyles.progressContainer}>
|
||||
<Animated.View
|
||||
style={[countdownStyles.progressBar, progressStyle]}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</BlurView>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const countdownStyles = StyleSheet.create({
|
||||
container: {
|
||||
position: "absolute",
|
||||
bottom: 180,
|
||||
right: 80,
|
||||
zIndex: 100,
|
||||
},
|
||||
blur: {
|
||||
borderRadius: 16,
|
||||
overflow: "hidden",
|
||||
},
|
||||
innerContainer: {
|
||||
flexDirection: "row",
|
||||
alignItems: "stretch",
|
||||
},
|
||||
thumbnail: {
|
||||
width: 180,
|
||||
backgroundColor: "rgba(0,0,0,0.3)",
|
||||
},
|
||||
content: {
|
||||
padding: 16,
|
||||
justifyContent: "center",
|
||||
width: 280,
|
||||
},
|
||||
label: {
|
||||
fontSize: 13,
|
||||
color: "rgba(255,255,255,0.5)",
|
||||
textTransform: "uppercase",
|
||||
letterSpacing: 1,
|
||||
marginBottom: 4,
|
||||
},
|
||||
seriesName: {
|
||||
fontSize: 16,
|
||||
color: "rgba(255,255,255,0.7)",
|
||||
marginBottom: 2,
|
||||
},
|
||||
episodeInfo: {
|
||||
fontSize: 20,
|
||||
color: "#fff",
|
||||
fontWeight: "600",
|
||||
marginBottom: 12,
|
||||
},
|
||||
progressContainer: {
|
||||
height: 4,
|
||||
backgroundColor: "rgba(255,255,255,0.2)",
|
||||
borderRadius: 2,
|
||||
overflow: "hidden",
|
||||
},
|
||||
progressBar: {
|
||||
height: "100%",
|
||||
backgroundColor: "#fff",
|
||||
borderRadius: 2,
|
||||
},
|
||||
});
|
||||
|
||||
export const Controls: FC<Props> = ({
|
||||
item,
|
||||
seek,
|
||||
|
||||
@@ -16,7 +16,6 @@ import {
|
||||
ActivityIndicator,
|
||||
Animated,
|
||||
Easing,
|
||||
Pressable,
|
||||
ScrollView,
|
||||
StyleSheet,
|
||||
TVFocusGuideView,
|
||||
@@ -25,8 +24,10 @@ import {
|
||||
import { Text } from "@/components/common/Text";
|
||||
import {
|
||||
TVCancelButton,
|
||||
TVLanguageCard,
|
||||
TVSubtitleResultCard,
|
||||
TVTabButton,
|
||||
useTVFocusAnimation,
|
||||
TVTrackCard,
|
||||
} from "@/components/tv";
|
||||
import {
|
||||
type SubtitleSearchResult,
|
||||
@@ -48,327 +49,6 @@ interface TVSubtitleSheetProps {
|
||||
|
||||
type TabType = "tracks" | "download";
|
||||
|
||||
// Track card for subtitle track selection
|
||||
const TVTrackCard = React.forwardRef<
|
||||
View,
|
||||
{
|
||||
label: string;
|
||||
sublabel?: string;
|
||||
selected: boolean;
|
||||
hasTVPreferredFocus?: boolean;
|
||||
onPress: () => void;
|
||||
}
|
||||
>(({ label, sublabel, selected, hasTVPreferredFocus, onPress }, ref) => {
|
||||
const { focused, handleFocus, handleBlur, animatedStyle } =
|
||||
useTVFocusAnimation({ scaleAmount: 1.05 });
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
ref={ref}
|
||||
onPress={onPress}
|
||||
onFocus={handleFocus}
|
||||
onBlur={handleBlur}
|
||||
hasTVPreferredFocus={hasTVPreferredFocus}
|
||||
>
|
||||
<Animated.View
|
||||
style={[
|
||||
styles.trackCard,
|
||||
animatedStyle,
|
||||
{
|
||||
backgroundColor: focused
|
||||
? "#fff"
|
||||
: selected
|
||||
? "rgba(255,255,255,0.2)"
|
||||
: "rgba(255,255,255,0.08)",
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Text
|
||||
style={[
|
||||
styles.trackCardText,
|
||||
{ color: focused ? "#000" : "#fff" },
|
||||
(focused || selected) && { fontWeight: "600" },
|
||||
]}
|
||||
numberOfLines={2}
|
||||
>
|
||||
{label}
|
||||
</Text>
|
||||
{sublabel && (
|
||||
<Text
|
||||
style={[
|
||||
styles.trackCardSublabel,
|
||||
{ color: focused ? "rgba(0,0,0,0.6)" : "rgba(255,255,255,0.5)" },
|
||||
]}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{sublabel}
|
||||
</Text>
|
||||
)}
|
||||
{selected && !focused && (
|
||||
<View style={styles.checkmark}>
|
||||
<Ionicons
|
||||
name='checkmark'
|
||||
size={16}
|
||||
color='rgba(255,255,255,0.8)'
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
</Animated.View>
|
||||
</Pressable>
|
||||
);
|
||||
});
|
||||
|
||||
// Language selector card
|
||||
const LanguageCard = React.forwardRef<
|
||||
View,
|
||||
{
|
||||
code: string;
|
||||
name: string;
|
||||
selected: boolean;
|
||||
hasTVPreferredFocus?: boolean;
|
||||
onPress: () => void;
|
||||
}
|
||||
>(({ code, name, selected, hasTVPreferredFocus, onPress }, ref) => {
|
||||
const { focused, handleFocus, handleBlur, animatedStyle } =
|
||||
useTVFocusAnimation({ scaleAmount: 1.05 });
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
ref={ref}
|
||||
onPress={onPress}
|
||||
onFocus={handleFocus}
|
||||
onBlur={handleBlur}
|
||||
hasTVPreferredFocus={hasTVPreferredFocus}
|
||||
>
|
||||
<Animated.View
|
||||
style={[
|
||||
styles.languageCard,
|
||||
animatedStyle,
|
||||
{
|
||||
backgroundColor: focused
|
||||
? "#fff"
|
||||
: selected
|
||||
? "rgba(255,255,255,0.2)"
|
||||
: "rgba(255,255,255,0.08)",
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Text
|
||||
style={[
|
||||
styles.languageCardText,
|
||||
{ color: focused ? "#000" : "#fff" },
|
||||
(focused || selected) && { fontWeight: "600" },
|
||||
]}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{name}
|
||||
</Text>
|
||||
<Text
|
||||
style={[
|
||||
styles.languageCardCode,
|
||||
{ color: focused ? "rgba(0,0,0,0.6)" : "rgba(255,255,255,0.5)" },
|
||||
]}
|
||||
>
|
||||
{code.toUpperCase()}
|
||||
</Text>
|
||||
{selected && !focused && (
|
||||
<View style={styles.checkmark}>
|
||||
<Ionicons
|
||||
name='checkmark'
|
||||
size={16}
|
||||
color='rgba(255,255,255,0.8)'
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
</Animated.View>
|
||||
</Pressable>
|
||||
);
|
||||
});
|
||||
|
||||
// Subtitle result card
|
||||
const SubtitleResultCard = React.forwardRef<
|
||||
View,
|
||||
{
|
||||
result: SubtitleSearchResult;
|
||||
hasTVPreferredFocus?: boolean;
|
||||
isDownloading?: boolean;
|
||||
onPress: () => void;
|
||||
}
|
||||
>(({ result, hasTVPreferredFocus, isDownloading, onPress }, ref) => {
|
||||
const { focused, handleFocus, handleBlur, animatedStyle } =
|
||||
useTVFocusAnimation({ scaleAmount: 1.03 });
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
ref={ref}
|
||||
onPress={onPress}
|
||||
onFocus={handleFocus}
|
||||
onBlur={handleBlur}
|
||||
hasTVPreferredFocus={hasTVPreferredFocus}
|
||||
disabled={isDownloading}
|
||||
>
|
||||
<Animated.View
|
||||
style={[
|
||||
styles.resultCard,
|
||||
animatedStyle,
|
||||
{
|
||||
backgroundColor: focused ? "#fff" : "rgba(255,255,255,0.08)",
|
||||
borderColor: focused
|
||||
? "rgba(255,255,255,0.8)"
|
||||
: "rgba(255,255,255,0.1)",
|
||||
},
|
||||
]}
|
||||
>
|
||||
{/* Provider/Source badge */}
|
||||
<View
|
||||
style={[
|
||||
styles.providerBadge,
|
||||
{
|
||||
backgroundColor: focused
|
||||
? "rgba(0,0,0,0.1)"
|
||||
: "rgba(255,255,255,0.1)",
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Text
|
||||
style={[
|
||||
styles.providerText,
|
||||
{ color: focused ? "rgba(0,0,0,0.7)" : "rgba(255,255,255,0.7)" },
|
||||
]}
|
||||
>
|
||||
{result.providerName}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{/* Name */}
|
||||
<Text
|
||||
style={[styles.resultName, { color: focused ? "#000" : "#fff" }]}
|
||||
numberOfLines={2}
|
||||
>
|
||||
{result.name}
|
||||
</Text>
|
||||
|
||||
{/* Meta info row */}
|
||||
<View style={styles.resultMeta}>
|
||||
{/* Format */}
|
||||
<Text
|
||||
style={[
|
||||
styles.resultMetaText,
|
||||
{ color: focused ? "rgba(0,0,0,0.6)" : "rgba(255,255,255,0.5)" },
|
||||
]}
|
||||
>
|
||||
{result.format?.toUpperCase()}
|
||||
</Text>
|
||||
|
||||
{/* Rating if available */}
|
||||
{result.communityRating !== undefined &&
|
||||
result.communityRating > 0 && (
|
||||
<View style={styles.ratingContainer}>
|
||||
<Ionicons
|
||||
name='star'
|
||||
size={12}
|
||||
color={focused ? "rgba(0,0,0,0.6)" : "rgba(255,255,255,0.5)"}
|
||||
/>
|
||||
<Text
|
||||
style={[
|
||||
styles.resultMetaText,
|
||||
{
|
||||
color: focused
|
||||
? "rgba(0,0,0,0.6)"
|
||||
: "rgba(255,255,255,0.5)",
|
||||
},
|
||||
]}
|
||||
>
|
||||
{result.communityRating.toFixed(1)}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Download count if available */}
|
||||
{result.downloadCount !== undefined && result.downloadCount > 0 && (
|
||||
<View style={styles.downloadCountContainer}>
|
||||
<Ionicons
|
||||
name='download-outline'
|
||||
size={12}
|
||||
color={focused ? "rgba(0,0,0,0.6)" : "rgba(255,255,255,0.5)"}
|
||||
/>
|
||||
<Text
|
||||
style={[
|
||||
styles.resultMetaText,
|
||||
{
|
||||
color: focused
|
||||
? "rgba(0,0,0,0.6)"
|
||||
: "rgba(255,255,255,0.5)",
|
||||
},
|
||||
]}
|
||||
>
|
||||
{result.downloadCount.toLocaleString()}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* Flags */}
|
||||
<View style={styles.flagsContainer}>
|
||||
{result.isHashMatch && (
|
||||
<View
|
||||
style={[
|
||||
styles.flag,
|
||||
{
|
||||
backgroundColor: focused
|
||||
? "rgba(0,150,0,0.2)"
|
||||
: "rgba(0,200,0,0.2)",
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Text style={styles.flagText}>Hash Match</Text>
|
||||
</View>
|
||||
)}
|
||||
{result.hearingImpaired && (
|
||||
<View
|
||||
style={[
|
||||
styles.flag,
|
||||
{
|
||||
backgroundColor: focused
|
||||
? "rgba(0,0,0,0.1)"
|
||||
: "rgba(255,255,255,0.1)",
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Ionicons
|
||||
name='ear-outline'
|
||||
size={12}
|
||||
color={focused ? "#000" : "#fff"}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
{result.aiTranslated && (
|
||||
<View
|
||||
style={[
|
||||
styles.flag,
|
||||
{
|
||||
backgroundColor: focused
|
||||
? "rgba(0,0,150,0.2)"
|
||||
: "rgba(100,100,255,0.2)",
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Text style={styles.flagText}>AI</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* Loading indicator when downloading */}
|
||||
{isDownloading && (
|
||||
<View style={styles.downloadingOverlay}>
|
||||
<ActivityIndicator size='small' color='#fff' />
|
||||
</View>
|
||||
)}
|
||||
</Animated.View>
|
||||
</Pressable>
|
||||
);
|
||||
});
|
||||
|
||||
export const TVSubtitleSheet: React.FC<TVSubtitleSheetProps> = ({
|
||||
visible,
|
||||
item,
|
||||
@@ -627,7 +307,7 @@ export const TVSubtitleSheet: React.FC<TVSubtitleSheetProps> = ({
|
||||
contentContainerStyle={styles.languageScrollContent}
|
||||
>
|
||||
{displayLanguages.map((lang, index) => (
|
||||
<LanguageCard
|
||||
<TVLanguageCard
|
||||
key={lang.code}
|
||||
code={lang.code}
|
||||
name={lang.name}
|
||||
@@ -708,7 +388,7 @@ export const TVSubtitleSheet: React.FC<TVSubtitleSheetProps> = ({
|
||||
contentContainerStyle={styles.resultsScrollContent}
|
||||
>
|
||||
{searchResults.map((result, index) => (
|
||||
<SubtitleResultCard
|
||||
<TVSubtitleResultCard
|
||||
key={result.id}
|
||||
result={result}
|
||||
hasTVPreferredFocus={index === 0}
|
||||
@@ -810,27 +490,6 @@ const styles = StyleSheet.create({
|
||||
paddingVertical: 8,
|
||||
gap: 12,
|
||||
},
|
||||
trackCard: {
|
||||
width: 180,
|
||||
height: 80,
|
||||
borderRadius: 14,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
paddingHorizontal: 12,
|
||||
},
|
||||
trackCardText: {
|
||||
fontSize: 16,
|
||||
textAlign: "center",
|
||||
},
|
||||
trackCardSublabel: {
|
||||
fontSize: 12,
|
||||
marginTop: 2,
|
||||
},
|
||||
checkmark: {
|
||||
position: "absolute",
|
||||
top: 8,
|
||||
right: 8,
|
||||
},
|
||||
languageScroll: {
|
||||
overflow: "visible",
|
||||
},
|
||||
@@ -839,22 +498,6 @@ const styles = StyleSheet.create({
|
||||
paddingVertical: 8,
|
||||
gap: 10,
|
||||
},
|
||||
languageCard: {
|
||||
width: 120,
|
||||
height: 60,
|
||||
borderRadius: 12,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
paddingHorizontal: 12,
|
||||
},
|
||||
languageCardText: {
|
||||
fontSize: 15,
|
||||
fontWeight: "500",
|
||||
},
|
||||
languageCardCode: {
|
||||
fontSize: 11,
|
||||
marginTop: 2,
|
||||
},
|
||||
resultsScroll: {
|
||||
overflow: "visible",
|
||||
},
|
||||
@@ -863,73 +506,6 @@ const styles = StyleSheet.create({
|
||||
paddingVertical: 8,
|
||||
gap: 12,
|
||||
},
|
||||
resultCard: {
|
||||
width: 220,
|
||||
minHeight: 120,
|
||||
borderRadius: 14,
|
||||
padding: 14,
|
||||
borderWidth: 1,
|
||||
},
|
||||
providerBadge: {
|
||||
alignSelf: "flex-start",
|
||||
paddingHorizontal: 8,
|
||||
paddingVertical: 3,
|
||||
borderRadius: 6,
|
||||
marginBottom: 8,
|
||||
},
|
||||
providerText: {
|
||||
fontSize: 11,
|
||||
fontWeight: "600",
|
||||
textTransform: "uppercase",
|
||||
letterSpacing: 0.5,
|
||||
},
|
||||
resultName: {
|
||||
fontSize: 14,
|
||||
fontWeight: "500",
|
||||
marginBottom: 8,
|
||||
lineHeight: 18,
|
||||
},
|
||||
resultMeta: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: 12,
|
||||
marginBottom: 8,
|
||||
},
|
||||
resultMetaText: {
|
||||
fontSize: 12,
|
||||
},
|
||||
ratingContainer: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: 3,
|
||||
},
|
||||
downloadCountContainer: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: 3,
|
||||
},
|
||||
flagsContainer: {
|
||||
flexDirection: "row",
|
||||
gap: 6,
|
||||
flexWrap: "wrap",
|
||||
},
|
||||
flag: {
|
||||
paddingHorizontal: 6,
|
||||
paddingVertical: 2,
|
||||
borderRadius: 4,
|
||||
},
|
||||
flagText: {
|
||||
fontSize: 10,
|
||||
fontWeight: "600",
|
||||
color: "#fff",
|
||||
},
|
||||
downloadingOverlay: {
|
||||
...StyleSheet.absoluteFillObject,
|
||||
backgroundColor: "rgba(0,0,0,0.5)",
|
||||
borderRadius: 14,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
},
|
||||
loadingContainer: {
|
||||
paddingVertical: 40,
|
||||
alignItems: "center",
|
||||
|
||||
Reference in New Issue
Block a user