diff --git a/app/login.tsx b/app/login.tsx
index 33d06d41..20c574b2 100644
--- a/app/login.tsx
+++ b/app/login.tsx
@@ -312,169 +312,7 @@ const Login: React.FC = () => {
}
};
- 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((prev) => ({ ...prev, username: text }))
- }
- onEndEditing={(e) => {
- const newValue = e.nativeEvent.text;
- if (newValue && newValue !== credentials.username) {
- setCredentials((prev) => ({ ...prev, username: newValue }));
- }
- }}
- value={credentials.username}
- keyboardType='default'
- returnKeyType='done'
- autoCapitalize='none'
- autoCorrect={false}
- textContentType='username'
- clearButtonMode='while-editing'
- maxLength={500}
- extraClassName='mb-4'
- autoFocus={false}
- blurOnSubmit={true}
- />
-
- {/* Password */}
-
- setCredentials((prev) => ({ ...prev, password: text }))
- }
- onEndEditing={(e) => {
- const newValue = e.nativeEvent.text;
- if (newValue && newValue !== credentials.password) {
- setCredentials((prev) => ({ ...prev, password: newValue }));
- }
- }}
- value={credentials.password}
- secureTextEntry
- keyboardType='default'
- returnKeyType='done'
- autoCapitalize='none'
- textContentType='password'
- clearButtonMode='while-editing'
- maxLength={500}
- extraClassName='mb-4'
- autoFocus={false}
- blurOnSubmit={true}
- />
-
-
-
-
-
-
-
-
-
- ) : (
- // ------------ 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);
- }}
- onQuickLogin={handleQuickLoginWithSavedCredential}
- onPasswordLogin={handlePasswordLogin}
- onAddAccount={handleAddAccount}
- />
-
-
-
- )}
-
-
- ) : (
+ return (
// Mobile layout
{
+ const api = useAtomValue(apiAtom);
+ const navigation = useNavigation();
+ const params = useLocalSearchParams();
+ const {
+ setServer,
+ login,
+ removeServer,
+ initiateQuickConnect,
+ loginWithSavedCredential,
+ loginWithPassword,
+ } = useJellyfin();
+
+ const {
+ apiUrl: _apiUrl,
+ username: _username,
+ password: _password,
+ } = params as { apiUrl: string; username: string; password: string };
+
+ const [loadingServerCheck, setLoadingServerCheck] = useState(false);
+ const [loading, setLoading] = useState(false);
+ const [serverURL, setServerURL] = useState(_apiUrl || "");
+ const [serverName, setServerName] = useState("");
+ const [credentials, setCredentials] = useState<{
+ username: string;
+ password: string;
+ }>({
+ username: _username || "",
+ password: _password || "",
+ });
+
+ // Save account state
+ const [saveAccount, setSaveAccount] = useState(false);
+ const [showSaveModal, setShowSaveModal] = useState(false);
+ const [pendingLogin, setPendingLogin] = useState<{
+ username: string;
+ password: string;
+ } | null>(null);
+
+ // PIN/Password entry for saved accounts
+ const [pinModalVisible, setPinModalVisible] = useState(false);
+ const [passwordModalVisible, setPasswordModalVisible] = useState(false);
+ const [selectedServer, setSelectedServer] = useState(
+ null,
+ );
+ const [selectedAccount, setSelectedAccount] =
+ useState(null);
+
+ // Server discovery
+ const {
+ servers: discoveredServers,
+ isSearching,
+ startDiscovery,
+ } = useJellyfinDiscovery();
+
+ // Auto login from URL params
+ useEffect(() => {
+ (async () => {
+ if (_apiUrl) {
+ await setServer({ address: _apiUrl });
+ setTimeout(() => {
+ if (_username && _password) {
+ setCredentials({ username: _username, password: _password });
+ login(_username, _password);
+ }
+ }, 0);
+ }
+ })();
+ }, [_apiUrl, _username, _password]);
+
+ // Update header
+ useEffect(() => {
+ navigation.setOptions({
+ headerTitle: serverName,
+ headerShown: false,
+ });
+ }, [serverName, navigation]);
+
+ const handleLogin = async () => {
+ const result = CredentialsSchema.safeParse(credentials);
+ if (!result.success) return;
+
+ if (saveAccount) {
+ setPendingLogin({
+ username: credentials.username,
+ password: credentials.password,
+ });
+ setShowSaveModal(true);
+ } else {
+ await performLogin(credentials.username, credentials.password);
+ }
+ };
+
+ const performLogin = async (
+ username: string,
+ password: string,
+ options?: {
+ saveAccount?: boolean;
+ securityType?: AccountSecurityType;
+ pinCode?: string;
+ },
+ ) => {
+ setLoading(true);
+ try {
+ await login(username, password, serverName, options);
+ } catch (error) {
+ if (error instanceof Error) {
+ Alert.alert(t("login.connection_failed"), error.message);
+ } else {
+ Alert.alert(
+ t("login.connection_failed"),
+ t("login.an_unexpected_error_occured"),
+ );
+ }
+ } finally {
+ setLoading(false);
+ setPendingLogin(null);
+ }
+ };
+
+ const handleSaveAccountConfirm = async (
+ securityType: AccountSecurityType,
+ pinCode?: string,
+ ) => {
+ setShowSaveModal(false);
+ if (pendingLogin) {
+ await performLogin(pendingLogin.username, pendingLogin.password, {
+ saveAccount: true,
+ securityType,
+ pinCode,
+ });
+ }
+ };
+
+ const handleQuickLoginWithSavedCredential = async (
+ serverUrl: string,
+ userId: string,
+ ) => {
+ await loginWithSavedCredential(serverUrl, userId);
+ };
+
+ const handlePasswordLogin = async (
+ serverUrl: string,
+ username: string,
+ password: string,
+ ) => {
+ await loginWithPassword(serverUrl, username, password);
+ };
+
+ const handleAddAccount = (server: SavedServer) => {
+ setServer({ address: server.address });
+ if (server.name) {
+ setServerName(server.name);
+ }
+ };
+
+ const handlePinRequired = (
+ server: SavedServer,
+ account: SavedServerAccount,
+ ) => {
+ setSelectedServer(server);
+ setSelectedAccount(account);
+ setPinModalVisible(true);
+ };
+
+ const handlePasswordRequired = (
+ server: SavedServer,
+ account: SavedServerAccount,
+ ) => {
+ setSelectedServer(server);
+ setSelectedAccount(account);
+ setPasswordModalVisible(true);
+ };
+
+ const handlePinSuccess = async () => {
+ setPinModalVisible(false);
+ if (selectedServer && selectedAccount) {
+ await handleQuickLoginWithSavedCredential(
+ selectedServer.address,
+ selectedAccount.userId,
+ );
+ }
+ setSelectedServer(null);
+ setSelectedAccount(null);
+ };
+
+ const handlePasswordSubmit = async (password: string) => {
+ if (selectedServer && selectedAccount) {
+ await handlePasswordLogin(
+ selectedServer.address,
+ selectedAccount.username,
+ password,
+ );
+ }
+ setPasswordModalVisible(false);
+ setSelectedServer(null);
+ setSelectedAccount(null);
+ };
+
+ const handleForgotPIN = async () => {
+ if (selectedServer) {
+ setSelectedServer(null);
+ setSelectedAccount(null);
+ setPinModalVisible(false);
+ }
+ };
+
+ const checkUrl = useCallback(async (url: string) => {
+ setLoadingServerCheck(true);
+ const baseUrl = url.replace(/^https?:\/\//i, "");
+ const protocols = ["https", "http"];
+ try {
+ return checkHttp(baseUrl, protocols);
+ } catch (e) {
+ if (e instanceof Error && e.message === "Server too old") {
+ throw e;
+ }
+ return undefined;
+ } finally {
+ setLoadingServerCheck(false);
+ }
+ }, []);
+
+ async function checkHttp(baseUrl: string, protocols: string[]) {
+ for (const protocol of protocols) {
+ try {
+ const response = await fetch(
+ `${protocol}://${baseUrl}/System/Info/Public`,
+ { mode: "cors" },
+ );
+ if (response.ok) {
+ const data = (await response.json()) as PublicSystemInfo;
+ const serverVersion = data.Version?.split(".");
+ if (serverVersion && +serverVersion[0] <= 10) {
+ if (+serverVersion[1] < 10) {
+ Alert.alert(
+ t("login.too_old_server_text"),
+ t("login.too_old_server_description"),
+ );
+ throw new Error("Server too old");
+ }
+ }
+ setServerName(data.ServerName || "");
+ return `${protocol}://${baseUrl}`;
+ }
+ } catch (e) {
+ if (e instanceof Error && e.message === "Server too old") {
+ throw e;
+ }
+ }
+ }
+ return undefined;
+ }
+
+ const handleConnect = useCallback(async (url: string) => {
+ url = url.trim().replace(/\/$/, "");
+ try {
+ const result = await checkUrl(url);
+ if (result === undefined) {
+ Alert.alert(
+ t("login.connection_failed"),
+ t("login.could_not_connect_to_server"),
+ );
+ return;
+ }
+ await setServer({ address: result });
+ } catch {}
+ }, []);
+
+ const handleQuickConnect = async () => {
+ try {
+ const code = await initiateQuickConnect();
+ if (code) {
+ Alert.alert(
+ t("login.quick_connect"),
+ t("login.enter_code_to_login", { code: code }),
+ [{ text: t("login.got_it") }],
+ );
+ }
+ } catch (_error) {
+ Alert.alert(
+ t("login.error_title"),
+ t("login.failed_to_initiate_quick_connect"),
+ );
+ }
+ };
+
+ return (
+
+
+ {api?.basePath ? (
+ // ==================== CREDENTIALS SCREEN ====================
+
+
+ {/* Back Button */}
+ removeServer()}
+ style={{
+ flexDirection: "row",
+ alignItems: "center",
+ marginBottom: 40,
+ }}
+ >
+
+
+ {t("login.change_server")}
+
+
+
+ {/* Title */}
+
+ {serverName ? (
+ <>
+ {`${t("login.login_to_title")} `}
+ {serverName}
+ >
+ ) : (
+ t("login.login_title")
+ )}
+
+
+ {api.basePath}
+
+
+ {/* Username Input */}
+
+
+ setCredentials((prev) => ({ ...prev, username: text }))
+ }
+ autoCapitalize='none'
+ autoCorrect={false}
+ textContentType='username'
+ returnKeyType='next'
+ hasTVPreferredFocus
+ />
+
+
+ {/* Password Input */}
+
+
+ setCredentials((prev) => ({ ...prev, password: text }))
+ }
+ secureTextEntry
+ autoCapitalize='none'
+ textContentType='password'
+ returnKeyType='done'
+ />
+
+
+ {/* Save Account Toggle */}
+
+
+
+
+ {/* Login Button */}
+
+
+
+
+ {/* Quick Connect Button */}
+
+
+
+ ) : (
+ // ==================== SERVER SELECTION SCREEN ====================
+
+
+ {/* Logo */}
+
+
+
+
+ {/* Title */}
+
+ Streamyfin
+
+
+ {t("server.enter_url_to_jellyfin_server")}
+
+
+ {/* Server URL Input */}
+
+
+
+
+ {/* Connect Button */}
+
+
+
+
+ {/* Server Discovery */}
+
+
+
+
+ {/* Discovered Servers */}
+ {discoveredServers.length > 0 && (
+
+
+ {t("server.servers")}
+
+ {discoveredServers.map((server) => (
+ {
+ setServerURL(server.address);
+ if (server.serverName) {
+ setServerName(server.serverName);
+ }
+ handleConnect(server.address);
+ }}
+ />
+ ))}
+
+ )}
+
+ {/* Previous Servers */}
+ handleConnect(s.address)}
+ onQuickLogin={handleQuickLoginWithSavedCredential}
+ onPasswordLogin={handlePasswordLogin}
+ onAddAccount={handleAddAccount}
+ onPinRequired={handlePinRequired}
+ onPasswordRequired={handlePasswordRequired}
+ />
+
+
+ )}
+
+
+ {/* Save Account Modal */}
+ {
+ setShowSaveModal(false);
+ setPendingLogin(null);
+ }}
+ onSave={handleSaveAccountConfirm}
+ username={pendingLogin?.username || credentials.username}
+ />
+
+ {/* PIN Entry Modal */}
+ {
+ setPinModalVisible(false);
+ setSelectedAccount(null);
+ setSelectedServer(null);
+ }}
+ onSuccess={handlePinSuccess}
+ onForgotPIN={handleForgotPIN}
+ serverUrl={selectedServer?.address || ""}
+ userId={selectedAccount?.userId || ""}
+ username={selectedAccount?.username || ""}
+ />
+
+ {/* Password Entry Modal */}
+ {
+ setPasswordModalVisible(false);
+ setSelectedAccount(null);
+ setSelectedServer(null);
+ }}
+ onSubmit={handlePasswordSubmit}
+ username={selectedAccount?.username || ""}
+ />
+
+ );
+};
+
+export default TVLogin;