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:
@@ -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
|
||||
Reference in New Issue
Block a user