From e2c378d42babf7a54cc401221c69c4867fdccda4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Thu, 1 Jan 2026 18:18:03 +0800 Subject: [PATCH] Add QR scanner support for macOS with camera selection - Refactor QR scanner into separate Scanner module for iOS and macOS - Add camera selection menu for devices with multiple cameras - Use Vision framework for QR detection on macOS - Remove tvOS QR scanner support (continuity camera) - Add camera entitlement and usage description for macOS - Fix minor issues in Intents and ExtensionProvider --- .../Views/Profile/NewProfileMenuView.swift | 54 +++- .../Views/Profile/NewProfileView.swift | 12 +- .../Views/Profile/QRCodeScannerView.swift | 63 ----- .../Views/Scanner/QRScanner.swift | 32 +++ .../Scanner/QRScannerController+iOS.swift | 227 +++++++++++++++++ .../Scanner/QRScannerController+macOS.swift | 241 ++++++++++++++++++ .../Views/Scanner/QRScannerView.swift | 127 +++++++++ IntentsExtension/Intents.swift | 2 +- .../Network/ExtensionPlatformInterface.swift | 3 +- Library/Network/ExtensionProvider.swift | 2 +- SFM.System/SFM.entitlements | 2 + SFM/Info.plist | 2 + SFM/SFM.entitlements | 2 + sing-box.xcodeproj/project.pbxproj | 17 -- .../xcshareddata/swiftpm/Package.resolved | 9 - 15 files changed, 687 insertions(+), 108 deletions(-) delete mode 100644 ApplicationLibrary/Views/Profile/QRCodeScannerView.swift create mode 100644 ApplicationLibrary/Views/Scanner/QRScanner.swift create mode 100644 ApplicationLibrary/Views/Scanner/QRScannerController+iOS.swift create mode 100644 ApplicationLibrary/Views/Scanner/QRScannerController+macOS.swift create mode 100644 ApplicationLibrary/Views/Scanner/QRScannerView.swift diff --git a/ApplicationLibrary/Views/Profile/NewProfileMenuView.swift b/ApplicationLibrary/Views/Profile/NewProfileMenuView.swift index ab8f41b..42f63ac 100644 --- a/ApplicationLibrary/Views/Profile/NewProfileMenuView.swift +++ b/ApplicationLibrary/Views/Profile/NewProfileMenuView.swift @@ -9,14 +9,15 @@ public struct NewProfileMenuView: View { @EnvironmentObject private var environments: ExtensionEnvironments @Environment(\.dismiss) private var dismiss @State private var alert: AlertState? - @State private var showFileImporter = false @State private var importRequest: NewProfileView.ImportRequest? @State private var localImportRequest: NewProfileView.LocalImportRequest? - #if os(iOS) - @State private var showQRScanner = false - #elseif os(tvOS) + #if os(tvOS) @State private var importCompleted = false - #elseif os(macOS) + #else + @State private var showFileImporter = false + @State private var showQRScanner = false + #endif + #if os(macOS) @State private var showNewProfile = false #endif @@ -76,6 +77,19 @@ public struct NewProfileMenuView: View { }) .environmentObject(environments) } + .sheet(isPresented: $showQRScanner) { + QRScannerView { result in + handleQRScanResult(result) + } + .frame(minWidth: 500, minHeight: 400) + } + .sheet(item: $importRequest) { request in + NewProfileView(request, onSuccess: { profile in + await SharedPreferences.selectedProfileID.set(profile.mustID) + dismiss() + }) + .environmentObject(environments) + } } #endif @@ -117,10 +131,10 @@ public struct NewProfileMenuView: View { handleFileImport(result) } #endif - #if os(iOS) + #if !os(tvOS) .sheet(isPresented: $showQRScanner) { - QRCodeScannerView { remoteProfile in - importRequest = NewProfileView.ImportRequest(name: remoteProfile.name, url: remoteProfile.url) + QRScannerView { result in + handleQRScanResult(result) } } #endif @@ -148,7 +162,7 @@ public struct NewProfileMenuView: View { } #endif - #if os(iOS) + #if !os(tvOS) FormButton { showQRScanner = true } label: { @@ -214,4 +228,26 @@ public struct NewProfileMenuView: View { } } #endif + + #if !os(tvOS) + private func handleQRScanResult(_ result: QRScanResult) { + var error: NSError? + let remoteProfile = LibboxParseRemoteProfileImportLink(result.string, &error) + if let error { + alert = AlertState( + title: String(localized: "Invalid QR Code"), + message: error.localizedDescription + ) + return + } + guard let remoteProfile else { + alert = AlertState( + title: String(localized: "Invalid QR Code"), + message: String(localized: "The QR code does not contain a valid profile import link.") + ) + return + } + importRequest = NewProfileView.ImportRequest(name: remoteProfile.name, url: remoteProfile.url) + } + #endif } diff --git a/ApplicationLibrary/Views/Profile/NewProfileView.swift b/ApplicationLibrary/Views/Profile/NewProfileView.swift index 13cd033..641c037 100644 --- a/ApplicationLibrary/Views/Profile/NewProfileView.swift +++ b/ApplicationLibrary/Views/Profile/NewProfileView.swift @@ -135,13 +135,11 @@ public struct NewProfileView: View { #if os(macOS) private var macOSBody: some View { VStack(alignment: .leading, spacing: 0) { - if !viewModel.isImport { - Text("New Profile") - .font(.headline) - .padding(.horizontal, 20) - .padding(.top, 20) - .padding(.bottom, 12) - } + Text(viewModel.isImport ? "Import Profile" : "New Profile") + .font(.headline) + .padding(.horizontal, 20) + .padding(.top, 20) + .padding(.bottom, 12) formContent } diff --git a/ApplicationLibrary/Views/Profile/QRCodeScannerView.swift b/ApplicationLibrary/Views/Profile/QRCodeScannerView.swift deleted file mode 100644 index e8b911a..0000000 --- a/ApplicationLibrary/Views/Profile/QRCodeScannerView.swift +++ /dev/null @@ -1,63 +0,0 @@ -#if os(iOS) - - import AVFoundation - import CodeScanner - import Libbox - import Library - import SwiftUI - - @MainActor - public struct QRCodeScannerView: View { - @Environment(\.dismiss) private var dismiss - @State private var alert: AlertState? - - private let onScan: (LibboxImportRemoteProfile) -> Void - - public init(onScan: @escaping (LibboxImportRemoteProfile) -> Void) { - self.onScan = onScan - } - - public var body: some View { - NavigationStackCompat { - CodeScannerView(codeTypes: [.qr], showViewfinder: true) { response in - handleScan(response) - } - .ignoresSafeArea() - .navigationTitle("Scan QR Code") - .navigationBarTitleDisplayMode(.inline) - .toolbar { - ToolbarItem(placement: .cancellationAction) { - Button("Cancel") { dismiss() } - } - } - } - .alert($alert) - } - - private func handleScan(_ result: Result) { - switch result { - case let .success(scanResult): - var error: NSError? - let remoteProfile = LibboxParseRemoteProfileImportLink(scanResult.string, &error) - if let error { - alert = AlertState(title: String(localized: "Invalid QR Code"), message: error.localizedDescription) - return - } - guard let remoteProfile else { - alert = AlertState(title: String(localized: "Invalid QR Code"), message: String(localized: "The QR code does not contain a valid profile import link.")) - return - } - dismiss() - onScan(remoteProfile) - case let .failure(error): - switch error { - case .permissionDenied: - alert = AlertState(title: String(localized: "Camera Access Denied"), message: String(localized: "Please enable camera access in Settings to scan QR codes.")) - default: - alert = AlertState(title: String(localized: "Scanner Error"), message: String(describing: error)) - } - } - } - } - -#endif diff --git a/ApplicationLibrary/Views/Scanner/QRScanner.swift b/ApplicationLibrary/Views/Scanner/QRScanner.swift new file mode 100644 index 0000000..a22f917 --- /dev/null +++ b/ApplicationLibrary/Views/Scanner/QRScanner.swift @@ -0,0 +1,32 @@ +import AVFoundation +import SwiftUI + +public enum QRScanError: Error, LocalizedError { + case cameraUnavailable + case permissionDenied + case scanFailed(Error) + case invalidCode + + public var errorDescription: String? { + switch self { + case .cameraUnavailable: + return String(localized: "Camera is not available") + case .permissionDenied: + return String(localized: "Camera access denied") + case let .scanFailed(error): + return error.localizedDescription + case .invalidCode: + return String(localized: "Invalid QR code") + } + } +} + +public struct QRScanResult: Sendable { + public let string: String + public let type: AVMetadataObject.ObjectType + + public init(string: String, type: AVMetadataObject.ObjectType) { + self.string = string + self.type = type + } +} diff --git a/ApplicationLibrary/Views/Scanner/QRScannerController+iOS.swift b/ApplicationLibrary/Views/Scanner/QRScannerController+iOS.swift new file mode 100644 index 0000000..c65b7a4 --- /dev/null +++ b/ApplicationLibrary/Views/Scanner/QRScannerController+iOS.swift @@ -0,0 +1,227 @@ +#if os(iOS) + + import AVFoundation + import SwiftUI + import UIKit + + @MainActor + final class QRScannerController: NSObject, ObservableObject { + private var captureSession: AVCaptureSession? + private var previewLayer: AVCaptureVideoPreviewLayer? + private var metadataOutput: AVCaptureMetadataOutput? + private var didFinishScanning = false + + @Published var availableCameras: [AVCaptureDevice] = [] + @Published var selectedCamera: AVCaptureDevice? + + let previewView = UIView() + var onScan: ((Result) -> Void)? + + override init() { + super.init() + previewView.backgroundColor = .black + refreshCameraList() + } + + func refreshCameraList() { + var deviceTypes: [AVCaptureDevice.DeviceType] = [ + .builtInWideAngleCamera, + .builtInUltraWideCamera, + .builtInTelephotoCamera, + ] + if #available(iOS 17.0, *) { + deviceTypes.append(.external) + } + let discoverySession = AVCaptureDevice.DiscoverySession( + deviceTypes: deviceTypes, + mediaType: .video, + position: .unspecified + ) + availableCameras = discoverySession.devices + if selectedCamera == nil { + selectedCamera = availableCameras.first + } + } + + func selectCamera(_ camera: AVCaptureDevice) { + guard camera.uniqueID != selectedCamera?.uniqueID else { return } + selectedCamera = camera + didFinishScanning = false + + if captureSession != nil { + stopScanning() + captureSession = nil + previewLayer?.removeFromSuperlayer() + previewLayer = nil + startScanning() + } + } + + func startScanning() { + guard captureSession == nil else { + if captureSession?.isRunning == false { + DispatchQueue.global(qos: .userInteractive).async { [weak self] in + self?.captureSession?.startRunning() + } + } + return + } + + switch AVCaptureDevice.authorizationStatus(for: .video) { + case .authorized: + setupCaptureSession() + case .notDetermined: + AVCaptureDevice.requestAccess(for: .video) { [weak self] granted in + DispatchQueue.main.async { + if granted { + self?.setupCaptureSession() + } else { + self?.onScan?(.failure(.permissionDenied)) + } + } + } + case .denied, .restricted: + onScan?(.failure(.permissionDenied)) + @unknown default: + onScan?(.failure(.cameraUnavailable)) + } + } + + func stopScanning() { + DispatchQueue.global(qos: .userInteractive).async { [weak self] in + self?.captureSession?.stopRunning() + } + } + + func reset() { + didFinishScanning = false + } + + private func setupCaptureSession() { + let session = AVCaptureSession() + + guard let videoCaptureDevice = selectedCamera ?? AVCaptureDevice.default(for: .video) else { + onScan?(.failure(.cameraUnavailable)) + return + } + + let videoInput: AVCaptureDeviceInput + do { + videoInput = try AVCaptureDeviceInput(device: videoCaptureDevice) + } catch { + onScan?(.failure(.scanFailed(error))) + return + } + + guard session.canAddInput(videoInput) else { + onScan?(.failure(.cameraUnavailable)) + return + } + session.addInput(videoInput) + + let metadataOutput = AVCaptureMetadataOutput() + guard session.canAddOutput(metadataOutput) else { + onScan?(.failure(.cameraUnavailable)) + return + } + session.addOutput(metadataOutput) + metadataOutput.setMetadataObjectsDelegate(self, queue: .main) + metadataOutput.metadataObjectTypes = [.qr] + + self.metadataOutput = metadataOutput + self.captureSession = session + + let previewLayer = AVCaptureVideoPreviewLayer(session: session) + previewLayer.videoGravity = .resizeAspectFill + previewLayer.frame = previewView.bounds + previewView.layer.addSublayer(previewLayer) + self.previewLayer = previewLayer + + DispatchQueue.global(qos: .userInteractive).async { [weak self] in + self?.captureSession?.startRunning() + } + } + + func updatePreviewFrame(_ frame: CGRect) { + previewLayer?.frame = frame + } + } + + extension QRScannerController: AVCaptureMetadataOutputObjectsDelegate { + nonisolated func metadataOutput( + _ output: AVCaptureMetadataOutput, + didOutput metadataObjects: [AVMetadataObject], + from connection: AVCaptureConnection + ) { + Task { @MainActor in + guard !didFinishScanning, + let metadataObject = metadataObjects.first, + let readableObject = metadataObject as? AVMetadataMachineReadableCodeObject, + let stringValue = readableObject.stringValue + else { + return + } + + didFinishScanning = true + AudioServicesPlaySystemSound(SystemSoundID(kSystemSoundID_Vibrate)) + + let result = QRScanResult(string: stringValue, type: readableObject.type) + onScan?(.success(result)) + } + } + } + + struct QRScannerControllerView: UIViewControllerRepresentable { + let controller: QRScannerController + + func makeUIViewController(context: Context) -> UIViewController { + let viewController = QRScannerViewController(controller: controller) + return viewController + } + + func updateUIViewController(_ uiViewController: UIViewController, context: Context) {} + } + + private class QRScannerViewController: UIViewController { + let controller: QRScannerController + + init(controller: QRScannerController) { + self.controller = controller + super.init(nibName: nil, bundle: nil) + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + view.backgroundColor = .black + view.addSubview(controller.previewView) + controller.previewView.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + controller.previewView.topAnchor.constraint(equalTo: view.topAnchor), + controller.previewView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + controller.previewView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + controller.previewView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + ]) + } + + override func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + controller.updatePreviewFrame(view.bounds) + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + controller.startScanning() + } + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + controller.stopScanning() + } + } + +#endif diff --git a/ApplicationLibrary/Views/Scanner/QRScannerController+macOS.swift b/ApplicationLibrary/Views/Scanner/QRScannerController+macOS.swift new file mode 100644 index 0000000..efe7f0a --- /dev/null +++ b/ApplicationLibrary/Views/Scanner/QRScannerController+macOS.swift @@ -0,0 +1,241 @@ +#if os(macOS) + + import AppKit + import AVFoundation + import SwiftUI + import Vision + + @MainActor + final class QRScannerController: NSObject, ObservableObject { + private var captureSession: AVCaptureSession? + private var previewLayer: AVCaptureVideoPreviewLayer? + private var videoOutput: AVCaptureVideoDataOutput? + private var didFinishScanning = false + + @Published var availableCameras: [AVCaptureDevice] = [] + @Published var selectedCamera: AVCaptureDevice? + + let previewView = NSView() + var onScan: ((Result) -> Void)? + + override init() { + super.init() + previewView.wantsLayer = true + previewView.layer?.backgroundColor = NSColor.black.cgColor + refreshCameraList() + } + + func refreshCameraList() { + let discoverySession = AVCaptureDevice.DiscoverySession( + deviceTypes: [.builtInWideAngleCamera, .externalUnknown], + mediaType: .video, + position: .unspecified + ) + availableCameras = discoverySession.devices + if selectedCamera == nil { + selectedCamera = availableCameras.first + } + } + + func selectCamera(_ camera: AVCaptureDevice) { + guard camera.uniqueID != selectedCamera?.uniqueID else { return } + selectedCamera = camera + didFinishScanning = false + + if captureSession != nil { + stopScanning() + captureSession = nil + previewLayer?.removeFromSuperlayer() + previewLayer = nil + startScanning() + } + } + + func startScanning() { + guard captureSession == nil else { + if captureSession?.isRunning == false { + DispatchQueue.global(qos: .userInteractive).async { [weak self] in + self?.captureSession?.startRunning() + } + } + return + } + + switch AVCaptureDevice.authorizationStatus(for: .video) { + case .authorized: + setupCaptureSession() + case .notDetermined: + AVCaptureDevice.requestAccess(for: .video) { [weak self] granted in + DispatchQueue.main.async { + if granted { + self?.setupCaptureSession() + } else { + self?.onScan?(.failure(.permissionDenied)) + } + } + } + case .denied, .restricted: + onScan?(.failure(.permissionDenied)) + @unknown default: + onScan?(.failure(.cameraUnavailable)) + } + } + + func stopScanning() { + DispatchQueue.global(qos: .userInteractive).async { [weak self] in + self?.captureSession?.stopRunning() + } + } + + func reset() { + didFinishScanning = false + } + + private func setupCaptureSession() { + let session = AVCaptureSession() + + guard let videoCaptureDevice = selectedCamera ?? AVCaptureDevice.default(for: .video) else { + onScan?(.failure(.cameraUnavailable)) + return + } + + let videoInput: AVCaptureDeviceInput + do { + videoInput = try AVCaptureDeviceInput(device: videoCaptureDevice) + } catch { + onScan?(.failure(.scanFailed(error))) + return + } + + guard session.canAddInput(videoInput) else { + onScan?(.failure(.cameraUnavailable)) + return + } + session.addInput(videoInput) + + let videoOutput = AVCaptureVideoDataOutput() + videoOutput.videoSettings = [ + kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_32BGRA, + ] + videoOutput.setSampleBufferDelegate(self, queue: DispatchQueue(label: "QRScannerQueue")) + + guard session.canAddOutput(videoOutput) else { + onScan?(.failure(.cameraUnavailable)) + return + } + session.addOutput(videoOutput) + + self.videoOutput = videoOutput + self.captureSession = session + + let previewLayer = AVCaptureVideoPreviewLayer(session: session) + previewLayer.videoGravity = .resizeAspectFill + previewLayer.frame = previewView.bounds + previewView.layer?.addSublayer(previewLayer) + self.previewLayer = previewLayer + + DispatchQueue.global(qos: .userInteractive).async { [weak self] in + self?.captureSession?.startRunning() + } + } + + func updatePreviewFrame(_ frame: CGRect) { + previewLayer?.frame = frame + } + + private func processQRCode(_ payloadString: String) { + guard !didFinishScanning else { return } + didFinishScanning = true + + DispatchQueue.main.async { [weak self] in + NSSound.beep() + let result = QRScanResult(string: payloadString, type: .qr) + self?.onScan?(.success(result)) + } + } + } + + extension QRScannerController: AVCaptureVideoDataOutputSampleBufferDelegate { + nonisolated func captureOutput( + _ output: AVCaptureOutput, + didOutput sampleBuffer: CMSampleBuffer, + from connection: AVCaptureConnection + ) { + guard let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else { return } + + let request = VNDetectBarcodesRequest { [weak self] request, _ in + guard let results = request.results as? [VNBarcodeObservation] else { return } + + for result in results { + if result.symbology == .qr, let payload = result.payloadStringValue { + self?.processQRCode(payload) + return + } + } + } + request.symbologies = [.qr] + + let handler = VNImageRequestHandler(cvPixelBuffer: pixelBuffer, options: [:]) + try? handler.perform([request]) + } + } + + struct QRScannerControllerView: NSViewControllerRepresentable { + let controller: QRScannerController + + func makeNSViewController(context: Context) -> NSViewController { + let viewController = QRScannerViewController(controller: controller) + return viewController + } + + func updateNSViewController(_ nsViewController: NSViewController, context: Context) {} + } + + private class QRScannerViewController: NSViewController { + let controller: QRScannerController + + init(controller: QRScannerController) { + self.controller = controller + super.init(nibName: nil, bundle: nil) + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func loadView() { + view = NSView() + view.wantsLayer = true + view.layer?.backgroundColor = NSColor.black.cgColor + } + + override func viewDidLoad() { + super.viewDidLoad() + view.addSubview(controller.previewView) + controller.previewView.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + controller.previewView.topAnchor.constraint(equalTo: view.topAnchor), + controller.previewView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + controller.previewView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + controller.previewView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + ]) + } + + override func viewDidLayout() { + super.viewDidLayout() + controller.updatePreviewFrame(view.bounds) + } + + override func viewWillAppear() { + super.viewWillAppear() + controller.startScanning() + } + + override func viewWillDisappear() { + super.viewWillDisappear() + controller.stopScanning() + } + } + +#endif diff --git a/ApplicationLibrary/Views/Scanner/QRScannerView.swift b/ApplicationLibrary/Views/Scanner/QRScannerView.swift new file mode 100644 index 0000000..dcef120 --- /dev/null +++ b/ApplicationLibrary/Views/Scanner/QRScannerView.swift @@ -0,0 +1,127 @@ +#if !os(tvOS) + + import AVFoundation + import Library + import SwiftUI + + @MainActor + public struct QRScannerView: View { + @Environment(\.dismiss) private var dismiss + @State private var alert: AlertState? + @StateObject private var controller = QRScannerController() + + private let onScan: (QRScanResult) -> Void + + public init(onScan: @escaping (QRScanResult) -> Void) { + self.onScan = onScan + } + + public var body: some View { + #if os(iOS) + iOSBody + #elseif os(macOS) + macOSBody + #endif + } + + #if os(iOS) + private var iOSBody: some View { + NavigationStackCompat { + QRScannerControllerView(controller: controller) + .ignoresSafeArea() + .navigationTitle("Scan QR Code") + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .cancellationAction) { + Button("Cancel") { dismiss() } + } + ToolbarItem(placement: .primaryAction) { + Menu { + if controller.availableCameras.count > 1 { + Menu("Camera") { + ForEach(controller.availableCameras, id: \.uniqueID) { camera in + Button { + controller.selectCamera(camera) + } label: { + if camera.uniqueID == controller.selectedCamera?.uniqueID { + Label(camera.localizedName, systemImage: "checkmark") + } else { + Text(camera.localizedName) + } + } + } + } + } + } label: { + Image(systemName: "ellipsis.circle") + } + } + } + } + .alert($alert) + .onAppear { + controller.onScan = handleScanResult + } + } + #endif + + #if os(macOS) + private var macOSBody: some View { + VStack(spacing: 0) { + QRScannerControllerView(controller: controller) + .frame(minWidth: 400, minHeight: 300) + + Divider() + + HStack { + if controller.availableCameras.count > 1 { + Picker("Camera", selection: Binding( + get: { controller.selectedCamera }, + set: { camera in + if let camera { + controller.selectCamera(camera) + } + } + )) { + ForEach(controller.availableCameras, id: \.uniqueID) { camera in + Text(camera.localizedName).tag(camera as AVCaptureDevice?) + } + } + .frame(maxWidth: 250) + } + Spacer() + Button("Cancel") { dismiss() } + .keyboardShortcut(.cancelAction) + } + .padding() + } + .alert($alert) + .onAppear { + controller.onScan = handleScanResult + } + } + #endif + + private func handleScanResult(_ result: Result) { + switch result { + case let .success(scanResult): + dismiss() + onScan(scanResult) + case let .failure(error): + switch error { + case .permissionDenied: + alert = AlertState( + title: String(localized: "Camera Access Denied"), + message: String(localized: "Please enable camera access in Settings to scan QR codes.") + ) + default: + alert = AlertState( + title: String(localized: "Scanner Error"), + message: error.localizedDescription + ) + } + } + } + } + +#endif diff --git a/IntentsExtension/Intents.swift b/IntentsExtension/Intents.swift index 2999f4c..0ec7bb3 100644 --- a/IntentsExtension/Intents.swift +++ b/IntentsExtension/Intents.swift @@ -144,7 +144,7 @@ struct GetCurrentProfile: AppIntent { } func perform() async throws -> some IntentResult & ReturnsValue { - guard let profile = try await ProfileManager.get(await SharedPreferences.selectedProfileID.get()) else { + guard let profile = try await ProfileManager.get(SharedPreferences.selectedProfileID.get()) else { throw NSError(domain: "No profile selected", code: 0) } return .result(value: profile.name) diff --git a/Library/Network/ExtensionPlatformInterface.swift b/Library/Network/ExtensionPlatformInterface.swift index 2ff1bbc..2ce9d09 100644 --- a/Library/Network/ExtensionPlatformInterface.swift +++ b/Library/Network/ExtensionPlatformInterface.swift @@ -249,7 +249,8 @@ public class ExtensionPlatformInterface: NSObject, LibboxPlatformInterfaceProtoc private func onUpdateDefaultInterface(_ listener: LibboxInterfaceUpdateListenerProtocol, _ path: Network.NWPath) { guard path.status != .unsatisfied, - let defaultInterface = path.availableInterfaces.first else { + let defaultInterface = path.availableInterfaces.first + else { listener.updateDefaultInterface("", interfaceIndex: -1, isExpensive: false, isConstrained: false) return } diff --git a/Library/Network/ExtensionProvider.swift b/Library/Network/ExtensionProvider.swift index bd99ccd..9799ea0 100644 --- a/Library/Network/ExtensionProvider.swift +++ b/Library/Network/ExtensionProvider.swift @@ -13,7 +13,7 @@ open class ExtensionProvider: NEPacketTunnelProvider { private var commandServer: LibboxCommandServer! private var platformInterface: ExtensionPlatformInterface! - override open func startTunnel(options startOptions: [String: NSObject]?) async throws { + override open func startTunnel(options _: [String: NSObject]?) async throws { let options = LibboxSetupOptions() options.basePath = FilePath.sharedDirectory.relativePath options.workingPath = FilePath.workingDirectory.relativePath diff --git a/SFM.System/SFM.entitlements b/SFM.System/SFM.entitlements index 73f30de..f22ea4f 100644 --- a/SFM.System/SFM.entitlements +++ b/SFM.System/SFM.entitlements @@ -30,5 +30,7 @@ com.apple.security.network.client + com.apple.security.device.camera + diff --git a/SFM/Info.plist b/SFM/Info.plist index 81cbca4..e7c2f49 100644 --- a/SFM/Info.plist +++ b/SFM/Info.plist @@ -115,5 +115,7 @@ sing-box uses the Location permission to provide users with routing based on WIFI SSID and BSSID rules, without reading your location. NSLocalNetworkUsageDescription As a universal proxy platform, sing-box configures routing according to your configuration. + NSCameraUsageDescription + Camera access is required to scan QR codes for importing profiles. diff --git a/SFM/SFM.entitlements b/SFM/SFM.entitlements index 69bb719..327a76d 100644 --- a/SFM/SFM.entitlements +++ b/SFM/SFM.entitlements @@ -28,5 +28,7 @@ com.apple.security.network.client + com.apple.security.device.camera + diff --git a/sing-box.xcodeproj/project.pbxproj b/sing-box.xcodeproj/project.pbxproj index a3f0951..86785d8 100644 --- a/sing-box.xcodeproj/project.pbxproj +++ b/sing-box.xcodeproj/project.pbxproj @@ -9,7 +9,6 @@ /* Begin PBXBuildFile section */ 3A017F922A4AB2E4009149FA /* GRDB in Frameworks */ = {isa = PBXBuildFile; productRef = 3A017F912A4AB2E4009149FA /* GRDB */; }; 3A096F8F2A4ED3DE00D4A2ED /* Extension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 3A096F862A4ED3DE00D4A2ED /* Extension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; - 3A1E8E692EDEC1AF00ADD104 /* CodeScanner in Frameworks */ = {isa = PBXBuildFile; productRef = 3A1E8E682EDEC1AF00ADD104 /* CodeScanner */; }; 3A2E87F22ED5A91100644195 /* Runestone in Frameworks */ = {isa = PBXBuildFile; productRef = 3A2E87F12ED5A91100644195 /* Runestone */; }; 3A2E87FB2ED5ABDA00644195 /* TreeSitterJSON5Runestone in Frameworks */ = {isa = PBXBuildFile; productRef = 3A2E87FA2ED5ABDA00644195 /* TreeSitterJSON5Runestone */; }; 3A3AA7FC2A4EFDAE002F78AB /* Library.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3AEC211D2A459B4700A63465 /* Library.framework */; }; @@ -564,7 +563,6 @@ files = ( 3A4EAD1B2A4FEB02005435B3 /* Library.framework in Frameworks */, 3A4A020D2B53E3DC004EFB87 /* QRCode in Frameworks */, - 3A1E8E692EDEC1AF00ADD104 /* CodeScanner in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -803,7 +801,6 @@ name = ApplicationLibrary; packageProductDependencies = ( 3A4A020C2B53E3DC004EFB87 /* QRCode */, - 3A1E8E682EDEC1AF00ADD104 /* CodeScanner */, ); productName = ApplicationLibrary; productReference = 3A4EAD102A4FEAE6005435B3 /* ApplicationLibrary.framework */; @@ -1140,7 +1137,6 @@ 3A2E87F02ED5A91100644195 /* XCLocalSwiftPackageReference "Frameworks/Runestone" */, 3A2E87F92ED5ABCF00644195 /* XCLocalSwiftPackageReference "Frameworks/TreeSitterJSON5" */, 3ACE5E012EE1A91100644196 /* XCRemoteSwiftPackageReference "CodeEditSourceEditor" */, - 3A1E8E672EDEC1AF00ADD104 /* XCRemoteSwiftPackageReference "CodeScanner" */, ); productRefGroup = 3AEC20C72A45991900A63465 /* Products */; projectDirPath = ""; @@ -2750,14 +2746,6 @@ minimumVersion = 6.15.1; }; }; - 3A1E8E672EDEC1AF00ADD104 /* XCRemoteSwiftPackageReference "CodeScanner" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/twostraws/CodeScanner.git"; - requirement = { - kind = upToNextMajorVersion; - minimumVersion = 2.5.2; - }; - }; 3A4A020B2B53E3DC004EFB87 /* XCRemoteSwiftPackageReference "qrcode" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/dagronf/qrcode.git"; @@ -2798,11 +2786,6 @@ package = 3A017F902A4AB2E4009149FA /* XCRemoteSwiftPackageReference "GRDB" */; productName = GRDB; }; - 3A1E8E682EDEC1AF00ADD104 /* CodeScanner */ = { - isa = XCSwiftPackageProductDependency; - package = 3A1E8E672EDEC1AF00ADD104 /* XCRemoteSwiftPackageReference "CodeScanner" */; - productName = CodeScanner; - }; 3A2E87F12ED5A91100644195 /* Runestone */ = { isa = XCSwiftPackageProductDependency; package = 3A2E87F02ED5A91100644195 /* XCLocalSwiftPackageReference "Frameworks/Runestone" */; diff --git a/sing-box.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/sing-box.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 17cfcb5..f660959 100644 --- a/sing-box.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/sing-box.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -46,15 +46,6 @@ "version" : "0.12.1" } }, - { - "identity" : "codescanner", - "kind" : "remoteSourceControl", - "location" : "https://github.com/twostraws/CodeScanner.git", - "state" : { - "revision" : "5e886430238944c7200fc9e10dbf2d9550dba865", - "version" : "2.5.2" - } - }, { "identity" : "grdb.swift", "kind" : "remoteSourceControl",