From 8371369b4c3f5b787ff66409ec04500ae97130d4 Mon Sep 17 00:00:00 2001 From: Beingpax Date: Sun, 25 May 2025 17:51:23 +0545 Subject: [PATCH] Created a better visualizer and layout styling for mini recorder and notch recorder --- .../Views/Recorder/AudioVisualizerView.swift | 113 ++++++++++ .../{ => Recorder}/MiniRecorderPanel.swift | 0 .../{ => Recorder}/MiniRecorderView.swift | 28 ++- .../{ => Recorder}/MiniWindowManager.swift | 0 .../{ => Recorder}/NotchRecorderPanel.swift | 4 +- .../{ => Recorder}/NotchRecorderView.swift | 198 +++--------------- .../Views/{ => Recorder}/NotchShape.swift | 0 .../{ => Recorder}/NotchWindowManager.swift | 0 VoiceInk/VisualizerView.swift | 91 -------- 9 files changed, 161 insertions(+), 273 deletions(-) create mode 100644 VoiceInk/Views/Recorder/AudioVisualizerView.swift rename VoiceInk/Views/{ => Recorder}/MiniRecorderPanel.swift (100%) rename VoiceInk/Views/{ => Recorder}/MiniRecorderView.swift (80%) rename VoiceInk/Views/{ => Recorder}/MiniWindowManager.swift (100%) rename VoiceInk/Views/{ => Recorder}/NotchRecorderPanel.swift (97%) rename VoiceInk/Views/{ => Recorder}/NotchRecorderView.swift (64%) rename VoiceInk/Views/{ => Recorder}/NotchShape.swift (100%) rename VoiceInk/Views/{ => Recorder}/NotchWindowManager.swift (100%) delete mode 100644 VoiceInk/VisualizerView.swift diff --git a/VoiceInk/Views/Recorder/AudioVisualizerView.swift b/VoiceInk/Views/Recorder/AudioVisualizerView.swift new file mode 100644 index 0000000..9bc4929 --- /dev/null +++ b/VoiceInk/Views/Recorder/AudioVisualizerView.swift @@ -0,0 +1,113 @@ +import SwiftUI + +struct AudioVisualizer: View { + let audioMeter: AudioMeter + let color: Color + let isActive: Bool + + private let barCount = 12 + private let minHeight: CGFloat = 4 + private let maxHeight: CGFloat = 28 + private let barWidth: CGFloat = 3.0 + private let barSpacing: CGFloat = 2.0 + private let hardThreshold: Double = 0.3 + + private let sensitivityMultipliers: [Double] + + @State private var barHeights: [CGFloat] + @State private var targetHeights: [CGFloat] + + init(audioMeter: AudioMeter, color: Color, isActive: Bool) { + self.audioMeter = audioMeter + self.color = color + self.isActive = isActive + + self.sensitivityMultipliers = (0.. 0.5 { + withAnimation( + isDecaying + ? .spring(response: 0.4, dampingFraction: 0.8) + : .spring(response: 0.3, dampingFraction: 0.7) + ) { + barHeights[i] = targetHeights[i] + } + } + } + } + + private func resetBars() { + withAnimation(.easeOut(duration: 0.15)) { + barHeights = Array(repeating: minHeight, count: barCount) + targetHeights = Array(repeating: minHeight, count: barCount) + } + } +} + +struct StaticVisualizer: View { + private let barCount = 12 + private let barWidth: CGFloat = 3.0 + private let staticHeight: CGFloat = 3.0 + private let barSpacing: CGFloat = 2.0 + let color: Color + + var body: some View { + HStack(spacing: barSpacing) { + ForEach(0.. 0 ? safeAreaInsets.left * 2 : 200 // Calculate total width including controls and padding - let controlsWidth: CGFloat = 44 // Space for buttons on each side (22 * 2) + let controlsWidth: CGFloat = 64 // Space for buttons on each side (increased width) let paddingWidth: CGFloat = 32 // 16pt on each side let totalWidth = baseNotchWidth + controlsWidth * 2 + paddingWidth diff --git a/VoiceInk/Views/NotchRecorderView.swift b/VoiceInk/Views/Recorder/NotchRecorderView.swift similarity index 64% rename from VoiceInk/Views/NotchRecorderView.swift rename to VoiceInk/Views/Recorder/NotchRecorderView.swift index 675db46..f4bf1af 100644 --- a/VoiceInk/Views/NotchRecorderView.swift +++ b/VoiceInk/Views/Recorder/NotchRecorderView.swift @@ -47,36 +47,7 @@ struct NotchRecorderView: View { } .frame(width: 22) - // Empty space for future use - Spacer() - .frame(width: 22) - } - .frame(width: 44) // Fixed width for controls - .padding(.leading, 16) - - // Center section with exact notch width - Rectangle() - .fill(Color.clear) - .frame(width: exactNotchWidth) - .contentShape(Rectangle()) // Make the entire area tappable - - // Right side group with fixed width - HStack(spacing: 8) { - // Visualizer - moved to first position - Group { - if whisperState.isProcessing { - NotchStaticVisualizer(color: .white) - } else { - NotchAudioVisualizer( - audioMeter: recorder.audioMeter, - color: .white, - isActive: whisperState.isRecording - ) - } - } - .frame(width: 22) - - // Power Mode Button - moved to second position + // Power Mode Button - moved from right side NotchToggleButton( isEnabled: powerModeManager.isPowerModeEnabled, icon: powerModeManager.currentActiveConfiguration.emoji, @@ -88,8 +59,40 @@ struct NotchRecorderView: View { .popover(isPresented: $showPowerModePopover, arrowEdge: .bottom) { PowerModePopover() } + + Spacer() } - .frame(width: 44) // Fixed width for controls + .frame(width: 64) // Increased width for both controls + .padding(.leading, 16) + + // Center section with exact notch width + Rectangle() + .fill(Color.clear) + .frame(width: exactNotchWidth) + .contentShape(Rectangle()) // Make the entire area tappable + + // Right side group with visualizer only + HStack(spacing: 0) { + Spacer() // Push visualizer to the right + + // Visualizer - contained within right area with scaling + Group { + if whisperState.isProcessing { + StaticVisualizer(color: .white) + } else { + AudioVisualizer( + audioMeter: recorder.audioMeter, + color: .white, + isActive: whisperState.isRecording + ) + // Apply a vertical scale transform to fit within the menu bar + .scaleEffect(y: min(1.0, (menuBarHeight - 8) / 25), anchor: .center) + } + } + .frame(width: 30) + .padding(.trailing, 8) // Add padding to keep it away from the edge + } + .frame(width: 64) // Increased width to match left side .padding(.trailing, 16) } .frame(height: menuBarHeight) @@ -248,139 +251,6 @@ struct NotchRecordButton: View { } } -struct NotchAudioVisualizer: View { - let audioMeter: AudioMeter - let color: Color - let isActive: Bool - - private let barCount = 5 - private let minHeight: CGFloat = 3 - private let maxHeight: CGFloat = 18 - private let audioThreshold: CGFloat = 0.01 - - @State private var barHeights: [BarLevel] = [] - - struct BarLevel { - var average: CGFloat - var peak: CGFloat - } - - init(audioMeter: AudioMeter, color: Color, isActive: Bool) { - self.audioMeter = audioMeter - self.color = color - self.isActive = isActive - _barHeights = State(initialValue: Array(repeating: BarLevel(average: minHeight, peak: minHeight), count: 5)) - } - - var body: some View { - HStack(spacing: 2) { - ForEach(0.. BarLevel { - let positionFactor = CGFloat(index) / CGFloat(barCount - 1) - let curve = sin(positionFactor * .pi) - - let randomFactor = Double.random(in: 0.8...1.2) - let averageBase = audioMeter.averagePower * randomFactor - let peakBase = audioMeter.peakPower * randomFactor - - let averageHeight = CGFloat(averageBase) * maxHeight * 1.7 * curve - let peakHeight = CGFloat(peakBase) * maxHeight * 1.7 * curve - - let finalAverage = max(minHeight, min(averageHeight, maxHeight)) - let finalPeak = max(minHeight, min(peakHeight, maxHeight)) - - - return BarLevel( - average: finalAverage, - peak: finalPeak - ) - } -} - -struct NotchVisualizerBar: View { - let averageHeight: CGFloat - let peakHeight: CGFloat - let color: Color - - var body: some View { - ZStack(alignment: .bottom) { - // Average level bar - RoundedRectangle(cornerRadius: 1.5) - .fill( - LinearGradient( - gradient: Gradient(colors: [ - color.opacity(0.6), - color.opacity(0.8), - color - ]), - startPoint: .bottom, - endPoint: .top - ) - ) - .frame(width: 2, height: averageHeight) - - } - .animation(.spring(response: 0.2, dampingFraction: 0.7, blendDuration: 0), value: averageHeight) - .animation(.spring(response: 0.2, dampingFraction: 0.7, blendDuration: 0), value: peakHeight) - } -} - -struct NotchStaticVisualizer: View { - private let barCount = 5 - private let barHeights: [CGFloat] = [0.7, 0.5, 0.8, 0.4, 0.6] - let color: Color - - var body: some View { - HStack(spacing: 2) { - ForEach(0.. 0.01 ? 1 : 0) - } - .frame(maxHeight: geometry.size.height, alignment: .bottom) - } - } -}