mirror of
https://github.com/zimplexing/OrionTV.git
synced 2026-02-04 03:36:29 +08:00
feat: implement user authentication and logout functionality
- Added login/logout buttons to the HomeScreen and SettingsScreen. - Integrated authentication state management using Zustand and cookies. - Updated API to support username and password for login. - Enhanced PlayScreen to handle video playback based on user authentication. - Created a new detailStore to manage video details and sources. - Refactored playerStore to utilize detailStore for episode management. - Added sourceStore to manage video source toggling. - Updated settingsStore to fetch server configuration. - Improved error handling and user feedback with Toast notifications. - Cleaned up unused code and optimized imports across components.
This commit is contained in:
@@ -2,29 +2,38 @@ import React, { useState } from "react";
|
||||
import { Modal, View, Text, TextInput, StyleSheet, ActivityIndicator } from "react-native";
|
||||
import Toast from "react-native-toast-message";
|
||||
import useAuthStore from "@/stores/authStore";
|
||||
import { useSettingsStore } from "@/stores/settingsStore";
|
||||
import useHomeStore from "@/stores/homeStore";
|
||||
import { api } from "@/services/api";
|
||||
import { ThemedView } from "./ThemedView";
|
||||
import { ThemedText } from "./ThemedText";
|
||||
import { StyledButton } from "./StyledButton";
|
||||
|
||||
const LoginModal = () => {
|
||||
const { isLoginModalVisible, hideLoginModal } = useAuthStore();
|
||||
const { isLoginModalVisible, hideLoginModal, checkLoginStatus } = useAuthStore();
|
||||
const { serverConfig } = useSettingsStore();
|
||||
const { refreshPlayRecords } = useHomeStore();
|
||||
const [username, setUsername] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const handleLogin = async () => {
|
||||
if (!password) {
|
||||
Toast.show({ type: "error", text1: "请输入密码" });
|
||||
const isLocalStorage = serverConfig?.StorageType === "localstorage";
|
||||
if (!password || (!isLocalStorage && !username)) {
|
||||
Toast.show({ type: "error", text1: "请输入用户名和密码" });
|
||||
return;
|
||||
}
|
||||
setIsLoading(true);
|
||||
try {
|
||||
await api.login(password);
|
||||
await api.login(isLocalStorage ? undefined : username, password);
|
||||
await checkLoginStatus();
|
||||
await refreshPlayRecords();
|
||||
Toast.show({ type: "success", text1: "登录成功" });
|
||||
hideLoginModal();
|
||||
setUsername("");
|
||||
setPassword("");
|
||||
} catch (error) {
|
||||
Toast.show({ type: "error", text1: "登录失败", text2: "密码错误或服务器无法连接" });
|
||||
Toast.show({ type: "error", text1: "登录失败", text2: "用户名或密码错误" });
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
@@ -36,6 +45,16 @@ const LoginModal = () => {
|
||||
<ThemedView style={styles.container}>
|
||||
<ThemedText style={styles.title}>需要登录</ThemedText>
|
||||
<ThemedText style={styles.subtitle}>服务器需要验证您的身份</ThemedText>
|
||||
{serverConfig?.StorageType !== "localstorage" && (
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
placeholder="请输入用户名"
|
||||
placeholderTextColor="#888"
|
||||
value={username}
|
||||
onChangeText={setUsername}
|
||||
autoFocus
|
||||
/>
|
||||
)}
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
placeholder="请输入密码"
|
||||
@@ -43,7 +62,6 @@ const LoginModal = () => {
|
||||
secureTextEntry
|
||||
value={password}
|
||||
onChangeText={setPassword}
|
||||
autoFocus
|
||||
/>
|
||||
<StyledButton text={isLoading ? "" : "登录"} onPress={handleLogin} disabled={isLoading} style={styles.button}>
|
||||
{isLoading && <ActivityIndicator color="#fff" />}
|
||||
|
||||
@@ -5,6 +5,8 @@ import { ThemedText } from "@/components/ThemedText";
|
||||
import { MediaButton } from "@/components/MediaButton";
|
||||
|
||||
import usePlayerStore from "@/stores/playerStore";
|
||||
import useDetailStore from "@/stores/detailStore";
|
||||
import { useSources } from "@/stores/sourceStore";
|
||||
|
||||
interface PlayerControlsProps {
|
||||
showControls: boolean;
|
||||
@@ -13,9 +15,8 @@ interface PlayerControlsProps {
|
||||
|
||||
export const PlayerControls: React.FC<PlayerControlsProps> = ({ showControls, setShowControls }) => {
|
||||
const {
|
||||
detail,
|
||||
currentEpisodeIndex,
|
||||
currentSourceIndex,
|
||||
episodes,
|
||||
status,
|
||||
isSeeking,
|
||||
seekPosition,
|
||||
@@ -30,12 +31,15 @@ export const PlayerControls: React.FC<PlayerControlsProps> = ({ showControls, se
|
||||
outroStartTime,
|
||||
} = usePlayerStore();
|
||||
|
||||
const videoTitle = detail?.videoInfo?.title || "";
|
||||
const currentEpisode = detail?.episodes[currentEpisodeIndex];
|
||||
const { detail } = useDetailStore();
|
||||
const resources = useSources();
|
||||
|
||||
const videoTitle = detail?.title || "";
|
||||
const currentEpisode = episodes[currentEpisodeIndex];
|
||||
const currentEpisodeTitle = currentEpisode?.title;
|
||||
const currentSource = detail?.sources[currentSourceIndex];
|
||||
const currentSource = resources.find((r) => r.source === detail?.source);
|
||||
const currentSourceName = currentSource?.source_name;
|
||||
const hasNextEpisode = currentEpisodeIndex < (detail?.episodes.length || 0) - 1;
|
||||
const hasNextEpisode = currentEpisodeIndex < (episodes.length || 0) - 1;
|
||||
|
||||
const formatTime = (milliseconds: number) => {
|
||||
if (!milliseconds) return "00:00";
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
import React from "react";
|
||||
import { View, Text, StyleSheet, Modal, FlatList } from "react-native";
|
||||
import { StyledButton } from "./StyledButton";
|
||||
import useDetailStore from "@/stores/detailStore";
|
||||
import usePlayerStore from "@/stores/playerStore";
|
||||
|
||||
export const SourceSelectionModal: React.FC = () => {
|
||||
const { showSourceModal, sources, currentSourceIndex, switchSource, setShowSourceModal } = usePlayerStore();
|
||||
const { showSourceModal, setShowSourceModal } = usePlayerStore();
|
||||
const { searchResults, detail, setDetail } = useDetailStore();
|
||||
|
||||
const onSelectSource = (index: number) => {
|
||||
if (index !== currentSourceIndex) {
|
||||
switchSource(index);
|
||||
if (searchResults[index].source !== detail?.source) {
|
||||
setDetail(searchResults[index]);
|
||||
}
|
||||
setShowSourceModal(false);
|
||||
};
|
||||
@@ -23,16 +25,16 @@ export const SourceSelectionModal: React.FC = () => {
|
||||
<View style={styles.modalContent}>
|
||||
<Text style={styles.modalTitle}>选择播放源</Text>
|
||||
<FlatList
|
||||
data={sources}
|
||||
data={searchResults}
|
||||
numColumns={3}
|
||||
contentContainerStyle={styles.sourceList}
|
||||
keyExtractor={(item, index) => `source-${item.source}-${item.id}-${index}`}
|
||||
keyExtractor={(item, index) => `source-${item.source}-${index}`}
|
||||
renderItem={({ item, index }) => (
|
||||
<StyledButton
|
||||
text={item.source_name}
|
||||
onPress={() => onSelectSource(index)}
|
||||
isSelected={currentSourceIndex === index}
|
||||
hasTVPreferredFocus={currentSourceIndex === index}
|
||||
isSelected={detail?.source === item.source}
|
||||
hasTVPreferredFocus={detail?.source === item.source}
|
||||
style={styles.sourceItem}
|
||||
textStyle={styles.sourceItemText}
|
||||
/>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import React, { useState, useEffect, useCallback } from "react";
|
||||
import { StyleSheet, View, Switch, ActivityIndicator, FlatList, Pressable, Animated } from "react-native";
|
||||
import React, { useState, useCallback } from "react";
|
||||
import { StyleSheet, Switch, FlatList, Pressable, Animated } from "react-native";
|
||||
import { useTVEventHandler } from "react-native";
|
||||
import { ThemedText } from "@/components/ThemedText";
|
||||
import { SettingsSection } from "./SettingsSection";
|
||||
import { api, ApiSite } from "@/services/api";
|
||||
import { useSettingsStore } from "@/stores/settingsStore";
|
||||
import useSourceStore, { useSources } from "@/stores/sourceStore";
|
||||
|
||||
interface VideoSourceSectionProps {
|
||||
onChanged: () => void;
|
||||
@@ -13,56 +13,18 @@ interface VideoSourceSectionProps {
|
||||
}
|
||||
|
||||
export const VideoSourceSection: React.FC<VideoSourceSectionProps> = ({ onChanged, onFocus, onBlur }) => {
|
||||
const [resources, setResources] = useState<ApiSite[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [focusedIndex, setFocusedIndex] = useState<number | null>(null);
|
||||
const [isSectionFocused, setIsSectionFocused] = useState(false);
|
||||
const { videoSource, setVideoSource } = useSettingsStore();
|
||||
const { videoSource } = useSettingsStore();
|
||||
const resources = useSources();
|
||||
const { toggleResourceEnabled } = useSourceStore();
|
||||
|
||||
useEffect(() => {
|
||||
fetchResources();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const fetchResources = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const resourcesList = await api.getResources();
|
||||
setResources(resourcesList);
|
||||
|
||||
if (videoSource.enabledAll && Object.keys(videoSource.sources).length === 0) {
|
||||
const allResourceKeys: { [key: string]: boolean } = {};
|
||||
for (const resource of resourcesList) {
|
||||
allResourceKeys[resource.key] = true;
|
||||
}
|
||||
setVideoSource({
|
||||
enabledAll: true,
|
||||
sources: allResourceKeys,
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
setError("获取播放源失败");
|
||||
console.error("Failed to fetch resources:", err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const toggleResourceEnabled = useCallback(
|
||||
const handleToggle = useCallback(
|
||||
(resourceKey: string) => {
|
||||
const isEnabled = videoSource.sources[resourceKey];
|
||||
|
||||
const newEnabledSources = { ...videoSource.sources, [resourceKey]: !isEnabled };
|
||||
|
||||
setVideoSource({
|
||||
enabledAll: Object.values(newEnabledSources).every((enabled) => enabled),
|
||||
sources: newEnabledSources,
|
||||
});
|
||||
|
||||
toggleResourceEnabled(resourceKey);
|
||||
onChanged();
|
||||
},
|
||||
[videoSource.sources, setVideoSource, onChanged]
|
||||
[onChanged, toggleResourceEnabled]
|
||||
);
|
||||
|
||||
const handleSectionFocus = () => {
|
||||
@@ -83,20 +45,20 @@ export const VideoSourceSection: React.FC<VideoSourceSectionProps> = ({ onChange
|
||||
if (focusedIndex !== null) {
|
||||
const resource = resources[focusedIndex];
|
||||
if (resource) {
|
||||
toggleResourceEnabled(resource.key);
|
||||
handleToggle(resource.source);
|
||||
}
|
||||
} else if (isSectionFocused) {
|
||||
setFocusedIndex(0);
|
||||
}
|
||||
}
|
||||
},
|
||||
[isSectionFocused, focusedIndex, resources, toggleResourceEnabled]
|
||||
[isSectionFocused, focusedIndex, resources, handleToggle]
|
||||
);
|
||||
|
||||
useTVEventHandler(handleTVEvent);
|
||||
|
||||
const renderResourceItem = ({ item, index }: { item: ApiSite; index: number }) => {
|
||||
const isEnabled = videoSource.enabledAll || videoSource.sources[item.key];
|
||||
const renderResourceItem = ({ item, index }: { item: { source: string; source_name: string }; index: number }) => {
|
||||
const isEnabled = videoSource.enabledAll || videoSource.sources[item.source];
|
||||
const isFocused = focusedIndex === index;
|
||||
|
||||
return (
|
||||
@@ -107,7 +69,7 @@ export const VideoSourceSection: React.FC<VideoSourceSectionProps> = ({ onChange
|
||||
onFocus={() => setFocusedIndex(index)}
|
||||
onBlur={() => setFocusedIndex(null)}
|
||||
>
|
||||
<ThemedText style={styles.resourceName}>{item.name}</ThemedText>
|
||||
<ThemedText style={styles.resourceName}>{item.source_name}</ThemedText>
|
||||
<Switch
|
||||
value={isEnabled}
|
||||
onValueChange={() => {}} // 禁用Switch的直接交互
|
||||
@@ -124,20 +86,11 @@ export const VideoSourceSection: React.FC<VideoSourceSectionProps> = ({ onChange
|
||||
<SettingsSection focusable onFocus={handleSectionFocus} onBlur={handleSectionBlur}>
|
||||
<ThemedText style={styles.sectionTitle}>播放源配置</ThemedText>
|
||||
|
||||
{loading && (
|
||||
<View style={styles.loadingContainer}>
|
||||
<ActivityIndicator size="small" />
|
||||
<ThemedText style={styles.loadingText}>加载中...</ThemedText>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{error && <ThemedText style={styles.errorText}>{error}</ThemedText>}
|
||||
|
||||
{!loading && !error && resources.length > 0 && (
|
||||
{resources.length > 0 && (
|
||||
<FlatList
|
||||
data={resources}
|
||||
renderItem={renderResourceItem}
|
||||
keyExtractor={(item) => item.key}
|
||||
keyExtractor={(item) => item.source}
|
||||
numColumns={3}
|
||||
columnWrapperStyle={styles.row}
|
||||
contentContainerStyle={styles.flatListContainer}
|
||||
@@ -154,22 +107,6 @@ const styles = StyleSheet.create({
|
||||
fontWeight: "bold",
|
||||
marginBottom: 16,
|
||||
},
|
||||
loadingContainer: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
padding: 16,
|
||||
},
|
||||
loadingText: {
|
||||
marginLeft: 8,
|
||||
color: "#888",
|
||||
},
|
||||
errorText: {
|
||||
color: "#ff4444",
|
||||
fontSize: 14,
|
||||
textAlign: "center",
|
||||
padding: 16,
|
||||
},
|
||||
flatListContainer: {
|
||||
gap: 12,
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user