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);