feat(updater): 增加应用自动更新入口 (#84)
H-Chris233: closes #33。 实现 Tauri 内置 updater 集成: - Cargo: tauri-plugin-updater v2 - tauri.conf.json: 加 updater 配置(pubkey + endpoint 指向 GitHub Releases latest) - capabilities/default.json: 授予 updater:default 能力 - lib.rs: 注册 updater 插件 + restart_app 命令 - src/pages/Settings.tsx + i18n: 关于区域加'检查'按钮、检查/下载/安装/重启对话框流 - 新脚本 scripts/write-updater-manifest.mjs: release 侧从 build artifact 拉签名生成 manifest - workflow: TAURI_SIGNING_PRIVATE_KEY env + 各平台 build 时 createUpdaterArtifacts true 注意:下次 v*-tauri tag 必须配 TAURI_SIGNING_PRIVATE_KEY secret,否则 CI 硬失败。
This commit is contained in:
1 parent
786bc574a7
commit
a9c81e6e0e
16 files changed
+695
-31
No files matched your search
@@ -30,11 +30,20 @@ jobs:
|
||||
include:
|
||||
- platform: macos-latest
|
||||
rust-target: aarch64-apple-darwin
|
||||
updater-target: darwin
|
||||
updater-arch: aarch64
|
||||
- platform: windows-latest
|
||||
rust-target: x86_64-pc-windows-msvc
|
||||
updater-target: windows
|
||||
updater-arch: x86_64
|
||||
- platform: ubuntu-22.04
|
||||
rust-target: x86_64-unknown-linux-gnu
|
||||
updater-target: linux
|
||||
updater-arch: x86_64
|
||||
runs-on: ${{ matrix.platform }}
|
||||
env:
|
||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
@@ -76,6 +85,17 @@ jobs:
|
||||
working-directory: 'openless-all/app'
|
||||
run: npm ci
|
||||
|
||||
- name: Check updater signing availability
|
||||
if: startsWith(github.ref, 'refs/tags/v') && endsWith(github.ref, '-tauri')
|
||||
shell: bash
|
||||
env:
|
||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
||||
run: |
|
||||
if [ -z "${TAURI_SIGNING_PRIVATE_KEY:-}" ]; then
|
||||
echo "::error::TAURI_SIGNING_PRIVATE_KEY is required for signed auto-update artifacts."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Check Apple signing availability
|
||||
if: matrix.platform == 'macos-latest' && startsWith(github.ref, 'refs/tags/v') && endsWith(github.ref, '-tauri')
|
||||
shell: bash
|
||||
@@ -159,19 +179,50 @@ jobs:
|
||||
working-directory: 'openless-all/app'
|
||||
env:
|
||||
INSTALL: '0' # CI 不要装到 /Applications,也不要 reset TCC
|
||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
|
||||
run: bash scripts/build-mac.sh
|
||||
|
||||
# ── Windows:仅 Windows 跑通用 tauri build(Linux 用下方专用 step,避免重复构建)──
|
||||
- name: Build (Windows)
|
||||
if: matrix.platform == 'windows-latest'
|
||||
shell: bash
|
||||
working-directory: 'openless-all/app'
|
||||
run: npm run tauri build
|
||||
env:
|
||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
|
||||
run: |
|
||||
if [ -n "${TAURI_SIGNING_PRIVATE_KEY:-}" ]; then
|
||||
npm run tauri -- build --config '{"bundle":{"createUpdaterArtifacts":true}}'
|
||||
else
|
||||
npm run tauri build
|
||||
fi
|
||||
|
||||
# ── Linux:产 deb / rpm / AppImage ──
|
||||
- name: Build (Linux)
|
||||
if: matrix.platform == 'ubuntu-22.04'
|
||||
shell: bash
|
||||
working-directory: 'openless-all/app'
|
||||
run: npm run tauri -- build --bundles deb,rpm,appimage
|
||||
env:
|
||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
|
||||
run: |
|
||||
if [ -n "${TAURI_SIGNING_PRIVATE_KEY:-}" ]; then
|
||||
npm run tauri -- build --bundles deb,rpm,appimage --config '{"bundle":{"createUpdaterArtifacts":true}}'
|
||||
else
|
||||
npm run tauri -- build --bundles deb,rpm,appimage
|
||||
fi
|
||||
|
||||
- name: Write updater manifest
|
||||
if: env.TAURI_SIGNING_PRIVATE_KEY != ''
|
||||
shell: bash
|
||||
working-directory: 'openless-all/app'
|
||||
env:
|
||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
||||
OPENLESS_UPDATE_TARGET: ${{ matrix.updater-target }}
|
||||
OPENLESS_UPDATE_ARCH: ${{ matrix.updater-arch }}
|
||||
OPENLESS_UPDATE_REPO: appergb/openless
|
||||
run: node scripts/write-updater-manifest.mjs
|
||||
|
||||
# ── 收集产物 ──
|
||||
- name: List artifacts (debug)
|
||||
@@ -206,6 +257,17 @@ jobs:
|
||||
openless-all/app/src-tauri/target/release/bundle/dmg/*.dmg
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload macOS updater artifacts
|
||||
if: matrix.platform == 'macos-latest' && env.TAURI_SIGNING_PRIVATE_KEY != ''
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: openless-macos-arm64-updater
|
||||
path: |
|
||||
openless-all/app/src-tauri/target/release/bundle/macos/*.app.tar.gz
|
||||
openless-all/app/src-tauri/target/release/bundle/macos/*.app.tar.gz.sig
|
||||
openless-all/app/src-tauri/target/release/bundle/latest-darwin-aarch64.json
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload Windows artifacts
|
||||
if: matrix.platform == 'windows-latest'
|
||||
uses: actions/upload-artifact@v4
|
||||
@@ -216,6 +278,17 @@ jobs:
|
||||
openless-all/app/src-tauri/target/release/bundle/msi/*.msi
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload Windows updater artifacts
|
||||
if: matrix.platform == 'windows-latest' && env.TAURI_SIGNING_PRIVATE_KEY != ''
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: openless-windows-x64-updater
|
||||
path: |
|
||||
openless-all/app/src-tauri/target/release/bundle/nsis/*.exe.sig
|
||||
openless-all/app/src-tauri/target/release/bundle/msi/*.msi.sig
|
||||
openless-all/app/src-tauri/target/release/bundle/latest-windows-x86_64.json
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload Linux artifacts
|
||||
if: matrix.platform == 'ubuntu-22.04'
|
||||
uses: actions/upload-artifact@v4
|
||||
@@ -227,6 +300,16 @@ jobs:
|
||||
openless-all/app/src-tauri/target/release/bundle/appimage/*.AppImage
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload Linux updater artifacts
|
||||
if: matrix.platform == 'ubuntu-22.04' && env.TAURI_SIGNING_PRIVATE_KEY != ''
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: openless-linux-x64-updater
|
||||
path: |
|
||||
openless-all/app/src-tauri/target/release/bundle/appimage/*.AppImage.sig
|
||||
openless-all/app/src-tauri/target/release/bundle/latest-linux-x86_64.json
|
||||
if-no-files-found: error
|
||||
|
||||
# ── tag 推送时,同步上传到 GitHub Release ──
|
||||
- name: Create / update release
|
||||
if: startsWith(github.ref, 'refs/tags/v') && endsWith(github.ref, '-tauri')
|
||||
@@ -239,8 +322,14 @@ jobs:
|
||||
generate_release_notes: true
|
||||
files: |
|
||||
openless-all/app/src-tauri/target/release/bundle/dmg/*.dmg
|
||||
openless-all/app/src-tauri/target/release/bundle/macos/*.app.tar.gz
|
||||
openless-all/app/src-tauri/target/release/bundle/macos/*.app.tar.gz.sig
|
||||
openless-all/app/src-tauri/target/release/bundle/nsis/*.exe
|
||||
openless-all/app/src-tauri/target/release/bundle/nsis/*.exe.sig
|
||||
openless-all/app/src-tauri/target/release/bundle/msi/*.msi
|
||||
openless-all/app/src-tauri/target/release/bundle/msi/*.msi.sig
|
||||
openless-all/app/src-tauri/target/release/bundle/deb/*.deb
|
||||
openless-all/app/src-tauri/target/release/bundle/rpm/*.rpm
|
||||
openless-all/app/src-tauri/target/release/bundle/appimage/*.AppImage
|
||||
openless-all/app/src-tauri/target/release/bundle/appimage/*.AppImage.sig
|
||||
openless-all/app/src-tauri/target/release/bundle/latest-*.json
|
||||
Generated
+12
-2
@@ -1,15 +1,16 @@
|
||||
{
|
||||
"name": "openless-app",
|
||||
"version": "1.1.4",
|
||||
"version": "1.2.2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "openless-app",
|
||||
"version": "1.1.4",
|
||||
"version": "1.2.2",
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^2.1.1",
|
||||
"@tauri-apps/plugin-shell": "^2.0.1",
|
||||
"@tauri-apps/plugin-updater": "^2.10.1",
|
||||
"i18next": "^26.0.8",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
@@ -1349,6 +1350,15 @@
|
||||
"@tauri-apps/api": "^2.10.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/plugin-updater": {
|
||||
"version": "2.10.1",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-updater/-/plugin-updater-2.10.1.tgz",
|
||||
"integrity": "sha512-NFYMg+tWOZPJdzE/PpFj2qfqwAWwNS3kXrb1tm1gnBJ9mYzZ4WDRrwy8udzWoAnfGCHLuePNLY1WVCNHnh3eRA==",
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^2.10.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/babel__core": {
|
||||
"version": "7.20.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^2.1.1",
|
||||
"@tauri-apps/plugin-shell": "^2.0.1",
|
||||
"@tauri-apps/plugin-updater": "^2.10.1",
|
||||
"i18next": "^26.0.8",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
|
||||
@@ -25,7 +25,11 @@ else
|
||||
fi
|
||||
|
||||
echo "▶ tauri build"
|
||||
npm run tauri build
|
||||
TAURI_BUILD_ARGS=(build)
|
||||
if [ -n "${TAURI_SIGNING_PRIVATE_KEY:-}" ] || [ -n "${TAURI_SIGNING_PRIVATE_KEY_PATH:-}" ]; then
|
||||
TAURI_BUILD_ARGS+=(--config '{"bundle":{"createUpdaterArtifacts":true}}')
|
||||
fi
|
||||
npm run tauri -- "${TAURI_BUILD_ARGS[@]}"
|
||||
|
||||
echo "▶ 校验 Info.plist / 签名"
|
||||
/usr/libexec/PlistBuddy -c "Print :NSMicrophoneUsageDescription" "$INFO" >/dev/null
|
||||
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
#!/usr/bin/env node
|
||||
import { existsSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
|
||||
import { basename, join } from 'node:path';
|
||||
import process from 'node:process';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const target = process.env.OPENLESS_UPDATE_TARGET;
|
||||
const arch = process.env.OPENLESS_UPDATE_ARCH;
|
||||
const repo = process.env.OPENLESS_UPDATE_REPO || 'appergb/openless';
|
||||
|
||||
if (!target || !arch) {
|
||||
throw new Error('OPENLESS_UPDATE_TARGET and OPENLESS_UPDATE_ARCH are required');
|
||||
}
|
||||
|
||||
const packageJson = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
|
||||
const bundleDir = fileURLToPath(new URL('../src-tauri/target/release/bundle/', import.meta.url));
|
||||
|
||||
const candidatesByTarget = {
|
||||
darwin: ['macos/OpenLess.app.tar.gz'],
|
||||
windows: ['nsis/OpenLess_*_x64-setup.exe', 'nsis/OpenLess*_x64-setup.exe'],
|
||||
linux: ['appimage/OpenLess_*.AppImage', 'appimage/OpenLess*.AppImage'],
|
||||
};
|
||||
|
||||
function findFirst(patterns) {
|
||||
for (const pattern of patterns) {
|
||||
if (!pattern.includes('*')) {
|
||||
const path = join(bundleDir, pattern);
|
||||
if (existsSync(path)) return path;
|
||||
continue;
|
||||
}
|
||||
const [dir, namePattern] = pattern.split('/');
|
||||
const dirPath = join(bundleDir, dir);
|
||||
if (!existsSync(dirPath)) continue;
|
||||
const prefix = namePattern.split('*')[0];
|
||||
const suffix = namePattern.split('*').at(-1);
|
||||
const match = readdirSync(dirPath)
|
||||
.filter(name => name.startsWith(prefix) && name.endsWith(suffix))
|
||||
.sort()[0];
|
||||
if (match) return join(dirPath, match);
|
||||
}
|
||||
}
|
||||
|
||||
const artifact = findFirst(candidatesByTarget[target] || []);
|
||||
if (!artifact) {
|
||||
throw new Error(`No updater artifact found for ${target} in ${bundleDir}`);
|
||||
}
|
||||
|
||||
const signaturePath = `${artifact}.sig`;
|
||||
if (!existsSync(signaturePath)) {
|
||||
throw new Error(`Missing updater signature: ${signaturePath}`);
|
||||
}
|
||||
|
||||
const assetName = basename(artifact);
|
||||
const manifestName = `latest-${target}-${arch}.json`;
|
||||
const manifest = {
|
||||
version: packageJson.version,
|
||||
pub_date: new Date().toISOString(),
|
||||
url: `https://github.com/${repo}/releases/latest/download/${assetName}`,
|
||||
signature: readFileSync(signaturePath, 'utf8').trim(),
|
||||
};
|
||||
|
||||
writeFileSync(join(bundleDir, manifestName), `${JSON.stringify(manifest, null, 2)}\n`);
|
||||
console.log(`Wrote ${manifestName} for ${assetName}`);
|
||||
Generated
+245
-9
@@ -119,6 +119,15 @@ version = "1.0.102"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
|
||||
|
||||
[[package]]
|
||||
name = "arbitrary"
|
||||
version = "1.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1"
|
||||
dependencies = [
|
||||
"derive_arbitrary",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "arboard"
|
||||
version = "3.6.1"
|
||||
@@ -858,7 +867,7 @@ dependencies = [
|
||||
"core-foundation-sys 0.8.7",
|
||||
"coreaudio-rs",
|
||||
"dasp_sample",
|
||||
"jni",
|
||||
"jni 0.21.1",
|
||||
"js-sys",
|
||||
"libc",
|
||||
"mach2",
|
||||
@@ -1026,6 +1035,17 @@ dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_arbitrary"
|
||||
version = "1.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_more"
|
||||
version = "0.99.20"
|
||||
@@ -1375,6 +1395,17 @@ dependencies = [
|
||||
"rustc_version",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "filetime"
|
||||
version = "0.2.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f98844151eee8917efc50bd9e8318cb963ae8b297431495d3f758616ea5c57db"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"libredox",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "find-msvc-tools"
|
||||
version = "0.1.9"
|
||||
@@ -2397,6 +2428,36 @@ dependencies = [
|
||||
"windows-sys 0.45.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jni"
|
||||
version = "0.22.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"combine",
|
||||
"jni-macros",
|
||||
"jni-sys 0.4.1",
|
||||
"log",
|
||||
"simd_cesu8",
|
||||
"thiserror 2.0.18",
|
||||
"walkdir",
|
||||
"windows-link 0.2.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jni-macros"
|
||||
version = "0.22.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rustc_version",
|
||||
"simd_cesu8",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jni-sys"
|
||||
version = "0.3.1"
|
||||
@@ -2560,7 +2621,10 @@ version = "0.1.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e02f3bb43d335493c96bf3fd3a321600bf6bd07ed34bc64118e9293bdffea46c"
|
||||
dependencies = [
|
||||
"bitflags 2.11.1",
|
||||
"libc",
|
||||
"plain",
|
||||
"redox_syscall 0.7.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2708,6 +2772,12 @@ version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
|
||||
|
||||
[[package]]
|
||||
name = "minisign-verify"
|
||||
version = "0.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "22f9645cb765ea72b8111f36c522475d2daa0d22c957a9826437e97534bc4e9e"
|
||||
|
||||
[[package]]
|
||||
name = "miniz_oxide"
|
||||
version = "0.8.9"
|
||||
@@ -3040,6 +3110,7 @@ checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272"
|
||||
dependencies = [
|
||||
"bitflags 2.11.1",
|
||||
"block2 0.6.2",
|
||||
"libc",
|
||||
"objc2 0.6.4",
|
||||
"objc2-core-foundation",
|
||||
]
|
||||
@@ -3067,6 +3138,18 @@ dependencies = [
|
||||
"objc2-foundation 0.2.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-osa-kit"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f112d1746737b0da274ef79a23aac283376f335f4095a083a267a082f21db0c0"
|
||||
dependencies = [
|
||||
"bitflags 2.11.1",
|
||||
"objc2 0.6.4",
|
||||
"objc2-app-kit 0.3.2",
|
||||
"objc2-foundation 0.3.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-quartz-core"
|
||||
version = "0.2.2"
|
||||
@@ -3124,7 +3207,7 @@ version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e8b61bebd49e5d43f5f8cc7ee2891c16e0f41ec7954d36bcb6c14c5e0de867fb"
|
||||
dependencies = [
|
||||
"jni",
|
||||
"jni 0.21.1",
|
||||
"ndk 0.8.0",
|
||||
"ndk-context",
|
||||
"num-derive",
|
||||
@@ -3196,6 +3279,7 @@ dependencies = [
|
||||
"tauri-build",
|
||||
"tauri-plugin-shell",
|
||||
"tauri-plugin-single-instance",
|
||||
"tauri-plugin-updater",
|
||||
"thiserror 1.0.69",
|
||||
"tokio",
|
||||
"tokio-tungstenite",
|
||||
@@ -3237,6 +3321,20 @@ dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "osakit"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "732c71caeaa72c065bb69d7ea08717bd3f4863a4f451402fc9513e29dbd5261b"
|
||||
dependencies = [
|
||||
"objc2 0.6.4",
|
||||
"objc2-foundation 0.3.2",
|
||||
"objc2-osa-kit",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror 2.0.18",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pango"
|
||||
version = "0.18.3"
|
||||
@@ -3286,7 +3384,7 @@ checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"redox_syscall",
|
||||
"redox_syscall 0.5.18",
|
||||
"smallvec",
|
||||
"windows-link 0.2.1",
|
||||
]
|
||||
@@ -3513,6 +3611,12 @@ version = "0.3.33"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e"
|
||||
|
||||
[[package]]
|
||||
name = "plain"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6"
|
||||
|
||||
[[package]]
|
||||
name = "plist"
|
||||
version = "1.9.0"
|
||||
@@ -3927,6 +4031,15 @@ dependencies = [
|
||||
"bitflags 2.11.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.7.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f450ad9c3b1da563fb6948a8e0fb0fb9269711c9c73d9ea1de5058c79c8d643a"
|
||||
dependencies = [
|
||||
"bitflags 2.11.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_users"
|
||||
version = "0.5.2"
|
||||
@@ -4041,15 +4154,20 @@ dependencies = [
|
||||
"http-body",
|
||||
"http-body-util",
|
||||
"hyper",
|
||||
"hyper-rustls",
|
||||
"hyper-util",
|
||||
"js-sys",
|
||||
"log",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"rustls",
|
||||
"rustls-pki-types",
|
||||
"rustls-platform-verifier",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sync_wrapper",
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
"tokio-util",
|
||||
"tower",
|
||||
"tower-http",
|
||||
@@ -4139,6 +4257,33 @@ dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-platform-verifier"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0"
|
||||
dependencies = [
|
||||
"core-foundation 0.10.1",
|
||||
"core-foundation-sys 0.8.7",
|
||||
"jni 0.22.4",
|
||||
"log",
|
||||
"once_cell",
|
||||
"rustls",
|
||||
"rustls-native-certs",
|
||||
"rustls-platform-verifier-android",
|
||||
"rustls-webpki",
|
||||
"security-framework",
|
||||
"security-framework-sys",
|
||||
"webpki-root-certs",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-platform-verifier-android"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f"
|
||||
|
||||
[[package]]
|
||||
name = "rustls-webpki"
|
||||
version = "0.103.13"
|
||||
@@ -4562,6 +4707,22 @@ version = "0.3.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214"
|
||||
|
||||
[[package]]
|
||||
name = "simd_cesu8"
|
||||
version = "1.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33"
|
||||
dependencies = [
|
||||
"rustc_version",
|
||||
"simdutf8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "simdutf8"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"
|
||||
|
||||
[[package]]
|
||||
name = "simplelog"
|
||||
version = "0.12.2"
|
||||
@@ -4622,7 +4783,7 @@ dependencies = [
|
||||
"objc2-foundation 0.3.2",
|
||||
"objc2-quartz-core 0.3.2",
|
||||
"raw-window-handle",
|
||||
"redox_syscall",
|
||||
"redox_syscall 0.5.18",
|
||||
"tracing",
|
||||
"wasm-bindgen",
|
||||
"web-sys",
|
||||
@@ -4805,7 +4966,7 @@ dependencies = [
|
||||
"gdkwayland-sys",
|
||||
"gdkx11-sys",
|
||||
"gtk",
|
||||
"jni",
|
||||
"jni 0.21.1",
|
||||
"libc",
|
||||
"log",
|
||||
"ndk 0.9.0",
|
||||
@@ -4837,6 +4998,17 @@ dependencies = [
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tar"
|
||||
version = "0.4.45"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "22692a6476a21fa75fdfc11d452fda482af402c008cdbaf3476414e122040973"
|
||||
dependencies = [
|
||||
"filetime",
|
||||
"libc",
|
||||
"xattr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "target-lexicon"
|
||||
version = "0.12.16"
|
||||
@@ -4860,7 +5032,7 @@ dependencies = [
|
||||
"gtk",
|
||||
"heck 0.5.0",
|
||||
"http",
|
||||
"jni",
|
||||
"jni 0.21.1",
|
||||
"libc",
|
||||
"log",
|
||||
"mime",
|
||||
@@ -5010,6 +5182,39 @@ dependencies = [
|
||||
"zbus",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-updater"
|
||||
version = "2.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "806d9dac662c2e4594ff03c647a552f2c9bd544e7d0f683ec58f872f952ce4af"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"dirs",
|
||||
"flate2",
|
||||
"futures-util",
|
||||
"http",
|
||||
"infer",
|
||||
"log",
|
||||
"minisign-verify",
|
||||
"osakit",
|
||||
"percent-encoding",
|
||||
"reqwest 0.13.3",
|
||||
"rustls",
|
||||
"semver",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tar",
|
||||
"tauri",
|
||||
"tauri-plugin",
|
||||
"tempfile",
|
||||
"thiserror 2.0.18",
|
||||
"time",
|
||||
"tokio",
|
||||
"url",
|
||||
"windows-sys 0.60.2",
|
||||
"zip",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-runtime"
|
||||
version = "2.10.1"
|
||||
@@ -5020,7 +5225,7 @@ dependencies = [
|
||||
"dpi",
|
||||
"gtk",
|
||||
"http",
|
||||
"jni",
|
||||
"jni 0.21.1",
|
||||
"objc2 0.6.4",
|
||||
"objc2-ui-kit",
|
||||
"objc2-web-kit",
|
||||
@@ -5043,7 +5248,7 @@ checksum = "e11ea2e6f801d275fdd890d6c9603736012742a1c33b96d0db788c9cdebf7f9e"
|
||||
dependencies = [
|
||||
"gtk",
|
||||
"http",
|
||||
"jni",
|
||||
"jni 0.21.1",
|
||||
"log",
|
||||
"objc2 0.6.4",
|
||||
"objc2-app-kit 0.3.2",
|
||||
@@ -5984,6 +6189,15 @@ dependencies = [
|
||||
"system-deps",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webpki-root-certs"
|
||||
version = "1.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f31141ce3fc3e300ae89b78c0dd67f9708061d1d2eda54b8209346fd6be9a92c"
|
||||
dependencies = [
|
||||
"rustls-pki-types",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webpki-roots"
|
||||
version = "1.0.7"
|
||||
@@ -6773,7 +6987,7 @@ dependencies = [
|
||||
"gtk",
|
||||
"http",
|
||||
"javascriptcore-rs",
|
||||
"jni",
|
||||
"jni 0.21.1",
|
||||
"libc",
|
||||
"ndk 0.9.0",
|
||||
"objc2 0.6.4",
|
||||
@@ -6837,6 +7051,16 @@ version = "0.13.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd"
|
||||
|
||||
[[package]]
|
||||
name = "xattr"
|
||||
version = "1.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"rustix",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "xkbcommon"
|
||||
version = "0.7.0"
|
||||
@@ -7018,6 +7242,18 @@ dependencies = [
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zip"
|
||||
version = "4.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "caa8cd6af31c3b31c6631b8f483848b91589021b28fffe50adada48d4f4d2ed1"
|
||||
dependencies = [
|
||||
"arbitrary",
|
||||
"crc32fast",
|
||||
"indexmap 2.14.0",
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zmij"
|
||||
version = "1.0.21"
|
||||
|
||||
@@ -16,6 +16,7 @@ tauri-build = { version = "2", features = [] }
|
||||
[dependencies]
|
||||
tauri = { version = "2", features = ["macos-private-api", "tray-icon"] }
|
||||
tauri-plugin-shell = "2"
|
||||
tauri-plugin-updater = "2"
|
||||
tauri-plugin-single-instance = "2"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
"core:window:allow-close",
|
||||
"core:webview:default",
|
||||
"core:event:default",
|
||||
"shell:allow-open"
|
||||
"shell:allow-open",
|
||||
"updater:default"
|
||||
]
|
||||
}
|
||||
@@ -46,6 +46,7 @@ pub fn run() {
|
||||
show_main_window(app);
|
||||
}))
|
||||
.plugin(tauri_plugin_shell::init())
|
||||
.plugin(tauri_plugin_updater::Builder::new().build())
|
||||
.manage(coordinator.clone())
|
||||
.setup(move |app| {
|
||||
// Capsule 启动时定位到屏幕底部居中并隐藏;coordinator 按需显示。
|
||||
@@ -165,6 +166,7 @@ pub fn run() {
|
||||
commands::read_credential,
|
||||
commands::set_active_asr_provider,
|
||||
commands::set_active_llm_provider,
|
||||
restart_app,
|
||||
])
|
||||
.build(tauri::generate_context!())
|
||||
.expect("error while building tauri application")
|
||||
@@ -187,6 +189,11 @@ pub fn run() {
|
||||
});
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn restart_app(app: AppHandle) {
|
||||
app.restart();
|
||||
}
|
||||
|
||||
/// 把日志同时写到 stderr + ~/Library/Logs/OpenLess/openless.log(match Swift `Log.swift`)。
|
||||
fn init_file_logger() {
|
||||
use simplelog::{
|
||||
|
||||
@@ -65,5 +65,13 @@
|
||||
"infoPlist": "Info.plist",
|
||||
"entitlements": "Entitlements.plist"
|
||||
}
|
||||
},
|
||||
"plugins": {
|
||||
"updater": {
|
||||
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDFERUFBODAzNTY0QzMyM0YKUldRL01reFdBNmpxSGE1K0JadlpONXNWTzhJcGZCRGxjUVdIWExNNFJpeUNsSGZwazdlQThhemkK",
|
||||
"endpoints": [
|
||||
"https://github.com/appergb/openless/releases/latest/download/latest-{{target}}-{{arch}}.json"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,8 +7,7 @@
|
||||
import { useEffect, useState, type CSSProperties } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Icon } from './Icon';
|
||||
import { APP_VERSION_LABEL } from '../lib/appVersion';
|
||||
import { Settings as SettingsContent, type SettingsSectionId } from '../pages/Settings';
|
||||
import { AboutUpdateControl, Settings as SettingsContent, type SettingsSectionId } from '../pages/Settings';
|
||||
import { Row } from './ui/Row';
|
||||
import { openExternal } from '../lib/ipc';
|
||||
import {
|
||||
@@ -219,17 +218,9 @@ function AboutMini() {
|
||||
<img src="AppIcon.png" alt="" style={{ width: 56, height: 56, borderRadius: 13, boxShadow: '0 4px 10px rgba(0,0,0,.10), 0 0 0 0.5px rgba(0,0,0,.06)' }} />
|
||||
<div>
|
||||
<div style={{ fontSize: 17, fontWeight: 600 }}>OpenLess</div>
|
||||
<div style={{ fontSize: 12, color: 'var(--ol-ink-3)' }}>{t('modal.about.tagline')} · {APP_VERSION_LABEL}</div>
|
||||
<AboutUpdateControl tagline={t('modal.about.tagline')} />
|
||||
</div>
|
||||
</div>
|
||||
<Row label={t('modal.about.checkUpdate')}>
|
||||
<button
|
||||
style={btnGhost}
|
||||
onClick={() => openExternal('https://github.com/appergb/openless/releases')}
|
||||
>
|
||||
{t('modal.about.checkUpdateBtn')}
|
||||
</button>
|
||||
</Row>
|
||||
<Row label={t('modal.about.docs')}>
|
||||
<button
|
||||
style={btnGhost}
|
||||
|
||||
@@ -15,6 +15,7 @@ export const en: typeof zhCN = {
|
||||
copy: 'Copy',
|
||||
delete: 'Delete',
|
||||
later: 'Later',
|
||||
cancel: 'Cancel',
|
||||
close: 'Close',
|
||||
show: 'Show',
|
||||
hide: 'Hide',
|
||||
@@ -243,6 +244,10 @@ export const en: typeof zhCN = {
|
||||
about: {
|
||||
tagline: 'Speak naturally, write perfectly',
|
||||
checkUpdate: 'Check for updates',
|
||||
checkUpdateBtn: 'Check',
|
||||
checkingUpdate: 'Checking…',
|
||||
upToDate: 'You are already on the latest version.',
|
||||
updateError: 'Update check or install failed. Please try again later.',
|
||||
openReleases: 'Open Releases',
|
||||
source: 'Source',
|
||||
docs: 'Docs',
|
||||
@@ -253,6 +258,31 @@ export const en: typeof zhCN = {
|
||||
privacy: 'Privacy',
|
||||
privacyDesc: 'All transcripts stay on this device. Cloud APIs are only called for real-time transcription/polish; no recordings are retained.',
|
||||
localFirst: 'Local-first',
|
||||
updateDialog: {
|
||||
available: {
|
||||
title: 'Update available',
|
||||
desc: 'OpenLess {{version}} is available. Update now?',
|
||||
},
|
||||
downloading: {
|
||||
title: 'Downloading update',
|
||||
desc: 'Downloading OpenLess {{version}}. Keep the app open.',
|
||||
},
|
||||
downloaded: {
|
||||
title: 'Update ready',
|
||||
desc: 'OpenLess {{version}} has been installed. Restart automatically now to apply it?',
|
||||
},
|
||||
installing: {
|
||||
title: 'Installing update',
|
||||
desc: 'Installing OpenLess {{version}}. Keep the app open.',
|
||||
},
|
||||
install: 'Update now',
|
||||
downloadingLabel: 'Downloading…',
|
||||
installingLabel: 'Installing…',
|
||||
later: 'Restart manually later',
|
||||
restartNow: 'Restart now',
|
||||
progress: '{{progress}}% · {{downloaded}} / {{total}}',
|
||||
progressUnknown: '{{downloaded}} downloaded',
|
||||
},
|
||||
},
|
||||
},
|
||||
modal: {
|
||||
|
||||
@@ -13,6 +13,7 @@ export const zhCN = {
|
||||
copy: '复制',
|
||||
delete: '删除',
|
||||
later: '稍后',
|
||||
cancel: '取消',
|
||||
close: '关闭',
|
||||
show: '显示',
|
||||
hide: '隐藏',
|
||||
@@ -241,6 +242,10 @@ export const zhCN = {
|
||||
about: {
|
||||
tagline: '自然说话,完美书写',
|
||||
checkUpdate: '检查更新',
|
||||
checkUpdateBtn: '检查',
|
||||
checkingUpdate: '检查中…',
|
||||
upToDate: '当前已是最新版本。',
|
||||
updateError: '检查或更新失败,请稍后重试。',
|
||||
openReleases: '打开 Releases',
|
||||
source: '源码',
|
||||
docs: '文档',
|
||||
@@ -251,6 +256,31 @@ export const zhCN = {
|
||||
privacy: '隐私',
|
||||
privacyDesc: '所有识别结果仅保存在本机。云端 API 仅用于实时转写与润色,不会保留你的录音。',
|
||||
localFirst: '本地优先',
|
||||
updateDialog: {
|
||||
available: {
|
||||
title: '发现新版本',
|
||||
desc: '发现 OpenLess {{version}},是否现在更新?',
|
||||
},
|
||||
downloading: {
|
||||
title: '正在下载更新',
|
||||
desc: '正在下载 OpenLess {{version}},请保持应用打开。',
|
||||
},
|
||||
downloaded: {
|
||||
title: '更新已准备好',
|
||||
desc: 'OpenLess {{version}} 已安装完成。是否现在自动重启以应用更新?',
|
||||
},
|
||||
installing: {
|
||||
title: '正在安装更新',
|
||||
desc: '正在安装 OpenLess {{version}},请保持应用打开。',
|
||||
},
|
||||
install: '现在更新',
|
||||
downloadingLabel: '下载中…',
|
||||
installingLabel: '安装中…',
|
||||
later: '稍后手动重启',
|
||||
restartNow: '现在重启',
|
||||
progress: '{{progress}}% · {{downloaded}} / {{total}}',
|
||||
progressUnknown: '已下载 {{downloaded}}',
|
||||
},
|
||||
},
|
||||
},
|
||||
modal: {
|
||||
|
||||
@@ -216,6 +216,10 @@ export function triggerMicrophonePrompt(): Promise<void> {
|
||||
return invokeOrMock('trigger_microphone_prompt', undefined, () => undefined);
|
||||
}
|
||||
|
||||
export function restartApp(): Promise<void> {
|
||||
return invokeOrMock('restart_app', undefined, () => undefined);
|
||||
}
|
||||
|
||||
export async function openExternal(url: string): Promise<void> {
|
||||
if (!isTauri) {
|
||||
window.open(url, '_blank', 'noopener,noreferrer');
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
// keep their inline-style literals 1:1 with the source JSX.
|
||||
|
||||
import { useEffect, useRef, useState, type CSSProperties, type ReactNode } from 'react';
|
||||
import type { Update, DownloadEvent } from '@tauri-apps/plugin-updater';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Icon } from '../components/Icon';
|
||||
import { APP_VERSION_LABEL } from '../lib/appVersion';
|
||||
@@ -11,11 +12,13 @@ import {
|
||||
checkAccessibilityPermission,
|
||||
checkMicrophonePermission,
|
||||
getHotkeyStatus,
|
||||
isTauri,
|
||||
openExternal,
|
||||
openSystemSettings,
|
||||
readCredential,
|
||||
requestAccessibilityPermission,
|
||||
requestMicrophonePermission,
|
||||
restartApp,
|
||||
setActiveAsrProvider,
|
||||
setActiveLlmProvider,
|
||||
setCredential,
|
||||
@@ -752,10 +755,9 @@ function AboutSection() {
|
||||
>OL</div>
|
||||
<div>
|
||||
<div style={{ fontSize: 16, fontWeight: 600 }}>OpenLess</div>
|
||||
<div style={{ fontSize: 12, color: 'var(--ol-ink-3)' }}>{t('settings.about.tagline')} · {APP_VERSION_LABEL}</div>
|
||||
<AboutUpdateControl tagline={t('settings.about.tagline')} />
|
||||
</div>
|
||||
</div>
|
||||
<SettingRow label={t('settings.about.checkUpdate')}><Btn variant="ghost" size="sm" onClick={() => openExternal('https://github.com/appergb/openless/releases')}>{t('settings.about.openReleases')}</Btn></SettingRow>
|
||||
<SettingRow label={t('settings.about.source')}><Btn variant="ghost" size="sm" icon="link" onClick={() => openExternal('https://github.com/appergb/openless')}>GitHub</Btn></SettingRow>
|
||||
<SettingRow label={t('settings.about.docs')}><Btn variant="ghost" size="sm" icon="link" onClick={() => openExternal('https://github.com/appergb/openless#readme')}>README</Btn></SettingRow>
|
||||
<SettingRow label={t('settings.about.feedback')}><Btn variant="ghost" size="sm" icon="link" onClick={() => openExternal('https://github.com/appergb/openless/issues')}>GitHub Issues</Btn></SettingRow>
|
||||
@@ -781,6 +783,190 @@ function AboutSection() {
|
||||
);
|
||||
}
|
||||
|
||||
type UpdateStatus = 'idle' | 'checking' | 'available' | 'none' | 'downloading' | 'installing' | 'downloaded' | 'error';
|
||||
|
||||
export function AboutUpdateControl({ tagline }: { tagline: string }) {
|
||||
const { t } = useTranslation();
|
||||
const updateRef = useRef<Update | null>(null);
|
||||
const [updateStatus, setUpdateStatus] = useState<UpdateStatus>('idle');
|
||||
const [version, setVersion] = useState('');
|
||||
const [downloaded, setDownloaded] = useState(0);
|
||||
const [contentLength, setContentLength] = useState<number | null>(null);
|
||||
|
||||
const checking = updateStatus === 'checking';
|
||||
const busy = updateStatus === 'downloading' || updateStatus === 'installing';
|
||||
const progress = contentLength && contentLength > 0 ? Math.min(100, Math.round((downloaded / contentLength) * 100)) : null;
|
||||
|
||||
const closeUpdate = async () => {
|
||||
const current = updateRef.current;
|
||||
updateRef.current = null;
|
||||
if (current) {
|
||||
try {
|
||||
await current.close();
|
||||
} catch (error) {
|
||||
console.warn('[settings] failed to close update resource', error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
void closeUpdate();
|
||||
};
|
||||
}, []);
|
||||
|
||||
const resetProgress = () => {
|
||||
setDownloaded(0);
|
||||
setContentLength(null);
|
||||
};
|
||||
|
||||
const checkForUpdates = async () => {
|
||||
setUpdateStatus('checking');
|
||||
setVersion('');
|
||||
resetProgress();
|
||||
await closeUpdate();
|
||||
try {
|
||||
if (!isTauri) {
|
||||
setUpdateStatus('none');
|
||||
return;
|
||||
}
|
||||
const { check } = await import('@tauri-apps/plugin-updater');
|
||||
const next = await check();
|
||||
updateRef.current = next;
|
||||
setVersion(next?.version ?? '');
|
||||
setUpdateStatus(next ? 'available' : 'none');
|
||||
} catch (error) {
|
||||
console.error('[settings] failed to check update', error);
|
||||
setUpdateStatus('error');
|
||||
}
|
||||
};
|
||||
|
||||
const installUpdate = async () => {
|
||||
const update = updateRef.current;
|
||||
if (!update) return;
|
||||
resetProgress();
|
||||
setUpdateStatus('downloading');
|
||||
try {
|
||||
await update.download((event: DownloadEvent) => {
|
||||
if (event.event === 'Started') {
|
||||
resetProgress();
|
||||
setContentLength(event.data.contentLength ?? null);
|
||||
} else if (event.event === 'Progress') {
|
||||
setDownloaded(value => value + event.data.chunkLength);
|
||||
} else if (event.event === 'Finished') {
|
||||
setUpdateStatus('installing');
|
||||
}
|
||||
});
|
||||
setUpdateStatus('installing');
|
||||
await update.install();
|
||||
await closeUpdate();
|
||||
setUpdateStatus('downloaded');
|
||||
} catch (error) {
|
||||
console.error('[settings] failed to install update', error);
|
||||
await closeUpdate();
|
||||
setUpdateStatus('error');
|
||||
}
|
||||
};
|
||||
|
||||
const dismissUpdateDialog = async () => {
|
||||
if (busy) return;
|
||||
await closeUpdate();
|
||||
setUpdateStatus('idle');
|
||||
setVersion('');
|
||||
resetProgress();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginTop: 2 }}>
|
||||
<span style={{ fontSize: 12, color: 'var(--ol-ink-3)' }}>{tagline} · {APP_VERSION_LABEL}</span>
|
||||
<Btn variant="ghost" size="sm" onClick={checkForUpdates} disabled={checking || busy}>
|
||||
{checking ? t('settings.about.checkingUpdate') : t('settings.about.checkUpdateBtn')}
|
||||
</Btn>
|
||||
</div>
|
||||
{(updateStatus === 'none' || updateStatus === 'error') && (
|
||||
<div style={{ fontSize: 11, color: updateStatus === 'error' ? 'var(--ol-err)' : 'var(--ol-ink-4)', marginTop: 4 }}>
|
||||
{updateStatus === 'none' ? t('settings.about.upToDate') : t('settings.about.updateError')}
|
||||
</div>
|
||||
)}
|
||||
{(updateStatus === 'available' || updateStatus === 'downloading' || updateStatus === 'installing' || updateStatus === 'downloaded') && (
|
||||
<UpdateDialog
|
||||
status={updateStatus}
|
||||
version={version}
|
||||
progress={progress}
|
||||
downloaded={downloaded}
|
||||
contentLength={contentLength}
|
||||
onInstall={installUpdate}
|
||||
onRestart={restartApp}
|
||||
onClose={dismissUpdateDialog}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function UpdateDialog({
|
||||
status,
|
||||
version,
|
||||
progress,
|
||||
downloaded,
|
||||
contentLength,
|
||||
onInstall,
|
||||
onRestart,
|
||||
onClose,
|
||||
}: {
|
||||
status: 'available' | 'downloading' | 'installing' | 'downloaded';
|
||||
version: string;
|
||||
progress: number | null;
|
||||
downloaded: number;
|
||||
contentLength: number | null;
|
||||
onInstall: () => void;
|
||||
onRestart: () => void;
|
||||
onClose: () => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const downloading = status === 'downloading';
|
||||
const installing = status === 'installing';
|
||||
return (
|
||||
<div style={{ position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.18)', display: 'grid', placeItems: 'center', zIndex: 40 }}>
|
||||
<div style={{ width: 360, borderRadius: 16, background: 'var(--ol-surface)', border: '0.5px solid var(--ol-line-strong)', boxShadow: '0 18px 42px rgba(0,0,0,0.18)', padding: 18 }}>
|
||||
<div style={{ fontSize: 15, fontWeight: 650, marginBottom: 8 }}>{t(`settings.about.updateDialog.${status}.title`)}</div>
|
||||
<div style={{ fontSize: 12, color: 'var(--ol-ink-3)', lineHeight: 1.6, marginBottom: 14 }}>
|
||||
{t(`settings.about.updateDialog.${status}.desc`, { version })}
|
||||
</div>
|
||||
{(downloading || installing || status === 'downloaded') && (
|
||||
<div style={{ marginBottom: 14 }}>
|
||||
<div style={{ height: 8, borderRadius: 999, background: 'var(--ol-surface-2)', overflow: 'hidden', border: '0.5px solid var(--ol-line)' }}>
|
||||
<div style={{ height: '100%', width: `${status === 'downloaded' || installing ? 100 : progress ?? 8}%`, background: 'var(--ol-blue)', transition: 'width 0.18s ease-out' }} />
|
||||
</div>
|
||||
<div style={{ marginTop: 6, fontSize: 11, color: 'var(--ol-ink-4)' }}>
|
||||
{installing
|
||||
? t('settings.about.updateDialog.installingLabel')
|
||||
: progress === null
|
||||
? t('settings.about.updateDialog.progressUnknown', { downloaded: formatBytes(downloaded) })
|
||||
: t('settings.about.updateDialog.progress', { progress, downloaded: formatBytes(downloaded), total: formatBytes(contentLength ?? 0) })}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: 8 }}>
|
||||
{status === 'available' && <Btn size="sm" onClick={onClose}>{t('common.cancel')}</Btn>}
|
||||
{status === 'available' && <Btn variant="blue" size="sm" onClick={onInstall}>{t('settings.about.updateDialog.install')}</Btn>}
|
||||
{(downloading || installing) && <Btn size="sm" disabled>{installing ? t('settings.about.updateDialog.installingLabel') : t('settings.about.updateDialog.downloadingLabel')}</Btn>}
|
||||
{status === 'downloaded' && <Btn size="sm" onClick={onClose}>{t('settings.about.updateDialog.later')}</Btn>}
|
||||
{status === 'downloaded' && <Btn variant="blue" size="sm" onClick={onRestart}>{t('settings.about.updateDialog.restartNow')}</Btn>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function formatBytes(value: number) {
|
||||
if (!Number.isFinite(value) || value <= 0) return '0 B';
|
||||
if (value < 1024) return `${value} B`;
|
||||
if (value < 1024 * 1024) return `${(value / 1024).toFixed(1)} KB`;
|
||||
return `${(value / 1024 / 1024).toFixed(1)} MB`;
|
||||
}
|
||||
|
||||
function HotkeyStatusPill({ status }: { status: HotkeyStatus | null }) {
|
||||
const { t } = useTranslation();
|
||||
if (!status) {
|
||||
|
||||
@@ -103,9 +103,10 @@ interface BtnProps {
|
||||
icon?: string;
|
||||
style?: CSSProperties;
|
||||
onClick?: () => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export function Btn({ children, variant = 'ghost', size = 'md', icon, style, onClick }: BtnProps) {
|
||||
export function Btn({ children, variant = 'ghost', size = 'md', icon, style, onClick, disabled = false }: BtnProps) {
|
||||
const variants: Record<BtnVariant, { bg: string; color: string; bd: string; sh: string }> = {
|
||||
primary: { bg: 'var(--ol-ink)', color: '#fff', bd: 'transparent', sh: '0 1px 2px rgba(0,0,0,.08)' },
|
||||
blue: { bg: 'var(--ol-blue)', color: '#fff', bd: 'transparent', sh: '0 1px 2px rgba(37,99,235,.18)' },
|
||||
@@ -119,7 +120,8 @@ export function Btn({ children, variant = 'ghost', size = 'md', icon, style, onC
|
||||
};
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
onClick={disabled ? undefined : onClick}
|
||||
disabled={disabled}
|
||||
style={{
|
||||
display: 'inline-flex', alignItems: 'center', gap: 6,
|
||||
background: v.bg, color: v.color,
|
||||
@@ -127,7 +129,8 @@ export function Btn({ children, variant = 'ghost', size = 'md', icon, style, onC
|
||||
borderRadius: 8,
|
||||
boxShadow: v.sh,
|
||||
fontFamily: 'inherit', fontWeight: 500,
|
||||
cursor: 'default',
|
||||
cursor: disabled ? 'not-allowed' : 'default',
|
||||
opacity: disabled ? 0.55 : 1,
|
||||
transition: 'background 0.12s ease-out, color 0.12s ease-out, border-color 0.12s ease-out, box-shadow 0.12s ease-out, transform 0.08s ease-out',
|
||||
...sizes[size],
|
||||
...style,
|
||||
|
||||
Reference in new issue
Block a user