diff --git a/openless-all/app/src-tauri/src/commands.rs b/openless-all/app/src-tauri/src/commands.rs index 10deede9..f3cd82d1 100644 --- a/openless-all/app/src-tauri/src/commands.rs +++ b/openless-all/app/src-tauri/src/commands.rs @@ -1879,8 +1879,12 @@ pub fn set_qa_hotkey( if let Some(binding) = binding.as_ref() { reject_dictation_qa_hotkey_overlap(&prefs.dictation_hotkey, binding)?; reject_qa_translation_hotkey_overlap(binding, &prefs.translation_hotkey)?; - reject_qa_switch_style_hotkey_overlap(binding, &prefs.switch_style_hotkey)?; - reject_qa_open_app_hotkey_overlap(binding, &prefs.open_app_hotkey)?; + if let Some(switch_style) = prefs.switch_style_hotkey.as_ref() { + reject_qa_switch_style_hotkey_overlap(binding, switch_style)?; + } + if let Some(open_app) = prefs.open_app_hotkey.as_ref() { + reject_qa_open_app_hotkey_overlap(binding, open_app)?; + } } prefs.qa_hotkey = binding; coord.prefs().set(prefs).map_err(|e| e.to_string())?; @@ -1920,8 +1924,12 @@ pub fn set_dictation_hotkey( reject_dictation_qa_hotkey_overlap(&binding, qa_hotkey)?; } reject_dictation_translation_hotkey_overlap(&binding, &prefs.translation_hotkey)?; - reject_dictation_switch_style_hotkey_overlap(&binding, &prefs.switch_style_hotkey)?; - reject_dictation_open_app_hotkey_overlap(&binding, &prefs.open_app_hotkey)?; + if let Some(switch_style) = prefs.switch_style_hotkey.as_ref() { + reject_dictation_switch_style_hotkey_overlap(&binding, switch_style)?; + } + if let Some(open_app) = prefs.open_app_hotkey.as_ref() { + reject_dictation_open_app_hotkey_overlap(&binding, open_app)?; + } prefs.dictation_hotkey = binding; sync_dictation_hotkey_legacy_fields(&mut prefs); coord.prefs().set(prefs).map_err(|e| e.to_string())?; @@ -1941,8 +1949,12 @@ pub fn set_translation_hotkey( if let Some(qa_hotkey) = previous.qa_hotkey.as_ref() { reject_qa_translation_hotkey_overlap(qa_hotkey, &binding)?; } - reject_translation_switch_style_hotkey_overlap(&binding, &previous.switch_style_hotkey)?; - reject_translation_open_app_hotkey_overlap(&binding, &previous.open_app_hotkey)?; + if let Some(switch_style) = previous.switch_style_hotkey.as_ref() { + reject_translation_switch_style_hotkey_overlap(&binding, switch_style)?; + } + if let Some(open_app) = previous.open_app_hotkey.as_ref() { + reject_translation_open_app_hotkey_overlap(&binding, open_app)?; + } let mut prefs = previous.clone(); prefs.translation_hotkey = binding; coord.prefs().set(prefs).map_err(|e| e.to_string())?; @@ -1956,40 +1968,55 @@ pub fn set_translation_hotkey( Ok(()) } +/// 设置「切换风格」全局快捷键。`binding == None`(前端传 null)= 停用:清空绑定并 +/// 反注册全局键。镜像 `set_qa_hotkey` 的 `Option=None` 停用模式(issue #576)。 #[tauri::command] pub fn set_switch_style_hotkey( coord: CoordinatorState<'_>, - binding: ShortcutBinding, + binding: Option, ) -> Result<(), String> { - crate::shortcut_binding::validate_binding(&binding).map_err(|e| e.to_string())?; - reject_modifier_only_action_shortcut(&binding)?; + if let Some(binding) = binding.as_ref() { + crate::shortcut_binding::validate_binding(binding).map_err(|e| e.to_string())?; + reject_modifier_only_action_shortcut(binding)?; + } let mut prefs = coord.prefs().get(); - reject_dictation_switch_style_hotkey_overlap(&prefs.dictation_hotkey, &binding)?; - reject_translation_switch_style_hotkey_overlap(&prefs.translation_hotkey, &binding)?; - if let Some(qa_hotkey) = prefs.qa_hotkey.as_ref() { - reject_qa_switch_style_hotkey_overlap(qa_hotkey, &binding)?; + if let Some(binding) = binding.as_ref() { + reject_dictation_switch_style_hotkey_overlap(&prefs.dictation_hotkey, binding)?; + reject_translation_switch_style_hotkey_overlap(&prefs.translation_hotkey, binding)?; + if let Some(qa_hotkey) = prefs.qa_hotkey.as_ref() { + reject_qa_switch_style_hotkey_overlap(qa_hotkey, binding)?; + } + if let Some(open_app) = prefs.open_app_hotkey.as_ref() { + reject_switch_style_open_app_hotkey_overlap(binding, open_app)?; + } } - reject_switch_style_open_app_hotkey_overlap(&binding, &prefs.open_app_hotkey)?; prefs.switch_style_hotkey = binding; coord.prefs().set(prefs).map_err(|e| e.to_string())?; coord.update_switch_style_hotkey_binding(); Ok(()) } +/// 设置「唤起 App」全局快捷键。`binding == None`(前端传 null)= 停用(同上)。 #[tauri::command] pub fn set_open_app_hotkey( coord: CoordinatorState<'_>, - binding: ShortcutBinding, + binding: Option, ) -> Result<(), String> { - crate::shortcut_binding::validate_binding(&binding).map_err(|e| e.to_string())?; - reject_modifier_only_action_shortcut(&binding)?; + if let Some(binding) = binding.as_ref() { + crate::shortcut_binding::validate_binding(binding).map_err(|e| e.to_string())?; + reject_modifier_only_action_shortcut(binding)?; + } let mut prefs = coord.prefs().get(); - reject_dictation_open_app_hotkey_overlap(&prefs.dictation_hotkey, &binding)?; - reject_translation_open_app_hotkey_overlap(&prefs.translation_hotkey, &binding)?; - if let Some(qa_hotkey) = prefs.qa_hotkey.as_ref() { - reject_qa_open_app_hotkey_overlap(qa_hotkey, &binding)?; + if let Some(binding) = binding.as_ref() { + reject_dictation_open_app_hotkey_overlap(&prefs.dictation_hotkey, binding)?; + reject_translation_open_app_hotkey_overlap(&prefs.translation_hotkey, binding)?; + if let Some(qa_hotkey) = prefs.qa_hotkey.as_ref() { + reject_qa_open_app_hotkey_overlap(qa_hotkey, binding)?; + } + if let Some(switch_style) = prefs.switch_style_hotkey.as_ref() { + reject_switch_style_open_app_hotkey_overlap(switch_style, binding)?; + } } - reject_switch_style_open_app_hotkey_overlap(&prefs.switch_style_hotkey, &binding)?; prefs.open_app_hotkey = binding; coord.prefs().set(prefs).map_err(|e| e.to_string())?; coord.update_open_app_hotkey_binding(); @@ -2030,8 +2057,12 @@ pub fn set_combo_hotkey(coord: CoordinatorState<'_>, binding: ComboBinding) -> R reject_dictation_qa_hotkey_overlap(&shortcut, qa_hotkey)?; } reject_dictation_translation_hotkey_overlap(&shortcut, &prefs.translation_hotkey)?; - reject_dictation_switch_style_hotkey_overlap(&shortcut, &prefs.switch_style_hotkey)?; - reject_dictation_open_app_hotkey_overlap(&shortcut, &prefs.open_app_hotkey)?; + if let Some(switch_style) = prefs.switch_style_hotkey.as_ref() { + reject_dictation_switch_style_hotkey_overlap(&shortcut, switch_style)?; + } + if let Some(open_app) = prefs.open_app_hotkey.as_ref() { + reject_dictation_open_app_hotkey_overlap(&shortcut, open_app)?; + } prefs.custom_combo_hotkey = Some(binding); prefs.dictation_hotkey = shortcut; sync_dictation_hotkey_legacy_fields(&mut prefs); @@ -2088,30 +2119,34 @@ fn reject_hotkey_overlap( } fn reject_hotkey_collisions(prefs: &UserPreferences) -> Result<(), String> { + // 停用(None)的 action 快捷键不参与任何冲突检测。 + let switch_style = prefs.switch_style_hotkey.as_ref(); + let open_app = prefs.open_app_hotkey.as_ref(); if let Some(qa_hotkey) = prefs.qa_hotkey.as_ref() { reject_dictation_qa_hotkey_overlap(&prefs.dictation_hotkey, qa_hotkey)?; reject_qa_translation_hotkey_overlap(qa_hotkey, &prefs.translation_hotkey)?; - reject_qa_switch_style_hotkey_overlap(qa_hotkey, &prefs.switch_style_hotkey)?; - reject_qa_open_app_hotkey_overlap(qa_hotkey, &prefs.open_app_hotkey)?; + if let Some(switch_style) = switch_style { + reject_qa_switch_style_hotkey_overlap(qa_hotkey, switch_style)?; + } + if let Some(open_app) = open_app { + reject_qa_open_app_hotkey_overlap(qa_hotkey, open_app)?; + } } reject_dictation_translation_hotkey_overlap( &prefs.dictation_hotkey, &prefs.translation_hotkey, )?; - reject_dictation_switch_style_hotkey_overlap( - &prefs.dictation_hotkey, - &prefs.switch_style_hotkey, - )?; - reject_dictation_open_app_hotkey_overlap(&prefs.dictation_hotkey, &prefs.open_app_hotkey)?; - reject_translation_switch_style_hotkey_overlap( - &prefs.translation_hotkey, - &prefs.switch_style_hotkey, - )?; - reject_translation_open_app_hotkey_overlap(&prefs.translation_hotkey, &prefs.open_app_hotkey)?; - reject_switch_style_open_app_hotkey_overlap( - &prefs.switch_style_hotkey, - &prefs.open_app_hotkey, - )?; + if let Some(switch_style) = switch_style { + reject_dictation_switch_style_hotkey_overlap(&prefs.dictation_hotkey, switch_style)?; + reject_translation_switch_style_hotkey_overlap(&prefs.translation_hotkey, switch_style)?; + } + if let Some(open_app) = open_app { + reject_dictation_open_app_hotkey_overlap(&prefs.dictation_hotkey, open_app)?; + reject_translation_open_app_hotkey_overlap(&prefs.translation_hotkey, open_app)?; + } + if let (Some(switch_style), Some(open_app)) = (switch_style, open_app) { + reject_switch_style_open_app_hotkey_overlap(switch_style, open_app)?; + } Ok(()) } @@ -4064,14 +4099,14 @@ mod tests { primary: "T".to_string(), modifiers: vec!["ctrl".to_string(), "alt".to_string()], }, - switch_style_hotkey: ShortcutBinding { + switch_style_hotkey: Some(ShortcutBinding { primary: "S".to_string(), modifiers: vec!["ctrl".to_string(), "alt".to_string()], - }, - open_app_hotkey: ShortcutBinding { + }), + open_app_hotkey: Some(ShortcutBinding { primary: "O".to_string(), modifiers: vec!["ctrl".to_string(), "alt".to_string()], - }, + }), hotkey: HotkeyBinding { trigger: HotkeyTrigger::Custom, mode: HotkeyMode::Hold, @@ -4488,7 +4523,7 @@ mod tests { }; let prefs = UserPreferences { translation_hotkey: binding.clone(), - switch_style_hotkey: binding, + switch_style_hotkey: Some(binding), ..Default::default() }; @@ -4507,8 +4542,8 @@ mod tests { modifiers: vec!["cmd".into(), "shift".into()], }; let prefs = UserPreferences { - switch_style_hotkey: binding.clone(), - open_app_hotkey: binding, + switch_style_hotkey: Some(binding.clone()), + open_app_hotkey: Some(binding), ..Default::default() }; diff --git a/openless-all/app/src-tauri/src/coordinator.rs b/openless-all/app/src-tauri/src/coordinator.rs index 72ce6dce..a7b12cef 100644 --- a/openless-all/app/src-tauri/src/coordinator.rs +++ b/openless-all/app/src-tauri/src/coordinator.rs @@ -728,7 +728,12 @@ impl Coordinator { } fn update_action_hotkey_binding(&self, kind: ActionHotkeyKind) { - let binding = action_hotkey_binding(&self.inner, kind); + // None = 用户主动停用:反注册全局键,立即生效。 + let Some(binding) = action_hotkey_binding(&self.inner, kind) else { + take_action_hotkey_on_main_thread(&self.inner, kind); + log::info!("[coord] action hotkey {kind:?} 已停用(用户清空)"); + return; + }; if is_modifier_only_shortcut(&binding) { take_action_hotkey_on_main_thread(&self.inner, kind); log::warn!("[coord] action hotkey {kind:?} 使用了不支持的 modifier-only 绑定,已关闭"); @@ -1517,7 +1522,12 @@ fn action_hotkey_supervisor_loop(inner: Arc, kind: ActionHotkeyKind) { if inner.shutdown.load(Ordering::SeqCst) { return; } - let binding = action_hotkey_binding(&inner, kind); + // None = 用户主动停用:反注册并睡着等 prefs 改动(由 update 路径唤醒)。 + let Some(binding) = action_hotkey_binding(&inner, kind) else { + take_action_hotkey_on_main_thread(&inner, kind); + std::thread::sleep(std::time::Duration::from_secs(5)); + continue; + }; if is_modifier_only_shortcut(&binding) { take_action_hotkey_on_main_thread(&inner, kind); std::thread::sleep(std::time::Duration::from_secs(5)); @@ -1711,7 +1721,7 @@ fn action_hotkey_slot( fn action_hotkey_binding( inner: &Arc, kind: ActionHotkeyKind, -) -> crate::types::ShortcutBinding { +) -> Option { let prefs = inner.prefs.get(); match kind { ActionHotkeyKind::SwitchStyle => prefs.switch_style_hotkey, @@ -1868,17 +1878,21 @@ fn reset_shortcut_held_state(inner: &Arc) { } } } - if !is_modifier_only_shortcut(&prefs.switch_style_hotkey) { - if let Some(monitor) = inner.switch_style_hotkey.lock().as_ref() { - if let Err(e) = monitor.update_binding(prefs.switch_style_hotkey.clone()) { - log::warn!("[coord] reset switch-style hotkey latch failed: {e}"); + if let Some(switch_style) = prefs.switch_style_hotkey.as_ref() { + if !is_modifier_only_shortcut(switch_style) { + if let Some(monitor) = inner.switch_style_hotkey.lock().as_ref() { + if let Err(e) = monitor.update_binding(switch_style.clone()) { + log::warn!("[coord] reset switch-style hotkey latch failed: {e}"); + } } } } - if !is_modifier_only_shortcut(&prefs.open_app_hotkey) { - if let Some(monitor) = inner.open_app_hotkey.lock().as_ref() { - if let Err(e) = monitor.update_binding(prefs.open_app_hotkey.clone()) { - log::warn!("[coord] reset open-app hotkey latch failed: {e}"); + if let Some(open_app) = prefs.open_app_hotkey.as_ref() { + if !is_modifier_only_shortcut(open_app) { + if let Some(monitor) = inner.open_app_hotkey.lock().as_ref() { + if let Err(e) = monitor.update_binding(open_app.clone()) { + log::warn!("[coord] reset open-app hotkey latch failed: {e}"); + } } } } diff --git a/openless-all/app/src-tauri/src/types.rs b/openless-all/app/src-tauri/src/types.rs index abf94245..5f8c16a3 100644 --- a/openless-all/app/src-tauri/src/types.rs +++ b/openless-all/app/src-tauri/src/types.rs @@ -604,10 +604,13 @@ pub struct UserPreferences { pub custom_combo_hotkey: Option, #[serde(default = "default_translation_hotkey")] pub translation_hotkey: ShortcutBinding, + /// 「切换风格」全局快捷键。`None` = 停用(不注册全局键);`Some(...)` = 注册。 + /// 默认 `Some(默认键)`,对老用户零行为变化,仅新增可清空(issue #576)。 #[serde(default = "default_switch_style_hotkey")] - pub switch_style_hotkey: ShortcutBinding, + pub switch_style_hotkey: Option, + /// 「唤起 App」全局快捷键。`None` = 停用;`Some(...)` = 注册。默认 `Some(默认键)`。 #[serde(default = "default_open_app_hotkey")] - pub open_app_hotkey: ShortcutBinding, + pub open_app_hotkey: Option, /// 本地 Qwen3-ASR 当前激活的模型 id("qwen3-asr-0.6b" / "qwen3-asr-1.7b")。 /// 仅在 active_asr_provider == "local-qwen3" 时有意义。 #[serde(default = "default_local_asr_model")] @@ -886,8 +889,9 @@ impl Default for UserPreferencesWire { qa_save_history: prefs.qa_save_history, custom_combo_hotkey: prefs.custom_combo_hotkey, translation_hotkey: None, - switch_style_hotkey: None, - open_app_hotkey: None, + // 默认携带默认键(Some),保证缺字段时仍是启用状态;None 专表「用户主动停用」。 + switch_style_hotkey: prefs.switch_style_hotkey, + open_app_hotkey: prefs.open_app_hotkey, local_asr_active_model: prefs.local_asr_active_model, local_asr_mirror: prefs.local_asr_mirror, local_asr_keep_loaded_secs: prefs.local_asr_keep_loaded_secs, @@ -968,10 +972,11 @@ impl<'de> Deserialize<'de> for UserPreferences { translation_hotkey: wire .translation_hotkey .unwrap_or_else(default_translation_hotkey), - switch_style_hotkey: wire - .switch_style_hotkey - .unwrap_or_else(default_switch_style_hotkey), - open_app_hotkey: wire.open_app_hotkey.unwrap_or_else(default_open_app_hotkey), + // 直传 Option:None = 用户主动停用,不再用 unwrap_or_else 塌缩成默认键 + // (那正是 #576「无法关闭」的根因)。缺字段时 wire 的 serde struct-default + // 会落到 Some(默认键),保证老用户/新用户仍是启用。 + switch_style_hotkey: wire.switch_style_hotkey, + open_app_hotkey: wire.open_app_hotkey, local_asr_active_model: wire.local_asr_active_model, local_asr_mirror: wire.local_asr_mirror, local_asr_keep_loaded_secs: wire.local_asr_keep_loaded_secs, @@ -1014,18 +1019,18 @@ fn default_translation_hotkey() -> ShortcutBinding { } } -fn default_switch_style_hotkey() -> ShortcutBinding { - ShortcutBinding { +fn default_switch_style_hotkey() -> Option { + Some(ShortcutBinding { primary: "S".into(), modifiers: default_app_shortcut_modifiers(), - } + }) } -fn default_open_app_hotkey() -> ShortcutBinding { - ShortcutBinding { +fn default_open_app_hotkey() -> Option { + Some(ShortcutBinding { primary: "O".into(), modifiers: default_app_shortcut_modifiers(), - } + }) } fn default_app_shortcut_modifiers() -> Vec { @@ -2320,6 +2325,53 @@ mod tests { assert!(!restored.audio_cue_on_record); } + #[test] + fn action_hotkeys_default_to_enabled() { + // issue #576:默认仍开启(Some 默认键),对老用户零行为变化。 + let prefs = UserPreferences::default(); + assert!(prefs.switch_style_hotkey.is_some()); + assert!(prefs.open_app_hotkey.is_some()); + } + + #[test] + fn missing_action_hotkeys_default_to_enabled() { + // 老用户/缺字段:wire 的 struct-default 落到 Some(默认键),不应被当成停用。 + let prefs: UserPreferences = serde_json::from_str("{}").unwrap(); + assert!(prefs.switch_style_hotkey.is_some()); + assert!(prefs.open_app_hotkey.is_some()); + } + + #[test] + fn disabled_action_hotkeys_round_trip_as_null() { + // issue #576:用户清空(None=停用)后存盘→读回必须仍是 None, + // 不能像旧逻辑那样被 unwrap_or_else 塌缩回默认键。 + let disabled = UserPreferences { + switch_style_hotkey: None, + open_app_hotkey: None, + ..Default::default() + }; + let json = serde_json::to_string(&disabled).unwrap(); + assert!( + json.contains("\"switchStyleHotkey\":null"), + "停用应序列化成 null,实际: {json}" + ); + let restored: UserPreferences = serde_json::from_str(&json).unwrap(); + assert!(restored.switch_style_hotkey.is_none()); + assert!(restored.open_app_hotkey.is_none()); + } + + #[test] + fn explicit_action_hotkey_binding_round_trips() { + // 旧 preferences.json 里带实际绑定 → 读回应保留为 Some(启用)。 + let prefs: UserPreferences = serde_json::from_str( + r#"{"switchStyleHotkey":{"primary":"S","modifiers":["cmd","shift"]}}"#, + ) + .unwrap(); + let binding = prefs.switch_style_hotkey.expect("应保留为 Some"); + assert_eq!(binding.primary, "S"); + assert_eq!(binding.modifiers, vec!["cmd".to_string(), "shift".to_string()]); + } + #[test] fn missing_custom_style_prompts_defaults_to_empty() { let prefs: UserPreferences = serde_json::from_str("{}").unwrap(); diff --git a/openless-all/app/src/i18n/en.ts b/openless-all/app/src/i18n/en.ts index 2957b4db..20c8aa74 100644 --- a/openless-all/app/src/i18n/en.ts +++ b/openless-all/app/src/i18n/en.ts @@ -684,6 +684,8 @@ export const en: typeof zhCN = { confirm: 'Confirm capsule insertion', switchStyle: 'Switch to previous style', openApp: 'Open OpenLess', + enable: 'Enable', + disable: 'Disable', confirmHint: 'Click ✓ on the capsule', notSupported: 'Not yet supported', }, diff --git a/openless-all/app/src/i18n/ja.ts b/openless-all/app/src/i18n/ja.ts index 4ca0a390..c6be55ab 100644 --- a/openless-all/app/src/i18n/ja.ts +++ b/openless-all/app/src/i18n/ja.ts @@ -686,6 +686,8 @@ export const ja: typeof zhCN = { confirm: 'カプセル入力を確定', switchStyle: '前のスタイルに切り替え', openApp: 'OpenLess を開く', + enable: '有効化', + disable: '無効化', confirmHint: '右側の ✓ をクリック', notSupported: '未対応', }, diff --git a/openless-all/app/src/i18n/ko.ts b/openless-all/app/src/i18n/ko.ts index 0b849849..53d41013 100644 --- a/openless-all/app/src/i18n/ko.ts +++ b/openless-all/app/src/i18n/ko.ts @@ -686,6 +686,8 @@ export const ko: typeof zhCN = { confirm: '캡슐 입력 확정', switchStyle: '이전 스타일로 전환', openApp: 'OpenLess 열기', + enable: '활성화', + disable: '비활성화', confirmHint: '오른쪽 ✓ 클릭', notSupported: '지원되지 않음', }, diff --git a/openless-all/app/src/i18n/zh-CN.ts b/openless-all/app/src/i18n/zh-CN.ts index ca387876..6cae44dd 100644 --- a/openless-all/app/src/i18n/zh-CN.ts +++ b/openless-all/app/src/i18n/zh-CN.ts @@ -682,6 +682,8 @@ export const zhCN = { confirm: '胶囊确认插入', switchStyle: '切换上一次风格', openApp: '打开 OpenLess', + enable: '启用', + disable: '停用', confirmHint: '点击右侧 ✓', notSupported: '暂未支持', }, diff --git a/openless-all/app/src/i18n/zh-TW.ts b/openless-all/app/src/i18n/zh-TW.ts index 043e71b9..d079e9d6 100644 --- a/openless-all/app/src/i18n/zh-TW.ts +++ b/openless-all/app/src/i18n/zh-TW.ts @@ -684,6 +684,8 @@ export const zhTW: typeof zhCN = { confirm: '膠囊確認插入', switchStyle: '切換上一次風格', openApp: '打開 OpenLess', + enable: '啟用', + disable: '停用', confirmHint: '點擊右側 ✓', notSupported: '暫未支持', }, diff --git a/openless-all/app/src/lib/hotkey.ts b/openless-all/app/src/lib/hotkey.ts index 208c0fa9..5e976930 100644 --- a/openless-all/app/src/lib/hotkey.ts +++ b/openless-all/app/src/lib/hotkey.ts @@ -12,6 +12,16 @@ export function defaultAppShortcutModifiers(): string[] { return currentPlatform().isMac ? ['cmd', 'shift'] : ['ctrl', 'shift']; } +// 「停用」后重新「启用」时恢复的默认键,与后端 default_switch_style_hotkey / +// default_open_app_hotkey 保持一致(issue #576)。 +export function defaultSwitchStyleShortcut(): ShortcutBinding { + return { primary: 'S', modifiers: defaultAppShortcutModifiers() }; +} + +export function defaultOpenAppShortcut(): ShortcutBinding { + return { primary: 'O', modifiers: defaultAppShortcutModifiers() }; +} + export function getHotkeyTriggerLabel(trigger: HotkeyTrigger | null | undefined): string { if (!trigger) return i18n.t('hotkey.fallback'); if (trigger === 'custom') return i18n.t('hotkey.triggers.custom'); diff --git a/openless-all/app/src/lib/ipc.ts b/openless-all/app/src/lib/ipc.ts index 842f103f..b3a249c8 100644 --- a/openless-all/app/src/lib/ipc.ts +++ b/openless-all/app/src/lib/ipc.ts @@ -1100,11 +1100,12 @@ export function setTranslationHotkey(binding: ShortcutBinding): Promise { return invokeOrMock("set_translation_hotkey", { binding }, () => undefined) } -export function setSwitchStyleHotkey(binding: ShortcutBinding): Promise { +// binding = null 表示停用(清空全局键),与 set_qa_hotkey 一致(issue #576)。 +export function setSwitchStyleHotkey(binding: ShortcutBinding | null): Promise { return invokeOrMock("set_switch_style_hotkey", { binding }, () => undefined) } -export function setOpenAppHotkey(binding: ShortcutBinding): Promise { +export function setOpenAppHotkey(binding: ShortcutBinding | null): Promise { return invokeOrMock("set_open_app_hotkey", { binding }, () => undefined) } diff --git a/openless-all/app/src/lib/types.ts b/openless-all/app/src/lib/types.ts index 6b1bbd86..9e1e660c 100644 --- a/openless-all/app/src/lib/types.ts +++ b/openless-all/app/src/lib/types.ts @@ -255,10 +255,10 @@ export interface UserPreferences { customComboHotkey: ComboBinding | null; /** 录音中触发翻译的全局快捷键。默认 Shift。 */ translationHotkey: ShortcutBinding; - /** 切换到上一个润色风格的全局快捷键。 */ - switchStyleHotkey: ShortcutBinding; - /** 打开 OpenLess 主窗口的全局快捷键。 */ - openAppHotkey: ShortcutBinding; + /** 切换到上一个润色风格的全局快捷键。null = 用户已停用(issue #576)。 */ + switchStyleHotkey: ShortcutBinding | null; + /** 打开 OpenLess 主窗口的全局快捷键。null = 用户已停用(issue #576)。 */ + openAppHotkey: ShortcutBinding | null; /** 本地 Qwen3-ASR 当前激活的模型 id。仅在 activeAsrProvider === 'local-qwen3' 时有意义。 */ localAsrActiveModel: string; /** 本地模型下载源镜像('huggingface' / 'hf-mirror')。 */ diff --git a/openless-all/app/src/pages/settings/ShortcutsSection.tsx b/openless-all/app/src/pages/settings/ShortcutsSection.tsx index e26b3f54..3f11fcfa 100644 --- a/openless-all/app/src/pages/settings/ShortcutsSection.tsx +++ b/openless-all/app/src/pages/settings/ShortcutsSection.tsx @@ -1,8 +1,9 @@ // 快捷键设置:开始/停止、翻译、问答、切风格、唤起 App、以及只读取消/确认提示。 +import type { CSSProperties } from 'react'; import { useTranslation } from 'react-i18next'; import { ShortcutRecorder } from '../../components/ShortcutRecorder'; -import { defaultQaShortcut } from '../../lib/hotkey'; +import { defaultOpenAppShortcut, defaultQaShortcut, defaultSwitchStyleShortcut } from '../../lib/hotkey'; import { setDictationHotkey, setOpenAppHotkey, @@ -15,6 +16,31 @@ import { Card } from '../_atoms'; import { SettingRow } from './shared'; import { detectOS } from '../../components/WindowChrome'; +const enableBtnStyle: CSSProperties = { + alignSelf: 'flex-start', + fontSize: 12, + padding: '5px 14px', + background: 'var(--ol-blue)', + color: '#fff', + border: 0, + borderRadius: 6, + fontFamily: 'inherit', + fontWeight: 500, + cursor: 'pointer', +}; + +const disableBtnStyle: CSSProperties = { + alignSelf: 'flex-start', + fontSize: 11, + padding: '3px 10px', + background: 'transparent', + color: 'var(--ol-ink-4)', + border: '0.5px solid var(--ol-line-strong)', + borderRadius: 6, + fontFamily: 'inherit', + cursor: 'pointer', +}; + export function ShortcutsSection() { const { t } = useTranslation(); const os = detectOS(); @@ -84,24 +110,72 @@ export function ShortcutsSection() { )} - { - await setSwitchStyleHotkey(binding); - await savePrefs({ ...prefs, switchStyleHotkey: binding }); - }} - /> + {prefs.switchStyleHotkey ? ( +
+ { + await setSwitchStyleHotkey(binding); + await savePrefs({ ...prefs, switchStyleHotkey: binding }); + }} + /> + +
+ ) : ( + + )}
- { - await setOpenAppHotkey(binding); - await savePrefs({ ...prefs, openAppHotkey: binding }); - }} - /> + {prefs.openAppHotkey ? ( +
+ { + await setOpenAppHotkey(binding); + await savePrefs({ ...prefs, openAppHotkey: binding }); + }} + /> + +
+ ) : ( + + )}
{readonlyRows.map(([k, v]) => (