diff --git a/app/index.tsx b/app/index.tsx
index 0ba5409..26e85d4 100644
--- a/app/index.tsx
+++ b/app/index.tsx
@@ -126,7 +126,7 @@ export default function HomeScreen() {
// TV端和平板端的顶部导航
const renderHeader = () => {
- if (deviceType === 'mobile') {
+ if (deviceType === "mobile") {
// 移动端不显示顶部导航,使用底部Tab导航
return null;
}
@@ -169,7 +169,7 @@ export default function HomeScreen() {
const dynamicStyles = StyleSheet.create({
container: {
flex: 1,
- paddingTop: deviceType === 'mobile' ? 0 : 40,
+ paddingTop: deviceType === "mobile" ? 0 : 40,
},
headerContainer: {
flexDirection: "row",
@@ -179,7 +179,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,
},
@@ -198,13 +198,13 @@ export default function HomeScreen() {
paddingHorizontal: spacing,
},
categoryButton: {
- paddingHorizontal: spacing / 2,
+ paddingHorizontal: deviceType === "tv" ? spacing / 4 : spacing / 2,
paddingVertical: spacing / 2,
- borderRadius: deviceType === 'mobile' ? 6 : 8,
- marginHorizontal: 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: {
@@ -286,13 +286,9 @@ export default function HomeScreen() {
);
// 根据设备类型决定是否包装在响应式导航中
- if (deviceType === 'tv') {
+ if (deviceType === "tv") {
return content;
}
- return (
-
- {content}
-
- );
-}
\ No newline at end of file
+ return {content};
+}
diff --git a/app/settings.tsx b/app/settings.tsx
index 7603a34..90f369a 100644
--- a/app/settings.tsx
+++ b/app/settings.tsx
@@ -133,10 +133,8 @@ export default function SettingsScreen() {
// ),
// key: "videoSource",
// },
- Platform.OS === 'android' && {
- component: (
-
- ),
+ Platform.OS === "android" && {
+ component: ,
key: "update",
},
].filter(Boolean);
@@ -144,8 +142,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 +158,15 @@ export default function SettingsScreen() {
[currentFocusIndex, sections.length, deviceType]
);
- useTVEventHandler(deviceType === 'tv' ? handleTVEvent : () => {});
+ useTVEventHandler(deviceType === "tv" ? handleTVEvent : () => {});
// 动态样式
const dynamicStyles = createResponsiveStyles(deviceType, spacing);
const renderSettingsContent = () => (
-
+
- {deviceType === 'tv' && (
+ {deviceType === "tv" && (
设置
@@ -186,7 +181,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 +193,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]}
/>
@@ -209,7 +201,7 @@ export default function SettingsScreen() {
);
// 根据设备类型决定是否包装在响应式导航中
- if (deviceType === 'tv') {
+ if (deviceType === "tv") {
return renderSettingsContent();
}
@@ -222,9 +214,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 +235,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 +249,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: {
diff --git a/components/UpdateModal.tsx b/components/UpdateModal.tsx
index 22c3f2a..78d7416 100644
--- a/components/UpdateModal.tsx
+++ b/components/UpdateModal.tsx
@@ -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(null);
- const laterButtonRef = React.useRef(null);
- const skipButtonRef = React.useRef(null);
+ const updateButtonRef = React.useRef(null);
+ const laterButtonRef = React.useRef(null);
+ const skipButtonRef = React.useRef(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 (
-
+
- 发现新版本
-
+ 发现新版本
+
-
- 当前版本: v{currentVersion}
-
- →
-
- 新版本: v{remoteVersion}
-
+ 当前版本: v{currentVersion}
+ →
+ 新版本: v{remoteVersion}
{downloading && (
-
+
- {downloadProgress}%
+ {downloadProgress}%
)}
- {error && (
- {error}
- )}
+ {error && {error}}
-
{downloading && !downloadedPath ? (
) : (
- {getButtonText()}
+ {getButtonText()}
)}
-
+
{!downloading && !downloadedPath && (
<>
-
-
- 稍后再说
-
-
+
+ 稍后再说
+
-
-
- 跳过此版本
-
-
+
+ 跳过此版本
+
>
)}
@@ -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',
- },
-});
\ No newline at end of file
+});
diff --git a/components/settings/UpdateSection.tsx b/components/settings/UpdateSection.tsx
index bb44ffb..3506bee 100644
--- a/components/settings/UpdateSection.tsx
+++ b/components/settings/UpdateSection.tsx
@@ -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 (
应用更新
-
+
当前版本
v{currentVersion}
@@ -39,9 +32,7 @@ export function UpdateSection() {
{updateAvailable && (
最新版本
-
- v{remoteVersion}
-
+ v{remoteVersion}
)}
@@ -53,26 +44,13 @@ export function UpdateSection() {
)}
-
+
{checking ? (
) : (
检查更新
)}
-
- {updateAvailable && !downloading && (
- setShowUpdateModal(true)}
- style={[styles.button, styles.updateButton]}
- >
- 立即更新
-
- )}
{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",
},
-});
\ No newline at end of file
+});
diff --git a/constants/UpdateConfig.ts b/constants/UpdateConfig.ts
index 735d76c..083b8e6 100644
--- a/constants/UpdateConfig.ts
+++ b/constants/UpdateConfig.ts
@@ -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,
diff --git a/package.json b/package.json
index e0afc5b..639ad43 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/services/updateService.ts b/services/updateService.ts
index 46eb1eb..0505753 100644
--- a/services/updateService.ts
+++ b/services/updateService.ts
@@ -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 {
- 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 {
+ 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 {
- 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 {
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;
}
}