feat: enhance LoginModal with TV event handling and input focus management

This commit is contained in:
zimplexing
2025-07-15 22:33:11 +08:00
parent d44e9fe9ae
commit 0b1fa9df6d
4 changed files with 73 additions and 35 deletions

View File

@@ -5,7 +5,7 @@ import { ThemedText } from "@/components/ThemedText";
import { api } from "@/services/api";
import VideoCard from "@/components/VideoCard.tv";
import { useFocusEffect, useRouter } from "expo-router";
import { Search, Settings, LogOut, Star } from "lucide-react-native";
import { Search, Settings, LogOut, Heart } from "lucide-react-native";
import { StyledButton } from "@/components/StyledButton";
import useHomeStore, { RowItem, Category } from "@/stores/homeStore";
import useAuthStore from "@/stores/authStore";
@@ -125,7 +125,7 @@ export default function HomeScreen() {
</View>
<View style={styles.rightHeaderButtons}>
<StyledButton style={styles.searchButton} onPress={() => router.push("/favorites")} variant="ghost">
<Star color={colorScheme === "dark" ? "white" : "black"} size={24} />
<Heart color={colorScheme === "dark" ? "white" : "black"} size={24} />
</StyledButton>
<StyledButton
style={styles.searchButton}

View File

@@ -1,5 +1,5 @@
import React, { useState, useRef } from "react";
import { Modal, View, TextInput, StyleSheet, ActivityIndicator } from "react-native";
import React, { useState, useRef, useEffect } from "react";
import { Modal, View, TextInput, StyleSheet, ActivityIndicator, useTVEventHandler } from "react-native";
import Toast from "react-native-toast-message";
import useAuthStore from "@/stores/authStore";
import { useSettingsStore } from "@/stores/settingsStore";
@@ -16,8 +16,49 @@ const LoginModal = () => {
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const [isLoading, setIsLoading] = useState(false);
const usernameInputRef = useRef<TextInput>(null);
const passwordInputRef = useRef<TextInput>(null);
const loginButtonRef = useRef<View>(null);
const [focused, setFocused] = useState("username");
const tvEventHandler = (evt: any) => {
if (!evt || !isLoginModalVisible) {
return;
}
const isUsernameVisible = serverConfig?.StorageType !== "localstorage";
if (evt.eventType === "down") {
if (focused === "username" && isUsernameVisible) {
passwordInputRef.current?.focus();
} else if (focused === "password") {
loginButtonRef.current?.focus();
}
}
if (evt.eventType === "up") {
if (focused === "button") {
passwordInputRef.current?.focus();
} else if (focused === "password" && isUsernameVisible) {
usernameInputRef.current?.focus();
}
}
};
useTVEventHandler(tvEventHandler);
useEffect(() => {
if (isLoginModalVisible) {
const isUsernameVisible = serverConfig?.StorageType !== "localstorage";
setTimeout(() => {
if (isUsernameVisible) {
usernameInputRef.current?.focus();
} else {
passwordInputRef.current?.focus();
}
}, 200);
}
}, [isLoginModalVisible, serverConfig]);
const handleLogin = async () => {
const isLocalStorage = serverConfig?.StorageType === "localstorage";
@@ -49,14 +90,14 @@ const LoginModal = () => {
<ThemedText style={styles.subtitle}></ThemedText>
{serverConfig?.StorageType !== "localstorage" && (
<TextInput
ref={usernameInputRef}
style={styles.input}
placeholder="请输入用户名"
placeholderTextColor="#888"
value={username}
onChangeText={setUsername}
autoFocus
returnKeyType="next"
onSubmitEditing={() => passwordInputRef.current?.focus()}
onFocus={() => setFocused("username")}
/>
)}
<TextInput
@@ -67,11 +108,13 @@ const LoginModal = () => {
secureTextEntry
value={password}
onChangeText={setPassword}
returnKeyType="next"
onSubmitEditing={() => loginButtonRef.current?.focus()}
returnKeyType="go"
onFocus={() => setFocused("password")}
onSubmitEditing={handleLogin}
/>
<StyledButton
ref={loginButtonRef}
onFocus={() => setFocused("button")}
text={isLoading ? "" : "登录"}
onPress={handleLogin}
disabled={isLoading}

View File

@@ -1,6 +1,6 @@
import { create } from "zustand";
import Cookies from "@react-native-cookies/cookies";
import { api } from "@/services/api";
import { api, ServerConfig } from "@/services/api";
interface AuthState {
isLoggedIn: boolean;
@@ -22,23 +22,18 @@ const useAuthStore = create<AuthState>((set) => ({
return;
}
try {
await api.login();
set({ isLoginModalVisible: true });
} catch {
try {
const cookies = await Cookies.get(api.baseURL);
const isLoggedIn = cookies && !!cookies.auth;
set({ isLoggedIn });
if (!isLoggedIn) {
set({ isLoginModalVisible: true });
}
} catch (error) {
console.info("Failed to check login status:", error);
if (error instanceof Error && error.message === "UNAUTHORIZED") {
set({ isLoggedIn: false, isLoginModalVisible: true });
} else {
set({ isLoggedIn: false });
}
const cookies = await Cookies.get(api.baseURL);
const isLoggedIn = cookies && !!cookies.auth;
set({ isLoggedIn });
if (!isLoggedIn) {
set({ isLoginModalVisible: true });
}
} catch (error) {
console.info("Failed to check login status:", error);
if (error instanceof Error && error.message === "UNAUTHORIZED") {
set({ isLoggedIn: false, isLoginModalVisible: true });
} else {
set({ isLoggedIn: false });
}
}
},

View File

@@ -122,7 +122,7 @@ const useDetailStore = create<DetailState>((set, get) => ({
}
}
} catch (error) {
console.warn(`Failed to fetch from ${resource.name}:`, error);
console.info(`Failed to fetch from ${resource.name}:`, error);
}
});
@@ -133,11 +133,11 @@ const useDetailStore = create<DetailState>((set, get) => ({
set({ error: "未找到任何播放源" });
}
if (get().detail) {
const { source, id } = get().detail!;
const isFavorited = await FavoriteManager.isFavorited(source, id.toString());
set({ isFavorited });
}
// if (get().detail) {
// const { source, id } = get().detail!;
// const isFavorited = await FavoriteManager.isFavorited(source, id.toString());
// set({ isFavorited });
// }
} catch (e) {
if ((e as Error).name !== "AbortError") {
set({ error: e instanceof Error ? e.message : "获取数据失败" });
@@ -151,9 +151,9 @@ const useDetailStore = create<DetailState>((set, get) => ({
setDetail: async (detail) => {
set({ detail });
const { source, id } = detail;
const isFavorited = await FavoriteManager.isFavorited(source, id.toString());
set({ isFavorited });
// const { source, id } = detail;
// const isFavorited = await FavoriteManager.isFavorited(source, id.toString());
// set({ isFavorited });
},
abort: () => {