MiniPlayer/Sources/QRScannerView.swift

172 lines
5.1 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import SwiftUI
import AVFoundation
#if os(macOS)
import AppKit
// MARK: -
struct QRScannerView: NSViewRepresentable {
let onCodeDetected: (String) -> Void
func makeCoordinator() -> Coordinator {
Coordinator(onCodeDetected: onCodeDetected)
}
func makeNSView(context: Context) -> NSView {
let view = QRScannerHostView()
view.coordinator = context.coordinator
context.coordinator.previewLayer = view.previewLayer
//
let session = AVCaptureSession()
context.coordinator.session = session
guard let device = AVCaptureDevice.default(for: .video),
let input = try? AVCaptureDeviceInput(device: device) else {
return view
}
if session.canAddInput(input) {
session.addInput(input)
}
let output = AVCaptureMetadataOutput()
if session.canAddOutput(output) {
session.addOutput(output)
output.setMetadataObjectsDelegate(context.coordinator, queue: .main)
output.metadataObjectTypes = [.qr]
}
view.previewLayer.session = session
view.previewLayer.videoGravity = .resizeAspectFill
//
DispatchQueue.global(qos: .userInitiated).async {
session.startRunning()
}
return view
}
func updateNSView(_ nsView: NSView, context: Context) {}
// MARK: - Coordinator
class Coordinator: NSObject, AVCaptureMetadataOutputObjectsDelegate {
let onCodeDetected: (String) -> Void
var session: AVCaptureSession?
weak var previewLayer: AVCaptureVideoPreviewLayer?
private var lastDetected: String?
private var lastDetectedTime: Date = .distantPast
init(onCodeDetected: @escaping (String) -> Void) {
self.onCodeDetected = onCodeDetected
}
func metadataOutput(_ output: AVCaptureMetadataOutput, didOutput metadataObjects: [AVMetadataObject], from connection: AVCaptureConnection) {
guard let metadata = metadataObjects.first as? AVMetadataMachineReadableCodeObject,
let code = metadata.stringValue else { return }
// 3
let now = Date()
if code == lastDetected, now.timeIntervalSince(lastDetectedTime) < 3 {
return
}
lastDetected = code
lastDetectedTime = now
onCodeDetected(code)
}
deinit {
session?.stopRunning()
}
}
}
// MARK: - 宿
class QRScannerHostView: NSView {
let previewLayer = AVCaptureVideoPreviewLayer()
weak var coordinator: QRScannerView.Coordinator?
override init(frame: CGRect) {
super.init(frame: frame)
wantsLayer = true
layer = previewLayer
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layout() {
super.layout()
previewLayer.frame = bounds
}
}
// MARK: -
struct QRScannerDialog: View {
@ObservedObject var bridge: PlayerBridge
@Environment(\.dismiss) private var dismiss
@State private var detectedCode: String?
@State private var isProcessing = false
var body: some View {
VStack(spacing: 16) {
Text(L.scanQRCode)
.font(.headline)
Text(L.scanHint)
.font(.caption)
.foregroundColor(.secondary)
QRScannerView { code in
detectedCode = code
handleCode(code)
}
.frame(width: 320, height: 240)
.cornerRadius(8)
if let code = detectedCode {
VStack(spacing: 8) {
Text(L.detectedCode)
.font(.caption)
.foregroundColor(.secondary)
Text(code)
.font(.system(.body, design: .monospaced))
.lineLimit(2)
.multilineTextAlignment(.center)
}
.padding(12)
.background(Color.secondary.opacity(0.1))
.cornerRadius(8)
}
HStack {
Button(L.close) { dismiss() }
.keyboardShortcut(.cancelAction)
Spacer()
}
}
.padding(20)
.frame(width: 380)
}
private func handleCode(_ code: String) {
guard !isProcessing else { return }
// URL
if let url = URL(string: code), url.scheme == "http" || url.scheme == "https" {
isProcessing = true
bridge.addURL(code)
//
DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) {
dismiss()
}
}
}
}
#endif