Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions openless-all/app/src-tauri/src/coordinator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,10 @@ struct Inner {
/// 与 `hotkey_trigger_held` 互补 —— held 防 press-without-release,本字段防
/// press-release-press 三连过快。
last_hotkey_dispatch_at: Mutex<Option<std::time::Instant>>,
/// end_session 成功收尾后将 phase 设为 Idle 时记录的时间戳 + POST_SESSION_COOLDOWN_MS。
/// handle_pressed 在 (Toggle, Idle) 分支检查此字段:未过期则忽略该次按键,
/// 防止胶囊离场动画期间误激活新听写(issue #545)。
session_cooldown_until: Mutex<Option<std::time::Instant>>,
shortcut_recording_active: AtomicBool,
/// 自定义组合键监听器(global-hotkey crate)。当 `prefs.hotkey.trigger == Custom` 时
/// 代替 modifier-only 的 hotkey monitor。`None` 表示不使用自定义组合键或还没成功安装。
Expand Down Expand Up @@ -318,6 +322,7 @@ impl Coordinator {
hotkey_status: Mutex::new(HotkeyStatus::default()),
hotkey_trigger_held: AtomicBool::new(false),
last_hotkey_dispatch_at: Mutex::new(None),
session_cooldown_until: Mutex::new(None),
shortcut_recording_active: AtomicBool::new(false),
combo_hotkey: Mutex::new(None),
translation_hotkey: Mutex::new(None),
Expand Down Expand Up @@ -379,6 +384,7 @@ impl Coordinator {
hotkey_status: Mutex::new(HotkeyStatus::default()),
hotkey_trigger_held: AtomicBool::new(false),
last_hotkey_dispatch_at: Mutex::new(None),
session_cooldown_until: Mutex::new(None),
shortcut_recording_active: AtomicBool::new(false),
combo_hotkey: Mutex::new(None),
translation_hotkey: Mutex::new(None),
Expand Down Expand Up @@ -4501,6 +4507,11 @@ fn enabled_phrases(inner: &Arc<Inner>) -> Vec<String> {
/// 用户点 ✕ / ✓ / 中途出错 / 按 Esc 都走这里,统一 2 秒。
const CAPSULE_AUTO_HIDE_DELAY_MS: u64 = 2000;

/// Toggle 模式下,end_session 将 phase 设为 Idle 后在此时间内禁止新的 begin_session。
/// 避免用户三连按时第 3 次按下误激活新听写(此时胶囊仍在离场动画周期内)。
/// 值取 capsule EXIT_ANIM_MS (360ms) + 余量 ≈ 600ms。
const POST_SESSION_COOLDOWN_MS: u64 = 600;

/// Coordinator 全局超时保护:防止 ASR await_final_result() 永远挂起。
/// 设置为 15 秒(比 ASR 的 12 秒 FINAL_RESULT_TIMEOUT 稍长),
/// 只在 ASR 超时机制失效时作为最后的防线触发。
Expand Down
25 changes: 25 additions & 0 deletions openless-all/app/src-tauri/src/coordinator/dictation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -460,6 +460,20 @@ pub(super) async fn handle_pressed(inner: &Arc<Inner>) {
log::info!("[coord] hotkey pressed (mode={mode:?}, phase={phase:?})");
match (mode, phase) {
(HotkeyMode::Toggle, SessionPhase::Idle) => {
// 冷却检查:end_session 刚收尾时禁止短时间内再次激活,
// 避免三连按第 3 次误触(此时胶囊仍在离场动画周期内,issue #545)。
let now = std::time::Instant::now();
let on_cooldown = inner
.session_cooldown_until
.lock()
.map(|deadline| now < deadline)
.unwrap_or(false);
if on_cooldown {
log::info!(
"[coord] toggle activation blocked by cooldown (session still winding down)"
);
return;
}
let _ = begin_session(inner).await;
}
(HotkeyMode::Toggle, SessionPhase::Listening) => {
Expand Down Expand Up @@ -1814,6 +1828,13 @@ pub(super) async fn end_session(inner: &Arc<Inner>) -> Result<(), String> {
state.phase = SessionPhase::Idle;
state.focus_target = None;
}
// Toggle 模式冷却:设冷却时间戳,POST_SESSION_COOLDOWN_MS 内禁止新的 activate。
// 覆盖胶囊离场动画周期,避免三连按第 3 次误激活(issue #545)。
{
let now = std::time::Instant::now();
*inner.session_cooldown_until.lock() =
Some(now + std::time::Duration::from_millis(POST_SESSION_COOLDOWN_MS));
}
schedule_capsule_idle(inner, CAPSULE_AUTO_HIDE_DELAY_MS);

Ok(())
Expand Down Expand Up @@ -1861,6 +1882,10 @@ pub(super) fn cancel_session(inner: &Arc<Inner>) {
if decision.phase != SessionPhase::Processing {
let mut state = inner.state.lock();
finish_cancel_session_state(&mut state, decision);
// 只有真正把 phase 设为 Idle 时才设冷却(避免离场动画期间误激活)。
let now = std::time::Instant::now();
*inner.session_cooldown_until.lock() =
Some(now + std::time::Duration::from_millis(POST_SESSION_COOLDOWN_MS));
}
emit_capsule(inner, CapsuleState::Cancelled, 0.0, 0, None, None);
log::info!("[coord] session cancelled (was {:?})", decision.phase);
Expand Down
Loading