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}
/>