Files
streamyfin/components/Button.tsx
Fredrik Burmester 3c57829360
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
feat: prefer downloaded file (#1217)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-16 21:47:51 +01:00

198 lines
5.0 KiB
TypeScript

import type React from "react";
import {
type PropsWithChildren,
type ReactNode,
useRef,
useState,
} from "react";
import {
Animated,
Easing,
Platform,
Pressable,
Text,
TouchableOpacity,
View,
} from "react-native";
import { useHaptic } from "@/hooks/useHaptic";
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
extends React.ComponentProps<typeof TouchableOpacity> {
onPress?: () => void;
className?: string;
textClassName?: string;
disabled?: boolean;
children?: string | ReactNode;
loading?: boolean;
color?: "purple" | "red" | "black" | "transparent" | "white";
variant?: "solid" | "border";
iconRight?: ReactNode;
iconLeft?: ReactNode;
justify?: "center" | "between";
}
export const Button: React.FC<PropsWithChildren<ButtonProps>> = ({
onPress,
className = "",
textClassName = "",
disabled = false,
loading = false,
color = "purple",
variant = "solid",
iconRight,
iconLeft,
children,
justify = "center",
...props
}) => {
const [focused, setFocused] = useState(false);
const scale = useRef(new Animated.Value(1)).current;
const animateTo = (v: number) =>
Animated.timing(scale, {
toValue: v,
duration: 130,
easing: Easing.out(Easing.quad),
useNativeDriver: true,
}).start();
const colorClasses = getColorClasses(color, variant, focused);
const lightHapticFeedback = useHaptic("light");
const textColorClass =
color === "white" && variant === "solid" ? "text-black" : "text-white";
return Platform.isTV ? (
<Pressable
className='w-full'
onPress={onPress}
onFocus={() => {
setFocused(true);
animateTo(1.08);
}}
onBlur={() => {
setFocused(false);
animateTo(1);
}}
>
<Animated.View
style={{
transform: [{ scale }],
shadowColor: "#a855f7",
shadowOffset: { width: 0, height: 0 },
shadowOpacity: focused ? 0.9 : 0,
shadowRadius: focused ? 18 : 0,
elevation: focused ? 12 : 0, // Android glow
}}
>
<View
className={`rounded-2xl py-5 items-center justify-center
${colorClasses}
${className}`}
>
<Text className={`${textColorClass} text-xl font-bold`}>
{children}
</Text>
</View>
</Animated.View>
</Pressable>
) : (
<TouchableOpacity
className={`
p-3 rounded-xl items-center justify-center
${(loading || disabled) && "opacity-50"}
${colorClasses}
${className}
`}
onPress={() => {
if (!loading && !disabled && onPress) {
onPress();
lightHapticFeedback();
}
}}
disabled={disabled || loading}
{...props}
>
{loading ? (
<View className='p-0.5'>
<Loader />
</View>
) : (
<View
className={`
flex flex-row items-center justify-between w-full
${justify === "between" ? "justify-between" : "justify-center"}`}
>
{iconLeft ? iconLeft : <View className='w-4' />}
<Text
className={`
${textColorClass} font-bold text-base
${disabled ? "text-gray-300" : ""}
${textClassName}
${iconRight ? "mr-2" : ""}
${iconLeft ? "ml-2" : ""}
`}
>
{children}
</Text>
{iconRight ? iconRight : <View className='w-4' />}
</View>
)}
</TouchableOpacity>
);
};