mirror of
https://github.com/zimplexing/OrionTV.git
synced 2026-02-15 20:34:43 +08:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
09c3931117 | ||
|
|
10a806a657 | ||
|
|
cb3f694cdc | ||
|
|
1cf3733ee2 | ||
|
|
108c20cd26 | ||
|
|
250c42e1ff | ||
|
|
68a1bc2081 | ||
|
|
d8e47dee7b | ||
|
|
5bf0d05820 |
91
.github/workflows/build-apk.yml
vendored
91
.github/workflows/build-apk.yml
vendored
@@ -7,7 +7,7 @@ permissions:
|
|||||||
contents: write
|
contents: write
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build_tv:
|
direct_build:
|
||||||
name: Build Android TV APK
|
name: Build Android TV APK
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
@@ -34,97 +34,24 @@ jobs:
|
|||||||
java-version: "17"
|
java-version: "17"
|
||||||
|
|
||||||
- name: Prebuild TV App
|
- name: Prebuild TV App
|
||||||
run: yarn prebuild-tv
|
run: yarn prebuild
|
||||||
|
|
||||||
- name: Build TV APK
|
- name: Build TV APK
|
||||||
run: yarn build-tv
|
run: yarn build
|
||||||
|
|
||||||
- name: Upload TV APK
|
- name: Rename APK file
|
||||||
uses: actions/upload-artifact@v3
|
|
||||||
with:
|
|
||||||
name: orion-tv-apk
|
|
||||||
path: android/app/build/outputs/apk/release/app-release.apk
|
|
||||||
|
|
||||||
build_mobile:
|
|
||||||
name: Build Android Mobile APK
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Check out repository
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
|
|
||||||
- name: Set up Node.js
|
|
||||||
uses: actions/setup-node@v3
|
|
||||||
with:
|
|
||||||
node-version: 18
|
|
||||||
cache: "yarn"
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: yarn install --frozen-lockfile
|
|
||||||
|
|
||||||
- name: Get version from package.json
|
|
||||||
id: package-version
|
|
||||||
run: echo "version=$(node -p "require('./package.json').version")" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- name: Setup JDK
|
|
||||||
uses: actions/setup-java@v3
|
|
||||||
with:
|
|
||||||
distribution: "zulu"
|
|
||||||
java-version: "17"
|
|
||||||
|
|
||||||
- name: Prebuild Mobile App
|
|
||||||
run: yarn prebuild-mobile
|
|
||||||
|
|
||||||
- name: Build Mobile APK
|
|
||||||
run: yarn build-mobile
|
|
||||||
|
|
||||||
- name: Upload Mobile APK
|
|
||||||
uses: actions/upload-artifact@v3
|
|
||||||
with:
|
|
||||||
name: orion-mobile-apk
|
|
||||||
path: android/app/build/outputs/apk/release/app-release.apk
|
|
||||||
|
|
||||||
release:
|
|
||||||
name: Create Release
|
|
||||||
needs: [build_tv, build_mobile]
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Check out repository
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
|
|
||||||
- name: Get version from package.json
|
|
||||||
id: package-version
|
|
||||||
run: echo "version=$(node -p "require('./package.json').version")" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- name: Download TV APK
|
|
||||||
uses: actions/download-artifact@v3
|
|
||||||
with:
|
|
||||||
name: orion-tv-apk
|
|
||||||
path: artifacts/tv
|
|
||||||
|
|
||||||
- name: Download Mobile APK
|
|
||||||
uses: actions/download-artifact@v3
|
|
||||||
with:
|
|
||||||
name: orion-mobile-apk
|
|
||||||
path: artifacts/mobile
|
|
||||||
|
|
||||||
- name: Rename APK files
|
|
||||||
run: |
|
run: |
|
||||||
mv artifacts/tv/app-release.apk artifacts/orionTV-tv.${{ steps.package-version.outputs.version }}.apk
|
mkdir -p artifacts
|
||||||
mv artifacts/mobile/app-release.apk artifacts/orionTV-mobile.${{ steps.package-version.outputs.version }}.apk
|
cp android/app/build/outputs/apk/release/app-release.apk artifacts/orionTV.${{ steps.package-version.outputs.version }}.apk
|
||||||
|
|
||||||
- name: Create Release and Upload APKs
|
- name: Create Release and Upload APK
|
||||||
uses: softprops/action-gh-release@v2
|
uses: softprops/action-gh-release@v2
|
||||||
with:
|
with:
|
||||||
tag_name: v${{ steps.package-version.outputs.version }}
|
tag_name: v${{ steps.package-version.outputs.version }}
|
||||||
name: Release v${{ steps.package-version.outputs.version }}
|
name: Release v${{ steps.package-version.outputs.version }}
|
||||||
body: |
|
body: Automated release for version v${{ steps.package-version.outputs.version }}.
|
||||||
Automated release for version v${{ steps.package-version.outputs.version }}.
|
|
||||||
- orionTV-tv.${{ steps.package-version.outputs.version }}.apk - Android TV版本
|
|
||||||
- orionTV-mobile.${{ steps.package-version.outputs.version }}.apk - 手机/平板版本
|
|
||||||
draft: false
|
draft: false
|
||||||
prerelease: false
|
prerelease: false
|
||||||
files: |
|
files: artifacts/orionTV.${{ steps.package-version.outputs.version }}.apk
|
||||||
artifacts/orionTV-tv.${{ steps.package-version.outputs.version }}.apk
|
|
||||||
artifacts/orionTV-mobile.${{ steps.package-version.outputs.version }}.apk
|
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|||||||
14
app.json
14
app.json
@@ -40,11 +40,6 @@
|
|||||||
"orientation": "default",
|
"orientation": "default",
|
||||||
"icon": "./assets/images/icon.png",
|
"icon": "./assets/images/icon.png",
|
||||||
"userInterfaceStyle": "dark",
|
"userInterfaceStyle": "dark",
|
||||||
"splash": {
|
|
||||||
"image": "./assets/images/splash.png",
|
|
||||||
"resizeMode": "contain",
|
|
||||||
"backgroundColor": "#000000"
|
|
||||||
},
|
|
||||||
"assetBundlePatterns": [
|
"assetBundlePatterns": [
|
||||||
"**/*"
|
"**/*"
|
||||||
],
|
],
|
||||||
@@ -54,10 +49,6 @@
|
|||||||
"hardwareAcceleration": true,
|
"hardwareAcceleration": true,
|
||||||
"networkSecurityConfig": "@xml/network_security_config",
|
"networkSecurityConfig": "@xml/network_security_config",
|
||||||
"icon": "./assets/images/icon.png",
|
"icon": "./assets/images/icon.png",
|
||||||
"adaptiveIcon": {
|
|
||||||
"foregroundImage": "./assets/images/adaptive-icon.png",
|
|
||||||
"backgroundColor": "#000000"
|
|
||||||
},
|
|
||||||
"permissions": [
|
"permissions": [
|
||||||
"android.permission.INTERNET",
|
"android.permission.INTERNET",
|
||||||
"android.permission.ACCESS_NETWORK_STATE",
|
"android.permission.ACCESS_NETWORK_STATE",
|
||||||
@@ -93,11 +84,6 @@
|
|||||||
"usesNonExemptEncryption": false
|
"usesNonExemptEncryption": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"web": {
|
|
||||||
"bundler": "metro",
|
|
||||||
"output": "static",
|
|
||||||
"favicon": "./assets/images/favicon.png"
|
|
||||||
},
|
|
||||||
"scheme": "oriontv",
|
"scheme": "oriontv",
|
||||||
"extra": {
|
"extra": {
|
||||||
"router": {
|
"router": {
|
||||||
|
|||||||
@@ -10,11 +10,11 @@ import { Search, Settings, LogOut, Heart } from "lucide-react-native";
|
|||||||
import { StyledButton } from "@/components/StyledButton";
|
import { StyledButton } from "@/components/StyledButton";
|
||||||
import useHomeStore, { RowItem, Category } from "@/stores/homeStore";
|
import useHomeStore, { RowItem, Category } from "@/stores/homeStore";
|
||||||
import useAuthStore from "@/stores/authStore";
|
import useAuthStore from "@/stores/authStore";
|
||||||
import { useSettingsStore } from "@/stores/settingsStore";
|
|
||||||
import CustomScrollView from "@/components/CustomScrollView";
|
import CustomScrollView from "@/components/CustomScrollView";
|
||||||
import { useResponsiveLayout } from "@/hooks/useResponsiveLayout";
|
import { useResponsiveLayout } from "@/hooks/useResponsiveLayout";
|
||||||
import { getCommonResponsiveStyles } from "@/utils/ResponsiveStyles";
|
import { getCommonResponsiveStyles } from "@/utils/ResponsiveStyles";
|
||||||
import ResponsiveNavigation from "@/components/navigation/ResponsiveNavigation";
|
import ResponsiveNavigation from "@/components/navigation/ResponsiveNavigation";
|
||||||
|
import { useApiConfig, getApiConfigErrorMessage } from "@/hooks/useApiConfig";
|
||||||
|
|
||||||
const LOAD_MORE_THRESHOLD = 200;
|
const LOAD_MORE_THRESHOLD = 200;
|
||||||
|
|
||||||
@@ -44,7 +44,7 @@ export default function HomeScreen() {
|
|||||||
clearError,
|
clearError,
|
||||||
} = useHomeStore();
|
} = useHomeStore();
|
||||||
const { isLoggedIn, logout } = useAuthStore();
|
const { isLoggedIn, logout } = useAuthStore();
|
||||||
const { apiBaseUrl } = useSettingsStore();
|
const apiConfigStatus = useApiConfig();
|
||||||
|
|
||||||
useFocusEffect(
|
useFocusEffect(
|
||||||
useCallback(() => {
|
useCallback(() => {
|
||||||
@@ -52,34 +52,44 @@ export default function HomeScreen() {
|
|||||||
}, [refreshPlayRecords])
|
}, [refreshPlayRecords])
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 统一的数据获取逻辑
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// 只有在 apiBaseUrl 存在时才调用 fetchInitialData,避免时序问题
|
if (!selectedCategory) return;
|
||||||
if (selectedCategory && !selectedCategory.tags && apiBaseUrl) {
|
|
||||||
fetchInitialData();
|
// 如果是容器分类且没有选择标签,设置默认标签
|
||||||
} else if (selectedCategory?.tags && !selectedCategory.tag) {
|
if (selectedCategory.tags && !selectedCategory.tag) {
|
||||||
const defaultTag = selectedCategory.tags[0];
|
const defaultTag = selectedCategory.tags[0];
|
||||||
setSelectedTag(defaultTag);
|
setSelectedTag(defaultTag);
|
||||||
selectCategory({ ...selectedCategory, tag: defaultTag });
|
selectCategory({ ...selectedCategory, tag: defaultTag });
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}, [selectedCategory, fetchInitialData, selectCategory, apiBaseUrl]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
// 只有在API配置完成且分类有效时才获取数据
|
||||||
// 只有在 apiBaseUrl 存在时才调用 fetchInitialData,避免时序问题
|
if (apiConfigStatus.isConfigured && !apiConfigStatus.needsConfiguration) {
|
||||||
if (selectedCategory && selectedCategory.tag && apiBaseUrl) {
|
// 对于有标签的分类,需要确保有标签才获取数据
|
||||||
fetchInitialData();
|
if (selectedCategory.tags && selectedCategory.tag) {
|
||||||
|
fetchInitialData();
|
||||||
|
}
|
||||||
|
// 对于无标签的分类,直接获取数据
|
||||||
|
else if (!selectedCategory.tags) {
|
||||||
|
fetchInitialData();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [fetchInitialData, selectedCategory, selectedCategory.tag, apiBaseUrl]);
|
}, [
|
||||||
|
selectedCategory,
|
||||||
|
selectedCategory?.tag,
|
||||||
|
apiConfigStatus.isConfigured,
|
||||||
|
apiConfigStatus.needsConfiguration,
|
||||||
|
fetchInitialData,
|
||||||
|
selectCategory,
|
||||||
|
]);
|
||||||
|
|
||||||
// 检查是否需要显示API配置提示
|
// 清除错误状态的逻辑
|
||||||
const shouldShowApiConfig = !apiBaseUrl && selectedCategory && !selectedCategory.tags;
|
|
||||||
|
|
||||||
// 清除错误状态,当API未配置时
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (shouldShowApiConfig && error) {
|
if (apiConfigStatus.needsConfiguration && error) {
|
||||||
// 如果需要显示API配置提示,清除之前的错误状态
|
|
||||||
clearError();
|
clearError();
|
||||||
}
|
}
|
||||||
}, [shouldShowApiConfig, error, clearError]);
|
}, [apiConfigStatus.needsConfiguration, error, clearError]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!loading && contentData.length > 0) {
|
if (!loading && contentData.length > 0) {
|
||||||
@@ -119,7 +129,7 @@ export default function HomeScreen() {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderContentItem = ({ item, index }: { item: RowItem; index: number }) => (
|
const renderContentItem = ({ item }: { item: RowItem; index: number }) => (
|
||||||
<VideoCard
|
<VideoCard
|
||||||
id={item.id}
|
id={item.id}
|
||||||
source={item.source}
|
source={item.source}
|
||||||
@@ -142,6 +152,9 @@ export default function HomeScreen() {
|
|||||||
return <ActivityIndicator style={{ marginVertical: 20 }} size="large" />;
|
return <ActivityIndicator style={{ marginVertical: 20 }} size="large" />;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 检查是否需要显示API配置提示
|
||||||
|
const shouldShowApiConfig = apiConfigStatus.needsConfiguration && selectedCategory && !selectedCategory.tags;
|
||||||
|
|
||||||
// TV端和平板端的顶部导航
|
// TV端和平板端的顶部导航
|
||||||
const renderHeader = () => {
|
const renderHeader = () => {
|
||||||
if (deviceType === "mobile") {
|
if (deviceType === "mobile") {
|
||||||
@@ -280,8 +293,21 @@ export default function HomeScreen() {
|
|||||||
{/* 内容网格 */}
|
{/* 内容网格 */}
|
||||||
{shouldShowApiConfig ? (
|
{shouldShowApiConfig ? (
|
||||||
<View style={commonStyles.center}>
|
<View style={commonStyles.center}>
|
||||||
<ThemedText type="subtitle" style={{ padding: spacing, textAlign: 'center' }}>
|
<ThemedText type="subtitle" style={{ padding: spacing, textAlign: "center" }}>
|
||||||
请点击右上角设置按钮,配置您的服务器地址
|
{getApiConfigErrorMessage(apiConfigStatus)}
|
||||||
|
</ThemedText>
|
||||||
|
</View>
|
||||||
|
) : apiConfigStatus.isValidating ? (
|
||||||
|
<View style={commonStyles.center}>
|
||||||
|
<ActivityIndicator size="large" />
|
||||||
|
<ThemedText type="subtitle" style={{ padding: spacing, textAlign: "center" }}>
|
||||||
|
正在验证服务器配置...
|
||||||
|
</ThemedText>
|
||||||
|
</View>
|
||||||
|
) : apiConfigStatus.error && !apiConfigStatus.isValid ? (
|
||||||
|
<View style={commonStyles.center}>
|
||||||
|
<ThemedText type="subtitle" style={{ padding: spacing, textAlign: "center" }}>
|
||||||
|
{apiConfigStatus.error}
|
||||||
</ThemedText>
|
</ThemedText>
|
||||||
</View>
|
</View>
|
||||||
) : loading ? (
|
) : loading ? (
|
||||||
|
|||||||
@@ -195,7 +195,7 @@ const VideoCard = forwardRef<View, VideoCardProps>(
|
|||||||
{isContinueWatching && (
|
{isContinueWatching && (
|
||||||
<View style={styles.infoRow}>
|
<View style={styles.infoRow}>
|
||||||
<ThemedText style={styles.continueLabel}>
|
<ThemedText style={styles.continueLabel}>
|
||||||
第{episodeIndex! + 1}集 已观看 {Math.round((progress || 0) * 100)}%
|
第{episodeIndex}集 已观看 {Math.round((progress || 0) * 100)}%
|
||||||
</ThemedText>
|
</ThemedText>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
@@ -343,4 +343,4 @@ const styles = StyleSheet.create({
|
|||||||
color: Colors.dark.primary,
|
color: Colors.dark.primary,
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,11 +3,19 @@ import { View, StyleSheet, Platform, ActivityIndicator } from "react-native";
|
|||||||
import { ThemedText } from "../ThemedText";
|
import { ThemedText } from "../ThemedText";
|
||||||
import { StyledButton } from "../StyledButton";
|
import { StyledButton } from "../StyledButton";
|
||||||
import { useUpdateStore } from "@/stores/updateStore";
|
import { useUpdateStore } from "@/stores/updateStore";
|
||||||
import { UPDATE_CONFIG } from "@/constants/UpdateConfig";
|
// import { UPDATE_CONFIG } from "@/constants/UpdateConfig";
|
||||||
|
|
||||||
export function UpdateSection() {
|
export function UpdateSection() {
|
||||||
const { currentVersion, remoteVersion, updateAvailable, downloading, downloadProgress, checkForUpdate } =
|
const {
|
||||||
useUpdateStore();
|
currentVersion,
|
||||||
|
remoteVersion,
|
||||||
|
updateAvailable,
|
||||||
|
downloading,
|
||||||
|
downloadProgress,
|
||||||
|
checkForUpdate,
|
||||||
|
isLatestVersion,
|
||||||
|
error
|
||||||
|
} = useUpdateStore();
|
||||||
|
|
||||||
const [checking, setChecking] = React.useState(false);
|
const [checking, setChecking] = React.useState(false);
|
||||||
|
|
||||||
@@ -36,6 +44,20 @@ export function UpdateSection() {
|
|||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{isLatestVersion && remoteVersion && (
|
||||||
|
<View style={styles.row}>
|
||||||
|
<ThemedText style={styles.label}>状态</ThemedText>
|
||||||
|
<ThemedText style={[styles.value, styles.latestVersion]}>已是最新版本</ThemedText>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<View style={styles.row}>
|
||||||
|
<ThemedText style={styles.label}>检查结果</ThemedText>
|
||||||
|
<ThemedText style={[styles.value, styles.errorText]}>{error}</ThemedText>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
{downloading && (
|
{downloading && (
|
||||||
<View style={styles.row}>
|
<View style={styles.row}>
|
||||||
<ThemedText style={styles.label}>下载进度</ThemedText>
|
<ThemedText style={styles.label}>下载进度</ThemedText>
|
||||||
@@ -96,6 +118,14 @@ const styles = StyleSheet.create({
|
|||||||
color: "#00bb5e",
|
color: "#00bb5e",
|
||||||
fontWeight: "bold",
|
fontWeight: "bold",
|
||||||
},
|
},
|
||||||
|
latestVersion: {
|
||||||
|
color: "#00bb5e",
|
||||||
|
fontWeight: "500",
|
||||||
|
},
|
||||||
|
errorText: {
|
||||||
|
color: "#ff6b6b",
|
||||||
|
fontWeight: "500",
|
||||||
|
},
|
||||||
buttonContainer: {
|
buttonContainer: {
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
gap: 12,
|
gap: 12,
|
||||||
|
|||||||
@@ -1,39 +1,36 @@
|
|||||||
import { DeviceUtils } from '@/utils/DeviceUtils';
|
|
||||||
|
|
||||||
export const UPDATE_CONFIG = {
|
export const UPDATE_CONFIG = {
|
||||||
// 自动检查更新
|
// 自动检查更新
|
||||||
AUTO_CHECK: true,
|
AUTO_CHECK: true,
|
||||||
|
|
||||||
// 检查更新间隔(毫秒)
|
// 检查更新间隔(毫秒)
|
||||||
CHECK_INTERVAL: 12 * 60 * 60 * 1000, // 12小时
|
CHECK_INTERVAL: 12 * 60 * 60 * 1000, // 12小时
|
||||||
|
|
||||||
// GitHub相关URL
|
// GitHub相关URL
|
||||||
GITHUB_RAW_URL: 'https://ghfast.top/https://raw.githubusercontent.com/zimplexing/OrionTV/refs/heads/master/package.json',
|
GITHUB_RAW_URL:
|
||||||
|
"https://gh-proxy.com/https://raw.githubusercontent.com/zimplexing/OrionTV/refs/heads/master/package.json",
|
||||||
|
|
||||||
// 获取平台特定的下载URL
|
// 获取平台特定的下载URL
|
||||||
getDownloadUrl(version: string): string {
|
getDownloadUrl(version: string): string {
|
||||||
const isTV = DeviceUtils.isTV();
|
return `https://gh-proxy.com/https://github.com/zimplexing/OrionTV/releases/download/v${version}/orionTV.${version}.apk`;
|
||||||
const platform = isTV ? 'tv' : 'mobile';
|
|
||||||
return `https://ghfast.top/https://github.com/zimplexing/OrionTV/releases/download/v${version}/orionTV-${platform}.${version}.apk`;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// 是否显示更新日志
|
// 是否显示更新日志
|
||||||
SHOW_RELEASE_NOTES: true,
|
SHOW_RELEASE_NOTES: true,
|
||||||
|
|
||||||
// 是否允许跳过版本
|
// 是否允许跳过版本
|
||||||
ALLOW_SKIP_VERSION: true,
|
ALLOW_SKIP_VERSION: true,
|
||||||
|
|
||||||
// 下载超时时间(毫秒)
|
// 下载超时时间(毫秒)
|
||||||
DOWNLOAD_TIMEOUT: 10 * 60 * 1000, // 10分钟
|
DOWNLOAD_TIMEOUT: 10 * 60 * 1000, // 10分钟
|
||||||
|
|
||||||
// 是否在WIFI下自动下载
|
// 是否在WIFI下自动下载
|
||||||
AUTO_DOWNLOAD_ON_WIFI: false,
|
AUTO_DOWNLOAD_ON_WIFI: false,
|
||||||
|
|
||||||
// 更新通知设置
|
// 更新通知设置
|
||||||
NOTIFICATION: {
|
NOTIFICATION: {
|
||||||
ENABLED: true,
|
ENABLED: true,
|
||||||
TITLE: 'OrionTV 更新',
|
TITLE: "OrionTV 更新",
|
||||||
DOWNLOADING_TEXT: '正在下载新版本...',
|
DOWNLOADING_TEXT: "正在下载新版本...",
|
||||||
DOWNLOAD_COMPLETE_TEXT: '下载完成,点击安装',
|
DOWNLOAD_COMPLETE_TEXT: "下载完成,点击安装",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
139
hooks/useApiConfig.ts
Normal file
139
hooks/useApiConfig.ts
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { useSettingsStore } from '@/stores/settingsStore';
|
||||||
|
import { api } from '@/services/api';
|
||||||
|
|
||||||
|
export interface ApiConfigStatus {
|
||||||
|
isConfigured: boolean;
|
||||||
|
isValidating: boolean;
|
||||||
|
isValid: boolean | null;
|
||||||
|
error: string | null;
|
||||||
|
needsConfiguration: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useApiConfig = () => {
|
||||||
|
const { apiBaseUrl, serverConfig, isLoadingServerConfig } = useSettingsStore();
|
||||||
|
const [validationState, setValidationState] = useState<{
|
||||||
|
isValidating: boolean;
|
||||||
|
isValid: boolean | null;
|
||||||
|
error: string | null;
|
||||||
|
}>({
|
||||||
|
isValidating: false,
|
||||||
|
isValid: null,
|
||||||
|
error: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
const isConfigured = Boolean(apiBaseUrl && apiBaseUrl.trim());
|
||||||
|
const needsConfiguration = !isConfigured;
|
||||||
|
|
||||||
|
// Validate API configuration when it changes
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isConfigured) {
|
||||||
|
setValidationState({
|
||||||
|
isValidating: false,
|
||||||
|
isValid: false,
|
||||||
|
error: null,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const validateConfig = async () => {
|
||||||
|
setValidationState(prev => ({ ...prev, isValidating: true, error: null }));
|
||||||
|
|
||||||
|
try {
|
||||||
|
await api.getServerConfig();
|
||||||
|
setValidationState({
|
||||||
|
isValidating: false,
|
||||||
|
isValid: true,
|
||||||
|
error: null,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
let errorMessage = '服务器连接失败';
|
||||||
|
|
||||||
|
if (error instanceof Error) {
|
||||||
|
switch (error.message) {
|
||||||
|
case 'API_URL_NOT_SET':
|
||||||
|
errorMessage = 'API地址未设置';
|
||||||
|
break;
|
||||||
|
case 'UNAUTHORIZED':
|
||||||
|
errorMessage = '服务器认证失败';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
if (error.message.includes('Network')) {
|
||||||
|
errorMessage = '网络连接失败,请检查网络或服务器地址';
|
||||||
|
} else if (error.message.includes('timeout')) {
|
||||||
|
errorMessage = '连接超时,请检查服务器地址';
|
||||||
|
} else if (error.message.includes('404')) {
|
||||||
|
errorMessage = '服务器地址无效,请检查API路径';
|
||||||
|
} else if (error.message.includes('500')) {
|
||||||
|
errorMessage = '服务器内部错误';
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setValidationState({
|
||||||
|
isValidating: false,
|
||||||
|
isValid: false,
|
||||||
|
error: errorMessage,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Only validate if not already loading server config
|
||||||
|
if (!isLoadingServerConfig) {
|
||||||
|
validateConfig();
|
||||||
|
}
|
||||||
|
}, [apiBaseUrl, isConfigured, isLoadingServerConfig]);
|
||||||
|
|
||||||
|
// Reset validation when server config loading state changes
|
||||||
|
useEffect(() => {
|
||||||
|
if (isLoadingServerConfig) {
|
||||||
|
setValidationState(prev => ({ ...prev, isValidating: true, error: null }));
|
||||||
|
}
|
||||||
|
}, [isLoadingServerConfig]);
|
||||||
|
|
||||||
|
// Update validation state based on server config
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isLoadingServerConfig && isConfigured) {
|
||||||
|
if (serverConfig) {
|
||||||
|
setValidationState(prev => ({ ...prev, isValid: true, error: null }));
|
||||||
|
} else {
|
||||||
|
setValidationState(prev => ({
|
||||||
|
...prev,
|
||||||
|
isValid: false,
|
||||||
|
error: prev.error || '无法获取服务器配置'
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [serverConfig, isLoadingServerConfig, isConfigured]);
|
||||||
|
|
||||||
|
const status: ApiConfigStatus = {
|
||||||
|
isConfigured,
|
||||||
|
isValidating: validationState.isValidating || isLoadingServerConfig,
|
||||||
|
isValid: validationState.isValid,
|
||||||
|
error: validationState.error,
|
||||||
|
needsConfiguration,
|
||||||
|
};
|
||||||
|
|
||||||
|
return status;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getApiConfigErrorMessage = (status: ApiConfigStatus): string => {
|
||||||
|
if (status.needsConfiguration) {
|
||||||
|
return '请点击右上角设置按钮,配置您的服务器地址';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status.error) {
|
||||||
|
return status.error;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status.isValidating) {
|
||||||
|
return '正在验证服务器配置...';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status.isValid === false) {
|
||||||
|
return '服务器配置验证失败,请检查设置';
|
||||||
|
}
|
||||||
|
|
||||||
|
return '加载失败,请重试';
|
||||||
|
};
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from "react";
|
||||||
import { Dimensions, Platform } from 'react-native';
|
import { Dimensions, Platform } from "react-native";
|
||||||
|
|
||||||
export type DeviceType = 'mobile' | 'tablet' | 'tv';
|
export type DeviceType = "mobile" | "tablet" | "tv";
|
||||||
|
|
||||||
export interface ResponsiveConfig {
|
export interface ResponsiveConfig {
|
||||||
deviceType: DeviceType;
|
deviceType: DeviceType;
|
||||||
@@ -17,40 +17,44 @@ export interface ResponsiveConfig {
|
|||||||
const BREAKPOINTS = {
|
const BREAKPOINTS = {
|
||||||
mobile: { min: 0, max: 767 },
|
mobile: { min: 0, max: 767 },
|
||||||
tablet: { min: 768, max: 1023 },
|
tablet: { min: 768, max: 1023 },
|
||||||
tv: { min: 1024, max: Infinity }
|
tv: { min: 1024, max: Infinity },
|
||||||
};
|
};
|
||||||
|
|
||||||
const getDeviceType = (width: number): DeviceType => {
|
const getDeviceType = (width: number): DeviceType => {
|
||||||
const isTV = process.env.EXPO_TV === '1' || Platform.isTV;
|
if (Platform.isTV) return "tv";
|
||||||
if (isTV) return 'tv';
|
|
||||||
|
if (width >= BREAKPOINTS.tv.min) return "tv";
|
||||||
if (width >= BREAKPOINTS.tv.min) return 'tv';
|
if (width >= BREAKPOINTS.tablet.min) return "tablet";
|
||||||
if (width >= BREAKPOINTS.tablet.min) return 'tablet';
|
return "mobile";
|
||||||
return 'mobile';
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const getLayoutConfig = (deviceType: DeviceType, width: number, height: number, isPortrait: boolean): ResponsiveConfig => {
|
const getLayoutConfig = (
|
||||||
const spacing = deviceType === 'mobile' ? 8 : deviceType === 'tablet' ? 12 : 16;
|
deviceType: DeviceType,
|
||||||
|
width: number,
|
||||||
|
height: number,
|
||||||
|
isPortrait: boolean
|
||||||
|
): ResponsiveConfig => {
|
||||||
|
const spacing = deviceType === "mobile" ? 8 : deviceType === "tablet" ? 12 : 16;
|
||||||
|
|
||||||
let columns: number;
|
let columns: number;
|
||||||
let cardWidth: number;
|
let cardWidth: number;
|
||||||
let cardHeight: number;
|
let cardHeight: number;
|
||||||
|
|
||||||
switch (deviceType) {
|
switch (deviceType) {
|
||||||
case 'mobile':
|
case "mobile":
|
||||||
columns = isPortrait ? 3 : 4;
|
columns = isPortrait ? 3 : 4;
|
||||||
// 使用flex布局,卡片可以更大一些来填充空间
|
// 使用flex布局,卡片可以更大一些来填充空间
|
||||||
cardWidth = (width - spacing) / columns * 0.85; // 增大到85%
|
cardWidth = ((width - spacing) / columns) * 0.85; // 增大到85%
|
||||||
cardHeight = cardWidth * 1.2; // 5:6 aspect ratio (reduced from 2:3)
|
cardHeight = cardWidth * 1.2; // 5:6 aspect ratio (reduced from 2:3)
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'tablet':
|
case "tablet":
|
||||||
columns = isPortrait ? 3 : 4;
|
columns = isPortrait ? 3 : 4;
|
||||||
cardWidth = (width - spacing) / columns * 0.85; // 增大到85%
|
cardWidth = ((width - spacing) / columns) * 0.85; // 增大到85%
|
||||||
cardHeight = cardWidth * 1.4; // slightly less tall ratio
|
cardHeight = cardWidth * 1.4; // slightly less tall ratio
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'tv':
|
case "tv":
|
||||||
default:
|
default:
|
||||||
columns = 5;
|
columns = 5;
|
||||||
cardWidth = 160; // Fixed width for TV
|
cardWidth = 160; // Fixed width for TV
|
||||||
@@ -72,12 +76,12 @@ const getLayoutConfig = (deviceType: DeviceType, width: number, height: number,
|
|||||||
|
|
||||||
export const useResponsiveLayout = (): ResponsiveConfig => {
|
export const useResponsiveLayout = (): ResponsiveConfig => {
|
||||||
const [dimensions, setDimensions] = useState(() => {
|
const [dimensions, setDimensions] = useState(() => {
|
||||||
const { width, height } = Dimensions.get('window');
|
const { width, height } = Dimensions.get("window");
|
||||||
return { width, height };
|
return { width, height };
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const subscription = Dimensions.addEventListener('change', ({ window }) => {
|
const subscription = Dimensions.addEventListener("change", ({ window }) => {
|
||||||
setDimensions({ width: window.width, height: window.height });
|
setDimensions({ width: window.width, height: window.height });
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -87,7 +91,7 @@ export const useResponsiveLayout = (): ResponsiveConfig => {
|
|||||||
const { width, height } = dimensions;
|
const { width, height } = dimensions;
|
||||||
const isPortrait = height > width;
|
const isPortrait = height > width;
|
||||||
const deviceType = getDeviceType(width);
|
const deviceType = getDeviceType(width);
|
||||||
|
|
||||||
return getLayoutConfig(deviceType, width, height, isPortrait);
|
return getLayoutConfig(deviceType, width, height, isPortrait);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -100,31 +104,31 @@ export const useResponsiveValue = <T>(values: { mobile: T; tablet: T; tv: T }):
|
|||||||
// Utility hook for responsive styles
|
// Utility hook for responsive styles
|
||||||
export const useResponsiveStyles = () => {
|
export const useResponsiveStyles = () => {
|
||||||
const config = useResponsiveLayout();
|
const config = useResponsiveLayout();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
// Common responsive styles
|
// Common responsive styles
|
||||||
container: {
|
container: {
|
||||||
paddingHorizontal: config.spacing,
|
paddingHorizontal: config.spacing,
|
||||||
},
|
},
|
||||||
|
|
||||||
// Card styles
|
// Card styles
|
||||||
cardContainer: {
|
cardContainer: {
|
||||||
width: config.cardWidth,
|
width: config.cardWidth,
|
||||||
height: config.cardHeight,
|
height: config.cardHeight,
|
||||||
marginBottom: config.spacing,
|
marginBottom: config.spacing,
|
||||||
},
|
},
|
||||||
|
|
||||||
// Grid styles
|
// Grid styles
|
||||||
gridContainer: {
|
gridContainer: {
|
||||||
paddingHorizontal: config.spacing / 2,
|
paddingHorizontal: config.spacing / 2,
|
||||||
},
|
},
|
||||||
|
|
||||||
// Typography
|
// Typography
|
||||||
titleFontSize: config.deviceType === 'mobile' ? 18 : config.deviceType === 'tablet' ? 22 : 28,
|
titleFontSize: config.deviceType === "mobile" ? 18 : config.deviceType === "tablet" ? 22 : 28,
|
||||||
bodyFontSize: config.deviceType === 'mobile' ? 14 : config.deviceType === 'tablet' ? 16 : 18,
|
bodyFontSize: config.deviceType === "mobile" ? 14 : config.deviceType === "tablet" ? 16 : 18,
|
||||||
|
|
||||||
// Spacing
|
// Spacing
|
||||||
sectionSpacing: config.deviceType === 'mobile' ? 16 : config.deviceType === 'tablet' ? 20 : 24,
|
sectionSpacing: config.deviceType === "mobile" ? 16 : config.deviceType === "tablet" ? 20 : 24,
|
||||||
itemSpacing: config.spacing,
|
itemSpacing: config.spacing,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// Learn more https://docs.expo.io/guides/customizing-metro
|
// Learn more https://docs.expo.io/guides/customizing-metro
|
||||||
const {getDefaultConfig} = require('expo/metro-config');
|
const { getDefaultConfig } = require("expo/metro-config");
|
||||||
const path = require('path');
|
const path = require("path");
|
||||||
|
|
||||||
// Find the project and workspace directories
|
// Find the project and workspace directories
|
||||||
// eslint-disable-next-line no-undef
|
// eslint-disable-next-line no-undef
|
||||||
@@ -16,24 +16,24 @@ const config = getDefaultConfig(projectRoot);
|
|||||||
// Metro will still resolve source files with standard extensions
|
// Metro will still resolve source files with standard extensions
|
||||||
// as usual if TV-specific files are not found for a module.
|
// as usual if TV-specific files are not found for a module.
|
||||||
//
|
//
|
||||||
if (process.env?.EXPO_TV === '1') {
|
// if (process.env?.EXPO_TV === '1') {
|
||||||
const originalSourceExts = config.resolver.sourceExts;
|
// const originalSourceExts = config.resolver.sourceExts;
|
||||||
const tvSourceExts = [
|
// const tvSourceExts = [
|
||||||
...originalSourceExts.map((e) => `tv.${e}`),
|
// ...originalSourceExts.map((e) => `tv.${e}`),
|
||||||
...originalSourceExts,
|
// ...originalSourceExts,
|
||||||
];
|
// ];
|
||||||
config.resolver.sourceExts = tvSourceExts;
|
// config.resolver.sourceExts = tvSourceExts;
|
||||||
}
|
// }
|
||||||
|
|
||||||
// This can be replaced with `find-yarn-workspace-root`
|
// This can be replaced with `find-yarn-workspace-root`
|
||||||
const monorepoRoot = path.resolve(projectRoot, '../..');
|
const monorepoRoot = path.resolve(projectRoot, "../..");
|
||||||
|
|
||||||
// 1. Watch all files within the monorepo
|
// 1. Watch all files within the monorepo
|
||||||
config.watchFolders = [monorepoRoot];
|
config.watchFolders = [monorepoRoot];
|
||||||
// 2. Let Metro know where to resolve packages and in what order
|
// 2. Let Metro know where to resolve packages and in what order
|
||||||
config.resolver.nodeModulesPaths = [
|
config.resolver.nodeModulesPaths = [
|
||||||
path.resolve(projectRoot, 'node_modules'),
|
path.resolve(projectRoot, "node_modules"),
|
||||||
path.resolve(monorepoRoot, 'node_modules'),
|
path.resolve(monorepoRoot, "node_modules"),
|
||||||
];
|
];
|
||||||
config.resolver.disableHierarchicalLookup = true;
|
config.resolver.disableHierarchicalLookup = true;
|
||||||
|
|
||||||
|
|||||||
22
package.json
22
package.json
@@ -2,24 +2,14 @@
|
|||||||
"name": "OrionTV",
|
"name": "OrionTV",
|
||||||
"private": true,
|
"private": true,
|
||||||
"main": "expo-router/entry",
|
"main": "expo-router/entry",
|
||||||
"version": "1.3.0",
|
"version": "1.3.1",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "EXPO_USE_METRO_WORKSPACE_ROOT=1 expo start",
|
"start": "EXPO_TV=1 EXPO_USE_METRO_WORKSPACE_ROOT=1 expo start",
|
||||||
"start-tv": "EXPO_TV=1 EXPO_USE_METRO_WORKSPACE_ROOT=1 expo start",
|
"android": "EXPO_TV=1 EXPO_USE_METRO_WORKSPACE_ROOT=1 expo run:android",
|
||||||
"start-mobile": "EXPO_USE_METRO_WORKSPACE_ROOT=1 expo start --tunnel",
|
"ios": "EXPO_TV=1 EXPO_USE_METRO_WORKSPACE_ROOT=1 expo run:ios",
|
||||||
"android": "EXPO_USE_METRO_WORKSPACE_ROOT=1 expo run:android",
|
"prebuild": "EXPO_TV=1 EXPO_USE_METRO_WORKSPACE_ROOT=1 expo prebuild --clean && yarn copy-config",
|
||||||
"android-tv": "EXPO_TV=1 EXPO_USE_METRO_WORKSPACE_ROOT=1 expo run:android",
|
|
||||||
"android-mobile": "EXPO_USE_METRO_WORKSPACE_ROOT=1 expo run:android --device",
|
|
||||||
"ios": "EXPO_USE_METRO_WORKSPACE_ROOT=1 expo run:ios",
|
|
||||||
"ios-tv": "EXPO_TV=1 EXPO_USE_METRO_WORKSPACE_ROOT=1 expo run:ios",
|
|
||||||
"ios-mobile": "EXPO_USE_METRO_WORKSPACE_ROOT=1 expo run:ios --device",
|
|
||||||
"prebuild": "EXPO_USE_METRO_WORKSPACE_ROOT=1 expo prebuild --clean",
|
|
||||||
"prebuild-tv": "EXPO_TV=1 EXPO_USE_METRO_WORKSPACE_ROOT=1 expo prebuild --clean && yarn copy-config",
|
|
||||||
"prebuild-mobile": "EXPO_USE_METRO_WORKSPACE_ROOT=1 expo prebuild --clean --platform android && yarn copy-config",
|
|
||||||
"copy-config": "cp -r xml/* android/app/src/*",
|
"copy-config": "cp -r xml/* android/app/src/*",
|
||||||
"build": "cd android && ./gradlew assembleRelease",
|
"build": "EXPO_TV=1 yarn prebuild && cd android && ./gradlew assembleRelease",
|
||||||
"build-tv": "EXPO_TV=1 yarn prebuild-tv && cd android && ./gradlew assembleRelease",
|
|
||||||
"build-mobile": "yarn prebuild-mobile && cd android && ./gradlew assembleRelease",
|
|
||||||
"build-debug": "cd android && ./gradlew assembleDebug",
|
"build-debug": "cd android && ./gradlew assembleDebug",
|
||||||
"test": "jest --watchAll",
|
"test": "jest --watchAll",
|
||||||
"test-ci": "jest --ci --coverage --no-cache",
|
"test-ci": "jest --ci --coverage --no-cache",
|
||||||
|
|||||||
@@ -166,11 +166,11 @@ const useHomeStore = create<HomeState>((set, get) => ({
|
|||||||
if (pageStart === 0) {
|
if (pageStart === 0) {
|
||||||
// 缓存新数据
|
// 缓存新数据
|
||||||
dataCache.set(cacheKey, newItems);
|
dataCache.set(cacheKey, newItems);
|
||||||
set((state) => ({
|
set({
|
||||||
contentData: newItems,
|
contentData: newItems,
|
||||||
pageStart: result.list.length,
|
pageStart: result.list.length,
|
||||||
hasMore: true,
|
hasMore: true,
|
||||||
}));
|
});
|
||||||
} else {
|
} else {
|
||||||
// 增量加载时不缓存,直接追加
|
// 增量加载时不缓存,直接追加
|
||||||
set((state) => ({
|
set((state) => ({
|
||||||
@@ -187,11 +187,25 @@ const useHomeStore = create<HomeState>((set, get) => ({
|
|||||||
set({ hasMore: false });
|
set({ hasMore: false });
|
||||||
}
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
|
let errorMessage = "加载失败,请重试";
|
||||||
|
|
||||||
if (err.message === "API_URL_NOT_SET") {
|
if (err.message === "API_URL_NOT_SET") {
|
||||||
set({ error: "请点击右上角设置按钮,配置您的服务器地址" });
|
errorMessage = "请点击右上角设置按钮,配置您的服务器地址";
|
||||||
} else {
|
} else if (err.message === "UNAUTHORIZED") {
|
||||||
set({ error: "加载失败,请重试" });
|
errorMessage = "认证失败,请重新登录";
|
||||||
|
} else if (err.message.includes("Network")) {
|
||||||
|
errorMessage = "网络连接失败,请检查网络连接";
|
||||||
|
} else if (err.message.includes("timeout")) {
|
||||||
|
errorMessage = "请求超时,请检查网络或服务器状态";
|
||||||
|
} else if (err.message.includes("404")) {
|
||||||
|
errorMessage = "服务器API路径不正确,请检查服务器配置";
|
||||||
|
} else if (err.message.includes("500")) {
|
||||||
|
errorMessage = "服务器内部错误,请联系管理员";
|
||||||
|
} else if (err.message.includes("403")) {
|
||||||
|
errorMessage = "访问被拒绝,请检查权限设置";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
set({ error: errorMessage });
|
||||||
} finally {
|
} finally {
|
||||||
set({ loading: false, loadingMore: false });
|
set({ loading: false, loadingMore: false });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { create } from 'zustand';
|
import { create } from 'zustand';
|
||||||
import updateService from '../services/updateService';
|
import updateService from '../services/updateService';
|
||||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||||
|
import Toast from 'react-native-toast-message';
|
||||||
|
|
||||||
interface UpdateState {
|
interface UpdateState {
|
||||||
// 状态
|
// 状态
|
||||||
@@ -15,6 +16,7 @@ interface UpdateState {
|
|||||||
lastCheckTime: number;
|
lastCheckTime: number;
|
||||||
skipVersion: string | null;
|
skipVersion: string | null;
|
||||||
showUpdateModal: boolean;
|
showUpdateModal: boolean;
|
||||||
|
isLatestVersion: boolean; // 新增:是否已是最新版本
|
||||||
|
|
||||||
// 操作
|
// 操作
|
||||||
checkForUpdate: (silent?: boolean) => Promise<void>;
|
checkForUpdate: (silent?: boolean) => Promise<void>;
|
||||||
@@ -43,11 +45,12 @@ export const useUpdateStore = create<UpdateState>((set, get) => ({
|
|||||||
lastCheckTime: 0,
|
lastCheckTime: 0,
|
||||||
skipVersion: null,
|
skipVersion: null,
|
||||||
showUpdateModal: false,
|
showUpdateModal: false,
|
||||||
|
isLatestVersion: false, // 新增:初始为false
|
||||||
|
|
||||||
// 检查更新
|
// 检查更新
|
||||||
checkForUpdate: async (silent = false) => {
|
checkForUpdate: async (silent = false) => {
|
||||||
try {
|
try {
|
||||||
set({ error: null });
|
set({ error: null, isLatestVersion: false });
|
||||||
|
|
||||||
// 获取跳过的版本
|
// 获取跳过的版本
|
||||||
const skipVersion = await AsyncStorage.getItem(STORAGE_KEYS.SKIP_VERSION);
|
const skipVersion = await AsyncStorage.getItem(STORAGE_KEYS.SKIP_VERSION);
|
||||||
@@ -58,6 +61,9 @@ export const useUpdateStore = create<UpdateState>((set, get) => ({
|
|||||||
// 如果有更新且不是要跳过的版本
|
// 如果有更新且不是要跳过的版本
|
||||||
const shouldShowUpdate = isUpdateAvailable && versionInfo.version !== skipVersion;
|
const shouldShowUpdate = isUpdateAvailable && versionInfo.version !== skipVersion;
|
||||||
|
|
||||||
|
// 检查是否已经是最新版本
|
||||||
|
const isLatest = !isUpdateAvailable;
|
||||||
|
|
||||||
set({
|
set({
|
||||||
remoteVersion: versionInfo.version,
|
remoteVersion: versionInfo.version,
|
||||||
downloadUrl: versionInfo.downloadUrl,
|
downloadUrl: versionInfo.downloadUrl,
|
||||||
@@ -65,8 +71,19 @@ export const useUpdateStore = create<UpdateState>((set, get) => ({
|
|||||||
lastCheckTime: Date.now(),
|
lastCheckTime: Date.now(),
|
||||||
skipVersion,
|
skipVersion,
|
||||||
showUpdateModal: shouldShowUpdate && !silent,
|
showUpdateModal: shouldShowUpdate && !silent,
|
||||||
|
isLatestVersion: isLatest,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 如果是手动检查且已是最新版本,显示提示
|
||||||
|
if (!silent && isLatest) {
|
||||||
|
Toast.show({
|
||||||
|
type: 'success',
|
||||||
|
text1: '已是最新版本',
|
||||||
|
text2: `当前版本 v${updateService.getCurrentVersion()} 已是最新版本`,
|
||||||
|
visibilityTime: 3000,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// 保存最后检查时间
|
// 保存最后检查时间
|
||||||
await AsyncStorage.setItem(
|
await AsyncStorage.setItem(
|
||||||
STORAGE_KEYS.LAST_CHECK_TIME,
|
STORAGE_KEYS.LAST_CHECK_TIME,
|
||||||
@@ -76,7 +93,8 @@ export const useUpdateStore = create<UpdateState>((set, get) => ({
|
|||||||
// console.info('检查更新失败:', error);
|
// console.info('检查更新失败:', error);
|
||||||
set({
|
set({
|
||||||
error: error instanceof Error ? error.message : '检查更新失败',
|
error: error instanceof Error ? error.message : '检查更新失败',
|
||||||
updateAvailable: false
|
updateAvailable: false,
|
||||||
|
isLatestVersion: false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -166,6 +184,7 @@ export const useUpdateStore = create<UpdateState>((set, get) => ({
|
|||||||
downloadedPath: null,
|
downloadedPath: null,
|
||||||
error: null,
|
error: null,
|
||||||
showUpdateModal: false,
|
showUpdateModal: false,
|
||||||
|
isLatestVersion: false, // 重置时也要重置这个状态
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -1,39 +1,39 @@
|
|||||||
import { Platform, Dimensions } from 'react-native';
|
import { Platform, Dimensions } from "react-native";
|
||||||
import { DeviceType } from '@/hooks/useResponsiveLayout';
|
import { DeviceType } from "@/hooks/useResponsiveLayout";
|
||||||
|
|
||||||
export const DeviceUtils = {
|
export const DeviceUtils = {
|
||||||
/**
|
/**
|
||||||
* 检测当前设备类型
|
* 检测当前设备类型
|
||||||
*/
|
*/
|
||||||
getDeviceType(): DeviceType {
|
getDeviceType(): DeviceType {
|
||||||
const isTV = process.env.EXPO_TV === '1' || Platform.isTV;
|
// if (Platform.isTV) return "tv";
|
||||||
if (isTV) return 'tv';
|
|
||||||
|
const { width } = Dimensions.get("window");
|
||||||
const { width } = Dimensions.get('window');
|
|
||||||
if (width >= 1024) return 'tv';
|
if (width >= 1024) return "tv";
|
||||||
if (width >= 768) return 'tablet';
|
if (width >= 768) return "tablet";
|
||||||
return 'mobile';
|
return "mobile";
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 检测是否为TV环境
|
* 检测是否为TV环境
|
||||||
*/
|
*/
|
||||||
isTV(): boolean {
|
isTV(): boolean {
|
||||||
return this.getDeviceType() === 'tv';
|
return this.getDeviceType() === "tv";
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 检测是否为移动设备
|
* 检测是否为移动设备
|
||||||
*/
|
*/
|
||||||
isMobile(): boolean {
|
isMobile(): boolean {
|
||||||
return this.getDeviceType() === 'mobile';
|
return this.getDeviceType() === "mobile";
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 检测是否为平板设备
|
* 检测是否为平板设备
|
||||||
*/
|
*/
|
||||||
isTablet(): boolean {
|
isTablet(): boolean {
|
||||||
return this.getDeviceType() === 'tablet';
|
return this.getDeviceType() === "tablet";
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -56,11 +56,11 @@ export const DeviceUtils = {
|
|||||||
getMinTouchTargetSize(): number {
|
getMinTouchTargetSize(): number {
|
||||||
const deviceType = this.getDeviceType();
|
const deviceType = this.getDeviceType();
|
||||||
switch (deviceType) {
|
switch (deviceType) {
|
||||||
case 'mobile':
|
case "mobile":
|
||||||
return 44; // iOS HIG minimum
|
return 44; // iOS HIG minimum
|
||||||
case 'tablet':
|
case "tablet":
|
||||||
return 48; // Material Design minimum
|
return 48; // Material Design minimum
|
||||||
case 'tv':
|
case "tv":
|
||||||
return 60; // TV optimized
|
return 60; // TV optimized
|
||||||
default:
|
default:
|
||||||
return 44;
|
return 44;
|
||||||
@@ -77,7 +77,7 @@ export const DeviceUtils = {
|
|||||||
tablet: 1.1,
|
tablet: 1.1,
|
||||||
tv: 1.25,
|
tv: 1.25,
|
||||||
}[deviceType];
|
}[deviceType];
|
||||||
|
|
||||||
return Math.round(baseSize * scaleFactor);
|
return Math.round(baseSize * scaleFactor);
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -91,7 +91,7 @@ export const DeviceUtils = {
|
|||||||
tablet: 1.0,
|
tablet: 1.0,
|
||||||
tv: 1.5,
|
tv: 1.5,
|
||||||
}[deviceType];
|
}[deviceType];
|
||||||
|
|
||||||
return Math.round(baseSpacing * scaleFactor);
|
return Math.round(baseSpacing * scaleFactor);
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -99,7 +99,7 @@ export const DeviceUtils = {
|
|||||||
* 检测设备是否处于横屏模式
|
* 检测设备是否处于横屏模式
|
||||||
*/
|
*/
|
||||||
isLandscape(): boolean {
|
isLandscape(): boolean {
|
||||||
const { width, height } = Dimensions.get('window');
|
const { width, height } = Dimensions.get("window");
|
||||||
return width > height;
|
return width > height;
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -114,10 +114,10 @@ export const DeviceUtils = {
|
|||||||
* 获取安全的网格列数
|
* 获取安全的网格列数
|
||||||
*/
|
*/
|
||||||
getSafeColumnCount(preferredColumns: number): number {
|
getSafeColumnCount(preferredColumns: number): number {
|
||||||
const { width } = Dimensions.get('window');
|
const { width } = Dimensions.get("window");
|
||||||
const minCardWidth = this.isMobile() ? 120 : this.isTablet() ? 140 : 160;
|
const minCardWidth = this.isMobile() ? 120 : this.isTablet() ? 140 : 160;
|
||||||
const maxColumns = Math.floor(width / minCardWidth);
|
const maxColumns = Math.floor(width / minCardWidth);
|
||||||
|
|
||||||
return Math.min(preferredColumns, maxColumns);
|
return Math.min(preferredColumns, maxColumns);
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -132,7 +132,7 @@ export const DeviceUtils = {
|
|||||||
tablet: 1.0,
|
tablet: 1.0,
|
||||||
tv: 1.2,
|
tv: 1.2,
|
||||||
}[deviceType];
|
}[deviceType];
|
||||||
|
|
||||||
return Math.round(baseDuration * scaleFactor);
|
return Math.round(baseDuration * scaleFactor);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user