mirror of
https://github.com/zimplexing/OrionTV.git
synced 2026-06-10 23:33:12 +08:00
feat: enhance LoginModal with TV event handling and input focus management
This commit is contained in:
@@ -5,7 +5,7 @@ import { ThemedText } from "@/components/ThemedText";
|
|||||||
import { api } from "@/services/api";
|
import { api } from "@/services/api";
|
||||||
import VideoCard from "@/components/VideoCard.tv";
|
import VideoCard from "@/components/VideoCard.tv";
|
||||||
import { useFocusEffect, useRouter } from "expo-router";
|
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 { StyledButton } from "@/components/StyledButton";
|
||||||
import useHomeStore, { RowItem, Category } from "@/stores/homeStore";
|
import useHomeStore, { RowItem, Category } from "@/stores/homeStore";
|
||||||
import useAuthStore from "@/stores/authStore";
|
import useAuthStore from "@/stores/authStore";
|
||||||
@@ -125,7 +125,7 @@ export default function HomeScreen() {
|
|||||||
</View>
|
</View>
|
||||||
<View style={styles.rightHeaderButtons}>
|
<View style={styles.rightHeaderButtons}>
|
||||||
<StyledButton style={styles.searchButton} onPress={() => router.push("/favorites")} variant="ghost">
|
<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>
|
||||||
<StyledButton
|
<StyledButton
|
||||||
style={styles.searchButton}
|
style={styles.searchButton}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useState, useRef } from "react";
|
import React, { useState, useRef, useEffect } from "react";
|
||||||
import { Modal, View, TextInput, StyleSheet, ActivityIndicator } from "react-native";
|
import { Modal, View, TextInput, StyleSheet, ActivityIndicator, useTVEventHandler } from "react-native";
|
||||||
import Toast from "react-native-toast-message";
|
import Toast from "react-native-toast-message";
|
||||||
import useAuthStore from "@/stores/authStore";
|
import useAuthStore from "@/stores/authStore";
|
||||||
import { useSettingsStore } from "@/stores/settingsStore";
|
import { useSettingsStore } from "@/stores/settingsStore";
|
||||||
@@ -16,8 +16,49 @@ const LoginModal = () => {
|
|||||||
const [username, setUsername] = useState("");
|
const [username, setUsername] = useState("");
|
||||||
const [password, setPassword] = useState("");
|
const [password, setPassword] = useState("");
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const usernameInputRef = useRef<TextInput>(null);
|
||||||
const passwordInputRef = useRef<TextInput>(null);
|
const passwordInputRef = useRef<TextInput>(null);
|
||||||
const loginButtonRef = useRef<View>(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 handleLogin = async () => {
|
||||||
const isLocalStorage = serverConfig?.StorageType === "localstorage";
|
const isLocalStorage = serverConfig?.StorageType === "localstorage";
|
||||||
@@ -49,14 +90,14 @@ const LoginModal = () => {
|
|||||||
<ThemedText style={styles.subtitle}>服务器需要验证您的身份</ThemedText>
|
<ThemedText style={styles.subtitle}>服务器需要验证您的身份</ThemedText>
|
||||||
{serverConfig?.StorageType !== "localstorage" && (
|
{serverConfig?.StorageType !== "localstorage" && (
|
||||||
<TextInput
|
<TextInput
|
||||||
|
ref={usernameInputRef}
|
||||||
style={styles.input}
|
style={styles.input}
|
||||||
placeholder="请输入用户名"
|
placeholder="请输入用户名"
|
||||||
placeholderTextColor="#888"
|
placeholderTextColor="#888"
|
||||||
value={username}
|
value={username}
|
||||||
onChangeText={setUsername}
|
onChangeText={setUsername}
|
||||||
autoFocus
|
|
||||||
returnKeyType="next"
|
returnKeyType="next"
|
||||||
onSubmitEditing={() => passwordInputRef.current?.focus()}
|
onFocus={() => setFocused("username")}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<TextInput
|
<TextInput
|
||||||
@@ -67,11 +108,13 @@ const LoginModal = () => {
|
|||||||
secureTextEntry
|
secureTextEntry
|
||||||
value={password}
|
value={password}
|
||||||
onChangeText={setPassword}
|
onChangeText={setPassword}
|
||||||
returnKeyType="next"
|
returnKeyType="go"
|
||||||
onSubmitEditing={() => loginButtonRef.current?.focus()}
|
onFocus={() => setFocused("password")}
|
||||||
|
onSubmitEditing={handleLogin}
|
||||||
/>
|
/>
|
||||||
<StyledButton
|
<StyledButton
|
||||||
ref={loginButtonRef}
|
ref={loginButtonRef}
|
||||||
|
onFocus={() => setFocused("button")}
|
||||||
text={isLoading ? "" : "登录"}
|
text={isLoading ? "" : "登录"}
|
||||||
onPress={handleLogin}
|
onPress={handleLogin}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { create } from "zustand";
|
import { create } from "zustand";
|
||||||
import Cookies from "@react-native-cookies/cookies";
|
import Cookies from "@react-native-cookies/cookies";
|
||||||
import { api } from "@/services/api";
|
import { api, ServerConfig } from "@/services/api";
|
||||||
|
|
||||||
interface AuthState {
|
interface AuthState {
|
||||||
isLoggedIn: boolean;
|
isLoggedIn: boolean;
|
||||||
@@ -22,23 +22,18 @@ const useAuthStore = create<AuthState>((set) => ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await api.login();
|
const cookies = await Cookies.get(api.baseURL);
|
||||||
set({ isLoginModalVisible: true });
|
const isLoggedIn = cookies && !!cookies.auth;
|
||||||
} catch {
|
set({ isLoggedIn });
|
||||||
try {
|
if (!isLoggedIn) {
|
||||||
const cookies = await Cookies.get(api.baseURL);
|
set({ isLoginModalVisible: true });
|
||||||
const isLoggedIn = cookies && !!cookies.auth;
|
}
|
||||||
set({ isLoggedIn });
|
} catch (error) {
|
||||||
if (!isLoggedIn) {
|
console.info("Failed to check login status:", error);
|
||||||
set({ isLoginModalVisible: true });
|
if (error instanceof Error && error.message === "UNAUTHORIZED") {
|
||||||
}
|
set({ isLoggedIn: false, isLoginModalVisible: true });
|
||||||
} catch (error) {
|
} else {
|
||||||
console.info("Failed to check login status:", error);
|
set({ isLoggedIn: false });
|
||||||
if (error instanceof Error && error.message === "UNAUTHORIZED") {
|
|
||||||
set({ isLoggedIn: false, isLoginModalVisible: true });
|
|
||||||
} else {
|
|
||||||
set({ isLoggedIn: false });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -122,7 +122,7 @@ const useDetailStore = create<DetailState>((set, get) => ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} 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: "未找到任何播放源" });
|
set({ error: "未找到任何播放源" });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (get().detail) {
|
// if (get().detail) {
|
||||||
const { source, id } = get().detail!;
|
// const { source, id } = get().detail!;
|
||||||
const isFavorited = await FavoriteManager.isFavorited(source, id.toString());
|
// const isFavorited = await FavoriteManager.isFavorited(source, id.toString());
|
||||||
set({ isFavorited });
|
// set({ isFavorited });
|
||||||
}
|
// }
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if ((e as Error).name !== "AbortError") {
|
if ((e as Error).name !== "AbortError") {
|
||||||
set({ error: e instanceof Error ? e.message : "获取数据失败" });
|
set({ error: e instanceof Error ? e.message : "获取数据失败" });
|
||||||
@@ -151,9 +151,9 @@ const useDetailStore = create<DetailState>((set, get) => ({
|
|||||||
|
|
||||||
setDetail: async (detail) => {
|
setDetail: async (detail) => {
|
||||||
set({ detail });
|
set({ detail });
|
||||||
const { source, id } = detail;
|
// const { source, id } = detail;
|
||||||
const isFavorited = await FavoriteManager.isFavorited(source, id.toString());
|
// const isFavorited = await FavoriteManager.isFavorited(source, id.toString());
|
||||||
set({ isFavorited });
|
// set({ isFavorited });
|
||||||
},
|
},
|
||||||
|
|
||||||
abort: () => {
|
abort: () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user