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
This commit is contained in:
世界
2026-01-01 18:18:03 +08:00
parent 251a79f5d7
commit e2c378d42b
15 changed files with 687 additions and 108 deletions
@@ -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
}
@@ -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
}
@@ -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<ScanResult, ScanError>) {
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