189 lines
6.7 KiB
Swift
189 lines
6.7 KiB
Swift
#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 {
|
|
ZStack {
|
|
QRScannerControllerView(controller: controller)
|
|
.ignoresSafeArea()
|
|
|
|
if controller.qrsMode {
|
|
qrsProgressOverlay
|
|
}
|
|
}
|
|
.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) {
|
|
ZStack {
|
|
QRScannerControllerView(controller: controller)
|
|
|
|
if controller.qrsMode {
|
|
qrsProgressOverlay
|
|
}
|
|
}
|
|
.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 var qrsProgressOverlay: some View {
|
|
ZStack {
|
|
Color.black.opacity(0.5)
|
|
.ignoresSafeArea()
|
|
|
|
let k = controller.decoder?.k ?? 0
|
|
let framesScanned = controller.framesScanned
|
|
let scanProgress = k > 0 ? min(1.0, Double(framesScanned) / Double(k) / 1.2) : 0
|
|
|
|
CircularProgressView(
|
|
progress: scanProgress,
|
|
total: k
|
|
)
|
|
}
|
|
}
|
|
|
|
private func handleScanResult(_ result: Result<QRScanResult, QRScanError>) {
|
|
switch result {
|
|
case let .success(scanResult):
|
|
dismiss()
|
|
Task { @MainActor in
|
|
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(error: error)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private struct CircularProgressView: View {
|
|
let progress: Double
|
|
let total: Int
|
|
|
|
private let size: CGFloat = 96
|
|
private let lineWidth: CGFloat = 8
|
|
|
|
var body: some View {
|
|
ZStack {
|
|
Circle()
|
|
.stroke(Color.white.opacity(0.3), lineWidth: lineWidth)
|
|
.frame(width: size, height: size)
|
|
|
|
Circle()
|
|
.trim(from: 0, to: progress)
|
|
.stroke(Color.white, style: StrokeStyle(lineWidth: lineWidth, lineCap: .round))
|
|
.frame(width: size, height: size)
|
|
.rotationEffect(.degrees(-90))
|
|
.animation(.easeInOut(duration: 0.2), value: progress)
|
|
|
|
if total > 0 {
|
|
Text("\(min(99, Int(progress * 100)))%")
|
|
.font(.system(size: 20, weight: .semibold))
|
|
.foregroundStyle(.white)
|
|
}
|
|
|
|
Text("QRS")
|
|
.font(.system(size: 32, weight: .bold))
|
|
.foregroundStyle(.white)
|
|
.offset(y: -size / 2 - 40)
|
|
}
|
|
}
|
|
}
|
|
|
|
#endif
|