Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3fdd1fc587 | ||
|
|
4b3d1c620b | ||
|
|
1f694f9245 | ||
|
|
ec949029fa | ||
|
|
2325b76f77 | ||
|
|
4473fd6ab3 | ||
|
|
c514a6d03e | ||
|
|
f6baa0523c | ||
|
|
9540aaa3b9 | ||
|
|
8a1c26991b |
10
README.md
@@ -71,9 +71,11 @@ yarn android-tv
|
||||
|
||||
## 部署
|
||||
|
||||
推荐使用 [MoonTV](https://github.com/senshinya/MoonTV) 部署,地址可直接使用部署后的访问地址。
|
||||
- 1.2.x 以上版本需配合 [MoonTV](https://github.com/senshinya/MoonTV) 部署使用,api 地址填部MoonTV署后的访问地址。
|
||||
|
||||
如果不想依赖 MoonTV,可以使用 1.1.x 版本。
|
||||
- **注意:** 地址后面不要带 `/` ,不要遗漏 `http://` 或者 `https://`
|
||||
|
||||
- 如果不想依赖 MoonTV,可以使用 1.1.x 版本。
|
||||
|
||||
## 其他
|
||||
|
||||
@@ -92,9 +94,9 @@ yarn android-tv
|
||||
## 📸 应用截图
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
## 📝 License
|
||||
|
||||
|
||||
23
app/play.tsx
@@ -96,6 +96,25 @@ export default function PlayScreen() {
|
||||
return () => backHandler.remove();
|
||||
}, [showControls, setShowControls, router]);
|
||||
|
||||
useEffect(() => {
|
||||
let timeoutId: NodeJS.Timeout | null = null;
|
||||
|
||||
if (isLoading) {
|
||||
timeoutId = setTimeout(() => {
|
||||
if (usePlayerStore.getState().isLoading) {
|
||||
usePlayerStore.setState({ isLoading: false });
|
||||
Toast.show({ type: "error", text1: "播放超时,请重试" });
|
||||
}
|
||||
}, 60000); // 1 minute
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (timeoutId) {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
};
|
||||
}, [isLoading]);
|
||||
|
||||
if (!detail) {
|
||||
return (
|
||||
<ThemedView style={[styles.container, styles.centered]}>
|
||||
@@ -122,10 +141,6 @@ 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
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "OrionTV",
|
||||
"private": true,
|
||||
"main": "expo-router/entry",
|
||||
"version": "1.1.2",
|
||||
"version": "1.2.2",
|
||||
"scripts": {
|
||||
"start": "EXPO_USE_METRO_WORKSPACE_ROOT=1 expo start",
|
||||
"start-tv": "EXPO_TV=1 EXPO_USE_METRO_WORKSPACE_ROOT=1 expo start",
|
||||
|
||||
|
Before Width: | Height: | Size: 731 KiB After Width: | Height: | Size: 533 KiB |
|
Before Width: | Height: | Size: 209 KiB After Width: | Height: | Size: 284 KiB |
|
Before Width: | Height: | Size: 520 KiB After Width: | Height: | Size: 293 KiB |
|
Before Width: | Height: | Size: 220 KiB After Width: | Height: | Size: 672 KiB |
@@ -121,7 +121,7 @@ export class API {
|
||||
}
|
||||
|
||||
async getFavorites(key?: string): Promise<Record<string, Favorite> | Favorite | null> {
|
||||
const url = key ? `/api/favorites?key=${key}` : "/api/favorites";
|
||||
const url = key ? `/api/favorites?key=${encodeURIComponent(key)}` : "/api/favorites";
|
||||
const response = await this._fetch(url);
|
||||
return response.json();
|
||||
}
|
||||
@@ -136,7 +136,7 @@ export class API {
|
||||
}
|
||||
|
||||
async deleteFavorite(key?: string): Promise<{ success: boolean }> {
|
||||
const url = key ? `/api/favorites?key=${key}` : "/api/favorites";
|
||||
const url = key ? `/api/favorites?key=${encodeURIComponent(key)}` : "/api/favorites";
|
||||
const response = await this._fetch(url, { method: "DELETE" });
|
||||
return response.json();
|
||||
}
|
||||
@@ -156,7 +156,7 @@ export class API {
|
||||
}
|
||||
|
||||
async deletePlayRecord(key?: string): Promise<{ success: boolean }> {
|
||||
const url = key ? `/api/playrecords?key=${key}` : "/api/playrecords";
|
||||
const url = key ? `/api/playrecords?key=${encodeURIComponent(key)}` : "/api/playrecords";
|
||||
const response = await this._fetch(url, { method: "DELETE" });
|
||||
return response.json();
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { create } from "zustand";
|
||||
import Cookies from "@react-native-cookies/cookies";
|
||||
import { api } from "@/services/api";
|
||||
import { useSettingsStore } from "./settingsStore";
|
||||
|
||||
interface AuthState {
|
||||
isLoggedIn: boolean;
|
||||
@@ -22,11 +23,21 @@ const useAuthStore = create<AuthState>((set) => ({
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const serverConfig = useSettingsStore.getState().serverConfig;
|
||||
const cookies = await Cookies.get(api.baseURL);
|
||||
const isLoggedIn = cookies && !!cookies.auth;
|
||||
set({ isLoggedIn });
|
||||
if (!isLoggedIn) {
|
||||
set({ isLoginModalVisible: true });
|
||||
if (serverConfig && serverConfig.StorageType === "localstorage" && !cookies.auth) {
|
||||
const loginResult = await api.login().catch(() => {
|
||||
set({ isLoggedIn: false, isLoginModalVisible: true });
|
||||
});
|
||||
if (loginResult && loginResult.ok) {
|
||||
set({ isLoggedIn: true });
|
||||
}
|
||||
} else {
|
||||
const isLoggedIn = cookies && !!cookies.auth;
|
||||
set({ isLoggedIn });
|
||||
if (!isLoggedIn) {
|
||||
set({ isLoginModalVisible: true });
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.info("Failed to check login status:", error);
|
||||
|
||||
@@ -68,14 +68,35 @@ export const useSettingsStore = create<SettingsState>((set, get) => ({
|
||||
setVideoSource: (config) => set({ videoSource: config }),
|
||||
saveSettings: async () => {
|
||||
const { apiBaseUrl, m3uUrl, remoteInputEnabled, videoSource } = get();
|
||||
|
||||
let processedApiBaseUrl = apiBaseUrl.trim();
|
||||
if (processedApiBaseUrl.endsWith("/")) {
|
||||
processedApiBaseUrl = processedApiBaseUrl.slice(0, -1);
|
||||
}
|
||||
|
||||
if (!/^https?:\/\//i.test(processedApiBaseUrl)) {
|
||||
const hostPart = processedApiBaseUrl.split("/")[0];
|
||||
// Simple check for IP address format.
|
||||
const isIpAddress = /^((\d{1,3}\.){3}\d{1,3})(:\d+)?$/.test(hostPart);
|
||||
// Check if the domain includes a port.
|
||||
const hasPort = /:\d+/.test(hostPart);
|
||||
|
||||
if (isIpAddress || hasPort) {
|
||||
processedApiBaseUrl = "http://" + processedApiBaseUrl;
|
||||
} else {
|
||||
processedApiBaseUrl = "https://" + processedApiBaseUrl;
|
||||
}
|
||||
}
|
||||
|
||||
await SettingsManager.save({
|
||||
apiBaseUrl,
|
||||
apiBaseUrl: processedApiBaseUrl,
|
||||
m3uUrl,
|
||||
remoteInputEnabled,
|
||||
videoSource,
|
||||
});
|
||||
api.setBaseUrl(apiBaseUrl);
|
||||
set({ isModalVisible: false });
|
||||
api.setBaseUrl(processedApiBaseUrl);
|
||||
// Also update the URL in the state so the input field shows the processed URL
|
||||
set({ isModalVisible: false, apiBaseUrl: processedApiBaseUrl });
|
||||
await get().fetchServerConfig();
|
||||
},
|
||||
showModal: () => set({ isModalVisible: true }),
|
||||
|
||||