diff --git a/VoiceInk/Views/Recorder/MiniRecorderView.swift b/VoiceInk/Views/Recorder/MiniRecorderView.swift index 7c47ad6..60ca62d 100644 --- a/VoiceInk/Views/Recorder/MiniRecorderView.swift +++ b/VoiceInk/Views/Recorder/MiniRecorderView.swift @@ -28,37 +28,16 @@ struct MiniRecorderView: View { } private var statusView: some View { - Group { - let currentState = whisperState.recordingState - - if currentState == .enhancing { - Text("Enhancing") - .foregroundColor(.white) - .font(.system(size: 10, weight: .medium, design: .default)) - .lineLimit(1) - .minimumScaleFactor(0.5) - } else if currentState == .transcribing { - Text("Transcribing") - .foregroundColor(.white) - .font(.system(size: 10, weight: .medium, design: .default)) - .lineLimit(1) - .minimumScaleFactor(0.5) - } else if currentState == .recording { - AudioVisualizer( - audioMeter: recorder.audioMeter, - color: .white, - isActive: currentState == .recording - ) - } else { - StaticVisualizer(color: .white) - } - } + RecorderStatusDisplay( + currentState: whisperState.recordingState, + audioMeter: recorder.audioMeter + ) } private var rightButton: some View { Group { if powerModeManager.isPowerModeEnabled { - NotchToggleButton( + RecorderToggleButton( isEnabled: powerModeManager.isPowerModeEnabled, icon: powerModeManager.currentActiveConfiguration.emoji, color: .orange, @@ -72,7 +51,7 @@ struct MiniRecorderView: View { PowerModePopover() } } else { - NotchToggleButton( + RecorderToggleButton( isEnabled: enhancementService.isEnhancementEnabled, icon: enhancementService.activePrompt?.icon.rawValue ?? "brain", color: .blue, @@ -109,7 +88,7 @@ struct MiniRecorderView: View { let isRecording = whisperState.recordingState == .recording let isProcessing = whisperState.recordingState == .transcribing || whisperState.recordingState == .enhancing - NotchRecordButton( + RecorderRecordButton( isRecording: isRecording, isProcessing: isProcessing ) { diff --git a/VoiceInk/Views/Recorder/NotchRecorderView.swift b/VoiceInk/Views/Recorder/NotchRecorderView.swift index 2dc3c1e..d0ca24e 100644 --- a/VoiceInk/Views/Recorder/NotchRecorderView.swift +++ b/VoiceInk/Views/Recorder/NotchRecorderView.swift @@ -36,7 +36,7 @@ struct NotchRecorderView: View { let isRecording = whisperState.recordingState == .recording let isProcessing = whisperState.recordingState == .transcribing || whisperState.recordingState == .enhancing - NotchRecordButton( + RecorderRecordButton( isRecording: isRecording, isProcessing: isProcessing ) { @@ -55,7 +55,7 @@ struct NotchRecorderView: View { private var rightToggleButton: some View { Group { if powerModeManager.isPowerModeEnabled { - NotchToggleButton( + RecorderToggleButton( isEnabled: powerModeManager.isPowerModeEnabled, icon: powerModeManager.currentActiveConfiguration.emoji, color: .orange, @@ -68,7 +68,7 @@ struct NotchRecorderView: View { PowerModePopover() } } else { - NotchToggleButton( + RecorderToggleButton( isEnabled: enhancementService.isEnhancementEnabled, icon: enhancementService.activePrompt?.icon.rawValue ?? "brain", color: .blue, @@ -106,33 +106,11 @@ struct NotchRecorderView: View { } private var statusDisplay: some View { - Group { - let currentState = whisperState.recordingState - - if currentState == .enhancing { - Text("Enhancing") - .foregroundColor(.white) - .font(.system(size: 10, weight: .medium, design: .default)) - .lineLimit(1) - .minimumScaleFactor(0.5) - } else if currentState == .transcribing { - Text("Transcribing") - .foregroundColor(.white) - .font(.system(size: 10, weight: .medium, design: .default)) - .lineLimit(1) - .minimumScaleFactor(0.5) - } else if currentState == .recording { - AudioVisualizer( - audioMeter: recorder.audioMeter, - color: .white, - isActive: currentState == .recording - ) - .scaleEffect(y: min(1.0, (menuBarHeight - 8) / 25), anchor: .center) - } else { - StaticVisualizer(color: .white) - .scaleEffect(y: min(1.0, (menuBarHeight - 8) / 25), anchor: .center) - } - } + RecorderStatusDisplay( + currentState: whisperState.recordingState, + audioMeter: recorder.audioMeter, + menuBarHeight: menuBarHeight + ) .frame(width: 70) .padding(.trailing, 8) } @@ -163,101 +141,4 @@ struct NotchRecorderView: View { -struct NotchToggleButton: View { - let isEnabled: Bool - let icon: String - let color: Color - let disabled: Bool - let action: () -> Void - - init(isEnabled: Bool, icon: String, color: Color, disabled: Bool = false, action: @escaping () -> Void) { - self.isEnabled = isEnabled - self.icon = icon - self.color = color - self.disabled = disabled - self.action = action - } - - private var isEmoji: Bool { - return !icon.contains(".") && !icon.contains("-") && icon.unicodeScalars.contains { !$0.isASCII } - } - - var body: some View { - Button(action: action) { - Group { - if isEmoji { - Text(icon) - .font(.system(size: 12)) - } else { - Image(systemName: icon) - .font(.system(size: 11)) - } - } - .foregroundColor(disabled ? .white.opacity(0.3) : (isEnabled ? .white : .white.opacity(0.6))) - } - .buttonStyle(PlainButtonStyle()) - .disabled(disabled) - } -} - - - -// Notch-specific button styles -struct NotchRecordButton: View { - let isRecording: Bool - let isProcessing: Bool - let action: () -> Void - - var body: some View { - Button(action: action) { - ZStack { - Circle() - .fill(buttonColor) - .frame(width: 22, height: 22) - - if isProcessing { - ProcessingIndicator(color: .white) - .frame(width: 14, height: 14) - } else if isRecording { - RoundedRectangle(cornerRadius: 3) - .fill(Color.white) - .frame(width: 8, height: 8) - } else { - Circle() - .fill(Color.white) - .frame(width: 8, height: 8) - } - } - } - .buttonStyle(PlainButtonStyle()) - .disabled(isProcessing) - } - - private var buttonColor: Color { - if isProcessing { - return Color(red: 0.4, green: 0.4, blue: 0.45) - } else if isRecording { - return .red - } else { - return Color(red: 0.3, green: 0.3, blue: 0.35) - } - } -} - -struct ProcessingIndicator: View { - @State private var rotation: Double = 0 - let color: Color - - var body: some View { - Circle() - .trim(from: 0.1, to: 0.9) - .stroke(color, lineWidth: 1.5) - .frame(width: 12, height: 12) - .rotationEffect(.degrees(rotation)) - .onAppear { - withAnimation(.linear(duration: 1).repeatForever(autoreverses: false)) { - rotation = 360 - } - } - } -} + diff --git a/VoiceInk/Views/Recorder/RecorderComponents.swift b/VoiceInk/Views/Recorder/RecorderComponents.swift new file mode 100644 index 0000000..6ecf21b --- /dev/null +++ b/VoiceInk/Views/Recorder/RecorderComponents.swift @@ -0,0 +1,141 @@ +import SwiftUI + +// MARK: - Generic Toggle Button Component +struct RecorderToggleButton: View { + let isEnabled: Bool + let icon: String + let color: Color + let disabled: Bool + let action: () -> Void + + init(isEnabled: Bool, icon: String, color: Color, disabled: Bool = false, action: @escaping () -> Void) { + self.isEnabled = isEnabled + self.icon = icon + self.color = color + self.disabled = disabled + self.action = action + } + + private var isEmoji: Bool { + return !icon.contains(".") && !icon.contains("-") && icon.unicodeScalars.contains { !$0.isASCII } + } + + var body: some View { + Button(action: action) { + Group { + if isEmoji { + Text(icon) + .font(.system(size: 12)) + } else { + Image(systemName: icon) + .font(.system(size: 11)) + } + } + .foregroundColor(disabled ? .white.opacity(0.3) : (isEnabled ? .white : .white.opacity(0.6))) + } + .buttonStyle(PlainButtonStyle()) + .disabled(disabled) + } +} + +// MARK: - Generic Record Button Component +struct RecorderRecordButton: View { + let isRecording: Bool + let isProcessing: Bool + let action: () -> Void + + var body: some View { + Button(action: action) { + ZStack { + Circle() + .fill(buttonColor) + .frame(width: 22, height: 22) + + if isProcessing { + ProcessingIndicator(color: .white) + .frame(width: 14, height: 14) + } else if isRecording { + RoundedRectangle(cornerRadius: 3) + .fill(Color.white) + .frame(width: 8, height: 8) + } else { + Circle() + .fill(Color.white) + .frame(width: 8, height: 8) + } + } + } + .buttonStyle(PlainButtonStyle()) + .disabled(isProcessing) + } + + private var buttonColor: Color { + if isProcessing { + return Color(red: 0.4, green: 0.4, blue: 0.45) + } else if isRecording { + return .red + } else { + return Color(red: 0.3, green: 0.3, blue: 0.35) + } + } +} + +// MARK: - Processing Indicator Component +struct ProcessingIndicator: View { + @State private var rotation: Double = 0 + let color: Color + + var body: some View { + Circle() + .trim(from: 0.1, to: 0.9) + .stroke(color, lineWidth: 1.5) + .frame(width: 12, height: 12) + .rotationEffect(.degrees(rotation)) + .onAppear { + withAnimation(.linear(duration: 1).repeatForever(autoreverses: false)) { + rotation = 360 + } + } + } +} + +// MARK: - Status Display Component +struct RecorderStatusDisplay: View { + let currentState: RecordingState + let audioMeter: AudioMeter + let menuBarHeight: CGFloat? + + init(currentState: RecordingState, audioMeter: AudioMeter, menuBarHeight: CGFloat? = nil) { + self.currentState = currentState + self.audioMeter = audioMeter + self.menuBarHeight = menuBarHeight + } + + var body: some View { + Group { + if currentState == .enhancing { + Text("Enhancing") + .foregroundColor(.white) + .font(.system(size: 10, weight: .medium, design: .default)) + .lineLimit(1) + .minimumScaleFactor(0.5) + } else if currentState == .transcribing { + Text("Transcribing") + .foregroundColor(.white) + .font(.system(size: 10, weight: .medium, design: .default)) + .lineLimit(1) + .minimumScaleFactor(0.5) + } else if currentState == .recording { + AudioVisualizer( + audioMeter: audioMeter, + color: .white, + isActive: currentState == .recording + ) + .scaleEffect(y: menuBarHeight != nil ? min(1.0, (menuBarHeight! - 8) / 25) : 1.0, anchor: .center) + } else { + StaticVisualizer(color: .white) + .scaleEffect(y: menuBarHeight != nil ? min(1.0, (menuBarHeight! - 8) / 25) : 1.0, anchor: .center) + } + } + } +} \ No newline at end of file