import SwiftUI import AVFoundation #if os(macOS) import AppKit // MARK: - 扫码窗口(独立 NSWindow,避免 SwiftUI sheet 布局崩溃) class QRScannerWindowController: NSObject, AVCaptureMetadataOutputObjectsDelegate { var window: NSWindow? var session: AVCaptureSession? var previewLayer: AVCaptureVideoPreviewLayer? var hostView: QRScannerHostView? weak var bridge: PlayerBridge? private var lastDetected: String? private var lastDetectedTime: Date = .distantPast init(bridge: PlayerBridge) { self.bridge = bridge super.init() } func show() { if let win = window, win.isVisible { win.makeKeyAndOrderFront(nil) return } let win = NSWindow( contentRect: NSRect(x: 0, y: 0, width: 400, height: 380), styleMask: [.titled, .closable], backing: .buffered, defer: false ) win.title = L.scanQRCode win.isReleasedWhenClosed = false win.center() // 创建宿主视图 let container = NSView(frame: win.contentRect(forFrameRect: win.frame).insetBy(dx: 0, dy: 0)) container.wantsLayer = true container.layer?.backgroundColor = NSColor.black.cgColor let hostView = QRScannerHostView(frame: NSRect(x: 20, y: 80, width: 360, height: 240)) container.addSubview(hostView) self.hostView = hostView // 提示文字 let label = NSTextField(labelWithString: L.scanHint) label.font = NSFont.systemFont(ofSize: 11) label.textColor = .secondaryLabelColor label.frame = NSRect(x: 20, y: 20, width: 360, height: 40) label.alignment = .center label.lineBreakMode = .byWordWrapping container.addSubview(label) // 检测结果文字 let resultLabel = NSTextField(labelWithString: "") resultLabel.font = NSFont.monospacedSystemFont(ofSize: 12, weight: .regular) resultLabel.textColor = .labelColor resultLabel.frame = NSRect(x: 20, y: 330, width: 360, height: 30) resultLabel.alignment = .center resultLabel.lineBreakMode = .byTruncatingMiddle resultLabel.tag = 100 container.addSubview(resultLabel) win.contentView = container // 配置摄像头 setupCapture(hostView: hostView) win.makeKeyAndOrderFront(nil) NSApp.activate(ignoringOtherApps: true) self.window = win } func close() { session?.stopRunning() window?.close() } private func setupCapture(hostView: QRScannerHostView) { let session = AVCaptureSession() self.session = session guard let device = AVCaptureDevice.default(for: .video), let input = try? AVCaptureDeviceInput(device: device) else { showError("No camera available") return } if session.canAddInput(input) { session.addInput(input) } let output = AVCaptureMetadataOutput() if session.canAddOutput(output) { session.addOutput(output) output.setMetadataObjectsDelegate(self, queue: .main) output.metadataObjectTypes = [.qr] } let previewLayer = AVCaptureVideoPreviewLayer(session: session) previewLayer.videoGravity = .resizeAspectFill hostView.previewLayer = previewLayer self.previewLayer = previewLayer // 后台启动 DispatchQueue.global(qos: .userInitiated).async { [weak self] in session.startRunning() DispatchQueue.main.async { self?.previewLayer?.frame = hostView.bounds } } } private func showError(_ msg: String) { guard let resultLabel = window?.contentView?.viewWithTag(100) as? NSTextField else { return } resultLabel.stringValue = msg resultLabel.textColor = .systemRed } // MARK: - AVCaptureMetadataOutputObjectsDelegate 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 // 显示检测结果 if let resultLabel = window?.contentView?.viewWithTag(100) as? NSTextField { resultLabel.stringValue = code resultLabel.textColor = .systemGreen } // 检查是否为有效 URL if let url = URL(string: code), (url.scheme == "http" || url.scheme == "https") { DispatchQueue.main.async { [weak self] in self?.bridge?.addURL(code) } // 延迟关闭 DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { [weak self] in self?.close() } } } deinit { session?.stopRunning() } } // MARK: - 宿主视图 class QRScannerHostView: NSView { var previewLayer: AVCaptureVideoPreviewLayer? override init(frame: CGRect) { super.init(frame: frame) wantsLayer = true } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func layout() { super.layout() previewLayer?.frame = bounds } } #endif