vOOice/VoiceInk/Views/Recorder/RecorderComponents.swift
2025-07-17 12:22:13 +05:45

141 lines
4.5 KiB
Swift

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)
}
}
}
}