feat(updater): Beta 渠道接通 auto-update(Builder.endpoints runtime 切换) (#465)
* feat(updater): Beta 渠道接通 auto-update(Builder.endpoints runtime 切换)
CLAUDE.md 之前的论断「tauri-plugin-updater 2.10 Builder 不暴露 endpoints()」在
2.10.1 已不成立 —— Builder 现在有 .endpoints(Vec<Url>) -> Result<Self>,build()
里 self.endpoints.unwrap_or_else(|| self.config.endpoints.clone()) 走 runtime 覆盖。
实现 channel-aware 自动更新(不 fork plugin、不写自定义下载/签名校验):
Rust(~100 行新增)
- commands.rs::app_check_update_with_channel:
Stable → webview.updater_builder().build()?.check()(默认 endpoints 从 tauri.conf)
Beta → fetch_latest_beta_release() → 拼镜像 + 直连两份 -beta manifest URL
→ builder.endpoints(urls)?.build()?.check()
- AppUpdateMetadata 字段形状对齐 plugin 的 UpdateMetadata(rid + currentVersion
+ version + body + rawJson),前端 new Update(metadata) 即可复用 plugin 的
download / install / close。
- lib.rs 注册新 command。
前端(~30 行改动)
- AutoUpdate.tsx::checkForUpdates 从 import('@tauri-apps/plugin-updater').check
改为 invoke('app_check_update_with_channel');其余 download / install / close
全走 plugin 原生 JS API(new Update(metadata) 后链式调用)。
- SettingsModal::BetaChannelControl 删掉「打开 GitHub 下载」按钮 ——
auto-update 接管了,手动下载路径冗余;保留「最新 Beta 版本号 + 刷新」做信息透明。
- i18n 5 个 locale 改 betaChannelDesc 文案,把「需要手动下载安装」改成
「自动接收最新 Beta 版本」。betaChannelDownloadBtn 等 i18n key 暂留(无用但无害)。
物理隔离仍生效:Beta tag 的 manifest 文件名带 -beta 后缀,跟 Stable 的
latest-{tgt}-{arch}.json 在 GitHub Release assets 里是分开两份;即使代码逻辑
错把 Beta URL 给 Stable 用户也是 HTTP 404,不会拿到错档。
验证:cargo test --lib serial 270 / 0 / 0;tsc clean;vite build clean。
* feat(settings): BetaChannelControl 加版本比对 + 「立即更新」按钮
之前 BetaChannelControl 在 toggle 打开后,只机械显示「最新 Beta: vX-beta-tauri」,
不管本机版本是不是已经等于那个 tag。用户报告:「显示有最新 Beta,但应该比对版本号」。
修复:
- parseVersionFromBetaTag("v1.3.4-1-beta-tauri") → "1.3.4-1"
- semverGreater(remote, local) 做 SemVer 风格比对(支持 X.Y.Z 和 X.Y.Z-N,覆盖
本仓库实际版本格式;prerelease 按 SemVer 规则 lower than no-prerelease)
- 本机 >= 最新 → 「最新 Beta: vX」 + 蓝色「已是最新」chip
- 本机 < 最新 → 「最新 Beta: vX」 + 「立即更新」按钮(蓝色高亮)
- 「立即更新」点击 → useAutoUpdate.checkForUpdates → 走和全局「检查更新」按钮
完全相同的 download/install 流程,UpdateDialog 直接挂在 BetaChannelControl 内部
- 用户的第二个 feature 诉求:「在 Beta 渠道旁边一键检查 + 自动更新」 = 这个按钮
i18n 5 个 locale 加 4 个 key:betaChannelUpToDate / betaChannelUpdateNow /
betaChannelUpdateNowTitle / betaChannelChecking。
* chore(release): bump 1.3.4-1 → 1.3.4-2 for Beta channel
`-2` 后缀让 appVersion.ts 的 IS_BETA_BUILD 仍判为 Beta,UI BETA 标签照常显示。
配本 PR 的 Beta auto-update + BetaChannelControl「立即更新」按钮,1.3.4-1 用户
切到 Beta 渠道后点检查会发现 1.3.4-2 并走 auto-update 流程一键升上来。
发布 tag:v1.3.4-2-beta-tauri。
---------
Co-authored-by: baiqing <lbx12309@icloud.com>
This commit is contained in:
1 parent
061558a0fa
commit
859fd5370f
14 files changed
+251
-28
No files matched your search
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "openless-app",
|
||||
"version": "1.3.4-1",
|
||||
"version": "1.3.4-2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "openless-app",
|
||||
"version": "1.3.4-1",
|
||||
"version": "1.3.4-2",
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^2.1.1",
|
||||
"@tauri-apps/plugin-autostart": "^2.5.1",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "openless-app",
|
||||
"private": true,
|
||||
"version": "1.3.4-1",
|
||||
"version": "1.3.4-2",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
Generated
+1
-1
@@ -3751,7 +3751,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "openless"
|
||||
version = "1.3.4-1"
|
||||
version = "1.3.4-2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"arboard",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "openless"
|
||||
version = "1.3.4-1"
|
||||
version = "1.3.4-2"
|
||||
description = "OpenLess — local voice input that types where your cursor is"
|
||||
authors = ["OpenLess"]
|
||||
edition = "2021"
|
||||
|
||||
@@ -271,11 +271,13 @@ pub(crate) fn activate_builtin_style_mode(
|
||||
// 渠道偏好的写入路径跟 set_settings 复用 persist_settings:保持热键兜底归一化
|
||||
// 跟其他 prefs 写入一致,且写完后 emit "prefs:changed",让前端跨 webview 同步。
|
||||
//
|
||||
// 注意:plugin-updater 2.10 的 Builder 不暴露 endpoints() 运行时 API,因此切到 Beta
|
||||
// 渠道**不会**改变 in-app「检查更新」的行为——它仍然只看正式版 manifest。Beta 用户
|
||||
// 通过 `fetch_latest_beta_release` 获取最新 prerelease,由前端跳浏览器手动下载,
|
||||
// 物理隔离 Beta 包不会通过 auto-update 推到正式版用户。详见 PR-B-2 description 与
|
||||
// CLAUDE.md `Branch & release-channel workflow` 段落。
|
||||
// 更新:plugin-updater 2.10.1 的 Builder 现在暴露 .endpoints() runtime API(CLAUDE.md
|
||||
// 当年记的"不支持"已不成立)。本节配合 `app_check_update_with_channel` 命令实现
|
||||
// Beta auto-update:Stable 渠道 → 走 tauri.conf 的默认 endpoints;Beta 渠道 →
|
||||
// fetch_latest_beta_release 拿最新 prerelease tag → 拼成 -beta manifest URL →
|
||||
// builder.endpoints(vec![url]).build().check()。Stable 用户绝对不会撞到 Beta 包
|
||||
// (Beta tag 的 manifest 文件名带 `-beta` 后缀,跟 Stable manifest 在 GitHub
|
||||
// Release assets 里物理分离)。
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_update_channel(coord: CoordinatorState<'_>) -> UpdateChannel {
|
||||
@@ -377,6 +379,101 @@ fn extract_between(haystack: &str, open: &str, close: &str) -> Option<String> {
|
||||
Some(haystack[start..start + end].to_string())
|
||||
}
|
||||
|
||||
// ─────────────────────── Channel-aware updater check ────────────────────────
|
||||
//
|
||||
// 替换前端原来直接 import('@tauri-apps/plugin-updater').check() 的路径:
|
||||
// - Stable 渠道:builder 不动 endpoints,沿用 tauri.conf 配的 stable manifest URL。
|
||||
// - Beta 渠道:先 fetch_latest_beta_release 拿最新 prerelease tag,拼成 -beta manifest
|
||||
// URL(同时给一对 mirror + direct),再 builder.endpoints(vec![url])?.build()?.check()。
|
||||
//
|
||||
// 返回的 Metadata 形状与 plugin-updater 的 JS UpdateMetadata 完全一致(rid +
|
||||
// currentVersion 等驼峰字段),前端可以直接 `new Update(metadata)` 复用 plugin
|
||||
// 的 download / install / close 实现,无需我们自己写下载和签名校验。
|
||||
//
|
||||
// 物理隔离:Beta tag 推出来的 manifest 文件名带 `-beta` 后缀(参见 release-tauri.yml
|
||||
// 第 382 行注释),跟 Stable 的 `latest-{tgt}-{arch}.json` 在 GitHub Release assets
|
||||
// 里是分开的两份文件 —— 即使代码逻辑写错把 Beta URL 传给 Stable 用户,HTTP 也是
|
||||
// 直接 404,绝不会拿到错档。
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AppUpdateMetadata {
|
||||
pub rid: tauri::ResourceId,
|
||||
pub current_version: String,
|
||||
pub version: String,
|
||||
pub date: Option<String>,
|
||||
pub body: Option<String>,
|
||||
/// 原始 manifest JSON——`new Update(metadata)` 在 JS 那边会校验它存在;
|
||||
/// 我们透传 plugin 自己 check 时拿到的字段。
|
||||
pub raw_json: serde_json::Value,
|
||||
}
|
||||
|
||||
/// 按 prefs.update_channel 决定 manifest 来源,再走 plugin-updater 的标准 check 流程。
|
||||
/// 返回 None = 当前是最新;Some(metadata) = 有新版可装。
|
||||
#[tauri::command]
|
||||
pub async fn app_check_update_with_channel<R: tauri::Runtime>(
|
||||
coord: CoordinatorState<'_>,
|
||||
webview: tauri::Webview<R>,
|
||||
timeout_ms: Option<u64>,
|
||||
) -> Result<Option<AppUpdateMetadata>, String> {
|
||||
use tauri_plugin_updater::UpdaterExt;
|
||||
|
||||
let channel = coord.prefs().get().update_channel;
|
||||
let mut builder = webview.updater_builder();
|
||||
if let Some(ms) = timeout_ms {
|
||||
builder = builder.timeout(std::time::Duration::from_millis(ms));
|
||||
}
|
||||
if matches!(channel, UpdateChannel::Beta) {
|
||||
let urls = resolve_beta_manifest_endpoints().await?;
|
||||
builder = builder
|
||||
.endpoints(urls)
|
||||
.map_err(|e| format!("set beta endpoints: {e}"))?;
|
||||
}
|
||||
let updater = builder.build().map_err(|e| format!("build updater: {e}"))?;
|
||||
let update = updater
|
||||
.check()
|
||||
.await
|
||||
.map_err(|e| format!("check update failed: {e}"))?;
|
||||
|
||||
let Some(update) = update else {
|
||||
return Ok(None);
|
||||
};
|
||||
// date 字段透传需要引 time crate;前端 AutoUpdate.tsx 实际并不用 date,所以这里
|
||||
// 直接置 None,避免拉一个新 dep 进 src-tauri/Cargo.toml。
|
||||
let metadata = AppUpdateMetadata {
|
||||
current_version: update.current_version.clone(),
|
||||
version: update.version.clone(),
|
||||
date: None,
|
||||
body: update.body.clone(),
|
||||
raw_json: update.raw_json.clone(),
|
||||
rid: webview.resources_table().add(update),
|
||||
};
|
||||
Ok(Some(metadata))
|
||||
}
|
||||
|
||||
/// 把 fetch_latest_beta_release 找到的最新 prerelease tag 拼成 -beta manifest URL 对。
|
||||
/// 顺序:先镜像(fastgit.cc 代理 GitHub),后直连 —— 跟 tauri.conf 现有 Stable
|
||||
/// endpoints 一致,让国内访问优先打到 CDN。
|
||||
async fn resolve_beta_manifest_endpoints() -> Result<Vec<url::Url>, String> {
|
||||
let Some(latest) = fetch_latest_beta_release().await? else {
|
||||
return Err("尚未发布过 Beta 版本".to_string());
|
||||
};
|
||||
let tag = latest.tag_name;
|
||||
// {{target}} / {{arch}} 占位符由 plugin 在 check 时替换。Rust raw string 用 r#""#
|
||||
// 不需要转义双花括号,比 format! 干净。
|
||||
let mirror = format!(
|
||||
"https://fastgit.cc/https://github.com/appergb/openless/releases/download/{tag}/latest-{{{{target}}}}-{{{{arch}}}}-beta-mirror.json"
|
||||
);
|
||||
let direct = format!(
|
||||
"https://github.com/appergb/openless/releases/download/{tag}/latest-{{{{target}}}}-{{{{arch}}}}-beta.json"
|
||||
);
|
||||
let mirror_url =
|
||||
url::Url::parse(&mirror).map_err(|e| format!("parse beta mirror url: {e}"))?;
|
||||
let direct_url =
|
||||
url::Url::parse(&direct).map_err(|e| format!("parse beta direct url: {e}"))?;
|
||||
Ok(vec![mirror_url, direct_url])
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_hotkey_status(coord: CoordinatorState<'_>) -> HotkeyStatus {
|
||||
coord.hotkey_status()
|
||||
|
||||
@@ -270,6 +270,7 @@ pub fn run() {
|
||||
commands::get_update_channel,
|
||||
commands::set_update_channel,
|
||||
commands::fetch_latest_beta_release,
|
||||
commands::app_check_update_with_channel,
|
||||
commands::get_hotkey_status,
|
||||
commands::get_hotkey_capability,
|
||||
commands::is_wayland_cli_mode,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "OpenLess",
|
||||
"version": "1.3.4-1",
|
||||
"version": "1.3.4-2",
|
||||
"identifier": "com.openless.app",
|
||||
"build": {
|
||||
"beforeDevCommand": "npm run dev",
|
||||
|
||||
@@ -1,14 +1,31 @@
|
||||
// 自动更新共用模块 — Settings 的"关于"section 和 footer 按钮共用同一套
|
||||
// 状态机 + 对话框 UI。两边各自调用 useAutoUpdate(),dialog 渲染条件相同。
|
||||
//
|
||||
// 渠道感知:check 不再走 plugin-updater 的 JS check()(它只看 tauri.conf 配的
|
||||
// Stable manifest URL),改为 invoke('app_check_update_with_channel')。
|
||||
// Rust 那边按 prefs.update_channel 决定 manifest URL;返回的 metadata 直接
|
||||
// `new Update(metadata)` 复用 plugin 的 download / install / close 实现,
|
||||
// 我们不重复造下载和签名校验。
|
||||
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import type { Update, DownloadEvent } from '@tauri-apps/plugin-updater';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import type { DownloadEvent } from '@tauri-apps/plugin-updater';
|
||||
import { Update } from '@tauri-apps/plugin-updater';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { isTauri, restartApp } from '../lib/ipc';
|
||||
import { Btn } from '../pages/_atoms';
|
||||
|
||||
const UPDATE_CHECK_TIMEOUT_MS = 8_000; // 缩短超时,让镜像站慢的情况能更快 fallback
|
||||
|
||||
interface AppUpdateMetadata {
|
||||
rid: number;
|
||||
currentVersion: string;
|
||||
version: string;
|
||||
date?: string | null;
|
||||
body?: string | null;
|
||||
rawJson: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export type UpdateStatus =
|
||||
| 'idle'
|
||||
| 'checking'
|
||||
@@ -79,11 +96,28 @@ export function useAutoUpdate(): UseAutoUpdate {
|
||||
setStatus('none');
|
||||
return;
|
||||
}
|
||||
const { check } = await import('@tauri-apps/plugin-updater');
|
||||
const next = await check({ timeout: UPDATE_CHECK_TIMEOUT_MS });
|
||||
// Rust 侧按 update_channel 拼 manifest URL:Stable → tauri.conf 默认;
|
||||
// Beta → fetch_latest_beta_release 拼出 -beta manifest URL 后再 check。
|
||||
const metadata = await invoke<AppUpdateMetadata | null>('app_check_update_with_channel', {
|
||||
timeoutMs: UPDATE_CHECK_TIMEOUT_MS,
|
||||
});
|
||||
if (!metadata) {
|
||||
setStatus('none');
|
||||
return;
|
||||
}
|
||||
// metadata 形状跟 plugin 自己 check 返回的 UpdateMetadata 完全一致;
|
||||
// new Update(metadata) 直接复用 plugin 的 download/install/close 实现。
|
||||
const next = new Update({
|
||||
rid: metadata.rid,
|
||||
currentVersion: metadata.currentVersion,
|
||||
version: metadata.version,
|
||||
date: metadata.date ?? undefined,
|
||||
body: metadata.body ?? undefined,
|
||||
rawJson: metadata.rawJson,
|
||||
});
|
||||
updateRef.current = next;
|
||||
setVersion(next?.version ?? '');
|
||||
setStatus(next ? 'available' : 'none');
|
||||
setVersion(next.version);
|
||||
setStatus('available');
|
||||
} catch (error) {
|
||||
console.error('[updater] failed to check update', error);
|
||||
setStatus('error');
|
||||
|
||||
@@ -21,8 +21,38 @@ import {
|
||||
type LatestBetaRelease,
|
||||
type UpdateChannel,
|
||||
} from '../lib/ipc';
|
||||
import { APP_VERSION } from '../lib/appVersion';
|
||||
import { isDialogStatus, UpdateDialog, useAutoUpdate } from './AutoUpdate';
|
||||
import type { OS } from './WindowChrome';
|
||||
|
||||
// 把 Beta tag 名(如 "v1.3.4-1-beta-tauri" / "v1.3.2-3-beta-tauri")解析回 semver 版本号
|
||||
// "1.3.4-1" / "1.3.2-3"。失败返回 null(让 UI fallback 到字符串相等比对)。
|
||||
function parseVersionFromBetaTag(tag: string): string | null {
|
||||
const trimmed = tag.replace(/^v/, '').replace(/-beta-tauri$/, '');
|
||||
return /^\d+\.\d+\.\d+(-\d+)?$/.test(trimmed) ? trimmed : null;
|
||||
}
|
||||
|
||||
// 简化版 semver 比较 —— 只覆盖本仓库实际用到的两种形态:X.Y.Z 和 X.Y.Z-N。
|
||||
// 返回 true 表示 a > b。按 SemVer 规则:major→minor→patch→prerelease(none > some)。
|
||||
function semverGreater(a: string, b: string): boolean {
|
||||
const parse = (v: string) => {
|
||||
const [main, pre] = v.split('-');
|
||||
const [major, minor, patch] = main.split('.').map(n => Number.parseInt(n, 10));
|
||||
const preNum = pre === undefined ? null : Number.parseInt(pre, 10);
|
||||
return { major, minor, patch, preNum };
|
||||
};
|
||||
const A = parse(a);
|
||||
const B = parse(b);
|
||||
if (A.major !== B.major) return A.major > B.major;
|
||||
if (A.minor !== B.minor) return A.minor > B.minor;
|
||||
if (A.patch !== B.patch) return A.patch > B.patch;
|
||||
// pre: null(无 prerelease)> 任何 prerelease
|
||||
if (A.preNum === B.preNum) return false;
|
||||
if (A.preNum === null) return true;
|
||||
if (B.preNum === null) return false;
|
||||
return A.preNum > B.preNum;
|
||||
}
|
||||
|
||||
interface SettingsModalProps {
|
||||
os: OS;
|
||||
onClose: () => void;
|
||||
@@ -417,10 +447,13 @@ function AboutMini() {
|
||||
);
|
||||
}
|
||||
|
||||
// Beta 渠道开关:物理隔离的 opt-in,不接 auto-update。
|
||||
// - 关闭状态 = 正式版渠道,默认行为,用户从「检查更新」拿正式 release
|
||||
// - 打开 = 用户主动加入 Beta;写 prefs(无重启需要)+ 拉一次最新 prerelease 信息
|
||||
// - 点"打开 GitHub"跳浏览器到具体的 Beta release 页面,用户手动下载安装
|
||||
// Beta 渠道开关:物理隔离的 opt-in,**已接 auto-update**(PR feat/beta-auto-update)。
|
||||
// - 关闭 = Stable 渠道,「检查更新」走 tauri.conf 默认 endpoints
|
||||
// - 打开 = 写 prefs.update_channel = 'beta';Rust 端 app_check_update_with_channel
|
||||
// 命令会自动拉最新 prerelease tag 拼成 -beta manifest URL,再走 plugin-updater
|
||||
// 的 check/download/install 标准流程
|
||||
// - 这里拉一次 fetch_latest_beta_release 仅用于「告诉用户最新 Beta 是哪个版本」做
|
||||
// 信息透明;不再渲染手动下载按钮(auto-update 接管了那条路)。
|
||||
// 不在 Beta 渠道时不发起 GitHub API 请求,避免空切换浪费配额。
|
||||
function BetaChannelControl() {
|
||||
const { t } = useTranslation();
|
||||
@@ -428,6 +461,18 @@ function BetaChannelControl() {
|
||||
const [latest, setLatest] = useState<LatestBetaRelease | null>(null);
|
||||
const [status, setStatus] = useState<'idle' | 'fetching' | 'empty' | 'error'>('idle');
|
||||
const [errorMessage, setErrorMessage] = useState<string>('');
|
||||
const updater = useAutoUpdate();
|
||||
|
||||
// 本机 vs 最新 Beta tag 比对:
|
||||
// - 'unknown' → 还没拿到 latest 或解析失败,按"未知"渲染
|
||||
// - 'up-to-date' → 本机版本 >= 远端最新 Beta,按"已是最新"渲染、隐藏更新按钮
|
||||
// - 'newer' → 远端有更新版本,渲染「立即更新」按钮触发 auto-update 流程
|
||||
const remoteVersion = latest ? parseVersionFromBetaTag(latest.tagName) : null;
|
||||
const comparison: 'unknown' | 'up-to-date' | 'newer' = !remoteVersion
|
||||
? 'unknown'
|
||||
: semverGreater(remoteVersion, APP_VERSION)
|
||||
? 'newer'
|
||||
: 'up-to-date';
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
@@ -495,9 +540,24 @@ function BetaChannelControl() {
|
||||
<span>
|
||||
{t('settings.about.betaChannelLatestPrefix')} <code style={{ fontFamily: 'var(--ol-font-mono)' }}>{latest.tagName}</code>
|
||||
</span>
|
||||
<button style={btnGhost} onClick={() => openExternal(latest.htmlUrl)}>
|
||||
{t('settings.about.betaChannelDownloadBtn')}
|
||||
</button>
|
||||
{comparison === 'up-to-date' && (
|
||||
<span style={{
|
||||
fontSize: 11, padding: '2px 8px', borderRadius: 999,
|
||||
background: 'var(--ol-blue-soft)', color: 'var(--ol-blue)', fontWeight: 500,
|
||||
}}>{t('settings.about.betaChannelUpToDate')}</span>
|
||||
)}
|
||||
{comparison === 'newer' && (
|
||||
<button
|
||||
style={{ ...btnGhost, borderColor: 'var(--ol-blue)', color: 'var(--ol-blue)' }}
|
||||
onClick={() => void updater.checkForUpdates()}
|
||||
disabled={updater.checking || updater.busy}
|
||||
title={t('settings.about.betaChannelUpdateNowTitle')}
|
||||
>
|
||||
{updater.checking
|
||||
? t('settings.about.betaChannelChecking')
|
||||
: t('settings.about.betaChannelUpdateNow')}
|
||||
</button>
|
||||
)}
|
||||
<button style={btnGhost} onClick={fetchBeta} title={t('settings.about.betaChannelRefresh')}>
|
||||
<Icon name="refresh" size={12} />
|
||||
</button>
|
||||
@@ -510,6 +570,17 @@ function BetaChannelControl() {
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{isDialogStatus(updater.status) && (
|
||||
<UpdateDialog
|
||||
status={updater.status}
|
||||
version={updater.version}
|
||||
progress={updater.progress}
|
||||
downloaded={updater.downloaded}
|
||||
contentLength={updater.contentLength}
|
||||
onInstall={() => void updater.installUpdate()}
|
||||
onClose={() => void updater.dismissDialog()}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -782,7 +782,7 @@ export const en: typeof zhCN = {
|
||||
privacyDesc: 'All transcripts stay on this device. Cloud APIs are only called for real-time transcription/polish; no recordings are retained.',
|
||||
localFirst: 'Local-first',
|
||||
betaChannelLabel: 'Join Beta channel',
|
||||
betaChannelDesc: 'Stable channel is the default. Enabling this exposes a manual download link to the latest Beta below; Beta builds are NOT pushed to regular users via auto-update — you have to download and install them yourself. May be unstable, only recommended if you are willing to test pre-release builds and report issues.',
|
||||
betaChannelDesc: 'Stable channel is the default. When enabled, the app’s "Check for updates" auto-fetches the latest Beta release (with features not yet promoted to Stable). Beta builds are physically isolated from Stable manifests, so regular users are never affected. May be unstable; only recommended if you are willing to test pre-release builds and report issues.',
|
||||
betaChannelFetching: 'Fetching the latest Beta…',
|
||||
betaChannelFetchBtn: 'Look up latest Beta',
|
||||
betaChannelLatestPrefix: 'Latest Beta:',
|
||||
@@ -790,6 +790,10 @@ export const en: typeof zhCN = {
|
||||
betaChannelRefresh: 'Refresh',
|
||||
betaChannelNoBeta: 'No Beta release has been published yet.',
|
||||
betaChannelFetchError: 'Failed to fetch Beta release info. Please try again later.',
|
||||
betaChannelUpToDate: 'Up to date',
|
||||
betaChannelUpdateNow: 'Update now',
|
||||
betaChannelUpdateNowTitle: 'Check and download the latest Beta, then show the update dialog',
|
||||
betaChannelChecking: 'Checking…',
|
||||
updateDialog: {
|
||||
available: {
|
||||
title: 'Update available',
|
||||
|
||||
@@ -784,7 +784,7 @@ export const ja: typeof zhCN = {
|
||||
privacyDesc: 'すべての認識結果はローカルにのみ保存されます。クラウド API はリアルタイム転写と整文にのみ使用され、録音は保持されません。',
|
||||
localFirst: 'ローカル優先',
|
||||
betaChannelLabel: 'Beta チャンネルに参加',
|
||||
betaChannelDesc: '既定は正式版です。オンにすると最新 Beta 版のダウンロードリンクが下に表示されます。Beta ビルドは自動更新で配布されず、手動でダウンロード・インストールする必要があります。不安定な場合があるため、検証とフィードバック協力に同意するユーザーのみ推奨。',
|
||||
betaChannelDesc: '既定は正式版です。オンにすると、アプリの「更新を確認」が最新 Beta 版(Stable 未公開機能を含む)を自動取得します。Beta ビルドは Stable とマニフェストが物理的に分離されており、一般ユーザーへ波及しません。不安定な場合があるため、検証とフィードバック協力に同意するユーザーのみ推奨。',
|
||||
betaChannelFetching: '最新 Beta 版を取得中…',
|
||||
betaChannelFetchBtn: '最新 Beta を確認',
|
||||
betaChannelLatestPrefix: '最新 Beta:',
|
||||
@@ -792,6 +792,10 @@ export const ja: typeof zhCN = {
|
||||
betaChannelRefresh: '再取得',
|
||||
betaChannelNoBeta: 'まだ Beta リリースは公開されていません。',
|
||||
betaChannelFetchError: 'Beta バージョン情報の取得に失敗しました。後で再試行してください。',
|
||||
betaChannelUpToDate: '最新です',
|
||||
betaChannelUpdateNow: '今すぐ更新',
|
||||
betaChannelUpdateNowTitle: '最新 Beta を確認・ダウンロードし、更新ダイアログを表示します',
|
||||
betaChannelChecking: '確認中…',
|
||||
updateDialog: {
|
||||
available: {
|
||||
title: '新しいバージョンがあります',
|
||||
|
||||
@@ -784,7 +784,7 @@ export const ko: typeof zhCN = {
|
||||
privacyDesc: '모든 인식 결과는 로컬에만 저장됩니다. 클라우드 API 는 실시간 전사와 정리에만 사용되며 녹음을 보관하지 않습니다.',
|
||||
localFirst: '로컬 우선',
|
||||
betaChannelLabel: 'Beta 채널 참여',
|
||||
betaChannelDesc: '기본은 정식 버전입니다. 켜면 최신 Beta 버전 다운로드 링크가 아래에 표시됩니다. Beta 빌드는 자동 업데이트로 배포되지 않으며 직접 다운로드해 설치해야 합니다. 불안정할 수 있으므로 사전 평가와 피드백을 제공할 의향이 있는 사용자에게만 권장합니다.',
|
||||
betaChannelDesc: '기본은 정식 버전입니다. 켜면 앱의 "업데이트 확인"이 최신 Beta 버전(아직 Stable에 올라가지 않은 기능 포함)을 자동으로 가져옵니다. Beta 빌드는 Stable 매니페스트와 물리적으로 분리되어 있어 일반 사용자에게 영향을 주지 않습니다. 불안정할 수 있으므로 사전 평가와 피드백을 제공할 의향이 있는 사용자에게만 권장합니다.',
|
||||
betaChannelFetching: '최신 Beta 버전을 가져오는 중…',
|
||||
betaChannelFetchBtn: '최신 Beta 확인',
|
||||
betaChannelLatestPrefix: '최신 Beta:',
|
||||
@@ -792,6 +792,10 @@ export const ko: typeof zhCN = {
|
||||
betaChannelRefresh: '새로 고침',
|
||||
betaChannelNoBeta: '아직 게시된 Beta 릴리스가 없습니다.',
|
||||
betaChannelFetchError: 'Beta 릴리스 정보를 가져오지 못했습니다. 잠시 후 다시 시도하세요.',
|
||||
betaChannelUpToDate: '최신',
|
||||
betaChannelUpdateNow: '지금 업데이트',
|
||||
betaChannelUpdateNowTitle: '최신 Beta를 확인·다운로드하고 업데이트 대화상자를 표시합니다',
|
||||
betaChannelChecking: '확인 중…',
|
||||
updateDialog: {
|
||||
available: {
|
||||
title: '새 버전 발견',
|
||||
|
||||
@@ -780,7 +780,7 @@ export const zhCN = {
|
||||
privacyDesc: '所有识别结果仅保存在本机。云端 API 仅用于实时转写与润色,不会保留你的录音。',
|
||||
localFirst: '本地优先',
|
||||
betaChannelLabel: '加入 Beta 渠道',
|
||||
betaChannelDesc: '默认拿到的是正式版。打开后可在下方看到最新 Beta 版的下载入口;Beta 包不会通过自动更新推到普通用户,需要手动下载安装。可能不稳定,仅推荐愿意尝鲜与反馈问题的用户开启。',
|
||||
betaChannelDesc: '默认拿到的是正式版。打开后,App 的「检查更新」会自动接收最新 Beta 版本(含未上 Stable 的功能)。Beta 包跟 Stable 渠道物理隔离,普通用户不会被波及。可能不稳定,仅推荐愿意尝鲜与反馈问题的用户开启。',
|
||||
betaChannelFetching: '正在获取最新 Beta 版本…',
|
||||
betaChannelFetchBtn: '查询最新 Beta',
|
||||
betaChannelLatestPrefix: '最新 Beta:',
|
||||
@@ -788,6 +788,10 @@ export const zhCN = {
|
||||
betaChannelRefresh: '重新查询',
|
||||
betaChannelNoBeta: '暂无已发布的 Beta 版。',
|
||||
betaChannelFetchError: '获取 Beta 版本信息失败,请稍后重试。',
|
||||
betaChannelUpToDate: '已是最新',
|
||||
betaChannelUpdateNow: '立即更新',
|
||||
betaChannelUpdateNowTitle: '检查并下载最新 Beta,然后弹出更新对话框',
|
||||
betaChannelChecking: '检查中…',
|
||||
updateDialog: {
|
||||
available: {
|
||||
title: '发现新版本',
|
||||
|
||||
@@ -782,7 +782,7 @@ export const zhTW: typeof zhCN = {
|
||||
privacyDesc: '所有識別結果僅保存在本機。雲端 API 僅用於實時轉寫與潤色,不會保留你的錄音。',
|
||||
localFirst: '本地優先',
|
||||
betaChannelLabel: '加入 Beta 渠道',
|
||||
betaChannelDesc: '預設拿到的是正式版。打開後可在下方看到最新 Beta 版的下載入口;Beta 包不會通過自動更新推送給普通用戶,需要手動下載安裝。可能不穩定,僅推薦願意嘗鮮並回報問題的用戶開啟。',
|
||||
betaChannelDesc: '預設拿到的是正式版。打開後,App 的「檢查更新」會自動接收最新 Beta 版本(含未上 Stable 的功能)。Beta 包跟 Stable 渠道物理隔離,普通用戶不會被波及。可能不穩定,僅推薦願意嘗鮮並回報問題的用戶開啟。',
|
||||
betaChannelFetching: '正在獲取最新 Beta 版本…',
|
||||
betaChannelFetchBtn: '查詢最新 Beta',
|
||||
betaChannelLatestPrefix: '最新 Beta:',
|
||||
@@ -790,6 +790,10 @@ export const zhTW: typeof zhCN = {
|
||||
betaChannelRefresh: '重新查詢',
|
||||
betaChannelNoBeta: '尚未發佈過 Beta 版。',
|
||||
betaChannelFetchError: '獲取 Beta 版本資訊失敗,請稍後重試。',
|
||||
betaChannelUpToDate: '已是最新',
|
||||
betaChannelUpdateNow: '立即更新',
|
||||
betaChannelUpdateNowTitle: '檢查並下載最新 Beta,然後彈出更新對話框',
|
||||
betaChannelChecking: '檢查中…',
|
||||
updateDialog: {
|
||||
available: {
|
||||
title: '發現新版本',
|
||||
|
||||
Reference in new issue
Block a user