From 2bed3a4d008e6a824ec699a5da3352f5f60f0ee6 Mon Sep 17 00:00:00 2001 From: zimplexing Date: Mon, 14 Jul 2025 22:55:55 +0800 Subject: [PATCH] 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. --- app/_layout.tsx | 8 +- app/detail.tsx | 125 ++---------------- app/index.tsx | 9 +- app/play.tsx | 57 ++++----- app/settings.tsx | 2 + backend/src/config/config.json | 12 +- backend/src/data/playrecords.json | 2 +- backend/src/routes/login.ts | 25 +++- components/LoginModal.tsx | 30 ++++- components/PlayerControls.tsx | 16 ++- components/SourceSelectionModal.tsx | 16 ++- components/settings/VideoSourceSection.tsx | 95 +++----------- package.json | 1 + services/api.ts | 18 +-- stores/authStore.ts | 27 ++++ stores/detailStore.ts | 141 +++++++++++++++++++++ stores/homeStore.ts | 21 +++ stores/playerStore.ts | 119 ++++++----------- stores/settingsStore.ts | 16 ++- stores/sourceStore.ts | 24 ++++ yarn.lock | 7 + 21 files changed, 413 insertions(+), 358 deletions(-) create mode 100644 stores/detailStore.ts create mode 100644 stores/sourceStore.ts diff --git a/app/_layout.tsx b/app/_layout.tsx index 08721d4..6068172 100644 --- a/app/_layout.tsx +++ b/app/_layout.tsx @@ -9,6 +9,7 @@ import Toast from "react-native-toast-message"; import { useSettingsStore } from "@/stores/settingsStore"; import { useRemoteControlStore } from "@/stores/remoteControlStore"; import LoginModal from "@/components/LoginModal"; +import useAuthStore from "@/stores/authStore"; // Prevent the splash screen from auto-hiding before asset loading is complete. SplashScreen.preventAutoHideAsync(); @@ -20,10 +21,13 @@ export default function RootLayout() { }); const { loadSettings, remoteInputEnabled } = useSettingsStore(); const { startServer, stopServer } = useRemoteControlStore(); + const { checkLoginStatus } = useAuthStore(); useEffect(() => { - loadSettings(); - }, [loadSettings]); + loadSettings().then(() => { + checkLoginStatus(); + }); + }, [loadSettings, checkLoginStatus]); useEffect(() => { if (loaded || error) { diff --git a/app/detail.tsx b/app/detail.tsx index 0df4093..111ab2e 100644 --- a/app/detail.tsx +++ b/app/detail.tsx @@ -1,134 +1,37 @@ -import React, { useEffect, useState, useRef } from "react"; +import React, { useEffect } from "react"; import { View, Text, StyleSheet, Image, ScrollView, ActivityIndicator } from "react-native"; import { useLocalSearchParams, useRouter } from "expo-router"; import { ThemedView } from "@/components/ThemedView"; import { ThemedText } from "@/components/ThemedText"; -import { api, SearchResult } from "@/services/api"; -import { getResolutionFromM3U8 } from "@/services/m3u8"; import { StyledButton } from "@/components/StyledButton"; -import { useSettingsStore } from "@/stores/settingsStore"; +import useDetailStore from "@/stores/detailStore"; export default function DetailScreen() { - const { source, q } = useLocalSearchParams(); + const { q } = useLocalSearchParams<{ q: string }>(); const router = useRouter(); - const [searchResults, setSearchResults] = useState<(SearchResult & { resolution?: string | null })[]>([]); - const [detail, setDetail] = useState<(SearchResult & { resolution?: string | null }) | null>(null); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - const [allSourcesLoaded, setAllSourcesLoaded] = useState(false); - const controllerRef = useRef(null); - const { videoSource } = useSettingsStore(); + + const { detail, searchResults, loading, error, allSourcesLoaded, init, setDetail, abort } = useDetailStore(); useEffect(() => { - if (controllerRef.current) { - controllerRef.current.abort(); + if (q) { + init(q); } - controllerRef.current = new AbortController(); - const signal = controllerRef.current.signal; - - if (typeof q === "string") { - const fetchDetailData = async () => { - setLoading(true); - setSearchResults([]); - setDetail(null); - setError(null); - setAllSourcesLoaded(false); - - try { - const allResources = await api.getResources(signal); - if (!allResources || allResources.length === 0) { - setError("没有可用的播放源"); - setLoading(false); - return; - } - - // Filter resources based on enabled sources in settings - const resources = videoSource.enabledAll - ? allResources - : allResources.filter((resource) => videoSource.sources[resource.key]); - - if (!videoSource.enabledAll && resources.length === 0) { - setError("请到设置页面启用的播放源"); - setLoading(false); - return; - } - - let foundFirstResult = false; - // Prioritize source from params if available - if (typeof source === "string") { - const index = resources.findIndex((r) => r.key === source); - if (index > 0) { - resources.unshift(resources.splice(index, 1)[0]); - } - } - - for (const resource of resources) { - try { - const { results } = await api.searchVideo(q, resource.key, signal); - if (results && results.length > 0) { - const searchResult = results[0]; - - let resolution; - try { - if (searchResult.episodes && searchResult.episodes.length > 0) { - resolution = await getResolutionFromM3U8(searchResult.episodes[0], signal); - } - } catch (e) { - if ((e as Error).name !== "AbortError") { - console.error(`Failed to get resolution for ${resource.name}`, e); - } - } - - const resultWithResolution = { ...searchResult, resolution }; - - setSearchResults((prev) => [...prev, resultWithResolution]); - - if (!foundFirstResult) { - setDetail(resultWithResolution); - foundFirstResult = true; - setLoading(false); - } - } - } catch (e) { - if ((e as Error).name !== "AbortError") { - console.error(`Error searching in resource ${resource.name}:`, e); - } - } - } - - if (!foundFirstResult) { - setError("未找到播放源"); - setLoading(false); - } - } catch (e) { - if ((e as Error).name !== "AbortError") { - setError(e instanceof Error ? e.message : "获取资源列表失败"); - setLoading(false); - } - } finally { - setAllSourcesLoaded(true); - } - }; - fetchDetailData(); - } - return () => { - controllerRef.current?.abort(); + abort(); }; - }, [q, source, videoSource.enabledAll, videoSource.sources]); + }, [abort, init, q]); - const handlePlay = (episodeName: string, episodeIndex: number) => { + const handlePlay = (episodeIndex: number) => { if (!detail) return; - controllerRef.current?.abort(); // Cancel any ongoing fetches + abort(); // Cancel any ongoing fetches router.push({ pathname: "/play", params: { + // Pass necessary identifiers, the rest will be in the store + q: detail.title, source: detail.source, id: detail.id.toString(), - episodeUrl: episodeName, // The "episode" is actually the URL episodeIndex: episodeIndex.toString(), - title: detail.title, - poster: detail.poster, }, }); }; @@ -217,7 +120,7 @@ export default function DetailScreen() { handlePlay(episode, index)} + onPress={() => handlePlay(index)} text={`第 ${index + 1} 集`} textStyle={styles.episodeButtonText} /> diff --git a/app/index.tsx b/app/index.tsx index 18f186d..c2f8b13 100644 --- a/app/index.tsx +++ b/app/index.tsx @@ -5,9 +5,10 @@ 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 } from "lucide-react-native"; +import { Search, Settings, LogOut } from "lucide-react-native"; import { StyledButton } from "@/components/StyledButton"; import useHomeStore, { RowItem, Category } from "@/stores/homeStore"; +import useAuthStore from "@/stores/authStore"; const NUM_COLUMNS = 5; const { width } = Dimensions.get("window"); @@ -31,6 +32,7 @@ export default function HomeScreen() { selectCategory, refreshPlayRecords, } = useHomeStore(); + const { isLoggedIn, logout } = useAuthStore(); useFocusEffect( useCallback(() => { @@ -132,6 +134,11 @@ export default function HomeScreen() { router.push("/settings")} variant="ghost"> + {isLoggedIn && ( + + + + )} diff --git a/app/play.tsx b/app/play.tsx index 6104f01..9ec5610 100644 --- a/app/play.tsx +++ b/app/play.tsx @@ -10,50 +10,51 @@ import { SourceSelectionModal } from "@/components/SourceSelectionModal"; import { SeekingBar } from "@/components/SeekingBar"; import { NextEpisodeOverlay } from "@/components/NextEpisodeOverlay"; import { LoadingOverlay } from "@/components/LoadingOverlay"; -import usePlayerStore from "@/stores/playerStore"; +import useDetailStore from "@/stores/detailStore"; import { useTVRemoteHandler } from "@/hooks/useTVRemoteHandler"; +import Toast from "react-native-toast-message"; +import usePlayerStore from "@/stores/playerStore"; export default function PlayScreen() { const videoRef = useRef