mirror of
https://github.com/zimplexing/OrionTV.git
synced 2026-02-04 03:36:29 +08:00
Merge branch 'v1.3.0' of github.com:zimplexing/OrionTV into v1.3.0
This commit is contained in:
@@ -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>;
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 });
|
||||
},
|
||||
}));
|
||||
Reference in New Issue
Block a user