diff --git a/openless-all/app/crates/openless-core/src/shared_types.rs b/openless-all/app/crates/openless-core/src/shared_types.rs index 6266a809b..eb0ab1b2e 100644 --- a/openless-all/app/crates/openless-core/src/shared_types.rs +++ b/openless-all/app/crates/openless-core/src/shared_types.rs @@ -566,6 +566,10 @@ pub struct UserPreferences { /// 手动检查按钮显式指定 channel,与此 pref 解耦。 #[serde(default)] pub update_channel: UpdateChannel, + /// 是否由用户明确选择过更新渠道。旧版默认会把 Stable 写入配置,单看 + /// `update_channel` 无法区分默认值与主动切换;历史 Beta 则必然来自用户 opt-in。 + #[serde(default)] + pub update_channel_explicit: bool, /// 历史记录保留天数。0 = 不按时间清理(仅受 200 条上限)。默认 7 天。 /// 写入新条目时执行清理,避免后台轮询。 #[serde(default = "default_history_retention_days")] @@ -887,6 +891,8 @@ struct UserPreferencesWire { sherpa_onnx_keep_loaded_secs: u32, #[serde(default)] update_channel: UpdateChannel, + #[serde(default)] + update_channel_explicit: Option, #[serde(default = "default_history_retention_days")] history_retention_days: u32, #[serde(default = "default_polish_context_window_minutes")] @@ -1054,6 +1060,8 @@ impl Default for UserPreferencesWire { sherpa_onnx_language_hint: prefs.sherpa_onnx_language_hint, sherpa_onnx_keep_loaded_secs: prefs.sherpa_onnx_keep_loaded_secs, update_channel: prefs.update_channel, + // None 保留旧配置缺少标记的信息;反序列化时只有历史 Beta 视为显式选择。 + update_channel_explicit: None, history_retention_days: prefs.history_retention_days, polish_context_window_minutes: prefs.polish_context_window_minutes, start_minimized: prefs.start_minimized, @@ -1121,6 +1129,9 @@ impl<'de> Deserialize<'de> for UserPreferences { }; let (local_asr_active_model, local_whisper_active_model) = migrate_local_asr_models(wire.local_asr_active_model, wire.local_whisper_active_model); + let update_channel_explicit = wire + .update_channel_explicit + .unwrap_or(matches!(wire.update_channel, UpdateChannel::Beta)); Ok(Self { hotkey: wire.hotkey, @@ -1216,6 +1227,7 @@ impl<'de> Deserialize<'de> for UserPreferences { sherpa_onnx_language_hint: wire.sherpa_onnx_language_hint, sherpa_onnx_keep_loaded_secs: wire.sherpa_onnx_keep_loaded_secs, update_channel: wire.update_channel, + update_channel_explicit, history_retention_days: wire.history_retention_days, polish_context_window_minutes: wire.polish_context_window_minutes, start_minimized: wire.start_minimized, @@ -1556,6 +1568,7 @@ impl Default for UserPreferences { sherpa_onnx_language_hint: String::new(), sherpa_onnx_keep_loaded_secs: default_local_asr_keep_loaded_secs(), update_channel: UpdateChannel::default(), + update_channel_explicit: false, history_retention_days: default_history_retention_days(), polish_context_window_minutes: default_polish_context_window_minutes(), start_minimized: false, @@ -3118,6 +3131,32 @@ mod tests { assert!(!prefs.streaming_insert_save_clipboard); } + #[test] + fn update_channel_migration_preserves_only_explicit_legacy_beta_opt_in() { + let legacy_stable: UserPreferences = + serde_json::from_str(r#"{ "updateChannel": "stable" }"#).unwrap(); + assert_eq!(legacy_stable.update_channel, UpdateChannel::Stable); + assert!(!legacy_stable.update_channel_explicit); + + let legacy_beta: UserPreferences = + serde_json::from_str(r#"{ "updateChannel": "beta" }"#).unwrap(); + assert_eq!(legacy_beta.update_channel, UpdateChannel::Beta); + assert!(legacy_beta.update_channel_explicit); + + let explicit_stable: UserPreferences = serde_json::from_str( + r#"{ + "updateChannel": "stable", + "updateChannelExplicit": true + }"#, + ) + .unwrap(); + assert!(explicit_stable.update_channel_explicit); + + let round_trip: UserPreferences = + serde_json::from_str(&serde_json::to_string(&explicit_stable).unwrap()).unwrap(); + assert!(round_trip.update_channel_explicit); + } + #[test] fn paste_shortcut_round_trips_explicit_values() { for (raw, expected) in [ diff --git a/openless-all/app/src-tauri/Cargo.lock b/openless-all/app/src-tauri/Cargo.lock index 93621dfb6..46e9fe791 100644 --- a/openless-all/app/src-tauri/Cargo.lock +++ b/openless-all/app/src-tauri/Cargo.lock @@ -4241,6 +4241,7 @@ dependencies = [ "regex", "reqwest 0.12.28", "rustls", + "semver", "serde", "serde_json", "sha1", diff --git a/openless-all/app/src-tauri/Cargo.toml b/openless-all/app/src-tauri/Cargo.toml index 347d85a22..813854258 100644 --- a/openless-all/app/src-tauri/Cargo.toml +++ b/openless-all/app/src-tauri/Cargo.toml @@ -30,6 +30,7 @@ tauri-plugin-shell = "2.3.5" tauri-plugin-dialog = "2.7.2" serde = { version = "1", features = ["derive"] } serde_json = "1" +semver = "1" # OpenRouter ASR 把音频以标准 base64(带 padding)放进 JSON body(issue #582)。 base64 = "0.22" sha2 = "0.10" diff --git a/openless-all/app/src-tauri/src/android/updater_logic.rs b/openless-all/app/src-tauri/src/android/updater_logic.rs index 2fe833286..072d7b675 100644 --- a/openless-all/app/src-tauri/src/android/updater_logic.rs +++ b/openless-all/app/src-tauri/src/android/updater_logic.rs @@ -22,25 +22,13 @@ pub fn map_abi_to_arch(abi: &str) -> &'static str { } pub fn version_is_newer(remote: &str, current: &str) -> bool { - fn parts(v: &str) -> Vec { - v.split(|c| c == '.' || c == '-') - .filter_map(|p| p.parse().ok()) - .collect() - } - let remote_parts = parts(remote); - let current_parts = parts(current); - let max = remote_parts.len().max(current_parts.len()); - for i in 0..max { - let r = remote_parts.get(i).copied().unwrap_or(0); - let c = current_parts.get(i).copied().unwrap_or(0); - if r > c { - return true; - } - if r < c { - return false; - } - } - false + let (Ok(remote), Ok(current)) = ( + semver::Version::parse(remote), + semver::Version::parse(current), + ) else { + return false; + }; + remote > current } pub fn stable_manifest_urls(arch: &str) -> Vec { @@ -78,9 +66,12 @@ mod tests { } #[test] - fn version_is_newer_handles_beta_suffix() { - assert!(version_is_newer("1.3.8-1", "1.3.8")); - assert!(!version_is_newer("1.3.8", "1.3.8-1")); + fn version_is_newer_uses_semver_prerelease_ordering() { + assert!(version_is_newer("1.3.18", "1.3.18-Beta.7")); + assert!(version_is_newer("1.3.18-Beta.8", "1.3.18-Beta.7")); + assert!(!version_is_newer("1.3.18-Beta.7", "1.3.18-Beta.7")); + assert!(!version_is_newer("1.3.17", "1.3.18-Beta.7")); + assert!(!version_is_newer("not-a-version", "1.3.18-Beta.7")); } #[test] diff --git a/openless-all/app/src-tauri/src/commands/settings.rs b/openless-all/app/src-tauri/src/commands/settings.rs index 2e16703ab..1c2255373 100644 --- a/openless-all/app/src-tauri/src/commands/settings.rs +++ b/openless-all/app/src-tauri/src/commands/settings.rs @@ -2,7 +2,9 @@ use super::*; #[tauri::command] pub fn get_settings(core: CoreState<'_>) -> UserPreferences { - core.get_preferences() + let mut prefs = core.get_preferences(); + prefs.update_channel = effective_update_channel(None, &prefs, env!("CARGO_PKG_VERSION")); + prefs } #[tauri::command] @@ -143,8 +145,10 @@ impl openless_core::SettingsRuntime for TauriSettingsRuntime<'_> { } } -pub(crate) fn persist_settings(coord: &Coordinator, prefs: UserPreferences) -> Result<(), String> { - let _host_guard = coord.lock_settings_host(); +fn persist_settings_with_host_lock_held( + coord: &Coordinator, + prefs: UserPreferences, +) -> Result<(), String> { coord .backend() .update_settings( @@ -163,11 +167,22 @@ pub(crate) fn persist_settings(coord: &Coordinator, prefs: UserPreferences) -> R .map_err(|error| error.to_string()) } +fn persist_settings_preserving_update_channel( + coord: &Coordinator, + mut prefs: UserPreferences, +) -> Result<(), String> { + let _host_guard = coord.lock_settings_host(); + // 在同一把写锁内读取并回填,避免并发渠道切换被旧设置快照覆盖。 + preserve_update_channel_preferences(&mut prefs, &coord.backend().get_preferences()); + persist_settings_with_host_lock_held(coord, prefs) +} + pub(crate) fn persist_strict_settings( coord: &Coordinator, - prefs: UserPreferences, + mut prefs: UserPreferences, ) -> Result<(), String> { let _host_guard = coord.lock_settings_host(); + preserve_update_channel_preferences(&mut prefs, &coord.backend().get_preferences()); coord .backend() .update_settings( @@ -198,7 +213,7 @@ pub async fn set_settings( // 广播给所有 webview。issue #205:QaPanel 跑在独立 webview, // 没有 HotkeySettingsContext,必须靠事件感知录音键变化,否则面板可见时 // 用户改键会让浮窗里的 "{recordHotkey}" 文案一直停留在旧值。 - persist_settings(&*coord, prefs)?; + persist_settings_preserving_update_channel(&*coord, prefs)?; let prefs = coord.backend().get_preferences(); // 保存即同步胶囊样式原子:下一次录音的入场帧就携带新样式,不依赖 emit_capsule // 主线程闭包的 ~30Hz 同步(Windows 主线程拥塞时闭包延迟 → 整场显示旧样式)。 @@ -257,7 +272,7 @@ pub fn set_settings(coord: CoordinatorState<'_>, mut prefs: UserPreferences) -> .map_err(|e| e.to_string())?; sync_style_pack_preferences(&mut prefs, &packs); prefs.android_overlay_trigger = prefs.android_overlay_trigger.normalized(); - persist_settings(&*coord, prefs)?; + persist_settings_preserving_update_channel(&*coord, prefs)?; let prefs = coord.backend().get_preferences(); // 保存即同步胶囊样式原子(Android 通知胶囊 payload 同源,见 emit_capsule)。 coord.sync_capsule_style_from_preferences(); @@ -297,6 +312,63 @@ mod tests { ); assert_eq!(stale_settings_payload.default_mode, PolishMode::Light); } + + #[test] + fn update_channel_defaults_to_build_channel_until_user_selects_one() { + let mut prefs = UserPreferences::default(); + + assert_eq!( + effective_update_channel(None, &prefs, "2.0.0-Beta.1"), + UpdateChannel::Beta + ); + assert_eq!( + effective_update_channel(None, &prefs, "2.0.0"), + UpdateChannel::Stable + ); + let legacy_beta = UserPreferences { + update_channel: UpdateChannel::Beta, + update_channel_explicit: true, + ..UserPreferences::default() + }; + assert_eq!( + effective_update_channel(None, &legacy_beta, "2.0.0"), + UpdateChannel::Beta + ); + assert_eq!( + effective_update_channel(Some(UpdateChannel::Stable), &prefs, "2.0.0-Beta.1"), + UpdateChannel::Stable + ); + + assert!(select_update_channel(&mut prefs, UpdateChannel::Stable)); + assert!(prefs.update_channel_explicit); + assert_eq!( + effective_update_channel(None, &prefs, "2.0.0-Beta.1"), + UpdateChannel::Stable + ); + assert!(!select_update_channel(&mut prefs, UpdateChannel::Stable)); + assert!(select_update_channel(&mut prefs, UpdateChannel::Beta)); + assert_eq!(prefs.update_channel, UpdateChannel::Beta); + assert!(prefs.update_channel_explicit); + } + + #[test] + fn general_settings_save_preserves_dedicated_update_channel_fields() { + let current = UserPreferences { + update_channel: UpdateChannel::Stable, + update_channel_explicit: true, + ..UserPreferences::default() + }; + let mut stale_payload = UserPreferences { + update_channel: UpdateChannel::Beta, + update_channel_explicit: false, + ..UserPreferences::default() + }; + + preserve_update_channel_preferences(&mut stale_payload, ¤t); + + assert_eq!(stale_payload.update_channel, UpdateChannel::Stable); + assert!(stale_payload.update_channel_explicit); + } } // ─────────────────────────── release channel (Beta opt-in) ─────────────────────────── @@ -312,9 +384,38 @@ mod tests { // (Beta tag 的 manifest 文件名带 `-beta` 后缀,跟 Stable manifest 在 GitHub // Release assets 里物理分离)。 +fn effective_update_channel( + requested: Option, + prefs: &UserPreferences, + app_version: &str, +) -> UpdateChannel { + requested.unwrap_or_else(|| { + if prefs.update_channel_explicit { + prefs.update_channel + } else if app_version.contains('-') { + UpdateChannel::Beta + } else { + UpdateChannel::Stable + } + }) +} + +fn select_update_channel(prefs: &mut UserPreferences, channel: UpdateChannel) -> bool { + let changed = prefs.update_channel != channel || !prefs.update_channel_explicit; + prefs.update_channel = channel; + prefs.update_channel_explicit = true; + changed +} + +fn preserve_update_channel_preferences(incoming: &mut UserPreferences, current: &UserPreferences) { + incoming.update_channel = current.update_channel; + incoming.update_channel_explicit = current.update_channel_explicit; +} + #[tauri::command] pub fn get_update_channel(core: CoreState<'_>) -> UpdateChannel { - core.get_preferences().update_channel + let prefs = core.get_preferences(); + effective_update_channel(None, &prefs, env!("CARGO_PKG_VERSION")) } #[tauri::command] @@ -322,12 +423,13 @@ pub fn set_update_channel( coord: CoordinatorState<'_>, channel: UpdateChannel, ) -> Result<(), String> { + // 渠道读取和持久化必须同属一个写临界区,避免反向覆盖并发常规设置。 + let _host_guard = coord.lock_settings_host(); let mut prefs = coord.backend().get_preferences(); - if prefs.update_channel == channel { + if !select_update_channel(&mut prefs, channel) { return Ok(()); } - prefs.update_channel = channel; - persist_settings(&*coord, prefs)?; + persist_settings_with_host_lock_held(&*coord, prefs)?; Ok(()) } @@ -465,7 +567,7 @@ pub struct AppUpdateMetadata { /// 决定 manifest 来源后走 plugin-updater 的标准 check 流程。 /// 渠道:显式传入 `channel` 时用它(关于页固定查 Stable、高级页 Beta 区查 Beta); -/// 不传则回落到 `prefs.update_channel`(后台 AutoUpdateGate 自动检查走这条)。 +/// 不传则使用用户明确选择的渠道;尚未选择时跟随当前构建类型。 /// 返回 None = 当前是最新;Some(metadata) = 有新版可装。 #[tauri::command] #[cfg(not(mobile))] @@ -477,7 +579,8 @@ pub async fn app_check_update_with_channel( ) -> Result, String> { use tauri_plugin_updater::UpdaterExt; - let channel = channel.unwrap_or_else(|| coord.backend().get_preferences().update_channel); + let prefs = coord.backend().get_preferences(); + let channel = effective_update_channel(channel, &prefs, env!("CARGO_PKG_VERSION")); let mut builder = webview.updater_builder(); if let Some(ms) = timeout_ms { builder = builder.timeout(std::time::Duration::from_millis(ms)); @@ -541,7 +644,8 @@ pub async fn app_check_update_with_channel( ) -> Result, String> { #[cfg(target_os = "android")] { - let channel = channel.unwrap_or_else(|| coord.backend().get_preferences().update_channel); + let prefs = coord.backend().get_preferences(); + let channel = effective_update_channel(channel, &prefs, env!("CARGO_PKG_VERSION")); return crate::android::updater::check_update(channel).await; } #[cfg(not(target_os = "android"))] diff --git a/openless-all/app/src/components/AutoUpdate.tsx b/openless-all/app/src/components/AutoUpdate.tsx index 11faeead8..ee06ee61b 100644 --- a/openless-all/app/src/components/AutoUpdate.tsx +++ b/openless-all/app/src/components/AutoUpdate.tsx @@ -19,9 +19,11 @@ import { logClientError, openExternal, restartApp, + setUpdateChannel, type AppUpdateMetadata, type UpdateChannel, } from '../lib/ipc'; +import { isStableChannelSwitch } from '../lib/appVersion'; import { Btn } from '../pages/_atoms'; const UPDATE_CHECK_TIMEOUT_MS = 15_000; @@ -50,6 +52,7 @@ export type CheckUpdateOptions = { export interface UseAutoUpdate { status: UpdateStatus; + currentVersion: string; version: string; progress: number | null; downloaded: number; @@ -72,6 +75,7 @@ export function useAutoUpdate(): UseAutoUpdate { const updateRef = useRef(null); const androidUpdateRef = useRef(null); const [status, setStatus] = useState('idle'); + const [currentVersion, setCurrentVersion] = useState(''); const [version, setVersion] = useState(''); const [downloaded, setDownloaded] = useState(0); const [contentLength, setContentLength] = useState(null); @@ -140,6 +144,7 @@ export function useAutoUpdate(): UseAutoUpdate { const checkForUpdates = async (channel?: UpdateChannel, options?: CheckUpdateOptions) => { setStatus('checking'); + setCurrentVersion(''); setVersion(''); setErrorMessage(null); resetProgress(); @@ -157,6 +162,7 @@ export function useAutoUpdate(): UseAutoUpdate { setStatus('none'); return; } + setCurrentVersion(metadata.currentVersion); if (isAndroid()) { storeAndroidMetadata(metadata); setVersion(metadata.version); @@ -203,12 +209,18 @@ export function useAutoUpdate(): UseAutoUpdate { }; const installUpdate = async () => { + const persistStableChannelSwitch = () => + isStableChannelSwitch(currentVersion, version) + ? setUpdateChannel('stable') + : Promise.resolve(); + if (isAndroid()) { const payload = androidUpdateRef.current; if (!payload) return; resetProgress(); setStatus('downloading'); try { + await persistStableChannelSwitch(); await appDownloadAndInstallAndroidUpdate(payload); androidUpdateRef.current = null; setStatus('downloaded'); @@ -227,6 +239,7 @@ export function useAutoUpdate(): UseAutoUpdate { resetProgress(); setStatus('downloading'); try { + await persistStableChannelSwitch(); await update.download((event: DownloadEvent) => { if (event.event === 'Started') { resetProgress(); @@ -255,12 +268,14 @@ export function useAutoUpdate(): UseAutoUpdate { if (busy) return; await closeUpdate(); setStatus('idle'); + setCurrentVersion(''); setVersion(''); resetProgress(); }; return { status, + currentVersion, version, progress, downloaded, @@ -280,6 +295,7 @@ export function isDialogStatus(status: UpdateStatus): status is 'available' | 'd export function UpdateDialog({ status, + currentVersion, version, progress, downloaded, @@ -289,6 +305,7 @@ export function UpdateDialog({ onClose, }: { status: 'available' | 'downloading' | 'installing' | 'downloaded' | 'installError'; + currentVersion: string; version: string; progress: number | null; downloaded: number; @@ -302,6 +319,8 @@ export function UpdateDialog({ const installing = status === 'installing'; const installError = status === 'installError'; const androidInstalled = isAndroid() && status === 'downloaded'; + const switchingToStable = status === 'available' + && isStableChannelSwitch(currentVersion, version); // Portal 到 document.body:WindowChrome / 设置弹窗带常驻 transform + will-change, // 会创建 containing block——`position: fixed` 的遮罩会相对设置面板定位,只压暗 // 白色内容区(侧边栏深色看不出,形成「内容变灰、断层感」,见 Modal.tsx 同款注释)。 @@ -309,13 +328,17 @@ export function UpdateDialog({ return createPortal(
-
{t(`settings.about.updateDialog.${status}.title`)}
+
+ {t(`settings.about.updateDialog.${switchingToStable ? 'stableChannelSwitch' : status}.title`)} +
{androidInstalled ? t('settings.about.updateDialog.androidInstalled.desc', { version, defaultValue: '系统安装器已打开,请按提示完成安装。安装后重新打开 OpenLess 即可使用 {{version}}。' }) : installError ? t('settings.about.updateDialog.installError.desc', { error: errorMessage || t('settings.about.updateError') }) - : t(`settings.about.updateDialog.${status}.desc`, { version })} + : switchingToStable + ? t('settings.about.updateDialog.stableChannelSwitch.desc', { currentVersion, version }) + : t(`settings.about.updateDialog.${status}.desc`, { version })}
{(downloading || installing || status === 'downloaded') && (
diff --git a/openless-all/app/src/components/AutoUpdateGate.tsx b/openless-all/app/src/components/AutoUpdateGate.tsx index e4051d9d2..b527318e4 100644 --- a/openless-all/app/src/components/AutoUpdateGate.tsx +++ b/openless-all/app/src/components/AutoUpdateGate.tsx @@ -53,6 +53,7 @@ export function AutoUpdateGate() { return ( + currentVersion.includes('-') && !targetVersion.includes('-'); diff --git a/openless-all/app/src/lib/ipc/mock-data.ts b/openless-all/app/src/lib/ipc/mock-data.ts index 28d1118cf..7e0735ff2 100644 --- a/openless-all/app/src/lib/ipc/mock-data.ts +++ b/openless-all/app/src/lib/ipc/mock-data.ts @@ -109,6 +109,7 @@ export let mockSettings: UserPreferences = { startMinimized: false, themeMode: "system", updateChannel: "stable", + updateChannelExplicit: false, streamingInsert: true, streamingInsertDefaultMigrated: true, streamingInsertSaveClipboard: true, diff --git a/openless-all/app/src/lib/types.ts b/openless-all/app/src/lib/types.ts index 97f6c2997..91e964495 100644 --- a/openless-all/app/src/lib/types.ts +++ b/openless-all/app/src/lib/types.ts @@ -256,8 +256,7 @@ export interface WindowsImeStatus { dllPath: string | null; } -/** 后台自动更新渠道。stable = 查正式版 manifest(默认);beta = 查 - * latest-android-{arch}-beta.json。手动「检查正式版/Beta 更新」按钮不受此字段影响。 */ +/** 后台自动更新渠道。未明确选择时跟随构建类型;手动检查按钮不受此字段影响。 */ export type UpdateChannel = 'stable' | 'beta'; export type ThemeMode = 'system' | 'light' | 'dark'; @@ -482,9 +481,11 @@ export interface UserPreferences { startMinimized: boolean; /** UI theme preference: follow OS, light, or dark. */ themeMode: ThemeMode; - /** 后台自动更新渠道。stable(默认)= AutoUpdateGate 查正式版 manifest; - * beta = 查 Beta manifest。About / Advanced 的手动检查按钮各自固定 stable/beta。 */ + /** 后台自动更新渠道。用户未明确选择时跟随当前构建类型; + * About / Advanced 的手动检查按钮各自固定 stable/beta。 */ updateChannel: UpdateChannel; + /** 是否由用户明确选择过更新渠道;缺失时由当前构建类型决定默认渠道。 */ + updateChannelExplicit?: boolean; /** 流式输入:润色 SSE 一边到达一边逐字模拟键盘事件输出到当前焦点。开启后用户感知到 * 的处理时延显著降低。v1 限定 macOS + OpenAI-compatible provider,其他配置自动回落 * 到原一次性插入。默认 true。 */ diff --git a/openless-all/app/src/pages/settings/BetaChannelSection.tsx b/openless-all/app/src/pages/settings/BetaChannelSection.tsx index 02f341e88..ea3958ec0 100644 --- a/openless-all/app/src/pages/settings/BetaChannelSection.tsx +++ b/openless-all/app/src/pages/settings/BetaChannelSection.tsx @@ -13,6 +13,7 @@ import { CheckUpdateButton } from './CheckUpdateButton'; export function BetaChannelSection() { const { t } = useTranslation(); const [channel, setChannel] = useState('stable'); + const [autoCheckChannel, setAutoCheckChannel] = useState(null); const [platformCaps, setPlatformCaps] = useState(null); useEffect(() => { @@ -34,7 +35,9 @@ export function BetaChannelSection() { await setUpdateChannel(target); } catch { setChannel(target === 'beta' ? 'stable' : 'beta'); + return; } + setAutoCheckChannel(target); }; if (platformCaps?.supportsAutoUpdate !== true) return null; @@ -49,7 +52,7 @@ export function BetaChannelSection() {
- +
); diff --git a/openless-all/app/src/pages/settings/CheckUpdateButton.tsx b/openless-all/app/src/pages/settings/CheckUpdateButton.tsx index 4d5587791..a799a5dce 100644 --- a/openless-all/app/src/pages/settings/CheckUpdateButton.tsx +++ b/openless-all/app/src/pages/settings/CheckUpdateButton.tsx @@ -8,7 +8,15 @@ import { Icon } from '../../components/Icon'; import { isDialogStatus, UpdateDialog, useAutoUpdate } from '../../components/AutoUpdate'; import type { UpdateChannel } from '../../lib/ipc'; -export function CheckUpdateButton({ channel, compact = false }: { channel: UpdateChannel; compact?: boolean }) { +export function CheckUpdateButton({ + channel, + compact = false, + autoCheckChannel, +}: { + channel: UpdateChannel; + compact?: boolean; + autoCheckChannel?: UpdateChannel | null; +}) { const { t } = useTranslation(); const updater = useAutoUpdate(); const { status, checking, busy } = updater; @@ -31,6 +39,12 @@ export function CheckUpdateButton({ channel, compact = false }: { channel: Updat : 'settings.about.checkStableUpdateBtn'; const label = checking ? t('settings.about.checkingUpdate') : t(labelKey); + useEffect(() => { + if (autoCheckChannel) void updater.checkForUpdates(autoCheckChannel); + // checkForUpdates changes with updater state; this effect is driven only by a channel switch. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [autoCheckChannel]); + return ( <>