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

View File

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

View File

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

View File

@@ -1,15 +1,9 @@
import React from 'react'; import React from "react";
import { import { Modal, View, StyleSheet, ActivityIndicator, Platform } from "react-native";
Modal, import { useUpdateStore } from "../stores/updateStore";
View, import { Colors } from "../constants/Colors";
Text, import { StyledButton } from "./StyledButton";
TouchableOpacity, import { ThemedText } from "./ThemedText";
StyleSheet,
ActivityIndicator,
Platform,
} from 'react-native';
import { useUpdateStore } from '../stores/updateStore';
import { Colors } from '../constants/Colors';
export function UpdateModal() { export function UpdateModal() {
const { const {
@@ -26,9 +20,9 @@ export function UpdateModal() {
downloadedPath, downloadedPath,
} = useUpdateStore(); } = useUpdateStore();
const updateButtonRef = React.useRef<TouchableOpacity>(null); const updateButtonRef = React.useRef<View>(null);
const laterButtonRef = React.useRef<TouchableOpacity>(null); const laterButtonRef = React.useRef<View>(null);
const skipButtonRef = React.useRef<TouchableOpacity>(null); const skipButtonRef = React.useRef<View>(null);
async function handleUpdate() { async function handleUpdate() {
if (!downloading && !downloadedPath) { if (!downloading && !downloadedPath) {
@@ -61,86 +55,59 @@ export function UpdateModal() {
if (downloading) { if (downloading) {
return `下载中 ${downloadProgress}%`; return `下载中 ${downloadProgress}%`;
} else if (downloadedPath) { } else if (downloadedPath) {
return '立即安装'; return "立即安装";
} else { } else {
return '立即更新'; return "立即更新";
} }
}; };
return ( return (
<Modal <Modal visible={showUpdateModal} transparent animationType="fade" onRequestClose={handleLater}>
visible={showUpdateModal}
transparent
animationType="fade"
onRequestClose={handleLater}
>
<View style={styles.overlay}> <View style={styles.overlay}>
<View style={styles.container}> <View style={styles.container}>
<Text style={styles.title}></Text> <ThemedText style={styles.title}></ThemedText>
<View style={styles.versionInfo}> <View style={styles.versionInfo}>
<Text style={styles.versionText}> <ThemedText style={styles.versionText}>当前版本: v{currentVersion}</ThemedText>
当前版本: v{currentVersion} <ThemedText style={styles.arrow}></ThemedText>
</Text> <ThemedText style={[styles.versionText, styles.newVersion]}>新版本: v{remoteVersion}</ThemedText>
<Text style={styles.arrow}></Text>
<Text style={[styles.versionText, styles.newVersion]}>
新版本: v{remoteVersion}
</Text>
</View> </View>
{downloading && ( {downloading && (
<View style={styles.progressContainer}> <View style={styles.progressContainer}>
<View style={styles.progressBar}> <View style={styles.progressBar}>
<View <View style={[styles.progressFill, { width: `${downloadProgress}%` }]} />
style={[
styles.progressFill,
{ width: `${downloadProgress}%` },
]}
/>
</View> </View>
<Text style={styles.progressText}>{downloadProgress}%</Text> <ThemedText style={styles.progressText}>{downloadProgress}%</ThemedText>
</View> </View>
)} )}
{error && ( {error && <ThemedText style={styles.errorText}>{error}</ThemedText>}
<Text style={styles.errorText}>{error}</Text>
)}
<View style={styles.buttonContainer}> <View style={styles.buttonContainer}>
<TouchableOpacity <StyledButton
ref={updateButtonRef} ref={updateButtonRef}
style={[styles.button, styles.primaryButton]}
onPress={handleUpdate} onPress={handleUpdate}
disabled={downloading && !downloadedPath} disabled={downloading && !downloadedPath}
variant="primary"
style={styles.button}
> >
{downloading && !downloadedPath ? ( {downloading && !downloadedPath ? (
<ActivityIndicator color="#fff" /> <ActivityIndicator color="#fff" />
) : ( ) : (
<Text style={styles.buttonText}>{getButtonText()}</Text> <ThemedText style={styles.buttonText}>{getButtonText()}</ThemedText>
)} )}
</TouchableOpacity> </StyledButton>
{!downloading && !downloadedPath && ( {!downloading && !downloadedPath && (
<> <>
<TouchableOpacity <StyledButton ref={laterButtonRef} onPress={handleLater} variant="primary" style={styles.button}>
ref={laterButtonRef} <ThemedText style={[styles.buttonText]}></ThemedText>
style={[styles.button, styles.secondaryButton]} </StyledButton>
onPress={handleLater}
>
<Text style={[styles.buttonText, styles.secondaryButtonText]}>
</Text>
</TouchableOpacity>
<TouchableOpacity <StyledButton ref={skipButtonRef} onPress={handleSkip} variant="primary" style={styles.button}>
ref={skipButtonRef} <ThemedText style={[styles.buttonText]}></ThemedText>
style={[styles.button, styles.textButton]} </StyledButton>
onPress={handleSkip}
>
<Text style={[styles.buttonText, styles.textButtonText]}>
</Text>
</TouchableOpacity>
</> </>
)} )}
</View> </View>
@@ -153,27 +120,28 @@ export function UpdateModal() {
const styles = StyleSheet.create({ const styles = StyleSheet.create({
overlay: { overlay: {
flex: 1, flex: 1,
backgroundColor: 'rgba(0, 0, 0, 0.7)', backgroundColor: "rgba(0, 0, 0, 0.7)",
justifyContent: 'center', justifyContent: "center",
alignItems: 'center', alignItems: "center",
}, },
container: { container: {
backgroundColor: Colors.dark.background, backgroundColor: Colors.dark.background,
borderRadius: 12, borderRadius: 12,
padding: 24, padding: 24,
width: Platform.isTV ? 500 : '90%', width: Platform.isTV ? 500 : "90%",
maxWidth: 500, maxWidth: 500,
alignItems: 'center', alignItems: "center",
}, },
title: { title: {
fontSize: Platform.isTV ? 28 : 24, fontSize: Platform.isTV ? 28 : 24,
fontWeight: 'bold', fontWeight: "bold",
color: Colors.dark.text, color: Colors.dark.text,
marginBottom: 20, marginBottom: 20,
paddingTop: 12,
}, },
versionInfo: { versionInfo: {
flexDirection: 'row', flexDirection: "row",
alignItems: 'center', alignItems: "center",
marginBottom: 24, marginBottom: 24,
}, },
versionText: { versionText: {
@@ -181,8 +149,8 @@ const styles = StyleSheet.create({
color: Colors.dark.text, color: Colors.dark.text,
}, },
newVersion: { newVersion: {
color: Colors.dark.primary || '#00bb5e', color: Colors.dark.primary || "#00bb5e",
fontWeight: 'bold', fontWeight: "bold",
}, },
arrow: { arrow: {
fontSize: Platform.isTV ? 20 : 18, fontSize: Platform.isTV ? 20 : 18,
@@ -190,64 +158,44 @@ const styles = StyleSheet.create({
marginHorizontal: 12, marginHorizontal: 12,
}, },
progressContainer: { progressContainer: {
width: '100%', width: "100%",
marginBottom: 20, marginBottom: 20,
}, },
progressBar: { progressBar: {
height: 6, height: 6,
backgroundColor: Colors.dark.border, backgroundColor: Colors.dark.border,
borderRadius: 3, borderRadius: 3,
overflow: 'hidden', overflow: "hidden",
marginBottom: 8, marginBottom: 8,
}, },
progressFill: { progressFill: {
height: '100%', height: "100%",
backgroundColor: Colors.dark.primary || '#00bb5e', backgroundColor: Colors.dark.primary || "#00bb5e",
}, },
progressText: { progressText: {
fontSize: Platform.isTV ? 16 : 14, fontSize: Platform.isTV ? 16 : 14,
color: Colors.dark.text, color: Colors.dark.text,
textAlign: 'center', textAlign: "center",
}, },
errorText: { errorText: {
fontSize: Platform.isTV ? 16 : 14, fontSize: Platform.isTV ? 16 : 14,
color: '#ff4444', color: "#ff4444",
marginBottom: 16, marginBottom: 16,
textAlign: 'center', textAlign: "center",
}, },
buttonContainer: { buttonContainer: {
width: '100%', width: "100%",
gap: 12, gap: 12,
justifyContent: "center", // 居中对齐
alignItems: "center",
}, },
button: { button: {
paddingVertical: Platform.isTV ? 14 : 12, width: "80%",
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',
}, },
buttonText: { buttonText: {
fontSize: Platform.isTV ? 18 : 16, fontSize: Platform.isTV ? 18 : 16,
fontWeight: '600', fontWeight: "600",
color: '#fff', 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"; import { UPDATE_CONFIG } from "@/constants/UpdateConfig";
export function UpdateSection() { export function UpdateSection() {
const { const { currentVersion, remoteVersion, updateAvailable, downloading, downloadProgress, checkForUpdate } =
currentVersion, useUpdateStore();
remoteVersion,
updateAvailable,
downloading,
downloadProgress,
checkForUpdate,
setShowUpdateModal,
} = useUpdateStore();
const [checking, setChecking] = React.useState(false); const [checking, setChecking] = React.useState(false);
@@ -30,7 +23,7 @@ export function UpdateSection() {
return ( return (
<View style={styles.sectionContainer}> <View style={styles.sectionContainer}>
<ThemedText style={styles.sectionTitle}></ThemedText> <ThemedText style={styles.sectionTitle}></ThemedText>
<View style={styles.row}> <View style={styles.row}>
<ThemedText style={styles.label}></ThemedText> <ThemedText style={styles.label}></ThemedText>
<ThemedText style={styles.value}>v{currentVersion}</ThemedText> <ThemedText style={styles.value}>v{currentVersion}</ThemedText>
@@ -39,9 +32,7 @@ export function UpdateSection() {
{updateAvailable && ( {updateAvailable && (
<View style={styles.row}> <View style={styles.row}>
<ThemedText style={styles.label}></ThemedText> <ThemedText style={styles.label}></ThemedText>
<ThemedText style={[styles.value, styles.newVersion]}> <ThemedText style={[styles.value, styles.newVersion]}>v{remoteVersion}</ThemedText>
v{remoteVersion}
</ThemedText>
</View> </View>
)} )}
@@ -53,26 +44,13 @@ export function UpdateSection() {
)} )}
<View style={styles.buttonContainer}> <View style={styles.buttonContainer}>
<StyledButton <StyledButton onPress={handleCheckUpdate} disabled={checking || downloading} style={styles.button}>
onPress={handleCheckUpdate}
disabled={checking || downloading}
style={styles.button}
>
{checking ? ( {checking ? (
<ActivityIndicator color="#fff" size="small" /> <ActivityIndicator color="#fff" size="small" />
) : ( ) : (
<ThemedText style={styles.buttonText}></ThemedText> <ThemedText style={styles.buttonText}></ThemedText>
)} )}
</StyledButton> </StyledButton>
{updateAvailable && !downloading && (
<StyledButton
onPress={() => setShowUpdateModal(true)}
style={[styles.button, styles.updateButton]}
>
<ThemedText style={styles.buttonText}></ThemedText>
</StyledButton>
)}
</View> </View>
{UPDATE_CONFIG.AUTO_CHECK && ( {UPDATE_CONFIG.AUTO_CHECK && (
@@ -99,6 +77,7 @@ const styles = StyleSheet.create({
fontSize: Platform.isTV ? 24 : 20, fontSize: Platform.isTV ? 24 : 20,
fontWeight: "bold", fontWeight: "bold",
marginBottom: 16, marginBottom: 16,
paddingTop: 8,
}, },
row: { row: {
flexDirection: "row", flexDirection: "row",
@@ -121,12 +100,16 @@ const styles = StyleSheet.create({
flexDirection: "row", flexDirection: "row",
gap: 12, gap: 12,
marginTop: 16, marginTop: 16,
justifyContent: "center", // 居中对齐
alignItems: "center",
}, },
button: { button: {
flex: 1, width: "90%",
}, ...(Platform.isTV && {
updateButton: { // TV平台焦点样式
backgroundColor: "#00bb5e", borderWidth: 2,
borderColor: "transparent",
}),
}, },
buttonText: { buttonText: {
color: "#ffffff", color: "#ffffff",
@@ -139,4 +122,4 @@ const styles = StyleSheet.create({
marginTop: 12, marginTop: 12,
textAlign: "center", textAlign: "center",
}, },
}); });

View File

@@ -7,7 +7,7 @@ export const UPDATE_CONFIG = {
// GitHub相关URL // GitHub相关URL
GITHUB_RAW_URL: 'https://raw.githubusercontent.com/zimplexing/OrionTV/refs/heads/master/package.json', 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, SHOW_RELEASE_NOTES: true,

View File

@@ -2,7 +2,7 @@
"name": "OrionTV", "name": "OrionTV",
"private": true, "private": true,
"main": "expo-router/entry", "main": "expo-router/entry",
"version": "1.2.11", "version": "1.2.9",
"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",

View File

@@ -1,6 +1,7 @@
import ReactNativeBlobUtil from "react-native-blob-util"; import ReactNativeBlobUtil from "react-native-blob-util";
import FileViewer from "react-native-file-viewer"; import FileViewer from "react-native-file-viewer";
import { version as currentVersion } from "../package.json"; import { version as currentVersion } from "../package.json";
import { UPDATE_CONFIG } from "../constants/UpdateConfig";
interface VersionInfo { interface VersionInfo {
version: string; version: string;
@@ -18,75 +19,168 @@ class UpdateService {
} }
async checkVersion(): Promise<VersionInfo> { async checkVersion(): Promise<VersionInfo> {
try { let retries = 0;
const response = await fetch( const maxRetries = 3;
"https://raw.githubusercontent.com/zimplexing/OrionTV/refs/heads/master/package.json"
); 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) { if (!response.ok) {
throw new Error("Failed to fetch version info"); 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(); // 清理旧的APK文件
const remoteVersion = remotePackage.version; private async cleanOldApkFiles(): Promise<void> {
try {
return { const { dirs } = ReactNativeBlobUtil.fs;
version: remoteVersion, // 使用DocumentDir而不是DownloadDir
downloadUrl: `https://github.com/zimplexing/OrionTV/releases/download/v${remoteVersion}/orionTV. const files = await ReactNativeBlobUtil.fs.ls(dirs.DocumentDir);
${remoteVersion}.apk`,
}; // 查找所有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) { } catch (error) {
console.error("Error checking version:", error); console.warn('Failed to clean old APK files:', error);
throw error;
} }
} }
async downloadApk(url: string, onProgress?: (progress: number) => void): Promise<string> { async downloadApk(url: string, onProgress?: (progress: number) => void): Promise<string> {
try { let retries = 0;
const { dirs } = ReactNativeBlobUtil.fs; const maxRetries = 3;
const fileName = `OrionTV_v${new Date().getTime()}.apk`;
const filePath = `${dirs.DownloadDir}/${fileName}`; // 清理旧文件
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({ const task = ReactNativeBlobUtil.config({
fileCache: true, fileCache: true,
path: filePath, path: filePath,
addAndroidDownloads: { timeout: UPDATE_CONFIG.DOWNLOAD_TIMEOUT,
useDownloadManager: true, // 移除 addAndroidDownloads 配置,避免使用系统下载管理器
notification: true, // addAndroidDownloads: {
title: "OrionTV 更新下载中", // useDownloadManager: true,
description: "正在下载新版本...", // notification: true,
mime: "application/vnd.android.package-archive", // title: UPDATE_CONFIG.NOTIFICATION.TITLE,
mediaScannable: true, // description: UPDATE_CONFIG.NOTIFICATION.DOWNLOADING_TEXT,
}, // mime: "application/vnd.android.package-archive",
}).fetch("GET", url); // mediaScannable: true,
// },
}).fetch("GET", url);
// 监听下载进度 // 监听下载进度
if (onProgress) { if (onProgress) {
task.progress((received: string, total: string) => { task.progress((received: string, total: string) => {
const receivedNum = parseInt(received, 10); const receivedNum = parseInt(received, 10);
const totalNum = parseInt(total, 10); const totalNum = parseInt(total, 10);
const progress = Math.floor((receivedNum / totalNum) * 100); const progress = Math.floor((receivedNum / totalNum) * 100);
onProgress(progress); 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> { async installApk(filePath: string): Promise<void> {
try { 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, { await FileViewer.open(filePath, {
showOpenWithDialog: false, showOpenWithDialog: true, // 显示选择应用对话框
showAppsSuggestions: false, showAppsSuggestions: true, // 显示应用建议
displayName: "OrionTV Update", displayName: "OrionTV Update",
}); });
} catch (error) { } catch (error) {
console.error("Error installing APK:", 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; throw error;
} }
} }

View File

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