Merge branch 'v1.3.0' of github.com:zimplexing/OrionTV into v1.3.0

This commit is contained in:
zimplexing
2025-08-13 18:47:48 +08:00
9 changed files with 270 additions and 246 deletions

View File

@@ -128,7 +128,7 @@ export default function HomeScreen() {
// TV端和平板端的顶部导航
const renderHeader = () => {
if (deviceType === 'mobile') {
if (deviceType === "mobile") {
// 移动端不显示顶部导航使用底部Tab导航
return null;
}
@@ -171,7 +171,7 @@ export default function HomeScreen() {
const dynamicStyles = StyleSheet.create({
container: {
flex: 1,
paddingTop: deviceType === 'mobile' ? insets.top : 40,
paddingTop: deviceType === "mobile" ? 0 : 40,
},
headerContainer: {
flexDirection: "row",
@@ -181,7 +181,7 @@ export default function HomeScreen() {
marginBottom: spacing,
},
headerTitle: {
fontSize: deviceType === 'mobile' ? 24 : deviceType === 'tablet' ? 28 : 32,
fontSize: deviceType === "mobile" ? 24 : deviceType === "tablet" ? 28 : 32,
fontWeight: "bold",
paddingTop: 16,
},
@@ -200,13 +200,13 @@ export default function HomeScreen() {
paddingHorizontal: spacing,
},
categoryButton: {
paddingHorizontal: deviceType === 'tv' ? spacing / 4 : spacing / 2,
paddingVertical: deviceType === 'tv' ? spacing / 4 : spacing / 2,
borderRadius: deviceType === 'mobile' ? 6 : 8,
marginHorizontal: deviceType === 'tv' ? spacing / 4 : spacing / 2,
paddingHorizontal: deviceType === "tv" ? spacing / 4 : spacing / 2,
paddingVertical: spacing / 2,
borderRadius: deviceType === "mobile" ? 6 : 8,
marginHorizontal: deviceType === "tv" ? spacing / 4 : spacing / 2, // TV端使用更小的间距
},
categoryText: {
fontSize: deviceType === 'mobile' ? 14 : 16,
fontSize: deviceType === "mobile" ? 14 : 16,
fontWeight: "500",
},
contentContainer: {
@@ -217,8 +217,8 @@ export default function HomeScreen() {
const content = (
<ThemedView style={[commonStyles.container, dynamicStyles.container]}>
{/* 状态栏 */}
{deviceType === 'mobile' && <StatusBar barStyle="light-content" />}
{deviceType === "mobile" && <StatusBar barStyle="light-content" />}
{/* 顶部导航 */}
{renderHeader()}
@@ -291,13 +291,9 @@ export default function HomeScreen() {
);
// 根据设备类型决定是否包装在响应式导航中
if (deviceType === 'tv') {
if (deviceType === "tv") {
return content;
}
return (
<ResponsiveNavigation>
{content}
</ResponsiveNavigation>
);
}
return <ResponsiveNavigation>{content}</ResponsiveNavigation>;
}

View File

@@ -26,7 +26,7 @@ export default function SearchScreen() {
const [error, setError] = useState<string | null>(null);
const textInputRef = useRef<TextInput>(null);
const [isInputFocused, setIsInputFocused] = useState(false);
const { showModal: showRemoteModal, lastMessage } = useRemoteControlStore();
const { showModal: showRemoteModal, lastMessage, targetPage, clearMessage } = useRemoteControlStore();
const { remoteInputEnabled } = useSettingsStore();
const router = useRouter();
@@ -36,14 +36,15 @@ export default function SearchScreen() {
const { deviceType, spacing } = responsiveConfig;
useEffect(() => {
if (lastMessage) {
if (lastMessage && targetPage === 'search') {
console.log("Received remote input:", lastMessage);
const realMessage = lastMessage.split("_")[0];
setKeyword(realMessage);
handleSearch(realMessage);
clearMessage(); // Clear the message after processing
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [lastMessage]);
}, [lastMessage, targetPage]);
// useEffect(() => {
// // Focus the text input when the screen loads
@@ -87,10 +88,10 @@ export default function SearchScreen() {
]);
return;
}
showRemoteModal();
showRemoteModal('search');
};
const renderItem = ({ item, index }: { item: SearchResult; index: number }) => (
const renderItem = ({ item }: { item: SearchResult; index: number }) => (
<VideoCard
id={item.id.toString()}
source={item.source}

View File

@@ -22,7 +22,7 @@ import { DeviceUtils } from "@/utils/DeviceUtils";
export default function SettingsScreen() {
const { loadSettings, saveSettings, setApiBaseUrl, setM3uUrl } = useSettingsStore();
const { lastMessage } = useRemoteControlStore();
const { lastMessage, targetPage, clearMessage } = useRemoteControlStore();
const backgroundColor = useThemeColor({}, "background");
// 响应式布局配置
@@ -44,12 +44,13 @@ export default function SettingsScreen() {
}, [loadSettings]);
useEffect(() => {
if (lastMessage) {
if (lastMessage && !targetPage) {
const realMessage = lastMessage.split("_")[0];
handleRemoteInput(realMessage);
clearMessage(); // Clear the message after processing
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [lastMessage]);
}, [lastMessage, targetPage]);
const handleRemoteInput = (message: string) => {
// Handle remote input based on currently focused section
@@ -133,10 +134,8 @@ export default function SettingsScreen() {
// ),
// key: "videoSource",
// },
Platform.OS === 'android' && {
component: (
<UpdateSection />
),
Platform.OS === "android" && {
component: <UpdateSection />,
key: "update",
},
].filter(Boolean);
@@ -144,8 +143,8 @@ export default function SettingsScreen() {
// TV遥控器事件处理 - 仅在TV设备上启用
const handleTVEvent = React.useCallback(
(event: any) => {
if (deviceType !== 'tv') return;
if (deviceType !== "tv") return;
if (event.eventType === "down") {
const nextIndex = Math.min(currentFocusIndex + 1, sections.length);
setCurrentFocusIndex(nextIndex);
@@ -160,18 +159,15 @@ export default function SettingsScreen() {
[currentFocusIndex, sections.length, deviceType]
);
useTVEventHandler(deviceType === 'tv' ? handleTVEvent : () => {});
useTVEventHandler(deviceType === "tv" ? handleTVEvent : () => {});
// 动态样式
const dynamicStyles = createResponsiveStyles(deviceType, spacing);
const renderSettingsContent = () => (
<KeyboardAvoidingView
style={{ flex: 1, backgroundColor }}
behavior={Platform.OS === "ios" ? "padding" : "height"}
>
<KeyboardAvoidingView style={{ flex: 1, backgroundColor }} behavior={Platform.OS === "ios" ? "padding" : "height"}>
<ThemedView style={[commonStyles.container, dynamicStyles.container]}>
{deviceType === 'tv' && (
{deviceType === "tv" && (
<View style={dynamicStyles.header}>
<ThemedText style={dynamicStyles.title}></ThemedText>
</View>
@@ -186,7 +182,7 @@ export default function SettingsScreen() {
}
return null;
}}
keyExtractor={(item) => item ? item.key : 'default'}
keyExtractor={(item) => (item ? item.key : "default")}
showsVerticalScrollIndicator={false}
contentContainerStyle={dynamicStyles.listContent}
/>
@@ -198,10 +194,7 @@ export default function SettingsScreen() {
onPress={handleSave}
variant="primary"
disabled={!hasChanges || isLoading}
style={[
dynamicStyles.saveButton,
(!hasChanges || isLoading) && dynamicStyles.disabledButton
]}
style={[dynamicStyles.saveButton, (!hasChanges || isLoading) && dynamicStyles.disabledButton]}
/>
</View>
</ThemedView>
@@ -209,7 +202,7 @@ export default function SettingsScreen() {
);
// 根据设备类型决定是否包装在响应式导航中
if (deviceType === 'tv') {
if (deviceType === "tv") {
return renderSettingsContent();
}
@@ -222,9 +215,9 @@ export default function SettingsScreen() {
}
const createResponsiveStyles = (deviceType: string, spacing: number) => {
const isMobile = deviceType === 'mobile';
const isTablet = deviceType === 'tablet';
const isTV = deviceType === 'tv';
const isMobile = deviceType === "mobile";
const isTablet = deviceType === "tablet";
const isTV = deviceType === "tv";
const minTouchTarget = DeviceUtils.getMinTouchTargetSize();
return StyleSheet.create({
@@ -243,7 +236,7 @@ const createResponsiveStyles = (deviceType: string, spacing: number) => {
fontSize: isMobile ? 24 : isTablet ? 28 : 32,
fontWeight: "bold",
paddingTop: spacing,
color: 'white',
color: "white",
},
scrollView: {
flex: 1,
@@ -257,7 +250,7 @@ const createResponsiveStyles = (deviceType: string, spacing: number) => {
},
saveButton: {
minHeight: isMobile ? minTouchTarget : isTablet ? 50 : 50,
width: isMobile ? '100%' : isTablet ? 140 : 120,
width: isMobile ? "100%" : isTablet ? 140 : 120,
maxWidth: isMobile ? 280 : undefined,
},
disabledButton: {

View File

@@ -1,15 +1,9 @@
import React from 'react';
import {
Modal,
View,
Text,
TouchableOpacity,
StyleSheet,
ActivityIndicator,
Platform,
} from 'react-native';
import { useUpdateStore } from '../stores/updateStore';
import { Colors } from '../constants/Colors';
import React from "react";
import { Modal, View, StyleSheet, ActivityIndicator, Platform } from "react-native";
import { useUpdateStore } from "../stores/updateStore";
import { Colors } from "../constants/Colors";
import { StyledButton } from "./StyledButton";
import { ThemedText } from "./ThemedText";
export function UpdateModal() {
const {
@@ -26,9 +20,9 @@ export function UpdateModal() {
downloadedPath,
} = useUpdateStore();
const updateButtonRef = React.useRef<TouchableOpacity>(null);
const laterButtonRef = React.useRef<TouchableOpacity>(null);
const skipButtonRef = React.useRef<TouchableOpacity>(null);
const updateButtonRef = React.useRef<View>(null);
const laterButtonRef = React.useRef<View>(null);
const skipButtonRef = React.useRef<View>(null);
async function handleUpdate() {
if (!downloading && !downloadedPath) {
@@ -61,86 +55,59 @@ export function UpdateModal() {
if (downloading) {
return `下载中 ${downloadProgress}%`;
} else if (downloadedPath) {
return '立即安装';
return "立即安装";
} else {
return '立即更新';
return "立即更新";
}
};
return (
<Modal
visible={showUpdateModal}
transparent
animationType="fade"
onRequestClose={handleLater}
>
<Modal visible={showUpdateModal} transparent animationType="fade" onRequestClose={handleLater}>
<View style={styles.overlay}>
<View style={styles.container}>
<Text style={styles.title}></Text>
<ThemedText style={styles.title}></ThemedText>
<View style={styles.versionInfo}>
<Text style={styles.versionText}>
当前版本: v{currentVersion}
</Text>
<Text style={styles.arrow}></Text>
<Text style={[styles.versionText, styles.newVersion]}>
新版本: v{remoteVersion}
</Text>
<ThemedText style={styles.versionText}>当前版本: v{currentVersion}</ThemedText>
<ThemedText style={styles.arrow}></ThemedText>
<ThemedText style={[styles.versionText, styles.newVersion]}>新版本: v{remoteVersion}</ThemedText>
</View>
{downloading && (
<View style={styles.progressContainer}>
<View style={styles.progressBar}>
<View
style={[
styles.progressFill,
{ width: `${downloadProgress}%` },
]}
/>
<View style={[styles.progressFill, { width: `${downloadProgress}%` }]} />
</View>
<Text style={styles.progressText}>{downloadProgress}%</Text>
<ThemedText style={styles.progressText}>{downloadProgress}%</ThemedText>
</View>
)}
{error && (
<Text style={styles.errorText}>{error}</Text>
)}
{error && <ThemedText style={styles.errorText}>{error}</ThemedText>}
<View style={styles.buttonContainer}>
<TouchableOpacity
<StyledButton
ref={updateButtonRef}
style={[styles.button, styles.primaryButton]}
onPress={handleUpdate}
disabled={downloading && !downloadedPath}
variant="primary"
style={styles.button}
>
{downloading && !downloadedPath ? (
<ActivityIndicator color="#fff" />
) : (
<Text style={styles.buttonText}>{getButtonText()}</Text>
<ThemedText style={styles.buttonText}>{getButtonText()}</ThemedText>
)}
</TouchableOpacity>
</StyledButton>
{!downloading && !downloadedPath && (
<>
<TouchableOpacity
ref={laterButtonRef}
style={[styles.button, styles.secondaryButton]}
onPress={handleLater}
>
<Text style={[styles.buttonText, styles.secondaryButtonText]}>
</Text>
</TouchableOpacity>
<StyledButton ref={laterButtonRef} onPress={handleLater} variant="primary" style={styles.button}>
<ThemedText style={[styles.buttonText]}></ThemedText>
</StyledButton>
<TouchableOpacity
ref={skipButtonRef}
style={[styles.button, styles.textButton]}
onPress={handleSkip}
>
<Text style={[styles.buttonText, styles.textButtonText]}>
</Text>
</TouchableOpacity>
<StyledButton ref={skipButtonRef} onPress={handleSkip} variant="primary" style={styles.button}>
<ThemedText style={[styles.buttonText]}></ThemedText>
</StyledButton>
</>
)}
</View>
@@ -153,27 +120,28 @@ export function UpdateModal() {
const styles = StyleSheet.create({
overlay: {
flex: 1,
backgroundColor: 'rgba(0, 0, 0, 0.7)',
justifyContent: 'center',
alignItems: 'center',
backgroundColor: "rgba(0, 0, 0, 0.7)",
justifyContent: "center",
alignItems: "center",
},
container: {
backgroundColor: Colors.dark.background,
borderRadius: 12,
padding: 24,
width: Platform.isTV ? 500 : '90%',
width: Platform.isTV ? 500 : "90%",
maxWidth: 500,
alignItems: 'center',
alignItems: "center",
},
title: {
fontSize: Platform.isTV ? 28 : 24,
fontWeight: 'bold',
fontWeight: "bold",
color: Colors.dark.text,
marginBottom: 20,
paddingTop: 12,
},
versionInfo: {
flexDirection: 'row',
alignItems: 'center',
flexDirection: "row",
alignItems: "center",
marginBottom: 24,
},
versionText: {
@@ -181,8 +149,8 @@ const styles = StyleSheet.create({
color: Colors.dark.text,
},
newVersion: {
color: Colors.dark.primary || '#00bb5e',
fontWeight: 'bold',
color: Colors.dark.primary || "#00bb5e",
fontWeight: "bold",
},
arrow: {
fontSize: Platform.isTV ? 20 : 18,
@@ -190,64 +158,44 @@ const styles = StyleSheet.create({
marginHorizontal: 12,
},
progressContainer: {
width: '100%',
width: "100%",
marginBottom: 20,
},
progressBar: {
height: 6,
backgroundColor: Colors.dark.border,
borderRadius: 3,
overflow: 'hidden',
overflow: "hidden",
marginBottom: 8,
},
progressFill: {
height: '100%',
backgroundColor: Colors.dark.primary || '#00bb5e',
height: "100%",
backgroundColor: Colors.dark.primary || "#00bb5e",
},
progressText: {
fontSize: Platform.isTV ? 16 : 14,
color: Colors.dark.text,
textAlign: 'center',
textAlign: "center",
},
errorText: {
fontSize: Platform.isTV ? 16 : 14,
color: '#ff4444',
color: "#ff4444",
marginBottom: 16,
textAlign: 'center',
textAlign: "center",
},
buttonContainer: {
width: '100%',
width: "100%",
gap: 12,
justifyContent: "center", // 居中对齐
alignItems: "center",
},
button: {
paddingVertical: Platform.isTV ? 14 : 12,
paddingHorizontal: 24,
borderRadius: 8,
alignItems: 'center',
justifyContent: 'center',
minHeight: Platform.isTV ? 56 : 48,
},
primaryButton: {
backgroundColor: Colors.dark.primary || '#00bb5e',
},
secondaryButton: {
backgroundColor: 'transparent',
borderWidth: 1,
borderColor: Colors.dark.border,
},
textButton: {
backgroundColor: 'transparent',
width: "80%",
},
buttonText: {
fontSize: Platform.isTV ? 18 : 16,
fontWeight: '600',
color: '#fff',
fontWeight: "600",
color: "#fff",
},
secondaryButtonText: {
color: Colors.dark.text,
},
textButtonText: {
color: Colors.dark.text,
fontWeight: 'normal',
},
});
});

View File

@@ -6,15 +6,8 @@ import { useUpdateStore } from "@/stores/updateStore";
import { UPDATE_CONFIG } from "@/constants/UpdateConfig";
export function UpdateSection() {
const {
currentVersion,
remoteVersion,
updateAvailable,
downloading,
downloadProgress,
checkForUpdate,
setShowUpdateModal,
} = useUpdateStore();
const { currentVersion, remoteVersion, updateAvailable, downloading, downloadProgress, checkForUpdate } =
useUpdateStore();
const [checking, setChecking] = React.useState(false);
@@ -30,7 +23,7 @@ export function UpdateSection() {
return (
<View style={styles.sectionContainer}>
<ThemedText style={styles.sectionTitle}></ThemedText>
<View style={styles.row}>
<ThemedText style={styles.label}></ThemedText>
<ThemedText style={styles.value}>v{currentVersion}</ThemedText>
@@ -39,9 +32,7 @@ export function UpdateSection() {
{updateAvailable && (
<View style={styles.row}>
<ThemedText style={styles.label}></ThemedText>
<ThemedText style={[styles.value, styles.newVersion]}>
v{remoteVersion}
</ThemedText>
<ThemedText style={[styles.value, styles.newVersion]}>v{remoteVersion}</ThemedText>
</View>
)}
@@ -53,26 +44,13 @@ export function UpdateSection() {
)}
<View style={styles.buttonContainer}>
<StyledButton
onPress={handleCheckUpdate}
disabled={checking || downloading}
style={styles.button}
>
<StyledButton onPress={handleCheckUpdate} disabled={checking || downloading} style={styles.button}>
{checking ? (
<ActivityIndicator color="#fff" size="small" />
) : (
<ThemedText style={styles.buttonText}></ThemedText>
)}
</StyledButton>
{updateAvailable && !downloading && (
<StyledButton
onPress={() => setShowUpdateModal(true)}
style={[styles.button, styles.updateButton]}
>
<ThemedText style={styles.buttonText}></ThemedText>
</StyledButton>
)}
</View>
{UPDATE_CONFIG.AUTO_CHECK && (
@@ -99,6 +77,7 @@ const styles = StyleSheet.create({
fontSize: Platform.isTV ? 24 : 20,
fontWeight: "bold",
marginBottom: 16,
paddingTop: 8,
},
row: {
flexDirection: "row",
@@ -121,12 +100,16 @@ const styles = StyleSheet.create({
flexDirection: "row",
gap: 12,
marginTop: 16,
justifyContent: "center", // 居中对齐
alignItems: "center",
},
button: {
flex: 1,
},
updateButton: {
backgroundColor: "#00bb5e",
width: "90%",
...(Platform.isTV && {
// TV平台焦点样式
borderWidth: 2,
borderColor: "transparent",
}),
},
buttonText: {
color: "#ffffff",
@@ -139,4 +122,4 @@ const styles = StyleSheet.create({
marginTop: 12,
textAlign: "center",
},
});
});

View File

@@ -7,7 +7,7 @@ export const UPDATE_CONFIG = {
// GitHub相关URL
GITHUB_RAW_URL: 'https://raw.githubusercontent.com/zimplexing/OrionTV/refs/heads/master/package.json',
GITHUB_RELEASE_URL_TEMPLATE: 'https://github.com/zimplexing/OrionTV/releases/download/v{version}/app-release.apk',
GITHUB_RELEASE_URL_TEMPLATE: 'https://github.com/zimplexing/OrionTV/releases/download/v{version}/orionTV.{version}.apk',
// 是否显示更新日志
SHOW_RELEASE_NOTES: true,

View File

@@ -2,7 +2,7 @@
"name": "OrionTV",
"private": true,
"main": "expo-router/entry",
"version": "1.2.11",
"version": "1.2.9",
"scripts": {
"start": "EXPO_USE_METRO_WORKSPACE_ROOT=1 expo start",
"start-tv": "EXPO_TV=1 EXPO_USE_METRO_WORKSPACE_ROOT=1 expo start",

View File

@@ -1,6 +1,7 @@
import ReactNativeBlobUtil from "react-native-blob-util";
import FileViewer from "react-native-file-viewer";
import { version as currentVersion } from "../package.json";
import { UPDATE_CONFIG } from "../constants/UpdateConfig";
interface VersionInfo {
version: string;
@@ -18,75 +19,168 @@ class UpdateService {
}
async checkVersion(): Promise<VersionInfo> {
try {
const response = await fetch(
"https://raw.githubusercontent.com/zimplexing/OrionTV/refs/heads/master/package.json"
);
let retries = 0;
const maxRetries = 3;
while (retries < maxRetries) {
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 10000); // 10秒超时
const response = await fetch(UPDATE_CONFIG.GITHUB_RAW_URL, {
signal: controller.signal,
});
clearTimeout(timeoutId);
if (!response.ok) {
throw new Error("Failed to fetch version info");
if (!response.ok) {
throw new Error(`HTTP ${response.status}: Failed to fetch version info`);
}
const remotePackage = await response.json();
const remoteVersion = remotePackage.version;
return {
version: remoteVersion,
downloadUrl: UPDATE_CONFIG.GITHUB_RELEASE_URL_TEMPLATE.replace(/{version}/g, remoteVersion),
};
} catch (error) {
retries++;
console.error(`Error checking version (attempt ${retries}/${maxRetries}):`, error);
if (retries === maxRetries) {
throw error;
}
// 等待一段时间后重试
await new Promise(resolve => setTimeout(resolve, 2000 * retries));
}
}
throw new Error("Maximum retry attempts exceeded");
}
const remotePackage = await response.json();
const remoteVersion = remotePackage.version;
return {
version: remoteVersion,
downloadUrl: `https://github.com/zimplexing/OrionTV/releases/download/v${remoteVersion}/orionTV.
${remoteVersion}.apk`,
};
// 清理旧的APK文件
private async cleanOldApkFiles(): Promise<void> {
try {
const { dirs } = ReactNativeBlobUtil.fs;
// 使用DocumentDir而不是DownloadDir
const files = await ReactNativeBlobUtil.fs.ls(dirs.DocumentDir);
// 查找所有OrionTV APK文件
const apkFiles = files.filter(file => file.startsWith('OrionTV_v') && file.endsWith('.apk'));
// 保留最新的2个文件删除其他的
if (apkFiles.length > 2) {
const sortedFiles = apkFiles.sort((a, b) => {
// 从文件名中提取时间戳进行排序
const timeA = a.match(/OrionTV_v(\d+)\.apk/)?.[1] || '0';
const timeB = b.match(/OrionTV_v(\d+)\.apk/)?.[1] || '0';
return parseInt(timeB) - parseInt(timeA);
});
// 删除旧文件
const filesToDelete = sortedFiles.slice(2);
for (const file of filesToDelete) {
try {
await ReactNativeBlobUtil.fs.unlink(`${dirs.DocumentDir}/${file}`);
console.log(`Cleaned old APK file: ${file}`);
} catch (deleteError) {
console.warn(`Failed to delete old APK file ${file}:`, deleteError);
}
}
}
} catch (error) {
console.error("Error checking version:", error);
throw error;
console.warn('Failed to clean old APK files:', error);
}
}
async downloadApk(url: string, onProgress?: (progress: number) => void): Promise<string> {
try {
const { dirs } = ReactNativeBlobUtil.fs;
const fileName = `OrionTV_v${new Date().getTime()}.apk`;
const filePath = `${dirs.DownloadDir}/${fileName}`;
let retries = 0;
const maxRetries = 3;
// 清理旧文件
await this.cleanOldApkFiles();
while (retries < maxRetries) {
try {
const { dirs } = ReactNativeBlobUtil.fs;
const timestamp = new Date().getTime();
const fileName = `OrionTV_v${timestamp}.apk`;
// 使用应用的外部文件目录,而不是系统下载目录
const filePath = `${dirs.DocumentDir}/${fileName}`;
const task = ReactNativeBlobUtil.config({
fileCache: true,
path: filePath,
addAndroidDownloads: {
useDownloadManager: true,
notification: true,
title: "OrionTV 更新下载中",
description: "正在下载新版本...",
mime: "application/vnd.android.package-archive",
mediaScannable: true,
},
}).fetch("GET", url);
const task = ReactNativeBlobUtil.config({
fileCache: true,
path: filePath,
timeout: UPDATE_CONFIG.DOWNLOAD_TIMEOUT,
// 移除 addAndroidDownloads 配置,避免使用系统下载管理器
// addAndroidDownloads: {
// useDownloadManager: true,
// notification: true,
// title: UPDATE_CONFIG.NOTIFICATION.TITLE,
// description: UPDATE_CONFIG.NOTIFICATION.DOWNLOADING_TEXT,
// mime: "application/vnd.android.package-archive",
// mediaScannable: true,
// },
}).fetch("GET", url);
// 监听下载进度
if (onProgress) {
task.progress((received: string, total: string) => {
const receivedNum = parseInt(received, 10);
const totalNum = parseInt(total, 10);
const progress = Math.floor((receivedNum / totalNum) * 100);
onProgress(progress);
});
// 监听下载进度
if (onProgress) {
task.progress((received: string, total: string) => {
const receivedNum = parseInt(received, 10);
const totalNum = parseInt(total, 10);
const progress = Math.floor((receivedNum / totalNum) * 100);
onProgress(progress);
});
}
const res = await task;
console.log(`APK downloaded successfully: ${filePath}`);
return res.path();
} catch (error) {
retries++;
console.error(`Error downloading APK (attempt ${retries}/${maxRetries}):`, error);
if (retries === maxRetries) {
throw new Error(`Download failed after ${maxRetries} attempts: ${error}`);
}
// 等待一段时间后重试
await new Promise(resolve => setTimeout(resolve, 3000 * retries));
}
const res = await task;
return res.path();
} catch (error) {
console.error("Error downloading APK:", error);
throw error;
}
throw new Error("Maximum retry attempts exceeded for download");
}
async installApk(filePath: string): Promise<void> {
try {
// 首先检查文件是否存在
const exists = await ReactNativeBlobUtil.fs.exists(filePath);
if (!exists) {
throw new Error(`APK file not found: ${filePath}`);
}
// 使用FileViewer打开APK文件进行安装
// 这会触发Android的包安装器
await FileViewer.open(filePath, {
showOpenWithDialog: false,
showAppsSuggestions: false,
showOpenWithDialog: true, // 显示选择应用对话框
showAppsSuggestions: true, // 显示应用建议
displayName: "OrionTV Update",
});
} catch (error) {
console.error("Error installing APK:", error);
// 提供更详细的错误信息
if (error instanceof Error) {
if (error.message.includes('No app found')) {
throw new Error('未找到可安装APK的应用请确保允许安装未知来源的应用');
} else if (error.message.includes('permission')) {
throw new Error('没有安装权限,请在设置中允许此应用安装未知来源的应用');
}
}
throw error;
}
}

View File

@@ -8,10 +8,12 @@ interface RemoteControlState {
startServer: () => Promise<void>;
stopServer: () => void;
isModalVisible: boolean;
showModal: () => void;
showModal: (targetPage?: string) => void;
hideModal: () => void;
lastMessage: string | null;
setMessage: (message: string) => void;
targetPage: string | null;
setMessage: (message: string, targetPage?: string) => void;
clearMessage: () => void;
}
export const useRemoteControlStore = create<RemoteControlState>((set, get) => ({
@@ -20,6 +22,7 @@ export const useRemoteControlStore = create<RemoteControlState>((set, get) => ({
error: null,
isModalVisible: false,
lastMessage: null,
targetPage: null,
startServer: async () => {
if (get().isServerRunning) {
@@ -28,7 +31,9 @@ export const useRemoteControlStore = create<RemoteControlState>((set, get) => ({
remoteControlService.init({
onMessage: (message: string) => {
console.log('[RemoteControlStore] Received message:', message);
set({ lastMessage: message });
const currentState = get();
// Use the current targetPage from the store
set({ lastMessage: message, targetPage: currentState.targetPage });
},
onHandshake: () => {
console.log('[RemoteControlStore] Handshake successful');
@@ -53,10 +58,14 @@ export const useRemoteControlStore = create<RemoteControlState>((set, get) => ({
}
},
showModal: () => set({ isModalVisible: true }),
hideModal: () => set({ isModalVisible: false }),
showModal: (targetPage?: string) => set({ isModalVisible: true, targetPage }),
hideModal: () => set({ isModalVisible: false, targetPage: null }),
setMessage: (message: string) => {
set({ lastMessage: `${message}_${Date.now()}` });
setMessage: (message: string, targetPage?: string) => {
set({ lastMessage: `${message}_${Date.now()}`, targetPage });
},
clearMessage: () => {
set({ lastMessage: null, targetPage: null });
},
}));