From 38fc41f1848fabfca55afd8e4a6be9ea60078885 Mon Sep 17 00:00:00 2001 From: Developer Date: Fri, 20 Mar 2026 14:06:52 +0800 Subject: [PATCH] =?UTF-8?q?feat:=E7=83=AD=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/release.yml | 67 ++++++++++++++++++++++++ app.json | 3 +- app/settings.tsx | 46 +++++++++++++++- app/video/[bvid].tsx | 2 +- hooks/useCheckUpdate.ts | 99 +++++++++++++++++++++++++++++++++++ package-lock.json | 45 ++++++++++------ package.json | 2 + scripts/bump-version.js | 35 +++++++++++++ 8 files changed, 278 insertions(+), 21 deletions(-) create mode 100644 .github/workflows/release.yml create mode 100644 hooks/useCheckUpdate.ts create mode 100644 scripts/bump-version.js diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..fd15f2e --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,67 @@ +name: Release APK + +on: + push: + branches: + - master + +jobs: + release: + runs-on: ubuntu-latest + if: "!contains(github.event.head_commit.message, '[skip ci]')" + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Setup Node 20 + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Bump version + id: bump + run: | + NEW_VERSION=$(node scripts/bump-version.js) + echo "version=$NEW_VERSION" >> $GITHUB_OUTPUT + + - name: Commit version bump + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git add app.json package.json android/app/build.gradle + git commit -m "chore: bump version to v${{ steps.bump.outputs.version }} [skip ci]" + git push + + - name: Setup Java 17 + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '17' + + - name: Setup Android SDK + uses: android-actions/setup-android@v3 + + - name: Grant Gradle execute permission + run: chmod +x android/gradlew + + - name: Build Release APK + run: | + cd android + ./gradlew assembleRelease --no-daemon + + - name: Create GitHub Release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh release create "v${{ steps.bump.outputs.version }}" \ + --title "JKVideo v${{ steps.bump.outputs.version }}" \ + --generate-notes \ + android/app/build/outputs/apk/release/app-release.apk diff --git a/app.json b/app.json index 35ab5a3..c5d1774 100644 --- a/app.json +++ b/app.json @@ -26,8 +26,7 @@ "permissions": [ "android.permission.RECORD_AUDIO", "android.permission.MODIFY_AUDIO_SETTINGS", - "android.permission.RECORD_AUDIO", - "android.permission.MODIFY_AUDIO_SETTINGS" + "android.permission.REQUEST_INSTALL_PACKAGES" ], "package": "com.anonymous.jkvideo" }, diff --git a/app/settings.tsx b/app/settings.tsx index 99e1399..12a4269 100644 --- a/app/settings.tsx +++ b/app/settings.tsx @@ -1,15 +1,17 @@ import React from 'react'; -import { View, Text, TouchableOpacity, StyleSheet } from 'react-native'; +import { View, Text, TouchableOpacity, StyleSheet, ActivityIndicator } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; import { useRouter } from 'expo-router'; import { Ionicons } from '@expo/vector-icons'; import { useAuthStore } from '../store/authStore'; import { useSettingsStore } from '../store/settingsStore'; +import { useCheckUpdate } from '../hooks/useCheckUpdate'; export default function SettingsScreen() { const router = useRouter(); const { isLoggedIn, logout } = useAuthStore(); const { coverQuality, setCoverQuality } = useSettingsStore(); + const { currentVersion, isChecking, downloadProgress, checkUpdate } = useCheckUpdate(); const handleLogout = async () => { await logout(); @@ -26,6 +28,35 @@ export default function SettingsScreen() { + + 版本信息 + + 当前版本 + v{currentVersion} + + + + + 更新 + + {isChecking ? ( + <> + + 检查中... + + ) : downloadProgress !== null ? ( + 下载中 {downloadProgress}% + ) : ( + 检查更新 + )} + + + 封面图清晰度 @@ -102,6 +133,19 @@ const styles = StyleSheet.create({ optionActive: { borderColor: '#00AEEC', backgroundColor: '#e8f7fd' }, optionText: { fontSize: 14, color: '#666' }, optionTextActive: { color: '#00AEEC', fontWeight: '600' }, + versionRow: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + }, + versionLabel: { fontSize: 14, color: '#212121' }, + versionValue: { fontSize: 14, color: '#999' }, + updateBtn: { + flexDirection: 'row', + alignItems: 'center', + paddingVertical: 6, + }, + updateBtnText: { fontSize: 14, color: '#00AEEC', fontWeight: '600' }, logoutBtn: { margin: 24, paddingVertical: 12, diff --git a/app/video/[bvid].tsx b/app/video/[bvid].tsx index 4789245..574bf78 100644 --- a/app/video/[bvid].tsx +++ b/app/video/[bvid].tsx @@ -460,7 +460,7 @@ const styles = StyleSheet.create({ paddingBottom: 0, paddingTop: 12, }, - avatar: { width: 48, height: 48, borderRadius: 19, marginRight: 10 }, + avatar: { width: 48, height: 48, borderRadius:30, marginRight: 10 }, upName: { flex: 1, fontSize: 14, color: "#212121", fontWeight: "500" }, followBtn: { backgroundColor: "#00AEEC", diff --git a/hooks/useCheckUpdate.ts b/hooks/useCheckUpdate.ts new file mode 100644 index 0000000..b04ef9f --- /dev/null +++ b/hooks/useCheckUpdate.ts @@ -0,0 +1,99 @@ +import { useState } from 'react'; +import { Alert, Platform } from 'react-native'; +import * as FileSystem from 'expo-file-system'; +import * as IntentLauncher from 'expo-intent-launcher'; +import Constants from 'expo-constants'; + +const GITHUB_API = 'https://api.github.com/repos/tiajinsha/JKVideo/releases/latest'; + +function compareVersions(a: string, b: string): number { + const pa = a.replace(/^v/, '').split('.').map(Number); + const pb = b.replace(/^v/, '').split('.').map(Number); + for (let i = 0; i < 3; i++) { + if ((pa[i] ?? 0) > (pb[i] ?? 0)) return 1; + if ((pa[i] ?? 0) < (pb[i] ?? 0)) return -1; + } + return 0; +} + +export function useCheckUpdate() { + const currentVersion = Constants.expoConfig?.version ?? '0.0.0'; + const [isChecking, setIsChecking] = useState(false); + const [downloadProgress, setDownloadProgress] = useState(null); + + const checkUpdate = async () => { + setIsChecking(true); + try { + const res = await fetch(GITHUB_API, { + headers: { Accept: 'application/vnd.github+json' }, + }); + if (!res.ok) throw new Error(`GitHub API ${res.status}`); + const data = await res.json(); + + const latestVersion: string = data.tag_name ?? ''; + const apkAsset = (data.assets as any[]).find((a) => + (a.name as string).endsWith('.apk') + ); + const downloadUrl: string = apkAsset?.browser_download_url ?? ''; + const releaseNotes: string = data.body ?? ''; + + if (compareVersions(latestVersion, currentVersion) <= 0) { + Alert.alert('已是最新版本', `当前版本 v${currentVersion} 已是最新`); + return; + } + + Alert.alert( + `发现新版本 ${latestVersion}`, + releaseNotes || '有新版本可用,是否立即下载?', + [ + { text: '取消', style: 'cancel' }, + { + text: '下载安装', + onPress: () => downloadAndInstall(downloadUrl, latestVersion), + }, + ] + ); + } catch (e: any) { + Alert.alert('检查失败', e?.message ?? '网络错误,请稍后重试'); + } finally { + setIsChecking(false); + } + }; + + const downloadAndInstall = async (url: string, version: string) => { + if (Platform.OS !== 'android') { + Alert.alert('提示', '自动安装仅支持 Android 设备'); + return; + } + const localUri = FileSystem.cacheDirectory + `JKVideo-${version}.apk`; + try { + setDownloadProgress(0); + const downloadResumable = FileSystem.createDownloadResumable( + url, + localUri, + {}, + ({ totalBytesWritten, totalBytesExpectedToWrite }) => { + if (totalBytesExpectedToWrite > 0) { + setDownloadProgress( + Math.round((totalBytesWritten / totalBytesExpectedToWrite) * 100) + ); + } + } + ); + await downloadResumable.downloadAsync(); + setDownloadProgress(null); + + const contentUri = await FileSystem.getContentUriAsync(localUri); + await IntentLauncher.startActivityAsync('android.intent.action.VIEW', { + data: contentUri, + flags: 1, + type: 'application/vnd.android.package-archive', + }); + } catch (e: any) { + setDownloadProgress(null); + Alert.alert('下载失败', e?.message ?? '请稍后重试'); + } + }; + + return { currentVersion, isChecking, downloadProgress, checkUpdate }; +} diff --git a/package-lock.json b/package-lock.json index 68f2430..9e597df 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,11 +1,11 @@ { - "name": "reactbilibiliapp", + "name": "jkvideo", "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "reactbilibiliapp", + "name": "jkvideo", "version": "1.0.0", "dependencies": { "@dr.pogodin/react-native-static-server": "^0.26.0", @@ -15,8 +15,10 @@ "expo": "~55.0.5", "expo-av": "^16.0.8", "expo-clipboard": "~55.0.9", + "expo-constants": "~55.0.9", "expo-dev-client": "~55.0.11", "expo-file-system": "~55.0.10", + "expo-intent-launcher": "~55.0.9", "expo-linear-gradient": "~55.0.8", "expo-media-library": "~55.0.10", "expo-network": "~55.0.9", @@ -1532,15 +1534,15 @@ } }, "node_modules/@expo/config": { - "version": "55.0.8", - "resolved": "https://registry.npmjs.org/@expo/config/-/config-55.0.8.tgz", - "integrity": "sha512-D7RYYHfErCgEllGxNwdYdkgzLna7zkzUECBV3snbUpf7RvIpB5l1LpCgzuVoc5KVew5h7N1Tn4LnT/tBSUZsQg==", + "version": "55.0.10", + "resolved": "https://registry.npmjs.org/@expo/config/-/config-55.0.10.tgz", + "integrity": "sha512-qCHxo9H1ZoeW+y0QeMtVZ3JfGmumpGrgUFX60wLWMarraoQZSe47ZUm9kJSn3iyoPjUtUNanO3eXQg+K8k4rag==", "license": "MIT", "dependencies": { - "@expo/config-plugins": "~55.0.6", + "@expo/config-plugins": "~55.0.7", "@expo/config-types": "^55.0.5", "@expo/json-file": "^10.0.12", - "@expo/require-utils": "^55.0.2", + "@expo/require-utils": "^55.0.3", "deepmerge": "^4.3.1", "getenv": "^2.0.0", "glob": "^13.0.0", @@ -1551,9 +1553,9 @@ } }, "node_modules/@expo/config-plugins": { - "version": "55.0.6", - "resolved": "https://registry.npmjs.org/@expo/config-plugins/-/config-plugins-55.0.6.tgz", - "integrity": "sha512-cIox6FjZlFaaX40rbQ3DvP9e87S5X85H9uw+BAxJE5timkMhuByy3GAlOsj1h96EyzSiol7Q6YIGgY1Jiz4M+A==", + "version": "55.0.7", + "resolved": "https://registry.npmjs.org/@expo/config-plugins/-/config-plugins-55.0.7.tgz", + "integrity": "sha512-XZUoDWrsHEkH3yasnDSJABM/UxP5a1ixzRwU/M+BToyn/f0nTrSJJe/Ay/FpxkI4JSNz2n0e06I23b2bleXKVA==", "license": "MIT", "dependencies": { "@expo/config-types": "^55.0.5", @@ -1798,9 +1800,9 @@ } }, "node_modules/@expo/require-utils": { - "version": "55.0.2", - "resolved": "https://registry.npmjs.org/@expo/require-utils/-/require-utils-55.0.2.tgz", - "integrity": "sha512-dV5oCShQ1umKBKagMMT4B/N+SREsQe3lU4Zgmko5AO0rxKV0tynZT6xXs+e2JxuqT4Rz997atg7pki0BnZb4uw==", + "version": "55.0.3", + "resolved": "https://registry.npmjs.org/@expo/require-utils/-/require-utils-55.0.3.tgz", + "integrity": "sha512-TS1m5tW45q4zoaTlt6DwmdYHxvFTIxoLrTHKOFrIirHIqIXnHCzpceg8wumiBi+ZXSaGY2gobTbfv+WVhJY6Fw==", "license": "MIT", "dependencies": { "@babel/code-frame": "^7.20.0", @@ -4451,12 +4453,12 @@ } }, "node_modules/expo-constants": { - "version": "55.0.7", - "resolved": "https://registry.npmjs.org/expo-constants/-/expo-constants-55.0.7.tgz", - "integrity": "sha512-kdcO4TsQRRqt0USvjaY5vgQMO9H52K3kBZ/ejC7F6rz70mv08GoowrZ1CYOr5O4JpPDRlIpQfZJUucaS/c+KWQ==", + "version": "55.0.9", + "resolved": "https://registry.npmjs.org/expo-constants/-/expo-constants-55.0.9.tgz", + "integrity": "sha512-iBiXjZeuU5S/8docQeNzsVvtDy4w0zlmXBpFEi1ypwugceEpdQQab65TVRbusXAcwpNVxCPMpNlDssYp0Pli2g==", "license": "MIT", "dependencies": { - "@expo/config": "~55.0.8", + "@expo/config": "~55.0.10", "@expo/env": "~2.1.1" }, "peerDependencies": { @@ -4570,6 +4572,15 @@ } } }, + "node_modules/expo-intent-launcher": { + "version": "55.0.9", + "resolved": "https://registry.npmjs.org/expo-intent-launcher/-/expo-intent-launcher-55.0.9.tgz", + "integrity": "sha512-s8k8dF8PfgzN0c+xzNTd9ceHh+/6mt2ncnMuqFaIIry2BeDGITbsgVijHr39eB5vS+KopCcOYYBYr+O1epx4TA==", + "license": "MIT", + "peerDependencies": { + "expo": "*" + } + }, "node_modules/expo-json-utils": { "version": "55.0.0", "resolved": "https://registry.npmjs.org/expo-json-utils/-/expo-json-utils-55.0.0.tgz", diff --git a/package.json b/package.json index a2211df..7623904 100644 --- a/package.json +++ b/package.json @@ -17,8 +17,10 @@ "expo": "~55.0.5", "expo-av": "^16.0.8", "expo-clipboard": "~55.0.9", + "expo-constants": "~55.0.9", "expo-dev-client": "~55.0.11", "expo-file-system": "~55.0.10", + "expo-intent-launcher": "~55.0.9", "expo-linear-gradient": "~55.0.8", "expo-media-library": "~55.0.10", "expo-network": "~55.0.9", diff --git a/scripts/bump-version.js b/scripts/bump-version.js new file mode 100644 index 0000000..c8e0273 --- /dev/null +++ b/scripts/bump-version.js @@ -0,0 +1,35 @@ +#!/usr/bin/env node +const fs = require('fs'); +const path = require('path'); + +const root = path.resolve(__dirname, '..'); + +// Read app.json +const appJsonPath = path.join(root, 'app.json'); +const appJson = JSON.parse(fs.readFileSync(appJsonPath, 'utf8')); +const currentVersion = appJson.expo.version; + +// Parse semver +const parts = currentVersion.split('.').map(Number); +parts[2] += 1; +const newVersion = parts.join('.'); +const newVersionCode = parts[0] * 10000 + parts[1] * 100 + parts[2]; + +// Update app.json +appJson.expo.version = newVersion; +fs.writeFileSync(appJsonPath, JSON.stringify(appJson, null, 2) + '\n'); + +// Update package.json +const pkgJsonPath = path.join(root, 'package.json'); +const pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf8')); +pkgJson.version = newVersion; +fs.writeFileSync(pkgJsonPath, JSON.stringify(pkgJson, null, 2) + '\n'); + +// Update android/app/build.gradle +const gradlePath = path.join(root, 'android', 'app', 'build.gradle'); +let gradle = fs.readFileSync(gradlePath, 'utf8'); +gradle = gradle.replace(/versionCode\s+\d+/, `versionCode ${newVersionCode}`); +gradle = gradle.replace(/versionName\s+"[^"]+"/, `versionName "${newVersion}"`); +fs.writeFileSync(gradlePath, gradle); + +console.log(newVersion);