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:
@@ -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) {
|
||||
|
||||
125
app/detail.tsx
125
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<string | null>(null);
|
||||
const [allSourcesLoaded, setAllSourcesLoaded] = useState(false);
|
||||
const controllerRef = useRef<AbortController | null>(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() {
|
||||
<StyledButton
|
||||
key={index}
|
||||
style={styles.episodeButton}
|
||||
onPress={() => handlePlay(episode, index)}
|
||||
onPress={() => handlePlay(index)}
|
||||
text={`第 ${index + 1} 集`}
|
||||
textStyle={styles.episodeButtonText}
|
||||
/>
|
||||
|
||||
@@ -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() {
|
||||
<StyledButton style={styles.searchButton} onPress={() => router.push("/settings")} variant="ghost">
|
||||
<Settings color={colorScheme === "dark" ? "white" : "black"} size={24} />
|
||||
</StyledButton>
|
||||
{isLoggedIn && (
|
||||
<StyledButton style={styles.searchButton} onPress={logout} variant="ghost">
|
||||
<LogOut color={colorScheme === "dark" ? "white" : "black"} size={24} />
|
||||
</StyledButton>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
|
||||
57
app/play.tsx
57
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<Video>(null);
|
||||
const router = useRouter();
|
||||
useKeepAwake();
|
||||
const { source, id, episodeIndex, position } = useLocalSearchParams<{
|
||||
source: string;
|
||||
id: string;
|
||||
const { episodeIndex: episodeIndexStr } = useLocalSearchParams<{
|
||||
episodeIndex: string;
|
||||
position: string;
|
||||
}>();
|
||||
const episodeIndex = parseInt(episodeIndexStr || "0", 10);
|
||||
|
||||
const { detail } = useDetailStore();
|
||||
const {
|
||||
detail,
|
||||
episodes,
|
||||
currentEpisodeIndex,
|
||||
status,
|
||||
isLoading,
|
||||
showControls,
|
||||
showEpisodeModal,
|
||||
showSourceModal,
|
||||
showNextEpisodeOverlay,
|
||||
initialPosition,
|
||||
introEndTime,
|
||||
setVideoRef,
|
||||
loadVideo,
|
||||
handlePlaybackStatusUpdate,
|
||||
setShowControls,
|
||||
setShowEpisodeModal,
|
||||
setShowSourceModal,
|
||||
setShowNextEpisodeOverlay,
|
||||
reset,
|
||||
playEpisode,
|
||||
togglePlayPause,
|
||||
seek,
|
||||
} = usePlayerStore();
|
||||
|
||||
useEffect(() => {
|
||||
setVideoRef(videoRef);
|
||||
if (source && id) {
|
||||
loadVideo(source, id, parseInt(episodeIndex || "0", 10), parseInt(position || "0", 10));
|
||||
}
|
||||
// The detail is already in the detailStore, no need to load it again.
|
||||
// We just need to set the current episode in the player store.
|
||||
usePlayerStore.setState({
|
||||
currentEpisodeIndex: episodeIndex,
|
||||
episodes: detail?.episodes.map((ep, index) => ({ url: ep, title: `第 ${index + 1} 集` })) || [],
|
||||
});
|
||||
|
||||
return () => {
|
||||
reset(); // Reset state when component unmounts
|
||||
};
|
||||
}, [source, id, episodeIndex, position, setVideoRef, loadVideo, reset]);
|
||||
}, [episodeIndex, detail, setVideoRef, reset]);
|
||||
|
||||
const { onScreenPress } = useTVRemoteHandler();
|
||||
|
||||
@@ -70,17 +71,9 @@ export default function PlayScreen() {
|
||||
const backHandler = BackHandler.addEventListener("hardwareBackPress", backAction);
|
||||
|
||||
return () => backHandler.remove();
|
||||
}, [
|
||||
showControls,
|
||||
showEpisodeModal,
|
||||
showSourceModal,
|
||||
setShowControls,
|
||||
setShowEpisodeModal,
|
||||
setShowSourceModal,
|
||||
router,
|
||||
]);
|
||||
}, [showControls, setShowControls, router]);
|
||||
|
||||
if (!detail && isLoading) {
|
||||
if (!detail) {
|
||||
return (
|
||||
<ThemedView style={[styles.container, styles.centered]}>
|
||||
<ActivityIndicator size="large" color="#fff" />
|
||||
@@ -88,7 +81,7 @@ export default function PlayScreen() {
|
||||
);
|
||||
}
|
||||
|
||||
const currentEpisode = episodes[currentEpisodeIndex];
|
||||
const currentEpisode = detail.episodes[episodeIndex];
|
||||
|
||||
return (
|
||||
<ThemedView focusable style={styles.container}>
|
||||
@@ -96,9 +89,9 @@ export default function PlayScreen() {
|
||||
<Video
|
||||
ref={videoRef}
|
||||
style={styles.videoPlayer}
|
||||
source={{ uri: currentEpisode?.url }}
|
||||
source={{ uri: currentEpisode }}
|
||||
usePoster
|
||||
posterSource={{ uri: detail?.videoInfo.poster ?? "" }}
|
||||
posterSource={{ uri: detail?.poster ?? "" }}
|
||||
resizeMode={ResizeMode.CONTAIN}
|
||||
onPlaybackStatusUpdate={handlePlaybackStatusUpdate}
|
||||
onLoad={() => {
|
||||
@@ -108,6 +101,10 @@ export default function PlayScreen() {
|
||||
}
|
||||
usePlayerStore.setState({ isLoading: false });
|
||||
}}
|
||||
onError={() => {
|
||||
usePlayerStore.setState({ isLoading: false });
|
||||
Toast.show({ type: "error", text1: "播放失败,请更换源后重试" });
|
||||
}}
|
||||
onLoadStart={() => usePlayerStore.setState({ isLoading: true })}
|
||||
useNativeControls={false}
|
||||
shouldPlay
|
||||
|
||||
@@ -6,6 +6,7 @@ import { ThemedView } from "@/components/ThemedView";
|
||||
import { StyledButton } from "@/components/StyledButton";
|
||||
import { useThemeColor } from "@/hooks/useThemeColor";
|
||||
import { useSettingsStore } from "@/stores/settingsStore";
|
||||
import useAuthStore from "@/stores/authStore";
|
||||
import { useRemoteControlStore } from "@/stores/remoteControlStore";
|
||||
import { APIConfigSection } from "@/components/settings/APIConfigSection";
|
||||
import { LiveStreamSection } from "@/components/settings/LiveStreamSection";
|
||||
@@ -16,6 +17,7 @@ import Toast from "react-native-toast-message";
|
||||
export default function SettingsScreen() {
|
||||
const { loadSettings, saveSettings, setApiBaseUrl, setM3uUrl } = useSettingsStore();
|
||||
const { lastMessage } = useRemoteControlStore();
|
||||
const { isLoggedIn, logout } = useAuthStore();
|
||||
const backgroundColor = useThemeColor({}, "background");
|
||||
|
||||
const [hasChanges, setHasChanges] = useState(false);
|
||||
|
||||
Reference in New Issue
Block a user