0.10.6
This commit is contained in:
@@ -0,0 +1,192 @@
|
||||
import AVFoundation
|
||||
import SwiftUI
|
||||
|
||||
#if os(iOS)
|
||||
import UIKit
|
||||
#elseif os(macOS)
|
||||
import AppKit
|
||||
#endif
|
||||
|
||||
@MainActor
|
||||
struct VideoNoteCaptureView: View {
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
@StateObject private var recorder = VideoNoteRecorder()
|
||||
@State private var errorMessage: String?
|
||||
@State private var isFinalizing = false
|
||||
|
||||
let onComplete: (VideoNoteRecorder.Recording) -> Void
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
VStack(spacing: 22) {
|
||||
ZStack {
|
||||
Circle().fill(Color.black)
|
||||
CameraPreview(session: recorder.session)
|
||||
.clipShape(Circle())
|
||||
if !recorder.isPrepared {
|
||||
ProgressView("Подготовка камеры…")
|
||||
.tint(.white)
|
||||
.foregroundStyle(.white)
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: 420)
|
||||
.aspectRatio(1, contentMode: .fit)
|
||||
.overlay(Circle().stroke(.white.opacity(0.16), lineWidth: 1))
|
||||
|
||||
Text(recordingStatus)
|
||||
.font(.system(.title3, design: .monospaced).weight(.semibold))
|
||||
.foregroundStyle(recorder.isRecording ? .red : .secondary)
|
||||
|
||||
Button {
|
||||
guard recorder.isRecording, !isFinalizing else { return }
|
||||
isFinalizing = true
|
||||
Task { @MainActor in
|
||||
await recorder.stop(
|
||||
afterMinimumDuration: VideoNoteStopPolicy.minimumCaptureDuration
|
||||
)
|
||||
}
|
||||
} label: {
|
||||
ZStack {
|
||||
Circle()
|
||||
.stroke(Color.red, lineWidth: 4)
|
||||
.frame(width: 76, height: 76)
|
||||
if isFinalizing {
|
||||
ProgressView()
|
||||
.controlSize(.regular)
|
||||
.tint(.red)
|
||||
} else {
|
||||
RoundedRectangle(cornerRadius: 7)
|
||||
.fill(Color.red)
|
||||
.frame(width: 32, height: 32)
|
||||
}
|
||||
}
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.disabled(!recorder.isRecording || isFinalizing)
|
||||
.accessibilityLabel(isFinalizing ? "Видео завершается" : "Завершить запись")
|
||||
|
||||
Text("Запись началась. Нажмите красную кнопку, чтобы завершить и отправить.")
|
||||
.font(.footnote)
|
||||
.foregroundStyle(.secondary)
|
||||
.multilineTextAlignment(.center)
|
||||
}
|
||||
.padding(24)
|
||||
.navigationTitle("Видеосообщение")
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .cancellationAction) {
|
||||
Button("Отмена") {
|
||||
recorder.cancel()
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.task {
|
||||
recorder.onFinished = { result in
|
||||
switch result {
|
||||
case .success(let recording):
|
||||
onComplete(recording)
|
||||
dismiss()
|
||||
case .failure(let error):
|
||||
isFinalizing = false
|
||||
errorMessage = error.localizedDescription
|
||||
}
|
||||
}
|
||||
do {
|
||||
try await recorder.prepare()
|
||||
guard !Task.isCancelled else { return }
|
||||
try recorder.start()
|
||||
} catch {
|
||||
errorMessage = error.localizedDescription
|
||||
}
|
||||
}
|
||||
.onDisappear {
|
||||
if recorder.isRecording {
|
||||
recorder.cancel()
|
||||
} else {
|
||||
recorder.shutdown()
|
||||
}
|
||||
}
|
||||
.alert("Luma", isPresented: Binding(
|
||||
get: { errorMessage != nil },
|
||||
set: { if !$0 { errorMessage = nil } }
|
||||
)) {
|
||||
Button("OK") { errorMessage = nil }
|
||||
} message: {
|
||||
Text(errorMessage ?? "Неизвестная ошибка")
|
||||
}
|
||||
#if os(macOS)
|
||||
.frame(minWidth: 540, minHeight: 650)
|
||||
#endif
|
||||
}
|
||||
|
||||
private func formatted(_ duration: TimeInterval) -> String {
|
||||
let seconds = max(0, Int(duration.rounded(.down)))
|
||||
return String(format: "%d:%02d", seconds / 60, seconds % 60)
|
||||
}
|
||||
|
||||
private var recordingStatus: String {
|
||||
if isFinalizing { return "Завершение…" }
|
||||
if recorder.isRecording { return formatted(recorder.elapsed) }
|
||||
return "Подготовка камеры…"
|
||||
}
|
||||
}
|
||||
|
||||
#if os(iOS)
|
||||
struct CameraPreview: UIViewRepresentable {
|
||||
let session: AVCaptureSession
|
||||
|
||||
func makeUIView(context: Context) -> PreviewView {
|
||||
let view = PreviewView()
|
||||
view.previewLayer.session = session
|
||||
view.previewLayer.videoGravity = .resizeAspectFill
|
||||
return view
|
||||
}
|
||||
|
||||
func updateUIView(_ uiView: PreviewView, context: Context) {
|
||||
uiView.previewLayer.session = session
|
||||
}
|
||||
|
||||
final class PreviewView: UIView {
|
||||
override class var layerClass: AnyClass { AVCaptureVideoPreviewLayer.self }
|
||||
|
||||
var previewLayer: AVCaptureVideoPreviewLayer {
|
||||
layer as! AVCaptureVideoPreviewLayer
|
||||
}
|
||||
}
|
||||
}
|
||||
#elseif os(macOS)
|
||||
struct CameraPreview: NSViewRepresentable {
|
||||
let session: AVCaptureSession
|
||||
|
||||
func makeNSView(context: Context) -> PreviewView {
|
||||
let view = PreviewView()
|
||||
view.previewLayer.session = session
|
||||
view.previewLayer.videoGravity = .resizeAspectFill
|
||||
return view
|
||||
}
|
||||
|
||||
func updateNSView(_ nsView: PreviewView, context: Context) {
|
||||
nsView.previewLayer.session = session
|
||||
}
|
||||
|
||||
final class PreviewView: NSView {
|
||||
let previewLayer = AVCaptureVideoPreviewLayer()
|
||||
|
||||
override init(frame frameRect: NSRect) {
|
||||
super.init(frame: frameRect)
|
||||
wantsLayer = true
|
||||
layer = previewLayer
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
return nil
|
||||
}
|
||||
|
||||
override func layout() {
|
||||
super.layout()
|
||||
previewLayer.frame = bounds
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
Reference in New Issue
Block a user