From d3609e3499cba02d08c761e3c5dece322ee24de4 Mon Sep 17 00:00:00 2001 From: lance chant <13349722+lancechant@users.noreply.github.com> Date: Fri, 29 Aug 2025 13:30:49 +0200 Subject: [PATCH] fix: tv login layout (#972) Signed-off-by: Lance Chant <13349722+lancechant@users.noreply.github.com> Co-authored-by: Gauvain <68083474+Gauvino@users.noreply.github.com> --- app/login.tsx | 138 +++++++++++++++++++++++++++++++++++- components/Button.tsx | 69 ++++++++++++++++-- components/common/Input.tsx | 29 ++++++-- 3 files changed, 224 insertions(+), 12 deletions(-) diff --git a/app/login.tsx b/app/login.tsx index c66e5c06..6a54cc35 100644 --- a/app/login.tsx +++ b/app/login.tsx @@ -207,7 +207,143 @@ const Login: React.FC = () => { } }; - return ( + return Platform.isTV ? ( + // TV layout + + + {api?.basePath ? ( + // ------------ Username/Password view ------------ + + {/* Safe centered column with max width so TV doesn’t stretch too far */} + + + {serverName ? ( + <> + {`${t("login.login_to_title")} `} + {serverName} + + ) : ( + t("login.login_title") + )} + + + {api.basePath} + + + {/* Username */} + + setCredentials({ ...credentials, username: text }) + } + value={credentials.username} + keyboardType='default' + returnKeyType='done' + autoCapitalize='none' + textContentType='oneTimeCode' + clearButtonMode='while-editing' + maxLength={500} + extraClassName='mb-4' + /> + + {/* Password */} + + setCredentials({ ...credentials, password: text }) + } + value={credentials.password} + secureTextEntry + keyboardType='default' + returnKeyType='done' + autoCapitalize='none' + textContentType='password' + clearButtonMode='while-editing' + maxLength={500} + extraClassName='mb-4' + /> + + + + + + + + + + ) : ( + // ------------ Server connect view ------------ + + + + + + + + Streamyfin + + + {t("server.enter_url_to_jellyfin_server")} + + + {/* Full-width Input with clear focus ring */} + + + {/* Full-width primary button */} + + + + + {/* Lists stay full width but inside max width container */} + + { + setServerURL(server.address); + if (server.serverName) setServerName(server.serverName); + await handleConnect(server.address); + }} + /> + { + await handleConnect(s.address); + }} + /> + + + + )} + + + ) : ( + // Mobile layout > = ({ 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> = ({ case "transparent": return "bg-transparent"; } - }, [color]); + }, [color, focused]); const lightHapticFeedback = useHaptic("light"); - return ( + return Platform.isTV ? ( + { + setFocused(true); + animateTo(1.08); + }} + onBlur={() => { + setFocused(false); + animateTo(1); + }} + > + + + {children} + + + + ) : ( (null); + const [isFocused, setIsFocused] = useState(false); return Platform.isTV ? ( inputRef?.current?.focus?.()}> setIsFocused(true)} + onBlur={() => setIsFocused(false)} {...otherProps} />