mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-01-15 23:59:08 +00:00
refactor: simplify PasswordInput component and standardize usage
Removes unnecessary props and internal state management from PasswordInput component to make it more focused and reusable. Wraps all PasswordInput instances in relative positioned Views for consistent layout behavior. Updates package.json to use caret version for @expo/vector-icons dependency for better version flexibility.
This commit is contained in:
@@ -277,20 +277,19 @@ const Login: React.FC = () => {
|
||||
/>
|
||||
|
||||
{/* Password */}
|
||||
<PasswordInput
|
||||
value={credentials.password}
|
||||
onChangeText={(text: string) =>
|
||||
setCredentials({ ...credentials, password: text })
|
||||
}
|
||||
onSubmitEditing={handleLogin}
|
||||
placeholder={t("login.password_placeholder")}
|
||||
className='mb-4'
|
||||
showPassword={showPassword}
|
||||
onShowPasswordChange={setShowPassword}
|
||||
topPosition='4'
|
||||
testID='tv-password-input'
|
||||
accessibilityLabel={t("login.password_placeholder")}
|
||||
/>
|
||||
<View className='relative'>
|
||||
<PasswordInput
|
||||
value={credentials.password}
|
||||
onChangeText={(text: string) =>
|
||||
setCredentials({ ...credentials, password: text })
|
||||
}
|
||||
placeholder={t("login.password_placeholder")}
|
||||
showPassword={showPassword}
|
||||
onShowPasswordChange={setShowPassword}
|
||||
topPosition='4'
|
||||
layout='tv'
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View className='mt-4'>
|
||||
<Button onPress={handleLogin}>{t("login.login_button")}</Button>
|
||||
@@ -403,20 +402,22 @@ const Login: React.FC = () => {
|
||||
textContentType='oneTimeCode'
|
||||
clearButtonMode='while-editing'
|
||||
maxLength={500}
|
||||
extraClassName='mb-4'
|
||||
/>
|
||||
|
||||
<PasswordInput
|
||||
value={credentials.password}
|
||||
onChangeText={(text) =>
|
||||
setCredentials({ ...credentials, password: text })
|
||||
}
|
||||
placeholder={t("login.password_placeholder")}
|
||||
showPassword={showPassword}
|
||||
onShowPasswordChange={setShowPassword}
|
||||
topPosition='3.5'
|
||||
testID='mobile-password-input'
|
||||
accessibilityLabel={t("login.password_placeholder")}
|
||||
/>
|
||||
<View className='relative mb-0.5'>
|
||||
<PasswordInput
|
||||
value={credentials.password}
|
||||
onChangeText={(text) =>
|
||||
setCredentials({ ...credentials, password: text })
|
||||
}
|
||||
placeholder={t("login.password_placeholder")}
|
||||
showPassword={showPassword}
|
||||
onShowPasswordChange={setShowPassword}
|
||||
topPosition='10'
|
||||
layout='mobile'
|
||||
/>
|
||||
</View>
|
||||
<View className='flex flex-row items-center justify-between'>
|
||||
<Button
|
||||
onPress={handleLogin}
|
||||
@@ -427,7 +428,7 @@ const Login: React.FC = () => {
|
||||
</Button>
|
||||
<TouchableOpacity
|
||||
onPress={handleQuickConnect}
|
||||
className='p-2 bg-neutral-900 rounded-xl h-12 w-12 flex items-center justify-center'
|
||||
className='p-3 bg-neutral-900 rounded-xl h-13 w-13 flex items-center justify-center'
|
||||
>
|
||||
<MaterialCommunityIcons
|
||||
name='cellphone-lock'
|
||||
|
||||
2
bun.lock
2
bun.lock
@@ -7,7 +7,7 @@
|
||||
"@bottom-tabs/react-navigation": "^0.9.2",
|
||||
"@expo/metro-runtime": "~5.0.4",
|
||||
"@expo/react-native-action-sheet": "^4.1.1",
|
||||
"@expo/vector-icons": "15.0.2",
|
||||
"@expo/vector-icons": "^15.0.2",
|
||||
"@gorhom/bottom-sheet": "^5.1.0",
|
||||
"@jellyfin/sdk": "^0.11.0",
|
||||
"@kesha-antonov/react-native-background-downloader": "^3.2.6",
|
||||
|
||||
@@ -1,104 +1,90 @@
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import type React from "react";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TouchableOpacity, View } from "react-native";
|
||||
import { TouchableOpacity } from "react-native";
|
||||
import { Input } from "./common/Input";
|
||||
|
||||
interface PasswordInputProps {
|
||||
// Discriminated union for password visibility control
|
||||
type PasswordVisibilityControlled = {
|
||||
value?: string;
|
||||
onChangeText: (text: string) => void;
|
||||
onSubmitEditing?: () => void;
|
||||
placeholder: string;
|
||||
className?: string;
|
||||
testID?: string;
|
||||
accessibilityLabel?: string;
|
||||
maxLength?: number;
|
||||
showPassword?: boolean;
|
||||
onShowPasswordChange?: (show: boolean) => void;
|
||||
topPosition?: "3.5" | "4";
|
||||
editable?: boolean;
|
||||
autoComplete?:
|
||||
| "password"
|
||||
| "username"
|
||||
| "name"
|
||||
| "email"
|
||||
| "tel"
|
||||
| "url"
|
||||
| "off";
|
||||
autoCorrect?: boolean;
|
||||
iconColor?: string;
|
||||
}
|
||||
showPassword: boolean;
|
||||
onShowPasswordChange: (show: boolean) => void;
|
||||
topPosition?: string;
|
||||
layout?: "tv" | "mobile";
|
||||
};
|
||||
|
||||
export const PasswordInput: React.FC<PasswordInputProps> = ({
|
||||
value = "",
|
||||
onChangeText,
|
||||
onSubmitEditing,
|
||||
placeholder,
|
||||
className = "",
|
||||
testID,
|
||||
accessibilityLabel,
|
||||
maxLength = 500,
|
||||
showPassword: controlledShowPassword,
|
||||
onShowPasswordChange,
|
||||
topPosition = "3.5",
|
||||
editable = true,
|
||||
autoComplete,
|
||||
autoCorrect,
|
||||
iconColor = "white",
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const [internalShowPassword, setInternalShowPassword] = useState(false);
|
||||
type PasswordVisibilityUncontrolled = {
|
||||
value?: string;
|
||||
onChangeText: (text: string) => void;
|
||||
placeholder: string;
|
||||
showPassword?: never;
|
||||
onShowPasswordChange?: never;
|
||||
topPosition?: string;
|
||||
layout?: "tv" | "mobile";
|
||||
};
|
||||
|
||||
// Use controlled state if provided, otherwise use internal state
|
||||
const showPassword = controlledShowPassword ?? internalShowPassword;
|
||||
const setShowPassword = onShowPasswordChange ?? setInternalShowPassword;
|
||||
type PasswordInputProps =
|
||||
| PasswordVisibilityControlled
|
||||
| PasswordVisibilityUncontrolled;
|
||||
|
||||
// Construct Tailwind class from validated topPosition
|
||||
const topClass = `top-${topPosition}`;
|
||||
export const PasswordInput: React.FC<PasswordInputProps> = (props) => {
|
||||
const {
|
||||
value = "",
|
||||
onChangeText,
|
||||
placeholder,
|
||||
topPosition = "3.5",
|
||||
layout = "mobile",
|
||||
} = props;
|
||||
|
||||
// Type guard to check if we're in controlled mode
|
||||
const isControlled =
|
||||
"showPassword" in props && "onShowPasswordChange" in props;
|
||||
|
||||
// For controlled mode, use the provided props
|
||||
// For uncontrolled mode, use internal state (but we need to handle this differently)
|
||||
const showPassword = isControlled
|
||||
? (props as PasswordVisibilityControlled).showPassword
|
||||
: false;
|
||||
|
||||
const handleTogglePassword = () => {
|
||||
if (isControlled) {
|
||||
(props as PasswordVisibilityControlled).onShowPasswordChange(
|
||||
!showPassword,
|
||||
);
|
||||
}
|
||||
// For uncontrolled mode, we could add internal state handling here if needed
|
||||
};
|
||||
|
||||
// Generate top position with pixel precision
|
||||
const getTopStyle = () => {
|
||||
// Use pixel values directly
|
||||
const positionInPx = parseFloat(topPosition);
|
||||
return { top: positionInPx };
|
||||
};
|
||||
|
||||
return (
|
||||
<View className={`relative ${className}`}>
|
||||
<>
|
||||
<Input
|
||||
placeholder={placeholder}
|
||||
onChangeText={onChangeText}
|
||||
value={value}
|
||||
secureTextEntry={!showPassword}
|
||||
keyboardType='default'
|
||||
returnKeyType='done'
|
||||
autoCapitalize='none'
|
||||
textContentType='password'
|
||||
clearButtonMode='while-editing'
|
||||
maxLength={maxLength}
|
||||
className='pr-12'
|
||||
onSubmitEditing={onSubmitEditing}
|
||||
testID={testID}
|
||||
accessibilityLabel={accessibilityLabel}
|
||||
editable={editable}
|
||||
autoComplete={autoComplete}
|
||||
autoCorrect={autoCorrect}
|
||||
className='pr-4'
|
||||
/>
|
||||
<TouchableOpacity
|
||||
onPress={() => setShowPassword(!showPassword)}
|
||||
className={`absolute right-3 ${topClass} p-1`}
|
||||
accessible={true}
|
||||
accessibilityRole='button'
|
||||
accessibilityLabel={
|
||||
showPassword
|
||||
? t("accessibility.hide_password")
|
||||
: t("accessibility.show_password")
|
||||
}
|
||||
accessibilityHint={t("accessibility.toggle_password_visibility")}
|
||||
accessibilityState={{ selected: showPassword }}
|
||||
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
|
||||
testID={testID ? `${testID}-toggle` : undefined}
|
||||
onPress={handleTogglePassword}
|
||||
className={`absolute right-3 p-1 ${
|
||||
layout === "tv" ? "h-10 justify-center" : ""
|
||||
}`}
|
||||
style={getTopStyle()}
|
||||
>
|
||||
<Ionicons
|
||||
name={showPassword ? "eye-off" : "eye"}
|
||||
size={24}
|
||||
color={iconColor}
|
||||
color='white'
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -20,7 +20,7 @@ export function Input(props: InputProps) {
|
||||
<TextInput
|
||||
ref={inputRef}
|
||||
className={`
|
||||
w-full text-lg px-5 py-4 rounded-2xl
|
||||
w-full text-lg px-5 py-5 rounded-2xl
|
||||
${isFocused ? "bg-neutral-700 border-2 border-white" : "bg-neutral-900 border-2 border-transparent"}
|
||||
text-white ${extraClassName}
|
||||
`}
|
||||
@@ -41,7 +41,7 @@ export function Input(props: InputProps) {
|
||||
) : (
|
||||
<TextInput
|
||||
ref={inputRef}
|
||||
className='p-4 rounded-xl bg-neutral-900'
|
||||
className='p-3 rounded-xl bg-neutral-900'
|
||||
allowFontScaling={false}
|
||||
style={[{ color: "white" }, style]}
|
||||
placeholderTextColor={"#9CA3AF"}
|
||||
|
||||
@@ -150,24 +150,20 @@ export const JellyseerrSettings = () => {
|
||||
<Text className='font-bold mb-2'>
|
||||
{t("home.settings.plugins.jellyseerr.password")}
|
||||
</Text>
|
||||
<PasswordInput
|
||||
value={jellyseerrPassword}
|
||||
onChangeText={setJellyseerrPassword}
|
||||
placeholder={t(
|
||||
"home.settings.plugins.jellyseerr.password_placeholder",
|
||||
{ username: user?.Name },
|
||||
)}
|
||||
className='border border-neutral-800'
|
||||
showPassword={showJellyseerrPassword}
|
||||
onShowPasswordChange={setShowJellyseerrPassword}
|
||||
editable={!loginToJellyseerrMutation.isPending}
|
||||
autoComplete='password'
|
||||
autoCorrect={false}
|
||||
testID='jellyseerr-password-input'
|
||||
accessibilityLabel={t(
|
||||
"home.settings.plugins.jellyseerr.password",
|
||||
)}
|
||||
/>
|
||||
<View className='relative'>
|
||||
<PasswordInput
|
||||
value={jellyseerrPassword}
|
||||
onChangeText={setJellyseerrPassword}
|
||||
placeholder={t(
|
||||
"home.settings.plugins.jellyseerr.password_placeholder",
|
||||
{ username: user?.Name },
|
||||
)}
|
||||
showPassword={showJellyseerrPassword}
|
||||
onShowPasswordChange={setShowJellyseerrPassword}
|
||||
layout='mobile'
|
||||
topPosition='22'
|
||||
/>
|
||||
</View>
|
||||
<Button
|
||||
loading={loginToJellyseerrMutation.isPending}
|
||||
disabled={loginToJellyseerrMutation.isPending}
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
"@bottom-tabs/react-navigation": "^0.9.2",
|
||||
"@expo/metro-runtime": "~5.0.4",
|
||||
"@expo/react-native-action-sheet": "^4.1.1",
|
||||
"@expo/vector-icons": "15.0.2",
|
||||
"@expo/vector-icons": "^15.0.2",
|
||||
"@gorhom/bottom-sheet": "^5.1.0",
|
||||
"@jellyfin/sdk": "^0.11.0",
|
||||
"@kesha-antonov/react-native-background-downloader": "^3.2.6",
|
||||
|
||||
Reference in New Issue
Block a user