things kinda work. adding debounce
This commit is contained in:
parent
372e946419
commit
740e1a7091
4 changed files with 149 additions and 12 deletions
|
|
@ -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 = "<group>"; };
|
||||
94CD1965283E662900973B93 /* emojis.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = emojis.json; sourceTree = "<group>"; };
|
||||
94CD1969283E666900973B93 /* EmojiManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EmojiManager.swift; sourceTree = "<group>"; };
|
||||
E278CB322AECECCA004B9143 /* QRScannerUIView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRScannerUIView.swift; sourceTree = "<group>"; };
|
||||
/* 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 = "<group>";
|
||||
|
|
@ -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 */,
|
||||
|
|
|
|||
|
|
@ -18,7 +18,9 @@
|
|||
<array>
|
||||
<string>remote-notification</string>
|
||||
</array>
|
||||
<key>FirebaseAppDelegateProxyEnabled</key>
|
||||
<false/>
|
||||
<key>FirebaseAppDelegateProxyEnabled</key>
|
||||
<false/>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>We need access to the camera for QR code scanning.</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
|
|||
70
ntfy/Utils/QRScannerUIView.swift
Normal file
70
ntfy/Utils/QRScannerUIView.swift
Normal file
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue