diff --git a/VoiceInk/Views/Recorder/MiniRecorderView.swift b/VoiceInk/Views/Recorder/MiniRecorderView.swift index 20bdb17..f788a52 100644 --- a/VoiceInk/Views/Recorder/MiniRecorderView.swift +++ b/VoiceInk/Views/Recorder/MiniRecorderView.swift @@ -39,42 +39,50 @@ struct MiniRecorderView: View { ) } + private var contentLayout: some View { + HStack(spacing: 0) { + if windowManager.isExpanded { + // Left button zone - only exists when expanded + RecorderPromptButton(showPopover: $showEnhancementPromptPopover) + .padding(.leading, 6) + .transition(.scale(scale: 0.5).combined(with: .opacity)) + + Spacer() + } + + // Fixed visualizer zone - takes full width when compact + statusView + .frame(maxWidth: .infinity) + + if windowManager.isExpanded { + Spacer() + + // Right button zone - only exists when expanded + RecorderPowerModeButton(showPopover: $showPowerModePopover) + .padding(.trailing, 6) + .transition(.scale(scale: 0.5).combined(with: .opacity)) + } + } + .padding(.vertical, 8) + } + + private var recorderCapsule: some View { + Capsule() + .fill(.clear) + .background(backgroundView) + .overlay { + Capsule() + .strokeBorder(Color.white.opacity(0.3), lineWidth: 0.5) + } + .overlay { + contentLayout + } + } + var body: some View { Group { if windowManager.isVisible { - Capsule() - .fill(.clear) - .background(backgroundView) - .overlay { - Capsule() - .strokeBorder(Color.white.opacity(0.1), lineWidth: 0.5) - } - .overlay { - HStack(spacing: 0) { - if windowManager.isExpanded { - // Left button zone - only exists when expanded - RecorderPromptButton(showPopover: $showEnhancementPromptPopover) - .padding(.leading, 6) - .transition(.scale(scale: 0.5).combined(with: .opacity)) - - Spacer() - } - - // Fixed visualizer zone - takes full width when compact - statusView - .frame(maxWidth: .infinity) - - if windowManager.isExpanded { - Spacer() - - // Right button zone - only exists when expanded - RecorderPowerModeButton(showPopover: $showPowerModePopover) - .padding(.trailing, 6) - .transition(.scale(scale: 0.5).combined(with: .opacity)) - } - } - .padding(.vertical, 8) - } + recorderCapsule .onHover { hovering in isHovering = hovering if hovering { diff --git a/VoiceInk/Views/Recorder/MiniWindowManager.swift b/VoiceInk/Views/Recorder/MiniWindowManager.swift index 1cbe606..ef05f7c 100644 --- a/VoiceInk/Views/Recorder/MiniWindowManager.swift +++ b/VoiceInk/Views/Recorder/MiniWindowManager.swift @@ -12,6 +12,9 @@ class MiniWindowManager: ObservableObject { // Callback to check if collapse should be prevented (e.g., when popovers are showing) var shouldPreventCollapse: (() -> Bool)? + // Debounced timer for auto-collapse + private var debounceTimer: Timer? + init(whisperState: WhisperState, recorder: Recorder) { self.whisperState = whisperState self.recorder = recorder @@ -19,6 +22,7 @@ class MiniWindowManager: ObservableObject { } deinit { + debounceTimer?.invalidate() NotificationCenter.default.removeObserver(self) } @@ -57,11 +61,17 @@ class MiniWindowManager: ObservableObject { } @objc private func handleFeedbackNotification() { - guard isVisible, !isExpanded else { return } + guard isVisible else { return } - expand() + // Only expand if not already expanded + if !isExpanded { + expand() + } - DispatchQueue.main.asyncAfter(deadline: .now() + 2.5) { + // Reset debounce timer - this cancels any existing timer and starts a new one + debounceTimer?.invalidate() + debounceTimer = Timer.scheduledTimer(withTimeInterval: 2.5, repeats: false) { [weak self] _ in + guard let self = self else { return } if self.isExpanded && !(self.shouldPreventCollapse?() ?? false) { self.collapse() } @@ -100,6 +110,10 @@ class MiniWindowManager: ObservableObject { func hide() { guard isVisible else { return } + // Cancel any pending auto-collapse timer + debounceTimer?.invalidate() + debounceTimer = nil + self.isVisible = false self.isExpanded = false self.miniPanel?.hide { [weak self] in