mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-06-23 06:10:30 +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 */}
|
||||||
|
<View className='relative'>
|
||||||
<PasswordInput
|
<PasswordInput
|
||||||
value={credentials.password}
|
value={credentials.password}
|
||||||
onChangeText={(text: string) =>
|
onChangeText={(text: string) =>
|
||||||
setCredentials({ ...credentials, password: text })
|
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'
|
||||||
testID='tv-password-input'
|
layout='tv'
|
||||||
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,8 +402,10 @@ const Login: React.FC = () => {
|
|||||||
textContentType='oneTimeCode'
|
textContentType='oneTimeCode'
|
||||||
clearButtonMode='while-editing'
|
clearButtonMode='while-editing'
|
||||||
maxLength={500}
|
maxLength={500}
|
||||||
|
extraClassName='mb-4'
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<View className='relative mb-0.5'>
|
||||||
<PasswordInput
|
<PasswordInput
|
||||||
value={credentials.password}
|
value={credentials.password}
|
||||||
onChangeText={(text) =>
|
onChangeText={(text) =>
|
||||||
@@ -413,10 +414,10 @@ const Login: React.FC = () => {
|
|||||||
placeholder={t("login.password_placeholder")}
|
placeholder={t("login.password_placeholder")}
|
||||||
showPassword={showPassword}
|
showPassword={showPassword}
|
||||||
onShowPasswordChange={setShowPassword}
|
onShowPasswordChange={setShowPassword}
|
||||||
topPosition='3.5'
|
topPosition='10'
|
||||||
testID='mobile-password-input'
|
layout='mobile'
|
||||||
accessibilityLabel={t("login.password_placeholder")}
|
|
||||||
/>
|
/>
|
||||||
|
</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?: string;
|
||||||
|
onChangeText: (text: string) => void;
|
||||||
|
placeholder: string;
|
||||||
|
showPassword?: never;
|
||||||
|
onShowPasswordChange?: never;
|
||||||
|
topPosition?: string;
|
||||||
|
layout?: "tv" | "mobile";
|
||||||
|
};
|
||||||
|
|
||||||
|
type PasswordInputProps =
|
||||||
|
| PasswordVisibilityControlled
|
||||||
|
| PasswordVisibilityUncontrolled;
|
||||||
|
|
||||||
|
export const PasswordInput: React.FC<PasswordInputProps> = (props) => {
|
||||||
|
const {
|
||||||
value = "",
|
value = "",
|
||||||
onChangeText,
|
onChangeText,
|
||||||
onSubmitEditing,
|
|
||||||
placeholder,
|
placeholder,
|
||||||
className = "",
|
|
||||||
testID,
|
|
||||||
accessibilityLabel,
|
|
||||||
maxLength = 500,
|
|
||||||
showPassword: controlledShowPassword,
|
|
||||||
onShowPasswordChange,
|
|
||||||
topPosition = "3.5",
|
topPosition = "3.5",
|
||||||
editable = true,
|
layout = "mobile",
|
||||||
autoComplete,
|
} = props;
|
||||||
autoCorrect,
|
|
||||||
iconColor = "white",
|
|
||||||
}) => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const [internalShowPassword, setInternalShowPassword] = useState(false);
|
|
||||||
|
|
||||||
// Use controlled state if provided, otherwise use internal state
|
// Type guard to check if we're in controlled mode
|
||||||
const showPassword = controlledShowPassword ?? internalShowPassword;
|
const isControlled =
|
||||||
const setShowPassword = onShowPasswordChange ?? setInternalShowPassword;
|
"showPassword" in props && "onShowPasswordChange" in props;
|
||||||
|
|
||||||
// Construct Tailwind class from validated topPosition
|
// For controlled mode, use the provided props
|
||||||
const topClass = `top-${topPosition}`;
|
// 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,6 +150,7 @@ 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>
|
||||||
|
<View className='relative'>
|
||||||
<PasswordInput
|
<PasswordInput
|
||||||
value={jellyseerrPassword}
|
value={jellyseerrPassword}
|
||||||
onChangeText={setJellyseerrPassword}
|
onChangeText={setJellyseerrPassword}
|
||||||
@@ -157,17 +158,12 @@ export const JellyseerrSettings = () => {
|
|||||||
"home.settings.plugins.jellyseerr.password_placeholder",
|
"home.settings.plugins.jellyseerr.password_placeholder",
|
||||||
{ username: user?.Name },
|
{ 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'
|
|
||||||
accessibilityLabel={t(
|
|
||||||
"home.settings.plugins.jellyseerr.password",
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
|
</View>
|
||||||
<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