feat: simplified push-to-talk with hands-free mode

This commit is contained in:
Beingpax 2025-03-28 12:15:37 +05:45
parent 62cf41a44d
commit 2e6423a531
3 changed files with 64 additions and 56 deletions

View File

@ -43,19 +43,19 @@ class HotkeyManager: ObservableObject {
private var runLoopSource: CFRunLoopSource?
private var visibilityTask: Task<Void, Never>?
// New properties for advanced key handling
// Key handling properties
private var keyPressStartTime: Date?
private var lastKeyPressEndTime: Date?
private var isLockedRecording = false // For toggle mode after double-press
private let doublePressThreshold = 0.3 // 300ms for faster double-press detection
private let briefPressThreshold = 1.0 // 1000ms threshold for brief press
private let briefPressThreshold = 1.0 // 1 second threshold for brief press
private var isHandsFreeMode = false // Track if we're in hands-free recording mode
// Add cooldown management
// Add cooldown management
private var lastShortcutTriggerTime: Date?
private let shortcutCooldownInterval: TimeInterval = 0.5 // 500ms cooldown
enum PushToTalkKey: String, CaseIterable {
case rightOption = "rightOption"
case leftOption = "leftOption"
case leftControl = "leftControl"
case fn = "fn"
case rightCommand = "rightCommand"
case rightShift = "rightShift"
@ -63,6 +63,8 @@ class HotkeyManager: ObservableObject {
var displayName: String {
switch self {
case .rightOption: return "Right Option (⌥)"
case .leftOption: return "Left Option (⌥)"
case .leftControl: return "Left Control (⌃)"
case .fn: return "Fn"
case .rightCommand: return "Right Command (⌘)"
case .rightShift: return "Right Shift (⇧)"
@ -72,6 +74,8 @@ class HotkeyManager: ObservableObject {
var keyCode: CGKeyCode {
switch self {
case .rightOption: return 0x3D
case .leftOption: return 0x3A
case .leftControl: return 0x3B
case .fn: return 0x3F
case .rightCommand: return 0x36
case .rightShift: return 0x3C
@ -81,6 +85,8 @@ class HotkeyManager: ObservableObject {
var flags: CGEventFlags {
switch self {
case .rightOption: return .maskAlternate
case .leftOption: return .maskAlternate
case .leftControl: return .maskControl
case .fn: return .maskSecondaryFn
case .rightCommand: return .maskCommand
case .rightShift: return .maskShift
@ -101,8 +107,7 @@ class HotkeyManager: ObservableObject {
private func resetKeyStates() {
currentKeyState = false
keyPressStartTime = nil
lastKeyPressEndTime = nil
isLockedRecording = false
isHandsFreeMode = false
}
private func setupVisibilityObserver() {
@ -182,21 +187,13 @@ class HotkeyManager: ObservableObject {
// Key is pressed down
if isKeyPressed {
// If we're in locked recording mode, key press should stop recording
if isLockedRecording && whisperState.isMiniRecorderVisible {
isLockedRecording = false
await whisperState.handleToggleMiniRecorder()
return
}
// Start timing the key press
keyPressStartTime = Date()
// Check for double press
if let lastEndTime = lastKeyPressEndTime,
Date().timeIntervalSince(lastEndTime) < doublePressThreshold {
// Double press detected - set locked recording mode
isLockedRecording = true
// If we're in hands-free mode, stop recording
if isHandsFreeMode {
isHandsFreeMode = false
await whisperState.handleToggleMiniRecorder()
return
}
// Show recorder if not already visible
@ -207,27 +204,19 @@ class HotkeyManager: ObservableObject {
// Key is released
else {
let now = Date()
lastKeyPressEndTime = now
// Calculate press duration
if let startTime = keyPressStartTime {
let pressDuration = now.timeIntervalSince(startTime)
// 1. Brief press (< 1s): Delay dismissal to check for double-press
if pressDuration < briefPressThreshold && !isLockedRecording {
// Wait to see if this is part of a double-press
try? await Task.sleep(nanoseconds: 200_000_000) // 200ms delay
// After waiting, check if we should still dismiss
if !isLockedRecording {
await whisperState.dismissMiniRecorder()
}
}
// 2. Normal press in non-locked mode: Use handleToggleMiniRecorder to stop and transcribe
else if !isLockedRecording && whisperState.isMiniRecorderVisible {
if pressDuration < briefPressThreshold {
// For brief presses, enter hands-free mode
isHandsFreeMode = true
// Continue recording - do nothing on release
} else {
// For longer presses, stop and transcribe
await whisperState.handleToggleMiniRecorder()
}
// 3. If in locked mode, we don't do anything on release
}
keyPressStartTime = nil
@ -241,9 +230,6 @@ class HotkeyManager: ObservableObject {
guard let self = self,
await self.whisperState.isMiniRecorderVisible else { return }
// Reset locked recording state when using Escape key
self.isLockedRecording = false
SoundManager.shared.playEscSound()
await self.whisperState.dismissMiniRecorder()
}

View File

@ -159,30 +159,40 @@ struct RecordView: View {
private var pushToTalkSection: some View {
VStack(alignment: .leading, spacing: 12) {
Toggle(isOn: $hotkeyManager.isPushToTalkEnabled) {
HStack {
Text("Push-to-Talk")
.font(.subheadline.weight(.medium))
}
.toggleStyle(.switch)
if hotkeyManager.isPushToTalkEnabled {
pushToTalkOptions
if hotkeyManager.isPushToTalkEnabled {
SelectableKeyCapView(
text: getKeySymbol(for: hotkeyManager.pushToTalkKey),
subtext: getKeyText(for: hotkeyManager.pushToTalkKey),
isSelected: true
)
}
}
}
}
private var pushToTalkOptions: some View {
VStack(alignment: .leading, spacing: 8) {
PushToTalkKeySelector(selectedKey: $hotkeyManager.pushToTalkKey)
HStack(spacing: 6) {
Image(systemName: "arrow.left.arrow.right.circle.fill")
.foregroundColor(.secondary)
.font(.system(size: 12))
Text("Click to switch")
.font(.caption)
.foregroundColor(.secondary)
}
private func getKeySymbol(for key: HotkeyManager.PushToTalkKey) -> String {
switch key {
case .rightOption: return ""
case .leftOption: return ""
case .leftControl: return ""
case .fn: return "Fn"
case .rightCommand: return ""
case .rightShift: return ""
}
}
private func getKeyText(for key: HotkeyManager.PushToTalkKey) -> String {
switch key {
case .rightOption: return "Right Option"
case .leftOption: return "Left Option"
case .leftControl: return "Left Control"
case .fn: return "Function"
case .rightCommand: return "Right Command"
case .rightShift: return "Right Shift"
}
}
@ -227,6 +237,10 @@ struct RecordView: View {
switch hotkeyManager.pushToTalkKey {
case .rightOption:
keyName = "right Option (⌥)"
case .leftOption:
keyName = "left Option (⌥)"
case .leftControl:
keyName = "left Control (⌃)"
case .fn:
keyName = "Fn"
case .rightCommand:

View File

@ -258,6 +258,10 @@ struct SettingsView: View {
switch hotkeyManager.pushToTalkKey {
case .rightOption:
return "Using Right Option (⌥) key to quickly start recording. Release to stop."
case .leftOption:
return "Using Left Option (⌥) key to quickly start recording. Release to stop."
case .leftControl:
return "Using Left Control (⌃) key to quickly start recording. Release to stop."
case .fn:
return "Using Function (Fn) key to quickly start recording. Release to stop."
case .rightCommand:
@ -364,6 +368,8 @@ struct PushToTalkKeySelector: View {
private func getKeySymbol(for key: HotkeyManager.PushToTalkKey) -> String {
switch key {
case .rightOption: return ""
case .leftOption: return ""
case .leftControl: return ""
case .fn: return "Fn"
case .rightCommand: return ""
case .rightShift: return ""
@ -373,6 +379,8 @@ struct PushToTalkKeySelector: View {
private func getKeyText(for key: HotkeyManager.PushToTalkKey) -> String {
switch key {
case .rightOption: return "Right Option"
case .leftOption: return "Left Option"
case .leftControl: return "Left Control"
case .fn: return "Function"
case .rightCommand: return "Right Command"
case .rightShift: return "Right Shift"