feat: simplified push-to-talk with hands-free mode
This commit is contained in:
parent
62cf41a44d
commit
2e6423a531
@ -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()
|
||||
}
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user