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
|
## 📝 License
|
||||||
|
|
||||||
|
|||||||
23
app/play.tsx
@@ -96,6 +96,25 @@ export default function PlayScreen() {
|
|||||||
return () => backHandler.remove();
|
return () => backHandler.remove();
|
||||||
}, [showControls, setShowControls, router]);
|
}, [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) {
|
if (!detail) {
|
||||||
return (
|
return (
|
||||||
<ThemedView style={[styles.container, styles.centered]}>
|
<ThemedView style={[styles.container, styles.centered]}>
|
||||||
@@ -122,10 +141,6 @@ export default function PlayScreen() {
|
|||||||
}
|
}
|
||||||
usePlayerStore.setState({ isLoading: false });
|
usePlayerStore.setState({ isLoading: false });
|
||||||
}}
|
}}
|
||||||
onError={() => {
|
|
||||||
usePlayerStore.setState({ isLoading: false });
|
|
||||||
Toast.show({ type: "error", text1: "播放失败,请更换源后重试" });
|
|
||||||
}}
|
|
||||||
onLoadStart={() => usePlayerStore.setState({ isLoading: true })}
|
onLoadStart={() => usePlayerStore.setState({ isLoading: true })}
|
||||||
useNativeControls={false}
|
useNativeControls={false}
|
||||||
shouldPlay
|
shouldPlay
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"name": "OrionTV",
|
"name": "OrionTV",
|
||||||
"private": true,
|
"private": true,
|
||||||
"main": "expo-router/entry",
|
"main": "expo-router/entry",
|
||||||
"version": "1.1.2",
|
"version": "1.2.2",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "EXPO_USE_METRO_WORKSPACE_ROOT=1 expo start",
|
"start": "EXPO_USE_METRO_WORKSPACE_ROOT=1 expo start",
|
||||||
"start-tv": "EXPO_TV=1 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> {
|
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);
|
const response = await this._fetch(url);
|
||||||
return response.json();
|
return response.json();
|
||||||
}
|
}
|
||||||
@@ -136,7 +136,7 @@ export class API {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async deleteFavorite(key?: string): Promise<{ success: boolean }> {
|
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" });
|
const response = await this._fetch(url, { method: "DELETE" });
|
||||||
return response.json();
|
return response.json();
|
||||||
}
|
}
|
||||||
@@ -156,7 +156,7 @@ export class API {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async deletePlayRecord(key?: string): Promise<{ success: boolean }> {
|
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" });
|
const response = await this._fetch(url, { method: "DELETE" });
|
||||||
return response.json();
|
return response.json();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
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 } from "@/services/api";
|
||||||
|
import { useSettingsStore } from "./settingsStore";
|
||||||
|
|
||||||
interface AuthState {
|
interface AuthState {
|
||||||
isLoggedIn: boolean;
|
isLoggedIn: boolean;
|
||||||
@@ -22,11 +23,21 @@ const useAuthStore = create<AuthState>((set) => ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
|
const serverConfig = useSettingsStore.getState().serverConfig;
|
||||||
const cookies = await Cookies.get(api.baseURL);
|
const cookies = await Cookies.get(api.baseURL);
|
||||||
const isLoggedIn = cookies && !!cookies.auth;
|
if (serverConfig && serverConfig.StorageType === "localstorage" && !cookies.auth) {
|
||||||
set({ isLoggedIn });
|
const loginResult = await api.login().catch(() => {
|
||||||
if (!isLoggedIn) {
|
set({ isLoggedIn: false, isLoginModalVisible: true });
|
||||||
set({ isLoginModalVisible: true });
|
});
|
||||||
|
if (loginResult && loginResult.ok) {
|
||||||
|
set({ isLoggedIn: true });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const isLoggedIn = cookies && !!cookies.auth;
|
||||||
|
set({ isLoggedIn });
|
||||||
|
if (!isLoggedIn) {
|
||||||
|
set({ isLoginModalVisible: true });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.info("Failed to check login status:", 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 }),
|
setVideoSource: (config) => set({ videoSource: config }),
|
||||||
saveSettings: async () => {
|
saveSettings: async () => {
|
||||||
const { apiBaseUrl, m3uUrl, remoteInputEnabled, videoSource } = get();
|
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({
|
await SettingsManager.save({
|
||||||
apiBaseUrl,
|
apiBaseUrl: processedApiBaseUrl,
|
||||||
m3uUrl,
|
m3uUrl,
|
||||||
remoteInputEnabled,
|
remoteInputEnabled,
|
||||||
videoSource,
|
videoSource,
|
||||||
});
|
});
|
||||||
api.setBaseUrl(apiBaseUrl);
|
api.setBaseUrl(processedApiBaseUrl);
|
||||||
set({ isModalVisible: false });
|
// Also update the URL in the state so the input field shows the processed URL
|
||||||
|
set({ isModalVisible: false, apiBaseUrl: processedApiBaseUrl });
|
||||||
await get().fetchServerConfig();
|
await get().fetchServerConfig();
|
||||||
},
|
},
|
||||||
showModal: () => set({ isModalVisible: true }),
|
showModal: () => set({ isModalVisible: true }),
|
||||||
|
|||||||