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 // 不在 init 时设置 layer = previewLayer,等 layout 时再添加为 sublayer } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func layout() { super.layout() if previewLayer.superlayer == nil { layer?.addSublayer(previewLayer) } 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