mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-04-22 00:34:43 +01: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 */}
|
{/* Password */}
|
||||||
<PasswordInput
|
<View className='relative'>
|
||||||
value={credentials.password}
|
<PasswordInput
|
||||||
onChangeText={(text: string) =>
|
value={credentials.password}
|
||||||
setCredentials({ ...credentials, password: text })
|
onChangeText={(text: string) =>
|
||||||
}
|
setCredentials({ ...credentials, password: text })
|
||||||
onSubmitEditing={handleLogin}
|
}
|
||||||
placeholder={t("login.password_placeholder")}
|
placeholder={t("login.password_placeholder")}
|
||||||
className='mb-4'
|
showPassword={showPassword}
|
||||||
showPassword={showPassword}
|
onShowPasswordChange={setShowPassword}
|
||||||
onShowPasswordChange={setShowPassword}
|
topPosition='4'
|
||||||
topPosition='4'
|
layout='tv'
|
||||||
testID='tv-password-input'
|
/>
|
||||||
accessibilityLabel={t("login.password_placeholder")}
|
</View>
|
||||||
/>
|
|
||||||
|
|
||||||
<View className='mt-4'>
|
<View className='mt-4'>
|
||||||
<Button onPress={handleLogin}>{t("login.login_button")}</Button>
|
<Button onPress={handleLogin}>{t("login.login_button")}</Button>
|
||||||
@@ -403,20 +402,22 @@ const Login: React.FC = () => {
|
|||||||
textContentType='oneTimeCode'
|
textContentType='oneTimeCode'
|
||||||
clearButtonMode='while-editing'
|
clearButtonMode='while-editing'
|
||||||
maxLength={500}
|
maxLength={500}
|
||||||
|
extraClassName='mb-4'
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<PasswordInput
|
<View className='relative mb-0.5'>
|
||||||
value={credentials.password}
|
<PasswordInput
|
||||||
onChangeText={(text) =>
|
value={credentials.password}
|
||||||
setCredentials({ ...credentials, password: text })
|
onChangeText={(text) =>
|
||||||
}
|
setCredentials({ ...credentials, password: text })
|
||||||
placeholder={t("login.password_placeholder")}
|
}
|
||||||
showPassword={showPassword}
|
placeholder={t("login.password_placeholder")}
|
||||||
onShowPasswordChange={setShowPassword}
|
showPassword={showPassword}
|
||||||
topPosition='3.5'
|
onShowPasswordChange={setShowPassword}
|
||||||
testID='mobile-password-input'
|
topPosition='10'
|
||||||
accessibilityLabel={t("login.password_placeholder")}
|
layout='mobile'
|
||||||
/>
|
/>
|
||||||
|
</View>
|
||||||
<View className='flex flex-row items-center justify-between'>
|
<View className='flex flex-row items-center justify-between'>
|
||||||
<Button
|
<Button
|
||||||
onPress={handleLogin}
|
onPress={handleLogin}
|
||||||
@@ -427,7 +428,7 @@ const Login: React.FC = () => {
|
|||||||
</Button>
|
</Button>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
onPress={handleQuickConnect}
|
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
|
<MaterialCommunityIcons
|
||||||
name='cellphone-lock'
|
name='cellphone-lock'
|
||||||
|
|||||||
2
bun.lock
2
bun.lock
@@ -7,7 +7,7 @@
|
|||||||
"@bottom-tabs/react-navigation": "^0.9.2",
|
"@bottom-tabs/react-navigation": "^0.9.2",
|
||||||
"@expo/metro-runtime": "~5.0.4",
|
"@expo/metro-runtime": "~5.0.4",
|
||||||
"@expo/react-native-action-sheet": "^4.1.1",
|
"@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",
|
"@gorhom/bottom-sheet": "^5.1.0",
|
||||||
"@jellyfin/sdk": "^0.11.0",
|
"@jellyfin/sdk": "^0.11.0",
|
||||||
"@kesha-antonov/react-native-background-downloader": "^3.2.6",
|
"@kesha-antonov/react-native-background-downloader": "^3.2.6",
|
||||||
|
|||||||
@@ -1,104 +1,90 @@
|
|||||||
import { Ionicons } from "@expo/vector-icons";
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
import type React from "react";
|
import type React from "react";
|
||||||
import { useState } from "react";
|
import { TouchableOpacity } from "react-native";
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import { TouchableOpacity, View } from "react-native";
|
|
||||||
import { Input } from "./common/Input";
|
import { Input } from "./common/Input";
|
||||||
|
|
||||||
interface PasswordInputProps {
|
// Discriminated union for password visibility control
|
||||||
|
type PasswordVisibilityControlled = {
|
||||||
value?: string;
|
value?: string;
|
||||||
onChangeText: (text: string) => void;
|
onChangeText: (text: string) => void;
|
||||||
onSubmitEditing?: () => void;
|
|
||||||
placeholder: string;
|
placeholder: string;
|
||||||
className?: string;
|
showPassword: boolean;
|
||||||
testID?: string;
|
onShowPasswordChange: (show: boolean) => void;
|
||||||
accessibilityLabel?: string;
|
topPosition?: string;
|
||||||
maxLength?: number;
|
layout?: "tv" | "mobile";
|
||||||
showPassword?: boolean;
|
};
|
||||||
onShowPasswordChange?: (show: boolean) => void;
|
|
||||||
topPosition?: "3.5" | "4";
|
|
||||||
editable?: boolean;
|
|
||||||
autoComplete?:
|
|
||||||
| "password"
|
|
||||||
| "username"
|
|
||||||
| "name"
|
|
||||||
| "email"
|
|
||||||
| "tel"
|
|
||||||
| "url"
|
|
||||||
| "off";
|
|
||||||
autoCorrect?: boolean;
|
|
||||||
iconColor?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const PasswordInput: React.FC<PasswordInputProps> = ({
|
type PasswordVisibilityUncontrolled = {
|
||||||
value = "",
|
value?: string;
|
||||||
onChangeText,
|
onChangeText: (text: string) => void;
|
||||||
onSubmitEditing,
|
placeholder: string;
|
||||||
placeholder,
|
showPassword?: never;
|
||||||
className = "",
|
onShowPasswordChange?: never;
|
||||||
testID,
|
topPosition?: string;
|
||||||
accessibilityLabel,
|
layout?: "tv" | "mobile";
|
||||||
maxLength = 500,
|
};
|
||||||
showPassword: controlledShowPassword,
|
|
||||||
onShowPasswordChange,
|
|
||||||
topPosition = "3.5",
|
|
||||||
editable = true,
|
|
||||||
autoComplete,
|
|
||||||
autoCorrect,
|
|
||||||
iconColor = "white",
|
|
||||||
}) => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const [internalShowPassword, setInternalShowPassword] = useState(false);
|
|
||||||
|
|
||||||
// Use controlled state if provided, otherwise use internal state
|
type PasswordInputProps =
|
||||||
const showPassword = controlledShowPassword ?? internalShowPassword;
|
| PasswordVisibilityControlled
|
||||||
const setShowPassword = onShowPasswordChange ?? setInternalShowPassword;
|
| PasswordVisibilityUncontrolled;
|
||||||
|
|
||||||
// Construct Tailwind class from validated topPosition
|
export const PasswordInput: React.FC<PasswordInputProps> = (props) => {
|
||||||
const topClass = `top-${topPosition}`;
|
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 (
|
return (
|
||||||
<View className={`relative ${className}`}>
|
<>
|
||||||
<Input
|
<Input
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
onChangeText={onChangeText}
|
onChangeText={onChangeText}
|
||||||
value={value}
|
value={value}
|
||||||
secureTextEntry={!showPassword}
|
secureTextEntry={!showPassword}
|
||||||
keyboardType='default'
|
className='pr-4'
|
||||||
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}
|
|
||||||
/>
|
/>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
onPress={() => setShowPassword(!showPassword)}
|
onPress={handleTogglePassword}
|
||||||
className={`absolute right-3 ${topClass} p-1`}
|
className={`absolute right-3 p-1 ${
|
||||||
accessible={true}
|
layout === "tv" ? "h-10 justify-center" : ""
|
||||||
accessibilityRole='button'
|
}`}
|
||||||
accessibilityLabel={
|
style={getTopStyle()}
|
||||||
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}
|
|
||||||
>
|
>
|
||||||
<Ionicons
|
<Ionicons
|
||||||
name={showPassword ? "eye-off" : "eye"}
|
name={showPassword ? "eye-off" : "eye"}
|
||||||
size={24}
|
size={24}
|
||||||
color={iconColor}
|
color='white'
|
||||||
/>
|
/>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export function Input(props: InputProps) {
|
|||||||
<TextInput
|
<TextInput
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
className={`
|
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"}
|
${isFocused ? "bg-neutral-700 border-2 border-white" : "bg-neutral-900 border-2 border-transparent"}
|
||||||
text-white ${extraClassName}
|
text-white ${extraClassName}
|
||||||
`}
|
`}
|
||||||
@@ -41,7 +41,7 @@ export function Input(props: InputProps) {
|
|||||||
) : (
|
) : (
|
||||||
<TextInput
|
<TextInput
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
className='p-4 rounded-xl bg-neutral-900'
|
className='p-3 rounded-xl bg-neutral-900'
|
||||||
allowFontScaling={false}
|
allowFontScaling={false}
|
||||||
style={[{ color: "white" }, style]}
|
style={[{ color: "white" }, style]}
|
||||||
placeholderTextColor={"#9CA3AF"}
|
placeholderTextColor={"#9CA3AF"}
|
||||||
|
|||||||
@@ -150,24 +150,20 @@ export const JellyseerrSettings = () => {
|
|||||||
<Text className='font-bold mb-2'>
|
<Text className='font-bold mb-2'>
|
||||||
{t("home.settings.plugins.jellyseerr.password")}
|
{t("home.settings.plugins.jellyseerr.password")}
|
||||||
</Text>
|
</Text>
|
||||||
<PasswordInput
|
<View className='relative'>
|
||||||
value={jellyseerrPassword}
|
<PasswordInput
|
||||||
onChangeText={setJellyseerrPassword}
|
value={jellyseerrPassword}
|
||||||
placeholder={t(
|
onChangeText={setJellyseerrPassword}
|
||||||
"home.settings.plugins.jellyseerr.password_placeholder",
|
placeholder={t(
|
||||||
{ username: user?.Name },
|
"home.settings.plugins.jellyseerr.password_placeholder",
|
||||||
)}
|
{ username: user?.Name },
|
||||||
className='border border-neutral-800'
|
)}
|
||||||
showPassword={showJellyseerrPassword}
|
showPassword={showJellyseerrPassword}
|
||||||
onShowPasswordChange={setShowJellyseerrPassword}
|
onShowPasswordChange={setShowJellyseerrPassword}
|
||||||
editable={!loginToJellyseerrMutation.isPending}
|
layout='mobile'
|
||||||
autoComplete='password'
|
topPosition='22'
|
||||||
autoCorrect={false}
|
/>
|
||||||
testID='jellyseerr-password-input'
|
</View>
|
||||||
accessibilityLabel={t(
|
|
||||||
"home.settings.plugins.jellyseerr.password",
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<Button
|
<Button
|
||||||
loading={loginToJellyseerrMutation.isPending}
|
loading={loginToJellyseerrMutation.isPending}
|
||||||
disabled={loginToJellyseerrMutation.isPending}
|
disabled={loginToJellyseerrMutation.isPending}
|
||||||
|
|||||||
@@ -25,7 +25,7 @@
|
|||||||
"@bottom-tabs/react-navigation": "^0.9.2",
|
"@bottom-tabs/react-navigation": "^0.9.2",
|
||||||
"@expo/metro-runtime": "~5.0.4",
|
"@expo/metro-runtime": "~5.0.4",
|
||||||
"@expo/react-native-action-sheet": "^4.1.1",
|
"@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",
|
"@gorhom/bottom-sheet": "^5.1.0",
|
||||||
"@jellyfin/sdk": "^0.11.0",
|
"@jellyfin/sdk": "^0.11.0",
|
||||||
"@kesha-antonov/react-native-background-downloader": "^3.2.6",
|
"@kesha-antonov/react-native-background-downloader": "^3.2.6",
|
||||||
|
|||||||
Reference in New Issue
Block a user