From 740e1a7091cac454bc53d3079bce4a7d27843ccc Mon Sep 17 00:00:00 2001 From: Tom Caputi Date: Sat, 28 Oct 2023 04:23:23 -0400 Subject: [PATCH] things kinda work. adding debounce --- ntfy.xcodeproj/project.pbxproj | 4 ++ ntfy/Info.plist | 6 ++- ntfy/Utils/QRScannerUIView.swift | 70 ++++++++++++++++++++++++ ntfy/Views/SubscriptionAddView.swift | 81 ++++++++++++++++++++++++---- 4 files changed, 149 insertions(+), 12 deletions(-) create mode 100644 ntfy/Utils/QRScannerUIView.swift diff --git a/ntfy.xcodeproj/project.pbxproj b/ntfy.xcodeproj/project.pbxproj index 8aff12f..daad103 100644 --- a/ntfy.xcodeproj/project.pbxproj +++ b/ntfy.xcodeproj/project.pbxproj @@ -50,6 +50,7 @@ 94CD196A283E666900973B93 /* EmojiManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94CD1969283E666900973B93 /* EmojiManager.swift */; }; 94CD196B283E666900973B93 /* EmojiManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94CD1969283E666900973B93 /* EmojiManager.swift */; }; 94E9196C28353E0100F30170 /* Log.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9474F216283531A200CDE4DD /* Log.swift */; }; + E278CB332AECECCA004B9143 /* QRScannerUIView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E278CB322AECECCA004B9143 /* QRScannerUIView.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -111,6 +112,7 @@ 94B736D6284AF9BE003D69FB /* MainView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainView.swift; sourceTree = ""; }; 94CD1965283E662900973B93 /* emojis.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = emojis.json; sourceTree = ""; }; 94CD1969283E666900973B93 /* EmojiManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EmojiManager.swift; sourceTree = ""; }; + E278CB322AECECCA004B9143 /* QRScannerUIView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRScannerUIView.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -237,6 +239,7 @@ 94867142283EC9950093C7A4 /* Actions.swift */, 948671462841B0B20093C7A4 /* NotificationContent.swift */, 948671492841D0CE0093C7A4 /* ActionExecutor.swift */, + E278CB322AECECCA004B9143 /* QRScannerUIView.swift */, ); path = Utils; sourceTree = ""; @@ -375,6 +378,7 @@ 9474F1F22830825600CDE4DD /* SubscriptionListView.swift in Sources */, 9486714A2841D0CE0093C7A4 /* ActionExecutor.swift in Sources */, 9474F1FD2831311A00CDE4DD /* SubscriptionAddView.swift in Sources */, + E278CB332AECECCA004B9143 /* QRScannerUIView.swift in Sources */, 9474F1FF28316ACE00CDE4DD /* Subscription.swift in Sources */, 94CD196A283E666900973B93 /* EmojiManager.swift in Sources */, 9474F1C1282F2AA700CDE4DD /* AppMain.swift in Sources */, diff --git a/ntfy/Info.plist b/ntfy/Info.plist index 6abed55..0ad242b 100644 --- a/ntfy/Info.plist +++ b/ntfy/Info.plist @@ -18,7 +18,9 @@ remote-notification - FirebaseAppDelegateProxyEnabled - + FirebaseAppDelegateProxyEnabled + + NSCameraUsageDescription + We need access to the camera for QR code scanning. diff --git a/ntfy/Utils/QRScannerUIView.swift b/ntfy/Utils/QRScannerUIView.swift new file mode 100644 index 0000000..5b607d9 --- /dev/null +++ b/ntfy/Utils/QRScannerUIView.swift @@ -0,0 +1,70 @@ +import AVFoundation +import SwiftUI + +struct QRScannerUIView: UIViewRepresentable { + var onCodeDetected: (String) -> Void + + func makeUIView(context: Context) -> some UIView { + let view = QRScannerUIViewContainer() + + let captureSession = AVCaptureSession() + + guard let videoCaptureDevice = AVCaptureDevice.default(for: .video), + let videoInput = try? AVCaptureDeviceInput(device: videoCaptureDevice), + captureSession.canAddInput(videoInput) else { + return view + } + captureSession.addInput(videoInput) + + let metadataOutput = AVCaptureMetadataOutput() + captureSession.addOutput(metadataOutput) + + metadataOutput.setMetadataObjectsDelegate(context.coordinator, queue: DispatchQueue.main) + metadataOutput.metadataObjectTypes = [.qr] + + let previewLayer = AVCaptureVideoPreviewLayer(session: captureSession) + previewLayer.frame = view.layer.bounds + previewLayer.videoGravity = .resizeAspectFill + view.layer.insertSublayer(previewLayer, at: 0) + + // Move the startRunning call to a background thread + DispatchQueue.global(qos: .userInitiated).async { + captureSession.startRunning() + } + + view.previewLayer = previewLayer + view.captureSession = captureSession + return view + } + + func updateUIView(_ uiView: UIViewType, context: Context) {} + + func makeCoordinator() -> Coordinator { + Coordinator(onCodeDetected: onCodeDetected) + } + + class Coordinator: NSObject, AVCaptureMetadataOutputObjectsDelegate { + var onCodeDetected: (String) -> Void + + init(onCodeDetected: @escaping (String) -> Void) { + self.onCodeDetected = onCodeDetected + } + + func metadataOutput(_ output: AVCaptureMetadataOutput, didOutput metadataObjects: [AVMetadataObject], from connection: AVCaptureConnection) { + if let metadataObject = metadataObjects.first as? AVMetadataMachineReadableCodeObject, let stringValue = metadataObject.stringValue { + onCodeDetected(stringValue) + } + } + } +} + + +class QRScannerUIViewContainer: UIView { + var previewLayer: AVCaptureVideoPreviewLayer? + var captureSession: AVCaptureSession? + + override func layoutSubviews() { + super.layoutSubviews() + previewLayer?.frame = self.bounds + } +} diff --git a/ntfy/Views/SubscriptionAddView.swift b/ntfy/Views/SubscriptionAddView.swift index 2fc8645..8b6b4cb 100644 --- a/ntfy/Views/SubscriptionAddView.swift +++ b/ntfy/Views/SubscriptionAddView.swift @@ -1,4 +1,5 @@ import SwiftUI +import AVFoundation struct SubscriptionAddView: View { private let tag = "SubscriptionAddView" @@ -17,6 +18,7 @@ struct SubscriptionAddView: View { @State private var loading = false @State private var addError: String? @State private var loginError: String? + @State private var hasCameraPermission: Bool = false private var subscriptionManager: SubscriptionManager { return SubscriptionManager(store: store) @@ -24,19 +26,29 @@ struct SubscriptionAddView: View { var body: some View { NavigationView { + VStack { + addView + // TODO: hide this if permission not granted + QRScannerUIView { code in + onQRCodeScanned(text: code) + } + .frame(height: 250) // You can adjust the height as needed. + .padding() + .onAppear(perform: checkCameraPermission) + } + // This is a little weird, but it works. The nagivation link for the login view - // is rendered in the backgroun (it's hidden), abd we toggle it manually. + // is rendered in the background (it's hidden), abd we toggle it manually. // If anyone has a better way to do a two-page layout let me know. - addView - .background(Group { - NavigationLink( - destination: loginView, - isActive: $showLogin - ) { - EmptyView() - } - }) + .background(Group { + NavigationLink( + destination: loginView, + isActive: $showLogin + ) { + EmptyView() + } + }) } } @@ -129,6 +141,55 @@ struct SubscriptionAddView: View { return topic.trimmingCharacters(in: .whitespaces) } + private func checkCameraPermission() { + switch AVCaptureDevice.authorizationStatus(for: .video) { + case .authorized: + self.hasCameraPermission = true + + case .notDetermined: + AVCaptureDevice.requestAccess(for: .video) { granted in + DispatchQueue.main.async { + self.hasCameraPermission = granted + if !granted { + self.hasCameraPermission = false + } + } + } + + case .denied, .restricted: + self.hasCameraPermission = false + + @unknown default: + break + } + } + + func onQRCodeScanned(text: String){ + // Check if the text is a valid URL with HTTP or HTTPS scheme + guard let url = URL(string: text), let scheme = url.scheme, ["http", "https"].contains(scheme) else { + return + } + + // Extract the base URL without the path + var components = URLComponents(url: url, resolvingAgainstBaseURL: false) + components?.path = "" + guard let foundBaseUrl = components?.url else { + return + } + + // Extract the route from the original URL + baseUrl = foundBaseUrl.absoluteString + useAnother = baseUrl != store.getDefaultBaseUrl() + + topic = url.path + if (topic.hasPrefix("/")) { + topic.removeFirst() + } + + print("------> \(baseUrl) : \(topic) : \(useAnother)") + subscribeOrShowLoginAction() + } + private func isAddViewValid() -> Bool { if sanitizedTopic.isEmpty { return false