mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-01-15 23:59:08 +00:00
fix: tv login layout (#972)
Some checks failed
🤖 Android APK Build (Phone + TV) / 🏗️ Build Android APK (phone) (push) Has been cancelled
🤖 Android APK Build (Phone + TV) / 🏗️ Build Android APK (tv) (push) Has been cancelled
🤖 iOS IPA Build (Phone + TV) / 🏗️ 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 / 📝 Validate PR Title (push) Has been cancelled
🚦 Security & Quality Gate / 🔍 Vulnerable Dependencies (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 / 🔍 Lint & Test (format) (push) Has been cancelled
🚦 Security & Quality Gate / 🔍 Lint & Test (lint) (push) Has been cancelled
Some checks failed
🤖 Android APK Build (Phone + TV) / 🏗️ Build Android APK (phone) (push) Has been cancelled
🤖 Android APK Build (Phone + TV) / 🏗️ Build Android APK (tv) (push) Has been cancelled
🤖 iOS IPA Build (Phone + TV) / 🏗️ 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 / 📝 Validate PR Title (push) Has been cancelled
🚦 Security & Quality Gate / 🔍 Vulnerable Dependencies (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 / 🔍 Lint & Test (format) (push) Has been cancelled
🚦 Security & Quality Gate / 🔍 Lint & Test (lint) (push) Has been cancelled
Signed-off-by: Lance Chant <13349722+lancechant@users.noreply.github.com> Co-authored-by: Gauvain <68083474+Gauvino@users.noreply.github.com>
This commit is contained in:
138
app/login.tsx
138
app/login.tsx
@@ -207,7 +207,143 @@ const Login: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
return Platform.isTV ? (
|
||||
// TV layout
|
||||
<SafeAreaView className='flex-1 bg-black'>
|
||||
<KeyboardAvoidingView
|
||||
behavior={Platform.OS === "ios" ? "padding" : "height"}
|
||||
style={{ flex: 1 }}
|
||||
>
|
||||
{api?.basePath ? (
|
||||
// ------------ Username/Password view ------------
|
||||
<View className='flex-1 items-center justify-center'>
|
||||
{/* Safe centered column with max width so TV doesn’t stretch too far */}
|
||||
<View className='w-[92%] max-w-[900px] px-2 -mt-12'>
|
||||
<Text className='text-3xl font-bold text-white mb-1'>
|
||||
{serverName ? (
|
||||
<>
|
||||
{`${t("login.login_to_title")} `}
|
||||
<Text className='text-purple-500'>{serverName}</Text>
|
||||
</>
|
||||
) : (
|
||||
t("login.login_title")
|
||||
)}
|
||||
</Text>
|
||||
<Text className='text-xs text-neutral-400 mb-6'>
|
||||
{api.basePath}
|
||||
</Text>
|
||||
|
||||
{/* Username */}
|
||||
<Input
|
||||
placeholder={t("login.username_placeholder")}
|
||||
onChangeText={(text: string) =>
|
||||
setCredentials({ ...credentials, username: text })
|
||||
}
|
||||
value={credentials.username}
|
||||
keyboardType='default'
|
||||
returnKeyType='done'
|
||||
autoCapitalize='none'
|
||||
textContentType='oneTimeCode'
|
||||
clearButtonMode='while-editing'
|
||||
maxLength={500}
|
||||
extraClassName='mb-4'
|
||||
/>
|
||||
|
||||
{/* Password */}
|
||||
<Input
|
||||
placeholder={t("login.password_placeholder")}
|
||||
onChangeText={(text: string) =>
|
||||
setCredentials({ ...credentials, password: text })
|
||||
}
|
||||
value={credentials.password}
|
||||
secureTextEntry
|
||||
keyboardType='default'
|
||||
returnKeyType='done'
|
||||
autoCapitalize='none'
|
||||
textContentType='password'
|
||||
clearButtonMode='while-editing'
|
||||
maxLength={500}
|
||||
extraClassName='mb-4'
|
||||
/>
|
||||
|
||||
<View className='mt-4'>
|
||||
<Button onPress={handleLogin}>{t("login.login_button")}</Button>
|
||||
</View>
|
||||
<View className='mt-3'>
|
||||
<Button
|
||||
onPress={handleQuickConnect}
|
||||
className='bg-neutral-800 border border-neutral-700'
|
||||
>
|
||||
{t("login.quick_connect")}
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
) : (
|
||||
// ------------ Server connect view ------------
|
||||
<View className='flex-1 items-center justify-center'>
|
||||
<View className='w-[92%] max-w-[900px] -mt-2'>
|
||||
<View className='items-center mb-1'>
|
||||
<Image
|
||||
source={require("@/assets/images/icon-ios-plain.png")}
|
||||
style={{ width: 110, height: 110 }}
|
||||
contentFit='contain'
|
||||
/>
|
||||
</View>
|
||||
|
||||
<Text className='text-white text-4xl font-bold text-center'>
|
||||
Streamyfin
|
||||
</Text>
|
||||
<Text className='text-neutral-400 text-base text-left mt-2 mb-1'>
|
||||
{t("server.enter_url_to_jellyfin_server")}
|
||||
</Text>
|
||||
|
||||
{/* Full-width Input with clear focus ring */}
|
||||
<Input
|
||||
aria-label='Server URL'
|
||||
placeholder={t("server.server_url_placeholder")}
|
||||
onChangeText={setServerURL}
|
||||
value={serverURL}
|
||||
keyboardType='url'
|
||||
returnKeyType='done'
|
||||
autoCapitalize='none'
|
||||
textContentType='URL'
|
||||
maxLength={500}
|
||||
/>
|
||||
|
||||
{/* Full-width primary button */}
|
||||
<View className='mt-4'>
|
||||
<Button
|
||||
onPress={async () => {
|
||||
await handleConnect(serverURL);
|
||||
}}
|
||||
>
|
||||
{t("server.connect_button")}
|
||||
</Button>
|
||||
</View>
|
||||
|
||||
{/* Lists stay full width but inside max width container */}
|
||||
<View className='mt-2'>
|
||||
<JellyfinServerDiscovery
|
||||
onServerSelect={async (server: any) => {
|
||||
setServerURL(server.address);
|
||||
if (server.serverName) setServerName(server.serverName);
|
||||
await handleConnect(server.address);
|
||||
}}
|
||||
/>
|
||||
<PreviousServersList
|
||||
onServerSelect={async (s: any) => {
|
||||
await handleConnect(s.address);
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
</KeyboardAvoidingView>
|
||||
</SafeAreaView>
|
||||
) : (
|
||||
// Mobile layout
|
||||
<SafeAreaView style={{ flex: 1, paddingBottom: 16 }}>
|
||||
<KeyboardAvoidingView
|
||||
behavior={Platform.OS === "ios" ? "padding" : "height"}
|
||||
|
||||
@@ -1,6 +1,20 @@
|
||||
import type React from "react";
|
||||
import { type PropsWithChildren, type ReactNode, useMemo } from "react";
|
||||
import { Text, TouchableOpacity, View } from "react-native";
|
||||
import {
|
||||
type PropsWithChildren,
|
||||
type ReactNode,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import {
|
||||
Animated,
|
||||
Easing,
|
||||
Platform,
|
||||
Pressable,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
} from "react-native";
|
||||
import { useHaptic } from "@/hooks/useHaptic";
|
||||
import { Loader } from "./Loader";
|
||||
|
||||
@@ -31,10 +45,23 @@ export const Button: React.FC<PropsWithChildren<ButtonProps>> = ({
|
||||
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 = useMemo(() => {
|
||||
switch (color) {
|
||||
case "purple":
|
||||
return "bg-purple-600 active:bg-purple-700";
|
||||
return focused
|
||||
? "bg-purple-500 border-2 border-white"
|
||||
: "bg-purple-600 border border-purple-700";
|
||||
case "red":
|
||||
return "bg-red-600";
|
||||
case "black":
|
||||
@@ -42,11 +69,43 @@ export const Button: React.FC<PropsWithChildren<ButtonProps>> = ({
|
||||
case "transparent":
|
||||
return "bg-transparent";
|
||||
}
|
||||
}, [color]);
|
||||
}, [color, focused]);
|
||||
|
||||
const lightHapticFeedback = useHaptic("light");
|
||||
|
||||
return (
|
||||
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
|
||||
${focused ? "bg-purple-500 border-2 border-white" : "bg-purple-600 border border-purple-700"}
|
||||
${className}`}
|
||||
>
|
||||
<Text className='text-white text-xl font-bold'>{children}</Text>
|
||||
</View>
|
||||
</Animated.View>
|
||||
</Pressable>
|
||||
) : (
|
||||
<TouchableOpacity
|
||||
className={`
|
||||
p-3 rounded-xl items-center justify-center
|
||||
|
||||
@@ -1,23 +1,40 @@
|
||||
import React from "react";
|
||||
import React, { useState } from "react";
|
||||
import {
|
||||
Platform,
|
||||
TextInput,
|
||||
type TextInputProps,
|
||||
TouchableOpacity,
|
||||
} from "react-native";
|
||||
export function Input(props: TextInputProps) {
|
||||
const { style, ...otherProps } = props;
|
||||
|
||||
interface InputProps extends TextInputProps {
|
||||
extraClassName?: string; // new prop for additional classes
|
||||
}
|
||||
|
||||
export function Input(props: InputProps) {
|
||||
const { style, extraClassName = "", ...otherProps } = props;
|
||||
const inputRef = React.useRef<TextInput>(null);
|
||||
const [isFocused, setIsFocused] = useState(false);
|
||||
|
||||
return Platform.isTV ? (
|
||||
<TouchableOpacity onFocus={() => inputRef?.current?.focus?.()}>
|
||||
<TextInput
|
||||
ref={inputRef}
|
||||
className='p-4 rounded-xl bg-neutral-900'
|
||||
className={`
|
||||
w-full text-lg px-5 py-4 rounded-2xl
|
||||
${isFocused ? "bg-neutral-700 border-2 border-white" : "bg-neutral-900 border-2 border-transparent"}
|
||||
text-white ${extraClassName}
|
||||
`}
|
||||
allowFontScaling={false}
|
||||
style={[{ color: "white" }, style]}
|
||||
placeholderTextColor={"#9CA3AF"}
|
||||
style={[
|
||||
style,
|
||||
{
|
||||
backgroundColor: isFocused ? "#ffffff88" : "#8f8d8d88",
|
||||
},
|
||||
]}
|
||||
placeholderTextColor={"#ffffffff"}
|
||||
clearButtonMode='while-editing'
|
||||
onFocus={() => setIsFocused(true)}
|
||||
onBlur={() => setIsFocused(false)}
|
||||
{...otherProps}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
|
||||
Reference in New Issue
Block a user