Reorganized the Recording package target into folders.
This commit is contained in:
@@ -0,0 +1,129 @@
|
||||
import SwiftUI
|
||||
|
||||
/// A view that drives a recording session.
|
||||
///
|
||||
/// The view shows a main button that starts, pauses, and resumes a recording, with a label above it displaying the elapsed recording time.
|
||||
/// While paused, a send button lets the user submit the recording for processing, showing a progress indicator until the processing finishes.
|
||||
///
|
||||
/// All state and control behavior lives in the view's ``Model``; the view itself only renders it and forwards button presses and state changes.
|
||||
public struct RecordingView: View {
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
/// The model that owns the recording state and drives the view's controls.
|
||||
@State private var model: Model
|
||||
|
||||
/// The closure invoked with the transcribed text of every processed recording.
|
||||
private let onTranscription: (String) -> Void
|
||||
|
||||
// MARK: Initializers
|
||||
|
||||
/// Creates a recording view in the not-recording state, attached to the given recording and transcribing services.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - recorder: The service that captures the audio from a microphone.
|
||||
/// - transcriber: The service that transcribes the recorded audio into text.
|
||||
/// - onTranscription: The closure invoked with the transcribed text of every processed recording. Defaults to a closure that does nothing.
|
||||
public init(
|
||||
recorder: any RecordingService,
|
||||
transcriber: any TranscribingService,
|
||||
onTranscription: @escaping (String) -> Void = { _ in }
|
||||
) {
|
||||
self.model = .init(
|
||||
recorder: recorder,
|
||||
transcriber: transcriber
|
||||
)
|
||||
self.onTranscription = onTranscription
|
||||
}
|
||||
|
||||
// MARK: Body
|
||||
|
||||
/// The content of the view: the timer label above the main and send buttons, with every change of the model's state forwarded back
|
||||
/// to ``Model/updatedState(shouldRestartTimer:)``.
|
||||
public var body: some View {
|
||||
VStack(
|
||||
spacing: Constant.Spacing.stack
|
||||
) {
|
||||
if model.shouldShowTimer {
|
||||
Text(model.textTimer)
|
||||
.font(.largeTitle)
|
||||
.fontWeight(.bold)
|
||||
.monospacedDigit()
|
||||
.contentTransition(.numericText())
|
||||
.animation(
|
||||
.easeInOut,
|
||||
value: model.textTimer
|
||||
)
|
||||
}
|
||||
|
||||
HStack(
|
||||
spacing: Constant.Spacing.stack
|
||||
) {
|
||||
Button {
|
||||
model.pressedMain()
|
||||
} label: {
|
||||
Image(model.iconMain)
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
}
|
||||
.disabled(model.shouldDisableMain)
|
||||
|
||||
if model.shouldShowSend {
|
||||
Button {
|
||||
model.pressedSend()
|
||||
} label: {
|
||||
if let icon = model.iconSend {
|
||||
Image(icon)
|
||||
.resizable()
|
||||
} else {
|
||||
ProgressView()
|
||||
}
|
||||
}
|
||||
.disabled(model.isProcessing)
|
||||
}
|
||||
}
|
||||
.buttonStyle(.recording(
|
||||
invertStyle: !model.isProcessing
|
||||
))
|
||||
}
|
||||
.onChange(
|
||||
of: model.state,
|
||||
initial: false
|
||||
) { oldValue, _ in
|
||||
model.updatedState(
|
||||
shouldRestartTimer: oldValue == .notRecording
|
||||
)
|
||||
}
|
||||
.onChange(
|
||||
of: model.textTranscription,
|
||||
initial: false
|
||||
) { _, newValue in
|
||||
if let newValue {
|
||||
onTranscription(newValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Constants
|
||||
|
||||
/// The constant values used across the view.
|
||||
private enum Constant {
|
||||
/// The spacing constants.
|
||||
enum Spacing {
|
||||
/// The spacing between the elements of a stack.
|
||||
static let stack: CGFloat = 8
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Previews
|
||||
|
||||
#Preview(
|
||||
"Recording view"
|
||||
) {
|
||||
RecordingView(
|
||||
recorder: DummyRecordingService(),
|
||||
transcriber: DummyTranscribingService()
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user