mirror of
https://gh-proxy.org/https://github.com/tiajinsha/JKVideo
synced 2026-07-07 23:18:38 +08:00
feat:热更新
This commit is contained in:
67
.github/workflows/release.yml
vendored
Normal file
67
.github/workflows/release.yml
vendored
Normal file
@@ -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
|
||||
3
app.json
3
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"
|
||||
},
|
||||
|
||||
@@ -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() {
|
||||
<View style={styles.spacer} />
|
||||
</View>
|
||||
|
||||
<View style={styles.section}>
|
||||
<Text style={styles.sectionLabel}>版本信息</Text>
|
||||
<View style={styles.versionRow}>
|
||||
<Text style={styles.versionLabel}>当前版本</Text>
|
||||
<Text style={styles.versionValue}>v{currentVersion}</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={styles.section}>
|
||||
<Text style={styles.sectionLabel}>更新</Text>
|
||||
<TouchableOpacity
|
||||
style={styles.updateBtn}
|
||||
onPress={checkUpdate}
|
||||
activeOpacity={0.7}
|
||||
disabled={isChecking || downloadProgress !== null}
|
||||
>
|
||||
{isChecking ? (
|
||||
<>
|
||||
<ActivityIndicator size="small" color="#00AEEC" style={{ marginRight: 8 }} />
|
||||
<Text style={styles.updateBtnText}>检查中...</Text>
|
||||
</>
|
||||
) : downloadProgress !== null ? (
|
||||
<Text style={styles.updateBtnText}>下载中 {downloadProgress}%</Text>
|
||||
) : (
|
||||
<Text style={styles.updateBtnText}>检查更新</Text>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<View style={styles.section}>
|
||||
<Text style={styles.sectionLabel}>封面图清晰度</Text>
|
||||
<View style={styles.optionRow}>
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
99
hooks/useCheckUpdate.ts
Normal file
99
hooks/useCheckUpdate.ts
Normal file
@@ -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<number | null>(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 };
|
||||
}
|
||||
45
package-lock.json
generated
45
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
35
scripts/bump-version.js
Normal file
35
scripts/bump-version.js
Normal file
@@ -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);
|
||||
Reference in New Issue
Block a user